Colas
Hasta el momento en este curso hemos visto tres formas de comunicación inter-tareas:
- Con notificaciones directas a la tarea.
- Con flujos.
- Con mensajes.
Y nos queda una por explorar: las colas. Este fue el primer mecanismo que implementó FreeRTOS para pasar grandes cantidades de datos entre tareas o entre interrupciones y tareas.
La diferencia entre los 3 mecanismos listados y las colas es que éstas puedes ser escritas y leídas por dos o más tareas. Además, la escritura y lectura se realiza estrictamente elemento por elemento.
Aunque históricamente las colas fueron el primer mecanismo para la comunicación inter-tareas, con el paso del tiempo, la retroalimentación de los usuarios de FreeRTOS, y la evolución de éste, los administradores del proyecto se dieron cuenta que podían implementar un mecanismo más ligero para pasar datos de una tarea a otra, ya que casi todo el tiempo esta es la forma de trabajar.
Pero eso no quiere decir que las colas sean obsoletas o que deban ser descartadas; todo lo contrario. En algún momento te verás en la necesidad de utilizarlas. En la sección de ejemplos vamos a ver una tarea que la hace de «controlador» o «concentrador», mientras que dos tareas «producen» datos.
Aunque en el Arduino UNO no es posible lo siguiente, piensa en una interfaz gráfica. En éstas diversos periféricos generan eventos: una pantalla touch, algún botón o teclado físico, sensores de posición, temporizadores internos, puerto serial, etc. Todas estos eventos se guardan en una cola de eventos conforme van llegando. El «controlador» de la interfaz gráfica va despachando evento por evento, desde el más antiguo al más reciente (siempre en este orden).
Además, debes saber que las colas son la estructura de datos subyacente para otros mecanismos de FreeRTOS, como los semáforos (y su API primitiva) y los temporizadores por software.
Tabla de contenidos
¿Qué es y qué necesito?
Una cola (queue, en inglés, y pronunciada como [kiú]) es una estructura de datos con las siguientes características:
- Es lineal. Es decir, todos los elementos, excepto el primero y el último, tienen un antecesor y un predecesor.
- Se escribe por un extremo de la cola, y se lee por el extremo opuesto. Esto logra que el elemento que llegó primero sea el primero en ser atendido (al contrario de las pilas, donde el último elemento en llegar es el primero en ser atendido). Las colas también son conocidas como estructuras FIFO (first in, first out, primero en llegar, primero en salir).
- El acceso a los elementos de enmedio, es decir, que no sean los extremos, está prohibido.
- Aunque las colas son un tipo especial de listas, en las colas no se puede buscar un elemento ni se puede ordenar.
Las colas tienen dos operaciones básicas, más una auxiliar (muy útil), cuyos nombres varían de autor a autor, pero que significan lo mismo:
- Insertar en la cola: Insert(), Insertar(), InsertFront(), Enqueue(), Push(), Send().
- Extraer de la cola: Remove(), Remover(), RemoveBack(), Retrieve(), Dequeue(), Pop(), Receive().
- Observar el elemento en el frente de la cola. Observar(), Peek().
(Los términos en negrita corresponden a los nombres que utiliza FreeRTOS para las respectivas operaciones, aunque yo en mis clases de Estructuras de Datos uso Enqueue(), Dequeue() y Peek(), pero como este es un curso de FreeRTOS, entonces utilizaré sus nombres para evitar confusiones.)
RECUERDA: Insertamos en la parte trasera (rear, back, tail) de la cola, y extraemos del frente (front, head) de la misma.
Al igual que con los mensajes, la operación Send() copia los datos en la cola, y Receive() copia los datos desde la cola. Si de plano es prohibitivo realizar las copias dado que cada dato sea muy grande, entonces puedes crear una cola de apuntadores (o referencias) y mantener los datos en búferes externos.
Para que puedas utilizar las colas en tus programas asegúrate incluir al encabezado #include <queue.h>
en cada uno de ellos. Recuerda también que si vas a crear las colas con memoria dinámica, entonces deberás poner a 1 la constante configSUPPORT_DYNAMIC_ALLOCATION
; y si vas a crear las colas con memoria estática, entonces deberás poner a 1 la constante configSUPPORT_STATIC_ALLOCATION
, ambas en el archivo FreeRTOSConfig.h:
#define configSUPPORT_STATIC_ALLOCATION 1 #define configSUPPORT_DYNAMIC_ALLOCATION 1
Para facilitar el uso de las colas en nuestros programas escribí 4 constantes simbólicas, las cuales vamos a utilizar en los ejemplos de esta lección. De éstas tú estableces las dos primeras; las últimas 2 se calculan de manera automática. Por supuesto que puedes escribir directamente los valores donde sea necesario, pero en mi experiencia es preferible hacerlo de esta manera:
#define QUEUE_ELEMS ( 8 )
Es el número de elementos (no de bytes) de la cola.
#define QUEUE_TYPE uint16_t
Aquí estableces el tipo de dato del o de los elementos que va a guardar la cola. En este ejemplo se trata de variables uint16_t
, pero podrían ser también de otros tipos básicos o tipos compuestos, como lo verás en los ejemplos.
#define QUEUE_TYPE_SIZE ( sizeof( QUEUE_TYPE ) )
Calcula el tamaño del tipo en bytes. No deberías manipularlo directamente.
#define QUEUE_BUFFER_SIZE ( ( QUEUE_ELEMS ) * ( QUEUE_TYPE_SIZE ) )
Calcula el número de bytes que necesita el búfer subyacente de la cola. No deberías manipularlo directamente.
Desarrollo
Las colas necesitan un búfer de datos interno, es decir, un lugar dónde guardar cada elemento. Este búfer es un arreglo de elementos uint8_t
de tamaño ( QUEUE_ELEMS x QUEUE_TYPE_SIZE
) que estableces cuando creas a los objetos. Cuando utilizas creación dinámica de objetos este arreglo se crea de manera automática dentro de la función xQueueCreate()
, por lo cual no debes preocuparte de nada (en apariencia. Siempre que te sea posible utiliza la creación estática de objetos, es más segura); mientras que si usas la creación estática de objetos, tú eres responsable de pasarle dicho arreglo a la función xQueueCreateStatic()
. A continuación veremos ambas formas.
(Para conocer o recordar la diferencia y la configuración entre objetos dinámicos y estáticos, te recomiendo leer esta lección y esta otra lección, respectivamente. Aunque ambas hablan sobre tareas, la teoría aplica para cualquier tipo de objeto.)
CREACIÓN DINÁMICA
La función para crear mensajes dinámicos es:
uxQueueLength. Es el número máximo de elementos (no de bytes) que la cola almacenará. Puedes utilizar el valor QUEUE_ELEMS
del que hablé hace un momento.
uxItemSize. Es el número de bytes que requiere un elemento. Puedes utilizar el valor QUEUE_TYPE_SIZE
del que hablé hace un momento.
Valor devuelto. Si hubo memoria suficiente para crear la cola, entonces la función devuelve el handler; en caso contrario (no hubo memoria suficiente) devuelve NULL
. Este handler lo debes de guardar, tanto para que el resto de funciones de flujos sepan sobre cuál van a operar, como para probar que efectivamente fue creado.
Por ejemplo, para crear una cola 10 elementos de tipo uint16_t
, utilizando las constantes simbólicas anteriores, tendrías lo siguiente:
#include <FreeRTOS.h> #include <task.h> #include <queue.h> QueueHandle_t g_queue_handler = NULL; // el handler debe ser global #define QUEUE_ELEMS ( 10 ) #define QUEUE_TYPE uint16_t #define QUEUE_TYPE_SIZE ( sizeof( QUEUE_TYPE ) ) void setup() { // código ... g_queue_handler = xQueueCreate( QUEUE_ELEMS, QUEUE_TYPE_SIZE ); configASSERT( g_queue_handler ); // Error creando a la cola // más código ... }
El handler, g_queue_buffer
, es global porque debe ser visible tanto en la tarea productora como en la consumidora.
La creación dinámica de objetos no asegura que los objetos se creen, principalmente por escasez de memoria RAM. Por eso siempre deberías preguntar si el objeto realmente fue creado, y aunque existen diversas maneras de hacerlo, en este ejemplo escogí la macro configASSERT()
que es una versión propia de la conocida assert()
de C (en el archivo FreeRTOSConfig.h podrás ver la implementación que utilicé para el projecto Molcajete). Por supuesto que puedes usar al condicional if
y tratar de recuperarte sin terminar el programa de manera abrupta.
Creación estática
Ahora veamos cómo crear a un mensaje de manera estática. Como es usual, esta forma de creación de objetos necesita más pasos, pero es mucho más segura ya que los objetos siempre serán creados.
uxQueueLength. Es el número máximo de elementos (no de bytes) que la cola almacenará. Puedes utilizar el valor QUEUE_ELEMS
del que hablé hace un momento.
uxItemSize. Es el número de bytes que requiere un elemento. Puedes utilizar el valor QUEUE_TYPE_SIZE
del que hablé hace un momento.
pucQueueStorageBuffer. Es el búfer interno de la cola donde se guardarán los datos, y es un arreglo de tamaño ( uxQueueLength
* uxItemSize
) bytes de elementos de tipo uint8_t
. Este arreglo deberá existir a lo largo del programa, por lo cual deberás crearlo de manera global al programa, o estática a la función donde fue llamada xQueueCreateStatic()
.
pxQueueBuffer. Esta es la variable que guarda el estado de la cola. Al igual que el parámetro pucQueueStorageBuffer
, deberá existir a lo largo del programa.
Valor devuelto. El handler al mensaje. La creación estática de objetos nunca falla, a menos que el arreglo o la variable de estado sean NULL
, cosa que tal vez nunca suceda.
Un ejemplo de su uso es:
#include <FreeRTOS.h> #include <task.h> #include <queue.h> QueueHandle_t g_queue_handler = NULL; // el handler debe ser global #define QUEUE_ELEMS ( 10 ) #define QUEUE_TYPE uint16_t #define QUEUE_TYPE_SIZE ( sizeof( QUEUE_TYPE ) ) void setup() { // código ... static uint8_t queue_buffer_array[ QUEUE_BUFFER_SIZE ]; static StaticQueue_t queue_buffer_struct; g_queue_handler = xQueueCreateStatic( QUEUE_ELEMS, QUEUE_TYPE_SIZE, queue_buffer_array, &queue_buffer_struct ); // más código ... }
En este ejemplo marqué a las variables queue_buffer_array
y queue_buffer_struct
como static
; esto para que perduren durante todo el programa, pero que a la vez sean invisibles al resto de funciones. Una alternativa es que las declares globales, aunque entre menos variables globales tengas en tus programas, mejor. Sin embargo el handler, g_message_buffer
, sí que debe ser global porque tanto la tarea productora como la consumidora deben tener acceso a él. Este es un ejemplo de las veces en que sí son necesarias las variables globales. El prefijo g_
nos recuerda que es una variable global y que debemos tener mucho cuidado con ella.
Escribiendo y leyendo hacia y desde la cola
Una vez que la cola fue creada ya puedes utilizarla escribiendo en y leyendo desde ella. Cuando escribes a la cola lo haces elemento por elemento (y por copia, no lo olvides); estos elementos son variables de tipos básicos o tipos compuestos. En el Ejemplo 2 más adelante utilizaremos estos últimos.
La función para escribir en la cola es:
xQueue. Es el handler a la cola.
pvItemToQueue. Es un apuntador al dato que quieres escribir. Éste debe ser promocionado a void* (apuntador a void). Esta función escribe en la parte trasera de la cola.
xTicksToWait. Si no hay espacio disponible en la cola para copiar un nuevo elemento, entonces la tarea productora (la que llama a esta función) entrará al estado Blocked por el tiempo determinado en este parámetro. Si el tiempo expira la tarea pasará al estado Ready. El tiempo de espera está en ticks y puedes usar la macro pdMS_TO_TICKS()
para convertir milisegundos a ticks. Así mismo, si escribes 0 la función regresa inmediatamente, y si escribes portMAX_DELAY
el tiempo de espera es infinito (siempre y cuando la constante simbólica INCLUDE_vTaskSuspend
esté a 1).
Valor devuelto. Si el elemento se copió a la cola, entonces la función devuelve pdTRUE
; en caso contrario devolverá errQUEUE_FULL
.
Para leer un elemento desde la cola hacia el búfer de tu programa usamos la función xQueueReceive()
:
xQueue. Es el handler a la cola.
pvBuffer. Es un apuntador a la variable donde quieres guardar el elemento. Esta función lee del frente de la cola.
xTicksToWait. Si la cola está vacía, entonces no hay nada qué leer, por lo tanto la tarea consumidora (la que llama a esta función) entrará al estado Blocked por el tiempo determinado en este parámetro. Si el tiempo expira la tarea pasará al estado Ready. El tiempo de espera está en ticks y puedes usar la macro pdMS_TO_TICKS()
para convertir milisegundos a ticks. Así mismo, si escribes 0 la función regresa inmediatamente, y si escribes portMAX_DELAY
el tiempo de espera es infinito (siempre y cuando la constante simbólica INCLUDE_vTaskSuspend
esté a 1).
Valor devuelto. Si el elemento se leyó de la cola, entonces la función devuelve pdTRUE
; en caso contrario devolverá pdFALSE
. (Esta es una inconsistencia en la documentación oficial de FreeRTOS, ya que si xQueueSend()
devuelve el valor errQUEUE_FULL
en caso de que la cola esté llena, entonces xQueueReceive()
debería devolver errQUEUE_EMPTY
cuando la cola esté vacía, la cual ¡sí existe! y además tiene el mismo valor que pdFALSE
. En lo que obtengo una respuesta por parte de FreeRTOS usemos pdFALSE
, o haz que tus condicionales se prueben contra pdTRUE
.)
Ejemplos
Una vez que tenemos claro el concepto de cola, su uso es bastante simple. Vamos a ver 4 ejemplos que mostrarán diversos aspectos de la utilización de las colas:
- Creación dinámica de una cola de tipo simple. Una tarea produce y una tarea consume.
- Creación estática de una cola de tipo simple. Dos tareas producen y una tarea consume.
- Creación estática de una cola de tipo compuesto. Dos tareas producen y una tarea consume.
- Creación estática de una cola de tipo compuesto. Dos tareas producen y una tarea consume. El handler es pasado en el parámetro
pvParameters
.
Ejemplo 1: Creación dinámica de una cola de tipo simple
Este primer ejemplo la cola es creada de manera dinámica y la tarea productora (la cual es una nada más) escribe en bloques de QUEUE_ELEMS
elementos, y la tarea consumidora los consume tan pronto como es posible. El uso del ciclo for
no es necesario, todo depende de la fuente de datos que desees transmitir, pero para este ejemplo se me hizo conveniente simular la llegada de n elementos y luego hacer una pausa, y repetir el procedimiento:
#define PROGRAM_NAME "queues_1.ino" #include <FreeRTOS.h> #include <task.h> #include <queue.h> QueueHandle_t g_queue_handler = NULL; // el handler debe ser global #define QUEUE_ELEMS ( 10 ) // ¿Cuántos elementos tendrá la cola? #define QUEUE_TYPE uint16_t // ¿De qué tipo es el búfer? #define QUEUE_TYPE_SIZE ( sizeof( QUEUE_TYPE ) ) // ¿Cuántos bytes ocupa una variable de tipo QUEUE_TYPE? void producer_task( void* pvParameters ) { QUEUE_TYPE data = 10; while( 1 ) { for( uint8_t i = 0; i < QUEUE_ELEMS; ++i ){ if( xQueueSend( g_queue_handler, (void*) &data, pdMS_TO_TICKS( 100 ) ) != errQUEUE_FULL ){ data += 10; vTaskDelay( pdMS_TO_TICKS( 10 ) ); // simulamos un retardo entre escritura y escritura } else{ // timeout: Serial.println( "TO(W)" ); } } vTaskDelay( pdMS_TO_TICKS( 500 ) ); } } void consumer_task( void* pvParameters ) { while( 1 ) { QUEUE_TYPE data; while( xQueueReceive( g_queue_handler, &data, pdMS_TO_TICKS( 500 ) ) != errQUEUE_FULL ){ Serial.println( data ); } } } void setup() { xTaskCreate( producer_task, "PROD", 128 * 3, NULL, tskIDLE_PRIORITY + 1, NULL ); xTaskCreate( consumer_task, "CONS", 128 * 3, NULL, tskIDLE_PRIORITY, NULL ); g_queue_handler = xQueueCreate( QUEUE_ELEMS, QUEUE_TYPE_SIZE ); configASSERT( g_queue_handler ); // Error creando a la cola pinMode( 13, OUTPUT ); Serial.begin( 115200 ); Serial.println( PROGRAM_NAME ); vTaskStartScheduler(); } void loop() {}
De este ejemplo nota que utilicé las constantes simbólicas que describí hace un momento, y que la variable que deseas copiar a la cola debe ser promocionada a void*
, como también ya lo había mencionado:
xQueueSend( g_queue_handler, (void*) &data, pdMS_TO_TICKS( 100 ) )
Así mismo, antes de salir de la función setup()
he agregado una función que imprime el nombre del programa. Esto no es necesario, pero como para este curso he creado muchos programas, nunca estoy seguro cuál es el último que subí a mis tarjetas.
La salida de este programa es:
Ejemplo 2: Creación estática de una cola de tipo simple
En este ejemplo vamos a crear una cola de un tipo básico de manera estática. Además, dos tareas van a escribir en ella, y una tarea leerá de ella. Una tarea productora escribirá valores múltiplo de 10, mientras que la otra escribirá valores múltiplo de 7, y cuando llegue al valor arbitrario 350 (no tiene nada de especial), volverá a 0. Esto lo hice así para distinguir en la salida cuál tarea productora escribió cuál valor.
En los retardos notarás valores raros que realmente son números primos. Los utilicé para que no hubiera relación entre las diferentes temporizaciones tratando de simular un ambiente real. Debido a los diferentes tiempos de escritura y lectura la cola se llenará y presentarán los mensajes: TO(P2), el cual significa timeout en el productor 2; o el mensaje TO(P1), el cual significa timeout en el productor 1 (en ambos casos significa que la cola estaba llena y no pudieron escribir en ella).
Finalmente agregué una constante, USER_DBG
, que permite habilitar (en diferentes grados) o deshabilitar información de depuración. Por supuesto que esto no sería necesario en un programa real (¿o tal vez sí?) pero como estamos experimentando es interesante saber lo que sucede en el programa.
#define PROGRAM_NAME "queues_2.ino" #include <FreeRTOS.h> #include <task.h> #include <queue.h> QueueHandle_t g_queue_handler = NULL; #define QUEUE_ELEMS ( 10 ) // ¿Cuántos elementos tendrá la cola? #define QUEUE_TYPE uint16_t // ¿De qué tipo es el búfer? #define QUEUE_TYPE_SIZE ( sizeof( QUEUE_TYPE ) ) // ¿Cuántos bytes ocupa una variable de tipo QUEUE_TYPE? #define QUEUE_BUFFER_SIZE ( ( QUEUE_ELEMS ) * ( QUEUE_TYPE_SIZE ) ) // ¿Cuántos bytes tiene el búfer subyacente? #define USER_DBG 1 // Para habilitar/deshabilitar información de depuración: // 0: Deshabilitada // 1: Información básica // 2: Más información, pero puede hacer que el sistema falle (debido a Serial.print()) void producer1_task( void* pvParameters ) { QUEUE_TYPE data = 10; while( 1 ) { #if USER_DBG > 1 Serial.print( "TX1: " ); Serial.println( data ); #endif if( xQueueSend( g_queue_handler, (void*) &data, pdMS_TO_TICKS( 100 ) ) == pdFALSE ){ Serial.println( "TO(P1)" ); } else{ data += 10; if( data > 1000 ) data = 10; } vTaskDelay( pdMS_TO_TICKS( 439 ) ); } } void producer2_task( void* pvParameters ) { QUEUE_TYPE data = 0; while( 1 ) { #if USER_DBG > 1 Serial.print( "TX2: " ); Serial.println( data ); #endif if( xQueueSend( g_queue_handler, (void*) &data, pdMS_TO_TICKS( 100 ) ) != errQUEUE_FULL ){ data += 7; if( data > 350 ) data = 0; } else{ //timeout: Serial.println( "TO(P2)" ); } vTaskDelay( pdMS_TO_TICKS( 397 ) ); } } void consumer_task( void* pvParameters ) { TickType_t last_wake_time = xTaskGetTickCount(); while( 1 ) { QUEUE_TYPE data; vTaskDelayUntil( &last_wake_time, pdMS_TO_TICKS( 311 ) ); if( xQueueReceive( g_queue_handler, &data, pdMS_TO_TICKS( 509 ) ) == pdTRUE ){ #if USER_DBG > 0 Serial.print( "RX: " ); Serial.println( data ); #endif #if USER_DBG > 1 Serial.print( "SA: " ); Serial.println( uxQueueSpacesAvailable( g_queue_handler ) ); #endif } else{ //timeout: Serial.println( "TO(R) " ); } } } void setup() { xTaskCreate( producer1_task, "PRD1", 128, NULL, tskIDLE_PRIORITY + 1, NULL ); xTaskCreate( producer2_task, "PRD2", 128, NULL, tskIDLE_PRIORITY + 1, NULL ); xTaskCreate( consumer_task, "CONS", 128, NULL, tskIDLE_PRIORITY, NULL ); static uint8_t queue_buffer_array[ QUEUE_BUFFER_SIZE ]; // búfer subyacente de la cola static StaticQueue_t queue_buffer_struct; // variable de estado de la cola g_queue_handler = xQueueCreateStatic( QUEUE_ELEMS, QUEUE_TYPE_SIZE, queue_buffer_array, &queue_buffer_struct ); pinMode( 13, OUTPUT ); Serial.begin( 115200 ); Serial.println( PROGRAM_NAME ); vTaskStartScheduler(); } void loop() {}
La salida de este programa con USER_DBG puesta a 1 es:
Ejemplo 3: Creación estática de una cola de tipo compuesto
En ocasiones tendremos que transferir datos compuestos debido a que así está diseñado el sistema (no todo en la vida son tipos simples), o porque, como mencioné en la introducción, necesitamos saber quién escribió qué. En el ejemplo que dí sobre un controlador para una interfaz gráfica, el «controlador» debe conocer qué periférico generó el evento. Pero dado que el tipo de la cola es fijo debemos idear una manera de homogenizar la información; esto es, que cada periférico escriba información del mismo tipo.
El ejemplo que doy a continuación es una versión alterna y completa, pero simple, de un ejemplo que FreeRTOS ha incluído en su documentación oficial para encarar dicho problema.
Lo primero que vas a notar es la enum
eración Event
, la cual indica la fuente generó la información. Después viene el tipo compuesto, Data
, que encapsula la fuente generadora y el dato generado:
typedef enum Event { Sensor1, Sensor2, } Event; typedef struct Data { Event evt; uint16_t value; } Data;
Luego cada una de las dos tareas que incluí en el ejemplo se identifican de manera permanente en el campo .evt
de una variable data
del tipo Data
, dado que lo único que va a cambiar mientras el programa se esté ejecutando es el valor escrito en el campo .value
de dicha variable (que hace las veces de búfer). Una tarea productora toma valores de temperatura, mientras que otra genera una secuencia numérica, y a diferencia de lo que hemos venido haciendo, ésta se puede bloquear (entrar al estado Blocked) por tiempo indefinido (solamente como ejemplo, en la vida real deberías evitar esta situación).
Finalmente, la tarea consumidora, que hace las veces de «controlador», implementa un switch
sobre el campo .evt
para determinar de dónde provienen los datos y proceder de manera correcta.
#define PROGRAM_NAME "queues_3.ino" #include <FreeRTOS.h> #include <task.h> #include <queue.h> typedef enum Event { Sensor1, Sensor2, } Event; typedef struct Data { Event evt; uint16_t value; } Data; QueueHandle_t g_queue_handler = NULL; #define QUEUE_ELEMS ( 5 ) // ¿Cuántos elementos tendrá la cola? #define QUEUE_TYPE Data // ¿De qué tipo es el búfer? #define QUEUE_TYPE_SIZE ( sizeof( QUEUE_TYPE ) ) // ¿Cuántos bytes ocupa una variable de tipo QUEUE_TYPE? #define QUEUE_BUFFER_SIZE ( ( QUEUE_ELEMS ) * ( QUEUE_TYPE_SIZE ) ) // ¿Cuántos bytes ocupa el búfer subyacente? #define USER_DBG 1 void sensor1_task( void* pvParameters ) { QUEUE_TYPE data = { .evt = Sensor1, .value = 0 }; while( 1 ) { data.value = analogRead( A0 ) & 0x03FF; // sensor de temperatura LM35 if( xQueueSend( g_queue_handler, (void*) &data, pdMS_TO_TICKS( 100 ) ) == errQUEUE_FULL ){ Serial.println( "TO(S1)" ); } vTaskDelay( pdMS_TO_TICKS( 457 ) ); } } void sensor2_task( void* pvParameters ) { QUEUE_TYPE data = { .evt = Sensor2, .value = 0 }; while( 1 ) { xQueueSend( g_queue_handler, (void*) &data, portMAX_DELAY ); data.value += 10; if( data.value > 500 ) data.value = 0; vTaskDelay( pdMS_TO_TICKS( 397 ) ); } } void controller_task( void* pvParameters ) { TickType_t last_wake_time = xTaskGetTickCount(); while( 1 ) { QUEUE_TYPE data; vTaskDelayUntil( &last_wake_time, pdMS_TO_TICKS( 257 ) ); if( xQueueReceive( g_queue_handler, &data, pdMS_TO_TICKS( 509 ) ) == pdTRUE ){ switch( data.evt ) { case Sensor1: Serial.print( "SENSOR1: " ); Serial.println( data.value ); break; case Sensor2: Serial.print( "SENSOR2: " ); Serial.println( data.value ); break; default: Serial.println( "ERR" ); break; } } else{ //timeout: Serial.println( "TO(R) " ); } } } void setup() { xTaskCreate( sensor1_task, "SNS1", 128, NULL, tskIDLE_PRIORITY + 1, NULL ); xTaskCreate( sensor2_task, "SNS2", 128, NULL, tskIDLE_PRIORITY + 1, NULL ); xTaskCreate( controller_task, "CTRL", 128, NULL, tskIDLE_PRIORITY, NULL ); static uint8_t queue_buffer_array[ QUEUE_BUFFER_SIZE ]; static StaticQueue_t queue_buffer_struct; g_queue_handler = xQueueCreateStatic( QUEUE_ELEMS, QUEUE_TYPE_SIZE, queue_buffer_array, &queue_buffer_struct ); pinMode( 13, OUTPUT ); Serial.begin( 115200 ); Serial.println( PROGRAM_NAME ); vTaskStartScheduler(); } void loop() {}
La salida de este programa es:
En el ejemplo anterior escribí el valor en bruto entregado por el sensor de temperatura LM35, que al momento de ejecutar el programa reportaba valores alrededor de 52-53. Pero si quisieras ver el valor correspondiente a la temperatura podrías modificar el case
de Sensor1:
case Sensor1: Serial.print( "SENSOR1: " ); Serial.println( (float) (data.value*500.0/1024.0) ); break;
(En esta entrada mi blog alternativo describí las diferentes partes de la fórmula, por si te interesa conocerlas.)
La salida del programa ya con la temperatura en grados centígrados es:
Ejemplo 4: Creación estática de una cola de tipo compuesto. El handler es pasado en el parámetro pvParameters
.
NOTA: La siguiente solución no funciona cuando uno de los productores o uno de los consumidores es una interrupción, ya que la única forma de que ésta tenga acceso al handler es haciéndolo global.
En todos los ejemplos vistos el handler a la cola fue declarado como global y cualquier tarea podría escribir hacia o leer desde ella. En muchas situaciones esto quizás no sea importante, pero en otras sería mejor restringir qué tareas tienen acceso a la cola.
Ya en la lección sobre Semáforos Mutex exploramos cómo limitar el acceso a un monitor solamente por las tareas interesadas. Aquella vez usamos, y hoy lo volveremos a hacer, el parámetro pvParameters
(que cada tarea incluye) para pasarle el handler. De esta manera solamente las tareas involucradas podrán accesar a la cola. Así mismo, el handler a la misma deja de ser global (entre menos globales mejor) ya que la declararemos static
en la función setup()
.
En principio esta modificación es sencilla, pero el demonio está en los detalles, y como siempre sucede en C/C++, el demonio se presenta en forma de apuntador. Voy a diseccionar el código para intentar explicar el procedimiento; al final de la sección estará el programa completo. ¡No parpadees!
En la función setup()
declaramos como static
el handler a la cola. Luego le pasamos a las funciones de creación de tareas la dirección del handler promocionada a void*
. (El handler ya es un apuntador, así que estamos pasando la dirección del apuntador.)
void setup() { static QueueHandle_t queue_handler; // este es el handler (internamente es un apuntador) xTaskCreate( sensor1_task, "SNS1", 128, (void*) &queue_handler, // aquí pasamos la dirección del handler tskIDLE_PRIORITY + 1, NULL ); xTaskCreate( sensor2_task, "SNS2", 128, (void*) &queue_handler, tskIDLE_PRIORITY + 1, NULL ); xTaskCreate( controller_task, "CTRL", 128, (void*) &queue_handler, tskIDLE_PRIORITY, NULL ); // más código ... }
Después, en cada tarea que deba recibir al handler de la cola debemos crear una variable que guarde la dirección del handler. Finalmente tenemos que desreferenciar dicha variable para accesar al handler propiamente dicho:
void sensor1_task( void* pvParameters ) { QueueHandle_t* queue = (QueueHandle_t*) pvParameters; // queue guarda la dirección del handler while( 1 ) { if( xQueueSend( *queue, // desreferenciamos la variable queue para obtener el handler (void*) &data, pdMS_TO_TICKS( 100 ) ) == pdFALSE ){ // más código ... } // más código ... } }
¡Wow, esos apuntadores que nos vuelven locos! A continuación te presento el programa completo:
#define PROGRAM_NAME "queues_4.ino" #include <FreeRTOS.h> #include <task.h> #include <queue.h> typedef enum Event { Sensor1, Sensor2, } Event; typedef struct Data { Event evt; uint16_t value; } Data; #define QUEUE_ELEMS ( 5 ) // ¿Cuántos elementos tendrá la cola? #define QUEUE_TYPE Data // ¿De qué tipo es el búfer? #define QUEUE_TYPE_SIZE ( sizeof( QUEUE_TYPE ) ) // ¿Cuántos bytes ocupa una variable de tipo QUEUE_TYPE? #define QUEUE_BUFFER_SIZE ( ( QUEUE_ELEMS ) * ( QUEUE_TYPE_SIZE ) ) // ¿Cuántos bytes el búfer subyacente? void sensor1_task( void* pvParameters ) { QueueHandle_t* queue = (QueueHandle_t*) pvParameters; QUEUE_TYPE data = { .evt = Sensor1, .value = 0 }; while( 1 ) { data.value = analogRead( A0 ) & 0x03FF; // sensor de temperatura LM35 if( xQueueSend( *queue, (void*) &data, pdMS_TO_TICKS( 100 ) ) == pdFALSE ){ Serial.println( "TO(S1)" ); } vTaskDelay( pdMS_TO_TICKS( 457 ) ); } } void sensor2_task( void* pvParameters ) { QueueHandle_t* queue = (QueueHandle_t*) pvParameters; QUEUE_TYPE data = { .evt = Sensor2, .value = 0 }; while( 1 ) { xQueueSend( *queue, (void*) &data, portMAX_DELAY ); data.value += 10; if( data.value > 500 ) data.value = 0; vTaskDelay( pdMS_TO_TICKS( 397 ) ); } } void controller_task( void* pvParameters ) { QueueHandle_t* queue = (QueueHandle_t*) pvParameters; TickType_t last_wake_time = xTaskGetTickCount(); while( 1 ) { vTaskDelayUntil( &last_wake_time, pdMS_TO_TICKS( 257 ) ); QUEUE_TYPE peek; if( xQueuePeek( *queue, &peek, 0 ) == pdFALSE ){ Serial.println( "NO EVENTS" ); } else{ Serial.print( "NEXT EVENT: " ); Serial.println( peek.evt ); } QUEUE_TYPE data; if( xQueueReceive( *queue, &data, pdMS_TO_TICKS( 509 ) ) == pdTRUE ){ switch( data.evt ) { case Sensor1: Serial.println( (float) ( data.value*500.0/1024.0) ); break; case Sensor2: Serial.println( data.value ); break; default: Serial.println( "ERR" ); break; } } else{ //timeout: Serial.println( "TO(R) " ); } } } void setup() { static QueueHandle_t queue_handler; xTaskCreate( sensor1_task, "SNS1", 128, (void*) &queue_handler, tskIDLE_PRIORITY + 1, NULL ); xTaskCreate( sensor2_task, "SNS2", 128, (void*) &queue_handler, tskIDLE_PRIORITY + 1, NULL ); xTaskCreate( controller_task, "CTRL", 128, (void*) &queue_handler, tskIDLE_PRIORITY, NULL ); static uint8_t queue_buffer_array[ QUEUE_BUFFER_SIZE ]; static StaticQueue_t queue_buffer_struct; queue_handler = xQueueCreateStatic( QUEUE_ELEMS, QUEUE_TYPE_SIZE, queue_buffer_array, &queue_buffer_struct ); pinMode( 13, OUTPUT ); Serial.begin( 115200 ); Serial.println( PROGRAM_NAME ); vTaskStartScheduler(); } void loop() {}
La función controller_task()
usa una operación muy importante de las colas que mencioné en la introducción, la operación xQueuePeek()
, la cual no habíamos utilizado hasta ahora, cuya función es devolver la copia del elemento al frente de la cola, pero sin extraerlo. No era necesaria en el ejemplo, pero quise que vieras su utilización.
La salida del programa es:
Otras funciones
La API de colas es muy grande (la puedes revisar aquí), por lo que sólo te voy a platicar de algunas funciones que pudieran ser importantes.
Colas e interrupciones
Como ya es costumbre en FreeRTOS, debes tener mucho cuidado con las funciones que llames desde dentro de una interrupción. Cuando se dé el caso asegúrate que las funciones sean thread-safe, es decir, aquellas que terminan en FromISR. Las versiones thread-safe de las funciones que hemos vistos hasta el momento son:
BaseType_t xQueueSendFromISR( QueueHandle_t xQueue, const void *pvItemToQueue, BaseType_t *pxHigherPriorityTaskWoken ); BaseType_t xQueueReceiveFromISR( QueueHandle_t xQueue, void *pvBuffer, BaseType_t *pxHigherPriorityTaskWoken ); BaseType_t xQueuePeekFromISR( QueueHandle_t xQueue, void *pvBuffer );
Los primeros dos argumentos son idénticos a sus contrapartes; el último, pxHigherPriorityTaskWoken
, indica si una tarea de mayor prioridad a la que está ejecutándose salió del estado Blocked cuando la respectiva función fue llamada y, en consecuencia, debe ser ejecutada a continuación.
Nota que ninguna de estas funciones incluye al parámetro de tiempo de espera, xTicksToWait
, ya que no tiene sentido (y jamás debería suceder) que una tarea se bloquee dentro de una interrupción.
En esta lección podrás ver cómo se utiliza el parámetro pxHigherPriorityTaskWoken
en una interrupción. No creo que tengas problemas en adaptar el ejemplo para usar colas.
Funciones para ver o alterar el estado de la cola
A veces es necesario preguntar por la cantidad de elementos que aún pueden ser insertados en la cola, o por la cantidad de elementos actualmente en la cola. FreeRTOS proporciona sendas funciones:
UBaseType_t uxQueueSpacesAvailable( QueueHandle_t xQueue ); // devuelve el número de elementos que aún pueden ser insertados UBaseType_t uxQueueMessagesWaiting( QueueHandle_t xQueue ); // devuelve el número de elementos actualmente en la cola UBaseType_t uxQueueMessagesWaitingFromISR( QueueHandle_t xQueue ); // es la versión thread-safe de uxQueueMessagesWaiting
IMPORTANTE: Nunca uses uxQueueMessagesWaiting()
ni uxQueueSpacesAvailable()
en situaciones como la siguiente, porque el programa no funcionará (ya le pasó al primo de un amigo. Aquí la discusión completa):
while( 1 ) { if( uxQueueMessagesWaiting( queue_handler ) > 0 ){ // aquí consumes los datos } // El programa llega aquí si no hay elementos en la cola, pero el proceso puede ser tan // rápido que no "preste" la CPU y el programa se quede colgado, dado que dentro de // uxQueueMessagesWaiting() las interrupciones se desactivan y reactivan (en ese orden) }
En su lugar agrega un else
que llame a una función bloqueante:
while( 1 ) { if( uxQueueMessagesWaiting( queue_handler ) > 0 ){ // aquí consumes los datos } else { vTaskDelay( 1 ); // o cualquier otra función que se bloquée } }
Cuando necesites «limpiar» la cola, sin necesidad de destruirla y volverla a crear, puedes usar a la función xQueueReset
:
BaseType_t xQueueReset( QueueHandle_t xQueue ); // siempre devuelve pdTRUE. Pone a su estado original a la cola
FreeRTOS incluye un par de funciones que deben ser usadas dentro de interrupciones para preguntar si una cola está llena o vacía:
BaseType_t xQueueIsQueueFullFromISR( const QueueHandle_t xQueue ); // devuelve pdTRUE si la cola está llena; o pdFALSE en caso contrario BaseType_t xQueueIsQueueEmptyFromISR( const QueueHandle_t xQueue ); // devuelve pdTRUE si la cola está vacía; o pdFALSE en caso contrario
NOTA: De manera extraña no existen las contrapartes de estas funciones que se puedan usar en tareas regulares.
¿Qué sigue?
Hoy vimos el 4to mecanismo que FreeRTOS incluye para llevar a cabo la comunicación inter-tareas, la cual es una actividad fundamental en cualquier programa secuencial o concurrente.
Estudiamos las funciones más importantes de la API de las colas; sin embargo, siempre podrás estudiar por tu cuenta aquellas que no vimos.
RECUERDA: Utiliza las constantes que te di al principio de esta lección, en lugar de calcular por tu cuenta los diferentes tamaños de búferes.
RECUERDA: Siempre que te sea posible utiliza objetos estáticos.
RECUERDA: Si vas a llamar a una función de FreeRTOS desde una interrupción, verifica que ésta sea la versión thread safe, es decir, tenga terminación FromISR
.
RECUERDA: Las colas se utilizan cuando en la transferencia de datos intervienen dos o más productores, o dos o más consumidores. Si la transferencia se va a llevar uno-a-uno, entonces podrías usar las versiones «ligeras», como los mensajes.
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.
- Esta clase poco conocida de Arduino le dará vida a tus displays de texto y te olvidarás de tener que escribir funciones de conversión - septiembre 20, 2024
- Printable: The class you didn’t know existed in Arduino and that you won’t be able to stop using - septiembre 1, 2024
- Printable: La clase que no sabías que existía en Arduino y que no podrás dejar de usar - agosto 3, 2024
1 COMENTARIO