Reutilizando el código de las tareas

Reutilizando el código de las tareas

Hola, ¿sabías que puedes reutilizar el código de tus tareas? Si te ves escribiendo dos tareas muy similares, que quizás sólo cambian en el actuador o en la temporización, entonces quizás querrías escribir el código una sola vez y utilizarlo muchas veces. En esta entrada te voy a mostrar la forma en qué puedes lograrlo.

Table of Contents

¿Qué es y qué necesito?

Repetir el código (quizás haciendo un copy and paste) y adaptarlo a su nueva función es una solución que no escala muy bien. Si encuentras un error, o quieres hacer una modificación importante en la tarea original, debes llevar esos cambios a todos los lugares donde hiciste la copia.

En FreeRTOS puedes escribir una tarea que se adapte a una serie de requerimientos. Por ejemplo, tienes 3 LEDs que deben parpadear a ritmos muy diferentes, t0, t1, t2, respectivamente. La solución inmediata es crear tres tareas, pero como ya dije, esta propuesta presenta problemas si posteriormente quieres hacer cambios, ya que éstos tienes que aplicarlos a las mismas 3 tareas. Entonces, ¿qué podemos hacer? La clave está en que las tareas reciben un parámetro, void* pvParameters, y cuando creas la, o las tareas, con las funciones xTaskCreate() y xTaskCreateStatic() tienes un parámetro para pasarle valores a la tarea que estás creando. Y es en este parámetro donde indicarías la salida asociada al LED y la temporización de cada uno. En caso de que tengas que hacer cambios, éstos los harás en un sólo lado y una sola vez, ahorrándote muchos dolores de cabeza.

Visita esta entrada en donde explico de manera extensa cómo se pasan argumentos a las funciones. Si no la has leído te recomiendo que lo hagas antes de continuar, ya que estaré utilizando todo lo visto allá. De hecho, esta entrada es como la segunda parte de la anterior.

Pasando un argumento simple constante

Vamos a comenzar definiendo la tarea de la cual estaremos reutilizando su código. El propósito de la tares es que un LED parpadée a intervalos constantes (más adelante cada LED parpadeará a su propio ritmo):

void led_task( void* pvParameters )
{
    uint8_t pin = (uint8_t) pvParameters;

    pinMode( pin, OUTPUT );

    while( 1 )
    {
        digitalWrite( pin, HIGH );

        vTaskDelay( pdMS_TO_TICKS( 497 ) );
   
        digitalWrite( pin, LOW );

        vTaskDelay( pdMS_TO_TICKS( 497 ) );
    }
}

En la línea 3 definimos la variable pin que almacenará el número de pin de Arduino asociado al LED. Cada tarea tendrá su propia copia de esta variable, por lo cual no importa cuantas tareas crees, todas serán independientes.

Luego debemos crear las tareas que reutilizarán este código:

   xTaskCreate( 
      led_task,   // es el mismo código ...
      "LD1",
      128,
      (void*) 13, // pero con datos diferentes
      tskIDLE_PRIORITY,
      NULL );

   xTaskCreate( led_task, "LD2", 128, (void*) 12, tskIDLE_PRIORITY, NULL );
   xTaskCreate( led_task, "LD3", 128, (void*) 11, tskIDLE_PRIORITY, NULL );
   xTaskCreate( led_task, "LD4", 128, (void*) 10, tskIDLE_PRIORITY, NULL );

Tenemos cuatro tareas idénticas cuya única, y fundamental diferencia, es el pin asociado. En este ejemplo estoy utilizando números constantes para el número de pin, pero también es posible utilizar variables (como veremos más adelante).

Aquí está el ejemplo completo con cuatro LEDs para que lo intentes:

#include <FreeRTOS.h>
#include <task.h>

void led_task( void* pvParameters )
{
    uint8_t pin = (uint8_t) pvParameters;

    pinMode( pin, OUTPUT );

    while( 1 )
    {
        digitalWrite( pin, HIGH );

        vTaskDelay( pdMS_TO_TICKS( 497 ) );
   
        digitalWrite( pin, LOW );

        vTaskDelay( pdMS_TO_TICKS( 497 ) );
    }
}

void setup()
{
   xTaskCreate( 
      led_task,
      "LD1",
      128,
      (void*) 13,
      tskIDLE_PRIORITY,
      NULL );

   xTaskCreate( led_task, "LD2", 128, (void*) 12, tskIDLE_PRIORITY, NULL );

   xTaskCreate( led_task, "LD3", 128, (void*) 11, tskIDLE_PRIORITY, NULL );

   xTaskCreate( led_task, "LD4", 128, (void*) 10, tskIDLE_PRIORITY, NULL );

   vTaskStartScheduler();
}

void loop() 
{
   // nada que hacer aquí, todo se realiza en las tareas
}

Pasando argumentos simples en variables

Tal vez quisieras pasar el número de LED en una variable, y piensas que solamente tienes que declarar las variables, ¿cierto?. Pues no, es un poco más difícil, y para los detalles debo remitirte a la entrada anterior de este curso. Sin embargo, haré un resumen: la variable debe existir cuando la tarea ya se esté ejecutando. Si tu declaras variables en la función setup() para usarlas como parámetros, éstas ya no existirán cuando las tareas se ejecuten. Esta situación se resuelve utilizando variables globales (poco recomendable), variables estáticas, o variables dinámicas.

Para ejemplificar este punto vamos a utilizar variables estáticas para pasar el número de pin a las tareas. Deberemos entonces crear cuatro variables estáticas y modificar el cuerpo de la tarea porque ahora recibirá direcciones de variables en lugar de constantes. Aquí está el ejemplo completo:

#include <FreeRTOS.h>
#include <task.h>

void led_task( void* pvParameters )
{
    uint8_t* pin = (uint8_t*) pvParameters;

    pinMode( *pin, OUTPUT );

    while( 1 )
    {
        digitalWrite( *pin, HIGH );

        vTaskDelay( pdMS_TO_TICKS( 497 ) );
   
        digitalWrite( *pin, LOW );

        vTaskDelay( pdMS_TO_TICKS( 497 ) );
    }
}

void setup()
{
   static uint8_t pin_led_1 = 13;
   xTaskCreate( 
      led_task,
      "LD1",
      128,
      (void*) &pin_led_1,
      tskIDLE_PRIORITY,
      NULL );

   static uint8_t pin_led_2 = 12;
   xTaskCreate( led_task, "LD2", 128, (void*) &pin_led_2, tskIDLE_PRIORITY, NULL );

   static uint8_t pin_led_3 = 11;
   xTaskCreate( led_task, "LD3", 128, (void*) &pin_led_3, tskIDLE_PRIORITY, NULL );

   static uint8_t pin_led_4 = 10;
   xTaskCreate( led_task, "LD4", 128, (void*) &pin_led_4, tskIDLE_PRIORITY, NULL );

   vTaskStartScheduler();
}

void loop() 
{
   // no se hace nada, todo el trabajo se lleva a cabo en las tareas
}

Observa que:

  • Línea 6: La variable pin ahora es un apuntador que guardará la dirección de la variable estática que reciba.
  • Líneas 8, 12, 16: Dado que la variable pin es un apuntador tienes que dereferenciarla para obtener el valor del pin asociado (* va a la dirección guardada en pin y devuelve el valor ahí almacenado. Apuntadores 101).
  • Líneas 24, 33, 36, 39: Las variables que se declaran para pasarlas como parámetros son marcadas como static. Aunque utilicé valores constantes en su declaración, estos valores podrían venir, posteriormente, de un cálculo o del usuario. Lo importante es que sean estáticas.
  • Líneas 29, 34, 37, 40: A diferencia del ejemplo anterior, aquí tienes que pasar la dirección de la variable estática. Para ello utilizas al operador referencia, &.

Notas:

1.- Si estás pensando en utilizar una misma variable para para las cuatro tareas, debo advertirte que eso no funciona. Cuando cada tarea se esté ejecutando necesita su propia variable, y si tú la compartes, entonces todas las tareas tomarán el mismo valor:

void setup()
{
   static uint8_t pin_led_1 = 13;
   xTaskCreate( 
      led_task,
      "LD1",
      128,
      (void*) &pin_led_1,
      tskIDLE_PRIORITY,
      NULL );

   pin_led_1 = 12;
   xTaskCreate( led_task, "LD2", 128, (void*) &pin_led_1, tskIDLE_PRIORITY, NULL );

   pin_led_1 = 11;
   xTaskCreate( led_task, "LD3", 128, (void*) &pin_led_1, tskIDLE_PRIORITY, NULL );

   pin_led_1 = 10;
   xTaskCreate( led_task, "LD4", 128, (void*) &pin_led_1, tskIDLE_PRIORITY, NULL );

   // Compila, pero no funciona como esperarías. TODAS las tareas, cuando comiencen a
   // ejecutarse, tomarán como valor el 10, el cual fue el último que se asignó a pin_led_1.

   vTaskStartScheduler();
}

2.- Puedes asignar nombres diferentes a cada tarea, como lo muestra el ejemplo, y también puedes asignar prioridades diferentes, si así lo requiere tu diseño (aunque el ejemplo no lo muestra).

3.- En el ejemplo utilicé variables estáticas, pero podrías usar variables globales (no lo recomiendo), o variables dinámicas (si el soporte dinámico está activado en FreeRTOS).

Pasando argumentos compuestos

Los ejemplos que hemos visto han utilizado tiempos constantes para el parpadeo de cada LED. Vamos a dar el siguiente paso: además de pasar el número de pin también pasaremos el tiempo de encendido y el tiempo de apagado de cada uno (por supuesto puedes extender esta idea según tus necesidades). Una vez más, para los detalles de esta solución puedes visitar esta entrada.

Para pasar argumentos compuestos necesitas usar estructuras de C y apuntadores a estructuras. Para nuestro ejemplo la estructura podría ser:

typedef struct
{
   uint8_t pin;    // pin
   uint16_t t_on;  // tiempo de encencido
   uint16_t t_off; // tiempo de apagado
} Led_Task_Params;

Vamos a ver el ejemplo completo y luego platicamos sobre él:

#include <FreeRTOS.h>
#include <task.h>

typedef struct
{
   uint8_t pin;
   uint16_t t_on;
   uint16_t t_off;
} Led_Task_Params;

void led_task( void* pvParameters )
{
    Led_Task_Params* params = (Led_Task_Params*) pvParameters;

    pinMode( params->pin, OUTPUT );

    while( 1 )
    {
        digitalWrite( params->pin, HIGH );

        vTaskDelay( pdMS_TO_TICKS( params->t_on ) );
   
        digitalWrite( params->pin, LOW );

        vTaskDelay( pdMS_TO_TICKS( params->t_off ) );
    }
}

void setup()
{
   static Led_Task_Params led1_params = { .pin = 13, .t_on = 100, .t_off = 900 };
   xTaskCreate( 
      led_task,
      "LD1",
      128,
      (void*) &led1_params,
      tskIDLE_PRIORITY,
      NULL );

   static Led_Task_Params led2_params = { .pin = 12, .t_on = 1401, .t_off = 1100 };
   xTaskCreate( led_task, "LD2", 128, (void*) &led2_params, tskIDLE_PRIORITY, NULL );

   static Led_Task_Params led3_params = { .pin = 11, .t_on = 899, .t_off = 199 };
   xTaskCreate( led_task, "LD3", 128, (void*) &led3_params, tskIDLE_PRIORITY, NULL );

   static Led_Task_Params led4_params;
   led4_params.pin = 10;
   led4_params.t_on = 497;
   led4_params.t_off = 497;
   xTaskCreate( led_task, "LD4", 128, (void*) &led4_params, tskIDLE_PRIORITY, NULL );

   vTaskStartScheduler();
}

void loop() 
{
   // no se hace nada, todo el trabajo se lleva a cabo en las tareas
}
  • Línea 13: Declaramos una variable params que guardará la dirección de la variable estructura que usamos para pasar datos a la tarea. Recuerda que hay una de estas variables params por cada tarea y que son independientes. Y por supuesto, también puedes ver la promoción desde void* hacia Led_Task_Params*.
  • Líneas 15, 19, 21, 23, 25: Accesamos a los campos de la estructura a través del operador flecha, ->, ya que params es un apuntador a estructura.
  • Líneas 31, 40, 43: Declaramos e inicializamos variables estructura estáticas con los valores que le queremos pasar a las tareas.
  • Línea 46: Similiar al punto anterior, pero quise que vieras una manera alternativa en caso de que los valores de los diferentes campos de led4_params no se puedan inicializar cuando declaras la variable. ¿Porqué usamos aquí al operador punto, ., y en el primer punto al operador flecha, ->? Porque dentro de la función setup() las 4 variables son “reales”; es decir, son variables normales con sus tres campos. En el código de la tarea lo que tienes es la dirección de una variable estructura.
  • Líneas 36, 41, 44, 50: Dado que las variables ledX_params son “reales” debemos pasarle su dirección a la tarea (habiendo convertido la dirección a apuntador void, por supuesto).

¿Qué sigue?

Hemos visto cómo reutilizar el código de una tarea para evitar la duplicación de código, de tres formas diferentes: con constantes, con variables simples, y con variables compuestas.

Como mencioné, lo que platicamos hoy era el post original, sin embargo, me encontré en la necesidad de realizarlo en dos partes debido a que es muchísima información, y lo que hoy leíste fue la segunda parte. Si tienes mucha prisa y sólo quieres la implementación, entonces lo que estudiamos hoy basta y sobra. Pero si tienes un poco de tiempo y quieres ahondar en los detalles también, entonces podrías visitar la entrada anterior.

En la siguiente entrada veremos qué son las funciones vTaskDelay() y vTaskDelayUntil(), sus diferencias y sus aplicaciones.

En la siguiente entrada veremos el tema de cómo reutilizar el código de las tareas. Puedes estar al pendiente, o mejor aún, suscribirte al blog.

Si tienes dudas o preguntas, ¡no dudes en hacérmelas llegar!

Índice del curso


Fco. Javier Rodríguez
Written by:

Fco. Javier Rodríguez

Soy Ingeniero Electrónico con 20+ años de experiencia en el diseño y desarrollo de productos electrónicos de consumo y a medida, y 12+ años como profesor. Egresado de la UNAM, también tengo el grado de Maestro en Ingeniería por la misma universidad. Mi perfil completo lo puede encontrar en: https://www.linkedin.com/in/fjrg76-dot-com/

View All Posts