Bootloader v mikrokontrolérech AVR

Tento článek se zabývá vlastností některých AVR mikrokontrolérů zvanou self-programming. Text čerpá z application note AVR109 a je přepisem přednášky, kterou jsem měl na nedávno proběhlé mini-konferenci (slajdy původní přednášky).

Některé mikrokontroléry řady ATtiny a ATmega mohou obsahovat kód zvaný bootloader, který umožňuje přepisovat paměť flash. V tomto textu se podíváme jak tento kód funguje, co k tomu využívá a na závěr si napíšeme jednoduchý kód spouštěný z paměti bootloaderu.

K čemu bootloader je?

Bootloader, česky zavaděč, je speciální aplikace, která slouží k zavedení/nahrání aplikace nebo operačního systému do paměti a jejího následného spuštění. V PC světě je to BIOS, který spustí bootloader, ten pak načte z požadovaného média jádro operačního systému do paměti a následně jej spustí. Ve světě mikrokontrolérů AVR je to o něco jednodušší. Po zapnutí napájecího napětí se (v případě že jsou správně nastaveny fuses) začne okamžitě vykonávat kód bootloaderu, který je uložen na konci paměti flash. Tento bootloader může např. přes rozhraní UART načíst aplikaci, kterou postupně uloží do paměti flash (tedy stejné paměti ve které se nachází on sám) a poté skočit na její první instrukci.

Ovšem bootloader se nemusí omezit pouze na rozhraní UART. Lze využít i jiných rozhraní – I2C, SPI, Ethernet, Bluetooth, cokoliv co autora napadne. Toto se s výhodou využívá při vývoji, protože není potřeba externí programátor, i při aktualizaci firmware na běžícím, finálním zařízení, které se může nacházet třeba na druhé straně zeměkoule u zákazníka.

Jeden příklad za všechny: Arduino Uno, které obsahuje MCU ATmega328 využívá bootloader k naprogramování kódu přes rozhraní UART.

Co to je self programming?

Obecně lze říct, že pokud AVR podporuje self programming, podporuje i bootloader. Self programming umožňuje kódu umístěném v paměti flash přepisovat sám sebe. K tomu využívá instrukci SPM (Store Program Memory), kterou je ale možné spouštět jenom z té části flash, která je označena jako bootloader section a jejíž začátek se vybírá pomocí fuses.

Například ATmega328: K zapnutí bootloaderu o velikosti 1024 B je potřeba nastavit 3 fuses:

  • BOOTRST = 0
  • BOOTSZ0 = 0
  • BOOTSZ = 1

V tom okamžiku se po zapnutí napájecího napětí začne vykonávat kód od adresy 0x3E00. Od této adresy pak můžeme uložit kód našeho bootloaderu, který může využívat instrukci SPM.

Rozdělení paměti

Rozdělení paměti AVR z pohledu bootloaderu.
Rozdělení paměti AVR z pohledu bootloaderu.

Tohle téma jsem již začal v předchozí sekci. Paměť AVR mikrokontrolérů se z pohledu bootloaderu dělí na dvě části. Na část pro bootloader a část pro aplikaci.

Dále je důležité vědět, že se paměť dělí na tzv. stránky (pages) a každá stránka má pevně danou velikost. Již zmíněný MCU ATmega328 má stránky o velikosti 64 slov, tedy 128 bajtů. Celková velikost paměti je 32kB, což nám dává celkem 256 stránek.

Aby to ale nebylo tak jednoduché, na flash paměť se můžeme ještě dívat z jiného pohledu. To nám vysvětlí následující obrázek:

Flash se dělí na RWW a NRWW sekci.
Flash se dělí na RWW a NRWW sekci.

Co přesně znamenají RWW a NRWW si povíme za chvíli. Zatím nám stačí vědět, že bootloader je celý uložen v sekci NRWW, ale nemusí ji nutně zabírat celou. Pokud ve fuses vybereme menší bootloader, zabere se menší část NRWW. Aplikace tak může být v sekci RWW a někdy může zasahovat i kousek do NRWW (ale to se stává málokdy).

V datasheetu ATmega328 se na stránce 294 píše, že pro RWW sekci připadá 224 stránek (tedy 224×128B=28kB) a pro NRWW je vyhrazeno 32 stránek (4kB). Bootloader v ATmega328 může tedy mít maximálně velikost 4kB, nikdy ne víc.

Poznámka: Některé menší AVR nemají RWW sekci. Např. ATmega163 a ATmega323 mají jenom NRWW, takže bootloader i aplikace jsou ve stejné sekci. Co to znamená si povíme za chvíli.

Jak pracuje instrukce SPM?

Nyní již víme, co je bootloader a kde musí být uložen, aby mohl vykonávat instrukci SPM (Store Program Memory). Tato instrukce umožňuje smazat a uložit kód na libovolnou stránku v paměti. K tomu používá:

  • speciálně vyhrazenou paměť zvanou page buffer,
  • registr SPMCR,
  • registr Z a
  • registry R0 a R1.

Page buffer je speciální paměť o velikosti jedné stránky, která není běžně přístupná pro čtení ani zápis. Do této paměti lze pouze zapisovat po slovech prostřednictvím instrukce SPM. Registr SPMCR (na některých zařízeních také nazývaný SPMCSR) slouží k nastavení činnosti, kterou má SPM vykonat (smazat, nahrát slovo, nahrát stránku, aktivovat RRW). Bity registru SPMCR jsou:

  • bit 0: SPMEN (povolí vykonání instrukce SPM),
  • bit 1: PGERS (zahájí mazání stránky),
  • bit 2: PGWRT (zahájí zápis stránky),
  • bit 3: BLBSET (nastaví lock bity pro RWW),
  • bit 4: RWWSRE (aktivuje RWW sekci),
  • bit 5: -,
  • bit 6: RWWSB (indikuje uzamčení sekce RWW),
  • bit 7: SPMIE (SPM přerušení).

Pokud chcete vytvořit bootloader, který načte kód aplikace např. z UART rozhraní a uloží jej do paměti MCU, je nutné, aby se pro každou stránku vykonala následující sekvence:

  • smazat požadovanou stránku,
  • načíst nový kód do page bufferu,
  • nahrát page buffer do stránky.

Nyní si podrobně popíšeme jak spustit jednotlivé operace.

Smazání stránky

V případě mazání je potřeba nastavit adresu stránky do registru Z. Ten je rozdělen na dvě části – část pro nastavení adresy slova v page bufferu a k nastavení adresy stránky. Mějme opět MCU ATmega328, která má 64 slov na stránku. To znamená, že k adresaci slov v page bufferu stačí 6 bitů uložených v registru Z na bitech 1 až 7 (nultý bit, LSB, je ignorován). To znamená, že pro adresaci stránky slouží osmý bit a vyšší. Na tuto pozici uložíme adresu stránky, nastavíme bity PGERS a SPMEN v SPMCR a vykonáme instrukci SPM.

Poznámka: Od nastavení bitu SPMEN a vykonání SPM nesmí uběhnout více jak 4 takty! Pokud uběhne více taktů, registr SPMCR je nulován. Toto je bezpečnostní opatření, které zabraňuje nechtěnému mazání a zápisu paměti.

Zápis slova do page bufferu

Do registrů R1:R0 zapíšeme slovo (dolní bajt na R0 a horní na R1), do registru Z zapíšeme adresu slova (viz sekci o mazání, adresa slova se zapisuje do spodní části registru), natavíme pouze SPMEN a vykonáme SPM.

Zápis stránky do paměti

V page bufferu již musíme mít připravená všechna data. Do registru Z uložíme adresu stránky, nastavíme bity PGWRT a SPMEN a vykonáme SPM.

Aktivace RWW sekce

Po první manipulaci s pamětí pomocí SPM je hardwarem nastaven flag RWWSB v SPMCR. Ten indikuje uzamčení RWW sekce pro čtení i zápis. Jakýkoliv pokus o čtení z libovolné adresy, když je RWW uzamčena vrátí hodnotu 0xFFFF.

Bootloader tedy musí po ukončení svojí činnosti znovuaktivovat RWW sekci a teprve poté se může ukončit. Aktivace se provede nastavením RWWSRE a SPMEN v SPMCR a vykonáním SPM.

Sekce RWW a NRWW

V tomto okamžiku toho již víme dost na to, abychom si mohli vysvětlit rozdíly v sekcích RWW a NRWW. Připomínám, že bootloader musí být vždy uložen v sekci NRWW, ale aplikace může být v obou.

Tyto zkratky znamenají: RWW = Read-While-Write a Non-Read-While-Write. Pro lepší pochopení je tu opět obrázek:

Rozdíly mezi RWW a NRWW.
Rozdíly mezi RWW a NRWW.

Pokud registr Z ukazuje na adresu stránky ležící v sekci RWW a my vykonáme zápis stránky, hardware začne na pozadí přepisovat page buffer do paměti, ale zároveň kód bootloaderu nadále běží. V jednodušších případech můžeme jednoduše počkat, dokud operace neskončí. V některých případech ale chceme dále pracovat. Představme si např. situaci, kdy přijímáme data ze sběrnice USB. Během zápisu stránky můžeme v klidu obsluhovat sběrnici a mezitím čekat na skončení zápisu.

Naopak, pokud registr Z ukazuje na stránku v NRWW a my provedeme zápis, CPU je okamžitě zastaveno a stojí po celou dobu zápisu. Pokud by nám v tomto okamžiku přišel na sběrnici USB synchronizační paket, my bychom nemohli odpovědět a USB host by nás odpojil.

Ukázka jednoduchého kódu pro bootloader

V této poslední sekci bych chtěl ukázat jak nahrát kód do sekce pro bootloader. Nebudu zde implementovat plnohodnotný bootloader, dokonce ani nebudu využívat instrukce SPM. Kód bude jednoduše blikat LED na portu D, ale bude vykonávat z bootloader sekce.

#include <util/delay.h>
#include <avr/io.h>

int main(void) {
  DDRD = 0xFF;

  for(int i = 0; i < 3; i++) {
    PORTD = 0xFF;
    _delay_ms(100);
    PORTD = 0x00;
    _delay_ms(100);
  }

  return 0;
}

Obyčejné blikání, na tom není nic zajímavého. Zajímavější je až samotný překlad tohoto kódu.

Jelikož vytváříme kód pro bootloader, nemůžeme zavolat avr-gcc jenom tak. Musíme překladači naznačit, aby nevypňoval vektory přerušení, nevytvářel rutiny standardní knihovny, prostě žádný další kód navíc. To uděláme přepínačem -nostdlib. Zároveň musíme říct, že chceme začínat od adresy bootloaderu. Např. pro ATmega328 s bootloaderem o velikosti 1024B je to adresa 0x3E00 a překladač zavoláme následujícím způsobem:

$ avr-gcc -Wl,--section-start=.text=0x3E00 -nostdlib ...

Celé volání pak může vypadat takto:

$ avr-gcc \
> -Wl,--section-start=.text=0x3E00 \
> -nostdlib \
> -Wall -Os -std=c99 -mmcu=m328 \
> -DF_CPU=16000000 \
> -o blink.elf blink.c

A to je celé. Po vygenerování HEX souboru pomocí objcopy dostaneme kód začínající od adresy 0x3E00.

Závěr

Tímto jsme ukončili povídání o bootloaderech a self programming v AVR. Se znalostmi zde uvedenými jste schopni si sami napsat vlastní bootloader, případně pochopit již existující.

Několik věcí jsem však zamlčel (už tak je článek velmi dlouhý). Vůbec jsem nepopsal jak funguje uzamčení aplikační paměti pomocí bootloaderu (slouží k tomu BLBSET) a také jsem vynechal přerušení generované instrukcí SPM (flag SPMIE). Tato témata jsou sice zajímavá, ale pro začátky s bootloaderem nejsou tak podstatná. Případné zájemce odkazuji na již zmíněný app note AVR109.