01 · Punto de partida
De preguntar a que nos avisen
En el tema anterior viste el techo del polling: la latencia es una vuelta del bucle, los eventos más cortos que la vuelta se pierden y el procesador nunca descansa. Una interrupción elimina las tres limitaciones a la vez. Es una señal que genera el propio hardware (un pin que cambia, un temporizador que vence, un byte que llega, una conversión que termina) y que obliga al procesador a suspender lo que está haciendo, ejecutar una función corta asociada al evento y luego continuar.
La analogía clásica es la de un teléfono. Con polling, descolgás el aparato cada diez segundos para ver si alguien llama; con interrupciones, el teléfono suena cuando hay una llamada y mientras tanto hacés otra cosa. Y como en la analogía, hay reglas de convivencia: no se atiende un llamado largo cuando entra uno urgente, y hay que volver a la tarea interrumpida sin haber perdido el hilo.
Usamos los Cortex-M como ejemplo (el controlador de interrupciones se llama NVIC), y como siempre las direcciones son las de la familia STM32F103; la idea es la misma en cualquier microcontrolador.
02 · El mecanismo
Cómo funciona una interrupción
El camino desde un evento físico hasta la primera instrucción de tu rutina atraviesa varias etapas, y vale la pena conocerlas todas porque si cualquiera de ellas está cerrada, la interrupción nunca llega:
| Etapa | Qué ocurre | Si está cerrada |
|---|---|---|
| 1. El evento en el periférico | El periférico levanta una bandera interna (el temporizador desbordó, llegó un byte, cambió el pin) | No hay evento: nada que atender |
| 2. Habilitación local | Un bit del propio periférico (por ejemplo, «habilitar interrupción de desbordamiento») permite que la bandera genere una solicitud | La bandera se activa, pero no se pide interrupción |
| 3. Habilitación en el NVIC | El controlador de interrupciones del núcleo tiene un bit por fuente que la deja pasar | La solicitud queda registrada como pendiente, sin atenderse |
| 4. Prioridad suficiente | La solicitud debe ser más urgente que lo que el procesador esté ejecutando | Espera a que termine la rutina actual |
| 5. Sin enmascarar | Las interrupciones no están globalmente deshabilitadas (PRIMASK) ni enmascaradas por prioridad (BASEPRI) | Espera a que se vuelvan a habilitar |
Cuando todas las compuertas están abiertas, el procesador hace por hardware lo siguiente, sin que ninguna instrucción tuya intervenga:
- Termina la instrucción en curso y guarda automáticamente en la pila ocho registros:
R0aR3,R12,LR,PCyxPSR(32 bytes). Es el «contexto» a preservar. - Busca el vector correspondiente: lee de la tabla de vectores (tema 8) la dirección de la rutina de esa fuente.
- Salta a la rutina, cargando en
LRun valor especial (EXC_RETURN) que recuerda cómo volver. - Ejecuta la rutina de servicio (ISR).
- Al terminar, la rutina retorna, el hardware reconoce el valor especial, desapila los ocho registros y el programa interrumpido sigue como si nada hubiera pasado.
Todo el trámite de entrada tarda 12 ciclos de reloj en un Cortex-M3 o M4 (≈ 0,17 µs a 72 MHz). Y un detalle de diseño genial de ARM: como el hardware guarda los registros que una función de C puede destruir, una ISR es una función de C común, sin atributos ni ensamblador especiales (en otras arquitecturas, el compilador necesita un modo especial «interrupt» que guarde todo).
Desde el punto de vista del programa principal, la interrupción es invisible: no sabe que fue detenido. Salvo por una consecuencia que va a ocupar todo el tema 16: lo que la ISR modifica, el programa principal lo ve cambiado, sin haber hecho nada para provocarlo.
03 · Escribir la rutina
La rutina de servicio (ISR)
Una ISR (Interrupt Service Routine) es una función sin argumentos y sin valor de retorno cuyo nombre lo fija la tabla de vectores. En el archivo de arranque del tema 12 las rutinas que no se definen apuntan a Default_Handler mediante el atributo weak; para atender una fuente, alcanza con definir una función con el nombre exacto, y el enlazador la elige en lugar de la genérica:
volatile uint32_t pulsaciones; // compartida con el programa principal (tema 16)
void EXTI0_IRQHandler(void) { // nombre fijado por la tabla de vectores del chip
EXTI_PR = (1u << 0); // 1) borrar la bandera de la línea 0 (se borra escribiendo un 1)
pulsaciones++; // 2) hacer lo mínimo
}
Hay dos reglas inmediatas que aparecen en esas dos líneas:
- Hay que borrar la causa de la solicitud. Si la ISR no limpia la bandera del periférico, cuando retorna el NVIC sigue viendo la solicitud activa y vuelve a entrar de inmediato, una y otra vez. Es una tormenta de interrupciones: el procesador pasa todo el tiempo en la ISR y el programa principal no vuelve a ejecutarse. Cómo se limpia depende de cada periférico: escribir un 1 en un registro de pendientes, leer el registro de datos, escribir un valor.
- El nombre debe ser exacto. Si escribís
EXTI0_Irqhandlerel compilador no se queja: simplemente define otra función, y la interrupción sigue atendiéndose enDefault_Handler, que es un bucle infinito. Es de las causas más comunes de «mi interrupción no funciona».
Para que la ISR se ejecute hay que abrir las compuertas de la tabla anterior, en este orden: configurar el periférico, habilitar su interrupción local, fijar la prioridad y, por último, habilitarla en el NVIC (la regla del tema 9: habilitar al final). Para el botón de PA0, con las funciones del núcleo que define CMSIS:
#define EXTI_IMR (*(volatile uint32_t *)0x40010400) // máscara de interrupciones
#define EXTI_FTSR (*(volatile uint32_t *)0x4001040C) // flanco de bajada
#define EXTI_PR (*(volatile uint32_t *)0x40010414) // pendientes
void boton_irq_iniciar(void) {
EXTI_FTSR |= 1u; // 1) interrumpir en el flanco descendente (botón a masa)
EXTI_IMR |= 1u; // 2) habilitar la línea 0 (por defecto apunta a PA0)
NVIC_SetPriority(EXTI0_IRQn, 2); // 3) prioridad del botón
NVIC_EnableIRQ(EXTI0_IRQn); // 4) recién ahora, abrir la compuerta del NVIC
}
NVIC_SetPriority y NVIC_EnableIRQ son funciones pequeñas de CMSIS que escriben los registros del NVIC (NVIC_IPR en 0xE000E400 y NVIC_ISER en 0xE000E100); son parte del núcleo del procesador y no del fabricante, así que funcionan igual en cualquier Cortex-M. El número de la fuente (EXTI0_IRQn = 6 en esta familia; TIM2_IRQn = 28; USART1_IRQn = 37) es el que figura en la tabla de vectores del manual del chip.
04 · Cuando llegan varias a la vez
Prioridades, preempción y anidamiento
Casi nunca hay una sola interrupción. Cuando varias se solicitan a la vez, o una llega mientras se atiende otra, el NVIC decide con prioridades. En un Cortex-M las reglas son pocas y muy precisas:
- Un número menor significa más urgente. La prioridad 0 es la más alta. Es la convención más contraintuitiva: «alta prioridad» es un número bajo.
- Una interrupción preempta (interrumpe) a la que se está atendiendo solo si es estrictamente más urgente. Si llega una de prioridad 1 mientras corre una de prioridad 2, la de 2 queda en pausa, se ejecuta la de 1 y después se reanuda la primera. A esto se le llama anidamiento.
- Una de igual o menor urgencia espera. Queda pendiente hasta que la actual termine; no la interrumpe.
- Si varias están pendientes a la vez, gana la más urgente; con igual prioridad, la de número de fuente menor.
- El programa principal es lo menos urgente: cualquier interrupción lo interrumpe.
| Excepción | Prioridad | Comentario |
|---|---|---|
| Reset | −3 (fija) | La más urgente de todas |
| NMI | −2 (fija) | No enmascarable: por ejemplo, el fallo del reloj |
| HardFault | −1 (fija) | Errores graves del procesador |
| SVCall, PendSV, SysTick | Configurable | Usadas por el sistema operativo y la base de tiempo |
| Interrupciones de periféricos | Configurable (0 a 15 en el STM32F103) | Los chips implementan solo los bits más altos: 4 bits = 16 niveles en esta familia; 2 bits = 4 niveles en un Cortex-M0 |
El NVIC admite además una subprioridad, que solo desempata entre interrupciones pendientes del mismo nivel de preempción pero nunca permite que una interrumpa a la otra. Para empezar conviene dejar todos los bits como prioridad de preempción y no usarla.
También hay dos formas de enmascarar interrupciones desde el programa. __disable_irq() (que activa PRIMASK) las deshabilita todas las configurables; es una sección crítica. Los Cortex-M3 y superiores tienen además BASEPRI, que enmascara solo las de prioridad igual o menos urgente que un valor dado, y deja pasar las más urgentes. Su uso correcto se ve en el tema 16.
05 · Cuánto tarda en atenderse
Latencia de interrupción
La interrupción no atiende al instante: hay un retraso entre el evento y la primera instrucción de la ISR. Con el vocabulario del tema 13, el tiempo de respuesta de una interrupción es su latencia más su tiempo de ejecución, y la latencia depende de qué más esté ocurriendo:
| Causa | Suma al peor caso | Cómo reducirla |
|---|---|---|
| Entrada del hardware (apilar y buscar el vector) | 12 ciclos (0,17 µs a 72 MHz; 1,5 µs a 8 MHz) | Reloj más rápido; vectores en una memoria sin esperas |
| Sección crítica en curso | La más larga del programa (hasta ms) | Secciones críticas cortas; usar BASEPRI (tema 16) |
| ISR de igual o mayor urgencia en ejecución | Su duración completa, y la de las que lleguen y también sean más urgentes | ISR cortas; reordenar prioridades |
| ISR de menor urgencia en ejecución | Solo la entrada del hardware (se la interrumpe) | No hace falta: la preempción lo resuelve |
| Interrupción de igual prioridad pendiente con número menor | Su duración | Asignar prioridades distintas |
La regla de oro: a una interrupción solo la retrasan las de igual o mayor urgencia y las secciones críticas. Las de menor urgencia no cuentan, porque se las interrumpe. Esto convierte la asignación de prioridades en una decisión de diseño con consecuencias medibles, no en un adorno.
Cuando una ISR termina y hay otra pendiente, el Cortex-M evita desapilar y volver a apilar: salta directamente a la siguiente (tail-chaining, encadenamiento de colas), con apenas 6 ciclos de transición. Es una de las razones por las que muchas ISR cortas seguidas son baratas en estos procesadores.
Una cuenta pequeña ayuda a no subestimar el costo de las interrupciones frecuentes. Un temporizador que interrumpe cada 100 µs, con una ISR de 12 µs más 0,4 µs de entrada y salida, consume 12,4 / 100 = 12,4 % del procesador. La UART a 115200 baudios interrumpe cada 86,8 µs (un byte); con una ISR de 3 µs son otro 3,5 %. Entre las dos, un 16 % de la CPU se va en atender interrupciones antes de que el programa principal haga una sola cosa.
06 · Un criterio de diseño
Cómo asignar prioridades
El criterio básico es: cuanto más corto es el plazo de una interrupción, mayor (más baja numéricamente) es su prioridad. Un plazo corto significa que no admite espera; uno largo admite que se atiendan otras antes. Es el principio de planificación «el plazo más corto primero», que volverá en el tema 19.
| Fuente | Plazo | Por qué ese plazo | Prioridad |
|---|---|---|---|
| UART, byte recibido | 87 µs | A 115200 baudios llega el siguiente byte y pisa al anterior si no se leyó (sin FIFO) | 1 (más urgente) |
| Temporizador del lazo de control | 500 µs | La salida debe actualizarse dentro del período de 1 ms | 2 |
| Botón | 1000 µs | Una persona no percibe 1 ms; el rebote ya impone esperas de decenas de ms | 3 |
Algunos principios que evitan los problemas habituales:
- Cada ISR es una interferencia para todas las de menor urgencia. Asignar prioridad alta «por las dudas» a una ISR larga es degradar a todas las demás. Una prioridad alta se gana con un plazo corto y una ISR corta.
- Las ISR largas deben ser de baja prioridad, o dejar de ser largas. Una ISR que dura 1 ms bloquea durante 1 ms a todo lo que sea de igual o menor urgencia; el laboratorio lo muestra.
- Los hardware con FIFO o DMA relajan el plazo. Una UART con un buffer de 16 bytes tiene un plazo de 16 × 87 µs ≈ 1,4 ms, y puede tener una prioridad más baja. Eligiendo bien el hardware, se alivian las prioridades.
- Evitar el empate. Dos fuentes con la misma prioridad no se interrumpen entre sí, y el desempate por número de fuente es arbitrario. Lo habitual es asignar niveles distintos a lo que importa.
- Reservar los niveles extremos. Se suele dejar el nivel 0 para lo realmente crítico (protecciones de seguridad) y los más bajos para tareas de fondo del sistema operativo (
PendSVen el nivel más bajo, por ejemplo, tema 20).
Por último, documentá la tabla de prioridades en un solo lugar del proyecto (una cabecera de configuración, tema 11) con el motivo de cada una. Las prioridades dispersas por el código, cada una fijada donde se inicializa su periférico, son una de las peores formas de heredar un firmware.
07 · Analogía con programación
Un simulador de interrupciones en Python
Este programa implementa las reglas del NVIC con tiempo discretizado en décimas de microsegundo: llegadas, preempción solo por mayor urgencia, espera de las demás, desempate por número de fuente y una sección crítica opcional del programa principal. Las tres fuentes son las de la tabla anterior: un temporizador que llega a los 100 µs y dura 150, una UART a los 150 µs que dura 15, y un botón a los 170 µs que dura 40. Se agregan 0,4 µs de entrada y salida a cada una. Se ejecutan tres escenarios.
Python en tu navegador. Los bloques son editables: cambiá los valores y presioná «Ejecutar Python». La primera vez tarda unos segundos en cargar.
U = 10 # ticks por µs (resolución de 0,1 µs)
OVERHEAD = 0.4 # µs de apilar y desapilar, por interrupción
FUENTES = { # nombre: (llegada µs, duración µs, plazo µs)
"TIM": (100, 150, 500),
"UART": (150, 15, 87),
"BTN": (170, 40, 1000),
}
def simular(prioridades, critica_us=0):
irqs = []
for i, (nombre, (llegada, dur, plazo)) in enumerate(FUENTES.items()):
irqs.append(dict(nombre=nombre, idx=i, prio=prioridades[nombre], llegada=llegada * U,
rem=round((dur + OVERHEAD) * U), plazo=plazo, inicio=None, fin=None))
crit_ini, crit_fin = 90 * U, (90 + critica_us) * U # el programa principal deshabilita interrupciones
pendientes, pila, hechos, t = [], [], 0, 0
while hechos < len(irqs):
for q in irqs:
if q["llegada"] == t:
pendientes.append(q)
tope = pila[-1] if pila else None
if pendientes:
pendientes.sort(key=lambda q: (q["prio"], q["idx"])) # la más urgente primero
cand = pendientes[0]
en_critica = crit_ini <= t < crit_fin
if (tope and cand["prio"] < tope["prio"]) or (not tope and not en_critica):
pendientes.pop(0) # preempta o arranca
if cand["inicio"] is None:
cand["inicio"] = t
pila.append(cand)
if pila:
q = pila[-1]
q["rem"] -= 1
if q["rem"] == 0:
q["fin"] = t + 1
pila.pop()
hechos += 1
t += 1
return irqs
ESCENARIOS = [
("UART la más urgente (TIM 2, UART 1, BTN 3)", {"TIM": 2, "UART": 1, "BTN": 3}, 0),
("UART la menos urgente (TIM 1, UART 3, BTN 2)", {"TIM": 1, "UART": 3, "BTN": 2}, 0),
("Prioridades buenas + sección crítica de 100 µs", {"TIM": 2, "UART": 1, "BTN": 3}, 100),
]
for titulo, prios, critica in ESCENARIOS:
print(titulo)
for q in simular(prios, critica):
lat = (q["inicio"] - q["llegada"]) / U
resp = (q["fin"] - q["llegada"]) / U
estado = "cumple" if resp <= q["plazo"] else "NO cumple"
print(f" {q['nombre']:5s} latencia {lat:6.1f} µs respuesta {resp:6.1f} µs plazo {q['plazo']:4d} µs {estado}")
print()
Resultado: en el primer escenario la UART, la más urgente, interrumpe al temporizador a los 150 µs y se resuelve en 15,4 µs sin latencia; el temporizador termina a los 165,8 µs de haber llegado y el botón espera 95,8 µs, y los tres cumplen. En el segundo escenario, con la UART la menos urgente, tiene que esperar a que terminen el temporizador y el botón: su latencia es de 140,8 µs y su respuesta de 156,2 µs, superior al plazo de 87 µs, así que se pierde un byte. En el tercero, con una sección crítica de 100 µs, nada puede empezar antes de los 190 µs: la UART espera 40 µs (todavía cumple, con 55,4 µs de respuesta) y el temporizador 105,4 µs. Probá una sección crítica de 500 µs, o cambiá la duración del temporizador a 1000 µs.
Las prioridades no aceleran nada: reparten el tiempo. El procesador hace el mismo trabajo total en los tres escenarios, y lo único que cambia es a quién se le cobra la espera. Asignar bien las prioridades es decidir quién puede esperar.
08 · Exploración interactiva
Prioridades en acción
El laboratorio ejecuta el simulador del apartado anterior. Asigná una prioridad a cada fuente (0 es la más urgente), elegí cuánto dura la rutina del temporizador y si el programa principal deshabilita las interrupciones durante un tiempo. Verás la línea de tiempo (quién corre en cada instante), la latencia y la respuesta de cada fuente y si cumple su plazo.
Tres fuentes de interrupción
Línea de tiempo
—
Cambiá las prioridades para ver quién interrumpe a quién.
Recorrido guiado
- Con los valores iniciales (UART 1, TIM 2, BTN 3): el temporizador empieza a los 100 µs; a los 150 µs la UART lo interrumpe porque es más urgente, se atiende en 15,4 µs y el temporizador se reanuda. El botón llega a los 170 µs, es menos urgente que el temporizador y espera a que termine (latencia de 95,8 µs). Los tres cumplen.
- Poné la UART en prioridad 3, el temporizador en 1 y el botón en 2: ahora la UART espera a los dos y tiene 140,8 µs de latencia y 156,2 µs de respuesta, más que el plazo de 87 µs. Se pierde un byte, con exactamente las mismas rutinas. La única diferencia fue el orden.
- Poné las tres prioridades en 1: nadie interrumpe a nadie. La UART espera al temporizador completo (100,4 µs de latencia) y falla su plazo. Dos fuentes con la misma prioridad no se preemptan.
- Volvé a las prioridades iniciales y poné la rutina del temporizador en 1000 µs: la UART, más urgente, sigue cumpliendo. Pero el temporizador ahora tarda 1015,8 µs, porque la UART lo interrumpió, y pierde su plazo de 500 µs. Una ISR larga sufre las interrupciones más urgentes.
- Poné la UART en 2 y el temporizador en 1 con la rutina larga de 1000 µs: la UART espera casi 950 µs. Una ISR larga de alta prioridad ahoga a todas las demás.
- Volvé a los valores iniciales y activá la sección crítica de 100 µs: durante ese lapso nada se atiende, aunque sea de prioridad 0. Al terminar, la UART pasa primero y las demás siguen en orden. Con 500 µs, el temporizador y la UART pierden sus plazos aunque las prioridades sean correctas.
Desafío: ¿se puede arreglar la UART sin cambiar prioridades?
Sí, y es lo que se hace en la práctica: cambiando el hardware o su uso. Si la UART tiene un FIFO de 16 bytes, o se atiende con DMA a un buffer, el plazo ya no es de 87 µs sino de más de 1 ms, y la prioridad puede ser baja. Es el criterio general: no se resuelve un plazo ajustado con prioridades cuando se lo puede relajar con hardware. Las prioridades son el último recurso, porque cada una que se sube perjudica a todas las demás.
09 · Método de trabajo
Método para introducir interrupciones
- Justificá la necesidad. Escribí qué evento no se puede atender por polling (por ser más corto que la vuelta, por una latencia exigente o por querer dormir el procesador). Si el polling alcanza, no agregues interrupciones.
- Escribí el plazo de cada fuente y su tiempo de ejecución en el peor caso. Con eso se calculan la carga y la latencia.
- Asigná las prioridades por plazo (el más corto, el más urgente) y documentalas en un único lugar.
- Configurá en orden: periférico, habilitación local, prioridad y, al final, habilitación en el NVIC.
- Escribí la ISR mínima: borrar la causa, tomar el dato o marcar el evento, y salir. El trabajo largo se hace en el programa principal (tema 16).
- Verificá con una medición, no con la intuición: poné un pin en alto al entrar a la ISR y en bajo al salir, y medí la latencia desde la señal de entrada con un analizador lógico.
- Calculá la carga total de interrupciones sumando la fracción del tiempo que consume cada una; si pasa del 20 o 30 %, replantea el diseño (más hardware, buffers, DMA).
- Probá los casos malos: varias interrupciones a la vez, la más frecuente en su ritmo máximo, la sección crítica más larga del programa. Verificá qué pasa cuando el plazo no se cumple.
Un diagnóstico útil para una interrupción que «no funciona»: recorré las cinco compuertas de la tabla del apartado 2 en orden, mirando los registros en el depurador. En la inmensa mayoría de los casos, una de ellas está cerrada o el nombre de la rutina tiene un error de tipeo.
10 · Comprensión
Confusiones frecuentes
«Las interrupciones hacen el programa más rápido»
No ejecutan más instrucciones por segundo; permiten responder a tiempo a lo urgente y que el procesador descanse cuando no hay nada para hacer. De hecho cada interrupción cuesta ciclos de entrada y salida. Mejoran la respuesta, no el rendimiento.
«Una prioridad alta es un número alto»
En los Cortex-M es al revés: el 0 es lo más urgente. Es una fuente constante de errores al configurar; conviene definir constantes con nombre (PRIO_UART, PRIO_CONTROL) y comentar el sentido.
«Una interrupción de mayor número interrumpe a una de menor número»
Solo se interrumpe a una ISR cuando llega otra estrictamente más urgente (de número menor). Dos con la misma prioridad esperan una a la otra, y una menos urgente espera a la más urgente.
«Si no borro la bandera no pasa nada»
Cuando la ISR retorna, el NVIC ve que la solicitud sigue activa y vuelve a entrar. El resultado es una tormenta de interrupciones: el programa principal deja de ejecutarse y, si hay watchdog, termina reiniciando el equipo.
«Dentro de una ISR puedo hacer lo mismo que en el programa principal»
Sintácticamente sí, pero bloquea a todo lo menos urgente mientras dura. No se debe esperar, imprimir, reservar memoria ni hacer cálculos largos dentro de una ISR; se marca el evento y se atiende afuera (tema 16).
«Mi ISR no se ejecuta, el chip está fallado»
Casi siempre es una compuerta cerrada: el periférico sin habilitar su interrupción, la fuente sin habilitar en el NVIC, las interrupciones enmascaradas o un nombre mal escrito en la rutina. Se revisa en el orden de la tabla del apartado 2.
11 · Práctica guiada
Predecí antes de comprobar
1. ¿Quién se ejecuta y en qué orden?
Corre la ISR A (prioridad 2). Llegan, en este orden: B (prioridad 1), C (prioridad 2) y D (prioridad 3). ¿En qué orden se ejecutan?
Ver solución
B interrumpe a A de inmediato, por ser estrictamente más urgente. C (misma prioridad que A) y D (menos urgente) quedan pendientes. Cuando B termina, A continúa. Al terminar A, hay dos pendientes: C (prioridad 2) y D (prioridad 3), y se ejecuta primero C. Orden de finalización: B, A, C, D.
2. Latencia de entrada
La entrada a una interrupción tarda 12 ciclos. ¿Cuánto es a 48 MHz y a 8 MHz? ¿Y el transcurso total si la ISR dura 200 ciclos más 12 de salida?
Ver solución
12 / 48.000.000 = 0,25 µs y 12 / 8.000.000 = 1,5 µs. El trámite total (12 + 200 + 12 = 224 ciclos) dura 4,67 µs a 48 MHz y 28 µs a 8 MHz. A reloj lento, todo el procesamiento de interrupciones se encarece en la misma proporción.
3. El byte perdido
Una UART a 115200 baudios (sin FIFO) y una ISR de temporizador de mayor prioridad que dura 100 µs. ¿Qué pasa con los bytes? ¿Y a 9600 baudios?
Ver solución
A 115200 llega un byte cada 86,8 µs. Si el temporizador (más urgente) ocupa 100 µs, la UART no se atiende dentro de ese plazo y el byte siguiente pisa al anterior: se pierde un dato (overrun). A 9600 baudios hay 1,04 ms por byte, mucho más que 100 µs: no se pierde nada. Con una UART a 115200 hay que acortar la ISR del temporizador, usar un FIFO o DMA, o darle más prioridad a la UART.
4. Tormenta
Una ISR de recepción lee el byte pero se olvida de limpiar la bandera de «desbordamiento». ¿Cuál es el síntoma visible en el equipo y cómo se diagnostica?
Ver solución
Si la bandera mantiene activa la solicitud, el NVIC vuelve a entrar en la ISR apenas retorna: el procesador pasa casi todo el tiempo en ella y el programa principal se «congela» (el LED de vida deja de parpadear; si hay watchdog, se reinicia el equipo). En el depurador, al detenerlo siempre se cae dentro de la ISR. Se corrige limpiando la causa en la rutina, según indica el manual del periférico.
5. «Mi interrupción nunca llega»
Configuraste un temporizador con interrupción de desbordamiento y la ISR TIM2_IRQHandler nunca se ejecuta. Listá, en orden, qué revisarías.
Ver solución
(1) ¿El temporizador tiene reloj y está contando? (2) ¿Está habilitada la interrupción local (el bit UIE del registro DIER)? (3) ¿La bandera de desbordamiento se activa realmente? (4) ¿Está habilitada la fuente en el NVIC (NVIC_EnableIRQ(TIM2_IRQn))? (5) ¿Hay interrupciones enmascaradas por PRIMASK o BASEPRI? (6) ¿El nombre de la rutina es exactamente el de la tabla de vectores, con la misma capitalización? Un error en cualquiera de las seis alcanza.
6. La carga de las interrupciones
Un temporizador interrumpe cada 200 µs con una ISR de 8 µs, la UART cada 86,8 µs con una ISR de 3 µs, y se suman 0,4 µs de entrada y salida a cada una. ¿Qué fracción del procesador consumen entre las dos?
Ver solución
Temporizador: (8 + 0,4) / 200 = 4,2 %. UART: (3 + 0,4) / 86,8 ≈ 3,9 %. Total ≈ 8,1 %. Queda un 92 % para el programa principal. Es una carga cómoda; si fuera del 40 %, el programa principal sufriría y el diseño debería aliviarse con DMA o buffers.
12 · Síntesis
Ideas para recordar
- Una interrupción es un aviso del hardware que hace que el procesador suspenda el programa, ejecute una rutina de servicio y retome donde estaba; supera el techo del polling.
- Para que llegue deben estar abiertas cinco compuertas: el evento, la habilitación local del periférico, la habilitación en el NVIC, una prioridad suficiente y las interrupciones sin enmascarar.
- El hardware apila ocho registros, busca el vector, ejecuta la ISR y desapila (12 ciclos de entrada en Cortex-M3/M4); por eso una ISR es una función de C común.
- La ISR debe llamarse exactamente como en la tabla de vectores y debe borrar la causa de la solicitud; si no, hay una tormenta de interrupciones.
- En un Cortex-M un número menor es más urgente; una interrupción solo preempta a otra si es estrictamente más urgente; las de igual o menor urgencia esperan, y el desempate es por número de fuente.
- Una interrupción solo es retrasada por las de igual o mayor urgencia y por las secciones críticas; el peor caso de su latencia es la suma de esos retrasos más la entrada del hardware.
- Las prioridades se asignan por plazo (el más corto, el más urgente); las ISR largas deben ser de baja prioridad o dejar de ser largas, y relajar un plazo con hardware (FIFO, DMA) es mejor que subir una prioridad.
- Se mide con un pin que marca la entrada y la salida de la ISR y un analizador lógico; y se calcula la carga total de las interrupciones.
¿Listo para el tema 16?
Antes de avanzar, explicá qué hace el hardware desde el evento hasta la primera instrucción de la ISR; decí en qué orden se ejecutan tres interrupciones que llegan con prioridades 1, 2 y 2; y enumerá las compuertas que revisarías si una ISR nunca se ejecuta. Si podés justificar las tres respuestas, entendés el mecanismo.
En el próximo tema vemos cómo escribir buenas ISR: rutinas cortas, la palabra volatile, las secciones críticas, las banderas y los datos compartidos entre la interrupción y el programa principal, donde aparecen los errores más difíciles de encontrar.