Desenvolvendo uma Aplicação IoT com FreeRTOS e coreMQTT com Arduino e ESP01

Este post faz parte da série Iniciando com FreeRTOS

Neste segundo artigo sobre FreeRTOS apresento uma aplicação utilizando a biblioteca coreMQTT para se comunicar com um broker MQTT e publicar e receber mensagens. Discutiremos mais sobre tarefas e também sobre queues e task notifications, recursos importantes para compartilhamento de dados e sincronismo entre tarefas disponíveis no FreeRTOS.

Conforme indicado no artigo anterior, o hardware para este projeto consiste em uma placa Arduino UNO R3 e um módulo ESP-01. Além disso, também utilizaremos um sensor ultrassônico HC-SR04 para medir distâncias até um determinado objeto em seu caminho.

O circuito utilizado é ilustrado a seguir:

circuit_diagram

Obtendo o código-fonte

Caso você tenha seguido o primeiro artigo e ainda possui o ambiente de desenvolvimento configurado naquela ocasião, basta rodar os seguintes comandos em FreeRTOS/FreeRTOS/AVR_ATMega328P_GCC para obter o código fonte da aplicação:

$ git fetch
$ git checkout main

Caso você não esteja com o ambiente de desenvolvimento configurado, execute os seguintes comandos para clonar o código-fonte do FreeRTOS e da aplicação que discutiremos a seguir:

$ git clone -b 202411.00 https://github.com/FreeRTOS/FreeRTOS.git --recurse-submodules
$ cd FreeRTOS/FreeRTOS
$ git clone -b main https://github.com/vinRocha/ATMega328P_FreeRTOS.git AVR_ATMega328P_GCC

Após concluído, você deverá ter os seguintes arquivos presentes em FreeRTOS/FreeRTOS/AVR_ATMega328P_GCC/src:

Para compilar o app, basta ter a toolchain GCC AVR 8-Bit configurada em sua variável de ambiente $PATH e executar o comando make ou make all em AVR_ATMega328P_GCC. Para mais detalhes sobre como instalar a toolchain, veja a seção 3 do artigo anterior ¹.

Arquitetura da aplicação

Em src/main.c vemos como a aplicação está estruturada:

A aplicação consiste em 3 tarefas, 8266RX responsável por receber e separar os dados de rede da sinalização de controle do módulo ESP-01, HCSR04 responsável por controlar e obter dados do sensor de distância e MQTT responsável por conectar e manter ativa a conexão entre o MCU e o broker MQTT. As definições de cada tarefa encontram-se implementadas em src/transport_esp8266.cpp, src/hcsr04_task.c e src/mqtt_task.c respectivamente. Como possuímos 3 tarefas, precisamos apenas de 3 prioridades distintas para execução da aplicação. Discutiremos mais sobre as prioridades a seguir.

MQTT

A tarefa MQTT é baseada na demo coreMQTT without TLS ² do projeto FreeRTOS, com algumas modificações para fazer com que mensagens recebidas disparem sub-rotinas no MCU, obtendo assim uma aplicação orientada a eventos. No código fornecido, a aplicação se inscreve no tópico /home/garage/control, e caso um agente externo publique uma mensagem pré determinada neste tópico, o MCU executará uma ação. As mensagens válidas e os eventos ativados estão definidos na função prvMQTTProcessIncomingPublish em src/mqtt_task.c:

Como podemos observar, as mensagens “ON” e “OFF” controlam remotamente o estado de uma porta digital, acendendo ou apagando o LED conectado à mesma, e a mensagem “UPDATE” aciona a execução da tarefa HCSR04. Discutiremos a seguir sobre a tarefa HCSR04 e posteriormente sobre a API vTaskResume() que permite a tarefa MQTT disparar a execução da tarefa HCSR04. Após a medição do sensor de distância concluir, a tarefa MQTT envia o resultado para o tópico /home/garage/state. Uma aplicação externa inscrita neste tópico pode então receber o dado, processá-lo, e apresentá-lo ao usuário.

HCSR04

O código da tarefa HCSR04 é muito simples. Ele aguarda pela reativação da tarefa, e, após reativada, ele realiza a medição do intervalo de tempo que o sensor levou para receber o eco de uma onda de som transmitida. Esse intervalo representa o tempo que a onda ultrassônica leva para viajar do sensor até um determinado obstáculo em seu caminho, e novamente de volta ao sensor. Por tanto, ele é proporcional a 2x a distância entre o sensor e o obstáculo que refletiu a onda transmitida. Conhecendo a velocidade do som no ar (0.0343 cm/us), podemos calcular a distância entre o sensor e o obstáculo. Contudo, essa tarefa não realiza o cálculo da distância, pois nosso MCU não disponibiliza instruções de ponto flutuante e o compilador tem que implementar as operações envolvendo ponto flutuante em SW, aumentando a quantidade de processamento necessária quando comparado dispositivos que disponibilizam operações com ponto flutuante em hardware. Isso não quer dizer que utilizar aritmética com ponto flutuante se torna proibitivo em hardwares sem FPU (Float Point Unit). Isso deve ser considerado em cada projeto. Porém, como estamos lidando com uma aplicação IoT, faz sentido minimizarmos o uso de processamento e consumo de energia no MCU, deixando as operações mais intensas para serem executadas em outros dispositivos que compõem o sistema e possuem maiores capacidades de processamento. Por fim, a tarefa atualiza o valor mensurado no campo sensor_read da variável app_data recebida via o ponteiro pvParameters no momento da criação da tarefa (src/main.c:57) e então a tarefa HCSR04 notifica a tarefa MQTT para publicação deste dado.

8266RX

A tarefa 8266RX é implementada na função rxThread em src/transport_esp8266.cpp. Ela também é uma tarefa muito simples, e é apenas responsável por separar os bytes recebidos na interface serial em duas filas, a de controle e a de dados. Não irei me aprofundar neste tópico, mas ao utilizar o módulo ESP-01 para transmitir e receber dados, tanto as respostas dos comandos enviados ao ESP-01 quando dados recebidos pela rede chegam ao MCU através do mesmo canal (UART RX), e cabe a aplicação utilizando este módulo separar dados de rede da sinalização de controle. Além disso, a tarefa 8266RX é apenas parte da camada de transporte utilizada na nossa aplicação. A biblioteca coreMQTT que necessita que implementemos ao menos duas funções, uma para receber e outra para transmitir dados. Elas também estão implementadas em /src/transport_esp8266.cpp e correspondem às funções esp8266AT_recv e esp8266AT_send respectivamente.

Tarefas, Filas e Notificações

Na seção anterior, comentamos sobre as 3 tarefas que compõem nossa aplicação, mas em nenhum momento definimos exatamente o que são tarefas e como controlá-las no FreeRTOS. Faremos agora uma observação mais profunda nas tarefas e recursos de comunicação entre tarefas disponíveis no FreeRTOS.

Tarefas

Tarefa no FreeRTOS é a unidade básica de execução de código. Cada tarefa possui seu contexto de execução (registradores de CPU) e memória (stack) e uma aplicação pode ser estruturada em diversas tarefas executando alternadamente.

Uma tarefa não possui contexto sobre a execução do escalonador do kernel FreeRTOS, tão pouco sobre outras tarefas. Quando utilizado o escalonador com preempção, uma tarefa pode ser interrompida a qualquer momento para execução de uma tarefa com prioridade maior. Se duas tarefas ou mais possuírem a mesma prioridade, então, por padrão, essas tarefas irão dividir igualmente o tempo de execução alocado para cada uma delas, sendo esse tempo baseado no período do ‘tick’ ³.

As tarefas podem existir em 1 de 4 estados:

  • RUNNING
  • READY
  • BLOCKED
  • SUSPENDED

Apenas uma tarefa pode estar em execução por CPU. No nosso caso, como o MCU utilizado possui apenas 1 CPU, então apenas 1 tarefa pode ser executada por vez. Se uma tarefa poderia estar executando, mas no momento há outra tarefa de maior prioridade em execução, essa tarefa encontra-se no estado “READY”.

Caso a tarefa esteja aguardando por um evento de tempo, ou algum outro evento interno ou externo, ela encontra-se no estado “BLOCKED”, e assim permanecerá até que o evento em si ou um ‘timeout’ aconteça. Então ela passará para o estado “READY”.

Por último, uma tarefa pode entrar no estado “SUSPENDED” através da API vTaskSuspend(), e apenas sairá desse estado através da API vTaskResume().

Utilizamos esse recurso na tarefa HCSR04 para que ela fique suspensa até que a tarefa MQTT a re-ative após receber a mensagem “UPGRADE” no tópico /home/garage/control.

Tarefas são criadas (registradas) com a API xTaskCreate() ⁴ que possui protótipo:

BaseType_t xTaskCreate(TaskFunction_t pvTaskCode,
                       const char * const pcName,
                       const configSTACK_DEPTH_TYPE uxStackDepth,
                       void *pvParameters,
                       UBaseType_t uxPriority,
                       TaskHandle_t *pxCreatedTask
                      );

pvTaskCode indica o ponto de entrada de uma tarefa e este deve ser uma função com protótipo:

void task(void *pvParameters);

essa função não deve retornar, e normalmente executa algum loop infinito. Contudo, uma tarefa pode ser deletada através da API vTaskDelete().

Os outros parâmetros de xTaskCreate incluem:

  • pcName – nome para identificar a tarefa em caso de depuração.
  • uxStackDepth – tamanho da stack alocado para a tarefa.
  • pvParameters – ponteiro de parâmetros a serem passados para a tarefa.
  • uxPriority – prioridade de execução da tarefa.
  • pxCreatedTask – ponteiro para uma variável do tipo TaskHandle_t que guardará um “ID” da tarefa criada.

Para mais detalhes sobre cada parâmetro, consulte a referência 4.

O código da tarefa não precisa estar completamente definido em seu ponto de entrada, ou seja, na função task. Podemos chamar outras funções dentro de task que, por sua vez, irão executar um loop infinito. O ponto importante a se lembrar é que task jamais deve retornar.

Filas

Filas são a principal forma de comunicação entre tarefas. Elas normalmente são utilizadas como buffers FIFO (first in, first out)e também podem ser utilizadas para sincronizar tarefas.

A API xQueueCreate() é utilizada para criar filas. Seu protótipo é:

QueueHandle_t xQueueCreate(UBaseType_t uxQueueLength, UBaseType_t uxItemSize);

Para enviar dados para fila, usamos xQueueSend():

BaseType_t xQueueSend(QueueHandle_t xQueue, const void * pvItemToQueue, TickType_t xTicksToWait);

e para recebê-los xQueueReceive():

BaseType_t xQueueReceive(QueueHandle_t xQueue, void *pvBuffer, TickType_t xTicksToWait);

Lembrando que essas APIs de envio de leitura de dados não devem ser utilizadas dentro de interrupções. O FreeRTOS disponibiliza APIs específicas para se trabalhar com filas dentro de sub-rotinas de interrupção (ver xQueueSendFromISR() ⁵ e xQueueReceiveFromISR ⁶).

Caso uma tarefa queira ler uma fila que esteja vazia, essa ficará bloqueada pelo tempo determinado em xTicksToWait, dando assim oportunidade para tarefas de menor prioridade executarem. A macro pdMS_TO_TICKS(x) pode ser utilizada para converter milissegundos em ‘ticks’.

Como exemplo, a tarefa 8266RX, implementada em rxThread em src/transport_esp8266.cpp chama a função xSerialGetChar(), definida em src/drivers/serial.c

301   if(xSerialGetChar(NULL, (signed char*) &c[0], RX_BLOCK)) {

que por sua vez chama a função xQueueReceive() passando RX_BLOCK em xTicksToWait.

122    if (xQueueReceive(xRxedChars, pcRxedChar, xBlockTime)) {

ou seja, caso a fila xRxedChars esteja vazia, a tarefa 8266RX ficará bloqueada abrindo oportunidade para tarefas de menor prioridade executarem até que um byte seja recebido na UART ou que RX_BLOCK expire.

Daqui concluímos que poderíamos utilizar filas de 1 byte para sincronizar tarefas. Caso uma fila esteja cheia e uma tarefa em execução tentar escrever nela, essa tarefa ficará bloqueada, abrindo oportunidade para uma outra tarefa executar e liberar espaço na fila. O mesmo raciocínio se aplica no caso de uma tarefa tentando ler uma fila que encontra-se vazia (como no exemplo acima).

Apesar de podermos utilizar uma fila para sincronizar tarefas, há um alto custo associado a isso. Cada fila criada possui um overhead de 76 bytes + espaço utilizado para armazenar os dados na fila , ou seja, para MCUs com pouca RAM disponível, utilizar filas como semáforos ou mutex pode ser proibitivo. Esse é o nosso caso considerando que o ATMega328P possui apenas 2KB de SRAM.

Contudo, existe um mecanismo chamado notificação de tarefas que serve como alternativa para nos ajudar.

Notificações de Tarefas

Quando uma tarefa é criada, junto a ela existe uma array de notificações, de tamanho configurado pela macro configTASK_NOTIFICATION_ARRAY_ENTRIES (1 por padrão). Nessa array existe um estado, e um valor de 32-bits. Podemos então utilizar a função de notificação de tarefa para sincronização entre tarefas sem a penalidade de maior consumo de RAM, quando comparado a utilização de filas.

Isso é exatamente o que as APIs xTaskNotifyGive() ⁸ e ulTaskNotifyTake() ⁹ possibilitam.

Como exemplo, a tarefa MQTT re-ativa a tarefa HCSR04 através da API vTaskResume() e chama a API ulTaskNotifyTake() aguardando por uma notificação de tarefa antes de publicar o resultado medido pela tarefa do sensor.

A tarefa HCSR04 por sua vez, atualiza a variável que armazena o intervalo medido pelo sensor ultrassônico e envia uma notificação para tarefa MQTT através da API xTaskNotifyGive(), liberando a execução da tarefa MQTT.

Note que a API ulTaskNotifyTake recebe um valor de timeout, indicando por quanto tempo a tarefa ficará bloqueada aguardando pela notificação. Caso o timeout expire antes da tarefa receber a notificação, ela continuará sua execução normalmente. Nesse caso, sabemos que a tarefa HCSR04 deve levar menos de 5 segundos para executar e enviar uma notificação a tarefa MQTT.

Com isso terminamos nossa discussão sobre tarefas, filas e notificações e possuímos os conhecimentos necessários para criar uma aplicação multi-thread, ou melhor multi-tarefas, utilizando o kernel FreeRTOS.

Biblioteca coreMQTT

Conforme informado anteriormente, nossa aplicação utiliza a tarefa MQTT baseada na demo coreMQTT without TLS ² para implementar as funções de um cliente MQTT. A demo implementa as chamadas necessárias às funções da biblioteca coreMQTT para o correto funcionamento da aplicação e, portanto, pode ser utilizada como base para outras aplicações que desejam implementar um cliente MQTT utilizando a bibloteca coreMQTT.

A biblioteca em si é mantida no diretório FreeRTOS/FreeRTOS-Plus/Source/Application-Protocols/coreMQTT e é implementada em C, em 3 arquivos-fontes alguns headers. Os arquivos fontes são core_mqtt.c, core_mqtt_serializer.c e core_mqtt_state.c. Para utilizar a biblioteca basta incluir os arquivos fontes na lista de compilação da sua aplicação e adicionar os headers no caminho de include do compilador:

Além disso, a biblioteca necessita de uma camada de transporte para enviar e receber dados de rede. No nosso caso, utilizamos o ESP-01 como módulo Wi-Fi conectado a UART do MCU, e a abstração de rede é implementada em src/transport_esp8266.cpp. Caso outra solução fosse utilizada, poderíamos reaproveitar todo o código da tarefa, substituindo apenas as funções utilizadas para enviar e receber bytes para o broker MQTT. O FreeRTOS disponibiliza uma stack TCP utilizada na demo original. Contudo, como o ESP-01 implementa a stack TCP para nós, nos não precisamos utilizar a implementação disponibilizada no FreeRTOS.

Executando a aplicação

Para executarmos a aplicação, precisamos de um broker MQTT disponível. No meu caso, eu utilizo uma instância do mosquitto mqtt ¹⁰ rodando em minha rede local. Meu servidor está configurado para aceitar conexões sem criptografia ou autenticação, ouvindo conexões no endereço 192.168.0.235:1883.

O endereço de IP e porta TCP podem ser configurados no inicio do arquivo /src/mqtt_task.c, através das macros

#define democonfigMQTT_BROKER_ENDPOINT           "192.168.0.235"

e

#define democonfigMQTT_BROKER_PORT               "1883"

Após realizar as alterações de acordo com o seu ambiente, compile o código rodando make ou make all em FreeRTOS/FreeRTOS/AVR_ATMega328P_GCC/.

A imagem para gravação rtosdemo.hex deverá ser compilada. Caso você enfrente algum problema, reveja os passos indicados na seção 3 do artigo anterior ¹.

A camada de transporte para o ESP8266 (src/transport_esp8266.cpp) não disponibiliza uma API para configurar uma rede Wi-Fi. Como o módulo irá se conectar automaticamente na última rede configurada, partimos da premissa que essa configuração já esteja presente no ESP-01. Caso necessite de ajuda para configurar uma rede WiFi no módulo ESP-01, siga as orientações deste artigo ¹¹ da Instructables.

Agora basta gravar a imagem gerada no MCU (ver seção 5 do artigo anterior), montar o circuito ilustrado na introdução e utilizar a aplicação. Para interagir com o MCU, enviando e recebendo mensagens MQTT, eu utilizo as ferramentas de comando mosquitto_pub e mosquitto_sub respectivamente. A aplicação em execução é mostrada no vídeo a seguir:

Considerações finais

Neste artigo, discutimos o funcionamento de uma simples aplicação orientada a eventos que utiliza a biblioteca coreMQTT do FreeRTOS para receber e enviar mensagens e executar ações baseadas nas mensagens recebidas.

Discutimos sobre tarefas e mecanismos de comunicação entre tarefas e como utilizá-los para construir nossa aplicação.

Por conta desta aplicação não utilizar criptografia e autenticação do broker MQTT e não conter um mecanismo de recuperação de execução em caso de falhas (e de fato há diversos outros pontos de melhoria na aplicação fornecida), ela não é recomendada para ser utilizada em produção, mas ela serve como base para que aplicações mais seguras e apropriadas sejam desenvolvidas.

Por fim, ao completar esta demonstração, o leitor terá conhecimentos sobre utilizar os recursos disponíveis no FreeRTOS para desenvolver suas aplicações, sejam essas IoT ou não.

Referências

  1. https://embarcados.com.br/iniciando-com-freertos-como-portar-e-rodar-uma-demo-no-arduino-uno-r3/
  2. https://freertos.org/Documentation/03-Libraries/03-FreeRTOS-core/02-coreMQTT/02-Demos/01-coreMQTT-demo
  3. https://freertos.org/Documentation/02-Kernel/02-Kernel-features/01-Tasks-and-co-routines/04-Task-scheduling
  4. https://freertos.org/Documentation/02-Kernel/04-API-references/01-Task-creation/01-xTaskCreate
  5. https://freertos.org/Documentation/02-Kernel/04-API-references/06-Queues/04-xQueueSendFromISR
  6. https://freertos.org/Documentation/02-Kernel/04-API-references/06-Queues/10-xQueueReceiveFromISR
  7. https://freertos.org/Why-FreeRTOS/FAQs/Memory-usage-boot-times-context#how-much-ram-does-freertos-use
  8. https://freertos.org/Documentation/02-Kernel/04-API-references/05-Direct-to-task-notifications/01-xTaskNotifyGive
  9. https://freertos.org/Documentation/02-Kernel/04-API-references/05-Direct-to-task-notifications/03-ulTaskNotifyTake
  10. https://mosquitto.org/
  11. https://www.instructables.com/Getting-Started-With-the-ESP8266-ESP-01/
  12. https://www.youtube.com/watch?v=4Cvv-qOR2lI

Iniciando com FreeRTOS

Iniciando com FreeRTOS: Como portar e rodar uma demo no Arduino UNO R3
Licença Creative Commons Esta obra está licenciada com uma Licença Creative Commons Atribuição-CompartilhaIgual 4.0 Internacional.
Comentários:
Notificações
Notificar
1 Comentário
recentes
antigos mais votados
Inline Feedbacks
View all comments
Fábio Souza
Admin
25/02/2025 09:27

Excelente artigo, Vinícius. Parabéns!
Muito obrigado por compartilhar.

Home » Internet Das Coisas » Desenvolvendo uma Aplicação IoT com FreeRTOS e coreMQTT com Arduino e ESP01

EM DESTAQUE

WEBINARS

VEJA TAMBÉM

JUNTE-SE HOJE À COMUNIDADE EMBARCADOS

Talvez você goste: