Bei einem Projekt geht es darum mehrere Steppermotoren gleichzeitig mit einem ESP32 anzusteuern. Bei der Bibliothek zur Ansteuerung eines einzelnen Schrittmotor vom Typ 28BYJ-48 werden die Schritte über einen Timer-Interrupt erzeugt. Die zugehörige ISR muss nun so erweitert werden, dass mehrere Motoren gleichzeitig mit passendem Schrittverhältnis angesteuert werden könnnen.
| Version | Anpassungen |
|---|---|
| 1.0 (2026-05-09) | Initiale Version |
| 1.1 (2026-06-04) | Implementierung für eine Arduino- (ESP32-) Bibliothek. |
| 1.2 (2026-06-09) | Fehler bei der Sortierung der Motoren behoben. |
| 1.3 (2026-06-19) | Funktion hasFinished hinzugefügt. |
Inhaltsverzeichnis
Linien-Algorithmus (Duale Motorsteuerung)
Download Projekt DualMotorControl
Download Projekt MultiMotorControl
Im einfachsten Fall müssen zwei Motoren so synchronisiert werden, dass sie die geforderten unterschiedlichen Schrittzahlen innerhalb der gleichen Zeit ausführen. Dies entspricht einem Linien-Algorithmus. Zur Prüfung habe ich zunächst ein kleines C#-Programm (DualMotorControl) geschrieben, das mit verschiedenen Algorithmen eine Linie zeichnet.
Verglichen werden drei Varianten:
Das Programm zeichnet die Linien in verschiedenen Farben und zeigt die Zielpositionen an.
Die Ergebnisse liegen recht nah beieinander, wie die folgenden Vergrößerungen zeigen (Line: Rot, Bresenham: Grün, MotorControl: Braun):
![]() |
![]() |
Der DDA-Algorithmus ist recht einfach aufgebaut und lässt sich leicht auf weitere Motoren erweitern. Er arbeitet jedoch mit Fließkommazahlen die auf μCs relativ aufwändig sind und zu Problemen führen können.
Der Bresenham-Algorithmus kommt mit Ganzzahladditionen aus, lässt sich aber schwer erweitern.
MotorControl kommt ebenfalls mit Ganzzahladditionen aus und lässt sich leicht auf weitere Motoren erweitern.
Die Aufgabe besteht darin, eine gerade Linie mit diskreten Schritten zu approximieren. Die Idee ist, den Motor, der die meisten Schritte ausführen muss, bei jedem Takt einen Schritt ausführen zu lassen. Der zweite Motor mit der geringeren Schrittzahl wird nur dann angesteuert, wenn ein Schritt in die andere Richtung notwendig ist.

Um die Steuerung zu vereinfachen, werden zunächst die Schrittrichtungen separiert. Dadurch ist es möglich, ausschließlich mit positiven Werten weiterzuarbeiten:
int Steps1 = StepsX;
int Steps2 = StepsY;
int dir1 = Steps1 >= 0 ? 1 : -1; // Richtung für Motor in X-Richtung
Steps1 = Abs(Steps1);
int dir2 = Steps2 >= 0 ? 1 : -1; // Richtung für Motor in Y-Richtung
Steps2 = Abs(Steps2);
Zur weiteren Vereinfachung werden die Motoren (MotorX und MotorY) nicht direkt, sondern über Referenzen (motor1 und motor2) angesteuert. Dadurch ist es möglich die Motoren so zu vertauschen, dass motor1 immer der mit der größeren Schrittzahl ist und trotzdem der richtige physikalische Motor angesteuert wird.
Motor motor1 = motorX; // Vertauschbare Referenz. Motor 1 ist der Motor mit den meisten Schritten
Motor motor2 = motorY;
if (Steps1 < Steps2) { // Motor 2 hat mehr Schritte als Motor 1, also tauschen
Swap(ref motor1, ref motor2);
Swap(ref Steps1, ref Steps2);
Swap(ref dir1, ref dir2);
}
Das Ergebnis ist, dass sich die notwendige Mathematik auf die untere Hälfte des ersten Quadranten beschränkt, wie in der obigen Grafik gezeigt. Der Winkel der Ziellinie ist kleiner oder gleich 45° und ihre Steigung ist ≤ 1. Nun muss bei jedem Takt motor1 um einen Schritt weiter gedreht werden, was man einfach mit einer Schleife erledigen kann. Zu überlegen, wann der zweite Motor angesteuert werden muss.
Die Steigung der Ziellinie ist S = StepsX / StepsY. Die Sollposition für y ist S * x. Ein Schritt in Y-Richtung muss immer dann ausgeführt werden, wenn die Ist-Position um mindestens einen Schritt von der Soll-Position abweicht: S * x - y ≥ 1. Also

Die Formel lässt sich umformen zu
(Formel 1)
Die Multiplikation lässt sich vermeiden, indem man sie durch fortgesetzte Addition ersetzt. Es werden Variablen prod1 und prod2 für die Produkte mitgeführt und bei bei jedem ausgeführten Schritt um StepsX bzw. StepsY erhöht:
motor1.Step(dir1);
prod1 += Steps2; // Führt das Produkt x * StepsY mit (ggf. getauscht)
if (prod1 >= prod2) {
motor2.Step(dir2); Führt das Produkt y * StepsX mit (ggf. getauscht)
prod2 += Steps1;
}
Ein kleines Problem gibt es noch zu lösen. Zu Beginn sind x und y Null. Sobald der erste Schritt in X-Richtung ausgeführt wird, ist StepsY*x = StepsY immer größer als StepsX*y = 0. D.h. der zweite Motor wird direkt beim 1. Takt angesteuert und würde damit der gelben Kurve folgen:

Dies lässt sich vermeiden, in dem man prod2 mit dem Wert von StepsX vorbelegt:
int prod1 = 0, prod2 = Steps2; // Es werden die Produkte der aktuellen Position mit der Anzahl der Schritte
// des anderen Motors benötigt. Dies entspricht i.W. der Steigung der Linie.
Das DualMotorControl.zip enthält die Visual Studio-Solution zu diesem Programm. Es enthält im Projekt DualMotorControlCS das C# Programm und im Projekt DualMotorControlCpp die für μC einsetzbare Methode DualMotorControl in C++.
Etwas anspruchsvoller wird es, wenn weitere Motoren gleichzeitig angesteuert werden sollen. Der oben gezeigte Algorithmus lässt sich erweitern. Im Bespiel werden fünf Motoren angesteuert:
![]() |
![]() |
Wie im vorhergehenden Beispiel dient die Klasse Motor als Schnittstelle zum anzusteuernden Motor:

Für das Beispiel nur die Methode Step benötigt, die den Motor veranlasst, eine angegebene Anzahl von Schritten auszuführen. Bei der synchronisierten Ansteuerung wird immer nur ein einzelner Schritt ausgeführt:
/// <summary>
/// Bewegt den Motor um die angegebene Anzahl von Schritten.
/// Positive Werte bewegen den Motor vorwärts, negative Werte rückwärts.
/// </summary>
/// <param name="step">Die Anzahl der Schritte, um die der Motor bewegt werden soll.</param>
public void Step(int step) {
Pos += step;
}
Zur Verwaltung der Motoren wird die Hilfsklasse MotorControl verwendet. In ihr werden alle prozessrelevanten Daten gespeichert, die bei der dualen Steuerung als eigenständige, motorbezogene Variablen geführt wurden.

Die Felder Dir und Steps enthalten die Richtung und absolut durchzuführende Schrittzahl. motor ist die Referenz auf den anzusteuernden Motor. prod1 und prod2 sind die Produkte der aktuellen Position mit der Anzahl der Schritte des Referenzmotors benötigt. Dies entspricht i.W. der Steigung der Linie. Die Methode Step leitet die Schrittanweisung an den betroffenen Motor weiter.
Die Methode MultiMotorControl übernimmt die Berechnung der Motorschritte. Sie ist direkt im Code-Teil der Windows.Form-Klasse implementiert. Dies erleichtert erheblich die Anzeige der berechneten Pfade.
Die Synchronisation der Motoren übernimmt die Funktion MultiMotorControl in der Klasse Form1. Die wurde dort implementiert, weil dass die die Anzeige der berechneten Pfade erheblich erleichtert. Die enthält eine Reihe von Anweisungen zur Anzeige, die in einer realen Implementierung nicht benötig werden. Dies ist eine bereinigte Version:
/// <summary>
/// Steuert mehrere Motoren gleichzeitig, damit sie alle gleichzeitig an ihrem Ziel ankommen.
/// </summary>
/// <param name="Motors">Ein Array von MotorControl-Objekten, die gesteuert werden sollen.</param>
void MultiMotorControl(MotorControl[] Motors) {
// Der Motor mit den meisten Schritten muss zuerst bearbeitet werden.
// Daher wird der Motor mit den meisten Schritten an die erste Stelle des Arrays verschoben.
for (int i = 1; i < Motors.Length; i++) {
if (Motors[i].Steps > Motors[0].Steps) {
Swap(ref Motors[0], ref Motors[i]);
}
}
// Vorbelegung von prod2, damit die Motoren mit weniger Schritten nicht sofort starten.
for (int i = 1; i < Motors.Length; i++) {
Motors[i].prod2 = Motors[0].Steps/2;
}
// Motoren ansteuern
for (int i = 0; i < Motors[0].Steps; i++) { // Anzahl Takte, die der Motor mit den meisten Schritten benötigt.
// ----- TAKT ----->>>>
Motors[0].Step(); // Der Motor mit den meisten Schritten wird in jedem Takt bewegt.
for (int k = 1; k < Motors.Length; k++) { // Alle anderen Motoren prüfen, ob sie in diesem Takt bewegt werden müssen.
Motors[k].prod1 += Motors[k].Steps;
if (Motors[k].prod1 > Motors[k].prod2) { // Nachlaufender Motor muss bewegt werden.
Motors[k].Step();
Motors[k].prod2 += Motors[0].Steps;
}
}
}
}
In einer echten Motorsteuerung muss diese Methode geteilt werden. Der obere Teil dient der Initialisierung. Der untere Teil (ab „TAKT”) muss durch einen Zeitgeber angesteuert werden, der die Schrittfrequenz für den am schnellsten laufenden Motor vorgibt, d. h. für den Motor, der die meisten Schritte ausführen muss.
Angesteuert wird die Methode mit einem Array von MotorControl-Objekten:
MultiMotorControl([new MotorControl(motorX, x),
new MotorControl(motorU, u),
new MotorControl(motorV, v),
new MotorControl(motorW, w),
new MotorControl(motorQ, q)
]);
motorX, motorU etc. sind die zu synchronisierenden Motoren. x, u etc. die jeweils auszuführenden Schritte.
Die benutzbare Implementierung in C++ ist etwas aufwändiger. Die Klassen Motor und MotorControl entsprechenden den in C# (s.o.).
Die Klasse MultiMotorControl stellt die Methoden zur Steuerung der Methoden bereit:

Der Methode init wird ein Array mit den zu steuernden Motoren übergeben. Diese werden jeweils durch ein MotorControl-Objekt repräsentiert. init sortiert das Array und berechnet die konstanten Prozesswerte. Alternativ kann init mit einem std::vector<MotorControl> versorgt werden.
Die Methode start startet den intern definierten Timer vom Typ MotorControlTicker, der die gewünschte Anzahl an Taktimpulsen liefert. Der Methode wird die Verzögerungszeit zwischen den einzelnen Schritten übergeben. Alternativ könnte eine Frequenzangabe implementiert werden.
isRunning und getRemainingSteps liefern Informationen über den Fortschritt. Mit stop kann der Vorgang vorzeitig beendet werden.
Das interne Feld motors speichert die übergebenen MotorControl-Objekte. motorControlTicker ist der Timer für die Taktimpulse.
Beim Start wird dem Timer ein Verweis auf die eigene Instanz übergeben. Er hat die Aufgabe, bei jedem Taktimpuls die Methode step aufzurufen, die bei jedem Aufruf einen Schritt beim Motor ausführt, der am schnellsten laufen muss, d. h. der mit den meisten auszuführenden Schritten. Falls notwendig, wird auch ein Schritt bei den weiteren Motoren ausgeführt.
Über die Methode hasFinished kann einmalig abgerufen werden, ob die aktuelle Aufgabe beendet ist. Nach Erledigung der Aufgabe wird intern ein Flag gesetzt, dass bei Abruf über hasFinished wieder zurück gesetzt wird. Wenn hasFinished true zurück liefert, kann z.B. ein Ereignis ausgelöst werden oder mit einem nächsten Programmschritt fortgefahren werden.
Ein Objekt dieser Klasse liefert die notwendigen Taktimpulse. Der zu ergänzende physikalische Timer hängt von der benutzten Hardware und deren Firmware ab. Der Methode start wird die Verzögerungszeit zwischen den einzelnen Schritten, die Anzahl der auszuführenden Schritte (ableitet von der Schrittzahl des Motors mit den meisten auszuführenden Schritten) und ein Verweis auf das zugehörige MultiMotorControl-Objekt, dessen Methode step bei den Taktimpulsen aufgerufen werden soll.
/**
* Startet den Ticker mit der angegebenen Verzögerungszeit und Anzahl der Schritte.
*
* @param delay Die Verzögerungszeit in Millisekunden.
* @param StepCount Die Anzahl der Schritte, die der Ticker ausführen soll.
* @param callback Zeiger auf die Callback-Funktion, die aufgerufen wird, wenn der Ticker abläuft.
*/
void MotorControlTicker::start(unsigned int delay, unsigned int stepCount, MultiMotorControl* controller) {
this->delay = delay;
this->stepCount = stepCount;
this->controller = controller;
running = true; // Ticker als laufend markieren.
elapsedSteps = 0; // Zurücksetzen der bereits abgelaufenen Schritte.
// Hier muss ein Timer oder eine Schleife implementiert werden, der die tick()-Methode regelmäßig aufruft.
// -------------------------------------------------------------------------------------------------------
// RealTimer.Start(...);
}
Der reale Timer muss bei jedem Taktimpuls die Methode tick aufrufen, die, wie oben gefordert step aufruft und weiterhin die Abbruchbedingung überprüft.
void MotorControlTicker::tick() {
controller->step(); // Führt einen Schritt in der Steuerung der Motoren aus.
elapsedSteps++; // Erhöhen der Anzahl der abgelaufenen Schritte.
if (elapsedSteps >= stepCount) {
stop(); // Stoppen des Tickers, wenn die gewünschte Anzahl von Schritten erreicht ist.
}
}
Das MultiMotorControl.zip enthält die Visual Studio-Solution zu diesem Programm. Es enthält im Projekt MultiMotorControlCS das C# Programm und im Projekt MultiMotorControlCpp die für μC einsetzbare Methode DualMotorControl in C++.
Das ZIP-Archiv UrsMultiMotorControl.zip enthält den Code für die Ansteuerung mehrerer Schrittmethoden. Beim Praxiseinsatz hat sich gezeigt, dass einige Änderungen an der Struktur notwendig waren:
Im Projekt Android steuert einen Schrittmotor über einen ESP32 wird eine Bibliothek vorgestellt, die einen Schrittmotor vom Typ 28BVJ-48 ansteuert.

Um mehrere Motoren dieses Typs anzusteuern wird eine Schnittstelle (Adapter, Wrapper) benötigt, die die beiden Bibliotheken miteinander verbindet. Dies erledigt die Klasse MotorWrapper. Sie wird überall anstatt der Klassen M28BYJ_48 oder Motor verwendet.
Es wird eine interne Wrapper-Klasse M28BYJ_48Wrapper für die Klasse M28BYJ_48 verwendet, die den Zugriff auf die geschützten Methoden M28BYJ_48::internalStepCW und M28BYJ_48::internalStepCCW ermöglicht. Diese sind bereits für die Nutzung in einer ISR vorbereitet.
![]() |
|
Die Klasse MotorWrapper selbst ist von der Klasse Motor abgeleitet, die elementaren Funktionen für die Bibliothek UrsMultiMotorControl bereit stellt. Dies sind die Methoden setDirection und step. Diese werden so überschrieben, dass sie einerseits die entsprechende Methode in der Basisklasse und die in der Klasse M28BYJ_48 bedienen. Hier das Beispiel für die Methode step:
// Bewegt den Motor um einen Schritt in die angegebene Richtung
virtual void ARDUINO_ISR_ATTR step() override {
Motor::step(); // Aktualisiert die Position basierend auf der Richtung
if (direction > 0) {
motor->stepCW();
}
else if (direction < 0) {
motor->stepCCW();
}
}
Darüber hinaus stellt sie sämtliche Methoden bereit, die für den Betrieb eines 28BYJ-48 notwendig sind. Zusätzlich ist eine Umkehrung der Bewegungsrichtung möglich. Normalerweise ist die Drehung im Uhrzeigersinn mit einer negativen, die im Gegenuhrzeigersinn mit einem positiven Wert verbunden. Dies kann umgekehrt werden.
// Wrapper-Klasse, die die Motor-Klasse auf die Klasse M28BYJ_48 abbildet.
// Diese Klasse ermöglicht es, die Motor-Klasse als Abstraktion zu verwenden,
// während die tatsächliche Steuerung des Motors über die M28BYJ_48-Klasse erfolgt.
class MotorWrapper : public Motor {
public:
class M28BYJ_48Wrapper : public M28BYJ_48 {
// ... (s.o.)
};
/** Konstruktor, der die Motor-Instanz mit den entsprechenden Pins und Einstellungen initialisiert.
* @param pin1..pin4 Die Pins des Motors.
* @param gearClearance Das Getriebeübersetzungsverhältnis des Motors.
* @param dirChange Gibt an, ob die Richtung des Motors umgekehrt werden soll (true) oder nicht (false).
*/
MotorWrapper(uint8_t pin1, uint8_t pin2, uint8_t pin3, uint8_t pin4, uint8_t gearClearance, bool dirChange) {
motor = new M28BYJ_48Wrapper(pin1, pin2, pin3, pin4, gearClearance);
directionChange = dirChange;
}
// Initialisiert den Motor
void begin() {
motor->begin();
}
// Deinitialisiert den Motor
void end() {
motor->end();
}
// Setzt die Richtung des Motors und aktualisiert die Richtung in der M28BYJ_48-Instanz entsprechend.
void setDirection(int dir) override {
if (directionChange)
dir = -dir;
Motor::setDirection(dir); // Aktualisiert die Richtung in der Basisklasse
if (direction >= 0)
motor->setDirection(Direction::CW);
else
motor->setDirection(Direction::CCW);
}
// Bewegt den Motor um einen Schritt in die angegebene Richtung
virtual void ARDUINO_ISR_ATTR step() override {
Motor::step(); // Aktualisiert die Position basierend auf der Richtung
if (direction > 0)
motor->stepCW();
else
motor->stepCCW();
}
// Setzt die Position des Motors zurück auf 0.
void resetPosition() {
Motor::resetPosition();
}
protected:
M28BYJ_48Wrapper* motor;
bool directionChange; // Gibt an, ob die Richtung des Motors umgekehrt werden soll (true) oder nicht (false).
};