Primeiros passos com o FreeRTOS no Arduino

Conceitos iniciais

O FreeRTOS é atualmente um dos sistemas operacionais de tempo real mais utilizados no universo de sistemas embarcados, isso se deve tanto ao fato de seu código ser aberto (open source) como também pela possibilidade de ser utilizado em inúmeros microcontroladores diferentes. Atualmente existem versões do sistema operacional para mais de 35 famílias de processadores, incluindo a linha Atmel AVR, que conta com o microcontrolador ATMega328, presente no Arduino UNO, que será utilizado para execução de nossos testes neste artigo.

Por ser um sistema operacional preemptivo, ou seja, capaz de interromper temporariamente uma tarefa sem a cooperação da mesma a fim de retoma-la posteriormente, o FreeRTOS permite criar um ambiente multitasking. Sendo assim, cada tarefa tem o direito de fazer uso do processador por um determinado período de tempo, denominado quantum, quando este tempo se esgota a tarefa seguinte toma o controle do processador. Ao se fechar o ciclo e retornar para a primeira tarefa, ela é executada exatamente do ponto onde parou anteriormente.

A ordem em que as tarefas são executadas está atrelada à prioridade dada a ela no programa. No FreeRTOS a prioridade da tarefa e definida por seu valor numérico, tendo a de número 0 a menor prioridade. Geralmente definine-se um nível prioridade diferente para cada tarefa, porém, pode ser necessário um cuidado especial para que certas tarefas não fiquem eternamente esperando sua vez de utilizar o processador, o que pode acontecer com tarefas que dependam de inputs.

Programando com FreeRTOS

Inicialmente, na IDE do Arduino vá no menu Ferramentas > Gerenciar bibliotecas busque e instale a versão mais recente da biblioteca FreeRTOS by Richard Barry.

Criação de tarefas (Tasks)

Deve-se declarar de maneira global as tarefas presentes no projeto, elas devem seguir o seguinte formato:

// Declaração das tarefas

void Tarefa1(void *pvParameters);
void Tarefa2(void *pvParameters);
.
.
.
void TarefaN(void *pvParameters);

É importante ressaltar que esta função deve ter, obrigatoriamente, tipo de retorno como void e parâmetro único do tipo ponteiro para void, dessa forma, pode-se passar qualquer tipo de parâmetros para a tarefa, bastando a tarefa, quando em execução, fazer o casting para o tipo desejado.

Para criar efetivamente as tarefas devemos utilizar o comando xTaskCreate. Essa é uma função disponibilizada pelo FreeRTOS que conta com diversos parâmetros, como se pode observar no protótipo a seguir:

// Criação de tarefas
  
xTaskCreate(
     Tarefa1        // -Tarefa a ser configurada
     , "Tarefa 01"  // -Nome (para facilitar o debug)
     , 128          // -Stack (em words) reservada para essa função
     , NULL         // -Parâmetros passados para a tarefa
     , 2            // -Prioridade
     , NULL         // -Handle para a tarefa. Este parâmetro é opcional,          
     );             //  desde que você não precise suspender e reiniciar 
                    //  a tarefa durante o tempo de execução da tarefa.
     

Como visto acima, um dos parâmetros para a criação de tarefas é o tamanho da stack, ou seja, a quantidade de memória RAM que a task terá a sua disposição. Mas como saber a quantidade que será suficiente e necessária?

Uma das maneiras mais eficazes de fazer isso é através do monitoramento do High Water Mark, que é um mecanismo que registra o máximo de memória utilizada pela task até o momento. O procedimento para realizar esse monitoramento se dá da seguinte forma: Inicialmete reserve uma quantidade que estime ser suficiente para aquela tarefa, um “chute” pra cima mesmo; Em seguida, faça com que o high water mark seja enviado de tempos em tempos para o monitor serial, vale ressaltar que o valor que será exibido diz respeito a quantidade da memória reservada para a task que NÃO foi utlizada por ela; Após um tempo de operação você poderá redimensionar a stack conforme a necessidade. É sempre recomendado reservar 10% a mais da memória que foi visualizada nos testes.

Em termos de código, o valor do high water mark deve ser armazenado numa variável do tipo UBaseType_t, como visto abaixo:

UBaseType_t HighWaterMark;

Dentro das tarefas, a leitura se dá da seguinte forma:

/* Obtém o High Water Mark da task atual.
   Lembre-se: tal informação é obtida em words! */
HighWaterMark = uxTaskGetStackHighWaterMark( NULL );
Serial.print("High water mark (words) da task atual: ");
Serial.println(HighWaterMark);

Praticando o uso de tasks

Como um exemplo inicial costruiremos duas tarefas simples que serão executadas em paralelo. As tarefas consistem em fazer dois led’s piscar em frequências distintas.

O circuito para esse projeto conta com apenas dois led’s conectados às portas digitais 12 e 13 do arduino, além de dois resistores limitadores de corrente de 220 ohms.

Circuito para o blink de led’s

O código fonte para esse projeto é demonstrado e comentado abaixo:

// Inclusão da biblioteca FreeRTOS

#include <Arduino_FreeRTOS.h>

// Defines

#define led01 13         // led 1 conectado no pino 13
#define led02 12         // led 2 conectado no pino 12
#define tempo_led01 1000 // 1 segundo
#define tempo_led02 500  // 0,5 segundo

// Declaração das tarefas

void TaskBlink01(void *pvParameters);
void TaskBlink02(void *pvParameters);

void setup() {

  pinMode(led01, OUTPUT); // Led 1 como saída
  pinMode(led02, OUTPUT); // Led 2 como saída 

// Criação das tarefas
  
  xTaskCreate(
   TaskBlink01
    , "Blink 01"   // Nome (para facilitar o debug)
    , 128          // Tamanho da stack reservada para essa função
    , NULL         // Parâmetros passados para a tarefa
    , 2            // Prioridade
    , NULL         // Handle da tarefa
    );

   xTaskCreate(
    TaskBlink02
    , "Blink 02"   // Nome (para facilitar o debug)
    , 128          // Tamanho da stack reservada para essa função
    , NULL         // Parâmetros passados para a tarefa
    , 1            // Prioridade
    , NULL         // Handle da tarefa
    );
}

void loop() {
 
  // Vazio, tudo é executado nas tarefas
  
}

// Tarefas:

void TaskBlink01(void *pvParameters){

  (void) pvParameters;
  
// Loop infinito                       
  while(1){
    digitalWrite(led01, HIGH);                    // Acende o led
    vTaskDelay(tempo_led01 / portTICK_PERIOD_MS); // Tempo aceso
    digitalWrite(led01, LOW);                     // Apaga o led
    vTaskDelay(tempo_led01 / portTICK_PERIOD_MS); // Tempo apagado
    } // Fim do loop infinito
  } 

  void TaskBlink02(void *pvParameters){

  (void) pvParameters;
  
// Loop infinito
  while(1){
    digitalWrite(led02, HIGH);                    // Acende o led
    vTaskDelay(tempo_led02 / portTICK_PERIOD_MS); // Tempo aceso
    digitalWrite(led02, LOW);                     // Apaga o led
    vTaskDelay(tempo_led02 / portTICK_PERIOD_MS); // Tempo apagado
    } // Fim do loop infinito
  }

Note que não devemos utilizar a função delay(), nativa da biblioteca do arduino, ja que ela deixaria o processador inativo para todas as tarefas. Ao invés dela utilizamos a função vTaskDelay(), ela informa que apenas a tarefa atual entrará em delay, liberando o processador para ser utilizado por outras tarefas. Como argumentos a função vTaskDelay() recebe o número de ticks do processador que a tarefa deverá ficar inativa, mas para facilitar podemos utilizar o tempo em milissegundos seguido da constante /portTICK_PERIOD_MS assim como foi utilizado no nosso código.

Criação de semáforos

O sistema de semáforos é um recurso disponibilizado pelo FreeRTOS para que haja um controle no acesso aos recursos do sistema (como a porta serial, por exmplo), evitando que duas tarefas tentem fazer o uso dele ao mesmo tempo, o que corromperia dados e comprometeria o funcionamento no sistema.

O FreeRTOS oferece outros tipos de semáforos, porém iremos nos concentrar no semáforo do tipo MUTEX (Mutual Exclusion – Exclusão Mútua). Um semáforo deste tipo tem por objetivo restringir o acesso à um recurso do sistema, permitindo que apenas uma tarefa faça uso de tal recurso até que ela “libere” ele.

Para fazer uso de um semáforo, inicialmente é preciso declarar de maneira global seu handle, da seguinte maneira:

SemaphoreHandle_t xSemaforo_teste;

Em seguida é necessário criar o semáforo em si que, no nosso caso, é do tipo MUTEX

xSemaforo_teste = xSemaphoreCreateMutex();

O ciclo de funcionamento do semáforo MUTEX se dá da seguinte forma:

  • Determinada tarefa tenta obter o controle do semáforo, através da função xSemaphoreTake.
  • Se conseguir, ou seja, se o recurso não estiver sendo utilizado por nenhuma outra tarefa, ela executa normalmente.
  • Finalizado o uso dos recursos que dependiam do semáforo, o semáforo é liberado, com o uso da função xSemaphoreGive.
  • Sendo assim, o recurso protegido pelo semáforo pode ser utilizado por outra tarefa.

Um ponto a se atentar é que as funções xSemaphoreTake e xSemaphoreGive possuem um parâmetro muito importante chamado xTicksToWait. Ele especifica quantos ticks do processador deve-se aguardar na tentativa de se obter ou liberar um semáforo. Este tempo pode ser infinito, se atribuído a este parâmetro a macro portMAX_DELAY. Veja no exemplo a seguir:

xSemaphoreTake(xSemaforo_teste, portMAX_DELAY );

Praticando o uso de semáforos

Para exemplificar o uso de semáforos podemos aprimorar o código visto anteriormente, adicionando o semáforo para realizar o controle de acesso ao monitor serial, que será utilizado para monitorar o High Water Mark das tarefas. Acompanhe no código fonte abaixo

// Inclusão das biblioteca FreeRTOS

#include <Arduino_FreeRTOS.h>
#include <task.h>
#include <semphr.h>

// Defines

#define led01 13         // led 1 conectado no pino 13
#define led02 12         // led 2 conectado no pino 12
#define tempo_led01 1000 // 1 segundo
#define tempo_led02 500  // 0,5 segundo

// Declaração das tarefas

void TaskBlink01(void *pvParameters);
void TaskBlink02(void *pvParameters);

// Declaração do handle do semáforo

SemaphoreHandle_t SemaforoSerial;

void setup() {

  pinMode(led01, OUTPUT); // Led 1 como saída
  pinMode(led02, OUTPUT); // Led 2 como saída 
  Serial.begin(9600); // Inicialiaza o serial

// Criação do semáforo

   SemaforoSerial = xSemaphoreCreateMutex();

// Configuração das tarefas
  
  xTaskCreate(
   TaskBlink01
    , "Blink 01"   // Nome (para facilitar o debug)
    , 128          // Tamanho da stack reservada para essa função
    , NULL         // Parâmetros passados para a tarefa
    , 2            // Prioridade
    , NULL         // Handle da tarefa
    );

   xTaskCreate(
    TaskBlink02
    , "Blink 02"   // Nome (para facilitar o debug)
    , 128          // Tamanho da stack reservada para essa função
    , NULL         // Parâmetros passados para a tarefa
    , 2            // Prioridade
    , NULL         // Handle da tarefa
    );
}

void loop() {
 
  // Vazio, tudo é executado nas tarefas
  
}

// Tarefas:

void TaskBlink01(void *pvParameters){

  (void) pvParameters;
  
  UBaseType_t HighWaterMark01;
  int contador01=0;
                         
  while(1){
    digitalWrite(led01, HIGH);                    // Acende o led
    vTaskDelay(tempo_led01 / portTICK_PERIOD_MS); // Tempo aceso
    digitalWrite(led01, LOW);                     // Apaga o led
    vTaskDelay(tempo_led01 / portTICK_PERIOD_MS); // Tempo apagado
    contador01++;
    
    // Imprime o High Water Mark a cada 6 segundos
   
    if(contador01 == 3){
      contador01=0;
      xSemaphoreTake(SemaforoSerial,portMAX_DELAY);//Solicita o semáforo
      HighWaterMark01 = uxTaskGetStackHighWaterMark(NULL);
      Serial.print("High Water Mark (em words) da Tarefa 1: ");
      Serial.println(HighWaterMark01);
      xSemaphoreGive(SemaforoSerial); // Libera o semáforo
      }
    } 
  }

  void TaskBlink02(void *pvParameters){

  (void) pvParameters;
  
  UBaseType_t HighWaterMark02;
  int contador02=0;
  
  while(1){
    
    digitalWrite(led02, LOW);                     // Apaga o led
    vTaskDelay(tempo_led02 / portTICK_PERIOD_MS); // Tempo aceso
    digitalWrite(led02, HIGH);                    // Acende o led
    vTaskDelay(tempo_led02 / portTICK_PERIOD_MS); // Tempo apagado
    contador02++;
    
    // Imprime o High Water Mark a cada 6 segundos
    
    if(contador02 == 6){
      contador02=0;
      xSemaphoreTake(SemaforoSerial,portMAX_DELAY);//Solicita o semáforo
      HighWaterMark02 = uxTaskGetStackHighWaterMark(NULL);
      Serial.print("High Water Mark (em words) da Tarefa 2: ");
      Serial.println(HighWaterMark02);
      xSemaphoreGive(SemaforoSerial); // Libera o semáforo
      }
    } 
  }

Após algum tempo de execução do código podemos observar a seguinte saída no monitor serial:

High Water Mark das tarefas

Conforme dito anteriormente, o número impresso diz respeito ao número de words que não foi utilizado pela tarefa. Como reservamos 128 words para ambas as tarefas, podemos facilmente deduzir que a tarefa 1 utiliza 63 words, já a tarefa 2 utiliza 82 words.

Criação de filas (Queues)

Filas (ou queues, em inglês) é o método utilizado pelo FreeRTOS para realizar a comunicação entre as tarefas para que nao haja o uso de variáveis globais como interface de comunicação. O método de escrita e leitura das filas obedece ao sistema FIFO (First Input First Output), ou seja, o primeiro dado a ser escrito é também o primeiro a ser lido.

Uma fila pode ser declarada da seguinte forma:

QueueHandle_t xQueue_Teste;

Para iniciar uma fila, é necessário fazer o seguinte:

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.

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

É importante ressaltar que, embora extremamente úteis e necessárias, as filas ocupam uma quantidade considerável de memória RAM, portanto use-as com cuidado.

Formas de escrever/ler dados de uma fila:

Existem diversas formas de adicionar elementos em uma fila, segue abaixo alguns deles:

  • 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 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 de um único item.
    Esta função não deve ser utilizada dentro do tratamento de uma interrupção.
  • xQueueOverwriteFromISR: análogo ao anterior, porém deve ser usada somente dentro de um tratamento de interrupção.

Exemplo de escrita de dados em uma fila:

xQueueOverwrite(HANDLE_DA_FILA, &DADO_A_SER_ESCRITO);

Já para ler/remover arquivos de uma fila, algumas das funções disponíveis são:

  • 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).

Código exemplo para leitura em filas:

xQueueReceive(HANDLE_DA_FILA, &VARIAVEL_DESTINO, TEMPO_MAXIMO);

Praticando o uso de filas

Para ilustrar o conceito e o funcionamento das filas, podemos elaborar um projeto que realiza a leitura de um teclado matricial e utiliza uma fila para enviar o caracter correspondente a tecla que foi pressionada para ser impresso no monitor serial, além disso, para reforçar o conceito de multitarefa, teremos novamente a tarefa responsável pelo blink do led, já vista nos exemplos anteriores. Para tal projeto será necessário a montagem do seguinte circuito:

Circuito para leitura do teclado matricial

O software a ser embarcado no arduino é exibido abaixo:

#include <Arduino_FreeRTOS.h>
#include <task.h>
#include <queue.h>

// Mapeamento de hardware

#define coluna1 6
#define coluna2 7
#define coluna3 8
#define coluna4 9
#define linha1 2
#define linha2 3
#define linha3 4
#define linha4 5

#define led 13
#define tempo_led 1000  

// Declaração das tarefas

void TaskTeclado(void *pvParameters);
void TaskBlink(void *pvParameters);
void TaskImprime(void *pvParameters);

// Declaração do handle da tarefa

TaskHandle_t HandleTeclado;

// Declaração da fila (queue)

QueueHandle_t QueueImprime; 

void setup() {

  pinMode(led, OUTPUT); // Led como saída digital

  Serial.begin(9600); // Inicializa o monitor serial
  
// Linhas como entradas (Utilizando resistores de Pull-Up interno)
  
  pinMode(linha1, INPUT_PULLUP);
  pinMode(linha2, INPUT_PULLUP);
  pinMode(linha3, INPUT_PULLUP);
  pinMode(linha4, INPUT_PULLUP);
    

// Colunas como saídas

  pinMode(coluna1, OUTPUT);
  pinMode(coluna2, OUTPUT);
  pinMode(coluna3, OUTPUT);
  pinMode(coluna4, OUTPUT);

// Configuração das tarefas

 xTaskCreate(
    TaskTeclado
    , "Varredura do teclado" // Nome
    , 128                    // Tamanho da Stack
    , NULL                   // Parâmetros passados
    , 1                      // Prioridade
    , &HandleTeclado         // Handle da tarefa
    );

  xTaskCreate(
   TaskBlink
    , "Blink led"  // Nome (para facilitar o debug)
    , 128          // Tamanho da stack reservada para essa função
    , NULL         // Parâmetros passados para a tarefa
    , 2            // Prioridade
    , NULL         // Handle da tarefa
    );

  xTaskCreate(
    TaskImprime
    , "Imprime"   // Nome
    , 128         // Stack
    , NULL        // Parâmetros
    , 3           // Prioridade
    , NULL        // Handle
    );

  // Criação da fila de 1 elemento
  // do tamanho de uma variavel tipo char
  
  QueueImprime = xQueueCreate(1, sizeof(char));
    
}

void loop() {
  
  // put your main code here, to run repeatedly:

}

// Tarefas

void TaskTeclado(void *pvParameters){

    (void)pvParameters;
    
    int coluna;
    char tecla;
    
    while(1){

    // Varredura do teclado matricial:
    // alterna entre as colunas e testa as linhas para
    // saber qual tecla foi pressionada
    
      for(coluna = coluna1; coluna <= coluna4; coluna++){
        digitalWrite(coluna1, HIGH);
        digitalWrite(coluna2, HIGH);
        digitalWrite(coluna3, HIGH);
        digitalWrite(coluna4, HIGH);
        digitalWrite(coluna, LOW);

        if(!digitalRead(linha1)){
          if(coluna == coluna1) tecla = '1';
          else if(coluna == coluna2) tecla = '2';
          else if(coluna == coluna3) tecla = '3';
          else if(coluna == coluna4) tecla = 'A';
          xQueueOverwrite(QueueImprime, &tecla);  // Escreve na fila
          vTaskSuspend(NULL);                     // Suspende a tarefa 
          while(!digitalRead(linha1));// Mantém a tarefa nesse ponto até
          }                           // que a tecla seja solta
          
        if(!digitalRead(linha2)){
          if(coluna == coluna1) tecla = '4';
          else if(coluna == coluna2 ) tecla = '5';
          else if (coluna == coluna3) tecla = '6';
          else if(coluna == coluna4) tecla = 'B';
          xQueueOverwrite(QueueImprime, &tecla);  // Escreve na fila
          vTaskSuspend(NULL);                     // Suspende a tarefa 
          while(!digitalRead(linha2));// Mantém a tarefa nesse ponto até
          }                           // que a tecla seja solta
          
        if(!digitalRead(linha3)){
          if(coluna == coluna1) tecla = '7';
          else if(coluna == coluna2) tecla = '8';
          else if(coluna == coluna3) tecla = '9';
          else if(coluna == coluna4) tecla = 'C';
          xQueueOverwrite(QueueImprime, &tecla);  // Escreve na fila
          vTaskSuspend(NULL);                     // Suspende a tarefa 
          while(!digitalRead(linha3));// Mantém a tarefa nesse ponto até
          }                           // que a tecla seja solta
          
        if(!digitalRead(linha4)){
          if(coluna == coluna1) tecla = '*';
          else if(coluna == coluna2) tecla = '0';
          else if(coluna == coluna3) tecla = '#';
          else if(coluna == coluna4) tecla = 'D';
          xQueueOverwrite(QueueImprime, &tecla);  // Escreve na fila
          vTaskSuspend(NULL);                     // Suspende a tarefa 
          while(!digitalRead(linha4));// Mantém a tarefa nesse ponto até
          }                           // que a tecla seja solta
        }
      }
    }

void TaskBlink(void *pvParameters){

  (void)pvParameters;

  while(1){
    digitalWrite(led, HIGH); // Acende o led
    vTaskDelay(tempo_led / portTICK_PERIOD_MS); // Tempo aceso
    digitalWrite(led, LOW); // Apaga o led
    vTaskDelay(tempo_led / portTICK_PERIOD_MS); // Tempo apagado
    } 
  }

void TaskImprime(void *pvParameters){

  (void)pvParameters;
  char teclaRecebida;
 
  while(1){
    
    // Aguarda até que haja algo na fila
    // quando houver, armaneza na variável teclaRecebida
    
    xQueueReceive(QueueImprime, &teclaRecebida, portMAX_DELAY);
    vTaskDelay(200/portTICK_PERIOD_MS); // delay anti bouncing
    Serial.print(teclaRecebida); // Imprime a tecla pressionada
    vTaskResume(HandleTeclado);  // Retoma a leitura do teclado
    }
  }

Podemos comprovar o funcionamento da nossa fila ao se observar o monitor serial após um pequeno teste:

Impressão dos dados do teclado

Conclusão

Com este artigo conseguimos, através de exemplos simples, dar uma base dos principais conceitos do FreeRTOS, bem como o uso de tarefas, filas e semáforos. Através disso torna-se possível o desenvolvimento de projetos cada vez mais complexos e desafiadores, para isso também é altamente recomendado o acesso ao site oficial do FreeRTOS (www.freertos.org). Bons estudos!

Referências

Seja o primeiro a comentar

Faça um comentário

Seu e-mail não será publicado.


*