Dipartimento di Matematica e Informatica
Corso di Laurea in Informatica
Progettazione e Realizzazione di un
Emulatore di Xerox Alto per
Raspberry Pi
Relatore:
Prof. Federico Bergenti
Candidato:
Beretta Davide
Matricola: 222565
Anno Accademico 2013/2014
Indice
Prefazione
1
1 Descrizione del progetto
3
1.1 Xerox Alto . . . . . . . . . .
1.1.1 Tastiera . . . . . . . .
1.1.2 Mouse . . . . . . . . .
1.1.3 Display graco . . . .
1.1.4 Disco sso/processore
1.1.5 Software . . . . . . . .
1.2 Salto . . . . . . . . . . . . . .
1.2.1 Architettura di Salto .
1.3 Raspberry Pi . . . . . . . . .
1.4 Strumenti . . . . . . . . . . .
2 Sviluppo di piAlto
2.1
2.2
2.3
2.4
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
Bare metal su Raspberry Pi . . .
Il kernel . . . . . . . . . . . . . .
Newlib . . . . . . . . . . . . . . .
Salto . . . . . . . . . . . . . . . .
2.4.1 Il le salto.c . . . . . . . .
2.4.2 Analisi e modica di Salto
2.5 CSUD . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
. 3
. 4
. 4
. 5
. 5
. 5
. 6
. 9
. 11
. 13
.
.
.
.
.
.
.
16
16
22
27
29
30
33
43
Conclusioni
51
Bibliograa
53
Prefazione
In questo testo verrà descritto lo sviluppo di piAlto, un emulatore del
personal computer Xerox Alto pensato per funzionare su Raspberry Pi (un
mini PC ARM) in ambiente bare metal, ovvero senza sistema operativo
residente. La scelta di utilizzare il Raspberry Pi in questo modo nasce dalla
volontà di ottenere un comportamento nale del sistema il più simile
possibile a quello dello Xerox Alto in quanto si riducono i tempi di avvio e
non ci sono altri elementi che possono far capire all'utente che
eettivamente non si tratta della macchina originale.
Date le dimensioni del PC scelto infatti è possibile, reperendo l'hardware
adatto, riprodurre anche esteticamente l'aspetto dello Xerox Alto per
esempio collegando un vecchio monitor CRT girato su di un anco al
Raspberry Pi ed utilizzando un mouse ed una tastiera somiglianti a quelli
dell'epoca.
Il progetto piAlto è stato infatti concepito allo scopo di costruire una
macchina da poter collocare nel Museo del Dipartimento di Matematica e
Informatica. Nel caso non fosse possibile riprodurre in modo fedele
l'aspetto della macchina originale si potrebbe utilizzare un normale
monitor LCD ed un mouse e tastiera USB standard oppure modicare
hardware recente per farlo sembrare storico, anche se quest'ultima ipotesi
comporterebbe una quantità di lavoro non indierente.
É stato scelto proprio l'Alto a causa del numero limitato di esemplari
funzionanti al mondo, il che lo rende estremamente raro, e perché fu un
computer molto innovativo, in particolar modo dal punto di vista
dell'interazione con l'utente. Lo Xerox Alto rappresenta una importante
tappa storica dell'informatica moderna, con lo sviluppo di questo computer
infatti vennero introdotte le prime interfacce grache così come le
conosciamo oggi.
La parte del progetto che eettivamente riproduce il comportamento dello
Xerox Alto non è stata scritta da zero ma si è partiti da un emulatore
scritto in C già parzialmente funzionante scaricabile dal Web: Salto ([11]).
É un progetto che non è più attivo da tempo ma emula già sucientemente
bene le varie parti della macchina; non si tratta di un emulatore funzionale
in quanto all'interno cerca di riprodurre in modo fedele il comportamento
delle varie parti dell'Alto compreso la gestione dei task.
1
INDICE
É stato quindi necessario adattare Salto in modo che potesse funzionare sul
dispositivo nale, per fare questo è stato necessario implementare la parte
di sistema che gestisce il boot del Raspberry Pi.
Dato che l'emulatore utilizza le librerie SDL per la scrittura a video è stato
poi necessario scrivere le funzioni per la gestione del framebuer che
andassero a sostituire le librerie originali.
Un altro problema è stata l'assenza delle librerie standard del C; per
evitare di riscrivere funzioni scritte ad hoc non ottimizzate sono state
utilizzate Newlib (https://sourceware.org/newlib), un set di librerie
standard per ambiente embedded.
Nel prossimo capitolo verrà descritta la macchina oggetto dell'emulazione
ovvero lo Xerox Alto, successivamente sarà più dettagliatamente spiegato il
funzionamento di Salto e le applicazioni disponibili. Verrà poi fatta una
piccola introduzione sul Raspberry Pi ed inne saranno introdotti gli
strumenti utilizzati per lo sviluppo di piAlto.
Nel capitolo 3 sarà invece descritto lo sviluppo eettivo e verrà inizialmente
spiegato quindi cosa signica sviluppare per la piattaforma utilizzata senza
sistema operativo. In seguito è presente la documentazione riguardante il
kernel sviluppato a supporto di Salto ed i problemi riscontrati nello
sviluppo. Inne si parlerà dell'integrazione del progetto con Newlib e delle
modiche apportate all'emulatore per ottenere un prodotto funzionante.
Nel capitolo 4 verranno inne tratte le conclusioni e saranno introdotti
possibili aggiustamenti e funzionalità aggiuntive per piAlto.
Occorre evidenziare che sarebbe stato estremamente dicile documentare
ogni prova fatta per cui quello che si cerca di fare in questo testo è di
presentare un riassunto riguardante lo sviluppo delle parti importanti del
progetto.
2
Capitolo 1
Descrizione del progetto
In questo capitolo sarà prima presentata la storia e l'architettura dello
Xerox Alto, in seguito verrà descritto l'emulatore Salto presente in rete e le
applicazioni da questo supportate.
Verrà poi fatta una piccola introduzione sul Raspberry Pi, il dispositivo
hardware per il quale è stato sviluppato piAlto. Inne verranno presentati
gli strumenti utilizzati per lo sviluppo come il compilatore e l'emulatore.
1.1
Xerox Alto
Lo Xerox Alto fu uno dei primi computer ad essere pensati per uso
individuale, quello che noi oggi chiamiamo Personal Computer. Fu prodotto
nel 1972 da Xerox Corporation per motivi di ricerca e il suo nome deriva
dal centro dove fu sviluppato (Xerox PARC).
L'obbiettivo era quello di ottenere un dispositivo che fosse abbastanza
piccolo da poter stare in un ucio ma abbastanza potente da poter
supportare un sistema operativo stabile e di alta qualità oltre ad un display
graco. Lo Xerox Alto era concepito per provvedere a tutte le necessità
dell'utente ed in particolare per permettere agli utilizzatori di scambiarsi
informazioni facilmente. Non era pensato per la produzione di massa e il
costo era proibitivo per permetterne l'utilizzo privato, i pochi esemplari
prodotti (circa 2000) vennero utilizzati perlopiù nelle università e nei centri
di ricerca.
I progettisti dello Xerox PARC si ispirarono all'oN Line System di Douglas
Engelbart e anche se non fu mai realmente commercializzato molte idee
introdotte con l'Alto si ritrovano anche nei computer dei giorni nostri. Lo
Xerox Alto è stato ad esempio il primo computer della storia ad utilizzare
la metafora della scrivania: nacque infatti l'interfaccia graca di tipo
WIMP (Window, Icon, Menu e Pointing device), oggi la norma nella
maggior parte dei computer. Molte altre furono le innovazioni
implementate per la prima volta, come ad esempio la tecnologia Ethernet.
3
1.1 Xerox Alto
A livello hardware l'Alto consiste in 4 parti principali: la tastiera, il mouse,
il display graco e il disco sso/processore. Il cabinet è metallico di color
beige, ben progettato ed il tutto era pensato per stare su di una scrivania o
un tavolo da lavoro.
Figura 1.1: Xerox Alto
1.1.1 Tastiera
La tastiera a prima vista è simile a quelle delle macchine da scrivere con
l'aggiunta di alcuni tasti speciali. É removibile ed abbastanza comoda per
scrivere; ha la particolarità per cui i tasti non sono codicati per cui è
presente un segnale apposito per ogni pulsante, il che permette ai
programmi di sapere facilmente se due o più tasti sono premuti
contemporaneamente.
1.1.2 Mouse
Il mouse è una piccola scatola con tre bottoni al di sopra e diverse rotelle
al di sotto, un cavo sottile lo collega all'Alto e i bottoni sono chiamati red,
yellow e blue. Il movimento viene rilevato usando le sfere presenti al di sotto
della scatola ed è riportato in modo che sia elaborato dalla macchina. Molte
applicazioni possono essere controllate esclusivamente con il mouse ed anche
in questo caso i tasti non sono codicati.
4
1.1 Xerox Alto
1.1.3 Display graco
Il display è forse la parte più impressionante della macchina, le dimensioni
siche sono 8 pollici di larghezza e 10 di altezza con una risoluzione di
606x808 in bianco e nero. É di tipo bit-mapped raster scan, il che signica
che ogni punto sul display è indirizzabile come un bit in memoria ed anche
se la RAM richiesta è maggiore il rendering risulta più veloce. Questa
tecnica mette a disposizione dell'utente un metodo conveniente per
l'accesso al display e permette di controllare in ogni momento cosa viene
visualizzato.
L'Alto può gestire 60 linee di 90 caratteri ciascuna ed il rendering non è
realizzato in hardware. Un set di caratteri può essere creato dall'utente e
visualizzato ed è possibile utilizzare font di dimensioni e forme diverse
contemporaneamente. Essendo ogni punto rappresentato in memoria come
un bit non esiste la possibilità di visualizzare le varie tonalità di grigio che
però possono essere rappresentate combinando i colori nero e bianco per
formare una sorta di texture che assomigli al colore desiderato.
1.1.4 Disco sso/processore
Il processore e il disco sso sono montati in un contenitore grande circa come
un piccolo frigorifero, l'Alto dispone di due dischi ssi da 3 MB ciascuno
mentre la CPU è a 16 bit costruita su misura utilizzando circuiti integrati
TTL. Il processore può eseguire circa 400000 istruzioni per secondo e la
macchina dispone di 128 KB di RAM (espandibile no a 512 KB). Lo Xerox
Alto può eseguire no a 16 task in modo concorrente ma l'utente ha il pieno
controllo solo del proprio; essendo questo il processo a più bassa priorità
gli altri task (controllo tastiera, mouse, disco, rete) possono interromperlo
quando necessario.
1.1.5 Software
L'Alto ha la particolarità di utilizzare il software per eseguire delle
operazioni tipicamente svolte in hardware. La macchina può eseguire tutto
il software utilizzando la rete ed è presente una ROM contente un piccolo
programma che imposta la rete locale utilizzata nel caso qualche parte dei
programmi di sistema non funzioni. Il sistema operativo è scritto in BCPL
(il linguaggio da cui deriva il C) ed è chiamato Alto Operating System,
questo provvede a fornire delle semplicazioni nella comunicazione tra gli
applicativi ed il dispositivo. Uno dei programmi principali che operano
sotto il controllo dell'Alto OS è l'Executive che provvede a interfacciarsi
direttamente con l'utente permettendo l'esecuzione di altri programmi e la
manipolazione di le. Una funzionalità molto interessante dell'Executive è
l'autocompletamento dei nomi; per sfruttare questa caratteristica si scrive
parzialmente il nome di un le o di un programma e si digita un Escape, se
5
1.2 Salto
l'Executive è in grado di identicare in modo univoco il le allora il suo
nome viene completato in automatico. Se invece è digitato `?' allora viene
mostrata la lista di tutti i le che possono corrispondere alla ricerca.
Le funzionalità più interessanti dell'Alto sono relative all'interfaccia utente.
Lo schermo infatti può essere diviso in nestre ed è possibile usare mouse o
tastiera per controllare le applicazioni. Per esempio si possono selezionare
voci ed elementi semplicemente premendo un tasto ed il cursore del mouse
cambia forma nel passare da una zona ad un'altra dello schermo. Sono
presenti programmi per i compiti più comuni come text editor, calcolatrice,
gestore dei le e addirittura un programma di disegno per la creazione e
manipolazione di immagini fatte di punti, curve e testo.
1.2
Salto
Salto è un emulatore dello Xerox Alto scaricabile dal Web che realizza le
principali funzioni della macchina originale cercando di emulare il più
precisamente possibile tutti i componenti della stessa.
Sfortunatamente non è perfettamente stabile e le applicazioni possono
chiudersi inaspettatamente, inoltre presenta dei problemi con il cursore del
mouse che sfarfalla in modo anomalo. Non implementa funzionalità di rete
e non si possono fare scritture permanenti di dati tuttavia è uno strumento
abbastanza completo e permette di rendersi conto sucientemente bene del
funzionamento della macchina originale. Dopo l'avvio di Salto viene
eseguito Alto Executive. Come già detto l'Executive è un interprete a riga
di comandi utilizzato per gestire i le ed eseguire i programmi. Sono
disponibili diverse versioni: Executive/7 (14/10/1976), Executive/11
(26/06/1980) e Executive/12 (8/01/1983).
Input e output del programma sono gestiti solamente in una nestra di
16 linee e si possono usare le wildcard per riferirsi ai le e rendere più facile
l'utilizzo del sistema. Lo Xerox Alto è un ambiente monotasking dove le
applicazioni prendono il completo controllo della macchina durante
l'esecuzione.
Siccome la risoluzione dello Xerox Alto era 606x808 e Salto apre una
nestra che ha almeno queste dimensioni (è presente anche un pannello con
alcuni pulsanti), questo richiede un monitor con una risoluzione di almeno
1280x1024 per poter visualizzare tutta la nestra. La velocità di Salto è
dipendente dalla macchina su cui lo si esegue per cui su computer più lenti
si avranno prestazioni inferiori e viceversa. Questo è dovuto al fatto che il
ciclo macchina principale è implementato utilizzando un concetto di tempo
virtuale legato all'uso di contatori che rappresentano nanosecondi.
6
1.2 Salto
Figura 1.2: Salto con Executive in attesa di input
Saranno di seguito descritte le principali applicazioni disponibili per
Salto:
Neptune è un le manager graco che permette di gestire drive e
cartelle utilizzando esclusivamente il mouse; altre applicazioni per Alto
invece lo utilizzano solo per puntare e richiedono poi la tastiera per iniziare
l'operazione. É presente una scrollbar per gestire la lista di voci e il cursore
cambia icona se ci si trova in quest'area. Il programma permette anche di
modicare o creare un le di testo. Esistevano diversi tipi di mouse per lo
Xerox Alto: quello più conosciuto aveva tre tasti orizzontali, ognuno di un
diverso colore e tutti i manuali fanno riferimento a questo modello in
particolare.
Bravo è un word processor ed era il primo editor del tipo
What You
See Is What You Get. Il testo è formattato tenendo conto dei font e delle
relative dimensioni. Le modiche al documento sono visibili
immediatamente sullo schermo e, a dierenza dei moderni editor, per
abilitare l'input era necessario immettere un comando da tastiera e non
c'erano menù da poter utilizzare. La lista dei comandi disponibili era
presente sui manuali. Il mouse era prevalentemente usato per selezionare
testo e, come nel caso di Neptune, è disponibile una scrollbar sulla sinistra
dello schermo per scorrere i documenti.
7
1.2 Salto
Figura 1.3: Neptune (alla sinistra) e Bravo (alla destra)
Draw è un programma di disegno vettoriale. Invece di trattare le
immagini come griglie di pixel queste vengono viste come un insieme di
punti connessi con le loro proprietà e si può copiare e trasformare ogni
oggetto indipendentemente. É presente un insieme di tool utilizzabili sul
lato sinistro dello schermo e si può selezionare qualsiasi strumento tramite
il mouse. Come nel caso di Bravo per utilizzare funzionalità avanzate è
necessario immettere comandi con la tastiera (vengono usate anche
combinazioni di tasti sfruttando il tasto Control) ed anche in questo caso la
lista di comandi si trova sul manuale.
Markup è un programma di disegno i cui le immagini sono viste come
griglie di pixel. Per eettuare operazioni è possibile, a dierenza di altre
applicazioni, usare menù che vengono richiamati tramite il tasto centrale.
Su Salto questa applicazione è molto instabile.
Laurel è un client e-mail per Xerox Alto che permette l'invio e la
ricezione di e-mail da parte di altri utenti attraverso un server
centralizzato. Questo programma non è utilizzabile in Salto in quanto le
funzionalità di rete non sono implementate; è tuttavia presente un le
e-mail di esempio. L'interfaccia graca è caratterizzata da interessanti
funzionalità: è possibile utilizzare parte del testo come pulsante
selezionabile per lanciare un comando e la toolbar si espande o meno a
seconda si faccia richiesta di informazioni aggiuntive.
8
1.2 Salto
Figura 1.4: Bravo (alla sinistra) e Laurel (alla destra)
Sono inoltre disponibili una calcolatrice (Calculator), un programma di
utilità per la gestione dei dischi (DIEX) e diversi giochi tra cui Galaxian,
Maze, Pac-Man e Astro-roids (un clone di Asteroids).
1.2.1 Architettura di Salto
Sul Web è possibile scaricare i sorgenti oppure la versione precompilata di
Salto per Windows. Utilizzando Linux sono stati scaricati i sorgenti ed
essendo Salto scritto in C sono stati compilati utilizzando GCC
preinstallato nel sistema. Perché la compilazione vada a buon ne occorre
che siano installate almeno le librerie SDL e i relativi le di sviluppo
(tipicamente il pacchetto libsdlxx-dev, dove xx è la versione desiderata).
All'interno del progetto Salto, una volta eseguito GCC, è possibile trovare
diversi eseguibili: aar, aasm, adasm, aldump, convbdf, dumpdsk, edasm,
ppm2c, salto.
Quest'ultimo è l'emulatore vero e proprio mentre gli altri sono semplici
utilità di supporto.
Salto ore molte funzionalità al di là della semplice emulazione, permette
infatti di eettuare screenshot oltre a poter registrare la nestra per
produrre video. Permette inoltre di fare il dump della memoria su di un le
9
1.2 Salto
Figura 1.5: Draw (alla sinistra) e un clone di Galaxian (alla destra)
ed è presente anche una modalità di debug che mostra in tempo reale il
contenuto della RAM e dei registri.
L'emulatore utilizza diversi le di ROM che carica all'avvio e richiede
all'utente di specicare un'immagine del contenuto del disco. Questo
conterrà quindi dati e applicazioni da poter eseguire. Sul Web sono presenti
diversi le di questo tipo, ognuno contenente uno specico insieme di
programmi, dai giochi no alle applicazioni da ucio. Sono presenti in
tutto 48 le rappresentanti la ROM dello Xerox Alto e 9 le contenenti
applicazioni.
É possibile poi cambiare l'associazione tra i pulsanti della propria tastiera e
quella della macchina emulata semplicemente scrivendo nel le
keyboard.conf, oltre a poter metter in pausa l'emulatore in qualsiasi
momento e far procedere l'esecuzione per passi.
Dato che è possibile utilizzare come immagini del contenuto del disco anche
le compressi, Salto utilizza internamente le librerie zlib (http://zlib.net)
per la decompressione ove necessario. Durante la compilazione è possibile
poi decidere se attivare la modalità di debug, la quale prevede molto più
output testuale sulla console di sistema durante l'esecuzione, ed è anche
possibile decidere se attivare o meno il pannello presente in alto nella
nestra da dove si abilita o meno l'utilizzo del mouse e le funzioni di
registrazioni video. Sullo stesso pannello sono presenti a destra due
indicatori dell'utilizzo del disco.
10
1.3 Raspberry Pi
1.3
Raspberry Pi
Figura 1.6: Raspberry Pi
Il Raspberry Pi è un computer a basso costo delle dimensioni di una
carta di credito che può utilizzare monitor dotati di ingresso HDMI o RCA.
Utilizza mouse e tastiere USB standard ed è stato concepito per motivare
persone di tutte le età ad esplorare il mondo della programmazione
attraverso i linguaggi Python e Scratch. É capace di assolvere a tutte le
principali funzioni richieste ad un PC desktop anche se le prestazioni sono
comunque limitate.
Proprio per come è stato concepito il Raspberry Pi viene molto utilizzato
per progetti dove il rapporto costo-prestazioni è importante. É stato scelto
come piattaforma di sviluppo proprio per le dimensioni, il costo basso e la
essibilità nell'impiego. É un computer che è stato pensato per far
sperimentare ed imparare le persone e questo è stato un motivo in più per
utilizzarlo.
A livello software sono presenti diverse distribuzioni Linux come Raspbian
(Debian), Arch Linux, Pidora (Fedora) ed anche alcune in grado di
trasformare il dispositivo in un player audio o addirittura un media center
completo. É disponibile inoltre un porting ancora non uciale di Android.
Sono presenti in rete diversi progetti che mostrano come possa essere usato
nei modi più disparati, dal server BitTorrent alla stazione meteo no ad
arrivare al cluster di Raspberry Pi per creare un (mini) supercomputer.
11
1.3 Raspberry Pi
Le speciche tecniche sono di seguito riportate:
Modello A
Modello B
SoC
Broadcom BCM2835 (CPU + GPU + DSP + SDRAM)
CPU
700 MHz ARM1176JZF-S core (famiglia ARM11)
GPU
Broadcom VideoCore IV, OpenGL ES 2.0, 1080p30 H.264
RAM
256 MB
512 MB
Porte USB
1
2
Output Video
RCA e HDMI
Output Audio
3,5 mm jack e audio tramite HDMI
Memoria
Slot SD
Rete (Ethernet)
Nessuna
Ethernet 10/100 (RJ-45)
2
Periferiche
SSPI, I C, UART, 2x13 pin di GPIO
RTC
Non presente
Potenza assorbita 300 mA, (1,5 W)
700 mA, (3,5 W)
Alimentazione
5 V tramite MicroUSB oppure GPIO
Dimesioni
85,60 mm × 53,98 mm
Come è possibile notare guardando la tabella, a livello hardware sono
presenti numerose periferiche utilizzabili. Un'altra cosa molto interessante è
il fatto che la fase di boot è controllata dalla GPU la quale carica il rmware
dalla scheda SD; in questo modo aggiornare il rmware non è un'operazione
dicile o pericolosa in quanto basta sostituire pochi le sulla scheda.
Tutte le prove sono state eettuate utilizzando un Raspberry Pi modello B in
quanto dotato di due porte USB: una per il mouse e l'altra per la tastiera. Per
l'alimentazione è stato usato un alimentatore dedicato a 1.25A mentre per la
scheda è stata usata una microSD da 16Gb classe 6 con relativo adattatore.
La macchina è stata collegata ad un Monitor TV tramite HDMI.
Per eettuare le prove è stata prima installata Raspbian sulla scheda SD,
in questo modo si è partiti da una congurazione funzionante. Una volta
completata l'installazione la scheda è suddivisa in due partizioni FAT32:
una di boot e l'altra contenente i rimanenti le di sistema. I le di avvio più
importanti sono 4:
ˆ bootloader.bin: il bootloader caricato nella GPU all'avvio.
ˆ
start.elf : il rmware proprietario del SoC Broadcom.
kernel.img: il le contenente il kernel linux il quale viene caricato dal
ˆ
cong.txt: viene usato per congurare il PC e permette tra l'altro
ˆ
rmware all'avvio della macchina.
di decidere la frequenza di lavoro della CPU, quella della RAM, la
risoluzione del monitor e molte altre cose. Utilizzando questo le e le
informazioni presenti in [6] è stato congurato il monitor in modo da
visualizzare correttamente la nestra di Salto.
12
1.4 Strumenti
Sfruttando il meccanismo del le di congurazione è possibile togliere della
complessità al sistema operativo che non è obbligato nella fase di boot a
congurare l'intera macchina.
Per vericare il comportamento di piAlto basta sostituire al le kernel.img
originale quello generato dal processo di compilazione. Un grosso svantaggio
di questo metodo è che per sostituire il kernel occorre ogni volta estrarre
dal Raspberry la scheda SD e reinserirla una volta nito; tutto questo può
portare all'usura del lettore presente sul dispositivo.
1.4
Strumenti
Prima di iniziare lo sviluppo eettivo è stato necessario stabilire quali
strumenti software utilizzare. Invece di eettuare le compilazioni
direttamente sul dispositivo nale si è deciso di utilizzare un notebook con
installato Linux (Xubuntu 14.04) ed un cross-compilatore. In questo modo
è stato molto più semplice e veloce eettuare le prove avendo a disposizione
solamente un Raspberry Pi. Durante la prima fase dello sviluppo è stato
usato un emulatore dell'hardware nale per velocizzare ancora di più lo
sviluppo. Come cross-compilatore è stato scelto GCC perché è open source,
ben integrato in Linux ed è quello più utilizzato dalla comunità di
sviluppatori del Raspberry Pi.
Salto utilizza le librerie standard C per cui era indispensabile ottenere
anche solo un sottoinsieme di queste funzionanti per ARM bare metal. Per
risolvere questo problema sono state prese in considerazione le librerie
Newlib essendo queste open source e molto usate nel mondo embedded.
Sono stati quindi individuati dei le di esempio per vericare il
funzionamento di GCC, questi non utilizzano le librerie standard e sono
scaricabili portandosi nella cartella di destinazione usando il terminale e
digitando il comando:
git clone https://github.com/brianwiddas/pi-baremetal.git.
I sorgenti utilizzati congurano lo stack, la paginazione, gli interrupt ed il
framebuer. Una volta preparato il tutto vengono eettuate a schermo
delle stampe e viene vericato il corretto funzionamento degli interrupt.
Questi le sono stati molto utili per capire il funzionamento ed il dialogo
con la CPU e la GPU di sistema.
Facendo delle ricerche sono state scaricate e provate due versioni della
toolchain (l'insieme degli strumenti per la compilazione e l'assemblaggio dei
sorgenti) precompilate per ARM bare metal e quindi con target
arm-none-eabi . Entrambe sono state scartate in quanto il più delle volte
riuscivano a compilare gli esempi in modo corretto ma quando venivano
inserite chiamate alle librerie standard venivano restituiti errori per cui si è
13
1.4 Strumenti
deciso di scaricare i sorgenti di GCC e Newlib per compilare e congurare
tutto in modo corretto. Basandosi su commenti, guide presenti in rete e
facendo prove con parametri di congurazione diversi si è poi giunti al
risultato ed inne, allo scopo di rendere il tutto il più ripetibile, si è creato
uno script che in automatico scarica tutti i componenti necessari, provvede
alle corrette congurazioni e compila il tutto ottenendo una toolchain
completa e funzionante. Partendo da una installazione di Xubuntu 14.04
prima di eseguire lo script è necessario installare i pacchetti Texinfo, m4 e
g++.
I componenti utilizzati per l'installazione sono Gnu Mpc 1.0.1, GMP 5.1.1,
GCC 4.8.0, Binutils 2.23.2, Gnu Mpfr 3.1.2 e Newlib 1.2.0.
Sullo stesso sistema Linux possono coesistere più toolchain, è suciente che
la cartella di installazione sia dierente; a questo punto occorre modicare
il le bash.rc nella home dell'utente inserendo il percorso della cartella
contenente gli eseguibili del compilatore e degli altri strumenti che si vuole
utilizzare.
Una volta ottenuta la toolchain funzionante, allo scopo eettuare più
velocemente le prove ed evitare di scrivere continuamente la scheda SD,
sono state eettuate delle ricerche per ottenere un emulatore di Raspberry
Pi funzionante. Per tutte le prove fatte in questo senso sono stati utilizzati
i le di esempio compilati con GCC.
La prima prova è stata fatta utilizzando QEMU, in particolare
qemu-system-arm presente tra i repository del sistema ospite. Come
risultato l'emulazione si bloccava in modo anomalo; questo è dovuto al
fatto che, come scoperto successivamente, QEMU non supporta ancora
direttamente il Raspberry Pi. Per la prova successiva sono stati scaricati
direttamente i sorgenti dell'emulatore e si è cercato di compilarli per
l'architettura desiderata; in questo caso è emerso in modo ancora più
evidente come QEMU non supporti l'hardware scelto in quanto
l'architettura disponibile che si avvicina di più a quella desiderata è
VersatilePCB, molto generica e non soddisfacente. Utilizzando la versione
compilata in questo modo infatti l'emulatore rimane inattivo e la nestra
principale risulta completamente nera.
Scartate queste due opzioni sono state fatte ricerche ed è stato individuato
un progetto interessante raggiungibile all'indirizzo:
https://github.com/Torlus/qemu/tree/rpi.
Questo progetto consiste in una versione di QEMU modicata che aggiunge
il supporto al Raspberry Pi; il tutto è ancora sperimentale e non fa parte
del codice sorgente uciale. Anche se è ancora in fase di sviluppo gestisce
correttamente il framebuer e le parti principali dell'ARM tuttavia non è
ancora completo il supporto per il controller USB. Per questo motivo
questo strumento è stato utilizzato solamente nella prima parte dello
14
1.4 Strumenti
sviluppo di piAlto; quando è stato invece necessario implementare la parte
relativa al mouse e alla tastiera è stato utilizzato il dispositivo sico. Per la
gestione del codice sorgente del progetto è stato usato un repository SVN
privato in rete. Anche se piAlto è stato sviluppato da una singola persona
l'utilizzo di questo strumento permette di avere una sola copia di
riferimento ed evita che esistano più versioni con date dierenti che
potrebbero portare confusione. Un altro motivo per cui si è scelto di
utilizzare Subversion è la possibilità di tenere la storia dello sviluppo e di
poter ottenere le versioni precedenti in modo facile e ripetibile.
15
Capitolo 2
Sviluppo di piAlto
In questo capitolo verrà prima introdotta l'architettura del SoC Broadcom
nel dettaglio e successivamente verranno descritte in modo più completo le
due parti principali di piAlto: il kernel e le modiche fatte a Salto per ottenere
un eseguibile funzionante. Verrà inoltre descritta l'integrazione del progetto
con le librerie Newlib e il driver USB.
2.1
Bare metal su Raspberry Pi
Studiando i sorgenti di esempio precedentemente citati e utilizzando la
documentazione online (wiki, esempi e manuale dell'ARM) è stato possibile
capire il funzionamento generale del Raspberry Pi.
A dierenza di altre architetture ore ben 7 modalità di funzionamento:
User, Fast Interrupt, Interrupt, Supervisor, Undened, Abort, e System
Mode. Tutte le modalità tranne quella User sono privilegiate, il che signica
che è possibile eseguire operazioni per la gestione degli interrupt,
controllare la paginazione e operare su registri tipicamente non accessibili
all'utente. Ogni modalità ha un suo stack pointer che all'avvio non è
impostato automaticamente. Al boot la CPU si trova in Supervisor Mode
per cui nel caso sia necessario lavorare sempre senza sistema operativo
residente basta rimanere in questa modalità ed impostare lo stack pointer
per poter eettuare chiamate a funzione.
Per la gestione degli interrupt l'ARMv4+ prevede la presenza di 7 vettori
in memoria a dierenza dell'architettura x86 che ne prevede 256. Non è
inoltre possibile utilizzare l'istruzione int N ma si deve usare il comando
SWI (SofWare Interrupt) in quanto è presente un solo vettore che identica
il gestore per l'interrupt software.
A dierenza dell'architettura x86 dove ogni vettore aveva un livello di
privilegio associato è stata introdotta l'idea di dierenziare le modalità di
funzionamento della CPU; quando l'istruzione SWI viene eseguita l'ARM
passa automaticamente in Supervisor Mode e, utilizzando il vettore di
16
2.1 Bare metal su Raspberry Pi
indirizzi fornito dal kernel, viene individuato ed eseguito il gestore corretto.
Le rimanenti modalità sono utilizzate per gli scopi più specici:
ˆ
System Mode: è una via di mezzo tra la modalità Supervisor e la
ˆ
User Mode: è l'unica modalità non privilegiata ed è stata concepita
ˆ
Abort Mode: è utilizzata nel caso ci sia un fallimento nel
ˆ
Fast Interrupt Mode (FIQ Mode): è usata per gestire gli interrupt
ˆ
Interrupt Mode (IRQ Mode): l'altra modalità lecita per la gestione
ˆ
Undened Mode: è usata per gestire l'esecuzione di istruzioni non
User in quanto permette il livello di privilegio massimo nonostante
condivida il set di registri della modalità User.
per permettere l'esecuzione di codice utente senza il rischio di
compromettere l'integrità del sistema operativo.
caricamento di un istruzione (Prefetch Abort) oppure di dati (Data
Abort). In quest'ultimo caso si verica l'abort prima che l'istruzione
alteri lo stato del sistema.
che necessitano di velocità elevate e ha un set di registri diverso dalla
IRQ Mode. Gli interrupt gestiti tramite FIQ hanno priorità maggiore
rispetto a quelli più lenti e possono interrompere questi ultimi se
necessario.
degli interrupt, in particolare tutti quelli che non richiedono velocità
elevate usano questa modalità.
denite.
Come già accennato il boot del Raspberry Pi è gestito dalla GPU che
carica il rmware e successivamente il le kernel.img all'indirizzo 0x8000. A
questo punto la GPU passa il controllo dell'esecuzione al le appena caricato
che deve preoccuparsi di portare il sistema ad uno stato consistente prima
di poter eettuare altre operazioni.
La prima operazione da fare all'avvio è impostare quindi lo stack pointer;
dato che lo stack cresce in memoria verso il basso è comodo tenerlo al di
sotto del punto in cui è stato caricato il kernel; subito dopo si può procedere
alla congurazione della memoria, del framebuer e degli interrupt.
Gestione della memoria
La gestione della memoria a un livello così basso di astrazione richiede di
tenere in considerazione sia gli indirizzi sici che quelli virtuali. Sfogliando
il manuale di riferimento del SoC utilizzato si può vedere come i primi
siano organizzati in due parti: la memoria di sistema e quella dedicata alle
periferiche. La prima parte è denita dall'indirizzo 0 no a 256MB o
512MB di RAM a seconda del modello del Raspberry Pi utilizzato. La
17
2.1 Bare metal su Raspberry Pi
memoria dedicata alle periferiche invece è posizionata al di sopra in un'area
non sicamente utilizzabile dal sistema operativo. In questa zona è
possibile accedere ai registri di controllo per le periferiche ed quindi
comunicare con queste. Per esempio si possono usare i registri per
impostare un timer, controllare i LED presenti sul PC o altro ancora.
Durante la prima parte dello sviluppo si è pensato di utilizzare un prolo
della memoria di tipo at dove l'indirizzo virtuale è uguale a quello sico e
senza utilizzare la paginazione, per fare questo semplicemente non veniva
abilitata l'MMU di sistema.
L'idea di fondo era che senza l'utilizzo della paginazione si avrebbe avuto
un guadagno di prestazioni dovuto al fatto che non era necessario sprecare
accessi alla memoria per via delle traduzioni necessarie. Probabilmente
questa convinzione avrebbe portato al risultato sperato su architettura x86
ma non lavorando con ARM. Su questa architettura infatti il
funzionamento dell'MMU è un requisito fondamentale per l'abilitazione
della cache di sistema. L'utilizzo della cache può incidere in modo pesante
sulle prestazioni complessive della macchina in modo particolare quando si
eettuano numerosi accessi in modo continuo a dati globali o statici per i
quali non vale il meccanismo dello stack.
Una soluzione semplice a questo problema è abilitare la paginazione e
applicare l'identity paging ovvero fare in modo che l'indirizzo sico e quello
virtuale siano lo stesso; in questo modo l'inizializzazione è semplice e
trasparente alle altre parti del programma. La paginazione può essere ad un
livello o a due, in base alla nezza che si vuole ottenere nella mappatura.
Possono per esempio essere utilizzate pagine rappresentanti 4096 byte ma
anche di 1MB (dette sezioni ). Possono essere impostate pagine che
rappresentano addirittura 16MB di RAM e vengono dette super section.
Si è scelto di utilizzare pagine di 1MB perché dovendo semplicemente fare
identity paging non era necessario utilizzarne di dimensioni più piccole. Per
abilitare l'MMU è quindi necessaria una sola tabella con 4096 voci, questo
perché lo spazio di indirizzamento su architettura a 32 bit è 4 GB (una
voce per ogni MB di memoria).
La tabella deve essere allineata alla parola cioè ad un multiplo di 4 byte e
sono presenti diversi registri appositamente predisposti per congurare la
paginazione.
Nel caso si utilizzino pagine di 1MB la voce corrispondente in tabella è di 4
byte di cui i 20 bit più signicativi identicano l'inizio dell'area di memoria
da associare all'indirizzo virtuale. Gli altri bit identicano le proprietà
denite per quella zona di RAM. I bit particolarmente importanti sono
10:11 (AP), 2 (B) e 3 (C); i primi servono per impostare i permessi di
lettura/scrittura per la sezione mentre i bit C e B indicano se per la pagina
deve essere abilitata la cache o se questa debba essere messa in un buer. I
bit di AP sono stati impostati a 1 entrambi per tutte le pagine per poterle
sia leggere che scrivere.
18
2.1 Bare metal su Raspberry Pi
La procedura di inizializzazione consiste quindi nel costruire la tabella di
4096 voci impostando le proprietà per tutte le sezioni. Si può decidere
quindi la politica di caching per la sezione utilizzando i bit C e B oltre a
12:14 (TEX). Per esempio impostando TEX = b000 e CB = b10 la pagina
è trattata come memoria normale con cache abilitata mentre impostando
TEX = b000 e CB = b01 la zona di memoria associata viene considerata
dedicata a periferiche condivise per la quale è quindi disabilitato il caching.
Sfruttando questo meccanismo è quindi possibile ottenere aree di RAM
disponibili per eventuali buer utili alla comunicazione con le periferiche
(come il controller USB) le quali, anché il trasferimento di informazioni
vada a buon ne, necessitano appunto che la cache non sia usata.
Subito dopo aver creato e riempito la tabella vengono modicati due
registri di controllo (System Control Register e Auxiliary Control Register )
per assicurare che i campi TEX e AP siano strutturati secondo quanto
detto nora. Occorre poi impostare un registro di controllo che contiene
l'indirizzo della tabella e invalidare il contenuto della cache perchè non
possa essere utilizzata in seguito. Viene inne scritto nuovamente il System
Control Register per abilitare la data cache, l'instruction cache e il program
ow prediction oltre all'MMU stessa.
Una volta terminata la procedura di inizializzazione sono quindi abilitate
tutte le ottimizzazioni principali disponibili ed il tutto risulta trasparente
alle altre parti del programma.
Il framebuer
Il framebuer può essere visto semplicemente come una matrice di pixel le
cui dimensioni variano in base alla risoluzione selezionata, ogni pixel
occupa una quantità di memoria che dipende dal bpp (bit per pixel ). Nel
caso si utilizzino 32 bpp si ha che ogni pixel occupa una zona di memoria di
4 byte; per colorare il pixel sullo schermo basta scrivere il valore associato
al colore utilizzando un opportuna codica dipendente dal bpp.
Per congurare il framebuer occorre comunicare con la GPU tramite il
meccanismo delle mailbox ([7]) il quale permette una comunicazione di tipo
request-response tra il programma e il VideoCore (GPU di sistema).
Utilizzando questa interfaccia è possibile non solo impostare il framebuer
ma anche ottenere informazioni importanti come dimensione della memoria
RAM usabile per il sistema o dedicata alla GPU, controllare l'overclocking
e molto altro.
La comunicazione avviene fornendo un buer che contiene le richieste
(ognuna identicata da un tag) e gli argomenti specici per quella richiesta;
è quindi possibile, tramite questo meccanismo, eettuare request multiple
con un unico trasferimento di informazioni.
É implementato il meccanismo dei canali: ne esistono 16 ed ognuno deve
essere utilizzato per realizzare un tipo di comunicazione. In particolare i
19
2.1 Bare metal su Raspberry Pi
canali 8 e 9 vengono usati rispettivamente per la comunicazione tra ARM e
VideoCore e viceversa.
Lo scambio di dati deve avvenire rispettando le seguenti regole:
ˆ La risposta sovrascrive la richiesta.
ˆ Il buer usato per la comunicazione deve essere allineato a 16 byte in
quanto solo i 28 bit più signicativi sono trasmessi mentre i rimanenti
4 bit identicano il canale.
ˆ I tag sconosciuti vengono ignorati.
ˆ La risposta può contenere tag non richiesti.
ˆ La lunghezza del tag di risposta può essere più lunga di quanto ci si
aspetti in quanto informazioni aggiuntive potranno essere fornite in
futuro.
ˆ Tutti i valori senza segno a 16/32/64 bit sono ordinati in base
all'architettura ospite (little o big endian).
ˆ I tag devono essere processati in ordine ad eccezione del caso in cui
l'interfaccia richieda tag multipli per una singola operazione.
Per iniziare la comunicazione occorre quindi allocare il buer e
formattare la richiesta al suo interno tenendo conto delle regole appena
citate. Di seguito verrà presentato come il contenuto del buer deve essere
organizzato, indicando con il presso u e s un intero rispettivamente senza
segno e con segno.
ˆ u32: dimensione del buer in byte includendo header, tag nale e
padding
ˆ u32: codice della richiesta/risposta
Richiesta
0x00000000: richiesta da processare
Tutti gli altri valori sono riservati
Risposta
0x80000000: richiesta completata con successo
0x80000001: errore nel parsing della richiesta
Tutti gli altri valori sono riservati
ˆ u8: sequenza di tag concatenati
20
2.1 Bare metal su Raspberry Pi
ˆ ...
ˆ u32: 0 (tag nale)
ˆ u8: padding
ˆ ...
Ogni tag è invece organizzato internamente in questo modo:
ˆ u32: identicatore del tag
ˆ u32: dimensione del buer del value buer in byte
ˆ u32: 1 bit (MSB) che indica una richiesta (0) oppure una risposta (1)
e i rimanenti 31 bit rappresentano la lunghezza del valore
ˆ u8: sequenza di byte che rappresentano il value buer
ˆ ...
Il value buer deve essere allineato a 32 bit per cui viene aggiunto del
padding alla ne quando necessario.
Gestione degli interrupt
All'avvio gli interrupt sono disabilitati e prima di poterli abilitare occorre
fornire gli opportuni gestori. Questi non sono altro che funzioni le quali
vengono chiamate quando l'interruzione è intercettata.
Come già detto esistono 7 tipi di interrupt e sono: Reset, Data Abort, FIQ,
IRQ, Prefetch Abort, SWI, Undened Istruction. Il primo avviene nel caso
di reset del core ARM; Data Abort e Prefetch Abort servono per gestire le
analoghe situazioni precedentemente descritte mentre FIQ e IRQ gestiscono
gli interrupt hardware veloci e non rispettivamente. Inne SWI viene
utilizzato per gestire le interruzioni software e Undened Instruction, come
suggerisce il nome, tratta l'esecuzione di istruzioni non riconosciute. Per
poter impostare i gestori richiesti esiste un apposito registro ed occorre
utilizzare il coprocessore CP15 il quale si occupa di controllare e fornire
informazioni di status per le funzioni del processore ARM. Il coprocessore
dispone di diversi registri, in particolare il registro Vector Base Address
Register permette di impostare l'indirizzo del vettore contenente gli
indirizzi dei gestori. É possibile usare un'unica istruzione assembly per
eettuare quanto detto:
asm volatile ( " mcr p15 , 0 , %[ addr ] , c12 , c0 , 0 " : : [ addr ] " r "
(& interrupt_vectors ) ) ;
dove addr è l'indirizzo del vettore. Per abilitare gli interrupt invece basta
utilizzare l'istruzione apposita cpsid:
asm volatile ( " cpsid i " ) ;
21
2.2 Il kernel
2.2
Il kernel
Il piccolo kernel sviluppato per supportare Salto è composto da solamente
da 12 le: 1 le assembly e 10 le C, oltre ad un ulteriore le C che permette
l'integrazione con le librerie Newlib. Sono stati creati poi un linker script
e un Makele ; il primo è necessario per indicare al linker come deve essere
assemblato l'eseguibile mentre il secondo è utile per rendere automatica e
veloce la compilazione del progetto. Saranno in seguito descritti in dettaglio
tutti questi le allo scopo di spiegarne lo scopo e come questi interagiscano.
linkscript
Un eseguibile è composto tipicamente da varie parti, in particolare le più
comuni sono data, bss, rodata e text. La sezione data contiene tutti i dati
inizializzati, rodata i dati costanti, text il codice eseguibile e bss tutti i dati
non inizializzati. Questi ultimi sono tipicamente tutti i dati globali o statici
che, in caso sia presente un sistema ospite, vengono sempre inizializzati in
automatico a 0. Nel caso si programmi senza sistema operativo residente
questo non avviene e deve essere fatto manualmente. Questo le serve per
comunicare al linker come devono essere disposte le varie parti
dell'eseguibile in memoria, in particolare kernel.elf (con cui vine creato
kernel.img) deve essere caricato all'indirizzo 0x8000.
Le sezioni vengono caricate in ordine ed allineate a 4096 byte (dimensione
tipica di una pagina) e l'ordine scelto è text, data, rodata e bss. Nel le
vengono dichiarati dei simboli preceduti da `_', questi sono visibili dai le
sorgente e sono utili per ricavare gli indirizzi di inizio e ne delle varie
sezioni. Per esempio se si vuole stabilire dove la memoria utilizzata nisce
basta leggere il valore di _bssend che indica appunto la ne della sezione
bss cioè l'ultima presente in RAM.
Makele
Per eettuare la compilazione e l'assemblaggio dell'eseguibile si è sin
dall'inizio utilizzato il comando make ed un makele. Questo però era
molto semplice ed ogni volta che veniva usato tutti i le oggetto venivano
cancellati ed il progetto era ricompilato completamente.
Con il crescere delle dimensioni del codice sorgente è diventato necessario
riscrivere questo le da zero in modo da ottenere che solamente i le
modicati vengano ricompilati accorciando i tempi necessari per lo
sviluppo ed i test.
22
2.2 Il kernel
start.s
Questo le è l'unico scritto completamente in assembly e si occupa solamente
di impostare lo stack pointer e abilitare la FPU per poi passare il controllo
dell'esecuzione alle funzioni di inizializzazione scritte in C.
Viene denito il simbolo globale _start che è utilizzato dal linker in fase
di assemblaggio dell'eseguibile, in questo modo si indica che quel punto è
l'inizio della sezione contenente il codice eseguibile e le istruzioni successive
dovranno essere le prime ad essere eseguite una volta avviato il dispositivo.
É stato necessario abilitare la FPU in quanto sono stati riscontrati problemi
con la funzione printf(), questa al suo interno fa uso di tipi oating point
per cui se la FPU è disabilitata e si cerca di utilizzare la funzione quello che
si ottiene è il blocco del programma.
libgcc.a
Il SoC Broadcom utilizzato ha la particolarità di non implementare le
operazioni di divisione e modulo tra interi in hardware, queste operazioni
devono essere riprodotte tramite software.
GGC fornisce la libreria di basso livello libgcc.a per alcune piattaforme e
genera le chiamate alle funzioni in questa libreria durante la compilazione
per sostituire quelle operazioni troppo complicate da implementare con
funzioni inline.
Oltre ad implementare divisione e modulo tra interi per alcune piattaforme
vengono fornite tutte le operazioni tra oating point e xed point oltre a
routine per la gestione delle eccezioni e molto altro. La maggior parte delle
funzionalità sono implementate in codice C indipendente dalla macchina
mentre una parte deve essere scritta in assembly quando l'architettura di
destinazione lo richiede.
divby0.c
Il le contiene un'unica funzione chiamata raise() la quale viene chiamata
ogni volta che viene eettuata una divisione per 0. Quello che si è scelto
di fare per gestire questa situazione è semplicemente segnalare il problema
tramite delle stampe opportune e bloccare la macchina tramite un ciclo.
initsys.c
Questo sorgente C contiene le prime due funzioni di inizializzazione del
dispositivo: la prima, chiamata get_info(), utilizza il meccanismo delle
mailbox per ottenere informazioni come la quantità di RAM destinata alla
CPU e quella invece riservata al VideoCore. Queste informazioni sono utili
per poter impostare la quantità massima di heap utilizzabile dal sistema.
La parte di RAM compresa tra la ne della sezione bss dell'eseguibile e la
23
2.2 Il kernel
ne della RAM riservata alla CPU ARM è infatti utilizzabile come stack
oppure come heap e, dato che si è deciso di sistemare lo stack al di sotto
del sistema in memoria, questa zona si può considerare totalmente
utilizzabile per allocazione dinamica. L'altra funzione di inizializzazione è
initsys() la quale viene chiamata alla ne del le start.s ; questa
semplicemente si occupa di preservare gli argomenti passati dalla CPU nei
registri r0, r1, r2 (utilizzati in seguito), inizializzare la sezione bss con 0
utilizzando i simboli preparati dal linker e chiamare get_info(). In seguito
il controllo viene dato ad un'altra funzione di nome kmain().
led.c
Contiene alcune funzioni per l'inizializzazione ed il controllo dei LED presenti
su Raspberry Pi. Questo le originariamente faceva parte degli esempi C di
partenza e non è stato modicato. Anche se non è utilizzato in piAlto è stato
mantenuto nel caso in futuro sia necessario l'output tramite i LED presenti
sul dispositivo.
atags.c
La CPU ARM all'avvio prepara una serie di informazioni in memoria per il
sistema operativo se queste vengono richieste (per disabilitare questa
funzionalità occorre modicare il le cong.txt ). Queste informazioni sono
organizzate tramite tag e sono dette ATAGS (ARM TAGS). Il le atags.c
contiene una serie di funzioni utilizzate allo scopo di stampare le
informazioni a video; questo le faceva parte degli esempi di partenza e è
stato mantenuto per permettere in caso di bisogno di ricavare le
informazioni necessarie. Tramite queste funzioni è infatti possibile capire
come sono strutturati gli ATAGS in memoria e come accedervi.
textutils.c
Contiene solamente due funzioni: tohex() e todec(). La prima converte un
intero senza segno in una stringa che è la sua rappresentazione in esadecimale
mentre la seconda eettua la stessa cosa con la dierenza che la stringa
generata è la rappresentazione in decimale dell'intero. Queste due funzioni
venivano utilizzate negli esempi per eettuare stampe di interi e sono state
quasi ovunque sostituite tramite printf(). Vengono ancora usate solamente
quando non è possibile utilizzare le funzioni delle librerie standard ovvero
nelle system call.
interrupts.c
In questo le sono deniti i gestori per i 7 tipi di interrupt e una piccola
funzione di inizializzazione. In particolare esistono i gestori per gli interrupt
24
2.2 Il kernel
di Abort (Data e Prefetch) che visualizzano stampe di debug e non
ripristinano la macchina. É presente un gestore per gli IRQ ma, non essedo
questo utile per piAlto, non fa nulla di rilevante e uno simile per gli
interrupt software (SWI). Tutti gli altri tipi di interrupt vengono gestiti
come situazione anomala da un'unica funzione che, in caso venga chiamata,
blocca il Raspberry Pi.
mailbox.c
Contiene due funzioni per leggere e scrivere le mailbox usando dei registri
appositi mappati in RAM. Normalmente tali registri hanno indirizzi
0x2000B880, 0x2000B898 e 0x2000B8A0. Il primo viene utilizzato per
leggere la mailbox, il secondo per scriverla ed il terzo per vericarne lo
stato. É importante sottolineare che se viene abilitata la cache di sistema
utilizzando questi registri possono sorgere problemi. Lo spazio di
indirizzamento del Raspberry è stato diviso in 4 parti e in ogni zona è
mappata la stessa memoria, cambia solo la modalità di accesso. É possibile
quindi accedere agli stessi registri senza che la cache venga utilizzata
semplicemente usando indirizzi diversi (utilizzando quindi i registri
0xE000B880, 0xE000B898 e 0xE000B8A0).
framebuer.c
Prima di poter visualizzare oggetti e testo sullo schermo occorre impostare
la risoluzione e la nestra di lavoro. Tutto questo è gestito da due funzioni:
fb_init() e fb_setup(). La prima si occupa di impostare la risoluzione
ottimale che, in caso l'esecuzione sia eettuata per mezzo dell'emulatore è
di 640x480 (il massimo possibile su QEMU al momento) mentre
normalmente viene scelta 800x600 in quanto, abilitando la rotazione dello
schermo a 90 gradi, è molto simile a quella originale dello Xerox Alto
(606x808). Una volta impostata la risoluzione tramite mailbox si
memorizzano tutte le informazioni relative allo schermo (pitch, bpp,
indirizzo sico e dimensione del framebuer).
La funzione fb_setup() si occupa di restringere lo spazio disponibile sullo
schermo a dimensioni pressate in modo che siano gestiti correttamente i
casi in cui la risoluzione dello Xerox Alto sia diversa da quella oerta dal
monitor usato. Per esempio se si decide di usare un monitor con una
risoluzione di 1024x768 viene creata una nestra di dimensioni 606x768
centrata in larghezza e solo all'interno di questa nestra sarà possibile la
visualizzazione. Tutto il software di gestione del video è stato pensato in
questo modo per cui usando qualsiasi delle funzioni grache presenti il
comportamento deve essere coerente a quanto detto.
Si è pensato di implementare anche la rotazione dello schermo tramite
software; è possibile abilitarla prima della compilazione agendo su di un
25
2.2 Il kernel
ag globale. Si è scoperto in seguito che questa funzionalità è implementata
anche in hardware dal Raspberry Pi ed è attivabile modicando il le
cong.txt ma, nonostante questo, si è deciso di lasciare il codice di gestione
della rotazione per completezza. Una volta completata la congurazione
dello schermo la nestra viene riempita con il colore di sfondo di default
(bianco).
graphics.c
Il servizio principale che il kernel deve fornire a Salto oltre a inizializzare
correttamente il Raspberry Pi è l'output a video. Sono state sviluppate
diverse funzioni per implementare le operazioni più basilari anche se molte
di queste non sono direttamente utilizzate da Salto ma sono state inserite
per permettere la stampa di testo da parte del kernel o per rendere il lavoro
più completo. La funzione di base è quella per colorare un pixel, usando
questa è stata implementata la stampa di un carattere. Sfruttando
quest'ultima sono state implementate due funzioni per la stampa di testo,
una permette la visualizzazione in un punto qualsiasi della nestra mentre
l'altra implementa il comportamento classico di una shell di sistema per cui
è gestito il newline, lo scroll e la posizione del testo è calcolata in modo
automatico. Sono state inoltre implementate diverse utilità per cambiare il
colore di sfondo o del carattere, per riempire la nestra di un colore o per
impostare la console in modalità debug. Questa è abilitata prima dell'avvio
di Salto e utilizza solo l'ultima riga disponibile nella nestra per la stampa
di informazioni di debug durante l'esecuzione dell'emulatore. In questo
modo è possibile usare Salto e visualizzare anche informazioni utili che
normalmente sarebbero mostrate sulla shell dal quale l'eseguibile è stato
lanciato.
La funzione maggiormente utilizzata da Salto durante l'esecuzione è
fb_write(), la quale è implementata usando put_pixel() e non fa altro che
visualizzare un array di 16 pixel contigui sulla nestra. Il fatto che molte
funzioni siano state scritte usandone altre più semplici rende il tutto molto
più essibile.
main.c
Questo è il le principale del kernel, al suo interno sono presenti 4 funzioni:
print_info(), MMU_init(), test(), kmain(). Prima di lanciare Salto deve
avvenire l'ultima parte di inizializzazione del Raspberry Pi. Questa è
implementata da kmain() che si occupa di inizializzare interrupt e
framebuer tramite le funzioni descritte precedentemente. Viene inoltre
inizializzato il driver USB, abilitata e congurata la MMU (usando la
funzione MMU_init()) e vengono stampate informazioni utili all'avvio. La
routine kmain() utilizza i registri r0, r1 e r2 ; questi sono stati preservati
26
2.3 Newlib
dalle funzioni eseguite in precedenza (initsys() in particolare) e contengono
alcune informazioni utili come l'indirizzo della struttura ATAGS e il tipo di
macchina ospite (deve essere Raspberry Pi). La funzione test() visualizza
nella nestra un gradiente colorato ed è servita per vericare il
funzionamento delle principali routine grache.
Una volta completata l'inizializzazione il controllo è trasferito a Salto.
2.3
Newlib
Salto, essendo concepito per essere eseguito su PC con sistema operativo
residente, utilizza fortemente le librerie standard del C. Come già
accennato precedentemente sono state impiegate le librerie Newlib per
sopperire alla mancanza di queste in ambiente bare metal.
Lo script di costruzione della toolchain scarica sia GCC che Newlib (oltre
ad altri componenti necessari) e congura tutto in modo che il
cross-compilatore conosca l'ubicazione delle librerie C. In questo modo
quando viene compilato un le sorgente e si utilizza la direttiva #include
includendo un header della libreria standard, non vengono restituiti errori
in quanto questo è individuato dal compilatore.
La fase di assemblaggio però presenta un problema: il le oggetto
contenente eettivamente il codice compilato di Newlib, chiamato libc.a,
non è individuato automaticamente. Questo signica che l'assemblaggio
dell'eseguibile presenta degli errori riguardo riferimenti non soddisfatti.
Non è stata trovata una soluzione vera e propria per cui si è aggirato il
problema; il le corretto è stato infatti individuato all'interno della cartella
di installazione di Newlib e copiato nella directory radice contente piAlto.
Successivamente il makele è stato modicato inserendo libc.a tra gli
argomenti del linker, in questo modo il le è assemblato manualmente e la
costruzione dell'eseguibile nale va a buon ne.
All'interno della cartella di installazione di Newlib sono presenti due
versioni del le, la prima non utilizza la FPU hardware mentre la seconda
si. É stato scelto di compilare i sorgenti di piAlto usando la FPU hardware
usando le opportune opzioni di compilazione indicate nel makele per
rendere il tutto più veloce e ottimizzato.
Proprio a causa di questo durante le prime prove con Newlib il linker
restituiva errori dovuti all'incompatibilità tra i le oggetto: veniva infatti
usato il le libc.a presente alla radice nella cartella di installazione (senza
supporto FPU hardware). Una volta capito quale fosse il problema questo è
stato sostituito con la versione corretta e gli errori sono scomparsi.
Il percorso del le corretto è:
cartellaDiInstallazione/arm-none-eabi/lib/fpu/libc.a.
27
2.3 Newlib
Per fare in modo che tutto funzioni correttamente non basta assemblare il
le oggetto delle librerie con quello di piAlto in quanto Newlib ha
comunque bisogno di alcuni servizi che devono essere oerti dal kernel. Per
esempio la scrittura su di un le o a video deve essere implementata da un
driver del sistema operativo così come l'incremento della zona dedicata allo
heap.
Siccome Newlib è pensata per ambiente embedded dove il sistema
operativo è spesso molto piccolo e ore un insieme di servizi limitato, la
libreria richiede solamente la denizione di 17 syscall che devono fungere da
interfaccia con il SO. Non è comunque necessario che siano tutte
funzionanti in quanto alcuni servizi potrebbero non essere necessari, per
esempio potrebbe essere inutile implementare la scrittura su le se non
occorre usare funzioni come fprintf() e simili. Per questo motivo è stato
creato il le syscalls.c contenente le 17 denizioni necessarie.
La maggior parte di queste semplicemente ritorna un codice di errore
mentre sono state eettivamente implementate 2 syscall: sbrk() e write().
La prima serve al gestore della memoria dinamica per incrementare o
diminuire la zona riservata allo heap, senza di questa funzioni come
malloc(), free() e molte altre non funzionerebbero. La seconda invece serve
per scrivere su le, stderr o stdout. Dato che non era necessaria la scrittura
su le tutte le stringhe passate a questa syscall vengono visualizzate sulla
console di sistema usando la funzione console_write().
Il fatto che tutte le dipendenze siano connate in queste poche syscall è
molto utile in quanto è abbastanza semplice riuscire ad integrare Newlib in
un ambiente in cui si ha un supporto quasi inesistente da parte del sistema
operativo.
Un problema serio riscontrato riguarda la funzione printf(). Come già detto
inizialmente l'utilizzo di questa routine causava un blocco del programma
irreversibile. Per risolvere il problema sono state provate diverse versioni
della funzione di stampa presenti in Newlib e si è notato come le uniche
funzionanti fossero quelle con supporto limitato alla stampa di interi. In
particolare la funzione iprintf() funziona perfettamente anche se non è in
grado di stampare numeri oating point.
Si è quindi deciso di utilizzare sempre quest'ultima funzione per le stampe
in quanto i numeri a virgola mobile e ssa non sono necessari per il
funzionamento di piAlto. É stato scoperto dopo che la FPU per funzionare
necessita di una inizializzazione non presente nel progetto; una volta
inserita questa piccola porzione di codice assembly nel le startup.s è stato
provato il funzionamento di printf(). Questa stampa correttamente ed il
Raspberry Pi non si blocca più in modo anomalo per cui tutte le chiamate
presenti sono state sostituite.
La funzione iprintf() è sicuramente più semplice ed eciente dell'originale
ma si è scelto di sostituirla comunque perché non è standard. É infatti
presente in Newlib perché utile in ambiente embedded dove spesso i numeri
28
2.4 Salto
a virgola mobile o ssa non sono utilizzati o addirittura implementati in
hardware. Sostituendola con printf() si sacrica l'ecienza ottenendo però
un codice sorgente più portabile nel caso in futuro si decida di cambiare
librerie standard C.
2.4
Salto
Una volta ottenuto un kernel funzionante in grado almeno di supportare
l'output video e delle librerie standard C ben integrate nel progetto si è
passati all'adattamento vero e proprio dell'emulatore.
La prima attività svolta è stata quella di studiare la struttura del codice, in
particolare quali fossero le opzioni di avvio disponibili. Saranno di seguito
riportate per completezza:
ˆ
-dr: eettua il dump dell'immagine del drive 0 sul le dump.raw
all'uscita
ˆ
-ee: abilita il task Ethernet
ˆ
-eh=n: imposta l'ID di rete per la macchina (da 1 a 254)
ˆ
-db[=n]: broadcast duckbreath (ogni n secondi; default 5)
ˆ
-dc: scrive un dump dello stato di Salto nel le alto.dump
ˆ
-kr: stampa su
ˆ
-b key: imposta un tasto per il boot
ˆ
-d: l'avvio è in modalità pausa e la nestra di debug è attiva
ˆ
-h: visualizza l'help
premuti
stderr i tasti sconosciuti/non gestiti che vengono
Come si può notare molte opzioni sono relative a funzionalità di Salto
non necessarie per la realizzazione di piAlto, per esempio la scrittura di
dump su le non è possibile in ambiente bare metal ntanto che non si ha a
disposizione un driver per gestire la scheda SD ed il lesystem. Anche la
modalità debug, la quale risulta interessante perché permette di controllare
lo stato della macchina completamente, è stata ritenuta superua.
L'obiettivo infatti è ottenere un comportamento dell'emulatore il più vicino
possibile a quello della macchina originale.
Dato che Salto viene eseguito immediatamente dopo l'inizializzazione del
Raspberry Pi non è possibile specicare le opzioni di avvio come su Linux
ma occorre che queste vengano impostate direttamente all'interno del
codice sorgente o al massimo possano essere trasferite al kernel in fase di
boot del dispositivo scrivendo negli opportuni le di congurazione.
29
2.4 Salto
L'analisi delle opzioni è stata utile per avere una prima idea sulle capacità
dell'emulatore, si è poi passati a studiare il le contenente la funzione
main() di Salto ovvero salto.c. L'emulatore è organizzato in diverse cartelle,
in particolare src contiene i le sorgenti principali mentre include contiene i
le header di riferimento.
2.4.1 Il le salto.c
Nel le salto.c, oltre alla funzione main(), viene implementato tutto il
software che si interfaccia con le librerie SDL. Viene quindi gestito l'output
video e vengono registrati gli eventi come l'input da tastiera o mouse e la
chiusura della nestra.
Sempre in questo le sono implementate lo scatto di screenshot oppure la
registrazione della nestra allo scopo di produrre video dimostrativi. Prima
di descrivere come eettivamente le funzioni presenti nel le interagiscano è
opportuno spiegare a cosa servono e qual'è il loro comportamento. Queste
sono state suddivise in base al tipo di funzionalità oerte e sono descritte
di seguito.
Registrazione della nestra e screenshot
Nella cartella src di Salto sono presenti altri due le chiamati mng.c e
png.c che orono il supporto necessario per lo svolgimento di questo
compito. Il codice descritto funge da interfaccia tra questi due le e l'utente
che può, premendo i pulsanti appositi, comandare la registrazione.
void screenshot ( void )
Comanda la creazione di uno screenshot della nestra.
void screenmng_frame ( void )
Scrive un frame nel usso di registrazione MNG.
void screenmng_start ( void )
Inizia la registrazione del video.
void screenmng_stop ( void )
Ferma la registrazione del video.
int fp_write_bytes ( void * cookie , uint8_t * bytes , int size )
30
2.4 Salto
É utilizzata per scrivere una serie di byte durante la memorizzazione di un
le PNG o MNG.
Supporto alla modalità debug
La maggior parte del codice relativo alla visualizzazione dello stato della
macchina nel dettaglio è contenuto nel le debug.c. Le funzioni seguenti
servono per realizzare l'output video durante la debug mode e
rappresentano il supporto essenziale perché questa funzioni correttamente.
int bitcount ( const chargen_t * cg , int i , int x , int y )
Conta i bit presenti in un punto (e in una zona circoscritta) all'interno di
una mappa di caratteri .
int sdl_chargen_alpha ( const chargen_t * cg , int col , uint32_t
rgb )
Permette il rendering di testo all'interno della schermata di debug.
int sdl_debug ( int x , int y , int ch , int color )
Scrive un carattere nella nestra di debug.
void debug_view ( int which )
Imposta la visualizzazione della nestra di debug oppure quella relativa alla
normale esecuzione di Salto.
Interazione con l'utente
Di seguito sono approfondite le funzioni che si occupano dell'inizializzazione
della nestra, dell'output video e della gestione degli eventi.
int sdl_make_cursor ( uint8_t * pix , uint8_t * msk , int w , int h ,
const char * src )
Costruisce un cursore per SDL partendo da una stringa.
void sdl_blit ( SDL_Surface * ds , SDL_Surface * ss , int dx , int dy ,
int sx , int sy , int w , int h )
31
2.4 Salto
Utilizza SDL_BlitSurface per trasferire una porzione di una supercie su di
un'altra.
int border_putch ( int x , int y , uint8_t ch )
Scrive un carattere sul bordo della supercie.
int border_printf ( int x , int y , const char * fmt , ...)
Scrive una stringa sul bordo della supercie.
int sdl_close_display ( void )
Dealloca tutte le risorse (cursore e superci) precedentemente allocate per
SDL.
int sdl_update ( int full )
Registra gli eventi dall'esterno come i click del mouse, la pressione di tasti
o la chiusura della nestra. Si occupa inoltre di eettuare un refresh
parziale o totale della nestra stessa.
int sdl_draw_icon ( int x , int y , int type )
Disegna una icona rappresentante un LED nella parte inferiore della
supercie.
int sdl_write ( int x , int y , uint32_t pixel )
Scrive una sequenza di 16 pixel in modo contiguo sulla nestra.
int sdl_open_display ( int width , int height , int depth , const
char * title )
Apre una nestra con SDL impostando dimensioni e titolo.
int sdl_init ( int width , int height , int depth , const char *
title )
Inizializza SDL abilitando gli eventi desiderati e crea le superci utili al
rendering.
void sdl_exit ( void )
Ferma il registratore della nestra e termina SDL ed i relativi sottosistemi.
32
2.4 Salto
Funzioni di utilità generale
void unknown_key ( int sym )
Stampa un avvertimento nel caso un tasto non gestito/sconosciuto venga
premuto.
void bootimg ( void )
Carica l'immagine di boot se specicata.
void fatal ( int exitcode , const char * fmt , ...)
Stampa un errore grave ed esce.
int main ( int argc , char ** argv )
Rappresenta il punto di controllo di Salto, qui vengono comandate le
inizializzazioni ed è presente il ciclo macchina principale.
void usage ( int argc , char ** argv )
Stampa l'help su shell.
2.4.2 Analisi e modica di Salto
La fase di adattamento dell'emulatore è iniziata con l'analisi della funzione
main() che è strutturata nel seguente modo:
int main ( int argc , char ** argv )
{
int i , drive ;
/* initialize SDL to 606 x808x [ default ] screen */
sdl_init ( DISPLAY_WIDTH , DISPLAY_HEIGHT , -1 , title ) ;
alto_init ( " roms " ) ;
timer_init () ;
init_memory () ;
init_hardware () ;
init_kbd () ;
init_printer () ;
init_eia () ;
drive_init () ;
disk_init () ;
display_init () ;
33
2.4 Salto
mouse_init () ;
debug_init () ;
for ( i = 1 , drive = 0; i < argc ; i ++) {
if ( argv [ i ][0] == ' - ' || argv [ i ][0] == '+ ') {
// ...
// Analisi delle opzioni
// ...
} else if (0 == drive_args ( argv [ i ]) ) {
disk_show_image_name ( drive ++) ;
} else if (0 == ether_args ( argv [ i ]) ) {
/* ether accepted a name */
} else {
bootimg_name = argv [i ];
}
}
drive_select (0 , 0) ;
alto_reset () ;
# if DEBUG
// ...
// ciclo macchina attivando DEBUG
// ...
# else
while (! halted ) {
ntime_t run , ran ;
while (( run = timer_next_time () ) < CPU_MICROCYCLE_TIME )
timer_fire () ;
if ( run <= 0)
run = 5000 * CPU_MICROCYCLE_TIME ;
global_ntime += run ;
ran = alto_execute ( run ) ;
global_ntime += ran - run ;
while ( paused && ! halted ) {
dbg_dump_regs () ;
sdl_update (1) ;
}
}
# endif
if ( dump ) {
// ...
// Scrittura del dump su file
// ...
}
return 0;
}
É presente una prima parte di inizializzazione dei componenti
dell'emulatore, le funzioni che includono la parola init nel nome servono
proprio a questo e si trovano in altri le C presenti nella cartella src. Il ciclo
for presente subito dopo serve ad analizzare le opzioni specicate
dall'utente all'avvio e a svolgere le azioni associate ad ognuna di queste. Se
un argomento inizia con `-' allora viene trattato come opzione e si controlla
34
2.4 Salto
se è riferito al disco, alla rete, al debug oppure alle altre opzioni
riconosciute. Viene perciò impostata la modalità pause o debug se
necessario, è abilitato il task di Ethernet se richiesto e molto altro. Se non
si tratta di un opzione l'argomento viene trattato come il path
dell'immagine del disco oppure del percorso di quella di avvio. In questa
fase viene anche caricata da le l'immagine che l'utente è tenuto a
specicare se questa esiste ed è valida (l'estensione del le è riconosciuta).
Una volta eettuata tutta l'inizializzazione si procede a selezionare la
prima unità del disco che conterrà i programmi e i dati appena caricati e si
eettua un reset di Salto. In questa fase vengono registrati i gestori per i
task di sistema e l'emulazione può iniziare.
Il vero cuore della funzione è il ciclo macchina presente subito dopo, ne
esistono due versioni: la prima è pensata in caso venga abilitato il debug di
Salto in fase di compilazione mentre l'altra viene usata in caso questa non
sia stata abilitata. La modalità debug appena citata non è da confondersi
con quella che visualizza lo stato della macchina sulla nestra. Questa
invece mostra molte informazioni sulle operazioni eseguite da Salto sulla
shell nel caso si vogliano trovare le cause di eventuali bug dell'emulatore.
Prima di spiegare come il ciclo è organizzato occorre aprire una piccola
parentesi: il tempo in Salto non è legato a quello reale ma è piuttosto
simulato tramite dei numeri interi. Questo implica che l'esecuzione su di
una macchina più performante comporta tempi di risposta ridotti e
viceversa. L'unità di misura del tempo sono i nanosecondi che sono
implementati usando interi con segno a 64 bit. Si è scelta questa
implementazione perché nel caso sia necessaria una conversione tra secondi
e nanosecondi il risultato non sarebbe memorizzabile in interi più piccoli.
La durata di un ciclo macchina dello Xerox Alto è rappresentato dall'intero
CPU_MICROCYCLE_TIME e ogni iterazione non può eseguire istruzioni
per un tempo virtuale maggiore di questo.
Per una corretta emulazione sono stati implementati i timer di sistema, ad
ognuno di questi è associata una funzione che deve essere richiamata allo
scadere del tempo associato. Tutte le funzioni per la gestione dei timer sono
appunto presenti in un le apposito chiamato timer.c.
Il ciclo principale non fa altro che vericare se la macchina è bloccata, in
tal caso halted vale 1 e Salto deve terminare, in caso contrario tramite il
ciclo più interno vengono eseguite le callback relative a tutti i timer che
terminano nel micro ciclo corrente. Viene infatti continuamente eseguita
timer_next_time() che preleva da una coda di timer il prossimo da
eseguire in ordine temporale. Se il tempo associato è minore di quello di un
micro cycle vuol dire che questo ha terminato e viene chiamata
timer_re() che, come suggerisce il nome, esegue la callback associata.
Allo scopo di tenere il tempo di sistema è presente l'intero global_ntime
che è utilizzata da time_next_time() allo scopo di sapere se il tempo
trascorso è suciente per l'esecuzione della callback. Una volta che tutti i
35
2.4 Salto
timer nel micro cycle corrente sono stati eseguiti il tempo di sistema viene
incrementato per portarlo in corrispondenza del primo timer nel prossimo
ciclo di esecuzione.
A questo punto viene eseguita alto_execute() che simula eettivamente la
CPU dell'Alto andando a modicare i registri ed a comunicare con le
periferiche per una durata che è quella di run ; alto_execute() ritorna il
numero di nanosecondi di durata dell'esecuzione nell'iterazione corrente. A
questo punto global_ntime è incrementato della dierenza tra la durata
dell'esecuzione eettivamente realizzata e quella prevista. In questo modo
se l'emulazione è durata meno del previsto il valore sommato è negativo e il
tempo di sistema viene allineato con la durata eettiva dell'emulazione.
Tutto questo meccanismo permette la sincronizzazione tra i timer e la
CPU; il comportamento complessivo sarà in questo modo uguale a quello
dello Xerox Alto perché tutto è simulato al livello più basso possibile.
Come già detto, questo però comporta un degrado delle prestazioni visibile
in modo più marcato sulle macchine più limitate come può essere il
Raspberry Pi utilizzato.
L'ultima parte del main() si occupa semplicemente di scrivere il dump della
memoria nel caso questo venga richiesto dall'utente ed è stata rimossa
quasi subito dopo lo studio del codice.
Il primo obbiettivo che ci si è posti è quello di avviare correttamente Salto
su Raspberry Pi senza input da tastiera e mouse. Per fare questo sono state
studiate le chiamate alle funzioni video presenti in salto.c descritte
precedentemente, in particolare la fase di inizializzazione presente nella
parte iniziale della funzione main(). La prima routine di inizializzazione è
sdl_init()
la quale, con l'ausilio di sdl_open_display() e
sdl_make_cursor(), si occupa di creare la nestra, disegnare le icone nel
pannello in alto ed inizializzare il puntatore del mouse da visualizzare sul
pannello stesso. Sono infatti presenti due puntatori dierenti per il mouse,
uno viene utilizzato per selezionare le icone sul pannello ed è gestito
tramite SDL mentre il secondo è visualizzato all'interno dell'area
predisposta per l'emulazione ed è gestito dal codice dello Xerox Alto
emulato: non è possibile quindi avere un controllo diretto sul disegno del
secondo puntatore.
Durante l'esecuzione del ciclo macchina principale vengono utilizzate le
funzioni sdl_write() e sdl_blit() per la scrittura dei pixel mentre
sdl_update() è chiamata regolarmente per eettuare la registrazioni degli
eventi e il refresh della nestra. Quest'ultima funzione è chiamata
alternando un refresh parziale ad uno completo.
Per aggiornare le informazioni presenti sul pannello in alto vengono anche
usate border_putch(), border_putstring(), sdl_draw_icon() mentre
sdl_debug(), bitcount(), sdl_chargen_alpha() e debug_view() sono utili
solamente in caso venga abilitata la debug mode. Le parti relative al debug
e alla registrazione della nestra sono poco integrate con le rimanenti e
36
2.4 Salto
vengono utilizzate in pochi punti del programma perciò è relativamente
facile capire dove intervenire per rimuoverle.
Come prima attività dopo l'analisi del codice sorgente sono state appunto
rimosse le funzionalità ritenute non necessarie eliminando i le debug.c,
mng.c, png.c ed i relativi header le. Si sono quindi commentate tutte le
funzioni non più necessarie e sono state individuate e commentate
all'interno di salto.c tutte le chiamate non più eettuabili. In questo modo
si è ottenuto un le più piccolo e più semplice da studiare in seguito.
A questo punto è stato fatto un primo tentativo di integrazione con il
kernel, modicando il makele ed inserendo i le da compilare ed
assemblare uno ad uno. Per quasi tutto lo sviluppo di piAlto il makele
consisteva di poche righe ed era pensato per un progetto di pochi le, a
causa di questo e della poca esperienza con make i le erano specicati in
una lista nome per nome ed ogni volta che veniva modicato qualcosa tutti
i le oggetto venivano cancellati ed il progetto doveva essere ricompilato e
assemblato completamente.
Inserendo i le di Salto nell'elenco uno ad uno e lanciando la compilazione
si sono osservati gli errori restituiti, questo è stato utile per capire le
dipendenze di ogni le dai rimanenti e le parti mancanti da implementare.
Avendo cancellato mng.c, png.c e debug.c in questa fase, sfruttando gli
errori che GCC forniva, si è provveduto a cancellare anche nei rimanenti
le di progetto tutte le chiamate a funzioni non più disponibili. Nel cercare
di togliere tutto il codice considerato inutile è stata comunque fatta molta
attenzione al ruolo che le chiamate in questione svolgevano nei rispettivi
le evitando di eliminare testo che potesse essere vitale per il corretto
funzionamento di Salto.
É stato fatto quindi un altro tentativo di integrazione con il kernel, in
questo caso però si voleva arrivare ad ottenere un eseguibile portando a
termine senza errori la compilazione. Per fare questo sono state
commentate tutte le parti di codice che portavano ad errori, il le salto.c è
stato ridotto alla sola funzione main() contente solamente la dichiarazione
delle variabili locali all'inizio. É stata mantenuta inoltre fatal() in quanto
fortemente usata negli altri le del progetto; sono state commentate anche
le chiamate a sdl_write() e sdl_update() presenti nel le display.c, queste
vengono usate come unico mezzo per la gestione dell'output video e degli
eventi. Il tutto è gestito da solamente 4 chiamate (3 di sdl_write() e 1 di
sdl_update()) mentre le altre funzioni presenti all'interno di salto.c
vengono quasi sempre usate solo internamente al le per cui non è stato
necessario molto altro lavoro per rimuovere i riferimenti rimanenti a queste.
Anche se non funzionante si è a questo punto ottenuto l'eseguibile e sono
stati inseriti nel progetto tutti i le sorgenti di Salto. Il passo successivo è
stato quello di rimuovere i commenti un pezzo alla volta apportando le
opportune modiche in modo da ottenere pian piano il risultato sperato.
Un primo problema si è vericato con l'inizializzazione della tastiera ed il
37
2.4 Salto
le associato keyboard.c il quale utilizzava fortemente SDL ed è quindi
stato temporaneamente rimosso dal progetto. Ovviamente le librerie SDL
non sono disponibili in ambiente bare metal per cui occorreva capire dove
intervenire per rendere trasparente a Salto la loro sostituzione con il piccolo
sistema graco oerto dal kernel. Si è osservato come gran parte del codice
di inizializzazione di SDL non fosse necessario dato che il sistema che si è
costruito prepara in automatico la nestra con le dimensioni più opportune
e la riempie con il colore bianco di sfondo.
Chiaramente la parte relativa alla gestione del pannello, la quale non è
gestita direttamente dal sistema, è stata completamente rimossa in quanto
quasi tutti gli indicatori presenti sarebbero stati svuotati del loro
signicato dato che le funzionalità associate non sono più presenti. Le
uniche parti mantenute sono la creazione del cursore SDL e poche righe per
l'inizializzazione di un array usato da sdl_write().
La nuova sdl_init() è stata quindi compilata senza problemi particolari ed
utilizzando QEMU si è vericato che l'immagine prodotta caricasse
correttamente no alla ne. Prima di eettuare l'integrazione degli
inizializzatori questi sono stati controllati per vericare che non avessero
dipendenze non soddisfatte, le uniche due funzioni che risultavano
problematiche erano init_kbd() e alto_init(). La prima è implementata in
keyboard.c il quale è stato rimosso temporaneamente mentre la seconda
eettua il caricamento da le delle ROM e, dato che non era presente un
driver per la scheda SD, si è scelto rimandare la modica in seguito. A
questo punto sono state inserite delle stampe di prova dopo ogni
inizializzazione fatta nel main() e queste sono state decommentate una ad
una vericandone man mano il funzionamento; durante la compilazione e
l'emulazione non sono stati riscontrati particolari problemi.
Perché Salto funzioni correttamente occorre che le ROM vengano caricate
in RAM per cui è stato necessario analizzare e modicare la funzione
alto_init() e le altre parti correlate. Il codice di caricamento è presente in
alto.c: in questo le sono implementate alto_init() e tutte le altre funzioni
utili. Le ROM sono suddivise in tre categorie: le PROM (Programmable
ROM) contenenti il microcodice della CPU dette ucode PROM, le PROM
costanti e quelle non appartenenti alle prime due categorie. Ogni ROM è
implementata usando un'opportuna struttura detta rom_load_t e sono
deniti tre array di ROM, uno per ogni categoria presente.
Proprio per le dierenze presenti tra i vari tipi di PROM sono presenti tre
funzioni dierenti per il caricamento e sono chiamate load_ucode(),
load_const() e load_proms(). Ognuna di queste eettua un controllo di
integrità sulle ROM caricate, ne viene infatti calcolato l'MD5 ed è poi
confrontato con quello ritenuto adabile denito nel sorgente. Per
eettuare questo controllo si utilizzano routine presenti nel le md5.c
contenuto nella cartella src di Salto. Il sottoprogramma alto_init() non fa
altro che eettuare un sommario test di funzionamento delle routine per il
38
2.4 Salto
calcolo dell'MD5 usando la funzione apposita md5_test(), vericare che la
cartella contenente le ROM sia presente e richiamare i 3 metodi per il
caricamento dei dati. Le modiche apportate a questi sono state minime in
quanto per l'eettiva lettura dei le utilizzano tutti la stessa funzione
chiamata load_le_md5(). Questa è denita come segue:
static uint8_t * load_file_md5 ( const char * path , const char *
name , size_t size , const char * md5 )
{
char filename [ FILENAME_MAX ];
const char * md5_check ;
FILE * fp ;
uint8_t * buf ;
buf = malloc ( size ) ;
if (! buf )
return NULL ;
snprintf ( filename , sizeof ( filename ) , " % s /% s " , path , name ) ;
fp = fopen ( filename , " rb " ) ;
if (! fp )
fatal (3 , " fopen (% s ,\" rb \") failed (% s ) \ n " ,
filename , strerror ( errno ) ) ;
if ( size != fread ( buf , 1 , size , fp ) )
fatal (3 , " fread (% d ) for file % s failed (% s ) \ n " ,
size , filename , strerror ( errno ) ) ;
if ( fclose ( fp ) )
fatal (3 , " fclose (% s ) failed (% s ) \ n " ,
filename , strerror ( errno ) ) ;
md5_check = md5_digest ( buf , size ) ;
}
if ( md5 && strcmp ( md5 , md5_check ) ) {
LOG ((0 ,0 , " md5 check failed ... " , filename , md5_check ) ) ;
}
return buf ;
Questa routine utilizza le stringe path e name per individuare il le
corretto ed utilizzando poi size e le chiamate di sistema opportune ne
scrive il contenuto in un buer (buf ). A questo punto utilizza le funzioni
citate precedentemente per calcolare l'MD5 e lo confronta con quello
presumibilmente corretto fornito dall'esterno. Se il confronto va a buon ne
l'indirizzo del buer è restituito al chiamante che può quindi utilizzarlo.
Prima di partire con le modiche si è studiato come aver accesso al
contenuto di un le senza accedere direttamente al lesystem. La soluzione
più semplice è quella di trasformare i le necessari in sorgenti C che
possono essere compilati ed assemblati utilizzando il linker. Per fare questo
si è utilizzato il programma di utilità xxd, tipicamente presente sui sistemi
Linux il quale è concepito per eettuare la conversione tra binario ed
esadecimale e viceversa.
39
2.4 Salto
Usando l'opzione `-i' è però possibile ottenere come output un le C: il le
binario è convertito in una array C con lo stesso nome del le originario,
nel sorgente nale è denito anche un numero intero contenente la
lunghezza dell'array stesso. Per default xxd scrive l'output su shell ma è
possibile facilmente redirezionarlo su le usando la sintassi xxd -i
le_binario le_C.
Usando il comando appena visto è stato quindi possibile ottenere sorgenti
C che sostituissero le ROM binarie, tuttavia è sorto il problema di come
reperire l'array giusto dato il nome del le. Sono possibili molte soluzioni
diverse al problema; si è scelto di creare una struttura che rappresenti il le
non più caricato da disco chiamata rom_info e denita come segue:
typedef struct {
unsigned char * content ;
unsigned int len ;
} rom_info ;
Il primo campo contiene l'indirizzo dell'array contenente la ROM ed il
secondo la sua lunghezza. A questo punto è stato denito un array di
rom_info chiamato rom_list avente lunghezza uguale al numero delle
ROM necessarie ed una funzione di inizializzazione che riempie il campi con
i dati corretti.
Si è fatto in modo che ogni le sia identicato da un valore numerico, per
questo è stata quindi denita una enumerazione. É stato poi aggiunto un
campo numerico alla struttura contenente le informazioni delle ROM
(rom_load_t) e nell'inizializzazione di queste strutture sono stati poi
scritti i valori opportuni.
La funzione che inizializza l'array di rom_info è rom_list_init() e una
volta chiamata load_le_md5() può accedere alle informazioni associate
(indirizzo e dimensione dell'array) semplicemente sfruttando rom_list ed
utilizzando come indice il valore numerico rappresentante la ROM.
Una volta implementato questo meccanismo è stato possibile eettuare le
modiche alla funzione load_le_md5() in modo che il caricamento
avvenga in modo trasparente. La nuova funzione è denita in questo modo:
static uint8_t * load_file_md5 ( unsigned int number , const char *
name , size_t size , const char * md5 ) {
const char * md5_check ;
unsigned int s = ( unsigned int ) size ;
unsigned char * buf = rom_list [ number ]. content ;
if ( s != rom_list [ number ]. len )
printf ( " Size of rom % s is wrong \ n " , name ) ;
md5_check = md5_digest ( buf , size ) ;
if ( md5 && strcmp ( md5 , md5_check ) )
printf ( " md5 check failed ...\ n " , name , md5_check ) ;
40
2.4 Salto
}
return buf ;
Anche se è stata utilizzata una soluzione laboriosa al problema precedente
come si può notare la funzione risulta molto semplicata. Invece di
richiedere il percorso del le è richiesto il valore numerico il quale identica
la ROM che viene usato per ottenere l'indirizzo dell'array contente i dati e
la loro dimensione. In modo analogo alla funzione originale viene poi fatto
il controllo MD5 e l'indirizzo del buer, il quale non è più allocato
dinamicamente, viene restituito al chiamante. Le modiche apportate alle
tre funzioni che comandano il caricamento dei dati sono minime, è bastato
sostituire il percorso con il numero associato alla ROM.
Una volta vericato il funzionamento di alto_init() è stato modicata la
porzione di main() che elabora le opzioni specicate. Dopo un'attenta
analisi si è scelto di rimuovere tutto il codice e le relative variabili globali in
quanto non utili. Le uniche due parti mantenute sono le chiamate a
drive_args() e ether_args() che impostano le opzioni riguardanti il disco e
la rete. Si è scelto di modicare ether_args() in modo che prenda le opzioni
come argomento invece di leggerle da una stringa e sono stati specicati dei
valori di default (rete disabilitata, duckbreath = 5 e id = 1).
L'altra funzione, ovvero drive_args(), è invece quella che si occupa di
caricare l'immagine del disco specicata dall'utente. Presentandosi lo stesso
problema visto per le ROM la soluzione usata è la stessa. É stata creata
anche in questo caso una struttura apposita (image_info ) e un array di
dimensioni unitarie (images ) in quanto si è scelto di supportare una sola
unità disco anche se Salto ne supporta al massimo due. Dato che
l'immagine può anche essere compressa è stato introdotto un campo
apposito nella struttura. Come per le ROM è stata denita una funzione di
inizializzazione dell'array images che deve essere chiamata prima di
utilizzare drive_args(). É stata quindi modicata la funzione di
caricamento in modo analogo a quanto visto in precedenza e ne è stato
vericato il funzionamento usando QEMU.
É stato a questo punto rimosso anche il ciclo macchina in versione debug e
si è passati ad adattare sdl_write() in modo da poter vedere Salto avviarsi
e vericarne il funzionamento.
La funzione sdl_write() colora 16 pixel di nero e necessita di tre argomenti,
ascissa e ordinata del primo pixel da colorare ed un intero che codica
come i punti devono essere anneriti. Viene utilizzato un array globale che
contiene tutte le possibili combinazioni di 16 pixel possibili. Utilizzando
l'intero passato come parametro si ricava la posizione della sequenza
desiderata presente nell'array. Originariamente questo era inizializzato in
sdl_init() usando le librerie SDL e quindi si è reso necessario modicare
questa porzione di codice in modo da garantirne comunque il
41
2.4 Salto
funzionamento. Avendo la possibilità di sapere quale sia la combinazione di
pixel da colorare è stata usata la funzione fb_write() messa a disposizione
dal kernel per la scrittura eettiva a video dei dati. Le chiamate a
sdl_write() sono state riabilitate nel le display.c ed è stato riattivato il
ciclo macchina principale nel main() di Salto. A questo punto è stato
eettuato il test di funzionamento usando l'emulatore e sorprendentemente
il boot avveniva regolarmente anche se non c'era la possibilità di interagire
tramite mouse o tastiera.
Subito dopo aver raggiunto l'obbiettivo iniziale è sorto un grosso problema:
la velocità. Osservando il boot infatti è emerso che questo era
tremendamente lento sia tramite QEMU sia usando il dispositivo sico. Per
capire se si trattasse di una limitazione hardware del Raspberry Pi, Salto è
stato compilato ed eseguito sul dispositivo usando Raspbian come sistema
residente riscontrando una velocità molto maggiore rispetto a quella dello
stesso programma in ambiente bare metal.
Allo scopo di comprendere da cosa derivasse questa enorme dierenza di
prestazioni sono state fatte numerose prove: le ottimizzazioni sono state
impostate al livello massimo durante la compilazione, è stata utilizzata una
diversa versione di Newlib (2.0.0) e si è disabilitata l'emulazione completa
della ALU nella funzione alto_execute(). Nessuna di queste modiche ha
portato miglioramenti signicativi; si è pensato quindi che il problema fosse
causato dal largo utilizzo di interi a 64 bit non direttamente supportati dal
Raspberry. Sono state fatte numerose prove per vericare la fondatezza di
questo dubbio, in particolare si è cercato di sostituire tutte le variabili a 64
bit con interi senza segno a 32 bit. Chiaramente l'emulatore non funzionava
correttamente a causa dell'overow che si vericava nelle conversioni tra
secondi e nanosecondi tuttavia mediante stampe nei punti opportuni è
stato possibile vericare che il guadagno eettivo nella prima parte del
boot era esiguo.
Sul web in alcune guide per la compilazione di GCC viene usata l'opzione
enable-long-long ; questo ha suggerito che il problema potesse dipendere da
un'errata congurazione della toolchain che non ottimizzasse i long long
(interi a 64 bit usati in Salto). É stato quindi riassemblato GCC con
l'opzione citata e ne è stato vericato il funzionamento anche se purtroppo
non sono state trovate dierenze con la toolchain usata in precedenza.
L'attenzione si è a questo punto spostata sulle operazioni non direttamente
supportate in hardware come la divisione ed il modulo tra interi. Sono state
rimosse tutte le porzioni di codice sensibile contenente queste operazioni
vericando se la velocità di boot fosse maggiore (anche se la funzionalità
dell'emulatore era compromessa) e anche in questo caso l'esito è stato
negativo.
Come prova denitiva si è deciso di misurare la durata dell'esecuzione di un
numero signicativo di volte del ciclo macchina. Si è usata la variabile
halted per controllare l'uscita dal ciclo la quale è denita globale. Si è
42
2.5 CSUD
vericato il tempo impiegato per l'esecuzione eliminando pezzi diversi del
ciclo arrivando a tenere il solo incremento di un contatore ed un controllo
sul valore di questo. Una volta appuntato il tempo impiegato, la variabile
halted è stata denita locale al main() e si è constatato un netto
miglioramento di prestazioni. Facendo ricerche in questo senso è emerso che
il problema dipendeva dal fatto che la cache della CPU non era abilitata.
Per poter sfruttare la cache è necessario infatti abilitare l'MMU che, come
già detto, non era stata abilitata in quanto si pensava che la paginazione
fosse un overhead non necessario. Una volta capito il problema sono stati
consultati i manuali dell'ARM e si è eettuato identity paging per tutto lo
spazio di indirizzamento usando pagine da 1MB. La funzione main() è stata
ripristinata ed il progetto è stato compilato e provato vericando che il
tempo di avvio fosse ragionevolmente simile a quello di Salto compilato su
Raspbian.
2.5
CSUD
L'ultima fase dello sviluppo è stata quella necessaria per implementare il
dialogo con tastiera e mouse. Dato che il Raspberry Pi dispone solamente
delle porte USB per interfacciarsi con questi dispositivi è necessario avere a
disposizione un driver per il controller. Lo standard USB è estremamente
lungo e complesso per cui implementare da zero un driver era impensabile,
è stata quindi eettuata una ricerca di un prodotto funzionante.
Sul Web si è individuato CSUD, sviluppato da Alex Chadwick e scaricabile
tramite:
git clone https://github.com/Chadderz121/csud.git.
Questo progetto implementa solo una parte dello standard, in particolare la
comunicazione con le periferiche è possibile solamente tramite polling.
Questo vuol dire che il kernel deve continuamente controllare lo stato delle
periferiche collegate. Normalmente invece tutto il trasferimento di
informazioni avviene tramite interrupt e non tutti i dispositivi USB
risultano funzionanti se si usa il polling.
In rete ([5]) è presente una guida che spiega come utilizzare il driver anche
se il linguaggio utilizzato è l'assembly.
Il progetto è stato creato per permettere agli sviluppatori di sistemi
operativi di ottenere il supporto USB ed è studiato per poter funzionare in
modo standalone, ovvero senza dipendenze esterne, oppure come un driver
tipico utilizzante alcuni servizi del sistema ospite. CSUD è modulare perciò
i suoi componenti possono essere sostituiti.
43
2.5 CSUD
Sono utilizzabili diversi driver a seconda delle periferiche da supportare:
ˆ Driver USB generico (usbd): implementa le parti dello standard che
non cambiano da un sistema ad un altro come la enumerazione e la
congurazione dei device.
ˆ Driver dell'Host Controller (hcd): è specico per l'ambiente usato e
gestisce le comunicazioni siche con il bus. Non esiste un driver HCD
generico ma è presente un le header comune che permette ad ogni
HCD di comunicare.
ˆ Driver DesignWare Core (dwc): è specico per il controller host
Synopsys Designware.
ˆ Driver Hub: è pensato per gli hub, i quali permettono a diverse
periferiche di utilizzare la stessa porta. Sviluppato usando usbd è
quasi completo in quanto non implementa i trasferimenti tramite
interrupt.
ˆ Driver Human Interface Device (hid): supporta lo standard USB HID
che denisce la comunicazione con i dispositivi di interazione utente.
Anche questo è sviluppato usando il driver usbd.
ˆ Driver per la tastiera (kbd): Costruito utilizzando il driver HID
implementa la comunicazione con le tastiere le quali rappresentano
particolari device HID. Siccome non implementa trasferimenti tramite
interrupt alcune periferiche possono non funzionare.
ˆ Driver per il mouse: è stato sviluppato da Steve White e costruito
sempre usando il driver HID.
CSUD è suddiviso principalmente in tre cartelle: conguration, include
e source. Nella cartella conguration sono presenti diversi makele con cui è
possibile congurare la compilazione. Le opzioni esistenti e i valori ammessi
sono elencati in un le apposito chiamato arguments. Dato che il driver può
essere utilizzato in modo standalone è presente il le /source/platform.c il
quale contiene le funzioni di supporto in mancanza del sistema operativo
sottostante. Vengono implementate in questo le le funzioni più comuni
(malloc(), memcpy(), free(), printf()) nel caso il sistema operativo non le
fornisca.
Per evitare conitti con funzioni standard disponibili sono state chiamate
in modo dierente, in particolare MemoryAllocate() svolge il compito di
malloc(), MemoryDeallocate() quello di free(), MemoryCopy() quello di
memcpy() e LogPrintF() è simile a printf().
Nel caso il sistema operativo disponga di librerie standard è comodo
sostituire le routine del driver con un sottoprogramma il quale chiama la
funzione di libreria corretta una volta invocato. Come prima cosa si è
44
2.5 CSUD
cercato di compilare CSUD e assemblarlo con il resto dei le oggetto
scegliendo una congurazione di tipo standalone e quindi senza dipendenze
esterne. Per eettuare la compilazione è necessario invocare make nel
seguente modo: make driver OPZIONE1=... OPZIONE2=....
Per ottenere il driver compilato come desiderato sono state specicate le
opzioni GNU, CONFIG, TARGET, TYPE, LIB_KBD e LIB_MOUSE. La
prima indica il presso di GCC corretto (in questo caso arm-none-eabi),
CONFIG imposta la modalità debug, TARGET decide per quale
architettura congurare il driver (in questo caso RPI ovvero Raspberry Pi).
L'opzione TYPE rappresenta come devono essere gestite le dipendenze
esterne: STANDALONE nel caso non si debbano usare funzioni fornite dal
sistema, LOWLEVEL richiede dipendenze minime (default) mentre
DRIVER ha diverse dipendenze come i più tipici driver esistenti. Le ultime
due opzioni sono invece semplici ag che indicano se devono essere attivati i
driver per la tastiera e per il mouse.
Dopo una prima compilazione andata a buon ne è iniziata l'integrazione
con il kernel utilizzando le funzioni messe a disposizione da CSUD. Sono
disponibili molte routine per la congurazione e l'inizializzazione dei
dispositivi, in particolare quelle di impiego più generale sono due:
UsbInitialise() e UsbCheckForChange() utilizzabili includendo l'header le
usbd.h.
La prima carica il driver, enumera le periferiche e tenta di comunicare con
queste. Generalmente impiega un secondo per essere eseguita e una volta
che questa ha terminato è possibile utilizzare tutti gli altri metodi
riguardanti tastiera e mouse.
La seconda invece controlla ogni porta presente su ogni hub ricorsivamente
e aggiunge nuovi device se sono stati collegati. Se non sono presenti
dierenze rispetto all'ultimo controllo l'esecuzione è molto veloce altrimenti
può impiegare no ad alcuni secondi. La funzione UsbInitialise() viene
eseguita una sola volta all'avvio dal kernel all'interno di kmain() prima che
il controllo passi a Salto. Un primo problema riscontrato è che usando la
compilazione standalone il linker restituiva degli errori in quanto la
funzione LogPrintF() usata dal driver non era denita dal kernel e
nemmeno da CSUD stesso: si è quindi deciso di ricompilare il driver con
l'opzione TYPE=LOWLEVEL. In questo caso la funzione mancante è
implementata dal driver e richiede solamente LogPrint() per la stampa di
una stringa senza formattazione. Quest'ultima è stata denita a parte e
funge semplicemente da interfaccia per printf().
Le rimanenti tre routine (MemoryAlloc(), MemoryDealloc() e
MemoryCopy()) sono state denite allo stesso modo utilizzando le librerie
standard, in particolare malloc(), free() e memcpy().
Una volta ottenuto l'eseguibile contenente l'inizializzazione del controller
USB è stata fatta una prova sul dispositivo sico per vericarne il
funzionamento: sono stati usati diversi mouse ma nessuno è stato
45
2.5 CSUD
correttamente rilevato. Dato che non si conosceva la natura del problema si
è deciso di implementare anche il gestore degli eventi relativi al mouse.
Tutto il codice che si occupa di controllare lo stato delle periferiche è
implementato in sdl_update() per ricalcare la struttura originale di Salto.
Questo infatti eettua già di per sé polling per emulare il comportamento
dello Xerox Alto per cui la funzione è chiamata ad intervalli regolari senza
che sia necessaria alcuna modica.
Per la gestione del mouse sono disponibili diverse routine:
u32 MouseCount ()
Ritorna il numero di mouse collegati al sistema.
u32 MouseGetAddress ( u32 index )
Ritorna l'indirizzo del mouse identicato da index.
Result MousePoll ( u32 mouseAddress )
Legge lo stato dei tasti e la posizione del cursore salvando il tutto nelle
strutture interne al driver.
u8 MouseGetButtons ( u32 mouseAddress )
Ritorna lo stato dei pulsanti del mouse.
Bit 0: Pulsante sinistro.
Bit 1: Pulsante destro.
Bit 2: Pulsante centrale .
s16 MouseGetPositionX ( u32 mouseAddress )
Ritorna l'ascissa della posizione del mouse.
s16 MouseGetPositionY ( u32 mouseAddress )
Ritorna l'ordinata della posizione del mouse.
L'implementazione di sdl_update() è molto semplice, viene usata
UsbCheckForChange() per aggiornare le informazioni sui device collegati,
subito dopo si controlla tramite MouseCount() se almeno un mouse è
disponibile e, nel caso questo accada, ne viene richiesto l'indirizzo usando
MouseGetAddress(). A questo punto si utilizza MousePoll() per aggiornare
le informazioni relative ai tasti ed al cursore. Inne tramite
MouseGetButton(), MouseGetPositionX() e MouseGetPositionY() si
ottengono le informazioni necessarie; queste vengono confrontate con i
precedenti valori salvati e nel caso siano diversi si sovrascrivono i dati
vecchi con i nuovi. Si procede inoltre a registrare i cambiamenti in Salto
tramite le funzioni mouse_motion() e mouse_button() permettendo
l'interazione tra l'utente ed i programmi.
46
2.5 CSUD
Una volta completato il gestore sono stati fatti diversi test con scarsi
risultati, tutti i mouse provati infatti non venivano riconosciuti. Non
trovando problemi evidenti con il codice scritto questo è stato analizzato, in
particolare quello del kernel, ma senza risultato.
Dopo numerose ricerche è stato individuato il problema, il quale sembrava
dipendere dall'utilizzo della cache. Questa è stata disabilitata e, anche se le
prestazioni complessive sono calate drasticamente, il primo mouse provato è
stato correttamente riconosciuto. Non potendo però lasciare la cache
disabilitata per motivi di usabilità del sistema è stata ricercata la causa del
problema all'interno di CSUD.
Grazie a preziose informazioni trovate in rete ([14] e [15]) è stato possibile
identicare la parte di driver da modicare. All'interno di CSUD, in
particolare nella funzione HcdTransmitChannel() presente nel le
designware20.c, viene utilizzata una zona di memoria chiamata buer per
la comunicazione tra controller USB e sistema. Se viene attivato il caching
in qualche modo si altera la lettura e la scrittura del buer e questo causa
problemi di comunicazione che portano al malfunzionamento delle
periferiche collegate.
Una prima soluzione adottata è stata quella di eettuare un cosiddetto
ush della cache dati (ovvero una cancellazione completa) tramite
l'inserimento della seguente porzione di codice assembly all'inizio della
funzione:
int cl = 0;
__asm volatile ( " mcr p15 , 0 , %0 , c7 , c14 , 0 " :: " r " ( cl ) ) ;
Questa soluzione ha però lo svantaggio di invalidare la cache ad ogni
trasferimento e quindi ridurre i vantaggi in termini di prestazioni
complessive del sistema. É stata quindi percorsa una strada alternativa
agendo sulla congurazione dell'MMU; nella prima versione della funzione
di inizializzazione del paging i primi 512MB di RAM, quelli usati dal
sistema per programmi e dati, potevano usufruire della cache.
Il problema sta nel fatto che il buer precedentemente citato si trova in
questa zona di memoria. É possibile impostare l'area di RAM occupata in
modo equivalente a quella dedicata alle periferiche, in questo modo la cache
non viene utilizzata durante il trasferimento di informazioni ed il problema
non si presenta. Per fare questo è stata modicata l'inizializzazione
dell'MMU abilitando la cache per i primi 64MB di RAM e la dimensione
dell'heap è stata limitata in modo che il sistema non utilizzi più di questa
quantità di memoria. Il resto dello spazio d'indirizzamento è stato invece
congurato come memoria per le periferiche dove la cache non è
utilizzabile.
É stato poi modicato CSUD e l'indirizzo del buer, il quale prima era
allocato dinamicamente, è stato ssato oltre i 64MB di RAM. In questo
modo la cache non viene cancellata ed il tutto risulta ugualmente
47
2.5 CSUD
funzionante. Avendo il mouse funzionante ed un tempo di boot che
superava di poco il minuto si è concluso implementando il supporto alla
tastiera. Questa è gestita sempre in sdl_update() in modo simile al caso
precedente per cui viene usata KeyboardCount() per vericare la presenza
di almeno un dispositivo, se ne richiede l'indirizzo con
KeyboardGetAddress() e si aggiornano le informazioni con KeyboardPoll().
Dato che, a dierenza del mouse, occorre gestire molti più tasti è stato
implementato un meccanismo più complesso per capire se un determinato
pulsante è stato premuto o rilasciato. A dierenza di SDL infatti non è
dato sapere se un evento rappresenta una pressione o un rilascio ma è il
sistema che si deve fare carico di riconoscerlo. Il driver mette a disposizione
dei metodi per poter controllare lo stato generale dei tasti, in particolare le
funzioni usate sono:
ˆ KeyboardGetKeyDownCount()
ˆ KeyboardGetKeyDown()
ˆ KeyboardGetModiers()
La prima restituisce il numero di tasti premuti, la seconda serve per vericare
lo stato di un particolare pulsante mentre l'ultima restituisce lo stato dei
modicatori ovvero alt, ctrl, GUI e shift.
Il corpo del gestore è scritto come segue:
uint32_t knum = KeyboardGetKeyDownCount ( addr ) ;
uint16_t key , kdown ;
uint16_t keydown_new [ MAX_KEYHOLD ];
if ( knum > MAX_KEYHOLD )
kdown = MAX_KEYHOLD ;
else
kdown = knum ;
uint32_t i , j ;
for ( i = 0; i < kdown ; ++ i ) {
key = KeyboardGetKeyDown ( addr , i ) ;
if ( key < 256) {
for ( j =0; MAX_KEYHOLD ; ++ j ) {
if ( key == keydown [ j ]) {
keydown [ j ] = 0;
break ;
}
}
keydown_new [ i ] = key ;
}
}
for (; i < MAX_KEYHOLD ; ++ i )
keydown_new [ i ] = 0;
48
2.5 CSUD
for ( i = 0; i < MAX_KEYHOLD ; ++ i ) {
if ( keydown [ i ] != 0)
kbd_key ( keydown [ i ] , 0) ;
keydown [ i ] = 0;
}
for ( i = 0; i < kdown ; ++ i ) {
keydown [ i ] = keydown_new [ i ];
kbd_key ( keydown [ i ] , 1) ;
}
struct KeyboardModifiers mod = KeyboardGetModifiers ( addr ) ;
kbd_key ( USB_LSHIFT , mod . LeftShift ) ;
kbd_key ( USB_RSHIFT , mod . RightShift ) ;
kbd_key ( USB_LCTRL , mod . LeftControl ) ;
kbd_key ( USB_RCTRL , mod . RightControl ) ;
Il gestore utilizza un buer denito globale chiamato keydown per
memorizzare i tasti premuti la chiamata precedente; il numero massimo di
pulsanti premuti nello stesso momento è limitato da MAX_KEYHOLD
denita col valore 3. La prima parte comprendente i primi due cicli scrive
in un array ausiliario chiamato keydown_new i tasti premuti che però non
erano selezionati la chiamata precedente.
Dopo questa fase in keydown saranno presenti solo quei pulsanti non più
premuti; il terzo ciclo infatti non fa altro che segnalare alle altre parti
dell'emulatore che i tasti rimasti sono stati rilasciati. Per farlo utilizza la
funzione kbd_key() che nel caso il secondo argomento valga 0 rilascia il
tasto altrimenti lo considera premuto.
Dopo il terzo ciclo keydown contiene solo 0 perciò è possibile segnalare a
Salto i nuovi tasti selezionati e scriverli nel buer globale per utilizzarli alla
chiamata successiva. Come ultima cosa viene letto lo stato dei modicatori
che è ritornato nella struttura mod e utilizzando questa vengono impostati
gli stati dei tasti associati sempre tramite kbd_key().
Una volta implementato il gestore sono state fatte le modiche al le
keyboard.c il quale era stato rimosso no a quel momento. Questo le
infatti utilizzava molte costanti facenti parte delle librerie SDL ed
eettuava letture e scritture su disco. I codici relativi ai tasti più
comunemente utilizzati sono standard per cui è stato creato il le header
usbkeys.h contenente le associazioni tra simbolo e codice. Usando le
costanti presenti in questo le sono state sostituite quelle delle librerie SDL
eliminando le associazioni relative ai simboli non gestiti per semplicare il
tutto. Sono poi state modicate le funzioni per la lettura e la scrittura della
congurazione (kbd_cong_write() e kbd_cong_read() ) in quanto
necessitavano di poter accedere al disco. Queste routine sono state ridotte
in modo che ritornino immediatamente quando chiamate rendendo così
ssa la mappatura tra la tastiera sica e quella dell'Alto.
49
2.5 CSUD
Completate le modiche relative alla tastiera si è passati alla fase di
testing, è stata provata la prima tastiera USB ed è emerso che la velocità di
boot è dimezzata e la reattività del sistema è decisamente peggiorata.
Facendo numerose prove ed isolando pezzo per pezzo parti del gestore si è
scoperto che questo rallentamento è causato da KeyboardPoll() cioè la
funzione che si occupa di ottenere le informazioni dalla tastiera. Non
capendo la causa di questo comportamento, in quanto non segnalato in
rete, è stata provata un'altra tastiera non riscontrando il rallentamento.
Si è quindi stabilito che, in base all'hardware utilizzato, possono esserci
dierenze di prestazioni anche notevoli. Questo può essere dovuto al fatto
che il driver utilizza il polling per ottenere le informazioni, metodo non
standard per dispositivi USB.
Sfruttando il mouse e la tastiera che mostravano un comportamento
migliore si è arrivati ad avere il sistema funzionante anche se occorre
precisare che i tempi di boot e la reattività di Salto sono ancora lontani
dall'essere ottimi.
É da segnalare anche un problema di stabilità che va oltre i bug presenti in
Salto. Utilizzando i rmware più recenti infatti sono stati riscontrati
occasionali malfunzionamenti con il driver CSUD. Questo eettua delle
operazioni che sporadicamente bloccano la macchina, il monitor si spegne e
si sente un sibilo sugli altoparlanti. Il Raspberry Pi rimane in questo stato
ntanto che non lo si spegne e casi analoghi sono stati documentati sul
Web da altri utenti.
Per migliorare la velocità d'esecuzione è stato tentato un overclock del
Raspberry Pi modicando il le cong.txt: la frequenza del core è stata
portata a 900 Mhz mentre quella delle RAM è di 450 Mhz. L'emulatore è
complessivamente usabile anche se non sucientemente veloce per poter
essere considerato completo.
50
Conclusioni
In questo testo è stato descritto lo sviluppo di un emulatore di Xerox Alto
per Raspberry Pi senza sistema operativo residente. Si è partiti descrivendo
la macchina emulata e gli strumenti utilizzati illustrando in seguito
dettagliatamente lo sviluppo ponendo particolare attenzione ai problemi
riscontrati e alle soluzioni utilizzate. É possibile ampliare e migliorare il
lavoro fatto in molti modi, per esempio si potrebbe cercare un rmware per
il Raspberry Pi che eviti gli occasionali crash della macchina. Per
migliorare le prestazioni si potrebbe aumentare l'overclocking anche se
questo probabilmente ridurrebbe il tempo di vita del dispositivo a causa
dell'eccessivo calore emesso da SoC. É tuttavia possibile acquistare un kit
di dissipatori termici da applicare al Raspberry Pi in questi casi per ridurre
il rischio di guasti.
Per vericare i limiti della macchina utilizzata Salto è stato anche
compilato ed eseguito su di un vecchio notebook Pentium II 233Mhz dotato
di 192MB di RAM. Quello che è emerso è che le performance sui due
dispositivi si assomigliano molto per cui è dicile dire di poter ottimizzare
ulteriormente l'emulatore in quanto sembra che ci sia un limite sico del
Raspberry Pi dicilmente superabile anche tramite overclocking.
Una soluzione alla scarsa velocità potrebbe essere quella di stravolgere il
codice sorgente di Salto riscrivendo buona parte del ciclo principale ma
questo richiede di studiare a fondo il funzionamento dello Xerox Alto per
ottenere un risultato soddisfacente. Un'altra soluzione, forse meno
rischiosa, sarebbe quella di cambiare piattaforma di sviluppo passando da
ARM a x86. Per fare questo occorre ricompilare la toolchain per
l'architettura desiderata e modicare buona parte del kernel; anche se il
lavoro sembra molto impegnativo fortunatamente è presente sulla rete una
grande quantità di documentazione ed esistono ampie comunità attive di
sviluppatori.
Se si utilizza infatti un normale PC non si dovrebbero più presentare
problemi di prestazioni anche se le dimensioni ed i consumi sarebbero
sicuramente maggiori.
Dato che l'idea iniziale dietro lo sviluppo di piAlto era di ottenere un PC
simile anche esteticamente allo Xerox Alto, utilizzare una macchina x86
comporterebbe molte dicoltà in questo senso date le dimensioni della
51
2.5 CSUD
scheda e dell'alimentatore.
Se invece non si ritiene necessario che la macchina nale sia simile in ogni
dettaglio è suciente usare un PC qualunque o addirittura un notebook
privato di schermo interno e collegato ad un monitor con risoluzione
sucientemente alta per contenere la nestra di Salto.
Mantenendo l'attuale piattaforma, per poter utilizzare un comune monitor,
occorre disporre di un adattatore attivo che converta il segnale da HDMI a
VGA standard. É preferibile utilizzarne uno alimentato esternamente in
quanto il Raspberry Pi potrebbe fornire una corrente insuciente al
convertitore e questo potrebbe addirittura comportare un guasto del PC.
Come già detto è molto dicile dare l'idea della quantità di prove
eettuate per cui in questo testo si è cercato di riassumere le tappe ed i
test più signicativi relativi allo sviluppo di piAlto. Anche se è stato un
lavoro molto impegnativo è stata un'esperienza molto interessante in
quanto ha toccato molti temi, dalla storia dell'Informatica no alla
programmazione ARM senza sistema operativo passando per graca ai
livelli più bassi ed allo studio di un'emulatore. Il progetto, anche se non
ancora perfetto, è comunque funzionante ed utilizzabile e questo risultato
non era scontato quando lo sviluppo è iniziato.
52
Bibliograa
[1] ARM1176JZF-S Technical Reference Manual
[2] BCM2835 ARM Peripherals
[3] Valter Minute Raspberry Pi Guida all'uso Volume 1 2014: Edizioni
Master.
[4] http://archive.org/stream/byte-magazine-1981-09/BYTE_Vol_
06-09_1981-09_Artical_Intelligence#page/n59/mode/2up
[5] http://www.cl.cam.ac.uk/projects/raspberrypi/tutorials/os/index.
html
[6] http://elinux.org/RPicong
[7] https://github.com/raspberrypi/rmware/wiki/
Mailbox-property-interface
[8] http://history-computer.com/ModernComputer/Personal/Alto.html
[9] http://infocenter.arm.com
[10] http://it.wikipedia.org/wiki/Raspberry_Pi
[11] http://toastytech.com/guis/salto.html
[12] http://wiki.osdev.org/ARM_Overview
[13] http://wiki.osdev.org/ARM_RaspberryPi
[14] http://www.raspberrypi.org/forums/viewtopic.php?f=72&t=29392
[15] http://www.raspberrypi.org/forums/viewtopic.php?f=72&t=70428
53
Scarica

Progettazione e Realizzazione di un Emulatore di Xerox Alto per