AVR-Tutorial: Uhr
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. Das 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 1 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, allerings 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. Das Compare Register OCR1A wird mit dem Wert 39999 vorbelegt. Dadurch vergehen exakt 40000 Taktzyklen von einem Compare Interrupt zum nächsten.
<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.