Projetos

FreeRTOS – Task, Trabalhando com Multitarefas

Eletrogate 16 de fevereiro de 2022

Introdução

Olá, caro leitor, tudo bem? Esse é primeiro de uma série de artigos sobre o FreeRTOS. Aqui, no Blog da Eletrogate, temos artigos explicando o que são RTOS e detalhando o processo de instalação na IDE do Arduino. Portanto, a série será focada em apresentar os recursos do FreeRTOS, tais como Task, Semaphore/Mutexes, Software Timers, Event Groups (Flag’s) e Queues. O objetivo de cada artigo é apresentar um recurso do FreeRTOS, explicar o conceito e, por fim, demonstrar a utilização do mesmo em projeto.
As ferramentas utilizadas nos projetos de demonstração serão o ESP32, VSCode e PlatformIO. Para mais detalhes, sugiro consultar o artigo “Como programar o Arduino com VS Code e PlatformIO”, escrito pelo Ricardo Lousada.


FreeRTOS

O FreeRTOS, tecnicamente, é um Kernel de Sistema Operacional de Tempo Real. Diferente de RTOS completo, que pode prover recursos para o gerenciar o uso da CPU, driver para acesso aos periféricos, driver de dispositivos, pilhas de protocolo e outros, o FreeRTOS gerencia apenas a CPU. Basicamente, ele fornece instrumentos para o gerenciamento de processo, mecanismos para comunicação e sincronismo entre estes e recursos de software timer.
O projeto teve início por volta de 2003, por Richard Barry. Em 2017, o projeto foi adquirido pela empresa Amazon Web Services (AWS). O FreeRTOS é distribuído sob a licença MIT.


Escalonador

Antes de começar a explicar sobre a utilização de “Tarefa” com o FreeRTOS, é importante falarmos um pouco do escalonador e kernel. Escalonador é o algoritmo do sistema operacional responsável pelo gerenciamento dos processos a serem executados pela CPU. Basicamente, ele é responsável por selecionar qual será a próxima tarefa a ser executada.


Kernel

O kernel é núcleo do sistema operacional, ele é responsável por gerenciar a execução dos processos, seguindo determinado conjunto de regras.
O kernel do FreeRTOS permite trabalhar no modo cooperativo (não-preemptivo) ou preemptivo. No modo cooperativo, cada tarefa é executava do início ao fim, isso significa que a próxima tarefa a ser executada pela a CPU tem que aguardar o término do processo em execução, mesmo que a tarefa que está aguardando seja de maior prioridade. Por padrão é o preemptivo que vem habilitado, assim, o kernel possui a capacidade de interromper a execução de tarefa, salvar os seus parâmetros e dar início à execução de uma tarefa de maior prioridade. Ao término da execução da tarefa de maior prioridade, o kernel retorna com a tarefa que havia sido interrompida. Esse recurso que permite pausar a execução de processo para iniciar um novo é chamado de troca de contexto.


Tarefa

Uma tarefa dentro de sistema operacional pode ser entendida como um pequeno algoritmo independente das outras tarefas do sistema, ficando a cargo do escalonador gerenciar o processamento de cada tarefa. O FreeRTOS permite classificar as tarefas por prioridade de execução.

Protótipo da função de uma tarefa:

void vTaskCode(void *pvParameters);

Estrutura da função de uma tarefa:

void vTaskCode(void *pvParameters)
{
    for(;;)
    {
        /* task code */
    }
}

Criação de Tarefa

Para criar tarefas a serem executadas pelo o RTOS, temos que, basicamente, agendar a execução de uma determinada função no escalonador. Para essa etapa de criação de tarefas no FreeRTOS, a função padrão é “xTaskCreate”. Para o ESP32, Dual Core, temos a função “xTaskCreatePinnedToCore” que permite selecionar em qual CPU será executada a tarefa.

Protótipo da função “xTaskCreate”:

BaseType_t xTaskCreate(TaskFunction_t pvTaskCode,
                       const char * const pcName,
                       const uint32_t usStackDepth,
                       void * const pvParameters,
                       UBaseType_t uxPriority,
                       TaskHandle_t * const pvCreatedTask)

Retorno da função:

  • A função retorna o parâmetro “pdPASS” se a tarefa foi criada com sucesso.

Parâmetros da função:

  • pvTaskCode: Ponteiro para a função da tarefa a ser criada. As tarefas devem ser implementadas para nunca retornar (ou seja, loop infinito).
  • pcName: Uma string com o nome da tarefa. Esse item é usado principalmente para facilitar a depuração. O seu comprimento padrão é de 16 caracteres, que pode ser configurado através do parâmetro “configMAX_TASK_NAME_LEN”.
  • usStackDepth: O tamanho da pilha de tarefas especificado com o número de palavras (words). Para mais informações, o link a seguir contém informações detalhadas de Qual deve ser o tamanho da pilha?.
  • pvParameters: Ponteiro que servirá de parâmetro para a tarefa que está sendo criada.
  • uxPriority: A prioridade na qual a tarefa criada será executada. Para mais informações, consulte o link.
  • pvCreatedTask: Usado para passar um identificador para a tarefa criada. Esse item é opcional e pode ser definido como NULL.

Nota: Para o ESP32, quando criada uma tarefa utilizando a função “xTaskCreate”, fica a cargo do escalonador selecionar em qual núcleo será executada, dependendo da disponibilidade das CPUs.

Protótipo da função “xTaskCreatePinnedToCore”:

BaseType_t xTaskCreatePinnedToCore(TaskFunction_t pvTaskCode,
                                   const char * const pcName,
                                   const uint32_t usStackDepth,
                                   void * const pvParameters,
                                   UBaseType_t uxPriority,
                                   TaskHandle_t * const pvCreatedTask,
                                   const BaseType_t xCoreID);

Essa função é semelhante a “xTaskCreate” com a diferença que permite a escolha do núcleo que será executada a tarefa. Essa configuração é feita por meio do parâmetro “xCoreID”.

  • xCoreID: Se o valor for “tskNO_AFFINITY”, a tarefa criada não está fixada em nenhuma CPU e o planejador pode executá-la em qualquer núcleo disponível. Os valores 0 (“PRO_CPU_NUM” – CPU primária) ou 1 (“APP_CPU_NUM” – CPU secundária) indicam o número de índice da CPU à qual a tarefa deve ser fixada.

Controle de Tarefa

Aplicações com o FreeRTOS podem conter diversas tarefas criadas. A grande maioria dos microcontroladores possui apenas uma CPU, portanto, apenas uma tarefa por vez pode ser executada. Então, podemos concluir que uma tarefa poder assumir apenas dois estados; executando e não-executando, sendo que não-executando é dividida em sub-estados, que são eles:

  • Pronta: Uma tarefa no estado pronta está aguardando ser selecionada pelo escalonador para ser executada.
  • Bloqueada / Pausada / Suspensa: A tarefa está aguardando um determinado evento ou que outra tarefa altere o seu estado para Pronta.

Para manipular as tarefas, o FreeRTOS possui algumas funções. A seguir, serão apresentadas algumas delas. Para encontrar mais funções para controlar as tarefas, sugiro consultar o link.

vTaskDelay
Pausa a execução da tarefa por um determinado tempo. O tempo real que a tarefa permanece bloqueada depende da taxa de tique. A constante “portTICK_PERIOD_MS” pode ser usada para calcular o tempo real.

Protótipo da função vTaskDelay:

void vTaskDelay( const TickType_t xTicksToDelay )

Parâmetro da função:

  • xTicksToDelay: A quantidade de tempo, em períodos de escala, que a tarefa deve permanecer bloqueada.

xTaskAbortDelay
Essa função faz com que uma determinada tarefa saia do estado Bloqueada e passe para o estado Pronta, mesmo se o evento pelo qual a tarefa estava no estado Bloqueado a aguardar não tenha ocorrido e qualquer tempo limite especificado não tenha expirado.

Protótipo da função:

BaseType_t xTaskAbortDelay (TaskHandle_t xTask);

Retorno da função:

  • Se a tarefa solicitada não estava no estado bloqueado, o parâmetro “pdFAIL” é retornado. Caso contrário, o parâmetro “pdPASS” é retornado.

Parâmetro da função:

  • xTask: O identificador da tarefa que será forçada a sair do estado Bloqueado.

vTaskSuspend
Suspende a execução da tarefa. A tarefa permanecerá no estado Bloqueado enquanto outra tarefa não a tirar desse estado. As chamadas para “vTaskSuspend” não são cumulativas.

Protótipo da função:

void vTaskSuspend (TaskHandle_t xTaskToSuspend);

Parâmetro da função:

  • xTaskToSuspend: O identificador da tarefa que será suspensa. Passar um identificador NULL fará com que a tarefa de chamada seja suspensa.

vTaskResume
Uma tarefa que foi suspensa será disponibilizada para execução novamente, o seu estado passará de Bloqueado para Pronto.

Protótipo da função:

void vTaskResume (TaskHandle_t xTaskToResume);

Parâmetro da função:

  • xTaskToResume: O identificador da tarefa que deve sair do estado Suspenso.

vTaskDelete
Remova uma tarefa do gerenciamento de kernels RTOS. A tarefa que está sendo excluída será removida de todas as listas de prontas, bloqueadas, suspensas e de eventos.

Protótipo da função:

void vTaskDelete (TaskHandle_t xTask);

Parâmetro da função:

  • xTask: O identificador da tarefa a ser deletada. Passar NULL fará com que a tarefa de chamada seja excluída.

Projeto de Demonstração

Para finalizar, será apresentado um projeto de demonstração utilizando o ESP32 e as funções de criação e controle de tarefas do FreeRTOS. O projeto desenvolvido é bem simples, tendo como objetivo exemplificar cada um dos recursos apresentados. A aplicação conta com três tarefas (Tarefa_A, Tarefa_B e Tarefa_C) que imprime na serial seu nome e número da CPU que está executando.

Código fonte do projeto

/**
 * @file main.cpp
 * @author Evandro Teixeira
 * @brief
 * @version 0.1
 * @date 06-01-2022
 *
 * @copyright Copyright (c) 2022
 *
 */
#include <Arduino.h>
#include <freertos/queue.h>
#include <freertos/task.h>

// Macro com as cores 
#define COLOR_BLACK     "\e[0;30m"
#define COLOR_RED       "\e[0;31m"
#define COLOR_GREEN     "\e[0;32m"
#define COLOR_YELLOW    "\e[0;33m"
#define COLOR_BLUE      "\e[0;34m"
#define COLOR_PURPLE    "\e[0;35m"
#define COLOR_CYAN      "\e[0;36m"
#define COLOR_WRITE     "\e[0;37m"
#define COLOR_RESET     "\e[0m"
#define PRIMARY_CORE    PRO_CPU_NUM
#define SECONDARY_CORE  APP_CPU_NUM
#define TASK_DELAY      1000

enum                          // Estado da tarefa A
{
  task_unsuspended = false,   //tarefa não suspensa 
  task_suspended = true       //tarefa suspensa
};

// Prototipo das funções
void Tarefa_A(void *parameters);
void Tarefa_B(void *parameters);
void Tarefa_C(void *parameters);
void status_tarefa_a_set(bool sts);
bool status_tarefa_a_get(void);

// Variaveis globais
TaskHandle_t Handle_Tarefa_A = NULL;
TaskHandle_t Handle_Tarefa_B = NULL;
const uint32_t TaskDelay = TASK_DELAY;
static bool status_tarefa_a;

void setup()
{
  // Inicializa a Serial 
  Serial.begin(115200);
  Serial.printf("\n\rFreeRTOS - Tarefas\n\r");

  // Set estado inicial da variavel status_tarefa_a como tarefa não suspensa 
  status_tarefa_a_set(task_unsuspended);

  // Cria as tarefa do projeto
  xTaskCreatePinnedToCore(Tarefa_A, "Tarefa_A", configMINIMAL_STACK_SIZE, NULL, tskIDLE_PRIORITY + 1, &Handle_Tarefa_A, PRIMARY_CORE);
  //xTaskCreatePinnedToCore(Tarefa_B, "Tarefa_B", configMINIMAL_STACK_SIZE, NULL, tskIDLE_PRIORITY + 2, &Handle_Tarefa_B, tskNO_AFFINITY);
  xTaskCreate(Tarefa_B, "Tarefa_B", configMINIMAL_STACK_SIZE, NULL, tskIDLE_PRIORITY + 2, &Handle_Tarefa_B);
  xTaskCreatePinnedToCore(Tarefa_C, "Tarefa_C", configMINIMAL_STACK_SIZE, (void*)TaskDelay, tskIDLE_PRIORITY + 3, NULL, SECONDARY_CORE);
}

void loop()
{
  Serial.printf("LOOP\n\r");
  vTaskSuspend(NULL);
}

/**
 * @brief 
 * 
 * @param sts 
 */
void status_tarefa_a_set(bool sts)
{
  status_tarefa_a = sts;
}

/**
 * @brief 
 * 
 * @return true 
 * @return false 
 */
bool status_tarefa_a_get(void)
{
  return status_tarefa_a; 
}

/**
 * @brief 
 * 
 * @param parameters 
 */
void Tarefa_A(void *parameters)
{
  static uint8_t counter_to_suspend = 0;          // Contador de suspender a tarefa A

  while (1)
  {
    Serial.print(COLOR_RED);                      // altera para vermelho impressão da mensagem na serial
    Serial.print( pcTaskGetTaskName(NULL) );
    Serial.print(" Core: ");
    Serial.println( xPortGetCoreID() );           // busca o identificador do CPU que esta executando a tarefa 
    Serial.print(COLOR_RESET);                    // reset a cor da impressão da mensagem na serial 

    if(counter_to_suspend < 4)
    {
      counter_to_suspend++;                       // incrementa contador 
      vTaskDelay(TASK_DELAY/portTICK_PERIOD_MS);  // Pausa a execução da tarefa por 1000 milessegundos 
    }
    else 
    {
      counter_to_suspend = 0;                     // reset contador 
      status_tarefa_a_set(task_suspended);        // Set estado inicial da variavel status_tarefa_a como tarefa suspensa 
      vTaskSuspend(Handle_Tarefa_A);              // Suspende a execução da tarefa 
    }
  }
}

/**
 * @brief 
 * 
 * @param parameters 
 */
void Tarefa_B(void *parameters)
{
  static uint8_t counter_to_activate_task = 0;    // contador para tirar da suspensão a tarefa A

  while (1)
  {
    Serial.print(COLOR_GREEN);                    // altera para verde impressão da mensagem na serial
    Serial.print( pcTaskGetTaskName(NULL) );
    Serial.print(" Core: ");
    Serial.println(xPortGetCoreID());             // busca o identificador do CPU que esta executando a tarefa 
    Serial.print(COLOR_RESET);                    // reset a cor da impressão da mensagem na serial 
    
    if(status_tarefa_a_get() == task_suspended)   // Checa se a tarefa A esta suspensa
    {
      if(counter_to_activate_task < 2)
      {
        counter_to_activate_task++;               // Incrementa contador
      }
      else 
      {
        counter_to_activate_task = 0;             // Reset contador
        status_tarefa_a_set(task_unsuspended);    // Set estado inicial da variavel status_tarefa_a como tarefa não suspensa 
        vTaskResume(Handle_Tarefa_A);             // Reativa a tarefa A
      }
    }
    vTaskDelay(TASK_DELAY/portTICK_PERIOD_MS);    // Pausa a execução da tarefa por 1000 milessegundos 
  }
}

/**
 * @brief 
 * 
 * @param parameters 
 */
void Tarefa_C(void *parameters)
{
  const TickType_t Delay = (TickType_t)parameters;  // recebe o parametro adicionado na criação da tarefa
  static uint8_t counter_to_delete_task = 0;        // contador para deletar a tarefa 

  while (1)
  {
    Serial.print(COLOR_YELLOW);                     // altera para verde impressão da mensagem na serial
    Serial.print( pcTaskGetTaskName(NULL) );
    Serial.print(" Core: ");
    Serial.println(xPortGetCoreID());               // busca o identificador do CPU que esta executando a tarefa 
    Serial.print(COLOR_RESET);                      // reset a cor da impressão da mensagem na serial 

    if(counter_to_delete_task < 4)
    {
      counter_to_delete_task++;                     // Incrementa contador
      vTaskDelay(Delay/portTICK_PERIOD_MS);         // Pausa a execução da tarefa por 1000 milessegundos 
    }
    else 
    {
      vTaskDelete(NULL);                            // Deleta tarefa da execução 
    }
  }
}

Resultado da Aplicação

A seguir temos a imagem com as mensagem de cada tarefa impressas no terminal serial.

LOG das mensagem

LOG das mensagens no barramento serial

As mensagem na figura foram enumeradas para detalhar o comportamento do algoritmo implementado no projeto de demonstração:

  1. Após a inicialização da comunicação a serial, é transmitida a mensagem “FreeRTOS – Tarefas”.
  2. Algo estranho aconteceu com as mensagens das tarefas A e B, notem que as mensagem ficam na mesma linha e permanecem com a cor vermelha. Isso ocorre pois temos duas tarefas acessando o mesmo recurso, neste caso o barramento da comunicação. Solucionar esse tipo de falha é simples, basta utilizar o recurso de semáforo. Porém, pretendo explicar tal recurso nos próximos artigos.
  3. Observe que a mensagem “LOOP” é impressa uma única vez. Isso ocorre por que a função loop se comporta como uma tarefa agendada no escalonador do sistema operacional. Portanto, podemos utilizar os recursos oferecidos pelo FreeRTOS. Neste caso, foi utilizada a função que suspende a execução de tarefa, “vTaskSuspend ”.
  4. O algoritmo elaborado para a tarefa C pública no barramento serial cinco vezes a sua mensagem. Em seguida, é destruída através da função “vTaskDelete”, então deixa de ser executada.
  5. A tarefa A transmite a sua mensagem por quatro vezes e em seguida muda o estado da variável global “status_tarefa_a” para igual “true” (task_suspended) e suspende a sua execução por meio da função “vTaskSuspend”.
  6. No algoritmo da tarefa B, além de publicar a sua mensagem no barramento Serial, é checado o valor da variável global que sinaliza o status da tarefa A. Caso a tarefa A esteja bloqueada, a tarefa B aguarda três ciclos para desbloquear.

Nota: O processo de troca de informação entre tarefas pode ser feita utilizando recursos do FreeRTOS. Utilizei variável global em conjunto com as funções de “get” e “set” simplesmente para facilitar o entendimento. Nos próximos artigos, serão detalhados os mecanismos de troca de informação entre tarefas.


Conclusão

Neste artigo inicial sobre os recursos oferecidos pelo o FreeRTOS, foram apresentadas as principais funções para manusear as tarefas no sistema operacional. Além disso, foi apresentado um projeto de demonstração, onde podemos observar a implementação destas funções. Ao final, analisamos o LOG das mensagens no barramento serial e pontuamos o comportamento de cada tarefa.
Para os próximos artigos, serão apresentados mais recursos do FreeRTOS. O que você achou? Você já utiliza o FreeRTOS em seus projetos? Deixe o seu comentário abaixo.


Sobre o autor


Evandro Teixeira

Desenvolvedor de sistemas embarcados com mais de 10 anos de experiência, com atuação em diferentes segmentos de mercado tais como eletromédicos, automação industrial, ITS (Sistemas Inteligentes de Transporte) e automotivo.


Eletrogate

16 de fevereiro de 2022

A Eletrogate é uma loja virtual de componentes eletrônicos do Brasil e possui diversos produtos relacionados à Arduino, Automação, Robótica e Eletrônica em geral.

Conheça a Metodologia Eletrogate e Lecione um Curso de Robótica nas Escolas da sua Região!

Eletrogate Robô

Cadastre-se e fique por
dentro de novidades!