Aus RN-Wissen.de
Version vom 30. Januar 2006, 18:05 Uhr von SprinterSB_alt (Diskussion | Beiträge) (Sections: Flash- und RAM-Verbrauch)

Wechseln zu: Navigation, Suche
Laderegler Test Tueftler Seite

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

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 "Hallo Welt für AVR (LED blinken)", 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.
size_t
size_t ist unsigned und immer 16 Bit groß, unabhängig davon , ob mit -mint8 übersetzt wird oder nicht.

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

Zusammenspiel zwischen avr-gcc und binutils

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.
Es ist auch möglich, den Linker mit der Optionen --oformat=... zu starten, damit er direkt das gewünschte Ausgabeformat erzeugt.

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
-MM
Für die angegebenen Eingabe-Dateien wird eine Ausgabe erzeugt, die als Makefile-Fragment dienen kann und die Anhängigkeiten (dependencies) der Objekte von den Quellen/Headern beschreibt.
-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)
-Wa,-a=<filename>
Assembler erzeugt ein Listing mit Name <filename>
-Wp,<options>
übergibt Komma-getrennte Liste <options> an den Preprozessor
-Wl,<options>
übergibt Komma-getrennte Liste <options> an den Linker (avr-ld)
-Wl,-Map=<filename>
Linker erzeugt ein Map-File mit Name <filename>
-Wl,--oformat=<format>
Linker erzeugt Ausgabe im Format <format>, z.b. ihex für Intel-HEX-File
-Wl,--section-start<section>=<address>
Linker legt die Section <section> ab Adresse <address>, z.B: .eeprom=0x810001
-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 I/O-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 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__
Definiert, falls GCC die Eingabe als Assembler-Code betrachtet und nicht compiliert. Weiterleitung an den Assembler.
__cplusplus
Es wird C++ übersetzt (Quell-Endung *.cpp, *.c++ oder Option -x c++).

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".

Tabelle: Bedeutung der Sections bei avr-gcc
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.

Flash- und RAM-Verbrauch bestimmen

Das geht nicht mit avr-gcc, sondern mit Werkzeugen, die zu den Binutils gehören und z.B bei WinAVR dabei sind.

Abhängig von der Section schlägt ihr Platzverbrauch in Flash/SRAM/EEPROM zu Buche:

Tabelle: Zuordung des Platzberbrauchs zur Section-Größe
belegter Speicher Sections (Einzelgrößen addieren) Beschreibung
Flash .text + .data Programmcode und Tabelle für initialisierte Daten
SRAM .data + .bss + .noinit Daten (initialisiert, zu 0 initialisiert, nicht initialisiert)
EEPROM .eeprom Daten, die man ins EEPROM gelegt hat

Beispiele:

Verbrauch der einzelnen Module auflisten:

> avr-size -x foo1.o foo2.o ...

Verbrauch des gesamten Programms auflisten:

> avr-size -x -A foo.elf

Platzverbrauch von Funktionen, Objekten, etc. nach Größe sortiert:

> avr-nm --size-sort -S foo.elf


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

Tabelle: Attribute von GCC (Auszug)
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
unused x Variable wird nicht verwendet, z.B. bei Funktionsparametern, die nicht gebraucht werden. Vermeidet entsprechende Warnungen.

Attribute von avr-gcc

Tabelle: 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

RAM-Layout für ein AVR mit 1kByte SRAM

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.

Resourcen-schonendere Möglichkeiten, zur Laufzeit an Speicher zu kommen, bieteten __builtin_alloca und dynamische Arrays. 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, denn es muss nur ein Wert zum Framepointer addiert werden. Den so erhaltenen Speicher braucht man auch nicht freizugeben. Das geschieht automatisch beim Verlassen der Funktion in deren Epilog, indem der Wert wieder vom Framepointer subtrahiert wird.

Von der Verwendung ist der mittels __builtin_alloca und dynamischer Arrays 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!

Der Speicherbereich ist dort gültig, wo auch die Adresse einer 'normalen' lokalen Variablen gültig wäre, wenn diese an der gleichen Stelle definiert würde.

Das Programm/der Algorithmus muss daher beim Beschreiten dieses Wegs darauf angepasst sein.

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]
   ...
}

oder mittels eines dynamischen Arrays:

void function (size_t num_data)
{
   // data_t hat man irgendwo selber definiert, oder es ist ein elementarer Typ
   data_t p[num_data];

   // Mach was mit p[0] ... p[num_bytes-1]
   ...
}

C++

Aus Effizienzgründen sollte der Einsatz von C++ sehr kritisch betrachtet werden. Ansonsten ist zu C++ nicht viel zu sagen. Als Treiber verwendet man wie immer avr-gcc. Standard-Endungen für C++ sind .c++ und .cpp. Bei anderen Endungen teilt man mit -x c++ mit, daß es sich um C++ Dateien handelt.

Interrupt-Service-Routinen (ISRs) sind C-Funktionen und werden definiert wie gehabt. Siehe auch Interrupts.

#include <avr/io.h>
#include <avr/signal.h>

#if defined (__cplusplus)
extern "C" {
#endif /* __cplusplus */   

SIGNAL (SIG_NAME)
{
   /* machwas */
}

INTERRUPT (SIG_NAME)
{
   /* mach was */
}

#if defined (__cplusplus)
}
#endif /* __cplusplus */   

__cplusplus ist ein Standard GCC-Builtin-Define.

Globale Konstruktoren werden in Section .init6 ausgeführt, die Destruktoren in .fini6.

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, sodaß die ISR nicht durch andere Interrupt-Anforderungen unterbrochen wird. Beim Verlassen der ISR werden Interrupts wieder aktiviert. Tritt während der ISR ein IRQ auf, wird diese erst nach Beenden des ISR-Codes ausgeführt. Der Interrupt geht also nicht verloren. Zwischen zwei ISRs wird zusätzlich mindestens ein Befehl des normalen Programm-Codes abgearbeitet.
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. Weiterhin kann man schneller auf bestimmte Ereignisse reagieren. Tritt während der ISR ein anderer IRQ auf, der schnell bedient werden muss, kann sofort der dringende ISR-Code ausgeführt werden. Ansonsten (Verwendung von SIGNAL) würde der Code erst ausgeführt werden, nachdem die aktuelle ISR beendet ist.

Dauert die ISR zu lange und wird sie nochmals von ihrem eigenen IRQ unterbrochen, stürzt man ab.

Nachschlagen kann man den Namen 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();

Sperrt man eine Code-Sequenz durch Einschachteln in ein cli/sei Paar (man macht das Codestück "atomar", also ununterbrechbar), gehen währenddessen keine Interrupt-Anforderungen verloren. Die entsprechenden IRQ-Flags bleiben gesetzt, und nach dem sei werden die IRQs in der Reihenfolge ihrer Prioritäten abgearbeitet. Ausnahme ist, wenn in einem atomaren Block der selbe IRQ mehrfach auftritt. Der ISR-Code wird dann trotzdem nur einmal ausgeführt.

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ückzusetzen 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.

So kann man z.B. eine Meldung ausgeben, eine Warnlampe blinken, in einer Endlosschleife landen, oder über den Watchdog einen richtigen Hardware-Reset auslösen, siehe auch Abschnitt "Reset auslösen".

ISR mit eigenem Prolog/Epilog

Wenn man in einer ISR komplett eigenes Zeug machen will, dann definiert man eine nackte Funktion. Mit naked befreit man die Routine vom Standard-Prolog/Epilog.

Dabei ist darauf zu achten, daß die ISR mit reti (return from interrupt) zurückkehrt und evtl. verwendete Register und den Status (SREG) sichert.

#include <avr/io.h>

void __attribute__ ((naked)) 
SIG_OVERFLOW0 (void)
{
   /* Port B.6 = 0                                                         */
   /* Diese Instruktion verändert nicht das SREG und kein anderes Register */
   /* so daß der eigentliche Code nur 1 Befehl lang ist                    */
   __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

Wiederum kann man als Funktionsname __vector_default nehmen, um nicht-implementierte IRQs abzufangen:

void __attribute__ ((naked)) 
__vector_default (void)
 ...

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++; 
    }
 }

Reset auslösen

Falls ein Reset per Software ausgelöst werden soll, dann geht das am besten über den Watchdog. Einfach nur an den RESET-Punkt an Adresse 0 zu springen mit

goto *((void**) 0);

initialisiert zwar den Controller von neuem, aber es macht keinen wirkliches RESET mit Zurücksetzen der Hardware und allen I/O-Registern.

Durch den Watchdog kann man ein 'richtiges' RESET-Signal erzeugen lassen, so daß die AVR-Hardware genau so initialisiert ist, wie nach einem externen RESET. So kann man z.B. via UART ein RESET-Kommando schicken. Allerdings lässt sich der Watchdog nur minimal auf 15ms einstellen:

#include <avr/wdt.h>
#include <avr/interrupt.h>
...   
   cli();                     // Interrupts global abschalten
   wdt_enable (WDTO_15MS);    // Watchdog aufziehen auf 15ms
   while (1);                 // warten, bis er zubeisst...

Welches Ereignis einen RESET ausgelöst hat, kann man im Register MCUCSR (MCU Control and Status Register) erfahren. Es gibt 4 mögliche RESET-Quellen:

  • Power-On Reset
  • External Reset
  • Brown-Out Reset
  • Watchdog Reset

Soll der Inhalt von Variablen einen Reset überleben – eine Variable also nicht initialisiert werden – dann geht das so:

#include <avr/io.h>

// status informiert z.B. darüber, ob wir selber den Watchdog ausgelöst haben
// oder nicht, oder andere Informationen
unsigned char status __attribute__ ((section (".noinit")));

void main (void)
{
   // Wert von MCUSCR merken, möglichst früh im Programm
   unsigned char mcucsr = MCUCSR;

   // MCUCSR zurücksetzen
   MCUCSR = 0;

   // Watchdog-Reset
   if (mcuscr & (1 << WDRF))
   {
       // status auswerten 
   }

   // Power-On Reset: status auf definierten Wert setzen
   if (mcuscr & (1 << PORF))
   {
       status = 0;
   }

   // status auswerten
   ...
}


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>

Als Pfad-Separator wird immer ein / verwendet, auch auf Windows-Betriebssystemen! Also kein \ !

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

Optimierungen, Tipps & Tricks

Beim Programmieren in C möchte man sich möglichst wenig mit der Codeerzeugung selbst auseinandersetzen. Man verwendet ja gerade deshalb einen Compiler und programmiert nicht in Assembler, weil man sich nicht um Register-Belegungen o.ä. kümmern will, sondern nur um die zu lösende Aufgabe. GCC erzeugt zwar recht guten Code, aber er ist nicht perfekt. Gerade auf Systemen wie AVR mit nur sehr begrenzten Resourcen muss man daher dem Compiler hilfreich zur Seite stehen, wenn man noch dichteren/schnelleren Code erhalten möchte.

Um das Ergebnis zu beurteilen, hilft ein Blick ins Listfile. Siehe dazu auch die Abschnitte "Listfile erstellen" und "Die Größe ermitteln" im Hallo Welt für AVR.

Optimierungsgrad

Als Optimierungsgrad erweist sich -Os (Optimize for Size) als der beste, evtl. noch -O2. Ohne Angabe eines Optimierungsgrades wird nicht optimiert, was gleichbedeutend mit der Option -O0 ist. Abzuraten ist von der maximalen Optimierung -O3, die wegen function inlining und loop unrolling zu sehr breitem Code führt und für AVR absolut nicht angesagt ist.

Vermeide printf, scanf, malloc

Funktionen von diesem Kaliber sind die absoluten Platz- und Zeitfresser.

Alternativen findet man reichlich in der avr-libc wie itoa und atoi. Und für malloc und Konsorten ist das Compiler-Builtin __builtin_alloca eine effiziente Alternative, siehe auch im Abschnitt "Dynamische Speicherallokierung".

Konstante Strings ins Flash

Konstante Strings wie sie zu Ausgabezwechen Verwendung finden werden im Programm oft nicht verändert und brauchen nicht SRAM zu belegen (und damit auch Flash, von wo aus sie vom Startup-Code ins SRAM kopiert werden), sondern gehören ins Flash!

Entsprechende Routinen, um auf Strings die im Flash liegen zuzugreifen, tragen die Suffix _P, wie z.B. strcmp_P mit dem Prototyp

extern int *strcmp_P (char *, const prog_char *)

Die Implementierungen befinden sich in der avr-libc.

Anwendung:

#include <avr/pgmspace.h>

const prog_char str_p[]     = "Ein String im Flash";
const char str2_p[] PROGMEM = "Noch ein String im Flash";
...
   // String im SRAM mit String im Flash vergleichen
   if (!strcmp_P (str_sram, str_p))
   {
       // mach was bei Gleichheit
   }
...
}

Sprungtabelle

Genauso macht man auch eine Sprungtabelle, um anhand von Kommando-Strings dazugehörige Funktionen ausführen zu lassen:

#include <avr/pgmspace.h>

int func1 (int arg)
{
   ...
}

#define TEXT_LEN 15

// Die Kommandostruktur
typedef struct 
{
   int (*func)(int);     // Zeiger auf die auszuführende Funktion
   int arg;              // das Argument, das mitübergeben wird
   char text[1+TEXTLEN]; // Text, maximal TEXT_LEN Zeichen lang
} command_t;

// Das Array mit den Kommandos.
// Die funcx sind vom Prototyp
// int funcx (int arg);
const command_t commands[] PROGMEM =
{
   { func1, 0, "Befehl 1" },
   { func2, 3, "Befehl für func2" }
};

// Sucht in commands[] nach text und führt gegebenenfalls
// die dazugehörige Funktion funcx mit Argument arg aus.
// Liefert den Rückgabewert von funcx 
// oder -1, falls text nicht gefunden wurde.
int execute (const char *text)
{
   // Schleifenvariable
   unsigned char i;

   // Wandert durch das Array mit Kommando-Strukturen
   const command_t * cmd = commands;

   // sizeof wird von gcc ausgewertet und ist wie eine Konstante,
   // denn beide sizeofs sind zur Compilezeit bekannt
   for (i=0; i < sizeof(commands) / sizeof(command_t); i++)
   {
      // Ist das der gesuchte String? 
      if (strcmp_P (text, cmd->text))
      {
        // Nein, dann weitersuchen
        cmd++;
        continue;
      }

      // Ja
      int (*func)(int), arg;

      // Dann Funktionszeiger und Argument besorgen,
      func = (int(*)(int)) pgm_read_word (& cmd->func);
      arg  = (int)         pgm_read_word (& cmd->arg);

      // Funktion ausführen und deren Wert zurückliefern      
      return func (arg);
   }

   // text ist nicht in commands
   return -1;
}

Nachteil dabei ist, daß jeder String den maximalen Platz von TEXT_LEN+1 Zeichen belegt. Falls man da noch weiter sparen will, dann kann man die Strings wieder ins Flash legen und ihre Adresse in der Struktur merken. Dadurch belegt ein String nur noch Länge+3 Zeichen (+3 wegen 1 Endezeichen und 2 Bytes für seine Adresse). Die Definition der Tabelle wird aber umständlicher, weil jeder String einzeln angegeben werden muss:

#include <avr/pgmspace.h>

// Die Kommandostruktur
typedef struct 
{
   ...
   char *text; // Zeiger auf Text
} command_t;

const prog_char str_1[] = "Befehl 1";
const prog_char str_2[] = "Befehl für func2";

const command_t commands[] PROGMEM =
{
   { func1, 0, str_1 },
   { func2, 3, str_2 }
};

// Sucht in commands[] nach text und führt gegebenenfalls
// die dazugehörige Funktion funcx mit Argument arg aus.
// Liefert den Rückgabewert von funcx 
// oder -1, falls text nicht gefunden wurde.
int execute (const char *text)
{
   // Schleifenvariable
   unsigned char i;

   // Wandert durch das Array mit Kommando-Strukturen
   const command_t * cmd = commands;

   // sizeof wird von gcc ausgewertet und ist wie eine Konstante,
   // denn beide sizeofs sind zur Compilezeit bekannt
   for (i=0; i < sizeof(commands) / sizeof (command_t); i++)
   {
      const prog_char * text_P;

      // Liest die Startadresse von str_x
      text_P = (const prog_char *) pgm_read_word (& cmd->text);

      // Ist das der gesuchte String?	
      if (strcmp_P (text, text_P))
      {
         ...

Lokale Variablen verwenden

Beim Manipulieren globaler Variablen kann es günstig sein, diese in eine lokale Variable zu kopieren, dort zu verändern, und sie danach wieder zu schreiben

char var;

void foo1()
{
   var++;
   if (var > 10)
      var = 1;
} 

Dadurch wird einmal unnötig gespeichert (der dritte Befehl kann vermieden werden).

foo1:
  lds r24,var        ; *movqi/4	[length = 2]
  subi r24,lo8(-(1)) ; addqi3/2	[length = 1]
  sts var,r24        ; *movqi/3	[length = 2]
  cpi r24,lo8(11)    ; cmpqi/2	[length = 1]
  brlt .L3           ; branch	[length = 1]
  ldi r24,lo8(1)     ; *movqi/2	[length = 1]
  sts var,r24        ; *movqi/3	[length = 2]
.L3:
  ret	

Indem man eine lokale Variable (var2) verwendet für die Änderung von var vermeidet man dies:

char var;

void foo2()
{
   char var2 = var;
   
   var2++;
   if (var2 > 10)
      var2 = 1;
      
   var = var2;
} 

Dadurch wird erst am Ende gespeichert. var2 lebt in Register r24.

foo2:
  lds r24, var       ; *movqi/4   [length = 2]
  subi r24,lo8(-(1)) ; addqi3/2   [length = 1]
  cpi r24,lo8(11)    ; cmpqi/2    [length = 1]
  brlt .L2           ; branch     [length = 1]
  ldi r24,lo8(1)     ; *movqi/2   [length = 1]
.L2:
  sts var, r24       ; *movqi/3   [length = 2]
  ret

Bei diesem einfachen Beispiel spart man lediglich eine Instruktion. Bei komplexeren Rechnungen oder längeren Datentypen kann es aber durchaus lohnender sein, in lokale Register zu kopieren.

Arithmetik

Daten zerlegen/zusammensetzen

In systemnahen Programmen hat man oft was Problem, auf die einzelnen Bytes oder Bitfelder einer grösseren Datenstruktur zuzugreifen. Indem man sich ein Komposit baut, das die gewünschten Strukturen überlagert, kann man effizient z.B. auf Bytes zugreifen. Ausnahme sind Bitfelder, deren Verwendung etwas breiten Code ergibt. Bitfelder "von Hand" zu manipulieren, ist da manchmal effizienter, führt jedoch zu schlecht lesbarem Code.

typedef ... foo_t;

typedef union
{
   unsigned char byte[4];       // Zugriff als Bytes (8 Bit)
   unsigned short word[2];      // Zugriff als Words (16 Bit)
   signed long slong;           // Zugriff als signed long (32 Bit)

   struct                       // Zugriff auf einzelne Bitgruppen
   {
      unsigned bit_0_3  : 4;
      unsigned bit_4_8  : 5;
      unsigned bit_9_21 : 13;
      unsigned bit_22_31: 10;
   };   

   foo_t foo;                   // Zugriff als foo-Struktur
} data_t;

...
{
   data_t data;

   data.b[2] = 12;             // setzt byte 2 auf 12
   data.bit_4_8 = 0x1f;        // setzt bits 4..8 (5 Stück) alle auf 1

   int anInt = data.foo.anInt; // liest ein Feld von foo (hier ein int)
   ...
}

libgcc2 verwenden

In der libgcc2 sind einige Arithmetik-Routinen in Assembler implementiert. Dazu gehören ein paar Algorithmen zu Division (mit Rest) und Multiplikation.

Von diesen Algorithmen werden durch die avr-libc jedoch nur zwei Strukturen und Funktionen veröffentlicht: div_t und ldiv_t resp. die Funktionen div() und ldiv(). Siehe dazu deine Dokumentation zur avr-libc. Damit kann man Quotient und zusätzlich den Rest bei einer Division 16/16 bzw. 32/32 berechnen lassen; den Rest bekommt man quasi kostenlos als Nebenprodukt. Das ist praktisch, wenn man z.b. eine Zahl in Dezimaldarstellung umwandeln möchte oder von/nach BCD.

Zusätzlich zu den via avr-libc veröffentlichten Funktionen gibt es aber noch Routinen, die z.B. auf 8-Bit-Werten operieren oder mit unsigned Typen und dementsprechend effizienter sind.

Beispiel: Umwandeln nach Dezimalstring

Hier ein Beispiel, das Division mit Rest für unsigned short verwendet, um eine 16-Bit-Zahl in Dezimaldarstellung zu wandeln:

// Struktur definieren und Funktion bekannt machen
typedef struct
{
	unsigned short quot;
	unsigned short rem;
} udiv_t;

extern udiv_t udiv (unsigned short, unsigned short) __asm__("__udivmodhi4");

// 5 Ziffern und evtl. noch eine führende 0
#define DIGITS 6

// +1 wegen String-Ende (wird im Startup auf 0 gesetzt)
char string[DIGITS+1];

// Wandelt zahl in Dezimaldarstellung um.
// Der return-Wert zeigt irgendwo ins string[]-Array.
// string[] wird verändert.
char* toString (unsigned short zahl)
{
	// s zeigt auf das Ende von string
	// string wird von hinten nach vorne gefüllt
	char *s = string + DIGITS;
	
	// qrem enthält Quotient (quot) und Rest (rem) der Divisionen
	udiv_t qrem = {.quot = zahl};                  

	do
	{
		// Division mit Rest durch 10
		// quot: Ergebnis für den nächsten Durchlauf
		// rem:  Rest ist die Ziffer im 10er-System
		qrem = udiv (qrem.quot, 10);
		
		// Ziffer in Zeichen wandeln und speichern
		*(--s) = '0' + qrem.rem;
	}	
	while (0 != qrem.quot);
	
	// Falls eine führende '0' gespeichert wurde: weg damit
	// ausser zahl war selbst schon 0
	if (*s == '0' && *(s+1) != '\0')
		s++;

	return s;
}

Falls man eine Division und/oder Rest für 8-Bit braucht, dann geht für unsigned analog.

Beispiel: BCD-Umrechnung

Wandeln einer 8-Bit-Zahl 0 <= num < 100 nach BCD

typedef struct
{
	unsigned char quot;  // Quotient
	unsigned char rem;   // Rest (remainder)
} udiv8_t;

extern udiv8_t udiv8 (unsigned char, unsigned char) __asm__ ("__udivmodqi4");

// Wandelt num nach BCD um, 0 <= num <= 99
// return-Wert ist dann 0x0 <= return <= 0x99
unsigned char to_bcd (unsigned char num)
{
	udiv8_t qrem = udiv8 (num, 10);
	
	return (unsigned char) (qrem.quot << 4) | qrem.rem;
}

Division durch Multiplikation

Vermeiden von float und double

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, uisp
  • AVR-Simlator: (simulavr),
  • Programmers Notepad (kleiner Editor)
  • Demo-Projekte
    • LED an PWM
    • TWI
  • 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
   ; low-Teil laden
   lds r24,i
   ; high-Teil laden
   ; 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;  // Das sollte UNBEDINGT eine lokale Variable sein, 
                               // sonst müßte man explizit auf den STACK pushen. 
                               // Sonst wäre nämlich diese Programmstelle nicht reentrant, was 
                               // recht tückisch sein kann. 
   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 Konstanten 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 Komma getrennten Anweisungen, so daß 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

Siehe dazu auch Warteschleife.

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

ToDo

  • inline asm
  • optimizing

Autor

--SprinterSB 11:27, 7. Dez 2005 (CET)


LiFePO4 Speicher Test