Interrupciones en Arduino: attachInterrupt, ISR y antirrebote
Cómo usar interrupciones en Arduino: attachInterrupt, pines 2 y 3 del Uno, RISING/FALLING/CHANGE, volatile, antirrebote, encoder y RPM, ESP32 y Pico.
Una interrupción en Arduino hace que el procesador deje lo que está haciendo en el momento en que ocurre en un pin el cambio que esperas (o salta un evento de un temporizador) y ejecute una función especial que tú has escrito. En resumen: en lugar de preguntar sin parar dentro de loop() "¿se ha pulsado el botón?", le dices a Arduino "avísame cuando se pulse". Esto se hace con la función attachInterrupt(); en el Arduino Uno los pines de interrupción son D2 y D3, mientras que en el ESP32 y en la Raspberry Pi Pico casi cualquier GPIO puede generar una interrupción.
Las interrupciones son imprescindibles para contar pulsos muy rápidos sin perder ninguno (encoders, sensores de velocidad, caudalímetros), capturar un evento breve en medio de una tarea larga y despertar una placa del modo de reposo. En esta guía verás paso a paso cómo funciona una interrupción en el hardware, qué pines puedes usar en cada placa, la diferencia entre los modos RISING, FALLING y CHANGE, las reglas para escribir una rutina de servicio de interrupción (ISR), cómo eliminar el rebote de un botón y ejemplos con un botón, un encoder y un contador de RPM. Al final también aprenderás a usar IRAM_ATTR en el ESP32 e irq con MicroPython en la Pico.

¿Qué es una interrupción y cómo funciona?
Piensa en un timbre: sin él tendrías que ir a la puerta cada minuto para comprobar si ha llegado alguien. Eso se llama sondeo (polling), y leer un pin con digitalRead() dentro de loop() es exactamente eso. El timbre es una interrupción: suena mientras estás ocupado con otra cosa, lo dejas, miras la puerta y vuelves a lo que estabas haciendo. Un microcontrolador funciona igual. Cuando en el pin ocurre el cambio que elegiste, el hardware activa un flag de interrupción; el procesador termina la instrucción actual, guarda sus registros, salta a través del vector de interrupción a tu ISR (Interrupt Service Routine, rutina de servicio de interrupción) y, cuando la ISR termina, continúa en loop() justo donde lo había dejado. En el Uno este cambio tarda solo unos pocos microsegundos.
| Característica | Sondeo (polling) | Interrupción |
|---|---|---|
| Cómo funciona | loop() lee el pin una y otra vez | El hardware detecta el cambio y ejecuta la ISR |
| Tiempo de respuesta | Cuanto más lento es loop(), mayor es el retraso | Unos pocos microsegundos |
| Con delay() en el código | Se pueden perder eventos | Los eventos se siguen capturando |
| Pulsos muy rápidos | Se pierden | Se cuentan uno a uno |
| Complejidad del código | Sencillo | Requiere volatile y lecturas atómicas |
| Ideal para | Botones, sensores lentos | Encoders, sensores de velocidad y caudal, despertar del reposo |
¿Qué pines admiten interrupciones? Arduino, ESP32 y Pico
| Placa | Pines de interrupción externa | Nota |
|---|---|---|
| Arduino Uno R3, Nano, Pro Mini | D2, D3 | D2 = INT0, D3 = INT1 |
| Arduino Uno R4 (Minima / WiFi) | D2, D3 | Los pines indicados en la referencia oficial de Arduino |
| Arduino Mega 2560 | 2, 3, 18, 19, 20, 21 | 20 y 21 no sirven para interrupciones mientras se usan para I2C |
| Arduino Leonardo, Micro | 0, 1, 2, 3, 7 | 0 y 1 son también los pines del puerto serie |
| Arduino Nano Every | Todos los pines digitales | Procesador ATmega4809 |
| ESP32 | Todos los GPIO | GPIO 34–39 son solo de entrada y no tienen pull-up interna |
| ESP8266 (NodeMCU) | Todos salvo GPIO 16 (D0) | Las ISR deben marcarse con IRAM_ATTR |
| Raspberry Pi Pico / Pico W | Todos, de GP0 a GP28 | Los pines son de 3,3 V, nunca apliques 5 V |
En el código no pases el número de pin directamente; conviértelo en número de interrupción con digitalPinToInterrupt(pin), porque ese número cambia de una placa a otra: en el Uno D2 es la interrupción 0 y D3 la 1, mientras que en el Leonardo la interrupción 0 corresponde a D3 y la 1 a D2. Si necesitas más de dos interrupciones en el Uno, las interrupciones por cambio de pin (Pin Change Interrupt, PCINT) funcionan en casi todos los pines; eso sí, solo te dicen que "algo ha cambiado" y todos los pines de un mismo puerto comparten una sola ISR. Las librerías PinChangeInterrupt y EnableInterrupt facilitan su uso. Encontrarás el mapa completo de pines de tu placa en la herramienta Pinout y las funciones de los pines del Uno en el artículo pines del Arduino Uno.
Cómo usar attachInterrupt y los modos de interrupción
La sintaxis es attachInterrupt(digitalPinToInterrupt(pin), ISR, modo). El segundo parámetro es el nombre de la función que se ejecuta cuando llega la interrupción; esta función no recibe parámetros ni devuelve nada (void). El tercer parámetro indica qué evento dispara la interrupción. Para desactivar una interrupción se usa detachInterrupt(digitalPinToInterrupt(pin)), para pausar todas un momento noInterrupts() y para volver a activarlas interrupts().
| Modo | ¿Cuándo se dispara? | Uso típico |
|---|---|---|
| LOW | Una y otra vez mientras el pin esté en LOW | Despertar del reposo |
| CHANGE | En cada cambio (ambos flancos) | Encoders, medir la duración de un pulso, antirrebote |
| RISING | Al pasar de LOW a HIGH (flanco de subida) | Inicio de un pulso del sensor, botón con pull-down |
| FALLING | Al pasar de HIGH a LOW (flanco de bajada) | Pulsar un botón con pull-up, sensores de colector abierto |
| HIGH | Mientras el pin esté en HIGH | Solo en Due, Zero y MKR1000; en el ESP32, ONHIGH |
Elige el modo según tu circuito. Si conectaste el botón con INPUT_PULLUP, el pin está normalmente en HIGH y baja a LOW al pulsarlo, así que la pulsación la detecta FALLING. Usa el modo LOW con cuidado: mientras el pin siga en LOW, la ISR se ejecuta una y otra vez y loop() apenas avanza. Por eso suele usarse solo para despertar la placa; para sacar al Uno de su modo de reposo más profundo (power-down), la hoja de datos recomienda una interrupción por nivel LOW o una interrupción por cambio de pin. En el ESP32, las constantes LOW y HIGH no funcionan como esperas en modo de interrupción: LOW no llega a activarla y HIGH se comporta como un flanco de subida. Para interrupciones por nivel allí se escribe ONLOW y ONHIGH.
Primer ejemplo: contar pulsaciones con una interrupción
- Conecta una pata del botón a D2 y la otra a GND. No hace falta resistencia externa; usaremos la pull-up interna.
- Sube el código de abajo y abre el Monitor Serie a 9600 baudios.
- Pulsa el botón varias veces y observa cómo sube el contador. A pesar del delay(500) dentro de loop(), no se pierde ninguna pulsación.
- No te sorprendas si a veces el número salta 2 o 3 de golpe: es el rebote de los contactos, y lo resolveremos enseguida.
1const byte BUTTON_PIN = 2; // kesme pini D2 (INT0) / interrupt pin D2 (INT0)
2volatile unsigned int pressCount = 0; // ISR ile paylaşılan sayaç / counter shared with the ISR
3
4void onPress() { // ISR: kısa ve hızlı / ISR: short and fast
5 pressCount++;
6}
7
8void setup() {
9 Serial.begin(9600);
10 pinMode(BUTTON_PIN, INPUT_PULLUP); // dahili pull-up, buton GND'ye / internal pull-up, button to GND
11 attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), onPress, FALLING);
12}
13
14void loop() {
15 noInterrupts(); // 2 baytlık değeri güvenle kopyala / copy the 2-byte value safely
16 unsigned int count = pressCount;
17 interrupts();
18
19 Serial.print(F("Basis / Presses: "));
20 Serial.println(count);
21 delay(500); // loop yavaş olsa da basış kaçmaz / presses are not missed even if loop is slow
22}En este código hay tres puntos importantes. Primero, onPress() solo suma uno al contador y termina enseguida; la tarea de una ISR es anotar el evento, no hacer el trabajo. Segundo, el contador se declara volatile, de modo que el compilador no lo guarda en un registro ni lee un valor antiguo, sino que toma el valor actual de la memoria cada vez. Tercero, loop() desactiva las interrupciones un instante mientras lee el contador. Un unsigned int ocupa 2 bytes en el Uno y el procesador de 8 bits lo lee en dos pasos. Cuando el contador pasa de 255 a 256, es decir, de 0x00FF a 0x0100, loop() lee primero el byte bajo (0xFF); si justo entonces llega la interrupción y lo incrementa, luego lee el byte alto (0x01) y el resultado es 0x01FF, o sea, 511. Como este fallo aparece muy pocas veces, también es difícil de encontrar. En las placas AVR la macro ATOMIC_BLOCK(ATOMIC_RESTORESTATE) de util/atomic.h ofrece la misma protección; las variables de un solo byte, como bool y byte, no tienen este problema.
Reglas de la ISR: corta, con volatile y sin delay
- Mantenla corta y rápida: mientras se ejecuta una ISR, las demás interrupciones esperan. En el Uno el contador de millis() y el puerto serie también funcionan con interrupciones, así que una ISR larga estropea la temporización y puede hacer que pierdas datos serie entrantes. Una ISR ideal termina en pocos microsegundos.
- No uses delay(): delay() mide el tiempo con contadores que actualiza la interrupción del Timer0; dentro de una ISR las interrupciones están desactivadas, así que no funciona. Puedes leer millis(), pero no avanza durante la ISR, y micros() empieza a dar valores erróneos tras 1–2 ms. Si de verdad necesitas una espera muy corta, delayMicroseconds() sí funciona.
- No uses Serial.print(): el envío serie también depende de interrupciones; alarga la ISR y se bloquea si el búfer se llena. Deja la impresión para loop().
- Haz volatile las variables compartidas: toda variable que se use tanto en la ISR como en loop() debe declararse volatile.
- Lee de forma atómica las variables de varios bytes: en loop(), copia las variables int, long, float y unsigned long entre noInterrupts() e interrupts().
- Sin parámetros ni valor de retorno: una ISR debe ser void y no recibir parámetros; los datos se intercambian mediante variables globales.
- No metas trabajo pesado en la ISR: escribir en una pantalla I2C, guardar en una tarjeta SD o llamar a librerías que necesitan interrupciones, como Wire, provoca bloqueos dentro de una ISR.
Leer un botón con interrupción y eliminar el rebote
Cuando pulsas un botón mecánico, los contactos metálicos no se asientan de inmediato; se abren y cierran varias veces durante unos 0,1 a 10 milisegundos. Puede que loop() sea demasiado lento para notarlo, pero una interrupción capta cada flanco, así que una sola pulsación puede generar 3–5 interrupciones. Además, el rebote no solo ocurre al pulsar sino también al soltar, por lo que un código que use solo FALLING con un bloqueo por tiempo puede contar doble si mantienes el botón pulsado un rato y lo sueltas. El siguiente código usa el modo CHANGE y solo acepta una pulsación si el pin está en LOW y la línea llevaba al menos 50 ms en reposo. Así se filtran tanto los rebotes al pulsar como al soltar.
1const byte BUTTON_PIN = 2; // kesme pini / interrupt pin
2const byte LED_PIN = 13; // dahili LED / onboard LED
3const unsigned long QUIET_MS = 50; // gereken sessizlik süresi / required quiet time
4
5volatile bool ledState = false; // ISR ile paylaşılır / shared with the ISR
6
7void onButtonChange() { // iki kenarda da çalışır / runs on both edges
8 static unsigned long lastEdgeMs = 0;
9 unsigned long now = millis(); // ISR'de okunur ama artmaz / readable in an ISR but does not advance
10 bool pressed = (digitalRead(BUTTON_PIN) == LOW);
11
12 if (pressed && now - lastEdgeMs > QUIET_MS) { // hat sessizdi: gerçek basış / line was quiet: real press
13 ledState = !ledState;
14 }
15 lastEdgeMs = now; // her kenar süreyi yeniden başlatır / every edge restarts the timer
16}
17
18void setup() {
19 pinMode(LED_PIN, OUTPUT);
20 pinMode(BUTTON_PIN, INPUT_PULLUP);
21 attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), onButtonChange, CHANGE);
22}
23
24void loop() {
25 digitalWrite(LED_PIN, ledState ? HIGH : LOW); // bool tek bayt, okuması atomik / bool is one byte, atomic read
26 // burada uzun işler olsa da basış kaçmaz / long tasks here will not make you miss a press
27}Además del software, también puedes suprimir el rebote por hardware: si colocas un condensador de 100 nF entre los dos terminales del botón y una resistencia pull-up de 10 kΩ en el pin, la constante de tiempo es τ = R × C = 10 kΩ × 100 nF = 1 ms, que suaviza los rebotes cortos; el disparador Schmitt de las entradas del Uno limpia además el flanco. Puedes calcular otros valores con la herramienta constante de tiempo RC. Siendo sinceros, para botones pulsados a mano muchas veces no hace falta una interrupción; si loop() es rápido, el sondeo basta. Para pull-up, pull-down y antirrebote con millis(), consulta cómo conectar un botón a Arduino. Reserva las interrupciones para señales rápidas y cortas.
Leer un encoder rotativo con interrupciones
Al girar un encoder rotativo (como el KY-040), genera en dos pines llamados CLK y DT ondas cuadradas desfasadas entre sí. Si lo giras deprisa, los pulsos duran menos de un milisegundo y con el sondeo es fácil perder pasos. El método más sencillo es conectar CLK a un pin de interrupción y mirar el nivel de DT en el instante en que CLK baja: si DT está en HIGH ha girado en un sentido, y si está en LOW, en el otro. Esta versión cuenta un paso por cada clic; si el sentido sale al revés, intercambia las líneas ++ y --. Los encoders baratos también rebotan, así que añadir un condensador de 10–100 nF entre CLK y GND y entre DT y GND mejora mucho el resultado. Para una versión que conecta ambos pines a interrupciones y usa una tabla de estados para no perder ni un paso, echa un vistazo a la guía del encoder rotativo KY-040.
1const byte PIN_CLK = 2; // kesme pini / interrupt pin
2const byte PIN_DT = 4; // normal dijital giriş / regular digital input
3
4volatile long position = 0; // 4 baytlık konum / 4-byte position
5
6void onClkFall() {
7 if (digitalRead(PIN_DT) == HIGH) { // yön DT seviyesinden / direction from the DT level
8 position++;
9 } else {
10 position--;
11 }
12}
13
14void setup() {
15 Serial.begin(115200);
16 pinMode(PIN_CLK, INPUT_PULLUP);
17 pinMode(PIN_DT, INPUT_PULLUP);
18 attachInterrupt(digitalPinToInterrupt(PIN_CLK), onClkFall, FALLING);
19}
20
21void loop() {
22 static long lastPos = 0;
23
24 noInterrupts(); // atomik kopya / atomic copy
25 long pos = position;
26 interrupts();
27
28 if (pos != lastPos) {
29 Serial.println(pos);
30 lastPos = pos;
31 }
32}Contar vueltas: medir las RPM de un motor
Medir la velocidad de giro de un motor o una rueda es el uso clásico de las interrupciones. Como sensor se usa un sensor óptico de ranura con LM393 y un disco encoder de 20 ranuras, o un sensor Hall como el A3144 con un imán pegado al eje. Cada ranura o cada paso del imán produce un pulso. A partir de los pulsos contados en un segundo, la velocidad es: RPM = pulsos por segundo × 60 / pulsos por vuelta. Por ejemplo, si un disco de 20 ranuras da 100 pulsos por segundo, el motor gira a 100 × 60 / 20 = 300 RPM.
1const byte SENSOR_PIN = 3; // kesme pini D3 (INT1) / interrupt pin D3 (INT1)
2const byte PULSES_PER_REV = 20; // diskteki yarık sayısı / slots on the encoder disc
3
4volatile unsigned long pulseCount = 0;
5
6void onPulse() {
7 pulseCount++;
8}
9
10void setup() {
11 Serial.begin(9600);
12 pinMode(SENSOR_PIN, INPUT_PULLUP);
13 attachInterrupt(digitalPinToInterrupt(SENSOR_PIN), onPulse, FALLING);
14}
15
16void loop() {
17 static unsigned long lastMs = 0;
18
19 if (millis() - lastMs >= 1000) { // her saniye hesapla / compute once per second
20 lastMs += 1000;
21
22 noInterrupts(); // kopyala ve sıfırla / copy and reset
23 unsigned long pulses = pulseCount;
24 pulseCount = 0;
25 interrupts();
26
27 float rpm = pulses * 60.0 / PULSES_PER_REV;
28 Serial.print(F("RPM: "));
29 Serial.println(rpm, 1);
30 }
31}Aquí loop() copia y pone a cero el contador cada segundo; ambas operaciones están dentro del mismo bloque noInterrupts() para que no se pierda ningún pulso si llega una interrupción entre ellas. Con un disco de 20 ranuras la resolución es de 3 RPM. Si quieres más precisión a baja velocidad, en lugar de contar pulsos mide en la ISR el tiempo entre dos pulsos con micros() y usa RPM = 60.000.000 / (periodo_µs × pulsos por vuelta). El mismo método sirve para el caudalímetro YF-S201, para medir velocidad con un sensor Hall y para las vueltas de la rueda de un motor DC con reductora.
Una nota breve sobre las interrupciones de temporizador
Las interrupciones no solo vienen de los pines. Los temporizadores internos del microcontrolador también pueden generarlas a intervalos fijos, de modo que una función se ejecuta exactamente cada 1 ms o cada 500 ms haga lo que haga loop(). El Uno tiene tres temporizadores y algunos ya los usan las propias funciones de Arduino:
| Temporizador | Tamaño | Lo usa | Pines PWM |
|---|---|---|---|
| Timer0 | 8 bits | millis(), delay(), micros() | 5, 6 |
| Timer1 | 16 bits | Librería Servo, TimerOne | 9, 10 |
| Timer2 | 8 bits | tone(), IRremote | 3, 11 |
En el Uno, lo más fácil es la librería TimerOne: Timer1.initialize(500000) fija el periodo en microsegundos y Timer1.attachInterrupt(funcion) asocia la ISR que se ejecuta cada 500 ms. Como usa el Timer1, entra en conflicto con la librería Servo y con analogWrite() en los pines 9 y 10. En el ESP32 con el núcleo de Arduino 3.x, una interrupción de temporizador se configura con timerBegin(1000000), timerAttachInterrupt() y timerAlarm(); timerAlarmWrite() y timerAlarmEnable() de la versión 2.x se eliminaron, por lo que los ejemplos antiguos fallan con el error "was not declared in this scope". En la Pico, MicroPython configura una interrupción periódica en una sola línea con Timer(period=500, mode=Timer.PERIODIC, callback=funcion). Para saber cómo se relacionan los temporizadores con el PWM, consulta ¿Qué es PWM?
Interrupciones en el ESP32: IRAM_ATTR y portMUX
En el ESP32 no necesitas buscar un pin especial; cualquier GPIO puede generar una interrupción. Solo GPIO 34–39 carecen de pull-up interna, así que un botón ahí necesita una pull-up externa de 10 kΩ; además, la lista de erratas de Espressif indica que GPIO 36 y 39 pueden producir interrupciones falsas mientras se usa el ADC. La verdadera diferencia en el ESP32 es el atributo IRAM_ATTR delante de la ISR. Este coloca la función en la RAM interna rápida (IRAM) en lugar de en la flash. Al escribir en la flash (Preferences, SPIFFS, OTA) la caché se desactiva; según la configuración, una interrupción que acceda en ese momento a código en la flash puede reiniciar la placa con "Guru Meditation Error: Core 1 panic'ed (Cache disabled but cached memory region accessed)". El ESP8266 es aún más estricto: una ISR sin IRAM_ATTR detiene la placa con el mensaje "ISR not in IRAM!" en cuanto se llama a attachInterrupt.
1const int BUTTON_PIN = 27; // her GPIO kesme verebilir / any GPIO can interrupt
2const int LED_PIN = 2; // çoğu DevKit'te dahili LED / onboard LED on most DevKits
3const uint32_t QUIET_MS = 50; // gereken sessizlik süresi / required quiet time
4
5volatile uint32_t pressCount = 0;
6portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED; // paylaşılan veri kilidi / lock for shared data
7
8void IRAM_ATTR onButtonChange() { // ISR IRAM'de çalışır / ISR runs from IRAM
9 static uint32_t lastEdgeMs = 0;
10 uint32_t now = millis();
11 bool pressed = (digitalRead(BUTTON_PIN) == LOW);
12
13 if (pressed && now - lastEdgeMs > QUIET_MS) {
14 portENTER_CRITICAL_ISR(&mux);
15 pressCount++;
16 portEXIT_CRITICAL_ISR(&mux);
17 }
18 lastEdgeMs = now;
19}
20
21void setup() {
22 Serial.begin(115200);
23 pinMode(LED_PIN, OUTPUT);
24 pinMode(BUTTON_PIN, INPUT_PULLUP);
25 attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), onButtonChange, CHANGE);
26}
27
28void loop() {
29 static uint32_t lastCount = 0;
30
31 portENTER_CRITICAL(&mux); // loop tarafında kilit / lock on the loop side
32 uint32_t count = pressCount;
33 portEXIT_CRITICAL(&mux);
34
35 if (count != lastCount) {
36 lastCount = count;
37 digitalWrite(LED_PIN, (count % 2) ? HIGH : LOW); // her basışta LED değişir / LED toggles on every press
38 Serial.printf("Basis / Press: %lu\n", (unsigned long)count);
39 }
40}Como el ESP32 ejecuta FreeRTOS y tiene dos núcleos, las variables compartidas se protegen con un cerrojo portMUX_TYPE en lugar del noInterrupts() del Uno: dentro de la ISR se usa portENTER_CRITICAL_ISR() y en loop() portENTER_CRITICAL(). Leer una variable de 32 bits ya es un solo paso en el ESP32, pero el cerrojo es imprescindible para operaciones como leer y poner a cero un valor o actualizar varias variables a la vez. Si quieres asociar la misma función a varios pines, attachInterruptArg() te permite pasar a la ISR un parámetro, como el número de pin. Para lo básico del ESP32 consulta ¿Qué es el ESP32? y para tareas y núcleos ESP32 FreeRTOS y doble núcleo.
Interrupciones en la Raspberry Pi Pico: irq en MicroPython
En la Raspberry Pi Pico todos los pines de GP0 a GP28 pueden generar una interrupción. En MicroPython se configura con el método irq() del objeto pin: en trigger pasas Pin.IRQ_FALLING, Pin.IRQ_RISING o ambos combinados, y handler es la función que se ejecuta al llegar la interrupción; recibe como parámetro el pin que la disparó. Python no tiene volatile; basta con declarar como global las variables que modificas en la ISR. Para medir el tiempo se usan time.ticks_ms() y time.ticks_diff(), que soporta el desbordamiento. El siguiente código lleva a la Pico la lógica de tiempo en reposo del ejemplo de Arduino:
1from machine import Pin
2import time
3
4led = Pin("LED", Pin.OUT) # dahili LED (Pico ve Pico W) / onboard LED (Pico and Pico W)
5button = Pin(14, Pin.IN, Pin.PULL_UP) # buton GP14 ile GND arasında / button between GP14 and GND
6
7QUIET_MS = 50 # gereken sessizlik süresi / required quiet time
8presses = 0
9last_edge = time.ticks_ms()
10
11def on_change(pin): # handler tetikleyen pini alır / handler receives the pin
12 global presses, last_edge
13 now = time.ticks_ms()
14 if pin.value() == 0 and time.ticks_diff(now, last_edge) > QUIET_MS:
15 presses += 1
16 led.toggle()
17 last_edge = now
18
19button.irq(trigger=Pin.IRQ_FALLING | Pin.IRQ_RISING, handler=on_change)
20
21while True:
22 print("Basis / Presses:", presses) # ana döngü uyurken bile basışlar sayılır / presses are counted while the loop sleeps
23 time.sleep(1)En la Pico, irq funciona por defecto como interrupción soft: MicroPython pone la función en cola y la ejecuta lo antes posible, así que usar print o crear una lista dentro no da problemas, aunque la latencia aumenta un poco. Si pasas hard=True, la latencia baja a microsegundos; a cambio no puedes reservar memoria dentro de la ISR, de modo que una lista nueva, un texto e incluso un cálculo con float pueden lanzar un "MemoryError". Si programas la Pico con el Arduino IDE (núcleo de Earle Philhower), attachInterrupt() funciona igual en todos los GPIO. Para la instalación consulta ¿Qué es la Raspberry Pi Pico? y para los comandos básicos fundamentos de MicroPython.
Ideas de proyectos con interrupciones
- Medidor de reflejos: mide cuánto tardas en pulsar el botón después de que se encienda un LED, usando micros() en la ISR con precisión inferior al milisegundo.
- Velocímetro para bicicleta: pon un imán en la rueda y un sensor Hall en el cuadro; calcula la velocidad y la distancia a partir del tiempo por vuelta.
- Contador de agua: cuenta los pulsos de un caudalímetro YF-S201 y muestra los litros consumidos.
- Coche robot que va recto: mide la velocidad de los dos motores con encoders en las ruedas e iguálalas.
- Control de menús: recorre un menú en pantalla y cambia ajustes con un encoder rotativo.
- Alarma a pilas: deja que la salida de un sensor PIR despierte la placa mientras duerme; para el ESP32, consulta el artículo sobre deep sleep.
Errores frecuentes y cómo solucionarlos
| Síntoma | Causa | Solución |
|---|---|---|
| La interrupción nunca se dispara | Se pasó un número de pin a attachInterrupt(2, ...) o se eligió un pin sin interrupción | Usa digitalPinToInterrupt(pin); en el Uno conecta a D2 o D3 |
| Una pulsación sube el contador 2–5 | Rebote de los contactos | Aplica antirrebote por software y/o añade un condensador de 100 nF |
| La variable nunca cambia en loop() | Falta volatile | Declara volatile la variable compartida |
| De vez en cuando, valores absurdamente grandes | Variable de varios bytes leída de forma no atómica | Cópiala entre noInterrupts() e interrupts() |
| El programa se cuelga o millis() se desvía | delay(), Serial o una tarea larga dentro de la ISR | En la ISR solo actualiza un flag o un contador |
| Llegan interrupciones sin tocar el botón | El pin está al aire (flotante), sin pull-up | Usa INPUT_PULLUP o una resistencia externa de 10 kΩ |
| La ISR se ejecuta una vez nada más volver a asociarla | Un flanco llegó con la interrupción desactivada y activó el flag (AVR) | En el Uno escribe EIFR = bit(INTF0); antes de attachInterrupt (INTF1 para D3) |
| El ESP32 se reinicia al azar | La ISR no está en IRAM o hace trabajo pesado | Añade IRAM_ATTR y acorta la ISR |
Las interrupciones son una herramienta potente que te permite captar cada evento aunque loop() esté ocupado con otras tareas; solo tienes que recordar tres reglas: mantén la ISR corta, declara volatile las variables compartidas y léelas de forma atómica en loop(). En la app BlueGrays encontrarás contadores con botón, un test de reflejos, proyectos de botones con ESP32 y un proyecto para la Pico que compara el antirrebote por interrupción (IRQ) y por sondeo en software, todos con esquema del circuito y código paso a paso.
Preguntas frecuentes
¿Qué pines del Arduino Uno admiten interrupciones?
El Arduino Uno R3, el Nano y el Pro Mini solo tienen dos pines de interrupción externa: D2 (INT0) y D3 (INT1). En el Mega 2560 puedes usar 2, 3, 18, 19, 20 y 21, y en el Leonardo 0, 1, 2, 3 y 7. En el resto de pines solo están disponibles las interrupciones por cambio de pin (PCINT), que únicamente avisan de que el pin ha cambiado.
¿Por qué no funciona delay() dentro de una interrupción (ISR)?
delay() mide el tiempo con contadores que actualiza la interrupción del Timer0. Mientras se ejecuta una ISR, las demás interrupciones esperan, así que esos contadores no avanzan y delay() no puede funcionar bien. Si algo necesita esperar, activa solo un flag en la ISR y haz la espera en loop() con millis().
¿Para qué sirve volatile y cuándo debo usarlo?
volatile le dice al compilador que la variable puede cambiar en cualquier momento fuera del flujo normal del programa, por ejemplo en una ISR, así que su valor no se guarda en un registro, sino que se lee de memoria cada vez. Toda variable usada en una ISR y en loop() debe ser volatile. Sin embargo, volatile no hace atómicas las lecturas de varios bytes; para eso se usa noInterrupts().
¿Se pueden usar más de 2 interrupciones en un Arduino Uno?
Sí. Gracias a las interrupciones por cambio de pin (PCINT), casi cualquier pin del Uno puede generar una interrupción. Solo avisan de un cambio, y los pines del mismo puerto (D0–D7, D8–D13, A0–A5) comparten una ISR. Con las librerías PinChangeInterrupt o EnableInterrupt su uso es muy parecido al de attachInterrupt.
¿Debo usar interrupciones o sondeo (polling)?
Para botones pulsados a mano y sensores que cambian despacio, el sondeo en loop() suele bastar y es más sencillo. Usa interrupciones para pulsos rápidos y cortos, como los de encoders, sensores de velocidad y caudalímetros, o para despertar la placa del reposo. Si loop() está lleno de delay() largos, cambiarlos por millis() también resuelve la mayoría de los problemas.
¿Qué pasa si no pongo IRAM_ATTR en el ESP32?
Normalmente el código compila y parece funcionar, pero la ISR se queda en la flash, arranca más despacio y, según la configuración, puede fallar con "Cache disabled but cached memory region accessed" mientras se escribe en la flash. En el ESP8266, sin IRAM_ATTR la placa se detiene al instante con "ISR not in IRAM!". Por eso, en las placas ESP, pon IRAM_ATTR delante de cada ISR.
Proyectos para hacer después de leer esto
Ver todos los proyectos
Encender y apagar un LED con un botón
Pulsas el botón una vez y la luz se enciende, lo pulsas otra vez y se apaga, igual que el interruptor de tu habitación. El Arduino nota cada pulsación y cambia el LED al estado contrario, esperando un

Raspberry Pi Pico Botón Debounce: IRQ vs Software Polling
¿Una pulsación y el código cuenta 3-4? ¡Es contact bounce! Compara 2 métodos de conteo en Serial: polling RAW (captura bounce, cuenta mal) vs IRQ + software debounce (preciso). Aprende física del bounce, software debounce, Pin.irq() interrupt, IRQ_FALLING trigger, ISR handlers, seguridad de variables globales en ISR.

Control de LED con botón en ESP32 (Toggle)
Cada vez que se pulsa el botón, cambia el estado del LED: si está encendido se apaga, si está apagado se enciende. Patrón de conmutación clásico. Con debounce limpiamos el ruido mecánico del pulsador para garantizar una sola conmutación con una sola pulsación.

Raspberry Pi Pico Botón Toggle LED
¡Botón + LED — siguiente nivel! A diferencia del proyecto básico de botón, no necesitas mantener el botón presionado. Presiona y suelta → LED encendido. Presiona de nuevo → LED apagado (toggle). Mismo circuito, gran diferencia de código. Aprende edge detection, state tracking y debounce.

Contador con display de 7 segmentos y botones
Un contador sencillo con tres botones: con uno el número de la pantalla sube de uno en uno, con otro baja, y el tercero lo devuelve a cero. La pantalla de 7 segmentos de un solo dígito cuenta del 0 al

Arduino Test de Reacción con Aviso Sonoro: Mide tus Reflejos en Milisegundos (LCD + Zumbador + 3 LED)
¿Tienes buenos reflejos? Pulsa el botón: el LED rojo dice preparados, el amarillo dice listos y empieza una espera aleatoria de 1,5 a 3,5 segundos. En cuanto se enciende el LED verde y suena el zumbador, vuelve a pulsar; tu tiempo de reacción aparece en milisegundos en la LCD I2C, con tu récord de la sesión y tu media debajo. Si pulsas antes del verde, la pantalla muestra TOO EARLY! y suena un tono de derrota. Mantén pulsado para un nuevo jugador con las mismas esperas.

Contador ESP32 WebSocket (2 botones + pantalla LCD + web en tiempo real)
En este proyecto, dos botones conectados al ESP32 (GPIO 5 suma, GPIO 4 resta) cambian un contador. El valor se actualiza al instante en una pantalla LCD I2C de 16x2 y, gracias al soporte WebSocket de ESPAsyncWebServer, en todos los navegadores conectados sin recargar la página. En lugar del sondeo HTTP se usa una única conexión persistente y el ESP32 envía el nuevo valor a todos con ws.textAll().

Juego de memoria con Arduino (4 LED + 4 pulsadores + zumbador)
¿Te fías de tu memoria? Cuatro LED de colores se encienden en secuencia, cada uno con su propia nota, y tú repites el mismo orden pulsando los botones. Cada ronda acertada añade un color nuevo a la se
Más artículos
App BlueGrays
Monta este circuito paso a paso en tu teléfono
Esquema, código y lista de piezas en una sola pantalla. Más de 200 proyectos de Arduino, ESP32 y Pico listos para montar.
Escanea con tu teléfono
Apunta la cámara al código y se abrirá la tienda de apps de tu teléfono.
o ábrela en la tienda

