Cómo hacer multitarea con Arduino
¿Es posible la multitarea en Arduino?
Digamos que tienes un proyecto de Arduino en el que quieres ejecutar varias acciones al mismo tiempo: leer datos de una entrada de usuario, hacer parpadear algunos LEDs, monitorizar un potenciómetro, etc. Así que, básicamente quieres hacer algunas multitareas con Arduino.
Y ahí es donde las cosas se complican un poco, sobre todo si ya estás acostumbrado a generar nuevos hilos cada vez que necesitas iniciar un nuevo programa paralelo.
La verdadera multitarea en Arduino no es posible.
Pero, aún así tenemos buenas noticias: todavía podéis hacer multitarea con Arduino. Todo lo que necesitas es un poco de comprensión para que las cosas funcionen sin problemas.
Antes de explicaros como hacer multitarea con Arduino, veamos por qué no podéis hacer programación paralela «estándar» en primer lugar.
¿Por qué no es posible la multitarea en Arduino?
Tienes que entender la diferencia entre un ordenador como un portátil o un servidor web, y una placa electrónica con un microcontrolador (Arduino).
Un ordenador clásico tiene varios núcleos y toneladas de RAM. Puedes instalar un sistema operativo (como Windows, Ubuntu, Debian, etc.), y generar cientos de tareas. Por ejemplo, cuando inicias Firefox o Chrome, se crean nuevos procesos, por lo que puedes seguir ejecutando todos tus programas mientras lanzas otros nuevos.
Con Arduino, las cosas son completamente diferentes.
El «cerebro» de una placa Arduino es un microcontrolador (ATmega328 para Arduino Uno). Un microcontrolador tiene un solo núcleo, y sólo es capaz de ejecutar una instrucción a la vez.
Así que si decides hacer una pausa dentro de una función, entonces todo tu programa se queda atascado esperando. Cada vez que escribes algo, tienes que pensar en su impacto en todo el código.
Cómo hacer multitarea con Arduino
Si tomas pequeñas acciones y las haces muy rápido, una tras otra, tendrás una sensación de multitarea. Ese es el principio detrás de la multitarea con Arduino.
Vamos a ilustrar esto con una ilusión óptica. Si coges un papel azul y otro rojo, y los alternas muy rápidamente delante de tus ojos (al menos 10 veces por segundo), verás el color púrpura.
Debido a que el cambio entre los colores ocurre tan rápido, tendrás la ilusión de que todos los colores se mezclan en otro color. Eso es más o menos lo que es la multitarea con Arduino, pero a una frecuencia mucho más alta.
Empecemos a hacer multitarea
Habréis oído que Arduino no es realmente potente. Bueno, esto está relacionado con el poder de cómputo global. De hecho, la velocidad de ejecución es todavía bastante alta para el manejo del hardware.
Por ejemplo, el microcontrolador ATmega328 de Arduino Uno tiene una frecuencia de 16MHz.
Para hacer multitarea con Arduino, sigue estos consejos:
- Manten el tiempo de ejecución de todas sus funciones muy corto. No digo que sus funciones deban tener un máximo de x líneas de código. Lo que digo es que deberíais controlar el tiempo de ejecución y aseguraros de que es bastante bajo.
- No uses delay(). Esta función bloqueará completamente tu programa. Como veremos más adelante con un ejemplo de código, hay otras formas de conseguir el mismo comportamiento que con la función delay(). Bueno, esta vale la pena repetirla: no uses delay().
- Nunca te bloquees esperando algo. Si tu programa está escuchando una entrada de usuario, por ejemplo un mensaje de texto a través de la comunicación en serie, entonces significa que no controlas cuándo ocurrirá este evento, porque es de una fuente externa. La forma más fácil de obtener la entrada del usuario es esperar por ella, y luego continuar la ejecución del programa cuando se obtienen los datos. Bueno, no hagas eso. Como veremos más adelante, hay otras formas de mantener la comunicación externa sin bloquearse para el resto del programa.
- Usar algo como una máquina de estados para procesos más largos. Digamos que tienes un proceso que realmente requiere un montón de acciones diferentes, y una cierta cantidad de tiempo para esperar entre 2 acciones. En este caso, es mejor separar este proceso en varias funciones pequeñas (ver el primer punto anterior), y crear una máquina de estados en tu programa principal para llamarlas una por una, cuando sea necesario. Esto te permitirá también computar cualquier otra parte del programa entre 2 pasos del proceso.
Un código de ejemplo
Aquí te mostraremos un ejemplo real usando una tabla de Arduino Uno.
Aquí está el esquema:
En este circuito tenemos:
- 4 LEDs conectados a 4 pines digitales (como salida). Las resistencias son de 220 Ohm.
- 1 pulsador conectado a 1 pin digital (como entrada). La resistencia es de 10k Ohm.
- 1 potenciómetro conectado a 1 pin analógico (como entrada).
- Y (no se muestra) una comunicación en serie a través de un cable USB con un ordenador.
Lo que queremos hacer:
- Hacer parpadear el LED 1 cada segundo.
- Leer la entrada del usuario de la serie (número entre 0 y 255) y escribir los datos en el LED 2.
- Encender el LED 3 si se presiona el botón.
- Encender el LED 4 si el valor del potenciómetro es mayor de 512.
- Imprimir el valor del potenciómetro a través del Serial cada 2 segundos.
Oh, y como has adivinado, haremos todo al mismo tiempo. ¡Eso es lo que se puede llamar un gran programa multitarea!
En el programa tenemos 5 tareas que hacer. Simplemente separaremos el código en pequeños trozos de código, que son muy rápidos de ejecutar. Un pequeño trozo de código para una tarea.
El código
#define LED_1_PIN 9 #define LED_2_PIN 10 #define LED_3_PIN 11 #define LED_4_PIN 12 #define POTENTIOMETER_PIN A0 #define BUTTON_PIN 5 unsigned long previousTimeLed1 = millis(); long timeIntervalLed1 = 1000; int ledState1 = LOW; unsigned long previousTimeSerialPrintPotentiometer = millis(); long timeIntervalSerialPrint = 2000; void setup() { // put your setup code here, to run once: Serial.begin(9600); pinMode(LED_1_PIN, OUTPUT); pinMode(LED_2_PIN, OUTPUT); pinMode(LED_3_PIN, OUTPUT); pinMode(LED_4_PIN, OUTPUT); pinMode(BUTTON_PIN, INPUT); } void loop() { // put your main code here, to run repeatedly: unsigned long currentTime = millis(); // task 1 if(currentTime - previousTimeLed1 > timeIntervalLed1) { previousTimeLed1 = currentTime; if (ledState1 == HIGH) { ledState1 = LOW; } else { ledState1 = HIGH; } digitalWrite(LED_1_PIN, ledState1); } // task 2 if (Serial.available()) { int userInput = Serial.parseInt(); if (userInput >= 0 && userInput < 256) { analogWrite(LED_2_PIN, userInput); } } // task 3 if (digitalRead(BUTTON_PIN) == HIGH) { digitalWrite(LED_3_PIN, HIGH); } else { digitalWrite(LED_3_PIN, LOW); } // task 4 int potentiometerValue = analogRead(POTENTIOMETER_PIN); if (potentiometerValue > 512) { digitalWrite(LED_4_PIN, HIGH); } else { digitalWrite(LED_4_PIN, LOW); } // task 5 if (currentTime - previousTimeSerialPrintPotentiometer > timeIntervalSerialPrint) { previousTimeSerialPrintPotentiometer = currentTime; Serial.print("Value : "); Serial.println(potentiometerValue); } }
Desglosemos el código paso a paso para que puedas entender de qué estamos hablando.
Código de configuración
#define LED_1_PIN 9 #define LED_2_PIN 10 #define LED_3_PIN 11 #define LED_4_PIN 12 #define POTENTIOMETER_PIN A0 #define BUTTON_PIN 5 unsigned long previousTimeLed1 = millis(); long timeIntervalLed1 = 1000; int ledState1 = LOW; unsigned long previousTimeSerialPrintPotentiometer = millis(); long timeIntervalSerialPrint = 2000;
Para mayor claridad, hemos usado algunas #define para usar nombres en lugar de números para todos los pines de hardware. También hemos declarado algunas variables para llevar la cuenta del tiempo.
void setup() { // put your setup code here, to run once: Serial.begin(9600); pinMode(LED_1_PIN, OUTPUT); pinMode(LED_2_PIN, OUTPUT); pinMode(LED_3_PIN, OUTPUT); pinMode(LED_4_PIN, OUTPUT); pinMode(BUTTON_PIN, INPUT); }
Como sabéis, la función setup() se llama primero en Arduino. Aquí sólo inicializamos la comunicación en serie y establecemos el modo correcto para los pines digitales (los pines analógicos no requieren una configuración, ya que se establecen automáticamente como pines de entrada).
void loop() { // put your main code here, to run repeatedly: unsigned long currentTime = millis();
Y… ¡saltamos justo en la función loop()! Esta función será llamada una y otra vez, durante todo el tiempo que dure tu programa.
Lo primero que hacemos aquí es obtener el tiempo actual con millis(). Esto es muy importante. Para la mayoría de las subtareas del programa, usaremos algunas técnicas de seguimiento del tiempo para desencadenar una acción, y así evitaremos usar la función delay().
Tarea 1: Parpadear el LED 1 cada segundo
// task 1 if(currentTime - previousTimeLed1 > timeIntervalLed1) { previousTimeLed1 = currentTime; if (ledState1 == HIGH) { ledState1 = LOW; } else { ledState1 = HIGH; } digitalWrite(LED_1_PIN, ledState1); }
Aquí calculamos la duración entre la hora actual y la última vez que disparamos el LED 1. Si la duración es mayor que el intervalo que hemos establecido previamente (1 segundo aquí), ¡podemos realmente hacer la acción!
Observa que cuando entramos en el bloque if, fijamos la hora anterior como la hora actual. De esta manera, le decimos al programa : «no vuelvas aquí antes de que haya pasado el siguiente intervalo de tiempo».
El uso de esta estructura de código es bastante común en Arduino. Si no estás familiarizado con esyo, tómate el tiempo de escribir el código y prueba algunos ejemplos por ti mismo. Una vez que entiendas como funciona, lo usarás en todos tus programas de Arduino.
Tarea 2: Leer la entrada de usuario del Serial (número entre 0 y 255) y escribir los datos en el LED 2
// task 2 if (Serial.available()) { int userInput = Serial.parseInt(); if (userInput >= 0 && userInput < 256) { analogWrite(LED_2_PIN, userInput); } }
Necesitamos «escuchar» el Serial para poder obtener la entrada del usuario. En lugar de bloquear, bueno, sólo llamamos al método Serial.available() cada vez que estamos en la función principal loop().
Como todos los demás bloques de código son bastante pequeños y rápidos, podemos esperar que el Serial sea monitoreado con bastante frecuencia. De esta manera, estamos seguros de que no nos perdemos ningún dato, mientras realizamos cualquier otra acción en el lateral.
Tarea 3: Encender el LED 3 si se presiona el botón
// task 3 if (digitalRead(BUTTON_PIN) == HIGH) { digitalWrite(LED_3_PIN, HIGH); } else { digitalWrite(LED_3_PIN, LOW); }
Este es bastante sencillo. Sólo monitoreamos el botón cada vez que ejecutamos la función loop() (Este código podría ser mejorado con una función debounce).
Tarea 4: Encender el LED 4 si el valor del potenciómetro es mayor de 512
// task 4 int potentiometerValue = analogRead(POTENTIOMETER_PIN); if (potentiometerValue > 512) { digitalWrite(LED_4_PIN, HIGH); } else { digitalWrite(LED_4_PIN, LOW); }
Igual que para el botón, sólo leemos el valor y actualizamos un LED dependiendo de ese valor. Obsérvese que usamos analogRead() para el potenciómetro, y el valor que obtenemos está entre 0 y 1024 (el convertidor analógico de Arduino tiene una resolución de 10 bits, y 2^10 = 1024).
Tarea 5: Imprimir el valor del potenciómetro vía Serial cada 2 segundos
// task 5
if (currentTime - previousTimeSerialPrintPotentiometer > timeIntervalSerialPrint) {
previousTimeSerialPrintPotentiometer = currentTime;
Serial.print("Value : ");
Serial.println(potentiometerValue);
}
Al igual que en la tarea 1, calculamos la diferencia de tiempo (duración) para comprobar si podemos ejecutar la acción o no. Esta es una buena alternativa a la función delay().
Y aquí, en lugar de disparar un LED, sólo enviamos un valor con comunicación en serie.
Bueno, parece que todo el código está corriendo muy rápido, y si construyes este circuito y ejecutas este programa, tendrás una verdadera sensación multitarea.
La multitarea finalmente no es tan difícil
Empieza a ser más fácil, ¿no?
Cada vez es lo mismo. Una vez que sabes cómo crear un pequeño bloque de código para que corra muy rápido (también puedes poner este código en una función), todo lo que necesitas hacer es repetir esta estructura para cada paso del complejo proceso que quieras hacer. No tiene por qué ser complicado.
Algunas otras formas de hacer multitarea
El método que hemos explicado es muy eficiente y muchos lo usamos personalmente en nuestros proyectos de Arduino.
También hay otras formas de «falsificar» la multitarea. Entre ellas:
Interrumpir las funciones
Algunos pines de Arduino, no todos, soportan la interrupción del hardware. Básicamente, se crea una función que es activada por un pulsador u otro actuador en un pin de hardware.
Cuando se dispara la interrupción, el programa se interrumpe, y su función se ejecuta. Una vez que la función ha terminado, el programa continúa donde estaba. Por supuesto, su función debe ser muy rápida, para que no detenga el «proceso» de ejecución principal por mucho tiempo.
Esto puede ser genial para ejecutar algún código dependiendo de algunas entradas externas, y asegurarte de no perder ninguna entrada si la frecuencia de la señal entrante es muy alta.
Como ejemplo, mira la tarea 3 del código de ejemplo anterior. Monitoreamos el botón pulsador tirando de su estado muy rápido. Aquí también podríamos usar interrupciones para activar el LED cada vez que se presiona el botón.
Aunque ten cuidado con eso porque hay un gran inconveniente: no puedes predecir cuándo se activará la función.
Es como el funcionamiento de una notificación en tu teléfono. No sabes cuándo recibirás las notificaciones push, pero puedes elegir revisar manualmente las notificaciones pull.
Las interrupciones de Arduino son como las notificaciones push. Puede ocurrir en cualquier momento.
Si eliges no utilizar las interrupciones, tendrás que comprobar manualmente (pull) la entrada para ver si puedes desencadenar la acción. La única diferencia es que con Arduino, si haces un «pull» para una notificación, y la notificación desaparece, no la verás. Pero esto no es necesariamente algo malo: si no necesitas ser extra-preciso cuando se ha disparado un pin, puedes tirar del estado cuando quieras. En este caso, tienes un control total sobre cuando compruebas la entrada.
Protothreads
Protothreads es una biblioteca de C pura. Requiere más conocimientos y es más compleja de manejar para los principiantes y los programadores de nivel medio.
Con Protothreads también puedes «fingir» el multiproceso para sistemas basados en eventos, por lo que es bastante útil para programas de Arduino más complejos.
Aunque, debes tener en cuenta que el uso de Protothreads no es en absoluto obligatorio. Puedes escribir un programa multitarea completo con los consejos básicos que te hemos dado antes en este post.
No te olvides de mantener las cosas simples
Cómo has visto la multitarea con Arduino puede llegar a ser bastante simple. Puede que te sientas abrumado por todas las cosas que lees sobre multitarea, y cómo debería ser una «cosa compleja de manejar».
Aunque de hecho, siguiendo algunas reglas bastante simples, puedes llegar bastante lejos con Arduino. Y cuando estés programando, recuerda siempre: lo simple es mejor. No quieres escribir cosas complicadas.
Debe estar conectado para enviar un comentario.