|
|
AVR-GCC-Tutorial[bearbeiten] VorwortDieses Tutorial soll den Einstieg in die Programmierung von Atmel AVR-Mikrocontrollern in der Programmiersprache C mit dem freien C-Compiler AVR-GCC aus der GNU Compiler Collection erleichtern. Vorausgesetzt werden Grundkenntnisse der Progammiersprache C. Diese Kenntnisse kann man sich online erarbeiten, z. B. mit dem C Tutorial von Helmut Schellong. Nicht erforderlich sind Vorkenntnisse in der Programmierung von Mikrocontrollern, weder in Assembler noch in einer anderen Sprache. In diesem Text wird häufig auf die Standardbibliothek avr-libc verwiesen, für die es eine Online-Dokumentation gibt, in der sich auch viele nützliche Informationen zum Compiler und zur Programmierung von AVR Controllern finden. Bei WinAVR gehört die avr-libc Dokumentation zum Lieferumfang und wird mitinstalliert. Der Compiler und die Standardbibliothek avr-libc werden stetig weiterentwickelt. Erläuterungen und Beispiele beziehen sich auf den C-Compiler avr-gcc ab Version 3.4 und die avr-libc ab Version 1.4.3. Die Unterschiede zu älteren Versionen werden im Haupttext und Anhang zwar erläutert, Anfängern sei jedoch empfohlen, die aktuellen Versionen zu nutzen (für MS-Windows: aktuelle Version des WinAVR-Pakets; für Linux gibt es CDK4AVR: http://cdk4avr.sf.net oder auch fertige Pakete bei verschiedenen Distributionen.). Das ursprüngliche Tutorial stammt von Christian Schifferle, viele neue Abschnitte und aktuelle Anpassungen von Martin Thomas. Dieses Tutorial ist in PDF-Form erhältlich bei: http://www.siwawi.arubi.uni-kl.de/avr_projects/AVR-GCC-Tutorial_-_www_mikrocontroller_net.pdf (nicht immer auf aktuellem Stand) [bearbeiten] Benötigte WerkzeugeUm eigene Programme für AVRs mittels avr-gcc/avr-libc zu erstellen und zu testen, wird folgende Hard- und Software benötigt:
[bearbeiten] Was tun, wenn's nicht "klappt"?
[bearbeiten] Erzeugen von MaschinencodeAus dem C-Quellcode erzeugt der avr-gcc Compiler (zusammen mit Hilfsprogrammen wie z. B. Präprozessor, Assembler und Linker) Maschinencode für den AVR-Controller. Üblicherweise liegt dieser Code dann im Intel Hex-Format vor ("Hex-Datei"). Die Programmiersoftware (z. B. AVRDUDE, PonyProg oder AVRStudio/STK500-plugin) liest diese Datei ein und überträgt die enthaltene Information (den Maschinencode) in den Speicher des Controllers. Im Prinzip sind also "nur" der avr-gcc-Compiler (und wenige Hilfsprogramme) mit den "richtigen" Optionen aufzurufen, um aus C-Code eine "Hex-Datei" zu erzeugen. Grundsätzlich stehen dazu zwei verschiedene Ansätze zur Verfügung:
Beim Wechsel vom makefile-Ansatz nach WinAVR-Vorlage zu AVRStudio ist darauf zu achten, dass AVRStudio (Stand: AVRStudio Version 4.13) bei einem neuen Projekt die Optimierungsoption (vgl. Abschnitt Exkurs: Makefiles, typisch: -Os) nicht einstellt und die mathematische Bibliothek der avr-libc (libm.a, Linker-Option -lm) nicht einbindet. (Hinweis: Bei Version 4.16 wird beides bereits gesetzt). Beides ist Standard bei Verwendung von makefiles nach WinAVR-Vorlage und sollte daher auch im Konfigurationsdialog des avr-gcc-Plugins von AVRStudio "manuell" eingestellt werden, um auch mit AVRStudio kompakten Code zu erzeugen. [bearbeiten] EinführungsbeispielZum Einstieg ein kleines Beispiel, an dem die Nutzung des Compilers und der Hilfsprogramme (der sogenannten Toolchain) demonstriert wird. Detaillierte Erläuterungen folgen in den weiteren Abschnitten dieses Tutorials. Das Programm soll auf einem AVR Mikrocontroller einige Ausgänge ein- und andere ausschalten. Das Beispiel ist für einen ATmega16 programmiert (Datenblatt), kann aber sinngemäß für andere Controller der AVR-Familie modifiziert werden. Zunächst der Quellcode der Anwendung, der in einer Text-Datei mit dem Namen main.c abgespeichert wird.
Um diesen Quellcode in ein auf dem Controller lauffähiges Programm zu übersetzen, wird hier ein Makefile genutzt. Das verwendete Makefile findet sich auf der Seite Beispiel Makefile und basiert auf der Vorlage, die in WinAVR mitgeliefert wird und wurde bereits angepasst (Controllertyp ATmega16). Man kann das Makefile bearbeiten und an andere Controller anpassen oder sich mit dem Programm MFile menügesteuert ein Makefile "zusammenklicken". Das Makefile speichert man unter dem Namen Makefile (ohne Endung) im selben Verzeichnis, in dem auch die Datei main.c mit dem Programmcode abgelegt ist. Detailliertere Erklärungen zur Funktion von Makefiles finden sich im folgenden Abschnitt Exkurs: Makefiles.
D:\tmp\gcc_tut\quickstart>dir
Verzeichnis von D:\tmp\gcc_tut\quickstart
28.11.2006 22:53 <DIR> .
28.11.2006 22:53 <DIR> ..
28.11.2006 20:06 118 main.c
28.11.2006 20:03 16.810 Makefile
2 Datei(en) 16.928 Bytes
Nun gibt man make all ein. Falls das mit WinAVR installierte Programmers Notepad genutzt wird, gibt es dazu einen Menüpunkt im Tools Menü. Sind alle Einstellungen korrekt, entsteht eine Datei main.hex, in der der Code für den AVR enthalten ist. D:\tmp\gcc_tut\quickstart>make all -------- begin -------- avr-gcc (GCC) 3.4.6 Copyright (C) 2006 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Compiling C: main.c avr-gcc -c -mmcu=atmega16 -I. -gdwarf-2 -DF_CPU=1000000UL -Os -funsigned-char -f unsigned-bitfields -fpack-struct -fshort-enums -Wall -Wstrict-prototypes -Wundef -Wa,-adhlns=obj/main.lst -std=gnu99 -Wundef -MD -MP -MF .dep/main.o.d main.c - o obj/main.o Linking: main.elf avr-gcc -mmcu=atmega16 -I. -gdwarf-2 -DF_CPU=1000000UL -Os -funsigned-char -funs igned-bitfields -fpack-struct -fshort-enums -Wall -Wstrict-prototypes -Wundef -W a,-adhlns=obj/main.o -std=gnu99 -Wundef -MD -MP -MF .dep/main.elf.d obj/main.o --output main.elf -Wl,-Map=main.map,--cref -lm Creating load file for Flash: main.hex avr-objcopy -O ihex -R .eeprom main.elf main.hex [...] Der Inhalt der hex-Datei kann nun zum Controller übertragen werden. Dies kann z. B. über In-System-Programming (ISP) erfolgen, das im AVR-Tutorial: Equipment beschrieben ist. Makefiles nach der WinAVR/MFile-Vorlage sind für die Nutzung des Programms AVRDUDE vorbereitet. Wenn man den Typ und Anschluss des Programmiergerätes richtig eingestellt hat, kann mit make program die Übertragung mittels AVRDUDE gestartet werden. Jede andere Software, die hex-Dateien lesen und zu einem AVR übertragen kann (z. B. Ponyprog, yapp, AVRStudio), kann natürlich ebenfalls genutzt werden. Startet man nun den Controller (Reset-Taster oder Stromzufuhr aus/an), werden vom Programm die Anschlüsse PB0 und PB1 auf 1 gesetzt. Man kann mit einem Messgerät nun an diesem Anschluss die Betriebsspannung messen oder eine LED leuchten lassen (Anode an den Pin, Vorwiderstand nicht vergessen). An den Anschlüssen PB2-PB7 misst man 0 Volt. Eine mit der Anode mit einem dieser Anschlüsse verbundene LED leuchtet nicht. [bearbeiten] Exkurs: MakefilesWenn man bisher gewohnt ist, mit integrierten Entwicklungsumgebungen à la Visual-C Programme zu erstellen, wirkt das makefile-Konzept auf den ersten Blick etwas kryptisch. Nach kurzer Einarbeitung ist diese Vorgehensweise jedoch sehr praktisch. Diese Dateien (üblicher Name: 'Makefile' ohne Dateiendung) dienen der Ablaufsteuerung des Programms make, das auf allen Unix/Linux-Systemen installiert sein sollte, und in einer Fassung fuer MS-Windows auch in WinAVR (Unterverzeichnis utils/bin) enthalten ist. Im Unterverzeichnis sample einer WinAVR-Installation findet man eine sehr brauchbare Vorlage, die sich einfach an das eigene Projekt anpassen lässt (lokale Kopie Stand Sept. 2004). Wahlweise kann man auch mfile von Jörg Wunsch nutzen. mfile erzeugt ein makefile nach Einstellungen in einer grafischen Nutzeroberfläche, wird bei WinAVR mitinstalliert, ist aber als TCL/TK-Programm auf nahezu allen Plattformen lauffähig. Die folgenden Ausführungen beziehen sich auf das WinAVR Beispiel-Makefile. Ist im Makefile alles richtig eingestellt, genügt es, sich drei Parameter zu merken, die über die shell bzw. die Windows-Kommandozeile (cmd.exe/command.com) als Parameter an "make" übergeben werden. Das Programm make sucht sich "automatisch" das Makefile im aktuellen Arbeitsverzeichnis und führt die darin definierten Operationen für den entsprechenden Aufrufparameter durch.
Diese Aufrufe können in die allermeisten Editoren in "Tool-Menüs" eingebunden werden. Dies erspart den Kontakt mit der Kommandozeile. Bei WinAVR sind die Aufrufe bereits im Tools-Menü des mitgelieferten Editors Programmers-Notepad eingefügt. Üblicherweise sind folgende Daten im Makefile anzupassen:
Die in den folgenden Unterabschnitten gezeigten Makefile-Ausschnitte sind für ein Programm, das auf einem ATmega8 ausgeführt werden soll. Der Quellcode besteht aus den c-Dateien superprog.c (darin main()), uart.c, lcd.c und 1wire.c. Im Quellcodeverzeichnis befinden sich diese Dateien: superprog.c, uart.h, uart.c, lcd.h, lcd.c, 1wire.h, 1wire.c und das makefile (die angepasste Kopie des WinAVR-Beispiels). Der Controller wird mittels AVRDUDE über ein STK200-Programmierdongle an der Schnittstelle lpt1 (bzw. /dev/lp0) programmiert. Im Quellcode sind auch Daten für die section .eeprom definiert (siehe Abschnitt Speicherzugriffe), diese sollen beim Programmieren gleich mit ins EEPROM geschrieben werden. [bearbeiten] Controllertyp setzenDazu wird die "make-Variable" MCU entsprechend dem Namen des verwendeten Controllers gesetzt. Eine Liste der von avr-gcc und der avr-libc unterstützten Typen findet sich in der Dokumentation der avr-libc. # Kommentare in Makefiles beginnen mit einem Doppelkreuz ... # ATmega8 at work MCU = atmega8 # oder MCU = atmega16 # oder MCU = at90s8535 # oder ... ... [bearbeiten] Quellcode-Dateien eintragenDer Name der Quellcodedatei, welche die Funktion main enthält, wird hinter TARGET eingetragen. Dies jedoch ohne die Endung .c. ... TARGET = superprog ... Besteht das Projekt wie im Beispiel aus mehr als einer Quellcodedatei, sind die weiteren c-Dateien (nicht die Header-Dateien, vgl. Include-Files (C)) durch Leerzeichen getrennt bei SRC einzutragen. Die bei TARGET definierte Datei ist schon in der SRC-Liste enthalten. Diesen Eintrag nicht löschen! ... SRC = $(TARGET).c uart.c lcd.c 1wire.c ... Alternativ kann man die Liste der Quellcodedateien auch mit dem Operator += erweitern. SRC = $(TARGET).c uart.c 1wire.c # lcd-Code fuer Controller xyz123 (auskommentiert) # SRC += lcd_xyz.c # lcd-Code fuer "Standard-Controller" (genutzt) SRC += lcd.c [bearbeiten] Programmiergerät einstellenDie Vorlagen sind auf die Programmiersoftware AVRDUDE angepasst, jedoch lässt sich auch andere Programmiersoftware einbinden, sofern diese über Kommandozeile gesteuert werden kann (z. B. stk500.exe, uisp, sp12). ... # Einstellung fuer STK500 an com1 (auskommentiert) # AVRDUDE_PROGRAMMER = stk500 # com1 = serial port. Use lpt1 to connect to parallel port. # AVRDUDE_PORT = com1 # programmer connected to serial device # Einstellung fuer STK200-Dongle an lpt1 AVRDUDE_PROGRAMMER = stk200 AVRDUDE_PORT = lpt1 ... Sollen Flash(=.hex) und EEPROM(=.eep) zusammen auf den Controller programmiert werden, ist das Kommentarzeichen vor AVRDUDE_WRITE_EEPROM zu löschen. ... # auskommentiert: EERPOM-Inhalt wird nicht mitgeschrieben #AVRDUDE_WRITE_EEPROM = -U eeprom:w:$(TARGET).eep # nicht auskommentiert: EERPOM-Inhalt wird mitgeschrieben AVRDUDE_WRITE_EEPROM = -U eeprom:w:$(TARGET).eep ... [bearbeiten] AnwendungDas erstellte Makefile und der Code müssen im gleichen Ordner sein, auch sollte der Dateiname nicht verändert werden. Die Eingabe von make all im Arbeitsverzeichnis mit dem Makefile und den Quellcodedateien erzeugt (unter anderem) die Dateien superprog.hex und superprog.eep. Abhängigkeiten zwischen den einzelnen c-Dateien werden dabei automatisch berücksichtigt. Die superprog.hex und superprog.eep werden mit make program zum Controller übertragen. Mit make clean werden alle temporären Dateien gelöscht (="aufgeräumt"). [bearbeiten] Sonstige Einstellungen[bearbeiten] OptimierungsgradDer gcc-Compiler kennt verschiedene Stufen der Optimierung. Nur zu Testzwecken sollte die Optimierung ganz deaktiviert werden (OPT = 0). Die weiteren möglichen Optionen weisen den Compiler an, möglichst kompakten oder möglichst schnellen Code zu erzeugen. In den weitaus meisten Fällen ist OPT = s die empfohlene Einstellung, damit wird kompakter und oft auch der schnellste Maschinencode erzeugt. Beim Update auf eine neue Compilerversion ist zu beachten, dass diese möglicherweise intern andere Optimierungsalgorithmen verwendet und sich dadurch die Größe des Machinencodes etwas ändert, ohne dass man im Quellcode etwas geändert hat. Als Orientierungswerte die Größe des Maschinencodes bei verschiedenen Optionen für einen nicht näher spezifizierten relativ kleinen Testcode bei Verwendung einer nicht näher spezifizierten Compilerversion.
Im diesem Testfall führt die Option -O2 mit zum kompaktesten Code, dies allerdings hier nur mit 25 Bytes "Vorsprung". Es kann durchaus sein, dass nur wenige Programmerweiterungen dazu führen, dass Compilieren mit -Os wieder in kompakteren Code resultiert. Siehe dazu auch:
[bearbeiten] Debug-FormatUnterstützt werden die Formate stabs und dwarf-2. Das Format wird hinter DEBUG = eingestellt. Siehe dazu Abschnitt Eingabedateien zur Simulation. [bearbeiten] Assembler-DateienDie im Projekt genutzten Assembler-Dateien werden hinter ASRC durch Leerzeichen getrennt aufgelistet. Assembler-Dateien haben immer die Endung .S (großes S). Ist zum Beispiel der Assembler-Quellcode eines Software-UARTs in einer Datei softuart.S enthalten, lautet die Zeile: ASRC = softuart.S [bearbeiten] TaktfrequenzNeuere Versionen der WinAVR/Mfile Vorlage für Makefiles beinhalten die Definition einer Variablen F_CPU (WinAVR 2/2005). Darin wird die Taktfrequenz des Controllers in Hertz eingetragen. Die Definition steht dann im gesamten Projekt ebenfalls unter der Bezeichnung F_CPU zur Verfügung (z. B. um daraus UART-, SPI- oder ADC-Frequenzeinstellungen abzuleiten). Die Angabe hat rein "informativen" Charakter, die tatsächliche Taktrate wird über den externen Takt (z. B. Quarz) bzw. die Einstellung des internen R/C-Oszillators bestimmt. Die Nutzung von F_CPU hat also nur Sinn, wenn die Angabe mit dem tatsächlichen Takt übereinstimmt. Innerhalb neuerer Versionen der avr-libc (ab Version 1.2) wird die Definition der Taktfrequenz (F_CPU) zur Berechnung der Wartefunktionen in delay.h genutzt. Diese funktionieren nur dann korrekt, wenn F_CPU mit der tatsächlichen Taktfrequenz übereinstimmt. F_CPU muss dazu jedoch nicht unbedingt im makefile definiert werden. Es reicht aus, wird aber bei mehrfacher Anwendung unübersichtlich, vor #include <util/delay.h> (veraltet: #include <avr/delay.h>) ein #define F_CPU [hier Takt in Hz]UL einzufügen. Bei Nutzung von delay.h ist darauf zu achten, dass die Optimierung des Compilers nicht ausgeschaltet ist, sonst wird sehr viel Code erzeugt und die Wartezeit stimmt nicht mit der gewünschten Zeitspanne überein. Außerdem sollte der delay-Funktion kein zur Laufzeit berechneter Wert übergeben werden. Vgl. dazu den entsprechenden Abschnitt der Dokumentation. [bearbeiten] Eingabedateien zur Simulation in AVR-StudioMit älteren AVR-Studio-Versionen kann man nur auf Grundlage so genannter coff-Dateien simulieren. Neuere Versionen von AVR-Studio (ab 4.10.356) unterstützen zudem das modernere aber noch experimentelle dwarf-2-Format, das ab WinAVR 20040722 (avr-gcc 3.4.1/Binutils inkl. Atmel add-ons) "direkt" vom Compiler erzeugt wird.
Statt des Software-Simulators kann das AVR-Studio auch genutzt werden, um mit dem ATMEL JTAGICE, einem Nachbau davon (BootICE, Evertool o. ä.) oder dem ATMEL JTAGICE MKII "im System" zu debuggen. Dazu sind keine speziellen Einstellungen im makefile erforderlich. Debugging bzw. "In-System-Emulation" mit dem JTAGICE und JTAGICE MKII sind in der AVR-Studio Online-Hilfe beschrieben. Die Verwendung von Makefiles bietet noch viele weitere Möglichkeiten, einige davon werden im Anhang Zusätzliche Funktionen im Makefile erläutert. [bearbeiten] Ganzzahlige (Integer) DatentypenBei der Programmierung von Mikrokontrollern ist die Definition einiger ganzzahliger Datentypen sinnvoll, an denen eindeutig die Bit-Länge abgelesen werden kann. Standardisierte Datentypen werden in der Header-Datei stdint.h definiert. Zur Nutzung der standardisierten Typen bindet man die "Definitionsdatei" wie folgt ein:
Einige der dort definierten Typen (avr-libc Version 1.0.4):
Die Typen ohne vorangestelltes u werden als vorzeichenbehaftete Zahlen abgespeichert. Typen mit vorgestelltem u dienen der Ablage von postiven Zahlen (inkl. 0). Siehe dazu auch: Dokumentation der avr-libc Abschnitt Modules/(Standard) Integer Types. [bearbeiten] BitfelderBeim Programmieren von Mikrocontrollern muss auf jedes Byte oder sogar auf jedes Bit geachtet werden. Oft müssen wir in einer Variablen lediglich den Zustand 0 oder 1 speichern. Wenn wir nun zur Speicherung eines einzelnen Wertes den kleinsten bekannten Datentypen, nämlich unsigned char, nehmen, dann verschwenden wir 7 Bits, da ein unsigned char ja 8 Bits breit ist. Hier bietet uns die Programmiersprache C ein mächtiges Werkzeug an, mit dessen Hilfe wir 8 Bits in eine einzelne Bytevariable zusammenfassen und (fast) wie 8 einzelne Variablen ansprechen können. Die Rede ist von so genannten Bitfeldern. Diese werden als Strukturelemente definiert. Sehen wir uns dazu doch am besten gleich ein Beispiel an:
Der Zugriff auf ein solches Feld erfolgt nun wie beim Strukturzugriff bekannt über den Punkt- oder den Dereferenzierungs-Operator:
Bitfelder sparen Platz im RAM, zu Lasten von Platz im Flash, verschlechtern aber unter Umständen die Les- und Wartbarkeit des Codes. Anfängern wird deshalb geraten, ein "ganzes" Byte (uint8_t) zu nutzen, auch wenn nur ein Bitwert gespeichert werden soll. [bearbeiten] Grundsätzlicher Programmaufbau eines µC-ProgrammsWir unterscheiden zwischen 2 verschiedenen Methoden, um ein Mikrocontroller-Programm zu schreiben, und zwar völlig unabhängig davon, in welcher Programmiersprache das Programm geschrieben wird. [bearbeiten] Sequentieller ProgrammablaufBei dieser Programmiertechnik wird eine Endlosschleife programmiert, welche im Wesentlichen immer den gleichen Aufbau hat: [bearbeiten] Interruptgesteuerter ProgrammablaufBei dieser Methode werden beim Programmstart zuerst die gewünschten Interruptquellen aktiviert und dann in eine Endlosschleife gegangen, in welcher Dinge erledigt werden können, welche nicht zeitkritisch sind. Wenn ein Interrupt ausgelöst wird, so wird automatisch die zugeordnete Interruptfunktion ausgeführt. [bearbeiten] Zugriff auf RegisterDie AVR-Controller verfügen über eine Vielzahl von Registern. Die meisten davon sind sogenannte Schreib-/Leseregister. Das heißt, das Programm kann die Inhalte der Register sowohl auslesen als auch beschreiben. Register haben einen besonderen Stellenwert bei den AVR Controllern. Sie dienen dem Zugriff auf die Ports und die Schnittstellen des Controllers. Wir unterscheiden zwischen 8-Bit und 16-Bit Registern. Vorerst behandeln wir mal die 8-Bit Register. Einzelne Register sind bei allen AVRs vorhanden, andere wiederum nur bei bestimmten Typen. So sind beispielsweise die Register, welche für den Zugriff auf den UART notwendig sind, selbstverständlich nur bei denjenigen Modellen vorhanden, welche über einen integrierten Hardware UART bzw. USART verfügen. Die Namen der Register sind in den Headerdateien zu den entsprechenden AVR-Typen definiert. Dazu muss man den Namen der controllerspezifischen Headerdatei nicht kennen. Es reicht aus, die allgemeine Headerdatei avr/io.h einzubinden:
Ist im Makefile der MCU-Typ z. B. mit dem Inhalt atmega8 definiert (und wird somit per -mmcu=atmega8 an den Compiler übergeben), wird beim Einlesen der io.h-Datei implizit ("automatisch") auch die iom8.h-Datei mit den Register-Definitionen für den ATmega8 eingelesen. Intern wird diese "Automatik" wie folgt realisiert: Der Controllertyp wird dem Compiler als Parameter übergeben (vgl. avr-gcc -c -mmcu=atmega16 [...] im Einführungsbeispiel). Wird ein Makefile nach der WinAVR/mfile-Vorlage verwendet, setzt man die Variable MCU, der Inhalt dieser Variable wird dann an passender Stelle für die Compilerparameter verwendet. Der Compiler definiert intern eine dem mmcu-Parameter zugeordnete "Variable" (genauer: ein Makro) mit dem Namen des Controllers, vorangestelltem __AVR_ und angehängten Unterstrichen (z.B. wird bei -mmcu=atmega16 das Makro __AVR_ATmega16__ definiert). Beim Einbinden der Header-Datei avr/io.h wird geprüft, ob das jeweilige Makro definiert ist und die zum Controller passende Definitionsdatei eingelesen. Zur Veranschaulichung einige Ausschnitte aus einem Makefile:
[...]
# MCU Type ("name") setzen:
MCU = atmega16
[...]
[...]
## Verwendung des Inhalts von MCU (hier atmega16) fuer die
## Compiler- und Assembler-Parameter
ALL_CFLAGS = -mmcu=$(MCU) -I. $(CFLAGS) $(GENDEPFLAGS)
ALL_CPPFLAGS = -mmcu=$(MCU) -I. -x c++ $(CPPFLAGS) $(GENDEPFLAGS)
ALL_ASFLAGS = -mmcu=$(MCU) -I. -x assembler-with-cpp $(ASFLAGS)
[...]
[...]
## Aufruf des Compilers:
## mit den Parametern ($(ALL_CFLAGS) ist -mmcu=$(MCU)[...] = -mmcu=atmega16[...]
$(OBJDIR)/%.o : %.c
@echo
@echo $(MSG_COMPILING) $<
$(CC) -c $(ALL_CFLAGS) $< -o $@
[...]
Da --mmcu=atmega16 übergeben wurde, wird __AVR_ATmega16__ definiert und kann in avr/io.h zur Fallunterscheidung genutzt werden:
Die Beispiele in den folgenden Abschnitten demonstrieren den Zugriff auf Register anhand der Register für I/O-Ports (PORTx, DDRx, PINx), die Vorgehensweise ist jedoch für alle Register (z.B. die des UART, ADC, SPI) analog. [bearbeiten] Schreiben in RegisterZum Schreiben kann man Register einfach wie eine Variable setzen. In Quellcodes, die für ältere Versionen des avr-gcc/der avr-libc entwickelt wurden, erfolgt der Schreibzugriff über die Funktion outp(). Aktuelle Versionen des Compilers unterstützen den Zugriff nun direkt und outp() ist nicht mehr erforderlich. Beispiel:
Die ausführliche Schreibweise sollte bevorzugt verwendet werden, da dadurch die Zuweisungen selbsterklärend sind und somit der Code leichter nachvollzogen werden kann. Atmel verwendet sie auch bei Beispielen in Datenblätten und in den allermeisten Quellcodes zu Application-Notes. Der gcc C-Compiler (genauer der Präprozessor) unterstützt ab Version 4.3.0 Konstanten im Binärformat, z.B. DDRB = 0b00011111 (für WinAVR wurden schon ältere Versionen des gcc entsprechend angepasst). Diese Schreibweise ist jedoch nicht standardkonform und man sollte sie daher insbesondere dann nicht verwenden, wenn Code mit anderen ausgetauscht oder mit anderen Compilern bzw. älteren Versionen des gcc genutzt werden soll. [bearbeiten] Verändern von RegisterinhaltenEinzelne Bits setzt und löscht man "Standard-C-konform" mittels logischer (Bit-) Operationen.
Es wird jeweils nur der Zustand des angegebenen Bits geändert, der vorherige Zustand der anderen Bits bleibt erhalten. Beispiel:
Mit dieser Methode lassen sich auch mehrere Bits eines Registers gleichzeitig setzen und löschen. Beispiel:
In Quellcodes, die für ältere Version den des avr-gcc/der avr-libc entwickelt wurden, werden einzelne Bits mittels der Funktionen sbi und cbi gesetzt bzw. gelöscht. Beide Funktionen sind nicht mehr erforderlich. Siehe auch:
[bearbeiten] Lesen aus RegisternZum Lesen kann man auf Register einfach wie auf eine Variable zugreifen. In Quellcodes, die für ältere Versionen des avr-gcc/der avr-libc entwickelt wurden, erfolgt der Lesezugriff über die Funktion inp(). Aktuelle Versionen des Compilers unterstützen den Zugriff nun direkt und inp() ist nicht mehr erforderlich. Beispiel:
Die Abfrage der Zustände von Bits erfolgt durch Einlesen des gesamten Registerinhalts und ausblenden der Bits deren Zustand nicht von Interesse ist. Einige Beispiele zum Prüfen ob Bits gesetzt oder gelöscht sind:
Die AVR-Bibliothek (avr-libc) stellt auch Funktionen (Makros) zur Abfrage eines einzelnen Bits eines Registers zur Verfügung, diese sind bei anderen Compilern meist nicht verfügbar (können aber dann einfach durch Makros "nachgerüstet" werden).
Die Funktionen (eigentlich Makros) bit_is_clear bzw. bit_is_set sind nicht erforderlich, man kann und sollte C-Syntax verwenden, die universell verwendbar und portabel ist. Siehe auch Bitmanipulation. [bearbeiten] Warten auf einen bestimmten ZustandEs gibt in der Bibliothek avr-libc Funktionen, die warten, bis ein bestimmter Zustand eines Bits erreicht ist. Es ist allerdings normalerweise eine eher unschöne Programmiertechnik, da in diesen Funktionen "blockierend" gewartet wird. Der Programmablauf bleibt also an dieser Stelle stehen, bis das maskierte Ereignis erfolgt ist. Setzt man den Watchdog ein, muss man darauf achten, dass dieser auch noch getriggert wird (Zurücksetzen des Watchdogtimers). Die Funktion loop_until_bit_is_set wartet in einer Schleife, bis das definierte Bit gesetzt ist. Wenn das Bit beim Aufruf der Funktion bereits gesetzt ist, wird die Funktion sofort wieder verlassen. Das niederwertigste Bit hat die Bitnummer 0.
Universeller und auch auf andere Plattformen besser übertragbar ist die Verwendung von C-Standardoperationen. Siehe auch:
[bearbeiten] 16-Bit Register (ADC, ICR1, OCR1, TCNT1, UBRR)Einige der Portregister in den AVR-Controllern sind 16 Bit breit. Im Datenblatt sind diese Register üblicherweise mit dem Suffix "L" (LSB) und "H" (MSB) versehen. Die avr-libc definiert zusätzlich die meisten dieser Variablen die Bezeichnung ohne "L" oder "H". Auf diese kann direkt zugewiesen bzw. zugegriffen werden. Die Konvertierung von 16-bit Wort nach 2*8-bit Byte erfolgt intern.
Falls benötigt, kann eine 16-Bit Variable auch recht einfach manuell in ihre zwei 8-Bit Bestandteile zerlegt werden. Folgendes Beispiel demonstriert dies anhand des pseudo- 16-Bit Registers UBRR.
Bei einigen AVR-Typen (z.B. ATmega8, ATMega16) teilen sich UBRRH und UCSRC die gleiche Memory-Adresse. Damit der AVR trotzdem zwischen den beiden Registern unterscheiden kann, bestimmt das Bit7 (URSEL) welches Register tatsächlich beschrieben werden soll. 1000 0011 (0x83) adressiert demnach UCSRC und übergibt den Wert 3 und 0000 0011 (0x3) adressiert UBRRH und übergibt ebenfalls den Wert 3. Speziell bei den 16-Bit-Timern und auch beim ADC ist es bei allen Zugriffen auf Datenregister erforderlich, dass diese Daten synchronisiert sind. Wenn z.B. bei einem 16-Bit-Timer das High-Byte des Zählregisters gelesen wurde und vor dem Lesezugriff auf das Low-Byte ein Überlauf des Low-Bytes stattfindet, erhält man einen völlig unsinnigen Wert. Auch die Compare-Register müssen synchron geschrieben werden, da es ansonsten zu unerwünschten Compare-Ereignissen kommen kann. Beim ADC besteht das Problem darin, dass zwischen den Zugriffen auf die beiden Teilregister eine Wandlung beendet werden kann und der ADC ein neues Ergebnis in ADCL und ADCH schreiben will, wodurch High- und Low-Byte nicht zusammenpassen. Um diese Datenmüllproduktion zu verhindern, gibt es in beiden Fällen eine Synchronisation, die jeweils durch den Zugriff auf das Low-Byte ausgelöst wird:
Das bedeutet für die Reihenfolge beim Lesezugriff: Erst Low-Byte, dann High-Byte und für den Schreibzugriff: Erst High-Byte, dann Low-Byte. Des weiteren ist zu beachten, dass es für all diese 16-Bit-Register nur ein einziges temporäres Register gibt, so dass das Auftreten eines Interrupts, in dessen Handler ein solches Register manipuliert wird, bei einem durch ihn unterbrochenen Zugriff i.d.R. zu Datenmüll führt. 16-Bit-Zugriffe sind generell nicht atomar! Wenn mit Interrupts gearbeitet wird, kann es erforderlich sein, vor einem solchen Zugriff auf ein 16-Bit-Register die Interrupt-Bearbeitung zu deaktivieren. Beim ADC-Datenregister ADCH/ADCL ist die Synchronisierung anders gelöst. Hier wird beim Lesezugriff (ADCH/ADCL sind logischerweise Read-only) auf das Low-Byte ADCL beide Teilregister für Zugriffe seitens des ADC so lange gesperrt, bis das High-Byte ADCH ausgelesen wurde. Dadurch kann der ADC nach einem Zugriff auf ADCL keinen neuen Wert in ADCH/ADCL ablegen, bis ADCH gelesen wurde. Ergebnisse von Wandlungen, die zwischen einem Zugriff auf ADCL und ADCH beendet werden, gehen verloren! Nach einem Zugriff auf ADCL muss grundsätzlich ADCH gelesen werden! In beiden Fällen (also sowohl bei den Timern als auch beim ADC) werden vom C-Compiler 16-Bit-Pseudo-Register zur Verfügung gestellt (z.B. TCNT1H/TCNT1L -> TCNT1, ADCH/ADCL -> ADC bzw. ADCW), bei deren Verwendung der Compiler automatisch die richtige Zugriffsreihenfolge regelt. In C-Programmen sollten grundsätzlich diese 16-Bit-Register verwendet werden. Sollte trotzdem ein Zugriff auf ein Teilregister erforderlich sein, sind obige Angaben zu berücksichtigen. Es ist darauf zu achten, dass auch ein Zugriff auf die 16-Bit-Register vom Compiler in zwei 8-Bit-Zugriffe aufgeteilt wird und dementsprechend genauso nicht-atomar ist wie die Einzelzugriffe. Auch hier gilt, dass u.U. die Interrupt-Bearbeitung gesperrt werden muss, um Datenmüll zu vermeiden. Beim ADC gibt es für den Fall, dass eine Auflösung von 8 Bit ausreicht, die Möglichkeit, das Ergebnis "linksbündig" in ADCH/ADCL auszurichten, so dass die relevanten 8 MSB in ADCH stehen. In diesem Fall muss bzw. sollte nur ADCH ausgelesen werden. ADC und ADCW sind unterschiedliche Bezeichner für das selbe Registerpaar. Üblicherweise kann man in C-Programmen ADC verwenden, was analog zu den anderen 16-Bit-Registern benannt ist. ADCW (ADC Word) existiert nur deshalb, weil die Headerdateien auch für Assembler vorgesehen sind und es bereits einen Assembler-Befehl namens adc gibt.
[bearbeiten] IO-Register als Parameter und VariablenUm Register als Parameter für eigene Funktionen übergeben zu können, muss man sie als einen volatile uint8_t Pointer übergeben. Zum Beispiel:
Ein Aufruf der Funktion mit call by value würde Folgendes bewirken: Beim Funktionseintritt wird nur eine Kopie des momentanen Portzustandes angefertigt, die sich unabhängig vom tatsächlichen Zustand das Ports nicht mehr ändert, womit die Funktion wirkungslos wäre. Die Übergabe eines Zeigers wäre die Lösung, wenn der Compiler nicht optimieren würde. Denn dadurch wird im Programm nicht von der Hardware gelesen, sondern wieder nur von einem Abbild im Speicher. Das Ergebnis wäre das gleiche wie oben. Mit dem Schlüsselwort volatile sagt man nun dem Compiler, dass die entsprechende Variable entweder durch andere Softwareroutinen (Interrupts) oder durch die Hardware verändert werden kann. Im Übrigen können mit volatile gekennzeichnete Variablen auch als const deklariert werden, um sicherzustellen, dass sie nur noch von der Hardware änderbar sind. [bearbeiten] Zugriff auf IO-PortsJeder AVR implementiert eine unterschiedliche Menge an GPIO-Registern (GPIO - General Purpose Input/Output). Diese Register dienen dazu:
Mittels GPIO werden digitale Zustände gesetzt und erfasst, d.h. die Spannung an einem Ausgang wird ein- oder ausgeschaltet und an einem Eingang wird erfasst, ob die anliegende Spannung über oder unter einem bestimmten Schwellwert liegt. Im Datenblatt Abschnitt Electrical Characteristics/DC Characteristics finden sich die Spannungswerte (V_OL, V_OH für Ausgänge, V_IL, V_IH für Eingänge). Die Verarbeitung von analogen Eingangswerten und die Ausgabe von Analogwerten wird in Kapitel Analoge Ein- und Ausgabe behandelt. Alle Ports der AVR-Controller werden über Register gesteuert. Dazu sind jedem Port 3 Register zugeordnet:
Die folgenden Beispiele gehen von einem AVR aus, der sowohl Port A als auch Port B besitzt. Sie müssen für andere AVRs (zum Beispiel ATmega8/48/88/168) entsprechend angepasst werden. [bearbeiten] Datenrichtung bestimmenZuerst muss die Datenrichtung der verwendeten Pins bestimmt werden. Um dies zu erreichen, wird das Datenrichtungsregister des entsprechenden Ports beschrieben. Für jeden Pin, der als Ausgang verwendet werden soll, muss dabei das entsprechende Bit auf dem Port gesetzt werden. Soll der Pin als Eingang verwendet werden, muss das entsprechende Bit gelöscht sein. Beispiel: Angenommen am Port B sollen die Pins 0 bis 4 als Ausgänge definiert werden, die noch verbleibenden Pins 5 bis 7 sollen als Eingänge fungieren. Dazu ist es daher notwendig, im für das Port B zuständigen Datenrichtungsregister DDRB folgende Bitkonfiguration einzutragen +---+---+---+---+---+---+---+---+
| 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 |
+---+---+---+---+---+---+---+---+
7 6 5 4 3 2 1 0
In C liest sich das dann so:
Die Pins 5 bis 7 werden (da 0) als Eingänge geschaltet. Weitere Beispiele:
[bearbeiten] Vordefinierte Bitnummern für I/O-RegisterDie Bitnummern (z.B. PCx, PINCx und DDCx für den Port C) sind in den io*.h-Dateien der avr-libc definiert und dienen lediglich der besseren Lesbarkeit. Man muss diese Definitionen nicht verwenden oder kann auch einfach "immer" PAx, PBx, PCx usw. nutzen, auch wenn der Zugriff auf Bits in DDRx- oder PINx-Registern erfolgt. Für den Compiler sind die Ausdrücke (1<<PC7), (1<<DDC7) und (1<<PINC7) identisch zu (1<<7) (genauer: der Präprozessor ersetzt die Ausdrücke (1<<PC7),... zu (1<<7)). Ein Ausschnitt der Definitionen für Port C eines ATmega32 aus der iom32.h-Datei zur Verdeutlichung (analog für die weiteren Ports):
[bearbeiten] Digitale SignaleAm einfachsten ist es, digitale Signale mit dem Mikrocontroller zu erfassen bzw. auszugeben. [bearbeiten] AusgängeWill man als Ausgang definierte Pins (entsprechende DDRx-Bits = 1) auf Logisch 1 setzen, setzt man die entsprechenden Bits im Portregister. Mit dem Befehl
wird also der Ausgang an Pin PB2 gesetzt (Beachte, dass die Bits immer von 0 an gezählt werden, das niederwertigste Bit ist also Bitnummer 0 und nicht etwa Bitnummer 1). Man beachte, dass bei der Zuweisung mittels = immer alle Pins gleichzeitig angegeben werden. Man sollte also, wenn nur bestimmte Ausgänge geschaltet werden sollen, zuerst den aktuellen Wert des Ports einlesen und das Bit des gewünschten Ports in diesen Wert einfließen lassen. Will man also nur den dritten Pin (Bit Nr. 2) an Port B auf "high" setzen und den Status der anderen Ausgänge unverändert lassen, nutze man diese Form:
"Ausschalten", also Ausgänge auf "low" setzen, erfolgt analog:
In Quellcodes, die für ältere Version den des avr-gcc/der avr-libc entwickelt wurden, werden einzelne Bits mittels der Funktionen sbi und cbi gesetzt bzw. gelöscht. Beide Funktionen sind in aktuellen Versionen der avr-libc nicht mehr enthalten und auch nicht mehr erforderlich. Falls der Anfangszustand von Ausgängen kritisch ist, muss die Reihenfolge beachtet werden, mit der die Datenrichtung (DDRx) eingestellt und der Ausgabewert (PORTx) gesetzt wird: Für Ausgangspins, die mit Anfangswert "high" initialisiert werden sollen:
Daraus ergibt sich die Abfolge für einen Pin, der bisher als Eingang mit abgeschaltetem Pull-Up konfiguriert ware:
Bei der Reihenfolge erst DDRx und dann PORTx, kann es zu einem kurzen "low-Puls" kommen, der auch externe Pull-Up-Widerstände "überstimmt". Die (ungünstige) Abfolge: Eingang -> setze DDRx: Ausgang (auf "low", da PORTx nach Reset 0) -> setze PORTx: Ausgang auf high. Vergleiche dazu auch das Datenblatt Abschnitt Configuring the Pin. [bearbeiten] Eingänge (Wie kommen Signale in den µC)Die digitalen Eingangssignale können auf verschiedene Arten zu unserer Logik gelangen. [bearbeiten] SignalkopplungAm einfachsten ist es, wenn die Signale direkt aus einer anderen digitalen Schaltung übernommen werden können. Hat der Ausgang der entsprechenden Schaltung TTL-Pegel dann können wir sogar direkt den Ausgang der Schaltung mit einem Eingangspin von unserem Controller verbinden. Hat der Ausgang der anderen Schaltung keinen TTL-Pegel so müssen wir den Pegel über entsprechende Hardware (z.B. Optokoppler, Spannungsteiler, "Levelshifter" aka Pegelwandler) anpassen. Die Masse der beiden Schaltungen muss selbstverständlich miteinander verbunden werden. Der Software selber ist es natürlich letztendlich egal, wie das Signal eingespeist wird. Wir können ja ohnehin lediglich prüfen, ob an einem Pin unseres Controllers eine logische 1 (Spannung größer ca. 0,7*Vcc) oder eine logische 0 (Spannung kleiner ca. 0,2*Vcc) anliegt. Detaillierte Informationen darüber, ab welcher Spannung ein Eingang als 0 ("low") bzw. 1 ("high") erkannt wird, liefert die Tabelle DC Characteristics im Datenblatt des genutzten Controllers. Die Abfrage der Zustände der Portpins erfolgt direkt über den Registernamen. Dabei ist wichtig, zur Abfrage der Eingänge nicht etwa Portregister PORTx zu verwenden, sondern Eingangsregister PINx. Die Abfrage der Pinzustände über PORTx statt PINx ist ein häufiger Fehler beim AVR-"Erstkontakt". (Ansonsten liest man nicht den Zustand der Eingänge, sondern den Status der internen Pull-Up-Widerstände.) Will man also die aktuellen Signalzustände von Port D abfragen und in eine Variable namens bPortD abspeichern, schreibt man folgende Befehlszeilen:
Mit den C-Bitoperationen kann man den Status der Bits abfragen.
[bearbeiten] Interne Pull-Up WiderständePortpins für Ein- und Ausgänge (GPIO) eines AVR verfügen über zuschaltbare interne Pull-Up Widerstände (nominal mehrere 10kOhm, z.B. ATmega16 20-50kOhm). Diese können in vielen Fällen statt externer Widerstände genutzt werden. Die internen Pull-Up Widerstände von Vcc zu den einzelnen Portpins werden über das Register PORTx aktiviert bzw. deaktiviert, wenn ein Pin als Eingang geschaltet ist. Wird der Wert des entsprechenden Portpins auf 1 gesetzt, so ist der Pull-Up Widerstand aktiviert. Bei einem Wert von 0 ist der Pull-Up Widerstand nicht aktiv. Man sollte jeweils entweder den internen oder einen externen Pull-Up Widerstand verwenden, aber nicht beide zusammen. Im Beispiel werden alle Pins des Ports D als Eingänge geschaltet und alle Pull-Up Widerstände aktiviert. Weiterhin wird Pin PC7 als Eingang geschaltet und dessen interner Pull-Up Widerstand aktiviert, ohne die Einstellungen für die anderen Portpins (PC0-PC6) zu verändern.
[bearbeiten] Tasten und SchalterDer Anschluss mechanischer Kontakte an den Mikrocontroller gestaltet sich ebenfalls ganz einfach, wobei wir zwei unterschiedliche Methoden unterscheiden müssen (Active Low und Active High): Der Widerstandswert von Pull-Up- und Pull-Down-Widerständen ist an sich nicht kritisch. Wird er allerdings zu hoch gewählt, ist die Wirkung eventuell nicht gegeben. Als üblicher Wert haben sich 10 kOhm eingebürgert. Die AVRs verfügen an den meisten Pins softwaremäßig über zuschaltbare interne Pull-Up Widerstände (vgl. Abschnitt Interne Pull-Up Widerstände), welche insbesondere wie hier bei Tastern und ähnlichen Bauteilen (z.B. Drehgebern) statt externer Bauteile verwenden werden können. [bearbeiten] (Tasten-)EntprellungNun haben alle mechanischen Kontakte, sei es von Schaltern, Tastern oder auch von Relais, die unangenehme Eigenschaft zu prellen. Dies bedeutet, dass beim Schließen des Kontaktes derselbe nicht direkt Kontakt herstellt, sondern mehrfach ein- und ausschaltet bis zum endgültigen Herstellen des Kontaktes. Soll nun mit einem schnellen Mikrocontroller gezählt werden, wie oft ein solcher Kontakt geschaltet wird, dann haben wir ein Problem, weil das Prellen als mehrfache Impulse gezählt wird. Diesem Phänomen muss beim Schreiben des Programms unbedingt Rechnung getragen werden. Beim folgenden einfachen Beispiel für eine Entprellung ist zu beachten, dass der AVR im Falle eines Tastendrucks 200ms wartet, also brach liegt. Bei zeitkritische Anwendungen sollte man ein anderes Verfahren nutzen (z.B. Abfrage der Tastenzustände in einer Timer-Interrupt-Service-Routine).
Zum Thema Entprellen siehe auch:
[bearbeiten] Der UART[bearbeiten] Allgemeines zum UARTÜber den UART kann ein AVR leicht mit einer RS-232-Schnittstelle eines PC oder sonstiger Geräte mit "serieller Schnittstelle" verbunden werden. Mögliche Anwendungen des UART:
Einige AVR-Controller haben ein bis zwei vollduplexfähigen UART (Universal Asynchronous Receiver and Transmitter) schon eingebaut ("Hardware-UART"). Übrigens: Vollduplex heißt nichts anderes, als dass der Baustein gleichzeitig senden und empfangen kann. Neuere AVRs (ATmega, ATtiny) verfügen über einen oder zwei USART(s), dieser unterscheidet sich vom UART hauptsächlich durch interne FIFO-Puffer für Ein- und Ausgabe und erweiterte Konfigurationsmöglichkeiten. Die Puffergröße ist allerdings nur 1 Byte. Der UART wird über vier separate Register angesprochen. USARTs der ATMEGAs verfügen über mehrere zusätzliche Konfigurationsregister. Das Datenblatt gibt darüber Auskunft. Die Folgende Tabelle gibt nur die Register für die (veralteten) UARTs wieder.
[bearbeiten] Die HardwareDer UART basiert auf normalem TTL-Pegel mit 0V (logisch 0) und 5V (logisch 1). Die Schnittstellenspezifikation für RS-232 definiert jedoch -3V ... -12V (logisch 1) und +3 ... +12V (logisch 0). Daher muss der Signalaustausch zwischen AVR und Partnergerät invertiert werden. Für die Anpassung der Pegel und das Invertieren der Signale gibt es fertige Schnittstellenbausteine. Der bekannteste davon ist wohl der MAX232. Streikt die Kommunikation per UART, so ist oft eine fehlerhafte Einstellung der Baudrate die Ursache. Die Konfiguration auf eine bestimmte Baudrate ist abhängig von der Taktfrequenz des Controllers. Gerade bei neu aufgebauten Schaltungen (bzw. neu gekauften Controllern) sollte man sich daher noch einmal vergewissern, dass der Controller auch tatsächlich mit der vermuteten Taktrate arbeitet und nicht z.B. den bei einigen Modellen werksseitig eingestellten internen Oszillator statt eines externen Quarzes nutzt. Die Werte der verschiedenen fuse-bits im Fehlerfall also beispielsweise mit AVRDUDE kontrollieren und falls nötig anpassen. Grundsätzlich empfiehlt sich auch immer ein Blick in die AVR_Checkliste. [bearbeiten] UART initialisierenWir wollen nun Daten mit dem UART auf die serielle Schnittstelle ausgeben. Dazu müssen wir den UART zuerst mal initialisieren. Dazu setzen wir je nach gewünschter Funktionsweise die benötigten Bits im UART Control Register. Da wir vorerst nur senden möchten und noch keine Interrupts auswerten wollen, gestaltet sich die Initialisierung wirklich sehr einfach, da wir lediglich das Transmitter Enable Bit setzen müssen:
Neuere AVRs mit USART haben mehrere Konfigurationsregister und erfordern eine etwas andere Konfiguration. Für einen ATmega16 z.B.:
Nun ist noch das Baudratenregister UBRR einzustellen. Bei neueren AVRs besteht es aus zwei Registern UBRRL und UBRRH. Der Wert dafür ergibt sich aus der angegebenen Formel durch Einsetzen der Taktfrequenz und der gewünschten Übertragungsrate. Das Berechnen der Formel wird dem Präprozessor überlassen.
Wieder für den Mega16 mit zwei Registern für die Baudrateneinstellung eine etwas andere Programmierung. Wichtig ist, dass UBRRH vor UBRRL geschrieben wird.
Für einige AVR (z.B. ATmega169, ATmega48/88/168, AT90CAN jedoch nicht für z.B. ATmega16/32, ATmega128, ATtiny2313) wird durch die Registerdefinitionen der avr-libc (io*.h) auch für Controller mit zwei UBRR-Registern (UBRRL/UBRRH) ein UBRR bzw. UBRR0 als "16-bit-Register" definiert und man kann auch Werte direkt per UBRR = UBRR_VAL zuweisen. Intern werden dann zwei Zuweisungen für UBRRH und UBRRL generiert. Dies ist nicht bei allen Controllern möglich, da die beiden Register nicht bei allen aufeinanderfolgende Addressen aufweisen. Die getrennte Zuweisung an UBRRH und UBRRL wie im Beispiel gezeigt ist universeller und portabler und daher vorzuziehen. Die Makros sind sehr praktisch, da sie sowohl automatisch den Wert für UBRR als auch den Fehler in der generierten Baudrate berechnen und im Falle einer Überschreitung (+/-1%) einen Fehler und somit Abbruch im Compilerablauf generieren. Damit können viele Probleme mit "UART sendet komische Zeichen" vermieden werden. Ausserdem kann man mühelos die Einstellung an eine neue Taktfrequenz bzw. Baudrate anpassen, ohne selber rechnen oder in Tabellen nachschlagen zu müssen. Inzwischen gibt es in der avr-libc Makros für obige Berechnung der UBRR Registerwerte aus Taktrate F_CPU und Baudrate BAUD in der Includedatei <util/setbaud.h> ([1]). Siehe auch: [bearbeiten] Senden mit dem UART[bearbeiten] Senden einzelner ZeichenUm nun ein Zeichen auf die Schnittstelle auszugeben, müssen wir dasselbe lediglich in das UART Data Register schreiben. Vorher ist zu prüfen, ob das UART-Modul bereit ist, das zu sendende Zeichen entgegenzunehmen. Die Bezeichnungen des/der Statusregisters mit dem Bit UDRE ist abhängig vom Controllertypen (vgl. Datenblatt).
[bearbeiten] Schreiben einer Zeichenkette (String)Die Aufgabe "String senden" wird durch zwei Funktionen abgearbeitet. Die universelle/controllerunabhängige Funktion uart_puts übergibt jeweils ein Zeichen der Zeichenkette an eine Funktion uart_putc, die abhängig von der vorhandenen Hardware implementiert werden muss. In der Funktion zum Senden eines Zeichens ist darauf zu achten, dass vor dem Senden geprüft wird, ob der UART bereit ist den "Sendeauftrag" entgegenzunehmen.
Die in uart_putc verwendeten Schleifen, in denen gewartet wird bis die UART-Hardware zum senden bereit ist, sind insofern etwas kritisch, da während des Sendens eines Strings nicht mehr auf andere Ereignisse reagieren werden kann. Universeller ist die Nutzung von FIFO(first-in first-out)-Puffern, in denen die zu sendenden bzw. empfangenen Zeichen/Bytes zwischengespeichert und in Interruptroutinen an die U(S)ART-Hardware weitergegeben bzw. von ihr ausgelesen werden. Dazu existieren fertige Komponenten (Bibliotheken, Libraries), die man recht einfach in eigene Entwicklungen integrieren kann. Es empfiehlt sich, diese Komponenten zu nutzen und das Rad nicht neu zu erfinden. [bearbeiten] Schreiben von VariableninhaltenSollen Inhalte von Variablen (Ganzzahlen, Fließkomma) in "menschenlesbarer" Form gesendet werden, ist vor dem Transfer eine Umwandlung in Zeichen ("ASCII") erforderlich. Bei nur einer Ziffer ist diese Umwandlung relativ einfach: man addiert den ASCII-Wert von Null zur Ziffer und kann diesen Wert direkt senden.
Soll mehr als eine Ziffer ausgegeben werden, bedient man sich zweckmäßigerweise vorhandener Funktionen zur Umwandlung von Zahlen in Zeichenketten/Strings. Die Funktion der avr-libc zur Umwandlung von vorzeichenbehafteten 16bit-Ganzzahlen (int16_t) in Zeichenketten heißt itoa (Integer to ASCII). Man muss der Funktion einen Speicherbereich zur Verarbeitung (buffer) mit Platz für alle Ziffern, das String-Endezeichen ('\0') und evtl. das Vorzeichen bereitstellen.
Für vorzeichenlose 16bit-Ganzzahlen (uint16_t) exisitert utoa. Die Funktionen für 32bit-Ganzzahlen (int32_t und uint32_t) heißen ltoa bzw. ultoa. Da 32bit-Ganzzahlen mehr Stellen aufweisen können, ist ein entsprechend größerer Pufferspeicher vorzusehen. Auch Fließkommazahlen (float/double) können mit breits vorhandenen Funktionen in Zeichenfolgen umgewandelt werden, dazu existieren die Funktionen dtostre und dtostrf. dtostre nutzt Exponentialschreibweise ("engineering"-Format). (Hinweis: z.Zt. existiert im avr-gcc kein "echtes" double, intern wird immer mit "einfacher Genauigkeit", entsprechend float, gerechnet.) dtostrf und dtostre benötigen die libm.a der avr-libc. Bei Nutzung von Makefiles ist der Parameter -lm in in LDFLAGS anzugeben (Standard in den WinAVR/mfile-Makefilevorlagen). Nutzt man AVRStudio als IDE für den GNU-Compiler (gcc-Plugin) ist die libm.a unter Libaries auszuwählen: Project -> Configurations Options -> Libaries -> libm.a mit dem Pfeil nach rechts einbinden. Siehe auch die FAQ
[bearbeiten] Empfangen[bearbeiten] einzelne Zeichen empfangenZum Empfang von Zeichen muss der Empfangsteil des UART bei der Initialisierung aktiviert werden, indem das RXEN-Bit im jeweiligen Konfigurationsregister (UCSRB bzw UCSR0B/UCSR1B) gesetzt wird. Im einfachsten Fall wird solange gewartet, bis ein Zeichen empfangen wurde, dieses steht dann im UART-Datenregister (UDR bzw. UDR0 und UDR1 bei AVRs mit 2 UARTS) zur Verfügung (sogen. "Polling-Betrieb"). Ein Beispiel für den ATmega16:
Diese Funktion blockiert den Programmablauf. Alternativ kann das RXC-Bit in einer Programmschleife abgefragt werden und dann nur bei gesetztem RXC-Bit UDR ausgelesen werden. Eleganter und in den meisten Anwendungsfällen "stabiler" ist die Vorgehensweise, die empfangenen Zeichen in einer Interrupt-Routine einzulesen und zur späteren Verarbeitung in einem Eingangsbuffer (FIFO-Buffer) zwischenzuspeichern. Dazu existieren fertige und gut getestete Bibliotheken und Quellcodekomponenten (z.B. UART-Library von P. Fleury, procyon-avrlib und einige in der "Academy" von avrfreaks.net). siehe auch:
[bearbeiten] Empfang von Zeichenketten (Strings)Beim Empfang von Zeichenketten, muß man sich zunächst darüber im klaren sein, daß es ein Kriterium geben muß, an dem der µC erkennen kann, wann ein String zu Ende ist. Sehr oft wird dazu das Zeichen 'Return' benutzt, um das Ende eines Strings zu markieren. Dies ist vom Benutzer einfach eingebbar und er ist auch daran gewöhnt, daß er eine Eingabezeile mit einem Druck auf die Return Taste abgeschlossen wird. Prinzipiell gibt es jedoch keine Einschränkung bezüglich dieses speziellen Zeichens. Es muß nur sichergestellt werden, daß dieses spezielle 'Ende eines Strings' - Zeichen nicht mit einem im Text vorkommenden Zeichen verwechselt werden kann. Wenn also im zu übertragenden Text beispielsweise kein ';' vorkommt, dann spricht nichts dagegen, einen String mit einem ';' abschließen zu lassen. Im Folgenden wird die durchaus übliche Annahme getroffen, daß eine Stringübertragung identisch ist mit der Übertragung einer Textzeile und daher mit einem Return ('\n') abgeschlossen wird. Das Problem der Übertragung eines Strings reduziert sich damit auf die Aufgabenstellung: Empfange und Sammle Zeichen in einem char Array, bis entweder das Array voll ist oder das 'String Ende Zeichen' empfangen wurde. Danach wird der empfangene Text noch mit einem '\0' Zeichen abgeschlossen um einen Standard C-String daraus zu machen, mit dem dann weiter gearbeitet werden kann.
Beim Aufruf ist darauf zu achten, dass das empfangende Array auch mit einer vernünftigen Größe definiert wird.
Bei der Benutzung von sizeof() ist allerdings zu beachten, dass sizeof() nicht die Anzahl der Elemente des Arrays liefert, sondern die Länge in Byte. Da ein char nur ein Byte lang ist, passt der Aufruf 'uart_gets(Line, sizeof( Line ) );' in diesem Fall. Falls man - aus welchen Gründen auch immer - andere Datentypen benutzen möchte, sollte man zur korrekten Angabe der Array-Länge folgende Vorgehensweise bevorzugen:
[bearbeiten] InterruptbetriebBeim ATMEGA8 muss das RXCIE Bit im Register UCSRB gesetzt werden, damit ein Interrupt ausgelöst werden kann. Der Interrupt wird immer ausgelöst, wenn Daten erfolgreich empfangen wurden. Zusätzlich braucht man die Routine:
natürlich muss "Global Interrupt Enable" Aktiviert sein. !! Nur getestet beim ATMEGA8 !! [BAUSTELLE! Aus Lerngründen eventuell als eigenen UART-Interrupt-Block hinter den grundlegenden Interrupt-Teil im Tutorial und hier eine kurze Einführung und einen Verweis darauf anbieten.]
[bearbeiten] Software-UARTFalls die Zahl der vorhandenen Hardware-UARTs nicht ausreicht, können weitere Schnittstellen über sogennante Software-UARTs ergänzt werden. Es gibt dazu (mindestens) zwei Ansätze:
Neuere AVRs (z.B. ATtiny26 oder ATmega48,88,168,169) verfügen über ein Universal Serial Interface (USI), das teilweise UART-Funktion übernehmen kann. Atmel stellt eine Application-Note bereit, in der die Nutzung des USI als UART erläutert wird (im Prinzip "Hardware-unterstützter Software-UART"). [bearbeiten] HandshakingWenn der Sender ständig sendet, wird irgendwann der Fall eintreten, daß der Empfänger nicht bereit ist, neue Zeichen zu empfangen. In diesem Fall muß durch ein Handshake-Verfahren die Situation bereinigt werden. Handshake bedeutet nichts anderes, als daß der Empfänger dem Sender mitteilt, daß er zur Zeit keine Daten annehmen kann und der Sender die Übertragung der nächsten Zeichen solange einstellen soll, bis der Empfänger signalisiert, daß er wieder Zeichen aufnehmen kann. [bearbeiten] Hardwarehandshake (RTS/CTS)Beim Hardwarehandshake werden zusätzlich zu den beiden Daten-Übertragungsleitungen noch 2 weitere Leitungen benötigt: RTS (Request To Send) und CTS (Clear To Send). Jeder der beiden Kommunikationspartner ist verpflichtet, bevor ein Zeichen gesendet wird, den Zustand der RTS Leitung zu überprüfen. Nur wenn die Gegenstelle darauf Empfangsbereitschaft signalisiert, darf das Zeichen gesendet werden. Um der Gegenstelle zu signalisieren, daß sie zur Zeit keine Zeichen schicken soll, wird die Leitung CTS benutzt. [bearbeiten] Softwarehandshake (XON/XOFF)Beim Softwarehandshake sind keine speziellen Leitungen notwendig. Statt dessen werden besondere ASCII-Zeichen benutzt, die der Gegenstelle signalisieren, daß Senden einzustellen bzw. wieder aufzunehmen.
Nachteilig bei einem Softwarehandshake ist es, dass dadurch keine direkte binäre Datenübertragung mehr möglich ist. Von den möglichen 256 Bytewerten werden ja 2 (nämlich XON und XOFF) für besondere Zwecke benutzt und fallen daher aus. [bearbeiten] FehlersucheErstaunlich oft wird im Forum der Hilferuf laut: "Meine UART funktioniert nicht, was mache ich falsch". In der überwiegenden Mehrzahl der Fälle stellt sich dann heraus, daß es sich um ein Hardwareproblem handelt, wobei da wiederrum der Löwenanteil auf das Konto einer nicht korrekt eingestellten Taktrate geht: Der µC benutzt nicht einen angeschlossenen Quarz, so wie er auch im Programm eingetragen ist, sondern läuft immer noch mit dem internen RC-Takt. Daraus resultiert aber auch, daß der Baudraten Konfigurationswert falsch berechnet wird. Eine Checkliste zum Aufspüren solcher Fehler findet sich hier. [bearbeiten] LinksFAQ zur Verarbeitung von Strings: http://www.mikrocontroller.net/articles/FAQ [bearbeiten] Analoge Ein- und AusgabeAnaloge Eingangswerte werden in der Regel über den AVR Analog-Digital-Converter (AD-Wandler, ADC) eingelesen, der in vielen Typen verfügbar ist (typisch 10bit Auflösung). Durch diesen werden analoge Signale (Spannungen) in digitale Zahlenwerte gewandelt. Bei AVRs, die über keinen internen AD-Wandler verfügen (z.B. ATmega162), kann durch externe Beschaltung (R/C-Netzwerk und "Zeitmessung") die Funktion des AD-Wandlers "emuliert" werden. Es existieren keine AVRs mit eingebautem Digital-Analog-Konverter (DAC). Diese Funktion muss durch externe Komponenten nachgebildet werden (z.B. PWM und "Glättung"). Unabhängig davon besteht natürlich immer die Möglichkeit, spezielle Bausteine zur Analog-Digital- bzw. Digital-Analog-Wandlung zu nutzen und diese über eine digitale Schnittstelle (z.b. SPI oder I2C) mit einem AVR anzusteuern. [bearbeiten] AC (Analog Comparator)Der Comparator vergleicht 2 Spannungen an den Pins AIN0 und AIN1 und gibt einen Status aus welche der beiden Spannungen größer ist. AIN0 Dient dabei als Referenzspannung (Sollwert) und AIN1 als Vergleichsspannung (Istwert). Als Referenzspannung kann auch alternativ eine interne Referenzspannung ausgewählt werden. Das Steuer- bzw. Statusregister ist wie folgt aufgebaut: ACSR (0x28) - Analog Comparator Status Register
[bearbeiten] ADC (Analog Digital Converter)Der Analog-Digital-Konverter (ADC) wandelt analoge Signale in digitale Werte um, welche vom Controller interpretiert werden können. Einige AVR-Typen haben bereits einen mehrkanaligen Analog-Digital-Konverter eingebaut. Die Genauigkeit, mit welcher ein analoges Signal aufgelöst werden kann, wird durch die Auflösung des ADC in Anzahl Bits angegeben, man hört bzw. liest jeweils von 8-Bit-ADC oder 10-Bit-ADC oder noch höher. ADCs die in AVRs enthalten sind haben zur Zeit eine maximale Auflösung von 10-Bit. Ein ADC mit 8 Bit Auflösung kann somit das analoge Signal mit einer Genauigkeit von 1/256 des Maximalwertes darstellen. Wenn wir nun mal annehmen, wir hätten eine Spannung zwischen 0 und 5 Volt und eine Auflösung von 3 Bit, dann könnten die Werte 0V, 0.625V, 1.25, 1.875V, 2.5V, 3.125V, 3.75, 4.375, 5V daherkommen, siehe dazu folgende Tabelle:
Die Angaben sind natürlich nur ungefähr. Je höher nun die Auflösung des Analog-Digital-Konverters ist, also je mehr Bits er hat, um so genauer kann der Wert erfasst werden. [bearbeiten] Der interne ADC im AVRWenn es einmal etwas genauer sein soll, dann müssen wir auf einen AVR mit eingebautem Analog-Digital-Wandler (ADC) zurückgreifen, die über mehrere Kanäle verfügen. Kanäle heißt in diesem Zusammenhang, dass zwar bis zu zehn analoge Eingänge am AVR verfügbar sind, aber nur ein "echter" Analog-Digital-Wandler zur Verfügung steht, vor der eigentlichen Messung ist also einzustellen, welcher Kanal ("Pin") mit dem Wandler verbunden und gemessen wird. Die Umwandlung innerhalb des AVR basiert auf der schrittweisen Näherung. Beim AVR müssen die Pins AGND und AVCC beschaltet werden. Für genaue Messungen sollte AVCC über ein L-C Netzwerk mit VCC verbunden werden, um Spannungsspitzen und -einbrüche vom Analog-Digital-Wandler fernzuhalten. Im Datenblatt findet sich dazu eine Schaltung, die 10uH und 100nF vorsieht. Das Ergebnis der Analog-Digital-Wandlung wird auf eine Referenzspannung bezogen. Aktuelle AVRs bieten 3 Möglichkeiten zur Wahl dieser Spannung:
Bei Nutzung von AVcc oder der internen Referenz wird empfohlen, einen Kondensator zwischen dem AREF-Pin und GND anzuordnen. Die Festlegung, welche Spannungsreferenz genutzt wird, erfolgt z.B. beim ATmega16 mit den Bits REFS1/REFS0 im ADMUX-Register. Die zu messende Spannung muss im Bereich zwischen AGND und AREF (egal ob intern oder extern) liegen. Der ADC kann in zwei verschiedenen Betriebsarten verwendet werden:
[bearbeiten] Die Register des ADCDer ADC verfügt über eigene Register. Im Folgenden die Registerbeschreibung eines ATMega16, welcher über 8 ADC-Kanäle verfügt. Die Register unterscheiden sich jedoch nicht erheblich von denen anderer AVRs (vgl. Datenblatt).
[bearbeiten] Aktivieren des ADCUm den ADC zu aktivieren, müssen wir das ADEN-Bit im ADCSR-Register setzen. Im gleichen Schritt legen wir auch gleich die Betriebsart fest. Ein kleines Beispiel für den "single conversion"-Mode bei einem ATmega169 und Nutzung der internen Referenzspannung (beim '169 1,1V bei anderen AVRs auch 2,56V). D.h. das Eingangssignal darf diese Spannung nicht überschreiten, gegebenenfalls mit Spannungsteiler einstellen. Ergebnis der Routine ist der ADC-Wert, also 0 für 0-Volt und 1023 für V_ref-Volt.
Im Beispiel wird bei jedem Aufruf der ADC aktiviert und nach der Wandlung wieder abgeschaltet, das spart Strom. Will man dies nicht, verschiebt man die mit (1) gekennzeichneten Zeilen in eine Funktion adc_init() o.ä. und löscht die mit (2) markierten Zeilen.
[bearbeiten] Analog-Digital-Wandlung ohne internen ADC[bearbeiten] Messen eines WiderstandesAnaloge Werte lassen sich ohne Analog-Digital-Wandler auch indirekt ermitteln. Im Folgenden wird die Messung des an einem Potentiometer eingestellten Widerstands anhand der Ladekurve eines Kondensators erläutert. Bei dieser Methode wird nur ein Portpin benötigt, ein Analog-Digital-Wandler oder Analog-Comparator ist nicht erforderlich. Es wird dazu ein Kondensator und der Widerstand (das Potentiometer) in Reihe zwischen Vorsorgungsspannung und Masse/GND geschaltet (sogen. RC-Netzwerk). Zusätzlich wird eine Verbindung der Leitung zwischen Kondensator und Potentiometer zu einem Portpin des Controllers hergestellt. Die folgende Abbildung verdeutlicht die erforderliche Schaltung. Wird der Portpin des Controllers auf Ausgang konfiguriert (im Beispiel DDRD |= (1<<PD2)) und dieser Ausgang auf Logisch 1 ("High", PORTD |= (1<<PD2)) geschaltet, liegt an beiden "Platten" des Kondensators das gleiche Potential VCC an und der Kondensator somit entladen. (Klingt komisch, mit Vcc entladen, ist aber so, da an beiden Seiten des Kondensators das gleiche Potential anliegt und somit eine Potentialdifferenz von 0V besteht => Kondensator ist entladen). Nach einer gewissen Zeit ist der Kondensator entladen und der Portpin wird als Eingang konfiguriert (DDRD &= ~(1<<PD2); PORTD &= ~(1<<PD2)), wodurch dieser hochohmig wird. Der Status des Eingangspin (in PIND) ist Logisch 1 (High). Der Kondensator lädt sich jetzt über das Poti auf, dabei steigt der Spannungsabfall über dem Kondensator und derjenige über dem Poti sinkt. Fällt nun der Spannungsabfall über dem Poti unter die Thresholdspannung des Eingangspins (2/5 Vcc, also ca. 2V), wird das Eingangssignal als LOW erkannt (Bit in PIND wird 0). Die Zeitspanne zwischen der Umschaltung von Entladung auf Aufladung und dem Wechsel des Eingangssignals von High auf Low ist ein Maß für den am Potentiometer eingestellten Widerstand. Zur Zeitmessung kann einer der im Controller vorhandenen Timer genutzt werden. Der 220 Ohm Widerstand dient dem Schutz des Controllers. Es würde sonst bei Maximaleinstellung des Potentionmeters (hier 0 Ohm) ein zu hoher Strom fließen, der die Ausgangsstufe des Controllers zerstört. Mit einem weiteren Eingangspin und ein wenig Software können wir auch eine Kalibrierung realisieren, um den Messwert in einen vernünftigen Bereich (z.B: 0...100 % oder so) umzurechnen.
[bearbeiten] ADC über KomparatorEs gibt einen weiteren Weg, eine analoge Spannung mit Hilfe des Komparators, welcher in fast jedem AVR integriert ist, zu messen. Siehe dazu auch die Application Note AVR400 von Atmel. Dabei wird das zu messende Signal auf den invertierenden Eingang des Komparators geführt. Zusätzlich wird ein Referenzsignal an den nicht invertierenden Eingang des Komparators angeschlossen. Das Referenzsignal wird hier auch wieder über ein RC-Glied erzeugt, allerdings mit festen Werten für R und C. Das Prinzip der Messung ist nun dem vorhergehenden recht
ähnlich. Durch Anlegen eines LOW-Pegels an Pin 2 wird der Kondensator zuerst
einmal entladen. Auch hier muss darauf geachtet werden, dass der Entladevorgang
genügend lang dauert. Ich habe es mir gespart, diese Schaltung auch aufzubauen und zwar aus mehreren Gründen:
Der Vorteil dieser Schaltung liegt allerdings darin, dass damit direkt Spannungen gemessen werden können. [bearbeiten] DAC (Digital Analog Converter)Mit Hilfe eines Digital-Analog-Konverters (DAC) können wir nun auch Analogsignale ausgeben. Es gibt hier mehrere Verfahren. [bearbeiten] DAC über mehrere digitale AusgängeWenn wir an den Ausgängen des Controllers ein entsprechendes Widerstandsnetzwerk aufbauen haben wir die Möglichkeit, durch die Ansteuerung der Ausgänge über den Widerständen einen Addierer aufzubauen, mit dessen Hilfe wir eine dem Zahlenwert proportionale Spannung erzeugen können. Das Schaltbild dazu kann etwa so aussehen: Es sollten selbstverständlich möglichst genaue Widerstände verwendet werden, also nicht unbedingt solche mit einer Toleranz von 10% oder mehr. Weiterhin empfiehlt es sich, je nach Anwendung den Ausgangsstrom über einen Operationsverstärker zu verstärken. [bearbeiten] PWM (Pulsweitenmodulation)Wir kommen nun zu einem Thema, welches in aller Munde ist, aber viele Anwender verstehen nicht ganz, wie PWM eigentlich funktioniert. Wie wir alle wissen, ist ein Mikrocontroller ein rein digitales Bauteil. Definieren wir einen Pin als Ausgang, dann können wir diesen Ausgang entweder auf HIGH setzen, worauf am Ausgang die Versorgungsspannung Vcc anliegt, oder aber wir setzen den Ausgang auf LOW, wonach dann 0V am Ausgang liegt. Was passiert aber nun, wenn wir periodisch mit einer festen Frequenz zwischen HIGH und LOW umschalten? - Richtig, wir erhalten eine Rechteckspannung, wie die folgende Abbildung zeigt: Diese Rechteckspannung hat nun einen arithmetischen Mittelwert, der je nach Pulsbreite kleiner oder größer ist. Wenn wir nun diese pulsierende Ausgangsspannung noch über ein RC-Glied filtern/"glätten", dann haben wir schon eine entsprechende Gleichspannung erzeugt. Mit den AVRs können wir direkt PWM-Signale erzeugen. Dazu dient der 16-Bit Zähler, welcher im sogenannten PWM-Modus betrieben werden kann. Hinweis:
Um den PWM-Modus zu aktivieren, müssen im Timer/Counter1 Control Register A TCCR1A die Pulsweiten-Modulatorbits PWM10 bzw. PWM11 entsprechend nachfolgender Tabelle gesetzt werden:
Der Timer/Counter zählt nun permanent von 0 bis zur Obergrenze und wieder zurück, er wird also als sogenannter Auf-/Ab Zähler betrieben. Die Obergrenze hängt davon ab, ob wir mit 8, 9 oder 10-Bit PWM arbeiten wollen:
Zusätzlich muss mit den Bits COM1A1 und COM1A0 desselben Registers die gewünschte Ausgabeart des Signals definiert werden:
Der entsprechende Befehl, um beispielsweise den Timer/Counter als nicht invertierenden 10-Bit PWM zu verwenden, heißt dann: alte Schreibweise (PWMxx wird nicht mehr akzeptiert)
neue Schreibweise
Damit der Timer/Counter überhaupt läuft, müssen wir im Control Register B TCCR1B noch den gewünschten Takt (Vorteiler) einstellen und somit auch die Frequenz des PWM-Signals bestimmen.
Also um einen Takt von CK / 1024 zu generieren, verwenden wir folgenden Befehl:
Jetzt muss nur noch der Vergleichswert festgelegt werden. Diesen schreiben wir in das 16-Bit Timer/Counter Output Compare Register OCR1A.
Die folgende Grafik soll den Zusammenhang zwischen dem Vergleichswert und dem generierten PWM-Signal aufzeigen. Ach ja, fast hätte ich's vergessen. Das generierte PWM-Signal wird am Output Compare Pin OC1 des Timers ausgegeben und leider können wir deshalb auch beim AT90S2313 nur ein einzelnes PWM-Signal mit dieser Methode generieren. Andere AVR-Typen verfügen über bis zu vier PWM-Ausgänge. Zu beachten ist außerdem, das wenn der OC Pin aktiviert ist, er nichtmehr wie üblich funktioniert und z.B. nicht einfach über PINx ausgelesen werden kann. Ein Programm, welches an einem ATMega8 den Fast-PWM Modus verwendet, den Modus 14, könnte so aussehen
PWM Mode Tabelle aus dem Datenblatt des Atmega 8515:
Für Details der PWM Möglichkeiten, muß immer das jeweilge Datenblatt des Prozessors konsultiert werden, da sich die unterschiedlichen Prozessoren in ihren Möglichkeiten doch stark unterscheiden. Auch muß man aufpassen, welches zu setzende Bit in welchem Register sind. Auch hier kann es sein, dass gleichnamige Konfigurationsbits in unterschiedlichen Konfigurationsregistern (je nach konkretem Prozessortyp) sitzen. [bearbeiten] LCD-Ansteuerung[bearbeiten] Das LCD und sein ControllerDie meisten Text-LCDs verwenden den Controller HD44780 oder einen kompatiblen (z.B. KS0070) und haben 14 oder 16 Pins. Die Pinbelegung an der LCD-Controller-Platine ist praktisch immer gleich. Trotzdem lohnt sich ein Blick in das Datenblatt des Displays, da es gelegentlich Ausnahmen gibt. Die normale Pinbelegung sieht wie folgt aus:
Achtung: Unbedingt von der richtigen Seite zu zählen anfangen! Meistens ist neben Pin 1 eine kleine 1 auf der LCD-Platine, ansonsten im Datenblatt nachschauen. Bei LCDs mit 16-poligem Anschluss sind die beiden letzten Pins für die Hintergrundbeleuchtung reserviert. Hier unbedingt das Datenblatt zu Rate ziehen, die beiden Anschlüsse sind je nach Hersteller verdreht beschaltet. Falls kein Datenblatt vorliegt, kann man mit einem Durchgangsprüfer feststellen, welcher Anschluss mit Masse (GND) verbunden ist. Vss wird ganz einfach an GND angeschlossen und Vcc an 5V. Vee kann man testweise auch an GND legen. Wenn das LCD dann zu dunkel sein sollte muss man ein 10k-Potentiometer zwischen GND und 5V schalten, mit dem Schleifer an Vee: Es gibt zwei verschiedene Möglichkeiten zur Ansteuerung eines solchen Displays: den 8-bit- und den 4-bit-Modus.
Der 4-bit-Modus hat den Vorteil, dass man 4 IO-Pins weniger benötigt als beim 8-bit-Modus, weshalb ich mich hier für eine Ansteuerung mit 4bit entschieden habe. Neben den vier Datenleitungen (DB4, DB5, DB6 und DB7) werden noch die Anschlüsse RS, RW und E (ist in manchen Unterlagen auch EN für Enable abgekürzt) benötigt.
[bearbeiten] Anschluss an den ControllerJetzt da wir wissen, welche Anschlüsse das LCDs benötigt, können wir das LCD mit dem Mikrocontroller verbinden:
Wenn man die Steuerleitungen EN und RS auf Pins an einem anderen Port legen möchte, kann man so wie in diesem Forumsbeitrag vorgehen. Ok, alles ist verbunden, wenn man jetzt den Strom einschaltet sollten ein oder zwei schwarze Balken auf dem Display angezeigt werden. Doch wie bekommt man jetzt die Befehle und Daten in das Display? [bearbeiten] ProgrammierungDatei lcd-routines.h
Datei lcd-routines.c:
Ein Hauptprogramm, welches die Funktionen benutzt, sieht zb. so aus:
Wichtig ist dabei, dass die Optimierung bei der Compilierung eingeschaltet ist, sonst stimmen die Zeiten der Funktionen _delay_us() und _delay_ms() nicht und der Code wird wesentlich länger (Siehe Dokumentation der libc im WinAVR). Ein Hauptprogramm, welches eine Variable ausgibt, sieht zb. so aus. Mittels der itoa() Funktion (itoa = Integer To Ascii ) wird von einem Zahlenwert eine textuelle Repräsentierung ermittelt (sprich: ein String erzeugt) und dieser String mit der bereits vorhandenen Funktion lcd_string ausgegeben:
Beim Einrichten eines Projekts muss man zu der Datei mit dem Hauptprogramm auch die Datei lcd-routines.c in das Projekt aufnehmen. Dies geschieht beim AVR Studio unter Source Files im Fenster AVR GCC oder bei WinAVR im Makefile (z.B. durch SRC += lcd-routines.c). [bearbeiten] Die Timer/Counter des AVRDie heutigen Mikrocontroller und insbesondere die RISC-AVRs sind für viele Steuerungsaufgaben zu schnell. Wenn wir beispielsweise eine LED oder Lampe blinken lassen wollen, können wir selbstverständlich nicht die CPU-Frequenz verwenden, da ja dann nichts mehr vom Blinken zu bemerken wäre. Wir brauchen also eine Möglichkeit, Vorgänge in Zeitabständen durchzuführen, die geringer als die Taktfrequenz des Controllers sind. Selbstverständlich sollte die resultierende Frequenz auch noch möglichst genau und stabil sein. Hier kommen die im AVR vorhandenen Timer/Counter zum Einsatz. Ein Timer ist ganz einfach ein bestimmtes Register im µC, das völlig ohne Zutun des Programms, also per Hardware, hochgezählt wird. Das alleine wäre noch nicht allzu nützlich, wenn nicht dieses Hardwareregister bei bestimmten Zählerständen einen Interrupt auslösen könnte. Ein solches Ereignis ist der Overflow: Da die Bitbreite des Registers beschränkt ist, kommt es natürlich auch vor, dass der Zähler so hoch zählt, dass der nächste Zählerstand mit dieser Bitbreite nicht mehr darstellbar ist und der Zähler wieder auf 0 zurückgesetzt wird. Dieses Ereignis nennt man den Overflow und es ist möglich an dieses Ereignis einen Interrupt zu koppeln.
Die folgenden Ausführungen beziehen sich auf den AT90S2313. Für andere Modelltypen müsst ihr euch die allenfalls notwendigen Anpassungen aus den Datenblättern der entsprechenden Controller herauslesen. Wir unterscheiden grundsätzlich zwischen 8-Bit Timern, welche eine Auflösung von 256 aufweisen und 16-Bit Timern mit (logischerweise) einer Auflösung von 65536. Als Eingangstakt für die Timer/Counter kann entweder die CPU-Taktfrequenz, der Vorteiler-Ausgang oder ein an einen I/O-Pin angelegtes Signal verwendet werden. Wenn ein externes Signal verwendet wird, so darf dessen Frequenz nicht höher sein als die Hälfte des CPU-Taktes. [bearbeiten] Der Vorteiler (Prescaler)Der Vorteiler dient dazu, den CPU-Takt vorerst um einen einstellbaren Faktor zu reduzieren. Die so geteilte Frequenz wird den Eingängen der Timer zugeführt. Wenn wir mit einem CPU-Takt von 4 MHz arbeiten und den Vorteiler auf 1024 einstellen, wird also der Timer mit einer Frequenz von 4 MHz / 1024, also mit ca. 4 kHz versorgt. Wenn also der Timer läuft, so wird das Daten- bzw. Zählregister (TCNTx) mit dieser Frequenz inkrementiert. [bearbeiten] 8-Bit Timer/CounterAlle AVR-Modelle verfügen über mindestens einen, teilweise sogar zwei, 8-Bit Timer. Der 8-Bit Timer wird z.B bei AT90S2313 über folgende Register angesprochen (bei anderen Typen weitestgehend analog):
Um nun also den Timer0 in Betrieb zu setzen und ihn mit einer Frequenz von 1/1024-tel des CPU-Taktes zählen zu lassen, schreiben wir die folgende Befehlszeile:
Der Zähler zählt nun aufwärts bis 255, um dann wieder bei 0 zu beginnen. Der aktuelle Zählerstand steht in TCNT0. Bei jedem Überlauf von 255 auf 0 wird das Timer Overflow Flag TOV0 im Timer Interrupt Flag TIFR-Register gesetzt und, falls so konfiguriert, ein entsprechender Timer-Overflow-Interrupt ausgelöst und die daran gebundene Interrupt-Routine abgearbeitet. Das TOV Flag lässt sich durch das Hineinschreiben einer 1 und nicht wie erwartet einer 0 wieder zurücksetzen. Beispiel für Compare Match Mode:
[bearbeiten] Timer-Bitzahlen verschiedener AVR's
[bearbeiten] 16-Bit Timer/CounterViele AVR-Modelle besitzen außer den 8-Bit Timern auch 16-Bit Timer. Die 16-Bit Timer/Counter sind etwas komplexer aufgebaut als die 8-Bit Timer/Counter, bieten dafür aber auch viel mehr Möglichkeiten, als da sind:
Folgende Register sind dem Timer/Counter 1 zugeordnet:
[bearbeiten] Die PWM-BetriebsartWenn der Timer/Counter 1 in der PWM-Betriebsart betrieben wird, so bilden das Datenregister TCNT1H/TCNT1L und das Vergleichsregister OCR1H/OCR1L einen 8-, 9- oder 10-Bit, frei laufenden PWM-Modulator, welcher als PWM-Signal am OC1-Pin (PB3 beim 2313) abgegriffen werden kann. Das Datenregister TCNT1H/TCNT1L wird dabei als Auf-/Ab-Zähler betrieben, welcher von 0 an aufwärts zählt bis zur Obergrenze und danach wieder zurück auf 0. Die Obergrenze ergibt sich daraus, ob 8-, 9- oder 10-Bit PWM verwendet wird, und zwar gemäß folgender Tabelle:
Wenn nun der Zählerwert im Datenregister den in OCR1H/OCR1L gespeicherten Wert erreicht, wird der Ausgabepin OC1 gesetzt bzw. gelöscht, je nach Einstellung von COM1A1 und COM1A0 im TCCR1A-Register. Ich habe versucht, die entsprechenden Signale in der folgenden Grafik zusammenzufassen [bearbeiten] Vergleichswert-Überprüfung (Compare Match)Hier wird in ein spezielles Vergleichswertregister (OCR1H/OCR1L) ein Wert eingeschrieben, welcher ständig mit dem aktuellen Zählerwert verglichen wird. Erreicht der Zähler den in diesem Register eingetragenen Wert, so kann ein Signal (0 oder 1) am Pin OC1 erzeugt und/oder ein Interrupt ausgelöst werden. Zu erwähnen ist in dem Zusammenhang, dass das zur Compare-Einheit gehörende Interrupt-Flag erst beim auf die Übereinstimmung der Werte folgenden Timertakt gesetzt wird. Das ist v.a. deshalb wichtig, da es sonst bei OCRnx = 0 einen undefinierten Zustand gäbe. Möchte man ein Compare-Ereignis 100 Takte nach dem Timerüberlauf auslösen, dann muss in das betreffende Compare-Register eine 99 geschrieben werden. [bearbeiten] CTC-Betriebsart (Clear Timer on Compare Match)Das sogenannte Compare Match-Ereignis kann auch dazu verwendet werden, um den Timer automatisch zurückzusetzen (d.h. das TCNT-Register wird zu Null gesetzt). Diese Betriebsart heißt "Clear Timer on Compare Match", also auf deutsch "Lösche Timer bei Vergleichsübereinstimmung". Mit dieser Funktionalität ist es möglich, sehr präzise Taktsignale zu erzeugen, ohne dabei programmtechnisch eingreifen zu müssen. Diese Funktion ersetzt das bei anderen Controllern und Timern ohne Compare-Einheit erforderliche Timer Reload (also das Nachladen des Zählregisters mit "Überlaufwert minus gewünschte Taktzahl bis zum Überlauf", v.a. verbreitet bei 8051er-µCs). Zur Erzeugung eines Taktes per Hardware muss lediglich eine der CTC-Betriebsarten ausgewählt werden und einer der OCnx-Pins so gesetzt werden, dass er bei Auftreten des Compare Match getoggelt wird (über die COM-Bits). Die Frequenz des Taktes am entsprechenden Ausgang ist dann
Diese Betriebsart macht das Timer-Nachladen, das bei AVRs, die ja im Unterschied zu 8051-Derivaten keine Auto-Reload-Funktion haben, immer mit Ungenauigkeiten und programmtechnischen Klimmzügen verbunden ist, überflüssig. Ist das OCRnx einmal gesetzt, dann wird das Signal am Ausgang kontinuierlich ausgegeben, ohne dass die Anwendersoftware eingreifen muss (es sei denn, die Frequenz soll geändert werden). Beim ATMega8 hat der 8-Bit-Timer 0 keine Compare-Einheit, so dass dort CTC und auch sonstige automatische Vergleichsoperationen nicht möglich sind. Bei Timer 1 und Timer 2 ist das jedoch möglich. Bei den neueren AVRs besitzen i.d.R. alle Timer eine oder mehrere Compare-Einheiten, so dass dort eine größere Flexibilität gegeben ist. Im Unterschied zu den PWM-Betriebsarten wird die Registeraktualisierung bei CTC nicht automatisch synchronisiert. Schreibt man einen neuen Compare-Wert, dann wird dieser sofort übernommen, was zu Fehlfunktionen führen kann, wenn der neue Compare-Wert höher ist, als der atuelle Stand von TCNTnx. In den PWM-Betriebsarten wird hingegen der TOP-Wert synchron bei Erreichen von TOP oder BOTTOM aktualisiert. [bearbeiten] Einfangen eines Eingangssignals (Input Capturing)Bei dieser Betriebsart wird an den Input Capturing Pin (ICP) des Controllers eine Signalquelle angeschlossen. Nun kann je nach Konfiguration entweder ein Signalwechsel von 0 nach 1 (steigende Flanke) oder von 1 nach 0 (fallende Flanke) erkannt werden und der zu diesem Zeitpunkt aktuelle Zählerstand in ein spezielles Register abgelegt werden. Gleichzeitig kann auch ein entsprechender Interrupt ausgelöst werden. Wenn die Signalquelle ein starkes Rauschen beinhaltet, kann die Rauschunterdrückung eingeschaltet werden. Dann wird beim Erkennen der konfigurierten Flanke über 4 Taktzyklen das Signal überwacht und nur dann, wenn alle 4 Messungen gleich sind, wird die entsprechende Aktion ausgelöst. [bearbeiten] Gemeinsame RegisterVerschiedene Register beinhalten Zustände und Einstellungen, welche sowohl für den 8-Bit, als auch für den 16-Bit Timer/Counter in ein und demselben Register zu finden sind.
[bearbeiten] Warteschleifen (delay.h)Der Programmablauf kann verschiedene Arten von Wartefunktionen erfordern:
Der einfachste Fall, das Zeitvertrödeln, kann in vielen Fällen und mit großer Genauigkeit anhand der avr-libc Bibliotheksfunktionen _delay_ms() und _delay_us() erledigt werden. Die Bibliotheksfunktionen sind einfachen Zählschleifen (Warteschleifen) vorzuziehen, da leere Zählschleifen ohne besondere Vorkehrungen sonst bei eingeschalteter Optimierung vom avr-gcc-Compiler wegoptimiert werden. Weiterhin sind die Bibliotheksfunktionen bereits darauf vorbereitet, die in F_CPU definierte Taktfrequenz zu verwenden. Ausserdem sind die Funktionen der Bibliothek wirklich getestet. Einfach!? Schon, aber während gewartet wird, macht der µC nichts anderes mehr. Die Wartefunktion blockiert den Programmablauf. Möchte man einerseits warten, um z.B. eine LED blinken zu lassen und gleichzeitig andere Aktionen ausführen z.B. weitere LED bedienen, sollten die Timer/Counter des AVR verwendet werden. Die Bibliotheksfunktionen funktionieren allerdings nur dann korrekt, wenn sie mit zur Übersetzungszeit (beim Compilieren) bekannten konstanten Werten aufgerufen werden. Der Quellcode muss mit eingeschalteter Optimierung übersetzt werden, sonst wird sehr viel Maschinencode erzeugt und die Wartezeiten stimmen nicht mehr mit dem Parameter überein. Abhängig von der Version der Bibliothek verhalten sich die Bibliotheksfunktionen etwas unterschiedlich. [bearbeiten] avr-libc Versionen kleiner 1.6Die Wartezeit der Funktion _delay_ms() ist auf 262,14ms/F_CPU (in MHz) begrenzt, d.h. bei 20 MHz kann man nur max. 13,1ms warten. Die Wartezeit der Funktion _delay_us() ist auf 768us/F_CPU (in MHz) begrenzt, d.h. bei 20 MHz kann man nur max. 38,4us warten. Längere Wartezeiten müssen dann über einen mehrfachen Aufruf in einer Schleife gelöst werden. Beispiel: Blinken einer LED an PORTB Pin PB0 im ca. 1s Rhythmus
[bearbeiten] avr-libc Versionen ab 1.6_delay_ms() kann mit einem Argument bis 6553,5 ms (= 6,5535 Sekunden) benutzt werden. Wird die früher gültige Grenze von 262,14 ms/F_CPU (in MHz) überschritten, so arbeitet _delay_ms() einfach etwas ungenauer und zählt nur noch mit einer Auflösung von 1/10 ms. Eine Verzögerung von 1000,10 ms ließe sich nicht mehr von einer von 1000,19 ms unterscheiden. Ein Verlust, der sich im Allgemeinen verschmerzen lässt. Dem Programmierer wird keine Rückmeldung gegeben, dass die Funktion ggf. gröber arbeitet, d.h. wenn es darauf ankommt, bitte den Parameter wie bisher geschickt wählen. Die Funktion _delay_us() wurde ebenfalls erweitert. Wenn deren maximal als genau behandelbares Argument überschritten wird, benutzt diese intern _delay_ms(). Damit gelten in diesem Fall die _delay_ms() Einschränkungen. Beispiel: Blinken einer LED an PORTB Pin PB0 im ca. 1s Rhythmus, avr-libc ab Version 1.6
[bearbeiten] Der WatchdogUnd hier kommt das ultimative Mittel gegen die Unvollkommenheit von uns Programmierern, der Watchdog. So sehr wir uns auch anstrengen, es wird uns kaum je gelingen, das absolut perfekte und fehlerfreie Programm zu entwickeln. Der Watchdog kann uns zwar auch nicht zu besseren Programmen verhelfen aber er kann dafür sorgen, dass unser Programm, wenn es sich wieder mal in's Nirwana verabschiedet hat, neu gestartet wird, indem ein Reset des Controllers ausgelöst wird. Betrachten wir doch einmal folgende Codesequenz:
Wenn wir die Schleife mal genau anschauen sollte uns auffallen, dass dieselbe niemals beendet wird. Warum nicht? Ganz einfach, weil eine als unsigned deklarierte Variable niemals kleiner als Null werden kann (der Compiler sollte jedoch eine ensprechende Warnung ausgeben). Das Programm würde sich also hier aufhängen und auf ewig in der Schleife drehen. Und hier genau kommt der Watchdog zum Zug. [bearbeiten] Wie funktioniert nun der Watchdog?Der Watchdog enthält einen separaten Timer/Counter, welcher mit einem intern erzeugten Takt von 1 MHz bei 5V Vcc getaktet wird. Einige Controller haben einen eigenen Watchdog Oszillator, z.B. der Tiny2313 mit 128kHz. Nachdem der Watchdog aktiviert und der gewünschte Vorteiler eingestellt wurde, beginnt der Counter von 0 an hochzuzählen. Wenn nun die je nach Vorteiler eingestellte Anzahl Zyklen erreicht wurde, löst der Watchdog einen Reset aus. Um nun also im Normalbetrieb den Reset zu verhindern, müssen wir den Watchdog regelmäßig wieder neu starten bzw. rücksetzen (Watchdog Reset). Dies sollte innerhalb unserer Hauptschleife passieren. Um ein unbeabsichtigtes Ausschalten des Watchdogs zu verhindern, muss ein spezielles Prozedere verwendet werden, um den WD auszuschalten. Es müssen zuerst die beiden Bits WDTOE und WDE in einer einzelnen Operation (also nicht mit sbi) auf 1 gesetzt werden. Dann muss innerhalb der nächsten 4 Taktzyklen das Bit WDE auf 0 gesetzt werden. Das Watchdog Control Register:
Um den Watchdog mit dem AVR-GCC Compiler zu verwenden, muss die Headerdatei wdt.h (#include <avr/wdt.h>) in die Quelldatei eingebunden werden. Danach können die folgenden Funktionen verwendet werden:
Selbstverständlich kann das WDTCR-Register auch mit den uns bereits bekannten Funktionen für den Zugriff auf Register programmiert werden. [bearbeiten] Watchdog-AnwendungshinweiseOb nun der Watchdog als Schutzfunktion überhaupt verwendet werden soll, hängt stark von der Anwendung, der genutzten Peripherie und dem Umfang und der Qualitätssicherung des Codes ab. Will man sicher gehen, dass ein Programm sich nicht in einer Endlosschleife verfängt, ist der Wachdog das geeignete Mittel dies zu verhindern. Weiterhin kann bei geschickter Programmierung der Watchdog dazu genutzt werden, bestimmte Stromsparfunktionen zu implementieren. Bei einigen neueren AVRs (z.B. dem ATTiny13) kann der Watchdog auch direkt als Timer genutzt werden, der den Controller aus einem Schlafmodus aufweckt. Auch dies kann im WDTCR-Register eingestellt werden. Außerdem bietet der WD die einzige Möglichkeit einen beabsichtigten System-Reset (ein "richtiger Reset", kein "jmp 0x0000") ohne externe Beschaltung auszulösen, was z.B. bei der Implementierung eines Bootloaders nützlich ist. Bei bestimmten Anwendungen kann die Nutzung des WD als "ultimative Deadlock-Sicherung für nicht bedachte Zustände" natürlich immer als zusätzliche Sicherung dienen. Es besteht die Möglichkeit herauszufinden, ob ein Reset durch den Watchdog ausgelöst wurde (beim ATmega16 z.B. Bit WDRF in MCUCSR). Diese Information sollte auch genutzt werden, falls ein WD-Reset in der Anwendung nicht planmäßig implementiert wurde. Zum Beispiel kann man eine LED an einen freien Pin hängen, die nur bei einem Reset durch den WD aufleuchtet oder aber das "Ereignis WD-Reset" im internen EEPROM des AVR absichern, um die Information später z.B. über UART oder ein Display auszugeben (oder einfach den EEPROM-Inhalt über die ISP/JTAG-Schnittstelle auslesen). Siehe auch:
[bearbeiten] Programmieren mit InterruptsNachdem wir nun alles Wissenswerte für die serielle Programmerstellung gelernt haben nehmen wir jetzt ein völlig anderes Thema in Angriff, nämlich die Programmierung unter Zuhilfenahme der Interrupts des AVR. Als erstes wollen wir uns noch einmal den allgemeinen Programmablauf bei der Interrupt-Programmierung zu Gemüte führen. Man sieht, dass die Interruptroutine quasi parallel zum Hauptprogramm abläuft. Da wir nur eine CPU haben ist es natürlich keine echte Parallelität, sondern das Hauptprogramm wird beim Eintreffen eines Interrupts unterbrochen, die Interruptroutine wird ausgeführt und danach erst wieder zum Hauptprogramm zurückgekehrt. [bearbeiten] Anforderungen an Interrupt-RoutinenUm unliebsamen Überraschungen vorzubeugen, sollten einige Grundregeln bei der Implementierung der Interruptroutinen beachtet werden. Interruptroutinen soll möglichst kurz und schnell abarbeitbar sein, daraus folgt:
Interruptroutinen (ISRs) sollten also möglichst kurz sein und keine Schleifen mit vielen Durchläufen enthalten. Längere Operationen können meist in einen "Interrupt-Teil" in einer ISR und einen "Arbeitsteil" im Hauptprogramm aufgetrennt werden. Z.B. Speichern des Zustands aller Eingänge im EEPROM in bestimmten Zeitabständen: ISR-Teil: Zeitvergleich (Timer,RTC) mit Logzeit/-intervall. Bei Übereinstimmung ein globales Flag setzen (volatile bei Flag-Deklaration nicht vergessen, s.u.). Dann im Hauptprogramm prüfen, ob das Flag gesetzt ist. Wenn ja: die Daten im EEPROM ablegen und Flag löschen. (*) Hinweis: Es gibt allerdings die seltene Situation, dass man gerade eingelesene ADC-Werte sofort verarbeiten muss. Besonders dann, wenn man mehrere Werte sehr schnell hintereinander bekommt. Dann bleibt einem nichts anderes übrig, als die Werte noch in der ISR zu verarbeiten. Kommt aber sehr selten vor und sollte durch geeignete Wahl des Systemtaktes bzw. Auswahl des Controllers vermieden werden! [bearbeiten] Interrupt-QuellenDie folgenden Ereignisse können einen Interrupt auf einem AVR AT90S2313 auslösen, wobei die Reihenfolge der Auflistung auch die Priorität der Interrupts aufzeigt.
Die Anzahl der möglichen Interruptquellen variiert zwischen den verschiedenen Typen. Im Zweifel hilft ein Blick ins Datenblatt ("Interrupt Vectors"). [bearbeiten] RegisterDer AT90S2313 verfügt über 2 Register die mit den Interrupts zusammen hängen.
[bearbeiten] Allgemeines über die Interrupt-AbarbeitungWenn ein Interrupt eintrifft, wird automatisch das Global Interrupt Enable Bit im Status Register SREG gelöscht und alle weiteren Interrupts unterbunden. Obwohl es möglich ist, zu diesem Zeitpunkt bereits wieder das GIE-bit zu setzen, wird dringend davon abgeraten. Dieses wird nämlich automatisch gesetzt, wenn die Interruptroutine beendet wird. Wenn in der Zwischenzeit weitere Interrupts eintreffen, werden die zugehörigen Interrupt-Bits gesetzt und die Interrupts bei Beendigung der laufenden Interrupt-Routine in der Reihenfolge ihrer Priorität ausgeführt. Dies kann eigentlich nur dann zu Problemen führen, wenn ein hoch priorisierter Interrupt ständig und in kurzer Folge auftritt. Dieser sperrt dann möglicherweise alle anderen Interrupts mit niedrigerer Priorität. Dies ist einer der Gründe, weshalb die Interrupt-Routinen sehr kurz gehalten werden sollen.
[bearbeiten] Interrupts mit dem AVR GCC Compiler (WinAVR)Funktionen zur Interrupt-Verarbeitung werden in den Includedateien interrupt.h der avr-libc zur Verfügung gestellt (bei älterem Quellcode zusätzlich signal.h).
Das Makro sei() schaltet die Interrupts ein. Eigentlich wird nichts anderes gemacht, als das Global Interrupt Enable Bit im Status Register gesetzt.
Das Makro cli() schaltet die Interrupts aus, oder anders gesagt, das Global Interrupt Enable Bit im Status Register wird gelöscht.
Oft steht man vor der Aufgabe, dass eine Codesequenz nicht unterbrochen werden darf. Es liegt dann nahe, zu Beginn dieser Sequenz ein cli() und am Ende ein sei() einzufügen. Dies ist jedoch ungünstig, wenn die Interrupts vor Aufruf der Sequenz deaktiviert waren und danach auch weiterhin deaktiviert bleiben sollen. Ein sei() würde ungeachtet des vorherigen Zustands die Interrupts aktivieren, was zu unerwünschten Seiteneffekten führen kann. Die aus dem folgenden Beispiel ersichtliche Vorgehensweise ist in solchen Fällen vorzuziehen:
[bearbeiten] ISR(ISR() ersetzt bei neueren Versionen der avr-libc SIGNAL(). SIGNAL sollte nicht mehr genutzt werden, zur Portierung von SIGNAL nach ISR siehe den Anhang.
Mit ISR wird eine Funktion für die Bearbeitung eines Interrupts eingeleitet. Als Argument muss dabei die Benennung des entsprechenden Interruptvektors angegeben werden. Diese sind in den jeweiligen Includedateien IOxxxx.h zu finden. Die Bezeichnung entspricht dem Namen aus dem Datenblatt, bei dem die Leerzeichen durch Unterstriche ersetzt sind und ein _vect angehängt ist. Als Beispiel ein Ausschnitt aus der Datei für den ATmega8 (bei WinAVR Standardinstallation in C:\WinAVR\avr\include\avr\iom8.h) in der neben den aktuellen Namen für ISR (*_vect) noch die Bezeichnungen für das inzwischen nicht mehr aktuelle SIGNAL (SIG_*) enthalten sind.
Mögliche Funktionsrümpfe für Interruptfunktionen sind zum Beispiel:
Auf die korrekte Schreibweise der Vektorbezeichnung ist zu achten. Der gcc-Compiler prüft erst ab Version 4.x, ob ein Signal/Interrupt der angegebenen Bezeichnung tatsächlich in der Includedatei definiert ist und gibt andernfalls eine Warnung aus. Bei WinAVR (ab 2/2005) wurde die Überprüfung auch in den mitgelieferten Compiler der Version 3.x integriert. Aus dem gcc-Quellcode Version 3.x selbst erstellte Compiler enthalten die Prüfung nicht (vgl. AVR-GCC). Während der Ausführung der Funktion sind alle weiteren Interrupts automatisch gesperrt. Beim Verlassen der Funktion werden die Interrupts wieder zugelassen. Sollte während der Abarbeitung der Interruptroutine ein weiterer Interrupt (gleiche oder andere Interruptquelle) auftreten, so wird das entsprechende Bit im zugeordneten Interrupt Flag Register gesetzt und die entsprechende Interruptroutine automatisch nach dem Beenden der aktuellen Funktion aufgerufen. Ein Problem ergibt sich eigentlich nur dann, wenn während der Abarbeitung der aktuellen Interruptroutine mehrere gleichartige Interrupts auftreten. Die entsprechende Interruptroutine wird im Nachhinein zwar aufgerufen jedoch wissen wir nicht, ob nun der entsprechende Interrupt einmal, zweimal oder gar noch öfter aufgetreten ist. Deshalb soll hier noch einmal betont werden, dass Interruptroutinen so schnell wie nur irgend möglich wieder verlassen werden sollten. [bearbeiten] Unterbrechbare Interruptroutinen"Faustregel": im Zweifel ISR. Die nachfolgend beschriebene Methode nur dann verwenden, wenn man sich über die unterschiedliche Funktionsweise im Klaren ist.
Hierbei steht XXX für den oben beschriebenen Namen des Vektors (also z.B. void TIMER0_OVF_vect(void)...). Der Unterschied im Vergleich zu ISR ist, dass hier beim Aufrufen der Funktion das Global Enable Interrupt Bit automatisch wieder gesetzt und somit weitere Interrupts zugelassen werden. Dies kann zu nicht unerheblichen Problemen von im einfachsten Fall einem Stack overflow bis zu sonstigen unerwarteten Effekten führen und sollte wirklich nur dann angewendet werden, wenn man sich absolut sicher ist, das Ganze auch im Griff zu haben. siehe auch: Hinweise in AVR-GCC siehe dazu: http://www.nongnu.org/avr-libc/user-manual/group__avr__interrupts.html [bearbeiten] Datenaustausch mit Interrupt-RoutinenVariablen die sowohl in Interrupt-Routinen (ISR = Interrupt Service Routine(s)), als auch vom übrigen Programmcode geschrieben oder gelesen werden, müssen mit einem volatile deklariert werden. Damit wird dem Compiler mitgeteilt, dass der Inhalt der Variablen vor jedem Lesezugriff aus dem Speicher gelesen und nach jedem Schreibzugriff in den Speicher geschrieben wird. Ansonsten könnte der Compiler den Code so optimieren, dass der Wert der Variablen nur in Prozessorregistern zwischengespeichert wird, die nichts von der Änderung woanders mitbekommen. Zur Veranschaulichung ein Codefragment für eine Tastenentprellung mit Erkennung einer "lange gedrückten" Taste.
Wird innerhalb einer ISR mehrfach auf eine mit volatile deklarierte Variable zugegriffen, wirkt sich dies ungünstig auf die Verarbeitungsgeschwindigkeit aus, da bei jedem Zugriff mit dem Speicherinhalt abgeglichen wird. Da bei AVR-Controllern innerhalb einer ISR keine Unterbrechungen zu erwarten sind, bietet es sich an, einen Zwischenspeicher in Form einer lokalen Variable zu verwenden, deren Inhalt zu Beginn und am Ende mit dem der volatile Variable synchronisiert wird. Lokale Variable werden bei eingeschalteter Optimierung mit hoher Wahrscheinlichkeit in Prozessorregistern verwaltet und der Zugriff darauf ist daher nur mit wenigen internen Operationen verbunden. Die ISR aus dem vorherigen Beispiel lässt sich so optimieren:
Zum Vergleich die Disassemblies (Ausschnitte der "lss-Dateien", compiliert für ATmega162) im Anschluss. Man erkennt den viermaligen Zugriff auf die Speicheraddresse von gKeyCounter (hier 0x032A) in der ISR ohne "Cache"-Variable und den zweimaligen Zugriff in der Variante mit Zwischenspeicher. Im Beispiel ist der Vorteil gering, bei komplexeren Routinen kann die Zwischenspeicherung in lokalen Variablen jedoch zu deutlicheren Verbesserungen führen.
ISR(TIMER1_COMPA_vect)
{
86a: 1f 92 push r1
86c: 0f 92 push r0
86e: 0f b6 in r0, 0x3f ; 63
870: 0f 92 push r0
872: 11 24 eor r1, r1
874: 8f 93 push r24
if ( !(KEY_PIN & (1<<KEY_PINNO)) ) {
876: ca 99 sbic 0x19, 2 ; 25
878: 0a c0 rjmp .+20 ; 0x88e <__vector_13+0x24>
if (gKeyCounter < CNTREPEAT) gKeyCounter++;
87a: 80 91 2a 03 lds r24, 0x032A
87e: 88 3c cpi r24, 0xC8 ; 200
880: 40 f4 brcc .+16 ; 0x892 <__vector_13+0x28>
882: 80 91 2a 03 lds r24, 0x032A
886: 8f 5f subi r24, 0xFF ; 255
888: 80 93 2a 03 sts 0x032A, r24
88c: 02 c0 rjmp .+4 ; 0x892 <__vector_13+0x28>
}
else {
gKeyCounter = 0;
88e: 10 92 2a 03 sts 0x032A, r1
892: 8f 91 pop r24
894: 0f 90 pop r0
896: 0f be out 0x3f, r0 ; 63
898: 0f 90 pop r0
89a: 1f 90 pop r1
89c: 18 95 reti
ISR(TIMER1_COMPA_vect)
{
86a: 1f 92 push r1
86c: 0f 92 push r0
86e: 0f b6 in r0, 0x3f ; 63
870: 0f 92 push r0
872: 11 24 eor r1, r1
874: 8f 93 push r24
uint8_t tmp_kc;
tmp_kc = gKeyCounter;
876: 80 91 2a 03 lds r24, 0x032A
if ( !(KEY_PIN & (1<<KEY_PINNO)) ) {
87a: ca 9b sbis 0x19, 2 ; 25
87c: 02 c0 rjmp .+4 ; 0x882 <__vector_13+0x18>
87e: 80 e0 ldi r24, 0x00 ; 0
880: 03 c0 rjmp .+6 ; 0x888 <__vector_13+0x1e>
if (tmp_kc < CNTREPEAT) {
882: 88 3c cpi r24, 0xC8 ; 200
884: 08 f4 brcc .+2 ; 0x888 <__vector_13+0x1e>
tmp_kc++;
886: 8f 5f subi r24, 0xFF ; 255
}
}
else {
tmp_kc = 0;
}
gKeyCounter = tmp_kc;
888: 80 93 2a 03 sts 0x032A, r24
88c: 8f 91 pop r24
88e: 0f 90 pop r0
890: 0f be out 0x3f, r0 ; 63
892: 0f 90 pop r0
894: 1f 90 pop r1
896: 18 95 reti
[bearbeiten] volatile und PointerBei volatile in Verbindung mit Pointern ist zu beachten, ob der Pointer selbst oder die Variable auf die der Pointer zeigt volatile ist.
Falls der Pointer volatile ist (zweiter Fall im Beispiel), ist zu beachten, dass der Wert des Pointers, also eine Speicheradresse, intern in mehr als einem Byte verwaltet wird. Lese- und Schreibzugriffe im Hauptprogramm (ausserhalb von Interrupt-Routinen) sind daher so zu implementieren, dass alle Teilbytes der Adresse konsistent bleiben, vgl. dazu den folgenden Abschnitt. [bearbeiten] Variablen größer 1 ByteBei Variablen größer ein Byte, auf die in Interrupt-Routinen und im Hauptprogramm zugegriffen wird, muss darauf geachtet werden, dass die Zugriffe auf die einzelnen Bytes außerhalb der ISR nicht durch einen Interrupt unterbrochen werden. (Allgemeinplatz: AVRs sind 8-bit Controller). Zur Veranschaulichung ein Codefragment:
Die avr-libc bietet ab Version 1.6.0(?) einige Hilfsfunktionen/Makros, mit der im Beispiel oben gezeigten Funktionalität, die zusätzlich auch so genannte memory barriers beinhalten. Diese stehen nach #include <util/atomic.h> zur Verfügung.
[bearbeiten] Interrupt-Routinen und RegisterzugriffeFalls Register sowohl im Hauptprogramm als auch in Interrupt-Routinen verändert werden, ist darauf zu achten, dass diese Zugriffe sich nicht überlappen. Nur wenige Anweisungen lassen sich in sogenannte "atomare" Zugriffe übersetzen, die nicht von Interrupt-Routinen unterbrochen werden können. Zur Veranschaulichung eine Anweisung, bei der ein Bit und im Anschluss drei Bits in einem Register gesetzt werden:
Der Compiler übersetzt diese Anweisungen für einen ATmega128 bei Optimierungsstufe "S" nach:
...
PORTA |= (1<<PA0);
d2: d8 9a sbi 0x1b, 0 ; 27 (a)
PORTA |= (1<<PA2)|(1<<PA3)|(1<<PA4);
d4: 8b b3 in r24, 0x1b ; 27 (b)
d6: 8c 61 ori r24, 0x1C ; 28 (c)
d8: 8b bb out 0x1b, r24 ; 27 (d)
...
Das Setzen des einzelnen Bits wird bei eingeschalteter Optimierung für Register im unteren Speicherbereich in eine einzige Assembler-Anweisung (sbi) übersetzt und ist nicht anfällig für Unterbrechnungen durch Interrupts. Die Anweisung zum Setzen von drei Bits wird jedoch in drei abhängige Assembler-Anweisungen übersetzt und bietet damit zwei "Angriffspunkte" für Unterbrechnungen. Eine Interrupt-Routine könnte nach dem Laden des Ausgangszustands in den Zwischenspeicher (hier Register 24) den Wert des Registers ändern, z.B. ein Bit löschen. Damit würde der Zwischenspeicher nicht mehr mit dem tatsächlichen Zustand übereinstimmen aber dennoch nach der Bitoperation (hier ori) in das Register zurückgeschrieben. Beispiel: PORTA sei anfangs 0b00000000. Die erste Anweisung (a) setzt Bit 0, PORTA ist danach 0b00000001. Nun wird im ersten Teil der zweiten Anweisung der Portzustand in ein Register eingelesen (b). Unmittelbar darauf (vor (c)) "feuert" ein Interrupt, in dessen Interrupt-Routine Bit 0 von PORTA gelöscht wird. Nach Verlassen der Interrupt-Routine hat PORTA den Wert 0b00000000. In den beiden noch folgenden Anweisungen des Hauptprogramms wird nun der zwischengespeicherte "alte" Zustand 0b00000001 mit 0b00011100 logisch-oder-verknüft (c) und das Ergebnis 0b00011101 in PortA geschrieben (d). Obwohl zwischenzeitlich Bit 0 gelöscht wurde, ist es nach (d) wieder gesetzt. Lösungsmöglichkeiten:
[bearbeiten] Was macht das Hauptprogramm?Im einfachsten (Ausnahme-)Fall gar nichts mehr. Es ist also durchaus denkbar, ein Programm zu schreiben, welches in der main-Funktion lediglich noch die Interrupts aktiviert und dann in einer Endlosschleife verharrt. Sämtliche Funktionen werden dann in den ISRs abgearbeitet. Diese Vorgehensweise ist jedoch bei den meisten Anwendungen schlecht: man verschenkt eine Verarbeitungsebene und hat außerdem möglicherweise Probleme durch Interruptroutinen, die zu viel Verarbeitungszeit benötigen. Normalerweise wird man in den Interruptroutinen nur die bei Auftreten des jeweiligen Interruptereignisses unbedingt notwendigen Operationen ausführen lassen. Alle weniger kritischen Aufgaben werden dann im Hauptprogramm abgearbeitet.
[bearbeiten] Sleep-ModesAVR Controller verfügen über eine Reihe von sogenannten Sleep-Modes ("Schlaf-Modi"). Diese ermöglichen es, Teile des Controllers abzuschalten. Zum Einen kann damit besonders bei Batteriebetrieb Strom gespart werden, zum Anderen können Komponenten des Controllers deaktiviert werden, die die Genauigkeit des Analog-Digital-Wandlers bzw. des Analog-Comparators negativ beeinflussen. Der Controller wird durch Interrupts aus dem Schlaf geweckt. Welche Interrupts den jeweiligen Schlafmodus beenden, ist einer Tabelle im Datenblatt des jeweiligen Controllers zu entnehmen. Die Funktionen (eigentlich Makros) der avr-libc stehen nach Einbinden der header-Datei sleep.h zur Verfügung.
Bei Anwendung von sleep_cpu() müssen Interrupts also bereits freigeben sein (sei()), da der Controller sonst nicht mehr "aufwachen" kann. sleep_mode() ist nicht geeignet für die Verwendung in ISR Interrupt-Service-Routinen, da bei deren Abarbeitung Interrupts global deaktiviert sind und somit auch die möglichen "Aufwachinterrupts". Abhilfe: stattdessen sleep_enable(), sei(), sleep_cpu(), sleep_disable() und evtl. cli() verwenden (vgl. Dokumentation der avr-libc).
In älteren Versionenen der avr-libc wurden nicht alle AVR-Controller durch die sleep-Funktionen richtig angesteuert. Mit avr-libc 1.2.0 wurde die Anzahl der unterstützten Typen jedoch deutlich erweitert. Bei nicht-unterstützten Typen erreicht man die gewünschte Funktionalität durch direkte "Bitmanipulation" der entsprechenden Register (vgl. Datenblatt) und Aufruf des Sleep-Befehls via Inline-Assembler oder sleep_cpu():
[bearbeiten] SpeicherzugriffeAtmel AVR-Controller verfügen typisch über drei Speicher:
Einige AVRs besitzen keinen RAM-Speicher, lediglich die Register können als "Arbeitsvariablen" genutzt werden. Da die Anwendung des avr-gcc auf solch "kleinen" Controllern ohnehin selten sinnvoll ist und auch nur bei einigen RAM-losen Typen nach "Bastelarbeiten" möglich ist, werden diese Controller hier nicht weiter berücksichtigt. Auch EEPROM-Speicher ist nicht auf allen Typen verfügbar. Generell sollten die nachfolgenden Erläuterungen auf alle ATmega-Controller und die größeren AT90-Typen übertragbar sein. Für die Typen ATtiny2313, ATtiny26 und viele weitere der "ATtiny-Reihe" gelten die Ausführungen ebenfalls. [bearbeiten] RAMDie Verwaltung des RAM-Speichers erfolgt durch den Compiler, im Regelfall ist beim Zugriff auf Variablen im RAM nichts Besonderes zu beachten. Die Erläuterungen in jedem brauchbaren C-Buch gelten auch für den vom avr-gcc-Compiler erzeugten Code. Um Speicher dynamisch (während der Laufzeit) zu reservieren, kann malloc() verwendet werden. malloc(size) "alloziert" (~reserviert) einen gewissen Speicherblock mit size Bytes. Ist kein Platz für den neuen Block, wird NULL (0) zurückgegeben. Wird der angelegte Block zu klein (groß), kann die Größe mit realloc() verändert werden. Den allozierten Speicherbereich kann man mit free() wieder freigeben. Wenn das Freigeben eines Blocks vergessen wird spricht man von einem "Speicherleck" (memory leak). malloc() legt Speicherblöcke im Heap an, belegt man zuviel Platz, dann wächst der Heap zu weit nach oben und überschreibt den Stack, und der Controller kommt in Teufels Küche. Das kann leider nicht nur passieren wenn man insgesamt zu viel Speicher anfordert, sondern auch wenn man Blöcke unterschiedlicher Größe in ungünstiger Reihenfolge alloziert/freigibt (siehe Artikel Heap-Fragmentierung). Aus diesem Grund sollte man malloc() auf Mikrocontrollern sehr sparsam (am besten gar nicht) verwenden. Beispiel zur Verwendung von malloc():
Wenn (wie in obigem Beispiel) dynamischer Speicher nur für die Dauer einer Funktion benötigt und am Ende wieder freigegeben wird, bietet es sich an, statt malloc() alloca() zu verwenden. Der Unterschied zu malloc() ist, dass der Speicher auf dem Stack reserviert wird, und beim Verlassen der Funktion automatisch wieder freigegeben wird. Es kann somit kein Speicherleck und keine Fragmentierung entstehen. siehe auch: [bearbeiten] Programmspeicher (Flash)Ein Zugriff auf Konstanten im Programmspeicher ist mittels avr-gcc nicht "transparent" möglich. D.h. es sind besondere Zugriffsfunktionen erforderlich, um Daten aus diesem Speicher zu lesen. Grundsätzlich basieren alle Zugriffsfunktionen auf der Assembler-Anweisung lpm (load program memory, bei AVR Controllern mit mehr als 64kB Flash auch elpm). Die Standard-Laufzeitbibliothek des avr-gcc (die avr-libc) stellt diese Funktionen nach Einbinden der Header-Datei pgmspace.h zur Verfügung. Mit diesen Funktionen können einzelne Bytes, Datenworte (16bit) und Datenblöcke gelesen werden. Deklarationen von Variablen im Flash-Speicher werden durch das "Attribut" PROGMEM ergänzt. Lokale Variablen (eigentlich Konstanten) innerhalb von Funktionen können ebenfalls im Programmspeicher abgelegt werden. Dazu ist bei der Definition jedoch ein static voranzustellen, da solche "Variablen" nicht auf dem Stack bzw. (bei Optimierung) in Registern verwaltet werden können. Der Compiler "wirft" eine Warnung falls static fehlt.
[bearbeiten] Byte lesenMit der Funktion pgm_read_byte aus pgmspace.h erfolgt der Zugriff auf die Daten. Parameter der Funktion ist die Adresse des Bytes im Flash-Speicher.
[bearbeiten] Wort lesenFür "einfache" 16-bit breite Variablen erfolgt der Zugriff analog zum Byte-Beispiel, jedoch mit der Funktion pgm_read_word.
Zeiger auf Werte im Flash sind ebenfalls 16 Bits "groß" (Stand avr-gcc 3.4.x). Damit ist der mögliche Speicherbereich für "Flash-Konstanten" auf 64kB begrenzt.
[bearbeiten] Strings lesenStrings sind in C ja nichts anderes als eine Abfolge von Zeichen. Der prinzipielle Weg ist daher identisch zu "Bytes lesen" wobei allerdings auf die Besonderheiten von Strings (0-Terminierung) geachtet werden muss, bzw. diese zur Steuerung einer Schleife über die Zeichen im String ausgenutzt werden kann
Zur Unterstützung des Programmierers steht das Repertoir der str... Funktionen auch in jeweils eine Variante zur Verfügung, die mit dem Flash Speicher arbeiten kann. Die Funktionsnamen wurden dabei um ein '_P' ergänzt.
[bearbeiten] Floats und Structs lesenUm komplexe Datentypen (structs), nicht-integer Datentypen (floats) aus dem Flash auszulesen, sind Hilfsfunktionen erforderlich. Einige Beispiele:
[bearbeiten] Array aus Strings im Flash-SpeicherArrays aus Strings im Flash-Speicher werden in zwei Schritten angelegt: Zuerst die einzelnen Elemente des Arrays und im Anschluss ein Array, in dem die Startaddressen der Strings abgelegt werden. Zum Auslesen wird zuerst die Adresse des i-ten Elements aus dem Array im Flash-Speicher gelesen, die im Anschluss dazu genutzt wird, auf das Element (den String) selbst zuzugreifen.
Siehe dazu auch die avr-libc FAQ: "How do I put an array of strings completely in ROM?" [bearbeiten] Vereinfachung für Zeichenketten (Strings) im FlashZeichenketten können innerhalb des Quellcodes als "Flash-Konstanten" ausgewiesen werden. Dazu dient das Makro PSTR aus pgmspace.h. Dies erspart die getrennte Deklaration mit PROGMEM-Attribut.
Aber Vorsicht: Ersetzt man zum Beispiel
durch
dann kann es zu Problemen mit AVR-GCC kommen. Zu erkennen daran, dass der Initialisierungsstring von "textImFlashProblem" zu den Konstanten ans Ende des Programmcodes gelegt wird (BSS), von dem aus er zur Benutzung eigentlich ins RAM kopiert werden sollte (und wird). Da der lesende Code (mittels pgm_read*) trotzdem an einer Stelle vorne im Flash sucht, wird Unsinn gelesen. Dies scheint ein weiters Problem des AVR-GCC (gesehen bei avr-gcc 3.4.1 und 3.4.2) bei der Anpassung an die Harvard-Architektur zu sein (konstanter Pointer auf variable Daten?!). Abhilfe ("Workaround"): Initialisierung bei Zeichenketten mit [] oder gleich im Code PSTR("...") nutzen. Übergibt man Zeichenketten (genauer: die Adresse des ersten Zeichens), die im Flash abglegt sind an eine Funktion, muss diese entsprechend programmiert sein. Die Funktion selbst hat keine Möglichkeit zu unterscheiden, ob es sich um eine Adresse im Flash oder im RAM handelt. Die avr-libc und viele andere avr-gcc-Bibliotheken halten sich an die Konvention, dass Namen von Funktionen die Flash-Adressen erwarten mit dem Suffix _p (oder _P) versehen sind. Eine Funktion, die einen im Flash abgelegten String z.B. an eine UART ausgibt, würde dann so aussehen:
Von einigen Bibliotheken werden Makros definiert, die "automatisch" ein PSTR bei Verwendung einer Funktion einfügen. Ein Blick in den Header-File der Bibliothek zeigt, ob dies der Fall ist. Ein Beispiel aus P. Fleurys lcd-Library:
[bearbeiten] Flash in der Anwendung schreibenBei AVRs mit "self-programming"-Option (auch bekannt als Bootloader-Support) können Teile des Flash-Speichers auch vom Anwendungsprogramm selbst beschrieben werden. Dies ist nur möglich, wenn die Schreibfunktionen in einem besonderen Speicherbereich (boot-section) des Programmspeichers/Flash abgelegt sind. Bei wenigen "kleinen" AVRs gibt es keine gesonderte Boot-Section, bei diesen kann der Flashspeicher von jeder Stelle des Programms geschrieben werden. Für Details sei hier auf das jeweilige Controller-Datenblatt und die Erläuterungen zum Modul boot.h der avr-libc verwiesen. Es existieren auch Application-Notes dazu bei atmel.com, die auf avr-gcc-Code übertragbar sind. [bearbeiten] Warum so kompliziert?Zu dem Thema, warum die Verabeitung von Werten aus dem Flash-Speicher so "kompliziert" ist, sei hier nur kurz erläutert: Die Harvard-Architektur des AVR weist getrennte Adressräume für Programm(Flash)- und Datenspeicher(RAM) auf. Der C-Standard und der gcc-Compiler sehen keine unterschiedlichen Adressräume vor. Hat man zum Beispiel eine Funktion string_an_uart(const char* s) und übergibt an diese Funktion die Adresse einer Zeichenkette (einen Pointer, z.B. 0x01fe), "weiß" die Funktion nicht, ob die Adresse auf den Flash-Speicher oder den/das RAM zeigt. Allein aus dem Pointer-Wert (der Zahl) kann nicht geschlossen werden, ob ein "einfaches" zeichen_an_uart(s[i]) oder zeichen_an_uart(pgm_read_byte(&s[i]) genutzt werden muss, um das i-te Zeichen auszugeben. Einige AVR-Compiler "tricksen" etwas, in dem sie für einen Pointer nicht nur die Adresse anlegen, sondern zusätzlich zu jedem Pointer den Ablageort (Flash oder RAM) intern sichern. Bei Aufruf einer Funktion wird dann bei Pointer-Parametern neben der Adresse auch der Speicherbereich, auf den der Pointer zeigt, übergeben. Dies hat jedoch nicht nur Vorteile; Erläuterungen warum dies so ist, führen an dieser Stelle zu weit.
[bearbeiten] EEPROMMan beachte, dass der EEPROM-Speicher nur eine begrenzte Anzahl von Schreibzugriffen zulässt. Beschreibt man eine EEPROM-Zelle öfter als die im Datenblatt zugesicherte Anzahl (typisch 100.000), wird die Funktion der Zelle nicht mehr garantiert. Dies gilt für jede einzelne Zelle. Bei geschickter Programmierung (z.B. Ring-Puffer), bei der die zu beschreibenden Zellen regelmäßig gewechselt werden, kann man eine deutlich höhere Anzahl an Schreibzugriffen, bezogen auf den Gesamtspeicher, erreichen. Schreib- und Lesezugriffe auf den EEPROM-Speicher erfolgen über die im Modul eeprom.h definierten Funktionen. Mit diesen Funktionen können einzelne Bytes, Datenworte (16bit) und Datenblöcke geschrieben und gelesen werden. Bei Nutzung des EEPROMs ist zu beachten, dass vor dem Zugriff auf diesen Speicher abgefragt wird, ob der Controller die vorherige EEPROM-Operation abgeschlossen hat. Die avr-libc-Funktionen beinhalten diese Prüfung, man muss sie nicht selbst implementieren. Man sollte auch verhindern, dass der Zugriff durch die Abarbeitung einer Interrupt-Routine unterbrochen wird, da bestimme Befehlsabfolgen vorgegeben sind, die innerhalb weniger Taktzyklen aufeinanderfolgen müssen ("timed sequence"). Auch dies muss bei Nutzung der Funktionen aus der avr-libc/eeprom.h-Datei nicht selbst implementiert werden. Innerhalb der Funktionen werden Interrupts vor der "EEPROM-Sequenz" global deaktiviert und im Anschluss, falls vorher auch schon eingeschaltet, wieder aktiviert. Bei der Deklaration einer Variable im EEPROM, ist das Attribut für die Section ".eeprom" zu ergänzen. Siehe dazu folgendes Beispiel:
[bearbeiten] Bytes lesen/schreibenDie avr-libc Funktion zum Lesen eines Bytes heißt eeprom_read_byte. Parameter ist die Adresse des Bytes im EEPROM. Geschrieben wird über die Funktion eeprom_write_byte mit den Parametern Adresse und Inhalt. Anwendungsbeispiel:
[bearbeiten] Wort lesen/schreibenSchreiben und Lesen von Datenworten erfolgt analog zur Vorgehensweise bei Bytes:
[bearbeiten] Block lesen/schreibenLesen und Schreiben von Datenblöcken erfolgt über die Funktionen eeprom_read_block() bzw. eeprom_write_block(). Die Funktionen erwarten drei Parameter: die Adresse der Quell- bzw. Zieldaten im RAM, die EEPROM-Addresse und die Länge des Datenblocks in Bytes (size_t). TODO: Vorsicht! die folgenden Beispiele sind noch nicht geprüft, erstmal nur als Hinweis auf "das Prinzip". Evtl. fehlen "casts" und möglicherweise noch mehr.
"Nicht-Integer"-Datentypen wie z.B. Fließkommazahlen lassen sich recht praktisch über eine union in "Byte-Arrays" konvertieren und wieder "zurückwandeln". Dies erweist sich hier (aber nicht nur hier) als nützlich.
Auch zusammengesetzte Typen lassen sich mit den Block-Routinen verarbeiten.
[bearbeiten] EEPROM-Speicherabbild in .eep-DateiMit den zum Compiler gehörenden Werkzeugen kann der aus den Variablendeklarationen abgeleitete EEPROM-Inhalt in eine Datei geschrieben werden (übliche Dateiendung: .eep, Daten im Intel Hex-Format). Damit können recht elegant Standardwerte für den EEPROM-Inhalt im Quellcode definiert werden. Makefiles nach WinAVR/MFile-Vorlage enthalten bereits die notwendigen Einstellungen (siehe dazu die Erläuterungen im Abschnitt Exkurs: Makefiles). Der Inhalt der eep-Datei muss ebenfalls zum Mikrocontroller übertragen werden (Write EEPROM), wenn die Initialisierungswerte aus der Deklaration vom Programm erwartet werden. Ansonsten enthält der EEPROM-Speicher nach der Übertragung des Programmers mittels ISP abhängig von der Einstellung der EESAVE-Fuse (vgl. Datenblatt Abschnitt Fuse Bits) die vorherigen Daten (EESAVE programmed = 0), deren Position möglicherweise nicht mehr mit der Belegung im aktuellen Programm übereinstimmt oder den Standardwert nach "Chip Erase": 0xFF (EESAVE unprogrammed = 1). Als Sicherung kann man im Programm nochmals die Standardwerte vorhalten, beim Lesen auf 0xFF prüfen und gegebenfalls einen Standardwert nutzen.
[bearbeiten] EEPROM-Variable auf feste Adressen legenGleich zu Beginn möchte ich darauf hinweisen, dass dieses Verfahren nur ein Workaround ist, mit dem man das Problem der anscheinend "zufälligen" Verteilung der EEPROM-Variablen durch den Compiler etwas in den Griff bekommen kann. Hilfreich kann dies vor allem dann sein, wenn man z.B. über einen Kommandointerpreter (o.ä. Funktionen) direkt bestimmte EEPROM-Adressen manipulieren möchte. Auch wenn man über einen JTAG-Adapter (mk I oder mkII) den Programmablauf manipulieren möchte, indem man die EEPROM-Werte direkt ändert, kann diese Technik hilfreich sein. Im folgenden nun zwei Sourcelistings mit einem Beispiel:
Mit den Macros #define EE_VALUE1 legt man den Namen und die Adresse der 'Variablen' fest. WICHTIG:Die Adressen sollten fortlaufend, zumindest aber aufsteigend sortiert sein! Ansonsten besteht die Gefahr, daß man sehr schnell ein Durcheinander im EEPROM Speicher veranstaltet. WICHTIG:Für den Compiler sind das lediglich Speicher-Adressen, über die auf das EEPROM zugegriffen wird. Der Compiler sieht nichts davon als eine echte Variable an und stößt sich daher auch nicht daran, wenn 2 Makros mit der gleichen Speicheradresse, bzw. überlappenden Speicherbereichen definiert werden. Es liegt einzig und alleine in der Hand des Programmierers, hier keinen Fehler zu machen.
Durch die Verwendung eines Array, welches das gesamte EEPROM umfasst, bleibt dem Compiler nicht anderes übrig, als das Array so zu platzieren, dass Element 0 des Arrays der Adresse 0 des EEPROMs entspricht. (Ich hoffe nur, dass die Compilerbauer daran nichts ändern!) Wie man in dem obigen Codelisting auch sehen kann, hat das Verfahren einen kleinen Haken. Variablen die größer sind als 1 Byte, müssen etwas umständlicher definiert werden. Benötigt man keine Initialisierung durch das Programm (was der Normalfall sein dürfte), dann kann man das auch so machen: Möchte man im EEPROM hintereinander beispielsweise Variablen, mit den Namen Wert, Anzahl, Name und Wertigkeit definieren, wobei Wert und Wertigkeit 1 Byte belegen sollen, Anzahl als 1 Wort (also 2 Bytes) und Name mit 10 Bytes reserviert werden soll, so geht auch folgendes:
Jedes Makro definiert also seine Startadresse durch die Startadresse der unmittelbar vorhergehende 'Variablen' plus der Anzahl der Bytes die von der vorhergehenden 'Variablen' verbraucht werden. Dadurch ist man zumindest etwas auf der sicheren Seite, dass keine 2 'Variablen' im EEPROM überlappend definiert werden. Möchte man eine weitere 'Variable' hinzufügen, so wird deren Name, einfach anstelle der EE_LAST eingesetzt und eine neue Zeile für EE_LAST eingefügt, in der dann die Größe der 'Variablen' festgelegt wird. Zb.
EE_PROZENT legt die Startadresse für eine neue 'Variable' des Datentyps double fest. Der Zugriff auf die EEPROM Werte kann dann z.B.so erfolgen:
Ob die in der avr-libc vorhandenen Funktionen dafür verwendet werden können, weiß ich nicht. Aber in einigen Fällen muss man sich sowieso eigene Funktionen bauen, welche die spezifischen Anforderungen (Interrupt - Atom Problem, etc.) erfüllen. Die oben beschriebene Möglichkeit ist nur eine Möglichkeit, wie man dies realisieren kann. Sie bietet einem eine relativ einfache Art die EEPROM-Werte auf beliebige Adressen zu legen oder Adressen zu ändern. Die Andere Möglichkeit besteht darin, die EEPROM-Werte wie folgt zu belegen:
Hierbei kann man Variablen, die größer sind als 1 Byte einfacher definieren und man muss nur die Highbyte- oder Lowbyte-Adresse in der "eeprom.h" definieren. Allerdings muss man hier höllisch aufpassen, dass man nicht um eine oder mehrere Positionen verrutscht! Welche der beiden Möglichkeiten man einsetzt, hängt vor allem davon ab, wieviele Byte, Word und sonstige Variablen man benutzt. Gewöhnen sollte man sich an beide Varianten können ;) Kleine Schlussbemerkung:
[bearbeiten] Bekannte Probleme bei den EEPROM-FunktionenVorsicht: Bei alten Versionen der avr-libc wurden nicht alle AVR Controller untersützt. Z.B. bei der avr-libc Version 1.2.3 insbesondere bei AVRs "der neuen Generation" (ATmega48/88/168/169) funktionieren die Funktionen nicht korrekt (Ursache: unterschiedliche Speicheradressen der EEPROM-Register). In neueren Versionen (z.B. avr-libc 1.4.3 aus WinAVR 20050125) wurde die Zahl der unterstüzten Controller deutlich erweitert und eine Methode zur leichten Anpassung an zukünftige Controller eingeführt. In jedem Datenblatt zu AVR-Controllern mit EEPROM sind kurze Beispielecodes für den Schreib- und Lesezugriff enthalten. Will oder kann man nicht auf die neue Version aktualisieren, kann der dort gezeigte Code auch mit dem avr-gcc (ohne avr-libc/eeprom.h) genutzt werden ("copy/paste", gegebenfalls Schutz vor Unterbrechnung/Interrupt ergänzen uint8_t sreg; sreg=SREG; cli(); [EEPROM-Code] ; SREG=sreg; return;, siehe Abschnitt Interrupts). Im Zweifel hilft ein Blick in den vom Compiler erzeugten Assembler-Code (lst/lss-Dateien).
[bearbeiten] EEPROM RegisterUm das EEPROM anzusteuern sind drei Register von Bedeutung. Das EECR steuert den Zugriff auf das EEPROM und ist wie folgt aufgebaut:
Bedeutung der Bits: Bit 3 (EERIE) - EEPROM Ready Interrupt Enable: Wenn das Bit gesetzt ist und globale Interrupts erlaubt sind in Register SREG (Bit 7) wird ein Interrupt ausgelöst nach Beendigung des Schreibzyklus (EEPROM Ready Interrupt). Ist einer der beiden Bits 0 wird kein Interrupt ausgelöst. Bit 2(EEMWE) - EEPROM Master Write Enable: Dieses Bit bestimmt, daß wenn EEWE = 1 gesetzt wird (innerhalb von 4 Taktzyklen), das EEPROM beschrieben wird mit den Daten in EEDR bei Adresse EEAR. Wenn EEMWE =0 ist und EEWE = 1 gesetzt wird hat das keine Auswirkungen. Der Schreibvorgang wird dann nicht ausgelöst.
Nach 4 Taktzyklen wird das Bit EEMWE automatisch wieder auf 0 gesetzt. Dieses Bit löst den Schreibvorgang nicht aus, es dient sozusagen als Sicherungsbit für EEWE. Bit 1 (EEWE) - EEPROM Write Enable: Dieses Bit löst den Schreibvorgang aus wenn es auf 1 gesetzt wird, sofern vorher EEMWE gesetzt wurde und seitdem nicht mehr als 4 Taktzyklen vergangen sind. Wenn der Schreibvorgang abgeschlossen ist wird dieses Bit automatisch wieder auf 0 gesetzt und sofern EERIE gesetzt ist ein Interrupt ausgelöst. [bearbeiten] Die Nutzung von sprintf und printfUm komfortabel, d.h. formatiert, Ausgaben auf ein Display oder die serielle Schnittstelle zu tätigen, bieten sich sprintf oder printf an. Alle *printf-Varianten sind jedoch ziemlich speicherintensiv und der Einsatz in einem Mikrocontroller mit knappem Speicher muss sorgsam abgewogen werden. Bei sprintf wird die Ausgabe zunächst in einem Puffer vorbereitet und anschliessend mit einfachen Funktionen zeichenweise ausgegeben. Es liegt in der Verantwortung des Programmierers genügend Platz im Puffer für die erwarteten Zeichen bereitzuhalten.
Eine weitere elegante Möglichkeit besteht darin, den STREAM stdout (Standardausgabe) auf eine eigene Ausgabefunktion umzuleiten. Dazu wird dem Ausgabemechanismus der C-Bibliothek eine neue Ausgabefunktion bekannt gemacht, deren Aufgabe es ist, ein einzelnes Zeichen auszugeben. Wohin die Ausgabe dann tatsächlich stattfindet, ist Sache der Ausgabefunktion. Im Beispiel unten wird auf UART ausgegeben. Alle anderen, höheren Funktionen wie z.B. printf greifen letztendlich auf diese primitive Ausgabefunktion zurück.
[bearbeiten] Assembler und Inline-AssemblerGelegentlich erweist es sich als nützlich, C- und Assembler-Code in einer Anwendung zu nutzen. Typischerweise wird das Hauptprogramm in C verfasst und wenige, extrem zeitkritische oder hardwarenahe Operationen in Assembler. Die "gnu-Toolchain" bietet dazu zwei Möglichkeiten:
[bearbeiten] Inline-AssemblerInline-Assembler bietet sich an, wenn nur wenig Assembleranweisungen benötigt werden. Typische Anwendung sind kurze Codesequenzen für zeitkritische Operationen in Interrupt-Routinen oder sehr präzise Warteschleifen (z.B. 1-Wire). Inline-Assembler wird mit asm volatile eingeleitet, die Assembler-Anweisungen werden in einer Zeichenkette zusammengefasst, die als "Parameter" übergeben wird. Durch Doppelpunkte getrennt werden die Ein- und Ausgaben sowie die "Clobber-Liste" angegeben. Ein einfaches Beispiel für Inline-Assembler ist das Einfügen einer NOP-Anweisung (NOP steht für No Operation). Dieser Assembler-Befehl benötigt genau einen Taktzyklus, ansonsten "tut sich nichts". Sinnvolle Anwendungen für NOP sind genaue Delay(=Warte)-Funktionen.
Weiterhin kann mit einem NOP verhindert werden, dass leere Schleifen, die als Warteschleifen gedacht sind, wegoptimiert werden. Der Compiler erkennt ansonsten die vermeintlich nutzlose Schleife und erzeugt dafür keinen Code im ausführbaren Programm.
Ein weiterer nützlicher "Assembler-Einzeiler" ist der Aufruf von sleep (asm volatile ("sleep");), da hierzu in älteren Versionen der avr-libc keine eigene Funktion existiert (in neueren Versionen sleep_cpu() aus sleep.h). Als Beispiel für mehrzeiligen Inline-Assembler eine präzise Delay-Funktion. Die Funktion erhält ein 16-bit Wort als Parameter, prüft den Parameter auf 0 und beendet die Funktion in diesem Fall oder durchläuft die folgende Schleife sooft wie im Wert des Parameters angegeben. Inline-Assembler hat hier den Vorteil, dass die Laufzeit unabhängig von der Optimierungsstufe (Parameter -O, vgl. makefile) und der Compiler-Version ist.
Das Resultat zeigt ein Blick in die Assembler-Datei, die der Compiler mit der option -save-temps nicht löscht:
Detaillierte Ausführungen zum Thema Inline-Assembler finden sich in der Dokumentation der avr-libc im Abschnitt Related Pages/Inline Asm. Siehe auch: [bearbeiten] Assembler-DateienAssembler-Dateien erhalten die Endung .S (grosses S) und werden im makefile nach WinAVR/mfile-Vorlage hinter ASRC= durch Leerzeichen getrennt aufgelistet. Wenn man mit dem AVR Studio arbeitet, kann alternativ auch das standardmäßig erstellte Makefile bearbeitet und folgende Zeilen eingefügt werden: ## Objects that must be built in order to link OBJECTS = (alte Dateien...) useful.o ## Compile ## Hier folgt eine Liste der gelinkten Dateien, darunter einfügen: useful.o: ../useful.S $(CC) $(INCLUDES) $(ASMFLAGS) -c $< Das war es schon. Allerdings gilt es zu beachten, dass das makefile über "Project -> Configuration options" selbst einzubinden ist, sonst wird es natürlich wieder überschrieben. Im Beispiel eine Funktion superFunc, die alle Pins des Ports D auf "Ausgang" schaltet, eine Funktion ultraFunc, die die Ausgänge entsprechend des übergebenen Parameters schaltet, eine Funktion gigaFunc, die den Status von Port A zurückgibt und eine Funktion addFunc, die zwei Bytes zu einem 16-bit-Wort addiert. Die Zuweisungen im C-Code (PORTx = ...) verhindern, dass der Compiler die Aufrufe wegoptimiert und dienen nur zur Veranschaulichung der Parameterübergaben. Zuerst der Assembler-Code. Der Dateiname sei useful.S:
Im Makefile ist der Name der Assembler-Quellcodedatei einzutragen: ASRC = useful.S Der Aufruf erfolgt dann im C-Code so:
Das Ergebnis wird wied |