- Elettronica Open Source - https://it.emcelettronica.com -

Bootloader per microcontrollori AVR

Uno degli aspetti più interessanti dei moderni microcontrollori è certamente la riprogrammabilità. E’ tuttavia forse una della procedure più ‘noiose’ che inevitabilmente si devono sopportare. La soluzione più semplice è certamente quella di utilizzare opportuni programmatori ma non sempre è praticabile; nel caso di hobbisti i costi di tali apparecchiature, ad esempio, potrebbero essere proibitivi. In altri casi potrebbe essere la stessa applicazione a non supportare un tale scenario; si pensi ad esempio ad installazioni in zone difficilmente accessibili.

In questo caso la possibilità di programmare il microcontrollore da remoto usando un protocollo dedicato su una porta di comunicazione standard rappresenta certamente una soluzione appetibile; sicuramente è una funzionalità importante quando è necessario aggiornare i parametri di configurazione del sistema o modificare parti di codice applicativo, ad esempio per risolvere bug. Questo tipo di funzionalità è in genere fornita da un particolare software denominato bootloader. Di seguito vedremo come può essere creato un semplice bootloader per i microcontrollori AVR [1] di Atmel; prenderemo come riferimento i dispositivi Xmega.

Organizzazione della memoria flash nei micro Xmega

La figura 1 mostra lo schema di principio dell’architettura dei micro AVR Xmega.

Figura 1: l’architettura dei micro AVR Xmega (da [6]).

Figura 1: l’architettura dei micro AVR Xmega (da [6]).

I dispositivi dispongono, come si vede, di una memoria Flash accessibile mediante il controller NVM. La memoria supporta programmazione in-circuit. E’ organizzata in pagine la cui dimensione dipende dal tipo di dispositivo selezionato; l’accesso è a word o byte. Le operazioni di lettura (read) possono accedere a locazioni singole mentre le istruzioni di programmazione (write) e cancellazione (erase) della memoria operano invece sull’intera pagina. La programmazione della pagina richiede che i dati siano prima caricati in un opportuno buffer. L’indirizzamento è di tipo indiretto mediante lo Z-pointer, implementato con i registri R31 ed R30 del processore; la parte alta dell’indirizzo individua la pagina all’interno della memoria mentre la parte bassa la parola all’interno della pagina. L’intera memoria flash è partizionata in due diverse sezioni, indicate come Application e Boot Section. Sono disponibili bit di lock indipendenti per ognuna delle sezioni al fine di definire diverse restrizioni per quanto riguarda le operazioni di read, write ed erase in accesso ad esse. La stessa Application section contiene a sua volta una sottosezione indicata come Application Table Section (e la cui dimensione è uguale a quella della sezione di Boot) che può essere indipendentemente protetta per prevenire accessi indesiderati e proteggere dati vitali per l’applicazione. La figura 2 chiarisce graficamente l’organizzazione della memoria flash dei micro Xmega come appena discussa.

Figura 2: l’organizzazione della memoria nei micro AVR Xmega (da [4]).

Figura 2: l’organizzazione della memoria nei micro AVR Xmega (da [4]).

Mediante opportuno fusibile il micro Xmega può essere programmato per iniziare ad eseguire il software dopo il reset a partire dalla locazione base della sezione Boot o di Application. Il partizionamento della memoria flash nelle due sezioni permette di accedere all’area di Boot mentre è in corso la programmazione di una pagina dell’area Application senza che il processore sia arrestato; l’Application Section, ovvero, è di tipo Read-While-Write (RWW). L’area di Boot è invece di tipo No-Read-While-Write (NRWW), nel senso che non è possibile accedervi in lettura mentre è in corso una operazione di programmazione di una delle sue pagine. In linea di principio, la sezione di Boot è quindi pensata per allocare il bootloader mentre quella di Application per memorizzare l’applicazione utente. In questo modo il bootloader potrà continuare ad essere eseguito anche mentre è in corso la riprogrammazione dell’applicazione. Questo non significa che non possa essere realizzato un bootloader in grado di autoriprogrammarsi; semplicemente durante la riprogrammazione, il processore sarebbe arrestato non potendo accedere a questa area per il fetch delle istruzioni da eseguire.

I comandi di base

Il controller NVM rende disponibili una serie di istruzioni per poter accedere alla memoria flash ed ai fusibili di programmazione del dispositivo Xmega. Queste sono divise in istruzioni NVM action-based e comandi LPM-based ed SPM-based in quanto richiedono diverse procedure per essere eseguiti. La tabella 1 riporta l’elenco delle istruzioni supportate, l’indicazione del tipo ed una loro breve descrizione.

Tabella 1 - Comandi a basso livello per la gestione della flash

Tabella 1 - Comandi a basso livello per la gestione della flash

L’Application Note [4] include un driver (sp_driver.S) scritto in linguaggio assembler che implementa tutti questi comandi; nella tabella 1 è indicato il nome mnemonico usato nel driver per ogni istruzione. Di seguito è riportata invece una descrizione della procedure necessarie per ogni tipo di comando. Le istruzioni NVM-action based, ad esempio, includono le operazioni di lettura di un fusibile di programmazione del dispositivo, scrittura/lettura dei bit di lock della memoria flash, generazione di CRC per i dati delle sezioni Application e Boot. La procedura per eseguire uno di questi comandi (secondo la descrizione riportata in [4]) è la seguente:

» caricare indirizzo e dati (se richiesti) nei registro ADDRn e DATAn del controller NVM;
» caricare il codice dell’istruzione da eseguire nel registro CMD del controller NVM;
» disabilitare le interruzioni delle CPU per i successivi 4 cicli di clock;
» attivare l’esecuzione del comando mediante scrittura del bit CMDEX nel registro CTRLA del controller NVM;
» attendere che la flag NVM Busy sia rilasciata ad indicare il termine dell’esecuzione del comando;
» leggere i dati (se richiesto) dal registro DATAn del controller NVM.

A titolo di esempio riportiamo nel listato 1 un estratto del codice che descrive il comando SP_ReadFuseByte.

1. .section .text
2. global SP_ReadFuseByte
3.
4. SP_ReadFuseByte:
5. sts NVM_ADDR0, r24               ; Load fuse byte index into NVM Address Register 0.
6. clr r24                          ; Prepare a zero.
7. sts NVM_ADDR1, r24               ; Load zero into NVM Address Register 1.
8. sts NVM_ADDR2, r24               ; Load zero into NVM Address Register 2.
9. ldi r20, NVM_CMD_READ_FUSES_gc   ; Prepare NVM command in R20.
10. rcall SP_CommonCMD              ; Jump to common NVM Action code.
11. movw r24, r22                   ; Move low byte to 1 byte return address.
12. ret
13.
14. .section .text
15.
16. SP_CommonCMD:
17. sts NVM_CMD, r20                ; Load command into NVM Command register.
18. ldi r18, CCP_IOREG_gc           ; Prepare Protect IO-register signature in R18.
19. ldi r19, NVM_CMDEX_bm           ; Prepare bitmask for setting NVM Command Execute bit into R19.
20. sts CCP, r18                    ; Enable IO-register operation (this disables interrupts for 4
cycles).
21. sts NVM_CTRLA, r19              ; Load bitmask into NVM Control Register A, which executes
: the command.
22. lds r22, NVM_DATA0              ; Load NVM Data Register 0 into R22.
23. lds r23, NVM_DATA1              ; Load NVM Data Register 1 into R23.
24. lds r24, NVM_DATA2              ; Load NVM Data Register 2 into R24.
25. clr r25                         ; Clear R25 in order to return a clean 32-bit value.
26. ret
Listato 1

I comandi LPM includono invece le operazioni di lettura dei byte di calibrazione e di segnatura accessibili dall’utente e l’istruzione di read alla memoria flash. La procedura per l’implementazione di questo tipo di comandi è la seguente :

» caricare l’indirizzo nel registro Z-pointer (se richiesto);
» caricare il codice dell’istruzione da eseguire nel registro CMD del controller NVM;
» attivare l’esecuzione del comando;
» leggere i dati (se richiesto) dal registro R0.

Come fatto in precedenza, riportiamo nel listato 2 l’esempio di come è implementato uno di questi comandi (SP_ReadCalibrationByte) nel driver distribuito da Atmel cui abbiamo fatto riferimento in precedenza.

1. .section .text
2. .global SP_ReadCalibrationByte
3.
4. SP_ReadCalibrationByte:
5. ldi r20, NVM_CMD_READ_CALIB_ROW_gc               ; Prepare NVM command in R20.
6. rjmp SP_CommonLPM                                ; Jump to common LPM code.
7. .section .text
8.
9. SP_CommonLPM:
10. movw ZL, r24                                    ; Load index into Z.
11. sts NVM_CMD, r20                                ; Load prepared command into NVM Command register.
12. lpm r24,Z
13. ret
Listato 2

I comandi SPM, infine, includono le operazioni di cancellazione e scrittura di una segnatura utente, di cancellazione della memoria flash, di caricamento di dati nel buffer di pagina di questa e di flush di tale buffer, di programmazione di una pagina della flash. Di seguito è descritta la procedura per la loro implementazione:

» caricare l’indirizzo nel registro Z-pointer (se richiesto);
» caricare i dati nei registri R0 ed R1 (se richiesto);
» caricare il codice dell’istruzione da eseguire nel registro CMD del controller NVM;
» disabilitare le interruzioni delle CPU per i successivi 4 cicli di clock;
» attivare l’esecuzione del comando;
» che la flag NVM Busy sia rilasciata ad indicare il termine dell’esecuzione del comando.

Il listato 3 riporta come esempio il codice assembler per implementare il comando di scrittura della pagina (SP_WriteApplicationPage).

1. .section .text
2. .global SP_WriteApplicationPage
3.
4. SP_WriteApplicationPage:
5. in r19, RAMPZ                            ; Save RAMPZ, which is restored in SP_CommonSPM.
6. out RAMPZ, r24                           ; Load RAMPZ with the MSB of the address.
7. movw r24, r22                            ; Move low bytes of address to ZH:ZL from R23:R22
8. ldi r20, NVM_CMD_WRITE_APP_PAGE_gc       ; Prepare NVM command in R20.
9. jmp SP_CommonSPM                         ; Jump to common SPM code.
10. .section .BOOT, “ax”
11.
12. SP_CommonSPM:
13. movw ZL, r24                            ; Load R25:R24 into Z.
14. sts NVM_CMD, r20                        ; Load prepared command into NVM Command register.
15. ldi r18, CCP_SPM_gc                     ; Prepare Protect SPM signature in R18
16. sts CCP, r18                            ; Enable SPM operation (this disables interrupts for 4 cycles).
17. spm                                     ; Self-program.
18. clr r1                                  ; Clear R1 for GCC _zero_reg_ to function properly.
19. out RAMPZ, r19                          ; Restore RAMPZ register.
20. ret
Listato 3

Un esempio

Le funzioni che abbiamo appena descritto sono tutto quanto è necessario per scrivere un bootloader. Supponiamo allora di volerne realizzare uno che riceva i dati dalla porta seriale. Dovremo definire un protocollo custom per l’applicazione che consenta di inoltrare da una macchina host i comandi e i dati da aggiornare; questi dovranno essere segmentati in pacchetti di dimensione corrispondente a quella della pagina della memoria flash del dispositivo selezionato. Per aggiornare, ad esempio, l’applicazione utente il bootloader dovrà eseguire un parte di programma secondo uno schema di questo tipo:

» cancellare l’intera sezione della flash mediante il comando SP_EraseApplicationSection;
» attendere che l’operazione sia terminata mediante l’istruzione SP_WaitForSPM;
» ricevere un pacchetto di dati attraverso la porta di comunicazione e memorizzarlo in SRAM;
» caricare i dati dalla SRAM nel buffer di pagina della flash mediante il comando SP_LoadFlashPage;
» programmare i nuovi dati nella pagina della flash SP_WriteApplicationPage;
» attendere che l’operazione sia terminata WaitForSPM;
» ripetere l’operazione per tutti i pacchetti/pagine di memoria da programmare.

Un esempio di bootloader che segue questo schema e basato appunto sul driver cui abbiamo accennato in precedenza è presentato in [5]. Tale bootloader supporta la comunicazione su porta UART con l’AVR Open Source Programmer (AVROSP), una applicazione host in linguaggio C++ distribuita in versione open-source da Atmel proprio per la programmazione da remoto dei micro AVR.

 

RIFERIMENTI
[1] “AVR109 : Self programming” – Atmel application note
[2] “AVR105 : Power efficient high endurance parameter storage in flash memory”Atmel application note
[3] “AVR106 : C function for reading and writing to flash memory” – Atmel application note
[4] “AVR1316 : Xmega self programming” – Atmel application note
[5] “AVR1605 : Xmega boot loader quick start guide” – Atmel application note
[6] “8/16 bit AVR Xmega A4 Microcontroller” – Preliminary datasheet