AVR Bootloader in C - eine einfache Anleitung
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 dieses Tutorial. Es soll anhand von Beispielen einen möglichst einfachen, verständlichen und nachvollziehbaren Weg zeigen, sich mit Hilfe der Hochsprache C in das Thema einzuarbeiten (dabei soll weder Assembler noch Inline-Assembler verwendet werden).
Vielleicht werden einige meinen dass es nicht möglich ist das Thema ohne tieferen Einblick in die Hardware und die AVR-Register zu beleuchten, ich möchte es aber trotzdem versuchen.
Der Artikel wird sich auf das notwendige Wissen beschränken, um mit Booloadern arbeiten zu können. Es wird ein genereller Weg gezeigt, der sich leicht auf andere AVR-Devices (mit Bootloader Sektion) übertragen lässt. Die Codebeispiele wurden für den ATmega88 kompiliert und getestet.
Einleitung
Zu Beginn soll das notwendige Wissen über die Bootloaderunterstützung im AVR vermittelt werden, um eine Arbeitsgrundlage zu schaffen.
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 Schnittstelle zu laden, ins Flash zu programmieren und zu starten.
Der Leser sollte bereits Erfahrungen im Umgang mit dem AVR Studio und der Programmiersprache C gemacht haben und schon Anwendungen geschrieben haben. Für absolute Einsteiger ist der Artikel ungeeignet.
Den Thread zum Artikel gibt es hier: http://www.mikrocontroller.net/topic/195102
Software
Für den Artikel werden folgende Software-Pakete benötigt:
- aktuelles AVR Studio (hier verwendet: AVR Studio v4.18)
- aktuelles WinAVR (hier verwendet: WinAVR20100110)
- PuTTY als serielle Konsole (Version v0.6)
Des Weiteren wurde auf der AVR-Seite für die serielle Kommunikation mit dem PC auf die 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 FT232 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.
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 AVR Studio 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 AVR Studio.
Für das Verständnis der Hardware und der seriellen Kommunikation sind folgende Artikel empfehlenswert:
Grundlagen
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 AVR Studio-Benutzer, muss ich mich jetzt mit makefiles beschäftigen? Kann man im AVR Studio mit C überhaupt einen Bootloader schreiben? Vielleicht hat sich der eine oder andere schon einmal diese oder ähnliche Fragen gestellt.
Der Programmcode des AVR steht in seinem Flashspeicher und wird von dort ausgeführt. Normalerweise kann während der Ausführung des Programms nicht auf den Flashspeicher geschrieben werden. Dies ist auch einleuchtend da sich das Programm ja sonst selbst überschreiben oder löschen könnte. Das Beschreiben des Flashs erfolgt beim AVR üblicherweise über die ISP-Schnittstelle, dabei befindet sich der Controller im Reset und es wird kein Programm ausgeführt. Dies ist für Prototyping und kleine Anwendungen hinnehmbar. Ist der Controller allerdings in einem größeren System oder in größerer räumlicher Entfernung verbaut und die ISP-Schnittstelle nicht mehr zugänglich, ist ein Update der Firmware nicht mehr ohne Weiteres möglich oder sehr teuer und aufwendig. Hier kann ein Bootloader Abhilfe schaffen, in dem er das Anwendungsprogramm auf einer definierten Schnittstelle entgegennimmt (UART, I2C, Wireless) und ins Flash transferiert. Ein Bootloader ist also in erster Linie ein kleines Programm, welches in einem besonderen 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 Teile des Flashs - der sogenannten Application Flash Section - zu schreiben. Die eigentliche Anwendung wird ausschließlich in der Application Flash Section ausgeführt. 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.
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 Multiplikation der Page-Größe mit der Anzahl der Pages ergibt die Speichergröße. Die Größe einer Page steht im Datenblatt und ist in Words - also Datenworte - angegeben. Ein Datenwort entspricht zwei Bytes. Hier offenbart sich eine Tücke des Datenblatts: Alle Speicherbezüge und Adressen 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.
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. 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.
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 festgelegt, dass der AVR nach dem Reset an die Startadresse der Bootloader Sektion im Flash springt. Auf das IVSEL-Bit (keine Fuse) möchte ich erst an späterer Stelle - wenn es um Interrupts geht - zurückkommen.
Noch ein wichtiger Hinweis für die Werte der Fuses im Datenblatt: Der Wert "0" bedeutet, dass die Fuse programmiert ist, es entspricht dem Häkchen im AVR Studio!
Der "Hallo Welt" - Bootloader
Wie oben erwähnt, wird für die Erstellung des Codes die kostenlose IDE von Atmel - das AVRStudio - benutzt. Ergänzt wird es durch C-Compiler und Tools des WinAVR Projektes. Des weiteren wird zur seriellen Kommunikation das Terminalprogramm benötigt. Im Tutorial wird PuTTY verwendet und sollte installiert sein. Die Hardware ist aufgebaut und via AVRISPmkII-Programmer an den PC angeschlossen - nun kann es losgehen!
Zu Beginn wird ein neues AVRStudio-Projekt erstellt. Danach werden folgende Schritte abgearbeitet:
Schritt 1 - Konfiguration der Projekteinstellungen
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. Dem Linker muss mitgeteilt werden, in welche Speicher er den Programmcode linken soll. Die Lokalisierung des Speichers sind die Sektionen. Die Sektion .text ist dem ausführbaren Programmcode - also den Befehlen - vorbehalten 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 es verschiedene Methoden, dem Linker mitzuteilen, dass man den Programmcode an die Stelle des Bootloaderbereichs 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 bereits erwähnt, wird für die serielle Kommunikation auf der AVR-Seite die UART-Library von Peter Fleury verwendet. Nach dem Download werden die uart.c und uart.h in das Projekt eingebunden (für die uart.c im AVR Studio 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
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 behandelt. Die eigentliche Bootloaderfunktionalität kommt später dazu. Also schreiben wir die Datei main.c wie folgt:
Achtung! Bitte unbedingt die Bezeichnung des SFRs für das Umschalten der Interruptvektoren beachten: bei Atmega88: MCUCR, bei Atmega8: GICR (s. auch etwas weiter unten)
#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/boot.h>
#include <util/delay.h>
#include "uart.h"
#define BOOT_UART_BAUD_RATE 9600 /* Baudrate */
#define XON 17 /* XON Zeichen */
#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 */
char sregtemp = SREG;
cli();
temp = MCUCR;
MCUCR = temp | (1<<IVCE);
MCUCR = temp | (1<<IVSEL);
SREG = sregtemp;
/* 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);
/* vor Rücksprung eventuell benutzte Hardware deaktivieren
und Interrupts global deaktivieren, da kein "echter" Reset erfolgt */
/* Interrupt Vektoren wieder gerade biegen */
cli();
temp = MCUCR;
MCUCR = temp | (1<<IVCE);
MCUCR = temp & ~(1<<IVSEL);
/* Rücksprung zur Adresse 0x0000 */
start();
return 0;
}
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 zu springen. 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. Eine Besonderheit ist zu beachten, wenn der verfügbare Flash-Speicher größer als 128kB ist. In der Regel wird der Bootloader im oberen Teil des Speichers liegen und daher ist ein einfacher Rücksprung mit normaler 2Byte Wordadressierung nicht möglich. Leider hat gcc hier einen Bug und verwendet das vom Controller zusätzlich herangezogene Register EIND nicht. Dieses Register muß daher explizit vor dem Rücksprung auf 0 gesetzt werden. Also
EIND = 0; start();
Ohne diesen Zusatz kann der Bootloader im Normalfall nicht mehr verlassen werden.
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. Von dort aus - wenn eine ISR programmiert ist - springt der Controller zur ISR (Interrupt Service Routine). Nun haben wir folgendes Problem: Wenn wir den Bootloadercode ab der Adresse 0x1800 ausführen, nützt es uns gar nichts, wenn der AVR nach Auslösen eines Interrupts an die Stelle 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. Das Verbiegen der Sprungtabelle passiert mit dem Setzen des IVSEL-Bits im MCUCR (ACHTUNG: beim Atmega8 GICR), also
beim Atmega88:
temp = MCUCR; MCUCR = temp | (1<<IVCE); MCUCR = temp | (1<<IVSEL);
bzw. beim Atmega8:
temp = GICR; GICR = temp | (1<<IVCE); GICR = 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. Um versehentliches verstellen der Interrupttabelle zu vermeiden muss das setzen von IVCE und IVSEL innerhalb von 4 Taktzyklen erfolgen. Um dies zu gewährleisten müssen alle interrupts während des Setzens deaktiviert sein. ACHTUNG Stolperfalle: Die Variable temp wird benötigt, da beim Setzen von IVSEL gleichzeitig IVCE gelöscht werden muss:
bei Atmega8/16:
GICR |= (1<<IVCE); // noch richtig. IVCE wird gesetzt GICR |= (1<<IVSEL); // falsch! IVSEL wird zwar gesetzt, IVCE bleibt jedoch in diesem Prozessortakt gesetzt.
In der Hauptschleife wird lediglich gepollt, ob ein neues Zeichen von der Konsole kommt. Nach dem Empfang eines Zeichens wird es ausgewertet (switch). Nach dem Drücken von "q" verlässt der Bootloader die Hauptschleife, setzt die 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. Weiter 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
Nach dem Start des AVRISPmkII-In-System-Programmers aus dem AVRStudio werden zunächst die Einstellungen geprüft. Die Signatur des AVRs muss stimmen und die ISP-Frequenz. Im Reiter Program muss 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 AVR Studio gewechselt werden. Mit einem beherztem Druck auf Program wird das Flash im ATmega88 programmiert. Nach dem Wechsel auf die Konsole erscheint folgendes Bild:
Nach dem Drücken von ein paar Tasten erscheint folgendes:
Nach dem Drücken von q erscheint folgendes Bild:
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. 0xFF ist für den AVR kein gültiger Opcode. Der Programmzähler zählt nur 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-Speicher laden.
Die Test-Anwendung
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 AVR Studios 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 wird wieder die UART-Bibliothek von Peter Fleury verwendet.
Schritt 3 - Programmieren der Anwendung
Wir starten also ein neues AVR Studio 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:
#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/pgmspace.h>
#include <util/delay.h>
#include "uart.h"
#define UART_BAUD_RATE 9600
int main()
{
unsigned int c;
void (*bootloader)( void ) = 0x0C00; // Achtung Falle: Hier Word-Adresse
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;
}
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 auf die Word-Adresse des Bootloaders. (Hier verhält sich der AVR-GCC leider etwas inkonsistent, da sonst bei Flash-Adressen mit Bytes gearbeitet wird. Gibt man hier versehentlich die Byteadresse an, kann es sein, dass der Sprung in den Bootloader klappt, es muss aber nicht funktionieren, da das Sprungziel undefiniert ist. Verwendet man stattdessen einen JMP befehl im Inline-Assembler, so ist die Byte-Adresse für das Sprungziel anzugeben.)
void (*bootloader)( void ) = 0x0C00;
Nach drücken der Taste b soll das Programm wieder zum Bootloader springen. Leider haben wir auch hier ein Problem, wenn der verfügbare Flash-Speicher größer als 128kB ist. Denn selbst wenn man dem Funktionspointer z.B. 0x1F000 (kleinste Bootloader Startadresse bei 256kB Devices) als Adresswert zuweist, werden nur die unteren zwei Bytes verwendet. Der Sprung in den Bootloader würde also nach 0xF000 gehen und man würde bei Applikationen, die größer sind als 120kB, irgendwo mitten im Code landen. Um sicher zu sein, daß an die korrekte Startadresse gesprungen wird, muß vor dem Sprung das EIND Register explizit auf 1 gesetzt werden. (Im Gegensatz zum Rücksprung mit EIND = 0) Also
EIND = 1; bootloader();
Das EIND Register wird vom Controller bei extended calls oder jumps als höchstwertigstes Adressbyte verwendet, aber leider vom gcc Compiler in der aktuellen Version nicht unterstützt. Siehe [1]
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 selbst erschließen.
Achtung: Es muss darauf geachtet werden, dass beim flashen der Anwendung nicht der Bootloader überschrieben wird.
Bei Verwendung von avrdude muss dazu die Option "-D" angegeben werden (Flash-Speicher nicht automatisch löschen).
Nun wollen wir uns der Erweiterung des Bootloaders widmen.
Der "echte" Bootloader
Zum Erstellen des Bootloaders wird wieder Schrittweise vorgegangen. Folgende Schritte sind zu befolgen:
Schritt 1 und 2 - siehe "Hallo Welt" Bootloader
Schritt 1 und 2 können vom "Hallo Welt" Bootloader übernommen werden. Es sind wieder die korrekte Taktfrequenz und die Verschiebung der Sektion .text auf die Bootresetadresse einzustellen.
Schritt 3 - Programmieren des Bootloaders
Nun soll der Bootloader erweitert werden. 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 "4A". 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.
Als 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 Linken der Standardbibliothek nach sich ziehen und damit den Code unnötig aufblähen. Daher werden wir uns eine einfache eigene Funktion schreiben, um die Zeichenfolgen umzuwandeln. In der HEX-Datei kommen 2 Byte und 4 Byte Hex-Zahlen im ASCII-Format vor. Wir brauchen also eine Funktion, welche die ASCII-Zeichenfolgen in Zahlen umwandelt, hier ist sie:
static uint16_t hex2num(const uint8_t * ascii, uint8_t num)
{
uint8_t i;
uint16_t val = 0;
for (i=0; i<num; i++)
{
uint8_t c = ascii[i];
/* Hex-Ziffer auf ihren Wert abbilden */
if (c >= '0' && c <= '9') c -= '0';
else if (c >= 'A' && c <= 'F') c -= 'A' - 10;
else if (c >= 'a' && c <= 'f') c -= 'a' - 10;
val = 16 * val + c;
}
return val;
}
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 '1' umgewandelt werden, wird vom ASCII-Code '1', also dezimal 49, 48='0' abgezogen: 49 - 48 = 1, somit haben wir ein ASCII-Zeichen in eine Zahl umgewandelt. Wenn das ASCII-Zeichen 'C' ist (Dezimal: 67), werden 'A' - 10 = 65 -10 = 55 abgezogen, um 12 zu erhalten, den Wert der hex-Ziffer C. 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 findet man alle Werkzeuge, die wir brauchen. Dabei sollte vor allen das API Usage Example in der Online Doku näher betrachtet werden. Dieses Beispiel soll weitestgehend übernommen werden, da es die nötige Funktionalität beinhaltet. Hier ist die Funktion:
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;
}
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. Dabei werden die Daten Wortweise in den Page-Puffer geschrieben. 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 Dokumentation der AVR Libc. 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 wie folgt aus:
#include <string.h>
#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/boot.h>
#include <util/delay.h>
#include "uart.h"
#define BOOT_UART_BAUD_RATE 9600 /* Baudrate */
#define XON 17 /* XON Zeichen */
#define XOFF 19 /* XOFF Zeichen */
#define START_SIGN ':' /* Hex-Datei Zeilenstartzeichen */
/* Zustände des Bootloader-Programms */
#define BOOT_STATE_EXIT 0
#define BOOT_STATE_PARSER 1
/* Zustände des Hex-File-Parsers */
#define PARSER_STATE_START 0
#define PARSER_STATE_SIZE 1
#define PARSER_STATE_ADDRESS 2
#define PARSER_STATE_TYPE 3
#define PARSER_STATE_DATA 4
#define PARSER_STATE_CHECKSUM 5
#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 (const uint8_t * ascii, uint8_t num)
{
uint8_t i;
uint16_t val = 0;
for (i=0; i<num; i++)
{
uint8_t c = ascii[i];
/* Hex-Ziffer auf ihren Wert abbilden */
if (c >= '0' && c <= '9') c -= '0';
else if (c >= 'A' && c <= 'F') c -= 'A' - 10;
else if (c >= 'a' && c <= 'f') c -= 'a' - 10;
val = 16 * val + c;
}
return val;
}
int main()
{
/* Empfangenes Zeichen + Statuscode */
uint16_t c = 0,
/* Intel-HEX Zieladresse */
hex_addr = 0,
/* Zu schreibende Flash-Page */
flash_page = 0,
/* Intel-HEX Checksumme zum Überprüfen des Daten */
hex_check = 0,
/* Positions zum Schreiben in der Datenpuffer */
flash_cnt = 0;
/* temporäre Variable */
uint8_t temp,
/* Flag zum steuern des Programmiermodus */
boot_state = BOOT_STATE_EXIT,
/* Empfangszustandssteuerung */
parser_state = PARSER_STATE_START,
/* Flag zum ermitteln einer neuen Flash-Page */
flash_page_flag = 1,
/* Datenpuffer für die Hexdaten*/
flash_data[SPM_PAGESIZE],
/* Position zum Schreiben in den HEX-Puffer */
hex_cnt = 0,
/* Puffer für die Umwandlung der ASCII in Binärdaten */
hex_buffer[5],
/* Intel-HEX Datenlänge */
hex_size = 0,
/* Zähler für die empfangenen HEX-Daten einer Zeile */
hex_data_cnt = 0,
/* Intel-HEX Recordtype */
hex_type = 0,
/* empfangene HEX-Checksumme */
hex_checksum=0;
/* Funktionspointer auf 0x0000 */
void (*start)( void ) = 0x0000;
/* Füllen der Puffer mit definierten Werten */
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) )
{
/* Programmzustand: Parser */
if(boot_state == BOOT_STATE_PARSER)
{
switch(parser_state)
{
/* Warte auf Zeilen-Startzeichen */
case PARSER_STATE_START:
if((uint8_t)c == START_SIGN)
{
uart_putc(XOFF);
parser_state = PARSER_STATE_SIZE;
hex_cnt = 0;
hex_check = 0;
uart_putc(XON);
}
break;
/* Parse Datengröße */
case PARSER_STATE_SIZE:
hex_buffer[hex_cnt++] = (uint8_t)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;
/* Parse Zieladresse */
case PARSER_STATE_ADDRESS:
hex_buffer[hex_cnt++] = (uint8_t)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;
hex_check += (uint8_t) (hex_addr >> 8);
if(flash_page_flag)
{
flash_page = hex_addr - hex_addr % SPM_PAGESIZE;
flash_page_flag = 0;
}
uart_putc(XON);
}
break;
/* Parse Zeilentyp */
case PARSER_STATE_TYPE:
hex_buffer[hex_cnt++] = (uint8_t)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;
/* Parse Flash-Daten */
case PARSER_STATE_DATA:
hex_buffer[hex_cnt++] = (uint8_t)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;
}
/* Puffer voll -> schreibe Page */
if(flash_cnt == SPM_PAGESIZE)
{
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;
/* Parse Checksumme */
case PARSER_STATE_CHECKSUM:
hex_buffer[hex_cnt++] = (uint8_t)c;
if(hex_cnt == 2)
{
uart_putc(XOFF);
hex_checksum = (uint8_t)hex2num(hex_buffer, 2);
hex_check += hex_checksum;
hex_check &= 0x00FF;
/* Dateiende -> schreibe Restdaten */
if(hex_type == 1)
{
uart_puts("P\n\r");
_delay_ms(100);
program_page((uint16_t)flash_page, flash_data);
boot_state = BOOT_STATE_EXIT;
}
/* Überprüfe Checksumme -> muss '0' sein */
if(hex_check == 0) parser_state = PARSER_STATE_START;
else parser_state = PARSER_STATE_ERROR;
uart_putc(XON);
}
break;
/* Parserfehler (falsche Checksumme) */
case PARSER_STATE_ERROR:
uart_putc('#');
break;
default:
break;
}
}
/* Programmzustand: UART Kommunikation */
else if(boot_state != BOOT_STATE_PARSER)
{
switch((uint8_t)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("Reset AVR!\n\r");
_delay_ms(1000);
/* Interrupt Vektoren wieder gerade biegen */
temp = MCUCR;
MCUCR = temp | (1<<IVCE);
MCUCR = temp & ~(1<<IVSEL);
/* Reset */
start();
return 0;
}
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 festgehalten werden, das der Bootloader in dieser Fassung einen 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 1796 Byte, wir sind also unterhalb der 2048 Byte die wir "verbraten" können. Der Datenspeicher wird mit 283 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.
WICHTIGER HINWEIS: Es hat sich gezeigt, das die Flußkontrolle durch XON/XOFF nicht immer funktioniert, da es eine Software-Flußsteuerung ist und u.U. der (UART-Sende-)Interrupt zu spät (oder gar nicht) ausgeführt wird. Bei Problemen beim Übertragen des HEX-Files sollte man als erstes versuchen die Baudrate zu senken (oder die Taktrate des Controllers erhöhen) um dem Controller mehr Zeit zum Verarbeiten und schnellerem Reagieren auf Interrupts zu geben. Eine andere Möglichkeit ist, eine Verzögerung zu aktivieren. z.B. kann mit dem Programm CoolTerm einen "Transmit Character delay" eingestellt werden. Dies funktioniert dann ab 2ms, wenn die Flusssteuerung versagt.
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 kennzeichnet ein P. Nach dem erfolgreichen Flashen startet die Anwendung automatisch.
Nach erfolgreichem Flashen des Bootloaders via AVRISPmkII erscheint nach dem Reset folgendes Bild:
Drückt man die Taste p, springt ist der Bootloader bereit zum Empfang der HEX-Datei:
Nach Kopieren & Einfügen der HEX-Datei der vorher kompilierten Anwendung, hier die Intel-HEX-Datei "Anwendung.hex":
:100000002CC046C045C044C043C042C041C040C0EF :100010003FC03EC03DC03CC03BC03AC039C038C004 :1000200037C036C064C08FC033C032C031C030C0AA :100030002FC02EC048696572206973742064617393 :1000400020416E77656E64756E677370726F67724C :10005000616D6D2E2E2E0D0A000011241FBECFEFF4 :10006000D4E0DEBFCDBF11E0A0E0B1E0EAE6F2E00F :1000700002C005900D92A434B107D9F711E0A4E4B1 :10008000B1E001C01D92A938B107E1F702D0EBC081 :10009000B7CFEF92FF920F931F93CF93DF9383E33A :1000A00090E07BD0789484E390E0D0D088ECE82E88 :1000B000F12C00E018E18BD0EC0190FDFCCF8236F2 :1000C00069F480E091E0B6D080E197E2F7013197E2 :1000D000F1F70197D9F7F8010995EDCF8CE191E09F :1000E000A9D08C2F91D081E491E0A4D0E4CF1F92CD :1000F0000F920FB60F9211242F938F939F93EF932C :10010000FF939091C0002091C600E0918601EF5FBF :10011000EF7180918701E81711F482E008C0892F00 :100120008871E0938601F0E0EC59FE4F20838093C4 :100130008801FF91EF919F918F912F910F900FBEAA :100140000F901F9018951F920F920FB60F921124C7 :100150008F939F93EF93FF939091840180918501FA :10016000981769F0E0918501EF5FEF71E0938501E9 :10017000F0E0EC5BFE4F80818093C60005C080916B :10018000C1008F7D8093C100FF91EF919F918F916E :100190000F900FBE0F901F9018959C011092840134 :1001A00010928501109286011092870197FF04C07A :1001B00082E08093C0003F773093C5002093C40055 :1001C00088E98093C10086E08093C20008959091F1 :1001D000860180918701981719F420E031E012C060 :1001E000E0918701EF5FEF71E0938701F0E0EC5958 :1001F000FE4F308120918801922F80E0AC01430FA7 :10020000511D9A01C9010895282F909184019F5F83 :100210009F71809185019817E1F3E92FF0E0EC5B85 :10022000FE4F2083909384018091C100806280936F :10023000C1000895CF93DF93EC0102C02196E4DF63 :1002400088818823D9F7DF91CF910895CF93DF93E9 :10025000EC0101C0D9DFFE01219684918823D1F7FA :0A026000DF91CF910895F894FFCFCD :10026A00537072696E6765207A756D20426F6F747C :10027A006C6F616465722E2E2E0D0A00447520681B :10028A0061737420666F6C67656E646573205A6566 :10029A00696368656E20676573656E6465743A2084 :0402AA00000A0D0039 :00000001FF
erscheinen folgende Ausgaben in PuTTY:
Nach erfolgreichem Flashen startet die Anwendung und meldet sich mit der Zeile
Hier ist das Anwendungsprogramm...
Jetzt können nach belieben Tasten gedrückt werden, was die Anwendung jedesmal quittiert:
Nach Drücken der Taste b springt die Anwendung wieder zur Startadresse des Bootloaders, also zur Adresse 0x1800 im Flash-Speicher. Nun hat man wieder 2 Sekunden Zeit, um die Taste p zu drücken, sonst startet die Anwendung wieder:
Damit ist das Tutorial abgeschlossen.
Viel Spass beim Ausprobieren und Weiterentwickeln!
Der "echte" Bootloader für Programme > 64k
Der Bootloader für AVRs mit mehr als 64K Flash-Speicher wird direkt vom Bootloader im vorherigen Kapitel abgeleitet.+
ACHTUNG: Der Abschnitt befindet sich zur Zeit in Bearbeitung!!
Schritt 1 und 2 - siehe "Hallo Welt" Bootloader
Schritt 1 und 2 können vom "Hallo Welt" Bootloader übernommen werden. Es sind wieder die korrekte Taktfrequenz und die Verschiebung der Sektion .text auf die Bootresetadresse einzustellen.
Schritt 3 - Programmieren des Bootloaders
Das Adresse eines Type 0 - Records im HEX86 - Format ist auf 16 Bit und somit auf 65535 Adressen beschränkt. Um den Adressbereich zu erweitern wurden die Record - Typen 2 bis 5 definert, wobei für uns nur Typ 2 und 4 relevant sind. Im Record-Typ 2 wird eine Offset- bzw Segmentadresse definiert. Sie wird mit 16 multipliziert und bei allen folgenden Schreiboperationen zur Adresse hinzuaddiert. Typ 4 sind die oberen 16 Bit einer 32 Bitadresse, die unteren 16 Bit sind in diesem Fall die im Record-Typ 0 angegebenen Adresse. Die Zustandsmaschine muß nun entsprechend erweitert werden:
#include <string.h>
#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/pgmspace.h>
#include <avr/boot.h>
#include <util/delay.h>
#include "uart.h"
#define BOOT_UART_BAUD_RATE 9600 /* Baudrate */
#define XON 17 /* XON Zeichen */
#define XOFF 19 /* XOFF Zeichen */
#define START_SIGN ':' /* Hex-Datei Zeilenstartzeichen */
/* Zustände des Bootloader-Programms */
#define BOOT_STATE_EXIT 0
#define BOOT_STATE_PARSER 1
/* Zustände des Hex-File-Parsers */
#define PARSER_STATE_START 0
#define PARSER_STATE_SIZE 1
#define PARSER_STATE_ADDRESS 2
#define PARSER_STATE_TYPE 3
#define PARSER_STATE_DATA 4
#define PARSER_STATE_CHECKSUM 5
#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 (const uint8_t * ascii, uint8_t num)
{
uint8_t i;
uint16_t val = 0;
for (i=0; i<num; i++)
{
uint8_t c = ascii[i];
/* Hex-Ziffer auf ihren Wert abbilden */
if (c >= '0' && c <= '9') c -= '0';
else if (c >= 'A' && c <= 'F') c -= 'A' - 10;
else if (c >= 'a' && c <= 'f') c -= 'a' - 10;
val = 16 * val + c;
}
return val;
}
void write_page(uint32_t page, uint8_t *buf)
{
uart_puts("P\n\r");
_delay_ms(100);
program_page(page, buf);
memset(buf, 0xFF, sizeof(SPM_PAGESIZE));
}
int main()
{
/* Intel-HEX Zieladresse */
uint32_t hex_addr = 0,
/* Intel-HEX Zieladress-Offset */
hex_addr_offset = 0,
/* Zu schreibende Flash-Page */
flash_page = 0;
/* Empfangenes Zeichen + Statuscode */
uint16_t c = 0,
/* Intel-HEX Checksumme zum Überprüfen des Daten */
hex_check = 0,
/* Positions zum Schreiben in der Datenpuffer */
flash_cnt = 0;
/* temporäre Variable */
uint8_t temp,
/* Flag zum steuern des Programmiermodus */
boot_state = BOOT_STATE_EXIT,
/* Empfangszustandssteuerung */
parser_state = PARSER_STATE_START,
/* Flag zum ermitteln einer neuen Flash-Page */
flash_page_flag = 1,
/* Datenpuffer für die Hexdaten*/
flash_data[SPM_PAGESIZE],
/* Position zum Schreiben in den HEX-Puffer */
hex_cnt = 0,
/* Puffer für die Umwandlung der ASCII in Binärdaten */
hex_buffer[5],
/* Intel-HEX Datenlänge */
hex_size = 0,
/* Zähler für die empfangenen HEX-Daten einer Zeile */
hex_data_cnt = 0,
/* Intel-HEX Recordtype */
hex_type = 0,
/* empfangene HEX-Checksumme */
hex_checksum=0;
/* Funktionspointer auf 0x0000 */
void (*start)( void ) = 0x0000;
/* Füllen der Puffer mit definierten Werten */
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) )
{
/* Programmzustand: Parser */
if(boot_state == BOOT_STATE_PARSER)
{
switch(parser_state)
{
/* Warte auf Zeilen-Startzeichen */
case PARSER_STATE_START:
if((uint8_t)c == START_SIGN)
{
uart_putc(XOFF);
parser_state = PARSER_STATE_SIZE;
hex_cnt = 0;
hex_check = 0;
uart_putc(XON);
}
break;
/* Parse Datengröße */
case PARSER_STATE_SIZE:
hex_buffer[hex_cnt++] = (uint8_t)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;
/* Parse Zieladresse */
case PARSER_STATE_ADDRESS:
hex_buffer[hex_cnt++] = (uint8_t)c;
if(hex_cnt == 4)
{
uart_putc(XOFF);
parser_state = PARSER_STATE_TYPE;
hex_cnt = 0;
hex_addr = hex_addr_offset;
hex_addr += hex2num(hex_buffer, 4);
hex_check += (uint8_t) hex_addr;
hex_check += (uint8_t) (hex_addr >> 8);
uart_putc(XON);
}
break;
/* Parse Zeilentyp */
case PARSER_STATE_TYPE:
hex_buffer[hex_cnt++] = (uint8_t)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 1: parser_state = PARSER_STATE_CHECKSUM; break;
case 0:
case 2:
case 4: parser_state = PARSER_STATE_DATA;
/* Berechnen der neue Flash-Page (abhängig von hex_type) */
/* Liegen die Daten noch in der aktuellen Flash-Page? */
if(!flash_page_flag && (flash_page != (hex_addr - hex_addr % SPM_PAGESIZE)) )
{
/* Wenn die Daten nicht in der aktuellen Flash-Page liegen, */
/* wird die aktuelle Page geschrieben und ein Flag */
/* zum berechnen der neuen Page-Startadresse gesetzt */
write_page(flash_page, flash_data);
flash_cnt = 0;
flash_page_flag = 1;
}
/* Muss die Page-Startadresse neu berechnet werden? */
if(flash_page_flag)
{
/* Berechnen der neuen Page-Startadresse */
flash_page = hex_addr - hex_addr % SPM_PAGESIZE;
/* Füllen des Flash-Puffers mit dem "alten" Inhalt der Page */
memcpy_PF(flash_data, flash_page, SPM_PAGESIZE);
/* Flag setzen um anzuzeigen das eine neue Adresse da ist */
flash_page_flag = 0;
}
break;
default: parser_state = PARSER_STATE_DATA; break;
}
uart_putc(XON);
}
break;
/* Parse Flash-Daten */
case PARSER_STATE_DATA:
hex_buffer[hex_cnt++] = (uint8_t)c;
switch(hex_type)
{
case 0: /* Record Typ 00 - Data Record auswerten */
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;
}
/* Puffer voll -> schreibe Page */
if(flash_cnt == SPM_PAGESIZE)
{
write_page(flash_page, flash_data);
flash_cnt = 0;
flash_page_flag = 1;
}
uart_putc(XON);
}
break;
case 2: /* Record Typ 02 - Extended Segment Address auswerten */
case 4: /* Record Typ 04 - Extended Linear Address auswerten */
if(hex_cnt == 4)
{
uart_putc(XOFF);
uart_putc('J');
hex_cnt = 0;
/* Schreibe angfangene Flash-Page vor Segment-Sprung */
write_page(flash_page, flash_data);
flash_cnt = 0;
flash_page_flag = 1;
/* Berechnen der Offsetadresse */
switch(hex_type)
{
case 2: hex_addr_offset = ((uint32_t)hex2num(hex_buffer, 4)) << 4; break;
case 4: hex_addr_offset = ((uint32_t)hex2num(hex_buffer, 4)) << 16; break;
}
/* Addieren der empfangenen Werte für die Checksumme */
hex_check += (uint8_t) hex2num(hex_buffer, 2);
hex_check += (uint8_t) hex2num(hex_buffer + 2, 2);
parser_state = PARSER_STATE_CHECKSUM;
hex_data_cnt=0;
hex_cnt = 0;
}
break;
default:
break;
}
break;
/* Parse Checksumme */
case PARSER_STATE_CHECKSUM:
hex_buffer[hex_cnt++] = (uint8_t)c;
if(hex_cnt == 2)
{
uart_putc(XOFF);
hex_checksum = (uint8_t)hex2num(hex_buffer, 2);
hex_check += hex_checksum;
hex_check &= 0x00FF;
/* Dateiende -> schreibe Restdaten */
if(hex_type == 1)
{
write_page(flash_page, flash_data);
boot_state = BOOT_STATE_EXIT;
}
/* Überprüfe Checksumme -> muss '0' sein */
if(hex_check == 0) parser_state = PARSER_STATE_START;
else parser_state = PARSER_STATE_ERROR;
uart_putc(XON);
}
break;
/* Parserfehler (falsche Checksumme) */
case PARSER_STATE_ERROR:
uart_putc('#');
break;
default:
break;
}
}
/* Programmzustand: UART Kommunikation */
else if(boot_state != BOOT_STATE_PARSER)
{
switch((uint8_t)c)
{
case 'p':
boot_state = BOOT_STATE_PARSER;
uart_puts("Kopiere die Hex-Datei und füge sie hier ein\n\r");
break;
case 'q':
boot_state = BOOT_STATE_EXIT;
uart_puts("Verlasse den Bootloader!\n\r");
break;
default:
uart_putc((unsigned char)c);
uart_puts("\n\r");
break;
}
}
}
}
while(boot_state!=BOOT_STATE_EXIT);
uart_puts("Reset AVR!\n\r");
_delay_ms(1000);
/* Interrupt Vektoren wieder gerade biegen */
temp = MCUCR;
MCUCR = temp | (1<<IVCE);
MCUCR = temp & ~(1<<IVSEL);
/* Reset */
start();
return 0;
}
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 kommende ATmega-Plattform angepaßt werden. Dazu muss
- die Linkereinstellung für das Verschieben der Sektion .text angepasst werden und (-Ttext = 0xXXXXX)
- evtl. der virtuelle Funktionspointer in der Anwendung geändert werden (falls ein Rücksprung zum Bootloader gewünscht ist void (*bootloader)(void) = 0xYYYYY).
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:
Device | Flash-Größe für Applikation |
Flash-Größe des Bootloaders |
Startadresse des Bootloaders (Byteadresse) 0xXXXXX |
Startadresse des Bootloaders (Wordadresse) 0xYYYYY |
---|---|---|---|---|
ATmega8/88 | 6144 Byte | 2048 Byte | 0x1800 | 0xC00 |
ATmega16/164/168 | 14336 Byte | 2048 Byte | 0x3800 | 0x1C00 |
ATmega32/324/328 | 28672 Byte | 4096 Byte | 0x7000 | 0x3800 |
ATmega64/644/640 | 57344 Byte | 8192 Byte | 0xE000 | 0x7000 |
ATmega128/1284/1280/1281 | 122880 Byte | 8192 Byte | 0x1E000 | 0xF000 |
ATmega2560/2561 | 253952 Byte | 8192 Byte | 0x3E000 | 0x1F000 |
Damit kann jeder den Bootloader nach seinen Wünschen anpassen. Eine gängige 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 interpretieren der Daten (Parsen). Eine weitere Idee ist es, den Bootloader so anzupassen, das er sich wie ein STK500 an der seriellen Schnittstelle verhält. Dazu muss man die Application Note AVR068 von Atmel umsetzen. Auch dies sollte nach dem Studium des Tutorial kein Problem mehr sein :)
FAQ
Wie kann man Bootloader und Anwendungsprogramm gemeinsam flashen?
Es ist in C ohne weiteres nicht möglich ein gemeinsames Hexfile aus Bootloader und Anwendungsprogramm zu kompilieren. Aber man kann das getrennt erstellte Hexfile des Bootloaders und das getrennt erstellte Hexfile des Anwendungsprogramms zusammenfügen und so gemeinsam mit ISP flashen. Dazu die letzte Zeile des 1. Hexfiles entfernen und dahinter das 2. Hexfile anfügen [2], [3].
Mit Atmel Studio 6 lässt sich das Vorgehen sehr leicht über die Post-Build Events automatisieren. Benötigt wird hierzu ein kleines Batch-Script, welches z.B. als "hexjoin.bat" im Solution Directory abgelegt wird.
type %1 | findstr /v :00000001FF > tmp.hex
type %2 >> tmp.hex
type tmp.hex > %1
Unter "Project->Properties->Build Events->Post-build event command line" kann nun das Script nach jedem Build ausgeführt werden und erzeugt eine kombinierte HEX-Datei. Im folgenden Beispiel wird davon ausgegangen, dass sowohl die Datei mit dem Bootloader, als auch das Batch-Script im Solution Directory liegen:
$(SolutionDir)hexjoin.bat "$(OutputDirectory)\$(OutputFileName).hex" "$(SolutionDir)bootloader.hex"
Fehler bei Flash > 64k
Neuere avr-gcc optimieren den oben beschriebenen Bootloader mit so genannten "table jumps". Darin verwendet der Compiler LPM Instruktionen. Laut dem "AVR Instruction Set" kann dieser Befehl aber nur für Adressen bis 64k verwendet werden. Liegt nun der Bootloader am Ende eines 128k Flashs kann dies zum Absturz resp. Neustart des Controllers führen. Abhilfe schafft z.B. die Optimierungsoption "-fno-jump-tables".
Referenzen / Links
- Arduino Bootloader (simuliert den AVR ISP) jetzt zu finden bei: code.google.com