Tareas periódicas

Tareas periódicas

Una actividad muy común en nuestros proyectos es la programación de código que debe ejecutarse de manera periódica. FreeRTOS cuenta con dos funciones que nos permiten tener tareas periódicas: vTaskDelay() y vTaskDelayUntil().

Quizás te estés preguntando por la función delay() de Arduino, ¿no es suficiente con ella? Pues no. Esta función, como otras tantas de Arduino, son funciones bloqueantes. Esto significa que mientras se están ejecutando nadie más en el sistema (a excepción de las interrupciones) puede hacer nada; es como si el sistema se “colgara” mientras transcurre el tiempo asignado a delay(). Esta situación es inaceptable en los sistemas de tiempo real.

¿Y qué hace tan especiales a las funciones vTaskDelay() y vTaskDelayUntil()? Estas funciones, como prácticamente todas las de FreeRTOS, liberan a la CPU mientras el tiempo programado se cumple, lo cual las hace diametralmente opuestas a su contraparte de Arduino, delay(). Cuando el tiempo programado transcurrió, FreeRTOS reactiva a la tarea para que continúe con su trabajo.

Por esta razón, si estás escribiendo un sistema de tiempo real, siempre deberías utilizar a las funciones vTaskDelay() y vTaskDelayUntil() en lugar de delay().

Tabla de contenidos

¿Qué es y qué necesito?

Como mencioné en la introducción, las funciones vTaskDelay() y vTaskDelayUntil() son parte de FreeRTOS y su principal cualidad es que liberan a la CPU mientras el tiempo programado transcurre. Una vez que esto sucede la tarea se reactiva para continuar con su trabajo.

Estamos a punto de comenzar con funciones de FreeRTOS que cambian el estado de una tarea debido a alguna transición. Por ello debemos discutir de manera breve sobre los estados por los que pasa una tarea en FreeRTOS para que le saques el mayor provecho a este sistema operativo. Inmediatamente después abordaremos a las funciones vTaskDelay() y vTaskDelayUntil(), y sus diferencias.

Estados de una tarea en FreeRTOS

Una tarea en FreeRTOS puede estar en uno de cuatro estados. La siguiente imagen los ilustra, así como las transiciones entre estados:

Estados de una tarea y transiciones entre estados.

Ready (listo). En este estado la tarea no se está ejecutando (no utiliza ciclos de CPU), pero es elegible para ejecutarse. Para el caso que nos ocupa, cuando el tiempo transcurrido en las funciones vTaskDelay() o vTaskDelayUntil() ha expirado la tarea pasa a este estado para ser ejecutada en cuanto sea posible.

Running (ejecutándose). En este estado la tarea está siendo ejecutada por el procesador, y por lo tanto, está haciendo un trabajo útil.

Blocked (bloqueada). En este estado la tarea no es elegible para ser ejecutada porque está esperando que un evento (interno o externo) suceda para pasar al estado Ready, y por lo tanto, no utiliza ciclos de CPU. Cuando mandas llamar a las funciones vTaskDelay() o vTaskDelayUntil() (las que estamos viendo en esta lección) la tarea se bloquea hasta que el tiempo programado transcurra. Con otras funciones de FreeRTOS (eventos, semáforos, colas, etc) las tareas se pueden auto-desbloquear luego de transcurrido un tiempo para evitar bloqueos infinitos que afecten al sistema (yo les llamo software-watchdog).

Suspended (suspendida). En este estado la tarea no es elegible para ser ejecutada, pero a diferencia del estado Blocked, no está esperando ningún evento; es como si la tarea no existiera. Sin embargo, es posible que otra tarea puede sacarla de este estado utilizando la primitiva vTaskResume(). Cabe mencionar que una tarea puede enviarse a sí misma a este estado.

Notarás que la transición del estado Running al estado Blocked se produce cuando llamas a una función bloqueante. En el contexto de los sistemas operativos, cuando decimos que una función se bloquea nos referimos a que libera la CPU y queda a la espera de algún evento (tiempo, semáforos, notificaciones, etc) para volver al estado Ready. Por favor no confundas este término con el de la programación secuencial (la de Arduino) donde una función bloqueante significa que dicha función acapara la CPU por todo el tiempo que quiera.

vTaskDelay()

Esta función ya la hemos estado utilizando. Ahora toca analizarla con más detalle. Comencemos viendo su firma:

Esta función manda a la tarea que la invoca al estado Blocked (en términos coloquiales, la “duerme”) por el tiempo establecido en su único argumento xTicksToDelay. Como ya he mencionado, mientras la tarea esté en dicho estado no ocupa ciclos de CPU.

Toma en cuenta que esta función trabaja con ticks del sistema, pero nosotros como humanos nos entendemos más en segundos (o fracciones de éstos), por lo que deberías evitar el uso directo de ticks; es mejor convertirlos a milisegundos (ms). (En mi proyecto Molcajete establecí que cada tick equivalga a 1ms, pero tú podrías cambiarlo si lo deseas; de cualquier manera, como práctica de buena programación, siempre deberías realizar la conversión para que tu aplicación sea portable.)

Existen dos formas para convertir ticks a milisegundos: utilizando a la constante portTICK_RATE_MS, o a través de la macro pdMS_TO_TICKS:

vTaskDelay( 100 / portTICK_RATE_MS ); 
// La tarea se 'duerme' durante 100 milisegundos

vTaskDelay( pdMS_TO_TICKS( 100 ) );
// La tarea se 'duerme' durante 100 milisegundos

Ambas formas son funcionalmente equivalentes. La diferencia es que la primera, portTICK_RATE_MS, tiene limitaciones técnicas: no se puede usar con ticks más rápidos de 1ms, por ejemplo, con ticks de 500 microsegundos (us). Aunque es difícil ver al sistema operativo usando ticks a esa velocidad, no es imposible.

Otro escenario donde es conventiente que utilices a la macro pdMS_TO_TICKS() en lugar de ticks directos, es cuando el tick de tu sistema es más lento, por ejemplo 10ms o 100ms, lo cual es muy común. No siempre querrás tener trabajando al sistema operativo a full speed. Mi recomendación es que siempre uses a la macro pdMS_TO_TICKS() aunque el tick sea de 1ms.

Esta función, al igual que vTaskDelayUntil(), deben ser “activadas” en FreeRTOS antes de ser utilizadas. Busca en el archivo de configuración FreeRTOSConfig.h las siguientes líneas y asegúrate que ambas estén a 1:

#define INCLUDE_vTaskDelayUntil 1
#define INCLUDE_vTaskDelay      1

(Si no recuerdas dónde está este archivo de configuración puedes refrescar tu memoria yendo a esta lección.)

El siguiente ejemplo crea dos tareas: una hace parpadear a un LED conectado en el pin 13, y otra envía mensajes a la consola. Ambas usan a la función vTaskDelay() y a la macro pdMS_TO_TICKS():

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

void task_1( void* pvParameters )
{
   pinMode( 13, OUTPUT );

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

      vTaskDelay( pdMS_TO_TICKS( 100 ) );

      digitalWrite( 13, LOW );

      vTaskDelay( pdMS_TO_TICKS( 900 ) );
   }
}

void task_2( void* pvParameters )
{
   uint16_t cont = 0;

   while( 1 )
   {
      vTaskDelay( pdMS_TO_TICKS( 1500 ) );

      Serial.print( cont++ );
      Serial.println( ": Hola mundo" );
   }
}

void setup()
{
   xTaskCreate( task_1, "TSK1", 128, NULL, tskIDLE_PRIORITY, NULL );

   xTaskCreate( task_2, "TSK2", 128, NULL, tskIDLE_PRIORITY, NULL );

   Serial.begin( 115200 );

   vTaskStartScheduler();
}

void loop() 
{
}

Antes de que pasemos a ver la siguiente función siento una obsesiva necesidad de recordarte:

Usa a vTaskDelay() o vTaskDelayUntil() en lugar de la función delay() de Arduino en tus sistemas de tiempo real!.

Atte: Programadores de sistemas de tiempo real.

vTaskDelayUntil()

FreeRTOS incluye una segunda función para mandar “dormir” a las tareas (es decir, enviarlas al estado Blocked): vTaskDelayUntil(), ¿y porqué ha incluído dos? Porque vTaskDelay() es relativa: comienza a descontar el tiempo desde el momento en que es llamada, mientras que vTaskDelayUntil() es absoluta: descuenta el tiempo con respecto a un punto de referencia en el pasado.

La mejor forma de entender la diferencia es con un gráfico. Supón que necesitas que una tarea se ejecute con periodo de 10 ms (milisegundos); y que cada tarea requiere (estoy exagerando) 2 ms. La figura superior A muestra el comportamiento con vTaskDelay(), y la figura inferior B muestra el comportamiento con vTaskDelayUntil(). En A puedes ver cómo se va desplazando el momento en que la tarea se ejecuta. Este tiempo es la suma del tiempo que necesita la tarea más el del tiempo de bloqueo (12, 24, 36, …). En B la tarea siempre empieza al tiempo establecido (10, 20, 30, …) sin importar cuánto dura la ejecución de la tarea.

Y como era de esperarse, vTaskDelayUntil() es ligeramente más complicada:

Esta función requiere dos argumentos. El primero, pxPreviousWakeTime, se refiere al punto de referencia en el pasado a partir del cual comenzará la cuenta, mientras que el segundo, xTimeIncrement, es el tiempo que la tarea se bloqueará. La tarea saldrá del estado Blocked cuando un contador interno de ticks de FreeRTOS alcance la cuenta pxPreviousWakeTime + xTimeIncrement. La propia función actualiza al argumento pxPreviousWakeTime con el resultado de la operación anterior para que vuelva a ser usada como punto de referencia.

Veamos un ejemplo:

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

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

   pinMode( *pin, OUTPUT );

   const TickType_t period_1 = pdMS_TO_TICKS( 100 );

   const TickType_t period_2 = pdMS_TO_TICKS( 200 );

   TickType_t last_wake_time = xTaskGetTickCount();

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

      vTaskDelayUntil( &last_wake_time, period_1 );

      digitalWrite( *pin, LOW );

      vTaskDelayUntil( &last_wake_time, period_2 );
   }
}

void setup()
{
   static uint8_t led1 = 13;

   xTaskCreate( 
         led_task_1,
         "LD1",
         128,
         (void*) &led1,
         tskIDLE_PRIORITY,
         NULL );

   vTaskStartScheduler();
}

void loop() 
{
}
  • Líneas 10 y 12: Establecemos los periodos ON/OFF para el LED. Observa que las variables tienen el tipo TickType_t, y que estamos usando la macro de conversión pdMS_TO_TICKS().
  • Línea 14: La variable last_wake_time es el punto de referencia para mantener un tiempo absoluto. La función xTaskGetTickCount() devuelve el tiempo actual del sistema en en ticks (es decir, la cantidad de ticks transcurridos desde que el sistema se inicio).
  • Líneas 20 y 24. Usamos la función. La variable last_wake_time es actualizada dentro de vTaskDelayUntil(), por eso debemos mandar su “original” (su dirección) a través del operador referencia, &.

(Si no recuerdas cómo pasar argumentos a las tareas, líneas 6 y 36, quizás quieras visitar esta lección, Pasando argumentos a las tareas. No es requisito usar este mecanismo para utilizar a vTaskDelayUntil()).

¿Qué sigue?

En esta lección hemos visto los estados por los que pasan las tareas. También vimos las dos funciones que FreeRTOS proporciona para realizar tareas periódicas, vTaskDelay() y vTaskDelayUntil(), una relativa y otra absoluta, respectivamente. También platicamos de la importancia de utilizar funciones que liberen a la CPU cuando están esperando un evento en sistemas de tiempo real, y que funciones nativas de Arduino se “cuelgan” mientras están haciendo su trabajo.

En la siguiente lección voy a mostrarte cómo enviar notificaciones simples entre tareas.

Índice del curso

Espero que esta lección haya sido de tu interés. Si fue así podrías suscribirte a mi blog, o escríbeme a fjrg76 dot hotmail dot com, o comparte esta entrada con alguien que consideres que puede serle de ayuda.


Fco. Javier Rodríguez
Escrito por:

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/

Ver todas las entradas

2 COMENTARIOS