Im dritten und letzten Teil dieser Reihe möchte ich auf eine weitere Realisierungsmöglichkeit für einen Bootloader eingehen. Der Unterschied bei dieser Variante besteht darin, dass der Bootloader kein eigenständiges Firmware-Projekt ist, sondern in der Hauptapplikation integriert wird. Hierdurch erhält die Applikation eine Möglichkeit ein Update von sich selbst durchzuführen, ohne das andere Funktionen der Firmware dadurch (vollständig) unterbrochen werden müssen. Wer aber vorher noch einmal die ersten beiden Teile lesen möchte, findet diese hier:
Problembeschreibung
Stellen wir uns eine Mikrocontrollerfirmware vor, welche über ein User-Interface mit einem Benutzer interagieren kann. Außerdem ist die Firmware für das Auslesen von Sensoren und die Steuerung von Aktoren zuständig. Die Firmware kann eigenständig Softwareupdates von einem Server herunterladen und installieren. Das Problem: Bei einem klassischen Update müsste die Firmware zunächst durch den Nutzer in einen Updatemodus gebracht werden, in dem dann mit dem Bootloader die neue Firmware aufgespielt wird. Das hat unter anderem folgende Nachteile:
- Der Nutzer kann während dem Update nicht auf die übrigen Funktionen der Firmware zugreifen.
- Der Bootloader braucht ein umfangreiches Wissen über die Hardware, sodass er alle Aktoren in einem sicheren Zustand halten kann.
- Ggf. muss der Bootloader einen komplexen Kommunikationsstack beherrschen (in diesem Fall WLAN). Hierdurch benötigt der Bootloader nicht nur viel Speicher, sondern wird auch Fehleranfällig. Somit können auch Updates des Bootloaders erforderlich sein.
- Da die Anwendungssoftware diesen Kommunikationsstack in der Regel ebenfalls benötigt, ist dieser immer doppelt vorhanden. Wird der Stack für die Applikation angepasst, muss dies entweder für den Bootloader passieren, oder die Host-Software muss 2 verschiedene Varianten des Protokolls unterstützen.
- Läuft beim Update etwas schief, ist die Firmware nicht mehr zu gebrauchen, bis ein neues Update aufgespielt wird.
In der Regel ist der erste Punkt besonders problematisch. Lange Updatezeiten können für den Nutzer frustrierend sein. Es kann daher von Vorteil sein das Update der Firmware im Hintergrund auszuführen, ohne dass der Nutzer dies mitbekommt.
Lösung
Wie beschrieben wäre wünschenswert das die Firmware sich selber auf den neusten Stand bringt, ohne das deren übrige Funktionen dabei beeinträchtigt werden. Dabei gibt es allerdings folgende Probleme:
- Eine Firmware die auf einem Flashspeicher läuft, kann sich nicht selbst überschreiben.
- So lange der Flash beschrieben wird, kommt es in der Regel zu CPU-stalling. Die CPU wartet also, bis der Schreibvorgang beendet ist, bevor der nächste Befehl vom Flash geladen wird. Die Applikation läuft dadurch deutlich langsamer.
Um dies zu lösen haben einige Mikrocontroller Hersteller (Beispiele sind unter anderem Microchip und ST) eine breite Palette an Mikrocontrollern herausgebracht, deren Flash Speicher aus zwei symmetrischen Flash Bänken besteht. Dies hat den Vorteil, dass die CPU gleichzeitig Befehle von einer Bank ausführen kann, während die andere Bank beschrieben wird.
Physikalische und virtuelle Adressen
Um zu verstehen, wie das Update nun funktioniert muss man zunächst wissen, dass es einen Unterschied zwischen physikalischen und virtuellen Adressen gibt. Grundsätzlich haben beide Flash Bänke, so wie auch RAM und die Register des Controllers, ihren eigenen physikalischen Adressbereich. Diese einzelnen physikalischen Adressen werden allerdings virtuell auf einen größeren Gesamtbereich gemapped. Das bedeutet, dass die Software in der Regel nicht die physikalische Adresse des Flashs kennt, sondern lediglich auf die virtuellen Adressen zugreift. Über die virtuelle Adresse kann die SW dann direkt auf Register, RAM oder eben eine der beiden Flash Bänke zugreifen. In der Regel werden die Flash Bänke dabei nach dem folgenden Schema gemapped:
Um den virtuellen Adressbereich voll auszuschöpfen, können wir nun entweder eine Firmware schreiben, die so groß ist, dass sie sich auf den vollen virtuellen Programmspeicher erstreckt. Der Linker kennt beim Erstellen der Firmware lediglich die virtuellen Adressen, so dass in diesem Fall vom Entwickler keine Wissen, über die Aufteilung der Flash Bänke erforderlich ist. Alternativ können wir aber auch unsere Firmware auf den halben virtuellen Programmspeicher beschränken. So wird sichergestellt, dass Bank 2 immer frei bleibt und dieser Bereich für Updates zur Verfügung steht. Nach einem Update kann dann das Mapping einfach auf folgendes Schema geändert werden:
So wird nach einem Reset immer die Firmware auf Bank 2 gestartet. Auf Bank 1 verbleibt die alte Firmware, auch wenn diese nicht mehr angesprungen wird.
Der Updatevorgang
Wie sieht aber nun der Updatevorgang aus? Im Grunde wie jeder andere, die Updatesoftware, welche Teil der Hauptapplikation ist, muss aber sicherstellen, sie sich nicht selbst löscht und ausschließlich auf die Flash Bank schreibt, von der Sie selber nicht ausgeführt wird. Das kann man recht einfach erreichen, indem man die Adresse beim Schreiben auf den Flash, immer um die halbe virtuelle Adresse inkrementiert. Programmdaten die eigentlich an Adresse null gehören, werden so auf Adresse 0 + halber Programmspeicher (und damit auf den Anfang der zweiten Flash Bank) geschrieben. Ist der Programmspeicher so gemapped das Bank 1 auf virtueller Adresse 0 liegt, wird dann immer auf Bank 2 geschrieben. Ist Bank 2 auf Adresse virtueller Adresse 0, wird immer auf Bank 1 geschrieben.
Durch die Aufteilung auf zwei Flash Bänke, kann eine Firmware, welche auf einer der Flash Bänke läuft, ein Update der anderen Flash Bank ausführen ohne dass es zu CPU-stalling kommt. Hierdurch kann das Update im Hintergrund erfolgen und die Firmware kann parallel ihre übrigen Tasks weiterhin bearbeiten. Vor allem, wenn ein Betriebssystem verwendet wird, und der Updateprozess in einem niederprioren Thread läuft, werden die übrigen Funktionen kaum beeinträchtigt.
Ist das Firmwareupdate abgeschlossen, kann die Applikation die beiden Flash Bänke ummappen, und durch einen Sprung an den Anfang des virtuellen Adressbereichs die neue Firmware starten.
Brauche ich trotzdem einen klassischen Bootloader?
Im Grunde wird durch das oben beschriebene Vorgehen der klassische Bootloader nicht vollständig ersetzt. Es fehlt zum Beispiel eine Überprüfung des Applikationsspeichers. Außerdem wird das Mapping der Flash Bänke – je nach Controller – nach einem Reset wieder zurückgesetzt. Es wird also ggf. ein Stück Code benötigt, welche die Entscheidung trifft welche Flash Bank angesprungen werden soll. Entsprechend muss das Mapping der Adressbereiche vom Bootloader vorgenommen werden:
Schauen wir uns zunächst den PIC32MZ an. Dieser hat neben dem Programmspeicher einen sogenannten Bootflash. Dieser ist unter anderem für den Bootloader reserviert. Bei einem Reset startet der PIC32MZ immer aus dem Bootflash. Zwar ist beim PIC32MZ ein Bootloader im Bootflash vorinstalliert, dieser führt allerdings immer einen Sprung in Bank 1 aus. Bei dieser Controllerfamilie muss also der Standard Bootloader (welcher von Microchip als SW-Projekt zur Verfügung gestellt wird) entsprechend angepasst werden.
Bei „dual-flash“ Varianten des STM32 sieht das Ganze etwas anders aus. Hier befindet sich ein vorinstallierter Bootloader im sogenannten „System Memory“. Dieser Bootloader entscheidet auf Grundlage des BFB2 („Boot from Bank 2“) Bits in den „Option Bytes“ darüber ob Bank 1 oder Bank 2 angesprungen werden soll. Die Option Bytes sind ebenfalls im Flash, werden also bei einem Reset nicht zurückgesetzt. Ist das BFB2 Bit gesetzt startet der Mikrocontroller nach einem reset zunächst in den vorinstallierten Bootloader, welcher dann wiederum die Applikation in Bank 2 startet. Ist das Bit gelöscht wird der Bootloader beim Start nicht durchlaufen und stattdessen die Applikation auf Bank 1 ausgeführt.
Man kann also bei Dual-Flash Varianten des STM32 auf einen Bootloader verzichten. Voraussetzung hierfür ist allerdings das nach einem Update auch eine Verifizierung des Speicherbereichs durchgeführt wird – man also kontrolliert, ob das Update erfolgreich war – bevor man das BFB2 Bit ändert. Zwar prüft beim ST auch der vorinstallierte Bootloader laut Application Note AN2606, ob Bank 2 „valid Code“ enthältBei dieser Überprüfung handelt es sich aber nicht um eine Check Summen-Überprüfung oder ähnliches. Anstatt dessen steht im Reference Manual des STM32L0X2 (RM0376): „The code is considered as valid when the first data located at the bank start address (which should be the stack pointer) points to a valid address (stack top address).“ Wurde das Update also fehlerhaft durchgeführt oder wird der Speicher nach erfolgreichem Update beschädigt, erkennt der Bootloader das nicht. Hinzukommt, wenn ein Update von Bank 2 auf Bank 1 durchgeführt wird, so dass hinterher der aktuelle Code in Bank 1 liegt, beim Start niemals eine Überprüfung durchgeführt wird.
Ich muss also wie gesagt überprüfen, ob ich das Update korrekt durchgeführt habe, bevor ich das BFB2 Bit ändere. Das verhindert zumindest das direkt nach dem Update eine defekte FW angesprungen wird.
Fazit
Mikrocontroller mit 2 symmetrischen Flash Bänken ermöglichen intelligente Updatemethoden. Durch die zusätzliche Flash Bank kann eine Firmware ein Update ihrer selbst durchführen, ohne dabei Ihre übrigen Funktionen einstellen zu müssen.
Ein weiteres interessantes Plus dieser Technik: Ich habe für den Fall, dass die Firmware auf einer der beiden Flash Bänke im Feld beschädigt wird, immer noch eine ältere lauffähige Firmware als Ersatz gespeichert. Das heißt, selbst wenn Teile des Flashs beschädigt werden, stellt das System seine Arbeit nicht vollständig ein, sofern ich in der Lage bin diesen Defekt zu erkennen.
Man sollte allerdings erwähnen, dass das Ganze auch einen Nachteil hat. Ich muss mein System immer so auslegen das mein Mikrocontroller über den doppelten Flash Speicher verfügt, als meine Firmware mit einem klassischen Updateprozess benötigen würde. Diesen Speicher muss ich natürlich bezahlen.