2.2 AVR C-Programmierung
2.2.1 Allgemeines
2.2.2 Compiler und Entwicklungsumgebung
Als Entwicklungsumgebung (IDE - Integrierte Entwicklungsumgebung, engl. Integrated Development Environment) wird Microchip Studio (früher Atmel Studio) verwendet. Dieser verwendet GCC als Compiler.
Was ist ein Compiler?
Um ein Programm in einer höheren Programmiersprache (z.B. C) in Maschinensprache zu übersetzen, wird ein Compiler benötigt. Der Compiler übersetzt den Quellcode in eine für den Mikrocontroller verständliche Form. Der Compiler prüft den Quellcode auf Syntaxfehler und erzeugt eine ausführbare Datei (z.B. HEX-Datei), die auf den Mikrocontroller übertragen werden kann.
Dabei wird der Quellcode in mehrere Schritte übersetzt:
- Preprocessing - Der Präprozessor verarbeitet die Direktiven im Quellcode (z.B. #include, #define, #ifdef, ...).
- Compiling - Der Compiler übersetzt den Quellcode in Assemblersprache.
- Assembling - Der Assembler übersetzt die Assemblersprache in Maschinensprache.
- Linking - Der Linker verknüpft die verschiedenen Teile des Programms (z.B. Bibliotheken) zu einer ausführbaren Datei.
Dies soll im folgenden Ablaufdiagramm verdeutlicht werden:

Die .hex-Datei enthält den Maschinencode, welcher auf den Mikrocontroller übertragen wird. Der Mikrocontroller kann diesen Code ausführen und somit das Programm starten. Die Datei ist im Intel HEX-Format gespeichert und enthält Informationen über die Speicheradressen und die Daten, die an diese Adressen geschrieben werden sollen. D.h. das Programmiergerät bzw. der Bootloader kann die .hex-Datei entgegen nehmen und die Daten an die entsprechenden Speicheradressen des Mikrocontrollers schreiben.
Die .o-Datei hingegen enthält keinen ausführbaren Code, sondern nur den Maschinencode, der noch nicht an die Speicheradressen gebunden ist. Diese Datei wird benötigt, um die .hex-Datei zu erstellen.
2.2.3 Programmierung des AVR Mikrocontrollers
Um ein zuvor kompiliertes Programm auf den Mikrocontroller zu übertragen ("Flashen", d.h. Beschreiben des Flash Programmspeichers), wird ein Programmiergerät (z.B. Atmel-ICE) benötigt. Dieses wird über USB mit dem Computer sowie mit dem Mikrocontroller verbunden. Auf der MEGACARD ist ein Bootloader vorinstalliert, dieser ermöglicht das Flashen über USB ohne zusätzliches Programmiergerät. Stattdessen wird eine Software (avrdude) verwendet, welche mithilfe des Bootloaders das kompilierte Programm auf den Mikrocontroller überträgt. Ein Bootloader ist ein kleines Programm, das beim Start des Mikrocontrollers ausgeführt wird. Der Bootloader muss jedoch zuerst auf den Mikrocontroller geladen werden.
Hinweis: Sollte die MEGACARD nicht über einen Bootloader verfügen oder dieser überschrieben worden sein, kann der Bootloader mittels Programmiergerät auf den Mikrocontroller geladen werden. Wenn avrdude die MEGACARD nicht erkennt, liegt es am wahrscheinlichsten an:
1. Falscher Port (COM-Port) ausgewählt
2. Bootloder nicht installiert
2.2.4 Struktur eines C-Programms
Ein typisches C-Programm besteht aus verschiedenen Teilen, die in der folgenden Reihenfolge im Quellcode angeordnet sind:
- Direktiven (Preprocessor direcives)
Beginnen mit # und werden vom Präprozessor, d.h. vor dem eigentlichen kompilieren, verarbeitet. Beispiele sind #include, #define, #ifdef, ...
#include <stdio.h> // Include standard input/output header file, part of gcc
#include "display.h" // Include user-defined header file
#define PI 3.14159
#ifdef DEBUG
#define DEBUG_PRINT(x) printf(x)
- Deklarationen von Funktionen (Function declarations)
Enthalten den eigentlichen Programmcode. Jedes C-Programm muss mindestens eine Funktion enthalten, die Funktion main().
int16_t add(int8_t, int8_t); // Function prototype for addition
- Globale Variablen (Global variables)
Speichern Daten, die im Programm verwendet werden und sind überall im Programm verfügbar, d.h. nicht nur innerhalb einer Funktion. Hier können auch globale Konstanten definiert werden.
int8_t a = 10; // Global variable declaration and initialization
int8_t a, b; // Global variable declaration
const int8_t MAX = 100; // Global constant declaration
- Funktionen (Functions)
Enthalten den eigentlichen Programmcode. Jedes C-Programm muss mindestens eine Funktion enthalten, die Funktion main().
// Function to add two integers
int16_t add(int8_t x, int8_t y) {
return x + y;
}
void main(int argc, char **argv) {
// program execution starts here
int8_t x = 5, y = 10; // Local variable declaration and initialization
int16_t sum; // Local variable declaration of sum
sum = add(x, y); // Call of function add
display_printf_pos(0, 4, "Sum of %d and %d is %d\n", x, y, sum); // Output the result on the display
}
Zu beachten sind weiters folgende Punkte:
- Groß- und Kleinschreibung - C ist case-sensitive
- Kommentare - Erläutern den Quellcode und erhöhen die Lesbarkeit (beginnen mit // oder / ... /). Es wird empfohlen, den Quellcode so zu kommentieren, dass auch Personen, die nicht mit dem Code vertraut sind, diesen verstehen (vorzugsweise in der Sprache Englisch)
- Leerzeilen - Erhöhen die Lesbarkeit des Quellcodes
- Leerzeichen - Erhöhen die Lesbarkeit des Quellcodes
- Einrückungen - Erhöhen die Lesbarkeit des Quellcodes
- Klammern - Begrenzen Blöcke von Anweisungen (beginnen mit { und enden mit })
- Semikolons - Beenden Anweisungen (";")
2.2.4 Datentypen in AVR-GCC
Datentypen sind "Formal die Zusammenfassung von Objektmengen mit allen darauf definierten Operationen" (Wikipedia).
Ganzzahlige Datentypen
Standardisierte Datentypen werden seit C99 in der Header-Datei stdint.h zur Verfügung gestellt. Diese Datei ist z.B. über Dependencies in einem Projekt einsehbar. Aus dem Namen ist die Bitbreite erkennbar.
Integertypen mit Vorzeichen
| Typename | Wertebereich | Alias (AVR-GCC) |
|---|---|---|
| int8_t | -128..127 | signed char |
| int16_t | -32768..32767 | signed int |
| int32_t | -2147483648..2147483647 | signed long int |
| int64_t | -9223372036854775808..9223372036854775807 | signed long long |
Integertypen ohne Vorzeichen
| Typename | Wertebereich | Alias (AVR-GCC) |
|---|---|---|
| uint8_t | 0..255 | unsigned char |
| uint16_t | 0..65535 | unsigned int |
| uint32_t | 0..4294967295 | unsigned long int |
| uint64_t | 0..18446744073709551615 | unsigned long long |
Weitere Datentypen
Floats/Doubles werden im Kapitel Verschiedenes behandelt.
char ... todo
Es ist in C auch möglich, eigen Datentypen zu definieren (typedef):
#include <stido.h>
#include <string.h>
typedef struct Telefon {
char hersteller[30];
char typ[20];
char telefonnummer[20];
} Telefon
int main(){
Telefon telefon;
strcpy(telefon.hersteller, "Samsung");
strcpy(telefon.typ, "SM-30");
strcpy(telefon.telefonnummer, "+43 664 1234567");
printf("Hersteller: %s\n", telefon.hersteller);
return(0);
}
2.2.5 Bitmanipulation
Hardwarenahe Programmierung erfordert oft die direkte Manipulation von Bits. Dies kann z.B. notwendig sein, um Register zu setzen oder zu löschen. Dazu werden Bitmasken verwendet, die mit den logischen Operatoren AND, OR, XOR und NOT verknüpft werden. Im folgenden sind die grundlegenden Bitoperationen aufgelistet.
2.2.7 Formatierung von Datentypen
Die Formatierung von Datentypen erfolgt über die Funktion printf() bzw. in unserem Fall über eine Funktion aus der Bibliothek "display.h", nämlich display_printf_pos. Die Formatierung erfolgt über Platzhalter, die durch die entsprechenden Variablen ersetzt werden.
Diese können z.B. sein:
| Formatierung | Beschreibung | Beispiel |
|---|---|---|
| %d oder %i | Dezimalzahl | 123 |
| %x | Hexadezimalzahl | 0x7B |
| %o | Oktalzahl | 0173 |
| %c | Zeichen | 'A' |
| %s | Zeichenkette | "Hello, World!" |
| %f | Fließkommazahl | 3.14 |
| %e | Fließkommazahl in Exponentialdarstellung | 3.14e+00 |
| %g | Fließkommazahl in kürzester Darstellung | 3.14 |
| %p | Zeiger | 0x7fffe0 |
2.2.8
In Bearbeitung.
2.2.9 Debugging
2.2.10 Verschiedenes
Float-Unterstützung in Microchip Studio
Standardmäßig ist in Microchip Studio die Unterstützung für floats ausgeschaltet.
Eine Anleitung, wie die Unterstützung für floats in Microchip-Studio eingeschaltet werden kann findet sich z.b. hier.
NOTE: Durch die Aktivierung der Unterstützung für floats, wird die compilierte Datei größer.
Wird mit float-Werten gerechnet, ist auf die Verwendung von Literals zu achten. So ergibt z.B. folgendes Beispiel...
int i;
i = 3 / 2;
... als Ergebnis 1.
Werden hingegen Float Literals verwendet (hier 3.0)...
int i;
i = 3.0 / 2;
... wird das korrekte Ergebnis 1.5 ausgegeben. Dabei spielt es keine Rolle, ob der erste Wert, der zweite Wert oder beide Werte als Literals angegeben sind (jedoch mindestens einer).
Üblicherweise sind in C float und double unterschiedlich definiert.
float 32-bit
double 64-bit
NOTE: In Compiler von Microchip-Studio sind float und double gleich groß (32-bit). Überprüfbar ist dies z.B. über den Aufruf der Funktion sizeof(double), welche dann die Anzahl an Bytes zurückgibt.
Literals
Literals in C sind Daten, welche direkt in den Quellcode eingefügt werden können und deren Werte sich während der Laufzeit des Programms nicht ändern.
Beispiele sind:
| Bezeichnung | Beispiel |
|---|---|
| Integer Literals | 123 (dezimal), 0x42 (hexadezimal), 077 (octal) |
| Floating-point Literals | 2.0 oder 145.2 |
| Character Literals | 'a', '\n' |
| String Literals | "Hello, World!" |
| Boolean Literals | true, false |
Pointer
Zeiger (engl. Pointer) ermöglichen den direkten Zugriff auf die Speicheradressen von Daten. Sie sind im Wesentlichen eine Variable, deren Wert die Adresse einer anderen Variable ist. Dadurch kann ein Programm effizienter - sprich platzsparender - mit Speicherressourcen umgehen und es können dynamischen Datenstrukturen (wie z.B. Listen) implementiert werden.
Beim AVR ist ein Pointer immer 16 bit breit und enthält zudem Informationen über den Datentypen, auf den er zeigt.
Switch-case-Anweisung
Die switch-case-Anweisung ist eine alternative Möglichkeit zur Verzweigung von Programmen. Sie ist besonders nützlich, wenn eine Variable auf verschiedene Werte geprüft werden soll.
#include <stdio.h>
int main() {
int i = 2; // Variable i wird auf 2 gesetzt
switch(i) { // Abfrage nach Variable i
case 1:
printf("i ist 1\n");
break;
case 2:
printf("i ist 2\n"); // Wird ausgegeben
break;
case 3:
printf("i ist 3\n");
break;
default:
printf("i ist weder 1, 2 noch 3\n");
}
return 0;
}