Konzept für einen ATtiny-Bootloader in C

Wechseln zu: Navigation, Suche

Von Frank M. (ukw)

Konzept für einen ATtiny-Bootloader in C[Bearbeiten]

Die ATtiny-Mikroprozessoren unterstützen das Prinzip des Bootloaders der ATMega-Prozessorreihe eigentlich nicht. Dafür fehlen den ATtinys folgende Merkmale:

  • Fehlende Fuses für einen definierten und geschützten Bootloader-Bereich
  • Fehlender Resetvektor exklusiv für den Bootloader
  • Fehlende Interruptvektor-Tabelle für ISRs innerhalb des Bootloaders

Trotzdem sind Bootloader-Implementationen für ATtinys durchaus möglich - wie zum Beispiel diese Threads aus der Codesammlung belegen:

Jedoch sind diese Bootloader allesamt in Assembler realisiert, denn nur hier hat der Programmierer alle Freiheiten, die er braucht, um die Speicherverwaltung im SRAM (und auch im Flash) selbst zu kontrollieren. C (hier im speziellen avr-gcc) ermöglicht es eigentlich nicht, ohne größere Klimmzüge einen Bootloader für ATtinys zu programmieren - aus oben genannten Gründen.

Hinzu kommt, dass ATtinys einen eher kleinen Flash-Speicher haben. Daher wäre es sinnvoll, wenn die Applikation Funktionen des Bootloaders selbst nutzen könnte, ohne sie neu zu implementieren und diese damit doppelt im Flash-Speicher abzulegen. Wenn der Bootloader zum Beispiel sowieso eigene Software-UART-Funktionen verwendet, ist es Unsinn, diese nochmals für die Applikation neu zu programmieren. Dabei ergibt sich aber wieder ein neues Problem: Der Bootloader wird dann eventuell globale Variablen benötigen, die in einem geschützten Bereich liegen müssen. Nur so ist ein "Parallelbetrieb" von Bootloader und Applikation möglich, damit die Applikation Funktionen des Bootloaders (wie z.B. den Software-UART) nutzen kann.

In folgenden Threads werden u.a. die oben angedeuteten Probleme angesprochen, ohne dafür Lösungen anzubieten:

Anlass zu diesem Artikel ist folgender Thread aus der Codesammlung:

Dieser Artikel soll zeigen, dass ein ATtiny-Bootloader auch in C möglich ist - ja, dass sogar eigene Interrupt-Routinen innerhalb des Bootloaders realisiert werden können, obwohl ein ATtiny keinen Austausch der Interruptvektor-Tabelle über das IVSEL-Bit bietet, wie es bei den ATMegas der Fall ist.

Da ein in Assembler geschriebener Bootloader in der Regel viel weniger Speicherplatz benötigt und daher effizienter auf einem kleinen µC ist als ein in C programmierter Bootloader, ist dieser Artikel eher akademischer Natur. Hier sollen lediglich die Mechanismen, die nötig sind, um für einen ATtiny einen Bootloader zu realisieren, ausführlichst vorgestellt werden. Ein Einsatz in der Praxis ist durchaus möglich, jedoch wird in vielen Fällen ein in Assember geschriebener Bootloader (auch wenn man ihn evtl. gar nicht versteht) die bessere Wahl sein. Nichtsdestotrotz: Der Bootloader ist umsetzbar und wird von mir auch in der Praxis eingesetzt.

Bei der Umsetzung eines ATtiny-Bootloaders in C sind folgende Themen zu behandeln:

  • Starten des Bootloaders bei RESET mit gesichertem Sprung zurück in die Applikation
  • Exportieren von C-Funktionen an die Applikation, z.B. SW-UART
  • Eventuell eigener gesicherter Speicherbereich des Bootloaders im SRAM
  • Eigene Interrupt-Service-Routinen im Bootloader
  • Aufruf des Bootloaders auf Wunsch auch direkt aus der Applikation
  • Schutz des Bootloaders vor Überschreiben

Im folgenden wird näher auf diese Problemstellungen eingegangen - anhand des Beispiels eines ATtiny85. Auf andere ATTtinys (wie z.B. den ATtiny45) lässt sich alles durch Berücksichtigung von abweichender Flash- und SRAM-Speichergröße übertragen.

Wie bei den ATMegas wird der Bootloader einzeln kompiliert und nicht mit der Applikation gelinkt. Damit Bootloader und Applikation bestimmte Funktion "sharen" können, sind spezielle Mechanismen nötig. Diese werden weiter unten besprochen.

Speicherlayout des Bootloaders[Bearbeiten]

Der auszuführende Code des Bootloaders sollte möglichst am Ende des Flashs gespeichert werden - so wie es beim "Vorbild" der ATMega-Familie auch ist. Benötigt der Bootloader beispielsweise 2KB an Code, sollte er bei einem ATtiny85 mit 8KB Flash (8192 Bytes) an der Stelle 6 x 1024 = 6144 = 0x1800 gespeichert werden. Dazu müssen für den Linker eigene Sections eingerichtet werden. Bei WinAVR könnte man dafür im AVR Studio den Dialog "Project" -> "Configuration Options" -> "Memory Settings" nutzen, jedoch machen wir davon keinen Gebrauch, da wir etwas mehr tricksen müssen, was in diesem Dialog nicht möglich ist.

Stattdessen ändern wir direkt die Linker-Optionen. Dies geht bei AVR Studio über Project -> Configuration Options -> Custom Options -> Linker Options. Hier fügen wir folgende 3 Zeilen hinzu:

  -Wl,--section-start=.text=0x1800
  -Wl,--section-start=.data=0x800100
  -Wl,--section-start=.bootreset=0x00

In anderen Entwicklungsumgebungen kann man direkt das Makefile bearbeiten, um diese 3 Optionen beim Linker-Aufruf hinzuzufügen.

Was passiert hier:

Zeile 1 sorgt dafür, dass der auszuführende Code ab dem Bereich 0x1800, also in den letzten 2KB des ATtiny-Flashs gespeichert werden. Dieses wird auch bei den ATMegas angewandt, ist also noch ganz normal. Dabei landen aber auch sämtliche Interrupt-Vektoren einschließlich des Reset-Vektors im oberen Speicherbereich. Bei den ATMegas werden sie automatisch genutzt, beim ATtiny ist dieses Vorgehen tödlich, denn die ATtinys erwarten immer die Vektoren im Flash ab der Adresse 0. Macht nichts, wir korrigieren das mit Zeile 3, siehe weiter unten.

Der Startwert für die Text-Section kann natürlich kleiner oder größer sein. Das kommt immer darauf an, wie groß der Bootloader ist. Die Größe kann man über das Studium der vom Compiler erzeugten LSS-Datei herausfinden. Im AVR-Studio wird die Größe des erzeugten Programms auch nach dem Linken ausgegeben. Wichtig: Der Startwert (hier 0x1800 = 6144) muss durch 64 (== SPM_PAGESIZE für ATtinys) teilbar sein!

Zeile 2 ist da schon etwas kniffliger: wir sorgen dafür, dass die globalen bzw. statischen Variablen des Bootloaders einen eigenen Speicherbereich im SRAM erhalten. Da die AVRs eine sog. Harvard-Architektur verwenden, wo der Speicher vom ausführbaren Code (Flash) und Variablen (SRAM) getrennt ist, muss man dem GNU-Linker explizit mitteilen, dass es sich hier um das SRAM handelt. Dies wird erreicht, indem man 0x800000 auf den gewünschten Bereich (0x100) hinzuaddiert. Das Ergebnis ist dann 0x800100. Der ATtiny85 hat 512 Bytes SRAM. In Hex umgerechnet sind es 0x200. In obigem Beispiel setzen wir den Startwert für den globalen Speicherbereich des Bootloaders auf die Hälfte, also 0x100. Damit haben Applikation und Bootloader erst einmal einen gleich großen Speicherplatz für globale Variablen. Die Grenze ist hier willkürlich gewählt, je nach Anwendungsfall muss man die Grenze selbst abschätzen und ändern.

Dabei muss man aber bedenken, dass alle dynamischen Variablen auf dem Stack liegen und "rückwärts" im SRAM beginnend an der höchsten Speicherstelle angelegt werden. Die Daten-Section für den Bootloader teilt sich somit die globalen Variablen des Bootloaders UND die dynamischen Variablen des gesamten Programms (Bootloader + Applikation). Daher sollte man die Speichergröße der Bootloader-Daten-Section im SRAM nicht zu knapp wählen!

Sollen keine Funktionen zwischen Bootloader und Applikation "geshared" werden und soll der Bootloader auch nicht direkt aus der Applikation aufrufbar sein, können wir auf Zeile 2 verzichten. Sollte der Bootloader globale oder statische Variablen nutzen, spielt es dann keine Rolle mehr, wo sie liegen. Nach dem Sprung des Bootloaders in die Applikation wird das SRAM sowieso neu initialisiert.

Zeile 3 ist der eigentliche Trick. Wir sorgen mit dem Anlegen einer BootReset-Section dafür, dass wir auch "etwas" im Flash ab der Adresse 0 abspeichern wollen. Und zwar werden wir dort den Reset-Vektor speichern. Dazu muss man wissen, dass diese ganzen Vektoren eigentlich RJMP-Befehle sind, also Sprungbefehle auf relative Adressen. Der Opcode dafür ist ein Wort (2 Bytes), der aus 0xC000 und der relativen Adresse im Flash gebildet wird. Diese Adresse ist eine Wort-Adresse, d.h. sie hat lediglich den halben Wert, den man erwarten würde. Das liegt daran, dass die Flash-Adressierung der AVRs wortweise - also in 16Bit-Einheiten - erfolgt. Unsere Bootloader Startadresse 0x1800 im Flash muss also durch die Hälfte repräsentiert werden.

In den Source unseres Bootloaders (z.B. tinyboot.c) legen wir nun nach den obligatorischen Include-Befehlen eine globale Variable namens "boot_reset" an, die in der BootReset-Section liegt:

uint16_t boot_reset __attribute__((section(".bootreset"))) = 0xC000U + 0x1800 / 2 - 1;

Hier wird also ein Sprungbefehl gespeichert, nämlich RJMP plus relative Adresse (Wort-Adresse = halber Wert der Byte-Adresse!). Das ganze muss noch um 1 vermindert werden, da der Programmzähler des µCs nach Lesen des Befehls schon um 1 Wort weitergesprungen ist. Wir haben hier also eine absolute Adresse in eine relative Adresse umgewandelt. Dies werden wir später noch mehrfach benutzen, wenn wir die komplette Interrupt-Vektortabelle im Flash verschieben.

Der Witz ist, dass die Startadresse der Bootreset-Section bei Adresse 0x0000 liegt. Der Linker wird damit den Initialisierungswert der Variablen im Flash speichern - und zwar an der Adresse 0x000. Genau dahin muss der Resetvektor für den ATtiny. Voilà, Bootproblem gelöst, wir haben einen Resetvektor an der richtigen Stelle im Flash!

Später werden wir dafür in der erzeugten HEX-Datei sehen:

   :02000000FFCB34

Diese Zeile bedeutet: 2 Bytes im Flash an der Adresse 0x0000, die Werte sind: FF CB (Little Endian, daher hier "rückwärts" zu lesen). Der Opcode ist also CB FF, was einem "RJMP +17FE" entspricht, also einem Sprung an die Adresse 0x1800. Dort steht der vom Linker erstellte Resetvektor, welcher dann den Sprung ins eigentliche Programm - unserem Bootloader - bedeutet.

Dazu programmieren wir uns noch eine (zunächst leere) main-Funktion und fügen noch die später notwendigen Include-Statements hinzu, dann haben wir schon einmal das Gerüst:

#include <inttypes.h>
#include <avr/io.h>
#include <util/delay.h>
#include <avr/eeprom.h>
#include <avr/pgmspace.h>
#include <avr/interrupt.h>
#include <string.h>
                                                                  // Dieser Wert muss evtl. angepasst werden:
#define BOOTLOADER_STARTADDRESS     0x1800                        // bootloader start address, e.g. 0x1800 = 6144
 
#define RJMP                        (0xC000U - 1)                 // opcode of RJMP minus offset 1
#define RESET_SECTION               __attribute__((section(".bootreset")))
 
uint16_t                            boot_reset RESET_SECTION = RJMP + BOOTLOADER_STARTADDRESS / 2;
 
int
main ()
{
    for (;;)
    {
        ;  // This statement intentionally left blank ;-)
    }
}

Dabei haben wir die Variable boot_reset nun auch eleganter formuliert. Wenn man dieses (noch unnütze Programm) übersetzt und ins Flash des ATtinys per Programmer übertragen würde, wäre unser Flash-Speicher nun folgendermaßen organisiert:

           +-------------------------------------+
  0x0000   | Reset-Vektor #1 auf Reset-Vektor #2 | >----+
  0x0002   | <leer>                              |      |
           |                                     |      |
           |                                     |      |
           |                                     |      |
           +-------------------------------------+      |
  0x1800   | Reset-Vektor #2 auf Bootloader-Code | <->--+
  0x1802   | Interrupt-Vektor 1                  |      |
           | ...                                 |      |
  0x181C   | Interrupt-Vektor N                  |      |
           +-------------------------------------+      |
  0x181E   | Bootloader-Code                     | <----+
           | ...                                 |
  0x2000   +-------------------------------------+

Der Reset-Vektor #1 (vorn im Flash) zeigt nun auf den Reset-Vektor #2, welcher wiederum auf den Bootloader-Code zeigt.

Erster Boot[Bearbeiten]

Nutzt der Bootloader eigene Interrupt-Service-Routinen (ISRs), so brauchen wir die dazugehörenden Interrupt-Vektoren auch "vorn" im Flash, damit der ATtiny diese auch ausführen kann. Dieses erreichen wir, indem wir beim ersten Start unseres Bootloaders die komplette Vektor-Tabelle nach "vorn" kopieren. Hier der entsprechende Auszug:

int
main ()
{
    uint16_t    rjmp;
    uint8_t     idx;
    uint8_t     sreg;
 
    // RJMP-Adresse (Reset) aus der Bootloader-Section holen und mit Offest 0x1800 versehen:
    rjmp = pgm_read_word (BOOTLOADER_STARTADDRESS) + BOOTLOADER_STARTADDRESS / 2;
 
    if (rjmp != pgm_read_word (0))                         // Mit Reset-Adresse vergleichen, wenn verschieden:
    {                                                      // Kopieren aller Vektoren nach "vorn" - unter Berücksichtigung des Offsets
        for (idx = 0; idx < _VECTORS_SIZE; idx += 2)
        {
            rjmp = pgm_read_word (BOOTLOADER_STARTADDRESS + idx) + BOOTLOADER_STARTADDRESS / 2;
            boot_program_page_fill ((uint32_t) idx, rjmp);
        }
 
        while (idx < SPM_PAGESIZE)                         // Füllen der restlichen Page (64 Bytes) mit Nullen
        {
            boot_program_page_fill ((uint32_t) idx, 0x0000);
            idx += 2;
        }
        boot_program_page_erase_write (0x0000);
    }
 
    for (;;)
    {
        ;                                                 // Später hier den eigentlichen Bootloader starten
    }
}

Die Vektoren werden kopiert und dabei der Offset 0x1800 hinzuaddiert, da der auszuführende code ja nun aus Sicht der neuen Vektortabelle 0x1800 Bytes "weiter hinten" steht. Dieses Kopieren wird nur beim allerersten Boot durchgeführt, denn danach sind die Vektoren (bis auf den Offset 0x1800) identisch und die Bedingung im if-Statement ist nicht mehr gegeben.

Wir benutzen in obigem Code den Aufruf zweier Makros (boot_program_page_fill() und boot_program_page_erase_write()), die wir noch schreiben müssen. So sehen sie aus:

/*-----------------------------------------------------------------------------------------------------------------------
 * Flash: fill page word by word
 *-----------------------------------------------------------------------------------------------------------------------
 */
#define boot_program_page_fill(byteaddr, word)      \
{                                                   \
    sreg = SREG;                                    \
    cli ();                                         \
    boot_page_fill ((uint32_t) (byteaddr), word);   \
    SREG = sreg;                                    \
}
 
/*-----------------------------------------------------------------------------------------------------------------------
 * Flash: erase and write page
 *-----------------------------------------------------------------------------------------------------------------------
 */
#define boot_program_page_erase_write(pageaddr)     \
{                                                   \
    eeprom_busy_wait ();                            \
    sreg = SREG;                                    \
    cli ();                                         \
    boot_page_erase ((uint32_t) (pageaddr));        \
    boot_spm_busy_wait ();                          \
    boot_page_write ((uint32_t) (pageaddr));        \
    boot_spm_busy_wait ();                          \
    boot_rww_enable ();                             \
    SREG = sreg;                                    \
}

Diese schreiben wir oberhalb der main-Funktion. Die Makros sind "interrupt-fest", d.h. sie gewährleisten das erfolgreiche Flashen auch bei eingeschalteten Interrupts. Doch dazu später.

Unser Flash-Speicher ist nach dem ersten Boot folgendermaßen organisiert:

           +-------------------------------------+
  0x0000   | Reset-Vektor #1 auf Bootloader-Code | >----+
  0x0002   | Interrupt-Vektor 1                  |      |
           | ...                                 |      |
  0x001C   | Interrupt-Vektor N                  |      |
  0x001E   | <leer>                              |      |
           |                                     |      |
           |                                     |      |
           |                                     |      |
           +-------------------------------------+      |
  0x1800   | Reset-Vektor #2 auf Bootloader-Code |      |
  0x1802   | Interrupt-Vektor 1                  |      |
           | ...                                 |      |
  0x181C   | Interrupt-Vektor N                  |      |
           +-------------------------------------+      |
  0x181E   | Bootloader-Code                     | <----+
           | ...                                 |
  0x2000   +-------------------------------------+

Wir sind nun auf dem Weg zu unserem ATtiny-Bootloader schon einen gehörigen Schritt weiter: Der Bootloader-Code wird nun vom ersten Reset-Vektor auf direktem (statt indirektem) Wege angesprungen. Die Interrupt-Vektoren sind nun auch vollständig vorn im Flash vorhanden.

Ansprung der Applikation[Bearbeiten]

Die aus main() des Bootloaders aufgerufene Funktion start_bootloader() muss nun folgendes machen:

 1. Warten auf zu flashende Bootloader-Daten
 2. Werden Daten empfangen, werden sie im Flash gespeichert, anschließend ein SW-Reset ausgeführt
 3. Werden innerhalb einer bestimmten Zeit (z.B. 3 Sek) keine Daten empfangen, wird zur Applikation im Flash gesprungen
 4. Gibt es bisher keine Applikation im Flash, muss der Bootloader in einer Endlossschleife auf Daten warten

Nur wie kann der Bootloader in eine irgendwann vormals im Flash gespeicherte Applikation springen? Die Antwort: Er muss sich dafür den vormals empfangenen Reset-Vektor aus den zu flashenden Daten für die Applikation merken, indem er diesen an das Ende des verfügbaren Flashs des µCs schreibt.

Dazu legen wir eine Include-Datei tinyboot.h an und schreiben dort hinein:

                                                                  // Diese beiden Werte müssen evtl. angepasst werden:
#define BOOTLOADER_STARTADDRESS     0x1800                        // bootloader start address, e.g. 0x1800 = 6144
#define BOOTLOADER_ENDADDRESS       0x2000                        // bootloader end   address, e.g. 0x2000 = 8192
#define BOOTLOADER_FUNC_ADDRESS (BOOTLOADER_ENDADDRESS - sizeof (BOOTLOADER_FUNCTIONS))
 
typedef struct
{
    void                        (*start_appl_main) (void);
} BOOTLOADER_FUNCTIONS;

Wenn sich nun jemand fragt, warum für einen einzelnen Funktionspointer eine Struct verwendet wird, der bekommt seine Antwort später. Wir werden diese Struct noch um weitere Elemente erweitern, wenn wir Funktionen des Bootloaders der Applikation bereitstellen wollen.

Der Pointer start_appl_main muss beim allerersten Boot im Flash als Nullpointer gespeichert werden. Denn zu diesem Zeitpunkt haben wir ja noch keine Applikation geladen. Dazu erweitern wir tinyboot.c folgendermaßen:

#include "tinyboot.h"
 
#define RJMP                        (0xC000U - 1)                 // opcode of RJMP minus offset 1
#define RESET_SECTION               __attribute__((section(".bootreset")))
 
uint16_t                            boot_reset RESET_SECTION = RJMP + BOOTLOADER_STARTADDRESS / 2;
 
static BOOTLOADER_FUNCTIONS bootloader_functions =
{
    (void (*)) NULL
};

Danach erweitern wir unsere main-Funktion:

int
main ()
{
    uint16_t    rjmp;
    uint8_t     idx;
    uint8_t     sreg;
 
    // RJMP-Adresse (Reset) aus der Bootloader-Section holen und mit Offest 0x1800 versehen:
    rjmp = pgm_read_word (BOOTLOADER_STARTADDRESS) + BOOTLOADER_STARTADDRESS / 2;
 
    if (rjmp != pgm_read_word (0))                         // Mit Reset-Adresse vergleichen, wenn verschieden:
    {                                                      // Kopieren aller Vektoren nach "vorn" - unter Berücksichtigung des Offsets
        for (idx = 0; idx < _VECTORS_SIZE; idx += 2)
        {
            rjmp = pgm_read_word (BOOTLOADER_STARTADDRESS + idx) + BOOTLOADER_STARTADDRESS / 2;
            boot_program_page_fill ((uint32_t) idx, rjmp);
        }
 
        while (idx < SPM_PAGESIZE)                         // Füllen der restlichen Page (64 Bytes) mit Nullen
        {
            boot_program_page_fill ((uint32_t) idx, 0x0000);
            idx += 2;
        }
        boot_program_page_erase_write (0x0000);
        pgm_write_block (BOOTLOADER_FUNC_ADDRESS, (uint16_t *) &bootloader_functions, sizeof (bootloader_functions));
    }
 
    for (;;)
    {
        start_bootloader ();                                                // den eigentlichen Bootloader starten
                                                                            // wenn Timeout, kommt dieser hierhin zurück durch return
        memcpy_P (&bootloader_functions, (PGM_P) BOOTLOADER_FUNC_ADDRESS, sizeof (bootloader_functions)); // Startadresse der Applikation auslesen
 
        if (bootloader_functions.start_appl_main) // Wenn Startadresse gesetzt, Applikation aufrufen
        {
            cli ();
            (*bootloader_functions.start_appl_main) ();
        }
    }
}

Hier ist also der Aufruf von pgm_write_block() hinzugekommen, welches am Ende des Flash-Speichers den Nullpointer beim allerersten Boot schreibt. Ausserdem wurde nun der Aufruf des eigentlichen Bootloaders und der Aufruf der Applikation eingebaut, wenn start_bootloader() wegen eines Timeouts (es gibt gerade nichts zu flashen) wieder zurückkommt.

Die neue Funktion pgm_write_block() sieht folgendermaßen aus:

/*-----------------------------------------------------------------------------------------------------------------------
 * write a block into flash
 *-----------------------------------------------------------------------------------------------------------------------
 */
static void
pgm_write_block (uint16_t flash_addr, uint16_t * block, size_t size)
{
    uint16_t        start_addr;
    uint16_t        addr;
    uint16_t        w;
    uint8_t         idx = 0;
    uint8_t         sreg;
 
    start_addr = (flash_addr / SPM_PAGESIZE) * SPM_PAGESIZE;        // round down (granularity is SPM_PAGESIZE)
 
    for (idx = 0; idx < SPM_PAGESIZE / 2; idx++)
    {
        addr = start_addr + 2 * idx;
 
        if (addr >= flash_addr && size > 0)
        {
            w = *block++;
            size -= sizeof (uint16_t);
        }
        else
        {
            w = pgm_read_word (addr);
        }
 
        boot_program_page_fill (addr, w);
    }
 
    boot_program_page_erase_write(start_addr);                      // erase and write the page
}

Da das Schreiben von Daten immer in Pages (SPM_PAGESIZE == 64 für ATtinys) erfolgen muss, sieht diese Funktion etwas komplizierter aus. Hier werden die Daten des Flashs nämlich erst gelesen und dann die zu schreibenden Daten "darübergelegt" und anschließend als ganze Page weggeschrieben.

Abgesehen vom eigentlichen Bootloader (um den es in diesem Artikel gar nicht geht, hat das einer schon gemerkt?) sind wir nun fast fertig. Wenn der Bootloader nun die Applikationsdaten empfängt, muss er sich das Word für die Stelle 0x0000 (das ist der Reset-Vektor für die Applikation) merken und durch den eigenen Reset-Vektor wieder ersetzen, z.B. so:

#define RESET_ADDR          0x0000
 
void
start_bootloader (void)
{
    uint16_t    word;
    uint16_t    reset_vector;
    void        (*start) (void) = 0x0000;
 
    reset_vector        = pgm_read_word (RESET_ADDR);                               // eigene Reset-Adresse merken
    ...
 
    Schleife über die empfangenen Daten...
    {
        wenn Word für Adresse 0x0000 empfangen wird:
        {
          bootloader_functions.start_appl_main  = (void *) (word - RJMP);          // Applikations-Adresse als absolute Adresse merken
          word = reset_vector;                                                     // word ersetzen durch eigenen Reset-Vektor
        }
        ....
    }
 
    wenn flash-vorgang erfolgreich, hier am Ende:
    {
        // Startaddresse der Applikation am Flash-Ende speichern
        pgm_write_block (BOOTLOADER_FUNC_ADDRESS, (uint16_t *) &bootloader_functions, sizeof (bootloader_functions));
        (*start) ();                                                              // Reset auslösen
    }
    return;
}

Dabei wird angenommen, dass gerade in der Variablen "word" das zu schreibende Word für die Adresse 0x0000 im Flash steht. Dieses Wort merken wir uns in start_appl_main, ersetzen es wieder durch den eigenen Reset-Vektor (denn auch zukünftig soll ja zuallererst unser Bootloader angesprungen werden) und schreiben den gemerkten Pointer am Ende des Flash-Vorgangs selbst ans Ende des Flashs. Dafür benutzen wir wieder pgm_write_word().

Unser Flash-Speicher sieht nach dem Flashen der Applikation nun folgendermaßen aus:

           +-------------------------------------+
  0x0000   | Reset-Vektor #1 auf Bootloader-Code | >----+
  0x0002   | Interrupt-Vektor 1 (Applikation)    |      | Ansprung des Bootloaders
           | ...                                 |      |
  0x001C   | Interrupt-Vektor N (Applikation)    |      |
           +-------------------------------------+      |
  0x001E   | Applikations-Code                   |      |  <--+
  .....    | ....                                |      |     |
           +-------------------------------------+      |     |
  0x1800   | Reset-Vektor #2 auf Bootloader-Code |      |     |
  0x1802   | Interrupt-Vektor 1                  |      |     |
           | ...                                 |      |     |
  0x181C   | Interrupt-Vektor N                  |      |     |
           +-------------------------------------+      |     |
  0x181E   | Bootloader-Code                     | <----+     |
           | ...                                 |            |
           +-------------------------------------+            | Ansprung der Applikation
  0x1FFE   | Pointer auf Applikation             | >----------+
  0x2000   +-------------------------------------+


Damit haben wir nun vom Konzept her einen funktionsfähigen Bootloader für ATtinys fertiggestellt, der komplett in C programmiert ist. Aber damit ist dieser Artikel noch nicht ganz abgeschlossen. Denn zwei weitere Themen sind ungeheuer interessant, nämlich das Sharen von Funktionen und das Sharen von Interrupts zwischen Bootloader und Applikation. Ein praktisches Feature kann auch der Bootloader-Aufruf direkt aus der Applikation sein. Dies alles wird in den folgenden Abschnitten besprochen.

Sharen von Funktionen[Bearbeiten]

Sind im Bootloader Funktionen implementiert, die auch sinnvoll für die Applikation sein könnten, kann der Bootloader diese Funktionen exportieren. Das könnten zum Beispiel SW-UART-Funktionen wie uart_getc() und uart_putc() sein. Es sollten aber keine Flash-Funktionen sein. Das Beschreiben des Flashs sollte besser dem Bootloader exklusiv vorbehalten sein.

Nehmen wir an, dass folgende Funktionen im Bootloader existieren:

void uart_putc (uint8_t ch)
{
    ...                          // Zeichen ausgeben
    return;
}
 
uint8_t uart_getc (void)
{
    uint8_t ch;
    ...                          // Zeichen lesen
    return (ch);
}

dann können wir in der Include-Datei tinyboot.h die Struktur BOOTLOADER_FUNCTIONS folgendermaßen erweitern:

typedef struct
{
    uint8_t                     (*uart_getc) (void);
    void                        (*uart_putc) (uint8_t);
    void                        (*start_appl_main) (void);
} BOOTLOADER_FUNCTIONS;

Anschließend erweitern wir wir die Initialisierung von bootloader_functions in tinyboot.c:

...
uint8_t         uart_getc (void);
void            uart_putc (unsigned char);
 
static BOOTLOADER_FUNCTIONS bootloader_functions =
{
    uart_getc,
    uart_putc,
    (void (*)) NULL
};

In main() selbst brauchen wir keine Erweiterungen vorzunehmen. Es werden nun ans Ende des Flashs 3 Pointer geschrieben, nämlich:

  • Pointer auf uart_getc()
  • Pointer auf uart_putc()
  • Pointer auf Start der Applikation, wie gehabt.

Durch Erweitern der Struktur können weitere Funktionen exportiert werden. Zu beachten ist jedoch, dass die Struktur von der Größe her SPM_PAGESIZE nicht überschreiten darf. Die maximale Anzahl der zu exportierenden Funktionen ist daher 31, was jedoch im allgmeinen ausreichen dürfte.

Die Applikation wiederum kann dann diese Pointer durch Füllen der Struct wieder auslesen und die exportierten Funktionen des Bootloader nutzen, nämlich folgendermaßen:

#include "tinyboot.h"
 
static BOOTLOADER_FUNCTIONS   boot;
 
int main ()
{
    memcpy_P (&boot, (void *) BOOTLOADER_FUNC_ADDRESS, sizeof (boot));
    ...
}

Dann können wir die vom Bootloader exportierten Funktionen in der Applikation mittels

     ch = boot.uart_getc ();
     boot.uart_putc (ch);

nutzen.

Hier wieder das Layout unseres Flash-Speichers:

          +-------------------------------------+
 0x0000   | Reset-Vektor #1 auf Bootloader-Code | >----+
 0x0002   | Interrupt-Vektor 1 (Applikation)    |      | Ansprung des Bootloaders
          | ...                                 |      |
 0x001C   | Interrupt-Vektor N (Applikation)    |      |
          +-------------------------------------+      |
 0x001E   | Applikations-Code                   |      |  <---+
 .....    | ....                                |      |      |
          +-------------------------------------+      |      |
 0x1800   | Reset-Vektor #2 auf Bootloader-Code |      |      |
 0x1802   | Interrupt-Vektor 1                  |      |      |
          | ...                                 |      |      |
 0x181C   | Interrupt-Vektor N                  |      |      |
          +-------------------------------------+      |      |
 0x181E   | Bootloader-Code                     | <----+      |
          | ...                                 |             |
          | uart_putc()                         | <------+    |
          | uart_getc()                         | <---+  |    |
          | ...                                 |     |  |    |
          +-------------------------------------+     |  |    | Ansprung der Applikation
 0x1FFA   | Pointer auf uart_getc()             | >---+  |    |
 0x1FFC   | Pointer auf uart_putc()             | >------+    |
 0x1FFE   | Pointer auf Applikation             | >-----------+
 0x2000   +-------------------------------------+

Sharen von Interrupts[Bearbeiten]

Normalerweise benötigt ein Bootloader keine Interrupts. Ein Bootloader hat eine klare einfache Aufgabe:

  • Empfangen von Daten
  • Schreiben in den Flash

Da hier nichts parallel abzuwickeln ist, braucht man hier in der Regel auch keine Interrupts.

In einer µC-Applikation jedoch sind über Interrupts arbeitende Kommunkationsschnittstellen eher die richtige Wahl, da hier meist "quasi-parallel" gearbeitet werden muss. Möchte man nun zum Beispiel ein- und denselben SW-UART-Code im Bootloader und in der Applikation nutzen, kommt man um Interrupts auch im Bootloader nicht herum.

Auf ATtinys gibt es keine separate Interruptvektor-Tabelle für den Bootloader, wie es für den ATmega vorgesehen ist. Daher muss man hier etwas tricksen.

Zunächst schreiben wir für den SW-UART eine eigene ISR, zum Beispiel:

ISR(TIMER1_COMPA_vect)
{
    uart_interrupt ();               // call SW-UART interrupt routine
}

Hier ist es (der Übersichtlichkeit halber) ein Funktionsaufruf, der SW-UART-ISR-Code könnte natürlich auch direkt in der ISR stehen.

Der vom Linker erzeugte Interrupt-Vektor steht nun in der Interruptvektor-Tabelle ab der Stelle 0x1800. Durch das Kopieren der Interruptvektor-Tabelle nach vorn im FLASH (siehe main-Funktion oben) wird auch gewährleistet, dass unsere ISR auch aufgerufen wird. Allerdings müssen wir nun verhindern, dass dieser Interrupt-Vektor beim Flashen der Applikation überschrieben wird. Analog zum Reset-Vektor muss daher auch der Interrupt-Vektor gesichert werden.

Wir müssen erst einmal herausfinden, an welcher Stelle der Tabelle unser Interrupt-Vektor steht. Beim avr-gcc finden wir für den ATtiny45/85 in der Include-Datei avr/iotnx5.h die Zeile

#define TIMER1_COMPA_vect               _VECTOR(3)

Damit ist unser Timer-ISR-Vektor der dritte (ab 0 beginnend). Daraus folgt, dass dieser Vektor im Flash an der Stelle 2 x 3 = 0x0006 im Flash stehen muss.

Somit ergibt sich für unsere Funktion start_bootloader() folgender neuer Pseudo-Code:

#define FLASH_RESET_ADDR                        0x0000                          // address of reset vector (in bytes)
#define FLASH_TIMER1_COMPA_ADDR                 0x0006                          // address of timer1 compa vector (in bytes)
 
void
start_bootloader (void)
{
    uint16_t    word;
    uint16_t    reset_vector;
    uint16_t    timer1_compa_vector;
    void        (*start) (void) = 0x0000;
 
    reset_vector        = pgm_read_word (RESET_ADDR);                           // eigene Reset-Adresse merken
    timer1_compa_vector = pgm_read_word (FLASH_TIMER1_COMPA_ADDR);              // eigene Timer-Adresse merken
    ...
 
    Schleife über die empfangenen Daten...
    {
        Wenn Word für Adresse FLASH_RESET_ADDR empfangen wird:
        {
          bootloader_functions.start_appl_main  = (void *) (word - RJMP);       // Applikations-Adresse als absolute Adresse merken
          word = reset_vector;                                                  // word ersetzen durch eigenen Reset-Vektor
        }
        Wenn Word für Adresse FLASH_TIMER1_COMPA_ADDR empfangen wird:
        {
            word = timer1_compa_vector;                                         // word ersetzen durch einen Timer-Vektor
        }
        ....
    }
 
    wenn Flash-Vorgang erfolgreich, hier am Ende:
    {
        // Startaddresse der Applikation am Flash-Ende speichern
        pgm_write_block (BOOTLOADER_FUNC_ADDRESS, (uint16_t *) &bootloader_functions, sizeof (bootloader_functions));
        (*start) ();                                                              // Reset auslösen
    }
    return;
}

Damit kann sowohl der Bootloader als auch die Applikation den SW-UART nutzen, welcher (einen oder sogar mehrere) Interrupts verwendet.

Wenn jedoch die Applikation selbst einen Interrupt verwenden will, der schon im Bootloader "verbraten" wird, wird die Applikation der Verlierer sein. Es geht aber trotzdem.

Zunächst erweitern wir in tinyboot.h unsere Struct um den Funktionspointer set_timer1_compa_isr:

typedef struct
{
    uint8_t                     (*uart_getc) (void);
    void                        (*uart_putc) (unsigned char);
    void                        (*set_timer1_compa_isr) (void (*) (void));
    void                        (*start_appl_main) (void);
} BOOTLOADER_FUNCTIONS;

Die Struct wird dann in tinyboot.c folgendermaßen initialisiert:

static BOOTLOADER_FUNCTIONS bootloader_functions =
{
    uart_getc,
    uart_putc,
    set_timer1_compa_isr,
    (void (*)) NULL
};

Desweiteren definieren wir noch einen globalen Funktionspointer:

static void                                     (* volatile timer1_compa_isr) ();

Unsere ISR im Bootloader erweitern wir folgendermaßen:

ISR(TIMER1_COMPA_vect)
{
    uart_interrupt ();               // call SW-UART interrupt routine
 
    if (timer1_compa_isr)
    {
        (*timer1_compa_isr) ();
    }
}

Dann ermöglichen wir es der Applikation, mittels der folgenden Funktion ihre "ISR" anzumelden:

/*-----------------------------------------------------------------------------------------------------------------------
 * set extern TIMER1 COMPA ISR
 *-----------------------------------------------------------------------------------------------------------------------
 */
void
set_timer1_compa_isr (void (*ptr) (void))
{
    uint8_t sreg = SREG;
    cli ();
    timer1_compa_isr = ptr;
    SREG = sreg;
}

Wir merken uns also im Pointer timer1_compa_isr die Adresse der Funktion in der Applikation.


Nehmen wir an, die Applikation hätte eine Funktion myInterruptRoutine(), die vom Timer aufgerufen werden soll:

void
myInterruptRoutine (void)
{
   FadeLEDs ();
}

Dann kann sie diese durch Aufruf von:

    boot.set_timer1_compa_isr (myInterruptRoutine);

anmelden.

Die im Bootloader definierte ISR wird dann zukünftig myInterruptRoutine() aus der Applikation aufrufen.

Abmelden kann die Applikation ihre eigene "ISR", indem sie einen Nullpointer an boot.set_timer1_compa_isr() heruntergibt.

Das ganze hat nur einen kleinen Wermutstropfen bei Timer-ISRs. Die Applikation muss sich mit dem vom Bootloader gewählten Timer-Modus und Timing abfinden und notfalls das eigene gewünschte Timing durch Vorteiler selbst herstellen.

Abschließend das Layout unseres Flash-Speichers - erweitert um das SRAM:

         Flash:
         +-------------------------------------+
0x0000   | Reset-Vektor #1 auf Bootloader-Code | >----+ Ansprung Bootloader
0x0002   | Interrupt-Vektor 1 (Applikation)    |      |
         | ...                                 |      |
0x0006   | Interrupt-Vektor 3 (Bootloader!)    | >----------+ Ansprung Bootloader-ISR
         | ...                                 |      |     |
0x001C   | Interrupt-Vektor N (Applikation)    |      |     |
         +-------------------------------------+      |     |
0x001E   | Applikations-Code                   |      |     | <---+ Ansprung Applikation
.....    | ....                                |      |     |     |
         | myInterruptRoutine()                |      |     |     |  <---+ Ansprung App-ISR
         +-------------------------------------+      |     |     |      |
0x1800   | Reset-Vektor #2 auf Bootloader-Code |      |     |     |      |
0x1802   | Interrupt-Vektor 1 (Bootloader)     |      |     |     |      |
...      | ...                                 |      |     |     |      |
0x1806   | Interrupt-Vektor 3 (Bootloader)     |      |     |     |      |
         | ...                                 |      |     |     |      |
0x181C   | Interrupt-Vektor N (Bootloader)     |      |     |     |      |
         +-------------------------------------+      |     |     |      |
0x181E   | Bootloader-Code                     | <----+     |     |      |
         | ...                                 |            |     |      |
         | ISR(TIMER1_COMPA_vect)              | <----------+     |      |
         | ...                                 |                  |      |
         | set_timer1_compa_isr()              | <----------+     |      |
         | uart_putc()                         | <-------+  |     |      |
         | uart_getc()                         | <----+  |  |     |      |
         | ...                                 |      |  |  |     |      |
         +-------------------------------------+      |  |  |     |      |
0x1FF8   | Pointer auf uart_getc()             | >----+  |  |     |      |
0x1FFA   | Pointer auf uart_putc()             | >-------+  |     |      |
0x1FFC   | Pointer auf set_timer1_compa_isr()  | >----------+     |      |
0x1FFE   | Pointer auf Applikation             | >----------------+      |
0x2000   +-------------------------------------+                         |
                                                                         |
         SRAM:                                                           |
         +-------------------------------------+                         |
0x0000   | Applikations-Bereich:               |                         |
         | ...                                 |                         |
         +-------------------------------------+                         |
0x0100   | Bootloader-Bereich:                 |                         |
         | ...                                 |                         |
         | timer1_compa_isr                    | >-----------------------+
         | ...                                 |
0x0200   +-------------------------------------+

Wichtig ist hier der Vektor 3 (ganz oben), der auf unsere ISR im Bootloader zeigt. timer1_compa_isr im SRAM zeigt auf die "ISR" der Applikation.

Aufruf des Bootloaders direkt aus der Applikation[Bearbeiten]

(TODO, folgt in den nächsten Tagen)

Schutz des Bootloaders vor Überschreiben[Bearbeiten]

(TODO, folgt in den nächsten Tagen)

Beispiel-Projekt[Bearbeiten]

(TODO, folgt in den nächsten Tagen)

Literatur[Bearbeiten]

Artikel Bootloader

Artikel AVR Bootloader in C - eine einfache Anleitung

"Bootloader" für ATtiny2313

AVR-Bootloader mit Verschlüsselung