Cuando empiezas con este micro, en Arduino, no eres consciente que el micro está usando un entorno FreeRTOS. ¡Si tenemos un micro con dos núcleos y con FreeRTOS podemos tener multitarea! De hecho, si miramos el archivo main.cpp del core de Arduino podemos ver que la función loop() es realmente una tarea de FreeRTOS: aquí.
Podéis ver con más detalle el funcionamiento de FreeRTOS en su web: https://www.freertos.org/ pero muy básicamente lo que hace es crear tareas (funciones) que parecen ejecutarse simultáneamente. Y realmente dos tareas en diferentes núcleos se ejecutan simultáneamente.
Una tarea es una función que nunca retorna, veamos un ejemplo de blink muy sencillo:
#include <Arduino.h>
const int pin = 2;
void tarea(void* parametro){
int pin = *(int*)parametro;
pinMode(pin,OUTPUT);
while(true){
digitalWrite(pin,HIGH);
vTaskDelay(500 / portTICK_PERIOD_MS);
digitalWrite(pin,LOW);
vTaskDelay(500 / portTICK_PERIOD_MS);
}
}
void setup() {
xTaskCreate(tarea,"miPrimeraTarea",1000,(void*)&pin,10,NULL);
}
void loop() {
}
La tarea se crea en setup() con xTaskCreate() a la que se pasan los siguientes parámetros:
- La función de la tarea.
- El nombre de la tarea.
- · La memoria del HEAP que vamos a reservar para la tarea.
- Un parámetro que se pasará a la función de la tarea en forma de puntero vacío (void*) para recibir parámetros. En el ejemplo se usa para pasar el pin sobre el que va a hacer los destellos, si no necesitara parámetro/s pasaríamos un puntero nulo (NULL). Se pasa un puntero (void*) para poder usar cualquier tipo, estructura, etc. debemos hacer un cast en la llamada y para recogerlo dentro de la función.
- La prioridad. Es un entero entre 0 y configMAX_PRIORITIES – 1, 25 en ESP32. Por lo tanto, 0 es la máxima prioridad y 25 es la mínima.
- Un Puntero del tipo TaskHandle* para recoger el manejador de la tareea. Si no vamos a usarlo pasamos NULL
En las tareas de FreeRTOS, antes de entrar en el bucle infinito, se suele inicializar los objetos que se va a usar la tarea. En este ejemplo simplemente recogemos el pin y lo asignamos a una variable local de la función.
Después entra en el bucle infinito while(true) que ejecuta las temporizaciones con vTaskDelay.
vTaskDelay le indica al planificador de tareas de FreeRTOS que ponga en suspensión la tarea un número de ticks determinado. Es como decirle al planificador “no necesito atención durante 500 ticks, no vuelvas a ejecutarme hasta que pase ese tiempo”, la tarea queda suspendida durante 500 ticks. ¿Pero qué medida de tiempo es el tick? Es el tiempo que usa el planificador para pasar de una a otra tarea. Se usan interrupciones disparadas por un timer del micro para generar el tick. Como este tiempo puede cambiar de uno a otro port (versiones de FreeRTOS para distintos micros) hay una macro que define cuantos milisegundos es un tick. Por eso se divide 500 por portTICK_PERIOD_MS, para que sean milisegundos. En el esp32 portTICK_PERIOD_MS vale 1, no haría falta dividir, aun así se pone para mantener la portabilidad del código a otros ports.
Esto está muy bien, tenemos a nuestro led destelleando con una frecuencia de 1Hz y no nos tenemos que preocupar de llamar a ninguna función, ya se encarga FreeRTOS.
Vamos a darle una vuelta de tuerca a la función blink.
Tal como está ahora para hacer blink en otro pin necesita definir otra tarea, otra función, con su tratamiento especifico de sus variables, que algunas tendrán que ser globales, etc. Podemos pensar que quizás podemos construir una clase que contenga una función que haga las veces de tarea. Lo primero que se nos ocurrirá es que la función de la tarea sea una función miembro de la clase… no tardaremos en darnos cuenta de que no podemos obtener el puntero a la función miembro de una clase a menos que sea estática, y si es estática, es la misma función para todas las instancias de la clase…
Hay varias soluciones y librerías en internet que abordan este problema, usar FreeRTOS con CPP. Para este ejemplo nos crearemos una clase simple para derivar de ella nuestras tareas. FreeRTOS lleva además un montón de utilidades en su librería para manejar el paso de información entre tareas como colas y controlar las tarea, semáforos y mutex. Como no los vamos a usar de momento la solución es sencilla:
Lo primero es definir una clase que llamaré Hilo de la que derivaremos las clases que harán de tarea. Como tiene poca sustancia lo pongo todo en Hilo.h:
#ifndef __HILO_RTOS__
#define __HILO_RTOS__
#include <FreeRTOS.h>
#define MAXIMA_PRIORIDAD 0
#define MEDIA_PRIORIDAD ((configMAX_PRIORITIES)/4)
#define POCA_PRIORIDAD (((configMAX_PRIORITIES)/4) + MEDIA_PRIORIDAD)
#define MINIMA_PRIORITY (configMAX_PRIORITIES - 1)
#define _1KB (1024)
class Hilo{
public:
Hilo(uint32_t stackDepth, UBaseType_t priority, const char* name=""){
strcpy(nombre,name);
xTaskCreate(task, name, stackDepth, this, priority, &this->taskHandle);
}
~Hilo(){
if(taskHandle){
vTaskDelete(taskHandle);
}
}
virtual void loop() =0;
protected:
static void task(void* _params ){
Hilo* p = static_cast<Hilo*>( _params );
p->loop();
}
TaskHandle_t taskHandle = NULL;
char nombre[configMAX_TASK_NAME_LEN];
};
#endif //__HILO_RTOS__
Lo de siempre, se envuelve con la macro #ifndef HILO_RTOS para evitar que se incluya varias veces. Después defino algunas otras macros, cuatro tipos de prioridades, que después nos será útil cuando tengamos varias tareas y queramos dedicar más tiempo de proceso a unas que a otras.
En la parte public:
En el constructor de la clase se recogen los parámetros que nos hace falta para crear la clase.
El nombre de la tarea lo guardamos en la variable nombre y llamamos a la función xTaskCreate, recogemos el puntero al manejador en taskHandle y le pasamos el puntero a la función task() de la propia clase.
En el destructor de la clase, si la función se creó correctamente el manejador será distinto de NULL, en ese caso, destruimos la tarea con vTaskDelete.
La siguiente función es un poco más extraña:
virtual void loop() = 0;
Es una función virtual. Si hay una función virtual en una clase, esa clase de convierte en abstracta y no se puede instanciar objetos directamente de ella. Por ejemplo, NO podemos hacer:
Hilo miHilo(1000,10,”prueba”);
El compilador se quejará. Las clases abstractas las tenemos que usar derivando de ellas y en la clase derivada definir las funciones virtuales. Es como un contenedor vacío.
Más adelante veremos cómo derivar la clase Hilo.
Las funciones/variables definidas en la parte protected se pueden alcanzar desde las clases derivadas. Aquí tenemos el puntero del manejador, el nombre de la tarea y una función estática que es la que hace la magia.
static void task(void* _params ){…
Esta función, como he dicho antes, es la misma para todas las instancias de Hilo y es la que hemos pasado en el constructor a la función xTaskCreate. Es por aquí donde entramos a nuestra función en la clase derivada, dentro de la función task encontramos:
Hilo* p = static_cast<Hilo*>(_params);
Marea un poco la definición, pero lo que se hace aquí es obtener el puntero a la clase derivada en p y después con:
p->loop();
¡¡Ejecutamos la función de la clase derivada, la que no es virtual!!
Con esto tenemos un contenedor para tareas. Vamos a usarlo con una clase que haga destellos en un led, pero con clase, quiero decir, con clases.
Led.h (1.71 KB)
Led.h (1.71 KB)
TAREASCPP.ino (340 Bytes)
Hilo.h (890 Bytes)