O Arduino é, sem sombra de dúvidas, uma das plataformas mais utilizadas para desenvolver projetos que envolvam sistemas embarcados, sobretudo no que diz respeito à prototipação rápida. Contando com uma comunidade grande, extremamente ativa e também com grande número de bibliotecas de suporte a hardware e comunicação (sensores, atuadores, protocolos de comunicação locais e em rede, etc.), o Arduino fica cada dia mais popular e adotada por aqueles que estão iniciando na área de sistemas embarcados.
Porém, o mundo não acaba nas facilidades que o Arduino oferece. Ao avançar os estudos, o aspirante a especialista em sistemas embarcados verá que há mundos novos em termos de conhecimento, sendo um deles o mundo dos Sistemas Operacionais de Tempo Real (ou, mais comumente encontrado na literatura, Real-Time Operating Systems – RTOS).
Dada a popularidade do Arduino, parece uma boa decisão utilizar a plataforma para ensinar novos conceitos e técnicas. E é justamente disso que este artigo irá tratar: a explicação dos principais conceitos de um RTOS para iniciantes, utilizando para exemplificar a plataforma o Arduino e como Sistema Operacional de Tempo Real o FreeRTOS.
O que é o FreeRTOS?
O FreeRTOS é um dos RTOS 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 desejar saber mais sobre o FreeRTOS, acesse o site oficial: www.freertos.org.
Por que usar um sistema operacional?
Essa é uma pergunta que os mais iniciantes devem se fazer ao conhecer a possibilidade de se usar um sistema operacional. Afinal, se é possível fazer projetos sem sistema operacional, por que aprender a utilizá-los? A resposta contém vários argumentos, sendo os principais listados abaixo.
Para melhor compreendê-los, considere todas as funcionalidades que o projeto/produto possue como sendo as aplicações e toda a “base” como sendo o sistema operacional.
- Confiabilidade: acesso a hardware, gerenciamento de memória, comunicação entre processos distintos e muito mais já são providos pelo sistema operacional. Logo, como há um elemento cuidando (e muito bem) dessas coisas vitais a todo sistema embarcado, a confiabilidade no projeto / produto aumenta. Isso significa que bugs referentes a estes tópicos também tendem a diminuir (ou até mesmo desaparecer, a depender do quanto o sistema operacional foi testado e depurado), tornando a solução final mais confiável.
- Portabilidade: portar uma aplicação de um projeto / produto para outro quando ambos utilizam o mesmo sistema operacional é, na maioria das vezes, algo simples. Isso ocorre pois toda a base do funcionamento (o sistema operacional em si) é comum. Isso leva a um ganho de tempo a prejuízo zero de confiabilidade.
- Reaproveitamento de código: aplicações podem ser instanciadas com parametrização diferentes (ou instanciadas com a mesma parametrização, como um novo processo). Isso significa reaproveitar código-fonte para realizar mais tarefas, o que é algo extremamente benéfico em ganho de tempo, confiabilidade (pois, se a aplicação já foi testada e depurada, provavelmente vai funcionar sendo instanciada várias vezes) e manutenção de código-fonte.
- Segurança: sistemas eletrônicos estão sempre sujeitos a ataques e busca por brechas para fraudes. Trabalhar com um sistema operacional significa que, uma vez que uma falha foi descoberta, a correção será feita a nível de sistema operacional, muitas vezes influenciando em nada no código-fonte de sua aplicação. Essa abordagem em camadas permite que sistemas embarcados sejam corrigidos de falhas graves sem alteração alguma nas aplicações.
Primeiro conceito: como arquitetar uma solução usando um RTOS
Como qualquer sistema operacional, os RTOS são capazes de executar diversas tarefas “simultaneamente”.
Aqui cabe o primeiro grande conceito: o termo “simultaneamente” é usado devido a percepção de que tudo está executando ao mesmo tempo. Na realidade, o sistema operacional fica chaveando / alternando entre as tarefas, executando parte de cada uma delas por vez por alguns milissegundos e, posteriormente, passando a vez à próxima tarefa. Logo, um sistema operacional é, do ponto de vista de software, um executor muito rápido de tarefas, capaz de executar uma por vez mas de forma tão rápida que a percepção é a de que tudo está rodando em paralelo.
O core de um sistema operacional é chamado de Kernel. O Kernel é o responsável por gerenciar o hardware, executar as tarefas e memória da forma mais adequada possível, manter tudo rodando de forma estável e facilitar o desenvolvimento de projetos (uma vez que ele cuida do gerenciamento de hardware e memória).
Sendo assim, um RTOS (ou Kernel de RTOS), além de gerenciar hardware e memória, executa tarefas. Tais tarefas, em conjunto com o próprio Kernel, formam a parte de software de um projeto baseado em RTOS. Mas, como arquitetar soluções / software embarcado para um projeto ou produto neste conceito RTOS?
Em uma abordagem de software onde se usa um RTOS, todas as operações e funcionalidades de um projeto são definidas em tarefas (ou tasks, conforme mais comumente encontrado na literatura). Uma tarefa é como se fosse um programa / rotina isolada, modularizada o suficiente para ser uma tarefa isolada. Em suma uma tarefa pode ser resumida como uma funcionalidade do sistema embarcado.
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 de um sistema embarcado pode ser encarado como uma tarefa ou task isolada, havendo ou não comunicação entre as tarefas. 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 desastrosas. No caso de uma UART, 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 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 recursos compartilhados por carros de um cruzamento, e se todos tentarem acessar a via ao mesmo tempo, acidentes acontecerão com certeza. 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 provido 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 dá ainda mais segurança, uma vez que a chance de algo estar errado na implementação do semáforo é muito menor do que se fosse implementada do zero novamente pelo projetista (isso considerando que o RTOS em questão tenha sido suficientemente testado, claro).
Terceiro conceito: comunicação entre tarefas distintas
Até aqui, vimos que cada funcionalidade do RTOS deve ser isolada / encapsulada numa tarefa (task). Porém, e se tasks precisarem se comunicar?
Para isso, os RTOS oferecem mecanismos de comunicação inter-processos, sendo os mais comuns as filas (queues). Filas (queues) são capazes de trafegar dados entre tarefas, sempre numa via única (Task1 -> Task2, para envio de informação do tipo Task2 -> Task1 uma nova fila deve ser usada), com a garantia do RTOS 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: em um RTOS, 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.
Paralelo dos conceitos com o FreeRTOS
De forma a exemplificar os conceitos de RTOS vistos neste artigo, seguem abaixo funções e métodos reais do FreeRTOS que atendem a tais conceitos.
Antes de tudo: prioridades de tarefas no FreeRTOS
As tarefas no FreeRTOS possuem prioridade. 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 (chamado quantum).
Conforme visto acima, a prioridade das tarefas é algo de importância crítica 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.
Criação de tarefas (tasks)
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 pelo programador do projeto) 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 descreve-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 a tarefa, quando em execução, fazer 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 ser alocada como stack da tarefa. Não se esqueça: são words, não bytes de memória!
– pvParameters: parâmetros (de quaisquer tipos) que devem 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 da prioridade, 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 tempo de execução da tarefa.
Ainda, é possível criar mais tarefas (com handlers distintos) que utilizam a mesma função-base / implementação. Ou seja, um mesmo código pode ser reutilizado (instanciado) em duas ou mais tarefas. Isto eleva o nível de reaproveitamento de código e de prevenção de falhas, já que uma mesma função, já testada e validada, pode ser utilizada em tarefas distintas. Para isso ocorrer, quase sempre faz-se uso da passagem de parâmetros (através de pvParameters) e tratamento dos mesmos dentro da função, de modo que esta possa desempenhar papéis ligeiramente diferentes / correlatos com um mesmo código.
Para mais informações, acesse https://www.freertos.org/a00125.html
Criação de semáforos
O FreeRTOS oferece mais de um tipo de semáforo, porém neste artigo vamos nos concentrar no semáforo do tipo MUTEX. O termo MUTEX significa Mutual Exclusion (exclusão mútua), e um semáforo deste tipo tem como objetivo restringir o acesso a um recurso (uma interface de comunicação serial, por exemplo) a uma única tarefa, enquanto esta não “liberar” o semáforo. Logo, o ciclo de uso de semáforos MUTEX é conforme a seguir:
- Antes das tarefas iniciarem, o handler do semáforo (do tipo SemaphoreHandle_t) é criado e, em momento oportuno (antes da criação das tarefas, por exemplo), o semáforo é criado utilizando o seu handler e a função xSemaphoreCreateMutex
- Uma tarefa tenta obter o semáforo (função xSemaphoreTake)
- Se conseguir (ou seja, se nenhuma outra tarefa estiver utilizando o semáforo), a tarefa segue a execução normal de suas tarefas dependentes deste semáforo (como a manipulação de recursos compartilhados, como uma interface serial, por exemplo).
- Finalizado o uso dos recursos que dependiam do semáforo, o semáforo é liberado (função xSemaphoreGive).
Dessa forma, o recurso protegido pelo semáforo pode ser utilizado por outra tarefa.
Pontos de atenção:
- De forma a serem visíveis por todas as tasks, é recomendável que os handlers dos semáforos sejam declarados globalmente.
- 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. Ainda, este tempo pode ser infinito (se atribuído a este parâmetro a macro portMAX_DELAY).
MUITA ATENÇÃO ao se usar a macro portMAX_DELAY. Se não bem pensado, este uso pode causar algo similar a um deadlock.
Para mais informações, consulte:
- xSemaphoreCreateMutex: https://www.freertos.org/CreateMutex.html
- xSemaphoreTake: https://www.freertos.org/a00122.html
- xSemaphoreGive: https://www.freertos.org/a00123.html
Criação de filas (queues)
Existe, a priori, um tipo de fila apenas no FreeRTOS, sendo que esta obedece ao tipo de leitura e escrita FIFO (First In First Out). Porém, existem várias formas de se escrever e ler desta fila, inclusive dependendo de onde no programa se está.
Primeiramente, a fila precisa ser criada. Para a criação de uma fila, primeiro é criado um handler (do tipo QueueHandle_t) e, em momento oportuno (antes de criar as tarefas, por exemplo), cria-se a fila propriamente dita usando o seu handler e a função xQueueCreate. A criação de uma fila compreende informar quantos elementos deseja que ela tenha e de qual tipo de dados é cada elemento.
De forma simplista, para inserir elementos numa fila há quatro opções de funções no FreeRTOS:
- xQueueSend: adiciona elemento a uma fila. Esta função NÃO deve ser utilizada dentro do tratamento de uma interrupção (ISR).
- xQueueSendFromISR: adiciona elemento a uma fila. Esta função DEVE SER SOMENTE USADA dentro do tratamento de uma interrupção (ISR).
- 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. Esta função NÃO deve ser utilizada dentro do tratamento de uma interrupção (ISR).
- xQueueOverwriteFromISR: sobrescreve o primeiro elemento de uma fila, devendo ser usada SOMENTE DENTRO DE UM TRATAMENTO DE INTERRUPÇÃO (ISR). 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.
Já para a leitura dos elementos de uma fila, também de forma simplista, há as seguintes opções no FreeRTOS:
- xQueueReceive: le um elemento da fila. Esta função NÃO deve ser utilizada dentro do tratamento de uma interrupção (ISR).
- xQueueReceiveFromISR: le um elemento da fila. Esta função SOMENTE DEVE SER UTILIZADA dentro do tratamento de uma interrupção (ISR).
- xQueuePeek: faz a leitura o 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. Esta função NÃO deve ser utilizada dentro do tratamento de uma interrupção (ISR).
- xQueuePeekFromISR: exatamente conforme explicação acima, faz a leitura o 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. Esta função SOMENTE DEVE SER UTILIZADA dentro do tratamento de uma interrupção (ISR).
Pontos de atenção:
- De forma a serem visíveis por todas as tasks, é recomendável que os handlers das filas sejam declarados globalmente.
- Todas as funções de inserção e leitura de elementos de uma fila possuem um parâmetro muito importante chamado xTicksToWait. Ele especifica quantos ticks do processador deve-se aguardar na tentativa de se ler ou interir elementos de uma fila. Ainda, este tempo pode ser infinito (se atribuído a este parâmetro a macro portMAX_DELAY).
MUITA ATENÇÃO ao se usar a macro portMAX_DELAY. Se não bem pensado, este uso pode causar algo similar a um deadlock. - Se possível, sempre criar uma queue para cada tipo de informação / operação. Isso ajuda a modularização, organização de código, diminui chances de erros e melhora o debug. Uma queue para comunicação exclusiva entre duas tasks seria o cenário ideal do ponto de vista de modularização e legibilidade de código.
Para mais informações, consulte:
- Queue Management: https://www.freertos.org/a00018.html
Ressalvas quanto ao port do FreeRTOS para o Arduino
O uso do FreeRTOS no Arduino abstrai a inicialização do scheduler de tarefas do FreeRTOS. Isso é feito de maneira automática, logo após a execução da função setup(). Em outras plataformas, isso pode não ser abstraído. Outra ressalva é que, no Arduino, as inicializações particulares do FreeRTOS referentes ao microcontrolador utilizado também são abstraídas, algo que é preciso ser feito em outras plataformas.
Em suma: tenha em mente que o Arduino abstrai algumas coisas do FreeRTOS para o desenvolvedor, logo o código-fonte do uso do FreeRTOS em outra plataforma poderá ser ligeiramente diferente.
Projeto simples com FreeRTOS e Arduino
Agora que você conhece três dos principais conceitos de um RTOS, será feito um paralelo do uso dos mesmos com FreeRTOS no Arduino. Neste exemplo, utilizaremos tais conceitos aplicados no FreeRTOS para controlar um display LCD e um LED. O display exibe o valor da leitura da tensão no canal 0 do ADC (o qual possui um potenciômetro) e o LED acende ou apaga dependendo do valor desta leitura.
Material necessário
Para reproduzir os experimentos escritos aqui, você precisará de:
- Um Arduino qualquer que utilize um microcontrolador Atmel com cabo de alimentação e programação.
Nos meus experimentos utilizei um Arduino Nano V3, mas você pode utilizar tranquilamente um Arduino Mega ou Arduino Uno se assim desejar, desde que faça a adequação de pinos onde os periféricos serão ligados. - Um LED
- Um potenciômetro linear de 100 kOhm
- Dois resistores de 330 Ohms / 0,25 W
- Um display LCD 16×2 I²C
- Um protoboard de 400 pontos
Biblioteca necessária
Antes de prosseguir, instale (utilizando o próprio gerenciador de bibliotecas da Arduino IDE) a biblioteca FreeRTOS by Richard Barry. Eu estou utilizando a versão 10.0.0-10. Se preferir, pode baixar a biblioteca do repositório Github oficial.
Circuito esquemático
O circuito esquemático é mostrado na figura 1.
Código-fonte completo
O código-fonte completo está abaixo. Leia com atenção os comentários nele contidos para maior compreensão do mesmo.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 |
#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, xQueue_LED; /* semaforos utilizados */ SemaphoreHandle_t xSerial_semaphore; 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 filas (queues) */ xQueue_LCD = xQueueCreate( 1, sizeof( float ) ); xQueue_LED = xQueueCreate( 1, sizeof( float ) ); /* Criação dos semaforos */ xSerial_semaphore = xSemaphoreCreateMutex(); if (xSerial_semaphore == NULL) { Serial.println("Erro: nao e possivel criar o semaforo"); while(1); /* Sem semaforo 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; UBaseType_t uxHighWaterMark; 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 as tarefas a partir de filas */ xQueueOverwrite(xQueue_LCD, (void *)&voltage); xQueueOverwrite(xQueue_LED, (void *)&voltage); /* Espera um segundo */ vTaskDelay( 1000 / portTICK_PERIOD_MS ); /* Para fins de teste de ocupação de stack, printa na serial o high water mark */ xSemaphoreTake(xSerial_semaphore, portMAX_DELAY ); uxHighWaterMark = uxTaskGetStackHighWaterMark( NULL ); Serial.print("task_sensor high water mark (words): "); Serial.println(uxHighWaterMark); Serial.println("---"); xSemaphoreGive(xSerial_semaphore); } } void task_lcd( void *pvParameters ) { (void) pvParameters; float voltage_rcv = 0.0; UBaseType_t uxHighWaterMark; while(1) { /* Espera até algo ser recebido na queue */ 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("Voltage: "); 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 */ xSemaphoreTake(xSerial_semaphore, portMAX_DELAY ); uxHighWaterMark = uxTaskGetStackHighWaterMark( NULL ); Serial.print("task_lcd high water mark (words): "); Serial.println(uxHighWaterMark); Serial.println("---"); xSemaphoreGive(xSerial_semaphore); } } void task_led( void *pvParameters ) { (void) pvParameters; float voltage_rcv = 0.0; UBaseType_t uxHighWaterMark; while(1) { /* Espera até algo ser recebido na queue */ xQueueReceive(xQueue_LED, (void *)&voltage_rcv, portMAX_DELAY); /* Uma vez recebida a informação na queue, verifica se o LED deve acender ou não */ if (voltage_rcv > LED_THRESHOLD) digitalWrite(LED_PIN, HIGH); else digitalWrite(LED_PIN, LOW); /* Para fins de teste de ocupação de stack, printa na serial o high water mark */ xSemaphoreTake(xSerial_semaphore, portMAX_DELAY ); uxHighWaterMark = uxTaskGetStackHighWaterMark( NULL ); Serial.print("task_led high water mark (words): "); Serial.println(uxHighWaterMark); Serial.println("---"); xSemaphoreGive(xSerial_semaphore); } } |
Considerações importantes
Se você for utilizar um Arduino que possui um microcontrolador com pouca memória Flash e/ou RAM (ATMEGA328, por exemplo) com FreeRTOS, evite trabalhar ou projetar sistemas com muitas tarefas. Recomenda-se isso pois pode ser que não haja memória suficiente.
Durante o desenvolvimento e testes, recomenda-se sempre avaliar o mínimo que a stack de cada tarefa atingiu (sugestão: utilize a função High Water Mark, disponibilizada pelo próprio FreeRTOS) e sempre fique de olho na memória RAM e Flash livres do microcontrolador.
Conclusão
Neste artigo, foi possível aprender o porque de utilizar um sistema operacional num sistema embarcado, conceitos de um Sistema Operacional de Tempo Real (RTOS) e a prática destes conceitos e demais paradigmas de um RTOS utilizando FreeRTOS e Arduino.
Com a base fornecida neste artigo, é possível que o leitor expanda para seus projetos o uso de um RTOS, visando maior confiabilidade, portabilidade, reaproveitamento de código e segurança.
Referências
Muito interessante. Estou fazendo um projeto de uma máquina caseira de pinball. Pesquisando no Google acabei caindo aqui. Estou vendo que poderei utilizar essa solução para implementar o jogo da máquina, para interagir com os sensores, motores e LEDs da máquina física.
Boa Pedrão!!!!
Isso me fez abrir a mente pra começar usar o freeRTOS… rs
Uma dúvida que fiquei é sobre a questão das filas.
Qual a melhor maneira de passar várias variáveis entre uma task e outra?
Imagino que usando unitariamente, como no seu exemplo, não seja a melhor maneira.
Pensei em fazer uma struct dentro da task e passar essa struct com todas variáveis para uma fila, etc.
Acha uma boa ideia, ou existe alguma outra boa maneira de fazer isso?
Parabéns pelo belo artigo.
Abraço!
Primeiramente, obrigado pelos elogios!
Sobre sua dúvida, também acredito ser a melhor forma utilizar uma struct (e colocar as informações que deseja passar nela). Só recomendo declarar essa struct (ou um tipo de dados baseado nela, ou seja, fazer um typedef) globalmente, de forma que ambas as tasks que forem usar tal struct consigam pegar o “esqueleto” dele de um só lugar.
Atenciosamente,
Pedro Bertoleti
Ah sim.
Show de bola…. é o que farei então.
Obrigado.
De nada
Olá Pedro
Parabéns pelo artigo.
Que pontos você considera na hora de decidir se um projeto deve ou não usar um RTOS?
Cesar, boa tarde.
Muito obrigado pelo elogio!
Então, essa é uma questão que depende muito de projeto pra projeto, pois pode ser que o projeto em questão não permita uso de multi-tasking (seja por limitação de memória, por questão de temporização ou porque multi-tasking seria um overkill no projeto em questão).
Assumindo que um projeto tenha hardware com memória suficiente e que tolere / suporte multi-tasking, eu pessoalmente não vejo por que não usar.
Respondi sua pergunta?
Bom dia.
Caramba, que artigo show de bola! Muito bom mesmo!
Thiago, muito obrigado!
Pedro, muito bom, gostei. Parabéns!
Ismael, muito obrigado!
Texto muito prolixo, cheio de blá blá blá desnecessário.
Show de artigo, obrigado por compartilhar seu conhecimento!
Fabio, muito obrigado
Excelente Pedro!
Deixo como referência os livros do Max Back sobre o assunto: https://www.amazon.com.br/s?i=digital-text&rh=p_27%3AMax+Back&s=relevancerank&text=Max+Back&ref=dbs_p_ebk_r00_abau_000000
Aproveitando, você já fez a implementação nos AVRs sem ser pelo Arduino? Se sim, há vantagens?
Fabio, obrigado pelo elogio!
Os livros do Max Black são muito bons mesmo! Um dos melhores em português sobre o assunto.
Respondendo sua pergunta: ainda não. Fora os Atmega usando no Arduino, eu trabalhei com FreeRTOS em microcontroladores STM32 e em alguns ARM Cortex M3 da NXP (linha LPC).
Atenciosamente,
Pedro Bertoleti
Interessantíssimo seu artigo!
Muito obrigado!
Muito bom o artigo. Fiz meu TCC aplicando o FreeRTOS no esp8266. Na época esse artigo iria ajudar muito.
André, muito obrigado