FreeRTOS: Sistema Operacional para Arduino 5

O FreeRTOS é um sistema operacional de tempo real para sistemas embarcados. Neste post veremos uma breve introdução aos conceitos de sistema operacional (SO) e como instalar o FreeRTOS no Arduino UNO e como utiliza-lo em um projeto simples de multitasking para controle paralelo de dois LEDs em frequências diferentes.

O que é um sistema Operacional?

Um sistema operacional (SO) é um software ou um conjunto de softwares responsável pelo gerenciamento de recursos do sistema e por fornecer uma camada de abstração do hardware. O SO atua como um intermediário entre os programas e aplicações.

Imagem 1 - Sistema Operacional

Em outras palavras, uma das principais funções de um SO é abstrair o hardware do software, de forma que o desenvolvedor do software não precise se preocupar com a configuração do hardware que irá executar sua aplicação.

Um exemplo clássico é a comunicação com uma impressora. Existem diversas impressoras de diferentes fabricantes com diferentes tecnologias e protocolos de comunicação. Entretanto, o software não precisa se preocupar com nenhuma desta questões de “baixo nível”, ele apenas solicita ao sistema operacional o uso da impressora e o sistema operacional, através de instruções descritas no driver do dispositivo, realiza a comunicação.

Além da abstração, outra função principal do SO é gerenciar os recursos do sistema hospedeiro, como memória, processador, disco rígido e interface de rede, permitindo que diversas aplicações utilizem estes recursos sem entrarem em conflito.

FreeRTOS: Sistema Embarcado de Tempo Real de Código Livre

O FreeRTOS é um kernel de sistema operacional (kernel, ou núcleo, é a parte principal do SO, responsável por todo o sistema) desenvolvido pela Real Time Enginners Ltd. sob a licença MIT. Por ser Open Source, qualquer um pode acessar os códigos fontes do kernel, modificá-los e distribuí-los sob os limites impostos pela licença MIT.

Por ser de código aberto, isso permitiu o surgimento de diversas versões do FreeRTOS suportando uma larga variedade de dispositivos, atualmente existem versões do SO para mais de 35 famílias de processadores, incluindo a Atmel AVR, cujo microcontrolador ATMega328 é o componente principal do Arduino UNO.

Existe uma extensão do kernel do FreeRTOS, desenvolvida pela Amazon, denominada Amazon FreeRTOS ou a:FreeRTOS. Esta versão consiste no kernel do FreeRTOS com bibliotecas para IOT, permitindo a conexão segura dos dispositivos aos serviços de nuvem da Amazon Web Services IOT.

Imagem 2 - FreeRTOS no Arduino

O FreeRTOS foi projetado para sistemas embarcados, sistemas com poucos recursos e pequeno consumo de energia, portanto não implementa muitas das funções executadas por um SO convencional. Geralmente é encarado como uma biblioteca de multi-thread (ou multitasking) ao invés de um sistema operacional propriamente dito, entretanto algumas funções definidas pela POSIX (POSIX – Portable Operational System Interface, ou Interface Portável entre Sistemas Operacionais),  um conjunto de normas para sistemas operacionais, seguida pelo Unix e Linux, estão implementadas no kernel FreeRTOS.

O FreeRTOS é um SO preemptivo, ou seja, permite a execução de tarefas em paralelo (multitasking) de forma que cada tarefa acesse o processador por um certo período de tempo (quantum) e, ao término, troque automaticamente para a próxima tarefa. Nesse processo todos os dados da tarefa corrente são salvos de forma que, ao obter novamente direito ao processador, a tarefa continue no exato ponto onde parou, como se nunca tivesse parado.

Os critérios para escolha da próxima tarefa variam, geralmente define-se uma prioridade para cada tarefa. Dependendo da forma que o sistema escolhe a próxima tarefa pode ser necessário um cuidado adicional para evitar que uma tarefa nunca ganhe direito ao processador, pois sempre há uma tarefa mais prioritária que ela, essa situação é chamada de inanição (ou starvation). SOs mais complexos implementam sistema de prioridade dinâmica, aumentando a prioridade das tarefas não escolhidas a fim de evitar a inanição.

O gerenciamento de tarefas pode ser feito de formas diferentes em outros sistemas operacionais. Além dos sistemas preemptivos destacam-se também os de lote (batch) que executam apenas uma única tarefa por vez de forma sequencial (não é muito utilizado atualmente, alguns sistemas mais antigos, como o MS-DOS da Microsoft, utilizavam este gerenciamento) e os cooperativos, que não realizam a troca automática de tarefas e exigem que a tarefa corrente termine ou abra mão do processador para executar outra.

A imagem abaixo ilustra o uso do processador, por diferentes Tarefas (T1, T2 e T3), nestas três formas de gerenciamento de tarefas.

Imagem 3 - Processador

No primeiro caso, cada tarefa termina antes de iniciar a próxima. No segundo, a tarefa corrente pode abrir mão do processador antes de terminar de executar, nesse caso, a próxima tarefa assume. E no terceiro caso, o SO altera a tarefa corrente a cada período (quantum).

Repare que apesar de ser multitasking, as tarefas não rodam, de fato, em paralelo, mas uma de cada vez de forma alternada. Isso pois estamos considerando um processador singlecore, no caso de um multicore é possível o verdadeiro paralelismo, cada tarefa rodando em um core diferente.

Como descrito pelo nome, o FreeRTOS é um RTOS (Real-time Operational System), ou Sistema Operacional de Tempo Real, cuja resposta temporal deve ser conhecida no melhor e no pior caso. Deve ser possível estabelecer prazos mínimos e máximos para conclusão de uma tarefa que devem ser respeitados, caso contrário pode-se configurar uma falha grave. Geralmente este tipo de SO prioriza tempo ao invés de quantidade, ou seja, suporta menos tarefas paralelas mas que possuem comportamento temporal bem definido.

O FreeRTOS permite a alocação dinâmica ou estática de memória para as tarefas e ferramentas do sistema.

No caso da alocação dinâmica, basta utilizar as funções para criar as tarefas, o próprio sistema irá reservar a memória necessária para a execução da mesma de forma dinâmica. Este método é mais prático e permite a criação dinâmica de tarefas, mas os erros relacionados à falta de memória não são detectados em tempo de compilação, ou seja, o desenvolvedor só saberá se ultrapassou o limite de memória do sistema ao rodar a aplicação e perceber alguma inconsistência.

No caso da alocação estática, necessita-se reservar manualmente as áreas de memória necessárias para as tarefas antes de criá-las, isso é feito na forma de declaração de variáveis. Esse método é menos prático, mas ajuda a lidar com situações de memória limitada, os erros referentes a falta de memória são descobertos em tempo de compilação.

Existe um arquivo de configuração do sistema operacional denominado FreeRTOSConfig.h, no qual é possível customizar o comportamento do sistema através da declaração de macros, é necessário incluir uma cópia deste arquivo no diretório local do projeto, a descrição deste arquivo e das macros utilizadas pode ser encontrado no site do FreeRTOS.

Multitasking com Sistemas Operacionais

No post Tarefas rodando em paralelo no Arduino é mostrado uma forma de realizar tarefas em paralelo utilizando um timer e um loop que verifica cada estouro para executar ou não um trecho de código. Essa é uma maneira muito simples de executar diversas tarefas e em muitos casos poder ser boa o bastante, sem necessidade de um SO (lembre-se que um SO oferece um custo adicional de memória e processamento para o sistema), entretanto há algumas limitações.

Esse modelo se assemelha a um sistema em lote, onde a próxima tarefa só é executada ao termino da atual, além disso as tarefas estão acopladas, ou seja, uma falha em uma delas pode afetar todas as outras. Em multitasking as tarefas devem rodar de forma “transparente” às demais, a execução de uma tarefa não deve afetar a execução das outras, também é necessário definir as regiões de memória de cada tarefa e gerenciar os acessos aos recursos do sistema de forma a evitar conflitos, esta forma simplificada não cumpre nenhuma dessas exigências.

Por ser um sistema preemptivo, o FreeRTOS não só fornece as interfaces para criação de tarefas paralelas, como também ferramentas de controle de acesso para evitar conflitos ao acessar recursos, como duas ou mais tarefas tentando acessar o mesmo recurso ao mesmo tempo, como uma variável global ou um periférico. Dependendo do recurso, pode-se tolerar o acesso simultâneo, tolerar um certo número de acessos simultâneos ou ser exclusivo (um único acesso por vez), os SOs que suportam concorrência contém ferramentas para lidar com as condições de concorrência ou Condições de Corrida (Race Condictions).

Multitasking em Arduino Uno com FreeRTOS

É importante ressaltar que o Atmega328, e consequentemente o arduino, não foi desenvolvido tendo em mente a execução de um sistema operacional, portanto, limitações poderão ser observadas em certas funcionalidades.

O arquivo FreeRTOSConfig já está incluído na biblioteca e não é necessário uma cópia local do mesmo no diretório do projeto, além disso algumas configurações não estão disponíveis devido as limitações do hardware, o uso de um arquivo de configurações modificado pode não ser muito útil.

Instalando o FreeRTOS

Pode ser instalado através do gerenciador de bibliotecas da Arduino IDE

Imagem 4 - FreeRTOS no Arduino

Procure por FreeRTOS e instale a versão mais recente do FreeRTOS by Richard Barry. Após isso basta incluir os cabeçalhos e a ArduinoIDE irá linkar o FreeRTOS automaticamente.

Projeto

Neste exemplo vamos realizar duas tarefas simples em paralelo: a geração de um sinal senoidal através do PWM e o piscar periódico de um LED. Cada tarefa irá rodar em uma task diferente e ao fim de um ciclo, enviará, através da porta serial, o tempo em que iniciou e finalizou o ciclo de execução.

Materiais Necessários

Circuito

O circuito utilizado para o teste é bem simples, composto apenas por dois LEDs, devemos poder ver a variação da luminosidade de cada LED, um a 1 Hz de maneira discreta (on/off) e o outro a 2 Hz de forma senoidal.

Imagem 4 - FreeRTOS no Arduino

Código

O código fonte utilizado está descrito abaixo:

#include "Arduino_FreeRTOS.h"
#include "task.h"
#include "semphr.h"

#include <math.h>

#define SEND_GANTT  1
#define AOUT        9
#define LED_OUT     7

//Tasks
TaskHandle_t        ledTaskH;
TaskHandle_t        sineTaskH;

//Mutex
SemaphoreHandle_t   SerialMutex;

void sendGantt(const char *name, unsigned int stime, unsigned int etime) {
    if(xSemaphoreTake(SerialMutex, portMAX_DELAY) == pdTRUE) {  //Solicita Mutex
        Serial.print("\t\t");
        Serial.print(name);
        Serial.print(": ");
        Serial.print(stime);
        Serial.print(", ");
        Serial.println(etime);
        xSemaphoreGive(SerialMutex);                            //Libera Mutex
    }
}

void setup() {
    //Inicializa Serial
    Serial.begin(9600);
    Serial.print("1s is ");
    Serial.print(configTICK_RATE_HZ);
    Serial.print(" ticks at ");
    Serial.print(F_CPU);
    Serial.print(" Hz\n\n");
    #if (defined(SEND_GANTT) && (SEND_GANTT==1))
        Serial.println("gantt\n\tdateFormat x\n\ttitle A gant diagram");
    #endif
    
    SerialMutex = xSemaphoreCreateMutex();
    
    //Cria tarefa ledTask
    xTaskCreate(ledTask,            //Funcao
                "ledTask",          //Nome
                128,                //Pilha
                NULL,               //Parametro
                1,                  //Prioridade
                &ledTaskH);
    
    //Cria tarefa sineTask
    xTaskCreate(SineTask,
                "SineTask",
                128,
                NULL,
                1,
                &sineTaskH);    
}

void loop() {
    // Nada é feito aqui, Todas as funções são feitas em Tasks
}

/* LedTask
 *  Pisca Led com frequência de 1Hz
 */
void ledTask(void *arg) {
    unsigned int stime; 
    pinMode(LED_OUT, OUTPUT);
    while(1) {
        stime = millis();
        digitalWrite(LED_OUT, HIGH);                        //Liga Led
        vTaskDelay(pdMS_TO_TICKS(500));                //Espera 0.5s
        digitalWrite(LED_OUT, LOW);                         //Desliga Led
        vTaskDelay(pdMS_TO_TICKS(500));                //Espera 0.5s
        #if (defined(SEND_GANTT) && (SEND_GANTT==1))
            sendGantt("Led", stime, millis());         //Envia Informações pela Serial
        #endif
    }
    //O codigo nunca deve chegar aqui
    vTaskDelete(NULL);      //Deleta a Task atual
}

/* SineTask
 *  Descreve uma senoide de 2Hz na saida analogica
 */
void SineTask(void *arg) {
    unsigned int stime=0, etime=0;
    pinMode(AOUT, OUTPUT);
    unsigned int outpv = 0;
    unsigned int period = 0;
    while(1) {
        stime = millis();
        for(period = 0; period < 16; ++period){
            etime = millis();
            outpv = (unsigned int)((sin(2*PI*2*(etime - stime)*0.001)+1)*127.5);  /*Senoide = seno(2*PI*Freq*t)*/
            analogWrite(AOUT, outpv);
            vTaskDelay(pdMS_TO_TICKS(33));       //Espera 33 milisegundos (32 ms)
        }
        #if (defined(SEND_GANTT) && (SEND_GANTT==1))
            sendGantt("Sine", stime, millis());
        #endif
    }
    //O codigo nunca deve chegar aqui
    vTaskDelete(NULL);      //Deleta a Task atual
}

O FreeRTOS não possui um arquivo de cabeçalho monolítico, portanto devemos incluir os cabeçalhos de cada componente utilizado em nosso software. O cabeçalho Arduino_FreeRTOS.h deve ser incluído antes dos demais, contém as funções do kernel do FreeRTOS. Seguindo tem-se task.h e semphr.h, contendo as interfaces para criação e gerenciamento de tasks e semáforos respectivamente.

Na função setup, inicializamos a porta serial e criamos duas tarefas através da função xTaskCreate, esta função recebe como argumentos:

  • Função que contém o código da tarefa (deve ser do tipo void e receber um ponteiro genérico como único argumento)
  • Uma string contendo um nome para identificar a tarefa
  • Tamanho da pilha em bytes. A pilha é uma área da memória usada para armazenar as variáveis, dependendo da complexidade da tarefa pode ser necessário aumentar esse valor, entretanto deve-se ter em mente a memória limitada do sistema embarcado (2KB no caso do arduino uno) mais os custos de memória que o próprio SO adiciona. Aplicações mais simples devem conseguir executar com 128 bytes.
  • Um ponteiro genérico (void*) para um parâmetro a ser utilizado pela sua tarefa, será o valor passado para a função que descreve a tarefa (caso a tarefa não necessite de parâmetros, deixem NULL)
  • Prioridade da tarefa (valor inteiro). O FreeRTOS considera menor números como menor prioridade, mas isso não é universal para todos os SOs.
  • Ponteiro para gerenciador da tarefa, opcional, utilizado somente quando se necessita modificar ou acessar externamente uma tarefa. (Caso não utilize, deixem NULL)

Algumas versões do FreeRTOS podem exigir que o usuário inicie manualmente o SO, na versão de Arduino isso é feito automaticamente, ao sair da função setup o sistema deve iniciar sozinho.

Não executamos nada na função loop, todas as funções são realizadas em tasks. Ao utilizar o FreeRTOS a função loop passa a ser chamada pela idleTask apenas quando não há nenhuma outra tarefa solicitando o processador, e deixa de executar assim que qualquer tarefa solicite o processador, por isso geralmente não é utilizada em aplicações multitasking.

A tarefa ledTask é responsável por piscar um LED em uma frequência de 1 Hz, para isso utiliza-se a função vTaskDelay, ao invés da função nativa delay, para gerar um atraso na execução do código. Isso pois a função vTaskDelay informa ao SO que a tarefa atual entrará em delay e o processador pode ser passado para outra tarefa enquanto isso. Caso utilizássemos a função nativa, o processador ficaria inativo (Idle) sem executar qualquer instrução até o fim do delay ou que a tarefa atual perca o processador (pelo fim de sua fatia de tempo). Esta função recebe o valor em ticks, a macro pdMS_TO_TICKS converte um valor em microssegundos para o valor em ticks.

A tarefa sine é responsável por gerar um sinal senoidal de 2 Hz utilizando o PWM, assim como ledTask, utiliza-se a função vTaskDelay para controle de tempo. Para gerar uma senoide de 2 Hz deve-se atualizar a saída analógica a uma frequência superior a 4 Hz (superior ao dobro da frequência do sinal, segundo a teoria da amostragem), neste caso a atualização ocorre a cada 32 ms (aproximadamente 30 Hz). Observe que no código há 33 ao invés de 32 na conversão de milissegundos para ticks, isso se deve a umas das limitações do arduino.

Ambas as tarefas enviam, pela serial, o tempo do início e do fim de cada ciclo de execução. No arduino uno há somente uma porta serial, portanto esse recurso deve ser compartilhado por ambas as tarefas. Enquanto uma tarefa estiver utilizando a porta serial, a outra deve esperar até que todos os dados sejam enviados, caso contrário veríamos os caracteres embaralhados, para isso utilizamos um controle de acesso, como um semáforo (ou Mutex).

Um semáforo é uma variável especial utilizada em SOs para controle de acesso. Cada semáforo contem uma contagem que indica quantas tarefas podem ter acesso a um recurso, quando a contagem é excedida, as subsequentes tarefas que solicitem acesso serão suspensas até que outra tarefa, que tenha conseguido acesso ao recurso, libere o semáforo. Quando o semáforo tem contagem 1 (apenas uma tarefa pode acessar o recurso por vez) é chamado de Mutex (Mutual Exclusion). No nosso caso usaremos um mutex pois apenas uma tarefa pode acessar a porta serial por vez.

Na função setup um mutex é criado pela função xSemaphoreCreateMutex. O utilizamos na função send gantt através da função xSemaphoreTake, cujos parametros são:

  • Semáforo (ou mutex) a ser utilizado
  • Time-Out. Tempo máximo que a tarefa ficará suspensa por não conseguir obter o semáforo. O valor portMAX_DELAY faz com que a tarefa somente retorne a executar quando conseguir obter o semáforo.

No caso de sucesso, a tarefa conseguiu obter o semáforo sem ocorrer time-out, a função retorna pdTRUE, caso contrário, um código de erro é retornado.

Após terminarmos de utilizar o recurso informamos ao SO, através de xSemaphoreGive, que libera a contagem do semáforo (permitindo outras tarefas utilizarem o recurso).

Deve-se utilizar semáforos e mutex com cuidado pois os mesmos podem ocasionar erros no sistema. Por exemplo, imagine uma situação que uma tarefa A aguarde a liberação de um recurso da tarefa B que, por sua vez, aguarda a liberação de um recurso da tarefa A, neste caso ambas as tarefas ficarão impedidas de continuar (essa situação é chamada de deadlock).

Imagem 6 - Tarefas

Geralmente é uma boa prática sempre definir um timeout e lidar com a situação onde o recurso não é obtido (principalmente quando se deseja desenvolver aplicações de Tempo Real).

Executando no Arduino

Imagem 7 - FreeRTOS no Arduino

Observe que a luminosidade dos leds variam de formas diferentes, o primeiro continuamente (senoidalmente)  com 2 Hz de frequência e o segundo discretamente com frequência de 1 Hz.

Diagrama de Gantt

Com os valores enviados pela Serial é possível observar o comportamento temporal das diferentes tarefas que rodam no sistema. Podemos observar esses dados de forma gráfica através de um diagrama de Gantt.

O diagrama de Gantt é um gráfico, utilizado no desenvolvimento de projetos, para ilustrar as diferentes etapas e o tempo previsto para a conclusão das mesmas. É composto por barra horizontais, que representam uma etapa do projeto, cujo comprimento é proporcional ao tempo demandado. É possível utilizar este diagrama para visualizar o comportamento temporal de um sistema multi-task.

Existe uma ferramenta online para a geração destes gráficos, o Mermaid Live Editor, os dados recebidos pela serial já estão formatados para o uso desta ferramenta.

Imagem 8 - Diagrama de Gantt

Observe que ambas as tarefas iniciam simultaneamente e executam em paralelo, a Sine no dobro da frequência da Led.

Erros

A versão do FreeRTOS para arduino utiliza o LED interno (LED_BUILTIN) para indicar erros, é possível utilizar esse componente em seus projetos, mas isso dificultará a detecção de erros do SO.

Dois erros são indicados através do LED:

  • Stack Overflow: Ocorre quando se excede a capacidade da pilha de uma tarefa. Neste caso o led pisca lentamente. Verifique o tamanho das pilhas das tarefas e tenha aumentar a que demandar mais memória.
  • Falha de Heap: Ocorre quando não se consegue alocar memória dinamicamente. Neste caso o led pisca rapidamente. Verifique as tarefas, tente reduzir as pilhas das tarefas mais simples (não exigem muita memória) ou diminuir o número de tarefas.

Limitações do Arduino

A versão do FreeRTOS para arduino utiliza o watchdog timer para gerar interrupções periódicas e contabilizar os ticks do SO, o menor valor possível de se configurar o watchdog é 15 ms (padrão), portanto o menor valor de tick possível é de 15 ms, entretanto o valor 16,13 ms é utilizado internamente como valor do tick, pois corresponde melhor ao valor do watchdog, isso quer dizer que é impossível utilizar a função vTaskDelay para gerar um delay menor que 16,13 ms (ou que não seja múltiplo). Na tarefa sine a conversão de 32 ms para ticks resulta em 1,92 ticks, como ticks é um valor inteiro a parte decimal é descartada (truncado), portanto o retorno da conversão dá 1 (aproximadamente 16,13 ms), por isso foi utilizado 33 ms cuja conversão resulta 2,045 ticks, truncado para 2 (aproximadamente 32,26 ms).

Outros processadores, como o Arm Cortex-M4, que foram desenvolvidos visando a execução de um SO possuem mecanismos mais eficientes. O M4 contém um timer especial para realizar as interrupções periódicas dos ticks (o systick timer), portanto permite configurar o valor do tick para a ordem do período do cristal utilizado.

Gostou do post sobre FreeRTOS? Deixe seu comentário logo abaixo.

Faça seu comentário

Acesse sua conta e participe

5 Comentários

  1. Poxa, que estou super interessado em embarcados e seu uso com FreeRTOS, por favor se possível produzir mais post assim sempre será bem vindo!

  2. Bacana! Obrigado pela aula!

  3. Muito bom. Uma verdadeira aula!!!

  4. Eai irmão, seu artigo está ótimo.
    Porém algumas imagens estão bugadas, vc poderia corrigir isso e ficaria melhor ainda para a gente entender.

    1. Olá, Kayann!

      Obrigado pelo aviso, já consertamos!

      Diogo – Equipe MakerHero