Arduino y las interrupciones: primera aproximación

Cuando un programa tiene que esperar a que ocurra un evento (p.ej. que se pulse un interruptor), tiene dos formas de hacerlo:

  • polling: comprobar el estado del interruptor. Si se activa, hacer lo que sea, y mientras no se active, volver a comprobar. Es fácil de implementar, pero presenta el inconveniente de que, hasta que no se active el interruptor, el código se quedará estancado haciendo siempre lo mismo y sin poder hacer otra cosa.
  • interrupciones: ir a hacer otra cosa (mismamente a dormir, para ahorrar energía), y cuando ocurra el evento que le avisen. El aviso se hace mediante una interrupción: el micro recibe una señal en una patilla y acto seguido ejecuta una función que previamente se la ha indicado.

El uso de interrupciones a veces es conveniente, a veces no, a veces es imprescindible y a veces puede ser horrible, ya que se puede recibir una interrupción en casi cualquier momento (p.ej. mientras estamos procesando otra interrupción).

Un ejemplo con Arduino. El software.

Hay formas de evitarse estos problemas, pero de momento, voy a presentar el uso básico de las interrupciones, y además lo haré desde la perspectiva del IDE de Arduino: con las directrices de Wiring, y no manipulando los registros del AVR.

En este ejemplo, primero configuramos el microcontrolador para hacer algo cuando se reciba una interrupción determinada:

const unsigned short pinInt0=2;
void setup()
{
pinMode(pinInt0, INPUT);
Serial.begin(9600);
attachInterrupt(0,INT0_vector,RISING);
}

Lo más interesante es esto:

interrupción puede ser 0 o 1 para, respectivamente, INT0 o INT1 en Arduino Duemilanove. Arduino Mega dispone de otras cuatro interrupciones más (INT2..5). INT0 se corresponde con el pin digital 2, e INT1 con el 3.

función es la función que se ejecutará cuando se active tal interrupción. A veces se llama rutina de servicio de interrupción (Interruption Service Routine, ISR)

modo es qué se entiende por activación de la interrupción: cuando pasa a estar alta (RISING), cuando cambia de valor (CHANGE), cuando está baja (LOW) o cuando pasa a estar baja (FALLING).

Una vez ejecutemos esa función, cada vez que la patilla digital 2 (INT0) suba de nivel se ejecutará la función INT0_vector:

void INT0_vector()
{
moehren++;
}

que lo que hace es incrementar una variable global:

volatile unsigned short moehren=0;

Está declarada como volatile, para evitar que el compilador la mantenga en los registros del micro. Cada vez que se vaya a utilizar se leerá su valor de memoria, y cada vez que se escriba, se escribirá su valor en memoria.

Y mientras se reciben interrupciones, pues vigilamos a ver si cambia el valor:

void loop()
{
static unsigned short oldMoehren=999;
if(oldMoehren!=moehren) {
Serial.print(“Ich hab neue Moehren gekriegt!! Sie sind schon “);
Serial.println(moehren);
oldMoehren=moehren;
}
delay(600);
}

Si el valor anterior (oldMoehren) es distinto al actual, se manda un mensaje y se actualiza su valor. En cualquier caso, a continuación se espera durante 600ms.

Acabando el ejemplo: el circuito.

Una resistencia (la mía es de 5,6K porque la tenía a mano, pero una de 10K, 1K o valores similares también son válidas) conectada a 5V por un lado y a un interruptor al otro.
El otro extremo del interruptor va conectado a GND. Así, cuando se cierre el interruptor, circulará corriente por la resistencia y por el interruptor.
La resistencia causa una caída de tensión de los 5V. de diferencia que hay, por lo que, cuando el interruptor está abierto, ambos extremos de la resistencia están a 5V. ya que no circula corriente.
Cuando el interruptor se cierra, circula corriente y los 5V. caen en la resistencia, por lo que en el extremo de la resistencia que va conectado al interruptor el voltaje es 0V.
Si finalmente conectamos un cable desde ese punto al pin 2 (correspondiente a INT0), el resultado es que cada vez que el interruptor se abra, el voltaje en ese punto vuelve a subir de 0V. a 5V., pasando la línea INT0 de bajo a alto y lanzando la función INT0_vector().

Y finalmente, la prueba

Su ejecución con el circuito:

Ich hab neue Moehren gekriegt!! Sie sind schon 0
Ich hab neue Moehren gekriegt!! Sie sind schon 1
Ich hab neue Moehren gekriegt!! Sie sind schon 2
Ich hab neue Moehren gekriegt!! Sie sind schon 3
Ich hab neue Moehren gekriegt!! Sie sind schon 6
Ich hab neue Moehren gekriegt!! Sie sind schon 7
Ich hab neue Moehren gekriegt!! Sie sind schon 9
Ich hab neue Moehren gekriegt!! Sie sind schon 10
Ich hab neue Moehren gekriegt!! Sie sind schon 11
Ich hab neue Moehren gekriegt!! Sie sind schon 13
Ich hab neue Moehren gekriegt!! Sie sind schon 15
Ich hab neue Moehren gekriegt!! Sie sind schon 16

Cada vez que se suelta el interruptor, la patilla 2 vuelve a valor alto y se ejecuta la función, incrementando la variable moehren. Posteriormente en la siguiente ejecución de loop() se detectará el cambio de valor y se notificará por el puerto serie.

¿Qué ocurre si hay dos subidas de nivel antes de que se ejecute loop() de nuevo? Pues que el valor que se notifica se habrá incrementado en dos unidades, una por cada subida.

Arduino

Comments (0)

Permalink

Las variables volátiles

Son aquellas variables cuyo valor siempre que se lee, se lee de memoria, y siempre que se escribe, se escribe en memoria. Pasan el mínimo tiempo posible en los registros del microprocesador.
Al declararlas volátiles se le está indicando al compilador que no haga optimizaciones que eviten accesos a memoria para leerlas/escribirlas.

En un código como éste:

moehren+=7;
moehren*=2;

El compilador podría optimizar y generar código que:

  1. lee el valor de la variable de la pila (p.ej. x<-[moehren]) y lo guarda en un registro del micro
  2. suma 7 al registro (x+7)
  3. multiplica el registro por 2 ((x+7)*2)
  4. guarda el registro en la posición de la pila dedicada a la variable moehren ((x+7)*2->[moehren])

Hasta aquí todo en orden, el valor de mohren==(mohren+7)*2 y el número de accesos a memoria se minimiza, lo que (casi) siempre es de agradecer.

Y ahora vamos a suponer, que mientras una sentencia y otra, se recibe una interrupción y se ejecuta entre medias una sentencia:

moehren-=8;

El orden de ejecución sería como sigue:

  1. moehren+=7;
  2. moehren-=8;
  3. moehren*=2;

Con las optimizaciones del compilador esto queda:

  1. lee el valor de la variable (x<-[moehren]) de la pila y lo guarda en un registro del micro
  2. suma 7 al registro (x+7)
  3. salta a la ISR (rutina de servicio de la interrupción, que es, sencillamente, una función que se ejecutará en el medio)
  4. guarda los registros en la pila
  5. lee el valor de la variable de la pila (y<-[moehren]) y lo guarda en un registro del micro
  6. le resta 8 a ese registro (y-8)
  7. guarda ese registro en la posición de la pila dedicada a la variable moehren (y-8 -> [moehren])
  8. restaura los registros guardados en la pila y retorna a donde estaba antes de saltar a la ISR
  9. multiplica el registro por 2 ((x+7)*2)
  10. guarda el registro en la posición de la pila dedicada a la variable moehren ((x+7)*2->[moehren])

Es decir, que el valor de moehren==(moehren+7)*2, como si la ISR no se hubiera ejecutado.

Si por el contratio declaramos moehren como volatile:

volatile int moehren;

Cada vez que se lea/escriba esa variable, se accederá a memoria, lo cual es más lento, pero nos evita situaciones como la anterior.

El orden de ejecución del código generado en un caso similar al ejemplo anterior sería algo así:

  1. lee el valor de la variable (x<-[moehren]) de la pila a un registro
  2. suma 7 al registro (x+7)
  3. guarda el registro en la variable moehren (x+7 -> [moehren])
  4. salta a la ISR
  5. guarda los registros en la pila
  6. lee el valor de la variable (y<-[moehren], o sea y==x+7) de la pila a un registro
  7. le resta 8 a ese registro (y-8, que es lo mismo que x+7-8)
  8. guarda el registro en la variable moehren (y -> [moehren], o bien x+7-8 -> [moehren])
  9. restaura los registros guardados en la pila y retorna a donde estaba antes de saltar a la ISR
  10. lee el valor de la variable (z<-[moehren], z==x+7-8) de la pila a un registro
  11. multiplica el registro por 2 (z*2, (x+7-8)*2)
  12. guarda el registro en la variable moehren (z -> [moehren], o bien (x+7-8)*2 -> [moehren])

El valor que tiene ahora moehren==(moehren-1)*2. Ahora sí que se nota la ejecución de la ISR.

Ojo, porque con esto no se evita la necesidad de las primitivas de sincronización. Nada garantiza que dos procesos no accedan paralelamente y de forma no atómica a la variable en memoria. Si estamos trabajando en un entorno monotarea (un microcontrolador, normalmente), esto nos dará igual.

Lenguajes de Programación

Comments (0)

Permalink