01 · Punto de partida
De consultar a ser llamado
En el tema 19 viste el mecanismo general de las interrupciones. Aquí aplicamos la idea a la fuente más directa: los pines de entrada. Con el sondeo del tema 17, el programa tenía que leer el pin varias veces por milisegundo para no perder un cambio breve; con una interrupción externa, el hardware detecta el cambio por su cuenta y la CPU solo interviene cuando ocurre.
El ATmega328P tiene dos mecanismos distintos, con compromisos opuestos. INT0 e INT1 son dos pines especiales con detección configurable (nivel o flanco). Las interrupciones de cambio de pin (pin change, PCINT) cubren casi todos los pines, pero con menos opciones. Elegir entre ambas es el tema central.
Retomamos las banderas que se borran con un 1 (tema 9), las tres condiciones de una interrupción (tema 19) y el rebote (tema 17).
02 · Dos pines con detección completa
INT0 e INT1: cuatro modos
INT0 está en el pin PD2 (D2 en la placa Uno) e INT1 en PD3 (D3). Cada una puede dispararse de cuatro maneras, que se eligen con dos bits ISCn1:0 (interrupt sense control) del registro EICRA:
| ISCn1:0 | Se dispara con… | Uso típico |
|---|---|---|
00 | Nivel bajo del pin | Despertar del sueño; entradas de «alerta» que el periférico mantiene en bajo hasta ser atendido |
01 | Cualquier cambio de nivel | Medir cuánto dura un pulso (con un temporizador) |
10 | Flanco descendente | Pulsador con pull-up; señales activas en bajo |
11 | Flanco ascendente | Contar pulsos; sensores que generan un pulso positivo |
Los registros que intervienen son tres:
EICRA: el modo de cada una (los bitsISC).EIMSK: la habilitación individual (bitsINT0eINT1).EIFR: las banderasINTF0eINTF1, que se borran escribiendo un 1.
Hay una diferencia importante entre nivel y flanco. Un flanco es un evento: la bandera INTFn lo registra aunque la CPU esté ocupada o las interrupciones estén deshabilitadas, y se atiende después (uno solo, por más que se hayan repetido). Un nivel bajo es un estado: no se guarda en una bandera, la interrupción se mantiene pedida mientras el pin siga en 0. Si la rutina termina y el pin sigue en bajo, vuelve a dispararse sin parar. Con el nivel hay que apagar la interrupción dentro de la rutina o resolver la causa que mantiene el pin en bajo.
Un detalle de hardware: la detección de nivel bajo es asíncrona (no necesita el reloj de la CPU). Por eso es una de las pocas fuentes que pueden despertar al microcontrolador de los modos de sueño profundo (bloque de energía, tema 40).
03 · El código
Configurar INT0 e INT1
Un pulsador en PD2 que cuenta pulsaciones con una interrupción por flanco descendente:
#include <avr/io.h>
#include <avr/interrupt.h>
volatile uint8_t pulsaciones = 0;
ISR(INT0_vect) {
pulsaciones++; // lo mínimo: contar
}
int main(void) {
DDRD &= ~(1 << PD2); // PD2 como entrada
PORTD |= (1 << PD2); // con pull-up
EICRA |= (1 << ISC01); // ISC01:0 = 10 -> flanco descendente
EIFR = (1 << INTF0); // borrar un evento viejo (asignación: se escribe un 1)
EIMSK |= (1 << INT0); // habilitar INT0
sei(); // habilitación global
while (1) {
/* …el programa principal hace otras cosas… */
}
}
Observá los tres pasos del tema 19 (configurar el modo, borrar la bandera, habilitar la máscara) y el uso de la asignación directa en EIFR, para no borrar la bandera de INT1 por accidente. Otro detalle: cambiar los bits ISC con la interrupción habilitada puede generar un disparo espurio; por eso se borra la bandera después de cambiar el modo y antes de habilitar.
La rutina corre apenas ocurre el flanco, sin importar lo que haga el programa principal. Si el pulsador no tiene ningún filtro, cada rebote disparará la rutina (sección 6).
04 · Casi cualquier pin
Interrupciones de cambio de pin
INT0 e INT1 son solo dos pines. Cuando hacen falta más entradas con interrupción, el AVR ofrece las interrupciones de cambio de pin: casi cada pin del chip puede generar una, y las 23 fuentes se agrupan en tres.
| Grupo | Pines | Nombres | Máscara de pines | Habilitación del grupo | Vector |
|---|---|---|---|---|---|
| 0 | PB0 a PB7 | PCINT0 a PCINT7 | PCMSK0 | PCIE0 en PCICR | PCINT0_vect |
| 1 | PC0 a PC6 | PCINT8 a PCINT14 | PCMSK1 | PCIE1 en PCICR | PCINT1_vect |
| 2 | PD0 a PD7 | PCINT16 a PCINT23 | PCMSK2 | PCIE2 en PCICR | PCINT2_vect |
La máscara del grupo (PCMSKx) elige qué pines participan; cualquiera que cambie, en cualquier sentido, activa el vector de su grupo. Tres diferencias con INT0/INT1:
- No se puede elegir el flanco: se dispara con cualquier cambio. El flanco hay que averiguarlo en la rutina.
- Un solo vector por grupo: si cambian dos pines del mismo grupo, la rutina es la misma y debe determinar cuál fue.
- Es sincrónica: necesita el reloj de E/S para detectar el cambio; una variación más breve que un ciclo puede perderse. Y se dispara aunque el pin esté configurado como salida: si el programa lo cambia, también interrumpe.
// Tres pulsadores en PB0, PB1 y PB2, con pull-up
DDRB &= ~0x07;
PORTB |= 0x07;
PCMSK0 |= (1 << PCINT0) | (1 << PCINT1) | (1 << PCINT2); // qué pines del grupo
PCIFR = (1 << PCIF0); // borrar un evento viejo
PCICR |= (1 << PCIE0); // habilitar el grupo
sei();
05 · Un vector, varios pines
¿Qué pin cambió?
Como el vector es único para todo el grupo, la rutina debe averiguar el pin responsable. La técnica es la de detección de flancos del tema 17, aplicada al puerto entero: guardar la lectura anterior y comparar con la actual.
volatile uint8_t previo = 0xFF; // con pull-up: 1 = suelto
ISR(PCINT0_vect) {
uint8_t actual = PINB;
uint8_t cambios = actual ^ previo; // bits que cambiaron
uint8_t bajaron = cambios & ~actual; // ahora valen 0: se presionaron
uint8_t subieron = cambios & actual; // ahora valen 1: se soltaron
previo = actual;
if (bajaron & (1 << PB0)) { /* el botón de PB0 acaba de apretarse */ }
if (bajaron & (1 << PB1)) { /* el de PB1 */ }
}
Dos advertencias. Si dos pines cambian casi a la vez, solo una interrupción queda pendiente: la rutina lee PINB una vez y debe procesar todos los bits que cambiaron, no solo el primero. Y si el pin cambia dos veces antes de que la rutina lea (un pulso muy corto), la lectura ve el mismo valor que antes y el evento parece no haber ocurrido; los pulsos más cortos que la latencia se pierden.
06 · El mundo real otra vez
Rebote, ruido y tiempo muerto
Cada rebote de un pulsador (tema 17) es un flanco y dispara una interrupción. Con una interrupción por flanco descendente, un toque se cuenta como 3, 7 o 15 pulsaciones. El filtro por muestreo del tema 17 sigue valiendo, pero hay una técnica específica y muy usada con interrupciones: el tiempo muerto (dead time).
La idea: aceptar un evento y ignorar los siguientes durante unos milisegundos. Se necesita una base de tiempo; la más simple es un contador de milisegundos incrementado por un temporizador (lo armaremos en el tema 24):
volatile uint16_t ahora_ms; // la incrementa la interrupción del temporizador
volatile uint8_t cuenta;
ISR(INT0_vect) {
static uint16_t ultimo = 0;
if ((uint16_t)(ahora_ms - ultimo) >= 20) { // pasaron al menos 20 ms desde el último
cuenta++;
ultimo = ahora_ms;
} // si no, es un rebote: se ignora
}
La resta sin signo de 16 bits funciona aunque el contador desborde. El tiempo muerto elegido debe ser mayor que el rebote (por ejemplo, 20 ms) pero menor que el intervalo mínimo entre pulsaciones reales. Es sencillo, pero tiene un costo: la rutina se ejecuta de todas formas con cada rebote (solo que sale en seguida), así que el rebote sigue consumiendo CPU. La alternativa es deshabilitar la interrupción en la primera detección y rehabilitarla desde el temporizador pasados los 20 ms.
También las perturbaciones eléctricas (relés cercanos, cables largos) producen pulsos espurios que disparan interrupciones. Un buen diseño añade un pull-up firme, un filtro RC o un cable corto, además del tiempo muerto.
07 · Hasta dónde llega
Límites de frecuencia
Cada evento cuesta decenas de ciclos (tema 19), así que hay una tasa máxima de pulsos que se pueden atender. Con una rutina de 44 ciclos a 16 MHz, el 50 % de la CPU se consume con unos 180 000 eventos por segundo; para un cuerpo mínimo, el límite teórico ronda los 400 000. En la práctica, conviene mantener la tasa muy por debajo.
| Frecuencia de los pulsos | Mecanismo recomendable |
|---|---|
| Hasta pocos kHz | Interrupción externa o de cambio de pin |
| De varios kHz a unas decenas de kHz | Interrupción con rutina mínima, con atención al presupuesto de CPU |
| Cientos de kHz o más | Un temporizador configurado para contar pulsos externos (pin T0 o T1), sin interrupciones: llega hasta la mitad de la frecuencia del reloj |
Cuando el interés es el ancho o el período de un pulso, y no solo contarlos, la mejor herramienta es la captura de entrada del temporizador 1 (tema 24): el hardware guarda el valor del contador en el instante exacto del flanco, sin la incertidumbre de la latencia de una interrupción.
08 · Una aplicación completa
Un caso: el encoder en cuadratura
Un encoder rotatorio es una perilla que gira sin fin y entrega dos señales, A y B, desfasadas un cuarto de ciclo. El sentido del giro está en cuál de las dos cambia primero: es el código Gray de dos bits, que va 00 → 01 → 11 → 10 → 00 en un sentido y al revés en el otro.
Se decodifica con una interrupción de cambio de pin sobre los dos canales y una tabla de 16 entradas. Cada vez que cambia el estado, el índice (anterior << 2) | actual da el incremento: +1, −1 o 0 (si no cambió o la transición es imposible):
static const int8_t TABLA[16] = { 0, +1, -1, 0,
-1, 0, 0, +1,
+1, 0, 0, -1,
0, -1, +1, 0 };
volatile int16_t posicion = 0;
static uint8_t previo = 0;
// Configuración: PCMSK2 = (1 << PCINT18) | (1 << PCINT19); PCICR |= (1 << PCIE2); sei();
ISR(PCINT2_vect) { // A en PD2 y B en PD3, con pull-up
uint8_t actual = (PIND >> 2) & 0b11; // 2 bits: A (bit 1) y B (bit 0)
posicion += TABLA[(previo << 2) | actual];
previo = actual;
}
La tabla es robusta al rebote: si A rebota (01 → 11 → 01), el primer cambio suma 1 y el segundo resta 1, y el resultado neto es cero. Contar flancos de A sin mirar el sentido, en cambio, sumaría cada rebote. Una transición imposible (00 → 11, por ejemplo, donde los dos canales cambian a la vez) vale 0: significa que se perdió una lectura. El simulador del laboratorio permite probarlo.
09 · Analogía con programación
Contar bien un encoder y un pulsador
Este programa decodifica un encoder con la tabla, con y sin rebote en el canal A, y compara contra el recuento de flancos de A. Después simula tres toques de pulsador, con rebote, y cuenta cuántas pulsaciones registra una interrupción con distintos tiempos muertos.
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(5)
# --- 1) Encoder en cuadratura: tabla de transiciones (estado anterior, estado actual) ---
TABLA = [0, +1, -1, 0,
-1, 0, 0, +1,
+1, 0, 0, -1,
0, -1, +1, 0]
CICLO = [0b00, 0b01, 0b11, 0b10] # secuencia de los canales A y B al girar en un sentido
def decodificar(estados):
posicion, previo = 0, estados[0]
for actual in estados[1:]:
posicion += TABLA[(previo << 2) | actual]
previo = actual
return posicion
adelante = [CICLO[i % 4] for i in range(9)] # 8 pasos hacia adelante
atras = [CICLO[(8 - i) % 4] for i in range(1, 5)] # 4 pasos hacia atrás
limpia = adelante + atras
con_rebote = []
for s in limpia: # cada tanto, el canal A rebota
con_rebote.append(s)
if random.random() < 0.4:
con_rebote += [s ^ 0b10, s] # A cambia y vuelve
flancos_de_a = sum(1 for a, b in zip(con_rebote, con_rebote[1:]) if (a ^ b) & 0b10)
print(f"Giro real: +8 y −4 = {decodificar(limpia):+d}")
print(f"Con rebote en A: tabla -> {decodificar(con_rebote):+d} contando flancos de A -> {flancos_de_a}")
# --- 2) Pulsador por INT0: contar flancos descendentes con y sin tiempo muerto ---
def flancos_de_un_toque(t0_ms):
"""Cada rebote produce un flanco descendente, separados por 0,3 a 0,9 ms."""
t, tiempos = t0_ms, []
for _ in range(random.randint(3, 7)):
tiempos.append(t)
t += random.uniform(0.3, 0.9)
return tiempos
eventos = flancos_de_un_toque(100) + flancos_de_un_toque(400) + flancos_de_un_toque(900)
def contar(eventos, tiempo_muerto_ms):
ultimo, cuenta = None, 0
for t in eventos:
if ultimo is None or t - ultimo >= tiempo_muerto_ms:
cuenta += 1
ultimo = t
return cuenta
print()
print(f"Tres toques reales, {len(eventos)} flancos descendentes en el pin:")
for muerto in (0, 5, 20, 100):
print(f" tiempo muerto de {muerto:>3} ms -> el programa cuenta {contar(eventos, muerto)} pulsaciones")
Resultado: el giro real (8 pasos hacia adelante y 4 hacia atrás) da +4 con la tabla, incluso con el rebote en A, mientras que contar flancos de A da un valor sin sentido (10). Y los tres toques con 17 flancos de rebote se cuentan como 17 pulsaciones sin tiempo muerto y como 3 con 5 ms o más. Probá un tiempo muerto de 1 ms o cambiá la semilla del generador aleatorio.
El modelo asume que se atiende cada flanco. En un chip real, una rutina lenta o con interrupciones bloqueadas puede perder transiciones del encoder, y la tabla marcaría saltos imposibles que se descartan.
10 · Exploración interactiva
Configurador y encoder
La primera herramienta calcula los registros y genera el código para INT0, INT1 o un grupo de cambio de pin. La segunda es un encoder virtual: girá en los dos sentidos, provocá rebotes y transiciones imposibles, y comparé la posición de la tabla con el recuento ingenuo de flancos.
Banco de interrupciones de pin
1. Configurar una interrupción de pin
2. Encoder en cuadratura
El configurador asume que se usan los pines como entradas con pull-up. En el encoder virtual, un «paso» es un cambio de estado (un cuarto de ciclo); una muesca del encoder suele ser de cuatro pasos.
Recorrido guiado
- Con INT0 y flanco descendente, comprobá que
EICRAvale0010yEIMSK,01. Cambiá a INT1 con flanco ascendente:EICRA = 1100yEIMSK = 10. - Elegí «Nivel bajo» y leé la advertencia: la rutina se repite mientras el pin siga en 0.
- Elegí «Cambio de pin, grupo 2» y marcá PD2 y PD4:
PCMSK2 = 00010100(0x14) yPCICR = 100. Las dos fuentes comparten el vectorPCINT2_vect. - En el encoder, girá ocho pasos hacia la derecha y cuatro hacia la izquierda: la posición es 4 y el recuento ingenuo da 6.
- Provocá varios «Rebote en A». La posición no cambia y el recuento ingenuo sigue sumando. Después probá «Transición imposible»: la tabla la ignora.
Desafío: perder una transición
Si la rutina tarda demasiado y se pierde una lectura intermedia, el estado salta de 00 a 11 sin pasar por 01 o 10. La tabla da 0 para esa transición: la posición queda con un error de dos pasos, que no se puede saber en qué sentido fue. En la práctica, un encoder girado a mano es lento (unos cientos de transiciones por segundo) y una rutina de pocas decenas de ciclos no pierde nada; con un motor a alta velocidad, hay que usar contadores por hardware.
11 · Método de trabajo
Cómo elegir el mecanismo
- Definí el evento: ¿flanco o nivel? ¿Un pin o varios? ¿Qué velocidad máxima?
- Un solo pin con flanco bien definido: INT0 o INT1 (D2 y D3 en la Uno), si están libres.
- Más pines o ya ocupados D2 y D3: cambio de pin, con la máscara del grupo; planificá la lógica para averiguar cuál cambió.
- Despertar del sueño profundo: nivel bajo en INT0/INT1 o cambio de pin.
- Filtrá el rebote: tiempo muerto en la ISR, o deshabilitar la interrupción hasta que pase.
- Mantené la rutina mínima y calculá la tasa máxima con el presupuesto de ciclos; si es muy alta, delegá en un contador por hardware.
- Verificá con instrumentos: una marca en un pin dentro de la ISR muestra cuándo y cuántas veces se dispara.
Asegurate también de que la entrada tenga un nivel definido (pull-up) y de que el cable no capte ruido: las interrupciones convierten cualquier pulso espurio en un evento real.
12 · Comprensión
Confusiones frecuentes
«Una interrupción por nivel bajo se atiende una sola vez»
Se repite mientras el pin siga en bajo, porque el nivel no queda guardado en una bandera. Si la ISR no lo resuelve, el programa principal se ahoga. Hay que deshabilitar la interrupción dentro de la rutina.
«El cambio de pin permite elegir el flanco»
No: se dispara con cualquier cambio. La rutina debe comparar la lectura con la anterior para saber si el pin subió o bajó.
«Cada pin tiene su propio vector»
INT0 e INT1 sí, pero los pines de cambio comparten un vector por grupo de hasta 8 pines. La rutina debe distinguir cuál cambió.
«La interrupción evita el rebote»
Cada rebote es un flanco y dispara una interrupción. Conviene filtrarlo con un tiempo muerto o con otra técnica.
«Con interrupciones nunca se pierde un pulso»
Un flanco queda registrado en su bandera, pero varios flancos seguidos se funden en uno si la CPU no llega a atenderlos, y un pulso más corto que el mínimo del hardware puede no registrarse. Con frecuencias altas hay que usar un contador por hardware.
13 · Práctica guiada
Predecí antes de comprobar
1. Registros de INT1
Configurá INT1 para flanco ascendente. ¿Qué valores llevan EICRA y EIMSK?
Ver solución
ISC11:0 = 11 en los bits 3 y 2 de EICRA: 0b00001100 = 0x0C. EIMSK |= (1 << INT1): bit 1, valor 0x02.
2. Cambio de pin en dos pines
Querés interrupciones de cambio en PD2 y PD4. ¿Qué escribís en PCMSK2 y PCICR?
Ver solución
PD2 es PCINT18 y PD4 es PCINT20, es decir, los bits 2 y 4 de PCMSK2: 0x14. El grupo 2 se habilita con PCIE2 (bit 2 de PCICR): 0x04.
3. Posición del encoder
El encoder recorre los estados 00 → 01 → 11 → 10. Con la tabla del tema, ¿cuánto suma la posición?
Ver solución
Tres transiciones: 00→01 (+1), 01→11 (+1), 11→10 (+1). La posición aumenta 3.
4. Tasa máxima de pulsos
Una ISR cuesta 44 ciclos a 16 MHz. ¿Qué tasa de eventos consume el 50 % de la CPU? ¿Qué harías con pulsos de 2 MHz?
Ver solución
0,5 × 16 000 000 / 44 ≈ 181 800 eventos por segundo. Para 2 MHz, ninguna interrupción alcanza: se usa un temporizador contando el pin externo T0 o T1, que admite frecuencias de hasta la mitad del reloj.
5. Un nivel que no se suelta
Una ISR de INT0 en modo nivel bajo atiende a un sensor que mantiene el pin en 0 hasta que se lo lee por I2C, lo que la rutina no hace. ¿Qué ocurre?
Ver solución razonada
Al volver de la rutina, el pin sigue en bajo y la interrupción se dispara de nuevo de inmediato: el programa principal casi no se ejecuta. Se corrige leyendo el sensor dentro de la rutina para que libere el pin, o deshabilitando INT0 en la rutina y rehabilitándola después.
14 · Síntesis
Ideas para recordar
- INT0 (PD2) e INT1 (PD3) se configuran con
EICRA(nivel bajo, cambio, flanco descendente o ascendente),EIMSKyEIFR. - Un flanco queda registrado en una bandera; un nivel bajo se repite mientras dure.
- Las interrupciones de cambio de pin cubren casi todos los pines en tres grupos, con un solo vector por grupo y sin elegir el flanco.
- En una ISR de cambio de pin se compara
PINxcon la lectura anterior para saber qué pin cambió y en qué sentido. - Cada rebote dispara una interrupción: se filtra con un tiempo muerto o deshabilitando la interrupción.
- Un encoder en cuadratura se decodifica con una tabla de 16 entradas, robusta al rebote.
- Para pulsos muy rápidos se usa un contador por hardware; para medir anchos, la captura de entrada.
¿Listo para el tema 21?
Antes de avanzar, configurá INT0 para un flanco ascendente, elegí el mecanismo para tres botones en PB0 a PB2 y explicá por qué el tiempo muerto elimina el rebote. Si podés justificar las tres respuestas, sabés reaccionar a un pin sin consultarlo.
En el próximo tema vemos datos compartidos y secciones críticas: cómo evitar que una interrupción corrompa un dato mientras el programa lo lee.