19 diciembre 2012

Manejo de Interrupciones con Arduino

El artículo anterior conectamos un simple display de 7 segmentos a nuestro Arduino y programamos un sencillo contador. Pero... ¿Qué tal si queremos interactuar con eventos externos?

Una de las formas comunes de interactuar con cosas que suceden "fuera" de nuestro control es por ejemplo monitorear el estado de las líneas. Dentro de nuestro "loop" principal podemos leer continuamente el valor de una línea de entrada y ejecutar por medio de una condicional un código especifico en caso de que detectemos un cambio.

El problema de este método es que el código se vuelve sumamente complejo cuando tenemos que monitorear muchas cosas o cuando nuestro loop principal simplemente tiene que hacer cosas que toman demasiado tiempo.

En esta entrada vamos a comprender como utilizar las interrupciones externas del Arduino. Esto nos será de mucha utilidad para interactuar ya sea con el usuario o con otros dispositivos de hardware que emitan "señales" que nos permitan saber que un evento ha sucedido.

Antes de continuar preparen su display de 7 segmentos porque en esta entrada lo vamos a re-utilizar.


Una cosa a la vez...


Los Arduino basados en los microcontroladores Amtel AVR pueden ejecutar una sola secuencia de instrucciones a la vez. El problema de esto es por ejemplo, si ustedes están enviando números a su display y luego el usuario presiona un botón. Si nuestro ciclo toma mucho tiempo en ejecutarse puede que pase mucho tiempo antes de que podamos leer la línea de nuestro usuario y perdamos el evento, o tal vez simplemente estamos ocupando un ciclo en hacer algo interesante. Esto es una "desventaja" de microcontroladores sencillos y es algo con lo que las computadoras han tenido que convivir prácticamente desde que fueron inventadas.

¿Como manejar eventos externos?


Afortunadamente los diseñadores de microcontroladores pensaron en este problema hace bastante tiempo y de ahí surgieron las famosas "interrupciones". Las interrupciones (omitiendo muchos detalles) funcionan de manera muy sencilla: Existe una lista de eventos internos o externos que genera ya sea el microcontrolador o una línea externa y al detectarse estos eventos el microcontrolador detiene la secuencia de comandos que está ejecutando. Luego de esto, llama al código definido previamente que se encarga de "manejar" esa interrupción en específico y luego, cuando ha terminado de "atender la interrupción" regresa a ejecutar la secuencia principal que había dejado pausada.

Tal vez no es una solución perfecta pero nos permite "atender" cosas que consideramos importantes por ejemplo cuando un dispositivo externo envía datos vía serial o cuando una línea de interrupción cambia su valor de 0 a 1 o viceversa.

Hablando un poco más sobre las interrupciones


No hay un "estándar" de como se manejan o cuales on las interrupciones disponibles. Cada fabricante implementa diferentes tipos de interrupciones dependiendo del microcontrolador que han diseñado y el uso que se le piensa dar. La ventaja con los Arduinos es que como comparten un microprocesador común los desarrolladores han creado unas bibliotecas que permiten manejar interrupciones de una manera muy sencilla.


Nota aclaratoria: Las funciones de manejo de interrupciones de la referencia del Arduino son muy, pero muy básicas. Los microprocesadores Amtel AVR tienen una larga lista de diferentes tipos interrupciones disponibles. Sin embargo el manejar estas interrupciones no resulta "tan trivial" y depende mucho de las características de cada microprocesador en particular. Para aplicaciones más avanzadas, si desean más información de como programar estas interrupciones, pueden revisar la documentación de AVR Libc en la sección dedicada al manejo de interrupciones.

En Arduino podemos asociar el código de las interrupciones a través de la función attachInterrupt(). Las interrupciones o eventos que podemos asociar a las líneas mediante esta función en el Arduino Mega son las siguientes:
  1. Cuando el nivel lógico de la línea es 0 (LOW)
  2. Cuando el nivel lógico de la línea cambia independientemente de su estado lógico (CHANGE)
  3. Cuando el nivel lógico cambia de 0 a 1 (RISING)
  4. Cuando el nivel lógico cambia de 1 a 0 (FALLING)
Antes de continuar, vamos a hacer un pequeño circuito que nos permita cambiar los valores lógicos de las líneas de interrupción:


Al circuito anterior se le conoce como "resistencia pull-up", su función es mantener  un nivel lógico en la compuerta aunque el circuito se encuentre abierto. Para nuestro caso mantendrá un 1 lógico permanente con el pulsador abierto y un 0 lógico cuando esté cerrado.

Luego, para capturar las interrupciones la tenemos un poquito más fácil, simplemente replicamos este circuito para cuantos interruptores querramos capturar y lo conectamos a las líneas correspondientes. Para este ejemplo utilizaremos las líneas 18, 19 y 20 que corresponden a las interrupciones 3, 4 y 5 del Arduino.

Esta es una pequeña foto de como se ven los pulsadores armados en la breadboard:

Notarán que uso resistencias de 10K. Usualmente se recomienda el uso de resistencias de 4.7K, lástimosamente no tenía a la mano a la hora de armar el circuito, sin embargo, en la práctica no hay demasiada diferencia ya que su único trabajo es evitar que cuando presionemos el pulsador se produsca un corto circuito.

Programando las Interrupciones


Lo primero que tenemos que hacer es definir algunas constantes que nos indiquen las lineas que utilizaremos como interrupciones, además de algunas variables "volatiles". En general, utilizaremos el modificador "volatile" para indicarle al compilador que nuestra variable podría ser accedida desde distintas ubicaciones en el código de nuestro programa. Esto obliga a asignar espacio de RAM a nuestra variable en vez de guardarla en la memoria de datos.

// Botones
const int BTN1 = 18;
const int BTN2 = 19;
const int BTN3 = 20;

// Display 7-Segmentos
const int D0 = 4;
const int D1 = 5;
const int D2 = 6;
const int D3 = 7;

// Contador
volatile int inc;

// Encendido y Apagado del display
volatile int onOff;

// Tiempo en que se realizo la última interrupción.
volatile long lastInt;

Las constantes de nombre "BTN" representan mis botones y las constantes "D" las líneas a donde tengo conectado el decodificador a 7 segmentos.

Definiendo las funciones que modificarán los valores


Vamos a definir un par de funciones "ayudantes" extra que se encargarán de incrementar y decrementar el contador.
// Incrementa y escribe el número al display
void increment() {
  inc++;
  inc = (inc < 10) ? inc : 0;
  writeTo7Seg(inc);
}

// Decrementa y escribe el número al display
void decrement() {
  inc--;
  inc = (inc >= 0) ? inc : 9;
  writeTo7Seg(inc);
}

El siguiente paso es definir el código que se encargará de manejar las interrupciones, vamos a realizar funciones muy simples, un botón incrementara el contador, el otro lo disminuira y el último encenderá y apagará el display de 7 segmentos. Con las funciones anteriores el código se vuelve muy fácil de entender:
// Codigo para la interrupcion 5
void int5() {
    increment();
}

// Codigo para la interrupcion 4
void int4() {
    decrement();
}

// Codigo para la interrupcion 3
void int3() {
    onOff = onOff^1;
    if(onOff) {
      writeTo7Seg(15);
    } else {
      writeTo7Seg(inc);
    }
}

Por defecto no hay ningún código asignado para manejar las interrupciones, por ello es necesario que definamos la asociación de las interrupciones en el "setup".
void setup() {
  // Establecemos las líneas del display como salida
  pinMode(D0,OUTPUT);
  pinMode(D1,OUTPUT);
  pinMode(D2,OUTPUT);
  pinMode(D3,OUTPUT);
  
  // Inicializamos nuestras variables
  inc = 0;
  lastInt = 0;
  onOff = 1; //Displa encendido al encender
  
  // Asociamos la interrupción a nuestro código
  attachInterrupt(5,int5,RISING); // línea 18
  attachInterrupt(4,int4,RISING); // línea 19
  attachInterrupt(3,int3,RISING); // línea 20
  
  //Colocamos un 0 en nuestro display de 7 segmentos
  writeTo7Seg(inc);
}

Tal vez en este momento se estén haciendo la siguiente pregunta: ¿Por qué uso RISING y no otro tipo de interrupción? La respuesta a esta pregunta realmente tiene mucho que ver con lo que estemos programando y como querramos que funcione. En este caso, quería que el contador se incrementara cuando el usuario dejara de presionar el botón, como nuestro botón genera un 0 lógico cuando esta presionado y un 1 lógico cuando esta sin presionar si queremos incrementar nuestro contador justo despues de que se ha presionado el botón el evento RISING es el más adecuado. Si en cambio estuvieramos programando algo como un control de arcade posiblemente deberíamos usar la interrupción FALLING para que el usuario no perciba un "retardo" a la hora de presionar el botón y ver una respuesta en el juego.

¿Qué hacemos luego con el loop principal?

Pues nada, lo dejamos vacío ya que no lo utilizaremos en este ejemplo:
// Loop vacío
void loop() {
}

Bonus: Eliminando el Rebote vía software.

Si toman el código que puse anteriormente junto con el circuito que coloqué, se van a dar cuenta que el contador funciona de manera errática como se muestra en el siguiente video: 

Este no es un problema de software sino más bien de hardware. Es el famoso "rebote" de interruptores

Uno de los grandes problemas al trabajar con circuitos digitales e interruptores, es que los interruptores simplemente no son perfectos. Dependiendo del diseño de los mismos, estos pueden dar lugar a este molésto fenómeno. 

El rebote literamente significa que los contactos dentro del interruptor rebotan brevemente, esto sucede tan rápido que simplemente no nos damos cuenta, pero el circuito digital en vez de recibir un solo pulso recibe varios. 

¿Cómo corregir el rebote?


Existen diferentes métodos vía hardware para eliminación de rebote de interruptores, sin embargo si su aplicación no necesita tiempos de respuesta "críticos". Pueden utilizar esta sencilla solución que les propongo a continuación.

Primero, ¿Recuerdan la variable "lastInt" que definimos al principio? Lo que vamos a hacer para eliminar el rebote es "filtrar" nuestras interrupciones. La lógica que utilizaremos es muy simple, si nuestra interrupción ocurre en menos del tiempo umbral especificado entonces simplemente la ignoraremos.

¿Cuanto es el umbral que deseamos definir? La respuesta no es sencilla ya que depende de las caraterísticas físicas interruptor que estemos usando. En mi circuito, luego de experimentar con un poco de prueba y error, un umbral de 200 milisegundos parece funcionar a la perfección.

Para hacer funcionar este eliminador de rebote por software simplemente vamos a usar una condicional y antes de salir de la función guardaremos el valor de millis() y vamos a comparar para ver si ha pasado tiempo suficiente.

¡¡¡Mucho Cuidado!!! el valor de millis() se reinicia automáticamente aproximadamente cada 70 días. Esto significa que si deciden usar este método y piensan tener funcionando su arduino sin parar durante más de 70 días, tienen que capturar la excepción de reinicio del contador de milisegundos porque si no, llegara un momento que nunca se cumplirá la condición y se ignorarán todas las interrupciones que usen este método. Así que: "advertidos están". 

Una vez dada esta advertencia modifiquemos un poco nuestro código de manejo de interrupciones:
void int5() {
  if((millis()-lastInt)>200) {
    increment();
    lastInt = millis();
  }
}

void int4() {
  if((millis()-lastInt)>200) {
    decrement();
    lastInt = millis();
  }
}

void int3() {
  if((millis()-lastInt)>200) {
    onOff = onOff^1;
    if(onOff) {
      writeTo7Seg(15);
    } else {
      writeTo7Seg(inc);
    }
    lastInt = millis();
  }
}

Nota: Si notan, comparto "lastInt" con todo el código de interrupciones, esto en la práctica significa que no solo estoy eliminando el rebote sino también la posibilidad de presionar dos botones diferentes en menos de 200ms. Una posible solución a este bug es utilizar un marcador de tiempo diferente para cada interrupción (latInt1, lastInt2, lastInt3), de esta manera podríamos eliminar efectivamente el rebote para cada interruptor de manera independiente, sin bloquear la ejecución del código en caso de recibir una interrupción de una línea diferente.

Probando Todo


Pueden descargar el código y cargarlo a su arduino. Si utilizan otra tableta diferente a la Mega solo revisen la lista de interrupciones y ajusten los pines de salida a su conveniencia en el código. 

Si todo funciona bien deberá de funcionar de manera similar a como se muestra en el siguiente video:



Conclusiones

Las interrupciones resultan sumamente útiles para ejecutar código de manera asíncrona, es decir que nuestro código reaccione a eventos externos sin necesidad de estarlos monitoreando continuamente.

En esta entrada he "abusado" un poco de las interrupciones externas. Jugando un poco con circuitos digitales podemos crear una especie de controlador de interrupciones que nos permita monitorear varios eventos utilizando una sola línea de interrupción, sin embargo esto lo podemos dejar para una entrada posterior.

Ya solo nos queda un artículo más para esta serie de tres entradas, en el último artículo leeremos datos de la Raspberry e intentaremos crear una aplicación interesante con nuestro módulo de pruebas. 

No habiendo nada más que decir por ahora... ¡Hasta la próxima!

10 comentarios:

Moises Alm dijo...

Saludos muy bueno el articulo, soy nuevo en el mundo de los micotroladores. Me gustaria saber si las interrupciones se limitan solo a las entredas digitales I/O, por que estoy necesitando una interrrupcion para un ADC. Estoy utilizando un ARDUINO NANO Ver.3. Gracias.

Mario Gómez dijo...

¡Hola Moises!

Según leo en la hoja técnica del ATMega328 (Pag #65), si es posible agregar código de interrupción para detectar el "final de conversión" (22 0x002A ADC ADC Conversion Complete).

Ahora, si lo que quieres es lanzar una interrupción cuando exista un cambio en el voltaje, existe otra interrupción (24 0x002E ANALOG COMP Analog Comparator).

Estas interrupciones no se pueden configurar con la instrucción "attach" de Arduino, tendrías que hacerlo a mano utilizando un código similar al que se muestra en este link: http://www.uchobby.com/index.php/2007/11/24/arduino-interrupts/

Voy a tratar de hacer de hacer tiempo para escribir un tutorial sobre usos avanzados de interrupciones.

¡Saludos!

miguel aguilar dijo...

Buen dia buen articulo pero falta la escritura en 7 segmentos podrias pasarmela para revisar

Homar Lopez dijo...

Hola, gracias por tu aporte, pero me podrias expliccar donde ubico los difrentes codigos? ya esta claro que el loop queda vacio, pero el resto donde va? al inicio? en el setup?? gracias..

Mario Gómez dijo...

¡Hola Homar!

Las interrupciones se ejecutan bajo "demanda" es decir, cuando se recibe un cambio en la línea de la interrupción. Por ello no se colocan en el loop principal.

La forma que Arduino te permite asociar un código específico a una interrupt es la función attachInterrupt donde especificas:

attachInterrupt(,,);

Por ejemplo, digamos que tienes el código siguiente que quieres que se ejecute con el cambio en la interrupción 3:

void cambioInt3() {
Serial.println("Cambio detectado");
}

Para asociarlo al cambio de línea, en el setup tendrías que colocar:

attachInterrupt(3, cambioInt3, FALLING);

FALLING indica que la interrupción se ejecutará cuando el nivel lógico de la interrupción 3 cambie de 1 a 0.

Espero haberte ayudado. ¡Saludos!

Javi dijo...

Hola Moises.
Antes de nada agradecerte esta publicación. A despejado muchas dudas, aunque como pasa siempre, abierto otras.

Te cuento, estoy intentando controlar mediante RC (emisora-receptor) un coche, robot o quad (aun no lo he hecho, estoy en fase aprendizaje), pero he visto que por problemas de tiempo de eventos (lo que tu bien explicas) no puedo utilizar PulseIn(), sino que tengo que utilizar las interrupciones.
Mi preguntas son:

a)Cómo leo 6 canales o entradas con un Arduino Uno que tiene disponibles dos interrupciones

b) No entiendo bien lo de las interrupciones asociadas a un pin. Significa eso que solo puedo hacer la interrupción de ese pin en concreto.

Muchas gracias de nuevo.
Un saludo,

Javi

eduardoz barea escobar dijo...

buenas, tengo una mini pro compatible jy-mcu y supuestamente deberia tener el attachinterrupt0 en el pin 2 pero no me funciona.

he estado buscando informacion pero no encuentro nada.

Se puede asignar el attachinterrupt el pin que yo defina??

muchas gracias

Juan Carlos Durán dijo...

Hola disculpa ¿no sabrás como realizar las interrupciones en un periodo de tiempo?
Por ejemplo, necesito contar cuantas interrupciones se dan en 10 segundos... ¿Tendrás alguna idea o referencia de cómo hacerlo?
Sería de gran ayuda, agradezco de antemano.

Cristian Veloso dijo...

Excelente articulo, justo estaba pensando en redactar un articulo referido a lo mismo.
se les interesa les comparto mis notas

http://www.electrontools.com/2014/08/registro-de-desplazamiento-74hc595.html

espero que les agrade.
saludos!

Laia Fernandez dijo...

Muchísimas Gracias!