FREERTOS en ESP32/ESP8266 (multi tarea)

Hoy os traigo un tutorial acerca de cómo utilizar FREERTOS para generar tareas en ESP32 y ESP8266. ¿Qué es FREERTOS?:, pues es un sistema operativo multitarea… Quizá algunos os preguntaréis cómo es posible teniendo en cuenta que no se parece al sistema operativo de tu ordenador. En el siguiente punto os explico qué es un sistema operativo y entenderéis por qué.

¿Qué es un sistema operativo?

Un sistema operativo es el núcleo de código encargado de gestionar el hardware de forma independiente de los programas. Por ejemplo, Windows se encarga de que hacer que tu pongas un juego y funcione sea cual sea el hardware que tiene la computadora. También se encarga de la gestión de ciclos de CPU, lo cual permite que puedas ejecutar varios programas de forma simultánea además de multitud de servicios de fondo, haciendo que te parezca que todo se ejecuta al mismo tiempo. Por supuesto, con los ordenadores multinúcleo modernos esto además es cierto, ya que se encarga de distribuir las tareas en los núcleos.

¿Qué me ofrece FREERTOS?

La diferencia que tiene FREERTOS es que es multitarea, y eso es lo interesante de él. En un Arduino por ejemplo puedes hacer tareas muy pequeñas, gestionarlas con milis e ir intercalándolas. El problema de este método es que no es multitarea real, por lo que si una tarea tarda 200ms, esta tarea bloqueará al resto hasta que termine.

Que FREERTOS sea multitarea real se debe a que es capaz de partir las tareas en unidades mucho más pequeñas. Esto le permite intercalar las tareas a un nivel más bajo y aprovecha mejor los tiempos muertos. Esto hace que si tienes una tarea de 200ms y otra de 50ms, estas se vayan ejecutando simultáneamente sin que la de 200ms retrase la de 50ms. Además, en microcontroladores como ESP32 con más de un núcleo, se encarga de distribuir las tareas entre los diferentes núcleos y permite acelerarlas. Esto es útil, pero hay que usarlo con precaución ya que el ESP32 utiliza el núcleo 0 para las tareas básicas como el WiFi, bluetooth… Estas tareas son críticas y pueden dar lugar a que el microcontrolador se reinicie si se interrumpen o no se ejecutan a tiempo (excepciones). Es por ello que es mejor poner las tareas en el núcleo 1.

Para verlo de una forma más gráfica, en un sistema operativo que no es multitarea la ejecución de dos tareas es la siguiente:

Si usamos un sistema operativo multitarea como FREERTOS para ejecutar esas mismas tareas, veremos que la ejecución se realiza de la siguiente forma:

Seguramente te preguntarás que si tarda lo mismo, para qué te sirve que intercale las tareas. La respuesta fácil te la di realmente antes, y es que ayuda a evitar que una tarea larga paralice al resto. Además de esto también ayudar a gestionar los tiempos muertos, ya que en el mundo real muchas tareas no usan la CPU de forma continua y realizan muchas esperas. Por ejemplo, si estás esperando a que se pulse una techa, puedes poner un loop while que provoque que se quede en espera, y esto no afectará al resto de tareas.

Otro ejemplo es una descarga HTTP, la cual es una tarea lenta y que tiene muchos tiempos de espera mientras se comunica con el servidor remoto. Este tiempo de espera puede ser de 100ms si es rápido, pero puede prolongarse fácilmente varios segundos si el servidor es lento o la conexión mala. Esto en un SO sin multitarea bloquearía el microcontrolador durante ese tiempo, pero con FREERTOS se aprovecharían esos tiempos muertos para ejecutar otras tareas.

Resumen

En resumen, FREERTOS ofrece las siguientes mejoras:

  • Slots de tiempo pequeños para ejecutar tareas «en paralelo»
  • Las tareas se mantienen separadas y protegidas
  • Comunicación entre tareas
  • Se pueden compartir recursos comunes como pantallas, el WiFi,…
  • Reacción rápida a las interrupciones
  • Ejecución basada en prioridades
  • Se ejecuta en muchas arquitecturas, por lo que su uso no se limita al ESP32 y el ESP8266

FREERTOS me parece muy complicado o no lo necesito, quiero algo más fácil

FREERTOS es muy útil para paralelizar tareas y gestionar mejor la CPU de tu ESP32. Si en tu caso no necesitas este nivel de gestión o te parece muy complejo, siempre puedes usar un Task Scheduler. En este post hablo de Arduino, pero es igualmente compatible con ESP32 y ESP8266. La diferencia entre FREERTOS y este programador de tareas, es que este último no intercalará partes de las tareas y sólo se ejecutará en un núcleo.

Comenzando con FREERTOS

FREERTOS va incorporado en nuestro microcontrolador ESP32 y ESP8266 y este lo usa principalmente para gestionar las tareas básicas del microcontrolador. Esto nos permite usarlo sin problema sin necesidad de preocuparnos de añadir cabeceras ni librerías para usarlo. Lo primero que aprenderemos es a crear una tarea simple, la cual es la unidad más simple para trabajar con FREERTOS.

Funciones básicas

Creando una tarea

Las tareas en FREERTOS se generan a través de funciones. Dichas funciones deben tener un retorno del tipo void, y un argumento del tipo puntero a void. Además, estas funciones deben contener un loop for o while que evite que terminen nunca, o la función vTaskDelete para eliminar la tarea al terminar, sino no funcionarán correctamente. Esto nos permite poner código antes del loop, el cual se ejecutará solo una vez como en la función setup, lo cual nos ayudará a tener un código más limpio.

void TaskBlink(void *pvParameters) {
  // initialize digital LED_BUILTIN on pin 13 as an output.
  pinMode(LED_BUILTIN, OUTPUT);

  for (;;) { // A Task shall never return or exit.
    digitalWrite(1322, !led_status);   // turn the LED on (HIGH is the voltage level)
    led_status = !led_status;
  }
}

En esta tarea simple podemos ver que ponemos el pin del LED interno como salida, y vamos cambiando de estado en cada ejecución. Como puedes ver no es diferente a lo que haces con Arduino normalmente, por lo que hasta aquí es fácil jejeje.

Lo siguiente que veremos es cómo programarla, para lo cual podemos dejar a FREERTOS se encargue de la gestión, o podemos vincularla a un núcleo concreto. Esto es muy útil para algunas tareas que si se cambian de núcleo fallan, o para evitar molestar a las tareas básicas en nuestro ESP32.

Crear tarea libre (xTaskCreate)

Una tarea libre le indicará a FREERTOS que puede ponerla en el núcleo que quiera, por lo que él se encargará de gestionar dónde ponerla. Por supuesto esto es en el caso de microcontroladores multinúcleo como ESP32, pero no para el ESP8266.

Para crear una tarea libre simplemente ejecutaremos la función xTaskCreate de la siguiente forma:

xTaskCreate(
  TaskFunction, // Function to call
  "Taskname", // Name for this task, mainly for debug
  1024, // Stack size
  NULL, // pvParameters to pass to the function
  1, // Priority 
  NULL // Task handler to use
);

Como puedes observar su uso es muy simple también, por lo que no tienes excusa para no usarlo. Los argumentos que recibe esta función son los siguiente en orden:

  • Función que ejecutara esta tarea
  • Nombre de esta tarea, principalmente para tareas de depuración. La longitud máxima es de 13 caracteres por defecto, pero se puede controlar con configMAX_TASK_NAME_LEN.
  • Tamaño de pila para la función, utilizada para el intercambio de datos.
  • Parámetros que se le pasarán a la función
  • Prioridad que tendrá la función, siendo 0 la menor prioridad y la máxima prioridad estará configurada por configMAX_PRIORITIES – 1. Las tareas de mayor prioridad serán ejecutadas primero si en algún momento dos o más están listas para ser ejecutadas.
  • Handler de la tarea que nos permitirá trabajar con ella. Puede ser NULL si la tarea es permanente, o una variable del tipo TaskHandle_t para poder gestionar esta tarea. Este parámetro es la dirección de memoria de dicha variable, por lo que hay que incluir el carácter ampersand antes de la variable (&variable).

Crear tarea vinculada a un núcleo (xTaskCreatePinnedToCore)

Otra forma que tenemos para crear tareas, es crearlas vinculadas a un núcleo. Esto es útil para funciones que siempre tienen que ejecutarse en el mismo núcleo (algunas sino crashean), o si queremos evitar que se ejecute por ejemplo en el núcleo 0 e interfiera con las funciones básicas. Para hacerlo el procedimiento será el mismo que para las tareas libres, sólo que usaremos la función xTaskCreatePinnedToCore y le indicaremos el núcleo donde se ejecutará.

xTaskCreatePinnedToCore(
  TaskFunction, // Function to call
  "Taskname", // Name for this task, mainly for debug
  1024, // Stack size
  NULL, // pvParameters to pass to the function
  1, // Priority 
  NULL, // Task handler to use
  1 // Core where to run
);

Los argumentos son exactamente los mismos que para la creación de una función libre, con la diferencia de que añadimos uno más que será el núcleo donde se ejecutará.

Eliminar una tarea (vTaskDelete)

Si por alguna razón una tarea que hemos creado ya no nos es útil o deseamos que sólo se ejecute una vez, podemos proceder a eliminarla. Para ello utilizaremos la función vTaskDelete, y le pasaremos como argumento el handler de la tarea (TaskHandle_t).

/*
 * Simple example to create and destroy a task by Daniel Carrasco (https://www.electrosoftcloud.com)
 */

TaskHandle_t TaskHandle_1;

#define LED_BUILTIN 1
bool led_status = false;

void setup() {
  // Create a new task pinned to core 1.
  // Note the handler TaskHandle_1 passed as argument
  xTaskCreatePinnedToCore(
    TaskBlink,
    "Taskname",
    1024,
    NULL,
    1,
    &TaskHandle_1,
    1
  );

  // Delete the previously created task
  vTaskDelete(TaskHandle_1);
}

void loop() {
  // put your main code here, to run repeatedly:

}

void TaskBlink(void *pvParameters) {
  // initialize digital LED_BUILTIN on pin 13 as an output.
  pinMode(LED_BUILTIN, OUTPUT);

  for (;;) { // A Task shall never return or exit.
    digitalWrite(LED_BUILTIN, led_status);
    led_status = !led_status;
  }
}

Por supuesto este ejemplo no funcionará debido a que tan pronto la crea, la elimina, pero os servirá para entender el concepto y saber hacerlo.

Esta función se puede utilizar desde la propia tarea, lo cual nos permite limitar su ejecución a una o varias veces. En este caso no hará falta que tengamos un handler de tarea, ya que FREERTOS entiende que el comando se está realizando sobre la tarea actual. Por ejemplo, si queremos ejecutar la tarea sólo una vez lo haríamos de la siguiente forma:

/*
 * Simple example to create and destroy a task by Daniel Carrasco (https://www.electrosoftcloud.com)
 */

#define LED_BUILTIN 1
bool led_status = false;

void setup() {
  // Create a new task pinned to core 1.
  xTaskCreatePinnedToCore(
    TaskBlink,
    "Taskname",
    1024,
    NULL,
    1,
    NULL,
    1
  );
}

void loop() {
  // put your main code here, to run repeatedly:

}

void TaskBlink(void *pvParameters) {
  // initialize digital LED_BUILTIN on pin 13 as an output.
  pinMode(LED_BUILTIN, OUTPUT);

  for (;;) { // A Task shall never return or exit.
    digitalWrite(LED_BUILTIN, led_status);
    led_status = !led_status;

    // Delete the task
    vTaskDelete(NULL);
  }
}

En este caso la diferencia es que la tarea se ejecutará al menos una vez y luego se destruirá, por lo que el led se quedará encendido.

Pausa una tarea (vTaskDelay)

Seguramente te habrás fijado en que las tareas de FREERTOS necesitan tener un loop permanente para que nunca terminen hasta ser eliminadas. Por ello la ejecución será continua y sin espera, lo cual posiblemente no nos interese muchas veces. Muy probablemente habrás pensado en la función delay para pausar la ejecución de tus tareas, pero te diré que es mala idea. Básicamente porque la función delay genera una pausa a base de tener el microcontrolador ocupado, lo cual es contraproducente en sistemas multitarea como FREERTOS.

Para pausar nuestras tareas en FREERTOS tendremos que utilizar la función vTaskDelay, la cual se diferencia principalmente porque en lugar de mantener el microcontrolador ocupado hasta que termina la pausa devuelve el control al SO, el cual aprovecha este tiempo en otras tareas. Pasado el tiempo de espera, FREERTOS continua la ejecución de la función desde el punto en el que se pausó.

Para utilizar esta función lo tendremos que hace desde la tarea que queremos pausar, y el único argumento que requiere son los «ticks» que quieres esperar. Estos «ticks» no corresponden al tiempo en ms, sino que es la cantidad de divisiones de tiempo realizadas por FREERTOS que queremos esperar. Estas divisiones suelen ser de 10ms en ESP32, pero para asegurarnos dividiremos el tiempo que deseamos esperar entre portTICK_PERIOD_MS (el cual es el tiempo que dura cada «tick» en ms). También podemos usar la macro pdMS_TO_TICKS() para sacar el número de ticks correspondientes a un tiempo en ms, por ejemplo pdMS_TO_TICKS(1000).

Por ejemplo, si queremos simular el ejemplo «blink» típico, utilizaremos el siguiente código:

/*
 * Simple blink example by Daniel Carrasco (https://www.electrosoftcloud.com)
 */

#define LED_BUILTIN 1
bool led_status = false;

void setup() {
  // Create a new task pinned to core 1.
  xTaskCreatePinnedToCore(
    TaskBlink,
    "Taskname",
    1024,
    NULL,
    1,
    NULL,
    1
  );
}

void loop() {
  // put your main code here, to run repeatedly:

}

void TaskBlink(void *pvParameters) {
  // initialize digital LED_BUILTIN on pin 13 as an output.
  pinMode(LED_BUILTIN, OUTPUT);

  for (;;) { // A Task shall never return or exit.
    digitalWrite(LED_BUILTIN, led_status);
    led_status = !led_status;

    // Delay the task
    vTaskDelay( pdMS_TO_TICKS(500) );
  }
}

Como puedes observar, se añade una pausa de 500ms en la ejecución, y cuando termina esa pausa continua con normalidad.

Pausar una tarea hasta (vTaskDelayUntil)

Esta función nos ayudará a mantener la regularidad en la ejecución de una tarea independientemente del tiempo de ejecución que tenga la misma. Esto es muy útil si ejecutamos tareas cuya duración es variable, pero queremos que se ejecuten cada x tiempo independientemente de lo que tarden. Si usásemos la función vTaskDelay el tiempo de espera sería el mismo independientemente de lo que dure la función.

Esta función recibe como argumentos el «tick» en el que se empezó a ejecutar la tarea (por lo que tendremos que recuperarlo primero), y cuántos ticks queremos esperar. Ella se encargará de hacer el cálculo de los ticks que han pasado desde que comenzó la ejecución, y pausará la ejecución los ticks restantes.

Para recuperar el tick actual usaremos una variable del tipo TickType_t y la función xTaskGetTickCount(). Con ello, la función de blink quedaría mejorada con esta función de la siguiente manera:

/*
 * Simple example to create and destroy a task by Daniel Carrasco (https://www.electrosoftcloud.com)
 */

#define LED_BUILTIN 1
bool led_status = false;

void setup() {
  // Create a new task pinned to core 1.
  xTaskCreatePinnedToCore(
    TaskBlink,
    "Taskname",
    1024,
    NULL,
    1,
    NULL,
    1
  );
}

void loop() {
  // put your main code here, to run repeatedly:

}

void TaskBlink(void *pvParameters) {
  // initialize digital LED_BUILTIN on pin 13 as an output.
  pinMode(LED_BUILTIN, OUTPUT);

  // Set wait time (in ticks)
  TickType_t xLastWakeTime;
  const TickType_t xFrequency =  pdMS_TO_TICKS(500);
  

  for (;;) { // A Task shall never return or exit.
    // Get the actual execution tick
    xLastWakeTime = xTaskGetTickCount();

    // Switch the led
    digitalWrite(LED_BUILTIN, led_status);
    led_status = !led_status;

    // Wait exactly 500ms from execution start
    vTaskDelayUntil( &xLastWakeTime, xFrequency );
  }
}

Esta función es muy útil si queremos mantener una ejecución estable de nuestra tarea.

Abortar la pausa de una tarea (xTaskAbortDelay)

Si por alguna razón necesitamos que se cancele la pausa de una tarea, podremos usar la función xTaskAbortDelay para hacerlo. Esta función recibirá el handler de la tarea como argumento. Esto nos es muy útil en casos que por ejemplo tengamos una tarea en espera hasta que otra llegue a cierto punto.

Suspender una tarea (vTaskSuspend)

Otra función bastante útil es la de suspender una tarea. Esta función nos permitirá pausar una tarea de forma indefinida sin llegar a eliminarla como se hace con xTaskDelete, por lo que esta tarea no usará tiempo de procesador y nos permitirá resumirla cuando queramos.

Un ejemplo de uso es el que hago yo por ejemplo con una tarea que requiere de Wifi. La tarea se crea en la función setup, y nada más crearse se suspende. Dicha tarea se mantiene en este estado hasta que el callback de conexión WiFi la activa de nuevo, momento en el que comienza su ejecución. Si por alguna razón se desconecta el WiFi otra función callback se encarga de suspender la tarea de nuevo. De este modo te ahorras el gastar ciclos de reloj verificando si el WiFi está conectado.

Esta función se puede utilizar pasándole el handler de la tarea que deseamos pausar, o pasándole NULL como argumento si la tarea que queremos pausar es desde la que se está lanzando.

Iniciar tarea suspendida en FREERTOS

FREERTOS no acepta un argumento para indicarle que la tarea no se lance nada más crearla, por lo que tendremos que suspenderla al iniciarla. Esto por suerte es muy fácil y se podrá hacer poniendo la función vTaskSuspend al principio de la función, por ejemplo:

void MyTaask(void *pvParameters) {
  // Pause the task just at startup.
  vTaskSuspend(NULL);

  for (;;) { // A Task shall never return or exit.
    ...
  }
}

Resumir una tarea (vTaskResume)

Pausar una tarea y no resumirla no es muy útil, así que como habrás podido deducir, esta función se encarga de resumir una tarea suspendida. Su funcionamiento es como el de la función vTaskSuspend y los argumentos también, pero la diferencia es que obviamente no se podrá ejecutar desde dentro de la propia tarea, ya que al estar esta pausada, nunca se ejecutará. Para usar esta función necesitaremos crear un handler para dicha tarea siempre, o sino no seremos capaces de resumirla.

Forzar cambio de contexto (taskYIELD):

Esta función es muy importante para trabajar de forma correcta con FREERTOS, sobre todo en microcontroladores de un sólo núcleo como el ESP8266. Se encarga de decirle a FREERTOS que deseas parar la ejecución de la tarea en ese punto y continuar con otra tarea, lo cual es muy importante cuando sólo tienes un núcleo y compartes dicho núcleo con el WiFi y el bluetooth (sino puede causar excepciones y reinicios incontrolados). Esta función no detiene la ejecución de la tarea, sino que simplemente cede el turno a la siguiente tarea.

Recuperar el handler de una tarea (xTaskGetHandle)

Otra función de la que disponemos para gestionar nuestras tareas, es la de recuperar el handler de una tarea a través del nombre que la hayamos puesto. Esto es útil, pero a la vez se desaconseja su uso, principalmente porque tarda mucho en recuperar el handler. Como argumentos sólo necesita el nombre que le pusimos a la tarea en el momento de crearla.

Colas en FREERTOS

Otro punto importante en FREERTOS son las colas. Las colas son buffers del tipo FIFO (First In First Out), que nos permitirán intercambiar variables entre las diferentes tareas que tengamos. Además, opcionalmente nos permite mantener la tarea pausada hasta que se reciba algún dato, lo cual es bastante útil.

Además de lo dicho arriba, nos puede servir para crear una aplicación multitarea con una misma función. Para ponernos en contexto, supongamos que tienes 1000 palabras que tienes que enviar a un servidor web una por una. Si la función de envío tarda 100ms, la ejecución total tardará en torno a 100s. Si paralelizamos la misma función y utilizamos las colas para gestionar qué palabras se han enviado, podremos reducir ese tiempo.

Las colas pueden gestionar distintos tipos de objeto, permitiendo crear un struct para enviar varios objetos a la vez. Un ejemplo de uso de struct podría ser si tenemos un acelerómetro, y queremos enviar las variables X, Y y Z al mismo tiempo.

Crear una cola en FREERTOS (xQueueSend)

Con la función xQueueCreate podremos crear una cola para intercambiar datos entre nuestras tareas. Esta función requiere dos argumentos, los cuales son el tamaño de la cola y el tamaño del objeto en ese orden.

// Create a queue of 10 object, with a size of uint32_t
QueueHandle_t xQueue1 = xQueueCreate( 10, sizeof( uint32_t ) );

Si por alguna razón la cola no ha sido creada, esta función devolverá 0 e indicará que no puede ser usada.

// Create a queue of 10 object, with a size of uint32_t
QueueHandle_t xQueue1 = xQueueCreate( 10, sizeof( uint32_t ) );

if( xQueue1 == 0 ) {
  // Queue was not created and must not be used.
}

Enviar un objeto a esta cola (xQueueSend)

Con esta función podremos enviar objetos al final de una cola que hemos creado, los cuales tienen que ser del tipo indicado a la hora de crear la cola. Esta función recibe como argumentos la cola a la que queremos enviar el objeto, un puntero hacia el objeto y cuánto tiempo quieres esperar a que haya espacio en al cola (recuerda que el tamaño es limitado). Este último argumento al igual que todos los medidores de tiempo en FREERTOS, se mide en ticks.

Como retorno, esta función devuelve pdTRUE si el mensaje ha sido enviado correctamente, o errQUEUE_FULL si la cola está llena

// Create a queue of 10 object, with a size of uint32_t
QueueHandle_t xQueue1 = xQueueCreate( 10, sizeof( uint32_t ) );
uint32_t ulVar = 10UL;

if( xQueue1 != 0 ) {
  // Queue is correct
  if (xQueueSend( xQueue1, ( void * ) &ulVar, ( TickType_t ) 0 ) == pdTRUE) {
    // The message was sent sucessfully
  }
}

Recuperar un objeto de la cola (xQueueReceive)

Por último os hablaré de la forma de recuperar los mensajes enviados a la cola. Para ello usaremos la función xQueueReceive, la cual recibe como argumentos el la cola de la que queremos recuperar le mensaje, la dirección de memoria de la variable donde guardaremos dicho mensaje, y el tiempo de espera en ticks para recibir un mensaje si la cola está vacía. Recalcar que la variable la tenemos que crear antes y tiene que ser un puntero, ya que esta función apuntará dicho puntero al objeto de la cola. Esta función devolverá psTRUE si se realizó correctamente, o pdFALSE si no pudo recuperar la variable.

// Create a queue of 10 object, with a size of uint32_t
QueueHandle_t xQueue1 = xQueueCreate( 10, sizeof( uint32_t ) );
uint32_t ulVar = 10UL;

if( xQueue1 != 0 ) {
  // Queue is correct
  if (xQueueSend( xQueue1, ( void * ) &ulVar, ( TickType_t ) 0 ) == pdTRUE) {
    // The message was sent sucessfully
  }

  uint32_t * rVar;
  if( xQueueReceive( xQueue1, &( rVar ), ( TickType_t ) 10 ) ) {
    // rVar now points to the uint32_t ulVar variable posted
    // by xQueueSend.
  }
}

Semáforos y Mutex

No podemos olvidarnos de los semáforos en FREERTOS, lo cual nos permitirá gestionar la sincronía de las tareas a lo largo de la ejecución cuando ambas tienen que acceder a un recurso protegido. Como su nombre indica, permiten pausar y continuar la ejecución de la tarea, en función de si dicho semáforo está en uso.

Por poner un ejemplo, si dos tareas tienen que acceder al mismo recurso compartido como por ejemplo una variable o una pantalla, nos permitirá bloquear la ejecución en una de ellas si la otra ya está accediendo a dicho recurso, evitando comportamientos erróneos.

Para usarlos simplemente crearemos un semáforo con la función xSemaphoreCreateMutex, y luego usaremos las funciones xSemaphoreTake y xSemaphoreGive.

xSemaphoreCreateMutex

Esta función creará un semáforo, el cual podremos usar más adelante para bloquear recursos. Devolverá el semáforo, o NULL si ocurrió un error.

SemaphoreHandle_t xSemaphore = xSemaphoreCreateMutex();

xSemaphoreTake

Bloquea un semáforo, esperando si ya está bloqueado el tiempo que le indiques en ticks. Esta función devuelve pdTRUE si se bloqueó el semáforo correctamente, o pdFALSE si se acabó el tiempo de espera y no fue posible bloquearlo.

xSemaphoreTake( xSemaphore, ( TickType_t ) 10 )

xSemaphoreGive

Libera un semáforo bloqueado anteriormente por xSemaphoreTake. Como argumento se le pasa el semáforo que queremos liberar, y devuelve pdTRUE si se liberó correctamente, o pdFALSE si no fue posible.

xSemaphoreGive( xSemaphore )

Ejemplo

SemaphoreHandle_t xSemaphore = NULL;

// A task that creates a semaphore.
void vATask( void * pvParameters )
{
   // Create the semaphore to guard a shared resource.
   vSemaphoreCreateBinary( xSemaphore );
}

// A task that uses the semaphore.
void vAnotherTask( void * pvParameters )
{
   // ... Do other things.

   if( xSemaphore != NULL )
   {
       // See if we can obtain the semaphore.  If the semaphore is not available
       // wait 10 ticks to see if it becomes free.
       if( xSemaphoreTake( xSemaphore, ( TickType_t ) 10 ) == pdTRUE )
       {
           // We were able to obtain the semaphore and can now access the
           // shared resource.

           // ...

           // We have finished accessing the shared resource.  Release the
           // semaphore.
           xSemaphoreGive( xSemaphore );
       }
       else
       {
           // We could not obtain the semaphore and can therefore not access
           // the shared resource safely.
       }
   }
}

Convertir parte del código en crítico en FREERTOS

Por último hablaré de cómo convertir parte de código de una tarea en crítico. Esto ayudará a que FREERTOS lo identifique como no pausable, y por lo tanto la ejecución se realice de forma continua sin intercalar tareas entre medias. Como habrás deducido, esto convierte esa parte del código en bloqueante y detiene el resto de tareas, por lo que es importante no abusar y por supuesto intentar que la parte del código sea lo más corta posible. Además como recordarás, en el ESP8266 el núcleo es compartido con las tareas del WiFi y el Bluetooth, por lo que puede llegar a ser más problemático abusar de ellas y puede provocar el reinicio del microcontrolador por una excepción.

Para convertir una parte del código como crítica simplemente la incluiremos entre las macros taskENTER_CRITICAL y taskEXIT_CRITICAL:

taskENTER_CRITICAL();
// Critical code to run
...
taskEXIT_CRITICAL();

Tamaño de pila

A la hora de crear una tarea en FREERTOS es muy importante tener en cuenta el tamaño de pila. Este tamaño indica cuánta memoria se guardará para esta parte del código, y es muy importante tenerlo en cuenta porque si es muy pequeño provocará que el microcontrolador se resetee, y si es demasiado grande desaprovecharemos memoria. Por desgracia no existe una fórmula clara para definir qué tamaño de pila debemos poner. Yo sugiero empezar por 1024, e ir multiplicando por 2 cada vez si vemos que nuestro microcontrolador se reinicia.

Recientemente tuve problemas con una función porque hacía que se reiniciara el microcontrolador, y el problema era precisamente este. Tras subir el tamaño de pila a 16384 ya funciona correctamente.

Más información

FREERTOS es un sistema complejo y que incluye muchas más funcionalidades como por ejemplo las interrupciones. Me es imposible realizar una entrada cubriendo todo, por lo que me he centrado en lo básico y más útil. Si te ha gustado y quieres saber más, no dudes en pasarte por la documentación de espressif:

https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/system/freertos.html

Espero que te haya gustado esta entrada y como siempre, no dudes en comentar (siempre comentarios constructivos), o compartirla si te parece que va a ser útil.

¡Un saludo!

1 comentario en «FREERTOS en ESP32/ESP8266 (multi tarea)»

Deja un comentario

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