Las interrupciones son un mecanismo muy potente y valioso en procesadores y autómatas. Arduino, por supuesto, no es una excepción. En esta entrada veremos qué son las interrupciones, y como usarlas en nuestro código.
Para entender la utilidad y necesidad de las interrupciones, supongamos que tenemos Arduino conectado a un sensor (por ejemplo encoder óptico que cuenta las revoluciones de un motor, un detector que emite una alarma de nivel de agua en un depósito, o un simple pulsador de parada).
Si queremos detectar un cambio de estado en esta entrada, el método que hemos usado hasta ahora es emplear las entradas digitales para consultar repetidamente el valor de la entrada, con un intervalo de tiempo (delay) entre consultas.
Este mecanismo se denomina “poll”, y tiene 3 claras desventajas:
- Suponer un continuo consumo de procesador y de energía, al tener que preguntar continuamente por el estado de la entrada.
- Si la acción necesita ser atendida inmediatamente (por ejemplo en una alerta de colisión) esperar hasta el punto de programa donde se realiza la consulta puede ser inaceptable.
- Si el pulso es muy corto, o si el procesador está ocupado haciendo otra tarea mientras se produce, es posible que nos saltemos el disparo y nunca lleguemos a verlo.
Para resolver este tipo de problemas, los microprocesadores incorporan el concepto de interrupción, que es un mecanismo que permite asociar una función a la ocurrencia de un determinado evento. Esta función de callback asociada se denomina ISR (Interruption Service Rutine).
Cuando ocurre el evento el procesador “sale” inmediatamente del flujo normal del programa y ejecuta la función ISR asociada ignorando por completo cualquier otra tarea (por esto se llama interrupción). Al finalizar la función ISR asociada, el procesador vuelve al flujo principal, en el mismo punto donde había sido interrumpido.
Como vemos, las interrupciones son un mecanismo muy potente y cómodo que mejora nuestros programas y nos permite realizar acciones que no serían posibles sin el uso de interrupciones.
Para usar interrupciones en dispositivos físicos (como pulsadores, sensores ópticos, … ) debemos antes eliminar el efecto “rebote”, como podéis ver en la entrada Aplicar debounce al usar interrupciones en Arduino
Interrupciones en Arduino
Arduino dispone de dos tipos de eventos en los que definir interrupciones. Por un lado tenemos las interrupciones de timers (que veremos en su momento al hablar de temporizadores). Por otro lado, tenemos las interrupciones de hardware, que responden a eventos ocurridos en ciertos pines físicos.
Dentro de las interrupciones de hardware, que son las que nos ocupan en esta entrada, Arduino es capaz de detectar los siguientes eventos.
RISING
, ocurre en el flanco de subida deLOW
aHIGH
FALLING
, ocurre en el flanco de bajada deHIGH
aLOW
CHANGING
, ocurre cuando el pin cambia de estado (rising + falling)LOW
, se ejecuta continuamente mientras está en estadoLOW
Los pines susceptibles de generar interrupciones varían en función del modelo de Arduino.
En Arduino y Nano se dispone de dos interrupciones, 0 y 1, asociados a los pines digitales 2 y 3.El Arduino Mega dispone de 6 interrupciones, en los pines 2, 3, 21, 20, 19 y 18 respectivamente. Arduino Due dispone de interrupciones en todos sus pines.
Modelo | INT0 | INT1 | INT2 | INT3 | INT4 | INT5 |
---|---|---|---|---|---|---|
UNO | 2 | 3 | ||||
Nano | 2 | 3 | ||||
Mini Pro | 2 | 3 | ||||
Mega | 2 | 3 | 21 | 20 | 19 | 18 |
Leonardo | 3 | 2 | 0 | 1 | 7 | |
Due | En todos los pines |
La función ISR
La función asociada a una interrupción se denomina ISR (Interruption Service Routines) y, por definición, tiene que ser una función que no recibe nada y no devuelva nada.
Dos ISR no pueden ejecutarse de forma simultánea. En caso de dispararse otra interrupción mientras se ejecuta una ISR, la función ISR se ejecuta una a continuación de otra.
La ISR, cuanto más corta mejor
Al diseñar una ISR debemos mantener el menor tiempo de ejecución posible, dado que mientras se esté ejecutando el bucle principal y todo el resto de funciones se encuentran detenidas.
Imaginemos, por ejemplo, que el programa principal ha sido interrumpido mientras un motor acercaba un brazo para coger un objeto. Una interrupción larga podría hacer que el brazo no para a tiempo, tirando o dañando el objeto.
Frecuentemente la función de la ISR se limitará a activar un flag, incrementar un contador, o modificar una variable. Esta modificación será atendida posteriormente en el hilo principal, cuando sea oportuno.
No empleéis en una ISR un proceso que consuma tiempo. Esto incluye cálculos complejos, comunicación (serial, I2C y SPI) y, en la medida de lo posible, cambio de entradas o salidas tanto digitales como analógicas.
Las variables de la ISR como “Volátiles”
Para poder modificar una variable externa a la ISR dentro de la misma debemos declararla como “volatile”.
El indicador “volatile” indica al compilador que la variable tiene que ser consultada siempre antes de ser usada, dado que puede haber sido modificada de forma ajena al flujo normal del programa (lo que, precisamente, hace una interrupción).
Al indicar una variable como Volatile el compilador desactiva ciertas optimizaciones, lo que supone una pérdida de eficiencia. Por tanto, sólo debemos marcar como volatile las variables que realmente lo requieran, es decir, las que se usan tanto en el bucle principal como dentro de la ISR.
Efectos de la interrupción y la medición del tiempo
Las interrupciones tienen efectos en la medición del tiempo de Arduino, tanto fuera como dentro de la ISR, porque Arduino emplea interrupciones de tipo Timer para actualizar la medición del tiempo.
Efectos fuera de la ISR
Durante la ejecución de una interrupción Arduino no actualiza el valor de la función millis y micros. Es decir, el tiempo de ejecución de la ISR no se contabiliza y Arduino tiene un desfase en la medición del tiempo.
Si un programa tiene muchas interrupciones y estas suponen un alto tiempo de ejecución, la medida del tiempo de Arduino puede quedar muy distorsionada respecto a la realidad (nuevamente, un motivo para hacer las ISR cortas).
Efectos dentro de la ISR
Dentro de la ISR el resto de interrupciones están desactivadas. Esto supone:
- La función millis no actualiza su valor, por lo que no podemos utilizarla para medir el tiempo dentro de la ISR. (sí podemos usarla para medir el tiempo entre dos ISR distintas)
- Como consecuencia la función delay() no funciona, ya que basa su funcionamiento en la función millis()
- La función micros() actualiza su valor dentro de una ISR, pero empieza a dar mediciones de tiempo inexactas pasado el rango de 500us.
- En consecuencia, la función delayMicroseconds funciona en ese rango de tiempo, aunque debemos evitar su uso porque no deberíamos introducir esperas dentro de una ISR.
Crear interrupciones en Arduino
Para definir una interrupción en Arduino usamos la función:
attachInterrupt(interrupt, ISR, mode);
Donde interrupt es el número de la interrupción que estamos definiendo, ISR la función de callback asociada, y mode una de las opciones disponibles (FALLING
, RISING
, CHANGE
y LOW
)
No obstante, es más limpio emplear la función digitalPinToInterrupt(), que convierte un Pin a la interrupción equivalente. De esta forma se favorece el cambio de modelo de placa sin tener que modificar el código.
attachInterrupt(digitalPinToInterrupt(pin), ISR, mode);
Otras funcionas interesantes para la gestión de interrupciones son:
DetachInterrupt(interrupt)
, anula la interrupción.NoInterrupts()
, desactiva la ejecución de interrupciones hasta nueva orden (equivale a cli())Interrupts()
, reactiva las interrupciones (equivale a sei())
Probando las interrupciones en Arduino
Para probar las interrupciones en Arduino, vamos a emplear una salida digital de Arduino para emular una señal digital.
En el mundo real, sería otro dispositivo (un sensor, otro procesador…) el que generaría esta señal, y nosotros la captaríamos con Arduino.
Conectamos mediante un cable el pin digital 10 al pin digital 2, asociado a la interrupción 0.
Haciendo parpadear un LED a través de interrupciones
En el siguiente código definimos el pin digital 10 como salida, para emular una onda cuadrada de periodo 300ms (150ms ON y 150ms OFF).
Para visualizar el funcionamiento de la interrupción, en cada flanco activo del pulso simulado, encendemos/apagamos el LED integrado en la placa, por lo que el LED parpadea a intervalos de 600ms (300ms ON y 300ms OFF)
Puede que a estas alturas ver parpadear un LED no parezca muy espectacular, pero no es tan simple como parece. No estamos encendiendo el LED con una salida digital, si no que es la interrupción que salta la que enciende y apaga el LED(el pin digital solo emula una señal externa).
const int emuPin = 10;
const int LEDPin = 13;
const int intPin = 2;
volatile int state = `LOW`; // definimos como volatil
void setup() {
pinMode(emuPin, OUTPUT);
pinMode(LEDPin, OUTPUT);
pinMode(intPin, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(intPin), blink, CHANGE);
}
void loop() {
//esta parte es para emular la salida
digitalWrite(emuPin, HIGH);
delay(150);
digitalWrite(emuPin, LOW);
delay(150);
}
void blink() {
state = !state; // cambiamos el state para que parpadee
digitalWrite(LEDPin, state);
}
Contando disparos de una interrupción
El siguiente código empleamos el mismo pin digital para emular una onda cuadrada, esta vez de intervalo 2s (1s ON y 1s OFF).
En cada interrupción actualizamos el valor de un contador. Posteriormente, en el bucle principal, comprobamos el valor del contador, y si ha sido modificado mostramos el nuevo valor.
Al ejecutar el código, veremos que en el monitor serie se imprimen números consecutivos a intervalos de dos segundos.
const int emuPin = 10;
const int intPin = 2;
volatile int ISRCounter = 0;
int counter = 0;
void setup()
{
pinMode(emuPin, OUTPUT);
pinMode(intPin, INPUT_PULLUP);
Serial.begin(9600);
attachInterrupt(digitalPinToInterrupt(intPin), interruptCount, FALLING);
}
void loop()
{
//esta parte es para emular la salida
digitalWrite(emuPin, HIGH);
delay(1000);
digitalWrite(emuPin, LOW);
delay(1000);
if (counter != ISRCounter)
{
counter = ISRCounter;
Serial.println(counter);
}
}
void interruptCount()
{
ISRCounter++;
}
Lógicamente, nuestro objetivo es emplear interrupciones de hardware no solo con otros dispositivos digitales, si no también con dispositivos físicos como pulsadores, sensores ópticos…
Sin embargo, como hemos adelantado, estos dispositivos generan mucho ruido en los cambios de estado, lo que provoca que las interrupciones se disparen múltiples veces. Este fenómeno se denomina “rebote” y aprenderemos a eliminarlo en la siguiente entrada.
Descarga el código
Todo el código de esta entrada está disponible para su descarga en Github.