Um projeto ESP32 FreeRTOS MQTT coloca a amostragem do sensor e a publicação na rede em tarefas separadas, para que uma reconexão lenta do Wi-Fi nunca atrase uma leitura. Neste tutorial, você vai montar um sketch com uma tarefa de sensor, uma tarefa MQTT e uma fila entre elas, testá-lo na área pública gratuita do broker.connectmq.com e depois passá-lo para TLS com suas próprias credenciais.
Por que usar tarefas FreeRTOS para MQTT no ESP32
A maioria dos sketches de iniciante faz tudo dentro do loop(): lê o sensor, verifica o Wi-Fi, chama client.loop(), publica e depois delay(). Isso funciona até a rede se comportar mal. Uma chamada connect() para um broker inacessível pode bloquear por vários segundos e, nesse tempo, nada é amostrado. Se você está contando pulsos, tirando a média de um sinal analógico ou monitorando um limite, essas lacunas importam.
O core Arduino do ESP32 já roda sobre o FreeRTOS. O próprio loop() é apenas uma tarefa fixada no core 1. Nada impede que você crie mais tarefas e, ao fazer isso, o tempo de um trabalho deixa de depender do outro:
- A tarefa de sensor acorda em um período fixo com
vTaskDelayUntil(), lê o hardware e coloca uma pequena struct em uma fila. Ela nunca toca na rede. - A tarefa MQTT é dona do Wi-Fi e do cliente MQTT. Ela esvazia a fila, publica, cuida dos keep-alives e reconecta quando necessário.
- A fila é o único objeto compartilhado. As filas do FreeRTOS copiam os dados e são seguras para uso entre tarefas, então você não precisa de mutex para as leituras.
Há uma regra que torna esse design robusto: apenas uma tarefa chama o cliente MQTT. O PubSubClient não é thread-safe, e chamar publish() de duas tarefas ao mesmo tempo corrompe seu buffer interno. Em vez disso, canalize tudo pela fila.
O que você precisa
- Qualquer placa ESP32 com dois núcleos (ESP32 DevKit, ESP32-S3). Chips de um núcleo, como ESP32-C3 ou ESP32-C6, também funcionam se você fixar as duas tarefas no core 0.
- Arduino IDE 2.x ou PlatformIO com o core esp32 3.x.
- A biblioteca PubSubClient de Nick O'Leary.
- Opcional: qualquer sensor. O sketch simula temperatura e umidade, então roda em uma placa sem nada conectado.
mosquitto_subno seu computador para testes.
Organização e prioridades das tarefas
| Tarefa | Core | Prioridade | Stack | Função |
|---|---|---|---|---|
sensorTask |
1 | 3 | 3072 bytes | Amostrar a cada 1 s, enviar para a fila |
mqttTask |
0 | 2 | 6144 bytes | Wi-Fi, conexão MQTT, publicação, keep-alive |
loop() do Arduino |
1 | 1 | padrão | Excluída, não é necessária |
A tarefa de sensor recebe a prioridade mais alta porque é curta e crítica em tempo; ela roda, enfileira um item e dorme. A tarefa MQTT roda no core 0 junto com a pilha Wi-Fi, que também fica lá, e faz o trabalho lento. No core Arduino do ESP32, os tamanhos de stack são informados em bytes, e não em words como no FreeRTOS original.
O sketch ESP32 FreeRTOS MQTT completo
Substitua as credenciais de Wi-Fi e a parte aleatória do prefixo de topic, depois faça o upload.
#include <Arduino.h>
#include <WiFi.h>
#include <PubSubClient.h>
// ---------- configuração ----------
const char* WIFI_SSID = "YOUR_WIFI_SSID";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const char* MQTT_HOST = "broker.connectmq.com";
const uint16_t MQTT_PORT = 1883;
const char* TOPIC_PREFIX = "connectmq/demo/k3v9q1"; // torne isto único
// ---------- objetos compartilhados ----------
struct Reading {
uint32_t seq;
uint32_t uptimeMs;
float temperature;
float humidity;
};
QueueHandle_t readingQueue;
WiFiClient net;
PubSubClient mqtt(net);
String clientId;
// ---------- tarefa de sensor ----------
void sensorTask(void* param) {
const TickType_t period = pdMS_TO_TICKS(1000);
TickType_t lastWake = xTaskGetTickCount();
uint32_t seq = 0;
for (;;) {
Reading r;
r.seq = seq++;
r.uptimeMs = millis();
// Substitua pela leitura de um sensor real (DHT22, BME280, ADC...)
r.temperature = 21.0f + (esp_random() % 400) / 100.0f;
r.humidity = 45.0f + (esp_random() % 1000) / 100.0f;
// Não bloquear: se a fila estiver cheia, descarta o item mais antigo e tenta de novo
if (xQueueSend(readingQueue, &r, 0) != pdTRUE) {
Reading discarded;
xQueueReceive(readingQueue, &discarded, 0);
xQueueSend(readingQueue, &r, 0);
}
vTaskDelayUntil(&lastWake, period);
}
}
// ---------- auxiliares MQTT (chamados apenas pela mqttTask) ----------
bool ensureWifi() {
if (WiFi.status() == WL_CONNECTED) return true;
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
for (int i = 0; i < 40 && WiFi.status() != WL_CONNECTED; i++) {
vTaskDelay(pdMS_TO_TICKS(250));
}
return WiFi.status() == WL_CONNECTED;
}
bool ensureMqtt() {
if (mqtt.connected()) return true;
String willTopic = String(TOPIC_PREFIX) + "/status";
bool ok = mqtt.connect(clientId.c_str(), willTopic.c_str(), 1, true, "offline");
if (ok) {
mqtt.publish(willTopic.c_str(), "online", true);
}
return ok;
}
// ---------- tarefa MQTT ----------
void mqttTask(void* param) {
mqtt.setServer(MQTT_HOST, MQTT_PORT);
mqtt.setBufferSize(512);
String topic = String(TOPIC_PREFIX) + "/telemetry";
uint32_t backoffMs = 1000;
for (;;) {
if (!ensureWifi() || !ensureMqtt()) {
vTaskDelay(pdMS_TO_TICKS(backoffMs));
backoffMs = min<uint32_t>(backoffMs * 2, 30000);
continue;
}
backoffMs = 1000;
mqtt.loop();
Reading r;
// Espera brevemente por dados para que a tarefa libere a CPU quando ociosa
if (xQueueReceive(readingQueue, &r, pdMS_TO_TICKS(50)) == pdTRUE) {
char payload[160];
snprintf(payload, sizeof(payload),
"{\"seq\":%lu,\"uptimeMs\":%lu,\"temperature\":%.2f,\"humidity\":%.2f,\"queued\":%u}",
(unsigned long)r.seq, (unsigned long)r.uptimeMs,
r.temperature, r.humidity,
(unsigned)uxQueueMessagesWaiting(readingQueue));
if (!mqtt.publish(topic.c_str(), payload)) {
// Devolve ao início da fila e tenta de novo após reconectar
xQueueSendToFront(readingQueue, &r, 0);
}
}
}
}
void setup() {
Serial.begin(115200);
clientId = "esp32-rtos-" + String((uint32_t)ESP.getEfuseMac(), HEX);
readingQueue = xQueueCreate(60, sizeof(Reading)); // um minuto de buffer
if (readingQueue == nullptr) {
Serial.println("Queue allocation failed");
while (true) delay(1000);
}
xTaskCreatePinnedToCore(sensorTask, "sensor", 3072, nullptr, 3, nullptr, 1);
xTaskCreatePinnedToCore(mqttTask, "mqtt", 6144, nullptr, 2, nullptr, 0);
}
void loop() {
vTaskDelete(nullptr); // a tarefa loop do Arduino não é necessária
}
Acompanhe a saída pelo seu computador:
mosquitto_sub -h broker.connectmq.com -p 1883 -i sub-k3v9q1 -t "connectmq/demo/k3v9q1/#" -v
Você deve ver uma mensagem status com online e uma mensagem de telemetria por segundo. Agora desligue o roteador por vinte segundos. O campo queued cresce enquanto a placa está offline e, quando a conexão volta, a tarefa MQTT esvazia o acúmulo em ordem. Esse é o objetivo do design: as leituras foram feitas no horário certo, mesmo com a publicação parada.
A área pública do broker pode ser lida por qualquer pessoa, então mantenha os dados de demonstração genéricos. Ela também limita cada cliente a 10 mensagens por segundo, o que o esvaziamento de um acúmulo pode atingir. Se você armazena muitas leituras, adicione um breve vTaskDelay(pdMS_TO_TICKS(120)) após cada publicação enquanto uxQueueMessagesWaiting() estiver alto. O guia do servidor MQTT público (gratuito) lista os outros limites.
Como as peças funcionam
A fila como buffer
xQueueCreate(60, sizeof(Reading)) aloca 60 slots de tamanho fixo no heap. Cada xQueueSend copia a struct de 16 bytes, então a tarefa de sensor pode reutilizar sua variável local imediatamente. Dimensionar a fila é um equilíbrio entre RAM e a duração da queda que você quer suportar. Sessenta slots de 16 bytes ocupam menos de 1 KB; até alguns milhares de slots cabem com facilidade em um ESP32 clássico, mas lembre-se de que os dados se perdem em uma reinicialização.
Quando a fila está cheia, o sketch descarta a leitura mais antiga, e não a mais nova. Para telemetria, o valor mais recente costuma ser o mais útil. Se cada amostra importa, bloqueie por alguns ticks ou agregue as leituras (mínimo, máximo, média) antes de enfileirar.
vTaskDelayUntil versus delay
vTaskDelayUntil() acorda a tarefa em uma taxa fixa medida a partir do último despertar, então o período não deriva pelo tempo que a leitura do sensor levou. Um delay(1000) simples depois de uma leitura de 30 ms resulta em um período de 1,03 s que vai escorregando aos poucos. As duas funções liberam a CPU, ao contrário de um loop ocupado com millis().
Reconexão com backoff
A tarefa MQTT tenta novamente com backoff exponencial limitado a 30 segundos. Bombardear o broker com tentativas imediatas desperdiça energia e, em um broker MQTT gerenciado, pode acionar a proteção contra flood ou o bloqueio de IP por falhas repetidas. O guia de Last Will MQTT no ESP32 explica por que a mensagem will offline retida é registrada no momento da conexão, para que os dashboards percebam um dispositivo inativo.
Armadilhas e solução de problemas
- Guru Meditation "Stack canary watchpoint triggered". O stack da tarefa é pequeno demais.
snprintfcom floats e TLS precisam de folga. ImprimauxTaskGetStackHighWaterMark(nullptr)dentro de cada tarefa durante o desenvolvimento; ele retorna o mínimo de bytes livres já observado. Mantenha pelo menos 512 bytes de sobra. - Resets do task watchdog. Uma tarefa que roda em loop sem nunca bloquear priva a tarefa idle do seu core. Toda iteração precisa chamar algo que ceda a CPU:
vTaskDelay,xQueueReceivecom timeout ou uma chamada de socket bloqueante. - Saída serial embaralhada. Duas tarefas imprimindo ao mesmo tempo intercalam caracteres. Imprima de uma única tarefa ou proteja o
Serialcom um mutex criado porxSemaphoreCreateMutex(). - Publicar a partir da tarefa de sensor. Vai parecer que funciona e depois falhar aleatoriamente sob carga. Mantenha o cliente MQTT em uma única tarefa.
- Callbacks rodam na tarefa MQTT. Se você assina comandos, o callback do PubSubClient roda dentro de
mqtt.loop(). Não faça trabalho lento ali; coloque o comando em uma segunda fila para outra tarefa tratar. - JSON grande truncado. O buffer padrão do PubSubClient é de 256 bytes, incluindo o topic. O sketch o aumenta com
setBufferSize(512).
Migrando para TLS e seu próprio broker
O servidor MQTT público é ideal para testes e protótipos, mas dispositivos em produção devem autenticar e criptografar. Crie uma conta gratuita em app.connectmq.com, crie um broker e copie o usuário, a senha e o client ID gerados. Depois, altere apenas a parte de rede da tarefa MQTT:
#include <WiFiClientSecure.h>
// ISRG Root X1, a raiz do Let's Encrypt. Cole o PEM publicado pelo Let's Encrypt.
static const char ROOT_CA[] = R"PEM(
-----BEGIN CERTIFICATE-----
...paste ISRG Root X1 here...
-----END CERTIFICATE-----
)PEM";
WiFiClientSecure net;
PubSubClient mqtt(net);
// na mqttTask, antes do loop:
net.setCACert(ROOT_CA);
mqtt.setServer("broker.connectmq.com", 8883);
// em ensureMqtt():
mqtt.connect("YOUR_CLIENT_ID", "YOUR_USERNAME", "YOUR_PASSWORD",
willTopic.c_str(), 1, true, "offline");
Aumente o stack da tarefa MQTT para cerca de 8192 bytes, porque o handshake TLS usa bem mais stack e heap. No ConnectMQ, o client ID fica vinculado às credenciais emitidas, então use exatamente o que o portal fornecer. Quando os dados estiverem fluindo, abra o explorador de topics ao vivo no portal para confirmar que as mensagens chegam e crie uma regra de alerta por limite no campo de temperatura.
O FreeRTOS no ESP32 é documentado em detalhes na referência FreeRTOS do ESP-IDF, incluindo as diferenças de SMP em relação ao FreeRTOS original. Para uma visão mais ampla de projetos com dispositivos, o tutorial de IoT com ESP32 reúne o restante desta série, e adicionar timestamps NTP à struct Reading é um próximo passo natural para que as amostras armazenadas carreguem o horário real de captura.
Comece com o ConnectMQ
Um sketch baseado em tarefas é apenas metade de um sistema confiável; a outra metade é um broker que permanece no ar enquanto seus dispositivos reconectam. O ConnectMQ é um broker MQTT gerenciado com MQTT 3.1.1 e 5.0, TLS na porta 8883 e um portal que mostra seus dispositivos e topics ao vivo. Comece no plano gratuito Serverless sem cartão de crédito e depois migre para um plano pago ou dedicado quando sua frota passar de 25 conexões. Com o firmware estável, o mesmo padrão funciona para sensores reais, como no projeto ESP32 com DHT22.
Perguntas frequentes
Posso chamar o PubSubClient de mais de uma tarefa FreeRTOS?
Não com segurança. O PubSubClient tem um único buffer compartilhado e nenhum bloqueio, então chamadas concorrentes corrompem os pacotes. Mantenha-o em uma tarefa e envie dados a ela por meio de uma fila.
Em qual core a tarefa MQTT deve rodar?
O core 0 é um bom padrão, porque as tarefas de Wi-Fi e lwIP já rodam nele. Em chips de um núcleo como ESP32-C3 ou ESP32-C6, fixe todas as tarefas no core 0 ou use xTaskCreate sem fixação.
Qual deve ser o tamanho da fila de leituras?
Dimensione-a para a maior queda que você quer cobrir. Multiplique a taxa de amostragem pela duração da queda e pelo tamanho da struct, e mantenha o total bem abaixo do heap livre informado por ESP.getFreeHeap().
Isso funciona com o AsyncMqttClient?
Sim, mas o AsyncMqttClient já roda sua própria tarefa de rede e callbacks, então você só precisa da tarefa de sensor e de uma fila. Publique a partir da tarefa que esvazia a fila, e não de dentro dos callbacks da biblioteca.



