(→Fazit) |
K (→Fazit) |
||
Zeile 1.406: | Zeile 1.406: | ||
Oder wenn ein 16-Bit-Wert übergeben wird sieht es so aus: | Oder wenn ein 16-Bit-Wert übergeben wird sieht es so aus: | ||
<pre> | <pre> | ||
− | void wait(unsigned short num) | + | void wait (unsigned short num) |
{ | { | ||
unsigned short i = 0; | unsigned short i = 0; | ||
__asm__ __volatile ( | __asm__ __volatile ( | ||
− | "0:" | + | "0:" "\n\t" |
"cp %A0, %A1" "\n\t" | "cp %A0, %A1" "\n\t" | ||
"cpc %B0, %B1" "\n\t" | "cpc %B0, %B1" "\n\t" |
Version vom 29. Dezember 2005, 17:37 Uhr
Die GNU Compiler Collection (GCC) unterstützt das Zielsystem (Target) AVR für die Sprachen C und C++. GCC ist ein sehr leistungsfähiger Compiler, und er kann als die wichtigste freie Software überhaupt bezeichnet werden. Immerhin sind das freie Betriebssystem Linux und viele andere Programme auch gcc und die Toolchains selbst mit gcc generiert.
Inhaltsverzeichnis
- 1 How to Read
- 2 Benutzer-Schnittstelle
- 3 Allgemeine Charakteristika von avr-gcc
- 4 Ablauf der Codegenerierung
- 5 Kommandozeilen-Optionen
- 6 Builtin Defines
- 7 Sections
- 8 Attribute
- 9 Dynamische Speicherallokierung
- 10 Code-Beispiele
- 11 Includes
- 12 Dateien (WinAVR)
- 13 Fallstricke und häufige Fehler
- 14 Abkürzungen und Bezeichnungen
- 15 Siehe auch
- 16 Weblinks
- 17 ToDo
- 18 Autor
How to Read
Dieser Artikel bespricht avr-gcc. Er ist kein Tutorial und kein AVR-Handbuch das würde den Umfang des Artikels bei weitem sprengen.
Der Artikel ist ein Handbuch zu avr-gcc. Er bespricht zum Beispiel, wie man avr-gcc anwendet und Besonderheiten von avr-gcc-C, die nicht zum Sprachumfang von C gehören. Dazu zählen die Definition von Interrupt Service Routinen (ISRs) oder wie man Daten ins EEPROM legt.
Es wird also besprochen, wie eine ISR zu definieren ist, aber nicht, warum das gegebenenfalls notwendig oder nicht notwendig ist. Warum etwas gemacht wird, ist abhängig von der gestellten Aufgabe, etwa "Initialisiere den UART zur Benutzung mit 9600 Baud". Dafür enthält dieser Artikel zusammen mit dem AVR-Handbuch das Rüstzeug, bietet aber keine Lösungen für konkrete Aufgaben.
In den C-Codebeispielen befindet sich das ausführlichere Beispiel "Blinky", das nur eine LED blinkt und zeigt, wie ein kleines Projekt mit avr-gcc compiliert werden kann.
Benutzer-Schnittstelle
Die Benutzer-Schnittstelle von GCC ist die Kommandozeile einer Shell, Console bzw. Eingabeaufforderung.
Im einfachsten Fall sieht ein Aufruf von avr-gcc also so aus:
> avr-gcc
Dabei das '>' nicht mittippen, und ein ENTER am Ende der Zeile drücken. Die Antwort bei korrekter Installation ist dann
avr-gcc: no input files
Was bedeutet: das Programm avr-gcc wurde vom Betriebssystem gefunden und konnte/durfte gestartet werden. Dann gibt avr-gcc eine Fehlermeldung aus und beendet die Ausführung, weil er keine Eingabedatei(en) bekommen hat was ja auch stimmt. Soweit ist also alles in Butter.
GCC war immer Kommandozeilen-orientiert und wird es auch immer bleiben, denn das hat gute Gründe:
- ein Compiler ist ein Compiler (und keine grafische Bedienschnittstelle)
- die Plattformabhängigkeit wird auf ein Minimum reduziert
- es gibt die Möglichkeit, GCC per Skript oder make zu starten
- GCC kann durchaus in eine Umgebung integriert werden: in einen Editor oder in eine GUI wie neuere Versionen von AVR-Studio, etc. Der GCC-Aufruf kann sogar von einem Server-Socket oder einer Web-Application heraus erfolgen, welche ein C-Programm empfängt, es von GCC übersetzen lässt, und das Resultat zurückschickt oder sonst was damit anstellt.
- Lizenzgründe: eine Umgebung, die GCC integriert, kann durchaus proprietär oder nicht quelloffen sein und muss nicht der GPL unterliegen.
Allgemeine Charakteristika von avr-gcc
- Groß- und Kleinschreibung
- C unterscheidet generell zwischen Groß- und Kleinschreibung, sowohl bei Variablen- und Funktionsnamen, bei Sprungmarken als auch bei Makros, und je nach Betriebssystem auch bei Pfad- und Dateinamen/Dateierweiterungen.
- Größe des Typs int
- Der Standard-Typ int ist 16 Bit groß
- Größe von Pointern
- Ein Pointer (Zeiger) ist 16 Bit groß
- Endianess
- avr-gcc implementiert Datentypen als little-endian, d.h. bei Datentypen, die mehrere Bytes groß sind, wird das niederwertigste Byte an der niedrigsten Adresse gespeichert. Dies gilt auch für Adressen und deren Ablage auf dem Stack sowie die Ablage von Werten, die mehrere Register belegen.
Binäre Konstanten
Einige Versionen von avr-gcc ermöglichen die Verwendung binärer Konstanten für 8-Bit-Werte:
unsigned char value = 0b00000010;
Davon sollte man absehen, denn zum einen hat man schnell eine 0 zu wenig oder zu viel getippselt, es ist kein Standard-C und man hat die leserlichere Alternative
unsigned char value = (1<<1);
Registerverwendung
- R0
- ein temporäres Register, in dem man rumwutzen darf
- R1
- enthält immer den Wert 0
- R2 R17, R28, R29
- allgemeine Register, die durch einen Funktionsaufruf nicht verändert bzw wieder auf den ursprünglichen Wert restauriert werden
- R18 R27, R30, R31
- können durch Funktionsaufrufe verändert werden
- R28 R29 (Y-Reg)
- enthält den Framepointer, sofern benötigt
Ablauf der Codegenerierung
Die Code-Erzeugung durch avr-gcc geschieht in mehreren, voneinander unabhängigen Schritten. Diese Schritte sind für den Anwender nicht immer erkennbar, und es auch nicht unbedingt notwendig, sie zu kennen. Für ein besseres Verständnis der Code-Generierung und zur Einordnung von Fehlermeldungen ist eine Kenntnis aber hilfreich.
Übersichts-Grafik
Schritte der Codegenerierung
Ohne die Angabe spezieller Optionen werden die Zwischenformate nur als temporäre Dateien angelegt und nach Beenden des gcc-Laufs wieder gelöscht. Dadurch fällt die Aufgliederung in Unterschritte nicht auf. In diesem Falle müssen Assembler und Linker/Locator auch nicht extra aufgerufen werden, sondern die Aufrufe erfolgen durch gcc. Ausnahme ist avr-objcopy, welches immer aufgerufen werden muss, wenn man z.B. eine HEX-Datei haben möchte.
- Precompileren
- Alle Preprozessor-Direktiven werden aufgelöst. Dazu gehören Direktiven wie
#include <avr/io.h> #include "meinzeug.h" #define MAKRONAME ERSATZTEXT #if !defined(__AVR__) #error einen Fehler ausgeben und abbrechen #else /* Alles klar, wir koennen loslegen mit C-Code fuer AVR */ #endif MAKRONAME
- Precompilieren besteht also nur aus reinem Textersatz: Auflösen von Makros, kopieren von anderen Dateien in die Quelle, etc.
- Compilieren
- In diesem Schritt geschieht der eigentliche Compilier-Vorgang: avr-gcc übersetzt die reine, precompilierte C-Quelle (*.i): Die Quelle wird auf Syntax-Fehler geprüft, es werden Optimierungen gemacht, und das übersetzte C-Programm als Assembler-Datei in (*.s) gespeichert.
- Assemblieren
- Der Assembler (avr-as) übersetzt den Assembler-Code (*.s) in das AVR-eigene Objektformat elf32-avr (*.o). Das Objekt enthält schon Maschinen-Code. Zusätzlich gibt es aber noch Lücken, die erst später gefüllt werden und Debug-Informationen und ganz viel anderes Zeug.
- Linken und Lokatieren
- Der Linker (avr-ld) bindet die angegebenen Objekte (*.o) zusammen und löst externe Referenzen auf. Der Linker entscheidet anhand der Beschreibung im Linker-Script, in welchen Speicheradressen und Sektionen die Daten landen: er lokatiert (von location, locate (en)). Module aus Bibliotheken (*.a) werden hinzugebunden (z.B. printf) und die elf32-avr Ausgabedatei (üblicherweise *.elf) erzeugt.
- Umwandeln ins gewünschte Objekt-Format
- Linker und Assembler erzeugen ihre Ausgabe im Objektformat elf32-avr. Wird ein anderes Objektformat wie Intel-HEX (*.hex), binary (*.bin) oder srec (*.srec) benötigt, kann avr-objcopy dazu verwendet werden, um diese zu erstellen. Der Inhalt einzelner Sections kann gezielt umkopiert oder ausgeblendet werden, so daß Dateien erstellt werden können, die nur den Inhalt des Flashs (Section .text) oder des EEPROMs (Section .eeprom) repräsentieren. Durch das Umwandeln in ein anderes Objektformat gehen üblicherweise Informationen wie Debug-Informationen verloren.
Kommandozeilen-Optionen
Die Codegenerierung bei avr-gcc wird über Kommandozeilen-Optionen gesteuert. Diese legen fest, für welchen Controller Code zu erzeugen ist, wie stark optimiert wird, ob Debug-Informationen erzeugt werden, etc. Die Optionen teilen sich in zwei Gruppen: Optionen, die für alle GCC-Ports verfürgbar sind und maschinenspezifische Optionen, die nur für AVR verfügbar sind.
Aus der Masse an GCC-Optionen kann hier nur ein kleiner Auszug der wichtigsten und am häufigsten verwendeten Optionen vorgestellt werden. Eine Auflistung aller GCC-Optionen mit Kurzbeschreibung umfasst knapp 1000 Zeilen ohne undokumentierte Optionen, versteht sich.
Allgemeine Optionen für GCC
- --help
- Anzeige der wichtigsten Optionen
- --help -v
- Überschüttet einen mit Optionen
- --target-help
- Anzeige der wichtigsten maschinenspezifischen Optionen
- -O0
- keine Optimierung
- -O1
- Optimierung
- -Os
- optimiert für Code-Größe
- -O2
- stärkere Optimierung für bessere Laufzeit
- -g
- erzeugt Debug-Informationen
- -c
- (pre)compilert und assembliert nur bis zum Objekt (*.o), kein link-Lauf
- -S
- (pre)compilert nur und erzeugt Assembler-Ausgabe (*.s)
- -E
- nur Precompilat (*.i) erzeugen, kein Compilieren, kein Assemblieren, kein Linken
- -o <filename>
- legt den Name der Ausgabedatei fest
- -v
- zeigt Versionsinformationen an und ist geschwätzig (verbose): Anzeige der aufgerufenen tools
- -I<path>
- Angabe eines weiteren Include-Pfads, in dem Dateien mit #include <...> gesucht werden
- -E -dM <filename>
- Anzeige aller Defines
- -D<name>
- Definiert Makro <name>
- -D<name>=<wert>
- Definiert Makro <name> zu <wert>
- -U<name>
- Undefiniert Makro <name>
- -save-temps
- Temporäre Dateien (*.i, *.s) werden nicht gelöscht. Teilweise fehlerhaft zusammen mit -c
- -Wa,<options>
- übergibt Komma-getrennte Liste <options> an den Assembler (avr-as)
- -Wp,<options>
- übergibt Komma-getrennte Liste <options> an den Preprozessor
- -Wl,<options>
- übergibt Komma-getrennte Liste <options> an den Linker (avr-ld)
- -Wall
- gibt mehr Warnungen, aber immer noch nicht alle
- -pedantic
- geht bedonders pedantisch mit Code um
- -ansi
- bricht mit einer Fehlermeldung ab, wenn kein ANSI-C verwendet wurde
- -ffreestanding
- Das erzeugte Programm läuft nicht in einer Umgebung wie einer Shell. Der Prototyp von main ist
void main (void);
Maschinenspezifische Optionen für avr-gcc
Maschinenabhängige Optionen beginnen immer mit -m
- -mmcu=xxx
- Festlegen des Targets, für das Code generiert werden soll. Je nach Target werden unterschiedliche Instruktionen verwendet und andere Startup-Dateien (crtxxx.o) eingebunden. Spezielle Defines werden gesetzt, um in der Quelle zwischen den Targets unterscheiden zu können:
#ifdef __AVR_AT90S2313__ /* Code fuer AT90S2313 */ #elif defined (__AVR_ATmega8__) || defined (__AVR_ATmega32__) /* Code fuer Mega8 und Mega32 */ #else #error Das ist noch nicht implementiert für diesen Controller! #endif
Zwar gibt es für alle AVR-Derivate die avr/io.h, aber die AVR-Familien unterscheiden sich in ihrer Hardware; z.B. darin, wie Register heissen oder wie Hardware zu initialisieren ist. Diese Abhängigkeit kann man in unterschiedlichen Codestücken aufteilen und wie oben gezeigt bedingt übersetzen. Dadurch hat man Funktionalitäten wie uart_init auf unterschiedlichen Controllern und wahrt den Überblick, weil nicht für jede Controller-Familie eine extra Datei notwendig ist.
|
|
|
|
AVR, nur Assembler mcu Builtin define avr1 __AVR_ARCH__=1 at90s1200 __AVR_AT90S1200__ attiny11 __AVR_ATtiny11__ attiny12 __AVR_ATtiny12__ attiny15 __AVR_ATtiny15__ attiny28 __AVR_ATtiny28__
- -minit-stack=xxx
- Festlegen der Stack-Adresse
- -mint8
- Datentyp int ist nur 8 Bit breit, anstatt 16 Bit. Datentypen mit 32 Bit wie long sind nicht verfügbar
- -mno-interrupts
- Ändert den Stackpointer ohne Interrupts zu deaktivieren
- -mcall-prologues
- Funktions-Prolog und -Epilog werden als Unterroutinen umgesetzt, um die Codegröße zu verkleinern
- -mtiny-stack
- Nur die unteren 8 Bit des Stackpointers werden verändert
- -mno-tablejump
- Für ein switch-Statement werden keine Sprungtabellen angelegt
- -mshort-calls
- Verwendet rjmp/rcall (begrenzte Sprungweite) auf Devices mit mehr als 8 kByte Flash
- -msize
- Ausgabe der Instruktonslängen im asm-File
- -mdeb
- (undokumentiert) Ausgabe von Debug-Informationen für GCC-Entwickler
- -morder1
- (undokumentiert) andere Register-Allokierung
- -morder2
- (undokumentiert) andere Register-Allokierung
Builtin Defines
Zur bedingten Codeerzeugung und zur Erkennung, welcher Compiler sich an der Quelle zu schaffen macht, sind folgende Defines hilfreich
GCC
- __GNUC__
- X wenn GCC-Version X.Y.Z
- __GNUC_MINOR__
- Y wenn GCC-Version X.Y.Z
- __GNUC_PATCHLEVEL__
- Z wenn GCC-Version X.Y.Z
- __VERSION__
- "X.Y.Z" wenn GCC-Version X.Y.Z
- __GXX_ABI_VERSION
- Version der ABI (Application Binary Interface)
- __OPTIMIZE__
- Optimierung ist aktiviert
- __NO_INLINE__
- Ohne Schalter -finline resp. -finline-all-functions etc.
- __ASSEMBLER__
- GCC betrachtet die Eingabe als Assembler-Code und compiliert nicht. Weiterleitung an den Assembler.
avr-gcc
- __AVR
- Definiert für Target avr, d.h. avr-gcc ist am Werk
- __AVR__
- dito
- __AVR_ARCH__
- codiert den AVR-Kern, für den Code erzeugt wird (Classic, Mega, ...).
- __AVR_XXXX__
- Gesetzt, wenn -mmcu=xxxx.
Siehe auch: Maschinenspezifische Optionen
Sections
Sections sind mit Fächern vergleichbar, in die Daten, Code, Debug-Informationen usw. einsortiert werden. Zur Section .text gehört z.B. der ausführbare Code, welcher letztendlich im Flash landet. Wo genau das ist, braucht man nicht zu wissen und es spielt auch keine Rolle, wo eine bestimmte Funktion landet.
Für 'normalen' Code und 'normale' Daten braucht man sich nicht um die Sections zu kümmern, sie werden von avr-gcc automatisch richtig zugeordnet. Für spezielle Anwendungen kann es aber notwendig sein, die Ablage in eine andere Section zu machen; etwa wenn man Daten im EEPROM lesen/schreiben will. Wie das genau gemacht wird, steht im Abschnitt "Attribute" und es gibt ein Beispiele in den Abschnitten "Datenablage am Beispiel Strings" und "Zufall".
Section | Ablage | Betrifft | Beschreibung |
---|---|---|---|
.text | Flash | Code | normaler Programm-Code |
.data | SRAM | Daten | wird vom Startup-Code initialisiert, u.a. aus .progmem |
.bss | SRAM | Daten | wird vom Startup-Code zu 0 initialisiert |
.progmem | Flash | Daten | wird vom Startup-Code nach .data kopiert |
.eeprom | EEPROM | Daten | Daten im EEPROM |
.noinit | SRAM | Daten | wird nicht initialisiert |
.initn | Flash | Code | wird vor main ausgeführt, n = 0...9 |
.finin | Flash | Code | wird nach main ausgeführt, n = 9...0 |
.vectors | Flash | Code | Vektor-Tabelle: Tabelle mit Sprüngen zur jeweiligen ISR |
.bootloader | Flash | Code | für den Bootloader |
Der Anfang einer Section kann auch dem Linker mitgegeben werden, etwa wenn wie üblich avr-gcc als Treiber für den Linker verwendet wird:
avr-gcc ... -Wl,--section-start=.eeprom=0x810001
Damit beginnt Section .eeprom nicht an der (virtuellen) Adresse 0x810000, sondern ein Byte später. Manche AVRs haben einen Silicon-Bug, der bei Verwendung der EEPROM-Adresse 0 zu Fehlern führt. Mit der obigen Linker-Option wird diese Adresse nicht mehr verwendet.
Attribute
Mit Attributen kann man die Codeerzeugung beeinflussen. Es gibt verschiedene Attribute, die auf Daten, Typen, und/oder Funktionen anwendbar sind.
Syntax
__attribute__ ((<name>)) __attribute__ ((<name1>, <name2>, ...)) __attribute__ ((<name> ("<wert>")))
Nützliche Attribute von GCC
Attribut | Funktionen | Daten | Typen | Beschreibung |
---|---|---|---|---|
section ("<name>") | (x) | (x) | (x) | Lokatiert nach Section <name> |
noreturn | x | Die Funktion wird nie zurückkehren | ||
inline | x | Funktion wird geinlinet falls möglich | ||
noinline | x | Funktion wird keinesfalls geinlinet | ||
packed | x | x | Datenablage in Strukturen erfolgt dicht, also ohne eventuelle Füllbytes |
Attribute von avr-gcc
Attribut | Funktionen | Daten | Typen | Beschreibung |
---|---|---|---|---|
progmem | x | x | x | Lokatiert ins Flash |
naked | x | Funktion wird ohne Prolog/Epilog erzeugt | ||
interrupt | x | Hier nur wegen der Vollständigkeit erwähnt | ||
signal | x | Hier nur wegen der Vollständigkeit erwähnt |
Beispiele:
#define EEPROM __attribute__ ((section (".eeprom"))) const char EE_HALLO_WELT[] EEPROM = "Hallo Welt"; const int EE_wert EEPROM = 0x1234; void __attribute__ ((noinline)) foo() { /* Code */ }
Dynamische Speicherallokierung
Zur dynamischen Speicherallokierung stehen Standard-Funktionen wie malloc zur Verfügung. Damit kann man zur Laufzeit Speicher anfordern und wenn man ihn nicht mehr benötigt, wieder freigeben. Funktionen wie malloc und calloc sind jedoch recht aufwändig. Die allokierten Speicherstücke werden intern in einer verketteten Liste verwaltet, und das verbraucht wertvollen Platz im Flash und im SRAM sowie Laufzeit.
Eine resourcen-schonendere Möglichkeit, zur Laufzeit an Speicher zu kommen, bietet __builtin_alloca. Der Speicher, der damit belegt wird, wird nicht auf dem Heap angelegt, sondern im Frame der Funktion. Das ist wesentlich effektiver als die Standard-Methoden. Den so erhaltenen Speicher braucht man nicht freizugeben, das geschieht beim Verlassen der Funktion in deren Epilog.
Von der Verwendung ist der mittels __builtin_alloca erhaltene Speicher also wie eine lokale Variable, mitsamt den bekannten Regeln für den Gültigkeitsbereich.
Insbesondere darf dieser Speicher nicht mit return an die darüberliegende Funktion zurückgegeben werden, weil er dann nicht mehr gültig ist und ein Zugriff darauf zu einem Laufzeitfehler führt!
Das Programm/der Algorithmus muss daher beim Beschreiten dieses Wegs darauf angepasst werden.
Verwendung:
void function (size_t num_data) { // data_t hat man irgendwo selber definiert, oder es ist ein elementarer Typ data_t * const p = (data_t * const) __builtin_alloca (num_data * sizeof (data_t)); // Mach was mit p[0] ... p[num_bytes-1] ... }
Code-Beispiele
Dieser Abschnitt enthält Code-Schnippsel für avr-gcc. Es werden Besonderheiten besprochen, die für avr-gcc zu beachten sind.
Dieser Abschnitt ist kein Tutorial zur C-Programmierung und keine Einführung in die Programmiersprache C im allgemeinen. Dafür sei auf einschlägige Tutorials/Bücher verwiesen.
Zugriff auf Special Function Registers (SFRs)
Zugiff auf Bytes und Worte
Auf SFRs wird generell über deren Adresse zugegriffen:
// Liest den Inhalt von SREG an Adresse 0x5f unsigned char sreg = *((unsigned char volatile*) 0x5f);
Das bedeutet in etwa: "Lies ein flüchtiges (volatile) Byte (unsigned char) von Adresse 0x5f". Der Speicherinhalt von SFRs ist flüchtig, denn er kann sich ändern, ohne daß avr-gcc dies mitbekommt. Daher muss bei jedem C-Zugriff auf ein SFR dieses wirklich gelesen/geschrieben werden, was der Qualifier volatile sicherstellt. Ansonst geht der Compiler u.U. davon aus, daß der Inhalt bekannt ist und verwendet einen alten, in einem GPR befindlichen Wert.
Um lesbaren, weniger fehleranfälligen und unter AVRs halbwegs portierbaren Code zu erhalten, gibt es Makrodefinitionen im Conroller-spezifischen Header ioxxxx.h, der neben anderen Dingen mit avr/io.h includet wird:
#include <avr/io.h> ... // SREG lesen uint8_t sreg = SREG; ... /// SREG schreiben SREG = sreg;
Die Bezeichner der SFRs sind die gleichen wie im Manual. Evtl verschafft ein Blick in den Header Klarheit. Dieser befinden sich in
<AVR_INSTALL_DIR>/avr/include/avr/io****.h
Dieser Zugriff geht auch für 16-Bit Register wie TCNT1, für die eine bestimmte Reihenfolge für den Zugriff auf Low- und High-Teil eingehalten werden muss: avr-gcc generiert die Zugriffe in der richtigen Reihenfolge.
uint16_t tcnt1 = TCNT1;
Zu beachten ist, daß dieser Zugriff nicht atomar erfolgt. Das Lesen/Schreiben mehrbytiger Werte muss vom Compiler in mehrere Byte-Zugriffe zerlegt werden. Zwischen diesen Zugriffen kann ein Interrupt auftreten, was zu fehlerhaften Resultaten führen kann. Entsprechende Codestücke müssen daher atomar gehalten werden!
Zugriff auf einzelne Bits
Zugriff auf Bits geht wie gewohnt mit den Bitoperationen & (and), | (or), ^ (xor) und ~ (not)
Wieder gibt es Defines in den AVR-Headern, mit denen man Masken für den Zugriff erhalten kann, etwa:
/* GIMSK / GICR */ #define INT1 7 #define INT0 6 #define IVSEL 1 #define IVCE 0
Masken ergeben sich durch schieben von 1 an die richtige Position.
// Ports B_0 und B_1 als Ausgang DDRB |= (1<<PB0) | (1<<PB1);
erzeugt
87 b3 in r24, 0x17 83 60 ori r24, 0x03 87 bb out 0x17, r24
Etwas anders sieht der Code aus, wenn die Bits einzeln gesetzt werden und das Register im bitadressierbaren Bereich liegt (SRAM 0x20 bis 0x3f resp. I/O 0x0 bis 0x1f):
// Ports B_0 und B_1 als Ausgang DDRB |= (1<<PB0); DDRB |= (1<<PB1);
erzeugt
b8 9a sbi 0x17, 0 b9 9a sbi 0x17, 1
Auch hier ist zu beachten, daß es Probleme geben kann, wenn nicht atomarer Code erzeugt wird, weil der AVR-Befehlssatz nicht mehr hergibt:
// toggle PORT B_0: wechseln 0 <--> 1 PORTB ^= (1<<PB0);
ergibt
88 b3 in r24, 0x18 ; Wenn hier ein Interrupt auftritt, in dessen ISR PORTB verändert wird, ; dann wird die Änderung durch die letzte Instruktion wieder überschrieben! 91 e0 ldi r25, 0x01 ; dito 89 27 eor r24, r25 ; dito 88 bb out 0x18, r24
Interrupts
Um zu kennzeichnen, daß es sich bei einer Funktion um eine Interrupt Sevice Routine (ISR) handelt, gibt es spezielle Attribute. Diese brauchen nicht explizit hingeschrieben zu werden, ebensowenig wie die genaue Nummer des Interrupt Requests (IRQ). Dafür gibt es die Includes und die Makros:
#include <avr/io.h> #include <avr/signal.h> SIGNAL (SIG_OUTPUT_COMPARE1A) { /* ISR-Code */ } INTERRUPT (SIG_OUTPUT_COMPARE1B) { /* ISR-Code */ }
Dadurch wird die Funktion mit dem richtigen Prolog/Epilog erzeugt, und es wird ein Eintrag in die Interrupt-Vektortabelle gemacht bei obigem Beispiel also zwei Einträge.
Die Schreibweise des Signal-Names muss genau die sein wie im Header, das schliesst auch Leerzeichen ein! Nicht alle GCC-Versionen bringen Fehler/Warnung, wenn die Schreibweise nicht stimmt.
SIGNAL (SIG_OUTPUT_COMPARE1A ) // !!! Macht NICHT das, was man will (Blank am Ende)!!!
- SIGNAL
- Mit Ausführung einer ISR deaktiviert die AVR-Hardware die Interrupts, so daß die ISR nicht durch andere Interrupt-Anforderungen unterbrochen wird. Beim Verlassen der ISR werden Interrupts wieder aktiviert.
- INTERRUPT
- Früh im ISR-Prolog werden mit sei die von der AVR-Hardware temporär deaktivierten Interrupts reaktiviert. Dadurch kann die ISR von einer IRQ unterbrochen werden. Das bietet die Möglichkeit, so etwas wie Interrupt-Priorisierung nachzubilden, was AVRs selbst nicht können.
Dauert die ISR zu lange und wird sie nochmals von ihrem eigenen IRQ unterbrochen, stürzt man ab.
Nachschlagen kann man den Name in
- <GCC_HOME>/avr/include/avr/ioxxxx.h
Interrupts aktivieren
Damit eine ISR überhaupt zur Ausführung kommt, müssen drei Bedingungen erfüllt sein
- Interrupts müssen global aktiviert sein
- Der entsprechen IRQ muss aktiviert worden sein
- Das zum IRQ gehörende Ereignis muss eintreten
#include <avr/io.h> #include <avr/interrupt.h> ... // enable OutputCompareA Interrupt für Timer1 TIMSK |= (1 << OCIE1A); // disable OutputCompareA Interrupt für Timer1 TIMSK &= ~(1 << OCIE1A); // Interrupts aktivieren sei(); // Interrupts abschalten cli();
default Interrupt
Für nicht implementierte Interrupts macht avr-gcc in die Vektortabelle einen Eintrag, der zu __bad_interrupt (definiert im Startup-Code crt*.o) springt und von dort aus weiter zu Adresse 0. Dadurch läuft der AVR wieder von neuem los, wenn ein Interrupt auftritt, zu dem man keine ISR definiert hat allerdings ohne die Hardware zurück zu setzen wie bei einem echten Reset.
Möchte man diesen Fall abfangen, dann geht das über eine globale Funktion namens __vector_default:
#include <avr/signal.h> SIGNAL (__vector_default) ...
Damit wird von __bad_interrupt aus nicht nach Adresse 0 gesprungen, sondern weiter zu __vector_default, welches durch SIGNAL oder INTERRUPT den üblichen ISR-Prolog/Epilog bekommt.
Wenn man komplett eigenes Zeug machen will, dann macht man eine nackte Funktion, die z.B. eine Meldung ausgibt, in einer Endlosschleife landet, oder über den Watchdog einen richtigen Reset auslöst. Falls __vector_default zurückkehren soll, dann ist darauf zu achten, daß man dies mit reti (return from interrupt) tut und evtl. verwendete Register und den Status (SREG) sichert:
void __attribute__ ((naked)) __vector_default (void) { __asm__ __volatile ( "; mach was" "\n\t" "; mach was" "\n\t" "reti" ); }
ISR mit eigenem Prolog/Epilog
Genauso implementiert man auch eine kleine ISR mit minimalem Overhead. Mit naked befreit man die Routine vom Standard-Prolog/Epilog und kümmert sich selbst darum:
#include <avr/io.h> void __attribute__ ((naked)) SIG_OVERFLOW0 (void) { /* Port B.6 = 0 */ __asm__ __volatile ( "cbi %0, %1" "\n\t" "reti" : : "M" (_SFR_IO_ADDR (PORTB)), "i" (6) ); }
Die ISR sieht dann so aus:
__vector_9: c6 98 cbi 0x18, 6 18 95 reti
SRAM, Flash, EEPROM: Datenablage am Beispiel Strings
Die Programmiersprache C kennt selber keine Strings; das einzige, was C bekannt ist, ist der Datentyp char, der ein einzelnes Zeichen repräsentiert.
Darstellung in C
Ein String im Sinne von C ist ein Array von Charactern bzw. ein Zeiger auf den Anfang des Arrays. Die einzelnen Zeichen folgen im Speicher direkt aufeinander und werden in aufsteigenden Adressen gespeichert. Am String-Ende folgt als Abschluss der Character '\0', um das Ende zu kennzeichnen. Dies ist besonders bei der Berechnung des Speicherplatzes für Strings zu berücksichtigen, denn für die 0 muss auch Platz reserviert werden.
Bestimmen der Stringlänge
/* Bestimmt die Laenge des Strings ohne die abschliessende '\0' zu zaehlen unsigned int strlength (const char *str) { unsigned int len = 0; while (*str++) len++; return len; }
Die Stringlänge kann auch mit der Standard-Funktion strlen bestimmt werden, deren Prototyp sich in string.h befindet:
#include <string.h> size_t strlen (const char*);
String im Flash belassen
Oftmals werden Strings nur zu Ausgabezwecken verwendet und nicht verändert. Verwendet man Sequenzen der Gestalt
char *str1 = "Hallo Welt!"; char str2[] = "Hallo Welt!";
dann werden die Strings im SRAM abgelegt. Im Startup-Code werden die Strings vom Flash ins SRAM kopiert und belegen daher sowohl Platz im SRAM als auch im Flash. Wird ein String nicht verändert, braucht er nicht ins SRAM kopiert zu werden. Das spart Platz im knapp bemessenen SRAM. Allerdings muss anders auf den String zugegriffen werden, denn wegen der Harvard-Architektur des AVR-Kerns kann avr-gcc anhand der Adresse nicht unterscheiden, ob diese ins SRAM, ins Flash oder ins EEPROM zeigt.
#include <avr/pgmspace.h> const prog_char str3[] = "Hallo Welt!"; unsigned int strlen_P (const prog_char *str) { unsigned int len = 0; while (1) { char c = (char) pgm_read_byte (str); if ('\0' == c) return len; len++; str++; } } void foo() { unsigned int len; len = strlen_P (str3); len = strlen_P (PSTR("String im Flash")); }
String ins EEPROM legen
Dies geht nach dem gleichen Muster, nach dem Strings ins Flash gelegt werden. Der Zugriff wird vergleichsweise langsam, denn der EEPROM ist langsamer als SRAM bzw. Flash.
#include <avr/eeprom.h> const char str4[] __attribute__ ((section(".eeprom"))) = "Hallo Welt!"; unsigned int strlen_EE (const char *str) { unsigned int len = 0; while (1) { char c = (char) eeprom_read_byte (str); if ('\0' == c) return len; len++; str++; } }
Zufall
"Echte" Zufallszahlen zu generieren ist leider nicht möglich, hierzu muss man externe Hardware wie einen Rauschgenerator verwenden. Funktionen wie rand und random basieren auf algebraischen Verfahren, die eine gute Verteilung der gelieferten Werte haben. Werden diese Funktionen mit dem selben Startwert (seed) initialisiert, liefern sie auch immer die gleiche Folge. In diesem Sinne sind die Werte nicht zufällig sondern nur scheinbar zufällig und "wüst umherhüpfend".
Um einen zufälligen Startwert zu erhalten, kann man den uninitialisierten Inhalt des SRAM verwenden, das nach dem power-up keinen definierten Zustand hat.
Startwert (seed) besorgen
Am einfachsten geht dies, indem man eine Variable in die Sektion .noinit lokatiert und den Wert liest:
unsigned long seed __attribute__ ((section (".noinit")));
Etwas bessere Resultate erhält man, wenn man den ganzen Inhalt des nicht verwendeten SRAMs zur Bildung der seed heranzieht. Das Symbol __heap_start wird übrigens im standard Linker-Script definiert, RAMEND ist ein Makro aus ioxxxx.h:
Das Beispiel interpretiert den SRAM-Inhalt als unsigned short Werte und berechnet die seed, indem die einzelnen Werte mit exor "überlagert" werden.
#include <avr/io.h> unsigned short get_seed() { unsigned short seed = 0; unsigned short *p = (unsigned short*) (RAMEND+1); extern unsigned short __heap_start; while (p >= &__heap_start + 1) seed ^= * (--p); return seed; }
Pseudozufall in der avr-libc
In der avr-libc finden sich Funktionen, um Pseudo-Zufallszahlen zu erhalten bzw. um Startwerte für die Algorithmen zu setzen:
#include <stdlib.h>
Prototypen und Defines:
#define RAND MAX 0x7FFF int rand (void); void srand (unsigned int seed); long random (void); void srandom (unsigned long seed);
Frühe Codeausführung vor main()
Mitunter ist es notwendig, Code unmittelbar nach dem Reset auszuführen, noch bevor man in main() mit der eigentlichen Programmausführung beginnt. Das kann zB zur Bedienung eines Watchdog-Timers erforderlich sein.
Nach einen Reset und vor Aufruf von main werden Initialisierungen ausgeführt wie
- setzen des Stackpointers
- Vorbelegung globaler Datenobjekte: Daten ohne Initializer werden zu 0 initialisert (Section .bss). Für Daten mit Initializer (Section .data) werden die Werte aus dem Flash ins SRAM kopiert.
- Initialisierung von Registern wie R1, in dem bei avr-gcc immer die Konstante 0 gehalten wird.
Im Linker-Script werden Sections von .init0 bis .init9 definiert, die nacheinander abgearbeitet werden. Erst danach wird main betreten. Um Code früh auszuführen, legt man die Funktion in eine dieser Sections:
/* !!! never call this function !!! void __attribute__ ((naked, section (".init3"))) code_init3 (void) { /* Code */ }
Zu beachten ist dabei
- Eine so definierte Funktion darf keinesfalls aufgerufen werden!
- Zuweisungen wie i=0; ergeben vor .init3 inkorrekten Code, da vor Ende von .init2 Register R1 noch nicht mit 0 besetzt ist, avr-gcc aber davon ausgeht, daß es eben diesen Wert enthält.
- Lokale Variablen müssen in Registern liegen, denn vor Ende von .init2 ist der Stackpointer noch nicht initialisiert. Zudem ist die Funktion naked, hat also insbesondere keinen Prolog, der den Framepointer (Y-Register) setzen könnte, falls er benötigt wird.
- Gegebenenfalls ist daher die Verwendung von inline-Assembler angezeigt oder die Implementierung in einem eigenen Assembler-Modul, das dazu gelinkt wird. Der erzeugte Code ist im List-File zu überfrüfen.
- Werden mehrere Funktionen in die gleiche init-Section gelegt, ist die Reihenfolge ihrer Ausführung nicht spezifiziert und i.a. nicht die gleiche wie in der Quelle.
Unbenutzte init-Sections haben die Nummern 0, 1, 3 und 5 bis 8. Die verbleibenden werden vom Startup-Code verwendet:
- .init2
- Initialisieren von R1 mit 0 und setzen des Stackpointers
- .init4
- Kopieren der Daten vom Flash ins SRAM (.data) und löschen von .bss
- .init6
- C++ Konstruktoren
- .init9
- Sprung zu main
Includes
Die mit
#include <...>
angegebenen Includes werden von avr-gcc in den mit der Option '-I' anegegenen Pfaden gesucht. Dem Compiler bekannt ist der Pfad <GCC_HOME>/avr/include. Gibt man z.B. an
#include <stdio.h>
dann wird automatisch in diesem Verzeichnis nach stdio.h gesucht. In dem Verzeichnis stehen Standard-Includes die benötigt werden, wenn man libc-Funktionen oder mathematische Funktionen verwendet. AVR-spezifische Dinge stehen im Unterverzeichnis avr, etwa:
#include <avr/io.h>
Standard
ctype.h character conversion macros and ctype macros errno.h provides symbolic names for various error codes inttypes.h Use [u]intN_t if you need exactly N bits. These typedefs are mandated by the C99 standard. math.h mathematical functions setjmp.h The C library provides the setjmp() and longjmp() functions to jump directly to another (non-local) function. stdio.h Standard IO facilities stdlib.h Declares some basic C macros and functions as defined by the ISO standard, plus some AVR-specific extensions string.h perform string operations on NULL terminated strings
AVR-spezifisch
Die AVR-spezifischen Includes finden sich wie gesagt im Unterverzeichnis avr. Die meisten dort befindlichen Header wird man nie direkt durch Angabe im C-File erhalten, sondern durch Angabe von
#include <avr/io.h>
Dadurch werden z.B. genau der I/O-Header eingebunden, der zum AVR-Modell passt, also avr/iom8.h für ATMega8, avr/iotn2313 für ATTiny2313, avr/io2313.h für AT90S2313, etc.
Verantwortlich dafür ist der Schalter '-mmcu=xxx'.
Obwohl diese Header nicht explizit angegeben werden müssen, kann ein Blick dorthin hilfreich sein, um die Namen von SFRs oder Signals nachzuschlagen. Diese Header werden im folgenden nicht alle einzeln aufgelistet. Ihre Namen sind immer 'avr/io*.h', für ATMega avr/iom*.h' und für ATTiny 'avr/iotn*.h'.
avr/boot.h Bootloader Support avr/crc16.h Prüfsumme CRC16 avr/delay.h Verzögerungsschleife - loops for small accurate delays avr/eeprom.h EEPROM-Routinen avr/ina90.h Kompatibilität mit IAR-AVR-Compiler avr/interrupt.h sei(), cli(), ... avr/io.h --> inttypes.h, io*.h avr/io*.h SFRs, SIG_****, SPM_PAGESIZE, RAMEND, XRAMEND, E2END, FLASHEND avr/parity.h Parität avr/pgmspace.h Zugriff aufs Flash: Byte lesen, PROGMEM, prog_char, prog_uint8_t, ... avr/portpins.h Makros für Port-Pins avr/signal.h Makros SIGNAL() und INTERRUPT(), ... avr/sleep.h Power-Safe avr/twi.h I2C avr/wdt.h Watchdog
Dateien (WinAVR)
WinAVR bringt wesentlich mehr mit als nur avr-gcc:
- Binutils (Assembler (avr-as), Linker (avr-ld), Tools (avr-size, avr-objdump, avr-objcopy, avr-nm, ...))
- Debugger (avr-gdb mit Oberfläche insight)
- Bibliothek avr-libc inclusive Dokumentation
- Progger avrdude
- Progger uisp
- AVR-Simlator (simulavr),
- Programmers Notepad (kleiner Editor)
- Demo-Projekte
- viele Linux-Tools wie make, grep, sed, tar, etc.
- ...
Verzeichnisbaum (Auszug):
. avr-gcc Installations-Verzeichnis ./avr/include Standard Includes ./avr/include/avr Includes AVR-spezifisch ./avr/lib Startup-Code, Libs (avr2) ./avr/lib/avr3 " (avr3) ./avr/lib/avr4 " (avr4) ./avr/lib/avr5 " (avr5) ./avr/lib/ldscripts Linker-Skripte ./bin Programme (avr-gcc.exe, giveio.sys, ... ./doc Doku (HTML + pdf) ./doc/avr-libc avr-libc ./doc/avrdude-xxx avrdude ./doc/simulavr-xxx simulavr ./doc/uisp-xxx uisp ./examples Beispiel-Projekte ./examples/demo PWM mit AT90S2313 ./examples/twitest I2C mit ATMega ./info info pages ./man man pages ./mfile mfile ./pn Programmers Notepad ./utils/bin bzip2, diff, gawk, grep, make, sed, tar, ...
Fallstricke und häufige Fehler
Tippfehler
Tippfehler können immer passieren. Besonders fies ist es, wenn der Tippfehler nicht zu einer Warnung oder zu einer Fehlermeldung führt, weil der entstandene Code korrekter C-Code ist.
Ein ; zu viel
Ein reflexartig eingetippter oder nach Ändeungen stehen gebliebener ; hat schon so manches Programm ausgeknockt:
if (a == 0); { /* mach was */ }
Wenn a == 0 ist, dann wird ; ausgeführt (also im Endeffekt garnichts). Danach kommt der Block, der im if stehen sollte. Der wird immer ausgeführt, denn er gehört nicht mehr zum if.
Zuweisung statt Vergleich
if (a = 0) { /* mach was */ }
Zuerst wird a = 0 gesetzt und dann überprüft, ob die if-Bedingung erfullt ist. Der Wert ist aber immer 0, was nicht erfüllt bedeutet. Der nachfolgende Block wird nie betreten.
Abhilfe schafft, sich angewohnen zu schreiben
if (0 == a)
Wenn man dann eine Zuweisung eintippt, gibt's einen Fehler.
Signal/Interrupt-Name vertippt oder Leerzeichen zu viel
SIGNAL (SIG_OVEFRLOW0) { /* mach was */ }
Nicht alle Compiler-Versionen meckern da. Der ISR-Code wird nicht in die Interrupt-Tabelle eingetragen. Kommt es zum Interrupt, dann landet man in RESET.
Nicht-atomarer Code
Verwendet man in einem Programm IRQs (Interrupts) und ändert in der ISR (Service Routine) ein Datum (Variable, SFR, ...), dann wird diese Änderung möglicherweise überschrieben, wenn die IRQ zu einem ungünstigen Zeitpunkt auftritt. Ein Beispiel, das zeigt, was passieren kann, findet sich im Abschnitt "Zugriff auf einzelne Bits". Ein weiteres Beispiel ist das Lesen, Schreiben oder Testen einer mehrbytigen Variable:
int volatile i; ... if (0 == i)
Weil i länger als 1 Byte ist, kann es nicht in einem Befehl gelesen werden; daher kann während des Lesens eine IRQ auftreten, in deren ISR der Wert von i verändert wird. Der obige Code könnte etwa so assembliert werden:
; i wird vom SRAM in das Registerpaar r24:r25 geladen lds r24,i ; wenn hier eine IRQ zuschlägt, in der i verändert wird, ist der Registerinhalt korrupt lds r25,(i)+1 or r24,r25 brne ...
Natürlich können auch komplexere Datenstrukturen von diesem Phänomen betroffen sein. Besonders unangenehm an dieser Klasse von Fehlern ist, daß sie nur sporadisch auftauchen und man sie daher selbst mit einem guten Debugger sehr schlecht orten kann, da die zugehörige Codestelle fast immer korrekt abgearbeitet wird.
Entweder man macht den ganzen betroffenen Block ununterbrechbar (atomar):
#include <avr/io.h> #include <avr/interrupt.h> ... unsigned char sreg = SREG; cli(); if (0 == i) { i = 1; SREG = sreg; } else { SREG = sreg; return; } ...
oder je nach Programmstruktur sichert man den Wert, z.B. in eine lokale Variable.
Mangelnde C-Kenntnis
Warnung ignoriert
- "Wieso soll das Probleme machen? Das ist doch nur eine Warnung!"
Warnungen zur Compile-Zeit werden gerne zu Fehlern zur Laufzeit. Letztere sind deutlich schwerer zu finden, als angewarnten Code zu korrigieren.
void foo (long*); void bar (int *p) { foo (p+1); // Was soll das sein?! // ((long*) p) + 1 oder // (long*) (p + 1) }
> avr-gcc -c -o prog.o prog.c prog.c: In function `bar': prog.c:5: warning: passing arg 1 of `foo' from incompatible pointer type
Array-Index
Informatiker am Bahnhof:
- "0, 1, 2, ... Wo ist mein dritter Koffer?!"
Gleiches gilt für Arrays:
#define NUM 10; int a[NUM]; // für die N Werte a[0] ... a[N-1]
Ein Zugriff auf a[N] greift igrdendwo hin. Wahlweise liest man Schrott oder überschreibt andere Daten, die in der Nähe liegen, und plötzlich seltsame Werte enthalten.
Bitweise vs. Logische Operatoren
Die Operatoren AND, OR und NOT gibt es in C in zwei Ausprägungen
- bitweise
- Die Operatoren ^, |, &, ~, |=, ^=, &=, |= operieren bitweise. Der entsprechende Operator wird also auf alle Bits des Wertes parallel angewandt und das Ergebnis für ein Bit ist unabhängig vom Inhalt der anderen Bits.
- logisch
- Die Operatoren ||, &&, !, &&=, ||= operieren auf dem ganzen (int) Wert und berücksichtigen nur, ob der Wert 0 (false) oder ungleich 0 (true) ist.
Dementsprechend liefert && i.d.R. ein anderes Ergebnis als &:
if (a && b) // erfüllt, wenn a!=0 und b!=0 if (a & b) // erfüllt, wenn in a und b an der gleichen Stelle ein Bit gesetzt ist
Ein Verwechseln bzw. unkorrektes Einsetzen der Operatoren gibt also ein falsches Programm.
TRUE ist nicht 1
Da C die Begriffe TRUE und FALSE eigentlich nicht kennt, wurde schon öfter beobachtet, daß folgendes definiert wurde:
#define TRUE 1 #define FALSE 0
solange man damit nur Schalter setzt und abfragt, ist dagegen nichts zu sagen.
a = TRUE; b = FALSE; if ( (a == TRUE) && (b == FALSE)) {.."this is true"..}
wenn man aber dann schreibt
if ( (a & b) == TRUE) {.."this is true"..}
kann man einen herbe Enttäuschung erleben. Man müßte für ein richtiges Ergebnis schreiben
if ( (a & b) != FALSE) {.."this is true"..}
Damit ist aber eine gute Lesbarkeit endgültig dahin.
Besser sind Definition wie
#define FALSE (0!=0) #define TRUE (0==0)
denn damit gilt einerseits, daß für alle boolschen Werte einschliesslich dieser Konstanten der Zusammenhang x = !!x besteht. Mit der allerersten Definition hat man das unerwünschte Ergebnis TRUE != !FALSE. Andererseits kann man direkt gegen diese Konstanden vergleichen:
a = (x < y); ... if (a == TRUE) // oder die 'klassische' Variante: if (a)
Ein , anstatt . in Konstante
In C sowie im angloamerikanischen Sprachraum werden Dezimalbrüche mit einem . (Punkt) geschrieben und nicht wie im Deutschen mit einem , (Komma):
float pi; pi = 3,14; // *AUTSCH* soll wohl heissen 3.14
Die zweite Zeile besteht aus zwei durch ein Komme getrennten Anweisungen, so das das ganze etwa gleichbedeutend ist mit
float pi; pi = 3; 14;
Jedenfalls ist es korrekter C-Code. Die 14; ist ein Ausdruck, der nicht weiter gebraucht wird und daher wegfällt, da er im Gegensatz zu einer void-Funktion keine Wirkung hat. Man rechnet also mit dem Wert 3 für [math]\pi[/math].
Das falsche Komma hat auch schon so manchen ebay-Freak aufs Kreuz gelegt...
Führende 0 in Konstanten
In C kennzeichnet eine führende 0 bei einer Zahlenkonstante, daß die Zahl oktal dargestellt ist. Somit ist 010 nicht gleich 10.
if (a == 030) // ist a gleich 24?
Warteschleife
Oft sieht man den Versuch, Warteschleifen zurch Zählschleifen zu realisieren:
void wait () { int i; for (i=0; i<50; i++); }
avr-gcc mit Optimierung erzeugt daraus
wait: ret
und das ist auch völlig in Ordnung, wenn man ein Blick in die C-Spezifikation wagt. Die Schleife hat keine Wirkung auf die Welt! Sie sagt: "Führe 50 mal ; aus". Und 50 mal Nichtstun ist eben nichts Tun...
Falls man wirklich auf diese Art warten möchte, hilft folgendes: Man gaukelt dem Compiler vor, es gäbe etwas unheimlich wichtiges in der Schleife zu tun, von dem er nichts mitbekommt.
void wait () { int i; for (i=0; i<50; i++) __asm__ __volatile ("; nur ein asm-Kommentar"); }
daraus entsteht
wait: ldi r24,lo8(49) ldi r25,hi8(49) .L5: /* #APP */ ; nur ein asm-Kommentar /* #NOAPP */ sbiw r24,1 sbrs r25,7 rjmp .L5 ret
Die Schleife wird nun 50 mal durchlaufen.
Wir bemerken, daß das inline Assembler nicht in Code resultiert und daß die Schleifenvariable nicht hochzählt, sondern hinunter. Auch diese Optimierung ist ok, denn i wird nirgends verwendet.
Daß bei dem Beispiel der gewünschte Code erzeugt wird ist mehr oder weniger Glückssache. Ein Compiler transformiert seine Eingabe in ein Assembler- oder Maschinenprogramm, das die gleiche Wirkung hat. Wie dies genau geschieht, ist in aller Regel nicht festgelegt, sondern nur, daß es passiert ansonsten ist der Compiler selbst fehlerhaft oder implementiert kein C.
In dem lezten Beispiel der Warteschleife ist die Bedeutung des C-Codes etwa "führe 50 mal das Inline-Assembler-Muster "; nur ein asm-Kommentar" in den Code ein". Dies wäre auch möglich komplett ohne Schleife(nvariable), indem der Compiler diese wegoptimiert und nur das asm 50 mal hintereinander ausgibt. Das Resultat wäre im Maschinencode dann wiederum absolut ohne Effekt. Bei gcc geschieht dies aber bestenfalls mit der Optimierungsstufe -O3, die für AVR absolut nicht zu emfehlen ist, oder indem man von Hand die Optionen setzt, die diese Optimierungsstufe nach sich zieht.
Eine Möglichkeit, dem auf C-Ebene zu begegnen, ist die Schleifenvariable i als volatile zu deklarieren, so daß sie angelegt werden und genau so wie codiert verwendet werden muss:
void wait () { int volatile i; for (i=0; i<50; i++) ; }
Das hat allerdings noch andere Konsequenzen, nämlich das Erzwingen des Framepointers (bei avr-gcc im Y-Register r28:r29), denn i lebt jetzt nicht mehr in einem Register, sondern im Frame und wird wirklich jedesmal von dort gelesen und geschrieben, was recht breiten Code ergibt:
wait: /* prologue: frame size=2 */ push r28 push r29 in r28,__SP_L__ in r29,__SP_H__ sbiw r28,2 in __tmp_reg__,__SREG__ cli out __SP_H__,r29 out __SREG__,__tmp_reg__ out __SP_L__,r28 /* prologue end (size=10) */ std Y+1,__zero_reg__ std Y+2,__zero_reg__ ldd r24,Y+1 ldd r25,Y+2 sbiw r24,50 brge .L7 .L5: ldd r24,Y+1 ldd r25,Y+2 adiw r24,1 std Y+1,r24 std Y+2,r25 ldd r24,Y+1 ldd r25,Y+2 sbiw r24,50 brlt .L5 .L7: /* epilogue: frame size=2 */ adiw r28,2 in __tmp_reg__,__SREG__ cli out __SP_H__,r29 out __SREG__,__tmp_reg__ out __SP_L__,r28 pop r29 pop r28 ret
Das ist der totale Overkill. Inakzeptabel breiter Code und ein einzelner Schleifendurchlauf dauert viele Instruktionen, so dass die verstrichene Zeit nur recht grobkörnig eingestellt werden kann, denn ein Durchlauf dauert 18 Zyklen.
Näher am Ziel liegt ein Ausbau der asm-Variante, indem man dort erzwingt, daß i wirklich vom Compiler angelegt wird. Das kann nur dadurch sichergestellt werden, daß es verwendet wird. Dazu wird erzwungen, daß i in ein Register geladen wird. Wir steuern also die Reload-Phase (die Phase, die Variablen in Register verteilt) von gcc. Da diese Ausgabe möglicherweise öfter zu bewältigen ist, wurde der erzwungene Reload als Makro definiert, der noch eine asm-Ausgabe macht, was reloadet wird:
#define RELOAD(reg,var) \ __asm__ __volatile (";RELOAD " reg " with " #var : "=" reg (var) : "0" (var) : "memory") void wait () { int i; for (i=0; i<50; i++) RELOAD ("r", i); }
Das Makro im Beispiel löst auf zu
__asm__ __volatile (";RELOAD r with i" : "=r" (i) : "0" (i) : "memory")
Das bedeutet, daß i in ein Register der Registerklasse r geladen wird. In allen GCC-Versionen steht dieses r für ein Standard-Register (GPR). Der erzeugte Code sieht nun besser aus, ist aber immer noch abhängig vom Optimierungsgrad etc.
wait: ldi r24,lo8(0) ldi r25,hi8(0) .L5: /* #APP */ ;RELOAD r with i /* #NOAPP */ adiw r24,1 cpi r24,50 cpc r25,__zero_reg__ brlt .L5 ret
Eine weitere reine C-Variante könnte so aussehen:
static int volatile dummy; void wait () { int i; for (i=0; i<50; i++) dummy = i; }
Was zu diesem Code führt:
wait: ldi r24,lo8(0) ldi r25,hi8(0) .L5: sts (dummy)+1,r25 sts dummy,r24 adiw r24,1 cpi r24,50 cpc r25,__zero_reg__ brlt .L5 ret
oder so was
void wait () { int i; for (i=0; i<50; i++) (void) (int * volatile) &i; }
was wieder den minimalen Code ergibt wie beim erzwungenen Reload:
wait: ldi r24,lo8(0) ldi r25,hi8(0) .L5: adiw r24,1 cpi r24,50 cpc r25,__zero_reg__ brlt .L5 ret
Fazit
Die einzig sichere Lösung, einen bestimmten Code generieren zu lassen, ist und bleibt das Gewünschte komplett in (Inline) Assembler auszudrücken.
Das kann dann so aussehen für eine feste Anzahl von Schleifendurchläufe von 1 bis 255:
void wait() { unsigned char i = 0; __asm__ __volatile ( "0:" "\n\t" "subi %0, 1" "\n\t" "cpi %0, lo8(%1)" "\n\t" "brlo 0b" : "=d" (i) : "d" (i), "M" (50) ); }
Oder wenn ein 16-Bit-Wert übergeben wird sieht es so aus:
void wait (unsigned short num) { unsigned short i = 0; __asm__ __volatile ( "0:" "\n\t" "cp %A0, %A1" "\n\t" "cpc %B0, %B1" "\n\t" "brsh 1f" "\n\t" "subi %A0, lo8(-1)" "\n\t" "sbci %B0, hi8(-1)" "\n\t" "rjmp 0b" "\n\t" "1:" : "=d" (i) : "0" (i), "r" (num) ); }
Abkürzungen und Bezeichnungen
- GCC
- GNU Compiler Collection
- gcc
- GNU C-Compiler
- GPR
- General Purpose Register
- ISR
- Interrupt Service Routine
- IRQ
- Interrupt Request
- Prolog/Epilog
- Code am Anfang/Ende jeder Funktionen/ISR, der dazu dient, verwendete Register zu sichern, den Stack-Frame für lokale Variablen anzulegen (falls benötigt), Stackpointer zu setzen, zurück zu springen (ret, reti), etc.
- SFR
- Special Function Register
- Target
- Zielsystem, in unserem Falle avr
Siehe auch
- Avr
- Atmel
- Compiler
- Sourcevergleich
- WinAVR
- Hallo Welt für AVR (Blinky) - ein erstes Beispiel für avr-gcc
- C-Codebeispiele
Weblinks
- Offizielle Homepage von GCC (en)
- GCC in der deutschen Wikipedia
- WinAVR-Projekt bei sourceforge.net (en)
- avr-gcc und toolchain als Linux-Paket bei sourceforge.net (en)
- avr-gcc-Tutorial auf mikrocontroller.net
- Tipps zu Build und Installation von avr-gcc, binutils und avr-libc unter Linux bei linuxfocus.org
- avr-gcc bei avrfreaks.net (en)
- Nützliche GCC Runtime-Libary
ToDo
- inline asm
- optimizing
Autor
--SprinterSB 11:27, 7. Dez 2005 (CET)