Cómo notificar a una tarea en FreeRTOS desde dos o más tareas

Cómo notificar a una tarea en FreeRTOS desde dos o más tareas

En esta lección de mi curso gratuito Arduino en tiempo real te mostré cómo utilizar un mecanismo para enviar notificaciones directas de tarea a tarea. En esta nota especial te mencioné que FreeRTOS incluyó un arreglo de notificaciones independientes en cada tarea, lo que significa que varias tareas podrían notificar a una misma (en teoría).

Aunque la documentación oficial y sus ejemplos no muestra cómo lograr esto último, me puse a pensar y llegué a una posible solución. Debo añadir que ni en el foro oficial de FreeRTOS saben para qué sirve tener un arreglo de notificaciones.

Por otro lado, en la nota especial señalé que por alguna razón las notificaciones indexadas (así le llama FreeRTOS a tener un arreglo de notificaciones) no funcionan, por lo que el desarrollo que te mostraré está basado en el microcontrolador de 32 bits LPC1549 y su herramienta de desarrollo gratuita LPCXpresso. Por supuesto que lo que mencionaré aplica para casi cualquier otro microcontrolador, excepto el ATmega328. Por esta razón no he puesto la presente entrada en el curso, ya que al no utilizar al chip ATmega328, no aplica.

Tabla de contenidos

¿Qué es y qué necesito?

Notificaciones directas

Las notificaciones directas a la tarea (direct to task notifications) de FreeRTOS son un mecanismo que permite que una tarea envíe un dato de 32 bits a otra tarea. Así de simple. El valor recibido puede tomar varias interpretaciones, desde un dato puro hasta como semáforos binarios o contadores. Algunas ventajas de este mecanismo es que consume pocos o casi nada de recursos, y además está integrado al sistema operativo, por lo que una tarea puede bloquearse mientras la notificación le llega; de ese modo la tarea bloqueada no consume ciclos de CPU.

Para más detalles de qué son y cómo se configuran por favor visita esta y esta lecciones.

Terminología

Tarea productora. Es una tarea que genera información para enviársela a otra tarea.

Tarea consumidora. Es una tarea que consume la información que otras tareas han generado

Teoría

La idea detrás del desarrollo que estoy a punto a describirte es que cada tarea productora escribirá su información en una entrada específica del arreglo de notificaciones; y además la tarea consumidora tendrá una entrada extra para recibir notificaciones. Esto es, cada vez que una tarea productora escriba en la entrada correspondiente, también deberá avisar que lo hizo señalizando en dicha entrada extra. Por ejemplo, si tu aplicación tiene 3 tareas productoras, entonces utilizarás 4 entradas en el arreglo de notificaciones.

La tarea consumidora será desbloqueada cada vez que una tarea productora se lo notifique. Luego leerá la información que le fue transmitida y volverá al estado de bloqueo.

PARA RECORDAR: FreeRTOS incluye dos mecanismos para transferir flujos y mensajes de información (Streams buffers y Message buffers, respectivamente). Éstos utilizan a la entrada 0 del arreglo de notificaciones, en caso de que actives a cualquiera de los dos. Entonces, si tu aplicación tiene 3 tareas productoras, utilizarás 4 entradas en el arreglo de notificaciones, pero deberías declarar 5:

/* Archivo de configuración FreeRTOSConfig.h */

#define configUSE_TASK_NOTIFICATIONS 1
// avisas que quieres utilizar notificaciones directas

#define configTASK_NOTIFICATION_ARRAY_ENTRIES   5
// avisas que cada tarea tendrá un arreglo de 5 entradas

Derivado de la configuración anterior podrás notar que hay un poco de desperdicio de memoria, ya que si tu aplicación tiene 7 tareas, entonces cada una de ellas tendrá un arreglo de 5 elementos para las notificaciones, en total 35 elementos de 32 bits cada uno, de los cuales ocuparás 5 (para este ejemplo). Tómalo en cuenta para que tengas cuidado cuando establezcas el número de entradas. Si no planeas utilizar los streams ni los messages, podrías, para este ejemplo, indicar 4 entradas.

Desarrollo

Para mostrarte cómo dos o más tareas pueden notificar a una misma, en este ejemplo vamos a tener dos tareas productoras y una consumidora, y no vamos a usar streams ni messages, por lo que nuestra configuración quedaría así:

/* Archivo de configuración FreeRTOSConfig.h */

#define configUSE_TASK_NOTIFICATIONS          1
#define configTASK_NOTIFICATION_ARRAY_ENTRIES 3

Como ya te decía, cada tarea productora deberá notificar la información y una notificación. La siguiente tarea simplemente incrementa en dos un valor y se lo transmite a la tarea consumidora. Por supuesto que tus tareas podrían realizar algo mucho más complejo, incluyendo notificar desde interrupciones:

void Producer1_task( void* pvParameters )
{
   pvParameters = pvParameters;

   uint32_t cont = 1;

   while( 1 )
   {
      vTaskDelay( pdMS_TO_TICKS( ( rand() % 100 ) + 100 ) );

      xTaskNotifyIndexed(
    		  consumer_h,            // handler de la tarea consumidora
			  1,                      // índice en el arreglo de notificaciones de la tarea consumidora para recibir la información
			  (uint32_t) cont,        // información que queremos transmitir
			  eSetValueWithOverwrite  // sobreescribe la información si la anterior no fue leída
	  );

      xTaskNotifyIndexed(
           consumer_h,             // handler de la tarea consumidora
			  0,                      // índice en el arreglo de notificaciones de la tarea consumidora para recibir "notificaciones"
			  ( 1 << 1 ),             // el bit 1 (0x01) corresponde a esta tarea
			  eSetBits                // hace una OR entre el valor 0x01 y lo que ya estuviera en el campo de notificaciones
	  );

      // tu proceso...

      cont += 2;

   }
}

Las tareas productoras necesitan conocer el handler de la tarea consumidora, consumer_h. Este handler lo puedes pasar como argumento a la tarea, o declararlo global. En este ejemplo, como verás más adelante, me fui por el camino fácil y poco recomendado: lo declaré global.

Así mismo, las tareas productoras tienen mayor prioridad que la tarea consumidora. Veamos el código de la otra tarea productora:

void Producer2_task( void* pvParameters )
{
	pvParameters = pvParameters;

   uint32_t cont = 2;

   while( 1 )
   {
      vTaskDelay( pdMS_TO_TICKS( ( rand() % 100 ) + 125 ) );

      xTaskNotifyIndexed(
    		  consumer_h,             // handler de la tarea consumidora
			  2,                      // índice en el arreglo de notificaciones de la tarea consumidora para recibir la información
			  (uint32_t) cont,        // información que queremos transmitir
			  eSetValueWithOverwrite  // sobreescribe la información si la anterior no fue leída
	  );

      xTaskNotifyIndexed(
           consumer_h,             // handler de la tarea consumidora
			  0,                      // índice en el arreglo de notificaciones de la tarea consumidora para recibir "notificaciones"
			  ( 1 << 2 ),             // el bit 2 (0x02) corresponde a esta tarea
			  eSetBits                // hace una OR entre el valor 0x02 y lo que ya estuviera en el campo de notificaciones
	  );

      // tu proceso...

      cont += 2;
   }
}

Ambas tareas son muy parecidas, pero lo hice así para mantener simple al ejemplo y concentrarme en la finalidad del mismo.

Después tenemos a la tarea consumidora, la cual como ya dije, se bloquea a la espera de ser notificada:

void Consumer_task( void* pvParameters )
{
   uint32_t p1_value;
   uint32_t p2_value;

   uint32_t         notified_value;
   const TickType_t max_block_time = pdMS_TO_TICKS( 2000 );

   while( 1 )
   {
      BaseType_t 
      ret_val = xTaskNotifyWaitIndexed(
         0,                       // índice donde recibe las notificaciones
         0,                       // no pone a cero ningún bit a la entrada a la función
         ( 0x01 | 0x02 ),         // pone a cero los bits 1 y 2 a la salida de la función
         &notified_value,         // en esta variable guarda quién lanzó las notificaciones (se usa más adelante)
         max_block_time );        // tiempo de espera antes de indicar error

      if( ret_val != pdFALSE ){   // las notificaciones se entregaron antes de que el tiempo terminara
         
         if( notified_value & 0x01 ){ // la tarea Producer1_task puso una notificación
            xTaskNotifyWaitIndexed( 
                  1,                  // en esta entrada está la información
                  0,                  // no pone a cero ningún bit a la entrada a la función
                  0xffffffff,         // pone el campo de información a 0 a la salida de la función
                  &p1_value,          // en esta variable guarda la información
                  0 );                // no espera ya que sabemos que hay información lista para ser leída

            // tu proceso ...

            DEBUGOUT( "%d\n\r", p1_value ); // imprime el dato
            Board_LED_Toggle( RED );
         }

         if( notified_value & 0x02 ){ // la tarea Producer2_task puso una notificación
            xTaskNotifyWaitIndexed( 
                  2,                  // en esta entrada está la información
                  0,                  // no pone a cero ningún bit a la entrada a la función
                  0xffffffff,         // pone el campo de información a 0 a la salida de la función
                  &p2_value,          // en esta variable guarda la información
                  0 );                // no espera ya que sabemos que hay información lista para ser leída

            // tu proceso ...

            DEBUGOUT( "%d\n\r", p2_value ); // imprime el dato
            Board_LED_Toggle( BLUE );
         }
      }
   }
}

Cuando cualquiera de las tareas productoras “avisa” que hay un dato listo, esta función sale del estado Block, pero como son dos o más tareas las que pueden despertarlo, debe verificar, una a una, la tarea que hizo la notificación revisando su respectivo bit. Una vez que la ha determinado, obtiene de manera inmediata la información. Dado que ya sabemos que hubo un dato, no hay necesidad de establecer un tiempo de espera, por eso el argumento timeout vale 0.

¿Qué sigue?

Hoy hemos visto una forma de utilizar al arreglo de notificaciones para que dos o más tareas notifiquen a una misma. Esta idea la podemos extender a interrupciones (ISRs) para que sea una tarea, y no las propias ISRs, quienes procesen la información (a este procedimiento le llamamos diferred tasks, o en español, tareas diferidas. Escribí un artículo sobre esto, en inglés, aquí en mi blog alternativo).

Ten en cuenta que este procedimiento es útil y ahorra muchísimos recursos, pero si tus tareas deben transferir grandes cantidades de información quizás debas considerar la utilización de otros mecanismos como los streams, los messages, o las colas.

Índice del curso

Espero que esta entrada 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