01 · Qué vamos a construir
Objetivo, materiales y resultado esperado
Partimos del esqueleto del tema 12 (arranque, reloj de 72 MHz, entrada y salida) y le agregamos lo aprendido en los temas 14 a 17. El producto es un regulador de velocidad: un motor de continua con un disco ranurado cuya velocidad se mide, se compara con un objetivo y se corrige para que se mantenga. El pulsador cambia el objetivo, y el LED indica que el programa está vivo.
Materiales.
- La placa del tema 12 (Blue Pill, LED en PC13, pulsador entre PA0 y masa) y el mismo entorno de compilación.
- Un motor de corriente continua pequeño (6 a 12 V) con una rueda ranurada de 20 ranuras y un sensor óptico de herradura que entrega un pulso por ranura. Va conectado a PB6 (canal 1 de
TIM4) con una resistencia a 3,3 V. - Un transistor MOSFET de nivel lógico como llave del motor, con una resistencia de 100 Ω en la compuerta, otra de 10 kΩ de la compuerta a masa y un diodo en antiparalelo con el motor. La compuerta se maneja desde PA6 (canal 1 de
TIM3). El motor tiene su propia fuente, con masa común con la placa. - Opcional, muy recomendable: un analizador lógico o un osciloscopio.
La resistencia de 10 kΩ de la compuerta a masa no es un adorno: cuando el microcontrolador se reinicia, el pin queda flotante unos instantes y sin ella la llave podría conducir y arrancar el motor sin que nadie lo ordene (el estado seguro del tema 9, esta vez en hardware).
Resultado esperado. Al encender, el motor está detenido y el LED parpadea a 1 Hz. Cada vez que se aprieta el pulsador el objetivo avanza por la secuencia 0 → 1200 → 2400 → 0 rpm. En régimen, la velocidad se mantiene dentro de ±2 % del objetivo y, ante un frenado repentino con la mano, se recupera en menos de un segundo. Si el motor se traba, el programa corta la potencia y el LED parpadea rápido.
Sin motor se puede igual: el simulador de Python y el laboratorio de este tema reproducen el lazo con un modelo del motor, y el tick, las tareas y el controlador se prueban de la misma manera.
02 · La estructura
Estructura del proyecto
Se agregan módulos al árbol del tema 12, respetando las reglas del tema 11: el controlador no sabe nada del hardware, y el hardware no sabe nada del controlador.
regulador/
├── src/
│ ├── main.c (inicializa y ejecuta las tareas)
│ ├── app.c, app.h (las tareas concretas y su cableado) ← reemplaza al parpadeo
│ ├── tick.c, tick.h (milisegundos por SysTick) ← reemplaza a tiempo.c
│ ├── tareas.c, tareas.h (planificador mínimo de tareas periódicas) ← nuevo
│ ├── control.c, control.h (controlador PI en enteros, sin hardware) ← nuevo
│ ├── reloj.c, reloj.h (igual que en el tema 12)
│ └── hal/
│ ├── hal_io.h, stm32/hal_io.c (LED y botón, igual que antes)
│ ├── hal_motor.h, stm32/hal_motor.c (PWM con TIM3) ← nuevo
│ ├── hal_tacometro.h, stm32/hal_tacometro.c (captura con TIM4) ← nuevo
│ └── stm32/nvic.h (habilitar interrupciones) ← nuevo
├── config/config.h
├── linker/stm32f103.ld
├── startup/startup.c (la tabla de vectores crece)
└── Makefile
Fijate en la separación: control.c es una función pura de enteros (recibe un objetivo y una medida, devuelve una potencia), sin ningún registro. Por eso se puede compilar y probar en la PC con el mismo código que irá al chip, y por eso es el módulo que más conviene tener bajo pruebas automáticas (tema 11). Los archivos nuevos se agregan a la variable SRC del Makefile (y se quita tiempo.c). Las constantes del producto van en config.h:
/* config/config.h (se agrega a lo del tema 12) */
#define TICK_HZ 1000u /* base de tiempo: 1 ms */
#define PERIODO_CONTROL_MS 10u /* el lazo corre a 100 Hz */
#define PERIODO_BOTON_MS 10u /* muestreo del pulsador para el antirrebote */
#define PERIODO_VIDA_MS 100u /* LED de vida */
#define RANURAS_POR_VUELTA 20u
#define RPM_MAX 6000u
#define TIMEOUT_PULSOS_MS 100u /* sin pulsos en este tiempo, la velocidad es cero */
#define POTENCIA_MAX_PM 1000 /* potencia máxima en milésimas (1000 = 100 %) */
03 · Paso 1
El tick por interrupción
La esperar_ms del tema 12 bloqueaba al procesador. La reemplazamos por un contador de milisegundos que incrementa el SysTick en una interrupción (tema 17). El módulo expone solo tres cosas: iniciar, leer y preguntar si venció un plazo.
/* src/tick.h */
#ifndef TICK_H
#define TICK_H
#include <stdint.h>
#include <stdbool.h>
void tick_iniciar(void);
uint32_t tick_ms(void); /* milisegundos desde el arranque */
static inline bool tick_vencido(uint32_t desde, uint32_t plazo) {
return (uint32_t)(tick_ms() - desde) >= plazo; /* restar, nunca sumar (tema 17) */
}
#endif
/* src/tick.c */
#include "tick.h"
#include "reloj.h"
#include "config.h"
#define SYST_CTRL (*(volatile uint32_t *)0xE000E010)
#define SYST_LOAD (*(volatile uint32_t *)0xE000E014)
#define SYST_VAL (*(volatile uint32_t *)0xE000E018)
#define SCB_SHPR3 (*(volatile uint32_t *)0xE000ED20) /* prioridades de SysTick (bits 31:24) y PendSV */
static volatile uint32_t ms; /* la escribe solo la ISR (tema 16) */
void SysTick_Handler(void) { ms++; }
uint32_t tick_ms(void) { return ms; } /* lectura de 32 bits: atómica */
void tick_iniciar(void) {
SCB_SHPR3 |= (0xFu << 28); /* prioridad 15: la menos urgente (el tick no tiene plazo corto) */
SYST_LOAD = reloj_frecuencia() / TICK_HZ - 1u; /* 71 999 a 72 MHz; 7999 si el cristal falló */
SYST_VAL = 0;
SYST_CTRL = (1u << 2) | (1u << 1) | 1u; /* reloj del procesador + interrupción + habilitado */
}
Dos decisiones para destacar. La primera es la prioridad: el tick es la ISR menos urgente del sistema, porque un atraso de unos microsegundos en incrementar el contador no afecta a nadie, mientras que la captura del tacómetro, con plazos de decenas de microsegundos, debe poder interrumpirlo (tema 15). La segunda es que la rutina hace una sola cosa. Y como antes, la frecuencia se le pregunta a reloj_frecuencia(): si el cristal falló, el tick sigue siendo de 1 ms.
Como ahora existen interrupciones de periféricos, la tabla de vectores de startup.c debe crecer hasta incluir la del TIM4, que es la número 30 de los periféricos (posición 46 de la tabla, porque las primeras 16 son del núcleo). Con una extensión de rango de GCC, el cambio es pequeño:
/* startup/startup.c: lo que cambia */
void TIM4_IRQHandler(void) __attribute__((weak, alias("Default_Handler")));
__attribute__((section(".isr_vector"), used))
void (* const vectores[47])(void) = {
(void (*)(void))(&_estack), Reset_Handler, NMI_Handler, HardFault_Handler,
Default_Handler, Default_Handler, Default_Handler, /* 4-6 */
0, 0, 0, 0, /* 7-10 */
SVC_Handler, Default_Handler, 0, PendSV_Handler, SysTick_Handler, /* 11-15 */
[16 ... 45] = Default_Handler, /* 30 interrupciones de periféricos sin atender */
[46] = TIM4_IRQHandler, /* interrupción 30: TIM4 */
};
Es el error número uno de este taller: si la tabla no llega hasta la posición de la interrupción, o si el nombre de la rutina tiene una errata, la interrupción cae en Default_Handler y el programa se queda quieto (tema 15). Una tabla de 47 entradas ocupa 188 bytes; si usás más periféricos, ampliala.
Punto de control. Con un programa que alterna el LED cada 500 ms usando solo tick_vencido(), medí con el analizador lógico: el período debe ser de 1000 ms con un error mucho menor al 1 %. Si no, revisá el reloj y el LOAD antes de seguir.
04 · Paso 2
Tareas periódicas
Con el tick se arma el planificador mínimo que prefigura el tema 19: una tabla de funciones, cada una con su período, y un bucle que ejecuta las que ya vencieron. Es la versión ordenada del superloop del tema 14.
/* src/tareas.h */
#ifndef TAREAS_H
#define TAREAS_H
#include <stdint.h>
typedef struct {
void (*funcion)(void); /* qué ejecutar */
uint32_t periodo_ms; /* cada cuánto */
uint32_t proximo; /* instante teórico de la próxima ejecución */
uint32_t duracion_max; /* ciclos de la ejecución más larga vista (diagnóstico) */
} Tarea;
void tareas_iniciar(Tarea *tabla, unsigned cantidad);
void tareas_ejecutar(void); /* llamar en cada vuelta del bucle principal */
uint32_t tareas_atraso_max(void); /* mayor atraso visto, en ms */
#endif
/* src/tareas.c */
#include "tareas.h"
#include "tick.h"
#define DEMCR (*(volatile uint32_t *)0xE000EDFC)
#define DWT_CTRL (*(volatile uint32_t *)0xE0001000)
#define DWT_CYCCNT (*(volatile uint32_t *)0xE0001004)
static Tarea *tabla;
static unsigned n;
static uint32_t atraso_max;
void tareas_iniciar(Tarea *t, unsigned cantidad) {
tabla = t; n = cantidad;
DEMCR |= (1u << 24); /* habilitar el bloque de depuración y traza */
DWT_CYCCNT = 0;
DWT_CTRL |= 1u; /* contador de ciclos del procesador */
uint32_t ahora = tick_ms();
for (unsigned i = 0; i < n; i++) tabla[i].proximo = ahora + tabla[i].periodo_ms;
}
void tareas_ejecutar(void) {
uint32_t ahora = tick_ms();
for (unsigned i = 0; i < n; i++) {
int32_t atraso = (int32_t)(ahora - tabla[i].proximo); /* ≥ 0: ya venció */
if (atraso >= 0) {
tabla[i].proximo += tabla[i].periodo_ms; /* por el instante teórico, no por 'ahora' */
if ((uint32_t)atraso > atraso_max) atraso_max = (uint32_t)atraso;
uint32_t t0 = DWT_CYCCNT;
tabla[i].funcion();
uint32_t dur = DWT_CYCCNT - t0;
if (dur > tabla[i].duracion_max) tabla[i].duracion_max = dur;
}
}
}
uint32_t tareas_atraso_max(void) { return atraso_max; }
El bucle repite tres ideas del tema 17. Se avanza con proximo += periodo, no con proximo = ahora + periodo: así el atraso de una vuelta no se acumula y la frecuencia media es la exacta (el programa de Python de este tema lo cuantifica). Se resta en lugar de comparar con una suma, y para eso se usa la diferencia con signo, que es correcta mientras el atraso sea menor que medio rango (24,8 días). Y se mide: el contador de ciclos del bloque DWT (disponible en el Cortex-M3) permite saber cuánto tarda la peor ejecución de cada tarea, sin instrumentos externos.
Hay una decisión de política: si main se retrasa más de un período (una tarea larga, una sección crítica), el avance por instante teórico hace que la tarea se ejecute varias veces seguidas hasta recuperar el tiempo, una ráfaga. Para un lazo de control suele ser indeseable. La alternativa es resincronizar cuando el atraso supera un período (proximo = ahora + periodo), perdiendo ejecuciones pero no haciéndolas en ráfaga. Cuál conviene depende del significado de la tarea, y hay que decidirlo por escrito.
Y la regla que gobierna todo este esquema, heredada del tema 14: ninguna tarea espera. Cada función hace su trabajo en una fracción de milisegundo y devuelve el control. Una sola esperar_ms dentro de una tarea desordena a todas las demás.
05 · Paso 3
Medir la velocidad
El disco tiene 20 ranuras: cada vuelta produce 20 pulsos. Hay dos maneras de convertirlos en velocidad: contar los pulsos en una ventana fija (por ejemplo los de 10 ms), o medir el período entre pulsos consecutivos (tema 17). A 1200 rpm hay 20 vueltas por segundo y 400 pulsos por segundo; en una ventana de 10 ms caen 4 pulsos, y un error de ±1 pulso es un 25 % de error. Es inaceptable. En cambio, el período entre dos pulsos a esa velocidad es de 2500 µs, y medirlo con un contador de 1 MHz tiene un error de ±1 µs, es decir, 0,04 %. Para velocidades bajas, medir el período es muchísimo mejor.
/* src/hal/hal_tacometro.h */
#ifndef HAL_TACOMETRO_H
#define HAL_TACOMETRO_H
#include <stdint.h>
void tacometro_iniciar(void);
uint32_t tacometro_rpm(void); /* 0 si no hay pulsos recientes */
#endif
/* src/hal/stm32/hal_tacometro.c */
#include "hal/hal_tacometro.h"
#include "hal/stm32/nvic.h"
#include "tick.h"
#include "config.h"
#define RCC_APB1ENR (*(volatile uint32_t *)0x4002101C)
#define RCC_APB2ENR (*(volatile uint32_t *)0x40021018)
#define GPIOB_CRL (*(volatile uint32_t *)0x40010C00)
#define GPIOB_ODR (*(volatile uint32_t *)0x40010C0C)
#define TIM4_CR1 (*(volatile uint32_t *)0x40000800)
#define TIM4_DIER (*(volatile uint32_t *)0x4000080C)
#define TIM4_CCMR1 (*(volatile uint32_t *)0x40000818)
#define TIM4_CCER (*(volatile uint32_t *)0x40000820)
#define TIM4_PSC (*(volatile uint32_t *)0x40000828)
#define TIM4_ARR (*(volatile uint32_t *)0x4000082C)
#define TIM4_CCR1 (*(volatile uint32_t *)0x40000834)
static volatile uint16_t periodo_us; /* escribe la ISR; un dato de 16 bits es atómico */
static volatile uint32_t ultimo_pulso; /* instante (en ms) del último pulso */
static uint16_t anterior;
void TIM4_IRQHandler(void) {
uint16_t actual = (uint16_t)TIM4_CCR1; /* leer el registro también borra la bandera */
uint32_t ahora = tick_ms();
if (tick_vencido(ultimo_pulso, TIMEOUT_PULSOS_MS)) {
anterior = actual; /* primer pulso tras una pausa: el intervalo no vale */
} else {
periodo_us = (uint16_t)(actual - anterior); /* resta sin signo de 16 bits (tema 17) */
anterior = actual;
}
ultimo_pulso = ahora;
}
uint32_t tacometro_rpm(void) {
uint16_t p = periodo_us; /* una sola lectura, guardada en una local */
if (p == 0 || tick_vencido(ultimo_pulso, TIMEOUT_PULSOS_MS)) return 0;
return (60000000u / RANURAS_POR_VUELTA) / p; /* 3 000 000 / período en µs */
}
void tacometro_iniciar(void) {
RCC_APB2ENR |= (1u << 3); /* reloj del puerto B */
GPIOB_CRL = (GPIOB_CRL & ~(0xFu << 24)) | (0x8u << 24); /* PB6: entrada con resistencia... */
GPIOB_ODR |= (1u << 6); /* ...pull-up */
RCC_APB1ENR |= (1u << 2); /* reloj del TIM4 */
TIM4_PSC = 71; /* 72 MHz / 72 = 1 MHz: 1 cuenta = 1 µs */
TIM4_ARR = 0xFFFF;
TIM4_CCMR1 = 1u; /* CC1S = 01: captura del pin TI1 */
TIM4_CCER = 1u; /* CC1E: captura, flanco de subida */
TIM4_DIER |= (1u << 1); /* CC1IE */
nvic_habilitar(30, 1); /* prioridad 1: más urgente que el tick */
TIM4_CR1 |= 1u;
}
Detalles que hacen que el módulo sea robusto y no solo correcto:
- El primer pulso después de una pausa se descarta. Si el motor estuvo detenido un rato, la resta entre la captura actual y la anterior (vieja de segundos) no significa nada; sin esta precaución, el primer valor de velocidad al arrancar es basura.
- El tiempo de espera convierte «sin pulsos» en cero rpm. Un motor trabado no produce ningún pulso nuevo, y sin esta lógica el programa seguiría informando la última velocidad medida. 100 ms sin pulsos equivalen a menos de 10 pulsos por segundo: 30 rpm con este disco.
- El rango de medición tiene un piso. Con un contador de 16 bits a 1 MHz el período máximo es de 65,5 ms, es decir, 45,8 rpm. Debajo de eso la resta sería ambigua. El tiempo de espera de 100 ms es coherente con ese piso.
- La división se protege del cero. Dos capturas idénticas darían
p = 0y un fallo del procesador por división por cero. - Los datos compartidos siguen las reglas del tema 16:
volatile, un único escritor (la ISR) y accesos de 16 y 32 bits, atómicos. Los dos datos que leetacometro_rpm()podrían ser de pulsos consecutivos distintos, pero eso solo afecta a una decisión de tiempo de espera y no corrompe nada.
El módulo nvic.h es el equivalente de CMSIS que el esqueleto no usa (el tema 12 no incluye bibliotecas del fabricante):
/* src/hal/stm32/nvic.h */
#ifndef NVIC_H
#define NVIC_H
#include <stdint.h>
#define NVIC_ISER ((volatile uint32_t *)0xE000E100)
#define NVIC_IPR ((volatile uint8_t *)0xE000E400)
static inline void nvic_habilitar(unsigned irq, uint8_t prioridad) {
NVIC_IPR[irq] = (uint8_t)(prioridad << 4); /* el STM32F103 implementa los 4 bits altos */
NVIC_ISER[irq >> 5] = 1u << (irq & 31u); /* habilitar al final, después de fijar la prioridad */
}
#endif
06 · Paso 4
El actuador PWM
La interfaz habla en el idioma del producto: potencia en milésimas, de 0 a 1000. El módulo traduce a cuentas del temporizador. La configuración es la del tema 17 (TIM3 canal 1, 20 kHz, 3600 pasos) y mantiene el estado seguro: la potencia empieza en cero antes de conectar el pin.
/* src/hal/hal_motor.h */
#ifndef HAL_MOTOR_H
#define HAL_MOTOR_H
#include <stdint.h>
void motor_iniciar(void);
void motor_potencia(int32_t milesimas); /* 0 a 1000; los valores fuera de rango se recortan */
#endif
/* src/hal/stm32/hal_motor.c */
#include "hal/hal_motor.h"
#include "config.h"
#define RCC_APB1ENR (*(volatile uint32_t *)0x4002101C)
#define RCC_APB2ENR (*(volatile uint32_t *)0x40021018)
#define GPIOA_CRL (*(volatile uint32_t *)0x40010800)
#define TIM3_CR1 (*(volatile uint32_t *)0x40000400)
#define TIM3_EGR (*(volatile uint32_t *)0x40000414)
#define TIM3_CCMR1 (*(volatile uint32_t *)0x40000418)
#define TIM3_CCER (*(volatile uint32_t *)0x40000420)
#define TIM3_PSC (*(volatile uint32_t *)0x40000428)
#define TIM3_ARR (*(volatile uint32_t *)0x4000042C)
#define TIM3_CCR1 (*(volatile uint32_t *)0x40000434)
#define PASOS 3600u /* 72 MHz / 20 kHz */
void motor_potencia(int32_t pm) {
if (pm < 0) pm = 0;
if (pm > POTENCIA_MAX_PM) pm = POTENCIA_MAX_PM;
TIM3_CCR1 = (uint32_t)pm * PASOS / 1000u; /* se aplica en el próximo período (precarga) */
}
void motor_iniciar(void) {
RCC_APB1ENR |= (1u << 1); /* reloj del TIM3 */
TIM3_PSC = 0;
TIM3_ARR = PASOS - 1u;
TIM3_CCR1 = 0; /* 1) potencia cero ANTES de conectar el pin */
TIM3_CCMR1 = (6u << 4) | (1u << 3); /* PWM modo 1 con precarga */
TIM3_CCER = 1u;
TIM3_CR1 = (1u << 7); /* precarga de ARR */
TIM3_EGR = 1u;
TIM3_CR1 |= 1u; /* arrancar el contador */
RCC_APB2ENR |= (1u << 2); /* 2) recién ahora, el pin PA6 como salida alternativa */
GPIOA_CRL = (GPIOA_CRL & ~(0xFu << 24)) | (0xBu << 24);
}
Con eso se puede hacer una prueba en lazo abierto, que es la primera de la lista de verificación: llamar a motor_potencia(200) y ver cuánto gira. Esa medición (cuántas rpm por cada 1 % de potencia, y cuánto tarda el motor en acelerar) es el modelo que se necesita para sintonizar el lazo, como se explica más adelante.
07 · Paso 5
El lazo de control
Un lazo cerrado mide lo que realmente ocurre, lo compara con lo deseado y corrige. El diagrama muestra los bloques que acabamos de construir:
El controlador que usamos es un PI (proporcional e integral). El término proporcional reacciona al error de ahora (Kp · e); el integral acumula el error pasado (Ki · Σe · T) y por eso es el único que logra que el error final sea cero: mientras haya algún error, la integral sigue creciendo y empujando. Se implementa en aritmética entera, porque el Cortex-M3 no tiene unidad de punto flotante y hacerlo en software sumaría kilobytes de código y cientos de ciclos por operación. La potencia se maneja en milésimas, y las ganancias se escalan por 1000:
/* src/control.h */
#ifndef CONTROL_H
#define CONTROL_H
#include <stdint.h>
typedef struct {
int32_t kp_m; /* ganancia proporcional × 1000: milésimas de potencia por rpm de error, ×1000 */
int32_t ki_m; /* ganancia integral × 1000, ya multiplicada por el período del lazo */
int32_t umax; /* tope de la salida, en milésimas */
int32_t integ; /* estado del integrador, × 1000 */
} PI;
void pi_iniciar(PI *c, int32_t kp_m, int32_t ki_m, int32_t umax);
void pi_reiniciar(PI *c);
int32_t pi_paso(PI *c, int32_t objetivo, int32_t medida); /* devuelve la potencia, 0..umax */
#endif
/* src/control.c */
#include "control.h"
void pi_iniciar(PI *c, int32_t kp_m, int32_t ki_m, int32_t umax) {
c->kp_m = kp_m; c->ki_m = ki_m; c->umax = umax; c->integ = 0;
}
void pi_reiniciar(PI *c) { c->integ = 0; }
int32_t pi_paso(PI *c, int32_t objetivo, int32_t medida) {
int32_t e = objetivo - medida; /* rpm */
int32_t nuevo = c->integ + c->ki_m * e; /* integral candidata */
int32_t u = (c->kp_m * e + nuevo) / 1000; /* potencia pedida, en milésimas */
int32_t u_sat = u;
if (u_sat > c->umax) u_sat = c->umax;
if (u_sat < 0) u_sat = 0;
if (u_sat == u) c->integ = nuevo; /* antiwindup: si saturó, no acumular */
return u_sat;
}
Los números salen de la sintonía que se encuentra en la simulación: Kp = 0,02 %/rpm son 0,2 milésimas por rpm, o sea kp_m = 200; Ki = 0,13 %/(rpm·s) son 1,3 milésimas por rpm y por segundo, y multiplicadas por el período de 10 ms dan 0,013, o sea ki_m = 13. Observá que la ganancia integral depende del período del lazo: si cambiás PERIODO_CONTROL_MS, hay que recalcularla. Con errores de hasta 6000 rpm los productos llegan a 1,2 millones, lejos del límite de 2 mil millones de un entero de 32 bits.
La antiwindup merece una explicación. Si la salida está saturada (el motor ya recibe el máximo y aun así no llega), un integrador ingenuo sigue acumulando error y crece sin límite; cuando la situación cambia, esa «deuda» tarda mucho en descargarse y el sistema se queda pasado de largo. La línea final evita acumular mientras la salida está recortada. El laboratorio y la simulación miden la diferencia: 2,26 s de recuperación sin la protección, 0,63 s con ella.
Ahora se cablea todo en la aplicación: las tres tareas, el botón con antirrebote, el LED de vida y la protección contra el motor trabado.
/* src/app.c */
#include "app.h"
#include "config.h"
#include "tick.h"
#include "tareas.h"
#include "control.h"
#include "hal/hal_io.h"
#include "hal/hal_motor.h"
#include "hal/hal_tacometro.h"
static const int32_t OBJETIVOS[] = { 0, 1200, 2400 }; /* rpm; el botón recorre la lista */
static unsigned objetivo_idx;
static bool fallo;
static unsigned trabado; /* iteraciones consecutivas sin girar */
static PI regulador;
static void tarea_control(void) { /* cada 10 ms */
int32_t objetivo = fallo ? 0 : OBJETIVOS[objetivo_idx];
int32_t medida = (int32_t)tacometro_rpm();
int32_t u = pi_paso(®ulador, objetivo, medida);
if (objetivo == 0) { u = 0; pi_reiniciar(®ulador); } /* detenido: sin potencia ni integral acumulada */
if (u > 300 && medida == 0) { /* mucha potencia y nada gira */
if (++trabado >= 100) { fallo = true; u = 0; } /* 100 × 10 ms = 1 s: motor trabado */
} else {
trabado = 0;
}
motor_potencia(u);
}
static void tarea_boton(void) { /* cada 10 ms: antirrebote por muestras consecutivas */
static unsigned estables;
static bool anterior;
bool ahora = boton_presionado();
estables = (ahora == anterior) ? estables + 1 : 0;
anterior = ahora;
if (estables == 3 && ahora) { /* 30 ms estable y apretado: una pulsación */
if (fallo) { fallo = false; trabado = 0; } /* el botón también rearma tras un fallo */
objetivo_idx = (objetivo_idx + 1) % (sizeof OBJETIVOS / sizeof OBJETIVOS[0]);
}
}
static void tarea_vida(void) { /* cada 100 ms */
static unsigned cuenta;
cuenta++;
if (fallo || cuenta % 5 == 0) { /* normal: cada 500 ms; fallo: cada 100 ms */
static bool encendido;
encendido = !encendido;
if (encendido) led_encender(); else led_apagar();
}
}
static Tarea tabla[] = {
{ tarea_control, PERIODO_CONTROL_MS, 0, 0 },
{ tarea_boton, PERIODO_BOTON_MS, 0, 0 },
{ tarea_vida, PERIODO_VIDA_MS, 0, 0 },
};
void app_iniciar(void) {
pi_iniciar(®ulador, 200, 13, POTENCIA_MAX_PM); /* Kp = 0,02 %/rpm; Ki = 0,13 %/(rpm·s) a 10 ms */
tareas_iniciar(tabla, sizeof tabla / sizeof tabla[0]);
}
void app_paso(void) { tareas_ejecutar(); }
/* src/main.c */
#include "app.h"
#include "tick.h"
#include "hal/hal_io.h"
#include "hal/hal_motor.h"
#include "hal/hal_tacometro.h"
int main(void) {
io_iniciar();
motor_iniciar(); /* el estado seguro primero: potencia cero */
tacometro_iniciar();
tick_iniciar(); /* el tick, el último en arrancar: ya todo está listo para usarlo */
app_iniciar();
while (1) { app_paso(); }
}
Cosas para notar en app.c. El controlador no conoce ningún registro: solo recibe números y devuelve números. Cuando el objetivo es cero se fuerza la salida a cero y se descarta la integral, porque «detenido» significa potencia nula y no un esfuerzo por llegar a cero. La detección de motor trabado es un diagnóstico de una situación anómala —potencia alta sin movimiento durante un segundo— y su respuesta es el estado seguro (potencia cero y aviso), nunca insistir. Y las tres tareas son cortas: ninguna espera, ninguna imprime, ninguna llama a algo bloqueante (tema 16).
08 · Comprobar que funciona
Verificación y diagnóstico
Un regulador se acepta con pruebas medibles, en este orden: primero la base de tiempo, luego cada medida, luego el lazo.
| Prueba | Cómo se hace | Resultado esperado |
|---|---|---|
| Tick | Conmutar un pin cada 500 ms con tick_vencido y medir con el analizador | Semiperíodo de 500 ms con error menor al 1 % |
| Estado seguro | Reiniciar con el analizador conectado a PA6 | Sin pulsos desde el primer instante; el motor no se mueve |
| Lazo abierto | motor_potencia(200) y leer tacometro_rpm() | Una velocidad estable (≈ 1200 rpm con el motor del modelo); anotar el valor y el tiempo que tarda en alcanzar el 63 % |
| Medición | Comparar tacometro_rpm() con una medida independiente (período en el analizador lógico) | Diferencia menor al 1 % |
| Parada | Frenar el motor con la mano y soltar | tacometro_rpm() devuelve 0 en menos de 100 ms y no da valores absurdos al arrancar |
| Régimen | Objetivo 1200 rpm; registrar la velocidad 10 s | Dentro de ±2 % del objetivo |
| Perturbación | Aplicar y soltar un frenado moderado | Recupera ±2 % en menos de 1 s, sin oscilar |
| Motor trabado | Bloquear el eje con objetivo 2400 | Potencia cero en 1 s y LED parpadeando a 100 ms; el botón rearma |
| Tareas | Leer tareas_atraso_max() y la duracion_max de cada tarea tras 10 minutos de uso | Atraso máximo de 1 ms o menos; duraciones de unos pocos microsegundos |
| Carga | Sumar duracion_max / período de cada tarea más las ISR (tema 15) | Muy por debajo del 5 %; el resto es holgura |
Una estimación de lo que deberías ver, para comparar con la medición: la interrupción del tick dura unas decenas de ciclos (menos de 1 µs por milisegundo); la de captura a 6000 rpm ocurre 2000 veces por segundo; la tarea de control, en aritmética entera, se resuelve en unos pocos microsegundos cada 10 ms. En total, el procesador está libre más del 99 % del tiempo. Es el mejor argumento contra el temor de que «las interrupciones hacen todo más lento».
| Síntoma | Causa probable | Qué hacer |
|---|---|---|
| Todo se congela al habilitar el tacómetro | La tabla de vectores no llega a TIM4, o el nombre de la rutina tiene un error: cae en Default_Handler | Depurador: detener y mirar dónde está el PC; revisar vectores[46] |
| El programa se queda en la ISR del tacómetro | No se lee CCR1 (la bandera no se borra) o la señal rebota | Leer el registro de captura; revisar la señal del sensor con el analizador |
| Las rpm medidas son el doble o la mitad | Número de ranuras o flanco equivocados | Contar las ranuras; revisar RANURAS_POR_VUELTA |
| La velocidad oscila | Ganancias demasiado altas o período del lazo largo | Bajar Kp y Ki; verificar que el lazo corre cada 10 ms |
| El motor no arranca desde cero | Fricción estática: la potencia necesaria para arrancar es mayor que para mantener | Aumentar la ganancia inicial o agregar una potencia mínima de arranque |
| El LED de vida va más lento o rápido de lo debido | Un error de reloj o del tick | Volver a la prueba del tick |
Los resultados cambian al cambiar PERIODO_CONTROL_MS | ki_m depende del período | Recalcular: ki_m = Ki · período_ms · 10 (con Ki en %/(rpm·s)) |
09 · Analogía con programación
Simular el lazo en Python
Antes de tocar el motor, el controlador se puede probar contra un modelo. El motor se aproxima por un sistema de primer orden: τ · dω/dt = K·u − ω, con 60 rpm por cada 1 % de potencia y una constante de tiempo de 150 ms (lo que da una velocidad de 1200 rpm con 20 %, la del modelo de la prueba en lazo abierto). El programa implementa exactamente el algoritmo de pi_paso —con antiwindup y saturación— cada 10 ms, mientras la planta avanza cada 1 ms. También convierte períodos en rpm como lo hace el tacómetro y compara las dos formas de programar una tarea periódica.
Python en tu navegador. Los bloques son editables: cambiá los valores y presioná «Ejecutar Python». La primera vez tarda unos segundos en cargar.
# Planta: un motor de continua es, en primera aproximación, un sistema de primer orden
K, TAU = 60.0, 0.150 # rpm por % de ciclo de trabajo; constante de tiempo en segundos
DT, TC = 0.001, 0.010 # la planta avanza cada 1 ms; el lazo se ejecuta cada 10 ms
def simular(kp, ki, objetivo, antiwindup=True, t_fin=6.0, umax=100.0, t_carga=None):
w = u = integ = t = 0.0
paso = round(TC / DT)
traza = []
for i in range(round(t_fin / DT)):
if i % paso == 0: # --- el lazo de control (tarea de 10 ms) ---
e = objetivo(t) - w
nuevo = integ + ki * e * TC
sin_sat = kp * e + nuevo
u = max(0.0, min(umax, sin_sat)) # el ciclo de trabajo solo puede estar entre 0 y umax
if not antiwindup or u == sin_sat: # antiwindup: no integrar mientras se satura
integ = nuevo
k = K * 0.7 if (t_carga is not None and t >= t_carga) else K
w += DT * (k * u - w) / TAU # --- la planta ---
t += DT
traza.append((t, w, u))
return traza
def asienta(traza, obj, desde, hasta, tol=0.02):
"""Instante (desde 'desde') a partir del cual la velocidad queda a ±2 % del objetivo."""
seg = [(t, w) for t, w, u in traza if desde <= t < hasta]
if abs(seg[-1][1] - obj) > tol * obj:
return None
for i in range(len(seg) - 1, -1, -1):
if abs(seg[i][1] - obj) > tol * obj:
return seg[i + 1][0] - desde
return 0.0
print("Parte 1: salto a 1200 rpm; a los 3 s se agrega una carga (-30 % de ganancia)")
for nombre, kp, ki in (("Solo P, Kp=0,05", 0.05, 0), ("Solo P, Kp=0,20", 0.20, 0),
("PI, Kp=0,02 Ki=0,13", 0.02, 0.13), ("PI, Kp=0,02 Ki=1,50", 0.02, 1.5)):
tr = simular(kp, ki, lambda t: 1200.0, t_carga=3.0)
pico = max(w for t, w, u in tr if t < 3.0)
antes = [w for t, w, u in tr if t < 3.0][-1]
minimo = min(w for t, w, u in tr if t >= 3.0)
a = asienta(tr, 1200, 0, 3.0)
print(f" {nombre:21s} sobrepaso {max(0, (pico - 1200) / 12):5.1f} % asienta {('%.2f s' % a) if a is not None else ' nunca':>7s}"
f" a los 3 s: {antes:5.0f} mínimo tras carga: {minimo:5.0f} final: {tr[-1][1]:5.0f} rpm")
print()
print("Parte 2: saturación al 40 % (máx 2400 rpm). Pide 3000 rpm durante 3 s y después 1200 rpm")
for aw in (False, True):
tr = simular(0.02, 0.13, lambda t: 3000.0 if t < 3.0 else 1200.0, antiwindup=aw, umax=40.0, t_fin=12.0)
a = asienta(tr, 1200, 3.0, 12.0)
print(f" antiwindup {str(aw):5s}: tarda {a:.2f} s en volver a 1200 rpm ±2 %")
# --- Parte 3: del período del tacómetro a las rpm ---------------------------------------------
RANURAS = 20 # pulsos por vuelta
def rpm(periodo_us):
return 60e6 / (RANURAS * periodo_us)
print()
print("Parte 3: velocidad a partir del período entre pulsos (contador a 1 MHz, 20 ranuras)")
for p in (50000, 2500, 500):
print(f" período {p:6d} µs -> {rpm(p):7.1f} rpm (±1 µs = ±{rpm(p) - rpm(p + 1):.2f} rpm, {100 / p:.3f} %)")
print(f" velocidad mínima medible con un contador de 16 bits: {rpm(65535):.1f} rpm")
# --- Parte 4: planificar por 'ahora' o por el instante teórico --------------------------------
RETRASOS = [0, 1, 2, 3, 0, 2] # ms que tarda main en llegar a la tarea (vueltas largas)
def planificar(acumulado, periodo=10, ejecuciones=1000):
proximo, instantes = periodo, []
for i in range(ejecuciones):
ahora = proximo + RETRASOS[i % len(RETRASOS)] # main atiende la tarea con algo de atraso
instantes.append(ahora)
proximo = proximo + periodo if acumulado else ahora + periodo
dif = [b - a for a, b in zip(instantes, instantes[1:])]
return sum(dif) / len(dif), min(dif), max(dif), instantes[-1]
print()
print("Parte 4: tarea de 10 ms con retrasos de 0 a 3 ms")
for etiqueta, acum in (("próximo = ahora + período", False), ("próximo += período", True)):
media, mn, mx, fin = planificar(acum)
print(f" {etiqueta:26s} período medio {media:6.3f} ms (mín {mn}, máx {mx}) -> 1000 ejecuciones en {fin / 1000:.2f} s")
Resultado, parte 1: con solo acción proporcional queda un error permanente. Con Kp = 0,05 la velocidad se estabiliza en 900 rpm (25 % por debajo del objetivo) y tras la carga cae a 813; subiendo a Kp = 0,20 el error baja a 1108 rpm antes de la carga y a 1072 después, pero nunca desaparece. Con el PI (Kp = 0,02, Ki = 0,13) llega a 1200 rpm sin sobrepaso y asienta en ±2 % en 0,52 s; ante la carga de los 3 s cae hasta 1054 rpm y vuelve a 1200. Con Ki = 1,5 (más de once veces mayor) llega rápido pero con un sobrepaso del 40,9 %. Parte 2: con la salida limitada al 40 % (el motor no llega a las 3000 rpm pedidas), el integrador sin protección tarda 2,26 s en volver a 1200 rpm cuando el objetivo baja; con antiwindup, 0,63 s. Parte 3: 2500 µs entre pulsos son 1200 rpm con un error de ±0,48 rpm (0,04 %); a 6000 rpm el mismo ±1 µs vale ±12 rpm (0,2 %); el piso de medición es 45,8 rpm. Parte 4: calcular el próximo instante como ahora + período da un período medio de 11,335 ms (las 1000 ejecuciones tardan 11,33 s en lugar de 10 s); avanzar por el instante teórico da 10,003 ms (el valor exacto sería 10 ms; la diferencia se debe a que 1000 no es múltiplo de la secuencia de retrasos), con variaciones individuales de 7 a 12 ms que se compensan.
Probá cambiar las ganancias, el período TC a 100 ms con Kp = 0,20 (el lazo oscila, como en el laboratorio) o el modelo del motor (una constante de tiempo de 50 ms). El valor del simulador es ese: las equivocaciones salen en la computadora, no en el motor.
10 · Exploración interactiva
Sintonizar el lazo
El laboratorio ejecuta el lazo del apartado anterior sobre el modelo del motor: 8 segundos de simulación, con un objetivo, un posible evento a los 3 s (una carga que quita el 30 % de ganancia, o un cambio de objetivo) y una gráfica de barras de la velocidad cada 0,25 s. Podés cambiar las dos ganancias, el período del lazo, el techo de la potencia y si el integrador tiene antiwindup.
Un regulador de velocidad PI sobre un motor simulado
Velocidad en el tiempo
—
Cambiá las ganancias para ver cómo responde el motor.
Recorrido guiado
- Con los valores iniciales (
Kp = 0,02,Ki = 0,13, 10 ms): el motor llega a 1200 rpm sin sobrepaso y asienta en 0,52 s. A los 3 s la carga lo hace caer a 1054 rpm, y el integrador lo devuelve a 1200 en 0,71 s. Es una buena sintonía. - Poné
Ki = 0yKp = 0,05(solo proporcional): la velocidad se estanca en 900 rpm y cae a 813 con la carga. El controlador necesita una salida del 15 % para sostener ese giro, y esa salida solo existe si el error no es cero. Sin integral, el error no desaparece. - Subí a
Kp = 0,20sin integral: el error baja (1108 rpm, 1072 tras la carga) pero no se anula. Un solo término proporcional nunca llega al objetivo, solo se acerca, y una ganancia mayor es una apuesta cada vez más peligrosa. - Con ese
Kp = 0,20pasá el período del lazo a 50 ms: el lazo ya oscila (entre 863 y 1422 rpm). Corregir cada 50 ms con esa ganancia es corregir tarde y con exceso. La regla práctica: el período del lazo debe ser como máximo la décima parte de la constante de tiempo del motor, 15 ms aquí. - Volvé a las ganancias iniciales y poné
Ki = 1,5: el sobrepaso salta al 40,9 %. La integral es lo que elimina el error, pero una integral demasiado fuerte para la inercia del motor se pasa de largo. - Probá el objetivo «3000 rpm y a los 3 s baja a 1200» con el techo al 40 %, sin carga y sin antiwindup: el motor llega a 2400 rpm (el máximo), el integrador sigue acumulando, y al bajar el objetivo tarda 2,26 s en llegar a 1200. Con antiwindup, solo 0,63 s.
Desafío: ¿qué cambiaría si el lazo midiera por conteo en 10 ms en lugar de por período?
La medida llegaría cuantizada: a 1200 rpm hay 4 pulsos en 10 ms, y con un pulso de más o de menos la velocidad medida saltaría 300 rpm (25 %). El controlador interpretaría esos saltos como errores reales y los corregiría, inyectando ruido en la potencia. El motor se movería a tirones. Es un caso donde la calidad de la medición limita al lazo: ningún ajuste de ganancias compensa un sensor ruidoso. Por eso el tacómetro mide el período.
11 · Método de trabajo
Método: cómo sintonizar un lazo
- Verificá el hardware en lazo abierto. Aplicá una potencia fija y medí la velocidad que resulta. Esa relación es la ganancia del motor (aquí 60 rpm por cada 1 %).
- Medí la respuesta al escalón. Registrá cuánto tarda la velocidad en llegar al 63 % de su valor final: es la constante de tiempo. Con el modelo, 150 ms.
- Fijá el período del lazo en la décima parte de esa constante de tiempo o menos, y que sea un múltiplo del tick. Un período más largo oscila; uno mucho más corto no mejora nada y gasta procesador.
- Empezá con solo la acción proporcional. Subí
Kphasta que la respuesta sea rápida sin oscilar, y anotá el error que queda. - Agregá la integral, empezando por una muy pequeña, hasta que el error final desaparezca en un tiempo razonable. Una guía inicial: el tiempo integral
Kp / Kicercano a la constante de tiempo del motor (aquí 0,02 / 0,13 ≈ 0,15 s). - Limitá la salida y protegé el integrador. Definí el tope de la potencia y verificá con el simulador que una saturación prolongada no deja al sistema «pasado» (antiwindup).
- Probá las perturbaciones reales: una carga repentina, un cambio grande de objetivo, el arranque desde cero, el eje bloqueado.
- Agregá las protecciones: una respuesta de estado seguro (potencia cero) ante un sensor mudo o un motor trabado, y un modo de rearmar.
- Medí la carga del procesador y el atraso de las tareas y guardá los números junto con las ganancias.
Una advertencia que evita noches en blanco: cuando un lazo «no anda», sospechá primero de la medición y del tiempo, no de las ganancias. Un sensor con ruido, un período del lazo que no es el que se cree (¡medilo!) o un reloj equivocado producen síntomas idénticos a una sintonía mala, y no se arreglan tocando Kp.
12 · Comprensión
Confusiones frecuentes
«Con una ganancia proporcional suficientemente alta no necesito la integral»
Una ganancia mayor reduce el error permanente pero no lo anula, y la estabilidad se deteriora: con un lazo de 10 ms, Kp = 0,20 deja un 7,7 % de error y con un lazo más lento ya oscila. La integral es la que lleva el error a cero sin tener que subir a ganancias peligrosas.
«Cuanto más rápido corra el lazo, mejor»
Hasta cierto punto. Por encima de una décima de la constante de tiempo ya no hay mejora de respuesta, y el costo es procesador, ruido de la medición amplificado y, a veces, nada de lo que medir: a 1200 rpm el tacómetro entrega una medida nueva cada 2,5 ms, así que un lazo a 1 kHz usaría la misma medida varias veces.
«Las tareas son independientes: una espera no molesta a las otras»
En un bucle cooperativo, todas comparten el procesador. Una tarea que espera 5 ms retrasa en 5 ms a todas las demás, incluyendo el lazo de control. Por eso se cuenta el atraso máximo y se prohíben las esperas (tema 14).
«El integrador acumula, así que no hay problema si se satura»
Acumula sin límite si el programador no pone uno. En una saturación prolongada (motor trabado, objetivo inalcanzable), esa acumulación convierte una situación temporal en un desorden de varios segundos cuando el sistema se libera. Siempre se limita o se congela.
«Detenido es solo objetivo = 0 rpm»
Con un integrador y un sensor que no mide por debajo de 45 rpm, intentar «regular» a cero es incorrecto: el controlador seguiría empujando. Detenido significa potencia cero y descarte de la integral. Es una orden distinta de «mantener una velocidad pequeña».
«Si el motor se traba, el lazo lo resolverá»
El lazo tratará de aumentar la potencia hasta el máximo para vencer el obstáculo, y quemará el motor o el transistor. La protección es una decisión del programa, con tiempo y umbrales, y su respuesta es siempre apagar.
«Si el diseño funciona en la simulación, funcionará en el motor»
La simulación vale tanto como el modelo. Un motor real tiene fricción, zona muerta, ruido y un voltaje de fuente que cambia. Sirve para descartar sintonías evidentemente malas y para pensar, no para reemplazar la medición en el equipo real.
13 · Práctica guiada
Ejercicios y variantes
1. Pasar ganancias a enteros
El código usa kp_m y ki_m para ganancias en %/rpm y %/(rpm·s). Calculá los valores para Kp = 0,05 %/rpm y Ki = 0,5 %/(rpm·s) con un lazo de 20 ms.
Ver solución
0,05 %/rpm = 0,5 milésimas por rpm: kp_m = 500. Ki: 0,5 %/(rpm·s) = 5 milésimas por rpm y por segundo; por 0,02 s son 0,1 milésimas por rpm, o sea ki_m = 100. La regla es kp_m = Kp · 10 000 y ki_m = Ki · período(s) · 10 000.
2. De pulsos a rpm
El período entre pulsos medido es de 4000 µs, con 20 ranuras. ¿Cuántas rpm son? Con un contador a 100 kHz (PSC = 719) en lugar de 1 MHz, ¿cuál es la velocidad mínima medible y cuál el error de cuantización a 1200 rpm?
Ver solución
rpm = 3 000 000 / 4000 = 750 rpm. A 100 kHz cada cuenta dura 10 µs y el período máximo es 655 ms: la velocidad mínima baja a 3 000 000 / 655 350 ≈ 4,6 rpm. A 1200 rpm hay 250 cuentas por período, así que el error de ±1 cuenta es de 0,4 % (diez veces peor que con 1 MHz). Se gana rango a costa de resolución, el compromiso de siempre.
3. La tarea con atraso
La tarea de control tiene programada una ejecución cada 10 ms, pero main la atiende con un atraso medio de 2 ms, y el planificador calcula el próximo instante como ahora + 10. ¿Cuál es la frecuencia real del lazo, y cómo afecta a la acción integral?
Ver solución
El período real es de 12 ms: la frecuencia baja de 100 a 83,3 Hz. Como ki_m está calculado por paso de 10 ms, la acción integral por segundo se reduce en un 16,7 % y el lazo se comporta distinto a lo sintonizado, de manera variable y difícil de diagnosticar. Con proximo += periodo, la frecuencia media es exactamente 100 Hz aunque cada ejecución llegue con atraso.
4. El motor trabado
El eje queda bloqueado con el objetivo de 2400 rpm. Sin la protección de motor trabado y sin antiwindup, ¿qué le pasa a la potencia a lo largo del tiempo, y qué ocurre cuando se libera el eje?
Ver solución
El error permanece en 2400 rpm: la parte proporcional pide 480 milésimas de inmediato, y el integrador suma 13 × 2400 = 31 200 unidades por paso (31,2 milésimas de potencia). Se alcanza el tope del 100 % en unos 17 pasos, menos de 200 ms y la corriente de bloqueo del motor circula sin pausa. Si se libera el eje, la integral acumulada sigue aplicando la potencia máxima hasta que el error cambie de signo: el motor dispara a altas rpm y recién entonces el integrador baja. Con la protección y el antiwindup, la potencia se corta al cabo de 1 s y no queda deuda acumulada.
5. La carga del procesador
La ISR del tick dura 0,4 µs por milisegundo; la de captura a 6000 rpm (2000 pulsos por segundo) dura 1,2 µs; la tarea de control dura 2 µs cada 10 ms. ¿Qué fracción del procesador consumen entre las tres?
Ver solución
Tick: 0,4 / 1000 = 0,04 %. Captura: 2000 × 1,2 µs = 2,4 ms por segundo, 0,24 %. Control: 2 / 10 000 = 0,02 %. Total ≈ 0,3 %. Aunque se multiplicara por diez por tareas adicionales, quedaría un 97 % libre. Este tipo de cuenta es la que hay que escribir antes de preocuparse por «eficiencia».
6. El tiempo de espera de los pulsos
El tacómetro declara «sin pulsos» a los 100 ms. ¿A partir de qué velocidad el motor sería considerado detenido por error? Si se quisiera medir desde 15 rpm, ¿qué dos cosas habría que cambiar?
Ver solución
100 ms sin pulsos equivale a menos de 10 pulsos por segundo, es decir, 10 × 60 / 20 = 30 rpm: por debajo de eso se informa cero. Para 15 rpm el período es de 200 ms, así que el tiempo de espera debe pasar a más de 200 ms (por ejemplo, 300 ms), y el contador debe poder medir esos 200 ms: con 1 MHz el máximo es 65,5 ms, hay que bajar la cuenta a 100 kHz (655 ms de rango) o contar los desbordes del contador.
14 · Síntesis
Ideas para recordar
- El tick de 1 ms por interrupción (SysTick) es la base de tiempo del sistema; su ISR hace una sola cosa y tiene la prioridad más baja, y los plazos se comparan siempre restando.
- Un planificador mínimo —una tabla de funciones con su período— ejecuta tareas que nunca esperan; se avanza por el instante teórico para que el atraso no se acumule, y se mide el atraso máximo y la duración de cada tarea.
- La tabla de vectores debe llegar hasta cada interrupción que se use: sin eso, la rutina cae en
Default_Handlery el programa se detiene. - Para velocidades bajas conviene medir el período entre pulsos y no contar pulsos en una ventana: la cuantización de la primera es de ±1 µs, la de la segunda puede ser de 25 %. El tacómetro descarta el primer intervalo tras una pausa y declara cero rpm si no hay pulsos recientes.
- Un controlador PI en enteros elimina el error permanente que deja el controlador proporcional; el integrador debe protegerse contra la saturación (antiwindup) y la ganancia integral depende del período del lazo.
- El período del lazo debe ser como máximo la décima parte de la constante de tiempo del motor: más lento oscila.
- El módulo de control es una función pura de enteros, sin hardware: se prueba en la PC con el mismo código que irá al chip. La simulación descarta las sintonías malas, pero no reemplaza la medición en el equipo real.
- Un sistema seguro decide su respuesta ante las fallas: potencia cero al arrancar, ante un motor trabado y ante la falta de sensor.
¿Listo para el tema 19?
Antes de avanzar, explicá por qué proximo += periodo mantiene la frecuencia media y proximo = ahora + periodo no; decí qué le pasa a un lazo PI cuando la salida se satura y no tiene protección; y describí cómo medirías la velocidad de un disco de 20 ranuras a 60 rpm. Si podés justificar las tres respuestas, el taller cumplió su objetivo.
En el próximo tema generalizamos el planificador que escribimos acá: la planificación cooperativa, con tareas periódicas, prioridades, un presupuesto de tiempo y la disciplina de ejecutar sin bloqueos, el paso previo a un sistema operativo de tiempo real.