113
LABORATORIO DI ARCHITETTURA DEGLI ELABORATORI DEI Università di Padova Scheda basata su processore ARM920T SAMSUNG s3c2440 Descrizione del sistema ed esercizi

LABORATORIO DI ARCHITETTURA DEGLI ELABORATORIunina.stidue.net/Calcolatori Elettronici 2/Materiale/Sergio Congiu... · 2 La prima versione di questo manuale, scritta ormai quasi cinque

  • Upload
    dangnhi

  • View
    224

  • Download
    0

Embed Size (px)

Citation preview

LABORATORIO DI ARCHITETTURA DEGLI ELABORATORI

DEI Università di Padova

Scheda basata su processore ARM920T SAMSUNG s3c2440

Descrizione del sistema ed esercizi

2

La prima versione di questo manuale, scritta ormai quasi cinque anni fa, riguardava l’attività di laboratorio del corso di Architettura degli Elaboratori e la scheda di sviluppo UNI-PD-PXA con chip Intel PXA255. La multinazionale Intel oggi non produce più questo integrato e nella sua linea di prodotti non si trovano più processori ARM. Come le schede UNI-PD-PXA, anche le schede di sviluppo oggetto di questo manuale, dotate di microprocessore ARM Samsung s3c2440, sono state progettate dall’ing. Egidio Gioia e costruite dalla SERP S.p.A. (un’azienda della provincia di Torino, www.serp.it). Questo manuale, che può essere considerato una revisione del precedente, ne conserva la struttura e gli argomenti trattati; ovviamente gli esercizi, che erano stati proposti per la precedente scheda di sviluppo, sono stati rivisti e adattati alla nuova scheda con processore Samsung. Si ringrazia anticipatamente chiunque segnali errori e correzioni da apportare a questo manuale. Si ringraziano per l’attenta revisione i docenti del corso di Architettura degli Elaboratori: Sergio Congiu, Michele Moro, Carlo Fantozzi, Matteo Comin, Gabriele Manduchi. Antonio Barbalace Padova, 21 marzo 2009

3

INDICE 1 INTRODUZIONE........................................................................................................................7 2 SISTEMA DI SVILUPPO ...........................................................................................................8 3 ARCHITETTURA ARM...........................................................................................................10

3.1 Nota storica ........................................................................................................................10 3.2 Il consorzio ARM ..............................................................................................................10 3.3 L’architettura ARM............................................................................................................11

3.3.1 Il set di istruzioni Thumb (variante T) .......................................................................12 3.3.2 Istruzioni di moltiplicazione con risultato a 64 bit (variante M) ...............................13 3.3.3 Istruzioni DSP (variante E) ........................................................................................13

3.4 Note....................................................................................................................................13 4 IL CHIP Samsung s3c2440........................................................................................................14

4.1 System on Chip Samsung s3c2440 ....................................................................................14 4.2 External Memory Controller..............................................................................................16 4.3 NAND Flash Controller .....................................................................................................17 4.4 Clock, Power Management ................................................................................................17 4.5 DMA controller..................................................................................................................18 4.6 Porte di Input/Output .........................................................................................................18 4.7 PWM Timer .......................................................................................................................19 4.8 UART.................................................................................................................................20 4.9 USB Host Controller..........................................................................................................20 4.10 USB Device Controller ......................................................................................................21 4.11 Interrupt Controller ............................................................................................................21 4.12 LCD Controller ..................................................................................................................22 4.13 ADC & Touch Screen........................................................................................................23 4.14 Real Time Clock ................................................................................................................23 4.15 WatchDog Timer................................................................................................................24 4.16 MMC/SD/SDIO Controller................................................................................................24 4.17 SPI......................................................................................................................................24 4.18 IIC Bus Interface................................................................................................................25 4.19 IIS Bus Interface ................................................................................................................25 4.20 AC97 Controller.................................................................................................................25 4.21 Camera Interface ................................................................................................................26

5 LA SCHEDA DI SVILUPPO....................................................................................................27 5.1 Schema a blocchi ...............................................................................................................29 5.2 SDRAM, Led e MicroSwitch, controller Ethernet ............................................................30 5.3 NAND Flash ......................................................................................................................31 5.4 MMC/SD/SDIO .................................................................................................................32 5.5 Il CODEC IIS.....................................................................................................................32 5.6 USB Connectors.................................................................................................................33 5.7 Seriali (UARTs) .................................................................................................................33 5.8 JTAG..................................................................................................................................33 5.9 LCD TFT Samsung LMS700KF05 ...................................................................................33 5.10 Mappa di Memoria della scheda ........................................................................................34 5.11 Firmware e Software..........................................................................................................34

6 SVILUPPO DEL SOFTWARE .................................................................................................36 6.1 Sviluppo di software nativo ...............................................................................................36 6.2 Cross Development ............................................................................................................36 6.3 GNU toolchain ...................................................................................................................37 6.4 Executable Formats............................................................................................................39

4

6.5 L’Ambiente di Sviluppo.....................................................................................................40 6.6 Compiling and Assembling sources...................................................................................41 6.7 Linking...............................................................................................................................43 6.8 Dissecting and Exploring Executables...............................................................................45

6.8.1 Disassembling ............................................................................................................45 6.8.2 Lista dei simboli e delle sezioni di un eseguibile.......................................................45 6.8.3 Format Conversion.....................................................................................................46

6.9 Aiuto in Linea ....................................................................................................................46 7 ESECUZIONE REMOTA .........................................................................................................47

7.1 Connessione remota ...........................................................................................................48 7.1.1 Connessione remota con Linux..................................................................................48 7.1.2 Connessione remota con Windows ............................................................................51

7.2 Download di file sul target.................................................................................................54 7.2.1 Download di file in Linux..........................................................................................54 7.2.2 Download di file in Windows ....................................................................................56

7.3 Esecuzione del file sul target .............................................................................................58 7.3.1 ELF Format ................................................................................................................58 7.3.2 bin Format ..................................................................................................................58

8 Il DEBUGGING ........................................................................................................................60 8.1 Local debugging.................................................................................................................60 8.2 Stand-alone simulator ........................................................................................................61 8.3 Remote debugging .............................................................................................................61 8.4 Debugging utilizzando il Simulatore .................................................................................62

8.4.1 Simulatore in Linux ...................................................................................................62 8.4.2 Simulatore in Windows..............................................................................................64

8.5 Debugging utilizzando la Scheda di Sviluppo ...................................................................65 8.5.1 Debugging Remoto in Linux .....................................................................................65 8.5.2 Debugging Remoto in Windows................................................................................70

9 ESERCIZI ..................................................................................................................................74 9.1 Primo Programma (somma di due numeri)........................................................................74

9.1.1 Il codice......................................................................................................................74 9.1.2 Compilazione .............................................................................................................75 9.1.3 Debugging..................................................................................................................76

9.2 Secondo programma (somma di due numeri) ....................................................................77 9.2.1 Il codice......................................................................................................................77 9.2.2 Compilazione .............................................................................................................77 9.2.3 Debugging..................................................................................................................78

9.3 Terzo Programma (somma di due numeri) ........................................................................79 9.3.1 Il codice......................................................................................................................79 9.3.2 Compilazione .............................................................................................................79 9.3.3 Debugging..................................................................................................................80

Somma degli elementi di un vettore ..............................................................................................81 9.3.4 Il codice......................................................................................................................81 9.3.5 Compilazione .............................................................................................................82 9.3.6 Debugging..................................................................................................................82

9.4 Somma degli elementi di un vettore (subroutine)..............................................................83 9.4.1 Il codice......................................................................................................................84 9.4.2 Compilazione .............................................................................................................84 9.4.3 Debugging..................................................................................................................85

9.5 Somma degli elementi di un vettore (subroutine ricorsiva)...............................................86 9.5.1 Il codice......................................................................................................................87

5

9.5.2 Compilazione .............................................................................................................87 9.6 Rovesciamento di una stringa di caratteri ..........................................................................88

9.6.1 Il codice......................................................................................................................89 9.6.2 Compilazione .............................................................................................................89 9.6.3 Debugging..................................................................................................................89

9.7 Rovesciamento di una stringa di caratteri (con sp allineato) .............................................90 9.7.1 Il codice......................................................................................................................91 9.7.2 Compilazione .............................................................................................................91 9.7.3 Debugging..................................................................................................................91

9.8 Ordinamento di un vettore (merge-sort) ............................................................................92 9.8.1 Il codice......................................................................................................................94 9.8.2 Compilazione .............................................................................................................94 9.8.3 Debugging..................................................................................................................94

9.9 I led e gli switch .................................................................................................................95 9.9.1 Il codice......................................................................................................................95 9.9.2 Compilazione .............................................................................................................96 9.9.3 Debugging..................................................................................................................96

9.10 I pulsanti.............................................................................................................................97 9.10.1 Il codice......................................................................................................................98 9.10.2 Compilazione .............................................................................................................98 9.10.3 Debugging..................................................................................................................98

9.11 Il display LCD....................................................................................................................99 9.12 Disegnare a 16 Colori ......................................................................................................103

9.12.1 Il codice....................................................................................................................104 9.12.2 Compilazione ...........................................................................................................105 9.12.3 Debugging................................................................................................................105

9.13 Disegnare a 256 Colori ....................................................................................................106 9.13.1 Compilazione ...........................................................................................................108 9.13.2 Debugging................................................................................................................108

9.14 Scrivere caratteri sul display ............................................................................................109 9.14.1 Il codice....................................................................................................................111 9.14.2 Compilazione ...........................................................................................................113 9.14.3 Note..........................................................................................................................113

6

INDICE DELLE FIGURE Figure 1 : componenti di un sistema di sviluppo embedded................................................................8 Figure 2 : canali di comunicazione tra il sistema host e il target .........................................................8 Figure 3 : host target development environment .................................................................................9 Figure 4 : schema a blocchi del SoC Samsung s3c2440....................................................................15 Figure 5 : address space dell’External Memory Controller ...............................................................16 Figure 6 : NAND Flash controller logic ............................................................................................17 Figure 7 : schema della logica di distribuzione dei clock ..................................................................18 Figure 8 : schema di principio dei generatori di tempo .....................................................................20 Figure 9 : UART block diagram ........................................................................................................20 Figure 10 : Interrupt Controller sources.............................................................................................22 Figure 11 : LCD Controller................................................................................................................22 Figure 12 Touch Screen controller.....................................................................................................23 Figure 13 : RTC block diagram .........................................................................................................24 Figure 14 Integrated Interchip Sound (IIS) controller .......................................................................25 Figure 15 : AC97 controller logic ......................................................................................................26 Figure 16 : foto scheda di sviluppo Samsung s3c2440......................................................................28 Figure 17 : schema a blocchi della scheda di sviluppo SERP ...........................................................29 Figure 18 : schema di connessione dei due banchi di memoria SDRAM..........................................30 Figure 19 : address space dell’External Memory Controller della scheda SERP..............................31 Figure 20 : speaker e connettore MMC/SD/SDIO scheda SERP ......................................................32 Figure 21 : scheda SERP connettori frontali e switch .......................................................................33 Figure 22 : mappa di memoria della scheda ......................................................................................34 Figure 23 : ELF file format ................................................................................................................39 Figure 24 : cygwin prompt.................................................................................................................40 Figure 25 : passi di compilazione in linguaggio C.............................................................................41 Figure 26 : connettere la scheda SERP all’host computer per lo sviluppo remoto............................48

7

1 INTRODUZIONE Questa guida intende fornire al lettore le conoscenze di base per poter operare in completa autonomia su una postazione di sviluppo per sistemi embedded, in cui il target è costituito da una scheda basata su processore Samsung s3c2440 che è connessa ad un computer, l’host, tramite un cavo seriale RS232 in ambiente INSIGHT/GDB. Si esamineranno i diversi componenti della stazione di sviluppo, dando ovviamente maggiore enfasi al target, ma senza trascurare gli strumenti software cui ci si affida per la scrittura e il debug dei vari programmi. Dopo una breve panoramica che prenderà in esame, oltre ad i molti dispositivi esterni presenti sul chip Samsung, anche il funzionamento dei tool di compilazione, verranno presentati degli esempi di cui, oltre a fornire la descrizione, si analizzerà il codice (normalmente scritto in linguaggio assembly) e verrà descritto come effettuarne il download sul target ed il debug. Nei capitoli successivi si identificheranno e analizzeranno le varie componenti della postazione di sviluppo. Poiché questo intende essere un manuale d’uso di una scheda basata su core ARM, piuttosto che una descrizione tecnica dei suoi componenti, il livello di dettaglio della descrizione sarà inevitabilmente tale da richiedere a volte approfondimenti da effettuare sui seguenti documenti reperibili nel CD di distribuzione: 319349S3C2440A_UserManual_Rev13.pdf 418582S3C2440A_ApplicationNote_Rev10.pdf Con questo testo ci si pone essenzialmente due obiettivi: il primo è permettere al lettore di acquisire familiarità con la postazione di lavoro (computer host e scheda target) e con l’ambiente di sviluppo (GNU INSIGHT/GDB) messi a disposizione dall’Università di Padova; il secondo obiettivo è di guidare il lettore nell’uso, in laboratorio, della scheda SERP con processore Samsung s3c2440, fornendo sia le informazioni relative agli strumenti software da utilizzare, sia alcuni esempi da collaudare.

8

2 SISTEMA DI SVILUPPO Un sistema di sviluppo per un processore embedded si presenta di solito come in Figure 1: un comune computer, chiamato host, che ospita tutti gli strumenti software necessari alla compilazione dei programmi che verranno eseguiti sull’embedded system. Quest’ultimo viene chiamato target perché è il sistema per il quale i programmi vengono creati e su cui devono essere eseguiti.

Figure 1 : componenti di un sistema di sviluppo embedded.

Per poter essere eseguiti i programmi per il sistema embedded devono essere trasferiti dall’host al target. Di solito questi due dispositivi vengono connessi utilizzando le seguenti interfaccie:

• BDM/JTAG • seriale RS232 • rete ethernet

Negli ultimi anni il solo trasferimento dei file può venire effettuato grazie a chiavette USB, oppure schede SD/MMC o SmartMedia. Il debugging richiede però di disporre di una connessione persistente tra le due macchine, caratteristica che hanno le tre interfaccie sopra elencate, si veda Figure 2.

Figure 2 : canali di comunicazione tra il sistema host e il target

9

Un sistema embedded è un dispositivo che, a differenza di un personal computer, non ha necessariamente un sistema operativo, può non avere un dispositivo di memorizzazione di massa, oppure, se c’è l’ha, è di capacità ridotta ed ha un quantitativo di memoria principale (RAM) esiguo rispetto alle capacità a cui oggi (2009) siamo avezzi. Per questi motivi, un programma in un tale sistema viene sviluppato e collaudato con un emulatore software, sul computer host, poi trasferito sul target e qui eseguito, con l’assistenza però di un debugger che gira sulla macchina host. Perciò la verifica passo passo delle funzionalità di un programma per il sistema target può essere fatta lasciando eseguire al processore reale della scheda target le istruzioni macchina, potendo però intervenire nell’esecuzione, per esempio fermandola, visualizzando e alterando il contenuto dei registri, inserendo dei breakpoint, etc. utilizzando il computer host. Questo può essere fatto grazie ad una connessione tra i due sistemi, come BDM/JTAG, seriale RS232, Ethernet, ed il software INSIGHT/GDB. Facendo sempre riferimento a Figure 2, è necessario sottolineare che le interfaccie BDM e JTAG permettono un controllo totale del processore embedded e funzionano indipendentemente dalla presenza di BIOS o bootloader. Le altre due soluzioni richiedono per funzionare la presenza di un BIOS o bootloader che mettano il processore in grado di comunicare attraverso l’interfaccia seriale RS232 oppure l’Ethernet; questo BIOS o bootloader mette inoltre a disposizione un specie di shell che accetta alcuni comandi relativi a funzionalità di base per sfruttare l’hardware della scheda target (si veda Figure 3).

Figure 3 : host target development environment

A differenza di quanto si intravede in Figure 3, il sistema in dotazione presso i laboratori dell’Università non ha come bootloader RedBoot ma U-Boot.

10

3 ARCHITETTURA ARM

3.1 Nota storica Il design del primo chip ARM ha inizio nel 1983 come progetto della sezione ricerca e sviluppo della Acorn Computer Ltd, con l’obiettivo di costruire una CPU RISC (Reduced Instruction Set Computer) compatta (per quegli anni). Il gruppo di progetto, guidato da Sophie Wilson e Steve Furber, si propose l’obiettivo di ottenere un prodotto con una latenza di ingresso/uscita minima. Fu così che un paio di anno dopo, nell’aprile del 1985, nacque il primo processore ARM (Acorn RISC Machine). Il processore ARM è un RISC; ogni istruzione ha quindi lunghezza fissa, alla memoria ed alle risorse hardware si accede con le medesime istruzioni di load e store, inoltre ogni istruzione viene eseguita in un solo ciclo di clock. Nei primi anni ottanta la Acorn era una delle compagnie più importanti sul mercato dei personal computer in Inghilterra ed utilizzava per i propri prodotti il processore ad 8bit 6502 della Rockwell (made by the Western Design Centre of Phoenix, Arizona), lo stesso utilizzato dal computer Apple II. Il primo successo della Acorn fu la progettazione e la vendita alla British Broadcasting Corporation (BBC) di una serie di home computer costruiti intorno al processore 6502. Grazie alle sue caratteristiche di basso consumo di energia, i processori ARM dominano il mercato dei dispositivi elettronici portatili: circa il 98 % del più di un miliardo di telefoni portatili venduti ogni anno usa un processore ARM. Nel novembre 1990 viene costituito il consorzio ‘Advanced RISC Machines’ (ARM) dalla APPLE, ACORN e VLSI technology. Nel 1991 la ARM introduce il suo primo core RISC embedded: l’ARM6. Gli obiettivi di ARM sono le prestazioni elevate, l’alta integrazione ed il basso consumo dei componenti.

3.2 Il consorzio ARM Il consorzio ARM progetta sistemi e microprocessori innovativi che poi fornisce alle maggiori compagnie mondiali del settore, che a loro volta le implementano nel proprio silicio. La architettura ARM negli anni ha monopolizzato il mercato giungendo, nel 2001, a coprire oltre il 75% delle applicazioni RISC a 32 bit.

11

3.3 L’architettura ARM Il set di istruzioni dell’architettura ARM è evoluto significativamente da quando è stato introdotto per la prima volta sul mercato e continuerà ad evolvere in futuro. Si sono succedute negli anni 6 versioni del set di istruzioni, denotate da un numero incrementale da 1 a 6. Un core ARM non viene però identificato solo in base alla versione delle istruzioni implementate ma, grazie all’utilizzo di lettere aggiuntive, viene indicata la presenza di istruzioni addizionali. Le 6 versioni del set di istruzioni dell’architettura ARM sono:

• Versione 1 Questa versione non è mai stata usata in prodotti commerciali. Implementava un set di istruzioni con indirizzi a 26 bit. Contiene:

o le istruzioni di base (non include la moltiplicazione); o istruzioni di lettura e scrittura per byte, word e multi-word; o istruzioni di salto; o istruzione di interrupt software.

• Versione 2 Il set di istruzioni è stato esteso con le seguenti funzionalità: o istruzioni di moltiplicazione e di moltiplicazione con accumulo; o supporto per coprocessore; o due registri extra per il fast interrupt mode; o istruzioni atomiche (indivisibili) di lettura-scrittura, chiamate SWP e SWPB.

• Versione 3 In questa versione sono stati introdotti indirizzi a 32 bit e, di conseguenza, lo spazio di indirizzamento è stato esteso a 4GB. Le informazioni di stato che precedentemente venivano memorizzate nel registro r15 sono state spostate in un nuovo registro ‘Current Program Status Register’ (CPSR) e nei ‘Saved Program Status Register’ (SPSR), che sono stati introdotti per preservare il contenuto di CPSR quando si verifica una eccezione. Lista dei cambiamenti:

o sono state introdotte due nuove istruzioni (MRS e MSR) per permettere l’accesso a CPSR e agli SPSR;

o due nuovi modi di operare sono stati aggiunti, necessari a rendere possibile l’uso delle eccezioni di data abort, prefetch abort e undefined instruction direttamente dal sistema operativo.

• Versione 4 In questa versione sono state aggiunte le seguenti estensioni: o istruzioni di lettura e scrittura di halfword (16 bit); o istruzioni di lettura con estensione del segno, di byte e halfword; o nella versione T è stata introdotta una istruzione necessaria al trasferimento in thumb

mode; o un nuovo modo privilegiato che usa i registri del modo user.

• Versione 5 In questa versione sono state introdotte le seguenti migliorie: o è stato migliorato il passaggio dal modo normale al modo thumb; o consente l’uso delle stesse tecniche di generazione del codice sia nella modalità

thumb che nella modalità non thumb; o è stato introdotta una istruzione di breakpoint software; o sono state introdotte più opzioni per l’uso di coprocessori.

• Versione 6 L’ultima versione presenta il supporto per l’accesso a word e halfword non allineato alla memoria; il sistema di memoria è stato completamente rivisitato. Introduce le seguenti nuove istruzioni per entrambe le modalità:

o nuove istruzioni per migliorare l’handling delle eccezioni; o istruzioni per lo swap dei dati da un indianità all’altra; o istruzioni di accesso esclusivo;

12

o aggiunta di Single Instruction Multiple Data (SIMD). Per ogni versione sono state introdotte delle varianti (di volta in volta indicate con delle lettere che affiancano il numero della versione). In Table 1 sono listate tutte le versioni fino ad ora commercializzate.

Name ARM

instruction set version

Thumb instruction set

version

Long Multiply instructions? Notes

ARMv3 3 None No - ARMv3M 3 None Yes - ARMv4xM 4 None No - ARMv4 4 None Yes - ARMv4TxM 4 1 No - ARMv4T 4 1 Yes - ARMv5xM 5 None No - ARMv5 5 None Yes - ARMv5TxM 5 2 No - ARMv5T 5 2 Yes - ARMv5TExP 5 2 Yes Enhanced DSP

instructions except LDRD, MCRR, MRRC, PLD and STRD

ARMv5TE 5 2 Yes Enhanced DSP instructions

ARMv5TEJ 5 2 Yes Additino of BXJ instruction and Jazelle Extension support over ARMv5TE

ARMv6 6 3 Yes Additional instructions (CPS, CPY, LDREX, MCRR2, PKH, QADD*, QSUB*, REV, RFE, SAL, SETEND …)

Table 1

3.3.1 Il set di istruzioni Thumb (variante T) Il set di istruzioni thumb è un subset del set di istruzioni ARM. Le istruzioni thumb hanno una dimensione che è pari alla metà della dimensione delle istruzioni ARM (16 bit invece che 32). Il risultato che ne deriva è che normalmente può essere ottenuta una maggiore densità di codice (minor fabbisogno di memoria) utilizzando il set Thumb invece che il set ARM. Utilizzando il set di istruzioni Thumb si hanno due limitazioni:

• Per la realizzazione della stessa funzione, il codice thumb usa più istruzioni del codice ARM e quindi il codice ARM è meglio indicato per massimizzare le prestazioni di un codice con criticità di tempi di esecuzione.

• Il set di istruzioni Thumb non include alcune istruzioni necessarie per la gestione delle eccezioni.

13

La presenza del set di istruzioni Thumb è denotata dalla lettera T (non si applica alle versioni antecedenti alla versione 4).

3.3.2 Istruzioni di moltiplicazione con risultato a 64 bit (variante M) La variante M al set di istruzioni ARM include 4 istruzioni che realizzano le operazioni 32x32 64 e 32x32 + 64 64 con accumulo. La presenza di queste istruzioni è denotata dalla lettera M. La prima introduzione di queste istruzioni si è vista a partire dalla versione 3 del set di istruzioni ARM.

3.3.3 Istruzioni DSP (variante E) La variante E del set di istruzioni ARM include un numero di istruzioni extra che migliorano le prestazioni del processore ARM, tipicamente in applicazioni di digital signal processing.

3.4 Note Le famiglie di processori ARM attualmente più diffuse sono: famiglia versione core link

ARM7 ARMv3 http://www.arm.com/products/CPUs/families/ARM7Family.html ARM9 ARMv4T http://www.arm.com/products/CPUs/families/ARM9Family.html ARM9E ARMv5TE http://www.arm.com/products/CPUs/families/ARM9EFamily.html ARM10E ARMv5TE http://www.arm.com/products/CPUs/families/ARM10Family.html XScale ARMv5TE ARM11 ARMv6 http://www.arm.com/products/CPUs/families/ARM11Family.html

Table 2

14

4 IL CHIP Samsung s3c2440

4.1 System on Chip Samsung s3c2440 Il chip Samsung s3c2440 integra un processore ARM ed un elevato numero di periferiche per il suo interfacciamento con dispositivi esterni. L’integrato Samsung s3c2440 viene quindi chiamato System on Chip (SoC). Il gran numero di periferiche integrate nello stesso silicio permette di minimizzare il costo totale di un prodotto che utilizzi il Samsung s3c2440 ed allo stesso tempo non richiede di configurare periferiche esterne (già presenti sull’s3c2440). Il Samsung s3c2440 è un core ARM920T, basato sulla versione ARMv4T, costruito con tecnologia CMOS a 0,13µm ed è caratterizzato elettronicamente da basso assorbimento di potenza, che è essenziale in dispositivi palmari, telefoni cellulari, player musicali e, in generale, nelle applicazioni portatili a basso consumo. Questo capitolo fornisce una breve descrizione delle periferiche presenti nel SoC, schematizzate in Figure 4. Segue l’elenco:

• External Memory controller; • NAND Flash controller; • Power control; • DMA controller with externl request pins (4 canali); • 130 general purpose I/O; • PWM timers (4 canali), internal timer, Watch Dog timer; • UARTs (3 canali, IrDA 1.0); • USB Host controller (2 canali); • USB Device controller; • 24 canali di interrupt esterni; • LCD controller with LCD dedicated DMA; • 8 canali 10bit ADC; • Touch Screen interface; • RTC con funzione calendario; • SD/SDIO Host interface, MMC Protocol; • SPIs (2 canali); • IIC bus interface; • IIS Audio CODEC interface; • AC’97 CODEC interface. • Camera interface (max 4096x4096);

15

Figure 4 : schema a blocchi del SoC Samsung s3c2440

Come si vede in Figure 4, sia le periferiche che il processore sono interconnesse grazie a due bus dati (AHB, APB) di cui nello schema ci sono anche i rispettivi controllori (chiamati BUS CONT. Arbitor/Decode).

16

4.2 External Memory Controller Il memory controller interno al Samsung s3c2440 fornisce tutti i segnali di controllo (senza alcun bisogno di elettronica aggiuntiva) per la gestione delle memorie esterne interfacciabili. Tutti i timing sul bus sono programmabili al fine di meglio adattarli alle varie esigenze delle memorie esterne. Questo controller nasce per supportare ROM, SRAM e SDRAM; gestisce fino ad otto banchi di 128Mbytes di memoria l’uno, coprendo uno spazio di indirizzi complessivo di 1GB. Per ogni banco di memoria c’è un pin esterno di chip select (in Figure 5 nGCS0, nGCS1, ... , nGCS7).

0x0000_0000

0x0800_0000

0x1000_0000

0x1800_0000

0x2000_0000

0x2800_0000

0x3000_0000

0x3800_0000

0x4000_0000

SROM/SDRAM(nGCS7)

SROM/SDRAM(nGCS6)

SROM(nGCS5)

SROM(nGCS4)

SROM(nGCS3)

SROM(nGCS2)

SROM(nGCS1)

Boot InternalSRAM (4KB)

128 MB

128MB

128MB

128MB

128MB

128MB

2MB/4MB/8MB/16MB/32MB/64MB/128MB

2MB/4MB/8MB/16MB/32MB/64MB/128MB

Refer toTable 5

1GBHADDR[29:0]

AccessibleRegion

SROM/SDRAM(nGCS7)

SROM/SDRAM(nGCS6)

SROM(nGCS5)

SROM(nGCS4)

SROM(nGCS3)

SROM(nGCS2)

SROM(nGCS1)

SROM(nGCS0)

OM[1:0] = 01,10

OM[1:0] = 00

[ Not using NAND flash for boot ROM ] [ Using NAND flash for boot ROM ]

Boot InternalSRAM (4KB)

Figure 5 : address space dell’External Memory Controller

Grazie a dei speciali pin nBE0, nBE1, nBE2, nBE3 è possibile accedere ad un banco di memoria a byte, word o double word (possiamo cioè non solo specificare l’indirizzo di una lettura o scrittura, address bus, ma anche indicare quanti byte vogliamo leggere/scrivere). Anche se il bus dati dell’s3c2440 è a 32bit, non è obbligatorio che le memorie collegate abbiano un bus dati anch’esse a 32bit: possono essere presenti memorie anche a 16bit o 8bit; tutto ciò è configurabile nei registri del SoC. In Figure 5 si può infine notare come solo i banchi 7 ed 8 (rispettivamente chip select 6 e 7) possono ospitare della memoria SDRAM equipaggiando il SoC con un massimo di 256MB.

17

4.3 NAND Flash Controller Grazie al ridotto costo delle memorie NAND Flash queste vengono oggi largamente utilizzate. A differenza delle memorie Flash di tipo NOR, quelle di tipo NAND, non consentono il fetch delle istruzioni da parte della CPU direttamente su di esse: per essere eseguito il codice che esse contengono va prima trasferito su SRAM o SDRAM. Infatti il NAND Flash Controller di questo chip è configurato in modo tale che al reset, esso provveda a copiare in una speciale memoria SRAM interna del processore (di soli 4kB) i primi 4kB della Flash: in questi 4kB ci dovrà essere non tutto il boot loader del sistema, ma almeno il codice che permetta il trasferimento di un intero bootloader o sistema operativo da altre periferiche o dalla periferica NAND Flash stessa. Le NOR Flash possono essere collegate sul bus di memoria del processore come fossero delle memorie RAM. Le memorie NAND Flash richiedono invece una logica di controllo molto più complicata, tale da portare alla definizione di un protocollo e di un bus di comunicazione per la lettura e scrittura dei dati sulle memorie Flash, non permettendo quindi un collegamento di questi dispositivi sul bus di memoria. Il controller implementa questo protocollo di comunicazione e dispone di parecchie opzioni di configurazioni che permettono al progettista di collegare memorie NAND con bus dati sia ad 8bit che a 16bit e pagine a diverse capacità. Lo schema logico del controller è in Figure 6.

SFR

ECCGen.

Stepping Stone(4KB SRAM)Stepping Stone Controller

SYST

EMBU

S

NAND FLASHInterface

CLEALE

nFCE

nFREnFWE

FRnBI/O0 - I/O15AHB

Slave I/F

Control &State Machine

Figure 6 : NAND Flash controller logic

4.4 Clock, Power Management I componenti di controllo del clock e dell’alimentazione vengono trattati insieme anche nel manuale dell’integrato: il consumo di potenza del SoC è infatti determinato dalla frequenza di lavoro delle sue unità e dal numero di periferiche attive in un determinato istante. Più periferiche sono contemporaneamente attive e maggiore è il consumo totale del dispositivo; perché una periferica sia funzionante deve però disporre in entrata di un segnale di clock. Questa unità di controllo del clock e della potenza permette, tramite una serie di registri, di decidere le frequenze dei generatori di clock sul chip e di attivarne o disattivarne il loro invio alle diverse periferiche. I più importanti segnali di clock interni al SoC sono: FCLK, HCLK, PCLK e UCLK. La distribuzione dei clock all’interno del Samsung s3c2440 è schematizzata in Figure 7; ogni periferica è raggiunta da almeno un segnale di clock.

18

INTCNTL

Po werM anagem ent

FCLK

Input Clock

FCLK defination

If SLOW modeFCLK = input clock/divider ratio

If Normal mode (P, M & S value)FCLK = MPLL clock (Mpll)

ARM920T

HCLK

PCLK

UPLL(96/48 MHz)

BUSCNTL

MEMCNTL

ARB/DMA

ExtMaster

LCDCNTL

Nand FlashController

Camera

WDT

SPI

PWM

I2C

SDI

ADC

UART

I2S

GPIO

RTC

USBDevice

Clock ControlRegister

USBHost I/F

1/d

1/21/1

AC97

1/n

MPLLin

Figure 7 : schema della logica di distribuzione dei clock

La logica di controllo delle alimentazioni permette di far funzionare il chip in un predeterminato modo di consumo di potenza. L’s3c2440 ha quattro modalità di consumo energetico: NORMAL, SLOW, IDLE, SLEEP.

4.5 DMA controller L’s3c2440 supporta 4 canali DMA. Il controller DMA è localizzato tra il bus di sistema e il bus delle periferiche (vedi Figure 4). Ogni canale DMA può spostare indipendentemente dagli altri canali dati tra dispositivi collegati ai due bus; sono possibili trasferimenti in entrambi i sensi e anche all’interno dello stesso bus. Un caratteristica interessante è che il trasferimento DMA può essere richiesto sia via software, per esempio dopo aver finito l’elaborazione di un’immagine in un’area di memoria, questa può essere copiata nel frame buffer usando il DMA (possibilmente sincrono con lo scan verticale del monitor); sia dalle periferiche che iniziano un trasferimento in DMA: si può pensare, ad esempio, ad una scheda di rete che riceve un pacchetto di dati e che, alla sua ricezione, dato che è dotata di poca memoria, la libera subito richiedendo un trasferimento via DMA del pacchetto di dati nella memoria RAM della macchina. Se si vuole beneficiare di questa seconda modalità di funzionamento si scoprirà che ogni canale può essere comandato da un sottoinsieme di tutte le periferiche, che ovviamente è diverso per canale.

4.6 Porte di Input/Output Il Samsung s3c2440 è un chip di silicio inserito in un contenitore FBGA a 289 piedini. 130 di questi piedini hanno più di una funzionalità: possono perciò essere controllati via software, come per

19

accendere o spegnere led, attivare o disattivare relè, rilevare quando un livello elettrico va dall’alto verso il basso o il contrario (interrupt) etc... Questi ultimi sono i pin di input/output e in questo integrato hanno tutti, oltre alla funzione di pin di I/O, almeno un’altra funzione. Ciò vuol dire che, per esempio, ci sono 3 pin che vengono usati per controllare la SPI ma possono essere usati anche come pin di I/O: o si usano per fare una cosa, o per l’altra. L’s3c2440 ha 8 port multi funzionali di I/O e un port multi funzionale di sola uscita. Segue l’elenco:

• Port A(GPA) : 25 out port; • Port B(GPB) : 11 in/out port; • Port C(GPC) : 16 in/out port; • Port D(GPD) : 16 in/out port; • Port E(GPE) : 16 in/out port; • Port F(GPF) : 8 in/out port; • Port G(GPG) : 16 in/out port; • Port H(GPH) : 9 in/out port; • Port J(GPJ) : 13 in/out port.

Ogni port di ingresso/uscita ha le sue caratteristiche e può essere configurato in base alle necessità progettuali. Ovviamente non tutti i port hanno le medesime caratteristiche e possibilità di configurazione. Si rammenta che un pin non può essere utilizzato contemporaneamente come input ed output.

4.7 PWM Timer Ci sono 5 timers a 16bit di cui 4 possono essere portati all’esterno. Questi quattro sono il timer 0, 1, 2 e 3 ed hanno una logica di controllo che permette la generazione di segnali PWM. I timer 0 ed 1 condividono un prescaler ed i timer 2, 3 e 4 ne condividono un altro, ogni timer invece ha un suo divisore di clock, lo schema è in figura Figure 8. Per ogni timer è possibile configurare il duty cycle dell’onda PWM. Per ogni timer è possibile scegliere alternativamente il funzionamento in modalità one-shot o auto reload mode.

ClockDivider

5:1M

UX

Dead ZoneGenerator

TOUT0

TOUT1

TOUT2

ControlLogic0

TCMPB0 TCNTB0

ControlLogic1

TCMPB1 TCNTB1

5:1M

UX

ClockDivider

5:1M

UX

5:1M

UX

ControlLogic2

TCMPB2 TCNTB2

TOUT3ControlLogic3

TCMPB3 TCNTB3

No Pin

PCLK

8-BitPrescaler

8-BitPrescaler

Dead Zone

Dead Zone

TCLK0

1/8

1/4

1/16

1/2

TCLK1

1/8

1/4

1/16

1/2

5:1M

UX

ControlLogic4

TCNTB4

20

Figure 8 : schema di principio dei generatori di tempo

4.8 UART L’s3c2440 ha tre porte seriali di input/output (Universal Asynchronous Receiver and Transmitter, UART) ognuna delle quali, quando riceve dati, o genera un interrupt oppure sposta dati in memoria via DMA. L’UART, utilizzando il clock di sistema, permette velocità dati fino a 921.6Kbps; utilizzando clock esterni, però, può arrivare a velocità maggiori. Ogni UART dispone di una FIFO di 64byte per la parte di ricezione e 64byte per la trasmissione. Supporta anche il protocollo infrared (IrDA ver 1.0) 1 o 2 bit di stop 5, 6, 7 e 8 bit di dati, con e senza parity check.

Buad-rateGenerator

ControlUnit

Transmitt er

Receiver

Peripheral BUS

TXDn

Clock Source(PCLK, FCLK/n,UEXTCLK)

RXDn

Transmit FIFO Register(FIFO mode)

Transmit Hold ing Register(Non-FIFO mode)

Receive FIFO Register(FIFO mode)

Receive Holding Register(Non-FIFO mode only)

In FIFO mode, all 64 Byte of Buffer register are used as FIFO register.In non-FIFO mode, only 1 Byte of Buffer register is used as Holding register.

Transmit Shif ter

Transmit Buf ferRegister(64 Byte)

Receive Shifter

Receive BufferReg ister(64 Byte)

Figure 9 : UART block diagram

4.9 USB Host Controller Il SoC Samsung dispone di ben due porte Universal Serial Bus (USB) versione 1.1 di tipo host, cioè porte USB a cui possiamo collegare pendrive, Hard Disk portatili, lettori MP3 e altri dispositivi che normalmente colleghiamo al PC di casa; possiamo infatti, disponendo del driver compilato per processore ARM, collegare anche una stampante. Entrambe le porte sono USB full speed. Il controller è conforme allo standard OHCI 1.0. Non si riporta lo schema di questa periferica per la sua eccessiva complessità.

21

4.10 USB Device Controller Il SoC Samsung dispone anche di una porta USB, versione 1.1 full speed, che permette di essere collegato ad un PC e di essere visto, dal PC, come un dispositivo esterno, quale una memoria o una stampante, in base a come viene programmato questo USB Device Controller. L’USB Device Controller permette di configurare fino a 5 endpoint USB e inoltre è possibile impostare un trasferimento DMA. Anche in questo caso si faccia riferimento al manuale del chip per lo schema elettrico della periferica.

4.11 Interrupt Controller L’interrupt controller può ricevere interrupt da ben 60 diverse sorgenti, interne al SoC oppure esterne, quest’ultime provengono dal port di I/O. Il core ARM920T ha solo due linee di interrupt: FIQ e IRQ. Ognuna delle 60 sorgenti di interrupt potrà essere configurata come FIQ oppure IRQ. Attraverso questo controller è possibile abilitare o disabilitare una sorgente di interrupt ed individuare quale sorgente di interrupt ha scatenato l’interruzione: l’ISR di IRQ o FIQ, prima di tutto, interrogherà questo controller per sapere quale sorgente ha generato l’evento. Questo controller ha un registro che, tramite una maschera di bit, identifica quale sorgente ha scatenato l’interrupt; per alcuni interrupt è necessario andare a controllare un secondo registro: per esempio, nel caso si abiliti alla generazione degli interrupt un pin del port di I/O, come EINT5, l’Interrupt controller ci avviserà che c’è stato un interrupt esterno, ma la linea può essere la 4, 5, 6 o la 7, sarà necessario verificare nei registri del port di I/O quale linea ha cambiato stato logico. Le sorgenti di IRQ hanno tra di loro una priorità, questa priorità è configurabile e la gestione delle priorità viene fatta a livello elettrico dall’Interrupt Controller; non deve perciò essere gestita via Software. In Figure 10 la logica di gestione delle priorità.

22

ARBITER6

ARBITER0

ARM IRQ

REQ1/EINT0

ARBITER1

ARBITER2

ARBITER3

ARBITER4

ARBITER5

REQ4/INT_TICKREQ5/INT_WDT_AC97

REQ0/INT_TIMER0

REQ3/INT_TIMER3REQ2/INT_TIMER2REQ1/INT_TIMER1

REQ4/INT_TIMER4

REQ0/INT_LCDREQ1/INT_DMA0

REQ3/INT_DMA2REQ2/INT_DMA1

REQ5/INT_UART2

REQ4/INT_DMA3REQ5/INT_SDI

REQ0/INT_SPI0REQ1/INT_UART1REQ2/INT_NFCONREQ3/INT_USBDREQ4/INT_USBHREQ5/INT_IIC

REQ1/INT_UART0REQ2/INT_SPI1REQ3/INT_RTCREQ4/INT_ADC

REQ0REQ1REQ2REQ3REQ4REQ5

REQ2/EINT1REQ3/EINT2REQ4/EINT3

REQ0/EINT4_7REQ1/EINT8_23REQ2/INT_CAMREQ3/nBATT_FLT

4 inputs16 inputs2 inputs

2 inputs

3 inputs

3 inputs

3 inputs

2 inputs

2 inputs

Figure 10 : Interrupt Controller sources

4.12 LCD Controller L’LCD Controller del Samsung s3c2440 contiene la logica elettronica necessaria a trasferire un’immagine da un buffer video in memoria RAM ad un monitor LCD esterno. Questa periferica può essere utilizzata sia per controllare LCD sia di tipo STN che di tipo TFT; in entrambi i casi supporta display di diverse grandezze e diverse profondità di colore con e senza l’utilizzo di palette. Una volta configurato l’LCD Controller con le caratteristiche di scansione dell’LCD esterno collegato, scrivendo su un’area di memoria un’immagine nell’opportuno formato all’indirizzo assegnato nei registri, sarà il controller stesso che si preoccuperà di disegnare l’immagine sul monitor esterno. La Figure 11 mostra lo schema logico di funzionamento di questo controller.

System Bus

LPC3600 is a timing control logic unit for LTS350Q1-PD1 or LTS350Q1-PD2.LCC3600 is a timing control logic unit for LTS350Q1-PE1 or LTS350Q1-PE2.

REGBANK

LCDCDMAVIDPRCS

LPC3600

TIMEGEN

VD[23:0]

VCLK /LCD_HCLKVLINE / HSYNC / CPVVFRAME / VSYNC / STVVM / VDEN / TP

LCD_LPCOE / LCD_LCCINVLCD_LPCREV / LCD_LCCREVLCD_LPCREVB / LCD_LCCREVB

...VIDEOMUX

LCC3600

Figure 11 : LCD Controller

23

LCDCDMA è un DMA engine interno all’LCD Controller che trasferisce, comandato dal resto della logica, i dati video sui pin VD[23:0].

4.13 ADC & Touch Screen Trattiamo qui insieme l’ADC e l’interfaccia di controllo del Touch Screeen perchè quest’ultima utilizza per il suo funzionamento l’ADC. L’ADC è un Convertitore Analogico Digitale a 10bit con 8 entrate multiplexate; quando si vuole utilizzare il Touch Screen, 4 delle 8 entrate al convertitore vengono utilizzate per il Touch Screen. In Figure 12 c’è lo schema dei collegamenti e logico del convertitore.

Pullup

Waitin g for Interrupt Mode

INT_TC

INT_ADC

8:1MUX

A/D Converter

ADC InputContro l

Interrup tGeneration

ADCInterface&TouchScreen

AVDD

AGND

XP

XM

YP

YM

A[3:0]

(note)

(note)

Touch Screen

Figure 12 Touch Screen controller

Il convertitore ADC è completo di sample and hold integrato sul chip e il range di tensioni in ingresso è limitato da 0 a 3.3Volt, la frequenza di conversione massima è di 2,5MHz.

4.14 Real Time Clock Il Real Time Clock (RTC) è l’unità che permette al dispositivo Samsung di mantenere aggiornata la data e l’ora; per fare questo il SoC deve però essere collegato ad una batteria di backup in modo tale che, se l’alimentazione principale viene staccata, questo può proseguire il suo conteggio orario. Come la maggior parte dei dispositivi RTC commerciali, che di solito non sono integrati in un SoC con il processore, questo Real Time Clock deve essere letto e scritto a byte, i valori che vengono letti sono codificati in Binary Coded Decimal (BCD).

24

215 Clock Divider

XTOrtc

XTIrtc

Control Register

SEC MIN HOUR DAY DATE MON YEAR

Leap Year Generator

Alarm Generator

Reset Register

1 Hz

INT_RTC

RTCCON RTCALM

RTCRST

Time Tick GeneratorTIME TICKTICNT

128 Hz

PMWKUP

Figure 13 : RTC block diagram

In Figure 13 è riportato lo schema interno dell’unità RTC, che contiene le informazioni relative ai secondi, i minuti , l’ora, il giorno, il mese e l’anno. Questa unità, per il suo funzionamento, ha bisogno di una batteria esterna e di un oscillatore al quarzo operante alla frequenza di 32,768kHz. A supporto di Sistemi Operativi in Tempo Reale, questo RTC può generare interrupt con la risoluzione di un millisecondo.

4.15 WatchDog Timer Il watchdog timer permette di comandare il reset del chip quando quest’ultimo è disturbato da malfunzionamenti come rumore o errori del sistema operativo. Questa periferica non è altro che un contatore, che può generare interrupt alla fine del conteggio, in più però permette anche di comandare il chip con un reset.

4.16 MMC/SD/SDIO Controller Il Samsung s3c2440 permette di leggere e scrivere Multi Media Card (MMC Spec. 2.11), Secure Digital Memory Card (SD ver 1.0) e SDIO Card (ver 1.0). Questi tre tipi di schede si presentano tutte con un formato meccanico simile, tra di loro però ci sono delle lievi differenze. Sia le MMC che le SD vengono fabbricate per memorizzare dati, ma le SD, che ne rappresentano l’evoluzione, contengono un switch meccanico per proteggere la scheda da scritture e il bay dove viene inserita la scheda ha un riconoscitore di inserimento scheda. Lo standard SDIO permette invece di costruire dispositivi come periferiche WiFi, convertitori VGA ed altri, che si possono infilare nel connettore SD: questi comunicano come se fossero delle SD, ma in più possono anche generare degli interrupt. Altrimenti, una schedina MMC o SD non può generare interrupt, bensì è il Controller che genera un interrupt quando finisce un trasferimento dati da o verso l’MMC/SD.

4.17 SPI Il Samsung s3c2440 dispone di due bus Serial Peripheral Interface (SPI ver. 2.11). Ognuno di questi ha due shift register a 8bit rispettivamente per ricevere e trasmettere dati. Durante un trasferimento dati SPI, lo standard prevede che i dati vengano simultaneamente trasmessi e ricevuti. Il SoC per ognuna delle due interfaccie ha esternamente le seguenti linee:

• una per il clock (SPICLK);

25

• una per i dati in ingresso (SPIMISO); • una per i dati in uscita (SPIMOSI); • un’ultima e sola linea di chip select: può perciò esser collegato un solo chip slave SPI.

4.18 IIC Bus Interface L’interfaccia Inter Integrated Circuit (IIC), meglio conosciuta come I2C, è un bus a 2 pin (SCL, SDA, rispettivamente linea di clock e linea dati e/o indirizzi) che permette di collegare più dispositivi che parlano attraverso questo protocollo. Il controllore IIC nell’s3c2440 è il bus master, tutti gli altri dispositivi collegati sono slave. Questa interfaccia supporta anche bus IIC multi-master.

4.19 IIS Bus Interface L’interfaccia Integrated Interchip Sound (IIS) è un bus seriale per connettere dispositivi audio digitale tra loro. Nello specifico si può connettere il SoC ad un DAC che supporta questa interfaccia. IIS è un interfaccia definita nel 1996 e viene ancora utilizzata nei dispositivi musicali per passare da segnale digitale a segnale analogico. Questa interfaccia si presenta elettricamente con almeno 3 pin, il Samsung ne supporta 5:

1. una linea di bit clock (SCLK); 2. una linea di selezione del canale (LRCLK); 3. un master clock (CDCLK); 4. una linea dati di ingresso (SDI); 5. una linea dati di uscita (SDO).

Le linee dati trasportano solo informazioni relative al suono da convertire in analogico, senza nessuna informazione aggiuntiva. La Figure 14 rappresenta lo schema logico di funzionamento dell’ interfaccia.

ADDR

DATA

CNTL

PCLK

BRFC

IPSR_A

IPSR_B

TxFIFO

RxFIFO

SCLKG

CHNC

SFTR

LRCK

SCLK

SD

CDCLKMPLLin

Figure 14 Integrated Interchip Sound (IIS) controller

4.20 AC97 Controller Il Controller Audio Codec ’97 supporta il protocollo AC97 revision 2. Il Controller AC97 comunica con un CODEC AC97, esterno al SoC, che converte in analogico i dati di un segnale PCM trasportati dal protocollo AC97 via AC-link.

26

A differenza della precedente interfaccia IIS, l’AC97 è un bus seriale in cui i dati viaggiano ad una velocità fissa e sono impacchettati in modo da trasportare sia il sonoro che informazioni di controllo. Nonostante queste differenze sostanziali, AC97 utilizza lo stesso numero di fili per la connessione con un dispositivo esterno:

1. una linea di clock a frequenza fissa (12,288MHz); 2. un segnale di sincronismo; 3. un segnale di reset; 4. una linea di dati in ingresso (sdata_in); 5. una linea di dati in uscita (sdata_out).

A causa del fatto che i dati trasferiti via AC-link sono a pacchetto, questo controllore è molto più complesso del precedente; per completezza si riporta comunque lo schema logico in Figure 15.

APBI/F

DMAEngine

InterruptControl

MIC inFIFO

PCMout FIFO

PCM inFIFO

SFR

AC-linkI/F

FSM & Control

APB

AC-link

Figure 15 : AC97 controller logic

Il Samsung s3c2440 non permette il funzionamento concorrente di AC97 e IIS: si deve scegliere perciò di utilizzare o una o l’altra interfaccia sonora; queste infatti condividono gli stessi 5 pin esterni del SoC.

4.21 Camera Interface Il Samsung s3c2440, in particolare la revisione s3c2440A, che è quella montata sulle schede del laboratorio, dispone della logica di controllo di una videocamera esterna conforme allo standard ITU-R BT.601/656 YCbCr a 8bit con una risoluzione massima di 4096x4096pixels (che equivalgono ad una risoluzione di 16 megapixels). Queste caratteristiche rendono il processore un candidato ideale per il controllo di una macchina fotografica digitale. Il controller della videocamera può essere configurato in modo tale da capovolgere automaticamente un’immagine sui due assi, oppure ruotarla. Un’ultima caratteristica è la possibilità di acquisire simultaneamente due immagini: una ad alta risoluzione (massimo 4096x4096 pixels) e una seconda a bassa risoluzione (massimo 640x480). Quando l’immagine arriva al SoC questo provvede a copiarla via DMA nella memoria di sistema: invece di fare una sola copia, ne vengono fatte due: una mantenendo la risoluzione e l’altra viene prima scalata dal controllore della Camera Interface.

27

5 LA SCHEDA DI SVILUPPO La scheda di sviluppo basata su processore ARM920T Samsung s3c2440A è stata progettata con lo scopo di rendere disponibile, presso i laboratori didattici del Dipartimento di Ingegneria dell’Informazione dell’Università di Padova, un computer embedded basato su un core RISC ARM all’avanguardia e di grande diffusione sul mercato dei dispositivi portatili, come lettori multimediali e cellulari (2008). Il cuore della scheda di sviluppo è il chip s3c2440A che integra un core ARM (tra i più veloci sul mercato) e presenta un elevato numero di interfacce interne per dispositivi periferici che lo rendono particolarmente adatto ad un ambiente didattico. La scheda di sviluppo è costituita da due Printed Circuit Board (PCB): una, più piccola, chiamata Credit Card su cui sono saldati il SoC Samsung, la memoria Flash, la SDRAM, il codec audio e parte della logica di alimentazione; una seconda, più grande, la Motherboard, su cui si trova invece la circuiteria necessaria alla connessione a tutte le periferiche, di cui alcune di uso prettamente didattico. Le caratteristiche tecniche e le periferiche presenti sulla scheda di sviluppo sono:

• Memoria NAND Flash 128MB (sulla Credit Card, ospita UBOOT, Linux/Windows CE kernel, FileSystem);

• Memoria RAM (SDRAM) 64MB (sulla Credit Card); • Codec audio IIS (sulla Credit Card), amplificatore di potenza, speaker (installati sulla

motherboard, nessuna possibilità di regolazione manuale del volume); • Ethernet 10/100Mb controller; • Una porta seriale RS232, una porta seriale a livelli TTL (entrambe senza hardware flow

control); • 16 led e 16 microswitch mappati al medesimo indirizzo di memoria; • 2 pulsanti connessi a pin di un porto di I/O general purpose;

• connettore per Display LCD e Display TFT 800x480 con 24bit di profondità di colore,

elettronica per backlight LED boost device, connettore Touch Screen e Touch Screen; • connettore MMC/SD/SDIO; • un connettore USB client; • un connettore USB host; • connettore JTAG connesso all’s3c2440A;

• batteria tampone per l’RTC del SoC; • logica di alimentazione con con pulsanti di accesione (ON), pulsante di spegnimento (OFF)

e bottone di RESET.

28

Figure 16 presenta una foto della scheda di sviluppo; la Credit Card è montata attraverso dei connettori sulla Motherboard. Lungo il lato inferiore, da sinistra verso destra, si riconoscono: il connettore USB host, il connettore USB client, un connettore RJ12 per la seriale RS232, il connettore Ethernet, i due microswitch (8 ed 8), più sopra ci sono i 16 led; infine i pulsanti OFF e ON.

Figure 16 : foto scheda di sviluppo Samsung s3c2440

Pulsanti OFF e ON Controller e

connettore Ethernet

Chip e connettore RS232

Led e switch

Pulsanti connessi al port I/O

USB client

USB host

CODEC IIS amplificatore speaker

Samsung s3c2440A

Pulsante RESET

Flash

SDRAM

Alimentatore 12V

29

5.1 Schema a blocchi Come dovrebbe ormai essere chiaro, la scheda è progettata intorno al SoC Samsung e l’elettronica esterna di comando delle periferiche è minimale; i chip digitali presenti sono il codec IIS e il controller Ethernet; tutto il resto è elettronica discreta, di alimentazione o analogica. In Figure 17 lo schema a blocchi della scheda in cui si vedono i diversi bus che collegano i componenti del sistema.

128MBNAND FlashK9F1G08U0B

S3C2440A(Based ARM920T)

EthernetSocket

USBDevice

USBHost

LEDsMicroswitches

IIS

StatusLED

IrD A

UARTch-1

UARTch-2

UAR Tch-0

SD

LCDController

JTAG Port

Touch panel

64 MB SDRAMx2 K4S561632

MemoryController

Clock

UD A1341

JTAGto Parallel

MMC/SD/SDIOCard

16.9344Mhz 32.768Khz

NAND FlashController

USB hostconnector

LAN9115

UART 3(TTL)

LCD TFT800x480SamsungMS700KF05

Ext.INT

LEDs

Switches

XTAL

Audio C odec

ARM920T

DATA Memory(SRAM 4KB)

DMA, RTC, Interrupt Controller

S3C2440A

USB clientconnector

x16

x16

x2

UART 2(RS232)

EthernetController

Figure 17 : schema a blocchi della scheda di sviluppo SERP

I bus presenti sulla scheda sono:

• il bus dati/indirizzi che collega al processore la memoria SDRAM, i led e i microswitch ed il controller Ethernet;

• un bus NAND Flash; • un bus MMC/SD/SDIO; • il bus IIS; • due bus USB (host e device); • due seriali; • ed un bus JTAG; • un bus LCD.

30

5.2 SDRAM, Led e MicroSwitch, controller Ethernet Questa scheda dispone di diversi bus digitali; di vitale importanza è il bus di memoria in cui si trova la memoria principale (RAM); l’intero bus ha una capacità di indirizzamento di 1GB. Su questo bus è installata la memoria RAM, i led e i Microswitch e il controller Ethernet.

ADDR2ADDR3ADDR4ADDR5ADDR6ADDR7ADDR8ADDR9ADDR10ADDR11ADDR12ADDR13ADDR14

DATA0DATA1DATA2DATA3DATA4DATA5DATA6DATA7DATA8DATA9DATA10DATA11DATA12DATA13DATA14DATA15

BA0BA1

ADDR6ADDR7ADDR8

ADDR2

BA0

ADDR9ADDR10

ADDR3ADDR4

ADDR11

ADDR5

ADDR12ADDR13ADDR14

BA1

DATA16DATA17DATA18DATA19DATA20DATA21DATA22DATA23DATA24DATA25DATA26DATA27DATA28DATA29DATA30DATA31

DATA[0..31][1,5,7]

ADDR[0..26][1,7]

nWB E 0[2,7]nWB E 1[2,7]

SCKE[2]SCLK0[2]

nGCS 6 [2]nSRA S [2]nSCA S [2]nW E [1,7] SCKE[2]

nSRA S [2]nGCS 6 [2]

nWB E 3[2,7]nWB E 2[2,7]

nW E [1,7]nSCA S [2]

SCLK1[2]

3.3V 3.3V

256M

bS

DR

AM

U32324252629303132333422

2021

1539

3837

245781011134244454748505153

39

284154

6124652

4349

19181716

3536

11427

A0A1A2A3A4A5A6A7A8A9A10

BA0BA1

LDQMUDQM

SCLKSCKE

DQ0DQ1DQ2DQ3DQ4DQ5DQ6DQ7DQ8DQ9

DQ10DQ11DQ12DQ13DQ14DQ15

VDDQ0VDDQ1

VSS0VSS1VSS2

VSSQ0VSSQ1VSSQ2VSSQ3

VDDQ2VDDQ3

nSCSnSRASnSCAS

nWE

A11A12

VDD0VDD1VDD2

256M

bS

DR

AM

U22324252629303132333422

2021

1539

3837

245781011134244454748505153

39

284154

6124652

4349

19181716

3536

11427

A0A1A2A3A4A5A6A7A8A9A10

BA0BA1

LDQMUDQM

SCLKSCKE

DQ0DQ1DQ2DQ3DQ4DQ5DQ6DQ7DQ8DQ9

DQ10DQ11DQ12DQ13DQ14DQ15

VDDQ0VDDQ1

VSS0VSS1VSS2

VSSQ0VSSQ1VSSQ2VSSQ3

VDDQ2VDDQ3

nSCSnSRASnSCAS

nWE

A11A12

VDD0VDD1VDD2

Figure 18 : schema di connessione dei due banchi di memoria SDRAM

Ci sono due chip da 32MB (ovvero 256Mb) l’uno; questi sono connessi in modo tale che dei 32 bit di ciascun word i primi 16bit vengono memorizzati sul primo chip e gli altri 16bit sull’altro. Utilizzando questa configurazione elettronica è come se si fosse collegato al SoC un'unica memoria SDRAM da 64MB. La SDRAM perciò occupa un singolo banco dello spazio di memoria: il 6. Si troverà perciò memoria RAM dall’indirizzo 0x30000000 all’indirizzo 0x33FFFFFF. La memoria SDRAM è indirizzata a byte e il bus dati è a 32bit. Su questo bus si trovano mappati anche i LED e i Microswitch; questi rispondono al Chip Select (banco) 5, dispongono di uno spazio indirizzabile di 128MB, ma ne utilizzano solo 2 byte. Allo stesso indirizzo 0x28000000, con una capacità di indirizzamento a word (16bit) , si trovano sia i LED che i Microswitch: utilizzando una operazione di load si leggono i Microswitch, mentre con un operazione di store si impostano i LED, accendendoli oppure spegnendoli. Sempre sullo stesso bus si trova il controller Ethernet che risponde questa volta al Chip Select 3, Il suo spazio di indirizzi va perciò da 0x18000000 a 0x1FFFFFFF. L’Ethernet controller è un SMSC LAN9115-MT. In Figure 19 si vede come si presenta il bus di memoria dopo l’avvio di UBOOT, che configura i registri del memory controller in modo tale che il SoC sappia che ci sono 64MB di SDRAM. E’ questo il mapping fisico dei dispositivi sul bus.

31

0x0000_0000

0x0800_0000

0x1000_0000

0x1800_0000

0x2000_0000

0x2800_0000

0x3000_0000

0x3800_0000

0x4000_0000

SROM/SDRAM(nGCS7)

SDRAM 64MB(nGCS6)

LEDs Microswitches(nGCS5)

SROM(nGCS4)

Ethernet Controller(nGCS3)

SROM(nGCS2)

SROM(nGCS1)

Boot InternalSRAM (4KB)

128MB

128MB

128MB

128MB

128MB

128MB

64MB

1GBHADDR[29:0]

AccessibleRegion

OM[1:0] = 00

[ Using NAND flash for boot ROM ]

128MB

Figure 19 : address space dell’External Memory Controller della scheda SERP

Tutti i dispositivi sul bus di memoria possono esser letti o scritti attraverso operazioni di accesso alla memoria, cioè load e store. Queste operazioni vengono effettuate attraverso operazioni sul bus esterno. Tutti gli altri dispositivi, presentati nel seguito, sono fisicamente situati nel SoC stesso; questi vengono utilizzati sempre attraverso operazioni di accesso alla memoria, ma le letture e/o scritture non vengono direttamente presentate su un bus esterno al chip se non previa conversione in qualche altro formato o protocollo (ad esempio il controller NAND Flash utilizza il bus e protocollo di comunicazione NAND Flash).

5.3 NAND Flash La scheda monta una NAND Flash da 128MB su cui risiede il boot loader (UBOOT), il kernel Linux (2.6.18) ed un File System UNIX. La Flash è partizionata in cinque parti:

PARTIZIONE START ADDR END ADDR SIZE UBOOT 0x0000_0000 0x0004_0000 256 kB UBOOT env 0x0004_0000 0x0006_0000 128 kB Kernel Linux 0x0006_0000 0x0026_0000 2 MB Splash 0x0026_0000 0x0030_0000 640 kB File System 0x0030_0000 0x0800_0000 125 MB

32

La maggior parte dello spazio disponibile sulla Flash è utilizzato dal FileSystem di Linux. La NAND Flash è utilizzata come dispositivo di boot del dispositivo attraverso la connessione fisica a massa o a livello alto di alcuni pin del SoC, che permettono anche il settaggio del tipo di blocchi di cui e fatta la Flash. Dopo il power on, appena il chip esce dal reset, vengono copiati i primi 4kB della NAND Flash in una piccola RAM interna da 4kB, mappata all’indirizzo di memoria 0x0000_0000. Quindi il processore esegue l’istruzione all’indirizzo 0x0000_0000, ove si trova il codice del bootloader U-BOOT.

5.4 MMC/SD/SDIO Sulla scheda di sviluppo, sul lato sinistro, nella parte inferiore della Motherboard, c’è un connettore MMC/SD/SDIO: questo permette il riconoscimento dell’inserimento di una scheda e se è impostata o meno la protezione in scrittura. Tutta la logica di controllo è sul SoC. I registri di configurazione di questa periferica sono all’indirizzo fisico 0x5A00_0000.

5.5 Il CODEC IIS Il codec audio UDA1341 converte una fonte audio digitale IIS in formato analogico. La scheda offre sia un connettore, J14, in cui si trova una linea mono in ingresso (microfono) e due linee stereo, una di ingresso e una di uscita; sia un connettore J9 per connettere un piccolo speaker la cui uscita è data dalla sovrapposizione dell’uscita stereo, il cui segnale è anche amplificato da un modesto amplificatore di potenza LM4871. La scheda ha un piccolo speaker nascosto sul sostegno inferiore del supporto (vedi Figure 20 : speaker e connettore MMC/SD/SDIO scheda SERP). Il controller del Codec IIS, sul SoC, è accessibile all’indirizzo fisico 0x5500_0000.

Figure 20 : speaker e connettore MMC/SD/SDIO scheda SERP

33

5.6 USB Connectors La logica elettronica del controller USB host e client risiede completamente sul SoC: sulla Motherboard ci sono solo i connettori USB. Come host USB la Motherboard fornisce i +5 Volt necessari all’alimentazione dei dispositivi che vengono connessi. Essendoci un solo connettore, sarà necessario connettere un hub USB, preferibilmente alimentato, per collegare più di un dispositivo. I controller USB host e device risiedono rispettivamente all’indirizzo fisico 0x4900_0000 e 0x5200_0000.

5.7 Seriali (UARTs) Il SoC implementa 3 porte seriali; tutte possono diventare porte seriali ad infrarossi; non c’è né nessuna però di questo tipo sulla scheda di sviluppo. Bensì c’è la logica per disporre dei livelli elettrici RS232, la porta con il connettore RJ12, vicino al connettore Ethernet, e la logica per i livelli elettrici TTL (connettore sotto la porta USB host, Figure 21).

Figure 21 : scheda SERP connettori frontali e switch

I segnali che arrivano alle porte sono TX, RX, CTS e RTS: una connessione full null modem non è perciò possible (è il SoC a non supportarla); una connessione full null modem richiede infatti anche altre linee di controllo. Il controller delle porte seriali (UARTs) è mappato all’indirizzo fisico 0x5000_0000.

5.8 JTAG Il protocollo JTAG viene più largamente utilizzato in ambiti elettronici: ai primordi permetteva la verifica dei collegamenti su un circuito stampato, ora viene utilizzato per il debugging a basso livello. La scheda è predisposta al collegamento con questa interfaccia e l’azienda produttrice fornisce un semplice cavetto JTAG to Parallel Port usato per scrivere sulla NAND flash il bootloader; più precisamente è possibile scrivere e leggere qualsiasi parte della flash; i tempi di lettura e scrittura sono piuttosto lunghi a causa della latenza della porta parallela.

5.9 LCD TFT Samsung LMS700KF05 L’LCD Samsung montato sulla scheda integra, oltre alla logica di controllo del display, il touch screen e i LED back light. Questo display supporta un’unica risoluzione di 800x480 con una diagonale di 7.0 pollici e una profondità massima di 16,7 milioni di colori (24bit).

34

5.10 Mappa di Memoria della scheda

Not Used0xFFFF_FFFF

0x6000_0000

0x4800_0000

0x4000_0000

SFR Area

Not Used

0x3800_0000

SROM/SDRAM(BANK7, nGCS7)

0x3000_0000

SDRAM 64MB(BANK6, nGCS6)

LEDs Microswitches(BANK5, nGCS5)

0x2000_0000

0x2800_0000

0x1000_0000

0x1800_0000

0x0800_0000

SROM(BANK4, nGCS4)

Ethernet Controller(BANK3, nGCS3)

SROM(BANK2, nGCS2)

SROM(BANK1, nGCS1)

BootSRAM (4KB)0x0000_0000

OM[1:0] = 00

MemoryController

USB HostController

InterruptController

DMA

ClockPower Management

LCD

NANDFlashController

Camera

UARTs

PWM Timers

USBDeviceController

Watchdog

IICController

IISController

I/OPorts

RTC

ADCTouch Screen

SPI

MMC/SD/SDIO

AC97

0x4800_0000

0x4900_0000

0x4A00_0000

0x4B00_0000

0x4C00_0000

0x4D00_0000

0x4E00_0000

0x4F00_0000

0x5000_0000

0x5100_0000

0x5200_0000

0x5300_0000

0x5400_0000

0x5500_0000

0x5600_0000

0x5700_0000

0x5800_0000

0x5900_0000

0x5A00_0000

0x5B00_0000

0x5C00_0000

0x3400_0000Not Used

Figure 22 : mappa di memoria della scheda

5.11 Firmware e Software Prima di chiudere il capitolo è necessario qualche accenno al firmware ed al software presente sulla scheda. La scheda di sviluppo è equipaggiata con un bootloader, UBOOT, e con il sistema operativo Linux. Non appena il processore si avvia, il primo codice che viene eseguito è appunto il bootloader UBOOT. Questo firmware è un po’ più ricco di un semplice bootloader; infatti, oltre ad inizializzare la memoria e le periferiche del SoC e a provvedere eventualmente al caricamento di Linux, mette a disposizione un’interfaccia tipo prompt, ricca di comandi.

35

Grazie a questi comandi è possibile esplorare e modificare i contenuti dei registri e delle locazioni di memoria fisica, leggere MMC/SD card, scrivere e leggere la memoria flash e in generale fare dei piccoli esperimenti per prendere dimestichezza con la schedina di laboratorio, senza necessariamente scrivere un programma. Per interagire con il prompt di UBOOT è necessario solo avere sull’host un programma che permette di interfacciarsi con la porta seriale, collegare il cavetto seriale tra l’host e il target e dopo aver dato l’alimentazione alla scheda è necessario bloccare l’avvio di Linux premendo un tasto qualsiasi. Alcuni comandi di UBOOT verranno utilizzati nel testo.

36

6 SVILUPPO DEL SOFTWARE Lo scopo di questo capitolo è di descrivere l’ambiente e gli strumenti di sviluppo software, adottati per la scheda basata su processore ARM920T Samsung s3c2440 (target), ospitati sull’host computer. Dopo una breve introduzione agli applicativi che ci permettono di trasformare un programma scritto in linguaggio assembly in un codice eseguibile dalla CPU, segue una descrizione dei comandi che si devono invocare per costruire questo codice eseguibile.

6.1 Sviluppo di software nativo Quando si sviluppa un nuovo programma si è soliti operare sullo stesso computer e sistema operativo, sia per scrivere il codice sorgente, sia per provare l’eseguibile. L’insieme dei tool utilizzati per compilare il codice sorgente produce codice eseguibile per la stessa macchina su cui viene invocato. Per esempio lavorando su un Personal Computer x86 a 32bit, con sistema operativo Linux, si scrive un semplice programma in linguaggio C utilizzando vi o emacs, e, richiamando gcc, viene creato un eseguibile in formato ELF x86 a 32bit per Linux. Questo tipo di sviluppo si dice “nativo”: si crea un programma che viene eseguito sulla stessa macchina su cui viene compilato. Il compilatore e gli strumenti utilizzati vengono quindi detti nativi. Sviluppo di software nativo 1. host scrivo i file sorgente in un linguaggio di

programmazione; 2. host compilo i sorgenti in un eseguibile; 3. host eseguo il programma compilato.

6.2 Cross Development Nello sviluppo di software embedded non è possible compilare nativamente perché spesso la macchina target ha poca memoria oppure capacità di calcolo limitata. In questo caso si parla di “cross development”: si utilizzano dei tool di compilazione che creano del codice che verrà eseguito su architetture e sistema operativo diversi rispetto alla macchina su cui viene compilato. In questo caso si parla di cross compilatore, di cross assemblatore oppure cross linker etc. .

37

La compilazione di un codice sorgente avviene seguendo gli stessi passi di una compilazione nativa, ma gli applicativi utilizzati hanno un nome diverso che palesa sia lo strumento che l’architettura target per cui creano il codice eseguibile. La compilazione avviene sull’host; successivamente l’eseguibile verrà trasferito sul target e lì eseguito. Il sistema di sviluppo presentato in questo capitolo, è una GNU Cross Development Toolchain x86 32bit per target ARM ELF 32bit, disponibile sul CD di distribuzione del materiale di laboratorio in versione per Linux e per Microsoft Windows. Cross Development 1. host scrivo i file sorgente in un linguaggio di

programmazione; 2. host cross compilo i sorgenti in un eseguibile; 3. host trasferisco l’eseguibile sul target; 4. target eseguo il programma compilato.

6.3 GNU toolchain Gli strumenti di sviluppo disponibili in laboratorio, ovvero compilatore, assemblatore, linker etc. fanno parte dei tool della famiglia GNU (www.gnu.org) contenuti nella GNU toolchain. I tool di sviluppo GNU sono una e vera e propria suite di programmi per costruire nuovi eseguibili: non ci sono infatti solo compilatore, assemblatore e linker, ma anche una serie di utilità per l’analisi dei file creati e per la loro conversione in diversi formati eseguibili; è inoltre possibile creare librerie statiche e dinamiche. Gli applicativi della GNU toolchain girano su diversi sistemi operativi e diverse architetture e possono altresì creare codice eseguibile per diversi sistemi operativi e diverse architetture. Per esempio è possibile facilmente reperire in rete una toolchain con compilatore C/C++ per architettura x86 sotto Linux, Darwin, Windows e QNX che compila il codice sorgente per essere eseguito su un SO diverso (infatti un programma per Windows non viene eseguito nativamente1 in Linux). Una GNU toolchain può quindi compilare codice per un’architettura diversa rispetto a quella su cui i suoi tool vengono eseguiti. Ad esempio è possibile assemblare un sorgente scritto in ARM assembly su un Personal Computer x86 Linux (o Windows). Questa è esattamente la configurazione presa in considerazione in questa guida e che sarà approfondita in questo capitolo. Da qui in poi con GNU toolchain si indicherà esclusivamente una GNU Cross Development Toolchain. Il sistema di sviluppo delle schede dell’Università di Padova è disponibile sia per Sistema Operativo Linux che per Microsoft Windows XP su architetture x86 a 32bit (funziona anche su macchine a 64bit). Questi due sistemi sono perciò i due possibili sistemi host. Il sistema target, per cui questi tool producono il codice è il processore ARM920T, il cuore del Samsung s3c2440. Dovrebbe essere chiaro a questo punto che gli eseguibili prodotti utilizzando gli applicativi di cross compilazione cross non possono essere eseguiti sull’host (a meno di utilizzo di emulatori o macchine virtuali) ma solo sul sistema target.

1 Un eseguibile WIN32 in formato Portable Executable (PE) può venire eseguito in Linux utilizzando per esempio Wine: si vuole qui focalizzare il fatto che Sistemi Operativi diversi richiedono una diversa compilazione dell’oggetto di codice eseguibile, anche se operano su una stessa architettura.

38

La GNU Cross Development Toolchain x86 32bit per target arm-elf- offre i seguenti strumenti necessari allo sviluppo:

archiver arm-elf-ar assembler arm-elf-as C preprocessor arm-elf-cpp C++ compiler arm-elf-g++ C compiler arm-elf-gcc profiler arm-elf-gcov debugger (command line) arm-elf-gdb debugger (ncurses) arm-elf-gdbtui debugger (Tcl/Tk) arm-elf-insight linker arm-elf-ld list symbols from object files arm-elf-nm copy and translate object files arm-elf-objcopy display information from object files arm-elf-objdump generate index to archive arm-elf-ranlib display information about ELF files arm-elf-readelf ARM simulator arm-elf-run list section size and total size arm-elf-size print strings of printable chars in file arm-elf-strings discard symbols from object files arm-elf-strip

Ogni strumento della suite di compilazione ha un nome prefisso da una medesima stringa che permette di riconoscere per che sistema crea il codice. Negli esempi che seguono il compilatore C gcc si chiamerà: arm-elf-gcc (in Linux) arm-elf-cc.exe (in MS Window).

E’ necessaria un’ultima nota sulla toolchain adottata: gli eseguibili che si ottengono non presuppongono l’esistenza di un sistema operativo per funzionare. E’ un particolare non banale perché un’applicazione per un sistema operativo non può essere eseguita senza di esso. I programmi di esempio che propone questa guida non vengono eseguiti all’interno di un Sistema Operativo si dicono perciò standalone e la GNU toolchain scelta permette appunto di crearli. Non sarà quindi possibile utilizzare tutte le chiamate a sistema che si è abituati ad usare, a partire da printf (println in Pascal).

arm-elf-gcc

il compilatore è per target ARM

produce codice rilocabile tipo “ELF”

questo è il compilatore C

39

6.4 Executable Formats Un programma può essere scritto in un linguaggio a basso livello, assembly, in un linguaggio ad alto livello, per esempio C, oppure in linguaggio macchina. Solo un programma in linguaggio macchina può essere direttamente interpretato dal processore e perciò, viene chiamato modulo eseguibile (o semplicemente, eseguibile). Un eseguibile non contiene solo istruzioni per il processore, ma anche diversi tipi di informazioni, utili per la sua esecuzione. Infatti un programma è un insieme di codice macchina e dati; per tenere separati questi due tipi di informazioni, un eseguibile è suddiviso in diversi segmenti in cui si possono trovare codice oppure dati. L’informazione su questi diversi segmenti deve però essere mantenuta da qualche parte all’interno del modulo eseguibile; per questo motivo è necessario che i moduli eseguibili abbiano una precisa formattazione e che tutte le informazioni sui suddetti segmenti siano mantenute in una struttura detta header (all’inizio del file). Essenziale sarà l’informazione su dove è la prima istruzione da eseguire. Esistono diversi formati per gli eseguibili, i più famosi sono senza dubbio il Portable Executable (PE) adottato nei prodotti Microsoft e l’Executable and Linkable Format (ELF) diffuso nei sistemi UNIX, vedi Figure 23. Un eseguibile in formato PE o ELF per poter essere eseguito ha bisogno di un ulteriore programma che ne legga l’header, riconoscendo le varie aree e, allocando per ognuna di queste l’adeguata memoria all’indirizzo corretto, settandone a volte i diritti di scrittura/lettura o esecuzione, crei un nuovo processo, ospitante il programma e che infine passi l’esecuzione alla prima istruzione del programma stesso. Il bootloader UBOOT, che si trova sulle schede con processore Samsung s3c2440, permette di caricare in memoria ed eseguire eseguibili in formato ELF e raw binary. Un eseguibile in formato raw binary (semplicemente binario) viene prodotto facendo un dump della memoria in cui è stato caricato un eseguibile in formato ELF: tutte le informazioni su gli indirizzi, le grandezze e le proprietà delle diverse aree in cui è diviso vengono perse. Sia l’indirizzo di memoria a cui questo binario deve essere collocato sia l’indirizzo di memoria della prima istruzione da eseguire non ci sono più; è il programmatore che è tenuto a conoscere queste informazioni.

...

.data

.rodata

.text

Program header table

ELF header

Section header table

{{

Figure 23 : ELF file format

Il firmware UBOOT permette anche il download via seriale di binari in formato S-record.

40

6.5 L’Ambiente di Sviluppo Questo testo assume che, per lo sviluppo del codice proposto, il lettore non utilizzi un Integrated Development Environment (IDE) come ad esempio Eclipse o Microsoft Visual Studio. In questo documento non ci si soffermerà su come configurare ed utilizzare tali ambienti perché spesso questi sofisticati strumenti nascondono all’utilizzatore i procedimenti di compilazione e linking dei programmi creati. Questa guida vuole invece focalizzare l’attenzione del lettore proprio su queste procedure. Un file sorgente in linguaggio assembly, ma anche in C, può essere redatto con un qualsiasi editor di testo su qualsiasi sistema operativo (Windows, o Linux, o altri). In Microsoft Windows si può utilizzare l’Editor MS-DOS (edit.com) il semplice Notepad (Blocco Note), il più avanzato WordPad, ma anche Microsoft Office Word, avendo però cura di salvare il file in formato testo (*.txt); editor più evoluti, non necessariamente commerciali, si possono trovare in rete, ad esempio Bloodshell DevC/C++, CodeBlocks, Eclipse etc. . Sviluppare in Linux può però risultare più agevole perché anche un semplice editor grafico, come kwrite, kate, gedit, offre la colorazione delle istruzioni anche in linguaggio assembly ARM. Senza dubbio altri editor più famosi, come emacs e vi o vim, che offrono però molte più funzionalità dei precedenti, e risultano al neofita un po’ più complessi, vanno benissimo per la stesura del codice. Anche in Linux esistono molti editor open source per lo sviluppo; Eclipse senza dubbio è un ottimo ambiente: scaricando il C/C++ Development Toolkit (CDT) di Eclipse è facilmente reperibile online un addon che permette anche lo sviluppo con le istruzioni ARM correttamente colorate. Una volta scritto il codice sorgente è necessario passare alla sua compilazione. Per la compilazione, dato che i tool GNU sono degli applicativi che funzionano a riga di comando, è necessario aprire un Terminale oppure una Shell in Linux, mentre in Windows si avvia il Prompt dei comandi. A questo punto si può procedere alla compilazione e al linking del codice di esempio.

Figure 24 : cygwin prompt

In Microsoft Windows per utilizzare gli strumenti della GNU è consigliato installare CYGWIN (www.cygwin.org) che “trasforma” Windows in una sistema UNIX compatibile, rendendo disponibili agli strumenti della GNU un ambiente più naturale al loro utilizzo. In questo modo invece di operare mediante il Prompt dei comandi si avrà a disposizione anche in Microsoft Windows una Shell o Terminale UNIX, come in Figure 24.

41

CYGWIN permette di emulare l’ambiente UNIX in sistemi operativi tipo WIN32 e basa il suo funzionamento su una libreria DLL (cygwin1.dll), la quale emula le chiamate ad un generico sistema operativo UNIX like. Una seconda parte di CYGWIN sono i programmi, applicativi che di solito si trovano in Linux, compilati però perché girino in CYGWIN/Windows. Nel suo insieme CYGWIN può essere visto come una distribuzione degli applicativi Linux in Windows. In questo ambiente è possibile sviluppare software che fa uso delle API standard WIN32 e/o delle API di CYGWIN; questo aspetto permette il “porting” di software sviluppato in Linux o, più in generale, in UNIX.

6.6 Compiling and Assembling sources In questa sezione si parlerà di moduli sorgente, di moduli oggetto e di moduli eseguibili, i quali sono contenuti (o sono destinati ad essere prodotti) in altrettanti file. Talvolta si parlerà di file sorgente, di file oggetto e di file eseguibile, intendendo riferirsi ai moduli in essi contenuti. Vengono ora descritti i diversi passi necessari per compilare un modulo sorgente scritto in un linguaggio ad alto livello, ad esempio in C. Questi passi coinvolgeranno l’utilizzo dell’assemblatore, su cui ci si soffermerà, perché servirà per trasformare in eseguibili gli esempi proposti per il laboratorio di Architettura degli Elaboratori. Normalmente quando si invoca il compilatore C (arm-elf-gcc), questo eseguirà ordinatamente il preprocessing, la compilazione, l’assembling ed il linking, creando un unico file eseguibile. Perciò, specificando un file sorgente scritto in un linguaggio ad alto livello, dopo una serie di fasi intermedie automatizzate, il compilatore produce l’eseguibile. Queste fasi possono però essere eseguite anche ad una ad una. In particolare, un programma in linguaggio C, viene prima preprocessato, poi convertito in linguaggio assembly e viene poi trasformato in codice macchina (Figure 25). Il risultato della conversione da linguaggio assembly a codice macchina è detto codice oggetto (o modulo oggetto). Il modulo oggetto è costituito, come un modulo eseguibile da diverse sezioni e da uno o più strutture dati che esplicitano la sua organizzazione interna.

Preprocessor C Compiler Linker

test.c test.i test.o a.out

0100101…

./a.out

Assembler

test.s

Preprocessor C Compiler Linker

test.c test.i test.o a.out

0100101…

./a.out

0100101…

./a.out

Assembler

test.s

Figure 25 : passi di compilazione in linguaggio C Un banale programma scritto in C può avere la seguente forma: /* somma.c */ int main (int argc, char* argv[]) { int a, b; int c; a = 10; b = 15; c = a + b; return c; }

42

Ciò che qui interessa è come avviene il processo di trasformazione da modulo sorgente ad eseguibile, non cosa fa il programma. Per ottenere da questo un eseguibile (per il target), aperta una shell nella directory ove è situato il file (che si chiama somma.c) contenente il modulo sorgente, si impartisce il comando: # arm-elf-gcc -o somma somma.c Questo comando crea un file eseguibile chiamato somma nella directory corrente. Nel caso ci si voglia fermare dopo il preprocessing: # arm-elf-gcc -E somma.c > somma.i Il preprocessore, un tool che serve basilarmente per sostituire pezzi di codice, non fa output su file ma su standard output (cioè sul terminale): ciò giustifica l’utilizzo del carattere ‘>’. Per fermare la compilazione al file oggetto: # arm-elf-gcc -c somma.c Questo comando crea un file con lo stesso nome di quello in ingresso ma con estensione *.o. Per fermare invece la compilazione al file assembly: # arm-elf-gcc -S somma.c Anche questo crea un file con lo stesso nome di quello in ingresso ma con estensione *.s. Un file assembly generato da un sorgente in linguaggio C contiene molte istruzioni, variabili e in generale nomi, di cui un programma scritto manualmente in assembly non necessita; risulta perciò di gran lunga più complesso. Ecco come appare il file tradotto in assembly: .file "somma.c" .text .align 2 .global main .type main, %function main: @ args = 0, pretend = 0, frame = 20 @ frame_needed = 1, uses_anonymous_args = 0 mov ip, sp stmfd sp!, {fp, ip, lr, pc} sub fp, ip, #4 sub sp, sp, #20 str r0, [fp, #-28] str r1, [fp, #-32] mov r3, #10 str r3, [fp, #-24] mov r3, #15 str r3, [fp, #-20] ldr r2, [fp, #-24] ldr r3, [fp, #-20] add r3, r2, r3 str r3, [fp, #-16] ldr r3, [fp, #-16] mov r0, r3 sub sp, fp, #12 ldmfd sp, {fp, sp, pc} .size main, .-main .ident "GCC: (GNU) 4.0.2"

43

Tutto ciò che è in più, rispetto alla banale addizione che si voleva ottenere, serve per l’inizializzazione dello stack, il salvataggio dei registri influenzati dall’esecuzione del codice e l’allocazione delle variabili (sullo stack). La computazione che si voleva ottenere si può ridurre alle seguenti istruzioni: .text .global _start _start: .global add1_func add1_func: mov r0, #10 mov r1, #15 add r2, r1, r0 add1_end: b add1_end Rispetto al codice assembly prodotto dal compilatore C, si sono usati registri diversi (la somma viene prodotta in r2), si è poi aggiunto il label _start (che indica al linker la prima istruzione da eseguire) si è aggiunto il label add1_func (che individua l’inizio della “funzione” che verrà eseguita), i due label sono dichiarati .global (per renderli visibili all’esterno del codice) e inoltre si è aggiunta l’ultima istruzione (che ha lo scopo di “intrappolare” l’esecuzione del processore dopo che ha eseguito le tre istruzioni precedenti). Si può provare ad assemblare queste poche righe di codice inserite in un file chiamato add1.s. Per assemblare questo file si invoca il GNU assembler: # arm-elf-as -o add1.o add1.s In questo modo si ottiene un modulo oggetto nel file add1.o nella directory in cui si sta lavorando. Di seguito, per poter fare il debugging del codice, si aggiungerà un argomento al comando, per includere nel modulo oggetto i simboli di debugging (--gdwarf-2).

6.7 Linking Il linker è lo strumento che, presi in ingresso un certo numero di moduli oggetto, crea un singolo modulo eseguibile in un particolare formato. Il linker della toolchain utilizzata permette di produrre in uscita solo file in formato ELF. Se l’applicazione che si sta sviluppando è costituita da più moduli oggetto, ognuno dei quali sarà a sua volta composto da diverse sezioni, è il linker che aggiusta tutte queste sezioni tra loro in modo che se una funzione, che si trova in uno specifico modulo, richiama una funzione in un altro modulo, la chiamata possa avvenire correttamente. Il formato ELF non prescrive come le varie sezioni debbano essere sistemate nel file eseguibile e neppure come debbano essere sistemate in RAM quando il programma viene caricato in memoria: se non vengono specificati, il linker utilizza una impostazione di default. Le impostazioni del formato della memoria vengono di solito inserite in un file chiamato ldscript. Le più importanti sezioni in cui si divide un modulo eseguibile sono:

• text contiene le istruzioni macchina del processore; • data contiene dati inizializzati; • bss contiene dati non inizializzati;

44

Gli esempi proposti in questa guida sono stati scelti in modo tale che possano essere provati senza un sistema operativo, sfruttando la possibilità di accedere agli indirizzi fisici della scheda di sviluppo, senza passare per il meccanismo di Memory Mapping. Per questo motivo i programmi di esempio verranno eseguiti dopo l’avvio del bootloader UBOOT, che è il primo programma ad essere eseguito sulla scheda e che ne configura la memoria e le periferiche. Pertanto tutti gli indirizzi di memoria, negli esempi, sono indirizzi fisici. Come esposto nel capitolo dedicato alla scheda, la RAM inizia all’indirizzo fisico 0x3000_0000. Tutte le sezioni del file eseguibile, una volta in memoria, dovranno essere collocate dall’indirizzo 0x3000_0000 in poi. Questo è importantissimo, indipendentemente dal formato eseguibile scelto2. Dato che la prima sezione ad essere posizionata in memoria nella maggior parte dei casi è la sezione di codice eseguibile (.text) è sufficiente configurare il suo indirizzo: tutte le altre seguiranno a catena. Il modo più semplice per creare un file eseguibile, specificando l’indirizzo della RAM è quindi, continuando l’esempio sopra: # arm-elf-ld -Ttext 0x30000000 -o add1 add1.o Questo comando crea un file eseguibile di nome add1 (in Windows add1.exe) in formato ELF per target ARM (32bit) che posizionerà le sue sezioni in memoria a partire dall’indirizzo 0x3000_0000. E’ possibile specificare anche l’indirizzo delle altre sezioni, utilizzando la sintassi –Ttext ADDRESS, -Tdata ADDRESS e –Tbss ADDRESS, ma ciò non è necessario nel caso della toolchain utilizzata. Se si affrontano progetti di una certa complessità è invece il caso di utilizzare un ldscript che definisca il posizionamento delle varie sezioni. Solo a scopo informativo ne viene qui di seguito fornito un esempio utilizzabile nell’ambito del corso. OUTPUT_FORMAT("elf32-littlearm”) OUTPUT_ARCH(arm) ENTRY(_start) SECTIONS { . = 0x30000000; .text : { *(.text) } .data : { *(.data) } .bss : { *(.bss) } .stack 0x80000 : { _stack = .; *(.stack) } } Questo script offre un ultimo spunto: da dove inizia il codice in un programma scritto in linguaggio assembly? Un programmatore C risponderebbe senza indugiare che inizia dalla funzione ‘main’. 2 Solo se si produce l’eseguibile con codice rilocabile non è necessario sapere dove verrà posizionato per l’esecuzione.

45

E’ lo script del linker che con la direttiva ENTRY imposta il nome simbolico (label), trasformato poi in un indirizzo, da cui ogni programma compilato con quello script inizierà; questa label dovrà essere visibile esternamente all’oggetto, va perciò dichiarata .global. Nello script di esempio il linker compilerà l’eseguibile scrivendo nel suo header che la prima istruzione da eseguire è quella che si trova all’indirizzo contrassegnato dalla label _start.

6.8 Dissecting and Exploring Executables Prima di passare all’esposizione su come eseguire e “debuggare” un applicazione creata, è utile dare uno sguardo ad alcuni strumenti (utilities), che servono per analizzare come sono fatti internamente gli eseguibili e come si possono convertire in diversi formati.

6.8.1 Disassembling Uno dei principali tool del Reversing Engineering è senza dubbio il disassemblatore, che permette di ricostruire in linguaggio assembly il codice binario generato. Con questo comando cioè si ricostruiscono le istruzioni assembly simboliche, a partire dal codice binario del modulo eseguibile. Per decompilare un eseguibile è sufficiente utilizzare l’utility ‘objdump’ nel seguente modo: # arm-elf-objdump –d add1 > add1.asm Il contenuto del file add1.asm ottenuto da questo comando è: add1: file format elf32-littlearm Disassembly of section .text: 30000000 <_start>: 30000000: e3a0000a mov r0, #10 ; 0xa 30000004: e3a0100f mov r1, #15 ; 0xf 30000008: e0812000 add r2, r1, r0 3000000c <add1_end>: 3000000c: eafffffe b 3000000c <add1_end> Esaminando il codice disassemblato è possibile ricavare alcune informazioni su ciò che si è ottenuto: la colonna di sinistra (contenente gli indirizzi di memoria) consente di constatare subito che il codice è effettivamente allocato a partire dall’indirizzo 0x3000_0000. Se al posto dell’argomento -d si usasse -D il disassemblatore interpreterebbe come istruzione i dati di tutte le sezioni dell’eseguibile, anche ciò che istruzione non è.

6.8.2 Lista dei simboli e delle sezioni di un eseguibile Un ulteriore strumento di analisi consente la visualizzazione dei simboli contenuti nel file eseguibile. Ciò si ottiene mediante il seguente comando: # arm-elf-nm add1 > add1.sym

46

Il risultato è contenuto nel file add1.sym. Nel caso il programma di esempio venisse compilato con le informazioni di debug (--dwarf-2), in questo file si possono trovare, oltre all’indirizzo delle sezioni in cui l’eseguibile è diviso, anche tutti i simboli di debugging (ad esempio il nome ed il percorso dei file sorgente). Un ulteriore tool, che fornisce la dimensione in byte delle varie sezioni contenute nel modulo eseguibile: # arm-elf-size add1 > add1.size

6.8.3 Format Conversion Per convertire un eseguibile da un formato all’altro il tool da utilizzare è ‘objcopy’. Continuando sempre con lo stesso esempio, per ottenere l’eseguibile in formato binario: # arm-elf-objcopy -O binary add1 add1.bin Se invece si vuole convertire l’eseguibile ELF in formato S-record: # arm-elf-objcopy -O srec add1 add1.srec

6.9 Aiuto in Linea Naturalmente tutti i tool riportati prevedono moltissime opzioni; per approfondimenti si consiglia sia di fare riferimento alla grande quantità di documentazione reperibile in Internet, sia di utilizzare il manuale in linea della GNU toolchainl (sia la versione x86 sia la versione per ARM): # man gcc # man as # man ld # man arm-elf-gcc # man arm-elf-as # man arm-elf-ld Esiste anche un help in linea embedded negli applicativi, richiamabile nel seguente modo: # arm-elf-gcc --help # arm-elf-as --help # arm-elf-ld --help In modo simile per tutti gli altri tool disponibili.

47

7 ESECUZIONE REMOTA Eseguire un file sulla propria macchina, cioè in locale, è molto semplice: in Microsoft Windows per esempio, basta premere con il puntatore del mouse due volte sull’icona del file; in UNIX, con un terminale, è sufficiente a volte, digitare il nome del file eseguibile e premere Invio. Un eseguibile per il target, invece, deve prima essere trasferito sulla scheda di sviluppo, poi, attraverso un link di comunicazione, è possibile richiedere che venga eseguito. In sostanza, non essendoci un Sistema Operativo, è necessario comandare da remoto, manualmente, l’operazione di load ed esecuzione del codice prodotto. UBOOT è un bootloader abbastanza evoluto e dispone di un programma monitor che offre una shell basilare ma, come del resto già detto, ricca di comandi utili nello sviluppo dei programmi di esempio. L’host ed il target sono collegati attraverso una linea seriale RS232; dopo l’accensione della scheda dal lato target è UBOOT che risponde sulla seriale. Utilizzando, sul computer host, un programma che permetta di scrivere e leggere sulla linea seriale, sarà possibile comunicare con UBOOT sul target. UBOOT indica che è pronto a ricevere comandi quando sul programma di comunicazione si vede il prompt samsung> .

48

7.1 Connessione remota In questa sezione si descrive come creare un collegamento seriale tra un computer host Linux (o Windows) e la scheda target.

Figure 26 : connettere la scheda SERP all’host computer per lo sviluppo remoto

Per la comunicazione non è sufficiente disporre solo di un link fisico ma sono necessari anche dei programmi da entrambi i lati che inviino e attendano dati sulle rispettive porte seriali in ognuna delle due macchine. Sul target il programma che risponde ai comandi ricevuti sulla porta seriale è il bootloader UBOOT. Sull’host si può usare un qualsiasi programma che funga da “emulatore di terminale”. In questa guida si utilizzeranno il programma minicom su Linux e Hyper Terminal su Windows. La prima cosa da fare è alimentare la scheda di sviluppo connettendo il trasformatore di rete, quindi è necessario connettere, come in Figure 26, il cavo seriale nel connettore RJ12 (vedi Figure 21 pagina 33, attenzione a non confondersi con il connettore Ethernet). L’altro capo del cavo RS232 va connesso alla macchina host.

7.1.1 Connessione remota con Linux Per connettersi al target in Linux si utilizzerà il programma minicom : è indifferente avviarlo all’interno dell’ambiente grafico, oppure nella console. Si vedono ora i semplici passi per stabilire una connessione seriale. I seguenti passi assumono che la scheda target sia inizialmente non alimentata; più avanti sarà indicato quando accenderla (tenendo premuto il pulsante ON). 1. In una sessione grafica avviare un terminale, ad esempio Konsole in KDE, oppure xterm. 2. Al prompt del terminale digitare minicom e premere invio.

49

3. E’ il caso di configurare la connessione con le caratteristiche preconfigurate in UBOOT.

Con la combinazione di tasti Ctrl-A, Z (premere Ctrl e il carattere A insieme, quindi rilasciarli e premere Z) si entra nel menu di configurazione di minicom.. Qui premendo il tasto P (comm Parameters) si attiva la finestra che permette di creare la configurazione.

50

L’impostazione corretta è (come in figura): Speed 115200bps Data 8 bit Parità None Stop 1 bit Si ottiene questa impostazione digitando sulla tastiera I, L, V, W; si preme quindi Invio per uscire. 4. In alcuni casi è altresì necessario disabilitare il controllo di flusso dei dati. Con la combinazione di tasti Ctrl-A, Z si entra nel menu di configurazione di minicom, quindi si sceglie cOnfigure minicom con il tasto O. Si seleziona quindi la terza opzione Serial port setup e si preme Invio.

E’ necessario configurare le opzioni F e G in modo tale che Hardware Flow Control e Software Flow Control siano entrambe a No. 5. A questo punto è possibile, premendo il pulsante ON della scheda, accederne l’alimentazione. Sul terminale vengono presentate le seguenti informazioni.

51

Si noti che i messaggi di errore non necessariamente appariranno. 6. Premendo subito un tasto sulla tastiera si ferma l’avvio automatico di Linux. Apparirà il prompt di UBOOT. Da questo momento in poi si potranno utilizzare i comandi di UBOOT.

7.1.2 Connessione remota con Windows Per connettersi al target in Microsoft Windows si utilizzerà il programma HyperTerminal. Si descrivono ora i semplici passi per stabilire una connessione seriale. I seguenti passi assumono che la scheda target sia inizialmente non alimentata, più avanti sarà indicato quando accenderla (tenendo premuto il pulsante ON). 1. Avviare HyperTerminal in uno dei seguenti modi:

• Start Tutti i Programmi Accessori Comunicazioni HyperTerminal HyperTerminal

• Start Esegui.., Apri: “hypertrm”, OK 2. Nella finestra che appare inserire il nome della connessione e scegliere un icona, come nome inserire per esempio “s3c2440”. Quindi premere OK.

52

3. In questa finestra scegliere il nome della porta seriale a cui è stato connesso il target. Premere quindi OK.

4. Nella finestra che appare si impostano i seguenti parametri di comunicazione: Bits per second 115200Data bits 8 Parità None Stop bits 1 Flow control None

5. Appare quindi la seguente finestra di comunicazione:

53

6. A questo punto è possibile, premendo il pulsante ON della scheda, accederne l’alimentazione. Premere subito nella finestra dell’HyperTerminal un tasto e si otterrà il prompt di UBOOT.

54

7.2 Download di file sul target In questa sezione si descrive brevemente come trasferire un file nella RAM del target. Il bootloader UBOOT permette diversi protocolli di trasferimento, non solo attraverso la linea seriale, ma anche utilizzando il bus USB, l’Ethernet e le schedine SD. Qui viene considerato il trasferimento di file via linea seriale RS232 utilizzando il protocollo ymodem. Nel prosieguo si considera il trasferimento di un file eseguibile chiamato add1. Questo è il primo programma degli esercizi ed il suo file sorgente è quello presentato nel capitolo precedente. L’obiettivo di questa sezione è insegnare al lettore come trasferire un file qualsiasi dal computer host al target.

7.2.1 Download di file in Linux Nei seguenti passi si assume che la scheda sia già stata accesa e che l’utente sia davanti al programma minicom da cui può già comunicare con il prompt di UBOOT. 1. La prima operazione da fare è dire ad UBOOT che si sta per trasferirgli un file, in modo che si prepari a riceverlo. L’informazione più importante è l’indirizzo della RAM a partire dal quale UBOOT deve mettere il file che riceverà. Si dovrà specificare perciò un indirizzo di memoria RAM, per esempio 0x3100_0000. Per far ciò si invia ad UBOOT il seguente comando: samsung> loady 0x31000000 Il programma loady si mette in attesa di dati sulla seriale, questi dati dovranno essere formattati in base al protocollo di comunicazione ymodem, ogni pacchetto di dati correttamente ricevuto verrà trasferito in RAM a partire dall’indirizzo di memoria specificato, in questo caso 0x3100_0000.

2. A questo punto UBOOT è in attesa del trasferimento perciò si entra nel menù di minicom con la combinazione di tasti Ctrl-A, Z .

55

3. Scegliendo S Send files, si sceglie dal menù che appare ymodem e si preme Invio.

4. In questa finestra si selezioni il file da trasferire, utilizzando la barra spaziatrice, quindi scegliere Okay.

56

5. Appare quindi la finestra di trasferimento, premere un tasto per concludere il trasferimento.

7.2.2 Download di file in Windows Nei seguenti passi si assume che la scheda sia già stata accesa e che l’utente sia davanti al programma HyperTerminal da cui può già comunicare con il prompt di UBOOT. 1. La prima operazione da fare è dire ad UBOOT che si sta per trasferirgli un file in modo che si prepari a riceverlo. L’informazione più importante è l’indirizzo della RAM a partire dal quale UBOOT deve mettere il file che riceverà. Si dovrà specificare perciò un indirizzo di memoria RAM, per esempio 0x3100_0000. Per far ciò si invia ad UBOOT il seguente comando: samsung> loady 0x31000000 Il programma loady si mette in attesa di dati sulla seriale, questi dati dovranno essere formattati in base al protocollo di comunicazione ymodem, ogni pacchetto di dati correttamente ricevuto verrà trasferito in RAM a partire dall’indirizzo di memoria specificato, in questo caso 0x3100_0000.

57

2. A questo punto UBOOT è in attesa del trasferimento. In HyperTerminal si seleziona dal menu Trasferimento Invia File... e apparirà la seguente finestra in cui si sceglierà il file eseguibile da trasferire e, nel menù a discesa, si selezionerà il protocollo Ymodem.

3. Premendo OK apparirà la seguente finestra di trasferimento.

4. In UBOOT dovrebbe apparire un messaggio di avvenuto trasferimento con successo o meno. Verificare.

58

7.3 Esecuzione del file sul target Di seguito si vedrà come scaricare ed avviare eseguibili in formato ELF e binario sulla scheda target.

7.3.1 ELF Format Come si è visto nel paragrafo dedicato ad i formati eseguibili, l’ELF contiene una serie di sezioni che vanno poi riarrangiate in memoria da un apposito programma che sia in grado di farne il caricamento. Questo programma è presente in UBOOT e perciò lo si può utilizzare. Considerando che nel capitolo precedente è stato compilato in formato ELF il codice dell’applicativo di esempio (add1.s) in modo tale che la sezione .text venga posizionata in memoria dall’indirizzo 0x3000_0000, non si può trasferire il modulo eseguibile sulla RAM del target in questa posizione. Dal capitolo precedente, i passi per ottenere sul computer host un modulo eseguibile in formato ELF (del file sorgente add1.s) sono (assemblying, linking): # arm-elf-as -o add1.o add1.s # arm-elf-ld -Ttext 0x30000000 -o add1 add1.o La sezione di codice eseguibile inizia dall’indirizzo 0x3000_0000 grazie all’utilizzo della direttiva -Ttext del linker. Il file eseguibile add1, prodotto della compilazione, è un file in formato ELF, non può perciò essere direttamente eseguito. Un programma apposito interpreta l’header e sposta le diverse sezioni agli indirizzi di memoria richiesti. Dato che il loader di file ELF presente in UBOOT dovrà spostare le diverse sezioni di add1 (in formato ELF) dall’indirizzo 0x3000_0000 in poi, si deve trasferire add1 sulla RAM del target in una posizione dove non intralci. Si sceglie, ad esempio, l’indirizzo 0x3100_0000 (si faccia attenzione che l’indirizzo scelto sia della RAM). Utilizzando le istruzioni dei paragrafi precedenti si trasferisca quindi sul target il file eseguibile add1 all’indirizzo 0x3100_0000. In UBOOT si darà inizio al trasferimento grazie al comando: samsung> loady 0x31000000 Per eseguirne il codice è sufficiente utilizzare l’istruzione UBOOT: samsung> loadelf 0x31000000 in cui è stato necessario specificare dove si trova l’ELF file. Ovviamente il codice che si è richiesto di eseguire non farà nient’altro che bloccare la macchina e non produrrà niente in output né sull’LCD né sulla seriale: esegue infatti solo la somma tra due registri del processore.

7.3.2 bin Format Il formato binario invece è un dump della memoria dopo che questa è stata riempita dal caricamento di un ELF. Per questo motivo il trasferimento del file dovremmo farlo non all’indirizzo 0x3100_0000 ma all’indirizzo 0x3000_0000. Dobbiamo cioè trasferire il file sul target esattamente dove verrà eseguito.

59

Dal capitolo precedente, i passi per ottenere sul computer host un modulo eseguibile in formato ELF (del file sorgente add1.s) sono (assemblying, linking): # arm-elf-as -o add1.o add1.s # arm-elf-ld -Ttext 0x30000000 -o add1 add1.o Per convertire il file ELF in fomato bin si utilizza sul computer host objcopy: # arm-elf-objcopy -O binary add1 add1.bin Quindi si trasferisce il file add1.bin all’indirizzo 0x3000_0000 sulla RAM del target, in UBOOT: samsung> loady 0x30000000 E quindi per eseguire il programma si utilizza il comando: samsung> go 0x30000000 che richiede ad UBOOT di eseguire l’istruzione all’indirizzo 0x3000_0000. Il comando produrrà lo stesso risultato prodotto dall’eseguibile in formato ELF.

60

8 Il DEBUGGING Il debugger viene utilizzato per individuare e correggere eventuali errori presenti nel codice in via di sviluppo. Esso permette di analizzare il flusso del codice eseguibile, impostando breakpoint e di controllare lo stato delle variabili, dei registri e degli stack. Il debugger adottato nel Laboratorio di Architettura degli Elaboratori è GDB (Gnu–DeBugger), che fa ovviamente parte della GNU Cross Development Toolchain. Informazioni dettagliate e complete sul debugger GDB si possono trovare all’indirizzo: http://www.gnu.org/softwre/gdb/gdb.html. Qui ci si limita a riportare solo alcune delle informazioni che servono per l’utilizzo pratico di gdb. GDB si presenta, nella sua forma più tradizionale, come un programma a linea di comando testuale, ma esistono diversi front-end grafici per questo strumento, come ad esempio ddd e Eclipse/CDT debugger. La comunità di sviluppo di GDB affianca al prodotto due front-end grafici:

• gdbtui, si trova nel pacchetto della distribuzione principale e si basa su ncurses. • insight, di RedHat Linux, è invece costruito con le librerie grafiche Tcl/Tk.

Utilizzando una Cross Devlopment Toolchain gli applicativi di debug che si utilizzeranno avranno rispettivamente i seguenti nomi: arm-elf-gdb, arm-elf-gdbtui, arm-elf-insight. Indipendentemente dall’interfaccia adottata è importante capire le differenze tra le diverse modalità di funzionamento del debugger GDB (trattate nel seguito):

• local debugging; • stand-alone simulator; • remote debugging.

8.1 Local debugging Si parla di debugging locale (local debugging) quando il software sviluppato su un host viene eseguito e controllato sullo stesso processore host. In questo caso il processore host ed il processore target coincidono, il tool gdb utilizzato deve essere di tipo “nativo” per quel tipo di host. Questa situazione non si verifica in un ambiente di Cross Development, perciò non viene ulteriormente trattata in questo documento.

61

8.2 Stand-alone simulator Nello sviluppo di sistemi embedded si utilizzerà il GDB accoppiato con il simulatore nei casi in cui non sia disponibile un dispositivo target fisico. Il simulatore permetterà di seguire quanto avverrebbe nei registri e nella memoria del dispositivo reale. Per lavorare in questa modalità è necessario specificare, all’avvio di una sessione di gdb, che il target è il simulatore “sim”. GDB viene distribuito con un simulare del core ARM920T, ma, come avviene per tutti i simulatori, non sono rispettati i tempi reali di esecuzione delle istruzioni; anzi questi tempi sono fortemente dipendenti dalla macchina host sulla quale avviene la simulazione. Un’altra limitazione del simulatore è che di fatto esso non è un simulatore a livello di scheda, ovvero non è possibile simulare il succedersi di eventi esterni come le richieste di interrupt o le richieste di DMA, se non con artifici molto complessi e difficili da mettere in atto e soprattutto non vengono simulate le periferiche esterne al core ARM920T, come ad esempio la porta seriale UART, l’LCD, il TouchScreen etc. .

8.3 Remote debugging Quello che avviene normalemente nello sviluppo di software di tipo embedded è che il processore host non coincide con il processore target, quindi emerge la necessità di effettuare un “remote debugging” che consenta di eseguire e controllare un codice su un processore diverso da quello su cui è stato sviluppato. Nella modalità “remote debugger” GDB interagisce con il target mediante una linea di comunicazione: tipicamente viene utilizzata una linea UART o un segmento di rete Ethernet; spesso nelle fasi iniziali di sviluppo del firmware di base viene utilizzata anche la connessione JTAG (standard IEEE 1149.1). Nel caso si utilizzi una linee seriale o si passi attraverso una rete Ethernet, il target deve essere equipaggiato di software, che provveda, oltre alla normale inizializzazione della scheda, anche alla inizializzazione della interfaccia che gestisce il canale di comunicazione e che attivi poi un segmento di codice che gestisca la comunicazione tra target e host: • gdb-stub nel caso in cui il target sia equipaggiato di un semplice firmware tipo bootloader, per

esempio UBOOT; • gdb-server nel caso in cui il target sia già dotato di sistema operativo, ad esempio Linux.

62

8.4 Debugging utilizzando il Simulatore Di seguito riportiamo la procedura passo passo, in Linux e in Windows, per fare il debugging del semplice esempio descritto nei capitoli precedenti, utilizzando il simulatore del processore ARM nell’ambiente integrato GDB/Insight.

8.4.1 Simulatore in Linux 1. Attivare il debugger GDB/Insight da una qualsiasi shell con il comando: # arm-elf-insight 2. Quindi nella Source Window scegliere File Open e selezionare il file prima compilato in formato ELF (in questo caso add1): ecco apparire il sorgente del nostro file nella finestra:

3. Sempre in Source Window richiedere al debugger di connettersi al simulatore selezionando Run

Connect to Target; si imposta quindi la finestra Target Selection con le seguenti opzioni come nella figura che segue, cioè: in Connection Target: Simulator Set breakpoint at ‘main’ unchecked Set breakpoint at ‘exit’ unchecked Set breakpoint at add1_func checked In Run Options Attach to target checked Download Program checked In Run method Run Program selected

63

Si noti che add1_func è la funzione da cui ha inizio il programma che si decide di provare. Premendo il pulsante OK si ottiene la seguente finestra di conferma:

4. A questo punto si è pronti per eseguire il programma con il simulatore. Si può procedere alla sua esecuzione utilizzando le icone dell’interfaccia grafica, tramite , oppure Control Run.

Grazie alle impostazioni inserite al punto 2 l’esecuzione si è fermata alla prima riga di codice del file. Si noti che tale riga non è stata ancora eseguita. L’evidenziazione verde chiaro indica dove è fermo il debugger; il quadratino rosso a inizio riga indica un breakpoint.

64

5. Si procede quindi con l’esecuzione passo passo usando oppure che stanno per Step Machine Instruction e Next Machine Instruction; la seconda non segue l’esecuzione passo passo delle istruzioni all’interno di una chiamata a funzione.

6. Se si vuole visualizzare il contenuto dei registri (del simulatore) basta premere il pulsante , oppure in Source Window View Registers: apparirà quindi una nuova finestra Registers, come qui di seguito:

8.4.2 Simulatore in Windows Essendo disponibile il programma insight anche per Windows ed essendo identico non si riporta la procedura per Windows, che è identical a quella in Linux. L’unica differenza è che insight verrà avviato da una Shell CYGWIN oppure da un Prompt di MS-DOS sempre però digitando arm-elf-insight.

65

8.5 Debugging utilizzando la Scheda di Sviluppo In questa parte si descrive il debugging remoto attraverso la linea seriale.

8.5.1 Debugging Remoto in Linux 1. Accedere alla linea di comunicazione seriale RS232 con un programma quale minicom: # minicom 2. I parametri di comunicazione con il target sono i seguenti:

Cioè, velocità 115200bps, 8 bit di dati, nessuna parità, 1 bit di stop. 3. Accendere la scheda target e premere un qualsiasi tasto per fermare il processo di boot automatico:

4. Dal target a questo punto si richiede di entrare nella modalità di debugging, inviando dal terminale di comunicazione seriale il comando: samsung> kgdb A questo punto viene stampata dal sistema target una stringa non comprensibile (è un insieme di caratteri che permettono ad un GDB remoto di capire chi è il target e dove si è bloccato): ci si può perciò connettere con il sistema di debugging.

66

5. Si deve quindi uscire dal programma di comunicazione: in minicom basta digitare nell’ordine la sequenza Ctrl-A Z X . 6. Si deve poi attivare Insight/GDB con il seguente comando: # arm-elf-insight 7. Quindi nella Source Window scegliere File Open e selezionare il file prima compilato in formato ELF (in questo caso add1). Ecco apparire il sorgente del file nella finestra:

8. Sempre in Source Window si richiede al debugger di connettersi alla scheda (target) selezionando Run Connect to Target. Si imposta quindi la finestra Target Selection con le seguenti opzioni: in Connection Target: Remote/Serial Baud Rate: 115200 Port: /dev/ttyS0 Set breakpoint at ‘main’ unchecked Set breakpoint at ‘exit’ unchecked Set breakpoint at add1_func checked In Run Options Attach to target checked Download Program checked In Run method Run Program selected

67

Si noti che add1_func è la funzione da cui ha inizio il programma che si intende provare. La porta di connessione, nel caso della figura sopra /dev/ttyUSB0, sarà più probabilmente /dev/ttyS0. Premendo il pulsante OK si ottiene la seguente finestra di conferma:

9. Una volta appare la seguente finestra, che potrebbe stupire, ma è tutto normale:

68

Infatti la scheda stava eseguendo il software U-BOOT necessario all’inizializzazione della scheda. 10. Nel menù a tendina (nella figura seguente quello in cui è evidenziata la scritta add1.s) selezionare il codice sorgente che era stato caricato in precedenza.

11. Eseguire quindi il Download del codice del programma sulla scheda target. 12. Impostare un breakpoint alla prima riga di codice del programma (esattamente riga 11), premere cioè con il tasto destro del mouse sulla riga di interesse e scegliere Set Breakpoint. Sul lato sinistro della riga apparirà un quadratino rosso. 13. A questo punto si è pronti per eseguire il programma sul target e si può procedere alla sua esecuzione utilizzando le icone dell’interfaccia grafica, tramite , oppure Control Continue.

69

Grazie a quanto fatto nei punti 11 e 12 l’esecuzione si è fermata alla prima riga di codice. Si noti che tale riga non è stata ancora eseguita. L’evidenziazione verde chiaro indica dove è fermo il debugging. 14. Si procede quindi con l’esecuzione passo passo usando oppure che stanno per Step Machine Instruction e Next Machine Instruction; la seconda non segue l’esecuzione passo passo delle istruzioni all’interno di una chiamata a funzione.

15. Se si vuole visualizzare il contenuto dei registri (della scheda) basta premere il pulsante , oppure in Source Window View Registers: apparirà quindi una nuova finestra Registers, come qui di seguito:

70

8.5.2 Debugging Remoto in Windows Anche se non strettamente necessario si riportano qui gli stessi passi anche per l’ambiente Windows. 1. Avviare il programma di comunicazione HyperTerminal. 2. Impostare il nome della comunicazione e i parametri della linea seriale visti in precedenza (riportati di seguito):

Cioè, velocità 115200bps, 8 bit di dati, nessuna parità, 1 bit di stop. 3. Accendere la scheda target e premere un qualsiasi tasto per fermare il processo di boot automatico:

71

4. Dal target a questo punto si richiede di voler entrare nella modalità di debugging, inviando dal terminale di comunicazione seriale il comando: samsung> kgdb A questo punto viene stampata dal sistema target una stringa non comprensibile (è un insieme di caratteri per il GDB remoto sull’host computer): possiamo perciò connetterci con il sistema di debugging.

5. Da una shell CYGWIN o dal Prompt di MS-DOS si avvia Insight/GDB richiamando l’eseguibile arm-elf-insight. 6. Quindi nella Source Window scegliere File Open e selezionare il file prima compilato in formato ELF (in questo caso add1). Ecco apparire il sorgente il file nella finestra:

72

7. Sempre in Source Window si richiede al debugger di connettersi alla scheda (target) selezionando Run Connect to Target. Si imposta quindi la finestra Target Selection con le seguenti opzioni: in Connection Target: Remote/Serial Baud Rate: 115200 Port: /dev/ttyS0 Set breakpoint at ‘main’ unchecked Set breakpoint at ‘exit’ unchecked Set breakpoint at add1_func checked In Run Options Attach to target checked Download Program checked In Run method Run Program selected

Si noti che add1_func è la funzione da cui ha inizio il programma che si intende provare. La porta di connessione, nel caso della figura sopra /dev/ttyUSB0, sarà più probabilmente /dev/ttyS0. 8. Una volta connessi, appare una finestra senza più il codice caricato. Ciò succede perchè il processore ARM stava eseguendo il software U-BOOT necessario all’inizializzazione della scheda.

73

9. Nel menù a tendina (nella figura seguente quello in cui è evidenziata la scritta add1.s) selezionare il codice sorgente che era stato caricato in precedenza. 10. Eseguire quindi il Download del codice del programma sulla scheda target. 11. Impostare un breakpoint alla prima riga di codice del programma (esattamente riga 11), premere cioè con il tasto destro del mouse sulla riga di interesse e scegliere Set Breakpoint. Sul lato sinistro della riga apparirà un quadratino rosso.

12. A questo punto si è pronti per eseguire il programma sul target e si può procedere alla sua esecuzione utilizzando le icone dell’interfaccia grafica, tramite , oppure Control Continue.

Grazie a quanto fatto nei punti 10 e 11, l’esecuzione si ferma alla prima istruzione. Si noti che tale istruzione non è stata ancora eseguita. L’evidenziazione verde chiaro indica dove è fermo il debugging. 13. Si procede quindi con l’esecuzione passo passo usando oppure che stanno per Step Machine Instruction e Next Machine Instruction; la seconda non segue l’esecuzione passo passo delle istruzioni all’interno di una chiamata a funzione. 14. Se si vuole visualizzare il contenuto dei registri (della scheda) basta premere il pulsante , oppure in Source Window View Registers; apparirà quindi una nuova finestra Registers.

74

9 ESERCIZI Questo capitolo presenta una serie di esericizi con cui lo studente può familiarizzare con l’obiettivo di rafforzare le sue capacità di usare l’ambiente di sviluppo e di programmare nel linguaggio assembly ARM. I primi otto esercizi si possono provare utilizzando esclusivamente il simulatore, gli altri richiedono invece l’uso della scheda di sviluppo basata su processore ARM920T Samsung s3c2440, perché interagiscono con le periferiche.

9.1 Primo Programma (somma di due numeri) Conviene iniziare con un primo semplice programma in linguaggio assembly, che calcola la somma di due numeri: 1 /*add1.s**************************************************** 2 * somma di due numeri : * 3 * addendi immediati, risultato su registro * 4 ***********************************************************/ 5 6 .text 7 .global _start 8 _start: 9 .global add1_func 10 add1_func: 11 mov r0, #10 @ carica il primo operando 12 mov r1, #15 @ carica il secondo operando 13 add r2, r1, r0 @ esegue la somma 14 add1_end: 15 b add1_end @ trappola 16 17 .end 18 19 /**********************************************************/ 20 /* Suggerimento/variante: */ 21 /* in esecuzione passo-passo cambiare il valore contenuto */ 22 /* nei registri r0, r1 prima di eseguire la somma */ 23 /**********************************************************/

9.1.1 Il codice Si esaminano, prima di tutto, le linee di testo che non contengono istruzioni eseguibili. Le linee di testa (1-4) e di coda (19-23) sono commenti. L'assemblatore GNU per ARM supporta due tipi di commenti: quelli in stile linguaggio C, che cominciano con /* e terminano con */ e possono essere composti da più righe, come nel primo blocco in cui le righe da 1 a 4 sono un unico

75

commento, oppure come nel blocco di coda, che è costituito da cinque linee ognuna delle quali è costituita da un commento aperto e chiuso; un secondo modo in cui è possibile inserire dei commenti nel codice, tipico della programmazione in linguaggio assembly, prevede di iniziare il commento con il carattere @: tutti i caratteri successivi, fino alla fine della riga corrente, verranno trattati come commento; un esempio sono le righe 11, 12, 13. Alcune righe di testo contengono direttive, caratterizzate dal fatto che di solito cominciano con un punto. Si ricorda che le direttive sono istruzioni particolari in quanto specificano comandi destinati ad essere eseguiti dall’assemblatore, nella fase di traduzione in linguaggio macchina (modulo oggetto) delle istruzioni assembly simboliche (modulo sorgente); ciò contraddistingue le direttive dalle altre istruzioni assembly, che specificano invece operazioni destinate ad essere eseguite, dal processore, nella fase di esecuzione del programma. La prima direttiva che si incontra, alla riga 6, è .text: il suo effetto è di indicare (all’assemblatore) che le istruzioni che seguono andranno a far parte del segmento text del programma finale; come spiegato in un capitolo precedente nel segmento text di norma sono situate le istruzioni destinate ad essere eseguite dal processore. Alla riga successiva si trova la direttiva .global: lo scopo di questa direttiva è di dare visibilità globale, al di fuori del modulo corrente, ad uno o più simboli. Questa informazione è utilizzata nella fase di linking per risolvere i riferimenti tra i simboli definiti e usati in moduli diversi. Il simbolo _start si definisce globale perché il linker deve conoscerne l’indirizzo essendo la prima istruzione del programma. Il simbolo add1_func, che ha il medesimo indirizzo di _start, serve per riconoscere in fase di debugging l’inizio del programma. Ultima direttiva presente è .end alla riga 17: indica semplicemente la fine del file assembly; tutto ciò che segue non è più elaborato dall'assemblatore, anche se si tratta di codice. Un particolare tipo di direttiva è costituito dai label, definiti da simboli seguiti da un due punti ':' (non sono permessi spazi tra il simbolo e i due punti). Una direttiva label associa al simbolo il valore del location counter (il location counter è una variabile, interna all'assemblatore, nella quale l’assemblatore, durante la scansione del modulo sorgente, mantiene l’indirizzo di memoria in cui verrà collocata la prossima istruzione di macchina che verrà generata). I simboli definiti da label sono spesso usati come operandi per indicare, in modo simbolico, gli indirizzi all’interno di un programma. Il resto del codice è molto semplice: le istruzioni alle linee 11 e 12 specificano il caricamento di due valori immediati nei registri r0 e r1; l’istruzione alla linea 13 provoca il calcolo della somma di quei due valori (r1 + r0) e il suo caricamento in r2. L'ultima riga di codice effettivo è una trappola: infatti contiene una istruzione di salto incondizionato alla istruzione stessa; per cui il processore, quando arriva ad eseguirla, continuerà ad eseguirla indefinitamente (se non lo si interrompe).

9.1.2 Compilazione Per ottenere un programma eseguibile partendo dai sorgenti in assembly, sono necessari due passaggi: prima creare, tramite l'assemblatore, un file contenente il modulo oggetto (*.o) e successivamente, tramite il linker, dal modulo oggetto ottenere il programma eseguibile. Posto che le istruzioni assembly (modulo sorgente) siano contenute nel file add1.s, il comando per l'invocazione dell'assemblatore è: # arm-elf-as --gdwarf-2 -o add1.o add1.s All'assemblatore vengono passati tre parametri:

• --gdwarf-2: specifica che si vogliono includere, nel modulo oggetto, informazioni di debug (utili per le operazioni che si possono eseguire con il debugger);

76

• -o <file>.o: specifica il file di output (in cui verrà prodotto il modulo oggetto); • <file>.s: specifica il file di input (contenente il modulo sorgente da tradurre).

L'invocazione del linker avviene con il comando: # arm-elf-ld -Ttext 0x30000000 -o add1 add1.o Anche qui sono presenti tre parametri: -Ttext 0x30000000 per specificare l’indirizzo di memoria a partire dal quale si vuole che venga caricato il segmento text e, come per l'assemblatore, il file di output (add1) e quello di input (add1.o).

9.1.3 Debugging Per invocare il debugger si usa il comando: # arm-elf-gdb add1 Seguire le istruzioni presenti nel capitolo sul debugging per stabilire la connessione al simulatore oppure ad un target remoto. Per verificare il corretto funzionamento del programma, si possono eseguire le istruzioni ad una ad una (modalità step) fino ad arrivare a quella situata alla riga 15 (oppure collocare un breakpoint su quest’ultima) e constatare che nel registro r2 viene prodotta la somma (25, ovvero 0x19) dei due operandi. In questo semplice programma, ad ogni esecuzione viene calcolata la somma degli stessi addendi ottenendo sempre lo stesso risultato. È possibile però utilizzare lo stesso modulo eseguibile per calcolare altre somme, utilizzando alcune delle possibilità aggiuntive che il debugger mette a disposizione: eseguendo le istruzioni con modalità step (oppure definendo un breakpoint), si arrivi ad avere evidenziata la riga 13 del codice, ovvero quella con l'istruzione add. In questa situazione l’istruzione evidenziata deve essere ancora eseguita. Operando sulla finestra dei registri è possibile modificare il contenuto dei registri r0 e r1, (facendo clic con il mouse sul valore del registro, se ne può modificare il contenuto), ad esempio 0x12 e 0x23. Tornando sulla finestra principale ed avanzando di un altro passo l'esecuzione, il risultato nel registro r2 non è più 25 ma 53.

77

9.2 Secondo programma (somma di due numeri) 1 /*add2.s***************************************************/ 2 /* somma di due numeri */ 3 /* addendi in memoria, risultato su registro */ 4 /**********************************************************/ 5 .text 6 .global _start 7 _start: 8 .global add2_func 9 add2_func: 10 ldr r8, =in1 @ indirizzo del primo operando 11 ldr r0, [r8] @ carica in r0 il primo operando 12 ldr r8, =in2 @ indirizzo del secondo operando 13 ldr r1, [r8] @ carica in r1 il secondo operando 14 add r2, r1, r0 @ esegue la somma 15 add2_end: 16 b add2_end @ trappola 17 18 .data 19 in1: .long 0x00000012 @ primo operando 20 in2: .long 0x00000034 @ secondo operando 21 22 .end

9.2.1 Il codice Il secondo semplice programma è una evoluzione del precedente; la differenza consiste nel fatto che i valori degli addendi vengono caricati dalla memoria anziché essere definiti come operandi immediati. Per caricare in un registro un valore contenuto in memoria bisogna procedere in due passi: prima caricare in un registro l’indirizzo della locazione di memoria e poi caricarne in un registro il contenuto con un'istruzione del tipo ldr Rn, [Rm]. Viste anche le limitazioni presenti negli indirizzamenti immediati non è di norma possibile caricare un indirizzo in un registro tramite una istruzione del tipo mov Rn, #imm. A questo scopo conviene utilizzare la pseudo-istruzione (o meglio lo pseudo-indirizzamento) ldr Rn, =imm nella quale spesso l’operando immediato è un simbolo definito come label. Oltre alla pseudo-istruzione appena vista, nel codice sono presenti anche due nuove direttive: .data e .long. La prima (.data) serve a specificare l'utilizzo, da quella riga del programma in poi, del segmento data: tutto quello che verrà generato dalle righe successive (si tratterà ovviamente di dati) verrà collocato nel segmento data. La direttiva .long riserva uno spazio di 4 byte (un long word) in memoria e lo inizializza con il valore specificato. Nell’esempio, usando questa direttiva, è stato definito lo spazio in memoria per i due addendi e se ne sono definiti anche i valori (0x12 e 0x34).

9.2.2 Compilazione Posto di aver salvato il codice sorgente nel file add2.s, i comandi sono gli stessi dell’esempioo precedente, cambiando ovviamente i nomi dei file: # arm-elf-as --gdwarf-2 -o add2.o add2.s # arm-elf-ld -Ttext 0x30000000 -o add2 add2.o

78

9.2.3 Debugging Per mandare in esecuzione il programma, i passi da seguire sono gli stessi dell'esempio precedente. In questo secondo esempio gli addendi si trovano in memoria ed è interessante vedere come il debugger GDB consenta di modificare il contenuto delle locazioni di memoria: con il programma in esecuzione e bloccato (ad esempio con un breakpoint) alla prima istruzione, si apra la finestra di visualizzazione della memoria (menu: View->Memory); si prosegua poi l’esecuzione della sola istruzione alla riga 10 del programma, in modo da ottenere, nel registro r8, l'indirizzo di memoria corrispondente al label in1; si inserisca poi questo indirizzo nel campo 'Address' della finestra della memoria; in questa situazione il primo word di memoria visualizzato è quello corrispondente al label in1 e il secondo è quello relativo a in2 (nel programma sorgente i 2 word sono stati posti in posizioni consecutive); con modalità analoghe a quelle utilizzate per modificare il contenuto dei registri, è ora possibile modificare il contenuto della memoria. NOTA: Ad ogni caricamento del programma tutte le zone di memoria di competenza del programma vengono reinizializzate, per cui le modifiche fatte “manualmente”, con le modalità descritte sopra, vengono perse, anche senza essere usciti dal debugger.

79

9.3 Terzo Programma (somma di due numeri) Si procede con un terzo semplice esempio, relativo ancora alla somma di due numeri, questa volta prevedendo che anche il risultato venga collocato in memoria. 1 /*add3.s***************************************************/ 2 /* somma di due numeri */ 3 /* addendi in memoria, risultato in memoria */ 4 /**********************************************************/ 5 .text 6 .global _start 7 _start: 8 .global add3_func 9 add3_func: 10 ldr r8, =in1 @ indirizzo del primo operando 11 ldr r0, [r8] @ carica in r0 il primo operando 12 ldr r8, =in2 @ indirizzo del secondo operando 13 ldr r1, [r8] @ carica in r1 il secondo operando 14 add r0, r0, r1 @ esegue la somma 15 ldr r8, =out @ indirizzo del risultato 16 str r0, [r8] @ memorizza il risultato 17 add3_end: 18 b add3_end @ trappola 19 20 .data 21 in1: .long 0x00000012 @ primo operando 22 in2: .long 0x00000034 @ secondo operando 23 24 .bss 25 .align 4 26 out: .space 4 @ spazio per il risultato 27 .end

9.3.1 Il codice Il risultato della operazione di somma viene memorizzato non nel segmento data, ma nel segmento bss, che è destinato a contenere, appunto, dati non inizializzati (cioè dati che, a differenza di quelli del segmento data, non hanno un valore assegnato quando il programma viene caricato in memoria; questi dati assumeranno dei valori solo durante l’esecuzione del programma, in seguito ad operazioni di scrittura). La direttiva .bss indica l'utilizzo del segmento bss per cui il successivo label out individua un indirizzo situato in tale segmento. La direttiva .space serve per riservare dello spazio libero (non inizializzato) nel segmento bss; l’argomento della direttiva (4 nell’esempio) indica l’estensione, in byte, dello spazio da riservare. La direttiva .align 4 serve ad allineare l’indirizzo di memoria ad un valore multiplo di 4, in modo da consentire l’accesso corretto ai word (4 byte) collocati a partire da quell’indirizzo.

9.3.2 Compilazione Avendo salvato il codice sorgente nel file add3.s, i comandi sono gli stessi dell’esempio precedente cambiando solo, come ovvio, i nomi dei file: # arm-elf-as --gdwarf-2 -o add3.o add3.s # arm-elf-ld -Ttext 0x30000000 -o add3 add3.o

80

9.3.3 Debugging Per questo terzo esempio ci si può limitare a controllare solo se il risultato viene scritto correttamente. Secondo le indicazioni dell'esempio precedente, per conoscere l'indirizzo corrispondente al label out, si dovrebbe arrivare ad eseguire l’istruzione alla linea 15; ma non sempre questo è accettabile: se è necessario conoscere fin dall'inizio l'indirizzo di memoria corrispondente ad un label si può utilizzare un altro dei tool disponibili nell’ambiente di sviluppo: 'nm'. Tramite il comando: # arm-elf-nm add3 è possibile ottenere l'indirizzo corrispondente a tutti i simboli definiti come label all'interno del modulo contenuto nel file specificato, tipicamente un modulo oggetto o un modulo eseguibile. Il risultato ottenuto con quel comando è: 30008050 A __bss_end__ 30008034 A __bss_start 30008034 A __bss_start__ 3000802c D __data_start 30008050 A __end__ 30008050 A _bss_end__ 30008034 A _edata 30008050 A _end 30000000 T _start 3000001c t add3_end 30000000 T add3_func 3000802c d in1 30008030 d in2 30008040 b out la prima colonna indica l'indirizzo (in esadecimale), la seconda, costituita da un solo carattere, il tipo di simbolo ed infine la terza il nome del simbolo. Una piccola legenda per i tipi di simbolo

a : simbolo corrispondente ad un indirizzo assoluto d : simbolo corrispondente ad un indirizzo situato nel segmento data t : simbolo corrispondente ad un indirizzo situato nel segmento text b : simbolo corrispondente ad un indirizzo situato nel segmento bss

con le lettere maiuscole (A, D, T, B) vengono contrassegnati i simboli che hanno visibilità globale; le lettere minuscole si riferiscono, invece, a simboli locali. Il simbolo out corrisponde all’indirizzo 0x3000_8040, che si trova nel segmento bss. Questo è il valore da inserire come indirizzo nella finestra per il controllo della memoria nel debugger.

81

Somma degli elementi di un vettore Si prosegue ora con tre ulteriori esempi, leggermente più complessi dei precedenti, anche questi intesi a raggiungere il medesimo obiettivo, il calcolo della somma degli elementi di un vettore non vuoto di numeri interi naturali a 32 bit, ma realizzati in tre modi diversi. 1 /*sum1.s***************************************************/ 2 /* somma di un vettore di numeri */ 3 /**********************************************************/ 4 .text 5 .equ ELEM, 10 @ numero di elementi del vettore 6 7 .global _start 8 _start: 9 .global sum1_func 10 sum1_func: 11 mov r0, #0 @ r0 conterrà la somma 12 ldr r8, =vec @ indirizzo del primo elemento 13 mov r7, #ELEM 14 sub r7, r7, #1 @ indice dell’ultimo elemento 15 add r7, r8, r7, lsl #2 @ indirizzo dell’ultimo elem. 16 loop: ldr r1, [r8], #4 @ carica un elemento 17 add r0,r0,r1 @ lo somma in r0 18 cmp r7, r8 @ era l’ultimo? 19 bpl loop @ no: carica il prossimo 20 ldr r8, =out @ indirizzo del risultato 21 str r0, [r8] @ memorizza il risultato 22 sum1_end: 23 b sum1_end @ trappola 24 25 .data 26 vec: .long 10,56,89,35,67,54,9,29,37,72 @ elementi 27 .bss 28 .align 4 29 out: .space 4 @ spazio per il risultato 30 .end

9.3.4 Il codice Conviene chiarire dapprima lo scopo con cui vengono utilizzati i vari registri.

r0: destinato a contenere la somma degli elementi del vettore r1: contiene, di volta in volta, il singolo elemento da sommare r7: contiene un puntatore all’ultimo elemento del vettore r8: contiene il puntatore all’elemento da sommare

Le righe dalla 11 alla 15 inizializzano i vari registri utilizzati: r0 viene azzerato; in r8 viene messo l'indirizzo del primo elemento del vettore; in r8 viene prima inserito il numero di elementi del vettore, poi viene decrementato in modo da ottenere l’indice dell'ultimo elemento (che è pari alla dimensione del vettore meno uno), infine questo indice viene trasformato in indirizzo, moltiplicandolo per 4 (lsl #2) poiché ogni elemento è di 4 byte e sommandolo poi all'indirizzo iniziale del vettore. Le righe da 16 a 19 costituiscono il ciclo (loop), che viene eseguito tante volte quanti sono gli elementi del vettore: ad ogni iterazione viene caricato in r1 l’elemento da sommare (il word puntato da r8), contestualmente r8, usato con post-incremento incremento immediato, viene incrementato di 4 e passa a puntare all’elemento successivo; il valore caricato in r1 viene aggiunto

82

alla somma parziale in r0; dopo la verifica che r8 non abbia superato l’ultimo elemento del vettore, viene effettuata un'altra interazione. Alla fine, il risultato viene salvato in memoria.

9.3.5 Compilazione I comandi sono i soliti: # arm-elf-as --gdwarf-2 -o sum1.o sum1.s # arm-elf-ld -Ttext 0x30000000 -o sum1 sum1.o

9.3.6 Debugging Per questo esempio può essere interessante provare a modificare in memoria i valori degli elementi del vettore al fine di constatare la correttezza del programma.

83

9.4 Somma degli elementi di un vettore (subroutine) Lo stesso scopo dell’esempio precedente, il calcolo della somma degli elementi di un vettore, può essere ottenuto definendo una subroutine (una funzione), che presenta il vantaggio di poter essere facilmente riutilizzata da altri programmi. 1 /*sum2.s***************************************************/ 2 /* somma di un vettore di numeri */ 3 /**********************************************************/ 4 .text 5 6 .equ ELEM, 10 @ numero di elementi del vettore 7 8 .global _start 9 _start: 10 .global sum2_func 11 sum2_func: 12 ldr sp, =stack 13 mov r2, #ELEM 14 ldr r1, =vec 15 bl somma @ chiama la subroutine somma 16 ldr r8, =out @ indirizzo del risultato 17 str r0, [r8] @ memorizza il risultato 18 sum2_end: 19 b sum2_end @ trappola 20 21 .func somma 22 /*--------------------------------------------------------*/ 23 /* calcola la somma degli elementi di un vettore */ 24 /* input: */ 25 /* R1: indirizzo del primo elemento del vettore */ 26 /* R2: numero di elementi del vettore */ 27 /* output: */ 28 /* R0: somma */ 29 /*--------------------------------------------------------*/ 30 somma: stmfd sp!, {r2,r3} @ salva nello stack i registri 31 bic r0, r0, r0 32 sub r2, r2, #1 @ indice dell’ultimo elemento 33 somma_l: ldr r3, [r1, r2, lsl #2] @ carica un elemento 34 add r0,r0,r3 @ lo somma in r0 35 subs r2, r2, #1 @ decrementa l’indice 36 bge somma_l @ se >=0, carica il prossimo 37 ldmfd sp!, {r2,r3} @ ripristina i registri salvati 38 mov pc, lr @ ritorna al programma chiamante 39 .endfunc 40 41 .data 42 vec: .long 1,2,3,4,5,6,7,8,9,10 @ elementi 43 44 .bss 45 .align 4 46 out: .space 4 @ spazio per il risultato 47 .space 4096 @ spazio per lo stack 48 stack: .space 4 @ base dello stack 49 50 .end

84

9.4.1 Il codice In questo esempio appaiono per la prima volta due elementi, lo stack e la chiamata a subroutine. Con riferimento allo stack, si osserva che, essendo esso di tipo full-descending, le direttive che lo definiscono (nel segmento bss) comprendono un label (stack) che individua l’indirizzo successivo allo spazio di memoria riservato allo stack stesso (base dello stack), Nella riga 10 allo stack pointer viene assegnato il valore iniziale (quando uno stack full-descending è vuoto, lo stack pointer punta proprio all’indirizzo successivo all’area di memoria riservata allo stack). La necessità di definire uno stack è legata al fatto che questo esempio comprende una subroutine e, come è noto, è buona disciplina di programmazione prevedere che ogni subroutine salvi all’inizio, con operazioni di push nello stack appunto, il contenuto dei registri che vengono da essa modificati (purché non contengano parametri di uscita, cioè valori restituiti dalla subroutine stessa) e che ne ripristini il contenuto, con altrettante operazioni di pop, prima di ritornare al programma chiamante. Si può constatare che il corpo principale del programma, quello compreso tra i label _start, sum2_func e sum2_end, è piuttosto semplice: le operazioni svolte sono solo l’inizializzazione dello stack pointer, l’inserimento nei registri r1 ed r2 dei parametri di ingresso alla subroutine somma, la chiamata della subroutine stessa e la memorizzazione del risultato da essa restituito in r0. Si segnala l’importanza delle righe da 22 a 29 che contengono, sotto forma di commento, le informazioni che specificano l'interfaccia della subroutine: la funzione che essa realizza, i parametri di ingresso, quelli di uscita e quali registri vengono eventualmente da essa modificati. L’interfaccia di una subroutine contiene le informazioni che devono essere note al programmatore che intende utilizzare la subroutine stessa. Nel codice sono presenti due nuove direttive per l'assemblatore (.func e .endfunc), le quali servono a delimitare il codice appartenente ad una funzione. Non sono necessarie per la definizione di una subroutine, ma sono utili in fase di debugging per isolare il codice delle varie routine presenti nel programma. La subroutine somma è organizzata in modo abbastanza simile al codice dell'esempio precedente: all’interno di un ciclo, ad ogni iterazione, un nuovo elemento viene aggiunto alla somma parziale contenuta in r0; a differenza dell’esempio precedente, però, ora gli elementi vengono scanditi dall'ultimo al primo (sono individuati dall’indice contenuto in r2, che viene decrementato ad ogni iterazione), e l’istruzione ldr alla riga 33 non usa metodi di indirizzamento che modificano i registri coinvolti. Nell’istruzione che decrementa l’indice, alla riga 35, viene usato il suffisso 's' che abilita la modifica dei bit di condizione nel registro di stato (CPSR): ciò consente di risparmiare l’uso di un’istruzione di confronto per stabilire quando terminare le iterazioni. Il ritorno al programma chiamante, dopo che r0 contiene la somma richiesta e dopo aver ripristinato i valori originari di r2 ed r3, viene ottenuto con l’istruzione alla riga 38, che rimette in pc il valore che era stato salvato in lr dall’istruzione di chiamata (riga 15), Si noti che i registri r2 ed r3, che subiscono modifiche nel corso della esecuzione della subroutine, vengono salvati nello stack all’inizio e ripristinati alla fine della subroutine, mentre lr, che non viene modificato, non necessita di venir salvato.

9.4.2 Compilazione I comandi sono i soliti: # arm-elf-as --gdwarf-2 -o sum2.o sum2.s # arm-elf-ld -Ttext 0x30000000 -o sum2 sum2.o

85

9.4.3 Debugging Si suggerisce di verificare, con il debugger GDB, sia le variazioni dei valori contenuti nei registri lr, pc ed sp in corrispondenza, rispettivamente, delle istruzioni di chiamata a subroutine (riga 15), di ritorno dalla subroutine (riga 38) e delle istruzioni di push (riga 30) e pop (riga 37). Inoltre anche per questo esempio può essere interessante provare a modificare in memoria i valori degli elementi del vettore al fine di constatare la correttezza del programma.

86

9.5 Somma degli elementi di un vettore (subroutine ricorsiva) Lo stesso esempio che calcola la somma degli elementi di un vettore può essere realizzato anche tramite un algoritmo ricorsivo, ottenuto con una subroutine che prevede di richiamare se stessa. 1 /*sum3.s***************************************************/ 2 /* somma di un vettore di numeri */ 3 /**********************************************************/ 4 .text 5 6 .equ ELEM, 10 @ numero di elementi del vettore 7 8 .global _start 9 _start: 10 .global sum3_func 11 sum3_func: 12 ldr sp, =stack @ inizializza lo stack pointer 13 mov r2, #ELEM @ numero di elementi 14 ldr r1, =vec @ indirizzo del primo elemento 15 bl somma @ chiama la subroutine somma 16 ldr r8, =out @ indirizzo del risultato 17 str r0, [r8] @ memorizza il risultato 18 sum3_end: 19 b sum3_end @ trappola 20 21 .func somma 22 /*--------------------------------------------------------*/ 23 /* funzione ricorsiva per la somma degli el. di un vettore*/ 24 /* input: */ 25 /* R1: indirizzo del primo elemento del vettore */ 26 /* R2: numero di elementi del vettore */ 27 /* output: */ 28 /* R0: somma */ 29 /*--------------------------------------------------------*/ 30 somma: stmfd sp!, {r1-r3,lr} @ salva i registri (anche lr) 31 cmp r2, #1 @ c’è un solo elemento? 32 bhi somma_split @ no: procede con l’algoritmo 33 ldr r0, [r1] @ si: la somma è l’elemento e ... 34 b somma_end @ la subr. termina restituendolo 35 somma_split: @ divide a metà il vettore 36 mov r3, r2, lsr #1 @ n. di elem. della seconda metà 37 sub r2, r2, r3 @ n. di elem. della prima metà 38 bl somma @ somma della prima metà in r0 39 add r1, r1, r2, lsl #2 @ indirizzo della seconda metà 40 mov r2, r3 @ n. di elem. della seconda metà 41 mov r3, r0 @ in r3 la somma della prima metà 42 bl somma @ somma della seconda metà in r0 43 add r0, r0, r3 @ somma delle due metà 44 somma_end: 45 ldmfd sp!, {r1-r3,pc} @ ripristina i registri (lr->pc) 46 .endfunc 47 48 .data 49 vec: .long 1,2,3,4,5,6,7,8,9,10 @ elementi 50 51 .bss 52 .align 4 53 out: .space 4 @ spazio per il risultato 54 .space 4096 @ spazio per lo stack 55 stack: .space 4 @ base dello stack

87

56 .end

9.5.1 Il codice La subroutine somma ha la stessa interfaccia della omonima subroutine dell’esempio precedente, ma, come si può constatare, l’organizzazione del codice è completamente diversa. L'idea alla base dell'algoritmo è di spezzare in due il vettore, calcolare le somme degli elementi di ciascuna delle due metà e sommarle tra loro per ottenere la somma dell'intero vettore. Allo stesso modo (ricorsivamente) si procede per calcolare la somma di ciascuna metà e così via, ricorsivamente, fino ad ottenere vettori di un solo elemento il cui valore coincide con la somma del vettore. Per prima cosa (riga 30) la subroutine provvede a salvare nello stack i registri (diversi da r0, in cui viene restituito il parametro di uscita) che vengono modificati nel corso della propria esecuzione. Si osservi che la subroutine contiene istruzioni di chiamata a subroutine, le quali modificano il registro lr (link register): per questo motivo è necessario che lr, che contiene l’indirizzo di ritorno dalla subroutine, sia incluso nella lista dei registri da salvare nello stack. Le righe (31..34) della subroutine verificano se il vettore è costituito da un solo elemento, nel qual caso il valore dell’elemento viene restituito in r0 come risultato, altrimenti si procede a spezzare in due il vettore e a richiamare ricorsivamente la subroutine somma stessa (due volte, una per ciascuna metà del vettore). A questo scopo vengono dapprima calcolate le lunghezze (il numero di elementi) di ciascuna delle due parti del vettore (attenzione: se il vettore originario ha un numero di elementi dispari, le due parti non hanno la stessa lunghezza); le due lunghezze vengono messe in r2 e r3 (righe 36 e 37). In r1 e r2 vi sono ora i parametri da passare alla stessa subroutine somma, che viene chiamata (riga 38) per calcolare la somma degli elementi della prima metà del vettore; successivamente in r1 e r2 vengono messi i parametri (indirizzo del primo elemento e numero di elementi) relativi alla seconda parte del vettore (righe 39 e 40) per la seconda chiamata alla subroutine somma (riga 42); per ottenere l’indirizzo del primo elemento della seconda parte, all’indirizzo iniziale del vettore, contenuto in r1, viene sommato la lunghezza della prima metà moltiplicata per 4 poiché gli elementi sono da 4 byte; prima della seconda chiamata alla subroutine somma (che restituirà in r0 la seconda somma), la prima somma viene spostata da r0 ad r3, altrimenti verrebbe sovrascritta. Al ritorno dalla seconda chiamata, in r3 e in r0 vi sono le somme delle due parti, sommando le quali (riga 43) si ottiene la somma complessiva da restituire in r0. Si osservi che le operazioni di ripristino degli altri registri modificati e di ritorno al programma chiamante sono ottenute con un’unica istruzione (riga 45) con la quale il valore di lr precedentemente salvato nello stack viene caricato nel pc.

9.5.2 Compilazione I comandi sono i soliti: # arm-elf-as --gdwarf-2 -o sum3.o sum3.s # arm-elf-ld -Ttext 0x30000000 -o sum3 sum3.o

88

9.6 Rovesciamento di una stringa di caratteri Questo esempio presenta una subroutine che opera il rovesciamento di una stringa di caratteri. L’esempio fornisce l’occasione per vedere come sia possibile usare lo stack (con operazioni di push e di pop) come area di lavoro (per collocarvi dati locali alla subroutine stessa). 1 /*strrev.s*************************************************/ 2 /* rovesciamento di una stringa di caratteri */ 3 /**********************************************************/ 4 .text 5 .global _start 6 _start: 7 .global strrev_func 8 strrev_func: 9 ldr sp, =stack @ inizializza lo stack pointer 10 ldr r1, =str @ indirizzo del primo elemento 11 bl strrev @ chiama la subroutine strrev 12 strrev_end: 13 b strrev_end @ trappola 14 15 .func strrev 16 /*--------------------------------------------------------*/ 17 /* funzione che rovescia una stringa: la stringa sia in */ 18 /* input che output è terminata da un byte nullo:"\0"(EOS)*/ 19 /* input/output: */ 20 /* R1: indirizzo primo carattere della stringa */ 21 /*--------------------------------------------------------*/ 22 strrev: 23 stmfd sp!,{r0-r3,lr} @ salva i registri (anche lr) 24 mov r3, r1 @ salva l’indirizzo della stringa 25 mov r2, sp @ salva sp 26 strrev_l: 27 ldrb r0, [r1], #+1 @ preleva un carattere, post-inc 28 strb r0, [sp, #-1]! @ lo inserisce nello stack, pre-dec 29 tst r0,#0xff @ si tratta di EOS? 30 bne strrev_l @ no: preleva il car. successivo 31 mov r1,r3 @ si: ripristina r1 (ind. stringa) 32 add sp,sp, #1 @ elimina dallo stack l’EOS 33 strrev_l2: 34 ldrb r0, [sp], #+1 @ pop un car. dallo stack, post-inc 35 strb r0, [r1], #+1 @ lo inserisce nella str., post-inc 36 cmp sp,r2 @ sp è tornato al val. originario? 37 blo strrev_l2 @ no: pop dallo stack il prossimo 38 ldmfd sp!,{r0-r3,pc} @ ripristina i registri (lr->pc) 39 /*--------------------------------------------------------*/ 40 .endfunc 41 42 .data 43 str: .ascii "Architettura degli elaboratori \0" 44 .bss 45 .align 4 46 .space 4096 @ spazio per lo stack 47 stack: .space 4 @ base dello stack 48 .end

89

9.6.1 Il codice La subroutine strrev opera il rovesciamento di una stringa inserendone dapprima i caratteri (escluso l’ultimo EOS), ad uno ad uno, nello stack con operazioni di push: essendo lo stack di tipo full-descending, in questo modo in cima allo stack, puntata da sp, si troverà la stringa rovesciata; successivamente i caratteri di questa stringa vengono estratti dallo stack, con operazioni di pop, e collocati (dall'ultimo al primo) al posto della stringa di input, sovrascrivendola (EOS rimane al suo posto). Alle righe 24 e 25 vengono salvati in altri due registri i valori iniziali di r1 e dello stack pointer (sp), perché serviranno più avanti; nel loop dalla riga 26 alla 30, viene effettuato il push sullo stack, ad uno ad uno, dei successivi caratteri della stringa, fino a che non viene incontrato il carattere 0x00 ovvero '\0', che segnala la fine della stringa (EOS); viene poi ripristinato r1 al valore iniziale (indirizzo della stringa) e viene incrementato lo sp in modo da eliminare dallo stack l’ultimo carattere inserito (EOS) che non va copiato come primo carattere della stringa e che si trova già al suo posto nella stringa; nel loop dalla riga 33 alla 37, i caratteri della stringa vengono estratti dallo stack e inseriti al posto della stringa di input in ordine opposto a quello con cui erano stati inseriti, dall’ultimo al primo, in modo da ottenere la stringa rovesciata. Il ciclo termina quando sp torna ad avere lo stesso valore che aveva all'inizio della subroutine. L’uso dello stack fatto in questo esempio, seppur corretto, non è consigliato in quando inserendo nello stack un byte alla volta, lo stack pointer può assumere anche valori dispari o comunque non multipli di 4: ciò impedirebbe di operare correttamente il push sullo stack di un word (4 byte), in quanto i word in memoria possono essere collocati solo ad indirizzi che siano multipli di 4. Conviene quindi che lo stack pointer sia sempre allineato ad indirizzi multipli di 4.

9.6.2 Compilazione I comandi sono i soliti: # arm-elf-as --gdwarf-2 -o strrev.o strrev.s # arm-elf-ld -Ttext 0x30000000 -o strrev strrev.o

9.6.3 Debugging Per verificare la correttezza del programma conviene utilizzare due finestre per monitorare la memoria: una in cui sia visualizzato lo spazio nel segmento dati occupato dalla stringa iniziale (e che conterrà anche la stringa rovesciata alla fine dell'esecuzione); una seconda in cui sia visualizzato il contenuto dello stack utilizzato come area di lavoro per collocarvi i caratteri della stringa. Per mettere in evidenza il problema dell'allineamento, si può inserire tra le righe 32 e 33 due operazioni inutili come stmfd sp!,{r0} ldmfd sp!,{r0} che effettuano il push sullo stack e, subito dopo, il pop del word (4 byte) contenuto nel registro r0. Eseguendo sulla scheda di sviluppo il programma modificato in questo modo, si verificherà un errore (eccezione SIGBUS) a seguito dell'esecuzione della stmfd, dovuta al fatto che lo sp ha un valore non multiplo di 4; eseguendo il programma sul simulatore, invece, il problema non viene rilevato (ciò conferma che l’esecuzione tramite simulatore non rispecchia completamente quanto avviene con l’esecuzione “vera” da parte del processore).

90

9.7 Rovesciamento di una stringa di caratteri (con sp allineato) Si presenta ora una seconda versione del programma che opera il rovesciamento di una stringa di caratteri. Il codice che segue elimina il problema riscontrato nella versione precedente, mantenendo allineato ad un multiplo di 4 l'indirizzo contenuto nello stack pointer. 1 /*strrev2.s************************************************/ 2 /* rovesciamento di una stringa di caratteri */ 3 /**********************************************************/ 4 .text 5 .global _start 6 _start: 7 .global strrev2_func 8 strrev2_func: 9 ldr sp, =stack @ inizializza lo stack pointer 10 ldr r1, =str @ indirizzo del primo elemento 11 bl strrev @ chiama la subroutine strrev 12 strrev2_end: 13 b strrev2_end @ trappola 14 15 .func strrev 16 /*--------------------------------------------------------*/ 17 /* funzione che rovescia una stringa: la stringa sia in */ 18 /* input che output è terminata da un byte nullo:"\0"(EOS)*/ 19 /* input/output: */ 20 /* R1: indirizzo primo carattere della stringa */ 21 /*--------------------------------------------------------*/ 22 strrev: 23 stmfd sp!,{r0-r4,lr} @ salva i registri (anche lr) 24 mov r2, r1 @ in r2 l’indirizzo della stringa 25 strrev_l0: 26 ldrb r0, [r2], #+1 @ preleva un carattere, post-inc 27 tst r0, #0xff @ si tratta di EOS? 28 bne strrev_l0 @ no: preleva il car. successivo 29 sub r2, r2, r1 @ si: in r2 n. car. str. (con EOS) 30 bic r4, r2, #3 @ azzera i bit 0 ed 1 31 cmp r2, r4 @ se uguali: r2 è un multiplo di 4 32 addne r4, r4, #4 @ se no: in r4 il mult. successivo 33 mov r3, sp @ in r3 lo sp 34 sub sp, sp, r4 @ alloca area riservata sullo stack 35 @ r3 punta alla base di quest’area 36 @ sp rimane allineato a mult. di 4 37 38 strrev_l1: 39 ldrb r0, [r1], #+1 @ preleva un carattere, post-inc 40 strb r0, [r3, #-1]! @ lo inserisce nello stack, pre-dec 41 tst r0, #0xff @ si tratta di EOS? 42 bne strrev_l1 @ no: preleva il car. successivo 43 add r3, r3, #1 @ elimina l’EOS 44 sub r1, r1, r2 @ r1 punta di nuovo al primo car. 45 sub r2, r2, #1 @ n. di car. da copiare (senza EOS) 46 strrev_l2: 47 ldrb r0, [r3], #+1 @ preleva car. da stack, post-inc 48 strb r0, [r1], #+1 @ lo inserisce nella str., post-inc 49 subs r2, r2, #1 @ decrementa il contatore di car. 50 bne strrev_l2 @ preleva il prossimo se cont ? 0 51 add sp, sp, r4 @ rimuove l’area dallo stack 52 ldmfd sp!,{r0-r4,pc} @ ripristina i registri (lr->pc) 53 /*--------------------------------------------------------*/

91

54 .endfunc 55 56 .data 57 str: .ascii "Architettura degli elaboratori \0" 58 .bss 59 .align 4 60 .space 4096 @ spazio per lo stack 61 stack: .space 4 @ base dello stack 62 .end

9.7.1 Il codice Rispetto alla versione del paragrafo precedente, la subroutine dapprima calcola, in r2, la lunghezza della stringa, (righe 26-29); viene poi calcolata, in r4, la lunghezza dello spazio da riservare sullo stack in modo che sia un multiplo di 4 byte (righe 30-32): r4 rimane uguale ad r2 se questo è multiplo di 4, altrimenti viene posto uguale al multiplo di 4 immediatamente superiore; viene quindi allocato sullo stack tale spazio sottraendo da sp la linghezza calcolata in r4 (riga 34). Per il resto l’organizzazione del codice è simile a quella della versione precedente.

9.7.2 Compilazione I comandi sono i soliti: # arm-elf-as --gdwarf-2 -o strrev2.o strrev2.s # arm-elf-ld -Ttext 0x30000000 -o strrev2 strrev2.o

9.7.3 Debugging Come per l'esercizio precedente, si suggerisce di utilizzare due finestre per il monitoraggio della memoria. Si può anche verificare che adesso l’inserzione delle istruzioni di push e pop di un word non provoca problemi.

92

9.8 Ordinamento di un vettore (merge-sort) In questo esempio viene presentata la realizzazione, in linguaggio assembly, dell'algoritmo di merge-sort per ordinare gli elementi di un vettore: si farà uso di 2 subroutine: la prima (sort) ricorsiva che ordina le due metà del vettore; la seconda (merge) che fonde in un unico vettore ordinato due vettori già ordinati. L’esempio prevede che gli elementi dei vettori da ordinare siano numeri naturali della dimensione di un singolo byte. 1 /*msort_b.s************************************************/ 2 /* merge-sort di un vettore di naturali (da un byte) */ 3 /**********************************************************/ 4 .text 5 .global _start 6 7 .equ ELEM, 10 @ numero di elementi del vettore 8 9 _start: 10 11 ldr sp, =stack @ inizializza lo stack pointer 12 13 ldr r1, =vec @ indirizzo del vettore 14 mov r2, #ELEM @ numero di elementi del vettore 15 bl sort @ chiama la subroutine sort 16 _end: b _end @ trappola 17 18 19 .func sort 20 /*--------------------------------------------------------*/ 21 /* funzione che ordina i componenti di un vettore */ 22 /* input/output: */ 23 /* R1: indirizzo del primo elemento del vettore */ 24 /* R2: lunghezza del vettore */ 25 /*--------------------------------------------------------*/ 26 sort: stmfd sp!, {r1-r6,lr} @ salva i registri (anche lr) 27 cmp r2, #1 @ c’è un solo elemento? 28 beq sort_end @ si: fine subr. (il vet. è ordinato) 29 sort_split: @ no: procede con l’algoritmo 30 @ r1,r2 indirizzo e lunghezza della prima metà del vettore 31 @ r3,r4 indirizzo e lunghezza della seconda metà del vettore 32 mov r4, r2 33 mov r2, r2, lsr #1 @ r2=r2/2 (n. el. della prima metà) 34 sub r4, r4, r2 @ n. di elementi della seconda metà 35 add r3, r1, r2 @ ind. del primo el. seconda metà 36 @ ordina la prima metà: 37 bl sort 38 mov r5, r1 @ salva r1, r2 in r5, r6 39 mov r6, r2 40 mov r1, r3 @ copia r3, r4 in r1, r2 41 mov r2, r4 42 @ ordina la seconda metà: 43 bl sort 44 mov r1, r5 @ ripristina r1, r2 (prima metà) 45 mov r2, r6 46 @ fonde le due metà già ordinate 47 bl merge 48 sort_end: 49 ldmfd sp!, {r1-r6,pc} @ ripristina i registri (lr->pc) 50 /*--------------------------------------------------------*/

93

51 .endfunc 52 53 .func merge 54 /*--------------------------------------------------------*/ 55 /* funzione che fa il merge di due vettori contigui gia` */ 56 /* ordinati mantenendo l'ordinamento */ 57 /* input: */ 58 /* R1: indirizzo del primo elemento del primo vettore */ 59 /* R2: lunghezza del primo vettore */ 60 /* R3: indirizzo del primo elemento del secondo vettore */ 61 /* R4: lunghezza del secondo vettore */ 62 /* output: */ 63 /* R1: indirizzo del primo elemento del vettore ordinato */ 64 /* R2: lunghezza del vettore ordinato */ 65 /*--------------------------------------------------------*/ 66 merge: 67 stmfd sp!,{r0-r7,lr} @ salva i registri (anche lr) 68 sub r0, sp, #1 @ r0: ind. primo byte sopra lo stack 69 add r7, r2, r4 @ lunghezza del vettore ordinato 70 sub sp, sp, r7 @ area di lavoro sullo stack 71 bic sp, sp, #3 @ allinea sp al multiplo di 4 72 @ immediatamente inferiore o uguale 73 sub r2, r2, #1 @ indice dell’ultimo el. del 1° vett. 74 sub r4, r4, #1 @ indice dell’ultimo el. del 2° vett. 75 ldrb r5, [r1, r2] @ preleva l’ultimo el. del 1° vett. 76 ldrb r6, [r3, r4] @ preleva l’ultimo el. del 2° vett. 77 merge_loop: 78 cmp r5, r6 @ confronta i due elementi prelevati 79 blo merge_v2 @ salta se r5 < r6 (cmp tra naturali) 80 merge_v1: @ copia nell’area di lavoro l’elemento del 1° vettore 81 strb r5, [r0], #-1 @ copia, post-dec 82 subs r2, r2, #1 @ indice el. precedente del 1° vett. 83 ldrgeb r5, [r1, r2] @ lo preleva, posto che ci sia 84 blt copy_v2 @ se non c’è, copia il resto del 2° 85 b merge_loop @ confrontalo con quello del 2° vet. 86 merge_v2: @ copia nell’area di lavoro l’elemento del 2° vettore 87 strb r6, [r0], #-1 @ copia, post-dec 88 subs r4, r4, #1 @ indice el. precedente del 2° vett. 89 ldrgeb r6, [r3, r4] @ lo preleva, posto che ci sia 90 blt copy_v1 @ se non c’è, copia il resto del 1° 91 b merge_loop @ confrontalo con quello del 1° vet. 92 copy_v1: @ copia nell’area di lavoro gli elementi restanti del 1° 93 strb r5, [r0], #-1 @ copia, post-dec 94 subs r2, r2, #1 @ indice el. precedente del 1° vett 95 ldrgeb r5, [r1, r2] @ lo preleva, posto che ci sia 96 bge copy_v1 @ se c’è, prova a copiarne un altro 97 b merge_end @ altrimenti il merge è finito 98 copy_v2: @ copia nell’area di lavoro gli elementi restanti del 2° 99 strb r6, [r0], #-1 @ copia, post-dec 100 subs r4, r4, #1 @ indice el. precedente del 2° vett 101 ldrgeb r6, [r3, r4] @ lo preleva, posto che ci sia 102 bge copy_v2 @ se c’è, prova a copiarne un altro 103 b merge_end @ altrimenti il merge è finito 104 merge_end: @ ricopia il vett. ordinato al posto del vett. di input 105 add r2, r0, #1 @ r2 punta all’ultimo el. inserito 106 mov r3, #0 @ r3 è l’offset 107 mcopy_l: ldrb r0, [r2, r3] @ carica l’elem. Dall’area di lavoro 108 strb r0, [r1, r3] @ lo ricopia nel vettore di input 109 add r3, r3, #1 @ incrementa l’offset 110 cmp r3, r7 @ ricopiato tutto? 111 blo mcopy_l @ no: ricopia il prossimo 112 add sp, sp, r7 @ si: rimuove l’area di lavoro 113 tst sp, #3 @ sp è allineato a multiplo di 4?

94

114 bicne sp, sp, #3 @ no: multiplo immediatamente infer. 115 addne sp, sp, #4 @ adesso è quello giusto 116 ldmfd sp!,{r0-r7,pc} @ ripristina i registri (lr->pc) 117 /*--------------------------------------------------------*/ 118 .endfunc 119 120 .data 121 vec: .byte 8,6,5,9,7,3,4,1,10,2 @ elementi 122 123 .bss 124 .align 4 125 .space 4096 @ spazio per lo stack 126 stack: .space 4 @ base dello stack 127 .end

9.8.1 Il codice Il codice rispecchia la nota organizzazione dell'algoritmo di merge-sort: la prima parte (subroutine sort), ricorsiva, dimezza il vettore fino ad arrivare a un vettore di un solo elemento e quindi già ordinato; la seconda parte (subroutine merge) effettua il merge di due vettori ordinati mantenendo l'ordinamento. Si può constatare, nella subroutine merge, la particolare attenzione posta nell'allocare sullo stack l’area di lavoro mantenendo lo stack pointer allineato a multiplo di 4. Sempre nella subroutine merge, i vettori vengono scanditi dall'ultimo elemento al primo: i due elementi scanditi, uno del primo vettore e uno del secondo, vengono confrontati (merge_loop) e, di volta in volta, viene copiato nell’area di lavoro il maggiore dei due, mantenendo l'ordinamento, (merge_v1 e merge_v2); quando poi uno dei due vettori è terminato, la rimanente parte dell'altro viene ricopiata senza ulteriori confronti, (copy_v1 e copy_v2). Infine il vettore ordinato costruito nell’area di lavoro sullo stack, viene ricopiato nell’area di memoria che contiene i due vettori ricevuti in input; si osservi che ciò presuppone che i due vettori in input alla subroutine merge siano contigui in memoria: l'uso della subroutine all'interno del sort garantisce il rispetto di questo requisito.

9.8.2 Compilazione I comandi sono i soliti: # arm-elf-as --gdwarf-2 -o msort_b.o msort_b.s # arm-elf-ld -Ttext 0x30000000 -o msort_b msort_b.o

9.8.3 Debugging Il debugging può essere complesso a causa dell'elevato numero di chiamate ricorsive: conviene esaminare la situazione del vettore parzialmente ordinato all'inizio della chiamata a merge ed eventualmente seguire solo alcune delle istanze in cui i vettori in input a merge hanno più di un elemento. Si suggerisce anche per questo esempio di utilizzare due finestre per monitorare la memoria: una in cui sia visualizzato lo spazio nel segmento dati occupato dal vettore iniziale (e che conterrà il vettore ordinato alla fine dell'esecuzione); una seconda in cui sia visualizzato il contenuto dello stack utilizzato come area di lavoro.

95

9.9 I led e gli switch Si esamina ora un semplice programma inteso ad illustrare l’uso di alcuni dispositivi presenti sulla motherboard della scheda: i led e gli switch. 1 /*led1.s***************************************************/ 2 .include "front.inc" 3 .global _start 4 _start: 5 .global led1_func 6 led1_func: 7 mov r1, #LED_ADDR @ indirizzo dei led e degli switch 8 loop: 9 ldrh r0, [r1] @ legge lo stato degli switch 10 strh r0, [r1] @ lo scrive sui led 11 b loop @ indefinitamente 12 led1_end: 13 b led1_end @ trappola 14 .end

9.9.1 Il codice Conviene esaminare dapprima la nuova direttiva .include presente alla riga 2. Questa viene usata per includere il contenuto di un altro file sorgente in modo simile a quello che la direttiva #include fa per il linguaggio C/C++. In un contesto nel quale è richiesta la scrittura di più moduli sorgenti assembly, di norma, i file di tipo include (contenenti codice sorgente destinato ad essere incluso in altri file) hanno estensione .inc. Nel caso in esame il file front.inc definisce alcuni simboli utili nell'utilizzo del pannello frontale. Ecco il contenuto del file : 1 /*front.inc*************************************************/ 2 LED_ADDR = 0x28000000 @ indirizzo dei led e switch (halfword) 3 4 BUT_CONF = 0x56000050 @ indirizzo del registro di configurazione pin GPIO 5 S3_CONF_MASK = 0x000C @ maschera per la configurazione del pulsante S3 6 S4_CONF_MASK = 0x00C0 @ maschera per la configurazione bit del pulsante S4 7 8 BUT_DATA = 0x56000054 @ indirizzo del registro dati pin GPIO 9 S3_DATA_MASK = 0x0002 @ maschera per leggere lo stato del pulsante S3 10 S4_DATA_MASK = 0x0008 @ maschera per leggere lo stato del pulsante S4 11 12 BUT_PULL = 0x56000058 @ indirizzo del registro di configurazione resistenze 13 S3_DATA_MASK = 0x0002 @ maschera per disabilitare la resistenza di pullup S3 14 S4_DATA_MASK = 0x0008 @ maschera per disabilitare la resistenza di pullup S4 Va precisato che l’indirizzo 0x2800_0000, associato al simbolo LED_ADDR (riga 2 di front.inc), individua un halfword (16 bit) sul quale sono mappati sia i 16 switch sia i 16 led presenti sul pannello frontale della scheda: con un’operazione di lettura si ottiene lo stato degli switch; con un’operazione di scrittura si imposta lo stato dei led. L’indirizzo 0x5600_0054, associato al simbolo BUT_DATA (riga 8), individua un word sul quale sono mappati i pin GPIO: il bit di indice 1 di quel word (maschera 0x0000_0002) corrisponde al pulsante S3, sinistro del pannello frontale; il bit di indice 3 (maschera 0x0000_0008) corrisponde al pulsante S4 destro.

96

Tornando al codice presente nel file led1.s, dopo aver caricato in r1 l'indirizzo dello halfword in cui sono mappati in memoria i led e gli switch (riga 7), viene eseguito un ciclo infinito nel quale viene ripetutamente letto lo stato degli switch (riga 9) e riscritto il medesimo valore per impostare lo stato dei led (riga 10).

9.9.2 Compilazione In questo esempio il modulo eseguibile viene costruito a partire da due moduli sorgente, contenuti in file separati: questi due file vanno quindi assemblati separatamente per produrre i corrispondenti moduli oggetto; questi ultimi devono poi essere collegati dal linker. Ecco i comandi con cui si ottiene quanto indicato : # arm-elf-as --gdwarf-2 -o led1.o led1.s # arm-elf-ld -Ttext 0x30000000 -o led1 led1.o

9.9.3 Debugging Solo una nota: dato che il programma è costituito essenzialmente da un ciclo che viene eseguito indefinitamente (righe 9-11) si suggerisce di collocare un breakpoint in almeno una delle istruzioni del ciclo stesso.

97

9.10 I pulsanti Sulla motherboard sono presenti due pulsanti, collegati a due dei pin GPIO (general purpose I/O) del SoC Samsung s3c2440. Le possibilità di uso dei pulsanti sono molte (ad es. possono essere generare interruzioni sul livello, oppure su un fronte); in questo esempio ci si limita a leggerne lo stato e a rilevare se sono premuti o meno: il programma fa scorrere verso destra o verso sinistra l’illuminazione di un led, in seguito alla pressione del pulsante corrispondente. 1 /*led2.s***************************************************/ 2 .include "front.inc" 3 .global _start 4 _start: 5 .global led2_func 6 led2_func: 7 ldr sp, =stack @ inizializza lo stack pointer 8 ldr r1, =BUT_CONF @ configura il pin del GPIO in input 9 ldr r0, [r1] 10 mov r2, #S3_CONF_MASK 11 orr r2, r2, #S4_CONF_MASK 12 mov r3, #0 13 sub r3, r3, #1 14 eor r2, r2, r3 15 and r0, r0, r2 16 str r0, [r1] 17 mov r1, #LED_ADDR @ indirizzo di led e switch 18 movs r0, #1 19 strh r0, [r1] @ illumina il led di sinistra 20 ldr r3, =BUT_DATA @ indirizzo dei pin GPIO 21 loop: 22 bl delay @ lascia passare un po' di tempo 23 ldr r2, [r3] @ legge lo stato dei pulsanti 24 tst r2, #S3_DATA_MASK @ è premuto S3? 25 moveq r0, r0, ror #31 @ si: ruota verso sinistra 26 tst r2, #S4_DATA_MASK @ è premuto S4? 27 moveq r0, r0, ror #1 @ si: ruota verso destra 28 29 @ azzera i bit che escono dall'area visibile (word meno signif.) 30 bics r0, r0, #0x00010000 @ bit uscito a sinistra 31 bicnes r0, r0, #0x80000000 @ bit uscito a destra 32 strh r0, [r1] @ riscrive sui led 33 bne loop @ continua se non nullo 34 led2_end: 35 b led2_end @ trappola 36 37 .func delay 38 delay: @ introduce un ritardo (~ 0.1 s) 39 stmfd sp!, {r0} @ 20.185.088 * 2 / 400MHz ~= 0.1 sec 40 mov r0, #0x01340000 @ 0x01340000 = 20.185.088 41 d_loop: subs r0, r0, #1 42 bne d_loop 43 ldmfd sp!, {r0} 44 mov pc, lr 45 .endfunc 46 47 .bss 48 .space 4096 49 stack: .space 4 50

98

51 .end

9.10.1 Il codice L'inizializzazione (righe 7-20) configura i due pin del port di I/O F come input; si parte poi con il led più a sinistra illuminato. Il corpo del programma è costituito dal loop compreso tra le righe 21 e 33: viene caricato in r2 il word contenente lo stato dei pulsanti; con le istruzioni tst (righe 24 e 26), che effettuano un AND bit a bit, si esamina lo stato dei bit corrispondenti ai pulsanti di interesse (sinistro e destro): se viene rilevato un bit nullo, si opera la rotazione di una posizione verso sinistra o, rispettivamente, verso destra (si ricordi che il bit corrispondente ad un pulsante viene azzerato quando questo è premuto); con le due istruzioni bic (righe 30 e 31) si azzera l'eventuale bit 1 che fosse fuoriuscito dai 16 bit visibili sui led, in seguito ad una rotazione verso sinistra del bit più significativo, oppure verso destra del bit meno significativo; viene inoltre abilitato l'aggiornamento dei bit di stato in modo da rilevare se il risultato è nullo: in tal caso, dopo aver scritto il valore sui led (riga 32), si termina il loop. Il loop comprende, come prima istruzione (riga 22), una chiamata alla subroutine delay, il cui unico scopo è di far trascorrere un po’ di tempo (circa 0.1 s.), al fine di rendere percepibile lo scorrimento della illuminazione dei led: in assenza di questo ritardo lo scorrimento sarebbe talmente veloce da essere non percepibile.

9.10.2 Compilazione Come nel caso precedente, è necessario assemblare separatamente i due moduli sorgente e collegare tramite il linker i corrispondenti moduli oggetto. I comandi sono: # arm-elf-as --gdwarf-2 -o led2.o led2.s # arm-elf-ld -Ttext 0x30000000 -o led2 led2.o

9.10.3 Debugging In questo programma le istruzioni che vengono eseguite dipendono da un evento esterno (la pressione di uno o dell’altro pulsante). Se si volessero eseguire ad una ad una le istruzioni del loop (righe 21-33), sarebbe consigliabile utilizzare la modalità next, anziché step (per evitare di eseguire ad una ad una anche le numerosissime istruzioni della subroutine delay). Si suggerisce di impostare un breakpoint alla riga 23, ove viene rilevato l'evento: se l’evento provoca una modifica di r2, si può procedere con modalità step per verificare l’effetto della modifica, altrimenti conviene lasciar proseguire l’esecuzione del programma. Può essere interessante provare ad eliminare la chiamata alla subroutine delay (commentando parte della riga 22), oppure provare a modificare il numero delle iterazioni da essa eseguite (impostando un valore immediato diverso alla riga 40) e constatare l’effetto di queste modifiche.

99

9.11 Il display LCD In questo e nei prossimi capitoli si affrontano degli esempi intesi a descrivere alcuni possibili utilizzi del display LCD TFT collegato alla scheda. Per utilizzare il display LCD è necessario inizializzarlo preventivamente: il sorgente assembly lcd.s contiene alcune subroutine con le quali è possibile inizializzare, abilitare e disabilitare il controller LCD del SoC Samsung s3c2440. 1 /*lcd.s****************************************************/ 2 .global lcd_init_16bpp, lcd_enable, lcd_disable, lcd_gpio_init 3 4 .text 5 6 .func lcd_init_16bpp 7 /**********************************************************/ 8 /* lcd_init_16bpp: inizializza il display per 16M colori */ 9 /* */ 10 /* input: r8 indirizzo del framebuffer */ 11 /* output: r8 indirizzo del framebuffer */ 12 /**********************************************************/ 13 WIDTH = 800 14 HEIGHT = 480 15 16 LCDCON0 = 0x4d000000 17 LCDCON1 = 0x4d000004 18 LCDCON2 = 0x4d000008 19 LCDCON3 = 0x4d00000c 20 LCDCON4 = 0x4d000010 21 22 LCDSADDR1 = 0x4d000014 23 LCDSADDR2 = 0x4d000018 24 LCDSADDR3 = 0x4d00001c 25 26 DITHMODE = 0x4d00004c 27 28 lcd_init_16bpp: 29 stmfd sp!, {r0, r1, r2, r3, lr} 30 31 @ configuriamo il framebuffer 32 mov r0, r8 33 bic r0, r0, #0xC0000000 34 mov r0, r0, lsr #21 35 mov r0, r0, asl #21 36 mov r0, r0, lsr #1 37 mov r1, r8 38 mov r2, #0xFF000000 39 bic r1, r1, r2, asr #3 40 mov r1, r1, lsr #1 41 orr r2, r0, r1 42 ldr r3, =LCDSADDR1 43 str r2, [r3] 44 45 mov r1, #WIDTH @ calcolo l'end address 46 mov r2, #HEIGHT 47 mul r2, r1, r2 48 add r0, r8, r2, lsl #1 49 mov r1, r0 50 bic r0, r0, #0xC0000000 51 mov r0, r0, lsr #21 52 mov r0, r0, asl #21

100

53 mov r0, r0, lsr #1 54 mov r2, #0xFF000000 55 bic r1, r1, r2, asr #3 56 mov r1, r1, lsr #1 57 orr r2, r0, r1 58 add r3, r3, #4 59 str r2, [r3] 60 61 add r3, r3, #4 62 mov r2, #0 63 str r2, [r3] 64 65 @ impostazione dei valori di timing TFT 66 ldr r0, =lcdcon_16bpp 67 ldr r1, =LCDCON0 68 ldmia r0, {r2, r3, r4, r5, r6} 69 stmia r1, {r2, r3, r4, r5, r6} 70 71 @ clear dithering mode for TFT 72 mov r0, #0 73 ldr r1, =DITHMODE 74 str r1, [r0] 75 76 ldmfd sp!,{r0, r1, r2, r3, lr} 77 mov pc,lr 78 .endfunc 79 80 .func lcd_enable 81 /**********************************************************/ 82 /* lcd_enable: attiva il controller LCD */ 83 /**********************************************************/ 84 LCDCON0 = 0x4d000000 85 86 lcd_enable: 87 ldr r1, =LCDCON0 88 ldr r0, [r1] 89 tst r0, #0x0001 90 bne enabled 91 92 orr r0, r0, #0x0001 93 str r0, [r1] 94 enabled: 95 mov pc, lr 96 .endfunc 97 98 .func lcd_disable 99 /**********************************************************/ 100 /* lcd_disable: disattiva il controller LCD */ 101 /**********************************************************/ 102 LCDCON0 = 0x4d000000 103 104 lcd_disable: 105 ldr r1, =LCDCON0 106 ldr r0, [r1] 107 tst r0, #0x0001 @ se non è già disattivo 108 beq disabled 109 110 orr r0, r0, #0x0001 @ imposta il campo ENABLE 111 str r0, [r1] 112 113 disabled: 114 mov pc, lr 115 .endfunc

101

116 117 118 .func lcd_gpio_init 119 /**********************************************************/ 120 /* Setup GPIO to act as LCD interface */ 121 /* using as LCD interface */ 122 /* alternate function 2 */ 123 /**********************************************************/ 124 GPCCON = 0x56000020 125 GPCCONF = 0xAAAAAAAA 126 GPDCON = 0x56000030 127 GPDCONF = 0xAAAAAAAA 128 129 lcd_gpio_init: 130 ldr r1, =GPCCON 131 ldr r0, =GPCCONF 132 str r0, [r1] 133 134 ldr r1, =GPDCON 135 ldr r0, =GPDCONF 136 str r0, [r1] 137 138 mov pc,lr 139 .endfunc 140 141 142 .data 143 144 lcdcon_16bpp: 145 .long 0x00000078 146 .long 0x0477c102 147 .long 0x00631f07 148 .long 0x00000002 149 .long 0x00000b01 150 151 .end Il principio di funzionamento del controller LCD è abbastanza semplice: l’insieme dei pixel che costituiscono l’immagine da visualizzare sul display va “scritto”, con le modalità previste, in un’apposita area di memoria (chiamata framebuffer); il controller LCD interpreta il contenuto di quell’area di memoria e lo trasferisce sul display a formare la corrispondente immagine. Il controller LCD del SoC Samsung s3c2440, per i display TFT come quello sulla scheda, prevede che ciascun pixel sia rappresentato, nel framebuffer, con 1, 2, 4, 8bit mediante palette. A 16 o 24bit senza palette. Per esempio nella modalità con 4 bit per pixel (bpp) ciascun punto dell’immagine può assumere uno di 24 = 16 colori diversi; i 16 colori sono definiti da una palette (tavolozza) costituita da una tabella di 16 elementi da 16 bit; i 16 bit di ciascun elemento definiscono il corrispondente colore secondo la codifica RGB 565 (in cui i 5 bit più significativi indicano la componente rossa, i 6 bit centrali la componente verde e i 5 bit meno significativi la componente blu del colore stesso); i 4 bit di ciascun pixel vengono interpretati dal controller LCD come l’indice dell’elemento nella palette che ne definisce il colore. Nella modalità con 16 bpp, invece non è presente una palette, in quanto ciascun pixel è direttamente rappresentato nel framebuffer tramite le sue componenti RGB 565. Le caratteristiche delle diverse modalità disponibili nel SoC sono riassunte nella seguente tabella:

102

colori bpp palette 2 1 SI 4 2 SI 16 4 SI 256 8 SI 65536 16 NO 16777216 24 NO

Nel file lcd.s si trova la routine di inizializzazione per 65 mila colori (16bpp), chiamata lcd_init_16bpp la subroutine lcd_enable per abilitare il display e lcd_disable per disabilitarlo. La subroutine di inizializzazione richiede, come parametro di ingresso, nel registro r8 l’indirizzo del framebuffer.

103

9.12 Disegnare a 16 Colori Il primo esempio prevede di inizializzare il display a 16 colori e di visualizzare sullo schermo 16 rettangoli colorati ciascuno con uno diverso colore. 1 /*color1.s*************************************************/ 2 WIDTH = 800 3 HEIGHT = 480 4 5 .text 6 .global _start 7 _start: 8 .global color1_func 9 color1_func: 10 ldr sp, =stack @ inizializza lo stack pointer 11 ldr r8, =frame_buffer @ inizializza il puntatore al FB 12 13 bl lcd_init_16bpp @ in r8: ind. del FB 14 bl lcd_enable @ enable TFT display 15 16 mov r3, #WIDTH/4 @ 1/4 della larghezza dello schermo 17 mov r4, #HEIGHT/4 @ 1/4 della altezza dello schermo 18 mov r0, #0 @ colore (16bpp) 19 m_loop: 20 bic r5, r0, #0x0C @ 2 LSb dell'indice colore: 0,1,2,3 21 mul r1, r5, r3 @ ascissa vertice del rettangolo 22 mov r5, r0, lsr #2 @ 2 MSb dell'indice colore: 0,1,2,3 23 mul r2, r5, r4 @ ordinata vertice del rettangolo 24 bl draw_box_16bpp @ disegna il rettangolo 25 add r0, r0, #1 @ incrementa l'indice colore 26 cmp r0, #16 @ era l'ultimo colore? 27 blt m_loop @ no: disegna un altro rettangolo 28 29 color1_end: 30 b color1_end @ trappola 31 32 .func draw_box_16bpp 33 /*************************************************************/ 34 /* r0 : indice colore */ 35 /* r1 : ascissa x (in pixel) del vertice superiore sinistro */ 36 /* r2 : ordinata y (in pixel) del vertice superiore sinistro */ 37 /* r3 : larghezza (in pixel) del rettangolo */ 38 /* r4 : altezza (in pixel) del rettangolo */ 39 /* r8 : indirizzo del framebuffer (FB) */ 40 /*************************************************************/ 41 draw_box_16bpp: 42 stmfd sp!,{r0,r3-r8} 43 ldr r7, =palette_4bpp @ scelgo il colore nella palette 44 add r7, r7, r0, lsl #1 45 ldrh r0, [r7] 46 mov r7, #WIDTH @ n. pixel in 1 riga dello schermo 47 mul r5, r2, r7 @ n. pixel in y righe (da saltare) (short) 48 add r5, r5, r1 @ + x pixel della riga y 49 add r8, r8, r5, lsl #1 @ r8 = indirizzo di memoria del 50 @ vertice superiore sinistro del 51 @ rettangolo (nel framebuffer) 52 add r0, r0, r0, lsl #16 @ 2 pixel uguali nel LSB di r0 53 sub r3, r3, #1 @ contatore di colonne (pixel/riga 54 sub r4, r4, #1 @ contatore di righe del box 55 mov r6, r3 @ salva il contatore di colonne

104

56 db_loop: @ scrive nel FB: parte dal vertice inf dx 57 mul r5, r7, r4 @ indice (pxl) di riga (parte dall'ultima) 58 add r5, r5, r3 @ indice (pxl) di colonna (dall'ultima) 59 mov r5, r5, lsl #1 60 strh r0, [r8, r5] 61 62 subs r3, r3, #1 @ decrementa l'indice di colonna 63 bpl db_loop @ scrivi altri pxl della riga 64 mov r3, r6 @ fine riga: ripristina il cont. di col. 65 subs r4, r4, #1 @ decrementa l'indice di riga 66 bpl db_loop @ scrivi un'altra riga 67 68 ldmfd sp!,{r0,r3-r8} 69 mov pc,lr 70 .endfunc 71 72 .data 73 .align 4 74 palette_4bpp: 75 .short 0x0000 @ black 76 .short 0x7800 @ dark red 77 .short 0x01e0 @ dark green 78 .short 0x79e0 @ dark yellow 79 .short 0x000f @ dark blue 80 .short 0x780f @ dark magenta 81 .short 0x03ef @ dark cyan 82 .short 0x39e7 @ dark grey 83 .short 0x7bef @ gray 84 .short 0xf800 @ red 85 .short 0x07e0 @ green 86 .short 0xffe0 @ yellow 87 .short 0x001f @ blue 88 .short 0xf81f @ magenta 89 .short 0x07ff @ cyan 90 .short 0xffff @ white 91 92 .bss 93 .align 8 94 frame_buffer: 95 .space 800*480*2 96 .space 4096 @ spazio per lo stack 97 stack: 98 .space 4 @ base dello stack 99 .end

9.12.1 Il codice Il codice è relativamente semplice: nel corpo principale del programma, dopo aver chiamato la subroutine di inizializzazione (modalità 16 bpp) e quella di abilitazione del frame buffer, conoscendo le dimensioni dello schermo (800 × 480 pixel) e volendo disegnare 16 rettangoli uguali e contigui posizionati come una matrice 4 per 4, vengono preparate le coordinate del vertice superiore sinistro di ciascuno dei rettangoli e ne viene comandata la visualizzazione chiamando la subroutine draw_box_16bpp. All'interno della subroutine draw_box_16bpp viene, per prima cosa (righe 42-49), calcolato l'effettivo indirizzo di memoria dell'angolo superiore sinistro del rettangolo da disegnare; successivamente, nel doppio loop principale (righe 56-66), viene disegnato il rettangolo per righe successive, partendo dall'angolo inferiore destro.

105

9.12.2 Compilazione I comandi sono i soliti: # arm-elf-as --gdwarf-2 -o color1.o color1.s # arm-elf-as --gdwarf-2 -o lcd.o lcd.s # arm-elf-ld -g -Ttext 0x30000000 -o color1 color1.o lcd.o

9.12.3 Debugging Impostando un breakpoint in corrispondenza dell’istruzione di chiamata alla subroutine draw_box_16bpp (riga 24) è possibile seguire la visualizzazione dei singoli rettangoli e verificare, nei registri r0-r4 ed r8, i valori dei parametri passati alla subroutine. Per verificare la modalità di colorazione di ciascun rettangolo si può definire un breakpoint nel loop esterno della subroutine draw_box_16bpp, oppure nel loop interno (ad esempio alla riga 57) se si volesse controllare, molto più lentamente, la colorazione di una coppia di pixel alla volta.

106

9.13 Disegnare a 256 Colori Il secondo esempio, simile al primo, prevede di inizializzare il display a 256 colori e di visualizzare sullo schermo 256 rettangoli, disposti a matrice 16 per 16. 1 /*color2.s*************************************************/ 2 WIDTH = 800 3 HEIGHT = 480 4 5 .text 6 .global _start 7 _start: 8 .global color2_func 9 color2_func: 10 ldr sp, =stack @ inizializza lo stack pointer 11 ldr r8, =frame_buffer @ inizializza il puntatore al FB 12 13 bl lcd_init_16bpp @ in r8: ind. del FB 14 bl lcd_enable @ enable TFT display 15 16 mov r3, #WIDTH/16 @ 1/16 della larghezza dello schermo 17 mov r4, #HEIGHT/16 @ 1/16 della altezza dello schermo 18 mov r0, #0 @ colore (16bpp) 19 20 m_loop: 21 bic r5, r0, #0xf0 @ 4 LSb dell'indice colore: 0..15 22 mul r1, r5, r3 @ ascissa vertice del rettangolo 23 mov r5, r0, lsr #4 @ 4 MSb dell'indice colore: 0..15 24 mul r2, r5, r4 @ ordinata vertice del rettangolo 25 bl draw_box_16bpp @ disegna il rettangolo 26 add r0, r0, #1 @ incrementa l'indice del colore 27 cmp r0, #256 @ era l'ultimo colore? 28 ble m_loop @ no: disegna un altro rettangolo 29 30 color2_end: 31 b color2_end @ trappola 32 33 .func draw_box_16bpp 34 /*************************************************************/ 35 /* r0 : indice colore */ 36 /* r1 : ascissa x (in pixel) del vertice superiore sinistro */ 37 /* r2 : ordinata y (in pixel) del vertice superiore sinistro */ 38 /* r3 : larghezza (in pixel) del rettangolo */ 39 /* r4 : altezza (in pixel) del rettangolo */ 40 /* r8 : indirizzo del framebuffer (FB) */ 41 /*************************************************************/ 42 draw_box_16bpp: 43 stmfd sp!,{r0,r3-r8} 44 ldr r7, =palette_8bpp @ scelgo il colore nella palette 45 add r7, r7, r0, lsl #1 46 ldrh r0, [r7] 47 mov r7, #WIDTH @ n. pixel in 1 riga dello schermo 48 mul r5, r2, r7 @ n. pixel in y righe (da saltare) (short) 49 add r5, r5, r1 @ + x pixel della riga y 50 add r8, r8, r5, lsl #1 @ r8 = indirizzo di memoria del 51 @ vertice superiore sinistro del 52 @ rettangolo (nel framebuffer) 53 add r0, r0, r0, lsl #16 @ 2 pixel uguali nel LSB di r0 54 sub r3, r3, #1 @ contatore di colonne (pixel/riga 55 sub r4, r4, #1 @ contatore di righe del box

107

56 mov r6, r3 @ salva il contatore di colonne 57 db_loop: @ scrive nel FB: parte dal vertice inf dx 58 mul r5, r7, r4 @ indice (pxl) di riga (parte dall'ultima) 59 add r5, r5, r3 @ indice (pxl) di colonna (dall'ultima) 60 mov r5, r5, lsl #1 61 strh r0, [r8, r5] 62 63 subs r3, r3, #1 @ decrementa l'indice di colonna 64 bpl db_loop @ scrivi altri pxl della riga 65 mov r3, r6 @ fine riga: ripristina il cont. di col. 66 subs r4, r4, #1 @ decrementa l'indice di riga 67 bpl db_loop @ scrivi un'altra riga 68 69 ldmfd sp!,{r0,r3-r8} 70 mov pc,lr 71 .endfunc 72 73 .bss 74 .align 8 75 frame_buffer: 76 .space 800*480*2 77 .space 4096 @ spazio per lo stack 78 stack: 79 .space 4 @ base dello stack 80 .end Nel programma precedente si è disegnato utilizzando solo 16 colori, in tal caso, sfruttando una grafica a 16bpp si è utilizzato una semplice tabella palette_4bpp di 16 elementi (a 16bit) da cui si pescava il colore per ogni rettangolo. In questo caso, volendo disegnare 256 rettangoli di colore diverso, si va sempre ad estrarre il colore tramite indice in una tabella, che in questo esercizio è localizzata in un diverso file sorgente chiamato palette256.s, che segue (un estratto). 1 /*palette256.s***********************************************/ 2 .global palette_8bpp 3 .data 4 .align 4 5 palette_8bpp: 6 .short 0x0420 @ 0 33 0 7 .short 0x0380 @ 0 28 0 8 .short 0x02c0 @ 0 22 0 9 .short 0x0200 @ 0 16 0 10 .short 0x0160 @ 0 11 0 11 .short 0x28a0 @ 5 5 0 12 .short 0x5800 @ 11 0 0 13 .short 0x8000 @ 16 0 0 14 .short 0x03e0 @ 0 31 0 ... 153 .short 0xfb3f @ 31 25 31 154 .short 0x0fdf @ 1 62 31 155 .short 0x3f1f @ 7 56 31 156 .short 0x667f @ 12 51 31 157 .short 0x95bf @ 18 45 31 158 .short 0xc4ff @ 24 39 31 259 .short 0xec5f @ 29 34 31 260 .short 0xfb9f @ 31 28 31 261 .short 0xfadf @ 31 22 31 262 263 .end

108

9.13.1 Compilazione I comandi sono i soliti: # arm-elf-as --gdwarf-2 -o color2.o color2.s # arm-elf-as --gdwarf-2 -o lcd.o lcd.s # arm-elf-as --gdwarf-2 -o palette256.o palette256.s # arm-elf-ld -g -Ttext 0x30000000 -o color2 color2.o lcd.o palette256.o

9.13.2 Debugging Le operazioni di debug come nel caso precedente possono essere complicate da problemi di trasferimento dei segmenti data da parte del debugger sulla scheda target.

109

9.14 Scrivere caratteri sul display Questo terzo esempio si propone di descrivere l’uso del display a 16bpp con un obiettivo un po' più complesso rispetto ai due esempi precedenti: scrivere dei caratteri sullo schermo. Per fare ciò è necessario avere a disposizione le immagini dei caratteri, nel formato richiesto, da poter ricopiare nelle posizioni desiderate all’interno del framebuffer. Per questo esempio ci si è limitati a rendere disponibile l'immagine contenente i soli caratteri corrispondenti alle cifre da 0 a 9 e alle lettere maiuscole da A a F, con le quali è possibile visualizzare un numero esadecimale. L’immagine di questi caratteri (nel formato RGBA a 32bpp, che necessiterà di essere convertito al formato RGB 565 a 16bpp) si trova nel file font_tab.s, che va assemblato separatamente e collegato tramite il linker. Il programma fa uso anche un file di tipo include (font_tab.inc), riportato qui sotto, nel quale sono definite le dimensioni (in pixel) dell'immagine e dei singoli caratteri. 1 /*font_tab.inc*********************************************/ 2 FONT_LINE = 160 @ full pixmap lenght in pixel 3 FONT_H = 12 @ altezza, in pixel, di un carattere 4 FONT_W = 10 @ larghezza, in pixel, di un carattere Il programma completo, contenuto nel file font1.s, riempie l’intero schermo (800 × 480 pixel) di cifre esadecimali, disposte in 16 righe da 32 caratteri. 1 /*font1.s**************************************************/ 2 WIDTH = 800 3 HEIGHT = 480 4 5 .global _start 6 .include "font_tab.inc" 7 .text 8 _start: 9 .global font1_func 10 font1_func: 11 ldr sp, =stack @ inizializza lo stack pointer 12 ldr r8, =frame_buffer @ r8=0 (framebuffer predefinito) 13 14 bl lcd_init_16bpp @ in r8: indirizzo del FB 15 bl lcd_enable 16 17 mov r0, #0 @ cifra esadec. (nei 4 LSb di r0) 18 mov r2, #0 @ posiz. verticale (indice di riga) 19 l1: 20 mov r1, #0 @ posiz. orizzont. (indice di col.) 21 l2: 22 add r0, r1, r2 @ incrementa la cifra esadecimale 23 bic r0, r0, #0x0ff0 @ isola i 4 bit (nibble) meno sign. 24 bl putnibble @ scrive il carattere sullo schermo 25 add r1, r1, #1 @ incrementa la posizione orizzont. 26 cmp r1, #WIDTH/FONT_W-1 @ oltre l'ultima pos. nella riga? 27 ble l2 @ no: prosegue con car. successivo 28 add r2, r2, #1 @ si: incrementa la posizione vert. 29 cmp r2, #HEIGHT/FONT_H-1 @ oltre l'ultima pos. nella col.? 30 ble l1 @ no: prosegue con riga successiva 31 32 font1_end: 33 b _end @ trappola 34 35 .func putnibble

110

36 /******************************************************************/ 37 /* calcola la pos. nel FB (480 righe × 800 colonne) ove scrivere */ 38 /* input : R0 valore (esadecimale) da scrivere */ 39 /* R1 posizione orizzontale [0..799] (indice di col.) */ 40 /* R2 posizione verticale [0..479] (indice di riga) */ 41 /* R8 indirizzo del framebuffer */ 42 /******************************************************************/ 43 putnibble: 44 stmfd sp!,{r1-r4,lr} 45 cmp r1, #WIDTH/FONT_W-1 @ verifica se ci sta nella riga 46 bhi pnb_end @ fuori schermo (lateralmente) 47 cmp r2, #HEIGHT/FONT_H-1 @ verifica se ci sta nella col. 48 bhi pnb_end @ fuori schermo (verticalmente) 49 cmp r0, #FONT_LINE/FONT_W-1 50 bhi pnb_end @ valore fuori range 51 52 mov r3, r1 @ posiz. oriz. (in larghezze car.) 53 ldr r4, =FONT_W @ larg. dei caratteri (in pixel) 54 mul r1, r3, r4 @ r1 = offset orizz. (in pixel) 55 @ del carattere da scrivere 56 ldr r4, =FONT_H*WIDTH @ n. di pixel in una riga di car. 57 mul r3, r2, r4 @ r3 = offset verticale (in pixel) 58 @ dell'inizio riga in cui scrivere 59 add r1, r1, r3 @ pos. (pixel) del car. da scriver 60 add r1, r8, r1, lsl #1 @ indir. di mem. in cui scrivere 61 @ il carattere (2 byte/pixel) 62 bl drawnibble 63 pnb_end: ldmfd sp!,{r1-r4,pc} 64 .endfunc 65 66 .func drawnibble 67 /**************************************************************/ 68 /* disegna un carattere sullo schermo */ 69 /* input : R0 valore (esadecimale) da scrivere */ 70 /* R1 indirizzo di mem. in cui scrivere il carattere */ 71 /**************************************************************/ 72 drawnibble: 73 stmfd sp!,{r0,r2-r6,lr} 74 ldr r3, =FONT_W*2 @ larg. car. (in byte: 565 a 16 bit) 75 mul r2, r0, r3 @ offset orizz.(byte) del car. in TAB 76 77 ldr r3, =gimp_image @ inizio della tabella (TAB) 78 add r2, r2, r3 @ indir. I riga del car. (in TAB) 79 ldr r3, =FONT_H-1 @ indice ultima riga car (di pixel) 80 ch_1: 81 ldr r4, =FONT_W-1 @ indice ultima colonna (di pixel) 82 ch_2: 83 ldr r6, =FONT_LINE @ n. pixel di una riga dei 16 car 84 mul r5, r3, r6 @ offset vert.(pixel) dell'ultima riga 85 @ r5=offset vert. (pxl in TAB) dell'inizio ultima riga dei 16 car. 86 87 mov r6, #WIDTH 88 mul r6, r3, r6 89 @ r6=offset vert. (pxl su schermo) inizio ultima riga dei 16 car. 90 91 add r6, r6, r4 @ r6=offset su sch. r4-esimo pxl del car. 92 add r5, r5, r4 @ r5=offset in TAB r4-esimo pxl del car. 93 @ inizia dal vertice inferiore destro (VID) del carattere 94 95 mov r5, r5, lsl #1 96 ldrh r0, [r2, r5] @ carica pixel del car. da TAB 97 mov r5, r5, lsr #1 98 mov r6, r6, lsl #1 @ offset su schermo (2byte/pxl)

111

99 strh r0, [r1, r6] @ lo scrive nel FB 100 subs r4, r4, #1 @ decrementa indice di colonna 101 bge ch_2 @ cicla se non era l'ultima 102 subs r3, r3, #1 @ decrementa indice di riga 103 bge ch_1 @ cicla se non era l'ultima 104 ldmfd sp!,{r0,r2-r6,pc} 105 .endfunc 106 107 .bss 108 .align 8 109 frame_buffer: 110 .space 800*480*2 111 .space 4096 112 stack: 113 .space 4 114 .end

9.14.1 Il codice Il programma è costituito, oltre che dal corpo principale, da due subroutine: putnibble, drawnibble. Per comprendere il codice è necessario tenere presente l’organizzazione del file out.s: in questo file i pixel che definiscono le 16 cifre esadecimali si riferiscono ad un’unica immagine dei 16 caratteri (0123456789ABCDEF), la cui altezza FONT_H è di 12 pixel (coincidente con l’altezza di ciascun singolo carattere) e la cui larghezza complessiva è di 16*FONT_W = 160 pixel (essendo FONT_W = 10 pixel la larghezza di ciascun singolo carattere). Questa immagine può essere pensata come una tabella (TAB) di pixel, con 12 righe e 160 colonne, in cui ciascun pixel è rappresentato con 2 byte (nel formato RGB 565 a 16bpp). Un pixel all’interno della tabella può essere individuato dall’indice di riga IR (0..14) e dall’indice di colonna IC (0..159) (la riga di indice 0 è quella superiore; la colonna di indice 0 è quella più a sinistra. Per copiare nel framebuffer il carattere corrispondente alla cifra i-esima (i=0..15), bisogna estrarre da TAB le 12 sequenze di 10 pixel (corrispondenti alle 12 righe dell’immagine del carattere) situate a partire dai pixel di indice: IR = i*10, i*10+160, i*10+160*2, ..., i*10+160*12 In sostanza gli indici iniziali delle 12 sequenze che costituiscono la cifra i si ottengono come somma di due componenti: una componente (i*10) è fissa e costituisce l’offset “orizzontale” (in pixel) della (prima riga di pixel della) cifra i in TAB; l’altra componente è variabile (160*k, con k=0..11) e costituisce l’offset “verticale” della riga k-esima di pixel in TAB (intendendo per offset verticale la somma dei pixel di tutte le k righe precedenti). Nel framebuffer i pixel sono rappresentati da 2 byte (16 bpp) codificati nel formato RGB 565. Per individuare la posizione dei pixel di ciascuna cifra nel framebuffer (e nel display) è utile tenere presente che il display può essere pensato sia come una tabella di pixel (da 480 righe e 800 colonne), sia come una tabella di caratteri (da 480/12 = 40 righe di caratteri e 800/10 = 80 colonne di caratteri, essendo FONT_H = 12 e FONT_W = 10). Dopo aver caricato in r8 l’indirizzo del framebuffer (allineato correttamente) inizializzato il controller LCD a 16 bpp ed averlo abilitato, il programma inserisce: in r0 il valore della cifra esadecimale (da 0 a15) da visualizzare (riga 17 e 22), in r1 ed r2 l’indice di riga (da 0 a 39) e, rispettivamente, l’indice di colonna (da 0 a 79) della posizione in cui il carattere va collocato sullo schermo (inteso come tabella di 40 × 80 caratteri) (righe 18 e 21). Nel loop interno (righe 21-27) viene effettuata la visualizzazione (tramite chiamata alla subroutine putnibble) di una riga di 80

112

caratteri sullo schemo, incrementando l’indice di colonna ad ogni iterazione; nel loop più esterno (righe 19-30) viene incrementato l’indice di riga in modo da visualizzare le 40 righe; si può osservare che il valore della cifra esadecimale da visualizzare è ottenuto (righe 22 e 23) come somma dei suoi indici di riga e di colonna: ciò fa sì che, nelle diverse righe, le stesse cifre non siano incolonnate, ma sfalsate orizzontalmente. La subroutine putnibble, viene chiamata per visualizzare sullo schermo la cifra esadecimale il cui valore è contenuto nel nibble (4 bit) meno significativo del registro r0; la posizione in cui trasferire, nel framebuffer, i pixel della cifra (o in cui visualizzare il carattere nel display) è passata alla subroutine nei registri r1 e r2, che contengono gli indici di colonna (di caratteri) (0..79) e, rispettivamente, di riga (di caratteri) (0.. 39) della posizione della cifra stessa: in questo modo è semplice scrivere le cifre in modo allineato sul display. Compito specifico della subroutine putnibble è di calcolare l'indirizzo di memoria (nel framebuffer) in cui copiare l'immagine corrispondente alla cifra: tenendo conto del fatto che ogni pixel occupa 2 byte nel framebuffer, questo indirizzo è ottenuto (riga 60) sommando all’indirizzo iniziale del framebuffer (r8) un offset pari a 2 volte la distanza (in pixel) tra il vertice superiore sinistro del rettangolo (VSSR) che conterrà il carattere e il vertice superiore sinistro del display; questa distanza è calcolata (riga 59) come somma di due componenti: una componente corrisponde alla posizione “verticale” della riga di pixel che contiene il VSSR ed è pari al numero di righe di pixel dal bordo superiore del display (dato dal prodotto dell’indice di riga r2 per l’altezza FONT_H, in pixel, di un carattere) moltiplicato per il numero (800) di pixel di una riga (righe 56 e 57); l’altra componente fornisce la posizione “orizzontale” del VSSR, cioè la sua distanza dal bordo sinistro del display ed è pari all’indice di colonna r1 moltiplicato per la larghezza FONT_W, in pixel, di ciascun carattere (righe 52-54). La seconda subroutine, drawnibble, ha il compito di scrivere sul framebuffer (e quindi sul display) la porzione dell'immagine contenuta in TAB corrispondente al carattere i-esimo scelto (costituita dalle 12 sequenze di 10 pixel che iniziano dai pixel di indice IR= i*10+160*k, con k=0..12); nel framebuffer queste 12 sequenze di 10 pixel vanno scritte direttamente a partire dalle posizioni corrispondenti ai pixel di indice i*10+800*k, per la prima serie di 16 caratteri visualizzati; 160+i*10+800*k, per la seconda serie di 16 caratteri (che vengono visualizzati di seguito, nella stessa riga); 320*15+i*10+800*k, per la terza serie; 160+320*15+i*10+800*k per la quarta, e così via. Il trasferimento dei pixel di ciascun carattere da TAB al framebuffer viene effettuato a partire dal vertice inferiore destro. Con le istruzioni alle righe 74 e 75 viene inserito in r2 l’offset (in byte) all’interno di TAB (dell’inizio della prima riga di pixel) del carattere i-esimo (essendo l’indice i contenuto in r0); questo offset, aggiunto all’indirizzo iniziale di TAB (gimp_image), viene trasformato nel corrispondente indirizzo di memoria (righe 77 e 78); in r3 ed r4 vengono poi inseriti gli indici (di riga e di colonna) del pixel corrispondente al vertice inferiore destro (VID) del carattere (righe 79 e 81); nel ciclo compreso tra le righe 82 e 101, a partire dal pixel VID e decrementando ad ogni iterazione l’indice di colonna, si trasferiscono nel framebuffer i 10 pixel di una riga del carattere: si calcolano (in r5) l’offset del pixel in TAB e (in r6) l’offset del corrispondente pixel nel framebuffer, si prelevano i 2 byte che rappresentano il pixel in TAB si moltiplica per 2 l’offset di pixel contenuto in r6 per ottenere l’offset di byte nel framebuffer (riga 98) (questa operazione va fatta con un’istruzione apposita perché la successiva istruzione strh, a differenza della str, non prevede lo scorrimento dell’operando), si scrive lo halfword contenente la codifica RGB del pixel alla posizione calcolata nel framebuffer (riga 99); ad ogni iterazione del ciclo più esterno (righe 80..103), si rieseguono le 10 iterazioni del ciclo interno, dopo aver decrementato l’indice di riga (riga 102), per trasferire nel framebuffer i pixel di un’altra riga.

113

9.14.2 Compilazione I comandi sono i soliti: # arm-elf-as --gdwarf-2 -o font1.o font1.s # arm-elf-as --gdwarf-2 -o lcd.o lcd.s # arm-elf-as --gdwarf-2 -o out.o out.s # arm-elf-ld -g -Ttext 0x30000000 -o font1 font1.o lcd.o out.o

9.14.3 Note È abbastanza semplice creare un nuovo font, usando il programma di grafica Gimp disponibile per Linux e Windows: bisogna dapprima, come si è fatto per l’esempio qui presentato, creare un'immagine contenente, in un’unica riga, tutti caratteri che si vogliono usare, rappresentati con un font di tipo non proporzionale (in cui tutti i caratteri abbiano la stessa larghezza), come ad esempio il courier; questa immagine va poi salvata nel formato C-Source, facendo attenzione ad utilizzare il formato dati RGBA (save alpha channel); successivamente il file .c ottenuto va copiato nel directory di lavoro con il nome font_tab.c. Quindi con il compilatore nativo della macchina: # gcc -c font_tab.c Nel caso segnali alcuni errori aprire il file e sostituire guint con unsigned int e guint8 con unsigned char, ricompilare quindi. A questo punto si converte il formato a 32bpp nel formato a 16bpp che ci serve nel nostro dispositivo: # gcc -o font_tab GIMPto565.o font_tab.o # ./font_tab Quindi nella directory si troveranno i due file out.raw e out.c. Rispettivamente l’immagine creata in formato raw, cioè direttamente trasferibile nel frambuffer e il C-source nel formato a 16bpp. Questo secondo file va trasformato in assembly con il seguente comando. # arm-elf-gcc -S out.c