Wie sieht eigentlich ein C-Programm für einen Microcontroller aus, wenn es durch den Compiler gelaufen ist? Wenn man spezielle Dinge vor hat, sollte man sich damit auskennen. Die Alternative ist natürlich ein Assembler-Programm für das "Spezielle" zu schreiben. Doch wer kann das schon?

Ein einfaches Programm für einen AVR-Microcontroller (hier ein ATtiny44) ist Basis für die Analyse. Es macht nicht mehr, als PB0 auf Output zu konfigurieren und ein den Pin auf High zu setzen.

#include <avr/io.h>

int main(void)
{ DDRB  |= _BV(PB0);  // Pin auf Output
  PORTB |= _BV(PB0);  // Einschalten
}

Nacdem dieses Programm im AVR-Studio 6.1 kompiliert wurde, kann man sich im Projektmappen-Explorer das Ergebnis anschauen:

Output Files

In diesem Thread unter www.mikrocontroller.net findet man viele Informationen.

Speicheraufbau

Für die Analyse muss die .lss-Datei angeschaut werden. Nach einer Beschreibung der einzelnen Section beginnt der interessante Teil mit "Disassembly of section .text". Eigentlich sollte man die wichtigen Teile ohne große Vorkenntnisse aus dem folgenden Assembler-Code erkennen können. Wer dennoch Unterstützung benötigt, findet sie im AVR-Assembler-Tutorial von Gerhard Schmidt.


Zu Beginn des Programmspeichers (Adresse 0, Label <__vectors>) befinden sich die Interruptvektoren. Beim AVR sind dies nicht einfache Adressangaben sondern Sprungbefehle. Dies hat den Vorteil, dass beim Eintritt eines Interrupts der Programcounter nur mit der Adresse des Interruptvektors geladen werden muss. Von dort erfolgt dann der Sprung zur Interrupt-Service-Routine (ISR).

Der erste Vektor ist immer der Reset-Vektor. Dieser wird bei jedem Reset angesprungen (natürlich auch beim Einschalten, Power-On-Reset). Die weiteren Vektoren folgen. Ist für einen Interrupt-Typ keine ISR hinterlegt, verweist der zugehörige Vektor auf die Marke <__bad_interrupt>. Von hier erfolgt ein Sprung zum Reset-Vektor. Es wird also ein Software-Reset ausgelöst.

Der Reset-Vektor verweist auf den Initialisierungsteil. Hier wird zunächst das Status-Register (SREG) und der Stackpointer belegt. Aus dem Programmspeicher werden die Werte für initialisierte globale Variablen an die entsprechende Stelle ins RAM übertragen, danach werden die uninitialisierten Variablen (BSS) auf Null gesetzt.

Sind alle Initialisierungen vorgenommen, erfolgt ein call auf main() und die eigentliche Applikation wird ausgeführt. Üblicherweise wird in main eine Endlos-Schleife ausgeführt. Ist dies nicht so (wie im Beispiel) erfolgt am Ende der Funktion ein return (im Zweifelsfall auch implizit). Dieser return führt zurück zum Initialisierungsteil. Von dort erfolgt ein Sprung zur Marke <_exit>. Hier werden die Interrupts ausgeschaltet (cli) und der Controller verharrt in einer Endlos-Schleife.

Werden weitere Funktionen oder ISRs eingebunden, werden diese in eine vom Linker bestimmte Reihenfolge nach <__bad_interrupt> und vor <_exit> eingebunden. Der Initialisierungsteil wird jedoch immer direkt nach den Interrupt-Vektoren eingefügt. Das Sprungkommando, das im Reset-Vektor abgelegt wird, ist somit für jeden Controller-Typ immer das gleiche, unabhängig vom Applikationsprogramm.

Dies ist eine wichtiger Umstand für den I²C-Bootloader, den ich an anderer Stelle beschrieben habe. Hier wird der Reset-Vektor so manipuliert, dass er auf den Bootloader-Code zeigt und somit nach jedem Reset ausgeführt wird. Am Ende des Bootloaders soll die Kontrolle an das Applikationsprogramm –genauer an den Initialisierungsteil– weiter gegeben werden. Dies ist beim AVR besonders einfach, weil –abhängig vom Controller-Typ– immer an die gleiche Adresse gesprungen werden kann.