Interrupts Timer
Ich habe die Tage das Thema "Interrupts allgemein" eingeschoben, damit wir hier wissen, um was es in etwa geht. Wir haben besprochen, was ein Interrupt ist, wie er funktioniert und dass er durch verschiedene Ereignisse ausgelöst werden kann. Ausserdem muss er generell erlaubt sein.
Heute schauen wir uns eine sehr praktische Angelegenheit der Atmels an, welche uns zwei Interrupts beschert (wovon wir aber nur einen nutzen werden), nämlich den/die Timer:
Stellen wir uns mal ein besonderes Register vor, welches, völlig unabhängig vom restlichen Programm, immer um 1 erhöht wird. Egal, was gerade passiert, in bestimmten Zeitabständen wird dieses Register inkrementiert und zwar zunächst ohne weiteren Code... einfach als Zuckerl von Atmel. Erreicht der Zähler seinen Höchststand wird eine bestimmte Aktion ausgeführt. Gerade für Lichteffekte aber auch für viele Anwendungen ein absoluter Traum, denn genau das gibt es und im ATM8 sogar dreimal! Nennen wir dieses Register mal "Timer" und schauen wir uns erstmal das Prinzip genauer an.
Wie schon geschrieben ist so ein Timer nichts anderes als ein Register, welches hardwaremäßig und fortlaufend hochgezählt wird. Ist der Zählerstand am Ende angelangt gibt es einen Überlauf (Overflow), welcher einen Interrupt auslöst. Wie wir ja bereits wissen wird das laufende Programm dann unterbrochen und die Interruptserviceroutine aufgerufen.
Unserem ATM8 hat man 3 Timer eingebaut. Dabei sind Timer0 und Timer2 jeweils 8-Bit-Register, Timer1 besitzt 16 Bit.
Wir nutzen für uns den recht einfachen Timer0 mit seinen 8 Bit und damit steigen wir ein. Das Register wird, wie schon beschrieben, unabhängig vom eigentlichen Programm, mit jedem Systemtakt um 1 erhöht. Bei unseren 8MHz und der Gegebenheit, dass es sich bei Timer0 um 8 Bit handelt wird der entsprechende Interrupt also alle 0,000032 Sekunden ausgelöst (1/8000000*255=0,000031875). Das ist schon ziemlich flott und für die meisten Anwendungen nicht sonderlich hilfreich. Aus diesem Grund gibt es für jeden Timer einen Vorteiler (Prescaler), den es zu definieren gilt.
Stelle ich diesen Prescaler z.B. auf 64, dann wird das Timerregister nicht mit jedem, sondern mit jedem 64sten Takt erhöht, die ISR also alle 1/8000000*64*255=0,00204 Sekunden aufgerufen.
Es gibt für Timer0 folgende Prescaler:
1
8
64
256
1024
Externer Takt (steigende oder fallende Flanke)
Damit wir das Ganze in der Praxis sehen basteln wir uns nochmal ein Blinklicht, welches im Sekundentakt ein und aus geht. Diesmal aber nicht mit doof verschachtelten Zeitschleifen sondern mit einem Timer.
Bevor es losgeht sollten wir uns überlegen, was wir brauchen:
Wir müssen einen Ausgang jede Sekunde umschalten (toggeln). Mit Timer0 und einen Prescaler 1024 bekommen wir alle 0,03264 Sekunden einen Aufruf der Interruptserviceroutine, in der wir den Ausgang toggeln können. Wir müssen dann dafür sorgen, dass der Ausgang nur nach jedem 30sten Unterprogrammaufruf (1/0,03264~30) getoggelt wird.
Dann noch zum Unterschied, wie unsere vorheriges Blinklicht funktioniert hat:
Code: Alles auswählen
Neu:
- Ist Timer 30mal gelaufen?
- Wenn ja, dann LED ein/aus
Dann sind wir also bei der Praxis und lernen erstmal zwei neue Befehle kennen:
SEI und CLI
Ob Interrups generell erlaubt sind oder nicht ist in Bit7 des Statustegisters (SREG) festgelegt, welches sich am Einfachsten durch sei (set interrupt enable) oder cli (clear interrupt) beeinflussen lässt.
Bevor wir jedoch die Interrupts global erlauben sollten wir unseren Timer nach unseren Bedürfnissen einstellen.
Dazu verweise ich mal wieder auf unser
geliebtes Datenblatt und dabei auf Seite 69, wo es erstmal allgemein für Timer0 zur Sache geht. Wirklich interessant wird es aber bei Seite 71 (unten), denn damit unser Timer das macht was wir wollen müssen wir ihn mit Hilfe von Registern einstellen.
Den Prescaler stellen wir mit Hilfe der ersten drei Bits des Registers TCCR0 ein, die anderen Bits bleiben 0.
000 = kein Vorteiler (Timer ist angehalten)
001 = 1
010 = 8
011 = 64
100 = 256
101 = 1024
110 = ext. Takt mit fallender Flanke
111 = ext. Takt mit steigender Flanke
Wir müssen also dafür sorgen, dass der Wert 0x00000101 (=Vorteiler 1024) in das Register TCCR0 geschrieben wird.
Auf Seite 72 wird dann noch das Timer-Interrupt-Mask-Register TIMSK angesprochen. Auch wenn der Name noch so kompliziert ist, uns interessiert davon für uns nur das Bit 0, mit dem eingestellt wird, ob beim Auftreten des Overflows an Timer0 der entsprechende Interrupt ausgelöst wird.
Das Timer/Counter Register TCNT0 ist das eigentliche Timerregister, welches wir, zusammen mit TIFR ausser Acht lassen.
Kommen wir also zur Praxis und damit zu einem ersten Programm mit Timer:
Code: Alles auswählen
; Blinklicht mit Timer0
.include "m8def.inc" ; Damit weis der Compiler, auf welchen Proz er compilieren muss
.def Time1 = r16
.org 0x0000
rjmp Init ; Springe nach einem Reset zum Label "Init"
.org 0x0009
rjmp Timer_ISR ; Interruptvektor für Overflow Timer0
Init: ; Hier beginnt die Initialisierung
; Setzen des Stackpointers
ldi r16, LOW(RAMEND) ; unteres Byte der hächstmöglichen Adresse holen
out SPL, r16 ; Unteres Byte des SP beschreiben
ldi r16, HIGH(RAMEND) ; oberes Byte der höchstmnöglichen Adresse holen
out SPH, r16 ; oberes Byte des SP beschreiben
; Initialisieren der Eingänge
ldi r16, 0x00 ; alle Bits in r16 auf 0 setzen
out ddrd, r16 ; Alle Pins von Port C sind Eingang
ldi r16, 0xFF ; Bit 2 und Bit 3 setzen
out portd, r16 ; PC2 und PC3 bekommen PullUp-Widerstände
; Initialisieren der Ausgänge
ldi r16, 0xFF ; alle Bits in r16 auf 1 setzen
out ddrb, r16 ; Alle Pins von Port B sind Ausgang
ldi r16, 0x00 ; Alle Bits in r16 löschen
out portb, r16 ; Alle LEDs aus
; Initialisieren von Timer0
ldi r16, 0b00000101 ; Prescaler 1024
out tccr0, r16 ; Prescaler in Timerregister schreiben
ldi r16, 0b00000001 ; TimerOverflow 0 einschalten
out TIMSK, r16
sei ; Interrupts erlauben
Hauptprogramm:
rjmp Hauptprogramm ; Das Hauptprogramm ist nur eine Endlosschleife
Timer_ISR: ; Interruptserviceroutine für Timer0
sbis PortB, 0 ; Ist LED1 ein?
rjmp LEDein ; Wenn nein, dann zum Einschalten springen
cbi PortB, 0 ; LED1 ausschalten
rjmp ISR_Ende ; Springe zum Ende
LEDein:
sbi PortB, 0 ; LED1 einschalten
ISR_Ende:
reti ; Ende ISR
Wichtig ist der Aufbau der Interruptvektortabelle:
Code: Alles auswählen
.org 0x0009
rjmp Timer_ISR ; Interruptvektor für Overflow Timer0
Neu dazugekommen sind hier folgende Zeilen bei der Initialisierung:
Code: Alles auswählen
; Initialisieren von Timer0
ldi r16, 0b00000101 ; Prescaler 1024
out tccr0, r16 ; Prescaler in Timerregister schreiben
ldi r16, 0b00000001 ; TimerOverflow 0 einschalten
out TIMSK, r16
Wie Ihr seht macht es keinen Unterschied, ob man die Datenrichtungsregister eines IO-Ports oder ein Steuerregister eines Timers initialisiert. Es gibt noch viel mehr Steuerregister, das Prinzip und die Art und Weise des Beschreibens ist immer identisch.
Diese Zeile erklärt sich selbst
Code: Alles auswählen
Hauptprogramm:
rjmp Hauptprogramm ; Das Hauptprogramm ist nur eine Endlosschleife
Man beachte die Größe des Hauptprogramms
Code: Alles auswählen
Timer_ISR: ; Interruptserviceroutine für Timer0
sbis PortB, 0 ; Ist LED1 ein?
rjmp LEDein ; Wenn nein, dann zum Einschalten springen
cbi PortB, 0 ; LED1 ausschalten
rjmp ISR_Ende ; Springe zum Ende
LEDein:
sbi PortB, 0 ; LED1 einschalten
Auch die Interruptserviceroutine ist nicht wirklich gross und im Gegensatz zu dreifach verschachtelten Schleifen richtig übersichtlich
Ganz wichtig: Interruptroutinen werden mit ret
i abgeschlossen!
Nun kopiert Euch dieses Programm in ein eigenes Projekt und probiert das mal aus. Wer einen Dauerblitzer haben will ist damit bereits am Ziel angelangt, wir wollen aber ~ 1 Sekunde haben und müssen nach vorheriger Berechnung dafür sorgen, dass der Ausgang nur alle 30 Aufrufe getoggelt wird.
Dazu sind in unserer ISR nur wenige Zeilen nötig:
Code: Alles auswählen
inc Time1 ; Time 1 hochzählen
cpi Time1, 0x1E ; Wurde die ISR 30mal aufgerufen?
brne ISR_Ende ; Wenn nein, dann springe zum Ende
ldi Time1, 0x00 ; Time1 zurücksetzen
Es wird eine Variable hochgezählt und solange die 30 (0x1E) nicht erreicht ist die ISR sofort wieder beendet. Wurde die 30 erreicht wird die Variable zurückgesetzt und danach der Ausgang getoggelt.
Das fertige Programm (etwa 1 Sekunde) schaut dann so aus:
Code: Alles auswählen
; Blinklicht mit Timer0
.include "m8def.inc" ; Damit weis der Compiler, auf welchen Proz er compilieren muss
.def Time1 = r16
.org 0x0000
rjmp Init ; Springe nach einem Reset zum Label "Init"
.org 0x0009
rjmp Timer_ISR ; Interruptvektor für Overflow Timer0
Init: ; Hier beginnt die Initialisierung
; Setzen des Stackpointers
ldi r16, LOW(RAMEND) ; unteres Byte der hächstmöglichen Adresse holen
out SPL, r16 ; Unteres Byte des SP beschreiben
ldi r16, HIGH(RAMEND) ; oberes Byte der höchstmnöglichen Adresse holen
out SPH, r16 ; oberes Byte des SP beschreiben
; Initialisieren der Eingänge
ldi r16, 0x00 ; alle Bits in r16 auf 0 setzen
out ddrd, r16 ; Alle Pins von Port C sind Eingang
ldi r16, 0xFF ; Bit 2 und Bit 3 setzen
out portd, r16 ; PC2 und PC3 bekommen PullUp-Widerstände
; Initialisieren der Ausgänge
ldi r16, 0xFF ; alle Bits in r16 auf 1 setzen
out ddrb, r16 ; Alle Pins von Port B sind Ausgang
ldi r16, 0x00 ; Alle Bits in r16 löschen
out portb, r16 ; Alle LEDs aus
; Initialisieren von Timer0
ldi r16, 0b00000101 ; Prescaler 1024
out tccr0, r16 ; Prescaler in Timerregister schreiben
ldi r16, 0b00000001 ; TimerOverflow 0 einschalten
out TIMSK, r16
sei ; Interrupts erlauben
Hauptprogramm:
rjmp Hauptprogramm ; Das Hauptprogramm ist nur eine Endlosschleife
Timer_ISR:
inc Time1 ; Time 1 hochzählen
cpi Time1, 0x1E ; Wurde die ISR 30mal aufgerufen?
brne ISR_Ende ; Wenn nein, dann springe zum Ende
ldi Time1, 0x00 ; Time1 zurücksetzen
sbis PortB, 0 ; Ist LED1 ein?
rjmp LEDein ; Wenn nein, dann zum Einschalten springen
cbi PortB, 0 ; LED1 ausschalten
rjmp ISR_Ende ; Springe zum Ende
LEDein:
sbi PortB, 0 ; LED1 einschalten
ISR_Ende:
reti ; Ende ISR
Bitte experimentiert ein wenig mit dem Timer, mit dem Prescaler und schaut mal, ob Euch noch was einfällt, was man damit machen kann.
Ihr könnt die ISR auch mal mit ret anstatt mit reti abschliessen. Schaut mal was passiert und überlegt Euch warum
Ich werde jetzt wieder ein Wochenende auslassen, damit Ihr das doch recht komplexe Thema ein wenig verdauen könnt. Natürlich stehe ich für Fragen Rede und Antwort und wünsche wieder viel Spass.