Motivation

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.

In­halts­ver­zeich­nis

Motivation

Linien-Algorithmus (Duale Motorsteuerung)

Bewertung

Methode MotorControl

Download Projekt DualMotorControl

Multi-Motorsteuerung

Struktur

Klasse Motor

Klasse MotorControl

Methode MultiMotorControl

Algorithmus

Implementierung in C++

Klasse MultiMotorControl

Klasse MotorControlTicker

Download Projekt MultiMotorControl

Arduino- (ESP32-) Bibliothek

Steuerung von Schrittmotoren vom Typ 28BVJ-48

Klasse MotorWrapper

Linien-Algorithmus (Duale Motorsteuerung)

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:

Ausgabe des Programms 'DualMotorControl'

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):

Vergleich der Algorithmen   Vergleich der Algorithmen

Bewertung

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.

Methode MotorControl

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.

Approximation der Ziellinie

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 * 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:

Falsche Ansteuerung des nachlaufenden Motors

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.

Download Projekt DualMotorControl

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

Multi-Motorsteuerung

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:

Multi-Motorsteuerung   Darstellung der Steuerung

Struktur

Klasse Motor

Wie im vorhergehenden Beispiel dient die Klasse Motor als Schnittstelle zum anzusteuernden Motor:

Klasse 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;
}

Klasse MotorControl

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.

Klasse MotorControl

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.

Methode MultiMotorControl

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.

Algorithmus

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.

Implementierung in C++

Die benutzbare Implementierung in C++ ist etwas aufwändiger. Die Klassen Motor und MotorControl entsprechenden den in C# (s.o.).

Klasse MultiMotorControl

Die Klasse MultiMotorControl stellt die Methoden zur Steuerung der Methoden bereit:

Klasse MultiMotorControl

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.

Klasse MotorControlTicker

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

Download Projekt MultiMotorControl

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

Arduino- (ESP32-) Bibliothek

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:

Steuerung von Schrittmotoren vom Typ 28BVJ-48

Im Projekt Android steuert einen Schrittmotor über einen ESP32 wird eine Bibliothek vorgestellt, die einen Schrittmotor vom Typ 28BVJ-48 ansteuert.

Schrittmotor vom Typ 28BYJ-48

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.

Klasse MotorWrapper

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.

Klasse M28BYJ_48Wrapper
class M28BYJ_48Wrapper : public M28BYJ_48 {
public:
   M28BYJ_48Wrapper(uint8_t pin1, uint8_t pin2, uint8_t pin3, uint8_t pin4, uint8_t gearClearance)
      : M28BYJ_48(pin1, pin2, pin3, pin4, gearClearance) {
   }

   void ARDUINO_ISR_ATTR stepCW() {
      M28BYJ_48::internalStepCW();
   }

   void ARDUINO_ISR_ATTR stepCCW() {
      M28BYJ_48::internalStepCCW();
   }
};

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).
};