Aus RN-Wissen.de
Wechseln zu: Navigation, Suche
Laderegler Test Tueftler Seite

(Spin-off)
(Siehe auch)
 
(81 dazwischenliegende Versionen von 10 Benutzern werden nicht angezeigt)
Zeile 3: Zeile 3:
 
wie man überhaupt ein Programm übersetzt und einen Compiler verwendet.
 
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",
+
Was für den PC das "Hallo Welt", ist für einen kleinen Microcontroller der "Hallo Blinky",
der einfch nur eine Leuchtdiode (LED) blinken lässt.
+
der einfach nur eine [[Diode|Leuchtdiode]] (LED) blinken lässt.
  
 
Im Vergleich zu "Hallo Welt" sieht der Blinky viel komplizierter aus,  
 
Im Vergleich zu "Hallo Welt" sieht der Blinky viel komplizierter aus,  
 
aber eigentlich ist er einfacher, dann es werden keine umfangreichen Funktionen oder
 
aber eigentlich ist er einfacher, dann es werden keine umfangreichen Funktionen oder
 
Black-Boxen benutzt wie etwa <tt>printf()</tt>.  
 
Black-Boxen benutzt wie etwa <tt>printf()</tt>.  
Ausser dem eingegebenen Quellcode kommt also kein andere Code zur Ausführung!
+
Ausser dem eingegebenen Quellcode kommt also kein anderer Code zur Ausführung!
 +
 
 +
 
 +
=Beschreibung=
 +
 
 +
Eine [[LED]] an Port B1 wird einmal pro Sekunde an bzw. ausgeschaltet. Die LED blinkt also mit einer Frequenz von 1/2 Hz.
 +
 
 +
Im Programm wird der Wert für das OCR1A-Register aus der Taktfrequenz des
 +
Controllers (<tt>F_CPU</tt>) und der Anzahl an Interrupts, die pro Sekunde
 +
ausgelöst werden sollen (<tt>IRQS_PER_SECOND</tt>), berechnet.
 +
 
 +
Standardmässig sind [[AVR|AVRs]] mit dem internen RC-Oszillator mit ca. 1&nbsp;MHz getaktet.
 +
Daher wird <tt>F_CPU</tt> in <tt>blinky.c</tt> zu 1000000 definiert:
 +
#define F_CPU 1000000
 +
{{FarbigerRahmen|
 +
Dieser Wert hat rein informativen Charakter und bewirkt ''nicht'', daß der Controller mit einer anderen Frequenz läuft! Das geht über einen anderen Quarz/Oszillator oder andere Fuse-Einstellungen.
 +
}}
 +
 
 +
Das Programm ist ausgelegt für eine Taktrate von 1&nbsp;MHz. Wenn dein [[Controller]] mit einem anderen Takt läuft, dann hast du zwei Möglichkeiten:
 +
* Du lässt das Programm so, wie es ist. Dann blinkt die LED entsprechend schneller bzw. langsamer. Hast du deinen AVR zB mit 16&nbsp;MHz getaktet, dann blinkt die LED mit 8&nbsp;Hz.
 +
* Du passt die Taktfrequenz in der Quelle oder per Kommandozeile an. Für 8 MHz:
 +
:<pre> > avr-gcc ... -DF_CPU=8000000</pre>
 +
 
 +
=Quellcode=
 +
 
 +
#include <avr/io.h>
 +
#include <avr/interrupt.h>
 +
 +
{{ccomment|Geblinkt wird PortB.1 (push-pull)}}
 +
{{ccomment|Eine LED in Reihe mit einem Vorwiderstand zwischen}}
 +
{{ccomment|PortB.1 und GND anschliessen.}}
 +
#define PAD_LED  1
 +
#define PORT_LED PORTB
 +
#define DDR_LED  DDRB
 +
 +
{{ccomment|Der MCU-Takt. Wird gebraucht, um Timer1 mit den richtigen}}
 +
{{ccomment|Werten zu initialisieren. Voreinstellung ist 1MHz.}}
 +
{{ccomment|(Werkseinstellung für AVRs mit internem Oszillator).}}
 +
{{ccomment|Das Define wird nur gemacht, wenn F_CPU noch nicht definiert wurde.}}
 +
{{ccomment|F_CPU kann man so auch per Kommandozeile definieren, z.B. für 8MHz:}}
 +
{{ccomment|avr-gcc ... -DF_CPU&#61;8000000}}
 +
{{ccomment| &nbsp;}}
 +
{{ccomment|! Der Wert von F_CPU hat rein informativen Character für}}
 +
{{ccomment|! die korrekte Codeerzeugung im Programm!}}
 +
{{ccomment|! Um die Taktrate zu ändern müssen die Fuses des Controllers}}
 +
{{ccomment|! und/oder Quarz/Resonator/RC-Glied/Oszillator}}
 +
{{ccomment|! angepasst werden!}}
 +
#ifndef F_CPU
 +
#define F_CPU    1000000
 +
#endif
 +
 +
{{ccomment|So viele IRQs werden jede Sekunde ausgelöst.}}
 +
{{ccomment|Für optimale Genauigkeit muss}}
 +
{{ccomment|IRQS_PER_SECOND ein Teiler von F_CPU sein}}
 +
{{ccomment|und IRQS_PER_SECOND ein Vielfaches von 100.}}
 +
{{ccomment|Ausserdem muss gelten F_CPU / IRQS_PER_SECOND <&#61; 65536}}
 +
#define IRQS_PER_SECOND  2000 {{comment|500 µs}}
 +
 +
{{ccomment|Anzahl IRQs pro 10 Millisekunden}}
 +
#define IRQS_PER_10MS    (IRQS_PER_SECOND / 100)
 +
 +
{{ccomment|Gültigkeitsprüfung.}}
 +
{{ccomment|Bei ungeeigneten Werten gibt es einen Compilerfehler}}
 +
#if (F_CPU/IRQS_PER_SECOND > 65536) || (IRQS_PER_10MS < 1) || (IRQS_PER_10MS > 255)
 +
#  error Diese Werte fuer F_CPU und IRQS_PER_SECOND
 +
#  error sind ausserhalb des gueltigen Bereichs!
 +
#endif
 +
 +
{{ccomment|Compiler-Warnung falls die Genauigkeit nicht optimal ist.}}
 +
{{ccomment|Wenn das nervt für deine Werte, einfach löschen :-)}}
 +
#if (F_CPU % IRQS_PER_SECOND != 0) || (IRQS_PER_SECOND % 100 != 0)
 +
#  warning Das Programm arbeitet nicht mit optimaler Genauigkeit.
 +
#endif
 +
 +
{{ccomment|Prototypen}}
 +
void wait_10ms (const uint8_t);
 +
void timer1_init (void);
 +
 +
{{ccomment|Zähler-Variable. Wird in der ISR erniedrigt und in wait_10ms benutzt.}}
 +
static volatile uint8_t timer_10ms;
 +
 +
{{ccomment|//////////////////////////////////////////////////////////////////////}}
 +
{{ccomment|Implementierungen der Funktionen}}
 +
{{ccomment|//////////////////////////////////////////////////////////////////////}}
 +
 +
#if !defined (TCNT1H)
 +
#error Dieser Controller hat keinen 16-Bit Timer1!
 +
#endif {{ccomment|TCNT1H}}
 +
 +
{{ccomment|//////////////////////////////////////////////////////////////////////}}
 +
{{ccomment|Timer1 so initialisieren, daß er IRQS_PER_SECOND }}
 +
{{ccomment|IRQs pro Sekunde erzeugt.}}
 +
void timer1_init (void)
 +
{
 +
    {{ccomment|Timer1: keine PWM}}
 +
    TCCR1A = 0;
 +
 +
    {{ccomment|Timer1 ist Zähler: Clear Timer on Compare Match (CTC, Mode #4)}}
 +
    {{ccomment|Timer1 läuft mit vollem MCU-Takt: Prescale &#61; 1}}
 +
#if defined (CTC1) && !defined (WGM12)
 +
    TCCR1B = (1 << CTC1)  | (1 << CS10);
 +
#elif !defined (CTC1) && defined (WGM12)
 +
    TCCR1B = (1 << WGM12) | (1 << CS10);
 +
#else
 +
#error Keine Ahnung, wie Timer1 fuer diesen AVR zu initialisieren ist!
 +
#endif
 +
 +
    {{ccomment|OutputCompare für gewünschte Timer1 Frequenz}}
 +
    {{ccomment|TCNT1 zählt immer 0...OCR1A, 0...OCR1A, ... }}
 +
    {{ccomment|Beim überlauf OCR1A -> OCR1A+1 wird TCNT1&#61;0 gesetzt und im nächsten}}
 +
    {{ccomment|MCU-Takt eine IRQ erzeugt.}}
 +
    OCR1A = (unsigned short) ((unsigned long) F_CPU / IRQS_PER_SECOND-1);
 +
 +
    {{ccomment|OutputCompareA-Interrupt für Timer1 aktivieren}}
 +
#if defined (TIMSK1)
 +
    TIMSK1 |= (1 << OCIE1A);
 +
#elif defined (TIMSK)
 +
    TIMSK  |= (1 << OCIE1A);
 +
#else
 +
#error Keine Ahnung, wie IRQs fuer diesen AVR zu initialisieren sind!
 +
#endif
 +
}
 +
 +
{{ccomment|//////////////////////////////////////////////////////////////////////}}
 +
{{ccomment|Wartet etwa t*10 ms. }}
 +
{{ccomment|timer_10ms wird alle 10ms in der Timer1-ISR erniedrigt. }}
 +
{{ccomment|Weil es bis zum nächsten IRQ nicht länger als 10ms dauert,}}
 +
{{ccomment|wartet diese Funktion zwischen (t-1)*10 ms und t*10 ms.}}
 +
void wait_10ms (const uint8_t t)
 +
{
 +
    timer_10ms = t;
 +
    while (timer_10ms);
 +
}
 +
 +
{{ccomment|//////////////////////////////////////////////////////////////////////}}
 +
{{ccomment|Die Interrupt Service Routine (ISR).}}
 +
{{ccomment|In interrupt_num_10ms werden die IRQs gezählt.}}
 +
{{ccomment|Sind IRQS_PER_10MS Interrups geschehen, }}
 +
{{ccomment|dann sind 10 ms vergangen.}}
 +
{{ccomment|timer_10ms wird alle 10 ms um 1 vermindert und bleibt bei 0 stehen.}}
 +
ISR (TIMER1_COMPA_vect)
 +
{
 +
    static uint8_t interrupt_num_10ms;
 +
 +
    {{ccomment|interrupt_num_10ms erhöhen und mit Maximalwert vergleichen}}
 +
    if (++interrupt_num_10ms == IRQS_PER_10MS)
 +
    {
 +
        {{ccomment|10 Millisekunden sind vorbei}}
 +
        {{ccomment|interrupt_num_10ms zurücksetzen}}
 +
        interrupt_num_10ms = 0;
 +
 +
        {{ccomment|Alle 10ms wird timer_10ms erniedrigt, falls es nicht schon 0 ist.}}
 +
        {{ccomment|Wird verwendet in wait_10ms}}
 +
        if (timer_10ms != 0)
 +
            timer_10ms--;
 +
    }
 +
}
 +
 +
{{ccomment|//////////////////////////////////////////////////////////////////////}}
 +
{{ccomment|Das Hauptprogramm: Startpunkt }}
 +
int main (void)
 +
{
 +
    {{ccomment|LED-Port auf OUT}}
 +
    DDR_LED  |= (1 << PAD_LED);
 +
 +
    {{ccomment|Timer1 initialisieren}}
 +
    timer1_init();
 +
 +
    {{ccomment|Interrupts aktivieren}}
 +
    sei();
 +
 +
    {{ccomment|Endlosschleife}}
 +
    {{ccomment|Die LED ist jeweils 1 Sekunde an und 1 Sekunde aus,}}
 +
    {{ccomment|blinkt also mit einer Frequenz von 0.5 Hz}}
 +
    while (1)
 +
    {
 +
        {{ccomment|LED an}}
 +
        PORT_LED |= (1 << PAD_LED);
 +
 +
        {{ccomment|1 Sekunde warten}}
 +
        wait_10ms (100);
 +
 +
        {{ccomment|LED aus}}
 +
        PORT_LED &= ~(1 << PAD_LED);
 +
 +
        {{ccomment|1 Sekunde warten}}
 +
        wait_10ms (100);
 +
    }
 +
 +
    {{ccomment|main braucht keine return-Anweisung, weil wir nie hier hin kommen}}
 +
}
  
 
=Von der C-Quelle zum hex-File=
 
=Von der C-Quelle zum hex-File=
  
Das Beispiel besteht ganz bewusst aus zwei getrennten
+
Das Übersetzen über Kommandozeilen-Eingaben erledigen und ''nicht''
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 erlegigt und ''nicht''
+
 
über Werkzeuge wie [[make]],  
 
über Werkzeuge wie [[make]],  
dessen inkorrekte Anwendung eine häufige Fehlerquelle ist.
+
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 ehtält dieses
+
Nach Speichern der Quelldatein in ein eigenes Verzeichnis enthält dieses die Datei
  blinky.c timer1.c timer1.h
+
  blinky.c
  
 
==Compilieren==
 
==Compilieren==
  
Zunächst werden die C-Dateien übersetzt.  
+
Zunächst wird die C-Datei übersetzt.  
 
Der Übersetzungsvorgang wird gesteuert durch Kommandozeilen-Parameter (siehe [[avr-gcc]]).
 
Der Übersetzungsvorgang wird gesteuert durch Kommandozeilen-Parameter (siehe [[avr-gcc]]).
Die Option <tt>-c</tt> tegt fest, daß nur compiliert wird (und nicht gelinkt),  
+
Die Option  
<tt>-o name</tt> gibt den Name der Ausgabedatei an,
+
;<tt>-c</tt>: legt fest, daß nur compiliert wird (und nicht gelinkt),  
<tt>-mmcu=xxxx</tt> legt den Controllertyp fest,
+
;<tt>-o name</tt>: gibt den Namen der Ausgabedatei an. Ohne diese Option heißt die Ausgabedatei immer <tt>a.out</tt>
<tt>-g</tt> erzeugt Debug-Infos und
+
;<tt>-mmcu=atmega8</tt>: legt den Controllertyp fest, in dem Beispiel den [[ATmega8]]
<tt>-Os</tt> optimiert auf Codegröße.
+
;<tt>-g</tt>: erzeugt Debug-Infos und
avr-gcc blinky.c -c -o blinky.o -Os -g -mmcu=atmega8
+
;<tt>-Os</tt>: optimiert auf Codegröße.
  avr-gcc timer1.c -c -o timer1.o -Os -g -mmcu=atmega8
+
;<tt>-DF_CPU=xxx</tt>: optional, falls der Controller nicht mit 1 MHz läuft. <tt>xxx</tt> ist die Taktfrequenz in Hertz.
Danach sind zwei neue Dateien entstanden (*.o)
+
  > avr-gcc blinky.c -c -o blinky.o -Os -g -mmcu=atmega8
  blinky.c blinky.o timer1.c timer1.h timer1.o
+
Danach ist eine neue Datei entstanden (*.o)
 +
  blinky.c blinky.o
  
 
==Linken==
 
==Linken==
Die beiden erzeugten Objekte werden nun zusammengebunden, um die Ausgabedatei (*.elf) zu erhalten:
+
Die erzeugte Objek-Datei wird nun zur Ausgabedatei (*.elf) gelinkt:
  avr-gcc blinky.o timer1.o -o blinky.elf -mmcu=atmega8
+
  > avr-gcc blinky.o -o blinky.elf -mmcu=atmega8
 
erzeugt die ausführbare Datei (*.elf), die noch zusätzliche Informationen wie
 
erzeugt die ausführbare Datei (*.elf), die noch zusätzliche Informationen wie
 
Debug-Infos etc. beinhaltet mit dem Namen <tt>blinky.elf</tt>:
 
Debug-Infos etc. beinhaltet mit dem Namen <tt>blinky.elf</tt>:
  blinky.c blinky.elf blinky.o timer1.c timer1.h timer1.o
+
  blinky.c blinky.elf blinky.o
  
 
==Umwandeln nach hex==
 
==Umwandeln nach hex==
  
 +
===Code und Daten===
 
Viele Progger wollen die zu ladende Datei im Intel-hex-Format (*.hex bzw. *.ihex).
 
Viele Progger wollen die zu ladende Datei im Intel-hex-Format (*.hex bzw. *.ihex).
 
Dazu gibt man an
 
Dazu gibt man an
  avr-objcopy -j .text -j .data -O ihex blinky.elf blinky.hex
+
  > avr-objcopy -j .text -j .data -O ihex blinky.elf blinky.hex
 
Damit enthält die hex-Datei die Sections <tt>.text</tt> (Programm) und <tt>.data</tt> (Daten).
 
Damit enthält die hex-Datei die Sections <tt>.text</tt> (Programm) und <tt>.data</tt> (Daten).
 
Das Verzeichnis beinhaltet jetzt
 
Das Verzeichnis beinhaltet jetzt
  blinky.c blinky.elf blinky.hex blinky.o timer1.c timer1.h timer1.o
+
  blinky.c blinky.elf blinky.hex blinky.o
 +
 
 +
===EEPROM===
 +
Ein hex-File, das den Inhalt des EEPROMs wiederspiegelt, erhält man mit
 +
> avr-objcopy -j .eeprom --change-section-lma .eeprom=0 -O ihex blinky.elf blinky_eeprom.hex
 +
Für das Beispiel ist das EEPROM-File <tt>blinky_eeprom.hex</tt> leer,
 +
da wir keine Daten ins EEPROM gelegt haben.
  
 
==Listfile erstellen==
 
==Listfile erstellen==
  
 
Falls man ein Listfile haben möchte, dann geht das mit
 
Falls man ein Listfile haben möchte, dann geht das mit
  avr-objdump -h -S -j .text -j .data blinky.elf > blinky.lst
+
  > avr-objdump -h -S -j .text -j .data blinky.elf > blinky.lst
 
Das Listfile ist eine Textdatei, die alle Assembler-Befehle auflistet,
 
Das Listfile ist eine Textdatei, die alle Assembler-Befehle auflistet,
 
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>&gt;</tt>' in die Datei <tt>blinky.lst</tt> umgeleitet.
 +
Hier ein Ausschnitt aus dem Listfile:
 +
<pre>
 +
00000072 <wait_10ms>:
 +
 +
// //////////////////////////////////////////////////////////////////////
 +
// Wartet etwa t*10 ms.
 +
// timer_10ms wird alle 10ms in der Timer1-ISR erniedrigt.
 +
// Weil es bis zum nächsten IRQ nicht länger als 10ms dauert,
 +
// wartet diese Funktion zwischen (t-1)*10 ms und t*10 ms.
 +
void wait_10ms (const uint8_t t)
 +
{
 +
    timer_10ms = t;
 +
  72: 80 93 61 00 sts 0x0061, r24
 +
    while (timer_10ms);
 +
  76: 80 91 61 00 lds r24, 0x0061
 +
  7a: 88 23      and r24, r24
 +
  7c: e1 f7      brne .-8      ; 0x76 <wait_10ms+0x4>
 +
  7e: 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 oder die Zieladressen von Sprüngen, wie bei dem <tt>brne</tt>-Befehl an Adresse 7c, der gegebenenfalls
 +
8 Bytes zurückspringt und dann an Adresse 76 landet.
 +
 +
==Mapfile erstellen==
 +
Ein Mapfile gibt Auskunft darüber, an welcher Adresse Code und Objekte
 +
landen. Erstellt wird das Mapfile während des Linkens, indem <tt>avr-gcc</tt>
 +
ein Option an den Linker weiterreicht, die diesem zum Erstellen eines solchen Files veranlasst:
 +
> avr-gcc blinky.o -o blinky.elf ... -Wl,-Map,blinky.map
 +
Dadurch entsteht das Mapfile <tt>blinky.map</tt>.
  
 
==Die Größe ermitteln==
 
==Die Größe ermitteln==
  
 
Die Größe der erhaltenen Objekte/Files können mit <tt>avr-size</tt> ausgegeben werden.
 
Die Größe der erhaltenen Objekte/Files können mit <tt>avr-size</tt> ausgegeben werden.
  avr-size -x blinky.o timer1.o
+
  > avr-size -x blinky.o
 
druckt aus:
 
druckt aus:
 
   text    data    bss    dec    hex filename
 
   text    data    bss    dec    hex filename
   0x42     0x0    0x2      68      44 blinky.o
+
   0x7c     0x0    0x2     126     7e blinky.o
  0x70    0x0    0x2    114      72 timer1.o
+
Das sind die Größen der einzelnen [[avr-gcc/Interna#Sections|Sections]].
Das ist die Größe der einzelnen Objekte.
+
 
Für das Flash relevant ist die Größe von  
 
Für das Flash relevant ist die Größe von  
 
<tt>.text</tt> (Code) + <tt>.data</tt> (initialisierte Daten),
 
<tt>.text</tt> (Code) + <tt>.data</tt> (initialisierte Daten),
 
für das SRAM relevent ist <tt>.data</tt> (initialisierte Daten)  
 
für das SRAM relevent ist <tt>.data</tt> (initialisierte Daten)  
+ <tt>.bss</tt> (null-initialisierte Daten.
+
+ <tt>.bss</tt> (null-initialisierte Daten).
avr-size -x -A blinky.elf
+
druckt aus:
+
<pre>
+
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
+
</pre>
+
  
Die Größe einzelner Funktionen lässt man anzeigen mit
+
Im Flash werden also 126+0=126 Bytes belegt, und im SRAM 0+2=2 Bytes.
avr-nm --size-sort -S blinky.elf
+
was druckt
+
<pre>
+
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
+
</pre>
+
 
+
=Quellcode=
+
 
+
==blinky.c==
+
  
 +
Ab binutils 2.16 gibt es für <tt>avr-size</tt> die Option <tt>-C</tt>, mit der man eine Zusammenfassung des ganzen Programms ausgeben kann:
 +
> avr-size -C --mcu=atmega8 blinky.elf
 +
druckt aus:
 
<pre>
 
<pre>
#include <avr/io.h>
+
AVR Memory Usage
#include <avr/interrupt.h>
+
----------------
 
+
Device: atmega8
#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    */
+
Program:     216 bytes (2.6% Full)
    sei();
+
(.text + .data + .bootloader)
  
    /* Nach main landen wir in exit(),                    */
+
Data:          2 bytes (0.2% Full)
    /* das nur aus einer Endlosschleife besteht (avr-gcc) */
+
(.data + .bss + .noinit)
    /* Der Interrupt lässt die LED weiterhin              */
+
    /* im Sekundentakt blinken                            */
+
    return 0;
+
}
+
 
</pre>
 
</pre>
  
==timer1.h==
+
Die Gesamtgrösse ergibt sich erst aus dem elf-File,
 
+
denn auch die Vektortabelle und der Startup-Code belegen Platz. Hier eine Auflistung der beteiligten Sections:
 +
> avr-size -x -A blinky.elf
 +
druckt aus:
 
<pre>
 
<pre>
#ifndef _TIMER1_H_
+
blinky.elf  :
#define _TIMER1_H_
+
section            size      addr
 
+
.text              0xd8        0x0
#include <inttypes.h>
+
.data              0x0  0x800060
 
+
.bss                0x2  0x800060
#ifndef INTERRUPTS_PER_SECOND
+
.noinit            0x0  0x800062
#define INTERRUPTS_PER_SECOND 1000
+
.eeprom            0x0  0x810000
#endif /* INTERRUPTS_PER_SECOND */
+
.stab            0x36c        0x0
 
+
.stabstr          0x84        0x0
extern void timer1_init (void (*) (void));
+
.debug_aranges    0x14        0x0
 
+
.debug_pubnames    0x48        0x0
#endif /* _TIMER1_H_ */
+
.debug_info        0xfc        0x0
</pre>
+
.debug_abbrev      0xa2        0x0
 
+
.debug_line      0x101        0x0
==timer1.c==
+
.debug_str        0xa6        0x0
 +
Total            0x86b</pre>
 +
Im Flash werden somit 0xd8+0=216 Bytes belegt, also 90 Bytes mehr als das Object benötigt; davon entfallen z.B. schon 38 Bytes auf die Vektortabelle des [[ATmega8]],
 +
die 2*19 Bytes groß ist.  
  
 +
Die Größen einzelner Funktionen/Variablen lassen sich anzeigen mit
 +
> avr-nm --size-sort -S blinky.elf
 +
was nach Größe sortiert ausdruckt:
 
<pre>
 
<pre>
#include <avr/io.h>
+
00800060 00000001 b interrupt_num_10ms.0
#include <avr/signal.h>
+
00800061 00000001 b timer_10ms
 
+
00000072 0000000e T wait_10ms
#include "timer1.h"
+
0000005c 00000016 T timer1_init
 
+
000000bc 0000001c T main
#ifndef F_CPU
+
00000080 0000003c T __vector_6
#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();
+
}
+
 
</pre>
 
</pre>
  
 +
= HEX-Dateien zu diesem C-Code =
 +
Zu dieser C-Datei gibt es HEX-Dateien für einige AVRs. Siehe dazu [[HEX Beispiel-Dateien für AVR]].
 
=Spin-off=
 
=Spin-off=
  
Eine LED blinken zu lassen könnte auch wesentlich einfacher implementert werden,
+
Eine [[LED]] blinken zu lassen könnte auch wesentlich einfacher implementert werden,
also z.B. ohne Funktionsaufrufe oder Interrupt-Programmierung.
+
also z.B. ohne Funktionsaufrufe oder [[Interrupt]]-Programmierung.
  
 
Neben der eigentlichen Aufgabe "LED blinken lassen" kann man an dem Code
 
Neben der eigentlichen Aufgabe "LED blinken lassen" kann man an dem Code
Zeile 263: Zeile 368:
 
* Initialisierung von Timer1 als Zähler mit "Clear Timer on Compare Match"
 
* Initialisierung von Timer1 als Zähler mit "Clear Timer on Compare Match"
 
* Bedingte Codeübersetzung/Controllerunterscheidung mit <tt>#ifdef</tt> (in <tt>timer1_init</tt>)
 
* Bedingte Codeübersetzung/Controllerunterscheidung mit <tt>#ifdef</tt> (in <tt>timer1_init</tt>)
* Abchecken der Güligkeit von Defines durch den Präprozessor
+
* Abchecken der Gültigkeit von Defines durch den Präprozessor
* Übergabe eines Funktionszeigers, um später eine Funktion indirekt aufzurufen
+
* Zusammenlinken mehrerer Module
+
  
 
=Siehe auch=
 
=Siehe auch=
 +
* [[LED-Blinken ohne Timer]]
 +
* [[HEX Beispiel-Dateien für AVR]] - Aus diesem Quellcode erzeugte Binärdateien zum Download und Proggen.
 +
* [[AVR]]
 +
* [[avr-gcc]]
 +
* [[Interrupt]]
 +
* [[:Kategorie:Quellcode C|weitere C-Code Beispiele]]
 +
  
 +
[[Kategorie:Grundlagen]]
 +
[[Kategorie:Microcontroller]]
 
[[Kategorie:Quellcode C]]
 
[[Kategorie:Quellcode C]]
 +
[[Kategorie:Software]]
 +
[[Kategorie:Praxis]]

Aktuelle Version vom 8. Juni 2012, 08:21 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 anderer Code zur Ausführung!


Beschreibung

Eine LED an Port B1 wird einmal pro Sekunde an bzw. ausgeschaltet. Die LED blinkt also mit einer Frequenz von 1/2 Hz.

Im Programm 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 (IRQS_PER_SECOND), berechnet.

Standardmässig sind AVRs mit dem internen RC-Oszillator mit ca. 1 MHz getaktet. Daher wird F_CPU in blinky.c zu 1000000 definiert:

#define F_CPU 1000000

Dieser Wert hat rein informativen Charakter und bewirkt nicht, daß der Controller mit einer anderen Frequenz läuft! Das geht über einen anderen Quarz/Oszillator oder andere Fuse-Einstellungen.

Das Programm ist ausgelegt für eine Taktrate von 1 MHz. Wenn dein Controller mit einem anderen Takt läuft, dann hast du zwei Möglichkeiten:

  • Du lässt das Programm so, wie es ist. Dann blinkt die LED entsprechend schneller bzw. langsamer. Hast du deinen AVR zB mit 16 MHz getaktet, dann blinkt die LED mit 8 Hz.
  • Du passt die Taktfrequenz in der Quelle oder per Kommandozeile an. Für 8 MHz:
 > avr-gcc ... -DF_CPU=8000000

Quellcode

#include <avr/io.h>
#include <avr/interrupt.h>

// Geblinkt wird PortB.1 (push-pull)
// Eine LED in Reihe mit einem Vorwiderstand zwischen
// PortB.1 und GND anschliessen.
#define PAD_LED  1
#define PORT_LED PORTB
#define DDR_LED  DDRB

// Der MCU-Takt. Wird gebraucht, um Timer1 mit den richtigen
// Werten zu initialisieren. Voreinstellung ist 1MHz.
// (Werkseinstellung für AVRs mit internem Oszillator).
// Das Define wird nur gemacht, wenn F_CPU noch nicht definiert wurde.
// F_CPU kann man so auch per Kommandozeile definieren, z.B. für 8MHz:
// avr-gcc ... -DF_CPU=8000000
//   
// ! Der Wert von F_CPU hat rein informativen Character für
// ! die korrekte Codeerzeugung im Programm!
// ! Um die Taktrate zu ändern müssen die Fuses des Controllers
// ! und/oder Quarz/Resonator/RC-Glied/Oszillator
// ! angepasst werden!
#ifndef F_CPU
#define F_CPU    1000000
#endif

// So viele IRQs werden jede Sekunde ausgelöst.
// Für optimale Genauigkeit muss
// IRQS_PER_SECOND ein Teiler von F_CPU sein
// und IRQS_PER_SECOND ein Vielfaches von 100.
// Ausserdem muss gelten F_CPU / IRQS_PER_SECOND <= 65536
#define IRQS_PER_SECOND   2000 /* 500 µs */

// Anzahl IRQs pro 10 Millisekunden
#define IRQS_PER_10MS     (IRQS_PER_SECOND / 100)

// Gültigkeitsprüfung.
// Bei ungeeigneten Werten gibt es einen Compilerfehler
#if (F_CPU/IRQS_PER_SECOND > 65536) || (IRQS_PER_10MS < 1) || (IRQS_PER_10MS > 255)
#   error Diese Werte fuer F_CPU und IRQS_PER_SECOND
#   error sind ausserhalb des gueltigen Bereichs!
#endif

// Compiler-Warnung falls die Genauigkeit nicht optimal ist.
// Wenn das nervt für deine Werte, einfach löschen :-)
#if (F_CPU % IRQS_PER_SECOND != 0) || (IRQS_PER_SECOND % 100 != 0)
#   warning Das Programm arbeitet nicht mit optimaler Genauigkeit.
#endif

// Prototypen
void wait_10ms (const uint8_t);
void timer1_init (void);

// Zähler-Variable. Wird in der ISR erniedrigt und in wait_10ms benutzt.
static volatile uint8_t timer_10ms;

// //////////////////////////////////////////////////////////////////////
// Implementierungen der Funktionen
// //////////////////////////////////////////////////////////////////////

#if !defined (TCNT1H)
#error Dieser Controller hat keinen 16-Bit Timer1!
#endif // TCNT1H

// //////////////////////////////////////////////////////////////////////
// Timer1 so initialisieren, daß er IRQS_PER_SECOND 
// IRQs pro Sekunde erzeugt.
void timer1_init (void)
{
    // Timer1: keine PWM
    TCCR1A = 0;

    // Timer1 ist Zähler: Clear Timer on Compare Match (CTC, Mode #4)
    // Timer1 läuft mit vollem MCU-Takt: Prescale = 1
#if defined (CTC1) && !defined (WGM12)
   TCCR1B = (1 << CTC1)  | (1 << CS10);
#elif !defined (CTC1) && defined (WGM12)
   TCCR1B = (1 << WGM12) | (1 << CS10);
#else
#error Keine Ahnung, wie Timer1 fuer diesen AVR zu initialisieren ist!
#endif

    // OutputCompare für gewünschte Timer1 Frequenz
    // TCNT1 zählt immer 0...OCR1A, 0...OCR1A, ... 
    // Beim überlauf OCR1A -> OCR1A+1 wird TCNT1=0 gesetzt und im nächsten
    // MCU-Takt eine IRQ erzeugt.
    OCR1A = (unsigned short) ((unsigned long) F_CPU / IRQS_PER_SECOND-1);

    // OutputCompareA-Interrupt für Timer1 aktivieren
#if defined (TIMSK1)
    TIMSK1 |= (1 << OCIE1A);
#elif defined (TIMSK)
    TIMSK  |= (1 << OCIE1A);
#else	 
#error Keine Ahnung, wie IRQs fuer diesen AVR zu initialisieren sind!
#endif
}

// //////////////////////////////////////////////////////////////////////
// Wartet etwa t*10 ms. 
// timer_10ms wird alle 10ms in der Timer1-ISR erniedrigt. 
// Weil es bis zum nächsten IRQ nicht länger als 10ms dauert,
// wartet diese Funktion zwischen (t-1)*10 ms und t*10 ms.
void wait_10ms (const uint8_t t)
{
    timer_10ms = t;
    while (timer_10ms);
}

// //////////////////////////////////////////////////////////////////////
// Die Interrupt Service Routine (ISR).
// In interrupt_num_10ms werden die IRQs gezählt.
// Sind IRQS_PER_10MS Interrups geschehen, 
// dann sind 10 ms vergangen.
// timer_10ms wird alle 10 ms um 1 vermindert und bleibt bei 0 stehen.
ISR (TIMER1_COMPA_vect)
{
    static uint8_t interrupt_num_10ms;

    // interrupt_num_10ms erhöhen und mit Maximalwert vergleichen
    if (++interrupt_num_10ms == IRQS_PER_10MS)
    {
        // 10 Millisekunden sind vorbei
        // interrupt_num_10ms zurücksetzen
        interrupt_num_10ms = 0;

        // Alle 10ms wird timer_10ms erniedrigt, falls es nicht schon 0 ist.
        // Wird verwendet in wait_10ms
        if (timer_10ms != 0)
            timer_10ms--;
    }
}

// //////////////////////////////////////////////////////////////////////
// Das Hauptprogramm: Startpunkt 
int main (void)
{
    // LED-Port auf OUT
    DDR_LED  |= (1 << PAD_LED);

    // Timer1 initialisieren
    timer1_init();

    // Interrupts aktivieren
    sei();

    // Endlosschleife
    // Die LED ist jeweils 1 Sekunde an und 1 Sekunde aus,
    // blinkt also mit einer Frequenz von 0.5 Hz
    while (1)
    {
        // LED an
        PORT_LED |= (1 << PAD_LED);

        // 1 Sekunde warten
        wait_10ms (100);

        // LED aus
        PORT_LED &= ~(1 << PAD_LED);

        // 1 Sekunde warten
        wait_10ms (100);
    }

    // main braucht keine return-Anweisung, weil wir nie hier hin kommen
}

Von der C-Quelle zum hex-File

Das Übersetzen über Kommandozeilen-Eingaben erledigen 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 Datei

blinky.c

Compilieren

Zunächst wird die C-Datei ü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 Namen der Ausgabedatei an. Ohne diese Option heißt 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.
-DF_CPU=xxx
optional, falls der Controller nicht mit 1 MHz läuft. xxx ist die Taktfrequenz in Hertz.
> avr-gcc blinky.c -c -o blinky.o -Os -g -mmcu=atmega8

Danach ist eine neue Datei entstanden (*.o)

blinky.c blinky.o

Linken

Die erzeugte Objek-Datei wird nun zur Ausgabedatei (*.elf) gelinkt:

> avr-gcc blinky.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

Umwandeln nach hex

Code und Daten

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

EEPROM

Ein hex-File, das den Inhalt des EEPROMs wiederspiegelt, erhält man mit

> avr-objcopy -j .eeprom --change-section-lma .eeprom=0 -O ihex blinky.elf blinky_eeprom.hex

Für das Beispiel ist das EEPROM-File blinky_eeprom.hex leer, da wir keine Daten ins EEPROM gelegt haben.

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:

00000072 <wait_10ms>:

// //////////////////////////////////////////////////////////////////////
// Wartet etwa t*10 ms.
// timer_10ms wird alle 10ms in der Timer1-ISR erniedrigt.
// Weil es bis zum nächsten IRQ nicht länger als 10ms dauert,
// wartet diese Funktion zwischen (t-1)*10 ms und t*10 ms.
void wait_10ms (const uint8_t t)
{
    timer_10ms = t;
  72:	80 93 61 00 	sts	0x0061, r24
    while (timer_10ms);
  76:	80 91 61 00 	lds	r24, 0x0061
  7a:	88 23       	and	r24, r24
  7c:	e1 f7       	brne	.-8      	; 0x76 <wait_10ms+0x4>
  7e:	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 oder die Zieladressen von Sprüngen, wie bei dem brne-Befehl an Adresse 7c, der gegebenenfalls 8 Bytes zurückspringt und dann an Adresse 76 landet.

Mapfile erstellen

Ein Mapfile gibt Auskunft darüber, an welcher Adresse Code und Objekte landen. Erstellt wird das Mapfile während des Linkens, indem avr-gcc ein Option an den Linker weiterreicht, die diesem zum Erstellen eines solchen Files veranlasst:

> avr-gcc blinky.o -o blinky.elf ... -Wl,-Map,blinky.map

Dadurch entsteht das Mapfile blinky.map.

Die Größe ermitteln

Die Größe der erhaltenen Objekte/Files können mit avr-size ausgegeben werden.

> avr-size -x blinky.o

druckt aus:

  text    data     bss     dec     hex filename
  0x7c     0x0     0x2     126      7e blinky.o

Das sind die Größen der einzelnen Sections. 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 126+0=126 Bytes belegt, und im SRAM 0+2=2 Bytes.

Ab binutils 2.16 gibt es für avr-size die Option -C, mit der man eine Zusammenfassung des ganzen Programms ausgeben kann:

> avr-size -C --mcu=atmega8 blinky.elf

druckt aus:

AVR Memory Usage
----------------
Device: atmega8

Program:     216 bytes (2.6% Full)
(.text + .data + .bootloader)

Data:          2 bytes (0.2% Full)
(.data + .bss + .noinit)

Die Gesamtgrösse ergibt sich erst aus dem elf-File, denn auch die Vektortabelle und der Startup-Code belegen Platz. Hier eine Auflistung der beteiligten Sections:

> avr-size -x -A blinky.elf

druckt aus:

blinky.elf  :
section            size       addr
.text              0xd8        0x0
.data               0x0   0x800060
.bss                0x2   0x800060
.noinit             0x0   0x800062
.eeprom             0x0   0x810000
.stab             0x36c        0x0
.stabstr           0x84        0x0
.debug_aranges     0x14        0x0
.debug_pubnames    0x48        0x0
.debug_info        0xfc        0x0
.debug_abbrev      0xa2        0x0
.debug_line       0x101        0x0
.debug_str         0xa6        0x0
Total             0x86b

Im Flash werden somit 0xd8+0=216 Bytes belegt, also 90 Bytes mehr als das Object benötigt; davon entfallen z.B. schon 38 Bytes auf die Vektortabelle des ATmega8, die 2*19 Bytes groß ist.

Die Größen einzelner Funktionen/Variablen lassen sich anzeigen mit

> avr-nm --size-sort -S blinky.elf

was nach Größe sortiert ausdruckt:

00800060 00000001 b interrupt_num_10ms.0
00800061 00000001 b timer_10ms
00000072 0000000e T wait_10ms
0000005c 00000016 T timer1_init
000000bc 0000001c T main
00000080 0000003c T __vector_6

HEX-Dateien zu diesem C-Code

Zu dieser C-Datei gibt es HEX-Dateien für einige AVRs. Siehe dazu HEX Beispiel-Dateien für AVR.

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ültigkeit von Defines durch den Präprozessor

Siehe auch


LiFePO4 Speicher Test