En esta guía, exploraremos qué son las interrupciones, cómo funcionan con Arduino y cómo utilizar la función ISR (Interrupt Service Routine). Las interrupciones son una funcionalidad muy importante no solo en el entorno Arduino, sino en los microcontroladores en general, ya que facilitan priorizar tareas de una forma bastante sencilla. Son un mecanismo muy potente y valioso en procesadores y autómatas; Arduino, por supuesto, no es una excepción.
Para resolver ciertos problemas en la programación, 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. Una interrupción es una suspensión temporal de la ejecución de un proceso, para pasar a ejecutar una subrutina de servicio de interrupción, la cual, por lo general, no forma parte del programa. Una vez finalizada dicha subrutina, se reanuda la ejecución del programa. 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.
Consideremos un ejemplo práctico: imagina que tienes un robot móvil aspirando el suelo de tu casa. ¿Qué pasaría si de repente el sensor que avisa al robot de si se va a chocar o no le envía un aviso de que, efectivamente, está a punto de chocarse? Si utilizas una interrupción, podrías hacer que cuando el sensor de colisión se activara (evento), el robot móvil se parara o diera marcha atrás (función asociada). Las interrupciones nos van a permitir reaccionar a eventos externos a la placa de una forma rápida, liberando al procesador para otras tareas.
Esto es crucial, ya que dentro de una aplicación o programa, no vamos a hacer una única cosa. Por ejemplo, si queremos que un LED se encienda o se apague cuando pulsamos un botón, sin interrupciones, tendríamos que estar comprobando constantemente el pin del botón, lo que consume valiosos ciclos de procesador. Un proceso síncrono es aquel que se ejecuta y hasta que no finaliza solo se ejecuta ese proceso (todo en el mismo loop), mientras que uno asíncrono comienza la ejecución y continúan ejecutándose otras tareas, por lo que el proceso total se completa en varios loops. Las interrupciones son los que permiten esta operación asíncrona, actuando como administradores de la ejecución de tareas, lo cual permite la multitarea.
Tipos de Interrupciones
Existen diferentes tipos de interrupciones que los microcontroladores manejan:
Lea también: IVA 21% Excel
- Interrupciones HW o externas: Estas son asíncronas a la ejecución del procesador, es decir, se pueden producir en cualquier momento independientemente de lo que esté haciendo el CPU en ese momento.
- Interrupciones SW: Las interrupciones por software son aquellas generadas por un programa en ejecución. Para generarlas, existen distintas instrucciones en el código máquina que permiten al programador producir una interrupción.
- Excepciones: Son aquellas que se producen de forma síncrona a la ejecución del procesador, típicamente causada por una condición de error en un programa.
- Un evento programado o Timer: Estas interrupciones se disparan cada cierto intervalo de tiempo que se debe configurar.
Interrupciones Específicas en Arduino
Internamente, Arduino (o más bien el microcontrolador AtMega) tiene ciertas interrupciones configuradas que lanza según la situación. El procesador dentro de cualquier Arduino tiene dos tipos principales de interrupciones: “externas” y “cambio de pin”. Además, Arduino dispone de interrupciones por temporizador. Dentro de la gama de interrupciones con Arduino están las interrupciones programadas (timers) y las interrupciones externas. La filosofía es la misma: poder ejecutar un código cuando sucede un evento concreto.
Pines de Interrupción Externa en Arduino
No todos los pines pueden ejecutar interrupciones. Los pines susceptibles de generar interrupciones varían en función del modelo de Arduino. Para las interrupciones externas o hardware, los modelos Arduino Uno y Nano disponen de dos interrupciones, 0 y 1, asociadas a los pines digitales 2 y 3. El Arduino Mega dispone de más pines, específicamente 6 interrupciones.
La siguiente tabla detalla los pines de interrupción para diferentes placas Arduino:
| Modelo de Arduino | Pines de Interrupción Externa (INT) |
|---|---|
| Arduino Uno / Nano / Mini Pro | 2 (INT0), 3 (INT1) |
| Arduino Mega | 2 (INT0), 3 (INT1), 18 (INT5), 19 (INT4), 20 (INT3), 21 (INT2) |
| Arduino Micro / Leonardo (ATMega32u4) | 0, 1, 2, 3, 7 |
| Arduino Zero | Todos los pines menos el 4 |
| Arduino Due | Todos los pines |
| Arduino MKR (todos los modelos) | 0, 1, 4, 5, 6, 7, 8, 9, A1, A2 |
| Arduino Nano 33 IoT | 2, 3, 9, 10, 11, 13, A1, A5, A7 |
| Arduino Nano 33 BLE / BLE Sense | Todos los pines |
La Función ISR (Interrupt Service Routine)
La función de callback asociada a una interrupción se denomina ISR (Interrupt Service Routine). Por definición, una ISR tiene que ser una función que no recibe nada y no devuelve nada. Dos ISR no pueden ejecutarse de forma simultánea; en caso de dispararse otra interrupción mientras se ejecuta una ISR, las funciones ISR se ejecutan una a continuación de otra.
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. La ISR frecuentemente 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. Las funciones de interrupción por ISR deben ser ejecutadas en el menor tiempo posible, siendo lo más cortas posible.
Lea también: Guía IVA reducido
Consideraciones dentro de una ISR
- Funciones de tiempo: Dentro de la función llamada desde la interrupción, la función delay() no funciona y el valor devuelto por millis() no aumenta. Esto se debe a que delay() se basa en millis(), y millis() no actualiza su valor dentro de la ISR, ya que usa interrupciones para su funcionamiento. La función micros() funciona dentro de ISR pero después de 1-2 ms se empieza a comportar de forma extraña. Sin embargo, delayMicroseconds() no usa ningún contador y funcionará correctamente dentro del ISR, aunque se debe evitar su uso para no introducir esperas dentro de una ISR.
- Operaciones complejas: 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.
Variables Volatile
Para poder modificar una variable externa a la ISR dentro de la misma, debemos declararla como “volatile”. Declarar una variable volatile es una directiva para el compilador que le dice que cargue la variable desde la RAM y no desde un registro de almacenamiento, que es una ubicación de memoria temporal. Una variable debe ser declarada volatile siempre que su valor pueda ser cambiado por algo más allá del control de la sección de código en la que aparece, como un subproceso que se ejecuta simultáneamente (lo que, precisamente, hace una interrupción).
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. Al indicar una variable como volatile, el compilador desactiva ciertas optimizaciones, lo que supone una pérdida de eficiencia. Por tanto, solo 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. Es recomendable usar variables globales que sean compartidas por el programa principal y la función de Interrupción.
Gestión de Interrupciones Externas en Arduino
Para configurar una interrupción externa en Arduino se usa la función attachInterrupt(). Esta función tiene tres parámetros:
- interrupt: El número de interrupción a usar. Para saber qué número de interrupción está asociado a un pin, debemos usar la función digitalPinToInterrupt(pin), que convierte un pin digital al número de interrupción equivalente. Esto es el método recomendado por Arduino para pasar este parámetro y favorece el cambio de modelo de placa sin tener que modificar el código.
- ISR: Es el nombre de la función de servicio de interrupción que se va a ejecutar cuando se produzca el evento. Esta función no debe tener parámetros y no devuelve nada.
- mode: Define cuándo debe ser disparada la interrupción. Los modos disponibles para las interrupciones de hardware en Arduino son:
- RISING: Ocurre en el flanco de subida (de LOW a HIGH).
- FALLING: Ocurre en el flanco de bajada (de HIGH a LOW).
- CHANGING: Ocurre cuando el pin cambia de estado (rising + falling).
- LOW: Se ejecuta continuamente mientras el pin está en estado LOW.
Si attachInterrupt() nos permite configurar un pin como interrupción, el método detachInterrupt(interrupt) anula esa configuración, deshabilitando la interrupción específica.
Control Global de Interrupciones
Además, existen funciones para controlar globalmente las interrupciones:
Lea también: ¿Cómo localizar tus XML del SAT?
- interrupts(): Habilita las interrupciones (si antes han debido ser inhabilitadas con noInterrupts()). Equivale a sei().
- noInterrupts(): Deshabilita las interrupciones hasta nueva orden. Equivale a cli().
Por defecto, las interrupciones están habilitadas. Algunas funciones no funcionarán si se deshabilitan las interrupciones, y las comunicaciones entrantes serán ignoradas; también podrán modificar ligeramente la temporización del código. Es importante notar que la función delay() no deshabilita las interrupciones, por lo tanto, los datos recibidos por el serial Rx son guardados, los valores PWM funcionan y las interrupciones externas funcionan.
Interrupciones de Cambio de Pin (Pin Change Interrupts)
Aunque el Atmega328p solo tiene dos interrupciones de hardware (INT0 e INT1), los microcontroladores AVR pueden tener una interrupción ante un cambio en cualquier pin, lo que se denomina "pin change interrupt". Estas interrupciones pueden habilitarse en más pines y para los ATmega328, pueden habilitarse en cualquiera de los pines de señal disponibles. Se disparan igual en flancos RISING o FALLING, pero depende del código de la interrupción configurar el pin que recibe la interrupción y determinar qué ha pasado.
Las interrupciones de cambio de pin están agrupadas en 3 puertos de la MCU, por lo tanto, hay 3 vectores de interrupciones para todo el conjunto de pines. Si se necesita más pines para interrupciones, hay un mecanismo para generar una interrupción cuando se cambia cualquier pin en uno de los puertos de 8 bits. No se identifica el pin específico, sino solo en qué puerto ocurrió el cambio. Si se genera una interrupción cuando se cambia uno de los pines ADC0 a ADC5 (utilizado como entrada digital), en ese caso, se llama a la rutina de servicio de interrupción ISR (PCINT1_vect).
Para configurar estas interrupciones, los indicadores de habilitación de interrupción de cambio de pin deben ajustarse en el registro PCICR. Estos son los bits PCIE0, PCIE1 y PCIE2 para los grupos de pines PCINT7..0, PCINT14..8 y PCINT23..16 respectivamente. Los pines individuales se pueden activar o deshabilitar en los registros PCMSK0, PCMSK1 y PCMSK2. En el circuito Arduino, en combinación con la hoja de datos Atmel Atmega328, se puede ver que el pin PCINT0 corresponde al pin 0 en el puerto B (llamado PB0), que es el pin 8 en el Arduino Uno.
También disponemos de librerías como la PinChangeInt Library, que implementa interrupciones de cambio de pin para el entorno Arduino. Esta biblioteca fue diseñada para el Arduino Uno / Duemilanove y funciona bien en el Nano.
Interrupciones por Temporizador (Timer Interrupts)
Hasta ahora hemos visto cómo utilizar interrupciones en Arduino mediante eventos externos usando los pines de entrada. Pero ¿qué pasa si queremos hacer algo de forma periódica? Para este tipo de caso, cuando necesitas ejecutar algo de forma periódica, Arduino tiene también un mecanismo de interrupciones utilizando el Timer. Los Timers son contadores que cuentan pulsos en función de un origen de tiempos (timebase). El timer consiste en un contador digital de 8-bit o 16-bit.
Una placa Arduino estándar tiene tres timers (Arduino Mega tiene 6) que gestionan las tareas. Tomando como ejemplo un Arduino UNO, este tiene tres temporizadores llamados timer0, timer1 y timer2. Cada uno de los temporizadores tiene un contador que se incrementa en cada ciclo del reloj del temporizador. Las interrupciones del temporizador se activan cuando el contador alcanza un valor específico, almacenado en el registro correspondiente. Una vez que el contador de un temporizador alcanza este valor, se borrará (se pone a cero) en el siguiente ciclo del reloj del temporizador, luego continuará contando hasta el valor de coincidencia de comparación nuevamente. Al elegir el valor de coincidencia y configurar la velocidad a la que el temporizador incrementa el contador, puedes controlar la frecuencia de las interrupciones del temporizador.
El reloj interno del Arduino UNO funciona a 16 MHz (Megahercios), esta es la velocidad más rápida a la que los temporizadores pueden incrementar sus contadores. El Timer0 y Timer2 son temporizadores de 8 bits, lo que significa que pueden almacenar un valor de contador máximo de 255. El Timer1 es un temporizador de 16 bits, lo que significa que puede almacenar un valor de contador máximo de 65535. Una vez que un contador alcanza su valor máximo, volverá a cero (esto se llama Overflow o desbordamiento).
Para hacer más cómodo y sobre todo útil el uso de los timers, se puede controlar la velocidad de incremento del contador usando algo llamado preescalador (prescaler). Un prescaler dicta la velocidad de su temporizador de acuerdo con la siguiente ecuación: (VELOCIDAD TIMER (Hz)) = (VELOCIDAD RELOJ (16MHz)) / PRESCALER. Por defecto todos los timers tienen un valor de preescalado de 64. El prescaler puede tener valores como 1, 8, 32, 64, 128, 256 y 1024, entre otros, permitiendo ajustar la frecuencia de las interrupciones.
Aquí se muestra una tabla con los rangos aproximados con los que se puede operar con Timer1 y Timer2 a 16MHz:
| Prescaler | Resolución Timer1 (ms) | Resolución Timer2 (ms) |
|---|---|---|
| 1 | 0.063 | 0.063 |
| 8 | 0.5 | 0.5 |
| 32 | 2 | 2 |
| 64 | 4 | 4 |
| 128 | 8 | 8 |
| 256 | 16 | 16 |
| 1024 | 64 | 64 |
Librerías para el Control de Interrupciones
Aunque se puede tener mucho más control configurando las interrupciones manualmente a través de los registros del microcontrolador, la plataforma Arduino busca facilitar el desarrollo. Para simplificar el manejo de interrupciones, existen varias librerías que pueden ser de gran ayuda.
- PinChangeInt Library: Esta librería implementa interrupciones de cambio de pin para el entorno Arduino, siendo muy útil para aquellos pines que no soportan interrupciones externas directas (INT0, INT1, etc.).
- Librería TimerInterrupt: Es una librería muy completa para interrupciones por temporizador, que incluye diferentes ejemplos en los que te podrás basar para implementar interrupciones periódicas de manera más sencilla.
Estas librerías facilitan el uso de un mecanismo avanzado como las interrupciones, permitiendo un desarrollo más rápido y eficiente en Arduino.
