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:

  1. Preprocessing - Der Präprozessor verarbeitet die Direktiven im Quellcode (z.B. #include, #define, #ifdef, ...).
  2. Compiling - Der Compiler übersetzt den Quellcode in Assemblersprache.
  3. Assembling - Der Assembler übersetzt die Assemblersprache in Maschinensprache.
  4. 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:

Compiler

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:

  1. 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)
  1. 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
  1. 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
  1. 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:

  1. Groß- und Kleinschreibung - C ist case-sensitive
  2. 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)
  3. Leerzeilen - Erhöhen die Lesbarkeit des Quellcodes
  4. Leerzeichen - Erhöhen die Lesbarkeit des Quellcodes
  5. Einrückungen - Erhöhen die Lesbarkeit des Quellcodes
  6. Klammern - Begrenzen Blöcke von Anweisungen (beginnen mit { und enden mit })
  7. 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;
}