Compiler und Standardbibliothek
Worum geht's?
[Bearbeiten]Sowohl der C Compiler der avr-gcc Suite als auch die Implementierung der Standardbibliothek AVR Libc sind bemüht sich konform zum C Standard zu verhalten. Dennoch gibt es eine Reihe von Abweichungen und Besonderheiten, über die Du Dich in diesem Kapitel informieren kannst.
In diesem Kapitel geht es nicht um Informationen, die im Sprachstandard nachgelesen werden können. Vielmehr geht es um Informationen, die sowohl für den Anwendungsfall AVR Microcontroller und, als auch für die eingesetzten Werkzeuge speziell sind.
Für erste Gehversuche mit der Microcontroller Programmierung ist es nicht zwingend erforderlich sich alle Details dieses Kapitels auswendig zu merken.
Das Kapitel soll einen Überblick über die wichtigsten Besonderheiten geben und als Anlaufstelle dienen.
Datentypen und Werte
[Bearbeiten]Abweichend vom C-Standard verfügt der Datentyp double nur über 32 Bit. Mit dem Compiler Schalter -mint8 können alle Datentypen bis hinauf zum Datentyp int auf eine Länge von 8 Bit verkürzt werden.
Werte
[Bearbeiten]Die Byte Reihenfolge, in der Daten im Speicher abgelegt werden, entspricht dem Little-Endian Format. Sowohl der Datentyp float als auch der Datentyp double werden im IEEE 754 single Format mit 32-bit gespeichert.
Ergänzend zu den im C Standard festgelegten Darstellungsformaten für literale Zahlenwerte stellt der C Compiler der avr-gcc Compiler Suite eine weitere Schreibweise zur Angabe von ganzzahligen Werten zur Verfügung. Mit dem Präfix 0b können literale Werte im Quelltext im Binärsystem angegeben werden.
uint8_t number = 0b00110011;
Größe
[Bearbeiten]Die Grunddatentypen der Programmiersprache C sollten Dir bekannt sein. Der C-Standard gibt keine genaue Auskunft über die Größe dieser Datentypen. Stattdessen lässt er im Rahmen gewisser Grenzen Spielraum.[1]
Gerade für Microcontroller ist es besonders interessant. Der Speicherplatz ist begrenzt. Zudem steht nur ein 8-bit ALU mit eingeschränktem Befehlssatz zur Verfügung. Kompliziertere Rechnungen und Rechnungen mit größeren Datentypen muss der Compiler in eine Serie von Maschinenbefehlen zerlegen.
Im allgemeinen ist es eine gute Idee die mit C99 eingeführten Datentypen fester Größe der Header Datei <stdint.h> zu verwenden. Spätestens bei der Verwendung von Funktionen der Standardbibliothek lässt sich die Frage nach der Größe der Grunddatentypen nicht umgehen. Die Signaturen dieser Funktionen sind mit Grunddatentypen angegeben.
Die folgende Tabelle enthält eine Übersicht über die Größe der Datentypen sowie der wichtigsten typedefs.
Typ | char | short | int | long | long long | wchar_t | void* | float | double | size_t | ptrdiff_t |
---|---|---|---|---|---|---|---|---|---|---|---|
Größe in Byte / alias | 1 | 2 | 2 | 4 | 8 | 2 | 2 | 4 | 4 | unisgned int | int |
Seit der avr-gcc Version 4.8 steht zusätzlich zu diesen Datentypen auch eine eingeschränkte Unterstützung für Festkommazahlen nach ISO/IEC TR 18037 zur Verfügung.
Angaben zur Größe der Datentypen, sowie zur Länge der Vor- und Nachkommastellen finden sich in folgender Tabelle:
Typ | Größe in Byte | unsigned | signed | ||
---|---|---|---|---|---|
short _Fract | 1 | 0 | 8 | ± 0 | 7 |
_Fract | 2 | 0 | 16 | ± 0 | 15 |
long _Fract | 4 | 0 | 32 | ± 0 | 31 |
short _Accum | 2 | 8 | 8 | ± 8 | 7 |
_Accum | 4 | 16 | 16 | ± 16 | 15 |
long _Accum | 8 | 32 | 32 | ± 32 | 31 |
Rechnen
[Bearbeiten]Welcher Datentyp bei der Programmierung verwendet wird spielt, ist nicht nur dann wichtig, wenn es darum geht Speicherplatz zu sparen. Auch wenn es darum geht Rechenzeit zu minimieren kann die Wahl der verwendeten Datentypen eine Rolle spielen.
Der Microcontroller arbeitet mit einer Geschwindigkeit die um Größenordnungen geringer ist als die eines PC. Die CPU und das Rechenwerk des Microcontrollers arbeiten nur mit 8 Bit. Rechenoperationen mit größeren Zahlen müssen in eine Serie von Maschinenbefehlen zerlegt werden. Für Division und Modulo Rechnung gibt es keinen Maschinenbefehl – auch nicht mit 8 Bit. Auch diese Operationen müssen in jedem Fall in eine Serie von Maschinenbefehlen zerlegt werden.
Das Rechenwerk bietet keine Maschinenbefehle für das Rechnen mit Fließkommazahlen. Berechnungen mit den Datentypen double und float müssen ebenfalls in Software ausgeführt werden. Der Compiler bringt zwar eine Standardimplementierung mit. Die Mathematik Bibliothek der AVR Libc bringt von Hand optimierte Routinen für die Berechnung mit. In der Regel ist es besser diese Funktionen zu verwenden. Um sie zu verwenden muss dass Programm gegen die Mathematik Bibliothek (libm) gelinkt werden.
Daten im Programmspeicher
[Bearbeiten]Die Programmiersprache C wurde für Rechner mit von von Neumann Architektur entworfen. Daten und Programmcode liegen bei Rechnern dieses Architekturmodells im selben Speicher. AVR Microcontroller folgen dem Modell der Harvard-Architektur. Rechner dieses Architekturmodells verfügen über zwei separate Speicherwerke – Datenspeicher und Befehls- oder auch Programmspeicher – in denen Daten, bzw. Programmcode liegen.
Um Platz im Datenspeicher zu sparen bieten AVR Microcontroller die Möglichkeit zusätzlich zum Programmcode auch konstante Werte im Programmspeicher abzulegen und von dort zu lesen. Die Programmiersprache C verfügt über keine Sprachmittel um dem Compiler mitzuteilen, dass bestimmte konstante Werte im Programmspeicher abgelegt, bzw. von dort gelesen werden sollen. Die Header Datei <avr/pgmspace.h> der AVR Libc stellt Makros und Funktionen zur Verfügung, die diese Aufgaben übernehmen können.
Um konstante Werte im Programmspeicher abzulegen kann das Makro PROGMEM verwendet werden.
#include <avr/pgmspace.h>
const float pi PROGMEM = 3.141;
const uint8_t values[] PROGMEM = { 1, 2, 3, 4 };
const char message[] PROGMEM = "hello world";
Um auf diese Weise im Programmspeicher abgelegte Werte im Programm verwenden zu können, müssen sie mit den in <avr/pgmspace.h> deklarierten Funktionen der AVR Libc eingelesen werden. Funktionen, die mit Zeichenketten arbeiten, die im Programmspeicher abgelegt sind, tragen die Endung _P.
float f = pgm_read_float(&pi);
uint8_t v = pgm_read_byte(values + 2);
size_t l = strlen_P(message);
Compiler Schalter
[Bearbeiten]Auf der Kommandozeile kann sowohl das Verhalten des C Compilers, als auch des Linkers der GNU Compiler Collection mit zahlreichen Optionen beinflusst werden. Die Übersicht hier beschränkt sich auf die Schalter, die im Makefile des vorangegangenen Kapitels verwendet werden und bisher noch nicht erklärt wurden. Eine vollständige Liste aller verfügbaren Optionen findet sich im GCC Manual.[2]
Verarbeitungsschritte
[Bearbeiten]Um eine Quelldatei in eine Objektdatei zu übersetzen nimmt der Compiler eine Serie von Verarbeitungsschritten vor. Auf dem Weg zur fertigen Objektdatei durchläuft die Quelldatei so eine Reihe von Zwischenstadien. Hin und wieder kann es nützlich sein den Übersetzungsvorgang nach einem bestimmten Verarbeitungsschritt zu unterbrechen um einen Blick auf den aktuellen Zwischenstand der Verarbeitung zu werfen.
Mit dem Schalter -E kann der Compiler dazu angewiesen werden die Verarbeitung einer Quelldatei zu beenden nachdem sie durch den Präprozessor gelaufen ist. Das Makefile des vorangegangenen Kapitels enthält eine Regel, die es ermöglicht diese Option zu nutzen.
%.E:%.c
$(CC) $(CPPFLAGS) -E -o $@ $<
Um aus der Datei quelle.c eine Datei quelle.E zu erzeugen, die den Zustand des Quellcodes nach der Bearbeitung durch den Präprozossor enthält, kann das Makefile mit dem folgenden Aufruf gestartet werden.
make quelle.E
Mit dem Schalter -S kann der Compiler angewiesen werden die Verarbeitung einer Quelldatei nach der Umwandlung in Assembler Code zu beenden. Auch um diese Option zu nutzen enthält das Makefile des vorangegangenen Kapitels eine entsprechende Regel.
%.s:%.c
$(CC) $(CPPFLAGS) -S -o $@ $<
Mit dem folgenden Aufruf kann das Makefile genutzt werden, um die Datei quelle.s mit dem aus der der Datei quelle.c erzeugten Assembler Code zu erstellen.
make quelle.s
Sprachversion
[Bearbeiten]Der C Compiler des avr-gcc unterstützt verschiedene Sprachversionen. Die Sprachversion kann mit dem Schalter -std Sprachversion gewählt werden.
Die im Makefile des vorangegangenen Kapitels vorgegebene Version gnu99 erlaubt es den Quellcode im ISO C99 Format zu schreiben. GNU Erweiterungen[3] wird im Falle eines Konflikts allerdings Vorrang eingeräumt.
Eine Liste verfügbarer Sprachversionen findet sich im GCC Manual.[4]
Achtung! Auch bei der Option c99 bleiben GNU Erweiterungen, die nicht im Konflikt mit dem Standard stehen weiterhin aktiv. Wenn Du Wert darauf legst ISO C99 konformen Code zu schreiben, kannst Du den Schalter -pedantic verwenden. Der Compiler wird dann für alle nicht ISO C konformen Konstrukte Warnungen ausgeben.
Warnungen
[Bearbeiten]Warnungen bieten eine nützliche Hilfe. Im allgemeinen ist es deshalb eine gute Idee die Ausgabe von Compiler Warnungen nicht nur zu aktivieren, sondern ausgegebene Warnungen auch ernst zu nehmen.
Der Compiler mag bei der Ausgabe von Warnungen häufig ein wenig kleinlich erscheinen und auch vor Schwierigkeiten warnen, die bereits im Programmfluss abgefangen wurden. Trotzdem können sie eine wertvolle Hilfe sein.
Die Wahrscheinlichkeit, dass der Compiler Stellen im Code anmahnt, an denen tatsächlich etwas schief gehen kann, mag zwar verschwinden gering erscheinen. Die Wahrscheinlichkeit, dass wenn etwas schief geht, dies an einer Stelle geschieht, die der Compiler angewarnt hat ist, ist dafür nahezu sicher.
Warnungen, die der avr-gcc ausgeben kann, können sehr fein kontrolliert werden. Der einfachste Weg einen gewissen Vorrat an Warnungen zu aktivieren besteht in der Benutzung des Schalters -Wall. Eine vollständige Liste aller Warnungen, die mit diesem Schalter aktiviert werden, findest Du im GCC Manual.[5] Auch alle weiteren Warnungen, die im Makefile aktiviert werden, findest Du an gleicher Stelle Dokumentiert
Optimierung
[Bearbeiten]Optimierung: -Os Beim Debuggen kann es nützlich sein, den Compiler anzuweisen den generierten Code nicht ganz so aggressiv zu optimieren. Zu diesem Zweck kann der Schalter -Og angegeben werden. Wenn alle Stricke reißen, kann die Optimierung mit dem Schalter -O0 ganz deaktiviert werden.
Zusätzlich zu den allgemeinen Einstellungen können einzelne Aspekte der Optimierung separat über Schalter konfiguriert werden. Eine Liste aller Optionen findest Du im GCC Manual.[6]
Platz sparen
[Bearbeiten]Ohne zusätzliche Optionen, legt der C Compiler generierten Code und initialisierte Variablen je in einer einzigen Sektion der erstellten Objektdatei ab. Generierter Code landet dabei in der Sektion .text, globale Daten in der Sektion .data.
Mit den Schaltern -ffunction-sections und -fdata-sections kann der Compiler dazu angewiesen werden den generierten Code für jede Funktion bzw. die Daten jeder Variable in einer eigenen Sektion abzulegen. Der generierte Code einer Funktion «funktion» liegt dann in der Sektion .text.«funktion», Daten der globalen Variable «daten» liegen dann in der Sektion .data.«funktion»
Liegen die einzelnen Funktionen und Variablen auf diese Weise getrennt je in eigenen Sektionen, so kann der Linker dazu angewiesen werden nur tatsächlich verwendete Funktionen und Daten in das fertige Programm zu linken. Im fertigen Programm wird dann nur Platz für Funktionen und Variablen benötigt, die auch tatsächlich verwendet werden. Der Schalter, der für diesen Zweck an den Linker übergeben werden muss trägt die Bezeichnung --gc-sections. Die Syntax, die wir verwenden müssen, um diese Option an den Linker weiter zu reichen findet sich im folgenden Abschnitt.
Linker Optionen
[Bearbeiten]Optionen, die an den Linker weiter gegeben werden sollen, können mit der folgenden Syntax auf der Kommandozeile angegeben werden.[7]
-Wl,<options>
Mehrere Optionen sowie Paare der Form Option Wert können durch ein Komma getrennt werden. Um die Option --gc-sections des vorangegangenen Abschnitts an den Linker weiter zu reichen, müssen wir folgende Syntax verwendet werden.
-Wl,--gc-sections
Dokumentation zum Linker selbst findet sich nicht im GCC Manual sondern in der Dokumentation des Pakets binutils.[8]
Standardbibliothek
[Bearbeiten]Die Implementierung der C Standardbibliothek AVR Libc wird vom GNU Projekt zur Verfügung gestellt.
Bitmanipulation
[Bearbeiten]Bei der Programmierung von Microcontrollern müssen sehr häufig einzelne Bits manipuliert und getestet werden. Für eine Reihe typischer und häufig anstehender Aufgaben stellt die AVR Libc Präprozessor Makros bereit, dir für die Umsetzung der jeweils gewünschten Operation verwendet werden können.
Mit den Makros bit_is_set(X, Y) und bit_is_clear(X, Y) kann getestet werden, ob ein bestimmtes Bit auf dem Wert 1 oder 0 steht.
Mit den Makros loop_unitl_bit_is_set(X, Y) und loop_unitl_bit_is_clear(X, Y) kann auf den jeweils gewünschten Zustand des angegebenen Bits gewartet werden.
stdio stdout und stderr
[Bearbeiten]Die Headerdatei <stdio.h> der AVR Libc stellt zwar die Dateihandles stdin, stdout und stderr bereit, sie sind aber nicht initialisiert.
Bevor Funktionen der printf() und scanf() Familie verwendet werden können, müssen die zugehörigen Dateihandles zuerst eingerichtet werden. Zum Einrichten eines Dateihandles steht das Makro FDEV_SETUP_STREAM() bereit. Dem Makro müssen zwei Funktionszeiger und eine Angabe darüber, ob das Dateihandle für die Eingabe bzw. für die Ausgabe verwendet werden kann, übergeben werden.
int put(char c, FILE *stream)
int write (FILE *stream)
Die Funktion put soll einen Rückgabewert von 0 liefern, wenn die Schreiboperation erfolgreich war.
Die Funktion write soll den gelesenen Wert als int zurückliefern, wenn die Leseoperation erfolgreich war. Bei Erreichen des Dateiendes soll sie den Wert _FDEV_EOF zurückliefern. Im Falle eines Fehlers soll sie den Wert _FDEV_ERR zurückliefern.
Eine Möglichkeit stdout mit dem FDEV_SETUP_STREAM() Makro so einzurichten, dass die Ausgabe über die serielle Schnittstelle erfolgt, findest Du im folgenden Kapitel.
printf und scanf
[Bearbeiten]Funktionen der printf() und der scanf() Familie können in verschiedenen Versionen verwendet werden. Neben der Standardversion stehen eine minimale und eine erweiterte Version bereit.
Um eine andere Version als die Standardversion zu verwenden, müssen dem Linker die folgenden Optionen übergeben werden.
Familie | minmale Version | erweiterte Version |
---|---|---|
printf() | -Wl,-u,vfprintf -lprintf_min | -Wl,-u,vfprintf -lprintf_flt -lm |
scanf() | -Wl,-u,vfscanf -lscanf_min | -Wl,-u,vfscanf -lscanf_flt -lm |
Formatstring
[Bearbeiten]Der Aufbau des Formatstrings entspricht den üblichen Regeln.
Eine Besonderheit: Formatstring im Programmspeicher (ROM) -> Funktionen haben das Suffix _P
Flags
[Bearbeiten]Flags werden von allen Versionen korrekt geparst. Flags, die von der eingesetzten Version nicht unterstüzt werden, werden dabei stillschweigend ignoriert.
Flag | Beschreibung | m | s | e |
---|---|---|---|---|
# | Fügt bei o eine führende 0 hinzu. Fügt bei x und X ein führendes 0x bzw. 0X hinzu | |||
0 | Füllt auf die vorgegebene Länge mit 0 statt Leerzeichen auf | |||
- | Stellt den Wert linksbündig statt rechtsbündig dar | |||
<space> | Fügt bei d und i bei positiven Zahlen ein führendes Leerzeichen ein | |||
+ | Positive Zahlen werden mit führendem + dargestellt |
Konvertierungs Spezifikation
[Bearbeiten]Die folgenden Konvertierungs Spezifikationen werden von allen Version korrekt geparst. Die Werte von nicht unterstützten Konvertierungen werden in der Ausgabe als ? dargestellt.
Zeichen | Argument | Ausgabeformat | m | s | e |
---|---|---|---|---|---|
s | char* im RAM | Zeichenkette '\0' terminiert | |||
S | char* im ROM | Zeichenkette '\0' terminiert | |||
c | int | konvertiert zu unsigned char | |||
d i | int | signed decimal | |||
u | int | unsigned decimal | |||
x X | int | unsigned hexadecimal | |||
o | int | unsigned octal | |||
p | void* | unsigned hexadecimal | |||
e E | float | [-]d.ddde(+/-)dd | |||
f F | float | [-]ddd.ddd | |||
g G | float | Für exponent < -4 oder exponent >= precision Ausgabe im Stil e/E sonst im Stil f/F |
Rückschau und Ausblick
[Bearbeiten]Glückwunsch! Mit Abschluss dieses Kapitels hast Du bereits einen ersten Überblick über die wichtigsten Besonderheiten der Werkzeuge gewonnen.
Auch wenn Du Dir bei der ersten Lektüre nicht alle Details auswendig behalten hast, so hast Du doch Deinen Blick für Besonderheiten geschärft. Wenn Du beim Programmieren eine Stelle entdeckst, an der Du mehr über die genauen Details wissen musst, kannst Du zu diesem Kapitel zurückkehren und die Einzelheiten nachlesen.
Fußnoten
[Bearbeiten]- ↑ (siehe [WCP]: Kapitel Datentypen)
- ↑ siehe: GCC Manual
- ↑ siehe: GCC Manual Extensions to the C Language Family
- ↑ siehe: GCC Manual Language Standards Supported by GCC
- ↑ siehe: GCC Manual Options to Request or Suppress Warnings
- ↑ siehe: GCC Manual Options That Control Optimization
- ↑ siehe: GCC Manual Options for Linking
- ↑ siehe Binutils Manual Command Line Options