Selbst balancierender Roboter

Hey zusammen,

Ich bin gerade an einem Projekt beschäftigt in dem ich einen selbst-balancierenden Roboter baue. Ich verwende dabei einen Arduino Nano, 2x Nema 17 Stepper Motor, 2x a4988 Treiber, Mpu6050. Hier der Code welcher ich verwende:

#include <MultiStepper.h>
#include <Wire.h>
#include <MPU6050.h>

MPU6050 mpu;

const int enaPin1 = 4;
const int stepPin1 = 5;                                         // Definiert den Pin 5 am Arduino welcher mit dem Steppin des 1 Motortreiber verbunden ist.
const int stepPin2 = 6;                                         // Definiert den Pin 6 am Arduino welcher mit dem Steppin des 2 Motortreiber verbunden ist.
const int dirPin1 = 7;                                          // Definiert den Pin 7 am Arduino welcher mit dem Dirpin des 1 Motortreiber verbunden ist.
const int dirPin2 = 8;                                          // Definiert den Pin 8 am Arduino welcher mit dem Dirpin des 2 Motortreiber verbunden ist. 

AccelStepper stepper1(AccelStepper::DRIVER,stepPin1, dirPin1);  // Erstellt ein AccelStepper Objekt und konfiguriert es als Driver(Treiber) unter Verwendung der definierten Pins.
AccelStepper stepper2(AccelStepper::DRIVER,stepPin2, dirPin2);  // Erstellt ein AccelStepper Objekt und konfiguriert es als Driver(Treiber) unter Verwendung der definierten Pins.

float setpoint = 34.4;                                          // Definiert die Variabel setpoint(Sollwert) als 0 (Abweichung von der Gleichgewichtslage soll 0 Grad sein)
float Kp = 80.0;                                                 // Definiert die PID Reglerkonstante Kp als 40                                  
float Ki = 0.0;                                                  // Definiert die PID Reglerkonstante Ki als 0.0
float Kd = 10.0;                                                 // Definiert die PID Reglerkonstante Kd als 3.0

float input, output, error;                                     // Deklatiert Variabel für den PID Regler (Aktueller Winkel, PID-Ausgabewert, Aktueller Fehlerwert).
float lastError = 0;                                            // Deklariert Variabel für Fehlerwert der vorherigen Iteration.
float integral = 0;                                             // Deklariert Variabel für den Integralanteil des Fehlers.

unsigned long lastTime;                                         // Deklariert eine Variabel für die Zeitmessung in millis(Millisekunden).

void setup() {
  Serial.begin(115200);                                         // Setzt die Baudrate (bits per second) für die serielle Datenübertragung fest. 
  Serial.println("Start initialize Mpu...");
  Serial.println("Initialize Wire...");
  Wire.begin();                                                 // Initialisiert die Wire Library und die I2C-Kommunikation
  Serial.println("Wire.h success");
  mpu.initialize();                                             // Initialisiert den MPU6050
  Serial.println("Mpu initialize success");
 
 Serial.println("Starting connection to MPU6050...");
 
  if (!mpu.testConnection()) {                                  // Überprüft die Verbindung (bei "false" wird der Loop ausgeführt, bei "true" nicht) ("!" wird als Umkehrwert verwendet)
    Serial.println("MPU6050 connection failed");                // Falls Verbindung fehlgeschlagen wird "MPU6050 connection failed" ausgegeben
    while (1);                                                  // Falls Verbindung fehlgeschlagen bleibt das Programm in einer Endlosschlaufe stecken
  }

Serial.println("MPU6050 connected successfully.");

pinMode(enaPin1, OUTPUT);
digitalWrite(enaPin1, LOW);

stepper1.setMaxSpeed(2000);                                     // Setzt die maximale Geschwindigkeit auf 2000 (Schritte pro Sekunde)
stepper1.setAcceleration(1000);                                 // Setzt die maximale Beschleunigung auf  1000 (Schritte pro Sekunde^2)
stepper2.setMaxSpeed(2000);                                     // Setzt die maximale Geschwindigkeit auf 2000 (Schritte pro Sekunde)
stepper2.setAcceleration(1000);                                 // Setzt die maximale Beschleunigung auf 1000 (Schritte pro Sekunde^2)

calibrateSetpoint();

lastTime = millis();                                            // Speichert die aktuelle Zeit in Millisekunden
}

void calibrateSetpoint() {
  float sumAngles = 0;
  const int numSamples = 1000;

  for (int i = 0; i < numSamples; i++) {
    unsigned long currentTime = millis();   	                    // Definiert eine Variabel in Millisekunden
    float deltaTime = (currentTime - lastTime) /1000.0;           // Berechnet die Zeitdifferenz zwischen zwei Loopdurchläufen -> deltaTime
    lastTime = currentTime;
    int16_t ax, ay, az;
    int16_t gx, gy, gz;
    mpu.getMotion6(&ax, &ay, &az, &gx, &gy, &gz);

    float angleY = atan2(ax, az) * 180 / PI;
    float gyroRateY = gy / 131.0;
    static float angle = 0;
    angle = (0.98 * (angle + gyroRateY * deltaTime) + (1 - 0.98) * angleY);
    sumAngles += angle;

    delay(1); // Optional: etwas Verzögerung zwischen den Messungen
  }

  setpoint = 1 + (sumAngles / numSamples);
  Serial.print("Calibrated Setpoint: ");
  Serial.println(setpoint);
}

void loop() {
  unsigned long currentTime = millis();   	                    // Definiert eine Variabel in Millisekunden
  float deltaTime = (currentTime - lastTime) /1000.0;           // Berechnet die Zeitdifferenz zwischen zwei Loopdurchläufen -> deltaTime
  lastTime = currentTime;                                       // Setzt die Variabel lastTime auf den selben Wert wie die Variabel currentTime

  int16_t ax, ay, az;
  int16_t gx, gy, gz;
  mpu.getMotion6(&ax, &ay, &az, &gx, &gy, &gz);
  float angleY = atan2(ax, az) * 180 / PI;
  float gyroRateY = gy / 131.0;
  const float alpha = 0.98;
  static float angle = 0;
  angle = (alpha * (angle + gyroRateY * deltaTime) + (1 - alpha) * angleY);
  input = angle; 
  

  error = setpoint - input;                                     // Unterschied zwischen Sollwert und aktuellem Wert
  integral += error * deltaTime;                                // Die Summe aller vergangenen Fehler, gewichtet mit der Zeit, um den kumulierten Fehler zu berücksichtigen.
  float derivative = (error - lastError) / deltaTime;           // Die Rate der Änderung des Fehlers, um die Reaktion auf schnelle Änderungen zu berücksichtigen.
  output = (Kp * error + Ki * integral + Kd * derivative);      // Das kombinierte Ergebnis des PID-Reglers, das als Steuersignal verwendet wird.
  lastError = error;                                            // Setzt den Lasterror auf den selben Wert wie Error für den nächsten Umlauf

float motorSpeed = constrain(output, -1800, 1800);            // Begrenzt Output auf den Bereich -1000 bis 1000

 if (abs(error) > 1.0) {  
    stepper1.setSpeed(motorSpeed);
    stepper2.setSpeed(-motorSpeed);

    stepper1.runSpeed();
    stepper2.runSpeed();
  } else {
    stepper1.setSpeed(0);
    stepper2.setSpeed(0);
  }

  Serial.print("Angle: ");                                      // Für Debugging
  Serial.print(input);                                          // Für Debugging
  Serial.print(" Output: ");                                    // Für Debugging
  Serial.println(output);                                       // Für Debugging
  Serial.print(" motorSpeed: ");  
  Serial.print(motorSpeed); 
  Serial.print(" error: ");  
  Serial.print(error); 
} 

Leider balanciert der Roboter nicht und ich weiss nicht so genau woran das liegt. Ebenso ist der Winkel oft 2-3 Grad neben dem Sollwert (Setpoint) welcher ja vorher basierend auf den aktuellen Winkeldaten definiert wird. Mit dem PID-Faktoren habe ich bereits viel ausgetestet, jedoch ohne wirklich Erfolg zu haben.

Vielen Dank für eure Hilfe und Tipps
LG

Hallo
Also balanciert er doch. Nur der Winkel stimmt nicht.

Und wie sollen wir da helfen, die grundlegenden Funktionen kannst doch nur du testen.
Ich denke mal das Ding soll immer im Wasser stehen. Auch wenn es auf einer schiefen Ebene steht. Oder wie soll ich mir das Ding vorstellen

Sorry so geht das ja nicht. Wenn er auf einer schiefen Ebene steht läuft ja das Wasser weg :thinking::wink:

Hallo,
wo hast Du denn den Sketch überhaupt her. Der Ausschnitt soll ja eigentlich den PID Regler darstellen. Ich hab da jetzt nicht im einzelnen nachvollzogen. Warum nutzt Di nicht ddie standard PID Regler Lib.

Letztlich hast Du eine Positionsregelung die Du mit einer Geschwindigkeit als Stellgrösse anfahren willst. Das kann man so machen. Du hast ja allerdings zwei Stepper die sich eigentlich hervorragend dazu eigen eine Position anzufahren.
messen
Position berechnen
Stepper ansteuern

die Frage ist allerdings wie dynamisch das sein muss.
Hier so ein paar Fragen die Du dir stellen solltest.
Du schreibst der Winkel wird manchmal nicht richtig eingestell. Wie sieht das denn mit der Berechnung der Geschwindikeit aus. Was gibt es für Rundungsfehler. Was ist die kleinste Geschwindigkeit die noch gefahren werden kann. Wie hoch ist die bleibende Regeldifferenz. Um die weg zu bekommen benötigt man einen I-Anteil am Regler.

Die Beschreibung "balanciert nicht" ist natürlich jetzt nicht so detailiert. Und wenn er nicht balanciert, welcher Winkel ist dann oft neben dem Sollwert?
Fällt er einfach um? Schwingt er sich auf?

Meine Tipps:
Am besten die Regler-Parameter irgendwie einstellbar machen, ohne das Programm immer neu übertragen zu müssen, damit man schnell rumprobieren kann. Kleine Wertänderungen machen u.U. einen großen Unterschied.
Erstmal nur mit dem P-Regler anfangen, bis er halbwegs balanciert.
Die Regelung nicht zu schnell machen.
Und natürlich ist der physikalische Aufbau auch entscheidend. Wie sieht der aus?

Da habe ich erhebliche Zweifel, dass das funktioniert, und schnell genug reagiert. Der Aufruf von runSpeed erzeugt maximal einen Step. D.h. so wie das programmiert ist, wird pro loop() - Durchlauf höchstens ein Step erzeugt. Und bei dem was Du da alles machst - vor allem auch die Druckausgaben in jedem loop - kann das nichts werden.

So eine Regelung rein über die Drehzahl könnte man vermutlich besser mit einem DC-Motor aufbauen.

Vielen Dank schon mal für die Antworten. Ich habe da wohl etwas wage formuliert un versuche es nun ein bisschen genauer. Das ganze ist ein stabiles Holzkonstrukt. Der Mpu befindet sich ganz oben auf dem Gerüst. Die Idee ist über die Winkelabweichung zum Sollwert (Balancierlage) und den PID die Motoren so anzusteuern das der Roboter balanciert. Mit den Motoren(mit der Software für die Motorsteuerung) habe ich bemerkt das irgendetwas nicht stimmen kann da sie bei einem bisschen höhere Kd wert beginnen herrumzuspringen, obwohl ich die maxspeed so eingestellt habe das er eigentlich keine Schritte auslassen kann. Wisst ihr evtl. wie ich dieses Problem lösen könnte( evtl. andere Libary)

Mit „nicht balancieren“ meine ich das wenn ich in in der Luft halte und in beginne zu neigen funktionieren die Motoren gut, denke ich jedenfalls. Auch die Daten im Serial Monitor machen Sinn, aber ich habe keine Chance ihn in irgendeiner Weise auch nur einige Sekunden zu balancieren. Grundsätzlich habe ich das Gefühl das die Motoren zuviel machen bei kleinen Abweichungen und bei grossen Abweichungen zu wenig.

Lg

Schmeiß mal alle Serial.print aus deinem loop raus, um zu sehen, ob das eine Veränderung beim Verhalten der Motore bringt.

Hab ich bereits gemacht. Es läuft nun zwar runder aber es ist noch weit entfernt von einem stillen balancierenden Stand.
Wie ja schon erwähnt wurde wird pro Loop immer nur ein Schritt ausgeführt. Ich sehe dabei ein bisschen schwarz, wie soll der PID mit nur einem Schritt als Output etwas am System bewirken das wirklich sinnvoll ist?
Gibt es evtl. eine Art Schrittmotoren anzusteuern, bei der man nicht auf sie Loopdauer angewiesen ist?

Du könntest die MobaTools probieren. Da werden die Schritte unabhängig vom loop() in Timerinterrupts erzeugt. Auf einem 16MHz AVR sind max. 2500 steps/sek möglich. MoToStepper ist aber primär ein Tool zur Positionierung und nicht zur Drehzahlsteuerung.

Hallo,
ich hab das mit dem Schrittmotor noch nicht verstanden. Wird da jetzt insgesamt nur eine halbe Umdrehung gefahren , ist da ein Getriebe dran, oder eine Spindel ? wie muss ich mir das vorstellen. (Foto, Skitze)
Wenn es sich nur um einen Winkel bis 180 ° handelt würde ich mal über Servos nachdenken die sind schnell.
@MicroBahner hatte ja bereits angesprochen das DC Motoren eventuell besser geeignet sind, um mittels Drehzahl eine Position anzufahren. Grundsätzlich denke ich das auch wenn man eine PID Regler nutzen will. Allerdings einen DC Motor nur den Teil einer Umdrehung zu fahren geht nicht wirklich. Dazu müsste man dann ein Getriebemotor einsetzten. Und dann sind wir sehr schnell bei einem Servo mit dem man dann gleich eine Position anfahren kann.

das deutet darauf hin das die Verstärkung eigentlich schon zu hoch ist für kleine Abweichungen, da reichen ja kleine Geschwindigkeiten. Bei großen Abweichungen reicht die verfügbare Geschwindigkeit nicht aus um eventuell schnell genug das Ding am umfallen zu hindern.

nur mal so vor mich hin gedacht :melting_face:
Hast Du schon mal versucht nur über einen Vergleich <=> die Stepper zu fahren, das könnte man mit der lib von @MicroBahner auch mit einer hohen Geschwindigkeit machen links- stop- rechts. Das ging dann auch nicht mit jeweils nur einem Schritt. Eventuell geht das auch mit einer Rampe und einer Fensterfunktion bei dem Vergleicher. Eventuell auch zwei Geschwindigkeiten. Dann wäre man bei einer schnell, langsam, stop Variante die für Positionierung ja auch oft Verwendung findet.
Warum nutzt Du nicht nur Maschineneinheiten, dann kannst Du auf die ganze Rechnerei verzichten und machst die nur einmalig für den Sollwert bei der Eingabe bzw Ermittlung. Dem Regler ist das doch völlig egal mit was er arbeitet. Einfach die physikalische Einheit verwenden die der Istwert zur Verfügung stellt.