AVR-GCC-Tutorial/Exkurs Makefiles

Wechseln zu: Navigation, Suche

Wenn man bisher gewohnt ist, mit integrierten Entwicklungsumgebungen à la Visual-C Programme zu erstellen, wirkt das makefile-Konzept auf den ersten Blick etwas kryptisch. Nach kurzer Einarbeitung ist diese Vorgehensweise jedoch sehr praktisch. Diese Dateien (üblicher Name: 'Makefile' ohne Dateiendung) dienen der Ablaufsteuerung des Programms make, das auf allen Unix/Linux-Systemen installiert sein sollte, und in einer Fassung fuer MS-Windows auch in WinAVR (Unterverzeichnis utils/bin) enthalten ist.

Einleitung

Im Unterverzeichnis sample einer WinAVR-Installation findet man eine sehr brauchbare Vorlage, die sich einfach an das eigene Projekt anpassen lässt (lokale Kopie Stand Sept. 2004). Diese Version kann nicht ohne Änderungen für C++ verwendet werden, da in diesem Fall der Quelltext mit dem Listing überschrieben wird.

Eine erweiterte Version davon ([Makefile für C++ - BrokenLink (Trollolol) ]) ermöglicht die Verwendung von C ebenso wie C++ und hat die Stellen markiert, an denen üblicherweise Änderungen vorgenommen werden müssen.

Wahlweise kann man auch mfile von Jörg Wunsch nutzen. mfile erzeugt ein makefile nach Einstellungen in einer grafischen Nutzeroberfläche, wird bei WinAVR mitinstalliert, ist aber als TCL/TK-Programm auf nahezu allen Plattformen lauffähig.

Die folgenden Ausführungen beziehen sich auf das WinAVR Beispiel-Makefile.

Ist im Makefile alles richtig eingestellt, genügt es, sich drei Parameter zu merken, die über die shell bzw. die Windows-Kommandozeile (cmd.exe/command.com) als Parameter an "make" übergeben werden. Das Programm make sucht sich "automatisch" das Makefile im aktuellen Arbeitsverzeichnis und führt die darin definierten Operationen für den entsprechenden Aufrufparameter durch.

make all Erstellt aus den im Makefile angegebenen Quellcodes eine hex-Datei (und ggf. auch eep-Datei).
make program Überträgt die hex-Datei (und wahlweise auch die eep-Datei für den EEPROM) zum AVR.
make clean löscht alle temporären Dateien, also auch die hex-Datei

Diese Aufrufe können in die allermeisten Editoren in "Tool-Menüs" eingebunden werden. Dies erspart den Kontakt mit der Kommandozeile. Bei WinAVR sind die Aufrufe bereits im Tools-Menü des mitgelieferten Editors Programmers-Notepad eingefügt.

Üblicherweise sind folgende Daten im Makefile anzupassen:

  • Controllertyp
  • Quellcode-Dateien (c-Dateien)
  • Typ und Anschluss des Programmiergeräts


Seltener sind folgende Einstellungen durchzuführen:

  • Grad der Optimierung
  • Methode zur Erzeugung der Debug-Symbole (Debug-Format)
  • Assembler-Quellcode-Dateien (S-Dateien)

Die in den folgenden Unterabschnitten gezeigten Makefile-Ausschnitte sind für ein Programm, das auf einem ATmega8 ausgeführt werden soll. Der Quellcode besteht aus den c-Dateien superprog.c (darin main()), uart.c, lcd.c und 1wire.c. Im Quellcodeverzeichnis befinden sich diese Dateien: superprog.c, uart.h, uart.c, lcd.h, lcd.c, 1wire.h, 1wire.c und das makefile (die angepasste Kopie des WinAVR-Beispiels).

Der Controller wird mittels AVRDUDE über ein STK200-Programmierdongle an der Schnittstelle lpt1 (bzw. /dev/lp0) programmiert. Im Quellcode sind auch Daten für die section .eeprom definiert (siehe Abschnitt Speicherzugriffe), diese sollen beim Programmieren gleich mit ins EEPROM geschrieben werden.

Controllertyp setzen

Dazu wird die "make-Variable" MCU entsprechend dem Namen des verwendeten Controllers gesetzt. Eine Liste der von avr-gcc und der avr-libc unterstützten Typen findet sich in der Dokumentation der avr-libc.

# Kommentare in Makefiles beginnen mit einem Doppelkreuz 
...

# ATmega8 at work
MCU = atmega8
# oder MCU = atmega16 
# oder MCU = at90s8535
# oder ...
...

Quellcode-Dateien eintragen

Der Name der Quellcodedatei, welche die Funktion main enthält, wird hinter TARGET eingetragen. Dies jedoch ohne die Endung .c.

...
TARGET = superprog
...

Besteht das Projekt wie im Beispiel aus mehr als einer Quellcodedatei, sind die weiteren c-Dateien (nicht die Header-Dateien, vgl. Include-Files (C)) durch Leerzeichen getrennt bei SRC einzutragen. Die bei TARGET definierte Datei ist schon in der SRC-Liste enthalten. Diesen Eintrag nicht löschen!

...
SRC = $(TARGET).c uart.c lcd.c 1wire.c 
...

Alternativ kann man die Liste der Quellcodedateien auch mit dem Operator += erweitern.

SRC = $(TARGET).c uart.c 1wire.c
# lcd-Code fuer Controller xyz123 (auskommentiert)
# SRC += lcd_xyz.c
# lcd-Code fuer "Standard-Controller" (genutzt)
SRC += lcd.c

Programmiergerät einstellen

Die Vorlagen sind auf die Programmiersoftware AVRDUDE angepasst, jedoch lässt sich auch andere Programmiersoftware einbinden, sofern diese über Kommandozeile gesteuert werden kann (z. B. stk500.exe, uisp, sp12).

...
# Einstellung fuer STK500 an com1 (auskommentiert)
# AVRDUDE_PROGRAMMER = stk500
# com1 = serial port. Use lpt1 to connect to parallel port.
# AVRDUDE_PORT = com1    # programmer connected to serial device

# Einstellung fuer STK200-Dongle an lpt1
AVRDUDE_PROGRAMMER = stk200
AVRDUDE_PORT = lpt1
...

Sollen Flash(=.hex) und EEPROM(=.eep) zusammen auf den Controller programmiert werden, ist das Kommentarzeichen vor AVRDUDE_WRITE_EEPROM zu löschen.

...
# auskommentiert: EERPOM-Inhalt wird nicht mitgeschrieben
#AVRDUDE_WRITE_EEPROM = -U eeprom:w:$(TARGET).eep

# nicht auskommentiert: EERPOM-Inhalt wird mitgeschrieben
AVRDUDE_WRITE_EEPROM = -U eeprom:w:$(TARGET).eep
...

Anwendung

Das erstellte Makefile und der Code müssen im gleichen Ordner sein, auch sollte der Dateiname nicht verändert werden.

Die Eingabe von make all im Arbeitsverzeichnis mit dem Makefile und den Quellcodedateien erzeugt (unter anderem) die Dateien superprog.hex und superprog.eep. Abhängigkeiten zwischen den einzelnen c-Dateien werden dabei automatisch berücksichtigt. Die superprog.hex und superprog.eep werden mit make program zum Controller übertragen. Mit make clean werden alle temporären Dateien gelöscht (="aufgeräumt").

Sonstige Einstellungen

Optimierungsgrad

Der gcc-Compiler kennt verschiedene Stufen der Optimierung. Nur zu Testzwecken sollte die Optimierung ganz deaktiviert werden (OPT = 0). Die weiteren möglichen Optionen weisen den Compiler an, möglichst kompakten oder möglichst schnellen Code zu erzeugen. In den weitaus meisten Fällen ist OPT = s die empfohlene Einstellung, damit wird kompakter und oft auch der schnellste Maschinencode erzeugt. Beim Update auf eine neue Compilerversion ist zu beachten, dass diese möglicherweise intern andere Optimierungsalgorithmen verwendet und sich dadurch die Größe des Machinencodes etwas ändert, ohne dass man im Quellcode etwas geändert hat.

Als Orientierungswerte die Größe des Maschinencodes bei verschiedenen Optionen für einen nicht näher spezifizierten relativ kleinen Testcode bei Verwendung einer nicht näher spezifizierten Compilerversion.

  • -O0 : 12'217 Byte
  • -O1 : 9'128 Byte
  • -O2 : 1'670 Byte
  • -O3 : 3'004 Byte
  • -Os : 1'695 Byte

Im diesem Testfall führt die Option -O2 mit zum kompaktesten Code, dies allerdings hier nur mit 25 Bytes "Vorsprung". Es kann durchaus sein, dass nur wenige Programmerweiterungen dazu führen, dass Compilieren mit -Os wieder in kompakteren Code resultiert.

Siehe dazu auch:

Debug-Format

Unterstützt werden die Formate stabs und dwarf-2. Das Format wird hinter DEBUG = eingestellt. Siehe dazu Abschnitt Eingabedateien zur Simulation.

Assembler-Dateien

Die im Projekt genutzten Assembler-Dateien werden hinter ASRC durch Leerzeichen getrennt aufgelistet. Assembler-Dateien haben immer die Endung .S (großes S). Ist zum Beispiel der Assembler-Quellcode eines Software-UARTs in einer Datei softuart.S enthalten, lautet die Zeile: ASRC = softuart.S

Taktfrequenz

Neuere Versionen der WinAVR/Mfile Vorlage für Makefiles beinhalten die Definition einer Variablen F_CPU (WinAVR 2/2005). Darin wird die Taktfrequenz des Controllers in Hertz eingetragen. Die Definition steht dann im gesamten Projekt ebenfalls unter der Bezeichnung F_CPU zur Verfügung (z. B. um daraus UART-, SPI- oder ADC-Frequenzeinstellungen abzuleiten).

Die Angabe hat rein "informativen" Charakter, die tatsächliche Taktrate wird über den externen Takt (z. B. Quarz) bzw. die Einstellung des internen R/C-Oszillators bestimmt. Die Nutzung von F_CPU hat also nur Sinn, wenn die Angabe mit dem tatsächlichen Takt übereinstimmt.

Innerhalb neuerer Versionen der avr-libc (ab Version 1.2) wird die Definition der Taktfrequenz (F_CPU) zur Berechnung der Wartefunktionen in delay.h genutzt. Diese funktionieren nur dann korrekt, wenn F_CPU mit der tatsächlichen Taktfrequenz übereinstimmt. F_CPU muss dazu jedoch nicht unbedingt im makefile definiert werden. Es reicht aus, wird aber bei mehrfacher Anwendung unübersichtlich, vor #include <util/delay.h> (veraltet: #include <avr/delay.h>) ein #define F_CPU [hier Takt in Hz]UL einzufügen. Bei Nutzung von delay.h ist darauf zu achten, dass die Optimierung des Compilers nicht ausgeschaltet ist, sonst wird sehr viel Code erzeugt und die Wartezeit stimmt nicht mit der gewünschten Zeitspanne überein. Außerdem sollte der delay-Funktion kein zur Laufzeit berechneter Wert übergeben werden. Vgl. dazu den entsprechenden Abschnitt der Dokumentation.

Eingabedateien zur Simulation in AVR-Studio

Mit älteren AVR-Studio-Versionen kann man nur auf Grundlage so genannter coff-Dateien simulieren. Neuere Versionen von AVR-Studio (ab 4.10.356) unterstützen zudem das modernere aber noch experimentelle dwarf-2-Format, das ab WinAVR 20040722 (avr-gcc 3.4.1/Binutils inkl. Atmel add-ons) "direkt" vom Compiler erzeugt wird.

Vorgehensweise bei dwarf-2
  • Im Makefile bei DEBUG:
    DEBUG=dwarf-2
  • make all (evtl. vorher make clean)
  • Die erzeugte elf-Datei (im Beispiel oben superprog.elf) in AVR-Studio laden
  • AVR-Simulator und zu simulierenden Controller wählen, "Finish"
  • Weiteres siehe AVR-Studio Online-Hilfe
Vorgehensweise bei extcoff
(sollte nur noch in Ausnahmefällen genutzt werden)
  • Im Makefile bei DEBUG:
    DEBUG=stabs
  • make extcoff (evtl. vorher make clean)
  • Die erzeugte cof-Datei (im Beispiel oben superprog.cof) in AVR-Studio laden
  • AVR-Simulator und zu simulierenden Controller wählen, "Finish"
  • Weiteres siehe AVR-Studio Online-Hilfe

Beim Simulieren scheinen oft "Variablen zu fehlen". Ursache dafür ist, dass der Compiler diese "Variablen" direkt Registern zuweist. Dies kann vermieden werden, indem die Optimierung abgeschaltet wird (im makefile). Man simuliert dann jedoch ein vom optimierten Code stark abweichendes Programm. Das Abschalten der Optimierung wird nicht empfohlen.

Statt des Software-Simulators kann das AVR-Studio auch genutzt werden, um mit dem ATMEL JTAGICE, einem Nachbau davon (BootICE, Evertool o. ä.) oder dem ATMEL JTAGICE MKII "im System" zu debuggen. Dazu sind keine speziellen Einstellungen im makefile erforderlich. Debugging bzw. "In-System-Emulation" mit dem JTAGICE und JTAGICE MKII sind in der AVR-Studio Online-Hilfe beschrieben.

Die Verwendung von Makefiles bietet noch viele weitere Möglichkeiten, einige davon werden im Anhang Zusätzliche Funktionen im Makefile erläutert.

Zusätzliche Funktionen im Makefile

Bibliotheken (Libraries/.a-Dateien) hinzufügen

Um Funktionen aus Bibliotheken ("echte" Libraries, *.a-Dateien) zu nutzen, sind dem Linker die Namen der Bibliotheken als Parameter zu übergeben. Dazu ist die Option -l (kleines L) vorgesehen, an die der Name der Library angehängt wird.

Dabei ist zu beachten, dass der Name der Library und der Dateiname der Library nicht identisch sind. Der hinter -l angegebene Name entspricht dem Dateinamen der Library ohne die Zeichenfolge lib am Anfang des Dateinamens und ohne die Endung .a. Sollen z. B. Funktionen aus einer Library mit dem Dateinamen libefsl.a eingebunden (gelinkt) werden, lautet der entsprechende Parameter -lefsl (vergl. auch -lm zum Anbinden von libm.a).

In Makefiles wird traditonell eine make-Variable LDLIBS genutzt, in die "l-Parameter" abgelegt werden. Die WinAVR-makefile-Vorlage enthält diese Variable zwar nicht, dies stellt jedoch keine Einschränkung dar, da alle in der make-Variable LDFLAGS abgelegten Parameter an den Linker weitergereicht werden.

Beispiele:

  1. Einbinden von Funktionen aus einer Library efsl (Dateiname libefsl.a)

LDFLAGS += -lefsl

  1. Einbinden von Funktionen aus einer Library xyz (Dateiname libxyz.a)

LDFLAGS += -lxyz

Liegen die Library-Dateien nicht im Standard Library-Suchpfad, sind die Pfade mittels Parameter -L ebenfalls anzugeben. Der vordefinierte Suchpfad kann mittels avr-gcc --print-search-dirs angezeigt werden.

Als Beispiel ein Projekt ("superapp2"), in dem der Quellcode von zwei Libraries (efsl und xyz) und der Quellcode der eigentlichen Anwendung in verschiedenen Verzeichnissen mit der folgenden "Baumstruktur" abgelegt sind:

superapp2 | +----- efslsource (darin libefsl.a) | +----- xyzsource (darin libxyz.a) | +----- firmware (darin Anwendungs-Quellcode und Makefile)

Daraus folgt, dass im Makefile die Verzeichnis efslsource und xyzsource in den Library-Suchpfad aufzunehmen sind:

LDFLAGS += -L../efslsource/ -L../xyzsource/

Fuse-Bits

Zur Berechnung der Fuse-Bits bietet sich neben dem Studium des Datenblattes auch der AVR Fuse Calculator an. Gewarnt werden muss vor der Benutzung von PonyProg, weil dort durch die negierte Darstellung gern Fehler gemacht werden.

Soll die Programmierung von Fuse- und Lockbits automatisiert werden, kann man dies ebenfalls durch Einträge im Makefile vornehmen, die beim Aufruf von "make program" an die genutzte Programmiersoftware übergeben werden. In der makefile-Vorlage von WinAVR (und mfile) gibt es dafuer jedoch keine "Ausfüllhilfe" (Stand 9/2006). Die folgenden Ausführungen gelten für die Programmiersoftware AVRDUDE (Standard in der WinAVR-Vorlage), können jedoch sinngemäß auf andere Programmiersoftware übertragen werden, die die Angabe der Fuse- und Lockbits-Einstellungen per Kommandozeilenparameter unterstützt (z. B. stk500.exe). Im einfachsten Fall ergänzt man im Makefile einige Variablen, deren Werte natürlich vom verwendeten Controller und den gewünschten Einstellungen abhängen (vgl. Datenblatt Fuse-/Lockbits):

  1. ---------------- Programming Options (avrdude) ----------------
  1. Beispiel! f. ATmega16 - nicht einfach uebernehmen! Zahlenwerte anhand
  2. --------- des Datenblatts nachvollziehen und gegebenenfalls aendern.

AVRDUDE_WRITE_LFUSE = -U lfuse:w:0xff:m AVRDUDE_WRITE_HFUSE = -U hfuse:w:0xd8:m AVRDUDE_WRITE_LOCK = -U lock:w:0x2f:m

Damit diese Variablen auch genutzt werden, ist der Aufruf von avrdude im Makefile entsprechend zu ergänzen:

  1. Program the device.

program: $(TARGET).hex $(TARGET).eep

  1. ohne Fuse-/Lock-Einstellungen (nach WinAVR Vorlage Stand 4/2006)
  2. $(AVRDUDE) $(AVRDUDE_FLAGS) $(AVRDUDE_WRITE_FLASH) \
  3. $(AVRDUDE_WRITE_EEPROM)
  4. mit Fuse-/Lock-Einstellungen
       $(AVRDUDE) $(AVRDUDE_FLAGS) $(AVRDUDE_WRITE_LFUSE) \
       $(AVRDUDE_WRITE_HFUSE) $(AVRDUDE_WRITE_FLASH) \
       $(AVRDUDE_WRITE_EEPROM) $(AVRDUDE_WRITE_LOCK)

Eine weitere Möglichkeit besteht darin, die Fuse- und Lockbit-Einstellungen vom Preprozessor/Compiler generieren zu lassen. Die Fuse-Bits werden dann bei Verwendung von AVRDUDE in eigene Hex-Files geschrieben. Hierzu kann man z. B. folgendes Konstrukt verwenden:

In eine der C-Sourcen wird eine Variable je Fuse-Byte vom Typ unsigned char deklariert und in eine extra Section gepackt. Dies kann entweder in einem vorhandenen File passieren oder in ein neues (z. B. fuses.c) geschrieben werden. Das File muss im Makefile aber auf jeden Fall mit kompiliert und gelinkt werden.

// tiny 2313 fuses low byte
#define CKDIV8  7
#define CKOUT   6
#define SUT1    5
#define SUT0    4
#define CKSEL3  3
#define CKSEL2  2
#define CKSEL1  1
#define CKSEL0  0

// tiny2313 fuses high byte
#define DWEN       7
#define EESAVE     6
#define SPIEN      5
#define WDTON      4
#define BODLEVEL2  3
#define BODLEVEL1  2
#define BODLEVEL0  1
#define RSTDISBL   0

// tiny2313 fuses extended byte
#define SELFPRGEN  0

#define LFUSE         __attribute__ ((section ("lfuses")))
#define HFUSE         __attribute__ ((section ("hfuses")))
#define EFUSE         __attribute__ ((section ("efuses")))


// select ext crystal 3-8Mhz
unsigned char lfuse LFUSE =
    ( (1<<CKDIV8) | (1<<CKOUT) | (1<<CKSEL3) | (1<<CKSEL2) | 
      (0<<CKSEL1) | (1<<CKSEL0) | (0<<SUT1) | (1<<SUT0) );
unsigned char hfuse HFUSE =
    ( (1<<DWEN) | (1<<EESAVE) | (0<<SPIEN) | (1<<WDTON) | 
      (1<<BODLEVEL2) | (1<<BODLEVEL1) | (0<<BODLEVEL0) | (1<<RSTDISBL) );
unsigned char efuse EFUSE =
    ((0<<SELFPRGEN));
ACHTUNG
Die Bitpositionen wurden nicht vollständig getestet!

Eine "1" bedeutet hier, dass das Fuse-Bit nicht programmiert wird - die Funktion also i.A. nicht aktiviert ist. Eine "0" hingegen aktiviert die meisten Funktionen. Dies ist wie im Datenblatt (1 = unprogrammed, 0 = programmed).

Das Makefile muss nun noch um folgende Targets erweitert werden (mit Tabulator einrücken - nicht mit Leerzeichen):

lfuses: build

       -$(OBJCOPY) -j lfuses --change-section-address lfuses=0 \
         -O ihex $(TARGET).elf $(TARGET)-lfuse.hex
       @if [ -f $(TARGET)-lfuse.hex ]; then \
        $(AVRDUDE) $(AVRDUDE_FLAGS) -U lfuse:w:$(TARGET)-lfuse.hex; \
       fi;

hfuses: build

       -$(OBJCOPY) -j hfuses --change-section-address hfuses=0 \
         -O ihex $(TARGET).elf $(TARGET)-hfuse.hex
       @if [ -f $(TARGET)-hfuse.hex ]; then \
        $(AVRDUDE) $(AVRDUDE_FLAGS) -U hfuse:w:$(TARGET)-hfuse.hex; \
       fi;

efuses: build

       -$(OBJCOPY) -j efuses --change-section-address efuses=0 \
        -O ihex $(TARGET).elf $(TARGET)-efuse.hex
       @if [ -f $(TARGET)-efuse.hex ]; then \
        $(AVRDUDE) $(AVRDUDE_FLAGS) -U efuse:w:$(TARGET)-efuse.hex;
       fi;

Das Target "clean" muss noch um die Zeilen

       $(REMOVE) $(TARGET)-lfuse.hex
       $(REMOVE) $(TARGET)-hfuse.hex
       $(REMOVE) $(TARGET)-efuse.hex

erweitert werden, wenn auch die Fuse-Dateien gelöscht werden sollen.

Um nun die Fusebits des angeschlossenen Controllers zu programmieren muss lediglichein "make lfuses", "make hfuses" oder "make efuses" gestartet werden. Bei den Fuse-Bits ist besondere Vorsicht geboten, da diese das Programmieren des Controllers unmöglich machen können. Also erst programmieren, wenn man einen HV-Programmierer hat oder ein paar Reserve-AVRs zur Hand ;-)

Um weiterhin den "normalen" Flash beschreiben zu können, ist es wichtig, für das Target "*.hex" im Makefile nicht nur "-R .eeprom" als Parameter zu übergeben sondern zusätzlich noch "-R lfuses -R efuses -R hfuses". Sonst bekommt AVRDUDE Probleme diese Sections in den Flash (wo sie ja nicht hingehören) zu schreiben.

Siehe auch: Vergleich der Fuses bei verschiedenen Programmen