Bootloader Tutorial, Teil 1: Grundlagen

Wer Mikrocontroller-Firmware im Feld hat, ist in der Regel darauf angewiesen diese updaten zu können. Viele gängigen Mikrocontroller haben vorinstallierte Bootloader, die zu diesem Zweck verwendet werden können. Oft reichen diese aber für die eigenen Zwecke nicht aus. Manchmal ist der Bootloader nicht flexibel genug, manchmal reicht die Performance nicht aus, manchmal wird ein höheres Maß an Zuverlässigkeit benötigt. Außerdem kann es vorkommen, dass die benötigte Hardware Peripherie (GPIOs oder Kommunikationsschnittstellen) nicht zugänglich ist. Auch kann es sein, dass die eigene Applikationsfirmware, während dem Update weiterlaufen muss.

Es kann also durchaus sinnvoll sein, sich nicht mit dem vorinstalliertem Bootloader zufriedenzugeben, sondern stattdessen seinen eigenen Code zu verfassen. In diesem Teil der Reihe werde ich auf die nötigen Grundlagen eingehen: Was muss ein Bootloader tun und wie wird der Code im Speicher platziert.

Da das Schreiben und Löschen des Flashs stark hardwareabhängig und das Protokoll zum Senden des Updatefiles stark von der Anwendung abhängig ist, werde ich an dieser Stelle nicht darauf eingehen und versuchen, mich auf die allgemeingültigen Grundlagen zu beschränken.

Wohin mit dem Code?

Fangen wir mit den Grundlagen an. Wer an einer Update-Funktion für Mikrocontroller-Firmware arbeitet, muss sich zwangsläufig mit dem Flash-Speicher beschäftigen. Dieser setzt sich in der Regel zusammen aus Pages, Sectors, Blocks und Banks (ich verwende hier bewusst die englischen Begriffe, da die deutschen Begriffe im Alltag kaum angewendet werden). Wie genau diese Komponenten geschrieben, gelesen und gelöscht werden können, unterscheidet sich in der Regel stark zwischen den Mikrocontroller-Herstellern. Bei einigen Herstellern – wie zum Beispiel ST – unterscheiden sich sogar die einzelnen Controller-Familien stark voneinander. Entscheidend ist hier vor allem die kleinste löschbare Einheit.

Nehmen wir als Beispiel den STM32L072RB. Hier lässt sich der Speicher (EEPROM ausgenommen) Wort-, Halbwort- und Byteweise lesen, allerdings nur Wort- und Halbwortweise schreiben. Die kleinste löschbare Einheit ist eine Page mit 128 Bytes. Wer einen Bootloader für diesen Mikrocontroller schreiben will, muss daher folgendes beachten:

  1. Teile von Bootloader und Hauptapplikation können nicht in einer gemeinsamen Page liegen, da der Bootloader ansonsten diesen Teil der Hauptapplikation niemals löschen könnte, ohne einen Teil von sich selber zu löschen. Es bietet sich daher an, die Startadresse der Hauptapplikation, an den Anfang einer Page zu setzen.
  2. Für den Bootloader müssen immer mindestens n-Pages reserviert werden.

Die folgende Abbildung zeigt eine mögliche Speicherverteilung. Bei der physikalischen Adresse 0x0800 0000 handelt es sich beim erwähnten STM32L072RB um die niedrigste Adresse des Programmspeichers. In der Regel wird der Beginn des Programmspeichers nach einem Reset zuerst angesprungen. Der Bootloader wird also nach einem Reset zuerst durchlaufen. Ist eine valide (siehe „Absicherung der Firmware“) enthalten, kann diese angesprungen werden. Die Platzierung der Hauptapplikation ist im Prinzip beliebig. Um den Platz möglichst effizient auszunutzen, empfiehlt es sich allerdings diese an den Anfang einer Page zu setzen.

Auch wenn das die Regel ist, muss der Bootloader nicht zwingend am Anfang des Speichers liegen. Prinzipiell kann die Hauptapplikation nach Belieben an eine andere Adresse springen, an der sich der Bootloader befindet. Hierbei ist jedoch zu beachten, dass bei einer defekten Hauptapplikation der Bootloader nicht mehr angesprungen werden kann. Schlägt also ein Update fehl, ist kein weiteres Update über den Bootloader möglich.

Ihr Ansprechpartner:

M.Sc. Björn Schmitz, Software Entwickler
E-Mail: schmitz@medtech-ingenieur.de
Tel.:  +49 9131 691 240
 

Benötigen Sie Unterstützung bei der Entwicklung Ihres Medizingeräts? Wir helfen gerne! Die MEDtech Ingenieur GmbH bietet Hardware-Entwicklung, Software-Entwicklung, Systems Engineering, Mechanik-Entwicklung und Beratung aus einer Hand. Nehmen Sie Kontakt mit uns auf.

Kontakt aufnehmen

Wird die Firmware im Speicher platziert?

Das Platzieren der einzelnen Firmwarekomponenten im Speicher übernimmt der Linker. Um zu wissen, welche Programmteile, welchen Adressen zugeordnet werden sollen, und wie viel Speicher überhaupt zur Verfügung steht, benötigt dieser ein Linkerscript. Handelt es sich bei Bootloader und Hauptapplikation um eigenständige Firmwareprojekte benötigen beide ein eigenes Linker File. Diese müssen aufeinander abgestimmt werden, um sicherzustellen, dass beide Komponenten wie gewünscht zusammenarbeiten. Wichtig ist dabei vor allem die Zeile in der Flash-Größe und Startadresse angegeben wird. Dabei muss die Startadresse des Linkers, außerhalb des Bootlaoder liegen (Startadresse_Hauptapplikation > (Startadresse_Bootlaoder + Länge_Bootloader)). Die folgende Abbildung zeigt einen Vergleich der beiden Linker Skripte. Wie zu sehen ist, müssen sich die beiden Skripte im Prinzip nur in einer Zeile unterscheiden.

Absicherung der Firmware

Nach dem Flashen der Hauptapplikation kann diese vom Bootloader ausgeführt werden. Was passiert aber, wenn während einem Update-Vorgang die Spannungsversorgung oder die Kommunikationsschnittstelle unterbrochen wurde? In diesem Fall befindet sich eine unvollständige Firmware im Speicher, welche sich voraussichtlich nicht wie gewollt verhält. Um dies auszuschließen, sollte der Bootloader vor jedem Sprung in die Hauptapplikation, eine Überprüfung der Hauptapplikation durchführen. Hierfür eignet sich z. B. eine CRC (zyklische Redundanzprüfung) die an eine bestimmte Adresse außerhalb von Bootloader und Hauptapplikation abgelegt wird. Als Speicherort kann z. B. eine Adresse am Ende des Programmspeichers gewählt werden. Die CRC muss dann vor jedem Update gelöscht werden. Nach einem Update berechnet der Bootloader dann eine CRC über den kompletten Speicherbereich der Hauptapplikation. Anschließend sollte diese ebenfalls auf dem Flash (oder im EEPROM falls vorhanden) abgelegt werden. Die folgende Abbildung zeigt ein mögliches Verhalten eines Bootloaders.

Wie startet man nun den Bootloader

Im Flussdiagramm des vorangegangenen Kapitels wurde eine „Bedingung für Update“ erwähnt. Diese ist zwingend nötig, wenn der Bootloader direkt nach dem Reset durchlaufen wird, um dem Bootloader mitzuteilen, dass er nicht in die Hauptapplikation springen darf, selbst wenn diese vorhanden ist. Oft wird in vorinstallierten Bootloadern dafür ein Mikrocontroller-Pin genutzt, welcher beim Reset einen bestimmten Pegel haben muss, damit der Bootloader in den Update-Modus geht. Dies ist für viele Anwendungen in Ordnung, oft aber nicht praktikabel, vor allem dann, wenn kein Hostcontroller im System vorhanden ist, welcher die Kontrolle über Bootloader- und Resetleitung hat.

Bei den Projekten von MEDtech Ingenieur haben sich Marker im nicht flüchtigen Speicherbereich als sinnvolle Alternative erwiesen. Hierbei handelt es sich um einen Wert, welcher vor Start des Updates an eine bestimmte Adresse in Flash oder EEPROM geschrieben wird. Der Update-Vorgang könnte z. B. folgendermaßen ablaufen:

  1. Im Initialzustand befindet sich nur der Bootloader im Flash. Sowohl Update Marker als auch CRC sind nicht vorhanden. Dies hat zur Folge, dass zwar die Bedingung für das Update nicht erfüllt ist, aber der Bootloader aufgrund der fehlerhaften CRC in den Update-Modus geht.
  2. Der Bootloader flasht die Hauptapplikation, speichert die CRC und führt diese aus.
  3. Der Hauptapplikation wird eine Nachricht übermittelt, welche den Update-Vorgang einleiten soll. Daraufhin speichert diese den Update Marker auf einem freien Flash Bereich und führt einen Softwarereset durch.
  4. Beim Neustart prüft der Bootloader den Update Marker. Ist dieser gesetzt, geht der Bootloader in den Update-Modus. Gleichzeitig wird der Update Marker zurückgesetzt. So wird sichergestellt, dass nach einem Timeout, oder durch einen Reset, ohne vorheriges Update, erneut die Hauptapplikation angesprungen werden kann.
  5. Schritt 2 und 3 wird wiederholt

Der Update-Marker muss dabei nicht zwangsläufig im nicht flüchtigen Speicher liegen. Wird in den beiden Linker Skripten eine RAM Region definiert, welche eine Variable enthält, die von beiden Firmwareprojekten genutzt wird, kann diese Variable den Update Marker ersetzen. Hierbei ist allerdings zu beachten, dass kein Softwarereset zum Übergang in den Bootloader verwendet worden kann. Das Starten des Bootloaders kann nur durch den Sprung an die entsprechende Adresse erreicht werden, wie sie im folgenden Kapitel beschrieben ist. Wichtig hierbei ist, dass alle Register des Mikrocontrollers vor dem Sprung in einen Zustand gesetzt werden, in dem der Bootloader uneingeschränkt arbeitsfähig ist.

Sprung in die Hauptapplikation

Nach dem erfolgreichen Durchführen eines Updates und nach bestandenem CRC-Check, soll in der Regel die Hauptapplikation ausgeführt werden. Dies erfolgt in der Regel durch einen Sprung in die Hauptapplikation. Im Prinzip ähnelt dieser Vorgang einem normalen Funktionsaufruf, es ist allerdings folgendes zu beachten:

  1. Während dem Sprung in die Hauptapplikation sollten keine Interrupts ausgelöst werden, aus diesem Grund sollten diese global deaktiviert werden.
  2. In der Regel wird nicht die eigentliche Startadresse der Hauptapplikation angesprungen, da sich hier für gewöhnlich der Interrupt Vector-table befindet (siehe die abgebildeten Linker Skripte weiter oben). Bei Arm Cortex-M CPUs ist dieser in der Regel 4 Bytes groß, sodass eine Adresse, vier Bytes größer als die Startadresse angesprungen wird.
  3. Der Main-Stackpointer muss an die Startadresse der Hauptapplikation gesetzt werden.
  4. Alle Mikrocontrollerperipherien (UART, ADC, GPIOs) sollten darauf geprüft werden, ob diese in den Initialzustand zurückversetzt werden müssen.

Die folgende Abbildung zeigt ein Beispiel für eine Sprungfunktion, mit der die Hauptapplikation ausgeführt werden kann. Die Funktion wurde für einen STM32 geschrieben, kann aber auf andere ARM Cortex-M basierten Mikrocontroller und mit leichten Änderungen auch auf weitere Architekturen übertragen werden.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* @breif Function jump to main application
*/
static void JumpToMainApplication(void)
{
    // function pointer that should point to main application
    void (*MainJump)(void);
 
    // set start address of main application
    volatile uint32_t addr = MAIN_APPLICATION_START_ADDRESS;
 
    /*
     * increment main application address by 4, this is the 
     * address behin the interrupt vector table where the 
     * actual code start
     */
    MainJump = (void (*) (void)) (*((uint32_t*)(addr+4)));
 
    // set stack main pointer
    __set_MSP(*(uint32_t *)addr);
 
    //jump to main application
    MainJump();
}

Ausblick

Im nächsten Teil der Serie werde ich das Konzept der Backup-Firmware vorstellen. Dieses wurde bei der Firma MEDtech Ingenieur entwickelt und dient zur Absicherung eines fehlgeschlagenen Updates mit dem vorinstallierten Standard Bootloader. Im dritten und letzten Teil werde ich das Konzept des Live-Updates vorstellen. Hierdurch wird es ermöglicht das die Firmware sich selber updatet, ohne dabei Ihren normalen Betrieb einzustellen.

Weiter zu Teil 2

Kontaktieren Sie uns!

Autor

  • Björn Schmitz

    Seit Juli 2017 gehöre ich zum MEDtech-Ingenieur Team und bin hier vor allem als Firmwareentwickler tätig. Schon in kürzester Zeit konnte ich an vielen spannenden Projekten aus dem Bereich Medizintechnik, aber auch aus anderen Bereichen mitwirken.

Auch interessant:

Requirements Engineering mit Enterprise Architect

Enterprise Architect
Dieser Blog-Artikel soll allen helfen, die Anforderungsspezifikationen immer noch mit Word und Excel erstellen. Ich selbst habe auch lange den "Marktführer" für Requirements Engineering benutzt und kann jedem nur raten auf ein Werkzeug umzusteigen. Doch warum sind Werkzeuge sinnvoll und was spricht gegen Word und Excel? Solange Sie kleine übersichtliche…
Getagged mit: , , ,

Schreibe einen Kommentar