← Índice del curso

Microcontroladores · Tema 19

Interrupciones: vectores y rutinas de servicio

Hasta ahora el programa preguntaba todo el tiempo si algo había pasado. Una interrupción invierte la relación: el hardware avisa, la CPU deja lo que estaba haciendo, atiende el evento y vuelve exactamente donde estaba. Es el mecanismo que permite reaccionar en microsegundos sin gastar la CPU en vigilar.

01 · Punto de partida

Preguntar o que te avisen

En el taller del tema 14 viste el límite del sondeo: para no perder un evento hay que consultar más seguido de lo que dura, y entre consulta y consulta el programa no puede hacer nada largo. Cuanto más rápido es el evento, peor. En el tema 17 el rebote de un pulsador mostró otra cara del mismo problema.

Si programaste con eventos, la idea te resulta familiar: un callback, un manejador de señales o un listener. El hardware genera el evento (un pin que cambia, un temporizador que llega a su valor, un byte que llega por la UART), y la rutina de servicio de interrupción (ISR, interrupt service routine) se ejecuta sola. El programa principal no se entera, salvo por el tiempo que le roba.

Retomamos la pila y las llamadas (tema 7), las banderas de eventos (temas 9 y 11) y el costo en ciclos (tema 14).

02 · Paso a paso

Qué hace la CPU al interrumpirse

Línea de tiempo de una interrupción Un evento ocurre mientras corre el programa principal. La CPU termina la instrucción en curso, guarda la dirección de retorno, salta al vector, ejecuta la rutina de servicio y, con RETI, retoma el programa principal en la instrucción siguiente. programa principalterminala instr. guarda PCy salta ISR: prólogo, cuerpo y epílogo RETIcontinúadonde estaba llega el evento latencia: ≈ 8 a 11 ciclos
  1. El periférico marca una bandera de evento (por ejemplo, TOV0 en TIFR0) y, si la interrupción está habilitada, pide atención a la CPU.
  2. La CPU termina la instrucción en curso; no se interrumpe una instrucción a mitad.
  3. Guarda en la pila la dirección de retorno (el PC, 2 bytes), deshabilita las interrupciones (pone en 0 el bit I del SREG) y carga el PC con la dirección del vector de ese evento.
  4. En el vector hay una instrucción de salto que lleva a la ISR, que ejecuta el cuerpo, a menudo con un prólogo que guarda registros y un epílogo que los restaura.
  5. La instrucción RETI saca la dirección de retorno de la pila, la carga en el PC y vuelve a habilitar las interrupciones.

La CPU no sabe cuál era la tarea del programa principal: la ISR debe dejar el estado (registros y banderas) exactamente como estaba, y de eso se ocupa el compilador. Visto desde afuera, la interrupción es invisible: el programa principal solo percibe que algunas instrucciones tardaron más.

03 · El mapa

La tabla de vectores

Cada evento que puede interrumpir tiene un vector: una posición fija al comienzo de la memoria Flash, donde la CPU salta. En el ATmega328P hay 26, cada uno de dos palabras (el espacio justo para una instrucción JMP). El primero es el reset; el programa de ejemplo del tema 5, con sus JMP en los primeros bytes del archivo HEX, era exactamente esta tabla.

%
Tabla de vectores del ATmega328P. Direcciones en palabras de la Flash; los números bajos tienen más prioridad.
N.ºDirecciónFuenteNombre en avr-libc
10x0000Reset (encendido, pin RESET, caída de tensión, vigilante)No es una ISR: arranca el programa
20x0002Interrupción externa 0 (pin PD2)INT0_vect
30x0004Interrupción externa 1 (pin PD3)INT1_vect
40x0006Cambio de pin, grupo 0 (PB0 a PB7)PCINT0_vect
50x0008Cambio de pin, grupo 1 (PC0 a PC6)PCINT1_vect
60x000ACambio de pin, grupo 2 (PD0 a PD7)PCINT2_vect
70x000CVigilante (modo interrupción)WDT_vect
80x000ETemporizador 2: coincidencia con OCR2ATIMER2_COMPA_vect
90x0010Temporizador 2: coincidencia con OCR2BTIMER2_COMPB_vect
100x0012Temporizador 2: desbordamientoTIMER2_OVF_vect
110x0014Temporizador 1: captura de entradaTIMER1_CAPT_vect
120x0016Temporizador 1: coincidencia con OCR1ATIMER1_COMPA_vect
130x0018Temporizador 1: coincidencia con OCR1BTIMER1_COMPB_vect
140x001ATemporizador 1: desbordamientoTIMER1_OVF_vect
150x001CTemporizador 0: coincidencia con OCR0ATIMER0_COMPA_vect
160x001ETemporizador 0: coincidencia con OCR0BTIMER0_COMPB_vect
170x0020Temporizador 0: desbordamientoTIMER0_OVF_vect
180x0022SPI: transferencia completaSPI_STC_vect
190x0024UART: byte recibidoUSART_RX_vect
200x0026UART: registro de datos vacíoUSART_UDRE_vect
210x0028UART: transmisión completaUSART_TX_vect
220x002AConversor analógico-digital: conversión terminadaADC_vect
230x002CEEPROM listaEE_READY_vect
240x002EComparador analógicoANALOG_COMP_vect
250x0030I2C (TWI): evento del busTWI_vect
260x0032Memoria de programa lista (SPM)SPM_READY_vect

La dirección de un vector se calcula como (n − 1) × 2 palabras. El vector 17 (TIMER0_OVF) está en la palabra 0x0020, es decir, en el byte 0x0040. Después de los 26 vectores (hasta la palabra 0x0033) empieza el código de inicio y tu programa; por eso el JMP del reset del tema 5 apuntaba a la palabra 0x0034.

El orden de la tabla es también el orden de prioridad: si dos eventos están pendientes a la vez, se atiende primero el de número menor. Por ejemplo, INT0 gana a cualquier temporizador y USART_RX gana a ADC.

04 · Los permisos

Habilitación global e individual

Para que una interrupción llegue a la CPU tienen que cumplirse tres condiciones, una detrás de la otra:

La cadena de permisos de una interrupción.
NivelQué esDónde
1. El eventoLa bandera del periférico vale 1Registro de estado del periférico (TIFR0, UCSR0A…)
2. La habilitación individualEl bit de máscara de ese evento vale 1Registro de control (TIMSK0, EIMSK…)
3. La habilitación globalEl bit I del SREG vale 1Se maneja con sei() y cli()

La razón de tres niveles: la bandera registra el evento aunque no se atienda (puede consultarse por sondeo, como en el tema 11); la máscara permite elegir qué eventos interesan; y el bit global permite suspender todas las interrupciones un instante (una sección crítica). Tras el reset, las máscaras y el bit global valen 0: no hay interrupciones hasta que las habilites.

La secuencia de configuración es siempre la misma:

  1. Configurar el periférico (por ejemplo, el modo y el prescaler del temporizador).
  2. Borrar la bandera por si quedó un evento viejo pendiente (recordá que se borra escribiendo un 1).
  3. Habilitar la máscara del evento.
  4. Habilitar las interrupciones con sei(), al final de la inicialización.

Una advertencia: dos instrucciones, SEI y la que sigue, no se pueden interrumpir entre sí; la CPU ejecuta siempre la instrucción siguiente a SEI antes de atender una interrupción pendiente. Es lo que permite el patrón sei(); sleep(); sin riesgo de perder el evento antes de dormir.

05 · El código

Escribir una ISR en C

Con avr-libc, una rutina de servicio se declara con la macro ISR() y el nombre del vector:

#include <avr/io.h>
#include <avr/interrupt.h>

volatile uint16_t milisegundos = 0;        // compartida con el programa principal

ISR(TIMER0_COMPA_vect) {                   // se ejecuta cada 1 ms (tema 11)
    milisegundos++;
}

int main(void) {
    DDRB |= (1 << PB5);

    TCCR0A = (1 << WGM01);                 // modo CTC
    OCR0A  = 249;                          // 1 ms a 16 MHz con prescaler 64
    TIMSK0 |= (1 << OCIE0A);               // habilitar la interrupción del temporizador
    TCCR0B = (1 << CS01) | (1 << CS00);    // arrancar el temporizador
    sei();                                 // habilitación global

    while (1) {
        /* …el programa principal hace otras cosas… */
        if (milisegundos >= 500) {         // (ver la sección 8 sobre este acceso)
            milisegundos = 0;
            PORTB ^= (1 << PB5);
        }
    }
}

Compará con el taller del tema 11: allí el programa esperaba la bandera con un bucle; aquí el hardware llama a la rutina, que solo cuenta, y el bucle principal queda libre. El compilador genera el prólogo y el epílogo: guarda R1, R0, SREG y los registros que la rutina usa, y los restaura antes de RETI.

Reglas de oro para una ISR

  • Corta. Hacer lo mínimo (contar, copiar un dato, activar una bandera) y dejar el trabajo largo para el programa principal.
  • Sin esperas. Nada de _delay_ms(), bucles que esperan eventos ni acceso lento a periféricos: mientras la ISR corre, las demás interrupciones esperan.
  • Sin funciones pesadas. Evitá printf, aritmética de coma flotante y divisiones: su costo en ciclos es alto y variable.
  • Variables compartidas volatile, como viste en el tema 10.
  • Limpiar el evento cuando el hardware no lo hace solo (leer UDR0 en la recepción de la UART).

06 · Cuánto cuesta y cuánto tarda

Latencia y costo

Dos números caracterizan una interrupción, y no hay que confundirlos:

  • La latencia: el tiempo desde que ocurre el evento hasta que se ejecuta la primera instrucción de la ISR. Incluye terminar la instrucción en curso (hasta 4 ciclos para las más largas), la respuesta de hardware (unos 4 ciclos, guardando el PC) y el salto al vector (3 ciclos con JMP): del orden de 8 a 11 ciclos, de 0,5 a 0,7 µs a 16 MHz, si no hay nada que la demore.
  • El costo: el tiempo total que la CPU dedica a cada evento. Suma la entrada, el prólogo, el cuerpo, el epílogo y RETI (4 ciclos).

El prólogo y el epílogo pesan: guardar R1, R0 y SREG y volver a ponerlos cuesta unos 15 ciclos, y cada registro adicional que la ISR use agrega 4 (un PUSH y un POP). Con la entrada y la salida, una ISR mínima que usa un registro y hace un incremento ronda los 30 a 35 ciclos, unos 2 µs a 16 MHz. Con un cuerpo de pocas instrucciones, el costo fijo domina.

Porcentaje de CPU consumido por una ISR a 16 MHz (cálculo con el modelo de costo anterior).
ISRCiclos por evento1 kHz20 kHz100 kHz
Contador (cuerpo de 3 ciclos, 1 registro)330,21 %4,1 %20,6 %
Cuerpo de 10 ciclos, 2 registros440,27 %5,5 %27,5 %
Cuerpo de 40 ciclos, 6 registros900,56 %11,3 %56,3 %

La latencia en el peor caso es mayor que la mínima, porque la interrupción puede encontrar a la CPU ocupada: si otra ISR está corriendo, hay que esperar a que termine; si hay una sección con interrupciones deshabilitadas (cli()), hasta que se levante. Para un diseño con requisitos de tiempo estrictos, la latencia máxima es la duración de la ISR más larga más la de la sección crítica más larga, más la entrada.

Es el mismo presupuesto de ciclos del tema 14, con un costo fijo por evento.

07 · Cuando hay varias

Prioridades y anidamiento

En el AVR, la prioridad no se programa: la fija el orden de la tabla (número de vector menor = más prioridad). Y solo decide qué pasa cuando dos interrupciones están pendientes a la vez, por ejemplo al salir de una sección crítica o de una ISR: se atiende la de menor número primero.

Una interrupción de alta prioridad no interrumpe a una ISR en curso: al entrar en una rutina, el hardware pone en 0 el bit I, así que mientras corra ninguna otra se atiende. Los eventos que lleguen quedan registrados en sus banderas y esperan. Si un evento llega varias veces durante la espera, la bandera vale 1 una sola vez: se pierde la cuenta, como las banderas del temporizador del tema 11. Una ISR demasiado larga es, por eso, una fuente de eventos perdidos para todos los demás.

Es posible el anidamiento si la propia ISR ejecuta sei() al comenzar. Permite que una interrupción urgente interrumpa a una larga, pero complica el análisis: la pila crece con cada nivel, se pueden reentrar rutinas, y es fácil introducir errores. En la mayoría de los proyectos con AVR se evita: se acortan las ISR en lugar de anidarlas.

Otros microcontroladores (los ARM Cortex-M, por ejemplo) tienen prioridades programables y un controlador de interrupciones con anidamiento por hardware. El ATmega328P es más simple, y por eso más fácil de razonar.

08 · Dos hilos de ejecución

Comunicar la ISR con el programa

Una ISR y el programa principal son, en la práctica, dos flujos de ejecución que comparten memoria. Todo lo que sepas de concurrencia vale aquí, con una diferencia: el flujo principal puede ser interrumpido en cualquier instrucción, pero la ISR nunca es interrumpida por el principal.

  • Banderas y contadores compartidos deben ser volatile, o el compilador puede guardarlos en un registro y no ver los cambios.
  • Los accesos de más de 8 bits no son atómicos. En el ejemplo de la sección 5, milisegundos es de 16 bits: el programa principal la lee con dos instrucciones, y si la ISR la incrementa entre ambas, la lectura puede dar un valor que nunca existió (por ejemplo, 0x00FF y 0x0100 combinados como 0x01FF). La solución es leer con las interrupciones deshabilitadas: una sección crítica. El tema 21 la desarrolla.
  • El patrón habitual es que la ISR haga lo mínimo (guardar un dato y marcar una bandera) y el programa principal haga el procesamiento. Cuando hay un flujo de datos (los bytes de la UART), la ISR escribe en un buffer circular y el principal lo lee.

Estas reglas son la base del diseño con interrupciones: no es difícil escribir una ISR; lo difícil es compartir bien lo que produce.

09 · Analogía con programación

Sondeo frente a interrupción, con números

Este programa compara el retraso con que se atiende un evento por sondeo (según la duración de una vuelta del bucle principal) y por interrupción. Después calcula el porcentaje de la CPU que consume una ISR según su cuerpo y la tasa de eventos.

Python en tu navegador. Los bloques son editables: cambiá los valores y presioná «Ejecutar Python». La primera vez tarda unos segundos en cargar.

import random

random.seed(1)
F_CPU = 16_000_000
ENTRADA = 4 + 3                  # respuesta de la CPU + salto al vector (ciclos)


def latencia_sondeo(ciclos_vuelta, eventos=20000):
    """El programa consulta la bandera una vez por vuelta del bucle principal."""
    lat = []
    for _ in range(eventos):
        llegada = random.uniform(0, ciclos_vuelta)       # el evento cae en un punto de la vuelta
        lat.append(ciclos_vuelta - llegada)               # espera hasta la próxima consulta
    return lat


def latencia_interrupcion(eventos=20000):
    """La CPU termina la instrucción en curso (1 a 4 ciclos) y entra a la rutina."""
    return [random.randint(1, 4) + ENTRADA for _ in range(eventos)]


def resumen(nombre, ciclos):
    promedio = sum(ciclos) / len(ciclos) / F_CPU * 1e6
    peor = max(ciclos) / F_CPU * 1e6
    print(f"{nombre:<34} promedio {promedio:9.2f} µs   peor caso {peor:9.2f} µs")


for vuelta in (160, 1_600, 16_000, 160_000):
    resumen(f"sondeo, vuelta de {vuelta / F_CPU * 1e6:>7.0f} µs", latencia_sondeo(vuelta))
resumen("interrupción", latencia_interrupcion())

print()
# El costo de cada interrupción: 26 ciclos fijos + 4 por registro guardado + el cuerpo.
for cuerpo, registros in ((3, 1), (10, 2), (40, 6)):
    total = 26 + 4 * registros + cuerpo
    for tasa in (1_000, 20_000, 100_000):
        uso = total * tasa / F_CPU * 100
        print(f"cuerpo {cuerpo:>2} ciclos, {registros} regs -> {total:>3} ciclos por evento, {tasa:>7} eventos/s -> {uso:6.2f} % de la CPU")

Resultado: con sondeo, la latencia promedio es la mitad de una vuelta y el peor caso, una vuelta entera (de 10 µs a 10 ms según la duración); con interrupción, 0,59 µs en promedio y 0,69 µs en el peor caso, sin importar qué haga el programa principal. A cambio, cada evento cuesta entre 33 y 90 ciclos. Probá agregar un bloque de sondeo de 1 600 000 ciclos (100 ms) o una ISR con cuerpo de 200 ciclos.

El modelo ignora que otras interrupciones o secciones críticas pueden demorar la entrada. La latencia de una ISR solo es constante si nada más compite; en un sistema real, la peor latencia depende de la ISR más larga.

10 · Exploración interactiva

Vectores y costo

La primera herramienta es un explorador de la tabla de vectores: elegí un evento y obtendrás su número, su dirección, su prioridad, su bandera, la forma de borrarla y el esqueleto de código para habilitarlo. La segunda calcula el costo y la latencia de una ISR a partir de su cuerpo, los registros que usa y la tasa de eventos.

Banco de interrupciones

1. Explorador de vectores

Posición y prioridad—
Dirección del vector—
Bandera del evento—
Cómo se borra la bandera—

2. Costo y latencia de una ISR

Costo por evento—
Uso de la CPU—
Latencia máxima—

El modelo de costo usa 26 ciclos fijos (entrada, salto, RETI y el guardado de R0, R1 y SREG) más 4 por registro adicional; es una estimación a partir de la tabla de ciclos de instrucciones, no la salida de un compilador concreto. La latencia máxima suma la sección bloqueada más larga, la instrucción más larga (4 ciclos) y la entrada (7 ciclos).

Recorrido guiado

  1. Elegí TIMER0_OVF_vect: vector 17, palabra 0x0020 (byte 0x0040), bandera TOV0. El código de habilitación coincide con la tabla del tema 11.
  2. Elegí USART_RX_vect: la bandera se borra leyendo UDR0. Si la ISR no lo hace, la bandera sigue en 1 y la rutina se repite sin fin.
  3. Elegí INT0_vect y ADC_vect: el primero tiene el vector 2, el segundo el 22. Si ambos ocurren a la vez, INT0 se atiende primero.
  4. En la herramienta 2, con 10 ciclos de cuerpo, 2 registros y 20 000 eventos por segundo: 44 ciclos (2,75 µs) y 5,5 % de la CPU.
  5. Subí los eventos a 500 000 por segundo: la ISR necesita más del 100 % de la CPU. Es el límite que el modelo del tema 14 ya anticipaba.
Desafío: la mayor tasa sostenible

Con la ISR de 44 ciclos y sin pasar del 50 % de la CPU, ¿cuántos eventos por segundo soporta? 0,5 × 16 000 000 / 44 ≈ 181 800 eventos por segundo. Probalo en la herramienta 2: el uso de la CPU llega justo al 50 %. Por encima de ese valor, el programa principal tiene menos de la mitad del procesador.

11 · Método de trabajo

Cómo diseñar con interrupciones

  1. Decidí si hace falta. Una interrupción se justifica cuando el evento es rápido, impredecible o el programa tiene que hacer otras cosas. Un evento lento y predecible se resuelve con sondeo.
  2. Ubicá el vector y los tres permisos: la bandera, la máscara y el bit global.
  3. Escribí la ISR mínima: guardar el dato o marcar una bandera, y salir.
  4. Declará volatile lo compartido y protegé los accesos de más de 8 bits.
  5. Calculá el presupuesto: costo por evento × tasa, y la latencia máxima, con la herramienta del laboratorio.
  6. Definí BADISR_vect mientras desarrollás, para detectar interrupciones sin rutina.
  7. Medí con una marca en un pin (tema 14): el pulso en la ISR mide su duración, y un segundo canal con el evento, la latencia.

Cuando algo falla, las causas típicas son: el evento ocurre pero la máscara o sei() faltan; la ISR no borra la bandera y se repite; una variable compartida sin volatile; una ISR demasiado larga que pierde otros eventos; o una interrupción habilitada sin ISR, que reinicia el chip.

12 · Comprensión

Confusiones frecuentes

«Con sei() alcanza para que funcione una interrupción»

También hace falta la máscara del evento (por ejemplo, TOIE0 en TIMSK0) y que el evento ocurra. Son tres permisos: bandera, máscara y habilitación global.

«Una interrupción de alta prioridad puede interrumpir a otra en curso»

No en el AVR por defecto: al entrar a una rutina, el hardware deshabilita las interrupciones. La prioridad solo decide el orden cuando hay varias pendientes. El anidamiento exige un sei() explícito dentro de la ISR.

«La interrupción es instantánea»

Tiene una latencia mínima de unos 8 a 11 ciclos y puede ser mayor si hay otra ISR o una sección crítica. Para tiempos estrictos hay que calcular el peor caso.

«Poner el trabajo en la ISR lo hace más rápido»

Una ISR larga bloquea todas las demás interrupciones y el programa principal. Es mejor hacer lo mínimo en la ISR y el resto en el flujo principal.

«Si dos eventos ocurren durante una ISR, se atienden los dos»

Cada fuente guarda una sola bandera: si el mismo evento ocurre dos veces mientras espera, se atiende una sola vez. La cuenta se pierde.

13 · Práctica guiada

Predecí antes de comprobar

1. Dirección de un vector

¿En qué palabra y en qué byte de la Flash está el vector de USART_RX_vect (vector 19)?

Ver solución

(19 − 1) × 2 = 36 = 0x0024 palabras, es decir, el byte 72 = 0x0048.

2. Costo de una ISR

Una ISR tiene un cuerpo de 10 ciclos y usa 2 registros. ¿Cuántos ciclos cuesta cada evento y qué parte de la CPU ocupa con 20 000 eventos por segundo a 16 MHz?

Ver solución

26 + 4 × 2 + 10 = 44 ciclos (2,75 µs). 44 × 20 000 = 880 000 ciclos por segundo = 5,5 % de 16 MHz.

3. Una rutina que se repite

Una ISR de USART_RX_vect no lee UDR0. ¿Qué ocurre?

Ver solución razonada

La bandera RXC0 solo se borra leyendo UDR0. Si la ISR no lo hace, al ejecutar RETI la bandera sigue en 1 y la interrupción se vuelve a disparar de inmediato: la rutina se repite sin parar y el programa principal casi no avanza.

4. Latencia con una sección crítica

Otra ISR dura 200 ciclos, la instrucción más larga es de 4 y la entrada, 7. ¿Cuál es la latencia en el peor caso a 16 MHz?

Ver solución

200 + 4 + 7 = 211 ciclos = 13,2 µs.

5. Interrupción sin rutina

Habilitás TIMER1_OVF pero te olvidás de escribir su ISR. El LED del programa se enciende y se apaga cada pocos milisegundos, aunque el programa no debería parpadearlo. ¿Qué ocurre?

Ver solución razonada

Sin rutina, se usa BADISR_vect, que salta a la dirección 0. Cada desbordamiento del temporizador 1 (cada 4,096 ms sin prescaler a 16 MHz) reinicia el programa. Al volver a arrancar, el LED se inicializa de nuevo y el ciclo se repite. Se corrige definiendo la ISR o deshabilitando la interrupción.

14 · Síntesis

Ideas para recordar

  • Una interrupción hace que el hardware avise: la CPU termina la instrucción, guarda el PC, salta al vector, ejecuta la ISR y vuelve con RETI.
  • El ATmega328P tiene 26 vectores de dos palabras cada uno; el orden de la tabla es el orden de prioridad.
  • Para que una interrupción actúe hacen falta la bandera, la máscara individual y el bit global I (sei()).
  • Una ISR se escribe con ISR(NOMBRE_vect); debe ser corta, sin esperas y con las variables compartidas volatile.
  • La latencia mínima es de unos 8 a 11 ciclos; el costo por evento es de decenas de ciclos, dominado por el prólogo y el epílogo.
  • Durante una ISR las demás interrupciones esperan; los eventos repetidos de una misma fuente se funden en uno.
  • Una interrupción habilitada sin ISR reinicia el programa; los accesos de más de 8 bits entre ISR y programa principal no son atómicos.

¿Listo para el tema 20?

Antes de avanzar, ubicá un vector en la tabla, nombrá las tres condiciones que debe cumplir una interrupción para atenderse y calculá el porcentaje de CPU que consume una ISR dada. Si podés justificar las tres respuestas, entendés el mecanismo.

En el próximo tema aplicamos la teoría a las entradas: interrupciones externas y de cambio de pin, cómo reaccionar a un flanco o a un nivel sin consultar el pin.