Cos'è un Bootloader?
Il Bootloader è il primo segmento di codice eseguito in un sistema embedded dopo l’accensione. Una volta completata l’inizializzazione della CPU e dell’hardware pertinente, carica l’immagine del sistema operativo o il programma applicativo embedded in memoria e quindi passa allo spazio in cui risiede il sistema operativo, avviando così il funzionamento del sistema operativo. Questo processo è fondamentale per chiunque sia coinvolto nella programmazione di microcontrollori specifici come STM32.
Simile a un programma applicativo, un Bootloader è un programma autonomo che contiene componenti essenziali come il codice di avvio, le interruzioni, un programma principale (funzione Boot_main) e, facoltativamente, un sistema operativo. Nonostante le sue dimensioni ridotte, il Bootloader racchiude funzionalità critiche.
I Bootloader dipendono tipicamente molto dall’hardware e sono particolarmente significativi nel campo dei sistemi embedded. Di conseguenza, creare un bootloader universalmente applicabile nel dominio embedded è quasi impossibile. Tuttavia, possiamo ancora generalizzare alcuni concetti sui bootloader per guidare gli utenti nella progettazione e nell’implementazione di bootloader specifici.
Modalità di funzionamento di un Bootloader
La maggior parte dei bootloader comprende due modalità di funzionamento distinte: la modalità boot e la modalità download.
All’accensione, il bootloader inizializza l’ambiente software e hardware del sistema e seleziona una delle modalità di funzionamento in base alle condizioni hardware correnti. Ciò comporta la configurazione della modalità di funzionamento della CPU, l’inizializzazione della memoria, la disabilitazione delle interruzioni e la gestione di attività come la disattivazione della MMU/Cache.
Modalità di caricamento (Boot Loading):
Questa modalità, nota anche come modalità “autonoma”, prevede che il Boot Loader carichi autonomamente il sistema operativo da un dispositivo di archiviazione a stato solido sulla macchina di destinazione nella RAM. L’intero processo avviene senza alcun intervento dell’utente. Questa modalità rappresenta il funzionamento normale del Boot Loader.
Modalità Download:
In questa modalità, il Boot Loader sulla macchina di destinazione avvia la comunicazione tramite mezzi quali connessioni seriali o di rete con una macchina host per scaricare file. I file ottenuti dalla macchina host vengono in genere archiviati nella RAM della macchina di destinazione da parte del Boot Loader prima di essere scritti nella memoria Flash o in un dispositivo di archiviazione a stato solido simile sulla macchina di destinazione.
Come funziona un Bootloader?
Esistono due tipi di processo di avvio per un Bootloader: Single-Stage e Multi-Stage. In generale, i Boot Loader multi-stage possiedono funzionalità più complesse e una maggiore portabilità. I Boot Loader che si avviano da dispositivi di archiviazione a stato solido utilizzano spesso un processo a due fasi, suddiviso in fase 1 e fase 2: la fase 1 esegue l’inizializzazione dell’hardware, prepara lo spazio di memoria per la fase 2, copia la fase 2 in memoria, imposta lo stack e quindi passa alla fase 2.
Fase 1 del Bootloader
Inizializzazione del dispositivo hardware
- Disabilita tutti gli interrupt: la gestione degli interrupt è in genere responsabilità dei driver di dispositivo del sistema operativo, quindi il Boot Loader può ignorare le risposte agli interrupt durante la sua esecuzione. La mascheratura degli interrupt può essere ottenuta modificando il registro di mascheramento degli interrupt o il registro di stato della CPU (come il registro CPSR di ARM).
- Imposta la velocità della CPU e la frequenza di clock.
- Inizializza la RAM: ciò comprende la corretta configurazione dei registri di funzione del controller di memoria del sistema e dei vari registri di controllo della banca di memoria.
- Inizializzazione del LED: i LED sono spesso pilotati tramite GPIO per indicare lo stato del sistema (OK o Errore). Se non sono presenti LED, l’inizializzazione di UART per stampare il logo del Boot Loader o le informazioni sui caratteri tramite comunicazione seriale può servire a questo scopo.
- Disabilita la cache interna di istruzioni/dati della CPU.
Prepara lo spazio RAM per caricare la fase 2 del Bootloader
Per un’esecuzione più rapida, la fase 2 viene comunemente caricata nella RAM. Pertanto, è necessario allocare un intervallo di memoria disponibile per caricare la fase 2 del Boot Loader.
Poiché la fase 2 contiene in genere codice in linguaggio C, lo spazio richiesto deve considerare sia la dimensione dell’eseguibile della fase 2 che lo spazio dello stack. Inoltre, lo spazio dovrebbe preferibilmente essere allineato alla dimensione della pagina di memoria (tipicamente 4 KB). In genere, 1 MB di spazio RAM è sufficiente. L’intervallo di indirizzi specifico può essere scelto arbitrariamente. Ad esempio, un approccio comune è allocare l’immagine eseguibile della fase 2 per l’esecuzione all’interno di uno spazio di 1 MB a partire dall’indirizzo base della RAM del sistema di 0xc0200000. Tuttavia, allocare la fase 2 all’ultimo MB dell’intero spazio RAM (ovvero (RamEnd-1MB) – RamEnd) è una strategia consigliata.
Denotiamo la dimensione dell’intervallo di spazio RAM allocato come “stage2_size” (in byte) e gli indirizzi di inizio e fine come “stage2_start” e “stage2_end” (entrambi gli indirizzi allineati a confini di 4 byte). Quindi:
stage2_end = stage2_start + stage2_size
Inoltre, è imperativo assicurarsi che l’intervallo di indirizzi allocato sia effettivamente uno spazio RAM scrivibile e leggibile. Per garantire ciò, è necessario testare l’intervallo di indirizzi allocato. Un metodo di test adatto, come quello utilizzato da “blob,” prevede il test delle prime due parole di ogni pagina di memoria per verificarne la capacità di lettura-scrittura.
Copia la fase 2 del Boot Loader nello spazio RAM
Per farlo, assicurati di avere due punti:
- La posizione dell’immagine eseguibile della fase 2 sul dispositivo di archiviazione a stato solido.
- L’indirizzo di inizio dello spazio RAM.
Imposta il puntatore dello stack (SP)
Impostare il puntatore dello stack (sp) prepara l’esecuzione del codice in linguaggio C. In genere, il valore di sp può essere impostato su (stage2_end-4), che rappresenta l’estremità superiore dello spazio RAM di 1 MB allocato nella sezione 3.1.2 (lo stack cresce verso il basso).
Inoltre, prima di impostare il puntatore dello stack, è possibile disattivare il LED come segnale agli utenti che si sta per effettuare una transizione alla fase 2.
Dopo questi passaggi di esecuzione, il layout di memoria fisica del sistema dovrebbe assomigliare al diagramma sottostante.

Vai al punto di ingresso C della fase 2
Dopo che tutto quanto sopra è pronto, puoi passare alla fase 2 del Boot Loader per eseguire. Ad esempio, sui sistemi ARM, questo può essere fatto modificando il registro PC all’indirizzo appropriato. Il layout di memoria del sistema quando l’immagine eseguibile della fase 2 del bootloader è appena stata copiata nello spazio RAM è mostrato nella figura sopra.
Fase 2 del Bootloader
Inizializzazione del dispositivo hardware
- Inizializzare almeno una porta seriale per la comunicazione di output I/O con gli utenti del terminale.
- Inizializzare timer e altri componenti hardware.
Prima di inizializzare questi dispositivi, è anche possibile illuminare il LED per indicare l’inizio dell’esecuzione della funzione main(). Dopo l’inizializzazione del dispositivo, è possibile visualizzare alcune informazioni come stringhe di nome del programma e numeri di versione.
Rilevamento della mappatura della memoria di sistema
La mappatura della memoria si riferisce all’allocazione di intervalli di indirizzi all’interno dell’intero spazio di indirizzi fisici di 4 GB per indirizzare le unità RAM di sistema. Ad esempio, nella CPU SA-1100, uno spazio di indirizzi di 512 MB a partire da 0xC000,0000 serve come spazio di indirizzi RAM di sistema. Nel caso della CPU Samsung S3C44B0X, uno spazio di indirizzi di 64 MB tra 0x0c00,0000 e 0x1000,0000 viene utilizzato per lo spazio di indirizzi RAM di sistema. Sebbene le CPU in genere riservino una parte sostanziale dello spazio di indirizzi alla RAM di sistema, non tutto lo spazio di indirizzi RAM riservato potrebbe essere utilizzato quando si costruiscono sistemi embedded specifici. Pertanto, i sistemi embedded spesso mappano solo una parte dello spazio di indirizzi RAM riservato della CPU alle unità RAM, lasciando inutilizzato una parte dello spazio di indirizzi RAM riservato. Dato questo fatto, la fase 2 del Boot Loader deve esaminare l’intera mappatura della memoria di sistema prima di tentare qualsiasi azione (come leggere un’immagine del kernel memorizzata nello spazio flash nella RAM). Deve sapere quali parti dello spazio di indirizzi RAM riservato della CPU sono effettivamente mappate alle unità di indirizzi RAM e quali sono in uno stato “non utilizzato”.
Descrizione della mappatura della memoria
La seguente struttura dati può essere utilizzata per descrivere un intervallo di indirizzi continuo nello spazio di indirizzi RAM:
typedef struct memory_area_struct {
u32 start; /* the base address of the memory region */
u32 size; /* the byte number of the memory region */
int used;
} memory_area_t;
Tali intervalli di indirizzi contigui all’interno dello spazio di indirizzi RAM possono essere in uno di due stati:
- used=1 indica che l’intervallo di indirizzi continuo è stato implementato ed è effettivamente mappato alle unità RAM.
- used=0 indica che l’intervallo di indirizzi continuo non è implementato nel sistema e rimane inutilizzato.
Sulla base della struttura dati memory_area_t descritta sopra, l’intero spazio di indirizzi RAM riservato della CPU può essere rappresentato da un array di tipo memory_area_t, come mostrato di seguito:
memory_area_t memory_map[NUM_MEM_AREAS] = {
[0 ... (NUM_MEM_AREAS - 1)] = {
.start = 0,
.size = 0,
.used = 0
},
};
Rilevamento della mappatura della memoria
Ecco un algoritmo semplice ma efficace per rilevare la situazione di mappatura della memoria all’interno dell’intero spazio di indirizzi RAM:
/* Initialize the array */
for(i = 0; i < NUM_MEM_AREAS; i++)
memory_map[i].used = 0;
/* Write 0 to all memory locations */
for(addr = MEM_START; addr < MEM_END; addr += PAGE_SIZE)
*(u32 *)addr = 0;
for(i = 0, addr = MEM_START; addr < MEM_END; addr += PAGE_SIZE) {
/*
* Check whether the address space starting from base address
* MEM_START + i * PAGE_SIZE, with a size of PAGE_SIZE, is a valid RAM address space.
*/
Call the algorithm test_mempage() from section 3.1.2;
if (current memory page is not a valid RAM page) {
/* no RAM here */
if (memory_map[i].used )
i++;
continue;
}
/*
* The current page is a valid address range mapped to RAM.
* However, we need to determine if it's an alias of some address page within the 4GB address space.
*/
if (*(u32 *)addr != 0) { /* alias? */
/* This memory page is an alias of an address page within the 4GB address space. */
if (memory_map[i].used )
i++;
continue;
}
/*
* The current page is a valid address range mapped to RAM,
* and it's not an alias of an address page within the 4GB address space.
*/
if (memory_map[i].used == 0) {
memory_map[i].start = addr;
memory_map[i].size = PAGE_SIZE;
memory_map[i].used = 1;
} else {
memory_map[i].size += PAGE_SIZE;
}
} /* end of for (…) */
Dopo aver eseguito l’algoritmo di cui sopra per rilevare lo stato di mappatura della memoria del sistema, il Boot Loader può anche stampare informazioni dettagliate sulla mappatura della memoria alla porta seriale.
Caricamento delle immagini del kernel e del file system root
Pianificazione del layout della memoria
Questo comporta due aspetti:
- l’intervallo di memoria occupato dall’immagine del kernel;
- l’intervallo di memoria occupato dal file system root. Quando si pianifica il layout della memoria, considerare l’indirizzo di base e la dimensione delle immagini.
Per l’immagine del kernel, è comune copiarla in un intervallo di memoria a partire da (MEM_START + 0x8000), circa 1 MB di dimensione (i kernel Linux embedded sono in genere inferiori a 1 MB). Perché lasciare uno spazio di 32 KB da MEM_START a MEM_START + 0x8000? Questo perché il kernel Linux posiziona alcune strutture dati globali in questo segmento di memoria, come i parametri di avvio e le tabelle delle pagine del kernel.
Per l’immagine del file system root, viene in genere copiata nella posizione a partire da MEM_START + 0x0010,0000. Se si utilizza un Ramdisk come immagine del file system root, la dimensione non compressa è in genere di circa 1 MB.
Copia da Flash
Poiché le CPU embedded come ARM in genere indirizzano Flash e altri dispositivi di archiviazione a stato solido all’interno di uno spazio di indirizzi di memoria unificato, la lettura dei dati da Flash è simile alla lettura dalle unità RAM. Un semplice ciclo è sufficiente per copiare l’immagine dal dispositivo Flash:
while (count) {
*dest++ = *src++; /* they are all aligned with word boundary */
count -= 4; /* byte number */
};
Impostazione dei parametri di avvio del kernel
Dopo aver copiato l’immagine del kernel e l’immagine del file system root nello spazio RAM, è possibile preparare l’avvio del kernel Linux. Ma prima di invocare il kernel, è necessario un passaggio di preparazione: impostare i parametri di avvio del kernel Linux.
I kernel Linux dalla versione 2.4.x in poi si aspettano che i parametri di avvio vengano passati sotto forma di un elenco contrassegnato. L’elenco dei parametri di avvio contrassegnato inizia con il tag ATAG_CORE e termina con il tag ATAG_NONE. Ogni tag è costituito da una struttura tag_header che identifica il parametro, seguita da strutture dati contenenti i valori dei parametri. Le strutture dati tag e tag_header sono definite nel file di intestazione include/asm/setup.h del codice sorgente del kernel Linux.
Nei sistemi Linux embedded, i parametri di avvio comuni che devono essere impostati dal Boot Loader includono ATAG_CORE, ATAG_MEM, ATAG_CMDLINE, ATAG_RAMDISK e ATAG_INITRD.
Ad esempio, ecco come impostare ATAG_CORE:
params = (struct tag *)BOOT_PARAMS;
params->hdr.tag = ATAG_CORE;
params->hdr.size = tag_size(tag_core);
params->u.core.flags = 0;
params->u.core.pagesize = 0;
params->u.core.rootdev = 0;
params = tag_next(params);
Qui, BOOT_PARAMS rappresenta l’indirizzo di base iniziale dei parametri di avvio del kernel in memoria e il puntatore params è di tipo struct tag. La macro tag_next() calcola l’indirizzo di partenza del tag successivo immediatamente successivo al tag corrente. È importante notare che l’ID del dispositivo per il file system root del kernel è impostato qui.
Di seguito è riportato un esempio di codice per impostare le informazioni sulla mappatura della memoria:
for (i = 0; i < NUM_MEM_AREAS; i++) {
if (memory_map[i].used) {
params->hdr.tag = ATAG_MEM;
params->hdr.size = tag_size(tag_mem32);
params->u.mem.start = memory_map[i].start;
params->u.mem.size = memory_map[i].size;
params = tag_next(params);
}
}
Nell’array memory_map[], ogni segmento di memoria valido corrisponde a un tag di parametro ATAG_MEM.
Il kernel Linux può ricevere informazioni come parametri della riga di comando durante l’avvio. Ciò ci consente di fornire informazioni sui parametri hardware che il kernel non è in grado di rilevare da solo o di sovrascrivere le informazioni che il kernel ha rilevato. Ad esempio, utilizziamo la stringa del parametro della riga di comando “console=ttyS0,115200n8” per istruire il kernel a utilizzare ttyS0 come console con impostazioni “115200 bps, nessuna parità, 8 bit di dati”. Ecco un esempio di codice per impostare la stringa del parametro della riga di comando del kernel:
char *p;
for (p = commandline; *p == ' '; p++)
;
if (*p == '\0')
return;
params->hdr.tag = ATAG_CMDLINE;
params->hdr.size = (sizeof(struct tag_header) + strlen(p) + 1 + 4) >> 2;
strcpy(params->u.cmdline.cmdline, p);
params = tag_next(params);
Nota che nel codice precedente, quando si imposta la dimensione di tag_header, deve includere il carattere di terminazione ‘\0’ nella stringa e deve essere arrotondato al multiplo di 4 byte più vicino, poiché il membro size della struttura tag_header rappresenta il numero di parole.
Di seguito è riportato un esempio di codice per impostare ATAG_INITRD, indicando la posizione nella RAM in cui è possibile trovare l’immagine initrd (in formato compresso), insieme alla sua dimensione:
params->hdr.tag = ATAG_INITRD2;
params->hdr.size = tag_size(tag_initrd);
params->u.initrd.start = RAMDISK_RAM_BASE;
params->u.initrd.size = INITRD_LEN;
params = tag_next(params);
Infine, impostare il tag ATAG_NONE per concludere l’intero elenco dei parametri di avvio:
static void setup_end_tag(void) {
params->hdr.tag = ATAG_NONE;
params->hdr.size = 0;
}
Invocazione del Kernel
Il Boot Loader chiama il kernel Linux saltando direttamente alla prima istruzione del kernel, ovvero saltando direttamente all’indirizzo MEM_START + 0x8000. Le seguenti condizioni devono essere soddisfatte quando si salta:
- Impostazioni del registro CPU:
R0 = 0
R1 = ID del tipo di macchina (per il numero del tipo di macchina, fare riferimento a linux/arch/arm/tools/mach-types)
R2 = Indirizzo di base iniziale dell’elenco contrassegnato dei parametri di avvio nella RAM - Modalità CPU:
Disabilitare le interruzioni (IRQs e FIQs)
La CPU deve essere in modalità SVC - Impostazioni della cache e della MMU:
La MMU deve essere disattivata
La cache delle istruzioni può essere attiva o disattiva
La cache dei dati deve essere disattivata
Se si utilizza C, l’invocazione del kernel può essere eseguita come segue:
void (*theKernel)(int zero, int arch, u32 params_addr) =
(void (*)(int, int, u32))KERNEL_RAM_BASE;
theKernel(0, ARCH_NUMBER, (u32)kernel_params_start);
Nota che la chiamata di funzione a theKernel() non deve mai restituire un valore. Se restituisce un valore, si è verificato un errore.
BootLoader di sistema embedded Vs Bootloader PC
Nelle architetture PC, il bootloader è costituito dal BIOS (essenzialmente firmware) e dal bootloader del sistema operativo situato nell’MBR del disco rigido (ad esempio, LILO, GRUB). Dopo che il BIOS completa il rilevamento dell’hardware e l’allocazione delle risorse, carica il bootloader dall’MBR del disco rigido nella RAM del sistema e passa il controllo al bootloader del sistema operativo. Il compito principale del bootloader è leggere l’immagine del kernel dal disco rigido nella RAM e quindi saltare al punto di ingresso del kernel per avviare il sistema operativo.
Nei sistemi embedded, in genere non esiste un programma firmware come il BIOS (anche se alcune CPU embedded potrebbero includere un piccolo programma di avvio embedded). Di conseguenza, l’intero compito di caricamento e avvio del sistema è svolto dal bootloader. Ad esempio, in un sistema embedded basato sul core ARM7TDMI, il sistema in genere inizia l’esecuzione all’indirizzo 0x00000000 durante l’accensione o il reset e questo indirizzo di solito contiene il programma bootloader del sistema.
CPU e scheda embedded supportate dal Boot Loader
Ogni diversa architettura CPU ha un Boot Loader diverso. Alcuni Boot Loader supportano anche CPU con architetture multiple. Ad esempio, U-Boot supporta sia l’architettura ARM che l’architettura MIPS. Oltre a fare affidamento sull’architettura della CPU, il Boot Loader dipende anche dalla configurazione di specifici dispositivi a livello di scheda embedded. Ciò significa che il Boot Loader non è necessariamente adatto per entrambe le due diverse schede embedded, anche se sono costruite sulla stessa CPU.
Supporto di installazione del Bootloader
Quando un sistema si accende o si resetta, le CPU in genere recuperano le istruzioni da un insieme di indirizzi predeterminati impostato dal produttore della CPU. Ad esempio, le CPU basate sul core ARM7TDMI in genere recuperano la loro prima istruzione dall’indirizzo 0x00000000 dopo un reset. I sistemi embedded basati su architetture CPU spesso mappano una sorta di dispositivo di archiviazione a stato solido (come ROM, EPROM o FLASH) a questo indirizzo predeterminato. Pertanto, dopo l’accensione del sistema, la CPU esegue prima il programma bootloader.




