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
© Copyright 2024 ExpyDoc