Mit einer ESP32-Schrittmotor-MQTT-Steuerung positionieren Sie einen Kameraslider, eine Dosierpumpe, eine Jalousie oder eine kleine Linearachse von überall im Netzwerk. In diesem Tutorial treiben Sie einen NEMA-17-Motor über einen A4988- oder DRV8825-Treiber mit der Bibliothek AccelStepper an, nehmen absolute und relative Fahrbefehle per MQTT entgegen und veröffentlichen den Fortschritt live, während der Motor läuft. Die eigentliche Herausforderung ist nicht MQTT, sondern gleichmäßige Schrittpulse, während das Netzwerk seine Arbeit erledigt. Der Sketch löst das, indem der Motor auf einem eigenen Kern läuft.
Befehle, Topics und Fortschrittsmeldungen
Schrittmotoren arbeiten im offenen Regelkreis: Der ESP32 zählt die gesendeten Schritte, und solange der Motor keine Schritte verliert, ist dieser Zählerstand die Position. Das macht sie ideal für die MQTT-Steuerung, denn das Gerät kann die Position ohne Encoder genau melden.
| Topic | Richtung | Retained | Payload |
|---|---|---|---|
connectmq/demo/<id>/stepper/cmd |
App zum ESP32 | Nein | JSON-Befehl, siehe unten |
connectmq/demo/<id>/stepper/progress |
ESP32 zu den Apps | Ja | {"pos":1200,"target":3200,"pct":37,"moving":true} |
connectmq/demo/<id>/status |
ESP32 zu den Apps | Ja | online / offline |
| Befehl | Beispiel | Wirkung |
|---|---|---|
| Absolute Fahrt | {"cmd":"moveTo","pos":3200} |
Fährt auf Schritt 3200 |
| Relative Fahrt | {"cmd":"move","steps":-400} |
Fährt 400 Schritte zurück |
| Geschwindigkeit | {"cmd":"speed","max":1500,"accel":800} |
Max. Schritte/s und Schritte/s² |
| Stopp | {"cmd":"stop"} |
Kontrolliert bis zum Stillstand abbremsen |
| Nullpunkt | {"cmd":"zero"} |
Aktuelle Position als 0 festlegen |
| Freigeben | {"cmd":"disable"} |
Spulenstrom abschalten |
Mit einem typischen 1,8-Grad-Motor und 1/16-Mikroschritt entspricht eine Umdrehung 3200 Schritten. Bei einer Gewindespindel mit 8 mm pro Umdrehung ergibt das 400 Schritte pro Millimeter, eine nützliche Umrechnung für Ihre App.
Hardware
- ESP32-Entwicklungsboard.
- NEMA-17-Schrittmotor (bipolar, vier Leitungen).
- A4988- oder DRV8825-Treibermodul, idealerweise auf einer Trägerplatine mit Schraubklemmen.
- Ein für den Motor geeignetes Netzteil mit 12 V bis 24 V sowie ein 100-µF-Kondensator am Motorversorgungseingang des Treibers.
| Treiber-Pin | Anschließen an |
|---|---|
| STEP | ESP32 GPIO 25 |
| DIR | ESP32 GPIO 26 |
| EN | ESP32 GPIO 27 (low = aktiviert) |
| MS1, MS2, MS3 (A4988) / M0, M1, M2 (DRV8825) | Auf High legen für 1/16- (A4988) bzw. 1/32-Mikroschritt (DRV8825) |
| VDD / GND (Logik) | ESP32 3V3 / GND |
| VMOT / GND | Motorversorgung |
| 1A, 1B, 2A, 2B | Motorspulen |
| RESET + SLEEP | Miteinander brücken |
Stellen Sie die Strombegrenzung mit dem Trimmpotentiometer des Treibers ein, bevor Sie den Motor für einen längeren Lauf anschließen. Zu viel Strom überhitzt Treiber und Motor, zu wenig führt zu Schrittverlusten. Schließen Sie den Motor niemals an oder ab, während der Treiber versorgt wird, denn die induktive Spannungsspitze kann den Treiber sofort zerstören.
Bibliotheken
- PubSubClient von Nick O'Leary.
- AccelStepper von Mike McCauley, die Beschleunigungsprofile und nicht blockierende Schrittpulse erzeugt.
- ArduinoJson Version 7.
Warum der Motor in einem eigenen Task läuft
AccelStepper verwendet keinen Hardware-Timer. Sie müssen stepper.run() so oft wie möglich aufrufen, und jeder Aufruf gibt höchstens einen Schritt aus, wenn einer fällig ist. Bei 2000 Schritten pro Sekunde ist alle halbe Millisekunde ein Schritt fällig. Blockiert mqtt.loop() für einige Millisekunden, während das WLAN beschäftigt ist, oder ein Reconnect für Sekunden, stottert der Motor oder bleibt mitten in der Fahrt stehen.
Der ESP32 hat zwei Kerne. Das Arduino-loop() läuft auf Kern 1, der WLAN-Stack hauptsächlich auf Kern 0. Der Sketch erzeugt einen FreeRTOS-Task mit niedriger Priorität, der an Kern 0 gebunden ist und nichts anderes tut, als run() aufzurufen, während loop() sich um MQTT kümmert. Ein Design ohne Mutex hält es einfach: Die MQTT-Seite schreibt "ausstehende" Anforderungen in volatile Variablen, und der Motor-Task übernimmt sie. Einen tieferen Einblick in dieses Muster bietet ESP32-FreeRTOS-Tasks mit MQTT.
Der vollständige ESP32-Schrittmotor-MQTT-Sketch
#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; // Verfahrgrenzen in Schritten
AccelStepper stepper(AccelStepper::DRIVER, STEP_PIN, DIR_PIN);
WiFiClient net;
PubSubClient mqtt(net);
// Anforderungen von der MQTT-Seite an den Motor-Task
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;
// Momentaufnahme des Motor-Tasks für die Meldungen
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; } // bremst ab
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); // im Leerlauf abgeben
}
}
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 ist active-low
stepper.disableOutputs();
disableCore0WDT(); // der Motor-Task hält Kern 0 während der Fahrt ausgelastet
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; } // abschließende Meldung
}
}
delay(2);
}
Einige Details verdienen Beachtung. Der Motor-Task gibt die CPU mit vTaskDelay(1) nur im Leerlauf ab, während einer Fahrt läuft er also mit voller Last auf Kern 0; mit Priorität 1 lässt er die WLAN-Tasks, die mit höherer Priorität laufen, trotzdem ihre Arbeit erledigen. Da der ausgelastete Task bei langen Fahrten den Idle-Task von Kern 0 aushungert, schaltet disableCore0WDT() die Watchdog-Prüfung des Idle-Tasks für diesen Kern ab; ohne das würden Sie bei Fahrten von mehreren Sekunden Task-Watchdog-Warnungen sehen. Die Software-Grenzen begrenzen jedes Ziel, sodass ein vertipptes "pos":320000 keinen Schlitten in den Rahmen fahren kann. Und die abschließende Fortschrittsmeldung mit "moving":false wird immer gesendet, was Apps ein sauberes Signal "Fahrt beendet" liefert.
Eine Fahrt starten und den Fortschritt beobachten
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}'
Der Subscriber gibt viermal pro Sekunde den Fortschritt aus, bis die Fahrt abgeschlossen ist:
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}
Senden Sie mitten in der Fahrt {"cmd":"stop"}, bremst der Motor mit der eingestellten Beschleunigung sanft ab, statt abrupt stehen zu bleiben, was Riemen und Kupplungen schont. Mit vier Meldungen pro Sekunde bleiben Sie bequem unter den 10 Nachrichten pro Sekunde, die pro Client auf dem kostenlosen öffentlichen MQTT-Broker erlaubt sind.
Checkliste für Referenzfahrt und Sicherheit
Eine Position im offenen Regelkreis ist nur so gut wie ihr Bezugspunkt. Nach dem Einschalten weiß der ESP32 nicht, wo der Schlitten steht, daher beginnt die gemeldete Position bei 0, wo auch immer der Motor gerade steht.
- Ergänzen Sie einen Endschalter und implementieren Sie eine Referenzfahrt: Fahren Sie langsam auf den Schalter zu, stoppen Sie, wenn er auslöst, und senden Sie dann
zero. - Verweigern Sie absolute Fahrten bis zur Referenzierung in der Produktiv-Firmware und veröffentlichen Sie ein
homed-Flag im Fortschritts-JSON. - Behalten Sie Software-Grenzen auch nach der Referenzierung innerhalb des mechanischen Verfahrwegs bei.
- Schalten Sie im Leerlauf die Ausgänge ab, wenn die Achse kein Haltemoment braucht; das spart Strom und hält Treiber und Motor kühl.
- Behandeln Sie eine verlorene Verbindung als Stoppbedingung bei allem, was jemanden verletzen könnte. Kombinieren Sie den Last Will mit lokaler Logik, die die Bewegung anhält, wenn keine Befehle mehr eintreffen.
- Verlassen Sie sich bei Not-Halt nie allein auf MQTT. Ein Hardware-Not-Halt, der die Motorversorgung trennt, ist die einzig akzeptable Sicherheitseinrichtung. In Maschinen gehört diese Funktion in die sicherheitsgerichtete Steuerung, nicht in die IoT-Firmware; MQTT liefert dort Sollwerte, Status und Fortschritt für Visualisierung, MES oder Wartung.
Fehlerbehebung
- Motor brummt, dreht sich aber nicht. Die Spulenpaare sind falsch verdrahtet oder die Beschleunigung ist für die Last zu hoch. Prüfen Sie die Paare mit einem Multimeter und halbieren Sie
accel. - Motor dreht, verliert aber bei höheren Geschwindigkeiten Schritte. Erhöhen Sie die Versorgungsspannung innerhalb der Treibergrenzen, passen Sie die Strombegrenzung an oder senken Sie
max. - Stottern, wenn MQTT-Nachrichten eintreffen. Der Motor läuft in
loop()statt in einem eigenen Task, oder der Task ist an denselben Kern gebunden wie rechenintensive Arbeit. - Treiber wird sehr heiß. Strombegrenzung zu hoch; montieren Sie einen Kühlkörper und senken Sie den Strom.
- Position driftet nach vielen Fahrten. Schrittverluste durch Blockieren; es gibt keine Rückmeldung, referenzieren Sie also regelmäßig.
Produktivbetrieb: Zugangsdaten, TLS und Monitoring
Bewegungsgeräte verdienen private Zugangsdaten und Verschlüsselung. Legen Sie im ConnectMQ-Portal einen Broker an, verwenden Sie die generierten Werte für Benutzername, Passwort und Client ID und wechseln Sie zu WiFiClientSecure auf Port 8883; der Leitfaden zu ESP32 MQTT über SSL/TLS behandelt die Zertifikatseinrichtung. Ein verwalteter MQTT-Broker wie ConnectMQ bietet außerdem einen Live-Topic-Explorer, mit dem Sie stepper/progress beim Abstimmen der Geschwindigkeiten verfolgen, sowie Schwellenwert-Alarmregeln, die Sie auf numerische Telemetrie wie die Treibertemperatur anwenden können, wenn Sie diese als eigenes Topic veröffentlichen. Für einfachere Aktoren findet sich dasselbe Befehls- und Zustandsmuster in der ESP32-Servosteuerung per MQTT, und das ESP32-IoT-Tutorial ist der Ausgangspunkt, wenn dies Ihr erstes ESP32-Projekt ist.
Starten Sie mit ConnectMQ
Erstellen Sie einen kostenlosen Serverless Broker unter app.connectmq.com, ohne Kreditkarte. Sie erhalten in einer Minute TLS-Zugangsdaten, einen Client im Browser zum Senden von Fahrbefehlen und eine Live-Ansicht des Fortschritts Ihres Motors.
Häufig gestellte Fragen
Warum stottert mein Schrittmotor, wenn der ESP32 MQTT-Nachrichten empfängt?
AccelStepper muss run() sehr häufig aufrufen. Teilt es sich loop() mit der Netzwerkkommunikation, führt jede Verzögerung zu verpassten Schritten. Lassen Sie den Motor wie oben gezeigt in einem separaten FreeRTOS-Task laufen oder verwenden Sie eine timerbasierte Schrittmotor-Bibliothek.
Kann ich mit diesem Sketch einen 28BYJ-48-Motor steuern?
Ja, mit einer ULN2003-Platine. Erzeugen Sie AccelStepper mit AccelStepper::HALF4WIRE und vier Pins (Reihenfolge IN1, IN3, IN2, IN4) statt mit DRIVER; der MQTT-Teil bleibt gleich.
Wie genau ist die gemeldete Position?
Sie entspricht exakt der Zahl der Schritte, die der ESP32 gesendet hat. Sie stimmt mit der realen Position überein, solange der Motor nie blockiert; eine Referenzfahrt gegen einen Schalter beseitigt jeden angesammelten Fehler.
Welche QoS sollte ich für Motorbefehle verwenden?
Verwenden Sie QoS 1 für Befehle. Absolute Fahrten (moveTo) dürfen problemlos doppelt ankommen, während eine doppelt zugestellte relative Fahrt zweimal fahren würde. Bevorzugen Sie daher absolute Positionen, wenn die Zustellung wiederholt werden kann.



