Funciones de retardo en FreeRTOS

¿Sabías que FreeRTOS tiene 3 y media funciones para generar retardos? En realidad son 2, pero incluye una variante y una extra que se usa para abortar la misión. Estas funciones son:

  • vTaskDelay()
  • vTaskDelayUntil()
  • xTaskDelayUntil()
  • xTaskAbortDelay()

En esta entrada trataré de explicarte cada una de ellas.

Nota: Para la siguiente explicación utilicé ticks del sistema como medida de tiempo, aunque al final de cuenta nosotros usemos milisegundos (ms). No te distraigas con esos detalles, ya que la conversión de ticks a milisegundos es muy fácil.

RECUERDA QUE: Un tick es una señal regular de tiempo que utilizamos como reloj del sistema.

Estaré utilizando los términos dormir y despertar a lo largo de la explicación, aunque los términos correctos son: mandar a la tarea al estado Blocked, y sacar a la tarea del estado Blocked y ponerla en el estado Ready, respectivamente. Una tarea en el estado Blocked está a la espera de un evento, como el tiempo transcurrido, y no usa ciclos de procesador. De ahí la analogia de que la tarea duerme. Una tarea en el estado Ready está lista para ejecutarse, aunque debe esperar su turno.

Sin más, comencemos.

vTaskDelay()

Esta es la más simple de todas las funciones; la tarea se va a dormir por el número de ticks que indiques en el único argumento de la función:

void vTaskDelay( const TickType_t xTicksToDelay );

El tiempo que la tarea dormirá comienza a contar a partir del momento en que se hizo la llamada; por ello decimos que el tiempo es relativo.

Por ejemplo, digamos que el contador de ticks del sistema vale 50 en el momento de la llamada, y que tú quieres que la tarea se duerma durante 100 ticks. Así la tarea se despertará cuando el contador de ticks del sistema llegue a 150 ticks. Cuando el contador de ticks llega a esta número, el sistema despertará a la tarea. Ahora supongamos que el trabajo de la misma es de 5 ticks antes de irse a dormir otra vez, por lo que cuando haga la siguiente llamada a vTaskDelay() el contador de ticks será de 155, por lo que la tarea se despertará cuando éste llegue a 255; y así sucesivamente. Como podrás notar, el tiempo se va desplazando.

El momento en que la tarea se debe despertar se va desplazando.

Que el tiempo se esté desplazando puede no ser importante para muchas de las tareas comunes en nuestros sistemas: un LED que parpadea, la lectura y decodificación de un teclado, imprimir a una pantalla, etc; por eso creo que es la función de retardo que más se utiliza.

El uso de esta función es muy simple, como lo muestra el siguiente ejemplo:

void task_delay( void* pvParameters )
{
   (void) pvParameters;

   pinMode( 13, OUTPUT );

   bool led_state = false;

   while( 1 )
   {
      digitalWrite( 13, !led_state );
      led_state = !led_state;

      vTaskDelay( pdMS_TO_TICKS( 20 ) );
   }
}

RECUERDA QUE: La macro pdMS_TO_TICKS(x) convierte el tiempo en milisegundos dado por el argumento x a ticks, los cuales son utilizados por FreeRTOS. Cuando un tick sucede cada milisegundo, entonces tenemos un mapeo de 1 a 1; sin embargo, es mala idea escribir directamente en ticks. Imagina lo que sucedería si más adelante debes modificar los ticks de tu sistema a, digamos, cada 10ms.

TRIVIA: ¿Existe alguna diferencia entre esta función y la función delay() de Arduino? Absolutamente. La función delay() de Arduino se queda con la CPU durante todo el tiempo que dure el retardo, y mientras tanto no se puede realizar ningún trabajo útil (fuera de las interrupciones); esta situación es terrible de cara a los sistemas de tiempo real. La función de FreeRTOS vTaskDelay() (y el resto de ellas) libera a la CPU mientras dura el retardo para que otras tareas realicen un trabajo útil; esto es sensacional para los sistemas de tiempo real.

vTaskDelayUntil()

Esta función también manda dormir a la tarea que la llamó, pero a diferencia de vTaskDelay(), el momento en que la tarea despertará es con referencia a un punto del tiempo en el pasado; y por ello decimos que el tiempo es absoluto. Mas aún, en cada llamada a la función dicha referencia temporal se va actualizando automáticamente; y para que ambas cosas sucedan (punto de referencia en el pasado y actualización del mismo) esta función requiere un parámetro extra:

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

El primer argumento, pxPreviousWakeTime, es la referencia temporal en el pasado; mientras que el segundo argumento, xTimeIncrement, indica el número de ticks del sistema que la tarea debe dormir.

Para que puedas usar a esta función deberás establecer la macro INCLUDE_vTaskDelayUntil al valor 1 en el archivo FreeRTOSConfig.h:

#define INCLUDE_vTaskDelayUntil 1

La inicialización del punto de referencia lo obtenemos con la función xTaskGetTickCounts():

TickType_t last_wakeup = xTaskGetTickCount();

Para dar un ejemplo con números, digamos que en un principio last_wakeup vale 50 ticks y tú quieres que tu tarea duerma por 100 ticks. Con estos datos la función:

  1. Primero calcula que debe despertarse cuando el contador de ticks del sistema marque 150 ticks (50+100), y le pasa el dato al sistema operativo, el cual se encargará de despertarla en el momento correcto.
  2. Después de hacer el cálculo la función actualiza el parámetro last_wakeup al valor calculado, 150 en este ejemplo para quedar listo para la siguiente llamada.
  3. Finalmente la tarea se va a dormir.

Una vez el sistema operativo observa que el número de ticks del sistema ha llegado a 150, entonces despierta a la tarea, y al igual que en el ejemplo anterior, digamos que ésta usa 5 ticks para hacer trabajo útil antes de irse a dormir otra vez. Cuando la tarea hace nuevamente la llamada a vTaskDelayUntil() ésta calcula que el nuevo tiempo para despertarse es lo que ya traía, 150, más lo que tú quieres, 100, es decir, 250. Esto es, mientras la tarea hacía su trabajo, ¡el tiempo ya estaba contando! Por lo tanto, y a diferencia de vTaskDelay(), ¡no hay desplazamientos del tiempo!

El momento en que la tarea se despierta NO se desplaza.

El siguiente ejemplo muestra cómo la tarea se ejecuta exactamente cada 20ms, independiente del tiempo que ocupe la tarea en hacer su trabajo:

void task_vdelayUntil( void* pvParameters )
{
   (void) pvParameters;

   TickType_t last_wakeup = xTaskGetTickCount();
   // obtenemos la primer referencia temporal

   pinMode( 2, OUTPUT );
   bool led_state = false;
   while( 1 )
   {
      digitalWrite( 2, (led_state = !led_state) );

      vTaskDelayUntil( &last_wakeup, pdMS_TO_TICKS( 20 ) );
      // 1. El tiempo para despertarse en el futuro es: last_wakeup + pdMS_TO_TICKS( 20 )
      // 2. La referencia temporal se actualiza con: last_wakeup = last_wakeup + pdMS_TO_TICKS( 20 )
   }
}

xTaskDelayUntil()

BaseType_t xTaskDelayUntil( TickType_t *pxPreviousWakeTime,<br>const TickType_t xTimeIncrement );

Igual que vTaskDelayUntil(), pero esta función devuelve un valor que indica si la tarea efectivamene se fue a dormir o no (pdTRUE, la tarea se fue a dormir; pdFALSE, la tarea no se fue a dormir).

Para que puedas usar a esta función deberás establecer la macro INCLUDE_xTaskDelayUntil, al valor 1 en el archivo FreeRTOSConfig.h:

#define INCLUDE_xTaskDelayUntil 1

NOTA: Esta funcionalidad también activa a vTaskDelayUntil(), y por alguna razón desconocida, si no desactivas a esta última, el sistema te marcará un error. Es decir, si quieres utilizar a xTaskDelayUntil() deberás hacer lo siguiente:

//#define INCLUDE_vTaskDelayUntil 1  /* no sirve ponerla a cero, hay que comentarla o borrarla */
#define INCLUDE_xTaskDelayUntil 1

Una tarea que usa a esta función podría no irse a dormir en el caso de que la suma del punto de referencia más el tiempo deseado fuera menor que la cuenta actual del contador de ticks del sistema; es decir, el tiempo actual del sistema “les ganó” (o se les adelantó).

Por ejemplo, digamos que la última referencia de tiempo es 50, y el tiempo deseado sea 100, en teoría la tarea debería despertarse cuando el contador de ticks valga 150. Luego supongamos que éste ya va en 200 porque una tarea “se colgó” un poco más de lo esperado, entonces, como 150 es menor que 200, la tarea no se duerme y la función regresa inmediatamente.

Esta situación podría darse si una tarea de más alta prioridad toma más tiempo de CPU del necesario, y por lo tanto no se le presta a nuestra tarea, por lo cual el defase es inminente.

Una tarea de mayor prioridad tardó mucho e hizo que la tercera llamada a xTaskDelayUntil() no se realizara; esto es, no hubo un tercer retardo. En esta imagen el tiempo cuando la llamada se hace es 33; sin embargo, la suma de la referencia temporal anterior (20) más el tiempo deseado (10) es menor que 33, y por eso el retardo no se ejecuta y la función devuelve pdFALSE. En esta situación deberías resincronizar la referencia temporal.

Si es importante para tu aplicación conocer si realmente se llevó a cabo el retardo, entonces deberías verificar el valor devuelto y hacer algo al respecto. El siguiente ejemplo muestra una escenario hipotético de cuando el retardo no se lleva a cabo:

void task0( void* pvParameters )
{
   (void) pvParameters;

   TickType_t last_wakeup = xTaskGetTickCount();

   uint16_t cont = 5;
   // cada 5 ciclos el sistema perderá sincronía

   while( 1 )
   {
      if( xTaskDelayUntil( &last_wakeup, pdMS_TO_TICKS( 500 ) ) == pdFALSE )
      {
         Serial.println( "T0: Perdí el deadline :(" );

         last_wakeup = xTaskGetTickCount();
         // recuperamos la sincronía

      } else
      {
         Serial.println( "T0: Ok :)" );

         --cont;
         if( cont == 0 ) // forzamos a que pierda el tiempo en el futuro ...
         {
            cont = 5;
            
            vTaskDelay( pdMS_TO_TICKS( 1000 ) );
            // simulamos una situación por la cual last_wakeup+500ms dejaría de cumplirse
         }
      }
   }
}

PRECAUCIÓN: Perder la sincronía puede traer varios problemas a tu sistema, entre ellos que parezca que se colgó (aunque no es así, simplemente el tiempo en el futuro se estableció muy, muy lejos). Por eso, cuando detectes que el retardo no se llevó a cabo deberías siempre recuperar la sincronía, tal cual como lo muestra el ejemplo.

xTaskAbortDelay()

BaseType_t xTaskAbortDelay( TaskHandle_t xTask );

Esta función despierta a la tarea indicada en su único argumento xTask. xTask es el handler de la tarea y el cual puedes obtener en el momento en que la creas, aunque también lo puedes obtener con la función xTaskGetHandle(). Además, también nos avisa, con el valor pdPASS, si la tarea efectivamente fue despertada. En caso de que la tarea no hubiera estado dormida en el momento de la llamada de esta función, entonces devolverá el valor pdFAIL.

Ten mucho cuidado cuando uses a esta función para despertar a una tarea que se durmió tras una llamada a vTaskDelayUntil() o xTaskDelayUntil(), ya que la referencia de tiempo podría verse severamente afectada haciendo que el sistema pierda la sincronización (como ya lo mencioné). Este problema no existe con vTaskDelay().

El siguiente ejemplo está completo y es un poco más complejo que los anteriores ya que para mostrarte el uso de esta función tuve que recurrir a otras herramientas, como la sincronización (si no sabes, o quieres recordar cómo se usan los semáforos binarios en FreeRTOS, puedes entrar a esta lección de mi curso gratuito de Arduino en tiempo real) entre tareas. Lo realicé para mi proyecto Molcajete (Arduino+FreeRTOS+programación desde una terminal), pero tú puedes adaptarlo a la plataforma que estés utilizando. Y por supuesto que también podrás interrumpir el sueño de una tarea conforme a las necesidades de tu aplicación:

#include <Arduino.h>
void initVariant() __attribute__((weak));
void setupUSB() __attribute__((weak));
int atexit(void (* /*func*/ )()) { return 0; }
void initVariant() { }
void setupUSB() { }

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


TaskHandle_t task0_handler;
// vamos a necesitar el handler de la tarea a la cual le interrumpiremos el sueño

TaskHandle_t task1_handler;
// lo usamos para la sincronización


void task1( void* pvParameters )
{
   pinMode( 13, OUTPUT );
   bool state = false;

   while( 1 )
   {
      vTaskDelay( pdMS_TO_TICKS( 500 ) ); // este es el sueño que vamos a interrumpir de vez en cuando

      digitalWrite( 13, (state=!state) );

      xTaskNotifyGive( task0_handler ); // sincronizamos a esta tarea con task0
   }
}

void task0( void* pvParameters )
{
   pinMode( 2, OUTPUT );
   bool state = false;

   uint16_t cont = 6;

   while( 1 )
   {
      ulTaskNotifyTake( pdTRUE, portMAX_DELAY ); // esperamos a que task0 nos avise...

      digitalWrite( 2, (state=!state) );

      --cont;
      if( cont == 0 )
      {
         cont = 6;

         vTaskDelay( pdMS_TO_TICKS( 100 ) ); // hacemos tiempo ...

         xTaskAbortDelay( task1_handler ); // aquí interrumpimos el sueño de la tarea task1
      }
   }
}

int main(void)
{
   cli();
   init();
   initVariant();
#if defined(USBCON)
	USBDevice.attach();
#endif


   if( xTaskCreate( 
         task1,
         (const portCHAR *)"T1",
         128 * 2,
         ( void* ) 0,
         tskIDLE_PRIORITY + 1,  // tiene mayor prioridad
         &task1_handler ) != pdPASS ) 
      configASSERT( 0 );

   if( xTaskCreate( 
         task0,
         (const portCHAR *)"T0",
         128 * 2,
         ( void* ) 0,
         tskIDLE_PRIORITY,
         &task0_handler ) != pdPASS ) 
      configASSERT( 0 );

   vTaskStartScheduler();

   while( 1 )
   {
      // nada
   }
}

¿Conocías a todas las funciones de FreeRTOS para generar retardos? ¿Cuál es la que más utilizas? ¡Platícamelo en los comentarios!

¿Te gustaría que platique sobre algún tema de FreeRTOS en particular? ¡Házmelo saber en los comentarios!

GRATIS
¿Te gustaría aprender a programar aplicaciones con Arduino en tiempo real? Entonces quizás quieras descargarte mi proyecto <Molcajete> y echarle un ojo a mi curso de <Arduino en tiempo real>.


Si el artículo te gustó, quizás quieras suscribirte para seguir recibiendo contenido similar, y de vez en cuando recibir un pequeño presente de mi parte para agradecer tu confianza, tal como mi guía gratuita para depurar programas de Arduino UNO en Linux.


Fco. Javier Rodríguez
Últimas entradas de Fco. Javier Rodríguez (ver todo)
Esta entrada fue publicada en Blog por Fco. Javier Rodríguez. Guarda el enlace permanente.

Acerca de 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/

Deja una respuesta