Arduino: multithreading

Hoy hablaremos de un tema bastante importante en el mundo de los microcontroladores, así como en el de los microprocesadores: el multithreading, o multihilo, en castellano.

El multithreading en arduino no es posible. Pero como tenemos un buen reloj, en la mayoría de los casos, lo que si podemos hacer es que las subrutinas que queramos ejecutar en “paralelo” se ejecuten de manera concurrente, de modo que “parezca” que realmente estamos ejecutándolas simultáneamente o, como comentábamos, en multithreading.

En Arduino este problema lo asociamos a la expresión “delay vs millis”, es decir, tiempo de espera contra contador de tiempo, por decirlo de alguna manera. Si tenéis cierta experiencia en Arduino sabréis a lo que me refiero. Sino, os lo explico rápidamente.

Un caso de ejemplo muy práctico es el de hacer parpadear un LED cada cierto tiempo, mientras escaneas la entrada, ya sea una pulsación de un botón o un dato… El caso práctico nos dice que, lo sencillo, es hacer que el LED se encienda, esperamos cierto tiempo, lo apagamos y, posteriormente (ojo que hay una coma), realizamos el escaneo del dato de entrada. Bien, en este tiempo de parpadeo nos hemos visto obligados a parar el proceso loop() cierto tiempo (delay()) en pro de poder encender y apagar el LED a nuestro antojo. Si en ese tiempo, hemos tenido una pulsación del botón de entrada, es posible que la hayamos perdido, ya que teníamos el sistema parado!

He aquí el problema: para hacer el parpadeo hemos parado el proceso loop(), cosa que hace que Arduino se duerma durante x milisegundos… Teniendo en cuenta a la velocidad que corre actualmente una placa Arduino (unos 16MHz), podríamos haber tenido unos cuantos ciclos de loop ejecutándose por segundo en los que podríamos haber hecho otras muchas cosas. Pero no, nosotros lo teníamos dormido…

Os muestro el claro ejemplo, el archiconocido Blink:

void loop() {
    digitalWrite(led, HIGH);   // turn the LED on (HIGH is the voltage level)
    delay(1000);               // wait for a second
    digitalWrite(led, LOW);    // turn the LED off by making the voltage LOW
    delay(1000);               // wait for a second
}

Fijáos, hemos detenido el proceso loop durante 2000ms, 1000ms para encender y 1000ms para apagar…

La solución es usar la función millis() en vez de delay(). La idea, no parar la ejecución, e ir preguntando constantemente si se debe apagar o encender el LED en función de si ha pasado el tiempo establecido para cada caso. Y no hay que preocuparse por el constantemente (que suele dar miedo), ya que loop está funcionando, también, constantemente…

Os dejo un código que lo ilustra:

unsigned long previousMillis = 0;  // Tiempo anterior 0
unsigned long timeToAction = 1000; // Tiempo de espera entre acciones, 1000ms

void setup() {
    ...
}

void loop() {
    // Obtenemos el momento actual en ms
    unsigned long currentMillis = millis();

    // Si ha pasado el tiempo establecido, ejecutamos la acción
    if(currentMillis - previousMillis >= timeToAction) {
         // Encendemos o apagamos el LED 
         digitalWrite(led, digitalRead(led)^1); 

        // Almacenamos el último momento en que hemos actuado 
        previousMillis = currentMillis; 
    } 
}

Como vemos, no es una solución difícil de comprender:

  • A cada paso de loop(), obtenemos en tiempo actual de ejecución (función millis()) en la variable currentMillis
  • Comparamos el tiempo actual (currentMillis) con el anterior momento en el que hemos actuado (previusMillis) y, si su diferencia es igual o mayor al tiempo de acción (timeToAction), significa que ha pasado el tiempo establecido y por lo tanto realizamos la acción (encender o apagar el LED)

Poco mas que eso para ver que es posible encender y apagar el LED cada 1000ms sin necesidad de parar el proceso loop(), con lo que podemos hacer otras muchas cosas mientras.

Sólo una cosa a tener en cuenta y es que, aunque seguramente no seremos capaces de verlo a simple vista, a tiempos muy bajos es posible que no seamos realmente exactos en el tiempo de acción, dado que seguramente en el transcurso de ejecutar las sentencias de comparación, hayan pasado algunos pocos ms que quedan fuera del tiempo de ejecución. ¿Esto qué significa? Pues que seguramente en vez de apagar o encender el LED en el milisegundo 1000 lo hagamos en el 1005 o el 1010… Pero, ¿quién puede detectar ese error a simple vista? El ojo humano no… Otra historia es que nuestra aplicación haga uso de un conteo preciso de tiempo… Habría que tenerlo en cuenta, aunque como os comento, para la mayoría de casos, ese tiempo, es negligible…

Espero que haya sido de vuestro interés. En otra ocasión os mostraré cómo manejar tareas de manera concurrente, algo mas complejas.

Jordi

Share Button

Un comentario

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.