ESP32 e Arduino

Como Corrigir Resets do Task Watchdog do ESP32 em Projetos MQTT

Por que sketches MQTT no ESP32 disparam resets do task watchdog, como ler a mensagem task_wdt e como reestruturar loops de reconexão, callbacks e código bloqueante para que o watchdog fique quieto.

Monitor serial do ESP32 mostrando um erro task_wdt em um projeto de task watchdog do ESP32 com MQTT
Rode isso no seu próprio broker MQTTTenha um broker MQTT privado e gerenciado no plano Free e comece com exemplos de código prontos.
Teste grátis Exemplos de código Sem cartão de crédito

Um reset do task watchdog do ESP32 em um projeto MQTT quase sempre significa que algum trecho de código está bloqueando uma task por mais tempo do que o watchdog permite. Este artigo mostra como ler a mensagem task_wdt, identifica os culpados de sempre em sketches MQTT e traz padrões não bloqueantes que mantêm o dispositivo conectado sem resets.

O que o task watchdog realmente faz

O ESP32 roda FreeRTOS. Mesmo em um sketch Arduino, setup() e loop() rodam dentro de uma task FreeRTOS chamada loopTask, o Wi-Fi e a pilha TCP/IP rodam em tasks próprias, e cada núcleo da CPU tem uma idle task que roda quando nada mais precisa da CPU.

O Task Watchdog Timer (TWDT) monitora um conjunto de tasks inscritas. Cada task inscrita precisa "alimentar" o watchdog periodicamente. Se uma delas deixar de fazer isso dentro do timeout, o TWDT imprime um erro e, dependendo da configuração, entra em pânico e reinicia o chip.

Quais tasks são vigiadas depende do seu ambiente:

Ambiente Vigiadas por padrão Timeout típico
ESP-IDF Idle tasks dos dois núcleos (configurável no menuconfig) 5 s
Arduino-ESP32 Idle task do núcleo 0; loopTask só se você chamar enableLoopWDT() 5 s
AsyncTCP (usado pelo AsyncMqttClient) Sua task async_tcp, quando compilado com a opção de watchdog ativada Herda o timeout do TWDT

A idle task é vigiada por um motivo indireto: se ela nunca consegue rodar, alguma outra task naquele núcleo está monopolizando a CPU sem ceder. Então uma mensagem com "IDLE0" não significa que a idle task está quebrada, e sim que outra coisa no núcleo 0 está girando sem parar.

Existe ainda um Interrupt Watchdog separado, que dispara quando as interrupções ficam desativadas por tempo demais ou uma ISR demora demais. A mensagem dele menciona "Interrupt wdt timeout" e as causas são outras, quase sempre em código de ISR. Este artigo trata do task watchdog.

Lendo a mensagem de erro

Um crash típico se parece com isto:

E (25391) task_wdt: Task watchdog got triggered. The following tasks did not reset the watchdog in time:
E (25391) task_wdt:  - IDLE0 (CPU 0)
E (25391) task_wdt: Tasks currently running:
E (25391) task_wdt: CPU 0: mqttTask
E (25391) task_wdt: CPU 1: loopTask
E (25391) task_wdt: Aborting.

Leia em duas partes. A primeira lista diz quem ficou sem CPU: aqui, a idle task do núcleo 0. A lista "currently running" diz quem estava segurando a CPU naquele momento: uma task chamada mqttTask no núcleo 0. Essa é a sua suspeita. Se a task que ficou sem CPU for async_tcp, o problema é código rodando dentro de um callback do AsyncTCP ou do AsyncMqttClient. Se for loopTask, o seu loop() bloqueou com o loop watchdog ativado.

Ative o exception decoder no monitor serial (no PlatformIO, monitor_filters = esp32_exception_decoder) para transformar os endereços do backtrace em nomes de arquivo e números de linha.

Os culpados de sempre nos crashes de task watchdog do ESP32 com MQTT

Loops de espera ativa sem ceder a CPU

O infrator clássico é um loop de espera vazio:

while (WiFi.status() != WL_CONNECTED) { }   // never yields
while (!mqtt.connect(clientId)) { }          // retries at full speed

No loop(), no núcleo 1, isso muitas vezes escapa sem reset, porque o core Arduino não vigia a idle task 1 por padrão. Mova o mesmo código para uma task fixada no núcleo 0, ou ative o loop watchdog, e ele reinicia após cinco segundos. Um delay(10) ou vTaskDelay() dentro do loop resolve a falta de CPU, mas não o design: o dispositivo continua sem conseguir fazer mais nada enquanto espera.

Chamadas de conexão bloqueantes mais longas que o timeout

O connect() do PubSubClient é bloqueante. Se o broker estiver inacessível, a conexão TCP subjacente e a espera pelo CONNACK do MQTT podem, juntas, levar mais de cinco segundos, principalmente com TLS, em que só o handshake pode levar segundos com sinal fraco. Uma função de reconexão que chama connect() várias vezes seguidas multiplica isso. Reduza o timeout do socket com mqtt.setSocketTimeout(4) e tente apenas uma vez por chamada.

Trabalho pesado dentro dos callbacks MQTT

Com o AsyncMqttClient, o onMessage roda dentro da task async_tcp. Analisar um documento JSON grande, gravar na flash, atualizar um display ou chamar delay() ali bloqueia a pilha de rede e pode disparar o watchdog dessa task. Com o PubSubClient, o callback roda dentro de mqtt.loop(), então trabalho pesado ali bloqueia o loop. Nos dois casos, copie o payload, marque uma flag ou coloque em uma fila e faça o trabalho em outro lugar.

Leituras de sensor e gravações na flash longas e bloqueantes

Algumas bibliotecas de sensores fazem polling com espera ativa, e um download OTA ou uma gravação grande na flash pode durar segundos. Isso não é problema em uma task própria que alimenta o watchdog, mas sim na mesma task que atende o MQTT.

Tempestades de reconexão após perda de rede

Quando o Wi-Fi cai, um sketch ingênuo chama WiFi.begin() e mqtt.connect() em sequência em um loop apertado. Cada chamada bloqueia um pouco, o loop nunca pausa e, mais cedo ou mais tarde, o watchdog dispara. O backoff exponencial resolve tanto o problema do watchdog quanto a carga sobre o broker.

Padrão MQTT não bloqueante para o loop()

Este sketch mantém o loop() curto o tempo todo. Cada tentativa de conexão acontece no máximo uma vez por intervalo de backoff, o callback só copia os dados e o trabalho roda depois, no loop(). Ele se conecta à área pública do broker.connectmq.com.

#include <WiFi.h>
#include <PubSubClient.h>

const char* WIFI_SSID = "YOUR_WIFI";
const char* WIFI_PASS = "YOUR_WIFI_PASSWORD";
const char* CMD_TOPIC = "connectmq/demo/b52e07/cmd";
const char* TELEMETRY_TOPIC = "connectmq/demo/b52e07/telemetry";

WiFiClient net;
PubSubClient mqtt(net);
String clientId;

volatile bool commandPending = false;
char commandBuf[128];

unsigned long nextMqttAttempt = 0;
unsigned long mqttBackoff = 1000;
unsigned long lastPublish = 0;

void onMessage(char* topic, byte* payload, unsigned int length) {
  size_t n = min((size_t)length, sizeof(commandBuf) - 1);
  memcpy(commandBuf, payload, n);
  commandBuf[n] = '\0';
  commandPending = true;          // no heavy work here
}

void handleCommand(const char* cmd) {
  Serial.printf("Handling command: %s\n", cmd);
  // slow work is fine here, as long as it stays under the watchdog timeout
}

void maintainMqtt() {
  if (WiFi.status() != WL_CONNECTED) return;   // Wi-Fi reconnects on its own
  if (mqtt.connected()) { mqtt.loop(); return; }
  if (millis() < nextMqttAttempt) return;

  if (mqtt.connect(clientId.c_str())) {
    mqtt.subscribe(CMD_TOPIC);
    mqttBackoff = 1000;
  } else {
    nextMqttAttempt = millis() + mqttBackoff;
    mqttBackoff = min(mqttBackoff * 2, 30000UL);
  }
}

void setup() {
  Serial.begin(115200);
  enableLoopWDT();                // watch loop() too, so blocking shows up in testing
  WiFi.mode(WIFI_STA);
  WiFi.setAutoReconnect(true);
  WiFi.begin(WIFI_SSID, WIFI_PASS);

  clientId = "esp32-wdt-" + String((uint32_t)ESP.getEfuseMac(), HEX);
  mqtt.setServer("broker.connectmq.com", 1883);
  mqtt.setSocketTimeout(4);       // keep each blocking connect well under 5 s
  mqtt.setCallback(onMessage);
}

void loop() {
  maintainMqtt();

  if (commandPending) {
    commandPending = false;
    handleCommand(commandBuf);
  }

  if (mqtt.connected() && millis() - lastPublish > 10000) {
    lastPublish = millis();
    char payload[64];
    snprintf(payload, sizeof(payload), "{\"uptime\":%lu,\"heap\":%u}",
             millis() / 1000, ESP.getFreeHeap());
    mqtt.publish(TELEMETRY_TOPIC, payload);
  }
}

O core Arduino alimenta o loop watchdog automaticamente toda vez que o loop() retorna, então, enquanto cada passagem pelo loop() ficar bem abaixo do timeout, ativá-lo não custa nada e pega regressões cedo. Ligá-lo durante o desenvolvimento é um jeito barato de encontrar a chamada bloqueante antes que ela chegue ao campo.

Rodando o MQTT em uma task FreeRTOS própria

Se você prefere uma task dedicada ao MQTT, inscreva-a no watchdog e alimente-o a cada iteração. A API esp_task_wdt no ESP-IDF 5 (usado pelo Arduino-ESP32 3.x) fica assim:

#include <esp_task_wdt.h>

void mqttTask(void* arg) {
  esp_task_wdt_add(NULL);         // watch this task
  for (;;) {
    maintainMqtt();               // must return quickly
    esp_task_wdt_reset();         // feed the watchdog
    vTaskDelay(pdMS_TO_TICKS(10)); // always yield
  }
}

void setup() {
  // ... Wi-Fi and mqtt setup as above ...
  xTaskCreatePinnedToCore(mqttTask, "mqttTask", 6144, NULL, 1, NULL, 1);
}

Mantenha a prioridade baixa (1 é a mesma do loopTask), a menos que tenha um motivo para aumentá-la; uma task de alta prioridade que nunca bloqueia deixa sem CPU tudo o que está abaixo dela. Fixe-a no núcleo 1 se quiser ficar longe do núcleo onde o Wi-Fi roda. Se o cliente MQTT for usado por mais de uma task, proteja-o com um mutex, porque o PubSubClient não é thread-safe. O guia de ESP32 com FreeRTOS e MQTT aborda filas e o design de tasks com mais detalhes.

Devo aumentar o timeout do watchdog?

Às vezes, mas trate isso como último recurso. Aumentar o timeout esconde o código bloqueante em vez de corrigi-lo, e o dispositivo passa a demorar mais para se recuperar de um travamento real. Casos legítimos são operações longas e inevitáveis, como uma gravação OTA na flash. No ESP-IDF 5 você pode reconfigurar o TWDT em tempo de execução:

esp_task_wdt_config_t cfg = {
  .timeout_ms = 15000,
  .idle_core_mask = (1 << 0),     // keep watching IDLE0
  .trigger_panic = true,
};
esp_task_wdt_reconfigure(&cfg);

Uma abordagem mais limpa para uma única operação longa é alimentar o watchdog de dentro dela, por exemplo em um callback de progresso do OTA, e deixar o timeout global em paz. Nunca desative o watchdog por completo em produção. Ele é a única coisa que reinicia um dispositivo travado em deadlock no campo.

Checklist de solução de problemas

  1. Leia qual task ficou sem CPU e qual estava rodando; esse par diz onde procurar.
  2. Decodifique o backtrace com o exception decoder para obter a linha exata.
  3. Procure no código loops while sem delay(), yield() ou vTaskDelay().
  4. Defina mqtt.setSocketTimeout() em alguns segundos e remova loops de novas tentativas em volta de connect().
  5. Tire a análise de JSON, as gravações na flash e as atualizações de display de dentro dos callbacks MQTT.
  6. Adicione backoff exponencial às reconexões de Wi-Fi e MQTT.
  7. Ative enableLoopWDT() durante os testes para que bloqueios no loop() apareçam cedo.
  8. Acompanhe o uptime do dispositivo via MQTT: um contador que vive zerando indica reboot por watchdog ou brownout.

Publicar uptime e esp_reset_reason() após cada boot é um hábito simples que transforma resets invisíveis em dados. Muitos problemas de conexão que parecem ser de watchdog são, no fundo, problemas de rede; se o dispositivo nunca chega a conectar, resolva primeiro por que o ESP32 não conecta ao broker MQTT e, para a estratégia de reconexão, veja o guia de reconexão MQTT no ESP32.

Monitore seus dispositivos no ConnectMQ

O sketch de teste acima roda contra o servidor MQTT público (gratuito) do ConnectMQ, perfeito para reproduzir um reset na sua bancada. Para uma frota, um broker MQTT gerenciado privado do ConnectMQ permite acompanhar a telemetria de cada dispositivo no explorador de tópicos ao vivo e definir regras de alerta por limite em valores como heap livre, para que um dispositivo que começa a reiniciar se destaque rapidamente. Você pode criar um no plano Serverless gratuito em app.connectmq.com, sem cartão de crédito. Se ainda está decidindo onde hospedar o broker, o guia de broker MQTT na nuvem compara as opções, e o tutorial de IoT com ESP32 mostra um projeto de dispositivo não bloqueante completo, de ponta a ponta.

Perguntas frequentes

O que significa "Task watchdog got triggered" com IDLE0?

Uma task rodando no núcleo 0 manteve a CPU ocupada por mais tempo que o timeout sem ceder, então a idle task nunca rodou. A linha "Tasks currently running" aponta o provável culpado.

O delay() reinicia o watchdog?

O delay() cede a CPU a outras tasks, o que deixa a idle task rodar, então evita a falta de CPU da IDLE. Ele não alimenta uma task que você inscreveu com esp_task_wdt_add(); essa task precisa chamar esp_task_wdt_reset() por conta própria.

Por que o AsyncMqttClient dispara o watchdog do async_tcp?

Os callbacks dele rodam dentro da task async_tcp. Trabalho demorado ali bloqueia a task, então mantenha os callbacks curtos e passe o payload para o loop() ou para outra task por meio de uma flag ou fila.

É seguro desativar o task watchdog?

Não em produção. Ele é o mecanismo que recupera um dispositivo de um travamento. Corrija o código bloqueante e só aumente o timeout para operações genuinamente longas.

Artigos relacionados

Exemplo de MQTT com ESP-IDF: Monte um Cliente esp-mqtt do Zero

Monte um projeto ESP-IDF a partir de uma pasta vazia com o componente esp-mqtt: configuração do Wi-Fi em modo station, o handler de eventos MQTT, publicação e inscrição, propriedades do MQTT 5 e TLS com o bundle de certificados.

8 min de leitura

Usando os Recursos do MQTT 5 do esp-mqtt no ESP32 com ESP-IDF

Um projeto ESP-IDF prático que ativa o MQTT 5.0 no esp-mqtt e usa user properties, expiração de mensagens, topic aliases, expiração de sessão e response topics com um broker MQTT 5 real.

8 min de leitura

Todos os artigos →

Seu broker MQTT privado está a poucos cliques de distância

Crie uma conta, receba as credenciais do seu broker e conecte seu primeiro dispositivo — sem servidores para instalar, atualizar ou escalar.