PID Regler einstellen - Mega-Probleme!

Hallo, brauche wieder einmal euren Tipp!
Für mein Schulprojekt (Tennisball-Wurf-Maschine) die 2 gegenläufige DC-Motoren hat, welche über Logic-MOS-FETs via PWM von einem Arduino angesteuert werden, versuche ich gerade den Regler einzustellen.
Ein entsprechendes Programm habe ich mir aus dem Internet #include <PID_v1.h> besorgt.
Momentan versuche ich, die Drehzahl eines Motors stabil zu halten. Leider nicht mit dem gewünschten Erfolg.
Die Soll-Drehzahl "Setpoint" kommt über einen analogen Input (Poti) herein.
Die IST-Drehzahl "Input" kommt von einer Gabellichtschranke, die die Impuls von einer 4-Lochscheibe an der Motorachse bekommt.
Die Regelung läuft zwar, aber der Motor läuft hörbar unruhig, speziell im unteren Drehzahlbereich.
Da ich kein Mathematiker bin und auch mit Regelungen nichts am Hut habe, würde ich doch gerne den Regler optimieren.

Wo ich Probleme habe:

  1. Ist die Loopzeit mit 1ms schnell genug für 2 Regelungen (hier nur eine zum Testen)
  2. Müsste der IST-WERT (Input) schneller zur Verfügung gestellt werden wie der
    Output(Stellgröße)? Gibt es Faustformeln oder Anhaltspunkte, in welchem Verhältnis die
    zueinander stehen müssen - sollten ...?
  3. 2-Regelungen parallel laufen lassen in einem Sketch, ich das möglich?

habe leider keine Ahnung ...

Hoffe auf eure Hilfe danke
Mad

Hier mein Code:

#include <stdio.h>
#include <PID_v1.h>

static int drehz_zaehler = 0;

//*******************************
//      Funktionen deklarieren
//*******************************
float drehz_ermitteln(int ms);
float drehz_ausgabe(int ms);
void blink_led(int ms);
void DREHZAHL_SENSOR();

//Define Variables we'll be connecting to
double Setpoint, Input, Output;

//Define the aggressive and conservative Tuning Parameters
double aggKp = 4, aggKi = 0.2, aggKd = 1;
double consKp = 1, consKi = 0.05, consKd = 0.25;

//Specify the links and initial tuning parameters
PID myPID(&Input, &Output, &Setpoint, consKp, consKi, consKd, DIRECT);

void setup()
{
  Serial.begin(38400);
  attachInterrupt(0, DREHZAHL_SENSOR, FALLING); // Encoder-Pin an Interrupt 0 (Pin 2)
  Input = 40;     // Solldrehzahl zu beginn
  Setpoint = 50;   // Führungsgröße
  //turn the PID on
  myPID.SetMode(AUTOMATIC);

  pinMode(2, INPUT);  // IR als Input
  pinMode(7, INPUT);  // Schalter für Start
  digitalWrite(2, HIGH);  // IR Pullup EIN
  digitalWrite(7, HIGH);    // Pullup
}

void loop()
{
  delayMicroseconds(100);
  //delay(1); // Loop-Zeit vorgeben (1ms)

  if (digitalRead(7) == HIGH)     // Motor start/stopp
  {
    analogWrite(3, 0);            // PWM auf 0 --> stopp Motor
  }
  else
  {
    Setpoint = analogRead(0) / 4; // Führungsgröße w vom Poti --> 1024 / 4 = 256 --> für das "analogeWrite" 8 bit -->  max 0-255 !!!
    drehz_ermitteln(30);          // Drehzahl ermitteln alle 30 ms (Loopzeit 1 ms * 30 = 30 ms)
    //*******************************
    //    *** SW-Regelung ***
    //*******************************
    double gap = abs(Setpoint - Input); //distance away from setpoint
    // *** default 10 ***
    if (gap < 10) //we're close to setpoint, use conservative tuning parameters
    {
      myPID.SetTunings(consKp, consKi, consKd);
    }
    else    //we're far from setpoint, use aggressive tuning parameters
    {
      myPID.SetTunings(aggKp, aggKi, aggKd);
    }

    myPID.Compute();
    drehz_ausgabe(1);             // Wert setzen alle 1 ms (Loopzeit 0,1 ms * 1 = 1 ms)
    // analogWrite(3, Output);    // Output max 255 (8 bit)
  }
}

// *****************************
//  Drehzahl Ermittlung/Amsgabe
// *****************************
void DREHZAHL_SENSOR()  {
  drehz_zaehler++;
}

Hier meine Unterfunktionen:

// *****************************
//  Drehzahl Ermittlung
// *****************************
float drehz_ermitteln(int ms)  {
  static int i_zaehler = 0;
  if (i_zaehler >= ms)  {
    Input = drehz_zaehler*30; // Umrechung auf Minuten
    drehz_zaehler = 0;            // IR-Zählvariable zurücksetzen
    i_zaehler = 0;
    Serial.print("Setpoint: ");   // Serial Monitor
    Serial.print(Setpoint);
    Serial. print("  -->  Input: ");
    Serial.print(Input);
    Serial.print("  --> Output: ");
    Serial.println(Output);   // Serial Plotter
  }
  i_zaehler++;
}

// *****************************
//  Drehzahl Ausgabe
// *****************************
float drehz_ausgabe(int ms)  {
  static int i_zaehler = 0;
  if (i_zaehler >= ms)  {
    i_zaehler = 0;
    analogWrite(3, Output);   // Output max 255 (8 bit)
    //Serial.println(Output);   // Serial Plotter
  }
  i_zaehler++;
}

drehz_zaehler mußt du volatile deklarieren.

Bevor ich jetzt deinen Code im Detail durchforste:
bekommst du ein brauchbares Drehzahlsignal?

Was mit noch aufgefallen ist:

Bau dir eine anständige Zeitschleife mit millis(), das mit delay ist murks.

Bei der Drehzahlermittlung die Zeitbasis aus einer delay basierten loop zeit zu ermitteln ist extrem ungenau und, verzeih den Ausdruck, ebenfalls Murks.

Bei einer Drehzahl von 1000 Umin bekommst du in 30ms 2 Impulse. Ab einer Drehzahl von 1500 Umin 3 Pulse. Dazwischen springt er immer hin und her.
Du misst also deine Drehzahl mit einer Auflösung von 500Umin.
Das ist sehr ungeeignet für eine Regelung!

Dein Regler kann gar nicht funktionieren, weil die Eingangssignale nicht passen.

Du musst zu allererst ein brauchbares Drehzahlsignal haben.

Nun kommt es drauf an, wie dein System ist.
Ist es sehr träge, kannst du einfach das Messfenster verlängern, (1 Sek: Auflösung 15Umin)
oder du vervielfachst die Zahl der Löcher.
oder du baust die Drehzahlerfassung um um misst die Zeit zwischen zwei Interrupts.

Hallo Gunther, danke für die rasche Rückmeldung. Die Loopzeit von 1ms verwende ich, damit ich mit einem Funktionsaufruf (funktion (1000)) eine Zeit realisieren kann, hier 1ms*1000 ergibt 1Sek. Alles in dieser Funktion wird jetzt 1 mal pro Sek. abgearbeitet ohne die Loopzeit von 1 ms zu verändern für die anderen Funktionen.
Die Drehzahlerfassung muss ich mir nochmal ansehen.

Danke Mad

Das ist ein völlig falscher Ansatz.

Nur weil du irgendwo eine delay(1); drinstehen hast, bedeuted das nicht, das die loop() 1ms braucht.
Es bedeutet nur, dass der Prozessor an dieser Stelle 1ms lang nichts tut.
Wie lange die loop läuft, hängt von den Befehlen ab, die du sonst noch drin hast.

ohne die Verwendung von millis() oder micros() kommst du da nicht weiter.
wenn du eine Funktion jede Sekunde mal abarbeiten willst, dann mach das so:

void MachWasAlle (unsigned long ms){
  static unsigned long lastcall;
  if (millis() - lastcall < ms) return;  // Abbrechen wenn zu früh
  lastcall = millis();

  Serial.println(millis());
} // end MachWasAlle

void setup() {
  Serial.begin(115200);
}
void loop() {
  MachWasAlle(1000);
}

Zu deiner Drehzahlerfassung:
der Fehler liegt nicht im Code.
Der Fehler liegt in deinem Konzept.

Hallo Gunther,
mit deiner Zeitfunktion läuft das natürlich genauer ... danke für den Tipp!
Das Problem bei meiner Regelung ist wahrscheinlich die Erfassung der Drehzahl.

Meine Fragen:

  • Zu schnelle Erfassung --> ständige Korrektur seitens Regler notwendig, oder...?
  • Messung der Drehzahl muss mindestens 10 Mal schneller sein, wie der Regler, oder liege ich da falsch?
  • Wie kann ich den zeitlichen Verlauf des Motors feststellen damit ich den Regler aufgrund dessen einstellen kann

Den Motor den ich betreibe läuft momentan im Leerlauf - kurzes Abbremsen (Störung) --> Regler regelt nach und Motor hält einigermaßen seine Geschwindigkeit (ca. 300 U/min). So genau weiß ich das nicht. Aber im Endeffekt soll der Motor mit ca. 4000 U/min laufen. Mit 4 Lochscheibe an der Motorachse und das noch dazu für 2 Motoren - eine schöne Leistung die der µC leisten muss.

Danke Mad

Wenn du etwas regeln willst, musst du den aktuellen Wert kennen.

Wenn du die Temperatur in einem Zimmer regeln sollst, aber dein Thermometer kaputt ist, hast du einfach keine Chance. Eben so wenig wie dein PID Regler. Wenn der die Drehzahl nicht kennt, kann er nicht regeln.

Also: zu allererst die Drehzahl vernünftig erfassen.
Probier mal den Sketch:

volatile unsigned long pulse[2] = {0, 0};
volatile byte flanke = 0;
unsigned long pulsAbstand;
unsigned long pulsAbstand_MittelW;
const int FF = 10;  // Filterfaktor für Mittelwert


// *****************************
//  Drehzahl Ermittlung Pulsabstand in µs
// *****************************
void DREHZAHL_SENSOR()  {
  if (flanke > 1) return;
  pulse[flanke] = micros();
  flanke++;
}


void setup() {
  Serial.begin(115200);
  attachInterrupt(0, DREHZAHL_SENSOR, FALLING); // Encoder-Pin an Interrupt 0 (Pin 2)
}

void loop() {

  if (flanke > 1) {
    noInterrupts();
    pulsAbstand = pulse[1] - pulse[0];
    flanke = 0;
    interrupts();
    Serial.println();
    Serial.print(pulsAbstand);
    pulsAbstand_MittelW = (pulsAbstand_MittelW * (FF - 1) + pulsAbstand) / FF;
  }


  /**************************************************************************************
  **   Beginn Zeitscheibe  100ms
  ***************************1***********************************************************/
  static unsigned long   Zeitscheibe_1 = 0;
  if (millis() - Zeitscheibe_1 > 100 ) {         // Zeitscheibe wird alle 100ms aufgerufen
    Zeitscheibe_1  = millis();


    Serial.print("\t Mittelwert: ");
    Serial.print(pulsAbstand_MittelW);
    Serial.print("\tDrehzahl: ");
    int Drehzahl = 15000000UL / pulsAbstand_MittelW;
    Serial.println(Drehzahl);

  }// end Zeitscheibe 100ms  *************************************************************
} // end loop

Zu deinen Fragen:
schnelle Erfassung und ständige Korrektur?
Was ist schnell und was ist ständig? Das steht und fällt mit deinem System. Aber bei einem mechanischen System sollte es reichen alle 100ms zu regeln. Mit der Methode die ich unten skizziert habe, wertet der Arduino nicht jeden Abstand aus, sondern (bei langsamen Drehzahlen) nur jeden zweiten. Hier könnte man noch optimieren.

Messung der Drehzahl 10 Mal schneller
nein. Natürlich ist es von Vorteil ein schnell abgetastetes, gefiltertes Signal zur Verfügung zu haben.
Aber es geht auch ohne. In deinem Fall ist die "Abtastrate" sowieso mechanisch vorgegeben durch die Löcher.

zeitlichen Verlauf des Motors feststellen
am einfachsten über den Seriellen Monitor.
Lass dir die Zeiten seriell ausgeben, kopiere sie in Excel, dort kannst du sie auch grafisch anzeigen.

Rechenleistung:
sollte kein Problem sein.
Einziges Problem könnte sein, dass sich die beiden Interrupts gegenseitig behindern, wenn sie gleichzeitig kommen. Aber auch dass sollte nur zu sehr geringen Fehlern führen.

Hallo madang,

eventuell hilft Dir das hier weiter, mir hat es auf jeden Fall geholfen :slight_smile:

Gruss
Walter

Hallo Walter, vielen Dank für den Tipp!
SG Mad

Hallo Gunther,
danke für die Mühe die du dir machst!
Musste mich erst in den für mich sehr komprimierten Code erst Mal einlesen.

Ein Paar Dinge verstehe ich hier nicht ganz:

  • Woher kommt der Inhalt der Variable "pulsAbstand_MittelW" - muss der nicht initialisiert werden?
  • Woher kommt die Zahl 15000000UL - hat das mit der Taktfrequenz des UNOs zu tun (16 Mhz)?
  • Wenn die Drehscheibe steht, zeigt der Serielle Monitor immer noch die letzte Drehzahl an

Danke Gunther
VG Mad

pulsAbstand_MittelW: hier wird ein gleitender Mittelwert der gemessenen Pulsabstände gebildet. Bei stabiler Drehzahlerfassung brauchst du das nicht.

15000000UL: Umrechnung des Pulsabstandes (in µs) in Drehzahl:
Drehzahl [U/min] =1000000 [µs>sek] *60 [sek>min] /4 [4Pulse pro Umdrehung] / Pulseabstand
1.000.000 * 60 / 4 = 15.000.000

Anzeige letzte Drehzahl: ja, das tut der Code sktuell so. Das ist ja auch nur ein Beispiel wie man Drehzahl anders (schneller) erfassen kann. Ein paar Sachen musst du noch ausssen rum bauen.
Nulldrehzahl könnte man z.b. über einen Timeout erkennen. Wenn länger als tbd sek flanke nicht mehr hochgezählt wurde, ist Drehzahl gleich Null.

Super, werde mal einiges davon umsetzen. Danke, melde mich wenn es läuft. ...
LG Mad

Hallo Gunther,
wenn ich 2 Motoren mittels PID-Regler laufen lassen möchte, wie muss ich diese PID-Lib einbinden/ändern/..., damit ich sie unabhängig von einander regeln kann? Habe keine Ahnung, wie dies zu realisieren ist.
Die Zeitmessung ist mir klar, dank deiner Vorlage :slight_smile: !

  • Die Freilaufdioden für die DC-Motoren, wie viel müssen die eigentlich aushalten, wenn ich DC-Motoren 12V/70W mittels PWM ansteuere?

Danke
Mad

Die Freilaufdioden müssen den gleichen Strom aushalten der durch den Motor fließt, als min 6A, besser 8A oder 10A.

Du sollest keine normalen Gleichrichterdioden nehmen, sondern Fast Recovery dioden.

Zur PID-Lib:
du must 2 Instanzen anlegen.

  //Specify the links and initial tuning parameters
  PID myPID1(&Input1, &Output1, &Setpoint1,2,5,1, DIRECT);
  PID myPID2(&Input2, &Output2, &Setpoint2,2,5,1, DIRECT);

Die Parameter am besten auch als Variable, aber du kannst wahrscheinlich für beide Regler die gleichen Variablen nehmen.

Super, danke...
Welche Sperrspannung muss so eine Diode aushalten. Wird das nur geschätzt oder kann das auch ein Bastler wie ich berechnen?
DANKE MAD

Die maximale Betriebsspannung des Motors, den für diese sperrt sie.

Gruß Tommy

Aber ist die Spannung beim Ausschalten nicht ein vielfaches Höher, als die Motorspannung?

Gruß Mad

Nicht, wenn du eine Freilaufdiode hast....

Geeignet wären:
MUR810, MUR815, MUR1510, MUR1515, SF61, SF63, 1N5811
oder Schottky: SR1504, MBR745, STPS745,
und viele andere mehr....

Ich nehme immer gerne die Parametersuche von Farnell, da kann man die Suche sehr schön eingrenzen:
Dioden

madang:
Aber ist die Spannung beim Ausschalten nicht ein vielfaches Höher, als die Motorspannung?

Gruß Mad

Für die induzierte Spannung beim Ausschalten ist sie leitend, sollte aber 1,2 bis 1,5 des Motorstroms vertragen.

Gruß Tommy

Super, danke für eure Hilfe. ...
Habe wieder eine Menge dazugelernt :slight_smile:
VG Mad