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
- El periférico marca una bandera de evento (por ejemplo,
TOV0enTIFR0) y, si la interrupción está habilitada, pide atención a la CPU. - La CPU termina la instrucción en curso; no se interrumpe una instrucción a mitad.
- Guarda en la pila la dirección de retorno (el PC, 2 bytes), deshabilita las interrupciones (pone en 0 el bit
IdelSREG) y carga el PC con la dirección del vector de ese evento. - 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.
- La instrucción
RETIsaca 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.
| N.º | Dirección | Fuente | Nombre en avr-libc |
|---|---|---|---|
| 1 | 0x0000 | Reset (encendido, pin RESET, caída de tensión, vigilante) | No es una ISR: arranca el programa |
| 2 | 0x0002 | Interrupción externa 0 (pin PD2) | INT0_vect |
| 3 | 0x0004 | Interrupción externa 1 (pin PD3) | INT1_vect |
| 4 | 0x0006 | Cambio de pin, grupo 0 (PB0 a PB7) | PCINT0_vect |
| 5 | 0x0008 | Cambio de pin, grupo 1 (PC0 a PC6) | PCINT1_vect |
| 6 | 0x000A | Cambio de pin, grupo 2 (PD0 a PD7) | PCINT2_vect |
| 7 | 0x000C | Vigilante (modo interrupción) | WDT_vect |
| 8 | 0x000E | Temporizador 2: coincidencia con OCR2A | TIMER2_COMPA_vect |
| 9 | 0x0010 | Temporizador 2: coincidencia con OCR2B | TIMER2_COMPB_vect |
| 10 | 0x0012 | Temporizador 2: desbordamiento | TIMER2_OVF_vect |
| 11 | 0x0014 | Temporizador 1: captura de entrada | TIMER1_CAPT_vect |
| 12 | 0x0016 | Temporizador 1: coincidencia con OCR1A | TIMER1_COMPA_vect |
| 13 | 0x0018 | Temporizador 1: coincidencia con OCR1B | TIMER1_COMPB_vect |
| 14 | 0x001A | Temporizador 1: desbordamiento | TIMER1_OVF_vect |
| 15 | 0x001C | Temporizador 0: coincidencia con OCR0A | TIMER0_COMPA_vect |
| 16 | 0x001E | Temporizador 0: coincidencia con OCR0B | TIMER0_COMPB_vect |
| 17 | 0x0020 | Temporizador 0: desbordamiento | TIMER0_OVF_vect |
| 18 | 0x0022 | SPI: transferencia completa | SPI_STC_vect |
| 19 | 0x0024 | UART: byte recibido | USART_RX_vect |
| 20 | 0x0026 | UART: registro de datos vacío | USART_UDRE_vect |
| 21 | 0x0028 | UART: transmisión completa | USART_TX_vect |
| 22 | 0x002A | Conversor analógico-digital: conversión terminada | ADC_vect |
| 23 | 0x002C | EEPROM lista | EE_READY_vect |
| 24 | 0x002E | Comparador analógico | ANALOG_COMP_vect |
| 25 | 0x0030 | I2C (TWI): evento del bus | TWI_vect |
| 26 | 0x0032 | Memoria 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:
| Nivel | Qué es | Dónde |
|---|---|---|
| 1. El evento | La bandera del periférico vale 1 | Registro de estado del periférico (TIFR0, UCSR0A…) |
| 2. La habilitación individual | El bit de máscara de ese evento vale 1 | Registro de control (TIMSK0, EIMSK…) |
| 3. La habilitación global | El bit I del SREG vale 1 | Se 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:
- Configurar el periférico (por ejemplo, el modo y el prescaler del temporizador).
- Borrar la bandera por si quedó un evento viejo pendiente (recordá que se borra escribiendo un 1).
- Habilitar la máscara del evento.
- 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
UDR0en 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.
| ISR | Ciclos por evento | 1 kHz | 20 kHz | 100 kHz |
|---|---|---|---|---|
| Contador (cuerpo de 3 ciclos, 1 registro) | 33 | 0,21 % | 4,1 % | 20,6 % |
| Cuerpo de 10 ciclos, 2 registros | 44 | 0,27 % | 5,5 % | 27,5 % |
| Cuerpo de 40 ciclos, 6 registros | 90 | 0,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,
milisegundoses 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
2. Costo y latencia de una ISR
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
- Elegí
TIMER0_OVF_vect: vector 17, palabra0x0020(byte0x0040), banderaTOV0. El código de habilitación coincide con la tabla del tema 11. - Elegí
USART_RX_vect: la bandera se borra leyendoUDR0. Si la ISR no lo hace, la bandera sigue en 1 y la rutina se repite sin fin. - Elegí
INT0_vectyADC_vect: el primero tiene el vector 2, el segundo el 22. Si ambos ocurren a la vez,INT0se atiende primero. - 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.
- 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
- 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.
- Ubicá el vector y los tres permisos: la bandera, la máscara y el bit global.
- Escribí la ISR mínima: guardar el dato o marcar una bandera, y salir.
- Declará
volatilelo compartido y protegé los accesos de más de 8 bits. - Calculá el presupuesto: costo por evento × tasa, y la latencia máxima, con la herramienta del laboratorio.
- Definí
BADISR_vectmientras desarrollás, para detectar interrupciones sin rutina. - 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 compartidasvolatile. - 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.