Projetos no Arduino com FreeRTOS – filas (queues) 1

No artigo “Projetos no Arduino com FreeRTOS –  conceitos, tasks e consumo de memória por task“, vimos os principais conceitos do FreeRTOS, como medir consumo de memória por uma task e como utilizar o FreeRTOS no Arduino. Agora, vamos prosseguir com o assunto, mostrando um outro recurso muito valioso: o uso de uma fila (queue) com o FreeRTOS.

Material necessário

A lista de material aqui é bem similar ao do artigo “Projetos no Arduino com FreeRTOS –  conceitos, tasks e consumo de memória por task“. A diferença aqui é que iremos acrescentar um potenciômetro linear de 100k ohm. Dessa forma, a lista de materiais completa é:

Filas – conceito principal

Uma fila (ou queue, como são mencionadas na literatura) é um recurso disponibilizado pelo FreeRTOS para permitir o armazenamento e leitura ordenada de informações. Utilizando uma fila, garante-se que os dados ali inseridos ficarão íntegros da escrita até a leitura, algo difícil de garantir quando, por exemplo, utilizamos variáveis globais para escrever informações que devem ser comuns a várias funções / métodos / tasks.

Quanto ao tipo / método de inserção e leitura de dados numa fila no FreeRTOS, é utilizado o padrão FIFO (First-In, First-Out). isso significa dizer que os itens sempre serão lidos de uma fila na mesma ordem em que foram inseridos. Isso é muito importante pois, em vários casos (na grande maioria deles, na verdade), a ordem das informações lidas é importante. Para melhor entender o funcionamento de uma FIFO, veja o gif animado abaixo. Nele, a seta verde simboliza a inserção de item na fila, enquanto a seta vermelha simboliza a leitura / remoção de item da fila.

Imagem 1 - Fila com FreeRTOS

Você pode estar se perguntando: mas porque é tão importante assim garantir a ordem de inserção e leitura de itens numa fila? Para mostrar o porque, imagine a seguinte situação: utilizar uma fila para armazenar caracteres de uma mensagem textual (em português) a serem enviados via serial, de forma a fazer um “chat serial”. Se a ordem de leitura fosse diferente da ordem de inserção, quem recebesse a informação via serial (o destinatário) poderia não conseguir interpretar o que recebeu. Logo, garantir a ordem de inserção e leitura/remoção de itens numa fila é fundamental para alguns sistemas.

O fato de ser seguro e garantir a ordem de inserção na leitura faz deste um recurso muito importante do FreeRTOS.

Declarando uma fila no FreeRTOS

Quando estiver usando o FreeRTOS, uma fila pode ser declarada da seguinte forma:

QueueHandle_t xQueue_Teste;

Ou seja, uma fila é para o FreeRTOS uma variável do tipo QueueHandle_t.

IMPORTANTE: embora seja um recurso extremamente útil, filas ocupam uma considerável quantidade de memória RAM. Você pode e DEVE utilizá-las, mas use com moderação (crie somente filas que sejam realmente necessárias). Um dos pontos-chave de um bom projeto com FreeRTOS é o correto uso de filas.

Para inicializar uma fila, faça conforme a seguir:

xQueue_Teste = xQueueCreate( NUMERO_ITENS_FILA, TAMANHO_DE_CADA_ITEM );

Onde:

  • NUMERO_ITENS_FILA: quantidade total de itens que você deseja que sua fila possua. Lembre-se que mais itens significa que mais memória RAM é ocupada.
  • TAMANHO_DE_CADA_ITEM: tamanho (em bytes) de cada item da fila.
    Por exemplo, se cada item de sua fila for um número inteiro, este campo deverá ser igual a sizeof(int).

Formas de inserir itens na fila com FreeRTOS

O FreeRTOS disponibiliza várias formas diferentes de inserir itens numa fila. A razão disso é que, durante o desenvolvimento, há várias situações possíveis nas quais podemos inserir itens numa fila (como, por exemplo, dentro de tratamentos de interrupções, dentro de tarefas e por aí vai). Logo, para o correto funcionamento do seu projeto, é essencial saber utilizar a forma adequada para a situação em que a task se encontra.

Segue abaixo as principais formas de inserção de itens numa fila no FreeRTOS:

  • xQueueSend: adiciona elemento a uma fila. Esta função não deve ser utilizada dentro do tratamento de uma interrupção (ou dentro de callbacks).
  • xQueueSendFromISR: adiciona elemento a uma fila. Esta função deve ser somente usada dentro do tratamento de uma interrupção (ou dentro de callbacks).
  • xQueueOverwrite: sobrescreve o primeiro elemento de uma fila. Essa função é especialmente útil quando se utiliza uma fila de um único elemento, onde somente o valor mais recente (última leitura de um sensor, por exemplo) é que importa ser mantido. Tipicamente, as filas que usam esse tipo de inserção são filas unitárias (de um só item).
    Esta função não deve ser utilizada dentro do tratamento de uma interrupção (ou dentro de callbacks).
  • xQueueOverwriteFromISR: análogo ao anterior, ou seja, sobrescreve o primeiro elemento de uma fila, porém deve ser usada somente dentro de um tratamento de interrupção(ou dentro de callbacks). Exatamente como o caso acima, essa função é especialmente útil quando se utiliza uma fila de um único elemento, onde somente o valor mais recente (última leitura de um sensor, por exemplo) é que importa ser mantido. Tipicamente, as filas que usam esse tipo de inserção são filas unitárias (de um só item).

Ainda, é permitido especificar o tempo de espera para conseguir incluir um item na fila, sendo de uma determinada quantidade de tempo (medida em ticks do processador) ou esperar até dar certo (portMAX_DELAY).

Para mais informações, acesse: https://www.freertos.org/a00018.html

Formas de ler / remover itens na fila com FreeRTOS

Assim como para inserção, o FreeRTOS também disponibiliza várias formas diferentes de se ler / remover itens numa fila, pelas mesmas razões descritas. Segue abaixo as principais formas de inserção de itens numa fila no FreeRTOS:

  • xQueueReceive: lê um elemento da fila. Esta função não deve ser utilizada dentro do tratamento de uma interrupção (ou dentro de callbacks).
  • xQueueReceiveFromISR: lê um elemento da fila. Esta função somente deve ser utilizada dentro do tratamento de uma interrupção (ou dentro de callbacks).
  • xQueuePeek: faz a leitura do elemento da fila, porém, sem retirá-lo dela. Isso é útil quando a tarefa deseja verificar se a informação na fila deve ou não ser tratada por ela, sem alterar nada da fila para isso. Em analogia livre, é como “dar uma espiadinha” no item a ser lido / removido da fila.
    Esta função não deve ser utilizada dentro do tratamento de uma interrupção (ou dentro de callbacks).
  • xQueuePeekFromISR: análogo ao anterior, ou seja, faz a leitura o elemento da fila, porém sem retirá-lo dela, porém somente deve ser utilizada dentro do tratamento de uma interrupção (ou dentro de callbacks).
    Isso é especialmente  útil quando a tarefa deseja verificar se a informação na fila deve ou não ser tratada por ela, sem alterar nada da fila para isso.

Ainda, é permitido especificar o tempo de espera para conseguir remover / ler um item da fila, sendo de uma determinada quantidade de tempo (medida em ticks do processador) ou esperar até dar certo (portMAX_DELAY).

Para mais informações, acesse: https://www.freertos.org/a00018.html

Boas práticas de uso de uma fila

Assim como ocorre com todo recurso de software e hardware disponível, há boas práticas no uso de filas no FreeRTOS. Estas boas práticas, como o nome diz, não são obrigações, mas sim práticas recomendadas se você deseja não ter dores de cabeça com uso de filas no FreeRTOS no futuro.

Imagem 2 - Fila com FreeRTOS

Estas boas práticas são:

  1. Utilize as filas com sabedoria: conforme dito, filas consomem uma quantidade considerável de memória RAM e, portanto, devem ser usadas somente se necessário. Lembre-se que estamos falando de sistemas de software que rodarão em microcontroladores com, no máximo, algumas centenas de kilobytes de memória RAM.
  2. Use filas para comunicar tarefas distintas: as filas têm muitas utilidades, sendo uma das principais permitir o envio de dados / comunicação de uma tarefa para outra. Isso tem até um nome especial na literatura: Inter-Processes Communication (IPC).
    Porém, para melhor organização do código (e pra facilitar sua vida caso tiver que debugá-lo), é uma boa prática usar uma fila para comunicar somente duas tarefas distintas.
  3. PRECISA enviar itens para mais de uma tarefa? Tudo bem: se não tiver jeito e você PRECISAR enviar itens / dados de uma tarefa para várias outras tarefas, é recomendável o uso de filas unitárias (de um só item) e a leitura ser no formato “espiadinha” (usando xQueuePeek e xQueuePeekFromISR). Motivo: quando várias tarefas precisam do mesmo dado, este não pode ser lido e retirado da fila (senão uma tarefa vai “matar a fila”, logo as tarefas devem ler o valor contido na fila unitária, copiar o valor para si e dar o tratamento necessário.
    Isso é muito comum quando uma leitura de sensor é requerida por várias tarefas, por exemplo.
  4. Utilize uma estrutura: uma forma inteligente de inserir vários tipos distintos de dados num só item de uma fila é usando uma estrutura. Dessa forma, cada item representará um “pacote” de dados.

Exemplo prático de uso de filas

Agora chegou a hora de botarmos em prática os conceitos de fila aqui aprendidos.
Vamos aprimorar o exemplo que vimos antes (no artigo “Projetos no Arduino com FreeRTOS – conceitos, tasks e consumo de memória por task“), utilizando uma fila para enviar o valor de uma leitura analógica (lida por uma tarefa dedicada a isso) para a tarefa de display LCD, de forma que esta possa escrever no display o valor de tal leitura.
Para este projeto, monte o seguinte circuito esquemático:

Imagem 3 - Fila com FreeRTOS

O código-fonte deste projeto está disponível abaixo.

IMPORTANTE: leia com atenção os comentários do código-fonte para total entendimento do mesmo.

#include <Arduino_FreeRTOS.h>
#include <queue.h>
#include <task.h>
#include <semphr.h>
#include <Wire.h> 
#include <LiquidCrystal_I2C.h>

/* defines - LCD */
#define LCD_16X2_CLEAN_LINE                "                "
#define LCD_16X2_I2C_ADDRESS               0x27
#define LCD_16X2_COLS                      16 
#define LCD_16X2_ROWS                      2 

/* defines - LED */
#define LED_PIN                      12
#define LED_THRESHOLD                3.58 /* V

/* defines - ADC */
#define ADC_MAX                      1023.0
#define MAX_VOLTAGE_ADC              5.0

/* tasks */
void task_breathing_light( void *pvParameters );
void task_serial( void *pvParameters );
void task_lcd( void *pvParameters );
void task_sensor( void *pvParameters );
void task_led( void *pvParameters );

/* Variaveis relacionadas ao LCD */
LiquidCrystal_I2C lcd(LCD_16X2_I2C_ADDRESS, 
                      LCD_16X2_COLS, 
                      LCD_16X2_ROWS);

/* filas (queues) */
QueueHandle_t xQueue_LCD;

void setup() {  
  /* Inicializa serial (baudrate 9600) */
  Serial.begin(9600);

  /* Inicializa o LCD, liga o backlight e limpa o LCD */
  lcd.init();
  lcd.backlight();
  lcd.clear();

  /* Inicializa e configura GPIO do LED */ 
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, LOW);
  
  while (!Serial) {
    ; /* Somente vá em frente quando a serial estiver pronta para funcionar */
  }

  /* Criação das filas (queues) */ 
  xQueue_LCD = xQueueCreate( 1, sizeof( float ) );
    
if (xQueue_LCD == NULL)
  {
     Serial.println("Erro: nao e possivel criar a fila");
     while(1); /* Sem a fila o funcionamento esta comprometido. Nada mais deve ser feito. */
  } 
  /* Criação das tarefas */
  xTaskCreate(
    task_sensor                     /* Funcao a qual esta implementado o que a tarefa deve fazer */
    ,  (const portCHAR *)"sensor"   /* Nome (para fins de debug, se necessário) */
    ,  128                          /* Tamanho da stack (em words) reservada para essa tarefa */
    ,  NULL                         /* Parametros passados (nesse caso, não há) */
    ,  3                            /* Prioridade */
    ,  NULL );                      /* Handle da tarefa, opcional (nesse caso, não há) */

  xTaskCreate(
    task_lcd
    ,  (const portCHAR *) "LCD"
    ,  156  
    ,  NULL
    ,  2 
    ,  NULL );

  xTaskCreate(
    task_led
    ,  (const portCHAR *)"LED"
    ,  128  
    ,  NULL
    ,  1  
    ,  NULL );

  /* A partir deste momento, o scheduler de tarefas entra em ação e as tarefas executam */
}

void loop()
{
  /* Tudo é executado nas tarefas. Há nada a ser feito aqui. */
}

/* --------------------------------------------------*/
/* ---------------------- Tarefas -------------------*/
/* --------------------------------------------------*/

void task_sensor( void *pvParameters )
{
    (void) pvParameters;
    int adc_read=0;
    float voltage = 0.0;
    
    while(1)
    {   
        adc_read = analogRead(0);
        voltage = ((float)adc_read/ADC_MAX)*MAX_VOLTAGE_ADC;
        
        /* Envia tensão lida em A0 para a tarefa de display usando uma fila */
        xQueueOverwrite(xQueue_LCD, (void *)&voltage);
        
        /* Espera um segundo */
        vTaskDelay( 1000 / portTICK_PERIOD_MS ); 
    }
}

void task_lcd( void *pvParameters )
{
    (void) pvParameters;
    float voltage_rcv = 0.0;
    UBaseType_t uxHighWaterMark;

    while(1)
    {        
        /* Espera até algo ser recebido na queue. 
           A leitura é obrigatória e não importa o tempo necessário 
           para acontecer (portMAX_DELAY) */
        xQueueReceive(xQueue_LCD, (void *)&voltage_rcv, portMAX_DELAY);
        
        /* Uma vez recebida a informação na queue, a escreve no display LCD */
        lcd.setCursor(0,0);
        lcd.print("Tensao ADC:");
        lcd.setCursor(0,1);
        lcd.print(LCD_16X2_CLEAN_LINE);
        lcd.setCursor(0,1);
        lcd.print(voltage_rcv);
        lcd.setCursor(15,1);
        lcd.print("V");

        /* Para fins de teste de ocupação de stack, printa na serial o high water mark */
        uxHighWaterMark = uxTaskGetStackHighWaterMark( NULL );
        Serial.print("task_lcd - high water mark (words): ");
        Serial.println(uxHighWaterMark);
        Serial.println("---");
    }  
}

void task_led( void *pvParameters )
{
    (void) pvParameters;
   
    while(1)
    {
        /* ATENÇÃO: NUNCA USE A FUNÇÃO delay() QUANDO ESTIVER USANDO FREERTOS!
          em seu lugar, use a função vTaskDelay( tempo / portTICK_PERIOD_MS );, substituindo "tempo" pelo tempo de
          delay (em ms) desejado.
        */

        digitalWrite(LED_PIN, HIGH);
        vTaskDelay( 250 / portTICK_PERIOD_MS );
        digitalWrite(LED_PIN, LOW);
        vTaskDelay( 250 / portTICK_PERIOD_MS );
    }
}

Recapitulando: este programa é um aprimoramento daquele feito no post “Projetos no Arduino com FreeRTOS – conceitos, tasks e consumo de memória por task“. Aqui, uma tarefa é acrescentada, sendo esta dedicada a fazer a leitura do ADC (task_sensor).  Esta tarefa envia a leitura do ADC, via fila dedicada (xQueue_LCD),  para a task que controla o display LCD 16×2 I²C (task_lcd).
Dessa forma, task_sensor e task_lcd se comunicam através de uma fila, mostrando um uso muito interessante das filas no FreeRTOS.

Gostou deste post sobre filas (queues) no FreeRTOS? Deixe seu comentário logo abaixo.

Faça seu comentário

Acesse sua conta e participe

Um Comentário

  1. Não tem um semáforo, xSerial_semaphore , perdido aí no código não?

    angelo jose roncali da silva