Keine Angst vor Mikrokontrollern! Der einfache Einstieg in die Welt der AVRs (5) Roland Walter, DL7UNO |
Die aktuellen Qelltexte und Compilate zu den Beiträgen
Im letzten Teil hatten wir anhand der UART die Arbeit mit Interrupts besprochen. Die Interrupts werden wir auch weiterhin benötigen. Der Schwerpunkt liegt jetzt aber darauf, weitere Peripheriebausteine des AT90S2313 vorzustellen.Externe Interrupts
Der AT90S2313 verfügt über zwei externe Interrupts, also über zwei Eingangsleitungen, mit denen man Interrupts auslösen kann. Die Pin-Belegung ist dabei fest und kann nicht verändert werden. Interrupt 0 ist dem Pin PD2 zugeordnet und Interrupt 1 dem Pin PD3. Mit Hilfe der externen Interrupts kann man ohne Pollen auf externe Ereignisse wie z.B. einen Tastendruck reagieren.'0006.BAS: Externe Interrupts 'Hardware: MAX232 an PD0/PD1, Nullmodemkabel zum PC ' PD4 an Taste 2, PD3 (Int1) an Taste 1 ' PD2 (Int0) an RTS-Ausgang vom MAX232 '--------------------------------------------------------- $Regfile = "2313def.dat" 'AT90S2313-Deklarationen $Crystal = 3686400 'Quarz: 3.6864 MHz $Baud = 9600 'UART-Baudrate: 9600 Baud DDRD = &B11101111 'PD4 Eingang, Rest Ausgang 'DDRD = &B11100011 'PD2+3+4 Eingang, Rest Ausgang 'PORTD = &B00011100 'Pullups von PD2+3+4 aktivieren PORTD.2 = 1 'PullUp von PD2/Int0 aktivieren PORTD.3 = 1 'PullUp von PD3/Int1 aktivieren PORTD.4 = 1 'PullUp von Pin PD4 aktivieren On INT0 OnRts 'Interrupt-Routine für RTS On INT1 OnTaste1 'Interrupt-Routine für Taste 1 Config INT0 = FALLING 'Interrupt 0 bei H/L-Flanke auslösen Config INT1 = FALLING 'Interrupt 1 bei H/L-Flanke auslösen Enable INT0 'Externen Interrupt 0 einschalten Enable INT1 'Externen Intterrupt 1 einschalten Enable Interrupts 'Interrupts global zulassen '--------------------------------------------------------- Print Bin(DDRD) Do 'Hauptschleife If PIND.4 = 0 Then 'Wenn Taste 2 gedrückt Print "T2 "; 'Ereignis via UART melden End If Loop '--------------------------------------------------------- OnRts: 'Int0-Routine Print "RTS "; 'Ereignis via UART melden Return '--------------------------------------------------------- OnTaste1: 'Int1-Routine Print "T1 "; 'Ereignis via UART melden Return |
Hier noch einmal zur Sicherheit die Verdrahtung
Nach dem Verbinden aller Leitungen brennen Sie das Programm bitte erst einmal so, wie es im Kasten wiedergegeben ist. Anschließend wird AVRTerm gestartet und die Anzeige auf "Plain text" eingestellt.
Nach dem Herstellen der Verbindung im Terminalprogramm machen Sie bitte unmittelbar nacheinander die folgenden drei Experimente:
1. Markieren Sie einmal die Checkbox "RTS" und entfernen die Checkmarke wieder. Beim Entfernen der Checkmarke (und nur dann) muß der AVR den Text "RTS" senden (und das nur einmal). Wiederholen sie das mehrfach. | |
2. Jetzt drücken Sie den Taster 1 auf dem Experimentierboard (das ist der äußere Taster) und lassen ihn wieder los. Sowohl beim Drücken als auch beim Loslassen des Buttons wird der AVR den Text "T1" mehrfach senden. Wiederholen Sie auch diesen Test mehrfach. | |
3. Nun drücken Sie den Taster 2 auf dem Experimentierboard. Solange Sie den Taster gedrückt halten, wird er AVR permanent den Text "T2" senden. Das Senden bricht ab, sobald Sie den Taster loslassen. |
Beim zweiten Experiment (Taste 1 an INT1) erhalten wir ein ausgesprochen miserables geprelltes Signal. Beim Drücken der Taste kratzen die Kontakte und erzeugen so gleich mehrere High-Low-Wechsel. Da der AVR schnell genug reagiert, erhalten wir als Folge mehrere Interrupts. Dies ist ein gut bekanntes Problem in der Digitaltechnik, welches ich einfach nicht unerwähnt lassen konnte. Als Problemlösung schaltet man im primitivsten Fall einen 100nF-Kondensator vom INT1-Eingang nach Vcc oder verwendet ein RC-Glied bzw. Monoflop. Die zusätzliche Hardware läßt sich aber einsparen, wenn im AVR noch freier Programmspeicher vorhanden ist. Beispielsweise kann man Interrupt 1 nach jeder Auslösung erst einmal ausschalten (Disable INT0) und schaltet ihn erst nach einer gewissen Zeit wieder ein (Enable INT0). Da der Mensch im Vergleich zum AVR extrem langsam reagiert, sind Zeiten bis hin zu etwa 300 Millisekunden in der Regel praktikabel. Das Wiedereinschalten kann z.B. über einen Timer erfolgen (die Timer werden später separat beschreiben) und vielleicht auch schlicht durch die große Ausführungszeit des Programms.
Ich will noch erwähnen, daß Bascom die Funktion Debounce zum Entprellen anbietet. Diese Funktion ist aber meines Erachtens nur in wenigen Anwendungsfällen praktikabel, weil man hier sehr viel Rechenzeit mit Warten verschenkt. Damit will ich das Thema Entprellen abschließen, denn dies ist nicht der eigentliche Gegenstand des Artikels.
Beim dritten Experiment (Taste 2 an Pin PD4) werten wir schließlich den Zustand des Pins ohne Interrupt aus. Das kennen wir schon und ist lediglich ein Vergleich zum Interrupt.
Software-Interrupt
Ein interessantes Detail des vorangegangenen Programms wurde noch gar nicht erwähnt: Am Anfang des Listings konfigurieren wir auf die bekannte Weise die I/O-Pins. Für das Datenrichtungs-Register DDRD sind im Programmlisting zwei Möglichkeiten vorhanden. Bei der auskommentierten Variante werden die Pins PD2/INT0 und PD3/INT1 auf Ausgang gesetzt und haben damit eigentlich die falsche Richtung. Wenn Sie diese "falsche" Variante ausprobieren, werden Sie feststellen, daß das Programm trotzdem korrekt läuft. Das Atmel-Datasheet gibt ausdrücklich an, daß die externen Interrupts 0 und 1 auch dann ausgelöst werden, wenn PD2/PD3 auf Ausgang gesetzt wurden. Damit sollte dem Anwender eine Möglichkeit zur Auslösung von Software-Interrupts gegeben werden.
Der folgende Kasten zeigt ein Beispielprogramm, bei dem Interrupt 0 als Software-Interrupt verwendet wird. Dieses Programm wird ohne externe Drahtverbindungen ausgeführt. Int0 wird hier (nur als Beispiel) so konfiguriert, daß es bei Low Level (0) Interrupts auslöst. Vor der Hauptschleife setzen wir PD2 softwareseitig einmalig auf 0. Dies löst dann ein Gewitter von Interrupts aus und in der Folge wird das Zeichen "s" ans Terminalprogramm geschickt. Das Ganze wird nur durch die gesetzte Baudrate von 9600 gebremst, sonst käme das Terminalprogramm niemals hinterher.
'0007.BAS: Software-Interrupt über INT0 'Hardware: MAX232 an PD0/PD1, Nullmodemkabel zum PC '--------------------------------------------------------- $Regfile = "2313def.dat" 'AT90S2313-Deklarationen $Crystal = 3686400 'Quarz: 3.6864 MHz $Baud = 9600 'UART-Baudrate: 9600 Baud DDRD = &B11111111 'Alle Pins Ausgang On INT0 OnInt0 Config INT0 = LOW LEVEL 'Int0 bei Low Enable INT0 Enable Interrupts '--------------------------------------------------------- PORTD.2 = 0 'PD2/INT0 einmalig auf 0 Do 'Hauptschleife Loop '--------------------------------------------------------- OnInt0: 'Int0-Routine Print "s "; 'Ereignis via UART melden Return |
Die beteiligten AVR-Register
Der Befehl Config INT0 führt zum Setzen von Bits im Register MCUCR (MCU General Control Register). Neben der Konfiguration der externen Interrupts dient dieses Register auch dazu, den Sleep-Modus des AVR festzulegen und diesen freizuschalten. Um Mißverständnissen vorzubeugen: Der Sleep-Modus hat nichts, aber auch rein gar nichts mit den externen Interrupts zu tun und wir werden ihn jetzt auch nicht behandeln. Für die Konfiguration der beiden externen Interrupts werden je zwei Bits verwendet und für den Sleep-Modus noch einmal 2 Bits. Statt einer ausführlichen Beschreibung will ich hier aber nur auf das Atmel-Datasheet verweisen.
Mit dem Befehl Enable INT0 wird das INT0-Bit (Bit 6) im Register GIMSK (General Interrupt Mask Register) gesetzt und mit Enable INT1 das INT1-Bit (Bit 7) im selben Register. Deshalb hätten die Befehle
GIMSK.INT0 = 1
GIMSK.INT1 = 1
exakt die gleiche Wirkung. Wer Platz im Programmspeicher sparen will, kann beide Zeilen auch in einem einzigen Befehl zusammenfassen:
GIMSK = &B11000000 'INT1+INT0 einschalten
Wenn mehrere Bits eines Registers gesetzt werden müssen, ist ein zusammenfassender Befehl immer besser, denn man spart ja nicht nur Programmspeicher, sondern erreicht auch mit einem einzigen Takt, was man sonst mit bis zu 8 Takten erledigen müßte. Allerdings sollte man niemals mit Kommentaren geizen, anderenfalls sieht man irgendwann nicht mehr durch.
Den Befehl Enable Interrupts kennen Sie bereits im Zusammenhang mit der seriellen Schnittstelle. Zur Erinnerung: Wenn Sie Enable Interrupts ausführen, dann setzt Bascom das Global-Interrupt-Enable-Bit (Bit 7, auch I-Bit genannt) im AVR-Status-Register SREG. Die Befehle SREG.7 = 1 und Set SREG.7 würden exakt zum gleichen Ergebnis wie Enable Interrupts führen.
Und schließlich ist auch noch das Register GIFR (General Interrupt Flag Register) beteiligt. In diesem Register werden die Bits INTF0 (Bit 6) bzw. INTF1 (Bit 5) gesetzt, sobald die Bedingung zur Auslösung des betreffenden Interrupts erfüllt ist. Mit dem Anspringen der Interrupt-Routine wird das entsprechende Bit vom AVR automatisch wieder gelöscht. Hier sollte man sich ins Gedächtnis rufen, daß alle Interrupts so lange gesperrt sind, wie eine andere Interrupt-Routine ausgeführt wird. Das Interrupt-Flag speichert die Interrupt-Anforderung zwischen, damit sie nicht verlorengeht. Die Interrupt-Flags lassen sich aber auch softwareseitig löschen, indem man GIFR.INTF0 = 1 bzw. GIFR.INTF1 = 1 schreibt (tatsächlich eine 1 und nicht, wie es logischer wäre eine 0). Das Löschen des Interrupt-Flags unterbindet die Auslösung des anstehenden Interrupts. Die Auswertung dieser Flags kann sinnvoll sein, wenn man sich in einer umfangreicheren Interrupt-Routine befindet und vielleicht effizienter reagieren will.
Bitte erst einmal Luft holen, denn wir gehen jetzt zu einem völlig anderen Peripherie-Baustein über.
Der AT90S2313 verfügt über zwei Timer, die völlig unabhängig und unbeeinflußt vom übrigen Programm laufen. Timer0 zählt mit 8 Bit (0-255) und Timer1 mit 16 Bit (0-65535). Beide Timer können mit oder ohne Vorteilung vom Quarz getaktet werden. Der Vorteiler kann für jeden Timer separat gesetzt werden. Die möglichen Teilerraten sind 1/8, 1 /64, 1/256 und 1/1024. Bei unserem 3,6864MHz-Quarz kann die Timer-Taktfrequenz also 3,6864MHz, 460,8KHz, 57,6KHz, 14,4KHz und 3,6KHz betragen. Als Takt-Quelle kann aber auch ein externes Signal dienen, welches nach Wahl entweder an der steigenden oder der fallenden Flanke gezählt werden kann. Damit sind die Timer auch als Zähler verwendbar, z.B. zum Decodieren eines Pulsabstands-modulierten Signals oder als reine Ereigniszähler.
Beide Timer können so eingestellt werden, daß sie bei einem Überlauf einen Interrupt auslösen. Darüber hinaus kann der 16 Bit breite Timer1 noch weit mehr: Er ist in der Lage, den aktuellen Zählerstand laufend mit einem eingestellten Zählerwert zu vergleichen und löst einen Interrupt aus, sobald der Zählerstand erreicht ist. Und noch mehr: Timer1 hat eine Input-Capture-Funktion, bei der als Eingang der integrierte Analog-Komparator verwendet werden kann. Habe ich etwas vergessen? - Ach ja, Timer1 kann auch noch ein pulsabstands-moduliertes Signal erzeugen. Und das alles erledigen die Timer (nahezu) selbständig und unabhängig vom laufenden Programm.
Die Fülle der Möglichkeiten dürfte sicher verwirrend sein. Wir fangen deshalb erst einmal mit dem leicht zu verstehenden 8-Bit-Timer an und arbeiten uns dann schön systematisch Stück für Stück durch. Ich werde mich bemühen, jedes Feature selbständig verwendbar zu beschreiben. Der Aufwand lohnt sich, denn mit den Timern kann man eine unglaubliche Anzahl von Praxisaufgaben lösen.
Timer0
Timer0, wir hatten es schon erwähnt, ist 8 Bit breit und kann deshalb von 0 aufwärts bis 255 zählen, bis er überläuft und wieder bei 0 beginnt. Das wollen wir zusammen mit einer Vorteilung an einem einfachen Beispiel demonstrieren.
Als Taktfrequenz für den Timer verwenden wir die Quarzfrequenz 3,6864MHz / 8 = 460,8KHz. Bei jedem Überlauf soll ein Interrupt ausgelöst werden. Dies erfolgt alle 256 Timer-Takte einmal, womit die Überlauffrequenz 14,4KHz / 256 = 1,8KHz beträgt. Und da diese Frequenz im hörbaren Bereich ist, können wir sie über den Speaker ausgeben. Bei jedem Überlauf invertieren wir den Zustand des Ausgabe-Pins, damit wir eine Wechselspannung erhalten. Durch die Invertierung erhalten wir schlußendlich eine Speaker-Frequenz von 1,8KHz / 2 = 900Hz, was geardezu ein ideal hörbarer Signalton ist.
Hier ist die Drahtverbindung
'0008.BAS: Timer0, einfachstes Beispiel 'Hardware: Draht von Pin PD2 zum Speaker 'Das Programm erzeugt einen hörbaren Ton mit 900Hz '--------------------------------------------------------- $Regfile = "2313def.dat" 'AT90S2313-Deklarationen $Crystal = 3686400 'Quarz: 3.6864 MHz DDRD.2 = 1 'Pin PD2 auf Ausgang setzen Dim i As Bit On TIMER0 OnTimer0 'Interrupt-Routine für Timer0-Overflow Config TIMER0 = TIMER , PRESCALE = 8 'Takt: Quarz/8 Enable TIMER0 'Timer0-Overflow-Interrupt einschalten Enable Interrupts 'Interrupts global zulassen '--------------------------------------------------------- Do 'Hauptschleife Loop '--------------------------------------------------------- OnTimer0: 'Interrupt-Routine i = Not i 'Bit i invertieren PORTD.2 = i 'Speaker-Ausgang Return |
Timer0 mit Voreinstellung
Die Timer können auf einen Wert voreingestellt werden, womit sich die Auflösung sehr verfeinern läßt. Das soll widerum mit dem Speaker vorgeführt werden. Die Verdrahtung bleibt also die gleiche. Zur Demonstration durchlaufen wir im folgenden Programm als Timer-Voreinstellung alle Werte zwischen 0 und 255, womit sich ein Ton mit anschwellender Frequenz ergibt. Die verwendete Timer-Taktfrequenz habe ich übrigens nur nach Gehör gewählt, da es ja lediglich um die Demonstration der Timer-Voreinstellung ging. Die Frequenzveränderung erfolgt zur besseren Hörbarkeit nur bei jedem zehnten Timer-Interrupt. Das trägt nicht zur Übersichtlichkeit im Listing bei, war aber nicht zu vermeiden.
Bitte beachten Sie die Reihenfolge der Befehle in der Interrupt-Routine, denn diese ist nicht ganz egal. Da der Timer unabhängig vom sonstigen Geschehen zählt, ist es sinnvoll, ihn sofort nach dem erfolgten Überlauf-Interrupt auf den gewünschten Wert voreinzustellen. Der Timer zählt mit diesem Wert beginnend sofort (unabhängig!) weiter. Würden wir ihn erst später voreinstellen, dann würde die Programmablaufzeit zur Timer-Zeit hinzukommen und sogar je nach aktueller Programmverzweigung ungleichmäßig werden.
Gleich nach dem Voreinstellen des Timers setzen wir den Speaker-Ausgang auf den aktuellen Wert. Auch das ist zeitkritisch.
Danach haben wir bis zum nächsten Überlauf-Interrupt genug Zeit für alle anderen Dinge. Der Timer ließe sich mit x = TIMER0 übrigens auch auslesen, was bei uns aber keinen Sinn gemacht hätte.
'0009.BAS: Timer0 mit Voreinstellung 'Hardware: Draht von Pin PD2 zum Speaker 'Das Programm erzeugt einen Ton mit ansteigender Frequenz '--------------------------------------------------------- $Regfile = "2313def.dat" 'AT90S2313-Deklarationen $Crystal = 3686400 'Quarz: 3.6864 MHz DDRD.2 = 1 'Pin PD2 auf Ausgang setzen Dim fToggle As Bit Dim bCounter As Byte Dim bFreqChange As Byte On TIMER0 OnTimer0 'Interrupt-Routine für Timer0-Overflow Config TIMER0 = TIMER , PRESCALE = 64 'Takt: Quarz/64 Enable TIMER0 'Timer0-Overflow-Interrupt einschalten Enable Interrupts 'Interrupts global zulassen '--------------------------------------------------------- Do 'Hauptschleife Loop '--------------------------------------------------------- OnTimer0: 'Interrupt-Routine TIMER0 = bCounter 'Timer0 voreinstellen fToggle = Not fToggle 'Toggle-Bit invertieren PORTD.2 = fToggle 'Speaker-Ausgang If bFreqChange = 10 Then 'Alle 10 Interrupts bFreqChange = 0 Incr bCounter 'Voreinstellwert erhöhen End If Incr bFreqChange Return |
Timer0 als Counter
Als letztes soll Timer0 als Counter Verwendung finden. In diesem Fall dient der Pin PD4/T0 als Clock-Eingang (das kann auch nicht geändert werden). Als Signalquelle verwenden wir Taster 1. Da Taster 1 an Masse angeschlossen ist, legen wir mittels Config TIMER0 fest, daß an der fallenden Flanke gezählt werden soll und aktivieren außerdem natürlich den internen PullUp-Widerstand das Pins PD4.
Bei jedem Tastendruck soll Timer1 erhöht werden. Und da wir Timer1 auf den Wert 246 voreinstellen, wird nach jeweils 10 Tastendrücken ein Überlauf-Interrupt ausgelöst. Nicht vergessen: Bei einem Byte gilt 246 + 10 = 0. Den Überlauf lassen wir mit der roten LED melden, die an Pin PB0 angeschlossen wird. Bei jedem Überlauf schaltet sich die LED ein oder aus.
Die Verdrahtung
'0010.BAS: Timer0 als Counter für externe Ereignisse 'Hardware: Taste 1 an PD4/T0-Pin, Rote LED an PB0 '10 Tastendrücke erzeugen einen Counter-Überlauf, 'falls die Taste entprellt ist. 'Alternativ kann als "Taste" auch der RTS-Ausgang 'des MAX232 verwendet werden. '--------------------------------------------------------- $Regfile = "2313def.dat" 'AT90S2313-Deklarationen $Crystal = 3686400 'Quarz: 3.6864 MHz DDRD.4 = 0 'Pin PD4/T0 auf Eingang setzen PORTD.4 = 1 'PullUp von PD4/T0 aktivieren DDRB.0 = 1 'Pin PB0 auf Ausgang setzen On TIMER0 OnTimer0 'Interrupt-Routine für Timer0-Overflow Config TIMER0 = COUNTER, EDGE = FALLING Enable TIMER0 'Timer0-Overflow-Interrupt einschalten Enable Interrupts 'Interrupts global zulassen '--------------------------------------------------------- TIMER0 = 246 'Counter initial voreinstellen Do 'Hauptschleife Loop '--------------------------------------------------------- OnTimer0: 'Interrupt-Routine TIMER0 = 246 'Counter (wieder) voreinstellen PORTB.0 = Not PORTB.0 'LED ein bzw. aus Return |
Die Timer0-Register
Zum Schluß wieder ein kleiner Blick hinter die Kulisse ins Register-Geschehen. Nehmen Sie das bitte nicht nur als Nebensächlichkeit oder totes Hintergrundwissen, denn der direkte Zugriff in die Register kann in vielen praktischen Fällen Code und Rechenzeit sparen.
Zunächst wäre da das Register TCCR0 (Timer/Counter0 Control Register). Der Befehl Config TIMER0 macht nichts anderes, als dieses Register zu setzen. Auch die noch gar nicht erwähnten Bascom-Befehle Stop TIMER0 und Start TIMER0 verändern lediglich das Register TCCR0. Die einzelnen Möglichkeiten sind in den folgenden Befehlszeilen aufgeführt:
TCCR0 = &B00000000 'Timer0 anhalten TCCR0 = &B00000001 'Timer0=Clock TCCR0 = &B00000010 'Timer0=Clock/8 TCCR0 = &B00000011 'Timer0=Clock/64 TCCR0 = &B00000100 'Timer0=Clock/256 TCCR0 = &B00000101 'Timer0=Clock/1024 TCCR0 = &B00000110 'Takt an Pin T0, fallende Flanke TCCR0 = &B00000111 'Takt an Pin T0, steigende Flanke |
Das Register TIMSK (Timer/Counter Interrupt Mask Register) enthält für Timer0 das Flag TOIE0 (Timer0 Overflow Interrupt Enable, Bit 1), das den Timer0-Überlauf-Interrupt einschaltet, wenn man es auf 1 setzt. Der Bascom-Befehl Enable TIMER0 macht nichts anderes, als dieses Bit zu setzen. Darüber hinaus enthält das Register aber auch zwei Flags für Timer1: das Überlauf-Flag und das Flag für den (beim Timer0 nicht vorhandenen) Vergleichs-Interrupt.
Das Register TIFR (Timer/Counter Interrupt Flag Register) schließlich enthält die Interrupt-Flags für beide Timer. Sie werden gesetzt, sobald die Bedingung für den betreffenden Timer-Interrupt erfüllt ist. Für Timer0 gibt es nur das TOV0-Flag (Bit 1), welches gesetzt wird, wenn ein Timer-Überlauf stattgefunden hat. Der AVR löscht dieses Bit beim Ausführen der Interrupt-Routine automatisch. Falls man sich beispielsweise noch in der Ausführung einer anderen Interrupt-Routine befindet, kann man das TOV0-Bit abfragen und vielleicht effizienter reagieren. Will man das TOV0-Bit softwareseitig löschen (um die Ausführung des anstehenden Interrupts zu unterbinden), dann muß man TIFR.TOV0 = 1 schreiben (1, nicht 0). Ich habe übrigens keine Ahnung, warum die Interrupt-Flags mit einer 1 auf 0 gesetzt werden. Und ich habe auch überhaupt den Eindruck, daß niemand den tieferen Grund kennt. So ist das manchmal mit der Logik.
Im nächsten Teil werden wir den Timer1 behandeln. Bis dahin haben Sie, denke ich, mehr als genug zu tun ;-)
(Wird fortgesetzt)