AVR-Tutorial: Uhr: Unterschied zwischen den Versionen

Aus der Mikrocontroller.net Artikelsammlung, mit Beiträgen verschiedener Autoren (siehe Versionsgeschichte)
Wechseln zu: Navigation, Suche
(Fehlenden push und pop Befehl in Routine timer1_compare ergänzt.)
K (→‎Ganggenauigkeit: TeX-Textsatz-Korrekturen, geschützte Leerzeichen)
 
(5 dazwischenliegende Versionen von 3 Benutzern werden nicht angezeigt)
Zeile 1: Zeile 1:
Eine beliebte Übung für jeden Programmierer ist die Implementierung einer Uhr. Die meisten Uhren bestehen aus einem Taktgeber 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
Eine beliebte Übung für jeden Programmierer ist die Implementierung einer Uhr. Die meisten Uhren bestehen aus einem Taktgeber 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
 
* [[AVR-Tutorial: LCD|Ansteuerung eines LC-Displays]] und
* [[AVR-Tutorial: LCD| Ansteuerung eines LC-Displays]]
* [[AVR-Tutorial: Timer|Timer]].
* [[AVR-Tutorial: Timer| Timer]]


== Aufbau und Funktion ==
== Aufbau und Funktion ==


Die Aufgabe des Taktgebers, der uns einen möglichst konstanten und genauen Takt liefert, übernimmt ein Timer. Der Timer ermöglicht einen einfachen Zugang zum Takt, den der AVR vom Quarz abgreift. Wie schon im Einführungskapitel über den [[AVR-Tutorial:_Timer|Timer]] beschrieben, wird dazu ein Timer Overflow Interrupt installiert, und in diesem Interrupt wird die eigentliche Uhr hochgezählt. Die Uhr besteht aus vier Registern. Drei davon repräsentieren die Sekunden, Minuten und Stunden unserer Uhr. Nach jeweils einer Sekunde wird das Sekundenregister um eins erhöht. Sind 60 Sekunden vergangen, wird das Sekundenregister wieder auf Null gesetzt und gleichzeitig das Minutenregister um eins erhöht. Dies ist ein Überlauf. Nach 60 Minuten werden die Minuten wieder auf Null gesetzt und für diese vergangenen 60 Minuten eine Stunde aufaddiert. Nach 24 Stunden schliesslich werden die Stunden wieder auf Null gesetzt, ein ganzer Tag ist vergangen.
Die Aufgabe des Taktgebers, der uns einen möglichst konstanten und genauen Takt liefert, übernimmt ein ''Timer''. Der Timer ermöglicht einen einfachen Zugang zum Takt, den der AVR vom Quarz abgreift. Wie schon im Einführungskapitel über den [[AVR-Tutorial: Timer|Timer]] beschrieben, wird dazu ein ''Timer Overflow Interrupt'' installiert, und in diesem Interrupt wird die eigentliche Uhr hochgezählt. Die Uhr besteht aus vier Registern. Drei davon repräsentieren die Sekunden, Minuten und Stunden unserer Uhr. Nach jeweils einer Sekunde wird das Sekundenregister um eins erhöht. Sind 60 Sekunden vergangen, wird das Sekundenregister wieder auf null gesetzt und gleichzeitig das Minutenregister um eins erhöht. Dies ist ein Überlauf. Nach 60 Minuten werden die Minuten wieder auf null gesetzt und für diese vergangenen 60 Minuten eine Stunde aufaddiert. Nach 24 Stunden schließlich werden die Stunden wieder auf Null gesetzt, ein ganzer Tag ist vergangen.


Aber wozu das vierte Register?
Aber wozu das vierte Register?


Der Mikrocontroller wird mit 4 MHz 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 Überlauf auslöst. Es ereignen sich also 3906,25 / 256 = 15,2587 Überläufe pro Sekunde. Die Aufgabe des vierten Registers ist es nun, diese 15 Überläufe zu zählen. Bei Auftreten des 15. ist eine Sekunde vergangen. Dies stimmt jedoch nicht exakt, denn die Division weist ja auch Nachkommastellen auf, hat einen Rest, der hier im Moment der Einfachheit halber ignoriert wird. In einem späteren Abschnitt wird darauf noch eingegangen werden.
Der Mikrocontroller wird mit 4 MHz betrieben. Bei einem Teiler von 1.024 zählt der Timer also mit 4.000.000 / 1.024 = 3.906,25 Pulsen pro Sekunde. Der Timer muss einmal bis 256 zählen, bis er einen Überlauf auslöst. Es ereignen sich also 3.906,25 / 256 = 15,2587 Überläufe pro Sekunde. Die Aufgabe des vierten Registers ist es nun, diese 15 Überläufe zu zählen. Bei Auftreten des 15. ist eine Sekunde vergangen. Dies stimmt jedoch nicht exakt, denn die Division weist ja auch Nachkommastellen auf, hat einen Rest, der hier im Moment der Einfachheit halber ignoriert wird. In einem späteren Abschnitt wird darauf noch eingegangen werden.


Im Überlauf-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 [[AVR-Tutorial:_LCD|LCD Funktionen]] benutzt.
Im Überlauf-Interrupt wird also diese Kette von Zählvorgängen auf den Sekunden, Minuten und Stunden durchgeführt und anschließend zur Anzeige gebracht. Dazu werden die in einem vorhergehenden Kapitel entwickelten [[AVR-Tutorial: LCD|LCD-Funktionen]] benutzt.


== Das erste Programm ==
== Das erste Programm ==


<syntaxhighlight lang="avrasm">  
<syntaxhighlight lang="asm">
.include "m8def.inc"
.include "m8def.inc"
 
.def temp1 = r16
.def temp1 = r16
.def temp2 = r17
.def temp2 = r17
.def temp3 = r18
.def temp3 = r18
.def flag  = r19
.def flag  = r19
 
.def SubCount = r21
.def SubCount = r21
.def Sekunden = r22
.def Sekunden = r22
Zeile 37: Zeile 36:


main:
main:
         ldi    temp1, HIGH(RAMEND)
         ldi    temp1, HIGH(RAMEND) ; Stackpointer initialisieren
         out    SPH, temp1
         out    SPH, temp1
         ldi    temp1, LOW(RAMEND) ; Stackpointer initialisieren
         ldi    temp1, LOW(RAMEND)
         out    SPL, temp1
         out    SPL, temp1


         ldi temp1, 0xFF   ; Port D = Ausgang
         ldi temp1, 0xFF             ; Port D = Ausgang
         out DDRD, temp1  
         out DDRD, temp1  
   
 
         rcall  lcd_init
         rcall  lcd_init
         rcall  lcd_clear
         rcall  lcd_clear
 
         ldi    temp1, (1<<CS02) | (1<<CS00) ; Teiler 1024
         ldi    temp1, (1<<CS02) | (1<<CS00)   ; Teiler 1024
         out    TCCR0, temp1
         out    TCCR0, temp1
 
         ldi    temp1, 1<<TOIE0    ; TOIE0: Interrupt bei Timer Overflow
         ldi    temp1, 1<<TOIE0    ; Interrupt bei Timer Overflow
         out    TIMSK, temp1
         out    TIMSK, temp1
 
         clr    Minuten            ; Die Uhr auf 0 setzen
         clr    Minuten            ; Die Uhr auf 0 setzen
         clr    Sekunden
         clr    Sekunden
Zeile 62: Zeile 60:


         sei
         sei
 
loop:
loop:
         cpi    flag,0
         cpi    flag, 0
         breq    loop                ; Flag im Interrupt gesetzt?
         breq    loop                ; Flag im Interrupt gesetzt?
         ldi    flag,0             ; flag löschen
         ldi    flag, 0             ; Flag löschen
 
 
         rcall  lcd_clear          ; das LCD löschen
         rcall  lcd_clear          ; das LCD löschen
         mov    temp1, Stunden      ; und die Stunden ausgeben
         mov    temp1, Stunden      ; und die Stunden ausgeben
Zeile 81: Zeile 79:


         rjmp    loop
         rjmp    loop
 
timer0_overflow:                    ; Timer 0 Overflow Handler
timer0_overflow:                    ; Timer 0 Overflow Handler


         push    temp1              ; temp 1 sichern
         push    temp1              ; temp1 sichern
         in      temp1,sreg         ; SREG sichern
         in      temp1, sreg         ; SREG sichern
        push    temp1


         inc    SubCount            ; Wenn dies nicht der 15. Interrupt
         inc    SubCount            ; Wenn dies nicht der 15. Interrupt
         cpi    SubCount, 15        ; ist, dann passiert gar nichts
         cpi    SubCount, 15        ; ist, dann passiert gar nichts.
         brne    end_isr
         brne    end_isr


Zeile 95: Zeile 92:
         clr    SubCount            ; SubCount rücksetzen
         clr    SubCount            ; SubCount rücksetzen
         inc    Sekunden            ; plus 1 Sekunde
         inc    Sekunden            ; plus 1 Sekunde
         cpi    Sekunden, 60        ; sind 60 Sekunden vergangen?
         cpi    Sekunden, 60        ; Sind 60 Sekunden vergangen?
         brne    Ausgabe            ; wenn nicht kann die Ausgabe schon
         brne    Ausgabe            ; Wenn nicht, kann die Ausgabe schon
                                     ; gemacht werden
                                     ; gemacht werden.


                                     ; Überlauf
                                     ; Überlauf
         clr    Sekunden            ; Sekunden wieder auf 0 und dafür
         clr    Sekunden            ; Sekunden wieder auf 0 und dafür
         inc    Minuten            ; plus 1 Minute
         inc    Minuten            ; plus 1 Minute
         cpi    Minuten, 60        ; sind 60 Minuten vergangen ?
         cpi    Minuten, 60        ; Sind 60 Minuten vergangen?
         brne    Ausgabe            ; wenn nicht, -> Ausgabe
         brne    Ausgabe            ; Wenn nicht, -> Ausgabe


                                     ; Überlauf
                                     ; Überlauf
         clr    Minuten            ; Minuten zurücksetzen und dafür
         clr    Minuten            ; Minuten zurücksetzen und dafür
         inc    Stunden            ; plus 1 Stunde
         inc    Stunden            ; plus 1 Stunde
         cpi    Stunden, 24        ; nach 24 Stunden, die Stundenanzeige
         cpi    Stunden, 24        ; Nach 24 Stunden die Stundenanzeige
         brne    Ausgabe            ; wieder zurücksetzen
         brne    Ausgabe            ; wieder zurücksetzen, sonst -> Ausgabe.


                                     ; Überlauf
                                     ; Überlauf
Zeile 115: Zeile 112:


Ausgabe:
Ausgabe:
         ldi    flag,1             ; Flag setzen, LCD updaten
         ldi    flag, 1             ; Flag setzen, LCD updaten


end_isr:
end_isr:
 
         out    sreg, temp1         ; SREG wiederherstellen
        pop    temp1
         pop    temp1               ; temp1 wiederherstellen
         out    sreg,temp1         ; sreg wieder herstellen
         reti                        ; Das war's. Interrupt ist fertig.
         pop    temp1
         reti                        ; das wars. Interrupt ist fertig


; Eine Zahl aus dem Register temp1 ausgeben
; Eine Zahl aus dem Register temp1 ausgeben


lcd_number:
lcd_number:
         push    temp2              ; register sichern,
         push    temp2              ; Register temp2 sichern,
                                     ; wird für Zwsichenergebnisse gebraucht    
                                     ; wird für Zwischenergebnisse gebraucht.
         ldi    temp2, '0'        
         ldi    temp2, '0'
lcd_number_10:              
lcd_number_10:
         subi    temp1, 10          ; abzählen wieviele Zehner in
         subi    temp1, 10          ; Abzählen, wieviele Zehner in
         brcs    lcd_number_1        ; der Zahl enthalten sind
         brcs    lcd_number_1        ; der Zahl enthalten sind
         inc    temp2
         inc    temp2
         rjmp    lcd_number_10
         rjmp    lcd_number_10
lcd_number_1:
lcd_number_1:
         push    temp1              ; den Rest sichern (http://www.mikrocontroller.net/topic/172026)
         push    temp1              ; den Rest sichern (https://www.mikrocontroller.net/topic/172026)
         mov    temp1,temp2         ;
         mov    temp1, temp2
         rcall  lcd_data            ; die Zehnerstelle ausgeben
         rcall  lcd_data            ; die Zehnerstelle ausgeben
         pop    temp1              ; den Rest wiederherstellen
         pop    temp1              ; den Rest wiederherstellen
         subi    temp1, -10          ; 10 wieder dazuzählen, da die
         subi    temp1, -10          ; 10 wieder dazuzählen, da die
                                     ; vorhergehende Schleife 10 zuviel
                                     ; vorhergehende Schleife 10 zuviel
                                     ; abgezogen hat
                                     ; abgezogen hat.
                                     ; das Subtrahieren von -10
                                     ; Das Subtrahieren von -10
                                     ; = Addition von +10 ist ein Trick
                                     ; = Addition von +10 ist ein Trick,
                                     ; da kein addi Befehl existiert
                                     ; da kein addi-Befehl existiert.
         ldi    temp2, '0'          ; die übrig gebliebenen Einer
         ldi    temp2, '0'          ; die übrig gebliebenen Einer
         add    temp1, temp2        ; noch ausgeben
         add    temp1, temp2        ; noch ausgeben
         rcall  lcd_data
         rcall  lcd_data


         pop    temp2              ; Register wieder herstellen
         pop    temp2              ; Register wiederherstellen
         ret
         ret
</syntaxhighlight>
</syntaxhighlight>


In der ISR wird nur die Zeit in den Registern neu berechnet, die Ausgabe auf das LCD erfolgt in der Hauptschleife. Das ist notwendig, da die LCD-Ausgabe bisweilen sehr lange dauern kann. Wenn sie länger als ~2/15 Sekunden dauert werden Timerinterrupts "verschluckt" und unsere Uhr geht noch mehr falsch. Dadurch, dass aber die Ausgabe in der Hauptschleife durchgeführt wird, welche jederzeit durch einen Timerinterrupt unterbrochen werden kann, werden keine Timerinterrupts verschluckt. Das ist vor allem wichtig, wenn mit höheren Interruptfrequenzen gearbeitet wird, z.&nbsp;B. 1/100s im Beispiel weiter unten. Auch wenn in diesem einfachen Beispiel die Ausgabe bei weitem nicht 2/15 Sekunden dauert, sollte man sich diesen Programmierstil allgemein angewöhnen. Siehe auch [[Interrupt]].
In der ISR wird nur die Zeit in den Registern neu berechnet, die Ausgabe auf das LCD erfolgt in der Hauptschleife. Das ist notwendig, da die LCD-Ausgabe bisweilen sehr lange dauern kann. Wenn sie länger als ≈2/15 Sekunden dauert, werden Timerinterrupts „verschluckt“ und unsere Uhr geht noch mehr falsch. Dadurch, dass aber die Ausgabe in der Hauptschleife durchgeführt wird, welche jederzeit durch einen Timerinterrupt unterbrochen werden kann, werden keine Timerinterrupts verschluckt. Das ist vor allem wichtig, wenn mit höheren Interruptfrequenzen gearbeitet wird, z.&nbsp;B. 1/100&nbsp;s im Beispiel weiter unten. Auch wenn in diesem einfachen Beispiel die Ausgabe bei weitem nicht 2/15 Sekunden dauert, sollte man sich diesen Programmierstil allgemein angewöhnen. Siehe auch [[Interrupt]].


Eine weitere Besonderheit ist das Register '''flag''' (=r19). Dieses Register fungiert als Anzeiger, wie eine Flagge, daher auch der Name. In der ISR wird dieses Register auf 1 gesetzt. Die Hauptschleife, also alles zwischen ''loop'' und ''rjmp loop'', prüft dieses Flag und nur dann, wenn das Flag auf 1 steht, wird die LCD Ausgabe gemacht und das Flag wieder auf 0 zurückgesetzt. Auf diese Art wird nur dann Rechenzeit für die LCD Ausgabe verbraucht, wenn dies tatsächlich notwendig ist. Die ISR teilt mit dem Flag der Hauptschleife mit, dass eine bestimmte Aufgabe, nämlich der Update der Anzeige gemacht werden muss und die Hauptschleife reagiert darauf bei nächster Gelegenheit, indem sie diese Aufgabe ausführt und setzt das Flag zurück. Solche Flags werden daher auch '''Job-Flags''' genannt, weil durch ihr setzten das Abarbeiten eines Jobs (einer Aufgabe) angestoßen wird. Auch hier gilt wieder: Im Grunde würde man in diesem speziellen Beispiel kein Job-Flag benötigen, weil es in der Hauptschleife nur einen einzigen möglichen Job, die Neuausgabe der Uhrzeit, gibt. Sobald aber Programme komplizierter werden und mehrere Jobs möglich sind, sind Job-Flags eine gute Möglichkeit, ein Programm so zu organsieren, dass bestimmte Dinge nur dann gemacht werden, wenn sie notwendig sind.
Eine weitere Besonderheit ist das Register '''<code>flag</code>''' (=&nbsp;r19). Dieses Register fungiert als Anzeiger, wie eine Flagge, daher auch der Name. In der ISR wird dieses Register auf 1 gesetzt. Die Hauptschleife, also alles zwischen <code>loop:</code> und <code>rjmp loop</code>, prüft dieses Flag und nur dann, wenn das Flag auf 1 steht, wird die LCD-Ausgabe gemacht und das Flag wieder auf 0 zurückgesetzt. Auf diese Art wird nur dann Rechenzeit für die LCD-Ausgabe verbraucht, wenn dies tatsächlich notwendig ist. Die ISR teilt mit dem Flag der Hauptschleife mit, dass eine bestimmte Aufgabe, nämlich das Update der Anzeige, gemacht werden muss und die Hauptschleife reagiert darauf bei nächster Gelegenheit, indem sie diese Aufgabe ausführt und setzt das Flag zurück. Solche Flags werden daher auch '''Job-Flags''' genannt, weil durch ihr Setzen das Abarbeiten eines Jobs (einer Aufgabe) angestoßen wird. Auch hier gilt wieder: Im Grunde würde man in diesem speziellen Beispiel kein Job-Flag benötigen, weil es in der Hauptschleife nur einen einzigen möglichen Job, die Neuausgabe der Uhrzeit, gibt. Sobald aber Programme komplizierter werden und mehrere Jobs möglich sind, sind Job-Flags eine gute Möglichkeit, ein Programm so zu organisieren, dass bestimmte Dinge nur dann getan werden, wenn sie notwendig sind.


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 die Bedienungsanleitung einer käuflichen 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. Als Zwischenlösung kann man im Programm die Uhr beim Start anstelle von 00:00:00 z.&nbsp;B. auch auf 20:00:00 stellen und exakt mit dem Start der Tagesschau starten. Wobei der Start der Tagesschau verzögert bei uns ankommt, je nach Übertragung können das mehrere Sekunden sein.
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 die Bedienungsanleitung einer käuflichen 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. Als Zwischenlösung kann man im Programm die Uhr beim Start anstelle von 00:00:00 z.&nbsp;B. auch auf 20:00:00 stellen und exakt mit dem Start der Tagesschau starten. Wobei der Start der Tagesschau verzögert bei uns ankommt, je nach Übertragung können das mehrere Sekunden sein.


== Ganggenauigkeit ==
== Ganggenauigkeit ==


Wird die Uhr mit einer gekauften Uhr verglichen, so stellt man schnell fest, dass sie ganz schön ungenau geht. Sie geht vor! Woran liegt das? Die Berechnung sieht so aus:
Wird die Uhr mit einer gekauften Uhr verglichen, so stellt man schnell fest, dass sie ganz schön ungenau geht. Sie geht vor! Woran liegt das? Die Berechnung sieht so aus:
* Frequenz des Quarzes: 4.0 MHz
* Frequenz des Quarzes: 4,0 MHz
* Vorteiler des Timers: 1024
* Vorteiler des Timers: 1024
* Überlauf alle 256 Timertakte
* Überlauf alle 256 Timertakte


Daraus errechnet sich, dass in einer Sekunde 4000000 / 1024 / 256 = 15.258789 Overflow Interrupts auftreten. Im Programm wird aber bereits nach 15 Overflows eine Sekunde weitergeschaltet, daher geht die Uhr vor. Rechnen wir etwas:
Daraus errechnet sich, dass in einer Sekunde 4.000.000 / 1.024 / 256 = 15,258789 Overflow-Interrupts auftreten. Im Programm wird aber bereits nach 15&nbsp;Overflows eine Sekunde weitergeschaltet, daher geht die Uhr vor. Rechnen wir etwas:


:<math>F_r = (\frac {15}{15,258789}-1) \cdot 100% = -1,69%</math>
:<math>F_r = \left(\frac{15}{15{,}258789} - 1\right) \cdot 100\,\% = -1{,}69\,\%</math>


So wie bisher läuft die Uhr also rund 1.7 % zu schnell. In einer Minute ist das immerhin etwas mehr als eine Sekunde. 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 15er Overflow Sekunden folgt eine Sekunde für die 16 Overflows ablaufen müssen. 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
So wie bisher läuft die Uhr also rund 1,7 % zu schnell. In einer Minute ist das immerhin etwas mehr als eine Sekunde. 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 15er-Overflow-Sekunden folgt eine Sekunde, für die 16 Overflows ablaufen müssen. Wie sieht die Rechnung bei einem (15, 15, 15, 16)-Schema aus? Für 4&nbsp;Sekunden werden exakt 15,258789 ·&nbsp;4 =&nbsp;61,035156 Overflow-Interrupts benötigt. Mit einem (15, 15, 15, 16)-Schema werden in 4&nbsp;Sekunden genau 61&nbsp;Overflow-Interrupts durchgeführt. Der relative Fehler beträgt dann


:<math>F_r = (\frac {61}{61,035156}-1) \cdot 100% = -0,0575%</math>
:<math>F_r = \left(\frac{61}{61{,}035156} - 1\right) \cdot 100\,\% = -0{,}0575\,\%</math>.


Mit diesem Schema ist der Fehler beträchtlich gesunken. Nur noch 0.06%. Bei dieser Rate muss die Uhr immerhin etwas länger als 0,5 Stunden laufen, bis der Fehler auf eine Sekunde angewachsen ist. Das sind aber immer noch 48 Sekunden pro Tag bzw. 1488 Sekunden (=24,8 Minuten) pro Monat. So schlecht sind nicht mal billige mechanische Uhren!
Mit diesem Schema ist der Fehler beträchtlich gesunken. Nur noch 0,06 %. Bei dieser Rate muss die Uhr immerhin etwas länger als ½&nbsp;Stunde laufen, bis der Fehler auf eine Sekunde angewachsen ist. Das sind aber immer noch 48&nbsp;Sekunden pro Tag bzw. 1488 Sekunden (=&nbsp;24,8&nbsp;Minuten) pro Monat. So schlecht sind nicht mal billige mechanische Uhren!


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?
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?


Im ersten Ansatz wurde ein Vorteiler von 1024 eingesetzt. Was passiert bei einem anderen Vorteiler? Nehmen wir mal einen Vorteiler von 64. Das heißt, 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
Im ersten Ansatz wurde ein Vorteiler von 1.024 eingesetzt. Was passiert bei einem anderen Vorteiler? Nehmen wir mal einen Vorteiler von 64. Das heißt, es müssen (4.000.000 /&nbsp;64) /&nbsp;256 =&nbsp;244,140625 Overflows auflaufen, bis 1&nbsp;Sekunde vergangen ist. Wenn also 244&nbsp;Overflows gezählt werden, dann beläuft sich der Fehler auf


:<math>F_r = (\frac {244}{244,140625}-1) \cdot 100% = -0,0576%</math>
:<math>F_r = \left(\frac{244}{244{,}140625} - 1\right) \cdot 100\,\% = -0{,}0576\,\%</math>.


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.
Nicht schlecht. Nur durch Verändern von zwei Zahlenwerten im Programm (Teilerfaktor und Anzahl der Overflow-Interrupts bis zu einer Sekunde) 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 ==
== Der CTC-Modus des Timers ==


Worin liegt denn das eigentliche Problem, mit dem die Uhr zu kämpfen hat? Es liegt darin, dass 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, heißt das auch, dass im ersten Fall nur in Vielfachen von 1024 * 256 = 262.144 Takten operiert werden kann, während im letzten Fall immerhin schon eine Granulierung von 64 * 256 = 16.384 Takten erreicht wird. Aber offensichtlich ist das nicht genau genug. Bei 4 MHz entsprechen 262.144 Takte bereits einem Zeitraum von 65,5 ms, während 16384 Takte einem Zeitbedarf von 4,096 ms entsprechen. Beide Zahlen teilen aber 1.000 ms nicht ganzzahlig, Nachkommareste fallen unter den Tisch und daraus summiert sich der Fehler auf. Angestrebt wird ein Timer, der seinen <em>Overflow</em> so erreicht, dass sich ein ganzzahliger Teiler von einer Sekunde einstellt. Dann gibt es keinen Rest. Dazu müsste man dem Timer aber vorschreiben können, bei welchem Zählerstand der <em>Overflow</em> erfolgen soll. Und genau dies ist im '''CTC'''-Modus möglich, allerdings nur beim Timer 1. '''CTC''' bedeutet "'''C'''lear '''T'''imer on '''C'''ompare match".
Worin liegt denn das eigentliche Problem, mit dem die Uhr zu kämpfen hat? Es liegt darin, dass 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, heißt das auch, dass im ersten Fall nur in Vielfachen von 1024 ·&nbsp;256 = 262.144 Takten operiert werden kann, während im letzten Fall immerhin schon eine Granulierung von 64 ·&nbsp;256 = 16.384 Takten erreicht wird. Aber offensichtlich ist das nicht genau genug. Bei 4&nbsp;MHz entsprechen 262.144 Takte bereits einem Zeitraum von 65,5&nbsp;ms, während 16.384 Takte einem Zeitbedarf von 4,096&nbsp;ms entsprechen. Beide Zahlen teilen aber 1.000&nbsp;ms nicht ganzzahlig, Nachkommareste fallen unter den Tisch und daraus summiert sich der Fehler auf. Angestrebt wird ein Timer, der seinen „Overflow“ so erreicht, dass sich ein ganzzahliger Teiler von einer Sekunde einstellt. Dann gibt es keinen Rest. Dazu müsste man dem Timer aber vorschreiben können, bei welchem Zählerstand der „Overflow“ erfolgen soll. Und genau dies ist im '''CTC-Modus''' möglich, allerdings nur beim Timer&nbsp;1. CTC bedeutet ''<u>C</u>lear <u>T</u>imer on <u>C</u>ompare Match''.


Timer 1, ein 16-Bit-Timer, wird mit einem Vorteiler von 1 betrieben. Dadurch wird erreicht, dass der Timer mit höchster Zeitauflösung arbeiten kann. Bei jedem Ticken des Systemtaktes von 4 MHz 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 39.999 vorbelegt. Dadurch vergehen exakt 40.000 Taktzyklen von einem Compare-Interrupt zum nächsten. "Zufällig" ist dieser Wert so gewählt, dass bei einem Systemtakt von 4 MHz von einem Interrupt zum nächsten genau 1/100 Sekunde vergeht, denn 40.000 / 4.000.000 = 0,01. Bei einem möglichen Umbau der Uhr zu einer Stoppuhr könnte sich die Hundertstelsekunde als nützlich erweisen. Im Interrupt wird das Hilfsregister SubCount bis 100 hochgezählt, und nach 100 Interrupts kommt wieder die Sekundenweiterschaltung wie oben in Gang.
Timer 1, ein 16-Bit-Timer, wird mit einem Vorteiler von 1 betrieben. Dadurch wird erreicht, dass der Timer mit höchster Zeitauflösung arbeiten kann. Bei jedem Ticken des Systemtaktes von 4&nbsp;MHz 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 39.999 vorbelegt. Dadurch vergehen exakt 40.000 Taktzyklen von einem Compare-Interrupt zum nächsten. „Zufällig“ ist dieser Wert so gewählt, dass bei einem Systemtakt von 4&nbsp;MHz von einem Interrupt zum nächsten genau 1/100 Sekunde vergeht, denn 40.000 / 4.000.000 = 0,01. Bei einem möglichen Umbau der Uhr zu einer Stoppuhr könnte sich die Hundertstelsekunde als nützlich erweisen. Im Interrupt wird das Hilfsregister SubCount bis 100 hochgezählt, und nach 100 Interrupts kommt wieder die Sekundenweiterschaltung wie oben in Gang.


<syntaxhighlight lang="avrasm">  
<syntaxhighlight lang="asm">
.include "m8def.inc"
.include "m8def.inc"
 
.def temp1 = r16
.def temp1 = r16
.def temp2 = r17
.def temp2 = r17
.def temp3 = r18
.def temp3 = r18
.def Flag  = r19
.def Flag  = r19
 
.def SubCount = r21
.def SubCount = r21
.def Sekunden = r22
.def Sekunden = r22
Zeile 212: Zeile 207:


main:
main:
         ldi    temp1, HIGH(RAMEND)
         ldi    temp1, HIGH(RAMEND) ; Stackpointer initialisieren
         out    SPH, temp1
         out    SPH, temp1
         ldi    temp1, LOW(RAMEND) ; Stackpointer initialisieren
         ldi    temp1, LOW(RAMEND)
         out    SPL, temp1
         out    SPL, temp1


         ldi temp1, 0xFF   ; Port D = Ausgang
         ldi temp1, 0xFF             ; Port D = Ausgang
         out DDRD, temp1    
         out DDRD, temp1


         rcall  lcd_init
         rcall  lcd_init
         rcall  lcd_clear
         rcall  lcd_clear
 
                                     ; Vergleichswert  
                                     ; Vergleichswert
         ldi    temp1, high( 40000 - 1 )
         ldi    temp1, high(40000 - 1)
         out    OCR1AH, temp1
         out    OCR1AH, temp1
         ldi    temp1, low( 40000 - 1 )
         ldi    temp1, low(40000 - 1)
         out    OCR1AL, temp1
         out    OCR1AL, temp1
                                     ; CTC Modus einschalten
                                     ; CTC-Modus einschalten
                                     ; Vorteiler auf 1
                                     ; Vorteiler auf 1
         ldi    temp1, ( 1 << WGM12 ) | ( 1 << CS10 )
         ldi    temp1, (1 << WGM12) | (1 << CS10)
         out    TCCR1B, temp1
         out    TCCR1B, temp1
 
         ldi    temp1, 1 << OCIE1A  ; OCIE1A: Interrupt bei Timer Compare
         ldi    temp1, 1 << OCIE1A  ; Interrupt bei Timer Compare Match
         out    TIMSK, temp1
         out    TIMSK, temp1
 
         clr    Minuten            ; Die Uhr auf 0 setzen
         clr    Minuten            ; Die Uhr auf 0 setzen
         clr    Sekunden
         clr    Sekunden
Zeile 244: Zeile 239:
         sei
         sei
loop:
loop:
         cpi    flag,0
         cpi    flag, 0
         breq    loop                ; Flag im Interrupt gesetzt?
         breq    loop                ; Flag im Interrupt gesetzt?
         ldi    flag,0             ; Flag löschen
         ldi    flag, 0             ; Flag löschen
 
 
         rcall  lcd_clear          ; das LCD löschen
         rcall  lcd_clear          ; das LCD löschen
         mov    temp1, Stunden      ; und die Stunden ausgeben
         mov    temp1, Stunden      ; und die Stunden ausgeben
Zeile 261: Zeile 256:


         rjmp    loop
         rjmp    loop
 
timer1_compare:                    ; Timer 1 Output Compare Handler
timer1_compare:                    ; Timer 1 Output Compare Handler


         push    temp1              ; temp 1 sichern
         push    temp1              ; temp1 sichern
         in      temp1,sreg         ; SREG sichern
         in      temp1, sreg         ; SREG sichern
        push    temp1


         inc    SubCount            ; Wenn dies nicht der 100. Interrupt
         inc    SubCount            ; Wenn dies nicht der 100. Interrupt
         cpi    SubCount, 100      ; ist, dann passiert gar nichts
         cpi    SubCount, 100      ; ist, dann passiert gar nichts.
         brne    end_isr
         brne    end_isr


Zeile 275: Zeile 269:
         clr    SubCount            ; SubCount rücksetzen
         clr    SubCount            ; SubCount rücksetzen
         inc    Sekunden            ; plus 1 Sekunde
         inc    Sekunden            ; plus 1 Sekunde
         cpi    Sekunden, 60        ; sind 60 Sekunden vergangen?
         cpi    Sekunden, 60        ; Sind 60 Sekunden vergangen?
         brne    Ausgabe            ; wenn nicht kann die Ausgabe schon
         brne    Ausgabe            ; Wenn nicht, kann die Ausgabe schon
                                     ; gemacht werden
                                     ; gemacht werden.


                                     ; Überlauf
                                     ; Überlauf
         clr    Sekunden            ; Sekunden wieder auf 0 und dafür
         clr    Sekunden            ; Sekunden wieder auf 0 und dafür
         inc    Minuten            ; plus 1 Minute
         inc    Minuten            ; plus 1 Minute
         cpi    Minuten, 60        ; sind 60 Minuten vergangen ?
         cpi    Minuten, 60        ; Sind 60 Minuten vergangen?
         brne    Ausgabe            ; wenn nicht, -> Ausgabe
         brne    Ausgabe            ; Wenn nicht, -> Ausgabe


                                     ; Überlauf
                                     ; Überlauf
         clr    Minuten            ; Minuten zurücksetzen und dafür
         clr    Minuten            ; Minuten zurücksetzen und dafür
         inc    Stunden            ; plus 1 Stunde
         inc    Stunden            ; plus 1 Stunde
         cpi    Stunden, 24        ; nach 24 Stunden, die Stundenanzeige
         cpi    Stunden, 24        ; Nach 24 Stunden die Stundenanzeige
         brne    Ausgabe            ; wieder zurücksetzen
         brne    Ausgabe            ; wieder zurücksetzen, sonst -> Ausgabe.


                                     ; Überlauf
                                     ; Überlauf
Zeile 295: Zeile 289:


Ausgabe:
Ausgabe:
         ldi    flag,1             ; Flag setzen, LCD updaten
         ldi    flag, 1             ; Flag setzen, LCD updaten


end_isr:
end_isr:
        pop    temp1
         out    sreg, temp1         ; SREG wiederherstellen
         out    sreg,temp1         ; sreg wieder herstellen
         pop    temp1               ; temp1 wiederherstellen
         pop    temp1
         reti                        ; Das war's. Interrupt ist fertig.
         reti                        ; das wars. Interrupt ist fertig


; Eine Zahl aus dem Register temp1 ausgeben
; Eine Zahl aus dem Register temp1 ausgeben


lcd_number:
lcd_number:
         push    temp2              ; register sichern,
         push    temp2              ; Register temp2 sichern,
                                     ; wird für Zwsichenergebnisse gebraucht    
                                     ; wird für Zwischenergebnisse gebraucht.
         ldi    temp2, '0'        
         ldi    temp2, '0'
lcd_number_10:              
lcd_number_10:
         subi    temp1, 10          ; abzählen wieviele Zehner in
         subi    temp1, 10          ; Abzählen, wieviele Zehner in
         brcs    lcd_number_1        ; der Zahl enthalten sind
         brcs    lcd_number_1        ; der Zahl enthalten sind
         inc    temp2
         inc    temp2
Zeile 316: Zeile 309:
lcd_number_1:
lcd_number_1:
         push    temp1              ; den Rest sichern (http://www.mikrocontroller.net/topic/172026)
         push    temp1              ; den Rest sichern (http://www.mikrocontroller.net/topic/172026)
         mov    temp1,temp2         ;
         mov    temp1, temp2
         rcall  lcd_data            ; die Zehnerstelle ausgeben
         rcall  lcd_data            ; die Zehnerstelle ausgeben
         pop    temp1              ; den Rest wieder holen
         pop    temp1              ; den Rest wiederherstellen
         subi    temp1, -10          ; 10 wieder dazuzählen, da die
         subi    temp1, -10          ; 10 wieder dazuzählen, da die
                                     ; vorhergehende Schleife 10 zuviel
                                     ; vorhergehende Schleife 10 zuviel
                                     ; abgezogen hat
                                     ; abgezogen hat.
                                     ; das Subtrahieren von -10
                                     ; Das Subtrahieren von -10
                                     ; = Addition von +10 ist ein Trick
                                     ; = Addition von +10 ist ein Trick,
                                     ; da kein addi Befehl existiert
                                     ; da kein addi-Befehl existiert.
         ldi    temp2, '0'          ; die übrig gebliebenen Einer
         ldi    temp2, '0'          ; die übrig gebliebenen Einer
         add    temp1, temp2        ; noch ausgeben
         add    temp1, temp2        ; noch ausgeben
         rcall  lcd_data
         rcall  lcd_data


         pop    temp2              ; Register wieder herstellen
         pop    temp2              ; Register wiederherstellen
         ret
         ret
</syntaxhighlight>
</syntaxhighlight>


In der Interrupt-Routine werden wieder, genauso wie vorher, die Anzahl der Interrupt-Aufrufe gezählt. Beim 100. Aufruf sind daher 40.000 * 100 = 4.000.000 Takte vergangen und da der Quarz mit 4.000.000 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, das nicht direkt behebbar ist. Am Quarz! Auch wenn auf dem Quarz drauf steht, dass er eine Frequenz von 4MHz hat, so stimmt das nicht exakt. Auch Quarze haben Fertigungstoleranzen und verändern ihre Frequenz mit der Temperatur. Typisch liegt die Fertigungstoleranz bei +/- 100ppm = 0,01% ('''p'''arts '''p'''er '''m'''illion, Millionstel Teile), die Temperaturdrift zwischen -40 Grad und 85 Grad liegt je nach Typ in der selben Größenordnung. Das bedeutet, dass die Uhr pro Monat um bis zu 268 Sekunden (~4 1/2 Minuten) falsch gehen kann. Diese Einflüsse auf die Quarzfrequenz sind aber messbar und per Hardware oder Software behebbar. 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). Ein Profi verwendet einen sehr genauen Frequenzzähler, womit er innerhalb weniger Sekunden die Frequenz sehr genau messen kann. Als Hobbybastler kann man die Uhr eine zeitlang (Tage, Wochen) laufen lassen und die Abweichung feststellen (z.&nbsp;B. exakt 20:00 Uhr zum Start der Tagsschau). 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 in dem Wikiartikel [[AVR - Die genaue Sekunde / RTC]] beschrieben.  
In der Interrupt-Routine wird wieder, genauso wie vorher, die Anzahl der Interrupt-Aufrufe gezählt. Beim 100. Aufruf sind daher 40.000 ·&nbsp;100 = 4.000.000 Takte vergangen und da der Quarz mit 4.000.000 Schwingungen in der Sekunde arbeitet, ist somit 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, das nicht direkt behebbar ist. Am Quarz! Auch wenn auf dem Quarz drauf steht, dass er eine Frequenz von 4&nbsp;MHz hat, so stimmt das nicht exakt. Auch Quarze haben Fertigungstoleranzen und verändern ihre Frequenz mit der Temperatur. Typisch liegt die Fertigungstoleranz bei ±100&nbsp;ppm = 0,01 % („ppm“ = ''<u>p</u>arts <u>p</u>er <u>m</u>illion'', Millionstel Teile), die Temperaturdrift zwischen −40 und 85&nbsp;°C liegt je nach Typ in der selben Größenordnung. Das bedeutet, dass die Uhr pro Monat um bis zu 268 Sekunden (≈&nbsp;4½ Minuten) falsch gehen kann. Diese Einflüsse auf die Quarzfrequenz sind aber messbar und per Hardware oder Software behebbar. 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). Ein Profi verwendet einen sehr genauen Frequenzzähler, womit er innerhalb weniger Sekunden die Frequenz sehr genau messen kann. Als Hobbybastler kann man die Uhr eine zeitlang (Tage, Wochen) laufen lassen und die Abweichung feststellen (z.&nbsp;B. exakt 20:00&nbsp;Uhr zum Start der Tagesschau). 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 in dem Wikiartikel [[AVR - Die genaue Sekunde / RTC]] beschrieben.


----
----

Aktuelle Version vom 5. März 2023, 14:02 Uhr

Eine beliebte Übung für jeden Programmierer ist die Implementierung einer Uhr. Die meisten Uhren bestehen aus einem Taktgeber 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 Taktgebers, der uns einen möglichst konstanten und genauen Takt liefert, übernimmt ein Timer. Der Timer ermöglicht einen einfachen Zugang zum Takt, den der AVR vom Quarz abgreift. Wie schon im Einführungskapitel über den Timer beschrieben, wird dazu ein Timer Overflow Interrupt installiert, und in diesem Interrupt wird die eigentliche Uhr hochgezählt. Die Uhr besteht aus vier Registern. Drei davon repräsentieren die Sekunden, Minuten und Stunden unserer Uhr. Nach jeweils einer Sekunde wird das Sekundenregister um eins erhöht. Sind 60 Sekunden vergangen, wird das Sekundenregister wieder auf null gesetzt und gleichzeitig das Minutenregister um eins erhöht. Dies ist ein Überlauf. Nach 60 Minuten werden die Minuten wieder auf null gesetzt und für diese vergangenen 60 Minuten eine Stunde aufaddiert. Nach 24 Stunden schließlich werden die Stunden wieder auf Null gesetzt, ein ganzer Tag ist vergangen.

Aber wozu das vierte Register?

Der Mikrocontroller wird mit 4 MHz betrieben. Bei einem Teiler von 1.024 zählt der Timer also mit 4.000.000 / 1.024 = 3.906,25 Pulsen pro Sekunde. Der Timer muss einmal bis 256 zählen, bis er einen Überlauf auslöst. Es ereignen sich also 3.906,25 / 256 = 15,2587 Überläufe pro Sekunde. Die Aufgabe des vierten Registers ist es nun, diese 15 Überläufe zu zählen. Bei Auftreten des 15. ist eine Sekunde vergangen. Dies stimmt jedoch nicht exakt, denn die Division weist ja auch Nachkommastellen auf, hat einen Rest, der hier im Moment der Einfachheit halber ignoriert wird. In einem späteren Abschnitt wird darauf noch eingegangen werden.

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

Das erste Programm

.include "m8def.inc"

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

.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, HIGH(RAMEND) ; Stackpointer initialisieren
        out     SPH, temp1
        ldi     temp1, LOW(RAMEND)
        out     SPL, temp1

        ldi temp1, 0xFF             ; Port D = Ausgang
        out DDRD, temp1 

        rcall   lcd_init
        rcall   lcd_clear

        ldi     temp1, (1<<CS02) | (1<<CS00) ; Teiler 1024
        out     TCCR0, temp1

        ldi     temp1, 1<<TOIE0     ; Interrupt bei Timer Overflow
        out     TIMSK, temp1

        clr     Minuten             ; Die Uhr auf 0 setzen
        clr     Sekunden
        clr     Stunden
        clr     SubCount
        clr     Flag                ; Merker löschen

        sei

loop:
        cpi     flag, 0
        breq    loop                ; Flag im Interrupt gesetzt?
        ldi     flag, 0             ; Flag löschen

        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

        rjmp    loop

timer0_overflow:                    ; Timer 0 Overflow Handler

        push    temp1               ; temp1 sichern
        in      temp1, sreg         ; SREG sichern

        inc     SubCount            ; Wenn dies nicht der 15. Interrupt
        cpi     SubCount, 15        ; ist, dann passiert gar nichts.
        brne    end_isr

                                    ; Überlauf
        clr     SubCount            ; SubCount rücksetzen
        inc     Sekunden            ; plus 1 Sekunde
        cpi     Sekunden, 60        ; Sind 60 Sekunden vergangen?
        brne    Ausgabe             ; Wenn nicht, kann die Ausgabe schon
                                    ; gemacht werden.

                                    ; Überlauf
        clr     Sekunden            ; Sekunden wieder auf 0 und dafür
        inc     Minuten             ; plus 1 Minute
        cpi     Minuten, 60         ; Sind 60 Minuten vergangen?
        brne    Ausgabe             ; Wenn nicht, -> Ausgabe

                                    ; Überlauf
        clr     Minuten             ; Minuten zurücksetzen und dafür
        inc     Stunden             ; plus 1 Stunde
        cpi     Stunden, 24         ; Nach 24 Stunden die Stundenanzeige
        brne    Ausgabe             ; wieder zurücksetzen, sonst -> Ausgabe.

                                    ; Überlauf
        clr     Stunden             ; Stunden rücksetzen

Ausgabe:
        ldi     flag, 1             ; Flag setzen, LCD updaten

end_isr:
        out     sreg, temp1         ; SREG wiederherstellen
        pop     temp1               ; temp1 wiederherstellen
        reti                        ; Das war's. Interrupt ist fertig.

; Eine Zahl aus dem Register temp1 ausgeben

lcd_number:
        push    temp2               ; Register temp2 sichern,
                                    ; wird für Zwischenergebnisse gebraucht.
        ldi     temp2, '0'
lcd_number_10:
        subi    temp1, 10           ; Abzählen, wieviele Zehner in
        brcs    lcd_number_1        ; der Zahl enthalten sind
        inc     temp2
        rjmp    lcd_number_10
lcd_number_1:
        push    temp1               ; den Rest sichern (https://www.mikrocontroller.net/topic/172026)
        mov     temp1, temp2
        rcall   lcd_data            ; die Zehnerstelle ausgeben
        pop     temp1               ; den Rest wiederherstellen
        subi    temp1, -10          ; 10 wieder dazuzählen, da die
                                    ; vorhergehende Schleife 10 zuviel
                                    ; abgezogen hat.
                                    ; Das Subtrahieren von -10
                                    ; = Addition von +10 ist ein Trick,
                                    ; da kein addi-Befehl existiert.
        ldi     temp2, '0'          ; die übrig gebliebenen Einer
        add     temp1, temp2        ; noch ausgeben
        rcall   lcd_data

        pop     temp2               ; Register wiederherstellen
        ret

In der ISR wird nur die Zeit in den Registern neu berechnet, die Ausgabe auf das LCD erfolgt in der Hauptschleife. Das ist notwendig, da die LCD-Ausgabe bisweilen sehr lange dauern kann. Wenn sie länger als ≈2/15 Sekunden dauert, werden Timerinterrupts „verschluckt“ und unsere Uhr geht noch mehr falsch. Dadurch, dass aber die Ausgabe in der Hauptschleife durchgeführt wird, welche jederzeit durch einen Timerinterrupt unterbrochen werden kann, werden keine Timerinterrupts verschluckt. Das ist vor allem wichtig, wenn mit höheren Interruptfrequenzen gearbeitet wird, z. B. 1/100 s im Beispiel weiter unten. Auch wenn in diesem einfachen Beispiel die Ausgabe bei weitem nicht 2/15 Sekunden dauert, sollte man sich diesen Programmierstil allgemein angewöhnen. Siehe auch Interrupt.

Eine weitere Besonderheit ist das Register flag (= r19). Dieses Register fungiert als Anzeiger, wie eine Flagge, daher auch der Name. In der ISR wird dieses Register auf 1 gesetzt. Die Hauptschleife, also alles zwischen loop: und rjmp loop, prüft dieses Flag und nur dann, wenn das Flag auf 1 steht, wird die LCD-Ausgabe gemacht und das Flag wieder auf 0 zurückgesetzt. Auf diese Art wird nur dann Rechenzeit für die LCD-Ausgabe verbraucht, wenn dies tatsächlich notwendig ist. Die ISR teilt mit dem Flag der Hauptschleife mit, dass eine bestimmte Aufgabe, nämlich das Update der Anzeige, gemacht werden muss und die Hauptschleife reagiert darauf bei nächster Gelegenheit, indem sie diese Aufgabe ausführt und setzt das Flag zurück. Solche Flags werden daher auch Job-Flags genannt, weil durch ihr Setzen das Abarbeiten eines Jobs (einer Aufgabe) angestoßen wird. Auch hier gilt wieder: Im Grunde würde man in diesem speziellen Beispiel kein Job-Flag benötigen, weil es in der Hauptschleife nur einen einzigen möglichen Job, die Neuausgabe der Uhrzeit, gibt. Sobald aber Programme komplizierter werden und mehrere Jobs möglich sind, sind Job-Flags eine gute Möglichkeit, ein Programm so zu organisieren, dass bestimmte Dinge nur dann getan werden, wenn sie notwendig sind.

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 die Bedienungsanleitung einer käuflichen 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. Als Zwischenlösung kann man im Programm die Uhr beim Start anstelle von 00:00:00 z. B. auch auf 20:00:00 stellen und exakt mit dem Start der Tagesschau starten. Wobei der Start der Tagesschau verzögert bei uns ankommt, je nach Übertragung können das mehrere Sekunden sein.

Ganggenauigkeit

Wird die Uhr mit einer gekauften Uhr verglichen, so stellt man schnell fest, dass sie ganz schön ungenau geht. Sie geht vor! Woran liegt das? Die Berechnung sieht so aus:

  • Frequenz des Quarzes: 4,0 MHz
  • Vorteiler des Timers: 1024
  • Überlauf alle 256 Timertakte

Daraus errechnet sich, dass in einer Sekunde 4.000.000 / 1.024 / 256 = 15,258789 Overflow-Interrupts auftreten. Im Programm wird aber bereits nach 15 Overflows eine Sekunde weitergeschaltet, daher geht die Uhr vor. Rechnen wir etwas:

[math]\displaystyle{ F_r = \left(\frac{15}{15{,}258789} - 1\right) \cdot 100\,\% = -1{,}69\,\% }[/math]

So wie bisher läuft die Uhr also rund 1,7 % zu schnell. In einer Minute ist das immerhin etwas mehr als eine Sekunde. 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 15er-Overflow-Sekunden folgt eine Sekunde, für die 16 Overflows ablaufen müssen. 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

[math]\displaystyle{ F_r = \left(\frac{61}{61{,}035156} - 1\right) \cdot 100\,\% = -0{,}0575\,\% }[/math].

Mit diesem Schema ist der Fehler beträchtlich gesunken. Nur noch 0,06 %. Bei dieser Rate muss die Uhr immerhin etwas länger als ½ Stunde laufen, bis der Fehler auf eine Sekunde angewachsen ist. Das sind aber immer noch 48 Sekunden pro Tag bzw. 1488 Sekunden (= 24,8 Minuten) pro Monat. So schlecht sind nicht mal billige mechanische Uhren!

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?

Im ersten Ansatz wurde ein Vorteiler von 1.024 eingesetzt. Was passiert bei einem anderen Vorteiler? Nehmen wir mal einen Vorteiler von 64. Das heißt, es müssen (4.000.000 / 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

[math]\displaystyle{ F_r = \left(\frac{244}{244{,}140625} - 1\right) \cdot 100\,\% = -0{,}0576\,\% }[/math].

Nicht schlecht. Nur durch Verändern von zwei Zahlenwerten im Programm (Teilerfaktor und Anzahl der Overflow-Interrupts bis zu einer Sekunde) 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 denn das eigentliche Problem, mit dem die Uhr zu kämpfen hat? Es liegt darin, dass 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, heißt das auch, dass im ersten Fall nur in Vielfachen von 1024 · 256 = 262.144 Takten operiert werden kann, während im letzten Fall immerhin schon eine Granulierung von 64 · 256 = 16.384 Takten erreicht wird. Aber offensichtlich ist das nicht genau genug. Bei 4 MHz entsprechen 262.144 Takte bereits einem Zeitraum von 65,5 ms, während 16.384 Takte einem Zeitbedarf von 4,096 ms entsprechen. Beide Zahlen teilen aber 1.000 ms nicht ganzzahlig, Nachkommareste fallen unter den Tisch und daraus summiert sich der Fehler auf. Angestrebt wird ein Timer, der seinen „Overflow“ so erreicht, dass sich ein ganzzahliger Teiler von einer Sekunde einstellt. Dann gibt es keinen Rest. Dazu müsste man dem Timer aber vorschreiben können, bei welchem Zählerstand der „Overflow“ erfolgen soll. Und genau dies ist im CTC-Modus möglich, allerdings nur beim Timer 1. 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 mit höchster Zeitauflösung arbeiten kann. Bei jedem Ticken des Systemtaktes von 4 MHz 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 39.999 vorbelegt. Dadurch vergehen exakt 40.000 Taktzyklen von einem Compare-Interrupt zum nächsten. „Zufällig“ ist dieser Wert so gewählt, dass bei einem Systemtakt von 4 MHz von einem Interrupt zum nächsten genau 1/100 Sekunde vergeht, denn 40.000 / 4.000.000 = 0,01. Bei einem möglichen Umbau der Uhr zu einer Stoppuhr könnte sich die Hundertstelsekunde als nützlich erweisen. Im Interrupt wird das Hilfsregister SubCount bis 100 hochgezählt, und nach 100 Interrupts kommt wieder die Sekundenweiterschaltung wie oben in Gang.

.include "m8def.inc"

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

.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, HIGH(RAMEND) ; Stackpointer initialisieren
        out     SPH, temp1
        ldi     temp1, LOW(RAMEND)
        out     SPL, temp1

        ldi temp1, 0xFF             ; Port D = Ausgang
        out DDRD, 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  ; Interrupt bei Timer Compare Match
        out     TIMSK, temp1

        clr     Minuten             ; Die Uhr auf 0 setzen
        clr     Sekunden
        clr     Stunden
        clr     SubCount
        clr     Flag                ; Flag löschen

        sei
loop:
        cpi     flag, 0
        breq    loop                ; Flag im Interrupt gesetzt?
        ldi     flag, 0             ; Flag löschen

        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

        rjmp    loop

timer1_compare:                     ; Timer 1 Output Compare Handler

        push    temp1               ; temp1 sichern
        in      temp1, sreg         ; SREG sichern

        inc     SubCount            ; Wenn dies nicht der 100. Interrupt
        cpi     SubCount, 100       ; ist, dann passiert gar nichts.
        brne    end_isr

                                    ; Überlauf
        clr     SubCount            ; SubCount rücksetzen
        inc     Sekunden            ; plus 1 Sekunde
        cpi     Sekunden, 60        ; Sind 60 Sekunden vergangen?
        brne    Ausgabe             ; Wenn nicht, kann die Ausgabe schon
                                    ; gemacht werden.

                                    ; Überlauf
        clr     Sekunden            ; Sekunden wieder auf 0 und dafür
        inc     Minuten             ; plus 1 Minute
        cpi     Minuten, 60         ; Sind 60 Minuten vergangen?
        brne    Ausgabe             ; Wenn nicht, -> Ausgabe

                                    ; Überlauf
        clr     Minuten             ; Minuten zurücksetzen und dafür
        inc     Stunden             ; plus 1 Stunde
        cpi     Stunden, 24         ; Nach 24 Stunden die Stundenanzeige
        brne    Ausgabe             ; wieder zurücksetzen, sonst -> Ausgabe.

                                    ; Überlauf
        clr     Stunden             ; Stunden rücksetzen

Ausgabe:
        ldi     flag, 1             ; Flag setzen, LCD updaten

end_isr:
        out     sreg, temp1         ; SREG wiederherstellen
        pop     temp1               ; temp1 wiederherstellen
        reti                        ; Das war's. Interrupt ist fertig.

; Eine Zahl aus dem Register temp1 ausgeben

lcd_number:
        push    temp2               ; Register temp2 sichern,
                                    ; wird für Zwischenergebnisse gebraucht.
        ldi     temp2, '0'
lcd_number_10:
        subi    temp1, 10           ; Abzählen, wieviele Zehner in
        brcs    lcd_number_1        ; der Zahl enthalten sind
        inc     temp2
        rjmp    lcd_number_10
lcd_number_1:
        push    temp1               ; den Rest sichern (http://www.mikrocontroller.net/topic/172026)
        mov     temp1, temp2
        rcall   lcd_data            ; die Zehnerstelle ausgeben
        pop     temp1               ; den Rest wiederherstellen
        subi    temp1, -10          ; 10 wieder dazuzählen, da die
                                    ; vorhergehende Schleife 10 zuviel
                                    ; abgezogen hat.
                                    ; Das Subtrahieren von -10
                                    ; = Addition von +10 ist ein Trick,
                                    ; da kein addi-Befehl existiert.
        ldi     temp2, '0'          ; die übrig gebliebenen Einer
        add     temp1, temp2        ; noch ausgeben
        rcall   lcd_data

        pop     temp2               ; Register wiederherstellen
        ret

In der Interrupt-Routine wird wieder, genauso wie vorher, die Anzahl der Interrupt-Aufrufe gezählt. Beim 100. Aufruf sind daher 40.000 · 100 = 4.000.000 Takte vergangen und da der Quarz mit 4.000.000 Schwingungen in der Sekunde arbeitet, ist somit 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, das nicht direkt behebbar ist. Am Quarz! Auch wenn auf dem Quarz drauf steht, dass er eine Frequenz von 4 MHz hat, so stimmt das nicht exakt. Auch Quarze haben Fertigungstoleranzen und verändern ihre Frequenz mit der Temperatur. Typisch liegt die Fertigungstoleranz bei ±100 ppm = 0,01 % („ppm“ = parts per million, Millionstel Teile), die Temperaturdrift zwischen −40 und 85 °C liegt je nach Typ in der selben Größenordnung. Das bedeutet, dass die Uhr pro Monat um bis zu 268 Sekunden (≈ 4½ Minuten) falsch gehen kann. Diese Einflüsse auf die Quarzfrequenz sind aber messbar und per Hardware oder Software behebbar. 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). Ein Profi verwendet einen sehr genauen Frequenzzähler, womit er innerhalb weniger Sekunden die Frequenz sehr genau messen kann. Als Hobbybastler kann man die Uhr eine zeitlang (Tage, Wochen) laufen lassen und die Abweichung feststellen (z. B. exakt 20:00 Uhr zum Start der Tagesschau). 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 in dem Wikiartikel AVR - Die genaue Sekunde / RTC beschrieben.