ESP32 e Arduino

ESP32 Motor de Passo MQTT: Controle de Posição com Progresso ao Vivo

Controle um motor de passo NEMA 17 com comandos de posição via MQTT usando um ESP32, a AccelStepper e um driver A4988 ou DRV8825 e publique o progresso ao vivo para que os apps saibam onde o motor está.

Motor NEMA 17 com driver A4988 e placa ESP32 em uma montagem ESP32 motor de passo 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 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.

  1. 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.
  2. Recuse movimentos absolutos até fazer o homing no firmware de produção e publique uma flag homed no JSON de progresso.
  3. Mantenha limites por software dentro do curso mecânico, mesmo depois do homing.
  4. 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.
  5. 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.
  6. 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 accel pela 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.

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.