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 |
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.
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++.