Bei diesem Projekt steht keine tatsächliche Anwendung im Vordergrund. Vielmehr geht es darum aufzuzeigen, wie ein preiswerter Schrittmotor vom Typ 28BYJ-48 mit Getriebe und Treiber-Board per Smartphone und einem ESP32 angesteuert werden kann.

Eine sehr ausführliche Beschreibung des Motors findet man bei der Hochschule Hamm-Lippstadt: Schrittmotor 28BYJ-48 mit ULN2003 Treiberplatine
Inhaltsverzeichnis
Das ZIP-Archiv 28BYJ-48.zip enthält die 3D-Druck-Dateien für die Fahne, den Quellcode des App-Inventor-Projekts und den Quellcode für den ESP32 in Form eines Visual-Studio/Visual-Micro-Projekts. Benutzer der Arduino-IDE benutzen einfach nur die Quellcodes (.ino-, .h- und .cpp-Dateien).
Ein mit App Inventor erstelltes Projekt für Android sendet per UDP Kommandos via WLAN an einen ESP32. Der ESP32 analysiert die UDP-Pakete und steuert das Treiber-Board des Schrittmotors an.
Ein einzelner Schrittmotor lässt sich einfach mit 5V aus der USB-Stromversorgung betreiben.
Die LEDs auf dem Treiber-Board zur Anzeige des Status können durch ziehen des Jumpers neben der Stromversorgung abgeschaltet werden.
Der Winkel pro Vollschritt beträgt 11,25°. Im Vollschrittmodus entspricht jeder Schritt einer Drehung von 11,25 °. Es gibt somit 360° / 11,25° = 32 Schritte pro Umdrehung.
Das Getriebe hat eine Untersetzung von 1:64 (genau sind es 1:63,68395). Für eine komplette Umdrehung (360°) sind also 2038 Schritte (genauer 2.037,8864) notwendig. Leider besitzt das Getriebe ein relativ großes Spiel, das sich bei einem Wechsel der Drehrichtung bemerkbar macht und kompensiert werden muss. Bei meinem Motor sind dazu 11 Schritte notwendig.
Jeweils 4 Schritte bilden eine Schrittsequenz. Der Betrieb im Halbschrittmodus macht wegen des relativ großen Getriebespiels und des recht feinen Schrittwinkels von 0,18° (360° / 2038) keinen wirklichen Sinn.
Um das Spiel zu ermitteln habe ich eine längere Fahne gedruckt, mit der man zuverlässig auch kleine Bewegungen erkennen kann:
![]() |
![]() |
Projektdate für OpenScad und eine .stl-Datei sind im Download-Verzeichnis enthalten.
Eine sehr ausführliche Beschreibung des Motors findet man bei der Hochschule Hamm-Lippstadt: Schrittmotor 28BYJ-48 mit ULN2003 Treiberplatine
Die Android App sendet UDP-Pakete in der Form
<Cmd><Id>;<Data1>;<Data2>
Beispiel: R7;100;250 = 100 Schritte im Uhrzeigersinn mit 250 Schritten pro Sekunde (lfd. Nachrichtennummer ist 7).
| Kommando | Bedeutung | Data1 | Data2 |
|---|---|---|---|
| R | Motor im Uhrzeigersinn drehen | Anzahl Schritte (-1 für unbegrenzt) | Schritte pro Sekunde |
| L | Motor im Gegenuhrzeigersinn drehen | Anzahl Schritte (-1 für unbegrenzt) | Schritte pro Sekunde |
| r | Motor einen Schritt im Uhrzeigersinn drehen | - | - |
| l | Motor einen Schritt im Gegenuhrzeigersinn drehen | - | - |
| S | Motor anhalten, aktuelle Position wird aktiv gehalten | - | - |
| C | Getriebespiel festlegen | Anzahl Schritte | - |
| O | Motor ausschalten, alle Spulen werden abgeschaltet | - | - |
Der Motor wird automisch eingeschaltet, wenn ein Kommando zum Drehen ausgeführt wird. "-" = beliebiger Wert vorzugsweise 0.
Da die Übermittlung bei UDP nicht vollständig gewährleistet ist, sendet der ESP32 das empfangene Kommando unverändert zurück.
Die Android App wartet auf diese Rückmeldung und gibt das System erst dann wieder für weitere Kommandos frei, wenn die Rückmeldung erhalten wurde. Erfolgt diese nicht innerhalb einer festgelegten Zeit (500 ms) wird das Kommando erneut versendet. Die Anzahl der Wiederholungen ist begrenzt (dreimal).
Der ESP32 prüft, ob anhand der fortlaufen Kommando-ID, ob er das Kommando schon erhalten und ausgeführt hat. Ist dies der Fall, wird das neu empfangene Kommando ignoriert.
Die Programme für den ESP32 wurden mit Microsoft Visual Studio Community 2022 und dem Visual Micro Arduino IDE for Visual Studio Plugin entwickelt. Ich muss eigestehen, dass nur etwa 10% des Codes von mir stammt. Das meiste hat die KI (Copilot) entworfen. Schon nach der Eingabe eines sprechenden Funktionsnamens wurde bereits ein Code vorgeschlagen, der meist nur geringfügig angepasst werden musste. Sämtliche Code-Kommentare sind von der KI geschrieben worden. Ein "//" an passender Stelle reicht und man erhält einen treffenden Kommentar. Als ob die KI "verstanden" hätte...
Die Software für den ESP32 besteht aus der Klasse M28BYJ_48, die Steuerung des Motors übernimmt, der Klasse UdpHandler, die eingehende UPD-Kommando-Pakete entgegennimmt und auswertet und dem Hauptprogramm (.ino-Datei), das das System initialisiert und Verbindung zwischen dem UDP-Handler und der Motorsteuerung übernimmt.
Die Klasse UdpHandler wertet eingehende UDP-Pakete aus und extrahiert den Kommandoschlüssel und die zugehörigen Argumente. Zum Empfang wird eine ESP32-UDP-Client Instanz (WiFiUDP, NetworkUDP) verwendet. Empfangene Kommandos werden über eine Callback-Funktion gemeldet.

| Methode | Funktion | Anmerkung |
|---|---|---|
| UdpHandler(uint16_t localPort, uint16_t remotePort) | Konstruktor localPort: Port für den Empfang von Kommandopaketen remotePort: Port an den Quittungspakete gesendet werden |
Im Bespiel 2222 und 2222 |
| void begin() | Initialisiert die ESP32-UDP-Client-Instanz | |
| void setCommandCallback(ProcessCommand_t callback) | Callback für empfangene Kommandos festlegen callback ist vom Typ void (*)(const char, const int, const int) |
|
| void handleUdp() | Arbeitsfunktion | Regelmäßig in loop aufrufen |
Die wesentliche Funktion dieser Klasse ist handleUdp: Es werden geschaut, ob UDP-Pakete empfangen wurden. Diese werden quittiert uns analysiert. Empfangene Kommandos werden an die registrierte Callback-Funktion weitergeleitet.
|
![]() |
Klasse M28BYJ_48 übernimmt die Ansteuerung des Schrittmotors.

| Methode | Funktion | Anmerkung |
|---|---|---|
| M28BYJ_48(uint8_t pin1, uint8_t pin2, uint8_t pin3, uint8_t pin4, uint8_t gearClearance, MotorTypes type = MotorTypes::Simple) |
Konstruktor pin1..pin4: Pins an die das Treiber-Board angeschlossen ist gearClearance: Anzahl Schritte zum Ausgleich des Getriebespiels type: Simple = Ansteuerung einer Spule Strong = zwei Spulen |
Der Parameter type ist über die Enumerationsklasse
MotorTypes abgesichert. Der Motortyp Strong besitzt ein höheres Dreh- und ein höheres Haltemoment, verbraucht aber doppelt so viel Strom. Wegen der hohen Getriebeübersetzung sind die Momente beim Typ Simple i.d.R. ausreichend. |
| void begin() | Initialisiert die Pins und den Timer | Der Motor wird in die Position 0 der Schrittsequenz gedreht. Es können demzufolge bis zu drei Schritte ausgeführt werden. |
| void end() | Der Timer wird freigegeben und die Pins auf INPUT gesetzt. | Alle Spulen des Motors werden angeschaltet. |
| void setDirection(Direction dir) | Richtung festlegen. | Bei einem Richtungswechsel werden die eingestellten Schritte zur Kompensation des Getriebespiels ausgeführt. Alle Methoden, die direkt oder indirekt eine Richtungsangabe beinhalten, rufen diese Methode auf. |
| void setGearClearance(uint8_t clearance) | Legt die Anzahl der Schritte zur Kompensation des Getriebespiels fest. | Der Wert muss für jeden Motor manuell ermittelt werden. |
| void stepCW() | Den Motor einen Schritt um Uhrzeigersinn drehen. | Die aktuelle Richtung wird festgelegt. Bei einem Richtungswechsel wird das Getriebespiel kompensiert. Ein laufender Motor wird vorher gestoppt. Eine vorgegebene Schrittzahl wird evtl. nicht ausgeführt. Ggf. vorher isRunning überprüfen. |
| void stepCCW() | Den Motor einen Schritt um Gegenuhrzeigersinn drehen. | Die aktuelle Richtung wird festgelegt. Bei einem Richtungswechsel wird das Getriebespiel kompensiert. Ein laufender Motor wird vorher gestoppt. Eine vorgegebene Schrittzahl wird evtl. nicht ausgeführt. Ggf. vorher isRunning überprüfen. |
| void step() | Den Motor einen Schritt in der aktuellen Richtung drehen. | Ein laufender Motor wird vorher gestoppt. Eine vorgegebene Schrittzahl wird evtl. nicht ausgeführt. Ggf. vorher isRunning überprüfen. |
| void run(Direction dir, int steps, uint16_t stepsPerSecond) | Den Motor in der angegebenen Richtung für eine bestimmte Anzahl von Schritten mit der angegebenen Geschwindigkeit
(Schritte pro Sekunde) laufen lassen. steps: Anzahl der auszuführenden Schritte oder -1 für unbegrenzt. |
Die aktuelle Richtung wird festgelegt. Bei einem Richtungswechsel wird das Getriebespiel kompensiert. Ein laufender Motor wird vorher gestoppt. Eine vorgegebene Schrittzahl wird evtl. nicht ausgeführt. Ggf. vorher isRunning überprüfen. |
| void run(int steps, uint16_t stepsPerSecond) | Den Motor in der aktuellen Richtung für eine bestimmte Anzahl von Schritten mit der vorgegebenen Geschwindigkeit (Schritte pro Sekunde) laufen lassen. | Ein laufender Motor wird vorher gestoppt. Eine vorgegebene Schrittzahl wird evtl. nicht ausgeführt. Ggf. vorher isRunning überprüfen. |
| void run(Direction dir, uint16_t stepsPerSecond) | Den Motor in der angegebenen Richtung mit der vorgegebenen Geschwindigkeit (Schritte pro Sekunde) laufen lassen. | Die aktuelle Richtung wird festgelegt. Bei einem Richtungswechsel wird das Getriebespiel kompensiert. Ein laufender Motor wird vorher gestoppt. Eine vorgegebene Schrittzahl wird evtl. nicht ausgeführt. Ggf. vorher isRunning überprüfen. |
| void run(uint16_t stepsPerSecond) | Den Motor in der aktuellen Richtung mit der vorgegebenen Drehzahl (Schritte pro Sekunde) laufen lassen. | Ein laufender Motor wird vorher gestoppt. Eine vorgegebene Schrittzahl wird evtl. nicht ausgeführt. Ggf. vorher isRunning überprüfen. |
| void stop() | Stoppt den Motor. | Der Timer für die Schritterzeugung wird angehalten. |
| bool isInitialized() | Gibt an, ob der Motor initialisiert wurde. | Wird durch begin gesetzt und durch end zurückgesetzt. |
| bool isRunning() | Gibt an, ob der Motor gerade läuft. | Wird durch run gesetzt und nach Ausführung der angegeben Schritte oder durch stop zurückgesetzt. |
Die zentralen Funktion zur Ausführung der Schritttakte sind die internen Funktionen setSinglePin und setDoublePin. Eine Schrittfolge besteht aus 4 Takten. Die Funktion schaltet die Spulen des Schrittmotors anhand eines übergeben Schrittindex ein. Für einen Schritt im Uhrzeigersinn muss der Schrittindex Modulo 4 erhöht werden, für die Gegenrichtung um 1 erniedrigt werden (ebenfalls Modulo 4). setSinglePin schaltet jeweils eine Spule ein. Bei setDoublePin sind es zwei Spulen. Das führt zu einem höheren Dreh- und Haltemoment, aber auch zu einem höheren Stromverbrauch.
|
|
Der Aufruf der Funktionen geschieht indirekt über den Funktionszeiger setPin:
// Pointer to the function that sets the motor pins
void (M28BYJ_48::* setPin)(uint8_t) const;
Welche der beiden Funktionen benutzt wird, wird im Konstruktor festgelegt.
switch (type) {
case MotorTypes::Simple:
setPin = &M28BYJ_48::setSinglePin;
break;
case MotorTypes::Strong:
setPin = &M28BYJ_48::setDoublePin;
break;
default:
break;
}
Sämtliche (!) Anweisungen zur Ausführung eines Schritts erfolgen über die Funktion internalStep:
void M28BYJ_48::internalStep(Direction dir) {
switch (dir) {
case Direction::CW:
internalStepCW();
break;
case Direction::CCW:
internalStepCCW();
break;
default:
break;
}
}
Diese leitet den Aufruf entsprechend der angegebenen Richtung an internalStepCW oder internalStepCCW weiter:
void M28BYJ_48::internalStepCW() {
stepIndex = (stepIndex + 1) % 4;
(this->*setPin)(stepIndex);
}
void M28BYJ_48::internalStepCCW() {
stepIndex = (stepIndex + 3) % 4;
(this->*setPin)(stepIndex);
}
Sämtliche öffentliche Funktionen zur Ausführung eines Einzelschritts erfolgen über die Funktion step:
void M28BYJ_48::step(Direction dir) {
if (isTimerRunning) {
stop();
}
setDirection(dir);
internalStep(dir);
}
Ein evtl. laufender Motor wird gestoppt. Eine vorgegebene Schrittzahl wird deshalb evtl. nicht ausgeführt. Ist dies nicht erwünscht, muss abgewartet werden, dass isRunning den Wert false annimmt wird.
Die Drehrichtung wird über setDirection festgelegt. Dabei wird, falls notwendig, das Getriebespiel kompensiert. Zuletzt wird ein Einzelschritt per internalStep ausgeführt.
Sämtliche Schrittfunktionen mit expliziter oder impliziter Angabe einer Drehrichtung rufen die Funktion setDirection zur Festlegung der (neuen) Richtung auf:
void M28BYJ_48::setDirection(Direction dir) {
if (dir != this->dir) {
// Apply clearance steps when changing direction
for (uint8_t i = 0; i < clearance; i++) {
internalStep(dir);
delay(50); // Delay to allow motor to step
}
} this->dir = dir;
}
Die aktuelle Drehrichtung wird im Feld dir festgehalten. Wechselt die Drehrichtung, werden zur Getriebespielkompensation die im Feld clearance hinterlegte Anzahl von Schritten in die neue Drehrichtung ausgeführt.
Mithilfe der Funktion run(...) kann die Ausführung einer angegebenen Anzahl von Schritten mit einer bestimmten Geschwindigkeit gestartet werden. Die Auslösung eines einzelnen Schrittes erfolgt dabei über einen Hardware-Timer (Feld motorTimer). Die Anforderung des Timers geschieht in der Funktion begin:
void M28BYJ_48::begin() {
...
motorTimer = timerBegin(1'000'000);
timerAttachInterruptArg(motorTimer, onMotorTimer, this);
isMotorInitialized = true;
}
Die Taktfrequenz für den Timer wird auf 1 MHz festgelegt. Die Interrupt-Service-Routine (ISR) ist die Funktion onMotorTimer. onMotorTimer erhält als Parameter einen Pointer auf die Klasseninstanz (this).
Sämtliche Aufrufe von run(...) werden an internalRun weitergeleitet.:
void M28BYJ_48::internalRun(Direction dir, int steps, uint16_t stepsPerSecond) {
if (isTimerRunning) {
stop();
}
setDirection(dir);
timerStop(motorTimer);
timerAlarm(motorTimer, 1'000'000 / stepsPerSecond, true, 0);
isTimerRunning = true;
remainingSteps = steps;
timerStart(motorTimer);
}
Ein eventuell bereits laufender Motor wird zunächst gestoppt und anschließend die neue Richtung festgelegt, gegebenenfalls mit Getriebespielkompensation. timerAlarm legt die Anzahl Takte zwischen zwei Interrupts fest (1'000'000 / stepsPerSecond). Im Feld remainingSteps wird festgehalten, wie viele Schritte noch auszuführen sind. timerStart startet dann den Timer.
Die ISR onMotorTimer prüft, ob noch weitere Schritte auszuführen sind und löst ggf. einen weiteren Schritt aus:
void ARDUINO_ISR_ATTR onMotorTimer(void* arg) {
M28BYJ_48* motor = static_cast<M28BYJ_48*>(arg);
if (motor->remainingSteps < 0) {
motor->internalStep(motor->dir);
return; // unlimited steps
}
if (motor->remainingSteps > 0) {
motor->remainingSteps--;
motor->internalStep(motor->dir);
return;
}
motor->stop();
}
Ist die vorgegebene Anzahl an Schritten ausgeführt, wird der Motor gestoppt. Dazu reicht es, den Timer zu stoppen:
void M28BYJ_48::stop() {
timerStop(motorTimer);
isTimerRunning = false;
}
Das Android-Programm wurde mit dem MIT App Inventor entwickelt. Zur Datenübertragung wird die Extension UrsAI2UDP verwendet. Mit der Extension UrsAI2Utils können komplette Arrangement-Blöcke aktiviert und deaktiviert werden.
![]() |
![]() |
|
| App Inventor Design | Android App Screenshot |
Die folgende Grafik zeigt die Implementierung des Datenübertragungsprotokolls:
Die einzelnen Schaltflächen rufen die Prozedur SendMessage mit Angabe des Kommandoschlüssel und der notwendigen Parameter auf:
![]() |
Sendesymbol anzeigen Neue Nachrichtennummer generieren Nachricht für Wiederholungen zwischenspeichern Nachricht versenden Wiederholungszähler zurücksetzen Timer für Wiederholungen starten Weitere Benutzeraktionen verhindern |
Wenn keine Quittung der Kommandoübertragung durch den ESP32 innerhalb des vorgegebenen Zeitraums erhalten wurde wird die vorher zwischengespeicherte Nachricht erneut versendet. Da die Eingabeelemente der App gesperrt sind, muss die Wiederholung irgendwann mit einer Fehlermeldung abgebrochen werden:

Nach Erhalt einer passenden Quittung werden weitere Wiederholungen unterbunden und die Eingabeelemente wieder freigegeben
