Software

Das Programm für den Mikrocontroller ist in C geschrieben und mit dem Gnu Compiler für den MSP430 compiliert. Man bekommt den Compiler als mspgcc von der GNU-Homepage.

Da der Mikrocontroller nur maximal 2KByte Programmspeicher bietet, macht es wenig Sinn das Programm in einzelne Module zu unterteilen. Daher besteht es nur aus zwei Quelldateien:

main.c und hardware.h.

Dieses Kapitel beschreibt die Software nur sehr grob. Wenn man es genau wissen will, muß man sich durch den Quelltext arbeiten, der extra ausführlich kommentiert ist. Wenn hier Auszüge aus den Quellen angegeben sind, so sind sie mit Zeilennummern versehen, die den Originalen entsprechen. Dadurch können Programmstellen eindeutig angesprochen werden.

hardware.h

 /* Pinbelegung */
 /* Pin1:  VCC MSP430F20x1
    Pin2:  P1.0/TACLK/ACLK/CA0:    Input, analoger Lichtsensor
    Pin3:  P1.1/TA0/CA1:           Input, Taste 1
    Pin4:  P1.2/TA1/CA2:           Input, Taste 2
    Pin5:  P1.3/CAOUT/CA3:         Input, Taste 3
    Pin6:  P1.4/SMCLK/CA4/TCK:     Output, LCD RS
    Pin7:  P1.5/TA0/CA5/TMS:       Output, LCD CSB
    
    Pin8:  P1.6/TA1/CA6/TDI/TCLK:  Output, LCD SI
    Pin9:  P1.7/CAOUT/CA7/TDO/TDI: Output, LCD CLK
    Pin10: RESET/NMI/SBWTDIO
    Pin11: TEST/SBWTCK
    Pin12: P2.7/XOUT:               Output, LED
    Pin13: P2.6/XIN/TA1:            Output, PWM Magnet
    Pin14: GND */
Anschlussbelegung der Elektronik

Zunächst sind die verwendeten Pins als Kommentar aufgeführt, damit man sich im weiteren Verlauf der Datei daran orientieren kann.

 // Port 1.x
 #define FOTODIODE_PORT 0x01
 
 #define TASTE1_PORT 0x02
 #define TASTE2_PORT 0x04
 #define TASTE3_PORT 0x08
 
 #define CS_PORT 0x20
 #define SI_PORT 0x40
 #define SCL_PORT 0x80
 #define RS_PORT 0x10
 
 // Port 2.x
 #define PWM_PORT 0x40
 #define NC2_7_PORT 0x80
 
 //Port Output Register 'P1OUT, P2OUT':
 #define P1OUT_INIT      TASTE1_PORT | TASTE2_PORT | TASTE3_PORT | CS_PORT | SI_PORT | RS_PORT
 #define P2OUT_INIT      0x00                       // Init Output data of port2
 
 //Port Direction Register 'P1DIR, P2DIR' (Out=1, Inp=0):
 #define P1DIR_INIT      CS_PORT | SI_PORT | SCL_PORT | RS_PORT
 #ifdef MESSBAR
 #undef P1DIR_INIT
 #define P1DIR_INIT      TASTE1_PORT | TASTE2_PORT | TASTE3_PORT | CS_PORT | SI_PORT | SCL_PORT | RS_PORT
 #endif
 #define P2DIR_INIT      PWM_PORT | NC2_7_PORT
 
 //Selection of Port or Module -Function on the Pins 'P1SEL, P2SEL' (0=GPIO, 1=Module)
 #define P1SEL_INIT      FOTODIODE_PORT
 #define P2SEL_INIT      PWM_PORT
 
 #define P1REN_INIT      TASTE1_PORT | TASTE2_PORT | TASTE3_PORT
 #define P2REN_INIT      0x00
 
 //Interrupt Enable (0=dis 1=enabled)
 #define P1IE_INIT       0
 #define P2IE_INIT       0
 
 // Interrupt Edge Select (0=pos 1=neg)
 #define P1IES_INIT      0
 #define P2IES_INIT      0
 
 #define WDTCTL_INIT     WDTPW|WDTHOLD
 
 #define TASTE1 ((P1IN & TASTE1_PORT) == 0)
 #define TASTE2 ((P1IN & TASTE2_PORT) == 0)
 #define TASTE3 ((P1IN & TASTE3_PORT) == 0)
 
 #define CS_LOW P1OUT = P1OUT & ˜CS_PORT
 #define CS_HIGH P1OUT = P1OUT | CS_PORT
 #define SI_LOW P1OUT = P1OUT & ˜SI_PORT
 #define SI_HIGH P1OUT = P1OUT | SI_PORT
 #define SCL_LOW P1OUT = P1OUT & ˜SCL_PORT
 #define SCL_HIGH P1OUT = P1OUT | SCL_PORT
 #define RS_LOW P1OUT = P1OUT & ˜RS_PORT
 #define RS_HIGH P1OUT = P1OUT | RS_PORT
 
 #ifdef MESSBAR
 #define DIAG1_LOW P1OUT = P1OUT & ˜TASTE1_PORT
 #define DIAG2_LOW P1OUT = P1OUT & ˜TASTE2_PORT
 #define DIAG3_LOW P1OUT = P1OUT & ˜TASTE3_PORT
 #define DIAG1_HIGH P1OUT = P1OUT | TASTE1_PORT
 #define DIAG2_HIGH P1OUT = P1OUT | TASTE2_PORT
 #define DIAG3_HIGH P1OUT = P1OUT | TASTE3_PORT
 #endif
Definitionen von Konstanten für bessere Lesbarkeit Seitenanfang

Danach werden dann diverse Konstanten definiert, so daß man das Programm leicht verfolgen kann.

Wie schon im Schaltplan erklärt, sind zu Debug-Zwecken und Inbetriebnahme verschiedene Pins auf Stecker geführt. An P1.1, P1.2, P1.3 können Tasten angeschlossen werden, über die die PID-Parameter zur Laufzeit verändert werden können. An P1.4 bis P1.7 kann ein Display angeschlossen werden, um die Effekte der Tastendrücke anzuzeigen. Um diese beiden Funktionen nutzen zu können, muß die Konstante EINSTELLBAR definiert sein. Das ist hier in hardware.h noch nicht sichtbar. Allerdings gibt es auch noch die Möglichkeit, die drei Tasteneingänge als Statusausgänge zu schalten, um Zeitmessungen per Oszilloskop zu ermöglichen. Dafür muß dann die Konstante MESSBAR definiert werden.

main.c

 //#define EINSTELLBAR  // Erzeugt eine Version, die Tasten und LCD ansteuert, um Parameter zu veraendern
 //#define MESSBAR  // Erzeugt eine Version, die die Tastenbits als Ausgang fuer Zeitmessungen verwendet
 
 #if defined(EINSTELLBAR)  && defined(MESSBAR)
 #error ERROR: Entweder EINSTELLBAR oder MESSBAR benutzen. Nicht beide gleichzeitig!
 #endif
 
 #ifdef MESSBAR
 #warning INFO: MESSBAR eingestellt
 #endif
 #ifdef EINSTELLBAR
 #warning INFO: EINSTELLBAR eingestellt
 #endif
Einleitung von main.c mit den beiden Debugkonstanten

Da die Pins P1.1 bis P1.3 nur entweder als Tasteneingänge oder als Meßausgänge fungieren können dürfen die Konstanten MESSBAR und EINSTELLBAR nicht gleichzeitig definiert sein. Die hier angegebene Konfiguration beschreibt die fertige Lösung in der keine der beiden Debugmöglichkeiten aktiviert ist.

 #include "hardware.h"
 #include <signal.h>
 #include <stdlib.h>
 
 /* Hier werden die grundlegenden Systemeinstellungen gemacht */
 /* Systemfrequenz in Hz */
 #define SYSFREQ 16000000l
 /* Vorteiler des Timers */
 #define TIMEDIVISOR 2l
 #define TIMERRELOAD (0x400)
 #define INTFREQUENZ (SYSFREQ/TIMEDIVISOR/TIMERRELOAD)
 
 #if TIMERRELOAD > 65535
 #error Gewuenschte Timerfrequenz zu niedrig!
 #endif
 
 /****************************************************************************/
 /* Einstellungen fuer Systemtakt */
 #if SYSFREQ ==  16000000l
 /* 16MHz Takt */
 #define CALDCO_XMHZ CALDCO_16MHZ
 #define CALBC1_XMHZ CALBC1_16MHZ
 
 #elif SYSFREQ == 12000000l
 /* 12MHz Takt */
 #define CALDCO_XMHZ CALDCO_12MHZ
 #define CALBC1_XMHZ CALBC1_12MHZ
 
 #elif SYSFREQ == 8000000l
 /* 8MHz Takt */
 #define CALDCO_XMHZ CALDCO_8MHZ
 #define CALBC1_XMHZ CALBC1_8MHZ
 
 #elif SYSFREQ == 1000000l
 /* 1MHz Takt */
 #define CALDCO_XMHZ CALDCO_1MHZ
 #define CALBC1_XMHZ CALBC1_1MHZ
 
 #else
 #error Keine gueltige Taktfrequenz gesetzt! (16, 12, 8 oder 1 MHz)
 #endif
 
 /****************************************************************************/
 /* Einstellungen fuer Timer */
 #if TIMEDIVISOR == 1l
 /* Teiler durch 1 */
 #define ID_DIVX ID_DIV1
 
 #elif TIMEDIVISOR == 2l
 /* Teiler durch 2 */
 #define ID_DIVX ID_DIV2
 
 #elif TIMEDIVISOR == 4l
 /* Teiler durch 4 */
 #define ID_DIVX ID_DIV4
 
 #elif TIMEDIVISOR == 8l
 /* Teiler durch 8 */
 #define ID_DIVX ID_DIV8
 
 #else
 #error Kein gueltiger Timer-Teiler gesetzt! (1, 2, 4 oder 8)
 #endif
 /****************************************************************************/
Festlegung und Berechnung von einigen wichtigen Timerparametern Seitenanfang

Die nötigen Registerwerte für die Timer werden über Berechnungen ermittelt. So kann man relativ leicht Änderungen vornehmen und erfasst in einem Zug die verschieden, zum Teil voneinander abhängigen, Registerinhalte. Dieser Programmteil ist für diese Anwendung vielleicht unnötig komplex, aber ich benutze ihn als Vorlage in all meinen Programmen, also auch hier.

 #define CONTROLWORD 1
 #define DATAWORD 0
 
 #define LCD_CLEAR_DISPLAY 0x01
 #define LCD_RETURN_HOME 0x02
 #define LCD_SHIFT_LEFT 0x07
 #define LCD_SHIFT_RIGHT 0x05
 #define LCD_CURSOR_LEFT 0x04
 #define LCD_CURSOR_RIGHT 0x06
 #define LCD_DISPLAY_DISP_ON 0x0C
 #define LCD_DISPLAY_DISP_CURS_ON 0x0E
 #define LCD_DISPLAY_DISP_BLINK_ON 0x0D
 #define LCD_DISPLAY_ALL_ON 0x0F
 #define LCD_DISPLAY_OFF 0x08
 #define LCD_FUNCTION_SET_0 0x30
 #define LCD_FUNCTION_SET_1 0x31
 #define LCD_FUNCTION_SET_2 0x32
 #define LCD_ZEILE2 40
 #define LCD_SET_CURSOR(x) (0x80 + (x))
 
 #define LCD_CONTRAST(x) (0x70 + (x))
 
 #define ADMITTEL 32
 #define TIME_2MS  ((2L*INTFREQUENZ)/1000)
 #define TIME_10MS ((10L*INTFREQUENZ)/1000)
 #define TIME_100MS ((100L*INTFREQUENZ)/1000)
 #define TIME_1S (INTFREQUENZ)
 #define TIME_256US 2
 
 #define SCHATTENSPANNE 200
 #define SCHATTENGRENZE 20
Weitere Konstanten für Displayansteuerung, Zeiten und AD-Werte

Hier werden noch weitere Konstanten definiert, die den Umgang mit dem LCD und konstanten Werten erleichtern sollen.

Seitenanfang
 volatile unsigned long countmax, countmin;
 volatile unsigned int timeslot;
 volatile unsigned int delaytimer;
 volatile signed int schatten, schatten_alt;
 volatile  signed int timer_reload, to;
 volatile signed int i_abweichung;
 volatile unsigned char regelung_an;
 signed int schattenmin;
 signed int schattenmax;
 signed int schattenmitte;
 char buf[10];
 volatile signed int p_anteil, i_anteil, d_anteil;
 
 const char * n[] = {"P   ", "I   ", "D   ", "Min ", "Max "}; // Moegliche Parameter die angezeigt werden koennen
Variablen

Die meisten Variablen sind global definiert und auch viele als volatile, da sie sowohl im Interrupt, als auch in der Endlosschleife des Programm benutzt werden. Die Bedeutung der Variablen ist in den folgenden Programmteilen ersichtlich.

 void delay(unsigned int d) {
   delaytimer = d;
   while (delaytimer) {
   }
 }
Warteroutine

An vielen Stellen im Programm muß mehr oder weniger lange gewartet werden. Zu diesem Zweck wird im Interrupt die Variable delaytimer heruntergezählt so lange sie größer Null ist. Die Warteroutine delay(..) setzt delaytimer auf den gewünschten Wert und wartet bis er zu Null geworden ist. In der vorliegenden Einstellung, ist eine Auflösung von ca. 128us möglich.

Im folgenden sind nur noch die Bereiche von main.c dargestellt, die für die eigentliche Funktion nötig sind. Alle Codezeilen, die durch MESSBAR und EINSTELLBAR aktiviert werden sind nicht gezeigt. Der Quellcode ist aber gut genug kommentiert, so daß die jeweilige Funktion bei dessen Betrachtung klar sein sollte.

Seitenanfang
 /* Der Interrupt wird bei jedem Durchlauf der PWM, also mit ca. 7800Hz aufgerufen. Der Durchlauf einer kompletten
    Mess- und Regelaufgabe dauert aber laenger als die 128us. Daher wird die Messung und die Regelung jeweils alternierend
    durchgefuehrt. Am Anfang gibt es noch einen gemeinsamen Teil fuer Timer. */
 interrupt (TIMERA0_VECTOR) Timer_A0(void)
 {
   int z;
 
   if (delaytimer) { // Timer runterzaehlen
     delaytimer--;
   }
   timeslot++;
   /* Nur bei jedem 2-ten Durchlauf des Interrupts wird geregelt */
   if (timeslot >= 2) {
   /* Hier ist der Teil mit der Regelung */
     timeslot = 0; /* Teiler wieder bei 0 anfangen lassen */
     i_abweichung = i_abweichung + schatten/8; /* Fuer die Integration wird die Abweichung abgeschwaecht. Ansonsten
                                                  haette  ich fuer Werte von i_anteil ueber ca. 65 long Werte nehmen
                                                  muessen, was aber durch die zustaetzlichen Bibliotheksroutinen viel
                                                  ROM braucht. Mit jedem Interrupt wird i_abweichung um die momentane
                                                  Abweichung zur Sollposition erweitert. */
   /* Der Integratorwert wird nach oben und unten so begrenzt, dass er alleine hoechstens fuer einen Maximalstellwert
      verantwortlich sein kann. Da in der Regelung durch i_anteil geteilt wird, wird der Grenzwert damit multipliziert.*/
     if (i_abweichung > i_anteil*(TIMERRELOAD/2)) {
       i_abweichung = i_anteil*(TIMERRELOAD /2);
     }
     if (i_abweichung < -i_anteil*(TIMERRELOAD /2)) {
       i_abweichung = -i_anteil*(TIMERRELOAD /2);
     }
   /* Hier ist die eigentliche Regelung: Die vorzeichenbehafteten Regelwerte werden zum Offset TIMERRELOAD/2 addiert, damit
      der PWM-Wert im Normalfall bei 50% liegt, also in beide Richtungen maximal aussteuerbar ist. Addieren heisst hier
      Subtrahieren, da groessere Reglerwerte kleinere PWM-Werte erfordern. Mit anderen Worten: Mehr Schatten braucht weniger
      PWM-Verhaeltnis und damit weniger Magnetkraft.
      i_abweichung/i_anteil ist der I-Anteil, der recht klein ausfaellt. Daher wird die kumulierte Abweichung durch i_anteil
      geteilt. Ansonsten waere die Regelung viel zu unruhig. Dieser Parameter sorg dafuer, dass die Kugel ueber laengere
      Zeit an der exakt gewuenschten Position haengt.
      (p_anteil* schatten)/10 ist der P-Anteil. Abhaengig vom Schatten wird direkt ein Korrekturwert erzeugt, der sofort eine
      Reaktion des Magneten ergibt.
      d_anteil * (schatten - schatten_alt) ist der D-Anteil. Wenn die Kugel absolut still stehen wuerde, waere die Differenz
      in der Klammer 0 und der Parameter haette keine Wirkung. Jede Aenderung wirkt sich aber durch den relativ grossen
      Wert von d_anteil stark auf das PWM-Verhaeltnis aus. */
     timer_reload =  TIMERRELOAD/2 - i_abweichung/i_anteil - (p_anteil* schatten)/10 - d_anteil * (schatten - schatten_alt);
   /* Die ermittelten PWM-Werte auf 0 bis 100% begrenzen. */
     if (timer_reload > TIMERRELOAD) {
       timer_reload = TIMERRELOAD;
     }
     if (timer_reload < 1) {
       timer_reload = 0;
     }
   /* Wenn die Regelung laeuft, wird der PWM-Wert aktualisiert */
     if (regelung_an) {
       TACCR1 = timer_reload;
     }
     else {
       /* Bei ausgeschalteter Regelung ist die PWM auf 0% und der Integrator wird auf Maximalwert eingestellt, damit das
          Einhaengen der Kugel sanft erfolgt. */
       TACCR1 = 0;
       i_abweichung = +i_anteil*(TIMERRELOAD/2);
     }
   /* Fuer D-Regler wird sich der jetzige Schattenwert fuer den naechsten Durchlauf gemerkt */
     schatten_alt = schatten;
   /* Waehrend des Betriebs wird ueberprueft ob die gemessenen Schattenwerte einige Zeit am Rand der Messwerte liegen.
      Dann ist die Kugel entweder oben gegen den Magneten gezogen worden, oder sie ist nicht mehr da. In beiden Faellen
      wird die Reglung und der Magnet ausgeschaltet und man wartet bis die Schattenwerte wieder im Regelbereich liegen. */
     if (schattenmax - (schatten + schattenmitte) < SCHATTENGRENZE) { /* Bei starker Abdeckung, Kugel oben */
       if (countmax) {
         countmax--;
       }
       else {
         regelung_an = 0;
       }
     }
     else {
       if((schatten+schattenmitte) - schattenmin < SCHATTENGRENZE) { /* Bei keinem Schatten, Kugel weg */
         if (countmin) {
           countmin--;
         }
         else {
           regelung_an = 0;
         }
       }
       else {
         countmax = TIME_10MS;
         countmin = TIME_100MS;
         regelung_an = 1;
       }
     }
   }
   else {
     /* Hier ist der Teil mit der Messung. */
     /* Es hat sich gezeigt, dass ein ruhiger Messwert fuer den Schatten erheblichen Einfluss auf die Qualitaet der Regelung
        hat. Daher wird mehrfach gemessen und dann gemittelt. */
     schatten = 0; /* in schatten wird der gemittelte Helligkeitswert gespeichert. Vor dem Messen wird er auf 0 gesetzt. */
     for (z = 0; z < ADMITTEL; z++) { /* Mitteln ueber x Werte */
       ADC10CTL0 = ADC10CTL0 | ADC10SC;
       while (ADC10CTL1 & ADC10BUSY) {
       }
       schatten += ADC10MEM; /* AD-Werte aufsummieren */
     }
     schatten = schatten / ADMITTEL - schattenmitte; /* Und durch die Anzahl der Summenterme teilen. Durch Subtrahieren
                                                        von schattenmitte wird ein vorzeichenbehafteter Wert erzeugt. */
   }
 }
Interruptroutine

Durch den D-Anteil, der auf jede Änderung deß Meßwertes stark reagiert, muß für eine rauscharme Messung des Lichteinfalls gesorgt werden. Das geschieht dadurch, daß mehrfach (ADMITTEL = 32) gemessen wird und über die Meßwerte der Durchschnitt gebildet wird. Die 32 habe ich experimentell bestimmt. Da nun die Messung zu lange dauert, wird im Interrupt abwechselnd gemessen und geregelt. Eigentlich müßte es auch möglich sein, die Interruptfrequenz zu halbieren und beides nacheinander ablaufen zu lassen aber irgendwie hat sich die hier gezeigte Lösung am Ende herausgestellt und ich habe sie einfach nicht nochmal ändern wollen.

Zeile 254 enthält den eigentlichen Regler. Der neue PWM-Wert wird aus den drei Komponenten P, I und D berechnet. Um die Regelung schnell genug durchlaufen zu lassen, kann man hier nicht einfach mit Gleitkommazahlen rechnen, sondern muß sich ein paar Gedanken über die Wertebereiche machen. Außerdem stehen im MSP430F2012 nur 2kB ROM zur Verfügung, was durch eine Gleitkommaarithmetik wohl auch schnell gesprengt würde.

Bei der Verwendung von Integerarithmetik muß man sich immer die jeweiligen Wertebereiche vor Augen führen. Es darf nicht vorkommen, daß zwischendurch Überläufe auftreten. Aber es dürfen auch keine kleine Zahlen unter den Tisch fallen, da sie sonst keine Wirkung mehr zeigen. In diesem Fall ist der Meßwert schatten durch den AD-Wandler maximal 10 Bit groß. Die Ergebnisse, die für die Magnetansteuerung gebraucht werden, dürfen auch maximal zwischen 0 und 1023 (10 Bit) liegen. Daß diese Grenzen eingehalten werden, wird in Zeile 256 bis 161 erreicht.

Der I-Anteil darf nur sehr vorsichtig in die Regelung eingehen, da es sonst zu langsamen Schwingungen kommt. In Zeile 254 wird daher auch durch i_anteil geteilt statt zu multiplizieren. Je größer i_anteil umso geringer der Einfluß auf die Regelung.

Der P-Anteil wird in seiner Bedeutung auch noch durch 10 geteilt, um den Wertebereich gut auszunutzen. Das wäre nicht wirklich notwendig, stammt aber noch aus der Versuchsphase wo durch die Tasten die Werte der drei Parameter verändert werden konnten.

Der D-Anteil fällt ziemlich stark aus und wird direkt in der Berechnung verwendet.

Die Werte i_anteil = 60, p_anteil = 10 und d_anteil = 230 habe ich in den Versuchsphasen ermittelt.

Würde der Regler dauerhaft so arbeiten, so würde bei fehlender Kugel der Magnet maximal angesteuert. Das würde zu einer starken Erwärmung führen, was die Funktionstüchtigkeit stark einschränkt. Daher gibt es den Code zwischen Zeile 277 bis 299. Der Regler wird abgeschaltet, wenn längere Zeit extreme Lichtwerte gemessen werden. Zeilen 277 bis 284 prüfen auf zu viel Schatten. Es kann passieren, daß die Kugel an den Magneten herangerissen wird. In diesem Fall ist die Magnetkraft immer noch so stark, daß sich die Kugel nicht von alleine löst. Dies wird erkannt und nach ca. 10ms wird die Regelung komplett abgeschaltet. Die Kugel fällt ab.

Ist die Kugel abgefallen oder noch gar nicht aufgehängt, fällt sehr viel Licht auf den Sensor und Zeilen 286 bis 293 sorgen dafür daß nach ca. 100ms abgeschaltet wird.

Seitenanfang