Projetos no Arduino com FreeRTOS – conceitos, tasks e consumo de memória por task 3

O Arduino se tornou a plataforma de aprendizagem, prototipação e desenvolvimento mais popular. Pessoas das mais diversas áreas, seja ligada à tecnologia ou não, tiveram contato com a plataforma e conseguiram fazer projetos, confirmando o Arduino como a plataforma de desenvolvimento de sistemas embarcados “mais democrática” atualmente.Logo, faz sentido utilizar o Arduino como plataforma-base para ensinar novos conceitos. E é justamente isso que este artigo fará: utilizando o Arduino, será ensinado o que é o FreeRTOS, o que são tasks (tarefas) no FreeRTOS e como medir o consumo de memória por parte delas. Isso será feito através de um projeto que controlará um display LCD 16×2 I²C e um LED utilizando Arduino e FreeRTOS.

Material necessário

Para acompanhar este post, você precisará de:

O que é o FreeRTOS?

O FreeRTOS é um dos RTOS (Sistema Operacional de Tempo Real) mais utilizados no mundo, devido tanto a questões de licença de software (ele pode ser aplicado em sistemas comerciais sem custos de licença) quanto ao grande número de microcontroladores e plataformas aos quais ele pode ser portado.

O FreeRTOS foi desenvolvido (e é constantemente melhorado) em parceria com as maiores fabricantes de chips, desenvolvimento esse que dura mais de 15 anos, seguindo normas rígidas de qualidade de software (ele segue quase todo o padrão MISRA). A popularidade é tanta que, em 2017, a Amazon adquiriu o FreeRTOS e lançou nele customizações interessantes para uso de seus serviços direcionados a IoT (leia mais disso aqui e aqui). Então sim, o FreeRTOS é “propriedade” da Amazon, e isso dá uma ideia da proporção desse projeto.

 

 

Caso deseje saber mais sobre o FreeRTOS, acesse o site oficial (www.freertos.org) e veja este artigo do blog.

Mas porque usar FreeRTOS?

Se você nunca trabalhou com um sistema operacional como o FreeRTOS antes, possivelmente essa pergunta surgiu na sua cabeça. A resposta para isso é composta de uma série de fatores  / vantagens do uso do FreeRTOS em seu projeto de sistemas embarcados:

  1. Confiabilidade: toda a parte de gerenciamento de memória, execução de tarefas, priorização de tarefas (e muito mais) são coisas  cuidadas pelo sistema operacional. Tais coisas, se feitas “na mão”, além de ser algo como “reinventar a roda”, demandariam muito tempo para implementação, testes e debug. E mesmo com muita dedicação, provavelmente o resultado ainda seria inferior ao que já é feito no FreeRTOS.
  2. Portabilidade: usando FreeRTOS para desenvolvimento, em teoria, qualquer outro projeto que usa FreeRTOS poderia ser portado para seu hardware / plataforma. Dessa forma, poupa-se tempo na hora de portar soluções, ainda com a garantia de desempenho e segurança do FreeRTOS.
  3. Segurança: o FreeRTOS é mantido por grandes empresas e pela comunidade, além de ser amplamente testado e usado em projetos comerciais e não-comerciais mundo afora. Sendo assim, usando FreeRTOS você tem a certeza de usar algo testado e, muito provavelmente, funcionando 100% para o que deseja.

Em suma, usar FreeRTOS facilita sua vida enquanto projetista de sistemas embarcados, uma vez que resolve uma série de problemas e permite um desempenho muito bom (pois várias tarefas são executadas “em paralelo”).

Imagem 1 - Task no FreeRTOS

Na verdade, em paralelo não é bem o termo correto, já que o FreeRTOS alterna entre a execução das tarefas milhares de vezes por segundo. Essa alternância / chaveamento entre tarefas é tão rápida que para nós, usuários de um sistema embarcado, é como se tudo rodasse em paralelo.

Primeiro conceito: como arquitetar uma solução usando um RTOS

Primeiramente, é preciso ter claro na sua cabeça o seguinte: uma solução que usa FreeRTOS é composta do sistema operacional de tempo real (o FreeRTOS) E as tarefas que você programar /  pedir para o sistema operacional executar. Ou seja, apesar de no fim a solução ser um único código e/ou binário compilado, o software é arquitetado seguindo essa divisão.

Com isso em mente, todas as funcionalidades de um projeto que usa um FreeRTOS são as tarefas que você irá programar. E, quase em 100% das vezes, uma funcionalidade equivale a uma task (tarefa) distinta.

Exemplo: imagine que você tenha que fazer o software embarcado de um rastreador simples (que obtém a posição via GPS, envia tal informação por rede e precisa piscar um LED indicando que está em operação, tudo utilizando um RTOS). Aqui, obter posições do GPS é uma tarefa / funcionalidade e o envio da mesma por rede e piscar o LED são outras duas tarefas distintas. Mesmo que, em algum momento, a tarefa de posicionamento tenha que se comunicar com a de envio por rede, são funcionalidades distintas e, portanto, são tarefas distintas. Então, cada funcionalidade desse sistema embarcado pode ser encarado como uma tarefa ou task isolada, havendo ou não comunicação entre as elas. Pensar nesse tipo de divisão dos problemas em tarefas é o primeiro conceito de um RTOS.

Segundo conceito: uso de recursos compartilhados de forma segura

Uma vez que as funcionalidades de um projeto / produto são divididas em tarefas distintas (ou isoladas), uma situação exige cuidado: compartilhamento de recursos. Isso vem à tona quando se é pensado que pode haver a chance, remota ou não, de um recurso (como uma interface de comunicação ou um GPIO, por exemplo) ser requerido por uma tarefa enquanto outra tarefa já está fazendo uso do mesmo.

As consequências de não se proteger quanto a isso podem ser muito desastrosas. No caso de uma porta serial /  UART  (Universal Asynchrounous Receiver/Transmiter), pode-se ter um buffer de envio ou recepção corrompido (duas ou mais tasks alterando tal buffer), algo que pode não ser destruidor. Mas já imaginou se o recurso concorrido é um GPIO, onde esse GPIO controla algo forte e grande que, se manipulado incorretamente, pode causar danos (ou até mesmo morte) às pessoas? Pode ser trágico.

Para evitar que tarefas distintas tentem utilizar um recurso ao mesmo tempo, os RTOS  (incluindo o FreeRTOS) possuem um recurso chamado semáforo. O semáforo não tem esse nome a toa. Ele se assemelha realmente ao semáforo de trânsito da vida real. No caso, as ruas / vias são os recursos compartilhados por carros de um cruzamento. Se todos os carros tentarem acessar a via ao mesmo tempo, certamente acontecerão acidentes. Logo, o semáforo libera o tráfego em uma via de cada vez, de modo a evitar acidentes. Nessa analogia, as ruas são os recursos visados pelas tarefas, e as tarefas são os carros.

Portanto, um semáforo é uma ferramenta (normalmente provida pelo próprio sistema operacional) que sinaliza se determinado recurso está sendo utilizado naquele momento por alguma tarefa ou não. Dessa forma, a verificação do semáforo (e a ação ou não-ação dependendo de seu estado) é suficientemente seguro para evitar problemas ao se compartilhar recursos entre tarefas distintas. O fato de o semáforo ser um recurso provido pelo sistema operacional fornece muito mais segurança ao projeto. Isso porque a chance de algo estar errado na implementação deste é muito menor do que se fosse implementada do zero novamente pelo projetista (isso considerando que o RTOS em questão tenha sido suficientemente testado, que é o caso do FreeRTOS).

Iremos ver mais detalhes sobre semáforos no FreeRTOS em outros artigos deste mesmo tema.

Terceiro conceito: comunicação entre tasks distintas no FreeRTOS

Até aqui, vimos que cada funcionalidade do RTOS deve ser isolada / encapsulada numa task. Porém, e se as tasks precisarem se comunicar?

Para isso, o FreeRTOS oferece mecanismos de comunicação inter-processos, sendo os mais comuns as filas (queues). Filas (queues) são capazes de trafegar dados entre tarefas, e isso acontece sempre numa via única. Por exemplo, para o tráfego entre a Task1 -> Task2 teremos uma fila, já para o envio de informação do tipo Task2 -> Task1 uma nova fila deve ser usada. Isso ocorre com a garantia do RTOS de que a informação está segura, íntegra (não corrompida) e disponível a qualquer tarefa que possuir o acesso (handler) da fila em questão.

Então lembre-se: quando projetar uma solução usando FreeRTOS, não utilize variáveis globais como “interface de comunicação” entre processos. Isso é estar totalmente sujeito a erros de escrita e leitura não íntegras e as filas resolvem esse problema definitivamente.

Iremos ver as filas disponíveis no FreeRTOS em detalhes em outros artigos deste mesmo tema.

Quarto conceito: prioridades das tasks no FreeRTOS

Quando se entra num nível de gerenciamento de tarefas distintas, sobretudo quando as tarefas são várias, priorizar a execução de tarefas é algo não só importante, mas fundamental.

Imagem 2 - Task no FreeRTOS

As tasks no FreeRTOS possuem prioridade, de modo que numa situação onde duas tarefas podem ser executadas, o FreeRTOS consiga escolher qual delas será executada primeiro. Isso significa que, num cenário onde há duas tarefas, A e B:

  • Se a prioridade da tarefa A for maior que prioridade da tarefa B: a execução da tarefa A será priorizada em relação à tarefa B;
  • Se a prioridade da tarefa A for menor que prioridade da tarefa B: a execução da tarefa B será priorizada em relação à tarefa A;
  • Se a prioridade da tarefa A for igual a prioridade da tarefa B: ambas as tarefas executarão em round-robin, ou seja, ambas vão “executar picado”, por um tempo definido por vez (tempo esse chamado de quantum na literatura sobre sistemas operacionais de tempo real).

Conforme visto acima, a prioridade das tarefas é algo de enorme importância para um projeto que use o FreeRTOS. Isso é ainda mais importante quando se trata de tarefas que dependem de inputs (como dados numa fila) de outra tarefa. Nestes casos, a prioridade deve ser cuidadosamente pensada para que tarefas de prioridade maior não fiquem “eternamente” esperando outras de prioridade menor.
Portanto, definir inadequadamente as prioridades das tarefas a serem executadas no FreeRTOS oferece um risco enorme ao funcionamento do projeto como um todo. Tenha muita atenção a isso na hora de fazer seu projeto com FreeRTOS!
Atenção: no FreeRTOS, quanto maior o número atribuído a prioridade de uma task, maior será a prioridade dela.

Criação de tasks no FreeRTOS

Para a criação de uma tarefa, o FreeRTOS disponibiliza a função xTaskCreate. Por “criação”, entenda-se agendar a execução de uma função (desenvolvida por você, projetista / desenvolvedor) no scheduler de tarefas do FreeRTOS. A criação de uma tarefa é feita no RTOS via função xTaskCreate, porém como esta contém peculiaridades em vários de seus parâmetros, vou descrevê-los um a um.

Segue abaixo o protótipo da função e descrição de seus parâmetros.

Protótipo: 
BaseType_t xTaskCreate( TaskFunction_t pvTaskCode,
const char * const pcName,
configSTACK_DEPTH_TYPE usStackDepth,
void *pvParameters,
UBaseType_t uxPriority,
TaskHandle_t *pxCreatedTask);

Descrição dos parâmetros:

  • pvTaskCode: ponteiro para a função a ser executada nesta tarefa. Esta função é a implementação da tarefa em si, escrita pelo desenvolvedor / programador. É importante ressaltar que esta função deve ter, obrigatoriamente, tipo de retorno como void e parâmetro único do tipo ponteiro para void (motivo: dessa forma, pode-se passar quaisquer tipo de parâmetros para a tarefa, bastando que esta, quando em execução, faça o casting para o tipo desejado).
  • pcName: nome (string) a ser associado a task. Isso é particularmente útil no debug de um projeto que utiliza o FreeRTOS.
  • usStackDepth: número de palavras (ou words) de memória a serem alocadas como stack da tarefa. Não se esqueça: são words, não bytes de memória!
    Obs: o tamanho de uma word é do tamanho de bits do microcontrolador. Por exemplo, se o microcontrolador que está usando é de 8 bits, uma word tem 8 bits (= 1 byte).
  • pvParameters: parâmetros (de quaisquer tipos) que devam ser passados à tarefa na sua inicialização.
  • uxPriority: prioridade da tarefa. Lembre-se que no FreeRTOS, ao contrário do comumente visto, quanto maior o número atribuído, mais prioritária a tarefa é.
  • pxCreatedTask: handle para a tarefa. Este parâmetro é opcional, desde que você não precise suspender e reiniciar a tarefa durante o seu tempo de execução.

Para mais informações sobre criação de tasks no FreeRTOS, acesse esse link: https://www.freertos.org/a00125.html

Monitoramento do consumo de memória por task no FreeRTOS

Como visto no tópico anterior, uma das informações que uma task deve receber em sua criação é o tamanho da stack, ou seja, a memória RAM que ela terá disponível. Isso significa dizer que:

  • Se você passar memória demais (além do necessário), você pode ter problemas no futuro com falta de memória para outras tasks
  • Se você passar memória de menos (abaixo do necessário), a task não executará e poderá comprometer o funcionamento de toda a solução, já que gera uma falha no FreeRTOS.

Você deve estar se perguntando: então como medir quanto de memória que preciso liberar para uma task? Uma das respostas mais simples e eficazes é via monitoramento do High Water Mark.

Para entender o High Water Mark, imagine a seguinte situação: você coloca achocolatado num copo de vidro transparente e bebe alguns goles. Independentemente de se saber o quanto foi consumido, poderemos ver a marca do nível inicial do achocolatado, indicando que ali foi o nível mais alto que a aquele copo comportou. Esse é o conceito do High Water Mark: é um mecanismo que registra o máximo de memória usada pela task até o momento. Dessa forma, após um tempo de operação, você consegue determinar o máximo de memória que a task requer e, dessa forma, saberá o quanto pode reduzir da quantidade de memória (stack) passada inicialmente.

Portanto, em linhas gerais, para determinar e melhor dimensionar o quanto de memória liberar para uma task, siga o procedimento abaixo:

  1. Primeiro, estabeleça uma quantidade que considera grande para aquela task (um chute “pra cima” mesmo)
  2. Feito isso, faça com que o High Water Mark seja enviado na Serial (para você poder ver via Serial Monitor) de tempos em tempos na task.
  3. Assim, após um tempo de operação que você considera suficiente para a task usar tudo o que poderia de memória, você redimensiona a memória liberada para a task com base no High Water Mark obtido no passo 2.

Mas atenção: mesmo obtendo o High Water Mark, sempre deixe um pouquinho a mais de memória disponível (os clássicos 10% a mais funcionam bem). Pode ser que a task passe por uma situação de uso onde precise de um pouco mais de memória que o visto nos testes. Nesse caso, se o dimensionamento foi feito cravado no valor de High Water Mark, você terá problemas.

Em termos de código, a High Water Mark deve ser armazenada numa variável do tipo UBaseType_t, conforme abaixo:

UBaseType_t uxHighWaterMark;

A leitura dentro de uma task é feita da seguinte maneira:

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

Atenção: o High Water Mark é medido também em words!

Instalação de bibliotecas

Utilizando o próprio gerenciador de bibliotecas da Arduino IDE, instale a biblioteca FreeRTOS by Richard Barry. Preferencialmente, baixe a versão 10.0.0-10, já que esta foi a mesma que usei ao elaborar o projeto que virá a seguir. Se preferir, pode baixar a biblioteca do repositório Github oficial.

Para operar o display LCD I²C 16×2, utilize esta biblioteca aqui.

Entendo o uso de tasks no FreeRTOS e no Arduino

Enfim chegamos a parte prática! É hora de botarmos os conceitos aprendidos em funcionamento. Aqui, iremos controlar um display LCD 16×2 I²C e um LED de forma independente, usando FreeRTOS no Arduino. Iremos piscar o LED e escrever mensagens no display, considerando cada uma dessas funcionalidades como uma tarefa distinta. Neste projeto-exemplo, iremos monitorar o High Water Mark somente da task que escreve no display.

Para fazer o projeto, primeiramente, monte o circuito abaixo.

Imagem 3 - Task no FreeRTOS

Feito isso, Utilize o código-fonte abaixo. Leia com atenção os comentários nele contidos para maior compreensão do mesmo.

#include <Arduino_FreeRTOS.h>
#include <task.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

/* tasks */
void task_lcd( void *pvParameters );
void task_led( void *pvParameters );

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

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 vai em frente quando a serial estiver pronta para funcionar */
    }

    /* Criação das tarefas */
    xTaskCreate(
    task_lcd
    , (const portCHAR *) "LCD" /* Nome (para fins de debug, se necessário) */
    , 156 /* Tamanho da stack (em words) reservada para essa tarefa */
    , NULL /* Parametros passados (nesse caso, não há) */
    , 2 /* Prioridade */
    , NULL ); /* Handle da tarefa, opcional (nesse caso, não há) */

    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_lcd( void *pvParameters )
{
    (void) pvParameters;
    UBaseType_t uxHighWaterMark;
    int contador = 0;

    while(1)
    {
        /* Exibe contador no LCD */
        lcd.setCursor(0,0);
        lcd.print("Contador: ");
        lcd.setCursor(0,1);
        lcd.print(LCD_16X2_CLEAN_LINE);
        lcd.setCursor(0,1);
        lcd.print(contador);
        contador++;

        if (contador == 100)
            contador = 0;

        /* Aguarda 1 segundo para escrever novamente o próximo número no display */
        vTaskDelay( 1000 / portTICK_PERIOD_MS );

        /* 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 exemplo faz uso de duas tasks, uma para cada funcionalidade do projeto (controle do display e controle do LED). Tais tasks foram criadas no setup() (através da função xTaskCreate, do próprio FreeRTOS). O controle do display (feito por task_lcd) consiste na escrita de um contador, de 0 a 99, com tempo de incremento de 1 segundo. Já o controle do LED (feito por task_led), fica encarregada de piscar um LED, com 250 ms de tempo ON e 250 ms de tempo OFF. Ambas as tasks dão a impressão de rodar ao mesmo tempo, fruto da ação do FreeRTOS.
Em suma, cada task  se comportará como um programa isolado, executando somente o que lhe é planejado. Fica a cargo do FreeRTOS executar estas tasks e fazer tudo rodar, garantindo tudo que “corre por baixos dos panos” em um sistema operacional de tempo real.

Conclusão

Neste post, você conheceu as informações básicas e fundamentais do FreeRTOS, um sistema operacional de tempo real largamente utilizado em projetos comerciais e não comerciais. Além disso, viu que ele é tão importante que é mantido pela Amazon, o que dá uma ideia do uso e credibilidade do FreeRTOS.

Além disso, aprendeu a como pensar / arquitetar uma solução que utiliza FreeRTOS, bem como aprendeu a criar tarefas (tasks), mensurar e dimensionar o quanto de memória RAM uma tarefa tem disponível e, por último mas não menos importante, viu a aplicação disso tudo na prática utilizando um Arduino.

Isso pode abrir sua mente para novas possibilidades de projetos, ou pelo menos melhoria de alguns já existentes.

Gostou deste post sobre o tasks no FreeRTOS e como utiliza-lo com Arduino? Deixe seu comentário logo abaixo.

Faça seu comentário

Acesse sua conta e participe

3 Comentários

  1. Opa consegui pelo seu link o Git HUB

  2. Opa consegui pelo seu link o Git HUB, outro freertos sem chance

  3. Pedro, antes que eu faça seu curso na Udemy. Como vc conseguiu compilar no Nano? Dá mensagem Sketch too big.