AVR Bootloader in C - eine einfache Anleitung

Aus der Mikrocontroller.net Artikelsammlung, mit Beiträgen verschiedener Autoren (siehe Versionsgeschichte)
Wechseln zu: Navigation, Suche

Dieser Artikel soll dazu dienen, das Thema Bootloader im AVR etwas zu demystifizieren. Es gibt schon einige Artikel und Codebeispiele für verschiedene Bootloader in Assembler oder C (bzw. gemischt), aber kein Artikel beleuchtet das Thema von einer einfachen Seite aus Anwendungssicht. In diese Lücke zielt das Tutorial, es soll einen möglichst einfachen und verständlichen Weg zeigen, sich mit Hilfe der Hochsprache C in das Thema einzuarbeiten (es sollkein Assembler noch inline-Assembler verwendet werden). Vielleicht werden einige sagen, das es nicht möglich ist das Thema zu beleuchten ohne tief in die Hardware und AVR-Register einzusteigen, ich möchte es aber trotzdem versuchen. Der Artikel wird sich auf das notwendigste Wissen beschränken, um mit Booloader arbeiten zu können. Im Artikel beschränkt sich nicht auf einen bestimmten ATmegaXXX aus der riesigen Atmel-Auswahl. Vielmehr wird ein genereller Weg gezeigt, der sich leicht auf andere AVR-Devices(mit Bootloader Sektion) portieren lässt. Als konkrete Beispielplattform für die Codebeispiele dient allerdings eine ATmega88-Plattform.

Einleitung

Zu Beginn soll das notwendigste Wissen über den AVR-Bootloaderbereich vermittelt werden, um eine Arbeitsgrundlage zu schaffen. Dabei wird sich auf die wesentlichen Teile beschränkt, welche zum Verstehen der Architektur notwendig sind. Im weiteren Verlauf des Artikels werden insgesamt drei Anwendungen programmiert: Zuerst ein einfacher Bootloader, welcher in der Bootloadersektion des Flashs ausgeführt wird, aber noch keine eigentliche Bootloader-Funktion hat, sozusagen ein "Hallo Welt"-Bootloader. Danach soll eine kleine Applikation programmiert werden, welche der spätere echte Bootloader ins Flash programmieren soll. Als großes Finale soll dann ein Bootloader entstehen, welcher in der Lage ist, Intel-Hex-Dateien über die serielle Konsole zu laden und ins Flash zu programmieren und zu starten. Als Voraussetzung für dieses Tutorial sollte der Leser bereits Erfahrungen im Umgang mit dem AVRStudio und der Programmiersprache C mitbringen und schon Anwendungen geschrieben haben. Für absolute Einsteiger ist der Artikel ungeeignet.

Software

Für den Artikel werden folgende Software-Pakete benötigt:

  • aktuelles AVRStudio (hier verwendet: AVRStudio v4.18)
  • aktuelles WinAVR (hier verwendet: WinAVR20100110)
  • PuTTY als serielle Konsole (Version v0.6)

Desweiteren wurde auf der AVR-Seite für die serielle Kommunikation mit dem PC auf den beliebte UART-Library von Peter Fleury zurückgegriffen, damit wir uns nicht um die gepufferte UART-Kommunikation kümmern müssen.

Hardware

Für den Artikel wurde eine kleine Hardware bestehend aus einem Atmega88 und einem FTDI232 als USB-Seriell-Wandler erstellt. Dies soll als Basis für die Experimente dienen. Der USB-Seriell-Wandler ist nicht zwingend notwendig und kann auch durch den üblichen Pegelwandler vom Typ MAX232 ersetzt werden, wenn der PC noch eine serielle Schnittstelle besitzt. Entscheidend ist nur die Möglichkeit der seriellen Kommunikation mit dem Rechner.

Schaltplan folgt noch

Für die ISP-Programmierung wurde der AVRISPmkII-In-System-Programmer von Atmel verwendet. Es kann natürlich auch ein anderer Programmer (z.B. STK500) verwendet werden, welcher mit dem AVRStudio zusammenarbeitet. Prinzipiell kann natürlich auch ein selbstgebastelter Parallel-Programmer verwendet werden, dann kann aber nicht via AVR-Studio programmiert werden, sondern mit AVRDude oder PonyProg o.ä. Der Artikel beschränkt sich auf die Verwendung vom AVRStudio.

Für das Verständis der Hardware und der seriellen Kommunikation sind folgende Artikel empfehlenswert:

Grundlagen

Flash Speicher Aufteilung

Was ist eigentlich ein Bootloader und was macht er? Wofür sollte ich so etwas brauchen? Ist das nicht viel zu kompliziert? Ich bin eingefleischter AVRStudio-Benutzer, muss ich mich jetzt mit makefiles beschäftigen? Kann man im AVRStudio mit C überhaupt einen Bootloader schreiben? Vielleicht hat sich der eine oder andere schon einmal diese oder ähnliche Fragen gestellt.

Nun, der Programmcode des AVR steht in seinem Flashspeicher und wird von dort ausgeführt. Ein Bootloader ist in erster Linie ein kleines Programm, welches in einem besonderem Teil des Flash steht - der Boot Loader Section. Durch die Lokalisierung des Bootloader-Programms in dieser besonderen Sektion des AVR ist es dem Programm möglich, auf den Flash (in dem er auch selbst steht) zu schreiben. Die eigentliche Anwendung steht dann in der Application Flash Section. Wenn man so will können im Flash des AVR also zwei unabhängige Programme stehen. Der Flash ist in zwei Bereiche mit unterschiedlichen Merkmalen aufgeteilt (siehe Bild). Auf die RWW bzw. NRWW-Sektion möchte ich an dieser Stelle (noch) nicht eingehen.

Wie man unschwer erkennen kann, liegt der Bootloader-Bereich am Ende des Flash-Speichers. Normalerweise startet der Controller die Abarbeitung seiner Programmierung an der Stelle 0x0000. Ein Bootloader soll ja aber vor der Abarbeitung der eigentlichen Applikation ausgeführt werden. Woher weiß also der AVR-Controller nach dem Reset, dass er nicht von Adresse 0x0000 sondern einer anderen Adresse starten soll? Diese Konfiguration ist wie alle wichtigen und grundlegenden Konfigurationen über die Fuses des AVRs geregelt. Wir beginnen mit der folgende Tabelle, welche die Speicheraufteilung des Programmspeichers veranschaulicht.

Boot Size Konfiguration, Tabelle 26-6 im Atmega88-Datenblatt S.280
BOOTSZ1 BOOTSZ0 Boot
Size
Pages Application
Flash Section
Boot Loader
Flash Section
End
Application
Section
Boot Reset
(Start Boot
Loader Section)
1 1 128 words 4 0x000 - 0xF7F 0xF80 - 0xFFF 0xF7F 0xF80
1 0 256 words 8 0x000 - 0xEFF 0xF00 - 0xFFF 0xEFF 0xF00
0 1 512 words 16 0x000 - 0xDFF 0xE00 - 0xFFF 0xDFF 0xE00
0 0 1024 words 32 0x000 - 0xBFF 0xC00 - 0xFFF 0xBFF 0xC00

Um die Tabelle zu verstehen, muss man wissen, dass der Flash-Speicher intern in sogenannten Pages (Seiten) organisiert ist. Die einzelnen Pages haben eine bestimmte Größe, welche im Datenblatt in Words - also Datenworte - angegeben ist. Ein Datenwort entspricht zwei Bytes. Hier offenbart sich eine Tücke des Datenblatts: Alle Speicherbezüge und Adresse sind in Datenworten (also immer 2 Bytes) angegeben! Aus der Tabelle erfahren wir auch, dass man mit den beiden Fuses BOOTSZ0 und BOOTSZ1 die Größe des Bootloaderbereichs einstellen kann. Eine weitere Tabelle aus dem Atmega88-Datenblatt gibt Auskunft über die Aufteilung der Pages und die Anzahl der Datenworte einer Page.

No.of Words in a Page and No.of Pages in Flash, Tabelle 27-9 im Atmega88-Datenblatt S.288
Device Flash Size Page Size PCWORD No. of Pages PCPAGE PCMSB
Atmega88 4K words (8 Kbytes) 32 words PC[4:0] 128 PC[11:5] 11

Aus der Tabelle ergibt sich, dass die Größe einer Page des verwendeten Atmega88 32 Words - also 64 Byte sind. Insgesamt gibt es 128 Pages, damit ergibt sich nach Adam Riese 128 * 64 = 8192 Byte, also 8 Kbytes. Das war ja einfach! In unserem späteren Codebeispiel soll die Größe des Bootloaderbereichs auf 1024 words - also 2048 Bytes gestellt werden (BOOTSZ0=0 und BOOTSZ1=0). Nun können wir ausrechnen, in welcher Flash-Page bzw. an welcher Flash-Adresse der Bootloaderbereich beginnt: Er beginnt in der 96 Page (128 - 32) an Word-Adresse 0xC00, also Byteadresse 0xC00 * 2 = 0x1800. Dies ist die exakte Startadresse unseres Bootloaderbereiches.

Weiter oben wurde die Frage gestellt, woher der AVR weiß, an welcher Stelle (entweder 0x0000 oder in unserem Fall 0x1800) er nach dem Reset starten soll. Um dem AVR dies mitzuteilen, ist eine weitere Fuse nötig - die BOOTRST-Fuse. Eine weitere Tabelle aus dem Atmega88-Datenblatt gibt Auskunft über diese Fuse.

Reset and Interrupt Vectors Placement in Atmega88, Tabelle 11-3 im Atmega88-Datenblatt S.58
BOOTRST IVSEL Reset Adress Interrupt Vectors Start Adress
1 0 0x000 0x001
1 1 0x000 Boot Reset Address + 0x001
0 0 Boot Reset Address 0x001
0 1 Boot Reset Address Boot Reset Address + 0x001

Mit der BOOTRST-Fuse wird also festgelegt, dass der AVR nach dem Reset an die Startadresse des Bootloaderbereichs springt. Auf die IVSEL-Bit (keine Fuse) möchte ich erst an späterer Stelle - wenn es um Interrupts geht - zurückkommen.
Noch ein wichtiger Hinweise für die Werte der Fuses im Datenblatt: Der Wert "0" bedeutet, dass die Fuse programmiert ist, es entspicht dem Häkchen im AVRStudio!

Das "Hallo Welt" - Bootloader Programm

Wie Eingangs erwähnt werden wir für die Erstellung des Codes die freie IDE von Atmel - das AVRStudio - benutzen. Ergänzt wird es durch C-Kompiler und -Tools des WinAVR Projektes. Des weiteren wird zur seriellen Kommunikation das Terminal Programm PuTTY benötigt und sollte installiert sein. Die Hardware ist aufgebaut und via AVRISPmkII-Programmer an den PC angeschlossen - nun kann es losgehen!

Zu Begin erstellen wir uns wie üblich ein neues AVRStudio-Projekt. Danach arbeiten wir folgende Schritte ab:

Schritt 1 - Konfiguration der Projekteinstellungen

Erstellen des Projekts
Generelle Optionen - setzten der Taktfrequenz
Linker Option eingeben

Als erstes öffnen wir die Projekteinstellungen (Menü Project/Configuration Options) und tragen die richtige Taktfrequenz ein. Im Beispiel nutzen wir den internen Oszillator mit 8 Mhz. Danach gehen wir zum Reiter Custom Options. Dort klicken wir auf [Linker Options] und geben dann im Textfeld daneben -Ttext=0x1800 ein. Danach drücken wir auf Add.

Was bewirkt dieser Linker Parameter? Dafür muß wieder etwas weiter ausgeholt werden. Nach dem Kompilieren der Programmquellen linkt der Linker den Programmcode an bestimmte Stellen in den drei verschiedenen Speichern Flash, EEPROM und SRAM des AVR. In der vom Compiler verwendeten AVR Libc ist der Speicher in verschiedene Sektionen aufgeteilt. Dies ist notwendig, da der AVR-Controller eine Harvard Architektur mit getrenntem Befehls- und Datenspeicher aufweist. Dem Linker muss mitgeteilt werden, in welche Speicher er den Programmcode linken soll. Die Lokalisierung des Speicher sind die Sektionen. Die Sektion .text ist dem ausführbaren Programmcode - also den Befehlen - vorbeihalten und liegt im Flash des AVR, des weiteren gibt es auch noch die Sektionen .data und .bss für die statischen und dynamischen Variablen im SRAM und eine Sektion .eeprom für den EEPROM und noch ein paar spezielle (Flash-)Sektionen.

Nun gibt verschiedene Methoden, dem Linker mitzuteilen, dass man den Programmcode an die Stelle des Bootloaderbereich haben möchte. Eine sehr einfache Methode ist die Verschiebung der Sektion .text, welche normalerweise ab Adresse 0x0000 beginnt. Eben dies geschieht mit dem Linker Parameter -Ttext=0x1800. Die Adresse des Beginns der .text Sektion wird auf die (Byte-)Adresse 0x1800 gesetzt.

Schritt 2 - Einbinden der UART Library von Peter Fleury

Wie Eingangs erwähnt werden wir für die serielle Kommunikation auf der AVR-Seite die UART-library von Peter Fleury verwenden. Nach dem Download werden die uart.c und uart.h in das Projekt eingebunden (für die uart.c im AVRStudio rechte Maustaste auf Source Files und dann Add Existing Source File(s)... und für die uart.h die rechte Maustaste auf Header Files und dann Add Existing Header File(s)...)

Schritt 3 - Programmieren des Bootloaders

Linker Option nach Add
Kompilieren des Bootloaders

Nun soll eine kleine Applikation geschrieben werden. Keine Angst, unser erstes Ziel ist es, eine kleine Anwendung in dem Bootloaderbereich zu positionieren, welche serielle Ein-und Ausgaben handelt. Die eigentliche Bootloaderfunktionalität kommt später dazu. Also schreiben wir die main.c wie folgt:

<c>

  1. include <avr/io.h>
  2. include <avr/interrupt.h>
  3. include <avr/boot.h>
  4. include <util/delay.h>
  5. include "uart.h"
  1. define BOOT_UART_BAUD_RATE 9600 /* Baudrate */
  2. define XON 17 /* XON Zeichen */
  3. define XOFF 19 /* XOFF Zeichen */

int main() {

   unsigned int 	c=0;               /* Empfangenes Zeichen + Statuscode */
   unsigned char	temp,              /* Variable */
                       flag=1,            /* Flag zum steuern der Endlosschleife */

p_mode=0; /* Flag zum steuern des Programmiermodus */

   void (*start)( void ) = 0x0000;        /* Funktionspointer auf 0x0000 */

   /* Interrupt Vektoren verbiegen */
   temp = MCUCR;
   MCUCR = temp | (1<<IVCE);
   MCUCR = temp | (1<<IVSEL);

   /* Einstellen der Baudrate und aktivieren der Interrupts */
   uart_init( UART_BAUD_SELECT(BOOT_UART_BAUD_RATE,F_CPU) ); 
   sei();

   uart_puts("Hallo hier ist der Bootloader\n\r");
   _delay_ms(1000);

   do
   {
       c = uart_getc();
       if( !(c & UART_NO_DATA) )
       {
           switch((unsigned char)c)
           {
                case 'q': 

flag=0;

                    uart_puts("Verlasse den Bootloader!\n\r");
                    break;
                 default:
                    uart_puts("Du hast folgendes Zeichen gesendet: ");
                    uart_putc((unsigned char)c);
                    uart_puts("\n\r");
                    break;
           }
       }
   }
   while(flag);

   uart_puts("Springe zur Adresse 0x0000!\n\r");
   _delay_ms(1000);

   /* Interrupt Vektoren wieder gerade biegen */
   temp = MCUCR;
   MCUCR = temp | (1<<IVCE);
   MCUCR = temp & ~(1<<IVSEL);

   /* Rücksprung zur Adresse 0x0000 */
   start(); 
   return 0;

} </c>

Erklärung des Codes

Beginnen wir mit den Defines: Die Baudrate erklärt sich von selbst. Die Defines XON und XOFF werden später, wenn die Bootloader-Funktionalität dazukommt, zur Flusssteuerung gebraucht. Wir werden also die XON/XOFF-Flussteuerung nutzen (merken für die Einstellung von PuTTY).

Bei den Variablen ist nur eine interessant: Der Funktionspointer

void (*start)( void ) = 0x0000;

ist ein einfacher Trick, um mit dem Programmcounter (PC) zur Adresse 0x0000 zuspringen. Wir definieren einfach eine (fiktive) Funktion an der Stelle 0x0000. Beim Aufruf der Funktion mit

start();

springt der Programmcounter und damit das Programm an Adresse 0x0000 und das Anwendungsprogramm - wenn es eins gibt - kann starten.

Nun folgt ein sehr wichtiger Teil, auf den ich noch eingehen muss - die Interrupt-Vektoren. Interrupt-Vektoren sind Einsprungpunkte der Interrupts, welche normalerweise fest ab Adresse 0x0001 im Flash liegen. Wird ein Interrupt ausgelöst, springt der AVR automatisch zu der festen Flash-Adresse. Vor dort aus - wenn eine ISR programmiert ist - springt der Controller zur ISR (Interrupt Service Routine). Nun haben wir folgendes Problem: Wenn wir den Bootloadercode aber der Adresse 0x1800 ausführen, nützt es uns gar nichts, wenn der AVR nach Auslösen eines Interrupts an die Stellt 0x0001 + X springt, da dieser Speicherbereich ja im Zweifelsfalle sogar von uns überschrieben wird. Unser Code soll nur ab Adresse 0x1800 stehen! Wir müssen also die Sprungtabelle "verbiegen", d.h. den AVR veranlassen, bei Auslösung eines Interrupts an Adresse 0x1801 + X zu springen und dann zur ISR. Die Verbiegung der Sprungtabelle passiert mit dem Setzen des IVSEL-Bits im MCUCR, also so:

temp = MCUCR;
MCUCR = temp | (1<<IVCE);
MCUCR = temp | (1<<IVSEL);

das IVCE-Bit wird nur benötigt, um dem Mikrocontroller zu sagen, dass wir als nächstes den Parameter IVSEL setzen wollen, das Bit wird nachher vom Controller wieder gelöscht.

In der Hauptschleife wird lediglich gepollt,, ob ein neues Zeichen von der Konsole kommt, ist dem so wird das Zeichen ausgewertet (switch). Nach dem Drücken von "q" verlässt der Bootloader die Hauptschleife, setzt die Verbiegung der Interrupt-Vektoren wieder zurück und startet die Hauptanwendung - wenn eine da ist.

Nach dem Kompilieren sagt uns der Linker, dass 754 Byte Programmspeicher und 265 Byte Datenspeicher verbraucht wurde. 754 Byte ist weit unter den 2048 Byte, welche uns ab der Adresse 0x1800 zur Verfügung stehen, wir haben also alles richtig gemacht.

Nun kontrollieren wir noch schnell, ob das Programm an der richtigen Stelle im Flash steht. Mit dem Hex-File (Bootloader.hex) wird auch ein List-File (Bootloader.lss) erzeugt. Im List-File findet sich das disassemblierte Programm und die Speicherzuordnungen, hier ein Auszug der Datei:

Bootloader.elf:     file format elf32-avr

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  0 .data         00000080  00800100  00001a68  000002fc  2**0
                  CONTENTS, ALLOC, LOAD, DATA
  1 .text         00000268  00001800  00001800  00000094  2**1
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  2 .bss          00000085  00800180  00800180  0000037c  2**0
                  ALLOC
  3 .debug_aranges 00000040  00000000  00000000  0000037c  2**0
                  CONTENTS, READONLY, DEBUGGING
  4 .debug_pubnames 00000095  00000000  00000000  000003bc  2**0
                  CONTENTS, READONLY, DEBUGGING
  5 .debug_info   00000459  00000000  00000000  00000451  2**0
                  CONTENTS, READONLY, DEBUGGING
  6 .debug_abbrev 00000238  00000000  00000000  000008aa  2**0
                  CONTENTS, READONLY, DEBUGGING
  7 .debug_line   000003eb  00000000  00000000  00000ae2  2**0
                  CONTENTS, READONLY, DEBUGGING
  8 .debug_frame  000000a0  00000000  00000000  00000ed0  2**2
                  CONTENTS, READONLY, DEBUGGING
  9 .debug_str    000001cf  00000000  00000000  00000f70  2**0
                  CONTENTS, READONLY, DEBUGGING
 10 .debug_loc    0000024a  00000000  00000000  0000113f  2**0
                  CONTENTS, READONLY, DEBUGGING
 11 .debug_ranges 00000048  00000000  00000000  00001389  2**0
                  CONTENTS, READONLY, DEBUGGING

Disassembly of section .text:

00001800 <__vectors>:
    1800:	19 c0       	rjmp	.+50     	; 0x1834 <__ctors_end>
    1802:	33 c0       	rjmp	.+102    	; 0x186a <__bad_interrupt>
    1804:	32 c0       	rjmp	.+100    	; 0x186a <__bad_interrupt>
    1806:	31 c0       	rjmp	.+98     	; 0x186a <__bad_interrupt>
    1808:	30 c0       	rjmp	.+96     	; 0x186a <__bad_interrupt>
    180a:	2f c0       	rjmp	.+94     	; 0x186a <__bad_interrupt>
    180c:	2e c0       	rjmp	.+92     	; 0x186a 

...
(viele Zeilen)
...

0000186c <main>:
#define BOOT_UART_BAUD_RATE     9600     /* Baudrate */
#define XON                     17       /* XON Zeichen */
#define XOFF                    19       /* XOFF Zeichen */
 
int main()
{
    186c:	cf 93       	push	r28
    186e:	df 93       	push	r29
    unsigned char	temp,            /* Variable */
                        flag=1;          /* Flag zum steuern der Endlosschleife */
    void (*start)( void ) = 0x0000;    /* Funktionspointer auf 0x0000 */
 
    /* Interrupt Vektoren verbiegen */
    temp = MCUCR;
    1870:	85 b7       	in	r24, 0x35	; 53
    MCUCR = temp | (1<<IVCE);
    1872:	98 2f       	mov	r25, r24
    1874:	91 60       	ori	r25, 0x01	; 1
    1876:	95 bf       	out	0x35, r25	; 53
    MCUCR = temp | (1<<IVSEL);
    1878:	82 60       	ori	r24, 0x02	; 2
    187a:	85 bf       	out	0x35, r24	; 53

...
(noch mehr Zeilen)

Wir erkennen, dass die Sektion .text ab der Adresse (VMA) 0x1800 beginnt. Des weiteren sehen wir im Disassembly der Sektion .text, dass unser Programm mit der Interrupt-Einsprungstabelle ab Adresse 0x1800 beginnt. Unsere main() beginnt ab Adresse 0x186C. Super, das hat geklappt. Aber nun schnell zu Schritt 4 - dem Flashen und Ausprobieren des Programms...

Schritt 4 - Flashen und Ausprobieren des Bootloaders

Setzen der Fuses
Serielle Konfiguration von PuTTY
Serielle Konfiguration von PuTTY

Nach dem Starten des AVRISPmkII-In-System-Programmers aus dem AVRStudio sollten zunächst die Einstellungen geprüft werden. Die Signatur des AVRs sollte stimmen und die ISP-Frequenz. Im Reiter Program sollte unter Flash die richtige Datei angegeben sein (Bootloader.hex). Danach können wir uns an das Setzen der Fuses machen. CKDIV8 sollte ausgeschalten werden, der interne Takt von 8 Mhz sollte genutzt werden (SUT_CKSEL) und BOOTSZ auf 1024 words gestellt werden. Zusätzlich muß die BOOTRST-Fuse gesetzt werden, damit der Bootloader an der richtigen Adresse anfängt. Für alle, die einen anderen Programmer benutzen (z.B. avrdude), hier die exakten Werte der Fuses:

  • Low Fuse: 0xE2
  • High Fuse: 0xD2
  • Extended Fuse: 0xF8

Jetzt kann PuTTY gestartet und konfiguriert werden. Der Connection type muss auf Serial gestellt werden. Die Baudrate beträgt 9600 Baud. Unter Connection/Serial muss der Flow control auf XON/XOFF gestellt werden. Nach dem Konfigurieren kann die Konsole mit Open geöffnet werden.

Jetzt kann wieder in das AVRStudio gewechselt werden. Mit einem beherztem Druck auf Program wird das Flash im ATmega88 programmiert. Nach dem Wechsel auf die Konsole erscheint folgendes Bild:

Bootloader in PuTTY

Nach dem Drücken von ein paar Tasten erscheint folgendes:

Bootloader nach Tastendruck in PuTTY

Nach dem Drücken von q erscheint folgendes Bild:

Bootloader nach Tastendruck in PuTTY

Der Bootloader versucht, zur Adresse 0x0000 zu springen, wo er allerdings keinen Programmcode findet. Wie auch, wir haben ja den ganzen Flash des AVR gerade gelöscht und mit dem Bootloader gefüllt. Nun muss man wissen, dass in einem gelöschten Flash 0xFF in jeder Speicherzelle steht. Dies bedeutet für den AVR, dass kein gültiger Opcode da ist, d.h. der Programmzähler zählt um eins nach oben. Damit hopst er sozusagen durch den gesamten Flash bis er wieder beim Bootloader landet.

Heureka wir haben es geschafft! Ein Programm wird im Bootloaderbereich des Flashs ausgeführt! Es bootet zwar schon schön, aber es loadet noch nichts in den Flash, aber die halbe Miete haben wir damit schon. Nun schreiben wir erst einmal eine kleine Anwendung, welche wir nach der Erweiterung unseres Bootloaders in den Flash laden.

Eine kleine Test-Anwendung

Erstellen des Projektes
Sourcecode im AVRStudio

Die Kategorie Anwendung möchte ich möglichst kurz halten. Ziel ist es, eine kleine Anwendung zu schreiben, welche dann mit dem (echten) Bootloader ins Flash gespeichert wird.

Schritt 1 - Erstellen des Projektes

Nach dem Erstellen eines neuen Projektes muss in den Projekt-Einstellungen des AVRStudios nur die Taktfrequenz eingetragen werden. Die Linker-Optionen werden nicht verändert, also bleibt wie es ist.

Schritt 2 - Einbinden der UART Library

Dieser Schritt kann vom Bootloader übernommen werden. Es wir wieder die UART-Bibliothek von Peter Fleury verwendet.

Schritt 3 - Programmieren der Anwendung

Wir starten also ein neues AVRStudio und legen ein neues Projekt an, konfigurieren die Taktfrequenz (8 Mhz) und laden die uart.c und uart.h dazu. Nun schreiben wir in die main.c folgende Zeilen:

<c>

  1. include <avr/io.h>
  2. include <avr/interrupt.h>
  3. include <avr/pgmspace.h>
  4. include <util/delay.h>
  5. include "uart.h"
  1. define UART_BAUD_RATE 9600

int main() {

  unsigned int 	c;
  void (*bootloader)( void ) = 0x1800;
  uart_init( UART_BAUD_SELECT(UART_BAUD_RATE,F_CPU) ); 
  sei();
   
  uart_puts_P("\n\rHier ist das Anwendungsprogramm...");

  for(;;)
  {
      c = uart_getc();
      if(!(c & UART_NO_DATA))
      {

switch( (unsigned char)c) { case 'b': uart_puts("\n\rSpringe zum Bootloader..."); _delay_ms(1000); bootloader(); break; default:

                   uart_puts("\n\rDu hast folgendes Zeichen gesendet: ");

uart_putc((unsigned char)c); break; }

       }
   }
   return 0;

}

</c> Erklärung des Codes

Viel interessantes ist nicht an diesem Code. Es gibt wie immer die berühmte Endlosschleife. Wir definieren wieder einen fiktiven Funktionspointer

void (*bootloader)( void ) = 0x1800;

Nach drücken der Taste b soll das Programm wieder zum Bootloader springen.

Nach dem Kompilieren des Programms sehen wir, dass der Programmspeicher mit 686 Byte belegt ist, der Datenspeicher mit 201 Bytes.

Schritt 4 - Ausprobieren der Anwendung

Wer möchte kann die Anwendung auf den AVR flashen und Ausprobieren. Die Funktion sollte sich von allein erschließen. Nun wollen wir uns der Erweiterung des Bootloader widmen.

Das "echte" Bootloader Programm

Programmieren des Bootloaders

Zum Erstellen des echten Bootloaders werden wir wieder Schrittweise vorgehen. Folgende Schritte sind zu befolgen:

Schritt 1 und 2

Schritt 1 und 2 können vom ersten Bootloader übernommen werden.

Schritt 3 - Programmieren des Bootloaders

Nun wollen wir unseren Bootloader erweitern. Nach dem Kompilieren des Anwendungsprogramms erhalten wir eine Datei Anwendung.hex im Intel-HEX-Format. Da wir im Bootloader diese Daten auswerten müssen, wollen wir uns kurz mit dem Format beschäftigen. Das Intel-HEX-Format ist geschaffen worden, um Binärdaten als ASCII-Daten zu übertragen. Jedes Byte ist in Form von zwei ASCII-Zeichen gespeichert, d.h. aus der Zahl 0x4A wird die ASCII-Zeichenfolge "4E". Das bedeutet aber auch, dass aus den Binärdaten die doppelte Anzahl von Zeichen wird, welche übertragen werden müssen, hinzu kommen noch Steuerzeichen und Zusatzinformationen. Jede Zeile in der Intel-HEX-Datei folgt einem bestimmten Schema, in dem u.a. die Anzahl der Bytes, die Zieladresse und Checksumme stehen.

Für weiterführende Erklärungen zum Thema HEX-Datei-Format empfehle ich folgende Lektüre:

Unser Bootloader muss in der Lage sein, dieses Format zu interpretieren. Wir müssen also einen Parser schreiben. Oje werden Manche denken, das ist ja wieder ein Thema für sich. Das stimmt prinzipiell auch. Allerdings kommt uns hier das einfache Format der Intel-Hex-Datei zugute, welches den Aufwand in Grenzen hält.

Also erstes brauchen wir also Funktionen, um die ASCII-Zeichenfolgen wieder in Binärdaten umzuwandeln. Normalerweise könnte man dafür die C-Funktion strtol aus der stdlib.h nehmen. Allerdings würde das Benutzen dieses Befehls das Inkludieren der Standardblibliothek nach sich ziehen und damit den Code unnötig aufblähen. Daher werden werden wir uns eine einfache eigene Methode schreiben, um die Zeichenfolgen umzuwandlen. In der HEX-Datei kommen 2 Byte und 4 Byte Hex-Zahlen im ASCII-Format vor. Wir brauchen also eine Funktion, welche die ASCII-Zeichen in Zahlen umwandelt, hier ist sie: <c> static uint16_t hex2num(unsigned char *ascii, uint8_t num) { uint8_t i; uint16_t var=0, erg=0, mult[4] = {1, 16, 256, 4096};

for(i=0; i<num; i++) { if(ascii[i]<65) var = ascii[i] - 48; /* 0..9 */ if(ascii[i]>64) var = ascii[i] - 55; /* A..F */ if(ascii[i]>96) var = ascii[i] - 87; /* a..f */ erg += (uint16_t)(var * mult[num-i-1]); } return erg; } </c> Wir benutzen hier einen sehr einfachen Ansatz, um die Zahlen zu generieren. Die Funktionen wandeln die ASCII-Zeichen entsprechend ihrer Wertigkeit in Zahlen um. Soll z.B. das ASCII-Zeichen "5" umgewandelt werden, wird vom ASCII-Code der "5", also dezimal 53, 48 abgezogen: 53 - 48 = 5, somit haben wir ein ASCII-Zeichen in eine Zahl umgewandelt. Wen das ASCII-Zeichen "A" ist (Dezimal: 65), werden 55 abgezogen, da das A ja schon die Wertigkeit 10 hat: 65 - 55 = 10 usw. Danach werden die Zahlen noch mit den Wertigkeiten der Stellen multipliziert, also 160, 161, 162 uund , 163. Näher möchte an dieser Stelle nicht darauf eingehen, wir wollen schnell weiter zum Beschreiben des Flashs kommen.

Um in den Flash zu schreiben, werden wir Makros aus der boot.h der AVR Libc verwenden. Hier finden wir alle Werkzeuge, die wir brauchen. Wir wollen uns vor allen das API Usage Example in der Online Doku anschauen. Diese Funktion wollen wir weitestgehend übernehmen, da sie alles beinhaltet, was wir brauchen. Hier ist die Funktion: <c> void boot_program_page(uint32_t page, uint8_t *buf) {

   uint16_t i;
   uint8_t sreg;
   /* Disable interrupts.*/
   sreg = SREG;
   cli();
   eeprom_busy_wait ();
   boot_page_erase (page);
   boot_spm_busy_wait ();      /* Wait until the memory is erased. */
   for (i=0; i<SPM_PAGESIZE; i+=2)
   {
       /* Set up little-endian word. */
       uint16_t w = *buf++;
       w += (*buf++) << 8;
   
       boot_page_fill (page + i, w);
   }
   boot_page_write (page);     /* Store buffer in flash page.		*/
   boot_spm_busy_wait();       /* Wait until the memory is written.*/
   /* Reenable RWW-section again. We need this if we want to jump back */
   /* to the application after bootloading. */
   boot_rww_enable ();
   /* Re-enable interrupts (if they were ever enabled). */
   SREG = sreg;

} </c> Als erstes fällt auf, dass man der Funktion die Page-Adresse übergibt. Es wird also immer seitenweise geschrieben, dies ist eine Spezialität des Flash-Speichers. Es muss immer die gesamte Seite geschrieben werden, dafür gibt es einen Page-Puffer, welcher die Daten enthält, welche mit der nächsten Schreiboperation in die entsprechende Page geschrieben werden. Die wesentlichen Funktionen der Routine sind boot_page_erase(page), boot_page_fill(page + i, w) und boot_page_write(page). Nicht zu vergessen auch boot_spm_busy_wait(). Die Bedeutung der Funktionen (naja es sind eher Makros) findet man in der Doku. Im wesentlichen läuft das Schreiben einer Page so ab:

  • Page löschen
  • Page-Puffer befüllen (aus der Variable buf)
  • Page schreiben

So einfach, so gut. Für den Bootloader bedeutet das, dass er die Daten sammeln muß, bis er genügend Daten für eine Page hat. Dann wird eine Page geschrieben und der Spaß fängt von vorn an.

Mit diesen beiden Funktionen sind wir nun in der Lage, den Parser zu schreiben. Die main.c sieht folgt aus: <c>

  1. include <string.h>
  2. include <avr/io.h>
  3. include <avr/interrupt.h>
  4. include <avr/boot.h>
  5. include <util/delay.h>
  6. include "uart.h"
  1. define BOOT_UART_BAUD_RATE 9600 /* Baudrate */
  2. define XON 17 /* XON Zeichen */
  3. define XOFF 19 /* XOFF Zeichen */
  4. define START_SIGN ':' /* Hex-Datei Zeilenstartzeichen */

/* Zustände des Bootloader-Programms */

  1. define BOOT_STATE_EXIT 0
  2. define BOOT_STATE_PARSER 1

/* Zustände des Hex-File-Parsers */

  1. define PARSER_STATE_START 0
  2. define PARSER_STATE_SIZE 1
  3. define PARSER_STATE_ADDRESS 2
  4. define PARSER_STATE_TYPE 3
  5. define PARSER_STATE_DATA 4
  6. define PARSER_STATE_CHECKSUM 5
  7. define PARSER_STATE_ERROR 6

void program_page(uint32_t page, uint8_t *buf) {

   uint16_t i;
   uint8_t sreg;
   /* Disable interrupts.*/
   sreg = SREG;
   cli();
   eeprom_busy_wait ();
   boot_page_erase (page);
   boot_spm_busy_wait ();      /* Wait until the memory is erased. */
   for (i=0; i<SPM_PAGESIZE; i+=2)
   {
       /* Set up little-endian word. */
       uint16_t w = *buf++;
       w += (*buf++) << 8;
   
       boot_page_fill (page + i, w);
   }
   boot_page_write (page);     /* Store buffer in flash page.		*/
   boot_spm_busy_wait();       /* Wait until the memory is written.*/
   /* Reenable RWW-section again. We need this if we want to jump back */
   /* to the application after bootloading. */
   boot_rww_enable ();
   /* Re-enable interrupts (if they were ever enabled). */
   SREG = sreg;

}

static uint16_t hex2num(unsigned char *ascii, uint8_t num) { uint8_t i; uint16_t var=0, erg=0, mult[4] = {1, 16, 256, 4096};

for(i=0; i<num; i++) { if(ascii[i]<65) var = ascii[i] - 48; /* 0..9 */ if(ascii[i]>64) var = ascii[i] - 55; /* A..F */ if(ascii[i]>96) var = ascii[i] - 87; /* a..f */ erg += (uint16_t)(var * mult[num-i-1]); } return erg; }

int main() {

   unsigned int    c = 0;                             /* Empfangenes Zeichen + Statuscode */
   uint16_t	    hex_addr = 0,                      /* Intel-HEX Zieladresse */
                   flash_page = 0,                    /* Zu schreibende Flash-Page */
                   hex_check = 0;                     /* Intel-HEX Checksumme zum Überprüfen des Daten */
   unsigned char   temp,                              /* Variable */
                   boot_state = BOOT_STATE_EXIT,	   /* Flag zum steuern des Programmiermodus */
                   parser_state = PARSER_STATE_START, /* Empfangszustandssteuerung */
                   flash_page_flag = 1,               /* Flag zum ermitteln einer neuen Flash-Page */
                   flash_cnt = 0,	                   /* Positions zum Schreiben in den Datenpuffer */
                   flash_data[SPM_PAGESIZE],          /* Datenpuffer für die Hexdaten*/
                   hex_cnt = 0,                       /* Position zum Schreiben in den HEX-Puffer */
                   hex_buffer[5],                     /* Puffer für die Umwandlung der ASCII in Binärdaten */
                   hex_size = 0,                      /* Intel-HEX Datenlänge */
                   hex_data_cnt = 0,                  /* Zähler für die empfangenen HEX-Daten einer Zeile */
                   hex_type = 0,                      /* Intel-HEX Recordtype */
                   hex_checksum=0;			           /* empfangene HEX-Checksumme */
   void (*start)( void ) = 0x0000;                    /* Funktionspointer auf 0x0000 */
   memset(hex_buffer, 0x00, sizeof(hex_buffer));
   memset(flash_data, 0xFF, sizeof(flash_data));

   /* Interrupt Vektoren verbiegen */
   temp = MCUCR;
   MCUCR = temp | (1<<IVCE);
   MCUCR = temp | (1<<IVSEL);

   /* Einstellen der Baudrate und aktivieren der Interrupts */
   uart_init( UART_BAUD_SELECT(BOOT_UART_BAUD_RATE,F_CPU) ); 
   sei();

   uart_puts("Hallo hier ist der echte Bootloader\n\r");
   _delay_ms(2000);

   do
   {
       c = uart_getc();
       if( !(c & UART_NO_DATA) )
       {

if(boot_state == BOOT_STATE_PARSER) /* Programmzustand: Parser */ { switch(parser_state) { case PARSER_STATE_START: /* Warte auf Zeilen-Startzeichen */ if((unsigned char)c == START_SIGN) { uart_putc(XOFF); parser_state = PARSER_STATE_SIZE; hex_cnt = 0; hex_check = 0; uart_putc(XON); } break; case PARSER_STATE_SIZE: /* Parse Datengröße */ hex_buffer[hex_cnt++] = (unsigned char)c; if(hex_cnt == 2) { uart_putc(XOFF); parser_state = PARSER_STATE_ADDRESS; hex_cnt = 0; hex_size = (uint8_t)hex2num(hex_buffer, 2); hex_check += hex_size; uart_putc(XON); } break; case PARSER_STATE_ADDRESS: /* Parse Zieladresse */ hex_buffer[hex_cnt++] = (unsigned char)c; if(hex_cnt == 4) { uart_putc(XOFF); parser_state = PARSER_STATE_TYPE; hex_cnt = 0; hex_addr = hex2num(hex_buffer, 4); hex_check += (uint8_t)((hex_addr & 0xFF00) >> 8); hex_check += (uint8_t)(hex_addr & 0x00FF); if(flash_page_flag) { flash_page = (uint16_t)((hex_addr / SPM_PAGESIZE) * SPM_PAGESIZE); flash_page_flag = 0; } uart_putc(XON); } break; case PARSER_STATE_TYPE: /* Parse Zeilentyp */ hex_buffer[hex_cnt++] = (unsigned char)c; if(hex_cnt == 2) { uart_putc(XOFF); hex_cnt = 0; hex_data_cnt = 0; hex_type = (uint8_t)hex2num(hex_buffer, 2); hex_check += hex_type; switch(hex_type) { case 0: parser_state = PARSER_STATE_DATA; break; case 1: parser_state = PARSER_STATE_CHECKSUM; break; default: parser_state = PARSER_STATE_DATA; break; } uart_putc(XON); } break; case PARSER_STATE_DATA: /* Parse Flash-Daten */ hex_buffer[hex_cnt++] = (unsigned char)c; if(hex_cnt == 2) { uart_putc(XOFF); uart_putc('.'); hex_cnt = 0; flash_data[flash_cnt] = (uint8_t)hex2num(hex_buffer, 2); hex_check += flash_data[flash_cnt]; flash_cnt++; hex_data_cnt++; if(hex_data_cnt == hex_size) { parser_state = PARSER_STATE_CHECKSUM; hex_data_cnt=0; hex_cnt = 0; } if(flash_cnt == SPM_PAGESIZE) /* Puffer voll -> schreibe Page */ { uart_puts("P\n\r"); _delay_ms(100); program_page((uint16_t)flash_page, flash_data); memset(flash_data, 0xFF, sizeof(flash_data)); flash_cnt = 0; flash_page_flag = 1; } uart_putc(XON); } break; case PARSER_STATE_CHECKSUM: /* Parse Checksumme */ hex_buffer[hex_cnt++] = (unsigned char)c; if(hex_cnt == 2) { uart_putc(XOFF); hex_checksum = (uint8_t)hex2num(hex_buffer, 2); hex_check += hex_checksum; hex_check &= 0x00FF; if(hex_type == 1) /* Dateiend -> schreibe restliche Daten */ { uart_puts("P\n\r"); _delay_ms(100); program_page((uint16_t)flash_page, flash_data); boot_state = BOOT_STATE_EXIT; } if(hex_check == 0) parser_state = PARSER_STATE_START; else parser_state = PARSER_STATE_ERROR; uart_putc(XON); } break; case PARSER_STATE_ERROR: uart_putc('#'); break; default: break;

} } else if(boot_state != BOOT_STATE_PARSER) /* Programmzustand: UART Kommunikation */ { switch((unsigned char)c) { case 'p':

                            boot_state = BOOT_STATE_PARSER;

uart_puts("Programmiere den Flash!\n\r"); uart_puts("Kopiere die Hex-Datei und füge sie hier ein (rechte Maustaste)\n\r"); break; case 'q':

                            boot_state = BOOT_STATE_EXIT;

uart_puts("Verlasse den Bootloader!\n\r"); break; default: uart_puts("Du hast folgendes Zeichen gesendet: "); uart_putc((unsigned char)c); uart_puts("\n\r"); break; } }

       }
   }
   while(boot_state!=BOOT_STATE_EXIT);

   uart_puts("Springe zur Adresse 0x0000!\n\r");
   _delay_ms(1000);

   /* Interrupt Vektoren wieder gerade biegen */
   temp = MCUCR;
   MCUCR = temp | (1<<IVCE);
   MCUCR = temp & ~(1<<IVSEL);

   /* Rücksprung zur Adresse 0x0000 */
   start(); 
   return 0;

}

</c> Erklärung des Codes

Der Bootloader wird als Zustandsmaschine programmiert. Die Variable boot_state beinhaltet den aktuelle Programmzustand. Für den Parser gibt es eine eigene Zustandsvariable parser_state, welche die einzelne Zustände des Parsers hält. Mit jeder HEX-Datei-Zeile läuft der Parser einmal durch alle Zustände. Nach Abarbeiten des aktuellen Zustands und Auswertung der empfangenen Daten wird der nächste Zustand aktiviert usw. Für das Verarbeiten der Datei wird jedes Mal die Kommunikation angehalten und ein XOFF gesendet. Nach Abarbeitung der Daten gibt der Parser den seriellen Empfang wieder frei (XON). Es ist also wichtig, dass in PuTTY die XON/XOFF-Flußkontrolle aktiviert wird! Durch den interruptgesteuerten Empfang mit einem 32 Byte tiefen Empfangspuffer geht kein Byte verloren. Am Ende einer HEX-Datei-Zeile wird die Checksumme ausgewertet. Bei falscher Checksumme springt der Parser in den PARSER_STATE_ERROR, aus dem er nicht mehr rauskommt. Somit wird kein weiteres Byte in den Flash geschrieben. Zusätzlich könnte man noch implementieren, dass der Flash wieder gelöscht wird, wenn falsche Daten empfangen wurden, damit kein unvollständiges Programm im Flash steht. Einschränkend muss noch festegehalten werden, das der Bootloader in dieser Fassung einn Adressraum bis 64K unterstützt (HEX-Zeilentyp 1). Die erweiterten Adressräume (HEX-Zeilentyp 2 bis 5) werden noch nicht unterstützt. Für ATmega-Devices mit > 64K Flash muss der Bootloader noch erweitert werden.

Nach dem Kompilieren beträgt die Größe des Bootloaders 1910 Byte, wir sind also unterhalb der 2048 Byte die wird "verbraten" können. Der Datenspeicher wird mit 307 Byte belastet.

Die Funktion des Bootloaders sieht wie folgt aus: Nach dem Reset wartet der Bootloader 2 Sekunden auf Eingaben (_delay_ms(2000);). Falls keine Eingaben von der Konsole kommen, springt der Bootloader zur Anwendung. Damit ist gewährleiset, dass die Anwendung später automatisch startet, auch wenn wir keine Taste drücken. Wird ein p gedrückt, springt der Bootloader in den BOOT_STATE_PARSER und erwartet eine HEX-Datei auf der Konsole. Dies ist der Zeitpunkt, die HEX-Datei der Anwendung mit Copy & Paste in die Konsole zu schreiben. Zum Einfügen von Daten aus dem Zwischenspeicher in die Konsole wird bei PuTTY die rechte Maustaste verwendet.

Schritt 4 - Flashen und Ausprobieren des Bootloaders

Nach dem Flashen via AVRISPmkII startet der Bootloader in der Konsole. Da noch kein Anwendungsprogramm im Flash liegt, startet der Bootloader nach 2 Sekunden immer wieder neu. Nach Drücken der Taste p erwartet der Bootlader die HEX-Datei der Anwendung. Nach dem Einfügen der Datei in Konsole erscheint für jedes empfangene Byte ein Punkt ("."). Das Beschreiben einer Flash-Page kenzeichnet ein P. Nach dem erfolgreichen Flashen startet die Anwendung automatisch.

PuTTY: Bootloader nach dem Reset

Zusammenfassung

Wer den Artikel bis hier hin nachvollzogen hat, ist jetzt in der Lage einen Bootloader selbst zu schreiben. Dabei kann der Bootloader an jede in Frage kommenden ATmega-Plattform angepaßt werden. Dazu muss

  • die Linkereinstellung für das Verschieben der Sektion .text angepaßt werden und (-Ttext = 0xXXXX)
  • evtl. der virtuelle Funktionspointer in der Anwendung geändert werden (falls en Rücksprung zum Bootloader gewünscht ist void (*bootloader)(void) = 0xXXXX).

Um dies zu erleichtern, habe ich hier die Sprungadressen für ausgewählte AVRs tabellarisch zusammengetragen. Dabei wurde immer die maximale Größe des Bootloader betrachtet, also BOOTSZ0=0 und BOOTSZ1=0:

Boot Size Konfiguration
Device Flash-Größe
für Applikation
Flash-Größe
des Bootloaders
Startadresse des Bootloaders
(Byteadresse)
ATmega8 6144 Byte 2048 Byte 0x1800
ATmega 16 14336 Byte 2048 Byte 0x3800
ATmega32 28672 Byte 4096 Byte 0x7000
ATmega64 57344 Byte 8192 Byte 0xE000
ATmega128 122880 Byte 8192 Byte 0x1E000
ATmega88 6144 Byte 2048 Byte 0x1800
ATmega168 14336 Byte 2048 Byte 0x3800
ATmega328 28672 Byte 4096 Byte 0x7000
ATmega640 57344 Byte 8192 Byte 0xE000
ATmega1280/1 122880 Byte 8192 Byte 0x1E000
ATmega2560/1 253952 Byte 8192 Byte 0x3E000

Somit kann jeder den Bootloader nach seinen Wünschen anpassen. Eine gägnige Praxis ist auch, ein kleines PC-Programm zu schreiben, welches dem Bootloader die Flash-Daten gleich Binär übergibt. Das geht schneller und spart das aufwendige Parsen. Eine weitere Idee ist es, den Bootloader so anzupassen, das er sich wie ein STK500 an der seriellen Schnitstelle verhält. Dazu muss man die AP068 von Atmel umsetzen. Auch dies sollte nach dem Studium des Tutorial kein Problem mehr sein :)

Der Artikel ist noch nicht ganz fertig (aber fast)...

Nachricht an den Author