Pfad:
Home =>
AVR-Überblick =>
Programmiertechniken => Timer
(This page in English:
)
Programmiertechnik für Anfänger in AVR Assemblersprache
Aufbau und Programmierung von AVR-Timern in Assembler
Die am häufigsten (nach den Ports) genutzte interne AVR-Hardware sind
Timer/Counter. Her kannst Du lernen, wie man
- diese startet (und anhält) und was Normal-Modus ist,
- Pins ein- und ausschaltet und was Vergleicher tun,
- Timer an verschiedene Taktfrequenzen anpasst,
- Timer im CTC-Modus betreibt,
- Timer im PWM-Modus betreibt,
- 16-Bit-Timer verwendet,
- Timer mit Interrupts betreibt, und
- wie man Counter zählen lässt.
Alle Berechnungsblätter in der LibreOffice-Calc-Datei
timer.ods gibt es hier, alle Zeichnungen als
LibreOffice-Draw-Datei gibt es hier.
Der folgende Quellcode startet den 8-Bit Timer 0:
ldi R16,1<<CS00 ; Setze Bit CS00 auf 1 (eine 1 null mal nach links geschoben)
out TCCR0B,R16 ; Schreiben in TC0-Kontrollregister TCCR0B
Was damit ausgelöst wird: der Vorteiler-Multiplexer vom Timer wird
auf 0b001 gesetzt, das Prozessortakt-Signal (standardmäßig
1 MHz) kommt an den Ausgang des Multiplexers und taktet den Timer,
der Timer zählt bei jedem Taktimpuls in seinem TCNT0-Portregister
Eins aufwärts.
Diese zwei Zeilen Code sind es schon: von nun an läuft der Timer.
Wenn Du ihn wieder stoppen willst: lösche das gesetzte CS00-Bit
(und alle anderen gesetzten CS-Bits), was den Multiplexer-Ausgang auf
Stumm schaltet und den Zähler sofort anhält.
Im Simulator avr_sim
sieht das dann so aus. Links ist der Timer am Laufen, TCNT0 erhöht
sich bei jedem Takt. Rechts hat der Zähler seinen höchsten
Wert erreicht: alle Bits in TCNT0 sind Eins, mehr geht nicht mehr.
Was dann? Er beginnt einfach wieder bei Null. Und so weiter, und so weiter
...
Um seinen Zählerstand zu lesen, braucht es folgende Instruktion:
in R16,TCCNT0 ; Lese 8-Bit-Zaehler TC0
Aber Obacht! Es macht keinerlei Sinn einen Timer/Counter lesen zu wollen.
Und zwar fast nie. Vergesse also das Lesen, bis Du mehr über Timer,
ihre Eigenschaften und ihre Handhabung gelernt hast.
Wie schnell zählt der Zähler und nach welcher Zeit läuft
er über? Das hängt nicht zuletzt von seiner Taktfrequenz ab:
Tabelle 1.1, auch als Rechenblatt
"normal" in der LibreOffice-Calc-Datei
timer.ods
Wie die Tabelle für ein MHz Takt ausweist, erfolgt der Überlauf
mit einer Frequenz von 3.906,25 Hz oder nach jeweils
0,256 Milli-Sekunden.
Aber gemach: wie das Bild oben schon gezeigt hat, hat der Timer nicht
nur ein Bit, sondern drei, und der Vorteiler kann auch das durch 8,
64, 256 oder 1,024 geteilte Taktsignal auf den Multiplexer-Ausgang
bringen. 1,024 kriegt man zum Beispiel so:
ldi R16,(1<<CS02)|(1<<CS00) ; Setze Bits CS02 und CS00 auf 1, | ist ein binäres ODER
out TCCR0B,R16 ; Schreibe in das Timer-Kontrollregister TCCR0B
Nun läuft der Timer mit 3,81 Hz oder alle 262,1 ms
über. Das ist langsam genug, dass wir das mit einer
angeschlossenen LED sehen könnten. Aber das ist der Stoff
für das nächste Kapitel.
Jeder Timer hat zwei assoziierte Pins, die der Timer einschalten,
ausschalten oder taumeln lassen kann. Diese Pins heißen OCnA
und OCnB für den Timer n. In einem ATtiny24 in PDIP sind diese
für TC0 an den Pins PB2 (OC0A) und PA7 (OC0B) lokalisiert.
Die Kontrollbits, die den Schaltmodus bestimmen, sind im Timer/Counter
Kontrollregister TCCRnA beheimatet. Für TC0 sind das die Bits
COM0A1 und COM0A0 für OC0A sowie COM0B1 und COM0B0 für OCR0B.
Sind beide Bits Null, dann sind die angesclossenen Pins inaktiv.
Wenn nur COMnx0 gesetzt ist, dann torkelt der Portpin von Null auf
Eins, dann von Eins auf Null usw. Wenn nur COMnx1 gesetzt ist, dann
wird das Portbit auf Null gesetzt, sind beide gesetzt, dann wird
das Portpin auf Eins gesetzt.
Aber wann passiert das denn nun alles? Das ist die Aufgabe der zwei
Vergleichsregister (Compare A und Compare B). Wann immer der Timer
die darin gespeicherten Werte erreicht, dann führt der Timer
beim nächsten eintreffenden Zählerereignis den aktivierten
Schaltvorgang aus.
Wenn wir den Compare-A-Wert auf Null setzen und den Compare-Wert B
auf 127 sowie die Schaltungsart auf Torkeln, dann kriegen wir an OC0A
und OC0B einen schönen Flip-Flop hin:
sbi DDRB,DDB2 ; Setze PB2 (OC0A) als Ausgang
sbi DDRA,DDA7 ; Setze PA7 (OC0B) als Ausgang
ldi R16,0 ; Compare A auf Null
out OCR0A,R16 ; In das Compare A Portregister
ldi R16,127 ; Compare B auf 127
out OCR0B,R16 ; In das Compare B Portregister
ldi R16,(1<<COM0A0)|(1<<COM0B0) ; Toggle A und B
out TCCR0A,R16 ; In das Timer/Counter-Kontrollregister A
ldi R16,(1<<CS02)|(1<<CS00) ; Vorteiler = 1,024
out TCCR0B,R16 ; In das Timer/Counter-Kontrollregister B
Loop: ; Die endlose Schleife
rjmp Loop
Die ersten beiden Instruktionen setzen die Datenrichtungsbits der
beiden Portpins als Ausgänge.
Die folgenden vier Instruktionen bringen die beiden Compare-Register
auf ihre zwei unterschiedlichen Werte, so dass sie bei verschiedenen
Zählerständen torkeln.
Die dann folgenden zwei Instruktionen versetzen beide OC-Pins in den
Torkel-Modus, wenn der Vergleichswert erreicht wird. Bitte beachten,
dass diese Bits im PWM-Modus des Timers eine andere Funktion hätten
(siehe weiter unten).
Die beiden letzten Instruktionen starten den Timer mit einem Vorteiler
von 1.024.
Das ist das was jetzt abgeht:
- erreicht der Timer die 1, dann torkelt OC0A (blau) und macht den
Ausgang Eins oder Null,
- erreicht der Timer 128, dann torkelt OC0B (rot) und macht den
Ausgang High,
- wenn der Timer wieder 1 erreicht (nach dem Überlauf von
255 auf 0), dann torkelt wieder OC0A,
- wenn der Timer wieder 128 erreicht, torkelt wieder OC0B,
- und so weiter und so weiter und so weiter ...
Nun nehmen wir an, wir hätten eine rot-grüne Duo-LED an die
beiden Pins angeschlossen, wie es hier zu sehen ist.
In Phase 1, mit OC0A auf High und OC0B auf Low, ist die rote LED an.
In Phase 2, wenn beide Ausgänge auf High sind, ist die LED aus.
In Phase 3, wenn OC0A Low und OC0B High ist, ist dann die grüne
LED an. Gefolgt von einer Pause in Phase 4, bei der beide Ausgänge
Low und die LED aus ist.
Bitte beachten, dass der Timer nun automatisch das alles immer und
wieder wiederholt. Es ist kein weiterer Programmeingriff nötig,
um das Ganze am Leben, Schalten und Leuchten zu halten. Der Timer macht
das nun ganz alleine, der Controller könnte nun schlafen gelegt
werden.
Ein netter rot/grüner Blinker mit Pausen dazwischen.
Einige Fragen:
Wie würde es denn blinken, wenn wir OCR0B auf 31 oder auf 1 setzen
würden? Du kannst das Rechenblatt "rotgrün" in
der LibreOffice-Calc-Datei timer.ods verwenden,
um das herauszufinden: nur den Wert von Compare B verändern.
Und: was wäre denn einzustellen, wenn ein pausenloses Blinken
veranstaltet werden soll?
Wenn Du mal einen Blick zurück auf
Tabelle 1.1 wirfst, siehst Du, dass die
Frequenzen bei verschiedenen Vorteilern recht krumme Werte haben:
sie haben keine Ganzzahlenwerte, sondern alle was hinter dem Komma.
Das kommt natürlich daher, weil das Teilen durch Zweierpotenzen
kaum einen Rest von 0,000 lässt, wenn Deine Ausgangsfrequenz
1 MHz ist. Sogar wenn wir den Takt auf 8 MHz erhöhen,
indem wir die CLKDIV8-Fuse des ATtiny24 löschen, hilft
das nicht viel weiter: nur der Vorteiler von 1 führt zu einer
Ganzzahl, alle anderen haben wieder irgendwas hinter dem Komma.
Das kann man nur ändern, wenn die Taktfrequenz selber eine
Zweier-Potenz ist, z. B. 2,048 MHz. Wenn Du das in das
Rechenblatt "normal" in der LibreOffice-Calc-Datei
timer.ods einträgst, dann siehst
Du, dass Vorteiler bis hinauf zu 64 jetzt zu einer Ganzzahl
führen (125 Hz).
So kann man den ATtiny24 mit einem Quarz takten, der eine Zweierpotenz
an Frequenz hat: die beiden Pins PB0 und PB1 wechseln ihre Funktion,
wenn man die entsprechenden Fuses setzt. Bitte nicht vergessen, die
CLKDIV8-Fuse ebenfalls zu löschen.
Dieser Auszug aus dem Rechenblatt "quarze" der LibreOffice-
Calc-Datei timer.ods zeigt alle kommerziell
verfügbaren Quarze, ihre Frequenzen, ihr Gehäuse, und die
Frequenzen an, mit denen Timer bei den Vorteilerwerten von 8, 64, 256
und 1.024 getaktet werden.
Eine Menge an Quarzen liefert schöne Frequenzen, mit vielen
Nullen hinter dem Komma (grün hinterlegt). Aber welche davon
liefern sogar nach dem weiteren Teilen durch 256 im Timer auch noch
Ganzzahlen?
Hier sind die Frequenzen nach dem Teilen durch 256 (8-Bit) und 1.024
(10-Bit) im Timer. Nur die Quarze mit 2,097152, 3,93216, 4,194304 und
6,5536 MHz liefern ganzzahlige Frequenzen bei allen möglichen
Vorteilern.
Timer können aber nicht nur immer von Null bis 255 zählen, sie
können auch bis weniger zählen und vorzeitig wieder von vorne
beginnen. Um das zu können, muss man die Obergrenze des Zählens,
den TOP-Wert, herabsetzen. Der ist standardmäßig bei 255, kann
aber durch Schreiben des Compare-A-Wertes und durch Setzen des CTC-Modus
(CTC = Clear-Timer-on-Compare = Timer-Rücksetzen-bei-Compare) dazu
gebracht werden, schon bei weniger Schluss zu machen.
Um den 8-Bit-Timer TC0 in diesen Modus zu bringen, müssen wir die
Wave-Generation-Mode-Bits (WGM, = Wellen-Erzeugungs.Modus-Bits) auf 0b010
und den Compare-A-Wert im Portregister OCR0A auf den gewünschten Wert
der Obergrenze minus 1 setzen.
Aus historischen Gründen sind die WGM-Bits je nach AVR-Typ über
die Portregister TCCR0A und TCCR0B verteilt. Im ATtiny24 sind diese im
TCCR0A (WGM01 und WGM00), wo auch die COM-Bits liegen, und in TCCR0B (WGM02),
wo auch die CS-Bits liegen, untergebracht.
Wir benutzen das Rechenblatt "ctc" in der LibreOffice-Calc-Datei
timer.ods, um unsere Frequenzen im CTC-Modus zu
berechnen. Bei 1 MHz und einem Vorteilerwert von 1.024 setzen wir einen
Teilerwert von 122. Dieser Wert, minus 1, geht in das Compare-A-Register.
Die resultierende Frequenz ist dann 8,004 Hz. Da das Torkeln immer
zwei Compare-Matches braucht, ist die Vollschwingung bei 4,002 Hz.
Der gesamte Assembler-Quellcode:
sbi DDRB,DDB2 ; Setze PB2 (OC0A) als Ausgabe-Pin
sbi DDRA,DDA7 ; Setze PA7 (OC0B) als Ausgabe-Pin
sbi PORTA,PORTA7 ; Setze PA7 High zu Beginn
ldi R16,121 ; Compare A auf 121
out OCR0A,R16 ; In das Compare A Portregister
out OCR0B,R16 ; Und in das Compare B Portregister
ldi R16,(1<<COM0A0)|(1<<COM0B0)|(1<<WGM01) ; Toggle A und B im CTC-Modus
out TCCR0A,R16 ; In das Timer/Counter Kontrollregister A
ldi R16,(1<<CS02)|(1<<CS00) ; Vorteiler = 1,024
out TCCR0B,R16 ; In das Timer/Counter Kontrollregister B
Loop: ; Endlosschleife
rjmp Loop
Der Code läuft wie geschmiert: die beiden Pins produzieren
eine Schwingung von 4,002 Hz und einer Pulsweite von 50%.
Um mit Teilerwerten, Compare A und Quarzfrequenzen etwas herum
zu spielen, kann man im Rechenblatt "quarze" in der
LibreOffice-Calc-Datei timer.ods
in den Spalten N bis R etwas herummachen. Hier
kann man sich was Passendes aussuchen, um seinen Quarz auf
genau ein, zehn oder 100 Hertz zu ziehen.
Den großartigen Effekt des CTC-Modus kann man am Besten
bei den 2,048- und 4,096 MHz-Quarzen sehen: mit
Teilerwerten von 250 (Compare A = 249) statt 256 liefern diese
famose Ganzzahlen-Frequenzen. Der geringe Unterschied von nur 6
macht einen immensen Effekt beim Teilen.
Und nochmal: keine weiteren Controller-Aktivitäten sind
nötig, alles ist automatisch und fertig eingestellt.
PWM heißt Puls-Weiten-Modus: ein Signalausgang geht auf
Eins, nach einer vordefinierten Zeit auf Null (An-Zeit) und
nach einer weiteren vordefinierten Zeit wieder auf High (Aus-Zeit).
Auch das invertierte Signal ist möglich: ist der Timer auf
Null, wird der Ausgang auf Null gesetzt. Wird der Vergleichswert
in Compare A erreicht, dann setzt der nächste Timer-Impuls
den Ausgang auf High. Das nennt man dann ein invertiertes PWM-Signal.
Für was kann man denn so was brauchen? Nun: wenn wir eine
LED mit einem Vorwiderstand an einen solchen Ausgang
anschließen, geht sie an und aus. Wenn die PWM-Frequenz
hoch genug ist, sehen wir sie aber nicht an- und ausgehen. Wir
sehen dann nur den Mittelwert, mit dem die LED an und aus ist.
Ist die AN-Zeit kurz, ist die LED weniger hell. Und zwar vollkommen
linear und nicht mit Eins durch, wie bei einem Poti als
Vorwiderstand.
Selbiges passiert, wenn wir an den Ausgang einen Treibertransistor
anschließen und damit einen Motor ein- und ausschalten. Der
träge Motor sieht nur den Mittelwert an Leistung: je weniger
AN-Zeit, desto weniger Drehleistung.
5.1 Zeiten im PWM-Modus
Das hier sind die Zeitbeziehungen eines PWM-Signals.
Die PWM-Zyklus-Zeit ist tPWM = 256 * NVorteiler /
Takt für eine 8-Bit-PWM. Mit einem Vorteilerwert von 1
und einer Taktfrequenz von 1 MHz sind das 0,256 ms
(entsprechend einer PWM-Frequenz von 3,9 kHz. So schnell
guckt unser Auge nicht (ca. 30 Hz), während unser
Ohr durchaus 3,9 kHz als ein Brummen des Motors hören
kann.
Wenn der Vergleichswert im Compare A 127 wäre, hätte
die Pulsweite genau 50% (bei (127 + 1) / 256 = 0,50). Die
kürzeste AN-Zeit wäre bei einem Compare-Wert von 0
(0 + 1) / 256 = 0,39%.
Bitte beachten, dass die TCNT-Werte sich nicht linear bewegen,
wie das das Diagramm suggeriert. Es sind 256 einzelne Stufen,
nur war ich etwas faul mit dem Zeichnen.
5.2 Die COM-Bits
Es gibt aber nicht nur einen PWM-Modus des Timers. Neben dem
schon beschriebenen Modus gibt es noch weitere. Bei allen Modi
haben die COM-Bits, die die beiden Ausgänge ansteuern,
unterschiedliche Bedeutung und Funktion.
5.2.1 Die COM-Bits im Fast-PWM-mode
Der Fast-PWM-mode wurde bereits oben beschrieben. Der Timer
zählt aufwärts und schaltet beim Vergleichswert plus
Eins. Es gibt aber zwei Fast-PWM-Modi:
- TOP = 255: der Timer hat 256 Stufen, beide Vergleichswerte
können zum Schalten von OCnA und OCnB benutzt werden,
- TOP = OCRnA: der Timer hat (OCRnA + 1) Stufen und setzt dann
wieder auf Null, nur der Vergleichswert B kann benutzt werden,
um OCnB zu schalten, B muss dabei aber kleiner oder gleich
A bleiben.
Im zweiten Fall ist die PWM-Frequenz höher, denn der
Zähler zählt nur noch bis OCRnAQ+1. Setzt man OCRnA auf
31, dann ist die PWM-Frequenz 8 mal höher, also bei 1 MHz
31,25 kHz. Jetzt hört man auch kein Motorbrummen mehr,
nur die Fledermäuse werden nun von den falschen Echo-Signalen
irritiert. Natürlich machen nun nur noch Vergleichswerte B
zwischen 0 und 31 Sinn.
Die COM-Bits in den beiden Fast-PWM-Modi haben die folgende Bedeutung:
COMnA/B1 | COMnA/B0 | Bottom | Compare Match A/B | Anmerkung |
0 | 0 | OCnA/B unbeeinflusst | - |
1 | 0 | Setze OCnA/B | Lösche OCnA/B | Nicht-invertierender PWM-Modus |
1 | 1 | Lösche OCnA/B | Setze OCnA/B | Invertierender PWM-Modus |
5.2.2 Die COM-Bits im Phasen-korrekten PWM-Modus
Im Phasen-korrekten PWM-Modus
- zählt der Timer zuerst aufwärts bis zum TOP-Wert,
dann abwärts bis auf Null ("bottom"),
- wird beim Aufwärtszählen der Vergleichswert A
oder B erreicht (plus Eins), dann wird der Ausgang High
gesetzt (Normalmodus) oder Low gelöscht (invertierter
Modus),
- erfolgt die Gleichheit beim Abwärtszählen, dann
wird der Ausgang gelöscht (Normalmodus) bzw. gesetzt
(invertierter Modus),
- Änderungen der Vergleichswerte im laufenden Betrieb
werden erst aktiv, wenn der Zähler Null erreicht.
Dieser Modus macht Sinn, wenn Vergleichswerte häufig wechseln.
Dabei wird durch die Zwischenspeicherung der Vergleichswerte und
ihr Aktivieren beim Zähler-Null erreicht, dass alle Impulse
eine korrekte Dauer aufweisen und dass kein Compare-Match verpasst
wird (was der Fall bei sofortiger Anwendung der Vergleichswerte
sein könnte, wenn der neue Vergleichswert unter dem aktuellen
Zählerstand läge: der Vergleichswert wird dann verpasst).
Das Auf- und Abwärtszählen reduziert die PWM-Frequenz
um den Faktor zwei, diese Modi sollten daher "Slow PWM mode"
genannt werden.
Die COM-Bits im Phasen-korrekten PWM-Modus haben die folgende
Bedeutung:
COMnA/B1 | COMnA/B0 | Aufwärts | Abwärts | Anmerkung |
0 | 0 | OCnA/B unbeeinflusst | - |
1 | 0 | Lösche OCnA/B | Setze OCnA/B | Invertierender Phasen-korrekter PWM-Modus |
1 | 1 | Setze OCnA/B | Lösche OCnA/B | Nicht-invertierender Phasen-korrekter PWM-Modus |
5.2.3 Die COM-Bits im Phasen- und Frequenz-korrekten PWM-Modus
Beim frequenz-korrekten PWM-Modus wird der TOP-Wert des Timers
erst dann verändert, wenn der Zähler den Bottom (Null)
erreicht. Den TOP-Wert kann man folgendermaßen verstellen:
- bei TOP = OCRnA-Modi durch Schreiben in das OCRnA-Register,
oder
- bei TOP = ICRn-Modi (16-Bit-Zähler) durch Schreiben in
das ICRn-Registerpaar, oder
- bei 8-/9-10-Bit-PWM-Modi: durch Schreiben der WGM-Mode-Bits
im TCCRnA- und TCCRnB-Register.
Bei allen diesen Änderungen des TOP-Wertes werden diese erst
beim Bottom ausgeführt.
Dieser Modus stellt sicher, dass nur komplette PWM-Zyklen auftreten
können und keine fehlerhaften Pulsdauern passieren (wenn
z. B. TOP- und Vergleichswert gar nicht zueinander passen).
Das ist ebenfalls ein "slow motion"-Modus.
16-Bit-Timer/Counter sind ihren 8-Bit-Pendants sehr ähnlich.
Ihr Vorteiler sieht genauso aus und funktioniert auch identisch,
nur hat der Zähler jetzt 16 Bits und kann daher von 0 bis
65.535 (= 216 - 1) zählen.
Weil AVRs 8-Bit-Mikrocontroller sind und keine Möglichkeit
besteht, 16 Bits gleichzeitig in Portregister-Paare zu schreiben
oder daraus zu lesen, muss der Zugriff darauf in zwei Portionen
erfolgen. Alle 16-Bit-Register (TCNT, OCRnA/B, etc.) haben daher
zwei Portregister, die intern als Paar geschaltet sind. Den Namen
wird ein "L" für das LSB und ein "H"
für das MSB angehängt. Daher hat TCNT1 ein TCNT1L- und
ein TCNT1H-Portregister.
Zwei spezielle Handhabungsvorgänge sind bei 16-Bit-Timern
spezifisch:
- der 8-Bit-Zugriff erfordert eine besondere Handhabung zur
Vermeidung von Zugriffsfehlern, und
- da 16-Bit-PWMs keinen Sinn machen, haben sie ein Extra-
Registerpaar, das als zusätzliches Vergleichsregister
sowie als CTC-Register dienen kann.
Siehe die folgenden Kapitel zu diesen beiden Themen.
6.1 16-Bit-Portregisterpaar-Zugriffe
8-Bit-Controller können nicht in einem einzigen Schritt
auf 16-Bit-Portregisterpaare zugreifen, sie brauchen dafür
zwei Instruktionen: das MSB und das LSB separat. Da der Timer
zwischen zwei Instruktionen aber weiterdreht, kann es in
seltenen Fällen dazu kommen, dass sich LSB und MSB beide
gleichzeitig ändern, und das auch noch nach dem ersten
und vor dem zweiten Zugriff.
Beim Lesen führt das dazu, dass LSB und MSB gar nicht
zusammen passen. Beim Schreiben, z. B. eines
Vergleichswerts, würde schon nach dem ersten Schreiben
ein Compare Match auftreten können (nicht im PWM-Modus,
aber sonst).
Damit das nicht passiert, wird beim Lesen eines LSBs das
zugehörige MSB in einen Zwischenspeicher bugsiert. Erfolgt
danach das Lesen des MSBs, dann greift der Controller gar nicht
auf den echten MSB des Registerpaars zu, sondern auf diesen
Zwischenspeicher.
Umgekehrt beim Schreiben: zuerst wird das MSB geschrieben, aber
erst mal in einen Zwischenspeicher abgelegt. Wird dann das LSB
geschrieben, dann werden beide Werte gleichzeitig in das
Registerpaar geschrieben.
Wenn man diese Reihenfolgen nicht einhält, kriegt man
lustige Effekte: liest man nur das MSB eines Registerpaars,
kriegt man immerzu den Wert des MSB beim letzten LSB-Zugriff.
Schreibt man nur das MSB des Registerpaars, dann kommt beim
Registerpaar gar nix an.
Bitte beachten, dass in PWM-Timer-Modi geschriebene
Vergleichswerte gar nicht direkt dort ankommen, sondern erst
beim ersten Bottom-Ereignis (TCNTn=0). Daher kann es beim
Simulieren zu erstaunlichen Fehlansichten kommen. Nicht
verzagen, der Vergleichswert kommt schon dort an, wo er hin
soll, nur halt erst später.
6.2 Das Input-Capture-Register in 16-Bit TCs
In Timern, die anstelle von einem, zwei oder drei ganze vier
Mode-Kontroll-Bits WGM haben (normalerweise haben
16-Bit-Zähler so viele), gibt es ein zusätzliche
Timer-Modi und ein zusätzliches Vergleichsregister:
- die WGM-Modus-Bits WGM können auf eine PWM mit
8-, 9- oder 10-Bit eingestellt werden, was TOP-Werte von
255, 511 oder 1.023 zur Folge hat,
- sie haben ein Input Capture Register (ICR), welches
sowohl als drittes Vergleichsregister (aber ohne OC-Pins)
als auch als TOP-Quelle verwendet werden kann, als solches
kann damit die PWM-Frequenz sehr feinfühlig zwischen
0 und 65.535 eingestellt werden.
Da mit ICRn die Einstellung des TOP-Wertes mit OCRnA nicht
mehr nötig ist, kann OC1A als PWM-Ausgang dienen.#
Damit ist man dann flexibel genug, um die Auflösung Deiner
PWM auf genau Deine Bedürfnisse einzustellen.
6.3 Eine Beispielanwendung
Das hier ist ein Beispiel mit zwei aktiven Timern als Paar:
TC0 als 8-Bit-Timer und TC1 als 16-Bit-Timer. Beide blinken
unabhängig voneinander mit ihrer eigenen Blinkfrequenz.
Ds hier ist der Quellcode:
; ***********************************************
; * Zwei Timer in Normalmodus in einem ATtiny24 *
; * Version 1 vom August 2022 *
; * (C)2022 by Gerhard Schmidt *
; ***********************************************
;
.nolist
.include "tn24adef.inc" ; Definiere ATtiny24A
.list
;
; Frequenzen, exportiert aus LibreOffice-Rechenblatt
.equ clock = 1000000 ; Taktfrequenz in Hz
; TC0
.equ cF8 = 1000 ; Frequenz in Hz
.equ cPresc8 = 8 ; Vorteilerwert
.equ cCs8 = 1<<CS01 ; Vorteiler-Bits
.equ cDiv8 = 63 ; Teilerwert
.equ cCmp8 = 62 ; Compare-Wert fuer CTC
; TC1
.equ cF16 = 200000 ; Frequenz in mHz
.equ cPresc16 = 1 ; Vorteilerwert
.equ cCs16 = 1<<CS10 ; Vorteiler-Bits
.equ cDiv16 = 2500 ; Teilerwert
.equ cCmp16 = 2499 ; Compare-Wert fuer CTC
;
.cseg
.org 000000
; Init TC0
sbi DDRB,DDB2 ; Setze PB2 als Ausgangs-Pin OC0A
sbi DDRA,DDA7 ; Setze PA7 als Ausgangs-Pin OC0B
sbi PORTA,PORTA7 ; Setze PA7 auf High
ldi R16,cCmp8 ; Compare A
out OCR0A,R16 ; In Compare A Portregister
out OCR0B,R16 ; In Compare B Portregister
ldi R16,(1<<COM0A0)|(1<<COM0B0)|(1<<WGM01) ; Toggle A und B, CTC-Modus
out TCCR0A,R16 ; In Timer/Counter Kontrollregister A
ldi R16,cCs8 ; Vorteiler CS-Bits
out TCCR0B,R16 ; In Timer/Counter Kontrollregister B
; Init TC1
sbi DDRA,DDA6 ; Setze PA6 als Ausgangspin OC1A
sbi DDRA,DDA5 ; Setze PA5 als Ausgangspin OC1B
sbi PORTA,PORTA6 ; Setze PA6 auf High
ldi R16,High(cCmp16) ; Compare A, MSB zuerst
out OCR1AH,R16 ; In Compare A, MSB
ldi R16,Low(cCmp16) ; dto., LSB
out OCR1AL,R16 ; In compare A, LSB und MSB
ldi R16,High(cCmp16) ; Compare B, MSB zuerst
out OCR1BH,R16 ; In compare B, MSB
ldi R16,Low(cCmp16) ; dto., LSB
out OCR1BL,R16 ; In compare B, LSB und MSB
ldi R16,(1<<COM1A0)|(1<<COM1B0)
out TCCR1A,R16
ldi R16,(1<<WGM12)|cCs16
out TCCR1B,R16
Loop:
rjmp loop
;
; Ende Quellcode
Bitte beachten, dass die Zeile
ldi R16,(1<<WGM12)|cCs16
das WGM12-Bit mit der CS-Bit-Kombination verheiratet, die aus dem
Import aus dem Rechenblatt "ctc" in der LibreOffice-Calc-
Datei timer.ods stammt.
Wenn Du den Import aus dem Rechenblatt vermeiden willst: den optimalen
Vorteiler aus der Frequenz in Assembleresprache zu errechnen ist nicht
so ganz einfach, weil Du ja fünf Vorteilerwerte nacheinander
durchprobieren musst. Mit .if, .else und .endif geht das dann so (hier
für den 16-Bit-Timer TC1):
.equ clock = 1000000 ; Die Taktfrequenz in Hz
.equ cFreq = 1000 ; Die gewuenschte Frequenz in Hz
.equ cTcTop1 = 65536 ; 256 fuer 8-Bit, 65536 fuer 16-Bit
.equ cFreq2 = 2 * cFreq
.if clock / cFreq2 <= cTcTop1
.equ cPresc = 1
.equ cCs = 1<<CS10
.else
.if clock / cFreq2 / 8 <= cTcTop1
.equ cPresc = 8
.equ cCs = 1<<CS11
.else
.if clock / cFreq2 / 64 <= cTcTop1
.equ cPresc = 64
.equ cCs = (1<<CS11)|(1<<CS10)
.else
.if clock / cFreq2 / 256 <= cTcTop1
.equ cPresc = 256
.equ cCs = 1<<CS12
.else
.if clock / cFreq2 / 1024 <= cTcTop1
.equ cPresc = 1024
.equ cCs = (1<<CS12)|(1<<CS10)
.else
.error "Vorteiler-Wert groesser als 1.024!"
.endif
.endif
.endif
.endif
.endif
; Berechne Vergleichswert mit Runden
.equ cDiv = ((clock + cFreq2 / 2) / cFreq2 + cPresc / 2) / cPresc
.equ cCmp = cDiv - 1
Ziemlich heftiger Code, funktioniert aber perfekt.
Wenn Du die Interrupt-Enable-Bits in TIMSK oder TIMSKn gesetzt
hast, funktioniert jeder Überlauf, Vergleichs-Match oder,
in 16-Bit-Timern, ICR-Match mit einem Interrupt. Das macht jedes
Polling des Timers überflüssig.
Wie bei jedem anderen Interrupt,
- setzt der Timer das entsprechende TIF-Bit, und
- wenn das allgemeine Interrupt-Bit I im Statusregister
gesetzt ist und kein höherwertiger Interrupt ansteht,
- wird die aktuelle Ausführungsadresse PC auf dem
Stapel (stack) abgelegt, und
- das allgemeine Interrupt-Flag-Bit I gelöscht, und
- der Controller springt zur weiteren Programmausführung
an die betreffende Interrupt-Vektoradresse, und
- löscht das TIF-Bit.
- bewirkt eine RETI-Instruktion, dass das I-Bit gesetzt und die
vorherige Adresse vom Stapel in den PC geladen wird und die
Programmausführung dort fortgesetzt wird.
Die Interruptvektoren des ATtiny24 sehen so aus:
rjmp Main ; Reset Vektor
reti ; EXT_INT0
reti ; PCI0
reti ; PCI1
reti ; WATCHDOG
reti ; ICP1: Timer 1 compare match ICR
reti ; OC1A: Timer 1 compare match A
reti ; OC1B: Timer 1 compare match B
reti ; OVF1: Timer 1 overflow
reti ; OC0A: Timer 0 compare match A
reti ; OC0B: Timer 0 compare match B
reti ; OVF0: Timer 0 overflow
reti ; ACI
reti ; ADCC
reti ; ERDY
reti ; USI_STR
reti ; USI_OVF
Mehr als die Hälfte aller Interrupts im ATtiny24 befasst sich
mit Timern als Quelle.
Der 16-Bit-Timer 1 hat eine höhere Priorität als
der 8-Bit-Timer 0.
Der ATtiny24 hat zwei Timer-Interrupt-Masken-Register: TIMSK0
für Timer 0 und TIMSK1 für Timer 1. Die
Interrupt-Enable-Bits für Timer 0 sind OCIE0A, OCIE0B und
TOIE0, für Timer 1 dasselbe nur mit 1 statt 0 sowie
zusätzlich noch ICIE1 für den Capture-Interrupt.
Innerhalb der Interrupt-Service-Routine von Timern kann und
darf alles gemacht werden: Flaggen setzen für
außerhalb der Routine, Softwarezähler in Registern
oder im SRAM abwärts zählen, Port-Pins ein- und
ausschalten die nicht OC heißen, usw. usf. In der Routine
können beliebige Pins manipuliert werden. Man kann sogar
den Timer abschalten, durch Nullsetzen der CS-Bits, wenn er
nicht mehr gebraucht wird (z. B. für einen
Software-Einzel-Puls).
Der folgende Quellcode zeigt die Erzeugung eines einzigen
sehr, sehr, sehr langen Impulses: nach dem Drücken eines
Tasters geht ein Ausgangspuls hoch und bleibt für eine
lange Zeit an. Er kann den Timer TC0 verwenden (minimum Zeit
0,26 Sekunden, maximum Zeit 4,77 Stunden) und benutzt
den Überlauf-Interrupt, um einen 16-Bit-Zähler in
R25:R24 von der Konstante cDelay herabzuzählen.
Die Konstante kann im Rechenblatt "langzeit" der
LibreOffice-Calc-Datei timer.ods
berechnet werden. Wenn Du ein 15-Minuten-Signal für die
Treppenhausbeleuchtung brauchst: setze cDelay auf 3.433.
Der Quellcode kann hier
heruntergeladen werden.
;
; ***********************************
; * Einzelpuls vom INT0-Eingang *
; * ATtiny24A, Version 1.0 *
; * (C)2022 by Gerhard Schmidt *
; ***********************************
;
.nolist
.include "tn24adef.inc" ; Definiere Device ATtiny24A
.list
;
; **********************************
; V E R Z O E G E R U N G
; **********************************
;
.equ cDelay = 1 ; Kann zwischen 0 und 65535 sein
;
; **********************************
; R E G I S T E R
; **********************************
;
; frei: R0 bis R15
; benutzt: R16 als Vielzweckregister
; frei: R17 bis R23
.def rCntL = R24
.def rCntH = R25
; frei: R26 bis R31
;
.cseg
.org 000000
;
; **********************************
; R E S E T & I N T - V E C T O R E N
; **********************************
rjmp Main ; Reset-Vektor
rjmp ExtInt0Isr ; EXT_INT0
reti ; PCI0
reti ; PCI1
reti ; WATCHDOG
reti ; ICP1
reti ; OC1A
reti ; OC1B
reti ; OVF1
reti ; OC0A
reti ; OC0B
rjmp Ovf0Isr ; OVF0
reti ; ACI
reti ; ADCC
reti ; ERDY
reti ; USI_STR
reti ; USI_OVF
;
; **********************************
; I N T - S E R V I C E R O U T .
; **********************************
;
ExtInt0Isr:
sbi PORTB,PORTB0 ; Setze den Ausgangspin
ldi rCntH,High(cDelay)
ldi rCntL,Low(cDelay)
ldi R16,(1<<CS02)|(1<<CS00) ; Vorteiler = 1024
out TCCR0B,R16
ldi R16,1<<TOIE0
out TIMSK0,R16
reti
;
Ovf0Isr:
sbiw rCntL,1
brne Ovf0IsrReti
cbi PORTB,PORTB0
clr R16
out TCCR0B,R16
Ovf0IsrReti:
reti
;
; **********************************
; H A U P T P R O G R A M M I N I T
; **********************************
;
Main:
.ifdef SPH
ldi R16,High(RAMEND)
out SPH,R16
.endif
ldi R16,Low(RAMEND)
out SPL,R16 ; Initiere LSB Stapelzeiger
; Init PB0 als Signalausgangspin
sbi DDRB,DDB0 ; Pin PB0 als Ausgang
; Init des INT0-Pins
sbi PORTB,PB2 ; Pull-up-Widerstand ein
ldi R16,1<<ISC01 ; Interrupt bei fallender Flanke
out MCUCR,R16
ldi R16,1<<INT0
out GIMSK,R16
sei ; Enable Interrupt
;
; **********************************
; P R O G R A M M S C H L E I F E
; **********************************
;
Loop:
rjmp loop
;
; Ende Quellcode
;
; Copyright information
; .db "(C)2022 by Gerhard Schmidt " ; Quellcode-lesbar
; .db "C(2)20 2ybG reahdrS hcimtd " ; Maschinencode-lesbar
;
Wenn Du noch längere Zeiten brauchst: der 16-Bit-TC1
verlängert die Zeiten um das 256-fache, bis mehr als
einen Monat lang. Nur einfach ein paar Zeilen im Code
ändern und den Interrupt OVF1 verwenden. Das gibt
dann 67 Sekunden für cDelay = 1 und das
Längste sind 50,9 Tage für cDelay = 0.
Einige Timer/Counter können auch als Zähler für
Impulse dienen. Nur TC0 und TC1 können das, TC2 meistens
nicht.
Timer/Counter, die als Zähler eingesetzt werden, haben
einen Puls-Eingabepin, der sich "Tn" nennt. Wenn Du
- die beiden Bits CSn2=1 und CSn1=1 setzt, und
- den Pull-up-Widerstand aktivierst, und
- wenn Du den Input-Pin torkeln lässt, dann
- erhöht sich der Timerstand bei jedem Drücken
der Taste, wenn CSn0=0 ist, oder
- er erhöht sich beim Loslassen der Taste, wenn
CSn0=1 ist.
In allen Fällen muss die Dauer der Pulse mindestens vier
Taktzyklen sein. Das ist der Grund, warum man bei 1 MHz Takt
nur maximal 250 kHz zählen kann, während man bei
20 MHz noch 4 MHz hinkriegt.
Aber: erwarten nicht, dass dieses Beispiel korrekt funktioniert.
Alle Schalter dieser Welt torkeln, wenn sie gesclossen und
geöffnet werden. Und die vier Taktzyklen sind bei weitem
nicht lkang genug, um 10 Millisekunden langes Torkeln
abzufangen. Verwende also besser ein kleines RC-Netzwerk, um
die 10- oder 20-Millisekunden-Torkelphase auszubügeln.
Oder halt einen weiteren Timer mit einem Einzelschuss.
&Uuuml;berlauferkennung, Verrgleicher, die COM-Pins und, in
16-Bit-TCs, das Input-Capture funktionieren auch in diesem
Modus wie gehabt und können mit Interrupts abgefangen
werden.
9 Schlussfolgerungen
Timer/Counter in AVRs sind vielseitige Hardware-Einzelstücke.
Man kann sie für jedes noch so kurze wie lange Vergnügen
einsetzen, für das man Zeiten braucht oder zählen muss.
Egal, ob es einmalig oder wiederkehrend sein soll. Ich verspreche,
dass Du niemals wieder langweilige Zählschleifen einsetzt,
wenn Du mal den Umgang mit Timern erlernt hast. Und andauernd
TCNT lesen wirst Du dann auch nicht mehr tun.
Zum Seitenanfang
©2022 by http://www.avr-asm-tutorial.net