Notificaciones (II)

En la lección anterior vimos la forma de pasar un dato de hasta 32 bits utilizando las notificaciones directas a las tareas que FreeRTOS provée como un mecanismo incorporado «de fábrica» (todas las tareas incluyen un campo de 32 bits). En la lección de hoy veremos cómo utilizar este mismo mecanismo pero para pasar notificaciones (banderas, avisos) de una tarea a otra.

Tabla de contenidos

¿Qué es y qué necesito?

Este tipo de notificaciones son a nivel de bit; un bit puesto a 1 podría indicar que un sensor se activó, o que una lectura del ADC ya está lista para ser leída, o que un botón se presionó. La tarea productora pondría uno o varios bits a 1 y después la tarea receptora, esperará por el evento, liberando la CPU hasta que le lleguen una o varias notificaciones.

El caso más simple es el de las notificaciones de un sólo bit: la conversión terminó, llegó un byte por el puerto serial (sólo avisa que el byte llegó, no el valor propio de la información), un botón fue presionado (no el valor del botón presionado).

Casos más complejos podrían ser que, además de indicar que el eventó sucedió, pasar la información relacionada a éste. En el ejemplo de una conversión podemos pasar, además, el canal del ADC y el valor de la lectura. En el caso del byte del puerto serial podemos pasar, también, el valor del byte. Y finalmente, para el ejemplo del botón podemos indicar el botón que fue presionado. En todos y cada uno de estos escenarios estaremos utilizando el campo de 32 bits asociado a las notificaciones.

Operaciones a nivel de bit

Máscaras de bits

Antes de entrar en los detalles para usar las notificaciones debemos recordar las máscaras de bits, o simplemente máscaras. Una máscara es una secuencia de bits que indican cuáles bits nos interesan y cuáles no tienen importancia. Para la siguiente explicación usaremos 8 bits, por simplicidad, pero la idea se traslada directamente a los 32 bits disponibles.

Entonces, tenemos 8 bits cuyas posiciones van de la 0 a la 7, contando de derecha a izquierda:

dibujo

Y supongamos que de estos bits nos interesan aquellos en las posiciones 1, 3 y 6. Por lo tanto, debemos descartar los bits en las posiciones 0, 2, 4, 5, 7. Para esto deberemos crear una máscara con 1s en los bits que nos interesan, y con 0s en los bits que no:

dibujo

Este valor lo podemos expresar ya sea en binario o en hexadecimal, siendo esta última la manera más común

#define MASK 0x4A

Una vez que tenemos a la máscara debemos aplicársela al dato del cual queremos discriminar los bits, es decir, quedarnos con los bits que nos interesan. Para ello utilizaremos la operación AND a nivel de bit de C, &. Digamos que nuestro dato tiene el valor 0xFD (en binario, 1111 1101), entonces, de forma gráfica, la aplicación de la máscara es así:

Y en código en C nos queda así:

uint8_t dato = 0xFD;
uint8_t res = dato & MASK; // res <- 0x48

El resultado es 0x48 (en binario, 0100 1000). Esto significa que de nuestro dato el bit 1 vale 0, el bit 3 vale 1, y el bit 6 vale 1. Ya dependerá de nuestra aplicación qué haremos con esta información, lo cual por supuesto estará en los ejemplos que veremos más adelante.

(Si quieres profundizar en las operaciones a nivel de bit, de las cuales hay muchas, puedes ir aquí; y para revisar las operaciones a nivel de bit en C, puedes ir aquí (en inglés)).

Notificaciones de 1 bit

El caso más simple es cuando tienes una sola notificación, la cual puedes codificar con un sólo bit. Si el bit está a 0, el evento no ha sucedido, y si el bit está a 1, entonces el eventó sucedió.

También tenemos el caso en que metemos varios bits independientes en el mismo campo de 32 bits. Esto es útil cuando una misma tarea debe notificar a otra tarea de diversas situaciones, donde cada situación requiere un bit. En el ejemplo a continuación utilizaremos varios bits individuales.

NOTA: Las notificaciones de un sólo bit se parecen más a un mecanismo de sincronización llamado semáforo binario. Éstos no incluyen ningún tipo de información, solamente avisan que un evento sucedió (o que no ha sucedido). Por lo tanto, y como el propio FreeRTOS lo menciona, una forma rápida y barata de implementar semáforos binarios es a través de las notificaciones. Debo señalar, sin embargo, que FreeRTOS incluye semáforos (binarios y contadores) con más características (entre otras, un productor puede notificar a varios receptores), pero que, naturalmente, consumen más recursos.

Ejemplo

En este ejemplo haremos lecturas analógicas para la temperatura ambiente (utilizando al sensor LM35), y avisaremos a la tarea receptora si la temperatura cae en uno de 4 estados: menor a 17 grados centígrados, entre 17 y 24.99 grados, entre 25 y 28.99 grados, y mayor o igual a 29 grados. Cada estado está representado por un bit. Escogí los bits 0-3 del campo de notificación, pero por supuesto tú puedes utilizar la distribución que desees:

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

TaskHandle_t consumidor_h;

void Productor1_task( void* pvParameters )
{
   TickType_t last_wake_time = xTaskGetTickCount();

   while( 1 )
   {
      vTaskDelayUntil( &last_wake_time, pdMS_TO_TICKS( pdMS_TO_TICKS( 1000 ) ) );

      float temp = ( analogRead( A0 ) * 5.0 * 100.0 ) / 1024;

      uint32_t flags = 0;

      if( temp < 17.0 ) flags = 0x00000001;
      else if( temp < 25.0 ) flags = 0x00000002;
      else if( temp < 29.0 ) flags = 0x00000004;
      else flags = 0x00000008;

      xTaskNotify( consumidor_h, flags, eSetValueWithOverwrite );
   }
}

void Consumidor_task( void* pvParameters )
{
   while( 1 )
   {
      uint32_t res;

      if( xTaskNotifyWait( 0xfffffff0, 0xffffffff, &res, pdMS_TO_TICKS( 2000 ) ) != pdFALSE ){

         if( res & 0x01 ){
            Serial.println( "Hace un poco de frío.\n" );
         } else if( res & 0x02 ){
            Serial.println( "El tiempo es agradable." );
         } else if( res & 0x04 ){
            Serial.println( "Hace algo de calor." );
         } else{
            Serial.println( "Hace mucho calor." );
         }

      } else{
         Serial.println( "ERR: No se recibió la notificación." );
      }
   }
}

void setup()
{
   Serial.begin( 115200 );

   xTaskCreate( Productor1_task, "PRD1", 256, NULL, tskIDLE_PRIORITY + 1, NULL );

   xTaskCreate( Consumidor_task, "CONS", 256, NULL, tskIDLE_PRIORITY, &consumidor_h );

   vTaskStartScheduler();
}

void loop() 
{
}

En la tarea consumidora, Consumidor_task(), podrás notar 2 cosas en la llamada a la función xTaskNotifyWait():

  • Utilicé las máscaras de entrada y salida (primero y segundo parámetro, respectivamente) para el campo de notificaciones.
  • Establecí un tiempo de guarda de 2000 ms.

Recordemos la firma de dicha función:

El parámetro ulBitsToClearOnEntry es una máscara que pone a 0 los bits indicados del campo de notificación cuando se entra a la función. Lo podemos utilizar para descartar los bits que no nos interesen. Un bit a 1 significa que el bit en esa posición del campo de notificaciones será puesto a 0. En el ejemplo observarás que descarté los bits 4-31, ya que en la tarea productora solamente utilicé del 0-3.

El parámetro ulBitsToClearOnExit es una máscara que pone a 0 los bits indicados del campo de notificación cuando se sale de la función. Lo podemos utilizar para avisar que ya leímos el campo de notificación. Recuerda que la función xTaskNotify() tiene un parámetro de control, eAction (de tipo eNotifyAction), con el cual la tarea productora podría fallar si pretendieras escribir una nueva notificación sin que la tarea consumida haya leído la anterior (cuando eAction es eSetValueWithoutOverwrite). En el ejemplo establecí que los 32 bits se pusieran a 0 a la salida de la función.

Finalmente, establecí un tiempo de guarda de 2000 ms. La tarea productora realiza una lectura cada 1000 ms, pero si por alguna razón no hiciera la notificación dentro de esa ventana de 2000 ms, entonces la tarea consumidora imprimiría un mensaje de error.

A continuación tienes un fragmento de la salida de este ejemplo:

Ejercicio: Comenta la línea

xTaskNotify( consumidor_h, flags, eSetValueWithOverwrite );

en la tarea productora y observa el resultado.

Codificando bits y datos

Otra cosa interesante que podemos hacer con el campo de notificaciones es codificar en el mismo tanto datos como bits. Al ejemplo anterior podríamos agregarle que, además de indicar con bits el rango de temperatura, notifique la temperatura (para esto deberemos haremos algunos cambios en el código).

Para lograr lo anterior necesitaremos revisar dos cosas antes: poner bits a 1 y desplazamientos.

Poniendo bits a 1

En la sección anterior vimos cómo poner a 0 bits de una máscara usando al operador a nivel de bits de C ‘and’, &. Lo que necesitamos ahora es poner bits a 1; para ello necesitamos al operador ‘or’ a nivel de bits de C, | (una barra). Requeriremos una máscara que indique los bits a poner a 1, y por supuesto el dato (variable) en el cual pondremos a 1 dichos bits.

Por ejemplo, supón que nuestro dato tiene el valor 0x28 y queremos poner sus bits en las posiciones 0, 1, 4, 5 a 1. Entonces creamos una máscara con 1’s en dichas posiciones, y 0’s en el resto: 0x33 (en binario 0011 0011). Después de aplicarle esta máscara a nuestro dato, éste queda con el valor 0x3B:

Los bits que en el dato original ya estaban a 1 y le tocó ponerse a 1, no cambian de valor. Así mismo, los bits que tenían 1 y le aplicas un 0, quedan con 1; es decir, una máscara OR con 0 no afecta el valor del bit.

La operación anterior en C queda como

uint8_t dato = 0x28;
uint8_t res = dato | 0x33; // res <- 0x3B

Desplazando bits

La siguiente operación a nivel de bit es el desplazamiento: mover uno o más bits una o más posiciones a la izquierda o a la derecha. C cuenta con los operadores << y >>, respectivamente. De paso debo comentar que el desplazamiento de un bit a la izquierda equivale a multiplicar al dato por 2; mientras que el desplazamiento de un bit a la derecha equivale a dividir al dato por 2. Dos desplazamientos a la derecha es como multiplicar por 4, y así sucesivamente.

Por ejemplo, si tienes el dato 0x03 (en decimal, 3) y lo desplazas 3 bits a la izquierda, tendrás al final el valor 0x18 (en decimal, 24). Desplazar 3 bits a la izquierda es como multiplicar 2^3=8, y 3*8=24:

La operación anterior en C queda así:

uint8_t dato = 0x03;
uint8_t res = dato << 3; res <- 0x18

Es muy común que el desplazamiento se haga sobre el propio dato; es decir, lo aplicas al dato y lo guardas en el propio dato. Esta operación en C queda así:

uint8_t dato = 0x03; // 0000 0011

dato <<= 3; // dato <- 0x18, es decir, 0001 1000

// la operación "larga" es: dato = dato << 3;

dato >>= 1; // dato <- 0x0C, es decir, 0000 1100, en decimal, 12

Ejemplo

Ahora sí, equipados con las herramientas adecuadas es momento de ver cómo podemos codificar en el campo de notificaciones tanto datos como bits. El ejemplo consta de realizar lecturas en 3 canales del convertidor ADC, una lectura por segundo. La tarea productora notificará a la tarea consumidora el canal del cuál se realizó la lectura (con bits individuales, como el ejemplo pasado) y el valor de la lectura correspondiente (sin procesar). Es decir, un canal del ADC será 0x01, otro canal será 0x02, y otro canal será 0x04. Por otro lado, el dato de 10 bits de la lectura lo insertaremos a partir del bit 16 (este valor no tiene nada de especial, solamente quise abarcar la mayor cantidad posible de bits en el campo de notificación).

NOTA: Mientras trabajaba en el siguiente ejemplo noté un error del compilador de C que Arduino utiliza (avr-gcc): Los desplazamientos a la izquierda NO FUNCIONAN con variables de más de 8 bits. Los desplazamientos a la derecha, sí. En el código notarás que realicé una multiplicación por 65536, lo cual es equivalente a un desplazamiento de 16 bits a la izquierda. Este es un problema de Arduino y su compilador, no de FreeRTOS.

(«No funcionan» significa que sí compilan, pero los resultados son incorrectos, por lo cual no son confiables y no deberías utilizarlos. Una solución es lo que yo hice, multiplicar por 2^n.)

Recuerda que debes activar el uso de las notificaciones en el archivo de configuración FreeRTOSConfig.h. Lo hicimos en el post anterior, pero en caso de que no lo hayas hecho, entra a dicho archivo y escribe la siguiente línea al final del mismo:

#define configUSE_TASK_NOTIFICATIONS 1

Veamos el ejemplo:

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

TaskHandle_t consumidor_h;

void Productor1_task( void* pvParameters )
{
   TickType_t last_wake_time = xTaskGetTickCount();

   uint8_t channel;
   uint16_t read;

   while( 1 )
   {
      for( uint8_t cont = 0; cont < 3; ++cont ){

         vTaskDelayUntil( &last_wake_time, pdMS_TO_TICKS( pdMS_TO_TICKS( 1000 ) ) );

         switch( cont ){
         case 0:
            read = analogRead( A0 );
            channel = 0x01; // 1
         break;

         case 1:
            read = analogRead( A2 );
            channel = 0x02; // 10
         break;

         case 2:
            read = analogRead( A3 );
            channel = 0x04; // 100
         break;

         default:
            while( 1 );
         break;
         }

         uint32_t notif = channel | ( read * 65536 );
         // aquí realizamos la codificación de la lectura y el canal
         //
         // NOTA: El compilador avr-gcc emite código erróneo con los desplazamientos
         // a la izquierda en variables de más de 8 bits. Sin embargo, desplazar a la
         // izquierda n bits equivale a multiplicar el valor por 2^n:
         // 2^16 = 65536

         xTaskNotify( consumidor_h, notif, eSetValueWithOverwrite );

      } // for cont

   } // while 1
}

void Consumidor_task( void* pvParameters )
{
   while( 1 )
   {
      uint32_t res;

      if( xTaskNotifyWait( 0xfc00fff8, 0xffffffff, &res, pdMS_TO_TICKS( 2000 ) ) != pdFALSE ){

         uint8_t channel = res & 0x00000007;
         // aislamos y obtenemos los bits que codifican al canal del ADC

         uint16_t read = ( res & 0x03ff0000 ) >> 16;
         // aislamos y obtenemos los bits que codifican a la lectura


         if( res & 0x01 ){
            Serial.print( "A0: " );
         } else if( res & 0x02 ){
            Serial.print( "A1: " );
         } else if( res & 0x04 ){
            Serial.print( "A2: " );
         } else{
            Serial.println( "Error! " );
         }
         Serial.println( read );

      } else{
         Serial.println( "ERR: No se recibió la notificación." );
      }
   }
}

void setup()
{
   Serial.begin( 115200 );

   xTaskCreate( Productor1_task, "PRD1", 256, NULL, tskIDLE_PRIORITY + 1, NULL );

   xTaskCreate( Consumidor_task, "CONS", 256, NULL, tskIDLE_PRIORITY, &consumidor_h );

   vTaskStartScheduler();
}

void loop() 
{
}

Escogí los canales del ADC A0, A2 y A3 sólo para complicar un poco el código (están «salteados»).

Nota que en primer parámetro de la función xTaskNotifyWait(), en la función consumidora, utilicé la máscara 0xfc00fff8, la cual descarta los bits que no usamos. Te dejo de tarea que lo compruebes (recuerda que el ADC es de 10 bits y que los bits a 1 en la máscara ponen a 0 los bits del campo de notificación).

¿Qué sigue?

Hoy vimos mucha información. Comenzamos con las operaciones a nivel de bit de C, las cuales deben estar en la caja de herramientas de cualquier programador en este lenguaje; sin embargo, hice un pequeño resumen para acordarnos de los temas que utilizaríamos. Vimos también dos usos del campo de notificaciones de las tareas: bits individuales y bits individuales más datos. Mencioné además que descubrí un error (bug) en el compilador que utiliza Arduino. Finalmente, hice mención de un tema que trataremos más adelante, el de los semáforos. Sin embargo, las notificaciones directas se pueden utilizar como semáforos binarios y semáforos contadores «ligeros», es decir, rápidos y que no consumen muchos recursos (a expensas de estar más limitados).

En la siguiente lección exploraremos el tema de los temporizadores por software. Muchas de las tareas que realizan nuestros sistemas están basadas, de una forma u otra, en temporizaciones; y FreeRTOS incluye un mecanismo que nos facilitará la vida.

Í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.


Si encuentras este blog interesante, entonces podrías considerar suscribirte a él y recibir información relevante sobre tecnología y sistemas embebidos, y de vez en cuando, uno que otro regalo.

Notificaciones (I)

Hasta hoy hemos visto ejemplos de tareas aisladas para demostrar algunos puntos. Sin embargo, las diferentes tareas de las que un sistema embebido está compuesto no trabajan en aislamiento; al contrario, todas ellas, aunque independientes, trabajan en equipo con un mismo objetivo. Para lograrlo las tareas deben pasarse información unas con otras y estar en sincronía. En esta lección veremos una primer forma de pasar información, y en un próxima trataremos el tema de la sincronización.

En FreeRTOS existen diferentes maneras para que una tarea pase información a otra, y cada una varía en su grado de complejidad:

  1. Notificaciones directas (más «fácil», pero más limitada).
  2. Colas.
  3. Flujos y mensajes (menos «fácil» pero más poderosa y versátil).

Las notificaciones directas son la forma más fácil y rápida para pasar información. Hoy vamos a ver cómo pasar un dato de 32 bits, y en la segunda parte veremos cómo pasar y procesar banderas (conocidas también como notificaciones). En lecciones posteriores estudiaremos las restantes.

Tabla de contenidos

¿Qué es y qué necesito?

Hasta antes de la versión 10.4.0 de FreeRTOS cada tarea en FreeRTOS incluía un campo de 32 bits para ser utilizado como canal de comunicaciones o de sincronización. A partir de dicha versión se incluyó un arreglo de campos. En esta lección estudiaremos las notificaciones simples (las anteriores a la versión 10.4.0).

El nombre oficial para los campos de 32 bits es «notificaciones» y pertenecen a un mecanismo de FreeRTOS llamado notificaciones directas a las tareas (en inglés, Direct to task notifications, DtTN). Aunque este nombre evoca más sincronización que comunicaciones lo utilizaré junto con el de campos para evitar futuras confusiones. Además, ten en cuenta que las notificaciones también sirven para notificar eventos a las tareas, lo cual es tema de una siguiente lección.

La idea detrás de las notificaciones es simple: si la tarea Ta debe pasarle un dato a la tarea Tb, entonces Ta deberá escribirlo en el campo de notificación de Tb, y ésta utilizarlo. Así de fácil. El campo de datos del cual estoy hablando es un entero de 32 bits, y en éste puedes pasar valores enteros, booleanos, banderas, e inclusive, apuntadores void (para pasar información más compleja).

Toma en cuenta que este mecanismo, DtTN, sólo funciona para pasar un dato a la vez de una tarea a otra. Si tu aplicación necesita pasar una colección (lista) de resultados a una o más tareas, entonces deberás utilizar colas, flujos o mensajes, los cuales son más complejos, más lentos y consumen más recursos que las notificaciones directas.

Quizás te estés preguntando, ¿porqué no simplemente usamos variables globales ? Bueno, por tres razones:

  1. Las variables globales son malas.
  2. La tarea receptora Tb, del ejemplo anterior, puede bloquearse (es decir, pasar al estado Blocked; en términos coloquiales, irse a «dormir») liberando la CPU hasta que reciba la notificación por parte de la tarea Ta. Cuando el dato esté listo, entonces Tb pasa al estado Ready (lista para ejecutarse).
  3. Si el dato no llega dentro de un cierto tiempo, entonces la tarea receptora Tb puede salir del estado Blocked y tomar las acciones necesarias. Esto es muy importante en sistemas embebidos seguros. Además, bajo ciertas configuraciones, existe la posibilidad de que la tarea productora Ta también tome acciones en caso de que por alguna razón la tarea Tb no pudiera accesar al dato.

Si quisieras utilizar variables globales en lugar de las notificaciones de FreeRTOS tu tarea receptora debería estar todo el tiempo despierta preguntando si ya hay un dato disponible; y como consecuencia estaría consumiendo valiosos ciclos de CPU. Y si eso no es suficiente, tendrías que agregar un temporizador para salir de la espera. Muy complicado, mejor usa las notificaciones.

Cuando la tarea emisora Ta escribe el dato en la tarea receptora Tb, la notificación (así se le llama en FreeRTOS) pasa al estado Pending (en español, pendiente) esperando a que Tb lo lea. Este estado Pending de la notificación es el que hace que Tb pase del estado Blocked a Ready. Luego, cuando Tb lo lee, entonces la notificación pasa al estado Not pending. El hecho de que el dato haya sido escrito en Tb no implica que tenga que ser leído inmediatamente, ya que podría pasar un tiempo entre una y otra cosa. Por ejemplo, supón que Tb es una tarea de baja prioridad, y después de que Ta escribió el dato, una tarea de más alta prioridad obtiene la CPU por lo cual, aunque Tb ya fue «notificada», aún no ha podido leer el dato, sino hasta que le toque su turno.

Por otro lado, en algunas configuraciones (que veremos después) si la notificación tiene el estado Not pending (Tb no la ha leído) y la tarea emisora Ta intenta escribir un nuevo dato, entonces la llamada Ta devolverá un error. Esto sirve porque a veces no querrás sobreescribir un valor nuevo sobre uno anterior si la tarea receptora no lo ha leído.

Para empezar a mandar información entre tareas (en inglés, intertask communication) vamos a usar dos funciones: xTaskNotify() y xTaskNotifyWait(). (Aquí puedes ver el conjunto completo de funciones para notificaciones.)

xTaskNotify()

Esta es la función que la tarea productora debe utilizar para pasar el dato. Su firma es:

xTaskToNotify. En este parámetro le pasamos el handler de la tarea receptora.

¿Recuerdas que en esta lección te mencioné que varios mecanismos de FreeRTOS requieren el handler de las tareas para operar sobre ellas? Pues no hay plazo que no se cumpla. Muchas de las funciones que estudiaremos a partir de esta lección necesitan saber sobre quién van a operar, y la única forma es a través de su handler. En FreeRTOS hay dos maneras de obtenerlo:

  1. Guardándolo (en una variable global) cuando creas tareas con xTaskCreate() y xTaskCreateStatic().
  2. Utilizando a la función xTaskGetHandle(), la cual utiliza a la cadena de texto que representó el nombre de la tarea cuando la creaste (aquí puedes ver su descripción).

En esta lección utilizaremos la primer forma, tanto por simplicidad, como para que veas cómo se guarda (ya que no lo hemos hecho en ejemplos anteriores).

ulValue. Aquí escribes el valor de 32 bits que quieres pasar a la tarea receptora xTaskToNotify.

De ser necesario deberás promocionar el dato que quieres escribir al tipo uint32_t.

eAction. Este parámetro le indica a la función qué hacer con el dato de 32 bits. Para efectos de pasar un dato, el cual es el tema de esta lección, deberás escribir la constante eSetValueWithOverwrite.

En la siguiente lección, Notificaciones (II), te explicaré con detalle el resto de valores que puedes escribir en este parámetro y su significado. Por lo pronto, con lo descrito es suficiente.

Para el caso que nos ocupa, esta función siempre devolverá el valor pdPASS.

xTaskNotifyWait()

La tarea receptora deberá llamar a esta función, cuya firma es:

Como ya lo había mencionado, una vez que la tarea receptora hace la llamada pasa al estado Blocked esperando a que un dato arribe, y mientras la tarea esté en ese estado no usa ciclos de la CPU, lo cual es bueno. Esta función también tiene la ventaja de que si el dato no llega dentro de un cierto tiempo, puede salir y avisarnos para que tomemos las medidas necesarias.

En el bajo mundo de los sistemas operativos cuando una tarea se queda esperando de manera indefinida por un dato o resultado que nunca llegará le llamamos starvation (en español, inanición), y como podrás imaginar, es una muy mala situación. Afortunadamente, muchas funciones de FreeRTOS incorporan una especie de software watchdog (perro guardían) que nos ayudará a salir de dicha situación.

Si deseas utilizar esta característica asegúrate que la tarea receptora tenga una prioridad alta, ya que de no ser así, el código asociado al manejo de la inanición tardará en ejecutarse, o peor aún, nunca hacerlo.

Veamos los parámetros de la función:

ulBitsToClearOnEntry. Este parámetro indica qué bits del dato deben ponerse a cero a la entrada de la función. Es una especie de máscara para eliminar bits que no nos interesan y se utiliza cuando pasas bits en lugar de valores.

En esta lección deberás escribir el valor 0x00. Con esto le estarás diciendo que no ponga ningún bit a cero, ya que eso alteraría nuestro dato. En la segunda parte de esta lección veremos cómo se usa.

ulBitsToClearOnExit. Este parámetro indica qué bits del dato deben ponerse a cero a la salida de la función. Es una especie de máscara para eliminar bits que no nos interesan y se utiliza cuando transmites bits en lugar de valores.

En esta lección deberás escribir el valor 0x00. Con esto le estarás diciendo que no ponga ningún bit a cero a la salida. FreeRTOS hace una copia del dato antes de aplicarle la limpieza de bits, y luego devuelve dicha copia, así que por lo pronto no te preocupes. En la segunda parte de esta lección veremos cómo se usa.

pulNotificationValue. En este parámetro escribirás la dirección de la variable donde quieres guardar el dato. Recuerda que dicha variable debe ser de 32 bits, es decir, del tipo uint32_t.

xTicksToWait. En este parámetro indicas cuánto tiempo (en ticks) quieres esperar por el dato; puedes esperar el tiempo que quieras (especificándolo en milisegundos a través de la macro pdMS_TO_TICKS(), o esperar por siempre especificando el valor portMAX_DELAY.

En un sistema embebido bien hecho JAMÁS deberías especificar portMAX_DELAY.

Atte: Cualquier programador de sistemas embebidos.

Me permito ser repetitivo: para usar esta característica asegúrate que la prioridad de la tarea receptora sea igual o mayor que la productora para que cuando el tiempo expire FreeRTOS la escoja para ser ejecutada. Si tiene prioridad baja, entonces deberá esperar su turno, y quizás éste nunca llegue.

En los ejemplos a continuación veremos ambas formas.

Valor de retorno. Esta función devolverá pdPASS cuando se reciba una notificación, o si la notificación ya estaba lista cuando hiciste la llamada. Si el tiempo programado expiró, entonces devolverá pdFAIL.

Configuración

Antes de ejecutar los ejemplos a continuación asegúrate que las siguientes constantes simbólicas (en el archivo de configuración FreeRTOSConfig.h) tienen el valor mostrado:

#define configUSE_TASK_NOTIFICATIONS            1
#define configTASK_NOTIFICATION_ARRAY_ENTRIES   1 // o cualquier valor igual o mayor

Ejemplo 1

En este primer ejemplo veremos un programa simple donde una tarea produce un dato cada 1000 ms y una tarea receptora los procesa. El dato producido es el valor 1, 2, 3. La tarea receptora parpadeará ese mismo número de veces. El periodo de cada parpadeo es de 200 ms.

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

TaskHandle_t consumidor_h;
// la tarea productora necesita saber quién es el consumidor

void Productor_task( void* pvParameters )
{
    uint32_t cont = 1;
    // el valor que queremos transmitir debe ser uint32_t

    TickType_t last_wake_time = xTaskGetTickCount();

    while( 1 )
    {
        vTaskDelayUntil( &last_wake_time, pdMS_TO_TICKS( 1000 ) );

        Serial.println( "P" );

        xTaskNotify( consumidor_h, cont, eSetValueWithOverwrite );

        ++cont;
        if( cont > 3 ) cont = 1;
    }
}

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

    uint32_t blinks;
    // la variable donde guardamos el dato debe ser uint32_t

    while( 1 )
    {
        xTaskNotifyWait( 0x00, 0x00, &blinks, portMAX_DELAY );

        Serial.println( blinks );
        // Depuración: Imprimos el número de parpadeos

        for( uint8_t i = 0; i < blinks; ++i ){

            digitalWrite( 13, HIGH );
            vTaskDelay( pdMS_TO_TICKS( 100 ) );
            digitalWrite( 13, LOW );
            vTaskDelay( pdMS_TO_TICKS( 100 ) );
        }
    }
}

void setup()
{
    Serial.begin( 115200 );

    xTaskCreate( Productor_task, "PROD", 128, NULL, tskIDLE_PRIORITY + 1, NULL );
    // nota que la prioridad del productor es más alta que la del consumidor;
    // casi siempre será así.

    xTaskCreate( Consumidor_task, "CONS", 128, NULL, tskIDLE_PRIORITY, &consumidor_h );
    // en consumidor_h guardamos el handle de la tarea consumidora

    vTaskStartScheduler();
}

void loop() 
{
}

Nota que la tarea productora tiene prioridad más alta que la consumidora. Además, no basta con que la productora escriba el dato, también debe prestarle la CPU a la tarea receptora; esto lo logra llamando a la función vTaskDelayUntil(), aunque por supuesto puedes usar cualquier otra función que haga que la tarea productora se bloquée.

Ejemplo 2

En este ejemplo agregué elementos para que la tarea productora falle (no notifique a la tarea receptora), y para que la receptora no se quede en un ciclo infinito esperando por un dato que nunca va a llegar. Ambas tareas tienen la misma prioridad.

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

TaskHandle_t consumidor_h;
// la tarea productora necesita saber quién es el consumidor

void Productor_task( void* pvParameters )
{
    uint32_t cont = 1;

    uint8_t cont_to_fail = 10;

    TickType_t last_wake_time = xTaskGetTickCount();

    while( 1 )
    {
        vTaskDelayUntil( &last_wake_time, pdMS_TO_TICKS( 1000 ) );

        Serial.println( "P" );

        xTaskNotify( consumidor_h, cont, eSetValueWithOverwrite );

        ++cont;
        if( cont > 3 ) cont = 1;

        // hacemos que falle después de 10 ciclos
        if( --cont_to_fail == 0 ){ 

           while( 1 ) taskYIELD();
           // esta tarea entrega voluntariamente la CPU,
           // aunque la vida real no es tan bonita

        }
    }
}

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

    uint32_t blinks;

    while( 1 )
    {
        if( xTaskNotifyWait( 0x00, 0x00, &blinks, pdMS_TO_TICKS( 2000 ) ) == pdPASS ){

           Serial.println( blinks );

           for( uint8_t i = 0; i < blinks; ++i ){

               digitalWrite( 13, HIGH );
               vTaskDelay( pdMS_TO_TICKS( 100 ) );
               digitalWrite( 13, LOW );
               vTaskDelay( pdMS_TO_TICKS( 100 ) );

           }
         } else{ // time-over:

            digitalWrite( 13, HIGH );

            while( 1 ){
               Serial.println( "Error" );
               vTaskDelay( pdMS_TO_TICKS( 1000 ) );
            }
         }
    }
}

void setup()
{
    Serial.begin( 115200 );

    xTaskCreate( Productor_task, "PROD", 128, NULL, tskIDLE_PRIORITY + 0, NULL );

    xTaskCreate( Consumidor_task, "CONS", 128, NULL, tskIDLE_PRIORITY, &consumidor_h );

    vTaskStartScheduler();
}

void loop() 
{
}

En la siguiente imagen podrás ver una ejecución de este programa:

¿Qué sigue?

En la lección de hoy vimos cómo utilizar un mecanismo intrínseco de FreeRTOS para comunicaciones y sincronización: las notificaciones directas a las tareas. Éste es un campo de 32 bits al cual podemos darle diversos usos. Uno de ellos ya lo discutimos, y otro uso, el de las notificaciones, lo veremos en la segunda parte.

También recalqué la idea de que las tareas no deberían quedarse esperando de manera indefinida. Muchas funciones de FreeRTOS incluyen un time-out para que la tarea receptora tome las acciones necesarias en caso de inanición (starvation). Úsalo siempre que te sea posible.

En la segunda parte de esta lección estudiaremos cómo usar este mismo mecanismo pero para enviar notificaciones (banderas, señalizaciones) como bits individuales.

(En esta página encontrarás información de suma importancia sobre los flujos, mensajes y notificaciones, aunque todavía no hayamos visto tales temas. Deberías leerla en cuanto te sea posible.)

Í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.


Si encuentras este blog interesante, entonces podrías considerar suscribirte a él y recibir información relevante sobre tecnología y sistemas embebidos, y de vez en cuando, uno que otro regalo.