5 Programmierung des Mikrocontrollers

Der Arduino kann auf verschiedene weisen programmiert werden. Zur Programmierung wird hier die Arduino IDE verwendet. Das Programm benötigt dabei folgenden Funktionsumfang:

  • Initialisierungssequenz und zyklischer Programmaufruf
  • Auswertung des Encoders mittels eines Quadraturdecoder
  • PID Controller für Spannung und Stromregelung
  • LCD Anzeige
  • Menü zur Steuerung
  • PWM Signalerzeugung mit Totzeit
  • 1ms Timerinterupt Routine

 

5.0 Programm Lizenzen

Folgende Programmteile besitzen folgende Lizenzen:

  • LiquidCrystal.h , Arduino.h und andere Standard Bibliotheken der Arduino IDE (Arduino Core)   --> GNU Lesser GPL Lizenz (vereinzelt auch MIT und ISC)
  • Meine Programmteile         --> MIT-Lizenz 
  • Arduino Bootloader            --> GNU GPL Lizenz

Prinzipiell können Sie den Arduino Nano auch ohne den Bootloader Programmieren. Sie benötigen hierfür nur einen Programmer oder einen zweiten Arduino der als Programmer Programmiert ist.

 

Mein Programm unter die GPL zu stellen, möchte ich persönlich nicht. Ich verstehe zwar die Beweggründe hinter GPL, jedoch schränkt die Lizenz den Nutzer in der Lizenzierung ein. Weswegen ich eine deutlich freiere Lizenz, die MIT bevorzuge. Sollten Sie mein Programm nutzten wollen, finden Sie eine Auflistung aller Programmbibliotheken und ihrer Lizenzen, die ich im Arduino Core finden konnte, in der README.txt

 

5.1 Hauptprogramm

Im Hauptprogramm wird im setup() die Initialisierung der Programmteile gestartet. In der Programmschleife wird lediglich das Menü alle 250 ms aufgerufen.

5.2 Der Quadraturdecoder

Ein inkrementaler Encoder oder Drehgeber gibt in der Regel 2 Rechtecksignale an zwei Kanälen (A,B) aus. Die Signale sind dabei Versetzt. Hierdurch kann auf die Drehrichtung geschlossen werden. Werden die Signalflanken gezählt, kann der zurückgelegte Winkel ermittelt werden.

Wird jede Signalflanke gezählt, vervierfacht sich die Auflösung des Encoders. Der KY-40 hat 20 Inkremente pro Kanal und Umdrehung. Durch den Quadraturdecoder resultiert eine Auflösung von 80 Inkrementen pro Umdrehung. Höhere Microkontroller, wie zum Beispiel ein STM32f103, besitzen ein Hardwaremodul, welches die Aufgabe eines Quadraturdecoders übernimmt. Der Arduino besitzt keinen Hardwarequadraturdecoder. Die Aufgabe muss Softwaretechnisch umgesetzt werden. Um die Signalflanken auszuwerten, werden die Interruptpins 2 und 3 des Arduinos benötigt. Dabei können die Zustände von den beiden Kanälen überwacht werden. Es ergeben sich 4 Mögliche Zustände. Vom jeweils aktuellen Zustand ist ein Wechsel in nur zwei Richtungen möglich.

Bei einem Flankenwechsel auf Kanal A, wird der vorherige Zustand von A und der derzeitige Zustand von A abgefragt. Sind beide gleich, wurde eine fehlerhafte Flanke erkannt. Wenn sich beide unterscheiden, wird der vorherige Zustand von A und B abgefragt. Waren beide Null oder beide Eins, hat sich der Encoder im Uhrzeigersinn gedreht. War vorher nur einer von beiden gesetzt, hat sich der Encoder gegen den Uhrzeiger gedreht. Abschließend wird der Aktuelle Zustand von A gespeichert.

 

Bei einer Flanke am Kanal B, erfolgt die Prüfung äquivalent. Waren hier beide Zustände gleichzeitig Null oder Eins, fand eine Drehung gegen den Uhrzeigersinn statt. War nur einer von beiden gesetzt, mit dem Uhrzeigersinn. Auch hier wird Abschließend der aktuelle Zustand gespeichert.

 

Mit jeder Flanke, wird ein Zähler je nach Drehrichtung hoch oder runter gezählt. Somit kann auf den Encoderwinkel zurück geschlossen werden. Alternativ kann über den Zählwert eine Menüsteuerung realisiert werden.

 

5.3 Der PI-Regelalgorithmus

Da ich den Regler als Objekt Programmiert habe, hier ein kurzer Ausflug in die Objektorientierte Programmierung. 

 

5.3.1 Objektorientierte Programmierung

Der Regelalgorithmus wird zwei mal im Programm benötigt. Ein wesentlicher Vorteil von C++ im Vergleich zu C, ist die Möglichkeit objektorientiert zu Programmieren. Der Vorteil ist, dass die Programmfunktionen einfach vervielfältigt werden können. Es muss lediglich ein neues Objekt erzeugt werden. Theoretisch kann dies auch als Funktion umgesetzt werden. Das Variablenhandling gestaltet sich hierbei jedoch schwieriger und die Programmübersichtlichkeit ist schlechter. Mit Hilfe der Objektorientierten Programmierung braucht nur das Objekt Regler zweimalig erzeugt werden.

Der erste Schritt ist die Erzeugung der Objektklasse in der Header Datei. In der Class werden alle Variablen und Funktionen in puplic und privat eingeteilt. In private sind die Variablen und Funktionen die nur im Objekt selbst sichtbar sind. In puplic sind alle Variablen und Funktionen die nach Außen sichtbar sind.

 

class Gasters_PID_Controller

{

    private:                                                  // alles was folgt, ist nur für den internen Gebrauch durch die Klasse selbst:

    int KP,KI,KD;                                      // Reglerverstärkungen

    int32_t Int, Int_max, Int_min;            // Integralwerte und Limits

    int last_val;                                         // letzter Wert der Regelabweichung für Differential

    int Sat_max, Sat_min;                       // Satturation --> Streckenlimits

 

    public:                                              // alles was folgt, ist auch nach außen sichtbar

 

    // Konstruktor um Daten im Objekt zu speichern beim Aufruf

    Gasters_PID_Controller (int KP, int KI, int KD, int Int_min, int Int_max, int Sat_min, int Sat_max);

 

    // Functionsprototypen

    int PID_controll (int Soll, int Ist);

    void Set_Int_Limit (int IntMax, int IntMin);

    void Set_Sat (int SatMax, int SatMin);

    int get_Int_val (void);

    void set_Int_val (int val);

};

 

Als zweiter Schritt wird in der Cpp Datei der Konstruktor erzeugt. Mit Hilfe des Konstruktors können dem Objekt bei Erzeugung, Variablen oder Werte übergeben werden.

 

Gasters_PID_Controller::Gasters_PID_Controller (int KP, int KI, int KD, int Int_min, int Int_max, int Sat_min, int Sat_max)

{

    this->KP = KP; …...

 

Der „::“ Operator weist die Funktion der Klasse Gasters_PID_Controller zu. Mit dem „this→“ Operator wird eindeutig definiert, dass es sich bei der Variable um die Variable des erzeugten Objektes handelt. Als dritter Schritt werden die vom Objekt benötigten Funktionen geschrieben. Das Objekt wird an entsprechender Stelle durch den Aufruf des Konstruktors erzeugt. Dem Objekt muss dabei ein Name vergeben werden. Beim Aufruf werden dem Regler seine Starteigenschaften zugewiesen.

 

Gasters_PID_Controller Current_Controller(Regler_KPi, Regler_KIi, 0, Regler_IntIMin, Regler_IntIMax, Regler_SatIMin, Regler_SatIMax);

 

5.3.2 Der Regelalgorithmus

Im Regelalgorithmus wird zuerst die Regelabweichung x gebildet.

Für den Integralteil, werden die Regelabweichungen aufsummiert.

Danach wird geprüft ob die Summe in seinen Grenzen liegt. Ist dies nicht der Fall, wird der Summenwert auf seine obere oder untere Grenze gesetzt. Um aus der Summe ein Integralwert zu erzeugen, muss die Abtastzeit mit dem Summenwert multipliziert werden. Da die Abtastfrequenz 1024 Hz beträgt, kann der Summenwert alternativ durch 1024 geteilt werden. Dies kann bei Integerwerten auch durch Schiebeoperatoren sehr schnell vom Mikrocontroller bewerkstelligt werden. Eine Division durch 1024 entspricht einer Rechtsschiebeoperation um 10 Stellen.

Die Regelausgangsgröße berechnet sich aus der Proportionalverstärkung Kp und der Regelabweichung x, sowie aus der Integralverstärkung KI und dem Integralwert. Um die Genauigkeit zu verbessern, wird der Summenwert erst mit der Integralverstärkung multipliziert und anschließend durch die 1024 geteilt.

5.4 LCD Anzeige

Für die LCD Anzeige wird die Standard Bibliothek LiquidCrystal.h von der Arduino IDE verwendet. Zur Kontrast und Helligkeitssteuerung, werden die PWM Ausgänge 5 und 6 verwendet. Zur Ansteuerung der Helligkeit und Kontrast wird die Standard Funktion analogWrite genutzt. Für das Display muss ein Objekt erzeugt werden. Im Konstruktor werden dabei die verwendeten Pins übergeben.

 

LiquidCrystal lcd(RS, E, D4, D5, D6, D7);

 

Mit lcd.begin wird festgelegt welche Größe das Display besitzt.

 

lcd.begin(Spalte,Zeile);

 

Weitere wichtige Befehle:

  • lcd.clear();
  • lcd.setCursor(x,y);
  • lcd.print(Wert oder „String“);

5.5 Steuerungsmenü

Im Steuerungsmenü können die Sollspannung, der Maximalstrom vorgegeben werden. Es kann weiterhin die Helligkeit und der Kontrast eingestellt werden. Bei Auswahl von z.B. Vo (Vout), ändert sich die Bezeichnung zu Vs (Vsetzten) und der Coursor links daneben fängt an zu Blinken.

Ist der Ausgang nicht freigeschaltet, steht oben Links im Menü Ein. Wird „Ein“ im Menü ausgewählt, ist der Spannungsausgang freigegeben. Der Schriftzug ändert sich zu Aus.

Weiter im Menü kann die Helligkeit und der Kontrast eingestellt werden.

 

Bei dem Menü handelt es sich Programmiertechnisch um eine Statemaschine. Es wird zyklisch der Encoderzählwert abgefragt. Bei Betätigung des Encodertasters wird je nach Zählwert in den entsprechenden Status gewechselt. Im jeweiligen State kann dann der Wert durch drehen des Encoders eingestellt werden. Das Einstellen der Regelwerte im Menü ist derzeit nicht möglich.

 

5.6 PWM Signalerzeugung mit Totzeit

Als PWM-Modus wird die Phasen korrekte und Frequenzrichtige PWM Erzeugung genutzt. Um eine möglichst hohe PWM Auflösung zu erreichen wird der 16 Bit Timer 1 verwendet. Die Halbbrücke ist low-aktiv. Überlappen sich beiden Signale, sind beide MOSFETs gesperrt. Dieser Zustand wird Totzeit genannt und ist wichtig da die Mosfets verzögert Aus- und Einschalten. Dies ist in der folgenden Abbildung zu sehen.

Zur Totzeiterzeugung werden zwei unterschiedliche PWM Werte benötigt. Die Totzeit wird vom Lowside Wert dazu addiert und vom High Side Ausgang abgezogen.

5.6.1 Konfiguration des PWM-Timer

Für die Konfiguration wird das Datenblatt des Atmega 328p benötigt. Die PWM Pins sind im DDR Register als Ausgänge zu definieren.

 

    pinMode(9, OUTPUT);       //A

    pinMode(10, OUTPUT);     //B

 

Für den oberen Mosfet, muss der Ausgang OC1A beim Hochzählen gesetzt werden und beim Runterzählen zurückgesetzt werden (Pin 9). 

 

Bitsetzen:

Ein bitweises "Oder" mit der Maske 1<<COM1A1 (eine 1 um COM1A1 Stellen nach links schieben) setzt das entsprechende Bit. Ein bitweises "Und", mit einer negativen (~) Maske wird zum löschen des Bits genutzt.

 

    TCCR1A = TCCR1A | (1<<COM1A1) & ~(1<<COM1A0);

 

Die PWM des unteren Mosfet, muss beim Hochzählen zurückgesetzt und beim Runterzählen gesetzt werden. (Pin 10)

 

    TCCR1A = TCCR1A | (1<<COM1B1) | (1<<COM1B0);

 

Als PWM wird die Phasenrichtige und Frequenzrichtige PWM benutzt. Das Match erfolgt auf ICR1. Das ermöglicht das Einstellen der PWM Frequenz.

 

    TCCR1A = TCCR1A & ~(1<< WGM10) & ~(1<< WGM11);

    TCCR1B = TCCR1B & ~(1<< WGM12) | (1<< WGM13);

 

Der Zählertakt ist der interne CPU Takt, es gibt keinen Vorteiler und der Zähler wird aktiviert.

 

    TCCR1B = TCCR1B & ~(1<<CS11) & ~(1<<CS12) | (1<<CS10);

 

Mit dem ICR1 Register wird der Maximale Zählwert festgelegt.

 

    ICR1 = PWM_MAX;

 

Die Bits im TCCR1C Register werden nicht benötigt.

 

    TCCR1C = TCCR1C & ~(1<<FOC1A) & ~(1<<FOC1B);

 

Da die Gatetreiber die Signale invertiert, wird das PWM Signal auf seinen Maximalwert gesetzt um keine Ausgangsspannung beim Einschalten zu erzeugen.

 

    OCR1A = PWM_MAX;

    OCR1B = PWM_MAX;

 

Der PWM Wert wird auch auf sein Maximum gesetzt, wenn im Menü der Ausgang ausgeschaltet wird. Ist der Ausgang aktiv geschaltet, wird die PWM Vorgabe des Reglers auf die Limits geprüft und auf den PWM Wert die Totzeit dazu addiert bzw. subtrahiert.

 

   if (value > PWM_LIMIT)

       value = PWM_LIMIT;

    else if (value < 0)

        value = 0;

        value = PWM_MAX - value;

    OCR1A = value + DEADTIME;

    OCR1B = value - DEADTIME;

 

Da keine Vorteiler für den Zähler verwendet wurde, berechnet sich die Totzeit nach CPU Takten. Bei 16 MHz ergeben sich pro Takt 62,5ns. Für eine Totzeit von 500ns, muss das Signal 8 CPU Takte verzögert schalten.

 

    #define DEADTIME 8 // 8 Takte Totzeit, 1 Takt 62,5ns --> 500ns

 

Der Maximalwert der PWM richtet sich nach der geforderten Frequenz von 20kHz. Für 20kHz muss sich das Signal alle 50 us Wiederholen. Der Zähler muss demnach 800 Takte Zählen. Da der Zähler auf und ab zählt, ist der maximale PWM Wert 400.

 

    #define PWM_MAX 400

 

Das Limit gibt an bei welchen Wert das Bootstrapping noch funktioniert. Diese Grenze wurde bei dem Schaltungsaufbau experimentell ermittelt. Bis zu einem Wert von 350 funktionierte das Bootstrapping bei dem DIY Gatetreiber.

 

    #define PWM_LIMIT 350

 

5.7 Die 1024 Hz Interrupt Routine

Für die Regelung ist es wichtig, dass die Regelalgorithmen mit einer Frequenz von 1024 Hz möglichst genau Aufgerufen werden. Hierfür wird ein Timerinterrupt ausgelöst. Beim Arduino Nano wird hierfür der Timer 2 verwendet. Die 1024 Hz resultieren daraus, dass eine Division im Regelalgorithmus mit 1024 durch eine Schiebeoperation mit wenig Prozessorauslastung umzusetzen ist.

 

5.7.1 Konfiguration

Zunächst werden alle Timer 2 Konfigurationen gelöscht.

 

    TCCR2A = 0;

    TCCR2B = 0;

    TIMSK2 = 0;

 

Der Timer wird im Clear Timer on Compare Match (CTC) Mode Konfiguriert.

 

    TCCR2A |= (1<<WGM21);     // CTC Mode

 

Da der Timer nur bis zum Wert 255 Zählen kann, wird ein Vorteiler von 64 benötigt.

 

    TCCR2B = TCCR2B | (1<<CS22); // Prescaler = 64

 

Der Vergleichswert für das Compare Register beträgt 244 - 1. Es werden 244 Zählwerte benötigt, die 0 wird mitgezählt weswegen eine 1 abgezogen wird.

 

    OCR2A = 244 -1;           // Vergleichswert für 1024 Hz (1.024,59 Hz)

 

Das Interrupt bei compare match muss aktiviert werden.

 

     TIMSK2 |= (1<<OCIE2A); // Enable Interupt on Compare match

 

5.7.2 Die Interrupt Service Routine

Die Interrupt Service Routine wird durch den TIMER2_COMPA_vect Vektor aufgerufen.

 

    ISR(TIMER2_COMPA_vect)

   {

 

In der Routine werden die Analogwerte ausgelesen.

 

    Analog_Iout = analogRead(A7) + Ioffset - 512;

    Analog_Vout = analogRead(A6);

    Analog_Vin = analogRead(A1);

 

Die Strommessung weicht ca. um den Faktor 1,25 ab. Diese Abweichung kann durch Multiplikation mit dem Faktor ausgeglichen werden.

 

    Analog_Iout = (Analog_Iout * 10) >> 3; // 1,25 Kalibrierungsfaktor

    if (Analog_Iout < 0 || Analog_Iout > 1024)

        Analog_Iout = 0;

 

Um den Voltage Control Factor zu ermitteln, werden die Spannungsmessungen der Eingangsspannung über 1024 Werte gemittelt. Für den VCFaktor gilt:

Um die Genauigkeit zu erhöhen wird der PWM Wert mit 1024 multipliziert. Bei der späteren Rückrechnung zu einem PWM Wert muss dies berücksichtigt werden.

 

    // --------- Calculate VC Factor ----------------

    Mean_Vin = Mean_Vin + Analog_Vin;

    counter++;

    if (counter > 1023 ) // jede Sekunde VC Faktor aktualisieren

   {

        counter = 0;

        if (Mean_Vin > 1024)

            VcFactor = ((uint32_t)PWM_MAX << 10) / (Mean_Vin >> 10);

            Mean_Vin = 0;

   }

 

Um einen Kurzschluss zu detektieren, wird über 255 Werte die Ausgangsspannung und der Strom gemittelt. Ist die Ausgangsspannung kleiner als 2 Volt und der Strom größer als der eingestellte, wird ein Kurzschluss vermutet und der Spannungsausgang wird abgeschaltet.

 

// ---------- Kurzschluss Überwachung -----------

    Short_cir_counter++;

    Mean_Vout = Mean_Vout + Analog_Vout;

    Mean_Iout = Mean_Iout + Analog_Iout;

    if (Short_cir_counter > 254)

    {

        Short_cir_counter = 0;

        Mean_Vout = Mean_Vout >> 8; // durch 255

        Mean_Iout = Mean_Iout >> 8; // durch 255

        if (Mean_Vout < 37 && Mean_Iout > Analog_Imax)

             PWM_ON (false);

        Mean_Vout = 0;

        Mean_Iout = 0;

   }

 

Als letzter Schritt folgt der Aufruf der Regelungen. Zuerst wird die Spannungsregelung aufgerufen und somit der Sollstrom berechnet. Im nächsten Schritt wird der Stromregler aufgerufen und die normierte Stellspannung berechnet. Diese muss über den  VCFaktor auf einen PWM Wert umgerechnet werden. Nach der Umrechnung ist die Multiplikation des  VCFaktor mit 1024 wieder Rückgängig zu machen.

 

    // ----------- Regelung -------------

    {

        Current_Soll = Voltage_Controller.PID_controll (Analog_Vsoll, Analog_Vout);

        PWM_set = ( Current_Controller.PID_controll (Current_Soll, Analog_Iout) * VcFactor ) >> 10; // Umrechnung in PWM Wert, Rückschieben auf Größe

        PWM( PWM_set );

    }

 

Das Programm und andere Projektdateien, können hier Heruntergeladen werden