ESP32 et Arduino

Moteur pas à pas ESP32 MQTT : contrôle de position avec suivi de progression en direct

Pilotez un moteur pas à pas NEMA 17 par des commandes de position MQTT avec un ESP32, AccelStepper et un driver A4988 ou DRV8825, et publiez la progression en direct pour que les applications sachent où se trouve le moteur.

Moteur NEMA 17 avec un driver A4988 et une carte ESP32 dans un montage moteur pas à pas ESP32 MQTT
Essayez-le sur votre propre broker MQTTObtenez un broker MQTT privé et managé avec l’offre Free et partez d’exemples de code prêts à l’emploi.
Essayer gratuitement Exemples de code Aucune carte bancaire requise

Un contrôleur de moteur pas à pas ESP32 MQTT vous permet de positionner un slider de caméra, une pompe doseuse, un store ou un petit axe linéaire depuis n'importe quel point du réseau. Dans ce tutoriel, vous allez piloter un moteur NEMA 17 via un driver A4988 ou DRV8825 avec la bibliothèque AccelStepper, accepter des déplacements absolus et relatifs via MQTT, et publier la progression en direct pendant que le moteur tourne. La difficulté ne réside pas dans MQTT, mais dans le maintien d'impulsions de pas régulières pendant que le réseau travaille ; le sketch résout ce problème en faisant tourner le moteur sur son propre cœur.

Commandes, topics et rapports de progression

Les moteurs pas à pas fonctionnent en boucle ouverte : l'ESP32 compte les pas qu'il envoie et, tant que le moteur ne décroche pas, ce compte correspond à la position. Ils sont donc idéaux pour un pilotage MQTT, car l'appareil peut rapporter sa position avec précision sans codeur.

Topic Sens Retained Payload
connectmq/demo/<id>/stepper/cmd Application vers ESP32 Non Commande JSON, voir ci-dessous
connectmq/demo/<id>/stepper/progress ESP32 vers applications Oui {"pos":1200,"target":3200,"pct":37,"moving":true}
connectmq/demo/<id>/status ESP32 vers applications Oui online / offline
Commande Exemple Effet
Déplacement absolu {"cmd":"moveTo","pos":3200} Aller au pas 3200
Déplacement relatif {"cmd":"move","steps":-400} Reculer de 400 pas
Vitesse {"cmd":"speed","max":1500,"accel":800} Pas/s maximum et pas/s²
Arrêt {"cmd":"stop"} Décélérer jusqu'à un arrêt contrôlé
Zéro {"cmd":"zero"} Définir la position actuelle comme 0
Libération {"cmd":"disable"} Couper le courant dans les bobines

Avec un moteur classique de 1,8 degré et un micro-pas de 1/16, un tour complet représente 3200 pas. Sur une vis-mère de 8 mm par tour, cela donne 400 pas par millimètre, une conversion utile à intégrer dans votre application.

Matériel

  • Une carte de développement ESP32.
  • Un moteur pas à pas NEMA 17 (bipolaire, quatre fils).
  • Un module driver A4988 ou DRV8825, idéalement sur une carte support avec borniers à vis.
  • Une alimentation de 12 V à 24 V adaptée au moteur, plus un condensateur de 100 µF sur l'entrée d'alimentation moteur du driver.
Broche du driver À relier à
STEP GPIO 25 de l'ESP32
DIR GPIO 26 de l'ESP32
EN GPIO 27 de l'ESP32 (bas = activé)
MS1, MS2, MS3 (A4988) / M0, M1, M2 (DRV8825) À relier au niveau haut pour un micro-pas de 1/16 (A4988) ou 1/32 (DRV8825)
VDD / GND (logique) 3V3 / GND de l'ESP32
VMOT / GND Alimentation du moteur
1A, 1B, 2A, 2B Bobines du moteur
RESET + SLEEP À relier ensemble

Réglez la limite de courant avec le potentiomètre ajustable du driver avant de faire tourner le moteur longtemps. Trop de courant fait surchauffer le driver et le moteur ; trop peu fait perdre des pas. Ne branchez ni ne débranchez jamais le moteur lorsque le driver est sous tension : le pic inductif peut détruire le driver instantanément.

Bibliothèques

  • PubSubClient de Nick O'Leary.
  • AccelStepper de Mike McCauley, qui génère des profils d'accélération et des impulsions de pas non bloquantes.
  • ArduinoJson version 7.

Pourquoi le moteur tourne dans sa propre tâche

AccelStepper n'utilise pas de timer matériel. Vous devez appeler stepper.run() aussi souvent que possible, et chaque appel émet au plus un pas lorsqu'un pas est dû. À 2000 pas par seconde, un pas est dû toutes les demi-millisecondes. Si mqtt.loop() bloque quelques millisecondes pendant que le Wi-Fi est occupé, ou si une reconnexion bloque plusieurs secondes, le moteur saccade ou s'arrête en plein mouvement.

L'ESP32 possède deux cœurs. Le loop() d'Arduino tourne sur le cœur 1, et la pile Wi-Fi principalement sur le cœur 0. Le sketch crée une tâche FreeRTOS épinglée au cœur 0, à faible priorité, qui ne fait qu'appeler run(), tandis que loop() gère MQTT. Une conception sans mutex garde les choses simples : le côté MQTT écrit des requêtes « en attente » dans des variables volatiles, et la tâche moteur les récupère. Pour approfondir ce schéma, consultez les tâches FreeRTOS de l'ESP32 avec MQTT.

Le sketch complet moteur pas à pas ESP32 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 course en pas

AccelStepper stepper(AccelStepper::DRIVER, STEP_PIN, DIR_PIN);
WiFiClient net;
PubSubClient mqtt(net);

// Requêtes du côté MQTT vers la tâche moteur
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;

// Instantané fourni par la tâche moteur pour les rapports
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; }          // décélère
    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);                        // céder la main au repos
  }
}

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 est actif à l'état bas
  stepper.disableOutputs();
  disableCore0WDT();                                  // la tâche moteur occupe le cœur 0 pendant les déplacements
  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; }   // rapport final
    }
  }
  delay(2);
}

Quelques détails méritent d'être soulignés. La tâche moteur ne cède la main avec vTaskDelay(1) qu'au repos : pendant un déplacement, elle tourne donc à plein régime sur le cœur 0 ; avec la priorité 1, elle laisse néanmoins travailler les tâches Wi-Fi, qui ont une priorité plus élevée. Comme cette tâche très occupée prive la tâche idle du cœur 0 de temps d'exécution pendant les longs déplacements, disableCore0WDT() désactive la surveillance watchdog de la tâche idle pour ce cœur ; sans cela, vous verriez des avertissements du watchdog de tâche sur les déplacements de plusieurs secondes. Les limites logicielles bornent chaque cible : un "pos":320000 mal saisi ne peut donc pas envoyer un chariot dans le châssis. Enfin, le rapport de progression final avec "moving":false est toujours envoyé, ce qui donne aux applications un signal clair de « déplacement terminé ».

Lancer un déplacement et suivre la progression

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}'

L'abonné affiche la progression quatre fois par seconde jusqu'à la fin du déplacement :

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}

Envoyez {"cmd":"stop"} en plein déplacement : le moteur décélère en douceur selon l'accélération configurée au lieu de s'arrêter net, ce qui protège les courroies et les accouplements. La cadence de quatre messages par seconde vous maintient confortablement sous les 10 messages par seconde autorisés par client sur le serveur MQTT public gratuit.

Prise d'origine et liste de sécurité

En boucle ouverte, la position ne vaut que ce que vaut le point de référence. Après la mise sous tension, l'ESP32 ignore où se trouve le chariot : la position rapportée démarre donc à 0, où que se trouve le moteur.

  1. Ajoutez un fin de course et implémentez une routine de prise d'origine : avancez lentement vers le contact, arrêtez-vous quand il se déclenche, puis envoyez zero.
  2. Refusez les déplacements absolus tant que l'origine n'est pas prise dans le firmware de production, et publiez un indicateur homed dans le JSON de progression.
  3. Conservez des limites logicielles à l'intérieur de la course mécanique, même après la prise d'origine.
  4. Désactivez les sorties au repos si l'axe n'a pas besoin de couple de maintien ; cela économise de l'énergie et garde le driver et le moteur au frais.
  5. Considérez une perte de connexion comme une condition d'arrêt pour tout ce qui pourrait blesser quelqu'un. Combinez le Last Will avec une logique locale qui stoppe le mouvement si les commandes cessent d'arriver.
  6. Ne comptez jamais uniquement sur MQTT pour les arrêts d'urgence. Un arrêt d'urgence matériel qui coupe l'alimentation du moteur est le seul dispositif de sécurité acceptable.

Dépannage

  • Le moteur ronronne mais ne tourne pas. Les paires de bobines sont mal câblées, ou l'accélération est trop élevée pour la charge. Vérifiez les paires au multimètre et divisez accel par deux.
  • Le moteur tourne mais perd des pas à vitesse élevée. Augmentez la tension d'alimentation dans les limites du driver, ajustez la limite de courant ou réduisez max.
  • Saccades à l'arrivée des messages MQTT. Le moteur tourne dans loop() au lieu de sa propre tâche, ou la tâche est épinglée sur le même cœur qu'un traitement lourd.
  • Le driver chauffe beaucoup. Limite de courant trop élevée ; ajoutez un dissipateur et réduisez-la.
  • La position dérive après de nombreux déplacements. Des pas perdus lors de décrochages ; il n'y a aucun retour, donc reprenez l'origine périodiquement.

Production : identifiants, TLS et supervision

Les appareils de mouvement méritent des identifiants privés et du chiffrement. Créez un broker dans le portail ConnectMQ, utilisez le nom d'utilisateur, le mot de passe et le client ID générés, et passez à WiFiClientSecure sur le port 8883 ; le guide ESP32 MQTT sur SSL/TLS couvre la configuration des certificats. Un broker MQTT managé comme ConnectMQ vous offre aussi un explorateur de topics en direct pour suivre stepper/progress pendant que vous ajustez les vitesses, ainsi que des règles d'alerte de seuil applicables à une télémétrie numérique comme la température du driver, si vous la publiez sur son propre topic. Pour des actionneurs plus simples, le même schéma commande/état figure dans le pilotage d'un servomoteur ESP32 via MQTT, et le tutoriel IoT ESP32 est le point de départ si c'est votre premier projet ESP32.

Démarrer avec ConnectMQ

Créez un broker Serverless gratuit sur app.connectmq.com, sans carte bancaire. Vous obtenez des identifiants TLS en une minute, un client dans le navigateur pour envoyer des commandes de déplacement et une vue en direct de la progression de votre moteur.

Questions fréquentes

Pourquoi mon moteur pas à pas saccade-t-il quand l'ESP32 reçoit des messages MQTT ?

AccelStepper a besoin que run() soit appelé très fréquemment. Lorsqu'il partage loop() avec la partie réseau, le moindre délai fait manquer des pas. Faites tourner le moteur dans une tâche FreeRTOS séparée, comme ci-dessus, ou utilisez une bibliothèque de moteur pas à pas basée sur un timer.

Puis-je piloter un moteur 28BYJ-48 avec ce sketch ?

Oui, avec une carte ULN2003. Construisez AccelStepper avec AccelStepper::HALF4WIRE et quatre broches (dans l'ordre IN1, IN3, IN2, IN4) au lieu de DRIVER ; la partie MQTT reste identique.

Quelle est la précision de la position rapportée ?

C'est le nombre exact de pas envoyés par l'ESP32. Il correspond à la position réelle tant que le moteur ne décroche jamais ; une prise d'origine sur un contact élimine toute erreur accumulée.

Quel QoS utiliser pour les commandes moteur ?

Utilisez le QoS 1 pour les commandes. Les déplacements absolus (moveTo) peuvent être reçus deux fois sans risque, alors qu'un déplacement relatif dupliqué se ferait deux fois : préférez donc les positions absolues lorsque la livraison peut être répétée.

Articles liés

Exemple MQTT ESP-IDF : construire un client esp-mqtt à partir de zéro

Construisez un projet ESP-IDF à partir d'un dossier vide avec le composant esp-mqtt : configuration du Wi-Fi en mode station, gestionnaire d'événements MQTT, publication et abonnement, propriétés MQTT 5 et TLS avec le bundle de certificats.

9 min de lecture

Horodatage NTP ESP32 MQTT : ajouter une heure précise à chaque message

Synchronisez l'horloge de l'ESP32 avec NTP, formatez des horodatages ISO 8601 et epoch en millisecondes, et joignez-les aux payloads MQTT pour que les mesures conservent leur véritable heure de capture même lorsqu'elles sont publiées en retard.

8 min de lecture

Tous les articles →

Votre broker MQTT privé, à quelques clics

Créez un compte, récupérez les identifiants de votre broker et connectez votre premier appareil — aucun serveur à installer, mettre à jour ou dimensionner.