Nach den Problemen mit dem Farbsensor wurde deutlich, dass ein Firmware-Update für einen unterstützenden Prozessor (z.B. für Sensorfunktionen) unumgänglich ist. Weil es häufig. nicht möglich ist, den Prozessor zum Update "frei zu legen", muss ein Firmware-Update im eingebauten Zustand möglich sein. Bei Applikationen für einen LEGO Mindstorms NXT steht dazu eigentlich nur das I²C-Interface zur Verfügung. Diese Schnittstelle wird sowieso zum Datenaustausch mit dem NXT genutzt.
Schwierigkeit 1: Von Hause aus unterstützt der ATtiny keinen Bootloader, wie dies z.B. die ATmega-Prozessoren
tun.
Schwierigkeit 2: Trotzt intensiver Bemühung von Google war kein passender Code zu finden.
Also: selbermachen!
Der Bootloader wurde in der Version V3 stark überarbeitet. Wesentliche Verbesserungen sind:
Das AS6-Projekt "Bootloader V3" zum Download. Die alte Version ist hier zu finden.
Das System besteht im Wesentlichen aus drei Komponenten:
Hinzu kommt ein Uploader, der das Laden der Nutz-Applikation übernimmt. Der typische Upload-Ablauf ist festgelegt.
Normalerweise übernimmt der in der GCC-Bibliothek enthaltene Start-Up-Code, der i.d.R. automatisch mit eingebunden wird, das Initialisieren und Starten des Programms. Für diesen Zweck ist er aber ungeeignet. Er ist a) zu lang, weil unnötige Schritte enthalten sind und b) liegt er am Anfang des Flash-Speicher (kleine Adressen). Dort soll aber später der Code der Nutz-Applikation liegen. Deshalb muss der Bootloader-Code an das Speicher-Ende verbannt werden. Die notwendige Programm-Initialisierung und die Speicherausrichtung lässt sich einfach mit ein paar Zeilen Assembler-Code erledigen. Da außerdem Assembler-Code unkompliziert im Atmel Studio 6 eingebunden werden kann, wurde diese Lösung gewählt. Damit die der Standard-Start-Up-Code nicht mit eingebunden wird, müssen dem Linker folgende Optionen übergeben werden: -nostartfiles -nodefaultlibs. Diese müssen unter "Projekt Eigenschaften -> Toolchain -> AVR/GNU C Linker -> Miscellaneous" manuell eingetragen werden. Auf Grund eines Bugs im Atmel Studio klappen die Checkboxen nicht, die eigentlich diese Funktion übernehmen sollen.
Der Code:
/*
* Main.S
*
* Project: Bootloader V3
* Created: 18.3.2013
* Author: Ulli
*/
;-----------------------------------------------------
; Code zur Initialisierung
; Ersetzt die Startsequenz bei 'normalen' C-Programmen
;-----------------------------------------------------
#include "GlobalConstants.h" ; Enthält die Startadresse des Bootloader-Codes
#include "avr\io.h" ; Enthält RAMEND
.section .init0 ; Damit es vor dem C-Code im Flash landet.
.ORG 0x0000
rjmp BootLoaderStart ; Power-On-Reset: Springe zum Bootloader
.ORG _VECTORS_SIZE ; Platz für die Interrupt-Vektoren lassen
;-------------------------------------------------------
Init: ; C-Programme landen hier
rjmp Init ; Endlosschleife, wenn kein Programm geladen ist
;ToDo: bei jedem Reset? Was ist bei Watchdog? ggf. per Konfiguration regeln!
;------------------------------------------------------
; Bootloader
;------------------------------------------------------
.ORG BOOTLOADERSTART, 0xFF ; Bis zum Bootloader mit 0xFF auffüllen.
; 0xFF ist der Wert einer nicht genutzten Flash-Zelle
BootLoaderStart:
;------------------------------------------------------
; Initialisierung des Stackpointer
;------------------------------------------------------
; Stackpointer auf letzte RAM-Adresse setzen
#ifdef SPH ; SPH gibt es nicht überall
ldi r24, RAMEND >> 8 ; High-Byte von RAMEND
out 0x3e, r24 ; SPH und SPL sind überall identisch (s. common.h).
#endif
ldi r24, RAMEND & 0xFF ; Low-Byte von RAMEND
out 0x3d, r24
eor r1, r1 ; r1 = 0; gcc möchte das so
rcall BootLoader ; Sprung zum BootLoader-Code (in C codiert)
rjmp Init ; Sprung ins geladene Programm.
; Diese Stelle wird nur dann erreicht,
; wenn der Bootloader erkennt, dass
; nichts geladen werden soll.
Der Code ist eigentlich selbsterklärend. Hier einige Hinweise:
Wenn eine Nutz-Applikation geladen ist, beginnt deren Code direkt nach den ISR-Vektoren (Label
'Init:
'). Hierfür sorgt der Compiler. Wenn der Bootloader nicht aktiviert werden soll,
ist es deshalb notwendig, dass diese Code-Stelle angesprungen wird. Die beim Flashen des Bootloaders
mitgelieferte "Nutz-Applikation" ist einfach eine Endlosschleife:
Init: ; C-Programme
landen hier
rjmp Init ;
Endlosschleife, wenn kein Programm geladen ist
Die .ORG
-Direktiven sorgen dafür, dass alle Komponenten
an der richtigen Stelle im Flash stehen.
Nach einem Power-On-Reset ist der Ablauf wie folgt:
rjmp BootLoaderStart.
BootLoaderStart
befindet sich bereits (ziemlich) am Ende Speichers.rcall BootLoader
return
verlassen. Der Code wird mit der nächsten Anweisung fortgesetzt. Dort steht
rjmp Init
und damit ein Sprung zur Nutz-Applikation.Damit ergibt sich folgende Speicheraufteilung:
Hinweis: Der Start-Up-Code für den Bootloader ist minimalistisch. Im C-Teil muss
man darauf achten, z.B. sind keine Variablen vorbelegt. Etwas wie "char x = 'A';
" funktioniert
nicht! x
bleibt undefiniert!
Achtung: BOOTLOADERSTART muss am Anfang einer Flash-Page liegen. Ist dies nicht der Fall, kann es sein, dass beim Seitenweisen flashen ein Teil des Bootloader-Codes überschrieben wird.
Der eigentliche Bootloader ist in C geschrieben und besteht aus folgenden Komponenten:
Damit der Bootloader aktiv wird, müssen nach einem Reset sowohl SCL als auch SDA für einen längeren Zeitraum konstant auf Low-Pegel liegen. Der normale Pegel der beiden Leitungen ist High. Hieran kann ziemlich eindeutig übermittelt werden, dass der Bootloader aktiviert werden soll.
Zur Prüfung wird in einer Schleife immer wieder geschaut, ob beide Pins auf Low-Pegel liegen.
Ist dies nicht der Fall, wird der Bootloader per return
verlassen. Die Gesamt-Schleifendauer beträgt ca. 721.000 Takt-Zyklen. Die Zeit ist abhängig von der
Taktfrequenz:
16 MHz: 45 mSec
8 MHz:
90 mSec
1 MHz: 721 mSec.
Sind beide Leitungen während der gesamten Zeit auf Low-Pegel wird der Bootloader aktiviert. Zunächst wird gewartet, dass sowohl an SCL als auch an SDA wieder High-Pegel anliegen, der reguläre I²C-Modus also auf dem Bus wieder vorherrscht.
Der Initialilisierungsteil ist recht einfach. Zuerst wird das gesamt I²C-Register mit 0x00 vorbelegt. Danach werden die vorgesehen Inhalte eingetragen. Die Komponenten "Device" und "Version" entsprechen in der Größe den Standards des Lego Mindstorms NXT sollten von diesem also Problemlos dargestellt werden können (zum Aufbau des Registers siehe unten).
Zum Schluss wird der I²C-Slave initialisiert.
Der Kommando-Interpreter wird aktiv, wenn im Feld 'CommandStart' des I²C-Registers der Wert 0xA5 und in das Feld 'Command' ein Kommando-Code geschrieben wird. Das Beschreiben von zwei Bytes sichert das versehentliche ABsetsen eines Kommandos bei möglichen Übertragungsfehlern ab. Es stehen zwei Kommandos zur Verfügung:
Entsprechende Routinen zum Kopieren einer Flash-Seite in das I²C-Register und das Flashen einer Seite auf Basis des I²C-Register-Inhalte stehen dem Kommando-Interpreter zur Verfügung.
Da der Bootloader, wenn er aktiv wird, eine Reihe von Registern belegt und das USI konfiguriert, stellt er keine Möglichkeit zur Verfügung, die Nutzapplikation zu starten. Hierzu müssten alle Veränderungen rückgängig gemacht werden. Dies ist zum einem unsicher und benötigt zu viel Code. Ein Reset erledigt das eleganter.
Im Bootloader erfolgt der Datenaustausch mit dem Uploader über eine Register-Struktur. Der Bootloader stellt sicher, dass die festen Daten nicht überschrieben werden können. Ein Beschreiben ist erst ab dem Feld PageNo möglich. Der Bootloader stellt ebenfalls sicher, dass nicht über das Ende des Registers hinaus geschrieben werden kann. Das Lesen unterliegt keinerlei Einschränkungen.
Der Aufbau des Registers ist wie folgt:
struct
{ char Device[8]; //0x00 - 0x07
char Version[8]; //0x08 - 0x0F
uint8_t Signature[3]; //0x10 - 0x12
uint8_t PageSize; //0x13
uint8_t FreePages; //0x14
uint8_t LastCommand; //0x15
uint8_t CommandCount; //0x16
uint8_t State; //0x17
uint8_t Reserved[5]; //0x18 - 0x1C
volatile uint8_t PageNo; //0x1D
volatile uint8_t CommandStart; //0x1E
volatile uint8_t Command; //0x1F
uint8_t Data[64]; //0x20 - 0x5F
} I2CRegister;
Position | Breite | Name | Bedeutung |
0x00 | 8 | Device | Gerätebezeichnung 'BT'1)9). |
0x08 | 8 | Version | Versionsnummer '30' = 3.01). |
0x10 | 3 | Signature | Prozessor-Signatur8). |
0x13 | 1 | PageSize | Seitengröße des Flash in Byte2). |
0x14 | 1 | FreePages | Anzahl freier Seiten, in die das zu ladende Programm übertragen werden kann2). Gültige Seitennummern sind 0 .. (FreePages-1). |
0x15 | 1 | LastCommand | Der Code des letzten verarbeiteten Kommandos5). |
0x16 | 1 | CommandCount | Anzahl bisher verarbeiteter Kommandos5) Auf 255 folgt 0. |
0x17 | 1 | State | Statusinformation zum aktuellen Kommando5)6). |
0x1d | 1 | PageNo | Seitennummer der vom Kommando betroffenen Seite3). Erste Seite hat Nummer 0. |
0x1e | 1 | CommandStart | Sicherheitsbyte3) muss den Wert 0xA5 erhalten. |
0x1f | 1 | Command | Auszuführendes Kommando3)4). |
0x20 | 64 | Data | Seitenpuffer7). |
1) Am Anfang der Struktur stehen —wie bei NXT-Sensoren üblich— Angaben zum Device und zur Version der Firmware. Der I²C-Master kann hieran erkennen, ob das erwartete Gerät angeschlossen ist und im richtigen Modus betrieben wird. Wegen des Speicherplatzes erfolgt eine nur spartanische Befüllung.
2) Informationen über den Flash-Speicher für den Uploader.
3) Der Bootloader kennt nur zwei Kommandos. Eine Seite aus den Flash auslesen und eine Flash-Seite beschreiben. Das betroffene Kommando kann ausgelöst werden, indem ab PageNo drei Bytes in einem Zug geschrieben werden. CommandStart muss mit dem Wert 0xA5 beschrieben werden. Dieser Mechanismus soll verhindern, dass versehentlich ein Kommando ausgelöst wird. Die Seitennummer kann evtl. getrennt belegt werden. CommandStart und Command müssen unbedingt in einer Schreibsequenz beschrieben werden.
4)
Es stehen die Kommandos
0x01: Programmiere geladene Seite,
0x02:
Lade Speicherseite ins I2C-Register
zur Verfügung.
5) Es ist nicht möglich nur anhand eines Ausführungsstatus zu unterscheiden, ob ein Kommando ausgeführt oder gar nicht übertragenen wurde. Im letzteren Fall liegen die Statusinformationen eines vorhergehenden Kommandos vor. Zur sicheren Kontrolle der Kommandoausführung stehen deshalb drei Informationen zur Verfügung. Zunächst ist dies der Code des letzten ausgeführten Kommandos. Dann ein Zähler, der die ausgeführten Kommandos mitzählt. Durch Prüfung dieser beiden Daten kann man sicher erkennen, ob das erwartete Kommando ausgeführt wurde. Über das Status-Byte kann der Erfolg der Ausführung abgefragt werden.
6)
Folgende Status-Codes werden verwandt:
0x00: Es wurde noch kein Kommando
ausgeführt.
0x01: Das letzte Kommando wurde erfolgreich ausgeführt.
0x02:
Die Seitenzahl der zu flashenden Seite lag nicht im Bereich der freien Seiten.
0x04:
Es wurde ein unbekanntes Kommando gesendet.
7) In den Seitenpuffer werden beim Auslesen des Flash der angeforderte Seiteninhalt geladen und kann von dort abgerufen werden. beim Flashen werden die Daten aus diesem Puffer verwandt.
8) Die Prozessor-Signatur-Bytes ermöglichen es dem Uploader zu prüfen, ob der erwartete Prozessor angeschlossen ist. Beim ATtiny z.B. ist dies 0x1E 0x92 0x06.
9) Nicht belegte Speicherstellen sind mit 0x00 belegt.
Der Code des I²C-Slave entspricht im Wesentlichen der Atmel Application Note (Stand 24.3.13: Listbox "Document Type" einstellen!) "AVR312: Using the USI module as a I2C slave". Hier findet man die Dokumentation und auch den Code, wie man mit dem USI-Modul einen I²C-Slave realisieren kann.
Die I²C-Geräte-Adresse ist fest mit 0x00 belegt. Dies ist praktisch, da man so nicht verschiedene Adressen dokumentieren muss. Die Notwendig des Unterscheidens verschiedener Geräte besteht sowieso i.d.R. nicht, da das Flashen in einem laufenden System bzw. I²C-Bus zu Problemen bei den anderen Geräten führen kann. Zur Aktivierung des Bootloaders, wird der I²C-Bus zur Erkennung der Aktivierungsaufforderung in einen nicht normgerechten Zustand versetzt.
Der typische Ablauf eines Upload-Vorgangs ist wie folgt:
Ein Uploader-Programm für diese Bootloader-Version ist in Entwicklung. Hier ein kleiner Vorgeschmack: