← Índice del curso

Sistemas Embebidos · Tema 15

Interrupciones

El polling obliga al programa a preguntar. Una interrupción invierte la relación: es el hardware el que avisa, y el procesador deja lo que estaba haciendo, atiende el evento y vuelve exactamente donde estaba. Es el mecanismo que permite responder en microsegundos sin que el bucle principal lo sepa.

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:

Las «compuertas» que debe atravesar una interrupción, y qué ocurre si una está cerrada.
EtapaQué ocurreSi está cerrada
1. El evento en el periféricoEl periférico levanta una bandera interna (el temporizador desbordó, llegó un byte, cambió el pin)No hay evento: nada que atender
2. Habilitación localUn bit del propio periférico (por ejemplo, «habilitar interrupción de desbordamiento») permite que la bandera genere una solicitudLa bandera se activa, pero no se pide interrupción
3. Habilitación en el NVICEl controlador de interrupciones del núcleo tiene un bit por fuente que la deja pasarLa solicitud queda registrada como pendiente, sin atenderse
4. Prioridad suficienteLa solicitud debe ser más urgente que lo que el procesador esté ejecutandoEspera a que termine la rutina actual
5. Sin enmascararLas 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:

  1. Termina la instrucción en curso y guarda automáticamente en la pila ocho registros: R0 a R3, R12, LR, PC y xPSR (32 bytes). Es el «contexto» a preservar.
  2. Busca el vector correspondiente: lee de la tabla de vectores (tema 8) la dirección de la rutina de esa fuente.
  3. Salta a la rutina, cargando en LR un valor especial (EXC_RETURN) que recuerda cómo volver.
  4. Ejecuta la rutina de servicio (ISR).
  5. 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).

Secuencia de una interrupción El programa principal se ejecuta hasta que ocurre un evento. El hardware apila el contexto, ejecuta la rutina de servicio y, al terminar, desapila el contexto y el programa principal continúa en la instrucción siguiente. programa principal apila ISR desapila el programa continúa donde estaba evento (solicitud de interrupción) el programa principal está detenido durante todo este tramo

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_Irqhandler el compilador no se queja: simplemente define otra función, y la interrupción sigue atendiéndose en Default_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.
Prioridades de las excepciones en un Cortex-M. Los valores negativos son fijos.
ExcepciónPrioridadComentario
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, SysTickConfigurableUsadas por el sistema operativo y la base de tiempo
Interrupciones de periféricosConfigurable (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:

Qué retrasa el comienzo de una ISR de prioridad p.
CausaSuma al peor casoCó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 cursoLa más larga del programa (hasta ms)Secciones críticas cortas; usar BASEPRI (tema 16)
ISR de igual o mayor urgencia en ejecuciónSu duración completa, y la de las que lleguen y también sean más urgentesISR cortas; reordenar prioridades
ISR de menor urgencia en ejecuciónSolo la entrada del hardware (se la interrumpe)No hace falta: la preempción lo resuelve
Interrupción de igual prioridad pendiente con número menorSu duraciónAsignar 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.

Un ejemplo de asignación para las tres fuentes del laboratorio.
FuentePlazoPor qué ese plazoPrioridad
UART, byte recibido87 µsA 115200 baudios llega el siguiente byte y pisa al anterior si no se leyó (sin FIFO)1 (más urgente)
Temporizador del lazo de control500 µsLa salida debe actualizarse dentro del período de 1 ms2
Botón1000 µsUna persona no percibe 1 ms; el rebote ya impone esperas de decenas de ms3

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 (PendSV en 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

TIM · llega a 100 µs · plazo 500 µs—
UART · llega a 150 µs · plazo 87 µs—
BTN · llega a 170 µs · plazo 1000 µs—
Resultado—

Línea de tiempo

—

Cambiá las prioridades para ver quién interrumpe a quién.

Recorrido guiado

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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

  1. 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.
  2. 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.
  3. Asigná las prioridades por plazo (el más corto, el más urgente) y documentalas en un único lugar.
  4. Configurá en orden: periférico, habilitación local, prioridad y, al final, habilitación en el NVIC.
  5. 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).
  6. 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.
  7. 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).
  8. 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.