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.

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.


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.

Compilando desde la consola

El siguiente procedimiento sólo funciona en Linux.

El proyecto Molcajete nació de mi necesidad de compilar programas para Arduino desde una consola, ya que es la forma en que he compilado mis programas de C/C++ los últimos 25 años de mi vida. Y francamente no soporto ni los sketches ni el modelo de súper-loop de Arduino. Además, me gusta tener el control, ¿te has puesto ha pensar dónde está la función main(), la cual es el punto de entrada de todos los programas, incluyendo los basados en sketches? Pues resulta que también me gusta empezar desde la función main().

Sin embargo, no soy lo suficientemente bueno trabajando con archivos Makefile, pero por suerte hay gente que sí lo es. Y fue que encontré el proyecto Arduino-mk que mi sueño se hizo realidad. Luego, resulta que también son fanático del sistema operativo FreeRTOS y pensé, ¿podré integrar FreeRTOS y al mismo tiempo compilar desde la consola? La respuesta fue afirmativa y eme aquí intentando explicarte cómo se hace, esperando que la siguiente información mueva tu curiosidad y también te sea de utilidad.

Tabla de contenidos

¿Qué es y qué necesito?

Los programas multiarchivo en C/C++, como los escritos para Arduino, utilizan la herramienta make para automatizar el proceso de compilación. Este proceso se configura a partir de archivos Makefile’s. Cuando presionas el botón de Verificar/Compilar (ó Ctrl+r) en la IDE de Arduino, ésta se encarga de generar los Makefile’s correspondientes y llamar al compilador. Después, si todo fue bien, presionas el botón Subir (Ctrl+u) y tu programa se sube a tu tarjeta gracias al programa avrdude. Afortunadamente para muchos, todo este proceso es automático; pero habemos algunos que queremos más.

Como mencioné en la introducción, es posible sacar el proceso descrito hacia una consola, gracias al proyecto Arduino-mk. Éste lo podrías instalar por sí solo si así lo quisieras, pero el proyecto Molcajete ya lo incluye, por lo que nada más necesitas descargarlo para tu computadora (actualmente sólo está disponible para Linux y Windows) y descomprimirlo en el lugar de tu preferencia. En este mismo directorio encontrarás las instrucciones para compilar programas en la IDE o en la consola (README.pdf).

A diferencia del trabajo con sketches, aquí son necesarios unos pasos adicionales para instalar un par de cosas: una versión de Arduino y una versión de Arduino-mk. Hice lo posible porque no tuvieras que instalar nada más aparte del proyecto Molcajete, pero no lo logré. Por esta razón deberás instalar:

  • Arduino-core. En una consola (o con tu administrador de paquetes) escribe: sudo apt install arduino-core.
  • Arduino-mk. En la misma consola escribe: sudo apt install arduino-mk.

Por alguna razón los scripts de Arduino-mk requieren que Arduino-core esté instalado, aunque no se use, y no supe cómo evitarlo, así que hay que vivir con ello.

Proyectos de consola

Cada proyecto de consola que realices está formado de al menos dos archivos. Un main.cpp y un Makefile en el mismo directorio. En el archivo main.cpp escribirás la función main(), y el resto de funciones que necesites, aunque es recomendable que dividas tu proyecto en módulos. En el archivo Makefile indicarás, principalmente, la ruta donde descomprimiste al proyecto Molcajete.

main.cpp

Un archivo main.cpp típico se ve así:

// main.cpp

#include <Arduino.h>

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

void led_task( void* pvParameters )
{
    (void) pvParameters;

    pinMode( 13, OUTPUT );

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

        vTaskDelay( 500 / portTICK_PERIOD_MS );

        digitalWrite( 13, LOW );

        vTaskDelay( 500 / portTICK_PERIOD_MS );
    }
}

void taskAnalog( void* pvParameters )
{
    (void) pvParameters;

    Serial.begin( 115200 );

    while( 1 )
    {
        Serial.println( analogRead( A0 ) );

        vTaskDelay( 1000 / portTICK_PERIOD_MS );
    }
}

int main(void)
{
    cli();

    xTaskCreate( taskLed,
        (const portCHAR *)"LED",
        128,
        NULL,
        tskIDLE_PRIORITY,
        NULL );

    xTaskCreate( taskAnalog,
        (const portCHAR *) "ANLG",
        128,
        NULL,
        tskIDLE_PRIORITY,
        NULL );

    init();
    // inicializa al hardware de Arduino

    vTaskStartScheduler();
    // inicia las operaciones del sistema operativo FreeRTOS

    while( 1 )
    {
        // si hubiera algún error, el programa quedaría atrapado aquí
    }
}

En esta entrada podrás encontrar información detallada de qué son las tareas dinámicas, la función de creación xTaskCreate(), sus parámetros, cómo se inicia al sistema operativo, y cómo se configura tu proyecto en el archivo FreeRTOSConfig.h.

Las diferencias entre un programa basado en sketch y uno de consola son las siguientes (en órden de aparición, es decir de arriba-abajo, del programa anterior):

  • Debes incluir al archivo de encabezado Arduino.h con: #include <Arduino.h>.
  • Debes escribir la función main(), que como sabes es el punto de entrada de cualquier programa escrito en C/C++.
  • La primer instrucción, cli(), deshabilita las interrupciones para que puedas crear tus tareas y arrancar de forma correcta a FreeRTOS. No queremos que interrupciones pendientes
  • Luego puedes crear tus tareas, ya sean estáticas o dinámicas; este procedimiento no tiene nada que ver con su naturaleza.
  • Después debes llamar a la función de Arduino init(). Esta función inicializa el hardware que un programa para Arduino UNO y compatibles necesita. Si olvidas hacerlo tus programas no funcionarán.
  • Lo siguiente es iniciar al sistema operativo con la función de FreeRTOS vTaskStartScheduler().
  • Si el sistema operativo inició correctamente nunca deberías regresar a la función main(). Sin embargo, si algo saliera mal, entonces el programa quedaría atrapado dentro del ciclo infinito para que no se salga de control y que además puedas buscar la fuente de error.

Makefile

Como ya te decía, en este archivo debes indicarle al compilador dónde está la carpeta /arduino-1.8.13 (el nombre puede variar según la versión instalada):

ARDUINO_DIR = /tu/ruta/de/instalación/arduino-1.8.13

Algo que tal vez necesites cambiar es el puerto serial. En Linux, el cual es el sistema operativo que yo uso, nombra (casi siempre) a los adaptadores seriales como /dev/ttyUSB* (y veces los nombra como /dev/tty/ACM*):

ARDUINO_PORT = /dev/ttyUSB*

También puedes cambiar el baudrate del puerto serial, si así lo deseas. Yo uso 115200 BPS (bits por segundo), pero he visto que mucha gente usa 9600 BPS:

MONITOR_BAUDRATE = 115200

Y esos son todos los cambios que deberís hacerle a este archivo.

Compilando y subiendo el proyecto

Es momento de compilar. Pero antes de que lo hagas revisa que tu archivo de configuración FreeRTOSConfig.h esté correcto y actualizado. Visita esta entrada si aún no estás familiarizado con él.

Abre una consola en el directorio donde están tus archivo main.cpp y Makefile, y ejecuta la orden make:

$ make

(El símbolo $ indica el prompt del sistema y no debes escribirlo). Si todo fue correcto, obtendrás una salida parecida a la siguiente con el ejemplo que estamos trabajando:

Extracto del resultado de la compilación del proyecto.

Si obtuviste una salida parecida, entonces toca subir el programa a la tarjeta. Ejecuta la orden make upload:

$ make upload

(Si estás usando alguna de las tarjetas UB-1S328, UB-C328A, o UB-PLR328, entonces deberás presionar y soltar el botón de reset al mismo tiempo que le das ENTER a la instrucción anterior.)

Si todo fue correcto, obtendrás una salida parecida a la siguiente y tu tarjeta ya debería estar ejecutando el programa:

Extracto del resultado de subir el programa a una tarjeta UB-1S328, la cual es una versión minimalista pero compatible con Arduino UNO.

Si tu programa incluye instrucciones del tipo Serial.println(), entonces querrás ver la salida. Para ello tienes dos opciones:

  • Abre la IDE de Arduino e inicia el monitor serial que viene incluído (Ctrl-Shift-m), o
  • En la misma consola abre un monitor ejecutando la instrucción make monitor. Para salir presiona Ctrl-a, k y luego y.
Monitor mostrando lecturas del ADC.

¿Que sigue?

¡Felicidades! Ya compilaste tu primer proyecto de Arduino en la consola utilizando un sistema operativo de tiempo real. Pero si la consola no es lo tuyo, no te preocupes, podrás seguir utilizando los sketches. Utilizar a FreeRTOS, en consola o en sketch, evitará que uses variables globales cuando no son necesarias, y también dejarás de usar el súper-loop de Arduino (función loop()).

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

Índice del curso


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.

Tareas estáticas en FreeRTOS para Arduino

Ya en la entrada anterior platicábamos de cúando y porqué utilizar un sistema operativo de tiempo real en nuestras aplicaciones (si no la has leído, te recomiendo hacerlo antes de continuar), y de que FreeRTOS tiene dos maneras de crear tareas. Platicamos largo y tendido de la creación de tareas dinámicas y de un montón de conceptos necesarios.

Hoy te voy a mostrar cómo crear tareas estáticas en el sistema operativo FreeRTOS para tus programas en Arduino UNO (y compatibles), e intentaré explicarte qué son, cómo se crean y porqué debes utilizar tareas estáticas siempre que te sea posible. También te darás cuenta de porqué en la entrada anterior te dije que crear tareas estáticas necesita de más pasos. Pero créeme, vale la pena utilizarlas.

Si ya leíste la entrada anterior verás que estaré repitiendo algo de la teoría ya vista. Lo hago así por aquellos lectores que no lo han hecho, y creo que te servirá como recordatorio.

Tabla de contenidos

¿Qué hay de malo con las tareas dinámicas?

Lo único malo son las llamadas a las funciones malloc() y free() que nos lleva a dos problemas críticos en los sistemas embebidos:

  • Fragmentación de la memoria.
  • No son deterministas.

Fragmentación de la memoria

Cuando un programa llama de forma repetida a las funciones malloc() y free() la memoria RAM comienza a fragmentarse; esto es, los bloques de memoria del heap cada vez se hacen más pequeños y dispersos, llegando al punto en el que la función malloc() ya no puede encontrar un bloque de memoria contiguo del tamaño requerido, y por tanto, falla.

¿Te imaginas que un circuito electrónico de sensado, que utiliza FreeRTOS, y que has instalado en la copa de un árbol en medio de la selva falle debido a que malloc() no pudo atender tu petición de memoria? ¡Yo tampoco!

No son deterministas

El siguiente problema con las llamadas a las funciones malloc() y free() es que el tiempo que necesitan para entregar un resultado es muy variable; esto es, una primer llamada a malloc() puede tardar X ticks, pero una segunda llamada quizás tarde 1.5X ticks, y una más 0.8X ticks, y así sucesivamente. Y ambas funciones son bloqueantes, es decir, nadie puede hacer nada hasta que terminen. Esto podría provocar que tu sistema pierda eventos críticos.

Existe un tercer problema relacionado con free(). Cuando tú mandas llamar a esta función, la memoria no se devuelve de manera inmediata, solamente se marca para ser devuelta. La verdadera devolución se lleva a cabo dentro de una tarea del sistema operativo llamada IDLE task (de la cual te hablaré más adelante) que si no se ejecuta (debido a que tus tareas están activamente ejecutándose), entonces esa memoria no es devuelta.

¿Podemos seguir usando tareas dinámicas?

Claro que sí. Los que saben del tema nos recomiendan crear las tareas (y en general, todos los objetos dinámicos del programa) al inicio de la aplicación, y nunca borrarlos.

¿Qué es y qué necesito?

Al igual que en la entrada anterior vas a necesitar una tarjeta Arduino UNO o compatible junto con el sistema operativo FreeRTOS, y este paso ya lo hice por tí. Descarga el proyecto Molcajete para tu computadora (actualmente sólo está disponible para Linux y Windows), descomprime el archivo en el lugar de tu preferencia, entra a la carpeta arduino-1.8.xx (al día de hoy xx=13), y finalmente arranca la IDE dando click en el archivo arduino (para Linux) o arduino.exe (para Windows). No debes instalar nada. En este mismo directorio encontrarás las instrucciones para compilar programas en la IDE o en la consola (README.pdf).

Recuerda que cuando escribes aplicaciones basadas en un sistema operativo, éstas deben ser descompuestas en tareas (o procesos. Aunque en la literatura el término más común es proceso, en FreeRTOS se le dice tarea, task en inglés). Crear tareas en FreeRTOS requiere algunos pasos:

  1. Escribir el código de las tareas.
  2. Registrar las tareas en el sistema operativo.
    1. Arrancar al sistema operativo.
  3. Configurar al sistema operativo.

Tareas

Una tarea en FreeRTOS es una función de C que no devuelve nada, tiene un sólo argumento, y muy importante, nunca debe terminar. Puedes pensar en ellas como las aplicaciones que están corriendo en tu computadora, aunque a una escala más pequeña. No existe ninguna diferencia en la estructura de las tareas ya sea que se creen de forma estática o dinámica; las diferencias las veremos en un momento.

A pesar de que una tarea es una función de C, existen dos características que las hacen diferentes del resto: como recién mencioné, nunca terminan, y sólo deben ser llamadas por el sistema operativo. Veamos una típica tarea:

void led_task( void* pvParameters )
{
   pinMode( 13, OUTPUT );
   // inicializamos al hardware que requiera la tarea

   const portTickType period = pdMS_TO_TICKS( 500 );
   // pdMS_TO_TICKS convierte milisegundos a ticks.
   // Internamente el sistema operativo trabaja con ticks.

   bool state = false;

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

      vTaskDelay( period );
      // manda "dormir" a la tarea por 'period' milisegundos
   }
}

El único parámetro que acepta la función, pvParameters, es del tipo void*. A través de este parámetro podremos pasarle datos a la tarea cuando la estemos registrando. Por lo pronto no lo utilizaremos, pero más adelante lo explicaré.

Registrando las tareas de manera estática

Después de escribir la o las tareas, lo que sigue es registrarlas ante el FreeRTOS. A esta operación se le llama con mucha frecuencia crear la tarea, aunque la realidad es que se está registrando (sin embargo, a partir de este momento le estaré diciendo «crear» porque no quiero parecer un tipo raro). Y aquí se pone muy interesante tanto por la forma en que se crearán las tareas, como por los parámetros para su creación.

FreeRTOS permite crear tareas de dos formas diferentes: de manera dinámica o estática. Que sea una u otra depende de la forma en que se asigne la memoria para dos estructuras de datos fundamentales para cada tarea, la pila y el bloque de control (stack y Task Control Block (TCB), respectivamente).

La pila se utiliza para guardar las variables locales de la tarea, los parámetros de llamadas a funciones, la dirección de regreso de una función llamada, y en su caso, el valor devuelto por la función. Cada tarea necesita de una pila.

El bloque de control, TCB, guarda información importante de cada tarea. Por ejemplo, cuando una tarea se va a dormir debe guardar su estado. Esto es como tomar una foto (o una instantánea) de la tarea en ese preciso momento para cuando regrese más adelante. Recuerda que la tarea libera la CPU al irse a dormir para que otras tareas la usen, pero llegará el momento en que necesite ejecutarse otra vez desde el punto exacto en el que se quedó, como si nunca hubiera pasado nada. Esta información se extrae del TCB cuando la tarea está lista para ejecutarse.

En esta entrada nos concentraremos en la creación estática de tareas, lo cual implica que tú como programador crearás la memoria para la pila y el TCB; pero no te preocupes, es tan fácil como declarar un arreglo y una variable, e involucra más pasos que su contraparte dinámica.

Veamos la firma de la función xTaskCreateStatic() la cual crea tareas de manera estática, y a continuación explicaré cada uno de sus argumentos:

Función para crear tareas estáticas.

pxTaskCode. Es el nombre de la función que implementa la tarea. En nuestro ejemplo, led_task (así, sin paréntesis). Es costumbre agregarle al nombre de la tarea la partícula _task sólo para diferenciarla con facilidad de las demás funciones.

pcName. Es una cadena de texto de C que representa un nombre que le quisiéramos dar a la tarea. Es opcional y lo puedes utilizar cuando estés depurando tus aplicaciones.

ulStackDepth. Es el número de elementos del arreglo puxStackBuffer (el cual veremos a continuación y que hace las veces de pila).

¿Cuál es el tamaño ideal de la pila? Depende. Depende del trabajo que tenga que hacer la tarea (variables locales y profundidad de llamadas a funciones). Escoger el valor idóneo es resultado de intentar y equivocarse (trial and error). Con un valor pequeño la pila se desbordaría, y con un valor muy grande estarías desperdiciando valiosa memoria RAM. FreeRTOS incluye algunas funciones y macros que pueden ayudarte a seleccionar el valor más adecuado. Este es tema de otra entrada. Lo que sí te puedo comentar en este momento es que FreeRTOS incluye una constante con el valor mínimo para el tamaño de pila, configMINIMAL_STACK_SIZE, para el procesador ATmega328.

pvParameters. Si necesitas pasarle argumentos a la tarea aquí es donde lo haces. Si te fijas el tipo de dato de este parámetro es void*, lo que significa que puedes pasarle casi cualquier tipo válido en C, desde un simple entero hasta un tipo compuesto. En esta entrada no lo usaremos, pero en un siguiente post te explicaré cómo usarlo, tanto para pasarle información a la tarea, como para reutilizar el código de una misma tarea.

Cuando no lo utilices deberás escribir el valor NULL.

uxPriority. En un sistema operativo de tiempo real hay tareas más importantes que otras, y las tareas más importantes deben ejecutarse antes, o interrumpir, a las menos importantes. En este parámetro le indicas a FreeRTOS el nivel de importancia de la tarea. El nivel más bajo, o menos importante, es el cero, también indicado por la constante tskIDLE_PRIORITY, y el nivel más alto, o más importante, está dado por configMAX_PRIORITIES – 1. Tú decides cuántos niveles de prioridad vas a tener en tu aplicación dándole un valor a esta constante, la cual se encuentra en el archivo de configuración FreeRTOSConfig.h, del cual hablaré más adelante.

Vamos a suponer que configMAX_PRIORITIES tiene el valor 5; esto significa que tu prioridad más alta será de 4, ya que empezamos en cero. Y las tareas con prioridad 4 (o configMAX_PRIORITIES – 1) serán más importantes que las que tengan prioridad 3, o 2, o 1, o 0.

Y sí, sí es posible tener varias tareas con el mismo nivel de prioridad. Digamos que tienes 3 tareas con la misma prioridad, lo que significa que las 3 están listas para ejecutarse. En este caso FreeRTOS aplica el método Round robin (una tarea depués de la otra). Para que este esquema funcione es necesario que cada tarea libere a la CPU a través de alguno de los mecanismos de FreeRTOS para ello (dormirse, esperar por un evento, o por una liberación voluntaria a partir de una llamada a la función taskYIELD()).

puxStackBuffer. Es un arreglo de tipo StackType_t el cual hará las veces de pila de la tarea. Este arreglo debe ser persistente y deberá ser de tamaño ulStackDepth.

Que sea persistente significa que debe ser declarado de manera global, o dentro de la función main(), o dentro de cualquier función (que no sea tarea y tras ser marcado como static). Lo que no deberías hacer es declararlo dentro de un tarea, ya que estarías utilizando pila de la tarea.

Ten en cuenta que cada elemento de este arreglo es del tipo StackType_t, el cual debe coincidir con el número de bytes utilizado por la pila del procesador elegido. Por ejemplo, en procesadores de 32 bits, como todos los de la familia ARM-Cortex, el tamaño de cada elemento en la pila es de 4 bytes, por lo cual, si estableciste un valor de 128 para ulStackDepth, en realidad estás utilizando 512 bytes.

Pero hay buenas noticias. Para el caso que nos ocupa, el Arduino Uno que utiliza al procesador de 8 bits ATmega328, la conversión es uno a uno, ya que la pila de este chip es también de 8 bits. En términos prácticos, si tú escribes el valor 128 en ulStackDepth , realmente estás utilizando 128 bytes.

Veamos como ejemplo el arreglo para una pila de 128 elementos (que no bytes) declarado con ámbito global:

const uint16_t TASK_LED_STACK_SIZE = 128;
// este valor se utilizará también para el parámetro ulStackDepth

StackType_t taskLedRAM[ TASK_LED_STACK_SIZE ];

El nombre del arreglo y el de la constante son los que tú quieras, pero como convención les agregamos el nombre de la tarea a la que pertenecen. Y si quisieras crear el arreglo dentro de la función main() te quedaría como (el uso del modificador static no es necesario con esta función, sin embargo, la he marcado así por si quisieras hacer lo mismo en una función diferente):

const uint16_t TASK_LED_STACK_SIZE = 128;
// este valor se utilizará también para el parámetro ulStackDepth

int main()
{
   // ...

   static StackType_t taskLedRAM[ TASK_LED_STACK_SIZE ];

   // ...
}

pxTaskBuffer. Es un apuntador a una variable estructura de tipo StaticTask_t, la cual hace la función de TCB (Task Control Block, o Bloque de Control) y deberá ser persistente.

Al igual que el arreglo puxStackBuffer, esta variable deberá ser declarada en un ámbito donde no deje de existir, es decir, siguiendo los mismos lineamientos dados más arriba para dicho arreglo.

Valor devuelto. Esta función devuelve una referencia a la tarea recién creada. Esta referencia puede ser utilizada por otras tareas para operar sobre ella. Por ejemplo, una tarea puede decidir que otra ya no es necesaria y la quiere borrar; para ello necesitaría dicha referencia. También las funciones de comunicaciones y sincronización de FreeRTOS necesitan de esta referencia.

Puedes omitir (no guardar) la referencia devuelta si no piensas utilizarla. Pero en caso de que sí la guardes, deberás utilizar una variable global, ya que dicha referencia debe estar disponible para que otras tareas/funciones tengan acceso a ella.

Las tares creadas de manera estática siempre se crean.

Vamos a aplicar todo lo explicado hasta este momento, aunque aún nos faltan varios pasos antes de poder compilar. Registremos la tarea asociada a nuestro ejemplo, led_task():

   xTaskCreateStatic(
      led_task,            // es la función que implementa a la tarea
      "LED",               // es el nombre que le quisimos dar a la tarea
      TASK_LED_STACK_SIZE, // es el número de elementos en la pila
      NULL,                // no vamos a pasarle arguementos a la tarea
      tskIDLE_PRIORITY,    // tskIDLE_PRIORITY es la prioridad más baja: 0
      taskLedRAM,          // es la pila
      &taskLedTCB );       // es la dirección del TCB

      // las tareas estáticas siempre se crean; obtendremos un error en tiempo de compilación
      // en caso de que se termine la memoria.

¿Ya podemos compilar? No, todavía no. Nos faltan dos pasos: arrancar al sistema operativo y configurarlo.

Iniciando al sistema operativo

A diferencia de las aplicaciones en nuestras computadoras de escritorio, donde el sistema operativo arranca apenas presionas el botón de encendido, en el caso de los sistemas embebidos somos nosotros quien lo debemos inicias, pero es muy simple, solamente debes llamar a la función vTaskStartScheduler().

Aún no podemos compilar, nos falta un paso muy importante: la configuración de FreeRTOS.

Configurando a FreeRTOS

FreeRTOS es un sistema operativo muy configurable, y toda la configuración y habilitación de funcionalidades se llevan a cabo en un archivo llamado FreeRTOSConfig.h. Éste lo puedes encontrar en el directorio /tu/ruta/de/instalación/arduino-1.8.13/libraries/FreeRTOS/src. Aunque el archivo incluído en el proyecto Molcajete muestra algunas opciones, en realidad hay muchas más, pero por el momento vamos a enfocarnos en las más importantes y las que tienen que ver con nuestro ejemplo.

configSUPPORT_STATIC_ALLOCATION. Esta constante debe estar puesta a 1 para que puedas utilizar tareas estáticas (y en general, cualquier otro objeto creado de forma estática).

#define configSUPPORT_STATIC_ALLOCATION 1

configMAX_PRIORITIES. Aquí estableces el número máximo de prioridades para tu aplicación. No obstante que puedes usar el número de niveles que tú quieras, debes saber que cada nivel ocupa memoria extra, por lo que este número deberías mantenerlo lo más bajo posible. También recuerda que las tareas pueden compartir el mismo nivel de prioridad.

#define configMAX_PRIORITIES 3

configMAX_TASK_NAME_LEN. ¿Recuerdas que en la función xTaskCreateStatic() hay un parámetro para asociar un nombre (cadena de carácteres) a la tarea? Bueno, aquí estableces el número máximo de carácteres para el nombre de las tareas, incluyendo al carácter de final de cadena. Puedes escribir el número que quieras, pero ten en cuenta que cada tarea creada estará reservando en memoria RAM esa cantidad de bytes. Por ejemplo, si planeas que los nombres tengan una longitud de 4 carácteres, deberas escribir 5 en esta constante. Si quisieras 8 carácteres, entonces escribirías 9, y así sucesivamente.

#define configMAX_TASK_NAME_LEN ( 5 )

INCLUDE_vTaskDelay. Escribiendo un 1 en esta constante estarás habilitando el uso de la función vTaskDelay() en tus programas. Para ahorrar memoria de programa (memoria Flash) FreeRTOS permite habilitar o deshabilitar diferentes funcionalidades. Acabas de ver la habilitación de una de ellas; y si, por ejemplo, no pretendes utilizar a la función vTaskDelayUntil(), entonces escribirías un 0 en la constante INCLUDE_vTaskDelayUntil:

#define INCLUDE_vTaskDelay      1
#define INCLUDE_vTaskDelayUntil 0

TIP: Puedes visitar este enlace para ver todas las opciones configurables posibles en FreeRTOS y su explicación.

TIP: El compilador de C sabe que si no usas una función en tu programa, entonces no debe generarle código. Esto significa que si a la constante INCLUDE_vTaskDelayUntil la pones a 1, pero nunca la llamas, entonces el compilador no le generará código, y ahorrarás un poco de memoria de programa.

IDLE task

Tienes que saber que FreeRTOS necesita (y de manera interna y privada la implementa) una tarea con prioridad 0 (la más baja) para que se ejecute cuando ninguna otra tarea lo esté haciendo, y aunque no tiene un nombre específico, ni nosotros tenemos acceso directo a ella, vamos a llamarle la IDLE task (tarea en espera). Como cualquier otra la tarea la IDLE task requiere memoria para su pila y su TCB. Cuando el soporte para creación de tareas dinámicas está activado (configSUPPORT_DYNAMIC_ALLOCATION vale 1) entonces FreeRTOS de manera automática se las crea, y tú, afortunadamente, te olvidas de ello; pero en el caso que nos ocupa, tareas estáticas, si el soporte para creación dinámico está desactivado (configSUPPORT_DYNAMIC_ALLOCATION vale 0), entonces deberás proporcionarle memoria como a cualquier otra tarea a través de ĺa función vApplicationGetIdleTaskMemory(). No te preocupes, es tan fácil como cortar y pegar el siguiente código, y más aún, ya lo hice por ti en el proyecto Molcajete. En otras palabras, ¡no tienes que hacer nada!, pero tenías que saberlo. En una entrada posterior hablaré más sobre la IDLE task y cómo utilizarla.

#ifdef __cplusplus
extern "C"
{
#endif
    void vApplicationGetIdleTaskMemory( 
        StaticTask_t **ppxIdleTaskTCBBuffer,
        StackType_t **ppxIdleTaskStackBuffer,
        uint32_t *pulIdleTaskStackSize )
    {
        static StaticTask_t xIdleTaskTCB;
        static StackType_t uxIdleTaskStack[ configMINIMAL_STACK_SIZE ];

        *ppxIdleTaskTCBBuffer = &xIdleTaskTCB;
        *ppxIdleTaskStackBuffer = uxIdleTaskStack;
        *pulIdleTaskStackSize = configMINIMAL_STACK_SIZE;
    }
#ifdef __cplusplus
}
#endif

Y como para este ejemplo no es necesario configurar ni habilitar más cosas llegamos al momento crucial de compilar el programa. Antes de que compiles revisemos un programa completo con la tarea de ejemplo led_task():

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

const uint16_t TASK_LED_STACK_SIZE = 128;
StaticTask_t taskLedTCB;
StackType_t taskLedRAM[ TASK_LED_STACK_SIZE ];

void led_task( void* pvParameters )
{
   pinMode( 13, OUTPUT );
   // inicializamos al hardware que requiera la tarea

   const portTickType period = pdMS_TO_TICKS( 500 );
   // pdMS_TO_TICKS convierte milisegundos a ticks.
   // Internamente el sistema operativo trabaja con ticks.

   bool state = false;

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

      vTaskDelay( period );
      // manda "dormir" a la tarea por 'period' milisegundos
   }
}

void setup() 
{
   xTaskCreateStatic(
      led_task,            // es la función que implementa a la tarea
      "LED",               // es el nombre que le quisimos dar a la tarea
      TASK_LED_STACK_SIZE, // es el número de elementos en la pila
      NULL,                // no vamos a pasarle arguementos a la tarea
      tskIDLE_PRIORITY,    // tskIDLE_PRIORITY es la prioridad más baja: 0
      taskLedRAM,          // es la pila
      &taskLedTCB );       // es la dirección del TCB

      // las tareas estáticas siempre se crean; obtendremos un error en tiempo de compilación
      // en caso de que se termine la memoria.

   vTaskStartScheduler();
   // arrancamos a FreeRTOS. Éste toma control del sistema a partir de este
   // punto.


   // ¡si llegamos aquí es porque hubo un problema grave!
}

void loop()
{
   // el cuerpo de loop() queda vacío cuando usamos a FreeRTOS 
}

Captura este programa en la IDE de Arduino. Compílalo y súbelo como siempre.

Presta atención a los siguientes puntos:

  • Todos los programas que usen a FreeRTOS deberán incluir a los archivos de encabezado FreeRTOS.h y task.h.
  • El tamaño de la pila está declarado como una constante TASK_LED_STACK_SIZE. Y además, las variables para la pila y el TCB, taskLedRAM y taskLedTCB, respectivamente, están todas declaradas en el ámbito global.
  • Si tu programa está basado en sketch, entonces deberás crear tus tareas en la función setup() de Arduino. Debes saber que es posible que una tarea cree a otra tarea, aunque ese tema lo dejaremos para después.
  • A diferencia del ejemplo de tarea dinámica de la entrada anterior, aquí no es necesario probar que la tarea se haya creado con éxito.
  • Para este ejemplo basado en sketch, en la misma función setup() inicias al sistema operativo.
  • ¡En la función loop() ya no tienes que escribir nada! A partir de ahora todo se lleva a cabo en el código de las tareas y en el propio sistema operativo.
Programa de ejemplo ejecutándose en la tarjeta minimalista UB-1S328, la cual es compatible con Arduino UNO.

¿Pueden las tareas estáticas y dinámicas convivir en el mismo programa?

Absolutamente. Si en tu programa requieres ambas formas, basta con que las constantes respectivas estén puestas a 1:

#define configSUPPORT_STATIC_ALLOCATION 1
#define configSUPPORT_DYNAMIC_ALLOCATION 1

¿Puedo utilizar únicamente tareas estáticas?

Si decides que sólo requieres tareas estáticas en tu aplicación, entonces puedes poner la definición configSUPPORT_DYNAMIC_ALLOCATION a 0. Sin embargo, toma en cuenta lo siguiente: FreeRTOS proporciona cinco versiones para las funciones malloc() y free(). Éstas se encuentran en archivos llamados heap_x.c, donde x va desde 1 hasta 5. En el proyecto Molcajete incluí heap_1.c, la cual supone que nunca vas a devolver la memoria; es decir, la función free() no está implementada.

Pero lo que te quiero decir es que si desactivas por completo el soporte para tareas (y objetos) dinámicos, entonces también deberás eliminar el archivo heap_x.c, porque de no hacerlo el compilador arrojará un error:

/home/fjrg76/arduino-1.8.13/libraries/FreeRTOS/src/heap_1.c:48:6: error: #error This file must not be used if configSUPPORT_DYNAMIC_ALLOCATION is 0
     #error This file must not be used if configSUPPORT_DYNAMIC_ALLOCATION is 0

Es mala idea borrarlo desde el punto de vista que vas a estar trabajando con diferentes proyectos, por lo que te recomiendo renombrarlo en lugar de eliminarlo. Lo que hago es cambiarle el nombre a heap_1.c.txt. Con este cambio el compilador deja de quejarse, y la siguiente vez que lo necesitas vuelves a ponerle su nombre original.

¿Qué sigue?

En la siguiente entrada voy a mostrarte cómo compilar desde la línea de comandos; mientras tanto puedes modificar el programa de ejemplo o escribir nuevos para que pongas en acción lo visto hoy.

Índice del curso

Espero que esta entrada haya sido de tu interés. Si fue así podrías suscribirte a mi blog, o escribirme a fjrg76 dot hotmail dot com, o compartir 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.

¡The Molcajete Project 0.9.3 ya está aquí!

Hoy liberé la versión 0.9.3 del proyecto que inicié para utilizar de manera conjunta a Arduino y FreeRTOS, Molcajete, además de la posibilidad de compilar desde la consola (como cualquier proyecto de C++).

Esta nueva versión incluye principalmente las versiones más recientes de Arduino (1.8.13) y FreeRTOS(202012.00).

Si has estado pensando en un Arduino de tiempo real, esta es tu oportunidad. Puedes descargarlo para Linux y Windows y escribir programas tanto en sketches como en la consola (desde la función main()).

Además, este sistema operativo te permite crear tareas tanto con memoria dinámica como estática. Los gurús de los sistemas embebidos recomiendan evitar la memoria dinámica, así que cuentas con ambas opciones.

No lo sigas pensando, descárgalo y comienza a escribir programas con un sistema operativo. El paquete de descarga incluye ejemplos tanto de sketch como de consola para que comiences de manera fácil.

Si conoces a algún colega ingeniero que se pudiera beneficiar de este proyecto, por favor compártele la noticia:

https://sourceforge.net/projects/molcajete/

Índice del curso


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.