Für den GNU C-Compiler GCC ist ein Port für AVR verfügbar. 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.
Im Gegensatz zu Bascom ist GCC ein reiner Compiler, bringt also keine umfangreiche Bibliothek an Funktionalitäten mit. Jedoch finden sich einige low-level Funktionen zum Lesen aus dem Flash oder Lesen/Schreiben des EEPROMs etc. in der avr-libc
.
Inhaltsverzeichnis
- 1 Allgemeine Charakteristika von avr-gcc
- 2 Ablauf der Codegenerierung
- 3 Kommandozeilen-Optionen
- 4 Builtin Defines
- 5 Sections
- 6 Attribute
- 7 GNU Assembler
- 8 Code-Beispiele
- 9 Fallstricke und häufige Fehler
- 10 Bugs
- 11 Abkürzungen und Bezeichnungen
- 12 Siehe auch
- 13 Weblinks
- 14 ToDo
- 15 Autor
Allgemeine Charakteristika von avr-gcc
- Groß- und Kleinschreibung
- wie in C üblich, wird unterschieden zwischen Groß- und Kleinschreibung, sowohl bei Variablen- und Funktionsnamen, bei Sprungmarken, Makros und ja nach Betriebssystem auch Dateinamen und -erweiterungen.
- 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 Byte|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.
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. 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.
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
) -
-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 classic, <= 8 kByte
mcu Builtin define avr2 __AVR_ARCH__=2
at90s2313 __AVR_AT90S2313__
at90s2323 __AVR_AT90S2323__
at90s2333 __AVR_AT90S2333__
at90s2343 __AVR_AT90S2343__
attiny22 __AVR_ATtiny22__
attiny26 __AVR_ATtiny26__
at90s4414 __AVR_AT90S4414__
at90s4433 __AVR_AT90S4433__
at90s4434 __AVR_AT90S4434__
at90s8515 __AVR_AT90S8515__
at90c8534 __AVR_AT90C8534__
at90s8535 __AVR_AT90S8535__
at86rf401 __AVR_AT86RF401__
- AVR classic, > 8 kByte
mcu Builtin define avr3 __AVR_ARCH__=3
atmega103 __AVR_ATmega103__
atmega603 __AVR_ATmega603__
at43usb320 __AVR_AT43USB320__
at43usb355 __AVR_AT43USB355__
at76c711 __AVR_AT76C711__
- AVR enhanced, <= 8 kByte
mcu Builtin define avr4 __AVR_ARCH__=4
atmega8 __AVR_ATmega8__
atmega8515 __AVR_ATmega8515__
atmega8535 __AVR_ATmega8535__
- AVR enhanced, > 8 kByte
mcu Builtin define avr5 __AVR_ARCH__=5
atmega16 __AVR_ATmega16__
atmega161 __AVR_ATmega161__
atmega162 __AVR_ATmega162__
atmega163 __AVR_ATmega163__
atmega169 __AVR_ATmega169__
atmega32 __AVR_ATmega32__
atmega323 __AVR_ATmega323__
atmega64 __AVR_ATmega64__
atmega128 __AVR_ATmega128__
at94k __AVR_AT94K__
- 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 wielong
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.
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
Section | Ablage | Betrifft | Beschreibung |
.text
|
Flash | 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 | |
.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 |
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
GCC
implementiert folgende Attribute (Auszug):
Attribut | Funktionen | Daten | Typen | Beschreibung |
section ("<name>")
|
(X) | (X) | (X) | Lokatiert nach Section <name>
|
noreturn
|
X | |||
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
avr-gcc
implementiert folgende Attribute:
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 */ }
GNU Assembler
#include <avr/io.h> ; das gibt den Controllertyp an .text ; was nun folgt, gehört in den FLASH-Speicher .global main ; main ist auch in anderen Modulen bekannt main: ; zu 'main' wird nach Reset hingesprungen ;.... eigene befehle Hauptschleife: ; ein Sprunglabel ;.... eigene befehle rjmp Hauptschleife ; immer wiederholen
Und das wars auch schon.
Es bietet sich an, GCC aufzurufen und ihn die Arbeit an Assembler und Linker delegieren zu lassen. Standard Datei-Erweiterung dazu ist *.S
:
avr-gcc -o beispiel.elf -mmcu=atmega8 beispiel.S
Falls die Plattform Ärger mit Groß/Kleinschreibung macht wie Windows, dann geht auch
avr-gcc -x assembler-with-cpp -o beispiel.elf -mmcu=atmega8 beispiel.ss
GCC legt die Vektortabelle selbständig an und nimmt die richtigen Einträge vor; auch für den RESET-Vektor. Bevor zu main
gesprungen wird, wird noch eine kleine Initialisierung gemacht: Stackpointer setzen und Y-Reg darauf initialisieren, um es als Framepointer nutzen zu können.
Falls man eine IRQ bedienen möchte, schreibt man
.text .global SIG_OVERFLOW0 ; alternativ: __vector_9 schreiben, wenn 9 die IRQ-Nummer ist ; SIG_XXX ist ein #define aus avr/ioxxxx.h ; das durch #include <avr/io.h> mitincludet wird SIG_OVERFLOW0: ; dito ; ISR-Code reti
Dadurch wird an der richtigen Stelle der Vektortabelle ein Eintrag veranlasst.
Ein Disassemble des Maschinencodes sieht dann so aus (Vectab gekürzt):
Disassembly of section .text: 00000000 <__vectors>: 0: 12 c0 rjmp .+36 ; 0x26 2: 18 c0 rjmp .+48 ; 0x34 4: 17 c0 rjmp .+46 ; 0x34 ... 12: 11 c0 rjmp .+34 ; 0x36 ... 00000026 <__ctors_end>: 26: 11 24 eor r1, r1 28: 1f be out 0x3f, r1 ; 63 2a: cf e5 ldi r28, 0x5F ; 95 2c: d4 e0 ldi r29, 0x04 ; 4 2e: de bf out 0x3e, r29 ; 62 30: cd bf out 0x3d, r28 ; 61 32: 02 c0 rjmp .+4 ; 0x38 00000034 <__bad_interrupt>: 34: e5 cf rjmp .-54 ; 0x0 00000036 <__vector_9>: 36: 18 95 reti 00000038 <main>: 38: ff cf rjmp .-2 ; 0x38
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 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, was über cli
und sei
realisierbar ist.
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
#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();
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 Funktionnaked
, 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
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 reflexartig gesetzter ;
zu viel:
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.
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.
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.
Bugs
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
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
- Code atomar machen
- inline Assembler
- Includes
- file-tree
- sh-interface
- Hacks
Autor
--SprinterSB 11:27, 7. Dez 2005 (CET)