Um controlador ESP32 de motor de passo via MQTT permite posicionar um slider de câmera, uma bomba dosadora, uma persiana ou um pequeno eixo linear de qualquer ponto da rede. Neste tutorial, você vai acionar um motor NEMA 17 por meio de um driver A4988 ou DRV8825 com a biblioteca AccelStepper, aceitar movimentos absolutos e relativos via MQTT e publicar o progresso ao vivo enquanto o motor gira. A parte difícil não é o MQTT, e sim manter os pulsos de passo suaves enquanto a rede faz o seu trabalho, e o sketch resolve isso rodando o motor em um núcleo próprio.
Comandos, Topics e Relatórios de Progresso
Motores de passo funcionam em malha aberta: o ESP32 conta os passos que envia e, desde que o motor não perca passos por travamento, essa contagem é a posição. Isso os torna ideais para controle via MQTT, porque o dispositivo consegue informar a posição com precisão sem encoder.
| Topic | Direção | Retido | Payload |
|---|---|---|---|
connectmq/demo/<id>/stepper/cmd |
App para o ESP32 | Não | Comando JSON, veja abaixo |
connectmq/demo/<id>/stepper/progress |
ESP32 para os apps | Sim | {"pos":1200,"target":3200,"pct":37,"moving":true} |
connectmq/demo/<id>/status |
ESP32 para os apps | Sim | online / offline |
| Comando | Exemplo | Efeito |
|---|---|---|
| Movimento absoluto | {"cmd":"moveTo","pos":3200} |
Move até o passo 3200 |
| Movimento relativo | {"cmd":"move","steps":-400} |
Recua 400 passos |
| Velocidade | {"cmd":"speed","max":1500,"accel":800} |
Máximo de passos/s e passos/s² |
| Parar | {"cmd":"stop"} |
Desacelera até uma parada controlada |
| Zerar | {"cmd":"zero"} |
Define a posição atual como 0 |
| Liberar | {"cmd":"disable"} |
Desliga a corrente nas bobinas |
Com um motor típico de 1,8 grau e microstepping de 1/16, uma volta tem 3200 passos. Em um fuso de 8 mm por volta, isso dá 400 passos por milímetro, uma conversão útil para colocar no seu app.
Hardware
- Placa de desenvolvimento ESP32.
- Motor de passo NEMA 17 (bipolar, quatro fios).
- Módulo driver A4988 ou DRV8825, de preferência em uma placa base com bornes de parafuso.
- Fonte de 12 V a 24 V adequada ao motor, mais um capacitor de 100 µF na entrada de alimentação do motor no driver.
| Pino do driver | Ligar em |
|---|---|
| STEP | GPIO 25 do ESP32 |
| DIR | GPIO 26 do ESP32 |
| EN | GPIO 27 do ESP32 (baixo = habilitado) |
| MS1, MS2, MS3 (A4988) / M0, M1, M2 (DRV8825) | Ligue em nível alto para microstepping de 1/16 (A4988) ou 1/32 (DRV8825) |
| VDD / GND (lógica) | 3V3 / GND do ESP32 |
| VMOT / GND | Fonte do motor |
| 1A, 1B, 2A, 2B | Bobinas do motor |
| RESET + SLEEP | Ligue um ao outro |
Ajuste o limite de corrente no trimpot do driver antes de ligar o motor para uma operação prolongada. Corrente demais superaquece o driver e o motor; corrente de menos faz perder passos. Nunca conecte nem desconecte o motor com o driver energizado, porque o pico indutivo pode destruir o driver na hora.
Bibliotecas
- PubSubClient, de Nick O'Leary.
- AccelStepper, de Mike McCauley, que gera perfis de aceleração e pulsos de passo não bloqueantes.
- ArduinoJson versão 7.
Por Que o Motor Roda em uma Tarefa Própria
A AccelStepper não usa um timer de hardware. Você precisa chamar stepper.run() com a maior frequência possível, e cada chamada emite no máximo um passo quando ele está pendente. A 2000 passos por segundo, há um passo pendente a cada meio milissegundo. Se mqtt.loop() bloquear por alguns milissegundos enquanto o Wi-Fi está ocupado, ou se uma reconexão bloquear por segundos, o motor engasga ou para no meio do movimento.
O ESP32 tem dois núcleos. O loop() do Arduino roda no núcleo 1, e a pilha Wi-Fi principalmente no núcleo 0. O sketch cria uma tarefa FreeRTOS fixada no núcleo 0, com prioridade baixa, que não faz nada além de chamar run(), enquanto loop() cuida do MQTT. Um projeto sem mutex mantém tudo simples: o lado MQTT grava pedidos "pendentes" em variáveis voláteis, e a tarefa do motor os recolhe. Para conhecer melhor esse padrão, veja tarefas FreeRTOS no ESP32 com MQTT.
O Sketch Completo ESP32 Motor de Passo MQTT
#include <WiFi.h>
#include <PubSubClient.h>
#include <AccelStepper.h>
#include <ArduinoJson.h>
const char* WIFI_SSID = "YOUR_WIFI";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const char* MQTT_HOST = "broker.connectmq.com";
const uint16_t MQTT_PORT = 1883;
const char* DEVICE_ID = "stepper-a19c44";
const String BASE = String("connectmq/demo/") + DEVICE_ID;
const int STEP_PIN = 25, DIR_PIN = 26, EN_PIN = 27;
const long SOFT_MIN = 0, SOFT_MAX = 32000; // limites de curso em passos
AccelStepper stepper(AccelStepper::DRIVER, STEP_PIN, DIR_PIN);
WiFiClient net;
PubSubClient mqtt(net);
// Pedidos do lado MQTT para a tarefa do motor
volatile bool reqMove = false, reqStop = false, reqZero = false;
volatile bool reqEnable = false, reqDisable = false;
volatile long reqTarget = 0;
volatile float reqMaxSpeed = 1000, reqAccel = 500;
volatile bool reqSpeed = true;
// Retrato do estado da tarefa do motor, para os relatórios
volatile long curPos = 0, curTarget = 0;
volatile bool moving = false;
long moveStart = 0;
unsigned long lastReport = 0, lastAttempt = 0;
bool wasMoving = false;
void motorTask(void*) {
for (;;) {
if (reqSpeed) { stepper.setMaxSpeed(reqMaxSpeed); stepper.setAcceleration(reqAccel); reqSpeed = false; }
if (reqEnable) { stepper.enableOutputs(); reqEnable = false; }
if (reqMove) { stepper.moveTo(reqTarget); reqMove = false; }
if (reqStop) { stepper.stop(); reqStop = false; } // desacelera
if (reqZero) { stepper.setCurrentPosition(0); reqZero = false; }
if (reqDisable) { stepper.stop(); stepper.setCurrentPosition(stepper.currentPosition()); stepper.disableOutputs(); reqDisable = false; }
stepper.run();
curPos = stepper.currentPosition();
curTarget = stepper.targetPosition();
moving = stepper.distanceToGo() != 0;
if (!moving) vTaskDelay(1); // cede a vez quando ocioso
}
}
void publishProgress(bool force) {
if (!force && millis() - lastReport < 250) return;
lastReport = millis();
long total = labs(curTarget - moveStart);
int pct = total == 0 ? 100 : (int)(100 * labs(curPos - moveStart) / total);
char buf[128];
snprintf(buf, sizeof(buf), "{\"pos\":%ld,\"target\":%ld,\"pct\":%d,\"moving\":%s}",
curPos, curTarget, constrain(pct, 0, 100), moving ? "true" : "false");
mqtt.publish((BASE + "/stepper/progress").c_str(), buf, true);
}
void requestMove(long target) {
target = constrain(target, SOFT_MIN, SOFT_MAX);
moveStart = curPos;
reqTarget = target;
reqEnable = true;
reqMove = true;
}
void onMessage(char* topic, byte* payload, unsigned int length) {
JsonDocument doc;
if (deserializeJson(doc, payload, length)) return;
const char* cmd = doc["cmd"] | "";
if (!strcmp(cmd, "moveTo") && doc["pos"].is<long>()) requestMove(doc["pos"].as<long>());
else if (!strcmp(cmd, "move") && doc["steps"].is<long>()) requestMove(curPos + doc["steps"].as<long>());
else if (!strcmp(cmd, "stop")) reqStop = true;
else if (!strcmp(cmd, "zero") && !moving) reqZero = true;
else if (!strcmp(cmd, "disable")) reqDisable = true;
else if (!strcmp(cmd, "speed")) {
reqMaxSpeed = constrain(doc["max"] | 1000.0f, 50.0f, 4000.0f);
reqAccel = constrain(doc["accel"] | 500.0f, 50.0f, 10000.0f);
reqSpeed = true;
} else return;
Serial.printf("cmd=%s\n", cmd);
}
bool connectMqtt() {
String status = BASE + "/status";
if (!mqtt.connect(DEVICE_ID, nullptr, nullptr, status.c_str(), 1, true, "offline")) {
Serial.printf("MQTT failed, state=%d\n", mqtt.state());
return false;
}
mqtt.publish(status.c_str(), "online", true);
mqtt.subscribe((BASE + "/stepper/cmd").c_str(), 1);
publishProgress(true);
return true;
}
void setup() {
Serial.begin(115200);
stepper.setEnablePin(EN_PIN);
stepper.setPinsInverted(false, false, true); // EN é ativo em nível baixo
stepper.disableOutputs();
disableCore0WDT(); // a tarefa do motor mantém o núcleo 0 ocupado durante os movimentos
xTaskCreatePinnedToCore(motorTask, "motor", 4096, nullptr, 1, nullptr, 0);
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
mqtt.setServer(MQTT_HOST, MQTT_PORT);
mqtt.setCallback(onMessage);
mqtt.setSocketTimeout(5);
}
void loop() {
if (WiFi.status() == WL_CONNECTED) {
if (!mqtt.connected()) {
if (millis() - lastAttempt > 5000) { lastAttempt = millis(); connectMqtt(); }
} else {
mqtt.loop();
if (moving) { publishProgress(false); wasMoving = true; }
else if (wasMoving) { publishProgress(true); wasMoving = false; } // relatório final
}
}
delay(2);
}
Alguns detalhes merecem atenção. A tarefa do motor só cede a vez com vTaskDelay(1) quando está ociosa, então, durante um movimento, ela roda a toda velocidade no núcleo 0; com prioridade 1, ela ainda deixa as tarefas de Wi-Fi, que têm prioridade maior, fazerem o seu trabalho. Como a tarefa ocupada deixa sem execução a tarefa ociosa do núcleo 0 durante movimentos longos, disableCore0WDT() desativa a verificação do watchdog da tarefa ociosa nesse núcleo; sem isso, você veria avisos do watchdog de tarefas em movimentos que duram vários segundos. Os limites por software restringem todos os destinos, então um "pos":320000 digitado errado não consegue jogar um carro contra a estrutura. E o relatório de progresso final com "moving":false é sempre enviado, o que dá aos apps um sinal claro de "movimento concluído".
Execute um Movimento e Acompanhe o Progresso
mosquitto_sub -h broker.connectmq.com -p 1883 -i watch-a19c44 -v -t "connectmq/demo/stepper-a19c44/#"
mosquitto_pub -h broker.connectmq.com -p 1883 -i cmd-a19c44 -t "connectmq/demo/stepper-a19c44/stepper/cmd" -m '{"cmd":"speed","max":1600,"accel":1200}'
mosquitto_pub -h broker.connectmq.com -p 1883 -i cmd-a19c44 -t "connectmq/demo/stepper-a19c44/stepper/cmd" -m '{"cmd":"moveTo","pos":6400}'
O assinante imprime o progresso quatro vezes por segundo até o movimento terminar:
connectmq/demo/stepper-a19c44/stepper/progress {"pos":312,"target":6400,"pct":4,"moving":true}
connectmq/demo/stepper-a19c44/stepper/progress {"pos":2101,"target":6400,"pct":32,"moving":true}
connectmq/demo/stepper-a19c44/stepper/progress {"pos":6400,"target":6400,"pct":100,"moving":false}
Envie {"cmd":"stop"} no meio do movimento, e o motor desacelera suavemente usando a aceleração configurada em vez de parar de uma vez, o que protege correias e acoplamentos. A taxa de quatro por segundo mantém você com folga abaixo das 10 mensagens por segundo permitidas por cliente no servidor MQTT público (gratuito).
Homing e Checklist de Segurança
A posição em malha aberta só é tão boa quanto o ponto de referência. Depois de ligado, o ESP32 não faz ideia de onde o carro está, então a posição informada começa em 0 onde quer que o motor esteja.
- Adicione uma chave de fim de curso e implemente uma rotina de homing: mova devagar em direção à chave, pare quando ela for acionada e depois envie
zero. - Recuse movimentos absolutos até fazer o homing no firmware de produção e publique uma flag
homedno JSON de progresso. - Mantenha limites por software dentro do curso mecânico, mesmo depois do homing.
- Desabilite as saídas quando ocioso se o eixo não precisar de torque de retenção; isso economiza energia e mantém o driver e o motor frios.
- Trate uma conexão perdida como condição de parada em qualquer coisa que possa machucar alguém. Combine o Last Will com uma lógica local que interrompa o movimento se os comandos pararem de chegar.
- Nunca dependa só do MQTT para paradas de emergência. Um botão de emergência em hardware que corte a energia do motor é o único dispositivo de segurança aceitável.
Solução de Problemas
- O motor zumbe, mas não gira. Os pares de bobinas estão ligados errado, ou a aceleração está alta demais para a carga. Confira os pares com um multímetro e reduza
accelpela metade. - O motor gira, mas perde passos em velocidades maiores. Aumente a tensão de alimentação dentro dos limites do driver, ajuste o limite de corrente ou reduza
max. - Engasgos quando chegam mensagens MQTT. O motor está rodando em
loop()em vez de na sua própria tarefa, ou a tarefa está fixada no mesmo núcleo que um trabalho pesado. - O driver esquenta muito. Limite de corrente alto demais; adicione um dissipador e reduza-o.
- A posição se desvia depois de muitos movimentos. Passos perdidos por travamentos; não há feedback, então faça o homing periodicamente.
Produção: Credenciais, TLS e Monitoramento
Dispositivos de movimento merecem credenciais privadas e criptografia. Crie um broker no portal do ConnectMQ, use o usuário, a senha e o client ID gerados e troque para WiFiClientSecure na porta 8883; o guia ESP32 MQTT com SSL/TLS cobre a configuração do certificado. Um broker MQTT gerenciado como o ConnectMQ também oferece um explorador de topics ao vivo para acompanhar stepper/progress enquanto você ajusta as velocidades e regras de alerta por limite que você pode aplicar a telemetria numérica, como a temperatura do driver, se publicá-la em um topic próprio. Para atuadores mais simples, o mesmo padrão de comando e estado aparece em controle de servo com ESP32 via MQTT, e o tutorial IoT com ESP32 é o ponto de partida se este for o seu primeiro projeto com ESP32.
Comece a Usar o ConnectMQ
Crie um broker Serverless gratuito em app.connectmq.com, sem precisar de cartão de crédito. Você recebe credenciais TLS em um minuto, um cliente no navegador para enviar comandos de movimento e uma visão ao vivo do progresso do seu motor.
Perguntas frequentes
Por que meu motor de passo engasga quando o ESP32 recebe mensagens MQTT?
A AccelStepper precisa que run() seja chamado com muita frequência. Quando ele divide o loop() com a rede, qualquer atraso causa perda de passos. Rode o motor em uma tarefa FreeRTOS separada, como mostrado acima, ou use uma biblioteca de motor de passo baseada em timer.
Posso controlar um motor 28BYJ-48 com este sketch?
Sim, com uma placa ULN2003. Construa o AccelStepper com AccelStepper::HALF4WIRE e quatro pinos (na ordem IN1, IN3, IN2, IN4) em vez de DRIVER; a parte MQTT continua igual.
Quão precisa é a posição informada?
É a contagem exata de passos que o ESP32 enviou. Ela corresponde à posição real desde que o motor nunca trave; o homing contra uma chave elimina qualquer erro acumulado.
Qual QoS devo usar para comandos de motor?
Use QoS 1 para comandos. Movimentos absolutos (moveTo) podem ser recebidos duas vezes sem problema, enquanto um movimento relativo duplicado moveria duas vezes, então prefira posições absolutas quando a entrega puder se repetir.



