Software

Die Programme für die Mikrocontroller sind in C geschrieben und mit dem Gnu Compiler für den MSP430 compiliert. Man bekommt den Compiler als mspgcc von der GNU-Homepage.

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

Pistole: main.c und hardware.h.

Ziel: 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.

Pistole

hardware.h

Im File hardware.h sind Definitionen, die die Hardware betreffen untergebracht. Zur Erinnerung und als Referenz ist zu Beginn eine Auflistung aller Pins eingetragen. Man kann dadurch schnell prüfen, ob die unten verwendeten Konstanten mit der Realität übereinstimmen.

 /* Pinbelegung */
 /* Pin1:  VCC MSP430F20x1
    Pin2:  P1.0/TACLK/ACLK/CA0:    Input, int. Pullup, NC
    Pin3:  P1.1/TA0/CA1:           Input, int. Pullup, NC
    Pin4:  P1.2/TA1/CA2:           Input, int. Pullup, NC
    Pin5:  P1.3/CAOUT/CA3:         Output fuer Widerstandsteiler und Comparator
    Pin6:  P1.4/SMCLK/CA4/TCK:     Comparator Input
    Pin7:  P1.5/TA0/CA5/TMS:       Output Piezo
    
    Pin8:  P1.6/TA1/CA6/TDI/TCLK:  Input, int. Pullup, NC
    Pin9:  P1.7/CAOUT/CA7/TDO/TDI: Input, int. Pullup, NC
    Pin10: RESET/NMI/SBWTDIO
    Pin11: TEST/SBWTCK
    Pin12: P2.7/XOUT:               Output, LED, High Aktiv
    Pin13: P2.6/XIN/TA1:            Output, NC
    Pin14: GND */
Anschlussbelegung der Pistolenelektronik

Die Defintionen in hardware.h sollten für sich selbst sprechen, damit man im Programm sofort erkennen kann was passiert. Ich hoffe das ist mir gelungen.

Seitenanfang

Grundstruktur von main.c


 /* PISTOLE */
 #include "hardware.h"
 #include <signal.h>
 
 #define SYSFREQ 16000000l /* Systemtakt in Hz */
 #define SMCLKDIVISOR 8
 .
 sfrw(MUETZENNUMMER, MUETZENNUMMER_ADDR); /* Assembler funktion um auf die Adresse MUETZENNUMMER_ADDR
                                             wortbreit zugreifen zu koennen */
 
 volatile int irrxbitcount;
 .
 int i;
 
 /**
 Verzoegerungsfunktion durch Abwarten bis Interrupt den delaytimer auf 0 heruntergezaehlt hat
 */
 void delay(unsigned int d) {
 .
   IR_INT_ENABLE; /* Jetzt wieder IR-Empfangsbereit sein */
 }
 
 /********************************************************************/
 int main(void) {
 /* Nach dem Einlegen der Batterien wird hier gestartet */
   WDTCTL = WDTCTL_INIT;               /*Init watchdog timer */
 .
   /* Endlosschleife, in der zyklisch alle Eingaben abgearbeitet werden */
   while (1) {
     /* Wenn kein Abzug gedrueckt ist, wird sich schlafen gelegt. Die Zaehler und Interrupts laufen weiter */
     if (!ABZUG_GEDRUECKT) {
 .
     if ((aktivzeit + INAKTIVDAUER) < sekundenzaehler) {
 .
     if (irempfang) {
 .
     else {
       /* Kein Signal empfangen */
       if (!(EINSTELLUNG_GESTECKT)) {
 .
       else { /* Einstellung ist gesteckt */
 .
       }
     }
   }
 }

Grundstruktur von main.c

In Zeile 1 bis 92 werden erst mal diverse Konstanten definiert. Der Zweck ist jeweils in einem Kommentar beschrieben. Danach folgen in Zeile 93 bis 112 alle verwendeten globalen Variablen. Die, die auch in den Interruptroutinen verwendet werden, müssen unbedingt als volatile deklariert werden, da der Compiler ansonsten eventuell den Code so optimiert, daß nicht immer wirklich auf die Variable, sondern auf einen Registerwert zugegriffen wird. Dann würde der Background eventuell eine Änderung einer Variable durch den Interrupt nicht mitbekommen.

Es folgen nun ab Zeile 113 die diversen Unterprogramme, von denen einige durch das Schlüsselwort interrupt als Interruptroutinen gekennzeichnet sind. Anhand der Angabe eines Vektors in den Klammern kann der Compiler die Adresse der Routine in die Vektortabelle des MSP430 eintragen.

Ab Zeile 373 beginnt nun das Hauptprogramm in das nach Einlegen der Batterien eingesprungen wird. Zwischen Zeile 375 und 431 werden zunächst alle möglichen Initialisierungen gemacht. In Zeile 432 beginnt dann die Endlosschleife des Backgrounds aus der der Prozessor nicht wieder herauskommt.

Der Background wird, um Strom zu sparen, nicht dauernd durchlaufen, sondern immer nur, wenn Ereignisse, wie Tastendrücke oder Timerinterrupt, dies anstoßen. Bei einem solchen Ereignis wird dann auf 5 Möglichkeiten geprüft:

  1. Prüfung ob Abzug gedrückt
  2. Prüfung ob Pistole lange nicht benutzt wurde
  3. Prüfung ob ein Signal von der eigenen Mütze empfangen wurde
  4. Prüfung ob irgend eine Taste gedrückt wurde
  5. Prüfung ob der externe Stecker eingesteckt ist

Die jeweiligen Abläufe der Software sind am besten im Quellcode nachzuvollziehen. Da aber die Signalerzeugung und Detektierung der IR-Signale die kompliziertesten Vorgänge sind, sollen sie hier nochmals kurz skizziert werden.

Seitenanfang

IR-Signalerzeugung

Zum Erzeugen der nötigen Pulse für die IR-Sende-LED werden zwei Timerinterrupts verwendet, die miteinander verkoppelt sind. Das IR-Signal ähnelt einem RC5-Code, der von vielen Fernbedienungen verwendet wird. Ich benutze hier die gleiche Art der Bitcodierung in Form von Pause und Burst bzw. Burst und Pause. Ich übertrage nur 3 Datenbits, was für 7 verschiedene Teilnehmer ausreicht. Da zunächst zwei Startbits gesendet werden, werden bei jeder Datenübertragung 5 Bits gesendet. Der Burst hat eine Frequenz von 38kHz und ist auf die IR-Empfänger abgestimmt. Ein Bit dauert ca. 666µs. Der Burst wird durch Timer1 erzeugt, der beim Senden mit einer Folgefrequenz von 76kHz seinen Ausgangszustand wechselt. Timer0 erzeugt alle 333µs Interrupts, so daß zwei für jede Bitzeit ausgelöst werden. Für jeden dieser Interrupts wird durch die Software entschieden, ob die IR-LED dunkel bleiben, oder ob ein IR-Burst ausgesendet werden soll.


 interrupt (TIMERA1_VECTOR) timera1_interrupt(void)
 {
   if (TAIV & 0x02) {
     if (empfangsmode) {
 .
     }
     else {
       /* Es wird ein 38*2kHz Interrupt erzeugt */
       /* Je nach Programmierung durch Timer0 wird der Ausgang gepulst oder auf 0 gehalten */
       TIMER1_IR_RELOAD;
     }
   }
 }

Timer1 Interrupt beim IR-Senden

Für den Fall daß man nicht im Empfangsmode ist (Zeile 245) wird einfach die neue Zeit, zu der der Timer wieder reagieren soll, in das TACCR1-Register eingetragen.

Dies allein erzeugt erst mal nur einen Interrupt mit einer Frequenz von 76kHz. Erst durch die Programmierung der entsprechenden Bits in TACCTL1 kommt es dazu, daß Ausgang P2.6 die richtigen Signale ausgibt.


 interrupt (TIMERA0_VECTOR) Timer_A(void)
 {
     eint(); /* Interrupts wieder zulassen, damit TIMERA1 weiterhin drankommen kann. */
     TIMER0_CCR_RELOAD; /* Neuen Zaehlervergleichswert speichern */
 
     /* Ab hier kommt die Behandlung des Sendeimpulses */
     /* Wenn irtxbitcount != 0 ist, muss das Senden von Bits bearbeitet werden */
     /* Pro Bit gibt es eine Puls- und eine Pausenhaelfte. Bei Bit=1 wird zunaechst ein */
     /* Puls und dann eine Pause gesendet. Beides wird durch Setzen der MODE-Bits fuer */
     /* Timer 0 gesteuert. Bei Bit=0 ist die Pause zuerst. */
     /* 'irtxbitcount' zaehlt 2 mal Bits da ein Bits jeweils aus Pause und Puls besteht */
     if (irtxbitcount != 0) {
       /* Wenn noch Bits zu senden sind */
       if ((irtxbitcount & 0x0001) == 0) {
         /* Erste Pulshaelfte (Gerade Anzahl von Bithaelften) */
         if (irtxdaten & 0x0001) {
           TIMER1_TOGGLE_INTERRUPT; /* Timer 1 laesst den Ausgang wechseln. 38kHz Puls. */
         }
         else {
           TIMER1_RESET_INTERRUPT;  /* Puls abschalten, Pause */
         }
       }
       else {
         /* Zweite Pulshaelfte */
         if (irtxdaten & 0x0001) {
           TIMER1_RESET_INTERRUPT;  /* Pause einschalten */
         }
         else {
           TIMER1_TOGGLE_INTERRUPT; /* Timer 1 laesst den Ausgang wechseln. 38kHz Puls. */
         }
         /* Nach zweiter Bithaelfte wird das nachste Bit vorbereitet */
         irtxdaten = irtxdaten >> 1;
       }
       /* Jede Bithaelfte mitzaehlen */
       irtxbitcount--;
     }
     else {
       /* Nach den IR-Bits wird eine Pause eingelegt in der nichts gesendet wird. (Stoppbits) */
       TIMER1_RESET_INTERRUPT;
       if (irtxpausecount) {
         irtxpausecount--;
       }
     }
 
 .
 }

Timer0 Interrupt beim IR-Senden

Zunächst wird die Folgefrequenz für diesen Interrupt auf die doppelte Bitrate eingestellt (Zeile 180). Dadurch wird diese Routine zweimal für jedes zu sendende Bit aufgerufen.

Wenn die variable irtxbitcount != 0 ist, dann sind im Folgenden (Zeile 189ff) noch weitere IR-Sendesignale zu erzeugen. Der name irtxbitcount ist etwas irreführend, da hier nicht wirklich die zu sendenden Bits, sondern die Bithälften (also das Doppelte) gezählt werden. Ist irtxbitcount gerade (Abfrage in Zeile 190), so geht es um die erste Hälfte des zu sendenden Bits. Wenn das eine 1 ist, wird zuerst ein Burst (Zeile 193) und dann in der zweiten Hälfte eine Pause (Zeile 202) gesendet. Bei einer zu sendenden 0 kommt zuerst die Pause (Zeile 196) und dann der Burst (Zeile 205). Die Befehle TIMER1_TOGGLE_INTERRUPT und TIMER1_RESET_INTERRUPT manipulieren direkt Bits im TACCTL1-Register, die dann durch die nächsten Interrupts des mit 76kHz laufenden Timer1 die Ausgänge umschalten. Ein Wechsel des Ausgangspegel mit einer Frequenz von 76kHz ergibt ein Signal mit den gewünschten 38kHz.

Auf jeden Fall wird nach dem Senden der 5 Datenbits noch eine Pause eingelegt, so daß in keinem Fall Daten zu schnell aufeinander folgen. Dazu wird in Zeile 217 ein Pausenzähler verwendet, der einfach noch für ein paar Interrupts herunterzählt, ohne daß in dieser Zeit IR-Signale gesendet werden. Erst wenn irtxbitcount und irtxpausecount Null sind, gilt der Sendevorgang als abgeschlossen.

Seitenanfang

IR-Signaldetektierung

Bei der Detektierung eines IR-Signals, das normalerweise von der Mütze stammt, braucht man den 38kHz Burst vom Mikrocontroller nicht zu beachten, da er bereits im IR-Empfänger ausgewertet wird. Wenn der Empfänger ein IR-Signal mit der Frequenz 38kHz sieht, geht sein Ausgang auf 0. Ansonsten bleibt er, durch einen im Empfänger eingebauten Pullup, auf 1.


 interrupt (PORT1_VECTOR) port1_interrupt(void)
 {
   if (P1IFG & IR_PORT) {
     /* Wenn IR-Empfaenger den Interupt ausgeloest hat */
     /* Timer stoppen und Timer-Interrupt auf halbe Bithaelfte einstellen */
     TIMER_STOP;
     TACCR1 = TAR + PERIODECOUNT / 2; /* Naechster Interrupt in der Mitte der Bithaelfte */
     TIMER_RUN;
     /* Merken, dass der Empfang laeuft */
     empfangsmode = 1;
     /* Jetzt keine Interrupts mehr durch den IR-Empfaenger zulassen */
     IR_INT_DISABLE;
     /* Es werden 2*IRBITS ausgewertet */
     /* Solange irbitcount != 0 ist, wird im Timerinterrupt geprueft */
     irrxbitcount = IRBITS*2;
     irrxpausecount = PAUSE;
     /* Das Datenwort wird erst mal auf 0 gesetzt */
     irrxdaten = 0;
     /* Den Timer auf Interruptausloesung einstellen */
     TIMER1_INT_RESET;
     TIMER1_RESET_INTERRUPT;
   }
 .
   P1IFG = 0;
 }

IR-Empfangsinterrupt bei Flanke an P1.4

Im Normalzustand wartet die Software laufend auf eine 0 durch den IR-Empfänger. Das geschieht dadurch, daß Pin 1.4 auf Interruptauslösung bei einer fallenden Flanke eingestellt ist. Wenn nun dieses Ereignis eintritt (Zeile 317), wird Timer1 auf einen Interrupt zur nächsten Mitte der Bithälfte eingestellt. Da dies der Beginn des IR-Empfangs darstellt, wird der Interruptroutine von Timer1 diese Tatsache durch die Variable empfangsmode (Zeile 324) mitgeteilt.

Da ab hier die Detektierung des IR-Signals zeitgesteuert geschieht, sollen weitere Flanken des IR-Empfängers keine Interrupts mehr auslösen (Zeile 326). Nach dem Setzen von ein paar notwendigen Variablen, wird Timer1 so eingestellt, daß er nun zur Mitte jeder Bithälfte einen Interrupt auslöst.


 interrupt (TIMERA1_VECTOR) timera1_interrupt(void)
 {
   if (TAIV & 0x02) {
     if (empfangsmode) {
       /* Es wird ein IR-Bitstrom empfangen und er muss mit doppelter Bitrate abgetastet werden */
       TIMER1_DET_RELOAD;
       if (irrxbitcount > 0) {
         /* Es ist noch was zu empfangen */
         if ((irrxbitcount & 0x0001) == 0) {
           /* Erste Bithaelfte */
           if (IR_ERKANNT) {
             /* Wenn Puls erkannt, eine 1 empfangen */
             irrxdaten = (irrxdaten >> 1) | (1 << (IRBITS - 1));
           }
           else {
             /* Sonst ist es eine 0 */
             irrxdaten = (irrxdaten >> 1);
           }
         }
         else {
           /* Zweite Bithaelfte */
           if (IR_ERKANNT) {
             if ((irrxdaten & (1 << (IRBITS - 1))) != 0) {
               /* Wenn ein Puls kommt, obwohl in der ersten Haelfte schon */
               /* einer da war, wird das Ergebnis auf 0xffff gesetzt und */
               /* durch irbitcount der Empfang beendet */
               irrxbitcount = 1;
               irrxdaten = 0xffff; /* Fehler melden */
             }
           }
           else {
             if ((irrxdaten & (1 << (IRBITS - 1))) == 0) {
               /* Beenden bei zweiter Pause in Folge */
               irrxbitcount = 1;
               irrxdaten = 0xffff; /* Fehler melden */
             }
           }
           /* Nichts machen */
         }
         irrxbitcount--;
       }
       else {
         if (irrxpausecount) {
           if (IR_ERKANNT) {
             irrxdaten = 0xfff8; /* Fehler melden, keine Pause erkannt */
           }
           irrxpausecount--;
         }
         else {
           /* Wenn keine Bits mehr zu empfangen sind, Timer Interrupt abschalten */
           TIMER1_NOINTERRUPT;
           /* Die Startbits rausschieben */
           irrxdaten = irrxdaten >> 2;
           /* Melden, dass Empfangsdaten anliegen */
           irempfang = 1;
           _BIC_SR_IRQ(LPM1_bits); /* Aufwachen und Background einmal durchlaufen lassen */
           empfangsmode = 0; /* Flasg fuer Empfangsmode zuruecksetzen */
         }
       }
     }
 .
   }
 }

IR-Empfangsinterrupt bei Timer1 Interrupt

Zuallererst wird der Zeitpunkt für den nächsten Interrupt bestimmt (Zeile 247). Danach wird geprüft, ob man nicht schon alle Bits empfangen hat (Zeile 248). Auch hier ist irrxbitcount wieder ein Zähler für Bithälften. Befindet man sich in der ersten Bithälfte, wird das emfangene Datenbit in irrxdaten gespeichert (Zeile 252-259). In der zweiten Bithälfte weiß man eigentlich schon was man zu erwarten hat. Daher wird nun nur noch überprüft, ob die Erwartung auch eintritt (Zeile 263-278). Im Falle eines Fehlers (2 Bursts, oder 2 Pausen), wird das Empfangsergebnis auf 0xFFFF gesetzt und die weitere Dekodierung abgebrochen indem man irrxbitcount auf 0 setzt.

Nachdem man alle Datenbits empfangen hat, muß ja noch eine Pause folgen, dies wird ab Zeile 284 geprüft. Ein Fehler hier führt zu einem Empfangsergebnis von 0xFFF8.

Wenn auch die Pause erfolgreich gefunden wurde, werden die beiden Startbits aus dem Ergebnis entfernt und der Timerinterrupt wird abgeschaltet (Zeile 292-294). Dem Background wird durch irempfang = 1; mitgeteilt, daß Daten empfangen wurden. Damit er damit auch was anfangen kann, wird er durch Zeile 297 im Anschluß an diesen Interrupt zum Laufen gebracht. Mit empfangsmode = 0; wird schließlich noch angezeigt, daß momentan kein IR-Empfang im Gange ist.

Seitenanfang

Ziel

hardware.h

Wie bei der Pistole sind in hardware.h die Definitionen, die die Hardware betreffen untergebracht.


 /* Pinbelegung */
 /* Pin1:  VCC MSP430F20x1
    Pin2:  P1.0/TACLK/ACLK/CA0:    Input, int. Pullup, IR-Empfaenger
    Pin3:  P1.1/TA0/CA1:           Input, int. Pullup, Taste gegen GND
    Pin4:  P1.2/TA1/CA2:           Output, DC-DC Wandler Ein-Aus
    Pin5:  P1.3/CAOUT/CA3:         Output, LED am Muetzenschirm
    Pin6:  P1.4/SMCLK/CA4/TCK:     Output, NC
    Pin7:  P1.5/TA0/CA5/TMS:       Output, NC
    
    Pin8:  P1.6/TA1/CA6/TDI/TCLK:  Output, NC
    Pin9:  P1.7/CAOUT/CA7/TDO/TDI: Output, NC
    Pin10: RESET/NMI/SBWTDIO:      RC fuer Reset
    Pin11: TEST/SBWTCK:            Pulldown
    Pin12: P2.7/XOUT:              Output, Rundum LEDs auf Muetze
    Pin13: P2.6/XIN/TA1:           Output, IR-LED am Muetzenschirm
    Pin14: GND */

Anschlussbelegung der Mützenelektronik Seitenanfang

Grundstruktur von main.c


 /* ZIEL */
 #include "hardware.h"
 #include <signal.h>
 
 #define SYSFREQ 16000000l /* Systemtakt in Hz */
 #define SMCLKDIVISOR 8
 .
 volatile char treffermeldecount;
 volatile int irrxbitcount;
 .
 /**
 Verzoegerungsfunktion durch Abwarten bis Interrupt den delaytimer auf 0 heruntergezaehlt hat
 */void delay(unsigned int d) {
   delaytimer = d;
 .
   IR_INT_RESET; /* Eventuell aufgelaufene IR_Interrupts loeschen */
   IR_INT_ENABLE; /* Jetzt wieder Empfangsbereit sein */
 }
 
 /********************************************************************/
 int main(void) {
 /* Nach dem Einlegen der Batterien wird hier gestartet */
   WDTCTL = WDTCTL_INIT;               /*Init watchdog timer */
 .
   while (1) {
     if ((aktivzeit + INAKTIVDAUER) < sekundenzaehler) {
 .
     }
     switch(zustand) { /* Je nach Zustand werden verschiedene Aktionen durchgefuehrt */
     case S_WARTEN: /* Muetze wartet auf Treffer oder Tastendruck */
       if (irempfang) { /* Wenn der Empfaenger was gesehen hat. */
 .
       }
       if (TASTE_GEDRUECKT) { /* Bei Tastendruck geht man in einen eigenen Zustand ueber */
         zustand = S_TASTE;
       }
       break;
     case S_GETROFFEN: /* Ist man getroffen, so wird einige Zeit mit allen LEDs geblinkt */
 .
     case S_SCHUTZ: /* Es gibt nun eine Zeit, in der man nicht getroffen werden kann. Nur die LED am Schirm blinkt. */
 .
     case S_TASTE:
 .
     default:
       break;
     }
   }
 }

Grundstruktur von main.c

Die Grundstruktur der Software des Ziels ähnelt der der Pistole. Zunächst die Konstantendefinitionen, die globalen Variablen und die Unterprogramme. Wie bei den Pistolen bleibt das Programm nach der Initialisierung in einer Endlosschleife in der es, je nach Zustand, Aktionen ausführt. Aktionen des Ziels beschränken sich hauptsächlich auf unterschiedliches Blinken mit den verschiedenen LEDs. Es gibt die folgenden Zustände oder Aktionen:

  1. Prüfung ob das Ziel wegen Inaktivität abgeschaltet werden soll
  2. Zustand in dem des Ziel auf einen Treffer wartet
  3. Zustand in dem das Zeil einen Treffer erkannt hat
  4. Zustand nach einem Treffer in dem keine weiteren Treffer zugelassen werden
  5. Zustand nach dem Drücken der Taste

Für die Erkennung eines Treffers und das Abschalten der eigenen Pistole werden die gleichen Mechanismen wie bei den Pistolen verwendet. Um die eigene Pistole sicher abzuschalten, wird bei einem Treffer 6 mal die eigene Mützen-ID gesendet. Dies hat sich als sinnvoll herausgestellt, da die Pistole nicht immer sofort auf die Abschaltung reagiert hat, vielleicht weil die IR-Sendediode der Mütze relativ schwach sendet.

Seitenanfang