Las interrupciones
Un programa que pregunta todo el tiempo «¿pasó algo?» desperdicia el procesador y llega tarde. Una interrupción invierte la relación: el hardware avisa, el programa deja lo que estaba haciendo, atiende y vuelve exactamente donde estaba.
01Qué ocurre exactamente
| Paso | Qué pasa |
|---|---|
| 1 | El periférico detecta el evento y levanta su bandera de pedido. |
| 2 | Si esa interrupción está habilitada y las interrupciones globales también, el núcleo termina la instrucción en curso. |
| 3 | Guarda en la pila la dirección de retorno y el estado del procesador: es el contexto. |
| 4 | Busca en la tabla de vectores la dirección de la rutina de atención y salta ahí. |
| 5 | Se ejecuta la rutina. En muchas familias hay que borrar la bandera a mano. |
| 6 | La instrucción de retorno restaura el contexto y el programa sigue como si nada. |
Es el tiempo entre el evento y la primera instrucción de la rutina. Se compone de lo que falta para terminar la instrucción en curso, el guardado del contexto y el salto: típicamente entre 4 y 20 ciclos. A 16 MHz eso es del orden de un microsegundo.
Pero la latencia real puede ser muchísimo mayor si en ese momento se está ejecutando otra rutina de atención o una sección crítica con interrupciones deshabilitadas. Ese es el motivo por el cual las rutinas deben ser cortas.
02De dónde vienen
| Fuente | Para qué se usa |
|---|---|
| Externa por pin | Un pulsador, un final de carrera, un sensor. Configurable por flanco de subida, de bajada, ambos o por nivel. |
| Cambio de estado en un puerto | Un solo vector para todo un grupo de pines: la rutina tiene que averiguar cuál cambió. |
| Temporizador | Desbordamiento o comparación: es la base de toda tarea periódica y de las bases de tiempo del sistema. |
| Comunicación serie | Byte recibido o registro de transmisión vacío. Evita perder datos sin tener que estar mirando. |
| Conversor A/D | Conversión terminada: el programa no espera, lo avisan. |
| Captura de entrada | Guarda el valor exacto del temporizador cuando ocurre un flanco. Es la forma precisa de medir períodos y anchos de pulso. |
| Perro guardián | Si el programa se cuelga y no lo refresca, reinicia el equipo. |
03Prioridades y anidamiento
- En los microcontroladores de 8 bits sencillos, la prioridad la da la posición en la tabla de vectores: no es configurable, y no hay anidamiento salvo que se habilite a mano.
- En los ARM Cortex-M, el controlador de interrupciones permite asignar una prioridad numérica a cada fuente, y el anidamiento es automático: menor número, mayor prioridad.
- Anidar da mejor respuesta a lo urgente, pero consume más pila y hace mucho más difícil razonar sobre el programa.
- Máxima: seguridad. Parada de emergencia, sobrecorriente, falla de alimentación.
- Alta: lo que se pierde si llega tarde. Captura de flancos, recepción serie a alta velocidad.
- Media: bases de tiempo y control periódico.
- Baja: interfaz de usuario, registro, comunicación no crítica.
04El problema de las variables compartidas
Una interrupción puede ocurrir entre dos instrucciones cualesquiera del programa principal. Todo lo que ambos toquen es una fuente potencial de errores que aparecen una vez cada mil, y sólo con el equipo en producción.
/* MAL: el compilador puede guardar 'listo' en un registro y no releerla nunca */ uint8_t listo = 0; ISR(INT0_vect) { listo = 1; } while (!listo) { } /* puede quedarse colgado para siempre */ /* BIEN: volatile obliga a leerla de memoria en cada vuelta */ volatile uint8_t listo = 0; /* MAL: 'pulsos' es de 32 bits; en un micro de 8 bits se lee en cuatro pasos */ volatile uint32_t pulsos; total = pulsos; /* si la interrupcion cae en el medio, sale un valor imposible */ /* BIEN: seccion critica, lo mas corta posible */ cli(); /* deshabilita interrupciones */ total = pulsos; sei(); /* las vuelve a habilitar */
- Toda variable compartida entre una rutina de interrupción y el programa principal se declara volatile.
- Todo acceso a una variable de más bits que el ancho del procesador se hace dentro de una sección crítica, lo más corta posible.
Y una tercera, más importante que las dos: la rutina de atención hace lo mínimo. Levanta una bandera, guarda un dato, y el trabajo pesado lo hace el programa principal.
- Esperas y demoras. Bloquean todo el sistema.
- Escribir por puerto serie o en una pantalla. Son operaciones lentas.
- Cálculos con coma flotante en un microcontrolador sin unidad de punto flotante.
- Reservar memoria dinámica.
- Esperar otra interrupción: si están deshabilitadas, no va a llegar nunca.
05El patrón que conviene usar siempre
/* Antirrebote de un pulsador por interrupcion + temporizador */ volatile uint8_t evento = 0; volatile uint16_t t_ultimo = 0; ISR(INT0_vect) /* flanco de bajada en el pulsador */ { uint16_t ahora = ms_actual(); if ((uint16_t)(ahora - t_ultimo) > 30) { /* 30 ms de rebote */ evento = 1; t_ultimo = ahora; } } /* la rutina termina en microsegundos */ int main(void) { inicializar(); while (1) { if (evento) { cli(); evento = 0; sei(); /* se consume el evento sin perder otro */ atender_pulsador(); /* aca si se puede tardar */ } otras_tareas(); } }
La rutina es corta y determinista, el antirrebote no consume tiempo de espera, y el trabajo pesado ocurre en el bucle principal, donde puede tardar sin afectar a nadie. El detalle de la resta con conversión explícita hace que el conteo siga siendo correcto cuando el contador de milisegundos da la vuelta, que es el error que aparece a los 65 segundos de funcionamiento y desconcierta a todo el mundo.
06En el laboratorio
Detectar un pulsador de dos maneras: preguntando en el bucle principal y por interrupción, con el programa haciendo además una tarea larga. Medir con el osciloscopio el tiempo entre el flanco y la respuesta en cada caso, y comparar el peor caso de los dos.
Poner un pin en alto como primera instrucción de la rutina de atención y observar en el osciloscopio la distancia hasta el flanco que la disparó. Repetir con otra interrupción activa y con una sección crítica larga en el programa principal: se ve crecer la latencia.
Escribir el bucle de espera sin volatile y compilar con optimización activada. Verificar que se cuelga. Mirar el listado en ensamblador para ver que la variable ni siquiera se relee. Agregar volatile y comprobar la diferencia. Es la mejor manera de que no se olvide más.
Usar la interrupción de captura de entrada para medir el período de una señal, y comparar la precisión con la de medirlo por sondeo. Con captura, el valor del temporizador se toma por hardware en el instante del flanco: la diferencia de exactitud es notable.
07Errores frecuentes
| Síntoma | Causa habitual |
|---|---|
| La interrupción se dispara sin parar | No se borra la bandera de pedido dentro de la rutina. |
| La interrupción no se dispara nunca | Falta habilitarla individualmente, o falta habilitar las globales, o el vector está mal escrito. |
| El programa se cuelga en un bucle de espera | La variable compartida no está declarada volatile. |
| Un contador de varios bytes da valores absurdos | Se lee sin sección crítica y la interrupción cae en el medio de la lectura. |
| Un pulsador genera varios eventos | Rebote mecánico. Se filtra por tiempo, no agregando esperas dentro de la rutina. |
| El sistema se vuelve errático al agregar interrupciones | Desbordamiento de pila por anidamiento, o rutinas demasiado largas. |
| Falla exactamente a los 65 segundos | Desbordamiento de un contador de 16 bits en milisegundos, mal comparado. |
| Todo se detiene por momentos | Hay una espera, una escritura serie o un cálculo pesado dentro de una rutina de atención. |
08Autoevaluación
¿Qué guarda el procesador al atender una interrupción?
La dirección de retorno y el estado del procesador —el contexto— en la pila, para poder continuar exactamente donde estaba al terminar la rutina.
¿Qué es la tabla de vectores?
Una tabla en memoria donde cada fuente de interrupción tiene la dirección de su rutina de atención. El núcleo la consulta para saber adónde saltar.
¿Qué es la latencia y de qué depende?
El tiempo entre el evento y la primera instrucción de la rutina. Depende de la instrucción en curso, del guardado de contexto y, sobre todo, de si hay otra rutina ejecutándose o una sección crítica abierta.
¿Por qué hace falta declarar volatile una variable compartida?
Porque sin esa palabra el compilador puede guardarla en un registro y no volver a leerla de memoria. Como la interrupción la modifica «por afuera», el programa principal nunca se entera del cambio.
¿Qué es una sección crítica y cuándo hace falta?
Un tramo con las interrupciones deshabilitadas. Hace falta al leer o escribir variables de más bits que el ancho del procesador, para que la interrupción no caiga en el medio de la operación.
Nombrá tres cosas que no deben ir dentro de una rutina de atención.
Esperas o demoras, escrituras lentas como el puerto serie o una pantalla, y cálculos pesados o reserva de memoria dinámica.
¿Cómo se resuelve el rebote de un pulsador con interrupciones?
Comparando el instante actual con el del último evento válido y descartando los que llegan antes de unos 20 a 50 ms. Nunca con una espera dentro de la rutina.
¿Qué ventaja tiene la interrupción de captura frente a medir por sondeo?
El valor del temporizador se toma por hardware en el instante exacto del flanco, así que la medición no depende de cuándo llegue el programa a leerlo.
¿Cómo se asignan las prioridades en un sistema real?
Máxima para la seguridad —paradas de emergencia, sobrecorriente—, alta para lo que se pierde si llega tarde, media para las bases de tiempo y baja para la interfaz de usuario y el registro.
¿Por qué las rutinas de atención deben ser cortas?
Porque mientras una se ejecuta, las demás esperan: cada rutina larga aumenta la latencia de todas las otras y puede hacer que se pierdan eventos.