K |
(→Listfile erstellen) |
||
Zeile 82: | Zeile 82: | ||
wie sie letztendlich auf den Controller geladen werden. | wie sie letztendlich auf den Controller geladen werden. | ||
+ | <tt>avr-objdump</tt> gibt seine Ausgabe auf das Terminal aus. | ||
+ | Diese Ausgabe wird mit '<tt>></tt>' in die Datei <tt>blinky.lst</tt> umgeleitet. | ||
+ | Hier ein Ausschnitt aus dem Listfile: | ||
+ | <pre> | ||
+ | 0000005c <job_timer1>: | ||
+ | uint16_t count; | ||
+ | |||
+ | /* irq_count um 1 erhöhen und */ | ||
+ | /* gegebenenfalls die LED blinken */ | ||
+ | count = 1+irq_count; | ||
+ | 5c: 20 91 60 00 lds r18, 0x0060 | ||
+ | 60: 30 91 61 00 lds r19, 0x0061 | ||
+ | 64: 2f 5f subi r18, 0xFF ; 255 | ||
+ | 66: 3f 4f sbci r19, 0xFF ; 255 | ||
+ | |||
+ | if (count >= INTERRUPTS_PER_SECOND) | ||
+ | 68: 83 e0 ldi r24, 0x03 ; 3 | ||
+ | 6a: 28 3e cpi r18, 0xE8 ; 232 | ||
+ | 6c: 38 07 cpc r19, r24 | ||
+ | 6e: 30 f0 brcs .+12 ; 0x7c | ||
+ | { | ||
+ | count = 0; | ||
+ | 70: 20 e0 ldi r18, 0x00 ; 0 | ||
+ | 72: 30 e0 ldi r19, 0x00 ; 0 | ||
+ | PORT_LED ^= (1 << PAD_LED); | ||
+ | 74: 88 b3 in r24, 0x18 ; 24 | ||
+ | 76: 92 e0 ldi r25, 0x02 ; 2 | ||
+ | 78: 89 27 eor r24, r25 | ||
+ | 7a: 88 bb out 0x18, r24 ; 24 | ||
+ | } | ||
+ | |||
+ | irq_count = count; | ||
+ | 7c: 30 93 61 00 sts 0x0061, r19 | ||
+ | 80: 20 93 60 00 sts 0x0060, r18 | ||
+ | 84: 08 95 ret | ||
+ | </pre> | ||
+ | Links stehen die Adressen, dann die Maschinencodes der Befehle, | ||
+ | danach die Maschinencodes in Assembler-Darstellung. | ||
+ | Ganz rechts nach dem Kommentarzeichen '<tt>;</tt>' stehen die Dezimalcodes von | ||
+ | Konstanten (z.B. 24 für die 0x18 an Adresse 74) oder die Zieladressen von | ||
+ | Sprüngen, wie bei dem <tt>brcs</tt>-Befehl an Adresse 6e, der gegebenenfalls | ||
+ | 12 Bytes überspringt und dann an Adresse 7c landet. | ||
==Die Größe ermitteln== | ==Die Größe ermitteln== |
Version vom 21. Dezember 2005, 14:02 Uhr
Das erste C-Programm, das man zu sehen bekommt, ist für die meisten das "Hallo Welt". Bei "Hallo Welt" geht es weniger um die Funktionalität an sich, sondern darum zu lernen, wie man überhaupt ein Programm übersetzt und einen Compiler verwendet.
Was für den PC das "Hallo Welt", ist für einen kleinen Microcontroller der "Hallo Blinky", der einfach nur eine Leuchtdiode (LED) blinken lässt.
Im Vergleich zu "Hallo Welt" sieht der Blinky viel komplizierter aus, aber eigentlich ist er einfacher, dann es werden keine umfangreichen Funktionen oder Black-Boxen benutzt wie etwa printf(). Ausser dem eingegebenen Quellcode kommt also kein andere Code zur Ausführung!
Inhaltsverzeichnis
Beschreibung
Von der C-Quelle zum hex-File
Das Beispiel besteht ganz bewusst aus zwei getrennten Quelldateien (Modulen) um zu zeigen, wie man Code auf mehrere Dateien aufteilen kann um die Übersichtlichkeit bei grösseren Projekten zu wahren.
Ausserdem wird das Übersetzen über Kommandozeilen-Eingaben erledigt und nicht über Werkzeuge wie make, das die Zusammenhänge eher verschleiert als erhellt, und dessen inkorrekte Anwendung eine häufige Fehlerquelle ist.
Nach Speichern der Quelldatein in ein eigenes Verzeichnis enthält dieses die Dateien
blinky.c timer1.c timer1.h
Compilieren
Zunächst werden die C-Dateien übersetzt. Der Übersetzungsvorgang wird gesteuert durch Kommandozeilen-Parameter (siehe avr-gcc). Die Option
- -c
- legt fest, daß nur compiliert wird (und nicht gelinkt),
- -o name
- gibt den Name der Ausgabedatei an. Ohne diese Option heisst die Ausgabedatei immer a.out
- -mmcu=atmega8
- legt den Controllertyp fest, in dem Beispiel den ATMega8
- -g
- erzeugt Debug-Infos und
- -Os
- optimiert auf Codegröße.
avr-gcc blinky.c -c -o blinky.o -Os -g -mmcu=atmega8 avr-gcc timer1.c -c -o timer1.o -Os -g -mmcu=atmega8
Danach sind zwei neue Dateien entstanden (*.o)
blinky.c blinky.o timer1.c timer1.h timer1.o
In der Quelle wird der Wert für das OCR1A-Register aus der Taktfrequenz des Controllers (F_CPU) und der Anzahl an Interrupts, die pro Sekunde ausgelöst werden sollen (INTERRUPTS_PER_SECOND), berechnet.
Standardmässig sind AVRs mit dem internen RC-Oszillator mit ca. 1 MHz getaktet. Daher wird F_CPU in timer1.c zu 1000000 definiert:
#ifndef F_CPU #define F_CPU 1000000 #endif /* F_CPU */
Hat man den AVR mit einem anderen Takt laufen, dann gibt man diesen einfach in der Kommandozeile an, z.B. für 8 MHz:
avr-gcc ... -DF_CPU=8000000
Analog kann man mit INTERRUPTS_PER_SECOND verfahren, das auf 1000 gesetzt ist, und auch mit der Option -D überschrieben werden kann.
Linken
Die beiden erzeugten Objekte werden nun zusammengebunden, um die Ausgabedatei (*.elf) zu erhalten:
avr-gcc blinky.o timer1.o -o blinky.elf -mmcu=atmega8
erzeugt die ausführbare Datei (*.elf), die noch zusätzliche Informationen wie Debug-Infos etc. beinhaltet mit dem Namen blinky.elf:
blinky.c blinky.elf blinky.o timer1.c timer1.h timer1.o
Umwandeln nach hex
Viele Progger wollen die zu ladende Datei im Intel-hex-Format (*.hex bzw. *.ihex). Dazu gibt man an
avr-objcopy -j .text -j .data -O ihex blinky.elf blinky.hex
Damit enthält die hex-Datei die Sections .text (Programm) und .data (Daten). Das Verzeichnis beinhaltet jetzt
blinky.c blinky.elf blinky.hex blinky.o timer1.c timer1.h timer1.o
Listfile erstellen
Falls man ein Listfile haben möchte, dann geht das mit
avr-objdump -h -S -j .text -j .data blinky.elf > blinky.lst
Das Listfile ist eine Textdatei, die alle Assembler-Befehle auflistet, wie sie letztendlich auf den Controller geladen werden.
avr-objdump gibt seine Ausgabe auf das Terminal aus. Diese Ausgabe wird mit '>' in die Datei blinky.lst umgeleitet. Hier ein Ausschnitt aus dem Listfile:
0000005c <job_timer1>: uint16_t count; /* irq_count um 1 erhöhen und */ /* gegebenenfalls die LED blinken */ count = 1+irq_count; 5c: 20 91 60 00 lds r18, 0x0060 60: 30 91 61 00 lds r19, 0x0061 64: 2f 5f subi r18, 0xFF ; 255 66: 3f 4f sbci r19, 0xFF ; 255 if (count >= INTERRUPTS_PER_SECOND) 68: 83 e0 ldi r24, 0x03 ; 3 6a: 28 3e cpi r18, 0xE8 ; 232 6c: 38 07 cpc r19, r24 6e: 30 f0 brcs .+12 ; 0x7c { count = 0; 70: 20 e0 ldi r18, 0x00 ; 0 72: 30 e0 ldi r19, 0x00 ; 0 PORT_LED ^= (1 << PAD_LED); 74: 88 b3 in r24, 0x18 ; 24 76: 92 e0 ldi r25, 0x02 ; 2 78: 89 27 eor r24, r25 7a: 88 bb out 0x18, r24 ; 24 } irq_count = count; 7c: 30 93 61 00 sts 0x0061, r19 80: 20 93 60 00 sts 0x0060, r18 84: 08 95 ret
Links stehen die Adressen, dann die Maschinencodes der Befehle, danach die Maschinencodes in Assembler-Darstellung. Ganz rechts nach dem Kommentarzeichen ';' stehen die Dezimalcodes von Konstanten (z.B. 24 für die 0x18 an Adresse 74) oder die Zieladressen von Sprüngen, wie bei dem brcs-Befehl an Adresse 6e, der gegebenenfalls 12 Bytes überspringt und dann an Adresse 7c landet.
Die Größe ermitteln
Die Größe der erhaltenen Objekte/Files können mit avr-size ausgegeben werden.
avr-size -x blinky.o timer1.o
druckt aus:
text data bss dec hex filename 0x42 0x0 0x2 68 44 blinky.o 0x70 0x0 0x2 114 72 timer1.o
Das ist die Größe der einzelnen Objekte. Für das Flash relevant ist die Größe von .text (Code) + .data (initialisierte Daten), für das SRAM relevent ist .data (initialisierte Daten) + .bss (null-initialisierte Daten).
Im Flash werden also 114+68+0+0=182 Bytes belegt, und im SRAM 0+0+2+2=4 Bytes.
Die Gesamtgrösse ergibt sich jedoch erst aus dem elf-File, denn auch die Vektortabelle und der Startup-Code belegen Platz:
avr-size -x -A blinky.elf
druckt aus:
blinky.elf : section size addr .text 0x110 0x0 .data 0x0 0x800060 .bss 0x4 0x800060 .noinit 0x0 0x800064 .eeprom 0x0 0x810000 .stab 0x798 0x0 .stabstr 0x6b0 0x0 Total 0xf5c
Im Flash werden somit 0x110+0=272 Bytes belegt, also 90 Bytes mehr als die Objekte benötigen; davon entfallen z.B. schon 38 Bytes auf die Vektortabelle des ATMega8, die 2*19 Bytes groß ist.
Die Größen einzelner Funktionen lassen sich anzeigen mit
avr-nm --size-sort -S blinky.elf
was nach Größe sortiert ausdruckt:
00800060 00000002 b irq_count 00800062 00000002 b timer1a_job 00000086 00000018 T main 0000009e 00000022 T timer1_init 0000005c 0000002a t job_timer1 000000c0 0000004e T SIG_OUTPUT_COMPARE_1A
Quellcode
blinky.c
#include <avr/io.h> #include <avr/interrupt.h> #include "timer1.h" /* PortB.1 blinkt jede Sekunde */ /* also mit einer Frequenz von 1/2 Hz */ #define DDR_LED DDRB #define PORT_LED PORTB #define PAD_LED 1 static void ioinit(); static void job_timer1(); /* Zählt in jedem aufgetretenem IRQ eins hoch */ static volatile uint16_t irq_count = 0; /* Diese Funktion ist ein 'Callback' */ /* Sie wird an timer1_init() übergeben */ /* und von dort aus aufgerufen, und zwar */ /* INTERRUPTS_PER_SECOND mal pro Sekunde. */ /* Sie wird also auf IRQ-Ebene ausgeführt */ void job_timer1() { uint16_t count; /* irq_count um 1 erhöhen und */ /* gegebenenfalls die LED blinken */ count = 1+irq_count; if (count >= INTERRUPTS_PER_SECOND) { count = 0; PORT_LED ^= (1 << PAD_LED); } irq_count = count; } void ioinit() { /* Port als Ausgang */ DDR_LED |= (1 << PAD_LED); /* Initialisiert Timer1, um jede Sekunde */ /* INTERRUPTS_PER_SECOND mal die Funktion job_timer1 */ /* aufzurufen */ timer1_init (job_timer1); } int main (void) { /* Peripherie initialisieren */ ioinit(); /* Interrupts aktivieren */ sei(); /* Nach main landen wir in exit(), */ /* das nur aus einer Endlosschleife besteht (avr-gcc) */ /* Der Interrupt lässt die LED weiterhin */ /* im Sekundentakt blinken */ return 0; }
timer1.h
#ifndef _TIMER1_H_ #define _TIMER1_H_ #include <inttypes.h> #ifndef INTERRUPTS_PER_SECOND #define INTERRUPTS_PER_SECOND 1000 #endif /* INTERRUPTS_PER_SECOND */ extern void timer1_init (void (*) (void)); #endif /* _TIMER1_H_ */
timer1.c
#include <avr/io.h> #include <avr/signal.h> #include "timer1.h" #ifndef F_CPU #define F_CPU 1000000 #endif /* F_CPU */ /* Test von F_CPU und INTERRUPTS_PER_SECOND */ /* auf Gültigkeitsbereich */ #if (F_CPU / INTERRUPTS_PER_SECOND -1 < 0) \ || (F_CPU / INTERRUPTS_PER_SECOND -1 >= 0x10000) #error Werte für F_CPU bzw. INTERRUPTS_PER_SECOND ungeeignet #error evtl. muss der Prescaler verwendet werden #endif /* Callback-Funktion */ static void (*timer1a_job) (void); void timer1_init (void (*job) (void)) { timer1a_job = job; #if defined (__AVR_AT90S2313__) /* AVR Classic: */ /* Timer1 läuft mit vollem Takt */ /* CTC: Clear Timer on CompareMatch */ /* Timer1 ist Zähler */ TCCR1A = 0; TCCR1B = _BV (CS10) | _BV (CTC1); #elif defined (__AVR_ARCH__) && ((__AVR_ARCH__==4) || (__AVR_ARCH__==5)) /* AVR Mega: */ /* Mode #4 für Timer1 (ATMega8 Manual S. 97) */ /* und voller MCU Takt (Prescale=1) */ TCCR1A = 0; TCCR1B = _BV (WGM12) | _BV (CS10); #else #error Dont know how to setup timer1 #endif /* OutputCompare1A Register setzen */ OCR1A = (uint16_t) ((uint32_t) F_CPU / INTERRUPTS_PER_SECOND -1); /* evtl. gesetztes OC1A-Flag zurücksetzen */ TIFR = (1 << OCF1A); /* OutputCompare1A Interrupt aktivieren */ TIMSK |= (1 << OCIE1A); } /* Die Interrupt Service Routine ruft lediglich */ /* timer1a_job auf (callback) */ SIGNAL(SIG_OUTPUT_COMPARE_1A) { timer1a_job(); }
Spin-off
Eine LED blinken zu lassen könnte auch wesentlich einfacher implementert werden, also z.B. ohne Funktionsaufrufe oder Interrupt-Programmierung.
Neben der eigentlichen Aufgabe "LED blinken lassen" kann man an dem Code aber noch andere Dinge lernen:
- Programmierung eines Interrupts
- Initialisierung von Timer1 als Zähler mit "Clear Timer on Compare Match"
- Bedingte Codeübersetzung/Controllerunterscheidung mit #ifdef (in timer1_init)
- Abchecken der Güligkeit von Defines durch den Präprozessor
- Übergabe eines Funktionszeigers, um später eine Funktion indirekt aufzurufen
- Zusammenlinken mehrerer Module