Pfad:
Home =>
AVR-Überblick =>
Programmiertechniken => Projektplanung
(This page in English:
)
Programmiertechnik für Anfänger in AVR Assemblersprache
Hier wird erklärt, wie man ein einfaches Projekt plant, das in
Assembler programmiert werden soll. Weil die verwendeten Hardwarekomponenten
eines Prozessors sehr vieles vorweg bestimmen, was und wie die Hard- und
Software aufgebaut werden muss, zunächst die
Überlegungen zur Hardware. Dann folgt ein Kapitel
zur Entscheidung über den Einsatz von
Interrupts und schließlich Einiges über
Timing.
In die Entscheidung, welchen AVR-Typ man verwendet, gehen eine Vielzahl
an Anforderungen ein. Hier einige häufiger vorkommende Forderungen
zur Auswahl:
- Welche festen Portanschlüsse werden gebraucht? Feste
Portanschlüsse sind Ein- oder Ausgänge von internen
Komponenten, die an ganz bestimmten Anschlüssen liegen
müssen und nicht frei wählbar sind. Sie werden zuerst
zugeordnet. Komponenten und Anschlüsse dieser Art sind:
- Soll der Prozessor in der Schaltung programmiert
werden können (ISP-Interface), dann werden die Anschlüsse
SCK, MOSI und MISO diesem Zweck fest zugeordnet. Bei entsprechender
Hardwaregestaltung können diese als Eingänge (SCK, MOSI)
oder als Ausgang doppelt verwendet werden (Entkopplung über
Widerstände oder Multiplexer empfohlen).
- Wird eine serielle Schnittstelle benötigt, sind RXD und TXD
dafür zu reservieren. Soll zusätzlich das
RTS/CTS-Hardware-Protokoll implementiert werden, sind zusätzlich
zwei weitere Portbits dafür zu reservieren, die aber frei
platziert werden können.
- Soll der Analogkomparator verwendet werden, dann sind AIN0 und
AIN1 dafür zu reservieren.
- Sollen externe Signale auf Flanken überwacht werden, dann
sind INT0 bzw. INT1 dafür zu reservieren.
- Sollen AD-Wandler verwendet werden, müssen entsprechend der
Anzahl benötigter Kanäle die Eingänge dafür
vorgesehen werden. Verfügt der AD-Wandler über die externen
Anschlüsse AVCC und AREF, sollten sie entsprechend extern
beschaltet werden.
- Sollen externe Impulse gezählt werden, sind dafür die
Timer-Input-Anschlüsse T0, T1 bzw. T2 zu reservieren. Soll die
Dauer externer Impulse exakt gemessen werden, ist dafür der
ICP-Eingang zu verwenden. Sollen Timer-Ausgangsimpulse mit
definierter Pulsdauer ausgegeben werden, sind die entsprechenden
OCx-Ausgänge dafür zu reservieren.
- Wird externer SRAM-Speicher benötigt, müssen alle
nötigen Address- und Datenports sowie ALE, RD und WR dafür
reserviert werden.
- Soll der Takt des Prozessors aus einem externen Oszillatorsignal
bezogen werden, ist XTAL1 dafür zu reservieren. Soll ein
externer Quarz den Takt bestimmen, sind die Anschlüsse XTAL1
und XTAL2 dafür zu verwenden.
- Welche zusammenhängenden Portanschlüsse werden gebraucht?
Zusammenhängende Portanschlüsse sind solche, bei denen zwei
oder mehr Bits in einer bestimmten Reihenfolge angeordnet sein sollten,
um die Software zu vereinfachen.
- Erfordert die Ansteuerung eines externen Gerätes das
Schreiben oder Lesen von mehr als einem Bit gleichzeitig, z.B.
eine vier- oder achtbittige LCD-Anzeige, sollten die
nötigen Portbits in dieser Reihenfolge platziert werden.
Ist das z.B. bei achtbittigen Interfaces nicht möglich,
können auch zwei vierbittige Interfaces vorgesehen werden.
Die Software wird vereinfacht, wenn diese 4-Bit-Interfaces links-
bzw. rechtsbündig im Port angeordnet sind.
- Werden zwei oder mehr ADC-Kanäle benötigt,
sollten diese in einer direkten Abfolge (z.B. ADC2/ADC3/ADC4)
platziert werden, um die Ansteuerungssoftware zu vereinfachen.
- Welche frei platzierbaren Portbits werden noch gebraucht? Jetzt wird
alles zugeordnet, was keinen bestimmten Platz braucht.
- Wenn es jetzt wegen eines einzigen Portbits eng wird, kann der
RESET-Pin bei einigen Typen als Eingang verwendet werden, indem die
entsprechende Fuse gesetzt wird. Da der Chip anschließend nur
noch über Hochvolt-Programmierung zugänglich ist, ist dies
für fertig getestete Software akzeptabel. Für Prototypen
in der Testphase ist für ISP ein Hochvolt-Programmier-Interface
am umdefinierten RESET-Pin und eine Schutzschaltung aus Widerstand und
Zenerdiode gegenüber der Signalquelle vonnöten (beim
HV-Programmieren treten +12 Volt am RESET-Pin auf).
In die Entscheidung, welcher Prozessortyp für die Aufgabe geeignet
ist, gehen ferner noch ein:
- Wieviele und welche Timer werden benötigt?
- Welche Werte sind beim Abschalten des Prozessors zu erhalten
(EEPROM dafür vorsehen)?
- Wieviel Speicher wird im laufenden Betrieb benötigt
(entsprechend SRAM dafür vorsehen)?
- Platzbedarf? Bei manchen Projekten mag der Platzbedarf des
Prozessors ein wichtiges Entscheidungskriterium sein.
- Strombedarf? Bei Batterie-/Akku-betriebenen Projekten sollte der
Strombedarf ein wichtiges Auswahlkriterium sein.
- Preis? Spielt nur bei Großprojekten eine wichtige Rolle.
Ansonsten sind Preisrelationen recht kurzlebig und nicht unbedingt
von der Prozessorausstattung abhängig.
- Verfügbarkeit? Wer heute noch ein Projekt mit dem AT90S1200
startet, hat ihn vielleicht als Restposten aus der Grabbelecke billig
gekriegt. Nachhaltig ist so eine Entscheidung nicht. Es macht
wesentlich mehr Mühe, so ein Projekt auf einen Tiny- oder
Mega-Typen zu portieren als der Preisvorteil heute wert ist. Das
"Portieren" endet daher meist mit einer kompletten Neuentwicklung,
die dann auch schöner aussieht, besser funktioniert und mit
einem Bruchteil des Codes auskommt.
Einfachste Projekte kommen ohne Interrupt aus. Wenn allerdings der Strombedarf
minimiert werden soll, dann auch dann nicht. Es ist daher bei fast allen
Projekten die Regel, dass eine Interruptsteuerung nötig ist. Und
die will sorgfältig geplant sein.
Grundanforderungen des Interrupt-Betriebs
Falls es nicht mehr parat ist, hier ein paar Grundregeln:
- Interrupts ermöglichen:
- Interruptbetrieb erfordert einen eingerichteten SRAM-Stapel!
==> SPH:SPL sind zu Beginn auf RAMEND gesetzt, der obere
Teil des SRAM (je nach Komplexität und anderweitigem Stapeleinsatz
8 bis x Byte) bleibt dafür freigehalten!
- Jede Komponente (z.B. Timer) und jede Bedingung (z.B. ein Overflow),
die einen Interrupt auslösen soll, wird durch Setzen des
entsprechenden Interrupt-Enable-Bits in seinen Steuerregistern dazu
ermutigt, dies zu tun.
- Das I-Flag im Statusregister SREG wird zu Beginn gesetzt und bleibt
möglichst während des gesamten Betriebs gesetzt. Ist es
bei einer Operation nötig, Interrupts zu unterbinden, wird das
I-Flag kurz zurückgesetzt und binnen weniger Befehlsworte wieder
gesetzt.
- Interrupt-Service-Tabelle:
- Jeder Komponente und jeder gesetzten Interruptbedingung ist
eine Interrupt-Service-Routine zugeordnet, die an einer ganz
bestimmten Addresse im Flash-Speicher beginnt. Die Speicherstelle
erfordert an dieser Stelle einen Ein-Wort-Sprung in die eigentliche
Service-Routine (RJMP; bei sehr großen ATmega sind Zwei-Wort-Sprünge
- JMP - vorgesehen).
- Die ISR-Addressen sind prozessortyp-spezifisch angeordnet! Beim
Portieren zu einem anderen Prozessortyp sind diese Addressen
entsprechend anzupassen.
- Jede im Programm nicht verwendete ISR-Adresse wird mit einem
RETI abgeschlossen, damit versehentlich eingeschaltete Enable-Bits
von Komponenten definiert abgeschlossen sind und keinen Schaden
anrichten könen. Die Verwendung der .ORG-Direktive zum
Einstellen der ISR-Addresse ist KEIN definierter Abschluss!
- Beim Vorliegen der Interrupt-Bedingung wird in den Steuerregistern
der entsprechenden Komponente ein Flag gesetzt, das im Allgemeinen
nach dem Anspringen der Interrupt-Service-Tabelle automatisch wieder
gelöscht wird. In wenigen Ausnahmefällen kann es nötig
sein (z.B. beim TX-Buffer-Empty-Interrupt der SIO, wenn kein weiteres
Zeichen gesendet werden soll), den Interrupt-Enable abzuschalten und
das bereits erneut gesetzte Flag zu löschen.
- Bei gleichzeitig eintreffenden Interrupt-Service-Anforderungen
sind die ISR-Addressen nach Prioritäten geordnet: die ISR
mit der niedrigsten Addresse in der Tabelle wird bevorzugt
ausgeführt.
- Interrupt-Service-Routinen:
- Jede Service-Routine beginnt mit der Sicherung des Prozessor-
Statusregisters in einem für diesen Zweck reservierten Register
und endet mit der Wiederherstellung des Status. Da die Unterbrechung
zu jeder Zeit erfolgen kann, - also auch zu einer Zeit, in der der
Prozessor mit Routinen des Hauptprogramms beschäftigt ist,
die das Statusregister verwenden, - kann eine Störung dieses
Registers unvorhersehbare Folgen haben.
- Beim Ansprung der ISR wird die Rücksprungaddresse auf dem
Stapel abgelegt, der dadurch nach niedrigeren Addressen hin wächst.
Der Interrupt und das Anspringen einer Interrupt-Service-Tabelle
schaltet die Ausführung weiterer anstehender Interrupts zunächst
ab. Jede Interrupt-Service-Routine endet daher mit der
Instruktion RETI, die den Stapel wieder in Ordnung bringt und die Interrupts
wieder zulässt.
- Da jede Interrupt-Service-Routine anstehende weitere Interrupts
solange blockiert, wie sie selbst zu ihrer Ausführung benötigt,
hat jede Interrupt-Service-Routine so kurz wie nur irgend möglich
zu sein und sich auf die zeitkritischen Operationen zu beschränken.
- Da auch Interrupts mit höherer Priorität blockiert werden,
sollte bei zeitkritischen Operationen niedriger prioritäre Ints
besonders kurz sein.
- Da eine erneute Unterbrechung während der Verarbeitung einer
Service-Routine nicht vorkommen kann, können in den
verschiedenen ISR-Routinen die gleichen temporären Register
verwendet werden.
- Schnittstelle Interrupt-Routine und Hauptprogramm:
- Die Kommunikation zwischen der Interruptroutine und dem
Hauptprogramm erfolgt über einzelne Flaggen, die in der ISR
gesetzt und im Hauptprogramm wieder zurückgesetzt werden.
Zum Rücksetzen der Flaggen kommen ausschließlich
Ein-Wort-Instruktionen zum Einsatz oder Interrupts werden
vorübergehend blockiert, damit während des
Rücksetzvorgangs diese oder andere Flaggen im Register bzw.
im SRAM nicht fälschlich überschrieben werden.
- Werte aus der Service-Routine werden in dezidierten Registern
oder SRAM-Speicherzellen übergeben. Jede Änderung von
Register- oder SRAM-Werten innerhalb der Service-Routine, die
außerhalb des Interrupts weiterverarbeitet werden, ist
daraufhin zu prüfen, ob bei der Übergabe durch weitere
Interrupts Fehler möglich sind. Die Übergabe und
Weiterverabeitung von Ein-Byte-Werten ist unproblematisch, bei
Übergabe von zwei und mehr Bytes ist ein eindeutiger
Übergabemechanismus zwingend (Interrupt Disable beim
Kopieren der Daten in der Hauptprogramm-Schleife,
Flag-Setzen/-Auswerten/-Rücksetzen, o.ä.)! Als Beispiel
sei der Übergabemechanismus eines 16-Bit-Wertes von einem
Timer/Counter an die Auswerteroutine beschrieben. Der Timer
schreibt die beiden Bytes in zwei Register und setzt ein Flag
in einem anderen Register, dass Werte zur Weiterverarbeitung
bereitstehen. Im Hauptprogramm wird dieses Flag ausgewertet,
zurückgesetzt und die Übergabe des Wertes gestartet.
Wenn nun das erste Byte kopiert ist und erneut ein Interrupt
des Timers/Counters zuschlägt, gehören anschließend
Byte 1 und Byte 2 nicht zum gleichen Wertepaar. Das gilt es
durch definierte Übergabemechanismen zu verhindern!
- Die Hauptprogramm-Routinen:
- Im Hauptprogramm legt ein Loop den Prozessor schlafen,
wobei der Schlafmodus "Idle" eingestellt sein muss. Jeder
Interrupt weckt den Prozessor auf, verzweigt zur Service-Routine
und setzt nach deren Beendigung die Verarbeitung fort. Es
macht Sinn, nun die Flags darauf zu überprüfen,
ob eine oder mehrere der Service-Routinen Bedarf an
Weiterverarbeitung signalisiert hat. Wenn ja, wird entsprechend
dorthin verzweigt. Nachdem alle Wünsche der ISRs
erfüllt sind, wird der Prozessor wieder schlafen gelegt.
Grundaufbau im Interrupt-Betrieb
Aus dem Dargestellten ergibt sich die folgende Grundstruktur eines Interrupt-
getriebenen Programmes an einem Beispiel:
;
; Registerdefinitionen
;
.EQU rsreg = R15 ; Status-Sicherungs-Register bei Interrupts
.EQU rmp = R16 ; Temporäres Register auäerhalb von Interrupts
.EQU rimp = R17 ; Temporäres Register innerhalb von Interrupts
.EQU rflg = R18 ; Flaggenregister zur Kommunikation
.EQU bint0 = 0 ; Flaggenbit zur Signalisierung INT0-Service
.EQU btc0 = 1 ; Flaggenbit zur Signalisierung TC0-Overflow
; ...
; ISR-Tabelle
;
.CSEG
.ORG $0000
rjmp main ; Reset-Vektor, wird beim Start ausgeführt
rjmp isr_int0 ; INT0-Vektor, wird bei eine Pegeländerung am INT0-Eingang ausgefürt
reti ; nicht belegter Interrupt
reti ; nicht belegter Interrupt
rjmp isr_tc0_Overflow ; TC0-Overflow-Vektor, wird bei Überlauf TC0 ausgeführt
reti ; nicht belegter Interrupt
reti ; nicht belegter Interrupt
; ... gegebenenfalls weitere ISR
;
; Interrupt-Service-Routinen
;
isr_int0: ; INT0-Service Routine
in rsreg,SREG ; sichere Status
in rimp,PINB ; lese Port B in Temp Register
out PORTC,rimp ; schreibe Temp Register in Port C
; ... mache irgendwas weiteres
sbr rflg,1<<bint0 ; signalisiere INT0 nach außerhalb
out SREG,rsreg ; stelle Status wieder her
reti ; Sprung zurück und Int wieder zulassen
isr_tc0_Overflow: ; TC0 Overflow Service Routine
in rsreg,SREG ; sichere Status
in rimp,PINB ; lese Port B in Temp Register
out PORTC,rimp ; schreibe Temp Register in Port C
; ... mache irgendwas weiteres
sbr rflg,1<<btc0 ; setze TC0-Weiterbehandlungs-Flagge
out SREG,rsreg ; stelle Status wieder her
reti ; Sprung zurück und Int wieder zulassen
;
; Hauptprogramm-Start
;
main:
ldi rmp,HIGH(RAMEND) ; setze Stapelregister
out SPH,rmp
ldi rmp,LOW(RAMEND)
out SPL,rmp
; ... weiteres
; INT Enable bei TC0 Overflow
ldi rmp,1<<TOIE0 ; Overflow Interrupt Enable Timer 0
out TIMSK,rmp ; Interrupt-Maske der Timer setzen
ldi rmp,(1<<CS00)|(1<<CS02) ; Teiler durch 1024
out TCCR0,rmp ; Timer starten
; INT Enable beim INT0-Eingang
ldi rmp,(1<<SE)|(1<<ISC00) ; SLEEP-Enable und INT0 bei allen Flanken
out MCUCR,rmp ; an das Kontrollregister
ldi rmp,1<<INT0 ; INT0 ermöglichen
out GICR,rmp ; im Interrupt-Kontrollregister
; Interrupt Status Flag setzen
sei ; setze Interrupts an
;
; Hauptprogramm-Loop
;
loop:
sleep ; schlafen legen
nop ; Dummy nach dem Aufwachen
sbrc rflg,bint0 ; keine INT0-Anforderung
rcall mache_int0 ; behandle INT0-Ergebnis
sbrc rflg,btc0 ; keine TC0-Overflow-Behandlung
rcall mache_tc0 ; behandle TC0-Overflow
rjmp loop ; lege dich wieder schlafen
;
; Behandlung der Anforderungen
;
mache_int0: ; Behandle INT0-Ergebnis
cbr rflg,1<<bint0 ; Setze INT0-Flagge zurück
; ... mache weiteres
ret ; fertig, zurück zum Loop
mache_tc0: ; Behandle TC0-Overflow
cbr rflg,1<<btc0 ; Setze TC0-Flage wieder zurück
; ... mache weiteres
ret ; fertig, zurück zum Loop
Geht ein Projekt darüber hinaus, ein Portbit abzufragen und
daraus abgeleitet irgendwas anderes zu veranlassen, dann sind
Überlegungen zum Timing des Projektes zwingend. Timing
- beginnt mit der Wahl der Taktfrequenz des Prozessors,
- geht weiter mit der Frage, was wie häufig und mit welcher
Präzision vom Prozesser erledigt werden muss,
- über die Frage, welche Timing-Steuerungsmöglichkeiten
bestehen, bis zu
- wie diese kombiniert werden können.
Wahl der Taktfrequenz des Prozessors
Die oberste Grundfrage ist die nach der nötigen Präzision
des Taktgebers.
Reicht es in der Anwendung, wenn die Zeiten im Prozentbereich
ungenau sind, dann ist der interne RC-Oszillator in vielen AVR-Typen
völlig ausreichend. Bei den neueren ATtiny und ATmega hat sich
die selbsttätige Oszillator-Kalibration eingebürgert,
so dass die Abweichungen vom Nominalwert des RC-Oszillators nicht
mehr so arg ins Gewicht fallen. Wenn allerdings die Betriebsspannung
stark schwankt, kann der Fehler zu groß sein.
Wem der interne RC-Takt zu langsam oder zu schnell ist, kann
bei einigen neueren Typen (z.B. dem ATtiny13) einen Vorteiler
bemühen. Der Vorteiler geht beim Anlegen der Betriebsspannung
auf einen voreingestellten Wert (z.B. auf 8) und teilt den
internen RC-Oszillator entsprechend vor. Entweder per Fuse-Einstellung
oder auch per Software-Einstellung kann der Vorteiler auf niedrigere
Teilerverhältnisse (höhere Taktfrequenz) oder auf einen
höheren Wert umgestellt werden (z.B. 128), um durch niedrigere
Taktfrequenz z.B. verstärkt Strom zu sparen. Obacht bei V-Typen!
Wer dem Prozessor einen zu hohen Takt zumutet, hat es nicht anders
verdient als dass der Prozessor nicht tut, was er soll.
Wem der interne RC-Oszillator zu ungenau ist, kann per Fuse
eine externe RC-Kombination, einen externen Oszillator, einen Quarz
oder einen Keramikschwinger auswählen. Gegen falsch gesetzte
Fuses ist kein Kraut gewachsen, es muss dann schon ein Board mit
eigenem Oszillator sein, um doch noch was zu retten.
Die Höhe der Taktfrequenz sollte der Aufgabe angemessen sein.
Dazu kann grob die Wiederholfrequenz der Tätigkeiten dienen. Wenn
eine Taste z.B. alle 2 ms abgefragt werden soll und nach
20 Mal als entprellt ausgeführt werden soll, dann ist
bei einer Taktfrequenz von 1 MHz für 2000 Takte
zwischen zwei Abfragen Platz, 20.000 Takte für die
wiederholte Ausführung des Tastenbefehls. Weit ausreichend
für eine gemütliche Abfrage und Ausführung.
Eng wird es, wenn eine Pulsweitenmodulation mit hoher Auflösung
und hoher PWM-Taktfrequenz erreicht werden soll. Bei einer
PWM-Taktfrequenz von 10 kHz und 8 Bit Auflösung sind
2,56 MHz schon zu langsam für eine software-gesteuerte
Lösung. Wenn das ein Timer mit Interrupt-Steuerung
übernimmt, um so besser.
Was ist wann und wie zu tun?
Mit welchen Methoden lässt sich das machen?
©2006 by http://www.avr-asm-tutorial.net