Resumen

Este codelab fue creado para estudiar concretamente el freeRTOS, y las principales funciones para programar, crear, eliminar, suspender, reanudar y establecer prioridades de tareas. Con este recurso se espera que usted al finalizar esté en capacidad de:

  • Implementar y gestionar tareas concurrentes en un entorno Arduino utilizando FreeRTOS. Los estudiantes aprenderán a crear, suspender, reanudar y sincronizar tareas, así como a interactuar con periféricos de hardware y entradas seriales.

Fecha de Creación:

2024/03/01

Última Actualización:

2024/03/01

Requisitos Previos:

Adaptado de:

Referencias:

https://esp32tutorials.com/

https://www.digikey.com/en/maker/projects/what-is-a-realtime-operating-system-rtos/28d8087f53844decafa5000d89608016

Escrito por:

Fredy Segura-Quijano

FreeRTOS es un sistema operativo de tiempo real (RTOS) diseñado para Sistemas Embebidos. Permite la ejecución de múltiples tareas en un microcontrolador, proporcionando mecanismos para la gestión del tiempo y la sincronización de tareas. FreeRTOS es ligero y altamente configurable, siendo una opción popular en aplicaciones de Sistemas Embebidos. FreeRTOS es un sistema operativo de código abierto que está diseñado para ejecutarse en microcontroladores, proporcionando una gestión eficiente de recursos y tareas.

FreeRTOS es compatible con una amplia gama de tarjetas de desarrollo y microcontroladores; desde varias familias de las tarjetas STM32, algunas referencias de Microchip, NXP semiconductors, Nordic Semiconductor, entre muchas otras. Una de las más populares actualmente es la ESP32 de Espressif Systems. De hecho el fabricante de la ESP32 confirma que esta tarjeta tiene integrado en su núcleo el FreeRTOS, lo cual quiere decir que este sistema operativo viene preinstalado y configurado para trabajar con el hardware del ESP32 de manera predeterminada. La integración de FreeRTOS en el núcleo del ESP32 ofrece varias ventajas para el desarrollo de aplicaciones, incluyendo:

Otras características importantes de FreeRTOS son:

FreeRTOS es ampliamente utilizado en la industria para aplicaciones como electrodomésticos, automoción, aviónica, dispositivos médicos, y más, donde se requiere confiabilidad, eficiencia y respuesta rápida a eventos en tiempo real. Su naturaleza liviana y su diseño modular lo hacen una opción atractiva para proyectos de sistemas embebidos de todo tipo.

¿Qué es la Unidad de Manejo de Memoria (MMU)?

La Unidad de Manejo de Memoria (MMU, por sus siglas en inglés) es un componente hardware que gestiona la traducción de direcciones de memoria virtual a direcciones físicas. También proporciona protección de memoria, controlando el acceso a las regiones de memoria para evitar que las tareas interfieran entre sí.

Funciones de la MMU:

El ESP32 no tiene una MMU completa como la que se encuentra en un procesador de aplicaciones de gama alta (como en PCs o teléfonos inteligentes). Sin embargo, el ESP32 incluye una MMU básica y una Unidad de Protección de Memoria (MPU) que ofrecen algunas funcionalidades de protección de memoria.

  1. Protección de Memoria Básica: La MPU del ESP32 permite definir regiones de memoria con diferentes permisos de acceso (lectura, escritura, ejecución). Esto se utiliza para proteger la memoria del sistema y evitar que las tareas accedan a regiones de memoria que no les corresponden.
  2. Integración con FreeRTOS: FreeRTOS puede aprovechar la MPU para mejorar la seguridad y la robustez del sistema. FreeRTOS tiene una versión denominada FreeRTOS-MPU que está diseñada específicamente para usar las características de protección de memoria de un MPU.
  3. Configuración y Uso: en sistemas como el ESP32, FreeRTOS puede ser configurado para usar la MPU. Esto implica definir las regiones de memoria y sus permisos en el código de configuración del sistema.
  4. Beneficios: Previene que las tareas accedan a memoria no autorizada, lo cual es crucial para la seguridad del sistema. Ayuda a detectar y prevenir errores de programación, como desbordamientos de pila y accesos a memoria fuera de límites y facilita la detección de errores de memoria, lo que simplifica la depuración del sistema.

Estados de tarea

Una tarea puede existir en uno de los siguientes estados:

Correr (Running): Cuando una tarea se está ejecutando realmente, se dice que está en estado En ejecución. Actualmente está utilizando el procesador. Si el procesador en el que se ejecuta RTOS solo tiene un núcleo, entonces solo puede haber una tarea en estado de ejecución en un momento dado.

Listo (Ready): Las tareas listas son aquellas que se pueden ejecutar (no están en el estado Bloqueado o Suspendido) pero que no se están ejecutando actualmente porque una tarea diferente de igual o mayor prioridad ya está en el estado En ejecución.

Obstruido (Blocked): Se dice que una tarea está en estado Bloqueada si actualmente está esperando un evento temporal o externo. Por ejemplo, si una tarea llama a vTaskDelay(), se bloqueará (se colocará en el estado Bloqueado) hasta que haya expirado el período de retraso: un evento temporal. Las tareas también se pueden bloquear para esperar cola, semáforo, grupo de eventos, notificación o evento de semáforo. Las tareas en el estado Bloqueado normalmente tienen un período de "tiempo de espera", después del cual la tarea expirará y se desbloqueará, incluso si el evento que la tarea estaba esperando no ha ocurrido. Las tareas en el estado Bloqueado no utilizan ningún tiempo de procesamiento y no se pueden seleccionar para ingresar al estado En ejecución.

Suspendido (Suspended): Al igual que las tareas que están en estado Bloqueado, las tareas en estado Suspendido no se pueden seleccionar para ingresar al estado En ejecución, pero las tareas en estado Suspendido no tienen tiempo de espera. En cambio, las tareas solo entran o salen del estado Suspendido cuando se les ordena explícitamente que lo hagan a través de las llamadas API vTaskSuspend() y xTaskResume() respectivamente.

[Posibles estados de las tareas en FreeRTOS.]

Planificador de tareas.

FreeRTOS utiliza un planificador de tareas basado en prioridades conocido como planificación por prioridades fijas con capacidad de prelación (fixed-priority preemptive scheduling). Esto quiere decir:

  1. Prioridades Fijas:
  1. Capacidad de Prelación (Preemptive):
  1. Planificación Round Robin:
  1. Interrupciones y Contexto de Tarea:

Introducción a las tareas de FreeRTOS

En una aplicación en tiempo real o RTOS, una aplicación generalmente consta de un conjunto de tareas o subrutinas independientes. En una MCU de un solo núcleo, solo se puede ejecutar una tarea a la vez. Por otro lado, en un procesador de doble núcleo como ESP32 se pueden ejecutar dos tareas simultáneamente, dado que estas dos tareas no dependen entre sí. El programador en FreeRTOS programa estas tareas según su prioridad, período de tiempo y tiempo de ejecución.

El planeador de FreeRTOS iniciará y detendrá con frecuencia cada tarea mientras la aplicación sigue ejecutándose. Una tarea no comprende la actividad del planeador RTOS, por lo tanto, es responsabilidad del planeador FreeRTOS confirmar que el contexto del procesador (valores de registro, contenido de la pila, etc.) cuando se activa una tarea es exactamente el mismo que cuando se cambió la misma tarea. Para ello, cada tarea cuenta con su propia pila. Cuando se intercambia la tarea, el contexto de ejecución se guarda en la pila de esa tarea, por lo que también se puede restaurar exactamente cuando se vuelve a intercambiar la misma tarea.

Las API de FreeRTOS proporcionan funciones para programar, crear, eliminar, suspender, reanudar y establecer prioridades de tareas. Veamos algunas de estas funciones:

1. xTaskCreate

En FreeRTOS, la creación de tareas es fundamental para aprovechar el potencial del sistema operativo de tiempo real (RTOS). Las tareas permiten la ejecución concurrente de múltiples funciones en un sistema embebido, facilitando el diseño y la gestión de sistemas complejos Esta es la función básica para crear una tarea:

BaseType_t xTaskCreate(
    TaskFunction_t pvTaskCode,
    const char * const pcName,
    configSTACK_DEPTH_TYPE usStackDepth,
    void *pvParameters,
    UBaseType_t uxPriority,
    TaskHandle_t *pxCreatedTask
);

Donde los parámetros son:

2. xTaskCreatePinnedToCore

Esta función es específica del ESP32 y permite crear tareas fijadas a un núcleo específico.

BaseType_t xTaskCreatePinnedToCore(
    TaskFunction_t pvTaskCode,
    const char * const pcName,
    const uint32_t usStackDepth,
    void *pvParameters,
    UBaseType_t uxPriority,
    TaskHandle_t *pvCreatedTask,
    const BaseType_t xCoreID
);

Donde los parámetros son como en la función anterior adicionando el siguiente parámetro:

3. vTaskDelete

Eliminar tareas en FreeRTOS es una operación importante relacionada principalmente con la gestión de recursos del sistema y el control del flujo de la aplicación.

En un sistema embebido con recursos limitados, es crucial gestionar la memoria y otros recursos de manera eficiente. Las tareas en FreeRTOS consumen memoria para su pila y su TCB (Task Control Block). Eliminar tareas que ya no son necesarias libera estos recursos, permitiendo que otras tareas o procesos puedan usarlos. Por ejemplo, si una tarea es responsable de inicializar ciertos periféricos y no se necesita después de la inicialización, eliminar esta tarea liberará memoria y recursos.

Eliminar tareas permite además un control más preciso del flujo de la aplicación. Se pueden crear y eliminar tareas dinámicamente según las necesidades del sistema, lo que proporciona flexibilidad para adaptarse a diferentes estados operativos. Por ejemplo, en un sistema de adquisición de datos, se puede tener una tarea que recopila datos durante un período de tiempo específico. Una vez que el período ha terminado y los datos han sido procesados, se puede eliminar la tarea de recopilación.

Si las tareas no se eliminan correctamente después de completar su trabajo, pueden causar fugas de memoria, lo que eventualmente puede llevar a un agotamiento de la memoria y fallos en el sistema. Además, eliminar tareas inactivas o innecesarias puede mejorar el rendimiento general del sistema al reducir la sobrecarga del planificador y permitir que la CPU se enfoque en tareas activas y críticas. Esta es la función para eliminar una tarea:

void vTaskDelete(TaskHandle_t xTaskToDelete);

Donde los parámetros son:

4. vTaskDelay

En FreeRTOS, la función vTaskDelay se utiliza para suspender la ejecución de la tarea que la llama durante un periodo de tiempo específico. Este periodo de tiempo se expresa en "ticks" del sistema, que son unidades de tiempo definidas por la configuración del temporizador del sistema (el "tick rate"). Esta es la función vTaskDelay:

void vTaskDelay(const TickType_t xTicksToDelay);

Donde los parámetros son:

5. vTaskDelayUntil

En FreeRTOS, la función vTaskDelayUntil se utiliza para crear tareas periódicas que necesitan ejecutarse a intervalos regulares y constantes. Esta función es especialmente útil cuando se necesita precisión en el timing, ya que garantiza que las tareas se ejecuten exactamente a intervalos específicos, independientemente de cualquier retardo acumulativo que pueda ocurrir en las ejecuciones de las tareas. En general, vTaskDelayUntil retrasa la tarea hasta que un número específico de ticks del sistema haya transcurrido. Esta es la función vTaskDelayUntil:

void vTaskDelayUntil(TickType_t * const pxPreviousWakeTime, const TickType_t xTimeIncrement);

Donde los parámetros son:

6. vTaskPrioritySet

En FreeRTOS, la función vTaskPrioritySet se utiliza para cambiar la prioridad de una tarea existente en tiempo de ejecución. La capacidad de ajustar las prioridades de las tareas dinámicamente es una característica poderosa de FreeRTOS que permite a los desarrolladores adaptar el comportamiento del sistema en función de las condiciones operativas. Esta es la función vTaskPrioritySet:

void vTaskPrioritySet(TaskHandle_t xTask, UBaseType_t uxNewPriority);

Donde los parámetros son:

7. vTaskSuspend

En FreeRTOS, la función vTaskSuspend se utiliza para suspender la ejecución de una tarea. Cuando una tarea es suspendida, deja de ser elegible para ser ejecutada por el planificador de FreeRTOS hasta que sea reanudada explícitamente usando la función vTaskResume. vTaskSuspend permite detener la ejecución de una tarea sin finalizarla, lo que es útil cuando una tarea necesita esperar indefinidamente hasta que ocurra un evento específico antes de continuar. Al suspender tareas que no son necesarias en ciertos momentos, se puede liberar recursos para otras tareas más importantes. Esto puede ser especialmente útil en sistemas embebidos con recursos limitados. Se puede utilizar vTaskSuspend para sincronizar tareas en función de ciertos eventos. Por ejemplo, una tarea puede suspenderse hasta que otra tarea complete un trabajo específico. En algunos casos, el uso de semáforos, colas o notificaciones de tareas puede ser una alternativa más adecuada para la sincronización entre tareas, proporcionando una mayor flexibilidad y control. Esta es la función vTaskSuspend:

void vTaskSuspend(TaskHandle_t xTaskToSuspend);

Donde los parámetros son:

8. vTaskResume

En FreeRTOS, la función vTaskResume se utiliza para reanudar la ejecución de una tarea que ha sido previamente suspendida utilizando vTaskSuspend. La capacidad de suspender y reanudar tareas dinámicamente permite un control preciso sobre el flujo de ejecución de las tareas en el sistema. Esta es la función vTaskResume:

void vTaskResume(TaskHandle_t xTaskToResume);

Donde los parámetros son:

Tareas dinámicas y tareas estáticas.

En FreeRTOS, las tareas pueden ser creadas de dos formas: de manera estática y de manera dinámica. La principal diferencia entre ellas radica en cómo se maneja la asignación de memoria para las tareas y sus estructuras asociadas.

Tareas Dinámicas

Las tareas dinámicas utilizan la asignación de memoria en tiempo de ejecución. La memoria necesaria para la pila de la tarea y la estructura de control de la tarea (TCB, Task Control Block) se asigna dinámicamente cuando la tarea se crea, y se libera cuando la tarea se elimina.

xTaskCreate(vTaskFunction, "Task 1", configMINIMAL_STACK_SIZE, NULL, 1, NULL);

Tareas Estáticas

Las tareas estáticas, en cambio, utilizan memoria pre-asignada. El usuario debe proporcionar los buffers para la pila de la tarea y la estructura de control de la tarea. Esto permite un control total sobre la asignación de memoria, evitando el uso de malloc y free.

static StackType_t xStack[STACK_SIZE];
static StaticTask_t xTaskBuffer;

 xTaskCreateStatic(
        vTaskFunction,       // Función de la tarea
        "Task 1",            // Nombre de la tarea
        STACK_SIZE,          // Tamaño de la pila
        NULL,                // Parámetro de entrada
        1,                   // Prioridad de la tarea
        xStack,              // Puntero a la pila de la tarea
        &xTaskBuffer         // Puntero al buffer de la TCB
    );

Ejemplo creación de tareas en FreeRTOS.

A continuación reforzaremos el aprendizaje de algunas funciones con ejemplos específicos. En este ejemplo se busca crear dos tareas independientes que hagan parpadear el mismo LED a dos velocidades diferentes. Eso significa controlar 1 LED con dos tiempos de retardo diferentes.

Sección de Librerías y Configuración

/*######################################################################
# LIBRARIES.
######################################################################*/
#include <Arduino.h>

// Use only core 1 for demo purposes
#if CONFIG_FREERTOS_UNICORE
static const BaseType_t app_cpu = 0;
#else
static const BaseType_t app_cpu = 1;
#endif

// LED rates
static const int rate_1 = 500;  // ms
static const int rate_2 = 323;  // ms

// Pins
static const int led_pin = LED_BUILTIN;

// Handle para la tarea necesario para poder referenciar la tarea específica 
TaskHandle_t TaskHandle1 = NULL;
TaskHandle_t TaskHandle2 = NULL;

[Ejemplo creación de tareas, sección librerías y configuraci

ón.]

Definición de Tareas

/*######################################################################
# TASKs.
######################################################################*/
// Task 1: blink an LED at one rate
void toggleLED_1(void *parameter) {
  while(1) {
    printf("Tarea 1 trabajando..\n");  
    digitalWrite(led_pin, HIGH);
    vTaskDelay(rate_1 / portTICK_PERIOD_MS);
    digitalWrite(led_pin, LOW);
    vTaskDelay(rate_1 / portTICK_PERIOD_MS);
  }
}

// Task 2: blink an LED at another rate
void toggleLED_2(void *parameter) {
  while(1) {
    printf("Tarea 2 trabajando..\n");
    digitalWrite(led_pin, HIGH);
    vTaskDelay(rate_2 / portTICK_PERIOD_MS);
    digitalWrite(led_pin, LOW);
    vTaskDelay(rate_2 / portTICK_PERIOD_MS);
  }
}

[Ejemplo creación de tareas, sección definición de tareas.]

Configuración Inicial (setup).

/*######################################################################
# SETUP.
######################################################################*/
void setup() {
  // Configure pin
  pinMode(led_pin, OUTPUT);

  // Task to run forever
  xTaskCreatePinnedToCore(  // Use xTaskCreate() in vanilla FreeRTOS
              toggleLED_1,  // Function to be called
              "Toggle 1",   // Name of task
              1024,         // Stack size (bytes in ESP32, words in FreeRTOS)
              NULL,         // Parameter to pass to function
              configMAX_PRIORITIES-1, // Task priority (0 to configMAX_PRIORITIES - 1)
              &TaskHandle1, // Task handle
              app_cpu); // Run on one core for demo purposes (ESP32 only)

  // Task to run forever
  xTaskCreatePinnedToCore(  // Use xTaskCreate() in vanilla FreeRTOS
              toggleLED_2,  // Function to be called
              "Toggle 2",   // Name of task
              1024,         // Stack size (bytes in ESP32, words in FreeRTOS)
              NULL,         // Parameter to pass to function
              configMAX_PRIORITIES-1, // Task priority (0 to configMAX_PRIORITIES - 1)
              &TaskHandle2, // Task handle
              app_cpu); // Run on one core for demo purposes (ESP32 only)
  // If this was vanilla FreeRTOS, you'd want to call vTaskStartScheduler() in
  // main after setting up your tasks.
}

[Ejemplo creación de tareas, sección configuración inicial (setup)]

Bucle Principal (loop).

/*######################################################################
# LOOP.
######################################################################*/
void loop() {
  // Do nothing
  // setup() and loop() run in their own task with priority 1 in core 1
  // on ESP32
}

[Ejemplo creación de tareas, sección bucle principal.]

Código Completo.

/*######################################################################
# C CODE.
######################################################################
# Copyright (C) 2024. F.E.Segura-Quijano (FES) fsegura@uniandes.edu.co
#  
# Este trabajo está licenciado bajo la Licencia: 
# Creative Commons Atribución-NoComercial 4.0 Internacional.
# Para ver una copia de esta licencia, visita 
# http://creativecommons.org/licenses/by-nc/4.0/ o envía una carta
# a Creative Commons, PO Box 1866, Mountain View, CA 94042, USA.
######################################################################*/

/*######################################################################
# Parte de este código se basa en los ejemplos FreeRTOS de:
# https://github.com/ShawnHymel/introduction-to-rtos/tree/main 
# Parte de este código fue generado con la asistencia de ChatGPT de OpenAI.
######################################################################*/

/*######################################################################
# LIBRARIES.
######################################################################*/
#include <Arduino.h>

// Use only core 1 for demo purposes
#if CONFIG_FREERTOS_UNICORE
static const BaseType_t app_cpu = 0;
#else
static const BaseType_t app_cpu = 1;
#endif

// LED rates
static const int rate_1 = 500;  // ms
static const int rate_2 = 323;  // ms

// Pins
static const int led_pin = LED_BUILTIN;

// Handle para la tarea necesario para poder referenciar la tarea específica 
TaskHandle_t TaskHandle1 = NULL;
TaskHandle_t TaskHandle2 = NULL;

/*######################################################################
# TASKs.
######################################################################*/
// Task 1: blink an LED at one rate
void toggleLED_1(void *parameter) {
  while(1) {
    printf("Tarea 1 trabajando..\n");  
    digitalWrite(led_pin, HIGH);
    vTaskDelay(rate_1 / portTICK_PERIOD_MS);
    digitalWrite(led_pin, LOW);
    vTaskDelay(rate_1 / portTICK_PERIOD_MS);
  }
}

// Task 2: blink an LED at another rate
void toggleLED_2(void *parameter) {
  while(1) {
    printf("Tarea 2 trabajando..\n");
    digitalWrite(led_pin, HIGH);
    vTaskDelay(rate_2 / portTICK_PERIOD_MS);
    digitalWrite(led_pin, LOW);
    vTaskDelay(rate_2 / portTICK_PERIOD_MS);
  }
}

/*######################################################################
# SETUP.
######################################################################*/
void setup() {
  // Configure pin
  pinMode(led_pin, OUTPUT);

  // Task to run forever
  xTaskCreatePinnedToCore(  // Use xTaskCreate() in vanilla FreeRTOS
              toggleLED_1,  // Function to be called
              "Toggle 1",   // Name of task
              1024,         // Stack size (bytes in ESP32, words in FreeRTOS)
              NULL,         // Parameter to pass to function
              configMAX_PRIORITIES-1, // Task priority (0 to configMAX_PRIORITIES - 1)
              &TaskHandle1, // Task handle
              app_cpu); // Run on one core for demo purposes (ESP32 only)

  // Task to run forever
  xTaskCreatePinnedToCore(  // Use xTaskCreate() in vanilla FreeRTOS
              toggleLED_2,  // Function to be called
              "Toggle 2",   // Name of task
              1024,         // Stack size (bytes in ESP32, words in FreeRTOS)
              NULL,         // Parameter to pass to function
              configMAX_PRIORITIES-1, // Task priority (0 to configMAX_PRIORITIES - 1)
              &TaskHandle2, // Task handle
              app_cpu); // Run on one core for demo purposes (ESP32 only)
  // If this was vanilla FreeRTOS, you'd want to call vTaskStartScheduler() in
  // main after setting up your tasks.
}

/*######################################################################
# LOOP.
######################################################################*/
void loop() {
  // Do nothing
  // setup() and loop() run in their own task with priority 1 in core 1
  // on ESP32
}

[Creación de tareas, código completo.]

Para probar la funcionalidad del ejemplo anterior, cree un proyecto desde PlatformIO. Recuerde seleccionar correctamente la tarjeta y demás elementos de configuración. En el archivo main.cpp copie el código completo del ejemplo y programe la tarjeta. Si todo es correcto, podrá ver el led de la tarjeta parpadeando a las dos frecuencias diferentes. Adicionalmente podrá ver en la consola serial los mensajes a medida que las tareas se ejecutan infinitamente.

[Consola serial ejecutando el código de ejemplo creación de tareas.]

Ejemplo eliminación de tareas en FreeRTOS.

Es importante revisar en detalle lo que se quiere realizar usando un FreeRTOS. En el siguiente ejemplo revisaremos cómo se podría eliminar una tarea. En la función toggleLED_2() se realizará una modificación en donde cambiaremos el ciclo infinito adicionando un ciclo de 5 iteraciones; que imprima el mensaje en consola a la frecuencia de parpadeo del led de esa función.

// Task 2: blink an LED at another rate
void toggleLED_2(void *parameter) {
//  while(1) {
  for(int i=0;i<5;i++){  
    printf("Tarea 2 trabajando..\n");
    digitalWrite(led_pin, HIGH);
    vTaskDelay(rate_2 / portTICK_PERIOD_MS);
    digitalWrite(led_pin, LOW);
    vTaskDelay(rate_2 / portTICK_PERIOD_MS);
  }
}

[Ejemplo eliminación de tareas, sección tarea 2 (reinicio del programa).]

Este ciclo finalizará luego de 5 iteraciones o parpadeos, por lo que la ESP32 se reiniciará en ese punto. Si ejecutamos el código así no más veríamos en la consola serial lo siguiente:

[Consola serial ejecutando el código cuando adicionamos un ciclo con 5 iteraciones.]

Nótese que la ESP32 llega a un punto de reinicio, porque luego de ejecutar 5 iteraciones para la tarea 2 ( toggleLED_2() ); el sistema queda "perdido" sin saber cómo continuar la ejecución de código.

En este tipo de situaciones y si el caso es realmente eliminar la tarea 2 (toggleLED_2()) podemos usar la función vTaskDelete(TaskHandle2),

A continuación se presenta el código modificado:

// Task 2: blink an LED at another rate
void toggleLED_2(void *parameter) {
  int count = 0;
  while(1) {
    count++;
    printf("Tarea 2 trabajando..\n");
    digitalWrite(led_pin, HIGH);
    vTaskDelay(rate_2 / portTICK_PERIOD_MS);
    digitalWrite(led_pin, LOW);
    vTaskDelay(rate_2 / portTICK_PERIOD_MS);
        if (count == 5)
        {
          printf("Tarea 2 eliminada!\n");
          vTaskDelete(TaskHandle2);
        }     
  }
}

[Ejemplo eliminación de tareas, sección tarea 2.]

Si ejecutamos el código veríamos en la consola serial lo siguiente:

[Consola serial ejecutando el código cuando adicionamos vTaskDelete.]

Como se puede ver, la tarea 2 (toggleLED_2() fue eliminada, la ESP32 no se reinicia y el sistema sigue completamente funcional con la ejecución de la tarea 1.

Ejemplo suspensión y reanudación de tareas en FreeRTOS.

Para suspender una tarea se usa la función vTaskSuspend(), especificando el identificador de la tarea a suspender como un parámetro dentro de ella.

vTaskSuspend(TaskHandle2);

[Función para suspender una tarea.]

Para reanudar una tarea se usa la función vTaskResume(), especificando el identificador de la tarea a reanudar como un parámetro dentro de ella.

vTaskResume(TaskHandle2);

[Función para reanudar una tarea.]

En el siguiente ejemplo vamos a realizar una modificación para poder evidenciar el uso de estas nuevas funciones. Aumentaremos el ciclo de la tarea 2 (toggleLED_2() a 10 iteraciones. En la tarea 1, se suspenderá la tarea 2 luego de 5 iteraciones y luego se reanudará luego de 3 iteraciones. Luego de 10 iteraciones la tarea 2 se eliminará para que la ESP32 no se reinicie.

Código Completo.

/*######################################################################
# C CODE.
######################################################################
# Copyright (C) 2024. F.E.Segura-Quijano (FES) fsegura@uniandes.edu.co
#  
# Este trabajo está licenciado bajo la Licencia: 
# Creative Commons Atribución-NoComercial 4.0 Internacional.
# Para ver una copia de esta licencia, visita 
# http://creativecommons.org/licenses/by-nc/4.0/ o envía una carta
# a Creative Commons, PO Box 1866, Mountain View, CA 94042, USA.
######################################################################*/

/*######################################################################
# Parte de este código se basa en los ejemplos FreeRTOS de:
# https://github.com/ShawnHymel/introduction-to-rtos/tree/main 
# Parte de este código fue generado con la asistencia de ChatGPT de OpenAI.
######################################################################*/

/*######################################################################
# LIBRARIES.
######################################################################*/
#include <Arduino.h>

// Use only core 1 for demo purposes
#if CONFIG_FREERTOS_UNICORE
static const BaseType_t app_cpu = 0;
#else
static const BaseType_t app_cpu = 1;
#endif

// LED rates
static const int rate_1 = 500;  // ms
static const int rate_2 = 323;  // ms

// Pins
static const int led_pin = LED_BUILTIN;

// Handle para la tarea necesario para poder referenciar la tarea específica 
TaskHandle_t TaskHandle1 = NULL;
TaskHandle_t TaskHandle2 = NULL;

// Task 1: blink an LED at one rate
void toggleLED_1(void *parameter) {
  int count = 0;
  while(1) {
    count++;
    printf("Tarea 1 trabajando..\n");
    digitalWrite(led_pin, HIGH);
    vTaskDelay(rate_1 / portTICK_PERIOD_MS);
    digitalWrite(led_pin, LOW);
    vTaskDelay(rate_1 / portTICK_PERIOD_MS);
        if (count == 5)
        {
          vTaskSuspend(TaskHandle2);
          printf("Tarea 2 esta suspendida!\n");
        }
        if (count == 8)
        {
          vTaskResume(TaskHandle2);
          printf("Tarea 2 está resumed!\n");
        }
        if (count == 10)
        {
          vTaskDelete(TaskHandle2);
          printf("Tarea 2 es elminada!\n");
        }
  }
}

// Task 2: blink an LED at another rate
void toggleLED_2(void *parameter) {
  while(1) {
    printf("Tarea 2 trabajando..\n");
    digitalWrite(led_pin, HIGH);
    vTaskDelay(rate_2 / portTICK_PERIOD_MS);
    digitalWrite(led_pin, LOW);
    vTaskDelay(rate_2 / portTICK_PERIOD_MS);
  }
}

void setup() {
  // Configure pin
  pinMode(led_pin, OUTPUT);

  // Task to run forever
  xTaskCreatePinnedToCore(  // Use xTaskCreate() in vanilla FreeRTOS
              toggleLED_1,  // Function to be called
              "Toggle 1",   // Name of task
              1024,         // Stack size (bytes in ESP32, words in FreeRTOS)
              NULL,         // Parameter to pass to function
              1,            // Task priority (0 to configMAX_PRIORITIES - 1)
              &TaskHandle1,  // Task handle
              app_cpu);     // Run on one core for demo purposes (ESP32 only)

  // Task to run forever
  xTaskCreatePinnedToCore(  // Use xTaskCreate() in vanilla FreeRTOS
              toggleLED_2,  // Function to be called
              "Toggle 2",   // Name of task
              1024,         // Stack size (bytes in ESP32, words in FreeRTOS)
              NULL,         // Parameter to pass to function
              1,            // Task priority (0 to configMAX_PRIORITIES - 1)
              &TaskHandle2, // Task handle
              app_cpu);     // Run on one core for demo purposes (ESP32 only)

  // If this was vanilla FreeRTOS, you'd want to call vTaskStartScheduler() in
  // main after setting up your tasks.
}

void loop() {
  // Do nothing
  // setup() and loop() run in their own task with priority 1 in core 1
  // on ESP32
}

[Suspención y reanudación de tareas, código completo.]

Si ejecutamos el código veríamos en la consola serial lo siguiente. Nótese que la tarea dos se ejecuta varias veces, sin depender de la variable count, esto por los cambios del planeador ejecutando las dos tareas.

[Consola serial ejecutando el código cuando adicionamos vTaskSuspend y vTaskResume.]

Prioridades para las tareas en FreeRTOS.

FreeRTOS permite establecer prioridades a las tareas. Esto permite al planificador de tareas adelantarse a las tareas de menor prioridad con tareas de mayor prioridad. El planificador hace parte del software determinando qué tarea debe ejecutarse en cada tick.

En FreeRTOS, el intervalo de tiempo predeterminado es 1 ms y un intervalo de tiempo se conoce como "tick". Así, un temporizador hardware se configura para crear una interrupción cada 1 ms. El servicio de interrupciones (ISR) de ese temporizador ejecuta el planificador de tareas, que elige la tarea que se ejecutará a continuación.

En cada interrupción de tick, se elige para ejecutar la tarea con mayor prioridad. Si las tareas de mayor prioridad tienen la misma prioridad, se ejecutan por turnos. Si una tarea con una prioridad más alta que la tarea que se está ejecutando actualmente está disponible (por ejemplo, en el estado "Listo" o "Ready"), se ejecutará inmediatamente sin esperar al siguiente tick.

Adicionalmente, siempre se considera que una interrupción de hardware tiene una prioridad más alta que cualquier tarea que se ejecute en el software. Como resultado, un servicio de interrupciones (ISR) de hardware puede interrumpir cualquier tarea. Debido a esto, es importante mantener el código del servicio de interrupción (ISR) lo más sencillo posible para reducir las interrupciones en las tareas en ejecución. Adicionalmente evitar que sea la rutina de interrupción la que asuma el control del Sistema Operativo.

Cuando creamos tareas, se les puede asignar prioridades. Incluso se puede cambiar las prioridades de las tareas con la función vTaskPrioritySet()

Recordemos el siguiente diagrama:

[Posibles estados de las tareas en FreeRTOS.]

Tan pronto como se crea una tarea, entra en el estado "Ready". Aquí, le dice al planificador que está listo para ejecutarse. En cada tick, el planificador elige una tarea para ejecutar que está en estado "ready" (en un sistema multinúcleo, el programador puede elegir varias tareas). Mientras se ejecuta, una tarea está en el estado "running" y el planificador puede devolverla al estado "ready".

Las funciones que hacen que la tarea espere, como vTaskDelay(), ponen la tarea en estado "blocked". Aquí, la tarea está esperando a que ocurra algún otro evento, como que termine el temporizador de vTaskDelay(). La tarea también puede estar esperando a que otra tarea libere algún recurso, como un semáforo (recurso que veremos más adelante). Las tareas en estado "blocked" permiten que se ejecuten otras tareas en su lugar.

Una llamada explícita a vTaskSuspend() puede poner una tarea en modo "suspended" (muy parecido a poner esa tarea en suspensión). Cualquier tarea puede poner cualquier tarea (incluida ella misma) en modo suspendido. Una tarea solo puede regresar al estado Listo mediante una llamada explícita a vTaskResume() por parte de otra tarea.

El objetivo del siguiente ejemplo usando FreeRTOS, es crear dos tareas separadas. Una escucha o permite la entrada de un número entero a través de UART (desde el monitor serie) y guarda su valor en una variable. La otra tarea hace parpadear el LED integrado en la tarjeta a una velocidad especificada por dicha variable de la primera tarea. En resumen, se desea crear un sistema de subprocesos múltiples que permita que la interfaz de usuario se ejecute simultáneamente con la tarea de control (el LED parpadeante).

Bibliotecas Necesarias

#include <Arduino.h>
#include <stdlib.h>

Definición del Núcleo a Utilizar (ESP32 Específico)

#if CONFIG_FREERTOS_UNICORE
  static const BaseType_t app_cpu = 0;
#else
  static const BaseType_t app_cpu = 1;
#endif

Definición de Constantes y Variables Globales

static const uint8_t buf_len = 20;
static const int led_pin = LED_BUILTIN;
static int led_delay = 500;   // ms

TaskHandle_t TaskHandle1 = NULL;
TaskHandle_t TaskHandle2 = NULL;

Función de Tarea para Parpadear el LED

void toggleLED(void *parameter) {
  while (1) {
    digitalWrite(led_pin, HIGH);
    vTaskDelay(led_delay / portTICK_PERIOD_MS);
    digitalWrite(led_pin, LOW);
    vTaskDelay(led_delay / portTICK_PERIOD_MS);
  }
}

Función de Tarea para Leer el Terminal Serial

void readSerial(void *parameters) {
  char c;
  char buf[buf_len];
  uint8_t idx = 0;

  memset(buf, 0, buf_len);

  while (1) {
    if (Serial.available() > 0) {
      c = Serial.read();
      Serial.print("Data available: ");
      Serial.println(c);
      if (c == '\n' || c == '\r') {
        if (idx > 0) {
          led_delay = atoi(buf);
          Serial.print("Updated LED delay to: ");
          Serial.println(led_delay);
          memset(buf, 0, buf_len);
          idx = 0;
        }
      } else {
        if (idx < buf_len - 1) {
          buf[idx] = c;
          idx++;
        }
      }
    }
  }
}

Configuración Inicial en setup()

void setup() {
  pinMode(led_pin, OUTPUT);

  Serial.begin(115200);
  vTaskDelay(1000 / portTICK_PERIOD_MS);
  Serial.println("Multi-task LED Demo");
  Serial.println("Enter a number in milliseconds to change the LED delay.");

  xTaskCreatePinnedToCore(toggleLED, "Toggle LED", 1024, NULL, 1, &TaskHandle1, app_cpu);
  xTaskCreatePinnedToCore(readSerial, "Read Serial", 1024, NULL, 1, &TaskHandle2, app_cpu);

  vTaskDelete(NULL);
}

Función loop()

void loop() {
  // Do nothing
  // setup() and loop() run in their own task with priority 1 in core 1
  // on ESP32
}

Código completo.

A continuación tienes el código completo que soluciona el ejemplo propuesto.

/*######################################################################
# C CODE.
######################################################################
# Copyright (C) 2024. F.E.Segura-Quijano (FES) fsegura@uniandes.edu.co
#  
# Este trabajo está licenciado bajo la Licencia: 
# Creative Commons Atribución-NoComercial 4.0 Internacional.
# Para ver una copia de esta licencia, visita 
# http://creativecommons.org/licenses/by-nc/4.0/ o envía una carta
# a Creative Commons, PO Box 1866, Mountain View, CA 94042, USA.
######################################################################*/

/*######################################################################
# Parte de este código se basa en los ejemplos FreeRTOS de:
# https://github.com/ShawnHymel/introduction-to-rtos/tree/main 
# Parte de este código fue generado con la asistencia de ChatGPT de OpenAI.
######################################################################*/

/*######################################################################
# EJEMPLO: Una tarea hace parpadear un LED a una velocidad especificada por un valor establecido en otra tarea. 
######################################################################*/

#include <Arduino.h>
// Needed for atoi()
#include <stdlib.h>
// Use only core 1 for demo purposes
#if CONFIG_FREERTOS_UNICORE
  static const BaseType_t app_cpu = 0;
#else
  static const BaseType_t app_cpu = 1;
#endif

// Settings
static const uint8_t buf_len = 20;
// Pins
static const int led_pin = LED_BUILTIN;
// Globals
static int led_delay = 500;   // ms
// Handle para la tarea necesario para poder referenciar la tarea específica 
TaskHandle_t TaskHandle1 = NULL;
TaskHandle_t TaskHandle2 = NULL;

//*****************************************************************************
// Task: Blink LED at rate set by global variable
void toggleLED(void *parameter) {
  while (1) {
    digitalWrite(led_pin, HIGH);
    vTaskDelay(led_delay / portTICK_PERIOD_MS);
    digitalWrite(led_pin, LOW);
    vTaskDelay(led_delay / portTICK_PERIOD_MS);
  }
}

//*****************************************************************************
// Task: Read from serial terminal
// Feel free to use Serial.readString() or Serial.parseInt(). I'm going to show
// it with atoi() in case you're doing this in a non-Arduino environment. You'd
// also need to replace Serial with your own UART code for non-Arduino.
void readSerial(void *parameters) {
  char c;
  char buf[buf_len];
  uint8_t idx = 0;
  // Clear whole buffer
  memset(buf, 0, buf_len);
  // Loop forever
  while (1) {
    // Read characters from serial
    if (Serial.available() > 0) {
      c = Serial.read();
      Serial.print("Data available: ");
      Serial.println(c);
            // Handle newline or carriage return characters
      if (c == '\n' || c == '\r') {
        if (idx > 0) {  // Only update if we have received some data
          led_delay = atoi(buf);
          Serial.print("Updated LED delay to: ");
          Serial.println(led_delay);
          memset(buf, 0, buf_len);
          idx = 0;
        }
      } else {
        // Only append if index is not over message limit
        if (idx < buf_len - 1) {
          buf[idx] = c;
          idx++;
        }
      }
    }
  }
}

void setup() {
  // Configure pin
  pinMode(led_pin, OUTPUT);

  // Configure serial and wait a second
  Serial.begin(115200);
  vTaskDelay(1000 / portTICK_PERIOD_MS);
  Serial.println("Multi-task LED Demo");
  Serial.println("Enter a number in milliseconds to change the LED delay.");

  // Start blink task
  xTaskCreatePinnedToCore(  // Use xTaskCreate() in vanilla FreeRTOS
            toggleLED,      // Function to be called
            "Toggle LED",   // Name of task
            1024,           // Stack size (bytes in ESP32, words in FreeRTOS)
            NULL,           // Parameter to pass
            1,              // Task priority
            &TaskHandle1,           // Task handle
            app_cpu);       // Run on one core for demo purposes (ESP32 only)
            
  // Start serial read task
  xTaskCreatePinnedToCore(  // Use xTaskCreate() in vanilla FreeRTOS
            readSerial,     // Function to be called
            "Read Serial",  // Name of task
            1024,           // Stack size (bytes in ESP32, words in FreeRTOS)
            NULL,           // Parameter to pass
            1,              // Task priority (must be same to prevent lockup)
            &TaskHandle2,           // Task handle
            app_cpu);       // Run on one core for demo purposes (ESP32 only)

  // Delete "setup and loop" task
  vTaskDelete(NULL);
}

void loop() {
  // Do nothing
  // setup() and loop() run in their own task with priority 1 in core 1
  // on ESP32
}

[Código completo solución ejemplo.]

Cuando se realice la programación y prueba, asegúrese de seleccionar en el monitor serial la opción Line ending: LF; esto hace que se genere un fin de línea o '\n' el cual hemos usado en el código para saber el fin de cada mensaje. A continuación se puede ver lo que aparece en la consola serial

[Programación y pruebas con el código completo solución ejemplo.]