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.


Start-Up-Code (C-Frame) und Speicheraufteilung

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:

  1. Start mit der Code-Adresse 0x0000. Dort steht rjmp BootLoaderStart.BootLoaderStart befindet sich bereits (ziemlich) am Ende Speichers.
  2. Bei BootLoaderStart werden die notwendigen Initialisierungen vorgenommen. Minimal müssen der Stackpointer und r1 initialisiert werden. Der Stack nimmt u.a. die Rücksprungadressen auf. r1 wird vom gcc genommen, wenn irgendetwas mit 0x00 belegt werden soll.
    Der Stackpointer ist abhängig vom Chip 8 oder 16 Bit groß. Bei 16-Bit Stackpointern ist SPH in <avr/io.h> definiert. Daran kann erkannt werden, ob eine 16- oder eine 8-Bit-Initialisierung notwendig ist.
  3. Danach erfolgt ein Aufruf des C-Programms: rcall BootLoader
  4. Wird der Bootloader nicht aktiv, wird er mit return verlassen. Der Code wird mit der nächsten Anweisung fortgesetzt. Dort steht rjmp Init und damit ein Sprung zur Nutz-Applikation.
  5. Die Nutz-Applikation wird ausgeführt. Ist dies ein reguläres C-Programm, erfolgt nun i.d.R. die Initialisierung und der Start des Programms.

Damit ergibt sich folgende Speicheraufteilung:

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.


Bootloader

Der eigentliche Bootloader ist in C geschrieben und besteht aus folgenden Komponenten:

Aktivierungsprüfung

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.

Initialisierung

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.

Kommando-Interpreter

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.

I²C-Register

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.


I²C-Slave

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.


Upload-Vorgang

Der typische Ablauf eines Upload-Vorgangs ist wie folgt:

  1. Bootloader aktivieren
    Dazu müssen zunächst SCL und SDA auf logisch Low gelegt werden. Danach muss bei dem zu flashenden µC ein Reset ausgelöst werden. Nach dem Reset bleiben beide I²C-Leitungen für ca. 1 Sekunde weiter auf Low. Dadurch wird der Bootloader aktiv. Zuletzt müssen die I²C-Leitungen wieder freigegeben werden.
  2. Angeschlossenen µC prüfen
    Bei aktiviertem Bootloader können Daten über die Bootloader-Version ('BT' im Feld Device des I²C-Registers, '30' im Feld Version) abgerufen werden. Im Feld Signature steht die Prozessor-Signatur zur Verfügung.
  3. Zu ladende .hex-Datei prüfen
    Der Bootloader stellt die Anzahl der für die Nutzapplikation zu Verfügung stehenden Seiten und deren Größe zur Verfügung. Die Nutzapplikation muss in diesen Bereich hineinpassen. Des Weiteren muss geprüft werden, Reset-Vektor den richtigen Wert hat. Dieser ist für jeden Prozessor-Typ verschieden. Im Bootloader ist diese Adresse prozessorabhängig fest eingestellt. Passt diese Adresse nicht, kann der Bootloader die Nutzapplikation nicht starten.
  4. Nutzapplikation flashen
    Das Image der Nutzapplikation wird Seitenweise in das Feld Data des I²C-Registers geschrieben. Danach wird die zugehörige Seitennummer in das Feld PageNo geschrieben. Zuletzt wird in die Felder CommandStart und Command die Kombination 0xA501 geschrieben. Dies startet das Flashen der angegebenen Seite.
    Während des Flashens kann keine I²C-Kommunikation stattfinden. Der Bootloader antwortet nicht.
    Nach Beendigung des Flashens wird überprüft, ob in den Feldern LastCommand, CommandCount und State die erwarteten Werte stehen.
    Hinweis: CommandStart und Command müssen in einem Schreibvorgang beschrieben werden!
    Hinweis: Wenn die Seite 0 geflasht wird, wird der darin enthaltene Reset-Vektor auf den Bootloader "umgebogen".
  5. Speicherinhalt prüfen
    Die geflashten Seiten zurücklesen und mit dem Image der Nutzapplikation vergleichen.
    Hinweis: Bei der Seite 0 wurde beim flashen der Reset-Vektor geändert. Bei dieser Seite stimmen die ersten beiden Bytes nicht mit dem Image überein. Im 2. Byte sollte immer 0xC0 sein. Das erste Byte ist (_VECTORS_SIZE / 2 - 1). Die Konstante _VECTORS_SIZE kann man über <avr/io.h> ermitteln.
  6. Reset durchführen
    Zum Starten der Nutzapplikation muss ein Reset des µC durchgeführt werden. Hierbei müssen SCL und SDA im normalen I²C-Modus betrieben werden.

Ein Uploader-Programm für diese Bootloader-Version ist in Entwicklung. Hier ein kleiner Vorgeschmack: