Neben dem Prozessor gibt es auf dem Chip des ATTiny 2313 noch etliche weitere nützliche Einheiten. Dazu zählen auch die beiden Timer/Counter T0 und T1. Ihre Aufgabe ist es, unabhängig vom Programmaublauf, Zeiten zu erfassen oder Ereignisse zu zählen, was letztlich auf das Gleiche herauskommt. Während der Timer/Counter0 mit einem Byte Breite (bis 255) zählen kann, ist Timer/Counter1 in der Lage in 2 Byte Breite (bis 65535) zu zählen.
Timer können durch den internen Systemtakt angesteuert werden oder durch einen externen Takt, wodurch externe Ereignisse gezählt werden können. Ferner kann der interne Prozessortakt separat für die Timer durch 8, 64, 256 und 1024 vorgeteilt werden. Wir werden den Prescaler (Vorteiler) auf 8 einstellen. Damit bringen wir den Timer0 dazu, in Mikrosekunden zu zählen.
Interruptroutinen
Interrupts (Unterbrechungen) sind Ereignisse, die während eines normal ablaufenden Programms von der Hardware des Systems hervorgerufen werden, um auf sich aufmerksam zu machen. Zur Behandlung der Unterbrechung sind Interruptroutinen vorgesehen, die m Wesentlichen vergleichbar mit Unterprogrammen (UP) sind. Allerdings werden letztere durch den Befehl RCALL (Relative Call) von einer ganz genau bekannten Stelle aus dem übergeordneten Programm aufgerufen. Nach Abarbeitung eines Unterprogramms wird an eine Stelle zurückgesprungen, die dem Programmierer beim Erstellen seines Programms wiederum bereits bekannt ist. So kann er gezielt auf Daten, die im UP bearbeitet wurden zurückgreifen. Die Anzahl von UP ist nicht beschränkt.
Interruptserviceroutinen (ISR) kann es nur so viele geben, wie die Hardware an möglichen Unterbrechungsquellen zur Verfügung stellt: 0x19 = 25 Stück beim Tiny 2313. Dazu kommt, dass ISR aus heiterem Himmel den Ablauf des gerade aktiven Programms unterbrechen, sobald eben die Bedingung für eine Unterbrechung von Seiten der Hardware vorliegt und die Unterbrechung zugelassen wird. Weil das Hauptprogramm nicht auf die Unterbrechung vorbereitet ist, kann es natürlich auch keine Vorkehrungen zur Datensicherung treffen. Es kann lediglich noch den gerade laufenden Befehl erledigen, sich die Adresse des nächsten Befehls auf den Stack legen und dann muss der Interrupt bedient werden.
Daraus ergeben sich Forderungen an eine ISR, die z. B. dafür sorgen muss, dass wichtige Register, die während ihres Ablaufs verändert werden könnten, gesichert werden bevor sie ihre eigentliche Arbeit aufnimmt. Dazu gehören in der Regel das Statusregister SREG des Prozessors und natürlich alle allgemeinen Register R0 bis R31, sofern sie in der ISR verändert werden. Vor der Rückgabe der Kontrolle an das unterbrochene Programm sind diese Register natürlich zu restaurieren. Zum Zwischenspeichern wird in der Regel des Stack des Prozessors benutzt. Der Befehl PUSH schiebt den Inhalt eines Registers auf den Stapel, der Befehl POP holt den Wert wieder in das Register zurück.
Das auftretende Hardwareereignis löst einen IRQ (Interrupt ReQuest = Unterbrechungsanforderung) aus. Im vorliegenden Fall wird das das Erreichen eines bestimmten Zählerstandes des Zählers0 sein. Weil die Hardware nicht wissen kann wo im Programmspeicher die zugehörige Behandungsroutine (ISR) steht, wurden die Interruptvektoren eingerichtet. Das sind feste, jedem Ereignis zugeordnete Stellen am Programmstart. An jede dieser Stellen kann man einen Sprungbefehl setzen, der an den Anfang der entsprechenden ISR verzweigt.
Von den Interruptvektoren (Datenblatt Seite 47) 0x0000 bis 0x001C sind für die Timer von Interesse :
Timer0: OVF0addr = 0x0006; OC0Aaddr = 0x000D und OC0Baddr = 0x000E
Timer1: OVF1addr = 0x0005; OC1Aaddr = 0x0004 und OC1Baddr = 0x000C
Wir werden eine Interruptserviceroutine (ISR) schreiben, die als eine Art Unterprogramm zu betrachten ist. Deren Startadresse im Programmspeicher legen wir als Sprungbefehl an der Adresse 0x000D ab. So ein Sprungbefehl wird als Interruptvektor bezeichnet.
.org 0x000D
rjmp timer0_oc0a
Die 25 möglichen Quellen für Programmunterbrechungen beim Tiny 2313 haben alle einen solchen definierten Einsprungpunkt. Die Tabelle auf Seite 47 im Datenblatt weist die Zusammenhänge auf. Für jeden der denkbaren Interrupts gibt es einen Bereich von einem word in dem ein Sprungbefehl rjmp xxxxx untergebracht werden kann. Der erste derartige Sprungbefehl ist der Reset-Vektor an der Adresse 0x0000. Hier steht in meinen Programmen auch stets der Sprung auf die eigentliche Start-Adresse des Programms - rjmp start. die restlichen Vektoren sind nur dann belegt, wenn sie auch tatsächlich verwendet werden.
Der Timer/Counter 0
Wie bereits erwähnt, werden wir den Timer/Counter 0 so einsetzen, dass er bis zum Erreichen eines von uns vorgegebenen Zahlenwerts zählt, um dann einen IRQ hervorzurufen. Der Timer/Counte 0 tut seine Arbeit unabhängig vom Prozessor. Der Prozessor wird dadurch frei, andere Dinge zu erledigen. Natürlich weiß der Prozessor auch nicht, wo der Timer/Counter0 gerade ist, und wann der Zählerstand erreicht ist. Daher muss der Timer die Möglichkeit haben, den Prozessor genau zu diesem Moment zu unterbrechen und dazu zu zwingen sich jetzt sofort um die Zeitmessung zu kümmern. Damit das geschehen kann muss man in den Registern, die mit dem Timer/Counter0 zu tun haben, ein paar Einstellungen tätigen.
Die Registerübersicht auf Seite 215 des Datenblatts zum Tiny2313 gibt unter anderem Aufschluss über die Adressen und Bitfunktionen der Timer/Counter. Werte, die den Timer/Counter0 betreffen sind hervorgehoben.
GTCCR (General Timer/Counter Control Register = 0x23)
PSR10 (Bit 0 = PreScaler Reset timer1 and timer0) = 1 setzt die Vorteiler
beider Timer zurück
Jeder Timer/Counter hat Controlregister, deren Bits die Funktionfestlegen.
TCCR0A (Timer/Counter Control Register 0 A = 0x30) Datenblatt Seite
77
Name
|
Bit7
|
Bit6
|
Bit5
|
Bit4
|
Bit3
|
Bit2
|
Bit1
|
Bit0
|
TCCR0A
|
COM0A1
|
COM0A0
|
COM0B1
|
COM0B0
|
WGM01
|
WGM00
|
||
0
|
0
|
0
|
0
|
1
|
0
|
Die Bits COM0A1 und COM0A0 (COmpare Match output mode Timer 0 unit A - Bit 1 und 0) stellen mit 0 0 das normale Portverhalten ein. Das heißt, dass von der Vergleichsoperation und deren Ergebnis am Anschluss OC0A (Pin14 = PB2) nichts zu verzeichnen ist. (Brauchen wir hier auch nicht)
Die Bits WGM01:0 legen zusammen mit WGM02 in TCCR0B den Modus 2 der Wellenformgenereators fest. In unserem Fall bedeutet das, dass immer dann, wenn der Timer/Counter0 bei Hochzählen des Timer/Counter-Taktes den Wert im Register OCR0A erreicht hat, wird sein Zählerwert auf Null gesetzt (CTC-Mode) und gleichzeitig ein Interrupt, eine Programmunterbrechung, ausgelöst. Gemäß der COM0A1:0-Bits unterbleiben weitere Aktionen am Pin14 des Bausteins.
Zusammenfassung: Immer, wenn der Zähler0 den Zahlenwert im Register OCR0A erreicht hat, wird er zurückgesetzt und es wird eine Programmunterbrechung (Interrupt) ausgelöst. Über den Interruptvektor, d.h. den Sprungbefehl bei der Adresse OC0Aaddr = 0x000D wird eine Interruptserviceroutine aufgerufen, die mit einem Unterprogramm im weiteren Sinn vergleichbar ist (siehe oben).
TCCR0B (Timer/Counter Control Register 0 B = 0x33) Datenblatt Seite
79-81
Name
|
Bit7
|
Bit6
|
Bit5
|
Bit4
|
Bit3
|
Bit2
|
Bit
|
Bit0
|
TCCR0B
|
FOC0A
|
FOC0B
|
WGM02
|
CS02
|
CS01
|
CS00
|
||
0
|
0
|
0
|
0
|
1
|
0
|
Tabelle 40 auf Seite 79 des Datenblatts zum Tiny2313 und Tabelle 41 auf Seite 81 liefern die Belegungen der Bits 3:0 des Registers TCCR0B. Die Bedeutung der Bits CS02:0 beschreibt die folgende Tabelle. Wir wollten, dass der Systemtakt durch 8 vorgeteilt wird, daher müssen wir die CS02:0-Bits mit 0 1 0 belegen.
CS02
|
CS01
|
CS00
|
Beschreibung |
0
|
0
|
0
|
Timer angehalten (zählt nicht) |
0
|
0
|
1
|
Takt = Sysemtakt = keine Vorteilung |
0
|
1
|
0
|
Vorteiler 8 |
0
|
1
|
1
|
Vorteiler 64 |
1
|
0
|
0
|
Vorteiler 256 |
1
|
0
|
1
|
Vorteiler 1024 |
1
|
1
|
0
|
externe Quelle - Fallende Flanke zählt |
1
|
1
|
1
|
externe Quelle - Steigende Flanke zählt |
Das Timer/Counterregister selbst ist TCNT0 (Timer/Counter 0 = 0x32). Wir haben damit unmittelbar nix zu tun. Das Hochzählen und Rücksetzen übernimmt für uns die Hardware des Tiny2313.
Jeder Timer/Counter besitzt ferner zwei Vergleichsregister. Von diesen brauchen wir das OCR0A. Wir lassen den Timer/Counter0 hochzählen, bis der Inhalt von TCNT0 mit dem von OCR0A übereinstimmt. Ist das der Fall, dann setzt dieses Ereignis das Zählerregister TCNT0 auf 0 und ein Interrupt (IRQ) wird ausgelöst, falls das TIMSK-Register die entsprechenden Bits gesetzt hat. Die IRQ-Anfrage der Hardware sorgt nach dem Abarbeiten des laufenden Befehls für einen Sprung des Programmzählers an die Adresse 0x000D. Dort steht der Sprungbefehl auf die Serviceroutine zu dieser IRQ-Anfrage. Das IRQ-Unterprogramm an der Marke (Label) timer0_oc0a: wird ausgeführt, bis ein RETI-Befehl gefunden wird. Der RETI (RETurn from Interrupt) - Befehl sorgt dafür, dass das unterbrochene Programm mit dem Befehl nach dem Eintreffen der Unterbrechungsanforderung im Hauptprogramm fortgesetzt wird.
OCR0A (Output Compare Register 0 A = 0x36)
Wir werden dieses Register auf den Wert 200 setzen. Nach 200µs wird somit
regelmäßig das Hauptprogramm unterbrochen. Das entspricht 200 * 8
= 1600 Takten.
OCR0B (Output Compare Register 0 B= 0x3C)
Die möglichen Interruptquellen der beiden Timer legt das Interruptmaskenregister fest.
TIMSK (Timer Interrupt MaSKregister = 0x39) Datenblatt
Seite 82, 113
Name
|
Bit7
|
Bit6
|
Bit5
|
Bit4
|
Bit3
|
Bit2
|
Bit1
|
Bit0
|
TIMSK
|
TOIE1
|
OCIE1A
|
OCIE1B
|
-
|
ICIE1
|
OCIE0B
|
TOIE0
|
OCIE0A
|
0
|
0
|
0
|
-
|
0
|
0
|
0
|
1
|
Damit der gewünschte Interrupt ausgelöst wird, müssen wird lediglich das Bit 0 = OCIE0A im Register TIMSK setzen und mit dem Befehl SEI (SEt Interruptflag) Unterbrechungen generell zulassen.
Das Demoprogramm
Beim vorliegenden, bereits recht umfangereichen, Demoprogramm übernimmt der Timer/Counter0 durch den von ihm ausgelösten Interrupt, der alle 200 µs erfolgt, die Zeitmessung und die Abfrage von zwei Tasten der Erweiterungsplatine nebst der dadurch hervorgerufenen Aktionen. Als Rückmeldung wird die LED A mit konstanter Frequenz (1 Sekunde) aber mit variabler Dauer (Dutycycle) zum Blinken gebracht. Das Verändern der Einschaltdauer der LED übernehmen die Tasten 1 (hochzählen) und 2 (herunterzählen). Langes Drücken der Tasten stellt die Leuchtdauer auf Maximum (99 durch Taste 1) und Minimum (1 Taste 2).
Das verwendete Verfahren hat den riesen Vorteil, dass die Zeitmessung ohne Klimmzüge sehr exact wird und dass sich vor allem der Processor im Vordergrund nur im Leerlauf befindet, während er bisher ständig beschäftigt war. Er bekommt während der 1600 Takte, die zwischen zwei IRQs liegen, grade um die hundert Takte was zu tun. Daraus wird ersichtlich, dass in der Haupschleife Dinge untergebracht werden können, die zeitaufwendig aber nicht dringend sind. So wie in der ISR selbst, kann man natürlich die Umsetzung von Tastatureingaben auch in die Hauptschleife übernehmen. Dieses ist, folgt man der Philosophie der Interrupts, besonders dann sinnvoll, wenn die Reaktion auf Tastatureingaben ein Wesentliches länger dauert, wie die Abfrage selbst. Während nämlich gerade eine IRQ-Behandlung läuft, kann nicht zeitgleich eine Zweite bedient werden. Die zweite Anfrage muss warten, bis die erste bearbeitet ist. Dauert also die Reaktion auf einen IRQ länger als die regelmäßige Folgezeit ausmacht, dann muss z. B. die nächste Tastenabfrage warten, bis die aktuelle Reaktion abgearbeitet ist. Das gilt natürlich nicht nur für gleichartige IRQs sondern auch für die IRQs aus verschiedenen Quellen. Ergänzend sollte vielleicht noch erwähnt werden, dass IRQs mit niedriger Vektoradresse mit höherer Priorität bedient werden. Der IRQ mit dem Vergleichsregister A (OCR0A) bei Timer 0 wird also vor einem IRQ des gleichen Timers mit Vergleichsregister B (OCR0B) bedient, wenn die Anforderungen gleichzeitig oder während der Abarbeitung eines aneren IRQs eintreffen. Über die Reihenfolge gibt die Tabelle 21 auf Seite 47 des Datenblatts zum Tiny 2313 Auskunft.
Doch jetzt zur
Behandlung des Demoprogramms.
Neu sind die Pseudobefehle ".nolist", ".list" und ".def". Die ersten beiden schalten die Listung von Befehlen für die Listingdatei "sekundentakter.lst" (Rechtsklick und "Ziel speichern unter") aus und wieder ein. Diese Datei verrät etliches mehr über das vorliegende Projekt. So gibt es zu jeder Zeile im Quellcode den Wert des Programmzählers und den Maschinencode des übersetzten Mnemonics. Auch die Zusammenfassungen am Ende des Listings sind interessant, besonders die Tabelle, wie oft jeder der insgesamt verfügbaren Befehle verwendet wurde. Diese Tabelle kann helfen den eigenen Programmierstil zu verbessern indem häufig aber auch nie verwendete Befehle ersichtlich werden. Eine Zusammenfassung aller im Projekt verwendeten Aliase (Variablen) bringt die Datei "sekundentakter.map" (Rechtsklick und "Ziel speichern unter") zum Vorschein. Wer das Programm nicht selber eintippen möchte, hier der Quellcode (Rechtsklick und "Ziel speichern unter") . Und für ganz Ungeduldige ist die HEX-Datei (Rechtsklick und "Ziel speichern unter") verfügbar.
Im Programm werden anstelle der Namen der Universalregister eigene Aliasnamen verwendet. Zur Declaration ist die Assemblerdirektive ".def" zu verwenden. Dieser Ansatz bringt Vorteile wie Nachteile. Zum einen sind die Namen der Register jetzt selbstredend, was den Verwendungszweck angeht. Andererseits sind die Programmmodule schlecht in andere Projekte portierbar. Man muss selbst gewichten was einem wichtiger ist.
Neu ist auch die Schreibweise (1<<4), deren Bedeutung im folgenden Programmvorspann erläutert wird. Die Notation tritt mehrfach im Quellcode auf.
Nach dem Vorspann folgt die Definition des Interruptvektors zum "Output Compare Interrupt" von Timer 0. Es folgen die Definitionen der Aus- und Eingänge für LED und Tasten. Während der Einrichtung der Eigenschaften des Timer/Counter0 wird dessen Taktung ausgeschaltet. Das Rücksetzen der Zeitregister und das grundlegende Einstellen des Dutycycle schließen die Vorbereitenden Aktionen ab.
Die Hauptschleife erzeugt nur Leerlauf und sorgt dafür, dass das Programm in einem definiertem Zustand verbleibt.
Es gibt nur ein Unterprogramm, nämlich die ISR zum "Output Compare A" Ereignis von Timer/Counter0. Der Timer wird immer dann, wenn der Zählerstand 200 erreicht hat, einen IRQ absetzen. Weil er mit einem 1,0000 MHz -Takt versorgt wird, ist das alle 200µs der Fall. Nachdem das Statusregister im zuvor gesicherten Register temp1 = r16 gemerkt wurde, werden noch die Register hundert und temp2 auf dem Stack in Sicherheit gebracht.
Für Kontrollzwecke wird PortD.5 auf 1 gesetzt. Nach Behandlung aller Eventualitäten geht der Anschluss wieder auf 0. Bei jedem IRQ wird so ein Puls abgesetzt, der am Pin 9 des Tiny 2313 eine Frequenz von 5000 Hz erzeugt. Ist diese mit einem Frequenzmesser feststellbar, kann man davon ausgehen, dass der Baustein ordnungsgemäß arbeitet.
Das Register Jiffies zählt die IRQ-Ereignisse. Nach 50 IRQs ist die die Zeit von 50 x 200µs = 10ms = 0,01s vergangen. 1 Jiffie ist also ein 50-stel einer hundertstel Sekunde. Umgekehrt stellt jedes Jiffie eine Zeitscheibe von 200µs entsprechend 1600 Taken dar. In jeder Zeitscheibe kann man also theoretisch bis zu 1600 Befehle unterbringen, die während dieser Zeit abzuarbeiten sind. Praktisch müssen es natürlich deutlich weniger sein, weil für die generelle Verwaltung und das Hauptprogramm ja auch noch etwas übrig bleiben muss. Wenn man jedoch davon ausgeht, dass je Zeitscheibe nur zu 25% bis 50% von IRQ-Jobs gebraucht wird, dann bleibt für den Rest noch ausreichend Zeit und der Benutzer hat den Eindruck, dass alles gleichzeitig abläuft (Multitasking). Im vorliegenden Entwurf ist dieser Ansatz dadurch realisiert, dass die Jobs auf die Nummern der Zeitscheiben dediziert sind, die je hunderstel Sekunde aber nur einmal einmal zum Zug kommen. Bis also der Tastenzähler von 1 auf 100 im Dauerdruck kommen kann, ist eine Sekunde vergangen. Für ein einzelnes Tastaturereignis ist das 3-malige Durchlaufen der IRQ-Routine ohne Pegelwechsel nötig. Das entspricht einer Zeit von 0,03s. Dieser Umstand wirkt einem Tastenprellen sicher entgegen behindert andererseits die Tastensteuerung der Anwendung nicht. Andere Ansätze zur Tastenentprellung erfodern wesentlich mehr Speicher- und Hardwareresourcen.
Die Komentare im Quelltext dürften ausreichen, die zugrundeliegende Struktur klar zu machen. Neben dem Erhöhen er Zeit, während der eine Taste gedrückt ist, werden auch die Zustände "nicht gedrückt" und "lange gedrückt" berücksichtigt. So wird der Zeitzähler der Taste sofort zurückgesetzt, wenn sie nicht mehr gedrückt ist. Andererseits überwacht die Sequenz, dass der Zähler nicht über 100 anwachsen kann.
Zusammenfassung: Nur einmal pro hunderstel Sekunde wird auf Tastenbetätigungen geprüft. Diese Prüfung findet im ersten 50-stel der hundertstel Sekunde statt. Im Weitern wird auf den Tastendruck nur reagiert, wenn er während 3 hundertstel Sekunden ununterbrochen stattgefunden hat. Die Auswertung des Tastendrucks erfolgt in der 2. Zeitscheibe einer hundertstel Sekunde. Somit wird ein Tastendruck frühestens nach 0,0302s ausgeführt.
In der zweiten Zeitscheibe (2. Hundertstelsekunde) wird auf eventuelle Tastendrucke reagiert, falls sich entweder ein Zähler von 3 oder 100 aufgebaut hat. Wieder sind die maximal nötigen Takte in dieser Zeitscheibe von 1600 Takten deutich niedriger als 100, selbst wenn man die Taste3 noch mit hochrechnet.
Der Rest der IRQ-Routine ist jetzt unabhängig von den Tastaturereignissen nur noch mit der LED, der Zeitverwaltung und dem Restaurieren der Register befasst. Die Komentare des Quellcodes sprechen wieder für sich selbst.