Interrupciones: ¿Qué son y cómo usarlas?
Introducción
Las interrupciones son un mecanismo muy potente y valioso en procesadores y autómatas. Arduino, por supuesto, no es una excepción.
En esta guía veremos qué son las interrupciones, y como usarlas en nuestro código.
Para entender la utilidad y necesidad de las interrupciones, supongamos que tenemos conectado a Arduino: "encoder óptico" que cuenta las revoluciones de un motor, un "sensor de líquidos" que emite una alarma de nivel de agua en un depósito, y un pulsador de parada.
Si queremos detectar un cambio de estado en estas entradas, el método básico procedimental 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:
- Consumo continuo 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 con un sensor de fin de carrera) esperar hasta el punto de programa donde se realiza la consulta puede ser inaceptable.
- Si el pulso de entrada 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 censarlo.
Para resolver este tipo de problemas, los microprocesadores incorporan el concepto de "interrupción", 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, etc) debemos antes eliminar el efecto "rebote":
Interrupciones
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: cuando el pin pase de LOW a HIGH. (ej. al pulsar un botón)
- FALLING: cuando el pin pase de HIGH a LOW. (ej. al soltar el botón)
- CHANGING: ocurre cuando el pin cambia de estado (rising + falling).
- HIGH: cuando el pin esté a HIGH.
- LOW: cuando el pin esté a LOW.
Los pines susceptibles de generar interrupciones varían en función del modelo de Arduino.
En la siguiente tabla resumimos la disponibilidad en cada modelo de Arduino
| 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 (Interruption Service Routines)
La función asociada a una interrupción se denomina ISR 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 función ISR, cuanto más corta mejor
Al diseñar una función ISR debemos mantener como objetivo que tenga el menor tiempo de ejecución posible, dado que mientras la función ISR se esté ejecutando, el loop principal y todo el resto de funciones se encuentran detenidas.
Por ejemplo, el programa principal ha sido interrumpido mientras un motor acercaba su brazo a un objeto. Una interrupción larga podría hacer que el brazo no se detenga a tiempo, tirando o dañando el objeto.
Frecuentemente la función de la función 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 emplear en una función 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 función ISR como "volátiles"
Para poder modificar una variable externa a la función 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 loop principal como dentro de la función 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 función ISR, porque Arduino emplea interrupciones de tipo Timer para actualizar la medición del tiempo.
Efectos fuera de la función 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 función 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 (por esto es IMPORTANTE hacer las funciones ISR cortas).
Efectos dentro de la función ISR
Dentro de la función 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 función ISR.
- 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
Para definir una interrupción en Arduino usamos la función attachInterrupt().
Sintaxis
attachInterrupt(interrupt, ISR, mode);
attachInterrupt(digitalPinToInterrupt(pin), ISR, mode);
Parámetros
- interrupt: número de la interrupción que estamos definiendo
- ISR: la función de callback asociada
- mode una de las opciones disponibles (Falling, Rising, Change y Low)
- la función digitalPinToInterrupt() convierte un Pin a la interrupción equivalente
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.
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()
Ejemplo práctico
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 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;
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;
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 loop 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, LOW);
}
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++;
timeCounter = millis();
}
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 un fenómeno conocido como “rebote”, que hace que las interrupciones se disparen múltiples veces.
