Bootloader

Aus der Mikrocontroller.net Artikelsammlung, mit Beiträgen verschiedener Autoren (siehe Versionsgeschichte)
Wechseln zu: Navigation, Suche

Der ursprüngliche Weg, ein Programm in einen Microcomputer zu bringen, war, ein EPROM mit dem Programmcode zu brennen und es mit dem Bus des Controllers zu verbinden. Controller mit internem PROM, EPROM oder Flash-ROM besitzen meist eine dem EPROM ähnliche parallele Programmiermöglichkeit.

Moderne Controller besitzen einen Bootloader. Dies ist ein im Controller befindliches Programm, dessen Aufgabe es ist, das eigentliche Programm in den Speicher zu laden. Bootloader gibt es in vielfältiger Ausprägung. Zumeist ist es ein fest im Controller integriertes Programm wie beim C166 oder 68HC11. Dieses ermöglicht das Laden des Programms über die serielle Schnittstelle. Speziell bei diesen beiden Controllern muss sich der zu programmierende Speicher nicht im Controller selber befinden. Neu ist die Möglichkeit, auch den Bootloader im Flash selbst zu programmieren. Diese Möglichkeit bieten z. B. die ATmegas.

Beispiel

PIC18, C-Implementierung, Stichpunkte

Überblick

Für gewöhnlich wird das Binary (es kann einem Intelhexfile oder einem Motorola S-Recordfile entnommen werden) über eine physische Schnittstelle vom PC auf den µC übertragen. Als physische Schnittstelle kommen z.B. RS232 (alias UART, USART, EUSART auch COM), USB oder I2C in Betracht. Diese Schnittstellen sind als Module direkt auf dem µC-Chip implementiert. Das Flashtool (PIC-Programmer) wird nur zum Aufspielen des Bootloaders selbst verwendet. Das Nutzprogramm (Firmware) spielt später der Benutzer auf, der kein Flashtool besitzt. Bei der Übertragung wird das Nutzprogramm in kleinen Stücken übertragen, da es nicht als Ganzes in den RAM des µC passt. Auf dem µC wird es blockweise im Flash-Speicher abgespeichert. Grund ist, dass der Flash das Speichern nur blockweise, das Lesen aber byteweise ermöglicht. Der Bootloader sollte auch das Rücklesen des Flash-Inhaltes ermöglichen um Übertragungsfehler auszuschließen. Der Bootloader benötigt zwei Modi: 1) Programmieren, 2) Nutzprogramm. Zum Starten wird der Programmzähler auf den Start im Flash-Speicher gesetzt wo sich der Interrruptvektor befindet.

Literatur

  • [1] µC Handbuch eines spezifischen PIC18
  • [2] Application Note AN851 für Assembler

Übertragungsprotokoll

Der Nachteil einer C-Implementierung ist ihr hoher Flash-Speicherverbrauch. Vorschlag: Für Kommandos nur ein ASCII-Zeichen verwenden. Das Nutzprogramm sollte als Roh-Bytes in 64-Byte-Blöcken übertragen werden, was der nutzbaren Flash-Block-Größe entspricht. Der halbe Block am Ende des Programmes wird vom PC mit 0xFF-Bytes aufgefüllt. Nach dem Senden von 64Byte Nutzdaten wartet der PC bis der Bootloader ein Antwortbyte gesendet hat um sicherzustellen dass vorher der zeitverbrauchende Speichervorgang abgeschlossen ist.

Speicherorganisation

Beginnend auf Adresse 0x0000 des Flash befindet sich ein schützbarer Speicherbereich, in dem man den Bootloader ablegen kann. Er hat eine Größe von 500 Byte bis 2 kByte. Auf der Adresse 0x0000 befindet sich der Interruptvektor mit den Startadressen:

0x0000 _entry_scn ... Init des Programmes und Aufruf von main() aus der Datei c:\Programme\Microchip\...\c018i.c

0x0008 High priority Interrupt.

0x0018 Low priority Interrupt.

Zunächst befinden sich Bootloader und Nutzprogramm auf Adresse 0x0000. Das Nutzprogramm wird im Linkerfile (z.B. auf Adresse 0x0800) verschoben. Somit beginnt der Interruptvektor des Nutzprogrammes im Beispiel auf 0x0800. Der µC legt den Programmzähler bei jedem Interrupt auf 0x0000 und folgende nachdem er die Rücksprungadresse auf dem Hardwarestack abgelegt hat. Der Interruptvektor auf Adresse 0x0000 ist im µC fest verdrahtet und kann nicht überschrieben werden. 0x0800 ist für Interrupts im µC nicht vorgesehen. Somit müssen Interrupts von 0x0008 auf 0x0808 und von 0x0018 auf 0x0818 bei jedem Aufruf weitergeitet werden. Man spricht von Remapping. Einmalig bei Booten des µC muss entschieden werden, ob das Flash-Programm oder das Nutzprogramm ausgeführt wird. Das Flash-Programm kann defaultmäßig starten und beim Vorliegen eines Bootloader-Flags den Programcounter auf 0x0800 setzen.

Mit

#pragma config ... 

werden mit dem PIC KIT 3 Progammer wesentliche Voreinstellungen im µC abgespeichert. Diese Einstellungen befinden sich nicht im INTEL HEX File. Bei einem Programm ohne Bootloader stehen diese Pragmas im Hauptprogramm, z.B. main.c. Im Falle des Bootloaders werden die angesprochenen Pragmas aus dem Hauptprogramm bei der Erstellung des Hex-Files ignoriert. Sie müssen aus dem Hauptprogramm entfernt werden und in den Bootloader übernommen werden.

Das Bootloader-Flag

Das Bootloader-Flag entscheidet ob das Flash-Programm oder das Nutzprogramm gestartet wird. Es wird während der Laufzeit des Bootloaders sowie zur Laufzeit des Nutzprogrammes dann abgefragt, wenn der Bootloader Interrupts benötigt und damit eine Interruptverzweigung notwendig wird. Datenübertragung über RS232 benötigt Interrupts, Datenübertragung über USB kommt mit Polling aus. Das Bootloaderflag ist im Code des Bootloaders definiert. Wenn es abgefragt wird, so wird in diesem Moment Bootloadercode ausgeführt.

Ein Bootloader-Flag im RAM muss mit Hilfe des Linkerfiles vor Zugriffen aus dem Nutzprogramm geschützt werden. Falls überhaupt möglich so muss man beachten, dass die beiden Linkerfiles von Flashprogramm und Nutzprogramm konkurierend Orte im RAM für globale Variablen definieren. Es werden auch Speicherorte für Variablen innerhalb von Funktionen dort definiert wobei diese Variablen nicht statisch deklariert worden sein müssen. Weiter legt der Linker Speicherorte für Funktionsübergabeparameter im RAM fest.

Ein Bootloader-Flag im EEPROM kann auf einem definierten Speicherort abgelegt werden. Somit treten keine ungewollten Schreibvorgänge auf. Es kann bei jeder Interruptverzweigung gelesen werden, sollte nur beim Flashen geändert werden nicht aber bei jedem Booten. Beim Flashen des Bootloaders selbst wird der EEPROM mit dem Byte 0xff initialisiert. Dieses Zeichen sollte den Bootloadermodus darstellen. Ohne weitere Schutzmaßnahmen kann es vorkommen, dass das Nutzprogramm nicht in den Bootmodus springt weil es kaputt ist. Somit wäre der Bootloader ohne Flashtool nicht erreichbar. Um das zu vermeiden sollte das Nutzprogramm vor dem Rückschalten auf den Nutzermodus zur Kontrolle rückgelesen werden.

Weitere Möglichkeiten um die beiden Modi zu unterscheiden wären das Auswerten der Rücksprungadresse auf dem HW-Stack sowie ein HW-Bootloader-Jumper.

Interrupt Verzweigung

Eigentlich wird angestrebt, dass der Bootloader keine Interrupts abarbeitet. Interruptroutinen im Bootloader können jedoch für das Entgegennehmen des zu ladenden Programmes über eine Schnittstelle (z.B. UART) notwendig werden. Dabei springt man vom Interruptvektor nicht direkt zur ISR sondern zu einer Interruptweiche die dann entweder zum Bootloader oder zum Nutzerprogramm verzweigt. Die ISR braucht für die Registersicherung und für den Rücksprung eine zusätzliche Instrumentierung welche durch das Pragma "interrupt" oder "interruptlow" ausgelöst wird.

  • Für die hochpriore ISR genügt der Sprungbefehl in der Weiche und das Pragma der ISR. In der Weiche kann ein einfaches Flag verglichen werden.
  • Die niedrigpriore Weiche sollte vermieden werden. Falls nicht möglich so führt bei der ISR mit dem Pragma ein Flagvergleich als Kriterium in der Weiche zunächst zu einem Fehler im Nutzerprogramm. Ursache ist die geringere Instrumentierung im Pragma inerruptlow. In der Weiche sind zusätzlich die Register STATUS, WREG und BSR durch Assemblercode zu sichern und vor dem Sprung in die ISR wiederherzustellen. Das Sichern der Register kann in globalen Variablen erfolgen. Die Variablen müssen im Bootloader und im Nutzerprogramm auf festen Adressen angelegt werden um konkurrierenden Zugriff durch andere Speicherinhalte im gemeinsamen RAM zu vermeiden. Diese Variante wird hier erstmal nicht wiedergegeben.

Die Interrupt-Weiterleitung mit Interruptverzweigung (1- Konzept mit schlechter Performance)

Dieses Konzept hat durch den Unterprogrammaufruf (CALL) eine schlechte Performance. Für zeitkritische Interruptweiterleitung siehe nächsten Punkt Intterrupt-2.

Sprunganweisungen sind:

a) _asm goto INTHANDLER _endasm // Ohne Rücksprung.

b) _asm call INTHANDLER, 0 _endasm // Mit Rücksprung, entweder return oder RETFIE

Das Pragma

#pragma interrupt INTHANDLER

void INTHANLDER(void) {

   // Impl.

}

fügt zu der Funktion INTHANLDER zwei Dinge hinzu: Zum einen das Sichern eines Kontextes und zum anderen die Rückkehr mittels RETFIE, einem return das die während des Interrupts gesperrten Interruptsprünge wieder entsperrt. Während Interrupt-Ausführung sind neue Interrupts gesperrt um Störungen zu vermeiden. Diese Routine darf nicht mit "goto" sondern nur mit "call" verlassen werden. Sie darf nur einmal unmittelbar aus dem Interruptvektor aufgerufen werden: Es darf eine Routine mit diesem Pragma keine zweite Routine mit ebenfalls diesem Pragma aufrufen. Sonst würde die Interruptfreigabe vor dem Verwerfen des Kontextes der äußeren Routine erfolgen.

Somit sollte folgende Aufrufkette existieren (hier nur INTHIGH, das selbe wird für INTLOW benötigt):

// Bootloader

#pragma code INTVEC_HIGH=0x0008

void INTVEC_HIGH(void) {

 _asm goto INTHANDLER_HIGH _endasm

} ...

#pragma interrupt INTHANDLER_HIGH

void INTHANDLER_HIGH(void) {

 if(BOOTFLAG) // Vorsicht bei jeglichen Zwischenvariablen.
 {
   HandleRS232();
 }
 else
 {
   _asm call 0x0808, 0 _endasm  // Hier kein goto verwenden.
 }

} ...

// User

#pragma code INTVEC_HIGH_REMAP=0x0808

void INTVEC_HIGH_REMAP(void) {

 _asm goto INTHANDLER_HIGH_REMAP _endasm

} ...

// Kein #pragma interrupt, entfernen falls Nutzerprogramm hierher verschoben wurde.

void INTHANDLER_HIGH_REMAP(void) {

 // Hier Interruptbehandlung des Nutzerprogrammes.
 // Rückkehr zur Adresse von CALL.

}

Achtung, falls innerhalb der Interrupt-Service-Routine in den Flash geschrieben wird, darf in dieser Routine nicht mit GIEH=... oder GIEL=... die Interrupt-Sperre freigegeben werden. Neue Interrupts sind in der ISR gesperrt und werden nachher automatisch entsperrt. Zusätzliche Freigabe führt zu Störung des Stacks für Funktionsrücksprünge.

Die Interrupt-Weiterleitung mit Interruptverzweigung (2 - Konzept mit besserer Performance)

Die unter Punkt 1 genannte Interruptweiterleitung ruft mit CALL eine Unterfunktion auf. Dadurch wird viel Rechenzeit verbraucht. Interruptroutinen verwendet man oft um byteweise Daten abzuspeichern. Deshalb müssen Interruptroutinen aber gerade schnell arbeiten. Das Vorkommen von CALL im Assemblerlisting kann man sich z.B. in der MPLAB-IDE mit einer Linkeroption anzeigen lassen (Option "/i" für den Linker unter Projekt->Optionen->Linker). Es erfolgt die Ausgabe einer Datei mit der Endung "*.lst". Die Assemblerinstruktion CALL kommt vor bei:

  • Direktem Aufruf mit _asm / _endasm
  • Aufruf von C-Funktionen (z.B. void func(void);)
  • Versteckten Aufruf von stdlib-Funktionen (Modulo-Division: "%", Ganzzahl-Division: "/" u.a.)

Der µC hat zwar eine Multiplikationseinheit, er kann jedoch nicht dividieren.

Kommt innerhalb der Interruptroutine einmal oder mehrmals die CALL-Instruktion vor, wird selbst dann, wenn CALL auf einem nur selten ausgeführten Pfad liegt, stets eine Sicherung des Funktionskontextes vorgenommen (Register, Rücksprungadresse, Math-Bereich für Zwischenergebnisse). Dies benötigt mindestens 100-150 Instruktionstakte. Es sollte vermieden werden.

Konzept der High-ISR:

// Bootloader

#pragma code INTVEC_HIGH = 0x0008
void INTVEC_HIGH(void) {
  _asm goto INTHANDLER_HIGH _endasm
}

// Hier kein Pragma.
void INTHANLDER_HIGH(void) {
  // BootFlag ist eine Variable im RAM. Sie muss in Bootloader und Userprog auf der gleichen Adresse liegen.
  if(BootFlag) {
    _asm goto HandleRS232 _endasm
  } else {
    _asm goto 0x0808 _endasm
  }
}

#pragma interrupt HandleRS232
void HandleRS232(void) {
  ...
}

// Userprog

#pragma code INTVEC_HIGH_REMAP = 0x0808
void INTVEC_HIGH_REMAP(void){
  _asm goto INTHANDLER_HIGH_REMAP _endasm
}

#pragma interrupt INTHANDLER_HIGH_REMAP
void INTHANDLER_HIGH_REMAP(void) {
  ...
}

Bei dem obigen Listing erfolgt die Interruptweiterleitung ausschließlich über GOTO. Nach eine Kette von zwei GOTO-Anweisungen folgt die ISR. Innerhalb der Routinen mit dem GOTO wird kein Kontext und keine Rücksprungadresse gesichert. Ist die ISR schließlich abgearbeitet, erfolgt mit RETFIE ein Rücksprung zu jener Rücksprungadresse die vor Eintritt in den Interruptvektor hinterlegt wurde.

Das Kriterium für die Auswahl einer der beiden ISR ist das Bootflag. Es handelt sich um eine Variable im RAM. Bootloader und Userprog können sich überlagernde Adressen im RAM benutzen. Das Bootflag muss deshalb sowohl im Bootloader als auch im Userprog auf der selben RAM-Adresse angelegt werden. Die Adresse kann mit dem MAP-File überprüft werden.

// Als "grp0" ist eine existierende Ram-Bank aus dem Linkerfile einzutragen.
#pragma udata grp0=0x100
// Wird am Anfang der jeweiligen main()-Routine initialisiert.
volatile unsigned char BootFlag;
#pragma udata

Das Konzept der Low-ISR gleicht dem der High-ISR. Es kann jedoch nicht verzweigt werden. Bei Unterbrechung der Low-ISR durch die High-ISR ginge das Ergebnis der IF-Bedingung verloren. Die Weiterleitungsfunktion mit dem GOTO kann nicht das Interrupt-Pragma tragen. Ihr Kontext wird deshalb nicht gesichert. In der Low-ISR wird daher nur zu 0x0808 weitergeleitet. Low-Interrupts im Bootloader müssen mit IPEN=1 und GIEL=0 abgeschaltet werden.

In den Flash schreiben

Es werden zunächst Bytes in 64 Zwischenspeicherplätze geschrieben, die sogenannten Holding-Register. Danach erfolgt die Übernahme in den Flash mittels einer Instruktion. Dabei hält der µC die Programmausführung an. Die Vorgehensweise ist mit Beispielen im µC-Handbuch beschrieben. Mir ist unklar, warum PREINCREMENT verwendet wird beim Beschreiben der Holdingregister, eigentlich sollte es POSTINCREMENT sein.

Achtung, falls Flash-Schreiben innerhalb der Interrupt-Service-Routine erfolgt, darf nicht mit Freigabe der Interrupt-Sperre gearbeitet werden, siehe vorherigen Abschnitt über Interrupts.

Siehe auch

Artikelsammlung

AVR Bootloader in C - eine einfache Anleitung

Forum

http://www.mikrocontroller.net/topic/132026#1196880

Weblinks

AVR

ARM

MSP430

Manche µC von TI aus der MSP430-er Familie haben einen sog. Boot Strap Loader. TI hat die Beschreibung des BSL und Beispielcode offen gelegt. Damit kann man eigene Bootloader schreiben.

PIC

R8C

Die R8C enthalten ab Werk bereits einen proprietären, unveränderlichen(?) Bootloader. Zur Flash-Programmierung mit Hilfe dieses Bootloaders gibt es folgende Referenzen:

ZNEO

Sonstige

  • Der U-Boot (Universal Bootloader): "The 'U-Boot' Universal Bootloader project provides firmware with full source code under GPL. Many CPU architectures are supported: PowerPC(MPC5xx, MPC8xx, MPC82xx, MPC7xx, MPC74xx, 4xx), ARM(ARM7, ARM9, StrongARM, Xscale), MIPS(4Kc,5Kc), x86, Blackfin." -- U-Boot Homepage