BOOT Linux su ARM

Utilizzare una distribuzione Linux su USB è certamente un aspetto interessante e molto utile per le applicazioni embedded. Esistono, però, anche delle soluzioni basate su interfaccia SPI. In questo articolo vedremo che, indipendentemente dal dispositivo utilizzato, la sequenza di boot rimane pressoché identica.

Da diverso tempo sono presenti sul mercato numerose proposte di Linux su USB anche per il mondo embedded. Le figure 1 e 2 mostrano, per esempio, le proposte commerciali della casa francese Calao.

Figura 1: un embedded computer di Calao.

Figura 1: un embedded computer di Calao.

 

Figura 2: Embedded Computer Calao con fattore di forma USB dongle.

Figura 2: Embedded Computer Calao con fattore di forma USB dongle.

Questa è una soluzione basata su Atmel AT91SAM9260, quindi con un core ARM926EJ-S a 190MHz. Il suo ingombro è abbastanza contenuto: 85 x 36 mm. Le figure 3 e 4 mostrano le connessioni verso USB secondo le differenti modalità.

Figura 3: USB Host Interface.

Figura 3: USB Host Interface.

 

Figura 4: USB Device Interface.

Figura 4: USB Device Interface.

Per quale ragione può essere utile disporre di Linux su USB per una soluzione embedded? I dispositivi  USB d’ultima generazione hanno ormai un peso non trascurabile per le applicazioni dedicate. Utilizzare un sistema di questo tipo ci consentirebbe di gestire un’enorme mole d’informazioni; per esempio, si potrebbe spedire da un software applicativo informazioni funzionali, dati di navigazione di un’autovettura, verso un dispositivo in grado di gestirle in maniera efficiente evitando nello stesso tempo di dover scrivere, anche, un file system. Insomma, è possibile utilizzare un modulo software estramemente versatile secondo l’ormai classica formula free of charge. Per realizzare soluzioni di questo tipo si richiedono almeno 64MB di SDARM e 256MB di flash. Grazie all’uso del processore ARM possiamo, inoltre, sfruttare le prerogative di I/O, per esempio USB, Ether net 10/100 e una coppia di host port USB 2.0.

Architettura ARM

L’architettura ARM indica una famiglia di microprocessori RISC a 32-bit utilizzata in gran parte delle applicazioni embedded. Grazie alle sue caratteristiche di basso consumo, in rapporto alle sue prestazioni, questa architettura domina il settore dei dispositivi mobili dove il risparmio energetico delle batterie è di fondamentale importanza. I processori ARM vengono utilizzati in PDA, cellulari, lettori multimediali, videogiochi portatili e periferiche per computer. Importanti rami della famiglia ARM sono i processori XScale e i processori OMAP prodotti da Texas Instruments. I dispositivi  ARM presentano un’architettura RISC ‘load and store’ per parole di 16 e 32 bit, un set di istruzioni ortogonali, 37 registri di interi a 32-bit (6 registri di stato e 31 di uso generale) e 7 modi di operare (USR, FIQ, IRQ, SVC, ABT, SYS, UND). Una delle caratteristiche più interessante dei processori ARM è la capacità di realizzare codici condizionali per ogni istruzione. Inoltre, permette di shiftare  i dati durante le normali operazioni sui dati (operazioni aritmetiche, logiche e di copia di registri). Queste caratteristiche rendono i programmi ARM normalmente più densi degli equivalenti programmi per altri processori RISC. Inoltre, il processore ricorre a meno accessi alla memoria e riesce a riempire meglio le pipeline. Quindi le CPU ARM possono utilizzare frequenze inferiori a quelle di altri processori consumando meno potenza per svolgere gli stessi compiti. Un processore ARM possiede anche caratteristiche viste raramente nei processore RISC come ad esempio l’indirizzamento relativo al PC (il PC negli ARM è un registro a 16 bit) , indirizzamento con il pre e post incremento. Altra interessantissima caratteristica dei processori ARM riguarda le instruction set presenti. Oltre al nativo ARM a 32 bit ne esistono altri due molto interessanti: il Thumb e il Jazelle.

Linux su ARM

Per portare Linux su una board sono richiesti i seguenti  strumenti:

» Un firmware di boostrap. In pratica è un firmware di inizializzazione  adattato per la piattaforma che si intende utilizzare in base alle nostre esigenze. A questo proposito possiamo anche ricorrere al sito di ATMEL, per esempio AT91Bootstrap1.11 rappresenta una buona soluzione. In passato, personalmente ho utilizzato una versione modificata del Darrell Harmon’s bootloader. Il firmware  di boostrap, se preso da una distribuzione GPL, deve essere modificato per rispondere correttamente alla nostra applicazione.

» Un bootloader. Un bootloader non è solo un software che inizializza la scheda, ma è un potente strumento di verifica e di diagnostica della board. Questo modulo deve essere in grado di caricare Linux sulla nostra scheda. Personalmente preferisco di gran lunga U-Boot. Come per boostrap, il bootloader deve essere adattato per la nostra board.

» Una distribuzione Linux (2.4 o 2.6: dipende dalle esigenze)  corredata dalle eventuali patch: a questo proposito può essere utile visitare il  sito http://maxim.org.za/AT91RM9200. Anche la distribuzione Linux deve essere modificata.

» Infine,  un Linux rootdisk scritta per un driver USB. In realtà, una volta generata la nostra distribuzione, occorre fare il set-up della distribuzione per USB. L’obiettivo di quest’articolo è di mostrare le fasi che si devono svolgere per scrivere una versione del bootloader per la nostra board basandoci sulla versione ufficiale, oltre a descrivere alcune caratteristiche di U-Boot. In seguito vedremo la sequenza di boot in ambito ARM: questo ci serve per comprendere come il firmware interagisce con la nostra board e, in caso di testing, come svolgere una sessione di debug per svolgere  il nostro lavoro di verifica e messa a punto della nostra applicazione. In un successivo articolo vedremo come realizzare una distribuzione Linux per la nostra piattaforma hardware. In questo articolo presupponiamo di disporre già di un driver USB come root file system. Con l’ausilio di un bootloader, per esempio Redboot, il sistema  operativo potrebbe essere scaricato via TFTP o prelevato dalla memoria flash per essere in seguito copiato in RAM. Nel primo caso, con Redboot, dovremo interrompere, attraverso il  comando CTRL-C, la sequenza di power up della board per fermare i vari script che il monitor esegue nelle sue varie fasi. Una volta ottenuto il controllo del monitor caricheremo l’immagine del kernel dalla memoria flash con il comando:

fis load zImage

oppure possiamo caricare l’immagine utilizzando  l’utility TFTP (Trivial File Transfer Protocol), ricorrendo al comando:

load -r -v -b 0x80000 -h <TFTP
 server IP address> <kernel image
 file name>

Infine, si utilizza il commando (con la versione 2.4.21 del kernel):

exec -c “console=ttyAM0
 root=/dev/sda1”

o con la versione 2.6.x:

exec -c “console=ttyAM0
 root=/dev/sda1 rootdelay=15”

In questo modo si utilizza il driver USB come root file system: con exec si pone in esecuzione  il kernel.  Il parametro di rootdelay deve essere impostato in relazione alla configurazione del sistema. Con UBoot la sequenza rimane pressoché uguale; infatti:

uboot# tftp 8000 uboot.bin

e, successivamente, U-Boot risponderà con:

From server 10.0.0.1; our IP address is 10.0.0.11
 Filename ‘uboot.bin’. Load address: 0x8000
 Loading: ###################
 done
 Bytes transferred = 95032 (17338 hex)

L’indirizzo e la dimensione del file da caricare sono presenti in alcune variabili di sistema di U-Boot: fileaddr e filesize. Con il seguente comando si visualizzano  i parametri della rete (TFTP e NFS) contenute sempre nella configurazione di U-Boot:

uboot # printenv
 baudrate=19200
 ethaddr=00:40:95:36:35:33
 netmask=255.255.255.0
 ipaddr=10.0.0.11
 serverip=10.0.0.1
 stdin=serial
 stdout=serial
 stderr=serial

Bootloader

Il  sistema operativo Linux per ARM, e per qualsiasi architettura, non può essere eseguito su piattaforme hardware senza la disponibilità di uno strato software che inizializzi la board e alcuni aspetti preliminari del sistema. Per questa ragione ARM Linux richiede un bootloader. Le operazioni che questo deve sovrintendere includono gli aspetti di configurazione della memoria di sistema,  il caricamento dell’immagine del kernel in una zona di memoria, tipicamente una RAM, l’inizializzazione  dei parametri di boot che devono essere passati al kernel, la definizione del codice identificativo dell’architettura e, infine, deve passare il controllo al kernel con valori appropriati dei registri della macchina. Tra le inizializzazioni che un bootloader deve prevedere c’è anche quella di una seriale e un video, in questo modo l’utente sarà in grado di interagire con la board. La tabella 1 non è sicuramente esaustiva; infatti, ATMEL consente di scaricare dal sito un boot loader.

Tabella 1: i bootloader più diffusi.

Tabella 1: i bootloader più diffusi.

Il listato 1 mostra i ciclo principale del bootloader di ATMEL.

main(void)
{
/* ================== 1st step: Hardware Initialization ================= */
/* Performs the hardware initialization */
#ifdef CFG_HW_INIT
                hw_init();
#endif
/* ==================== 2nd step: Load from media ==================== */
                /* Load from Dataflash in RAM */
#ifdef CFG_DATAFLASH
                load_df(AT91C_SPI_PCS_DATAFLASH, IMG_ADDRESS, IMG_SIZE, JUMP_ADDR);
#endif
                /* Load from Nandflash in RAM */
#ifdef CFG_NANDFLASH
                load_nandflash(IMG_ADDRESS, IMG_SIZE, JUMP_ADDR);
#endif
/* Load from Norflash in RAM */
#ifdef CFG_NORFLASH
load_norflash(IMG_ADDRESS, IMG_SIZE, JUMP_ADDR);
#endif
/* ==================== 3rd step: Process the Image =================== */
                /* Uncompress the image */
#ifdef GUNZIP
                decompress_image((void *)IMG_ADDRESS, (void *)JUMP_ADDR, IMG_SIZE);
                /* NOT IMPLEMENTED YET */
#endif /* GUNZIP */
/* ==================== 4th step: Start the application =================== */
               /* Set linux arguments */
#ifdef LINUX_ARG
               linux_arg(LINUX_ARG); /* NOT IMPLEMENTED YET */
#endif /* LINUX_ARG */
               /* Jump to the Image Address */
               return JUMP_ADDR;
}
Listato 1 – bootloader di ATMEL

Il  pacchetto U-Boot è certamente quello che ha il maggiore consenso dalla comunità degli sviluppatori: sia perché è un open-source e sia per la molteplicità di architetture presenti.  Il kernel è visto, rispetto al bootloader, come un programma separato. Per questa ragione il kernel Linux accetta dei parametri dalla linea di comando e questo risulta utile per configurare  il kernel al boot senza dover ricompilare  il sistema. Per esempio:

root=/dev/ram0 rw init=/linuxrc \
 console=ttyS0,115200n8
 console=tty0 \
 ramdisk_size=8192
 cachepolicy=writethrough \

O ancora con:

bootm 0x01030000

si vuole eseguire  il kernel da un indirizzo fisico 0x01030000 in RAM o in flash (dipende dall’architettura della nostra board). Con root si identifica il filesystem root, mentre con init uno script che deve essere eseguito al termine dell’inizializzazione del kernel. Il suo parametro di default è /sbin/init. Con il  parametro console le periferiche utilizzate come I/O, con ro e/o rw si danno le indicazioni al kernel per fare il mount del device di root, rispettivamente, come readonly e/o readwrite. In realtà quello che è mostrato rappresenta solo un piccolo sottoinsieme delle sue possibilità; per una descrizione esaustiva di tutti i parametri  è opportuno consultare il file kernelparameters.txt presente in Documentation.

Scrivere  una variante per bootloader

A questo punto siamo di fronte ad un bivio: o scriverne un bootloader secondo le nostre specifiche, anche se questa soluzione è la meno diffusa, o portare sulla nostra piattaforma una soluzione già presente (come U-Boot). Per prima cosa dobbiamo procurarci l’ultima versione di U-Boot, o una sua versione più recente, ma stabile (a questo riguardo possiamo ricorrere anche alle patch). Il sito www.denx.de/wiki/UBoot rappresenta un buon punto di partenza. Le cartelle che risultano coinvolte in questo sono diverse, tra cui include, board, Cpu, Libxxx (lib-ppc, lib-asm, ..). Vediamo schematicamente la procedura da utilizzare per scrivere una variante di U-Boot per la nostra board. Per prima cosa occorre definire  il nostro config file, nella cartella include/Configs: questo file deve rispecchiare le caratteristiche della nostra board. In questo caso è necessario definire i parametri corretti per i limiti di memoria della nostra applicazione,  definire i parametri della memoria flash (quali i  limiti dei settori), configurare  il chip select in accordo alla mappa di memoria presente sulla scheda, il register  space address, la definizione della SDRAM, il  contenuto della linea di comando da passare al kernel e ogni altro parametro utile per una corretta definizione della board. Successivamente è necessario definire la mappa di memoria da utilizzarsi nella procedura di linking: definire la dimensione dello heap, lo spazio del sistema operativo, la dimensione dei dati globali e l’indirizzo di U-Boot in SDRAM. Successivamente si dovrà definire una nuova regola in makefile per stabilire le regole di compilazione e di linking della nostra configurazione. Per svolgere  il nostro lavoro può essere utile basarsi su un progetto già svolto e presente nella distribuzione, per esempio in board/at91rm9200dk. In questa cartella possiamo  vedere i vari file scritti, gli eventuali driver, il makefile, le direttive del linker, e cosi via. Inoltre è necessario inserire in mach-types.h (include/asm-arm) l’identificativo della nostra architettura, il machine  type. Se risultasse necessario cambiare l’ambiente di compilazione dobbiamo modificare, come ovvio, il Makefile con la seguente regola:

ifeq ($(ARCH),arm)
 CROSS_COMPILE = arm-linux-
 endif

In seguito si specifica  il path per il nostro compilatore, per esempio

export PATH=/usr/local/uclibc0.9.282/
 arm/bin:$PATH

A questo punto possiamo terminare il nostro lavoro con il comando:

make <new board Config target>,
 successivamente con <make all >

otteniamo  il nostro eseguibile (u-boot.bin): una variante di U-Boot senza TFTP.

Boot di linux

Le componenti software coinvolte nel boot del sistema operativo Linux sono essenzialmente tre: il bootloader, l’immagine del kernel e il root file system. La figura 5 mostra la sequenza di boot vista da un PC: un ambiente abbastanza comune e convenzionale.

Figura 5: Linux Boot Process.

Figura 5: Linux Boot Process.

La figura 6, invece, mostra la sequenza principale del boot svolto in ambito ARM.

Figura 6: Linux Boot Arm Boot.

Figura 6: Linux Boot Arm Boot.

A fronte di una richiesta di reset o di power-on del sistema,  il processore esegue una porzione di codice che si trova in un indirizzo di memoria stabilito. In un PC il modulo che è posto in esecuzione, all’accensione, è il BIOS, localizzato in una zona di memoria flash. È il BIOS del PC che si preoccupa di fare una verifica preliminare sui dispositivi presenti e, se superata con successo, avvia la fase di boot del kernel di Linux. Viceversa in un sistema dedicato, il processo di boot è controllato da un differente modulo software con delle leggere differenze rispetto al BIOS. Infatti, a volte questo modulo non è semplicemente un loader, ma è corredato da una serie di utility che permettono di controllare e fare la diagnostica della board. È possibile, per esempio, scaricare il kernel, o qualsiasi applicazione, attraverso TFTP via Ethernet. Dopo la fase di diagnostica e di inizializzazione si passa il controllo al kernel che si dovrà, poi, preoccupare di caricare l’immagine del kernel in memoria. L’entry point dell’immagine del kernel è localizzato in start(), posizionato nel file arch/***/boot/compressed/head.S. Nel caso di un processore ARM, il file sarà presente in “arch/arm/boot/compressed/head.S“. Questo modulo dovrà inizializzare l’ambiente che deve essere utilizzato da Linux, azzerare il segmento BSS di memoria, configurare  il set dei registri utilizzato in Linux, e, successivamente, invocare la funzione decompress_kernel(). L’utility per la decompressione del kernel si trova in “arch/***/boot/compressed/misc.c: decompress_kernel(). Questa funzione decomprime l’immagine del kernel, lo pone in RAM e restituisce l’indirizzo dell’immagine posta in memoria. In un’architettura ARM, la funzione si trova in “arch/arm/boot/compressed/misc.c”.

Una volta completata con successo questa operazione, viene posto in esecuzione  il kernel stesso che è stato precedentente messo in memoria: l’esecuzione parte con l’invocazione della funzione kernel(), in start(). L’entry point del kernel si trova in arch/***/kernel/head.S. Come vediamo il codice è scritto in assembly ed esistono due punti d’ingresso: una per CPU master e l’altra per secondary (per sistemi SMP). In questa funzione si abilita il gestore della memoria paginata, si determina  il tipo di CPU e si rileva se l’eventuale FPU risultasse essere presente. Per configurare il master CPU, la funzione start_kernel() è la prima funzione C, in kernel, ad essere invocata. Su processori ARM la funzione si trova in “arch/arm/kernel/head.S”.  Questa contiene il punto di entrata (entry point) del kernel per le configurazioni master e secondary. Per le configurazioni master, la funzione “mmap_switched()” è chiamata non appena il gestore MMU è abilitato. La prima funzione C del kernel, come si è scritto, è start_kernel(), in init/main.c. Questa funzione si preoccupa di chiamare una lunga lista di funzioni ognuno deputata ad un compito diverso, per esempio c’è chi inizializza  il modulo di gestione dell’interrupt, chi configura la memoria e carica initrd. Al termine si invoca rest_init(). Esiste anche la variante per la CPU secondaria, o secondary per sistemi SMP, denominata secondary_start_kernel(): in questa variante non è invocata la chiamata a rest_init(). La funzione secondary_start_kernel() può essere trovata in arc/arm/kernel/smp.c. Successivamente, è attivato il processo Init, in init/main.c. Il processo è eseguito solo sulla CPU master. La funzione rest_init() provvederà a creare nuovi processi attraverso la chiamata alla funzione  ker nel_thread(), definita  in “arch/arm/kernel/process.c”. Invocazione dello schedulatore. La funzione rest_init(), al termine, invoca la cpu_idle(), dopo aver creato il processo di init. Per i sistemi SMP la cpu_idle() è invocata direttamente da secondary_start_kernel. La funzione cpu_idle si trova in “arch/arm/kernel/process.c”. Al termine si attiva l’immagine initrd che nei sistemi embedded è il root file system. A questo punto la sequenza di boot termina e il sistema operativo prende il controllo del sistema.

Scrivi un commento

EOS-Academy

Ricevi GRATIS le pillole di Elettronica

Ricevi via EMAIL 10 articoli tecnici di approfondimento sulle ultime tecnologie