AVR-Tutorial: Uhr

Aus der Mikrocontroller.net Artikelsammlung, mit Beiträgen verschiedener Autoren (siehe Versionsgeschichte)
Wechseln zu: Navigation, Suche

Eine beliebte Übung für jeden Programmierer ist die Implementierung einer Uhr. Die meisten Uhren bestehen aus einem Schwingkreis und einer Auswerte- und Anzeigevorrichtung. Wir wollen hier beides mittels eines Programmes in einem Mikrocontroller realisieren.

Voraussetzung für diese Fallstudie ist das Verständnis der Kapitel über

Aufbau und Funktion

Die Aufgabe des Schwingkreises, der uns einen möglichst regelmässiges Signal liefert, übernimmt ein Timer. Der Timer ermöglicht einen einfachen Zugang zur regelmäßigen Schwingung, die der µC vom Quarz abgreift. Wie schon im Einführungskapitel über den Timer wird dazu einen Timer Overflow Interrupt installiert und in diesem Interrupt wird die eigentliche Uhr hochgezählt. Die Uhr selbst besteht aus 4 Registern. 3 davon repräsentieren die Sekunden, Minuten und Stunden unserer Uhr. Nach jeweils einer Sekunde wird das Sekundenregister um 1 erhöht. Sind 60 Sekunden vergangen, dann wird das Sekundenregister wieder auf 0 gesetzt und dafür das Minutenregister um 1 erhöht. Nach 60 Minuten werden die Minuten wieder auf 0 gesetzt und für diese 60 Minuten eine Stunde mehr gezählt. Nach 24 Stunden schliesslich werden die Stunden wieder auf 0 gesetzt, ein ganzer Tag ist vergangen.

Aber wozu das 4. Register?

Der Mikrocontroller wird mit 4MHz betrieben. Bei einem Teiler von 1024 zählt der Timer also mit 4000000 / 1024 = 3906.25 Pulsen pro Sekunde. Der Timer muss einmal bis 256 zählen, bis er einen Overflow auslöst. Das heist, es ereignen sich 3906.25 / 256 = 15.2587 Overflows pro Sekunde. Die Aufgabe des 4. Registers ist es nun diese 15 Overflows zu zählen. Bei Auftreten des 15.ten Overflow ist 1 Sekunde vergangen. Dass dies nicht exakt stimmt, da ja die Division auch Nachkommastellen aufwies, wird im Moment der Einfachheit halber ignoriert. In einem späteren Abschnitt wird darauf noch eingegangen.

Im Overflow Interrupt wird also diese Kette von Zählvorgängen auf den Sekunden, Minuten und Stunden durchgeführt und anschliessend zur Anzeige gebracht. Dazu werden die in einem vorhergehenden Kapitel entwickelten LCD Funktionen benutzt.

Programm

<avrasm> .include "m8def.inc"

.def temp1 = r16 .def temp2 = r17 .def temp3 = r18

.def SubCount = r21 .def Sekunden = r22 .def Minuten = r23 .def Stunden = r24

.org 0x0000

       rjmp    main                  ; Reset Handler

.org OVF0addr

       rjmp    timer0_overflow       ; Timer Overflow Handler

.include "lcd-routines.asm"

main:

       ldi     temp1, LOW(RAMEND)     ; Stackpointer initialisieren
       out     SPL, temp1
       ldi     temp1, HIGH(RAMEND)
       out     SPH, temp1
       rcall   lcd_init
       rcall   lcd_clear


       ldi     temp1, 0b00000101    ; Teiler 1024
       out     TCCR0, temp1

       ldi     temp1, 0b00000001    ; TOIE0: Interrupt bei Timer Overflow
       out     TIMSK, temp1

       clr     Minuten              ; Die Uhr auf 0 setzen
       clr     Sekunden
       clr     Stunden
       clr     SubCount
       sei

loop: rjmp loop

timer0_overflow:  ; Timer 0 Overflow Handler

       inc     SubCount              ; Wenn dies nicht der 15. Interrupt
       cpi     SubCount, 15          ; ist, dann passiert gar nichts
       brne    end_isr
       clr     SubCount
       inc     Sekunden              ; 1 Sekunde registrieren
       cpi     Sekunden, 60          ; sind 60 Sekunden vergangen?
       brne    Ausgabe               ; wenn nicht kann die Ausgabe schon
                                     ; gemacht werden
       clr     Sekunden              ; Sekunden wieder auf 0 und dafür
       inc     Minuten               ; 1 Minute registrieren
       cpi     Minuten, 60           ; sind 60 Minuten vergangen ?
       brne    Ausgabe               ; wenn nicht, -> Ausgabe
       clr     Minuten               ; Minuten zurücksetzen und dafür
       inc     Stunden               ; 1 Stunde registrieren
       cpi     Stunden, 24           ; nach 24 Stunden, die Stundenanzeige
       brne    Ausgabe               ; wieder zurücksetzen
       clr     Stunden

Ausgabe:

       rcall   lcd_clear             ; das LCD löschen
       mov     temp1, Stunden        ; und die Stunden ausgeben
       rcall   lcd_number
       ldi     temp1, ':'            ; zwischen Stunden und Minuten einen ':'
       rcall   lcd_data
       mov     temp1, Minuten        ; dann die Minuten ausgeben
       rcall   lcd_number
       ldi     temp1, ':'            ; und noch ein ':'
       rcall   lcd_data
       mov     temp1, Sekunden       ; und die Sekunden
       rcall   lcd_number

end_isr:

       reti                          ; das wars. Interrupt ist fertig

</avrasm>

Aufgaben und Übungen

Form der Anzeige

Abgesehen von der mangelnden Genauigkeit, gibt es noch ein Problem mit der Anzeige. Abhängig davon, welche Version von lcd_number benutzt wird, sieht die Anzeige für 8 Uhr, 6 Minuten und 9 Sekunden entweder so

008:006:009

oder so

8:6:9

aus. Keine der beiden Versionen entspricht aber der üblichen Darstellungsform auf einer Digital-Uhr, die so aussieht:

08:06:09

Worin liegt das eigentliche Problem?

  • Bei Version 1 würden alle Zahlen zwar mit führenden Nullen, jedoch 3-stellig ausgegeben.
  • Bei Version 2 hingegen werden führende Nullen unterdrückt.

Was hier aber benötigt würde, ist eine Zahlenausgabefunktion die zwar führende Nullen ausgibt aber dies nur 2-stellig tut. Diese Funktion zu schreiben, sei hier als Übungsaufgabe gestellt. Wenn dabei Schwierigkeiten entstehen, sei ein nochmaliges Studium des Abschnittes LCD empfohlen.

zusätzliche Eingabeelemente

Im Moment gibt es keine Möglichkeit, die Uhr auf eine bestimmte Uhrzeit einzustellen. Um dies tun zu können, müssten noch zusätzlich Taster an den Mikrocontroller angeschlossen werden, mit deren Hilfe die Sekunden, Minuten und Stunden händisch vergrößert bzw. verkleinert werden können. Studieren sie mal eine käufliche Digitaluhr und versuchen sie zu beschreiben, wie dieser Stellvorgang bei dieser Uhr vor sich geht. Sicherlich werden sie daraus eine Idee entwickeln können, wie ein derartiges Stellen mit der hier vorgestellten Digitaluhr funktionieren könnte.

Ganggenauigkeit

Wird die Uhr mit einer gekauften Uhr verglichen, so stellt man schnell fest, dass die ganz schön ungenau geht. Sie läuft vor!

Woran liegt das?

Die Berechnung sah so aus:

  • Frequenz des Quarzes: 4.0 MHz
  • Teilerfaktor des Timers: 1024
  • Overflow alle 256 Timertakte.

Daraus errechnet sich, daß in einer Sekunde ( 4000000 / 1024 ) / 256 = 15.258789 Overflow Interrupts auftreten. Im Programm wird aber bereits nach 15 Overflows eine Sekunde weitergeschaltet, daher läuft die Uhr vor. Im Grunde ist das ein ähnliches Problem wie mit unserer Jahreslänge. Ein Jahr dauert nicht exakt 365 Tage, sondern in etwa einen viertel Tag länger. Die Lösung, die im Kalender dafür gemacht wurde - der Schalttag -, könnte man fast direkt übernehmen. Nach 3 Stück 15-Overflow Sekunden folgt eine Sekunde für die 16 Overflows anlaufen müssen. Rechnen wir wieder etwas:

  15.258789   ........ 100%
  15          ........ x
  -------------------------
         15 * 100
   x =  ---------- = 98.304 %
         15.258789

So wie bisher, läuft die Uhr also rund 1.7 % zu schnell. In einer Minute ist das immerhin etwas mehr als eine Sekunde.

Wie sieht die Rechnung bei einem 15, 15, 15, 16 Schema aus? Für 4 Sekunden werden exakt 15.258789 * 4 = 61.035156 Overflow Interrupts benötigt. Mit einem 15, 15, 15, 16 Schema werden in 4 Sekunden genau 61 Overflow Interrupts durchgeführt. Der relative Fehler beträgt dann

     61.035156  ...... 100%
     61         ...... x
     ----------------------
           61 * 100
      x = ---------- = 99.9424 %
           61.035156

Mit diesem Schema ist der Fehler beträchtlich gesunken. Nur noch 0.05%. Bei dieser Rate muss die Uhr immerhin etwas länger als 0.5 Stunden laufen, bis der Fehler auf eine Sekunde angewachsen ist.

Jetzt könnte man natürlich noch weiter gehen und immer kompliziertere "Schalt-Overflow"-Schemata austüfteln, und damit die Genauigkeit näher an 100% bringen, aber gibt es noch andere Möglichkeiten?

Hier wurde ein Teiler von 1024 eingesetzt. Was passiert bei einem anderen Teiler? Nehmen wir mal einen Teiler von 64. Das heist, es müssen ( 4000000 / 64 ) / 256 = 244.140625 Overflows auflaufen, bis 1 Sekunde vergangen ist. Wenn also 244 Overflows gezählt werden, dann beläuft sich der Fehler auf

     244.140625  ..... 100%
     244         ..... x
     ----------------------
           244 * 100
      x = ----------- = 99.9424 %
           244.140625

Nicht schlecht. Nur durch Verändern von 2 Zahlenwerten im Programm (Teilerfaktor und Anzahl der Overflow Interrupts bis zu einer Sekunden) kann die Genauigkeit gegenüber dem ursprünglichen Overflow-Schema beträchtlich gesteigert werden.

Aber geht das noch besser?

Ja das geht. Allerdings nicht mit dem Overflow Interrupt

Der CTC Modus des Timers

Worin liegt den das eigentliche Problem, mit dem die Uhr zu kämpfen hat?

Das Problem liegt darin, daß jedesmal ein kompletter Timerzyklus bis zum Overflow abgewartet werden muss, um darauf zu reagieren. Da aber nur jeweils ganzzahlige Overflowzyklen abgezählt werden können, heist das auch, dass im ersten Fall nur in Vielfachen von 1024 * 256 = 262144 Takten operiert werden kann, während im letzten Fall immerhin schon eine Grannulierung von 64 * 256 = 16384 Takten erreicht wird.

Aber offensichtlich ist das nicht genau genug. Bei 4 Mhz entsprechen 262144 Takte bereits einem Zeitraum von 0.0655 Sekunden, während 16384 Takte einem Zeitbedarf von 0.004096 Sekunden entsprechen. Beide Zahlen teilen aber 1000 [ms] nicht ganzzahlig. Und daraus entsteht der Fehler.

Angestrebt wird ein Timer der seinen Overflow so erreicht, dass sich ein ganzzahliger Teiler von 1 Sekunde einstellt. Dazu müssten man dem Timer aber vorschreiben können, bei welchem Zählerstand der Overflow erfolgen soll. Und genau dies ist im CTC Modus, allerdings beim Timer 1, möglich. CTC bedeutet Clear Timer on Compare match.

Timer 1, ein 16 Bit Timer, wird mit einem Vorteiler von 1 betrieben. Dadurch wird erreicht, dass der Timer taktgenau arbeiten kann. Bei jedem Ticken des Systemtaktes wird auch der Timer um 1 erhöht. Zusätzlich wird noch das WGM12 Bit bei der Konfiguration gesetzt. Dadurch wird der Timer in den CTC Modus gesetzt. Dabei wird der Inhalt des Timers hardwaremäßig mit dem Inhalt des OCR1A Registers verglichen. Stimmen beide überein, so wird der Timer auf 0 zurückgesetzt und im nächsten Taktzyklus ein OCIE1A Interrupt ausgelöst. Dadurch ist es möglich exakt die Anzahl an Taktzyklen festzulegen, die von einem Interrupt zum nächsten vergehen sollen. Das Compare Register OCR1A wird mit dem Wert 39999 vorbelegt. Dadurch vergehen exakt 40000 Taktzyklen von einem Compare Interrupt zum nächsten. 'Zufällig' ist dieser Wert so gewählt, daß bei einem Systemtakt von 4Mhz von einem Interrupt zum nächsten genau 1 hunderstel Sekunde vergeht. Bei einem möglichen Umbau der Uhr zu einer Stoppuhr könnte sich das als nützlich erweisen.

Im Interrupt wird das Hilfsregister bis 100 hochgezählt und nach 100 Interrupts kommt wieder die Sekundenweiterschaltung wie oben in Gang.

<avrasm> .include "m8def.inc"

.def temp1 = r16 .def temp2 = r17 .def temp3 = r18

.def SubCount = r21 .def Sekunden = r22 .def Minuten = r23 .def Stunden = r24

.org 0x0000

          rjmp    main                ; Reset Handler

.org OC1Aaddr

          rjmp    timer1_compare      ; Timer Compare Handler

.include "lcd-routines.asm"

main:

          ldi     temp1, LOW(RAMEND)  ; Stackpointer initialisieren
          out     SPL, temp1
          ldi     temp1, HIGH(RAMEND)
          out     SPH, temp1
          rcall   lcd_init
          rcall   lcd_clear

                                      ; Vergleichswert 
          ldi     temp1, high( 40000 - 1 )
          out     OCR1AH, temp1
          ldi     temp1, low( 40000 - 1 )
          out     OCR1AL, temp1
                                      ; CTC Modus einschalten
                                      ; Vorteiler auf 1
          ldi     temp1, ( 1 << WGM12 ) | ( 1 << CS10 )
          out     TCCR1B, temp1

          ldi     temp1, 1 << OCIE1A  ; OCIE1A: Interrupt bei Timer Compare
          out     TIMSK, temp1

          clr     Minuten           ; Die Uhr auf 0 setzen
          clr     Sekunden
          clr     Stunden
          clr     SubCount
          sei

loop: rjmp loop

timer1_compare:  ; Timer 1 Compare Handler

          inc     SubCount
          cpi     SubCount, 100     ; Sind 100 Interrupts vergangen
          brne    end_isr           ; Nein -> bereits fertig
          clr     SubCount          ; Zaehler wieder auf 0 und ...
          inc     Sekunden          ; ... 1 Sekunde registrieren
          cpi     Sekunden, 60      ; Sind 60 Sekunden vergangen?
          brne    Ausgabe           ; Nein -> fertig zur Anzeige
          clr     Sekunden          ; Ja: Sekunden wieder auf 0 und ...
          inc     Minuten           ; ... 1 Minute registrieren
          cpi     Minuten, 60       ; Sind 60 Minuten vergangen?
          brne    Ausgabe           ; Nein -> fertig zur Anzeige
          clr     Minuten           ; Ja: Minuten wieder auf 0 und ...
          inc     Stunden           ; ... 1 Stunde registrieren
          cpi     Stunden, 24       ; Sind 24 Stunden vergangen?
          brne    Ausgabe           ; Nein -> fertig zur Anzeige
          clr     Stunden           ; Ja: Stunden wieder auf 0

Ausgabe:

          rcall   lcd_clear         ; Display löschen
          mov     temp1, Stunden    ; Stunden 2-stellig ausgeben
          rcall   lcd_number_60
          ldi     temp1, ':'        ; danach ein ':'
          rcall   lcd_data
          mov     temp1, Minuten    ; anschiessend die Minuten 2-stellig
          rcall   lcd_number_60
          ldi     temp1, ':'        ; und wieder ein ':'
          rcall   lcd_data
          mov     temp1, Sekunden   ; Sekunden 2-stellig ausgeben
          rcall   lcd_number_60

end_isr:

          reti                      ; alles fertig. Interrupt abschliessen
 ; Eine Zahl aus dem Register temp1 ausgeben

lcd_number_60:

          push  temp2
          mov   temp2, temp1
                                 ; abzählen wieviele Zehner in
                                 ; der Zahl enthalten sind
          ldi   temp1, '0'

lcd_number_3_60:

          subi  temp2, 10
          brcs  lcd_number_4_60
          inc   temp1
          rjmp  lcd_number_3_60
                                 ; die Zehnerstelle ausgeben

lcd_number_4_60:

          rcall lcd_data
          subi  temp2, -10       ; 10 wieder dazuzählen, da die
                                 ; vorhergehende Schleife 10 zuviel
                                 ; abgezogen hat
                                 ; die übrig gebliebenen Einer
                                 ; noch ausgeben
          ldi   temp1, '0'
          add   temp1, temp2
          rcall lcd_data
          pop   temp2
          ret

</avrasm>

In der Interrupt Routine werden wieder, genauso wie vorher, die Anzahl der Interrupt Aufrufe gezählt. Beim 100. Aufruf sind daher 40000 * 100 = 4000000 Takte vergangen und da der Quarz mit 4000000 Schwingungen in der Sekunde arbeitet, ist daher eine Sekunde vergangen. Sie wird genauso wie vorher registriert und die Uhr entsprechend hochgezählt.

Wird jetzt die Uhr mit einer Kommerziellen verglichen, dann fällt nach einiger Zeit auf ... Sie geht immer noch falsch!

Was ist jetzt die Ursache?

Die Ursache liegt in einem Problem, dass nicht direkt behebbar ist. Am Quarz! Auch wenn auf dem Quarz drauf steht, dass er 4Mhz macht, so stimmt das nicht. Auch Quarze haben Fertigungstoleranzen und Quarze verändern ihre Frequenz mit der Temperatur. In Uhren kommen normalerweise genauer gefertigte Uhrenquarze zum Einsatz, die vom Uhrmacher auch noch auf die exakte Frequenz abgeglichen werden (mittels Kondensatoren und Frequenzzähler). Diese Einflüsse auf die Quarzfrequenz sind aber messbar und sind per Software behebbar. Dazu wird einfach die Uhr eine zeitlang (Tage, Wochen) laufen gelassen und die Abweichung festgestellen. Aus dieser Abweichung lässt sich dann errechnen, wie schnell der Quarz wirklich schwingt. Und da, dank CTC, die Messperiode taktgenau eingestellt werden kann, ist es möglich diesen Frequenzfehler auszugleichen. Der genaue Vorgang ist zb. hier beschrieben.