Manuale Scheda s3c2440

Transcript

Manuale Scheda s3c2440
LABORATORIO DI ARCHITETTURA
DEGLI ELABORATORI
DEI Università di Padova
Scheda basata su processore
ARM920T SAMSUNG s3c2440
Descrizione del sistema ed esercizi
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
2
INDICE
1
2
3
INTRODUZIONE........................................................................................................................7
SISTEMA DI SVILUPPO ...........................................................................................................8
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
3
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
4
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
5
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
6
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.
7
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
8
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.
9
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.
10
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;
11
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.
ARM
instruction set
version
3
3
4
4
4
4
5
5
5
5
5
Thumb
instruction set
version
None
None
None
None
1
1
None
None
2
2
2
ARMv5TE
5
2
Yes
ARMv5TEJ
5
2
Yes
ARMv6
6
3
Yes
Name
ARMv3
ARMv3M
ARMv4xM
ARMv4
ARMv4TxM
ARMv4T
ARMv5xM
ARMv5
ARMv5TxM
ARMv5T
ARMv5TExP
Long Multiply
instructions?
No
Yes
No
Yes
No
Yes
No
Yes
No
Yes
Yes
Notes
Enhanced DSP
instructions except
LDRD, MCRR,
MRRC, PLD and
STRD
Enhanced DSP
instructions
Additino of BXJ
instruction and Jazelle
Extension support over
ARMv5TE
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.
12
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
ARM7
ARM9
ARM9E
ARM10E
XScale
ARM11
versione core
ARMv3
ARMv4T
ARMv5TE
ARMv5TE
ARMv5TE
ARMv6
link
http://www.arm.com/products/CPUs/families/ARM7Family.html
http://www.arm.com/products/CPUs/families/ARM9Family.html
http://www.arm.com/products/CPUs/families/ARM9EFamily.html
http://www.arm.com/products/CPUs/families/ARM10Family.html
http://www.arm.com/products/CPUs/families/ARM11Family.html
Table 2
13
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:
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
14
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);
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).
15
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).
OM[1:0] = 01,10
Boot Internal
SRAM (4KB)
OM[1:0] = 00
SROM/SDRAM
(nGCS7)
SROM/SDRAM
(nGCS7)
0x4000_0000
2MB/4MB/8MB/16MB
/32MB/64MB/128MB
Refer to
Table 5
0x3800_0000
SROM/SDRAM
(nGCS6)
SROM/SDRAM
(nGCS6)
SROM
(nGCS5)
SROM
(nGCS5)
128MB
SROM
(nGCS4)
SROM
(nGCS4)
128MB
SROM
(nGCS3)
SROM
(nGCS3)
128MB
SROM
(nGCS2)
SROM
(nGCS2)
128MB
SROM
(nGCS1)
SROM
(nGCS1)
128MB
2MB/4MB/8MB/16MB
/32MB/64MB/128MB
0x3000_0000
0x2800_0000
1GB
HADDR[29:0]
Accessible
Region
0x2000_0000
0x1800_0000
0x1000_0000
0x0800_0000
SROM
(nGCS0)
Boot Internal
SRAM (4KB)
128 MB
0x0000_0000
[ Not using NAND flash for boot ROM ]
[ Using NAND flash for boot ROM ]
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.
16
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.
ECC
Gen.
NAND FLASH
Interface
SYSTEM BUS
SFR
Control &
State Machine
AHB
Slave I/F
Stepping Stone Controller
nFCE
CLE
ALE
nFRE
nFW E
FRnB
I/O0 - I/O15
Stepping Stone
(4KB SRAM)
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.
17
Clock Control
Register
ARM920T
WDT
MEMCNTL
SPI
FCLK
INTCNTL
PWM
HCLK
Input Clock
Po w e r
M a n a g em en t
BUSCNTL
PCLK
I2C
UPLL(96/48 MHz)
ARB/DMA
SDI
ExtMaster
FCLK defination
ADC
LCDCNTL
If SLOW mode
FCLK = input clock/divider ratio
If Normal mode (P, M & S value)
FCLK = MPLL clock (Mpll)
1/n
MPLLin
UART
Nand Flash
Controller
I2S
1/d
Camera
1/2
1/1
USB
Host I/F
GPIO
RTC
USB
Device
AC97
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
18
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.
TCMPB0
1/2
5:1 MUX
PCLK
TCNTB0
TOUT0
Dead Zone
Generator
Control
Logic0
Dead Zone
1/4
8-Bit
Prescaler
1/8
TCMPB1
TCNTB1
1/16
Clock
Divider
TOUT1
5:1 MUX
TCLK0
TCMPB2
Control
Logic1
Dead Zone
TCNTB2
5:1 MUX
TOUT2
Control
Logic2
1/2
1/4
8-Bit
Prescaler
1/8
TCMPB3
TCNTB3
1/16
5:1 MUX
TCLK1
Clock
Divider
TOUT3
Control
Logic3
TCNTB4
5:1 MUX
Control
Logic4
No Pin
19
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.
Peripheral BUS
Transmitt er
Transmit FIFO Registe r
(FIFO mode )
Transmit Buf fer
Regi ster(64 Byte)
Transm it Hold ing Register
(Non-FIFO mode)
TXDn
Transmit Shif ter
Control
Unit
Buad-rate
Gener ator
Clock Source
(PCLK, FCLK/n,UEXTCLK)
Receiver
RXDn
Receive Shifter
Receive Buff er
Reg ister( 64 Byte)
Receive Holding Regi ster
(Non-FIFO mode only)
Receive FIFO Register
(FIFO mode)
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.
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à.
20
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à.
21
ARM IRQ
ARBITER6
REQ0
REQ1
REQ2
REQ3
REQ4
REQ5
ARBITER0
REQ1/EINT0
REQ2/EINT1
REQ3/EINT2
REQ4/EINT3
ARBITER1
REQ0/EINT4_7
REQ1/EINT8_23
REQ2/INT_CAM
REQ3/nBATT_FLT
REQ4/INT_TICK
REQ5/INT_W DT_AC97
ARBITER2
REQ0/INT_TIMER0
REQ1/INT_TIMER1
REQ2/INT_TIMER2
REQ3/INT_TIMER3
REQ4/INT_TIMER4
REQ5/INT_UART2
ARBITER3
REQ0/INT_LCD
REQ1/INT_DMA0
REQ2/INT_DMA1
REQ3/INT_DMA2
REQ4/INT_DMA3
REQ5/INT_SDI
ARBITER4
REQ0/INT_SPI0
REQ1/INT_UART1
REQ2/INT_NFCON
REQ3/INT_USBD
REQ4/INT_USBH
REQ5/INT_IIC
ARBITER5
REQ1/INT_UART0
REQ2/INT_SPI1
REQ3/INT_RTC
REQ4/INT_ADC
4 inputs
16 inputs
2 inputs
2 inputs
3 inputs
2 inputs
3 inputs
3 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
TIMEGEN
REGBANK
LPC3600
VIDEO
MUX
LCC3600
LCDCDMA
VIDPRCS
VCLK /LCD_HCLK
VLINE / HSYNC / CPV
VFRAME / VSYNC / STV
VM / VDEN / TP
.
.
.
LCD_LPCOE / LCD_LCCINV
LCD_LPCREV / LCD_LCCREV
LCD_LPCREVB / LCD_LCCREVB
VD[23:0]
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.
Figure 11 : LCD Controller
22
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.
AVDD
Touch Scr een
AGND
Pullu p
XP
(note)
XM
8:1
MUX
YP
A/D Converter
(note)
ADC
Interface
&Touc h
Screen
YM
INT_ADC
A[3:0]
ADC Input
Contro l
Interrup t
Generation
INT_TC
Waitin g for Interrupt Mod e
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).
23
TICNT
TIME TICK
Time Tick Generator
128 Hz
215
Clock Divider
RTCRST
Reset Register
Leap Year Generator
XTIrtc
1 Hz
SEC
MIN
HOUR
DAY
DATE
MON
YEAR
XTOrtc
Alarm Generator
Control Register
RTCCON
RTCALM
PMWKUP
INT_RTC
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);
24
•
•
•
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.
TxFIFO
ADDR
DATA
BRFC
SFTR
SD
CNTL
RxFIFO
CHNC
SCLK
IPSR_A
PCLK
SCLKG
IPSR_B
LRCK
CDCLK
MPLLin
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.
25
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.
SFR
FSM & Control
PCM in
FIFO
APB
APB
I/F
DMA
Engine
Interrupt
Control
PCM
out FIFO
AC-link
I/F
AC-link
MIC in
FIFO
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.
26
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.
27
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.
Samsung
s3c2440A
SDRAM
Alimentatore
12V
Flash
Pulsante
RESET
CODEC IIS
amplificator
e speaker
Pulsanti
connessi
al port I/O
Led e switch
USB
host
USB
client
Chip e
connettore
RS232
Controller e
connettore
Ethernet
Figure 16 : foto scheda di sviluppo Samsung s3c2440
28
Pulsanti
OFF e ON
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.
JTAG
to Parallel
UART 2
(RS232)
Ethernet
Controller
Ethernet
Socket
LAN9115
UAR T
ch- 0
JTAG Port
U ART
ch-1
UART 3
(TTL)
UART
ch-2
USB client
connector
USB
Host
USB host
connector
USB
Device
IrD A
Audio C odec
x16
x16
LEDs
Microswitches
S3C2440A
Memory
Controller
64 MB SDRAM
x2 K4S561632
128MB
NAND Flash
K9F1G08U0B
S3C2440A
(Based ARM920T)
NAND Flash
Controller
DATA Memory
(SRAM 4KB)
LCD
Controller
DM A, RTC, Interrupt Controller
Touch panel
LEDs
Switches
UD A1341
ARM920T
Status
LED
x2
IIS
LCD TFT
800x480
Samsung
MS700KF05
Ext.
INT
Clock
SD
16.9344Mhz 32.768Khz
MMC/
SD/SDIO
Card
XTAL
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.
29
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.
[1,5,7]
DATA[0..31]
ADDR[0..26]
23
24
25
26
29
30
31
32
33
34
22
35
36
BA0
BA1
20
21
[2,7]
[2,7]
nWB E 0
nWB E 1
15
39
[2]
[2]
SCKE
SCLK0
37
38
28
41
54
6
12
46
52
A0
A1
A2
A3
A4
A5
A6
A7
A8
A9
A10
A11
A12
BA0
BA1
LDQM
UDQM
SCKE
SCLK
VSS0
VSS1
VSS2
VSSQ0
VSSQ1
VSSQ2
VSSQ3
U2
DQ0
DQ1
DQ2
DQ3
DQ4
DQ5
DQ6
DQ7
DQ8
DQ9
DQ10
DQ11
DQ12
DQ13
DQ14
DQ15
nSCS
nSRAS
nSCAS
nWE
VDD0
VDD1
VDD2
VDDQ0
VDDQ1
VDDQ2
VDDQ3
2
4
5
7
8
10
11
13
42
44
45
47
48
50
51
53
DATA0
DATA1
DATA2
DATA3
DATA4
DATA5
DATA6
DATA7
DATA8
DATA9
DATA10
DATA11
DATA12
DATA13
DATA14
DATA15
19
18
17
16
nGCS 6
nSRA S
nSCA S
nW E
1
14
27
[2]
[2]
[2]
[1,7]
ADDR2
ADDR3
ADDR4
ADDR5
ADDR6
ADDR7
ADDR8
ADDR9
ADDR10
ADDR11
ADDR12
ADDR13
ADDR14
23
24
25
26
29
30
31
32
33
34
22
35
36
BA0
BA1
20
21
[2,7]
[2,7]
nWB E 2
nWB E 3
15
39
[2]
[2]
SCKE
SCLK1
37
38
28
41
54
3
9
43
49
6
12
46
52
A0
A1
A2
A3
A4
A5
A6
A7
A8
A9
A10
A11
A12
BA0
BA1
LDQM
UDQM
SCKE
SCLK
VSS0
VSS1
VSS2
VSSQ0
VSSQ1
VSSQ2
VSSQ3
U3
256M b SDRAM
ADDR2
ADDR3
ADDR4
ADDR5
ADDR6
ADDR7
ADDR8
ADDR9
ADDR10
ADDR11
ADDR12
ADDR13
ADDR14
256M b SDRAM
[1,7]
DQ0
DQ1
DQ2
DQ3
DQ4
DQ5
DQ6
DQ7
DQ8
DQ9
DQ10
DQ11
DQ12
DQ13
DQ14
DQ15
nSCS
nSRAS
nSCAS
nWE
VDD0
VDD1
VDD2
VDDQ0
VDDQ1
VDDQ2
VDDQ3
3.3V
2
4
5
7
8
10
11
13
42
44
45
47
48
50
51
53
DATA16
DATA17
DATA18
DATA19
DATA20
DATA21
DATA22
DATA23
DATA24
DATA25
DATA26
DATA27
DATA28
DATA29
DATA30
DATA31
19
18
17
16
nGCS 6
nSRA S
nSCA S
nW E
[2]
[2]
[2]
[1,7]
1
14
27
3
9
43
49
3.3V
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.
30
OM[1:0] = 00
0x4000_0000
SROM/SDRAM
(nGCS7)
128MB
SDRAM 64MB
(nGCS6)
64MB
0x3800_0000
0x3000_0000
LEDs Microswitches
(nGCS5)
128MB
SROM
(nGCS4)
128MB
Ethernet Controller
(nGCS3)
128MB
SROM
(nGCS2)
128MB
SROM
(nGCS1)
128MB
0x2800_0000
0x2000_0000
1GB
HADDR[29:0]
Accessible
Region
0x1800_0000
0x1000_0000
0x0800_0000
0x0000_0000
128 MB
Boot Internal
SRAM (4KB)
[ Using NAND flash for boot ROM ]
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
UBOOT
UBOOT env
Kernel Linux
Splash
File System
START ADDR
0x0000_0000
0x0004_0000
0x0006_0000
0x0026_0000
0x0030_0000
END ADDR
0x0004_0000
0x0006_0000
0x0026_0000
0x0030_0000
0x0800_0000
SIZE
256 kB
128 kB
2 MB
640 kB
125 MB
31
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 UBOOT.
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
32
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).
33
5.10 Mappa di Memoria della scheda
0x5C00_0000
AC97
0x5B00_0000
MMC/SD/SDIO
0x5A00_0000
SPI
0x5900_0000
ADC
Touch Screen
0x5800_0000
RTC
0x5700_0000
0xFFFF_FFFF
Not Used
I/O Ports
SFR Area
IIS
Controller
Not Used
IIC
Controller
SROM/SDRAM
(BANK7, nGCS7)
Watchdog
Not Used
USBDevice
Controller
SDRAM 64MB
(BANK6, nGCS6)
PWM Timers
LEDs Microswitches
(BANK5, nGCS5)
UARTs
SROM
(BANK4, nGCS4)
Camera
Ethernet Controller
(BANK3, nGCS3)
NAND Flash
Controller
SROM
(BANK2, nGCS2)
LCD
SROM
(BANK1, nGCS1)
Clock
Power Management
0x6000_0000
0x4800_0000
0x4000_0000
0x3800_0000
0x3400_0000
0x3000_0000
0x2800_0000
0x2000_0000
0x1800_0000
0x1000_0000
0x0800_0000
0x0000_0000
0x5600_0000
0x5500_0000
0x5400_0000
0x5300_0000
0x5200_0000
0x5100_0000
0x5000_0000
0x4F00_0000
0x4E00_0000
0x4D00_0000
0x4C00_0000
BootSRAM (4KB)
OM[1:0] = 00
DMA
Interrupt
Controller
USB Host
Controller
Memory
Controller
0x4B00_0000
0x4A00_0000
0x4900_0000
0x4800_0000
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.
34
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.
35
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. .
36
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.
37
La GNU Cross Development Toolchain x86 32bit per target arm-elf- offre i seguenti strumenti
necessari allo sviluppo:
archiver
assembler
C preprocessor
C++ compiler
C compiler
profiler
debugger (command line)
debugger (ncurses)
debugger (Tcl/Tk)
linker
list symbols from object files
copy and translate object files
display information from object files
generate index to archive
display information about ELF files
ARM simulator
list section size and total size
print strings of printable chars in file
discard symbols from object files
arm-elf-ar
arm-elf-as
arm-elf-cpp
arm-elf-g++
arm-elf-gcc
arm-elf-gcov
arm-elf-gdb
arm-elf-gdbtui
arm-elf-insight
arm-elf-ld
arm-elf-nm
arm-elf-objcopy
arm-elf-objdump
arm-elf-ranlib
arm-elf-readelf
arm-elf-run
arm-elf-size
arm-elf-strings
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).
arm-elf-gcc
il compilatore è per target
ARM
produce codice rilocabile tipo
“ELF”
questo è il compilatore C
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).
38
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.
ELF header
Program header table
{
.text
.rodata
{
...
.data
Section header table
Figure 23 : ELF file format
Il firmware UBOOT permette anche il download via seriale di binari in formato S-record.
39
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.
40
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.
0100101…
Preprocessor
test.c
C Compiler
test.i
Linker
Assembler
test.s
test.o
a.out
./a.out
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;
}
41
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"
42
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;
43
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.
44
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
30000004:
e3a0100f
30000008:
e0812000
mov
mov
add
r0, #10
r1, #15
r2, r1, r0
3000000c <add1_end>:
3000000c:
eafffffe
b
3000000c <add1_end>
; 0xa
; 0xf
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
45
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
# arm-elf-as
# arm-elf-ld
--help
--help
--help
In modo simile per tutti gli altri tool disponibili.
46
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> .
47
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.
48
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.
49
L’impostazione corretta è (come in figura):
Speed 115200bps
8 bit
Data
Parità None
1 bit
Stop
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.
50
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.
51
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 115200
8
Data bits
None
Parità
1
Stop bits
None
Flow control
5. Appare quindi la seguente finestra di comunicazione:
52
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.
53
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 .
54
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.
55
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.
56
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.
57
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.
58
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.
59
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.
60
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.
61
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
62
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.
63
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 armelf-insight.
64
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.
65
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
66
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:
67
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.
68
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:
69
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:
70
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:
71
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.
72
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.
73
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*add1.s****************************************************
* somma di due numeri :
*
* addendi immediati, risultato su registro
*
***********************************************************/
.text
.global _start
_start:
.global add1_func
add1_func:
mov r0, #10
mov r1, #15
add r2, r1, r0
add1_end:
b add1_end
@ carica il primo operando
@ carica il secondo operando
@ esegue la somma
@ trappola
.end
/**********************************************************/
/* Suggerimento/variante:
*/
/* in esecuzione passo-passo cambiare il valore contenuto */
/* nei registri r0, r1 prima di eseguire la somma
*/
/**********************************************************/
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
74
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);
75
•
•
-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.
76
9.2 Secondo programma (somma di due numeri)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*add2.s***************************************************/
/* somma di due numeri
*/
/* addendi in memoria, risultato su registro
*/
/**********************************************************/
.text
.global _start
_start:
.global add2_func
add2_func:
ldr
r8, =in1
@ indirizzo del primo operando
ldr
r0, [r8]
@ carica in r0 il primo operando
ldr
r8, =in2
@ indirizzo del secondo operando
ldr
r1, [r8]
@ carica in r1 il secondo operando
add
r2, r1, r0
@ esegue la somma
add2_end:
b
add2_end
@ trappola
in1:
in2:
.data
.long
.long
0x00000012 @ primo operando
0x00000034 @ secondo operando
.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
77
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.
78
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/*add3.s***************************************************/
/* somma di due numeri
*/
/* addendi in memoria, risultato in memoria
*/
/**********************************************************/
.text
.global _start
_start:
.global add3_func
add3_func:
ldr
r8, =in1
@ indirizzo del primo operando
ldr
r0, [r8]
@ carica in r0 il primo operando
ldr
r8, =in2
@ indirizzo del secondo operando
ldr
r1, [r8]
@ carica in r1 il secondo operando
add
r0, r0, r1
@ esegue la somma
ldr
r8, =out
@ indirizzo del risultato
str
r0, [r8]
@ memorizza il risultato
add3_end:
b
add3_end
@ trappola
in1:
in2:
out:
.data
.long
.long
.bss
.align
.space
.end
0x00000012
0x00000034
@ primo operando
@ secondo operando
4
4
@ spazio per il risultato
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
79
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
30008034
30008034
3000802c
30008050
30008050
30008034
30008050
30000000
3000001c
30000000
3000802c
30008030
30008040
A
A
A
D
A
A
A
A
T
t
T
d
d
b
__bss_end__
__bss_start
__bss_start__
__data_start
__end__
_bss_end__
_edata
_end
_start
add3_end
add3_func
in1
in2
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.
80
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/*sum1.s***************************************************/
/* somma di un vettore di numeri
*/
/**********************************************************/
.text
.equ ELEM, 10
@ numero di elementi del vettore
.global
_start
_start:
.global sum1_func
sum1_func:
mov
r0, #0
@ r0 conterrà la somma
ldr
r8, =vec
@ indirizzo del primo elemento
mov
r7, #ELEM
sub
r7, r7, #1
@ indice dell’ultimo elemento
add
r7, r8, r7, lsl #2 @ indirizzo dell’ultimo elem.
loop:
ldr
r1, [r8], #4 @ carica un elemento
add
r0,r0,r1
@ lo somma in r0
cmp
r7, r8
@ era l’ultimo?
bpl
loop
@ no: carica il prossimo
ldr
r8, =out
@ indirizzo del risultato
str
r0, [r8]
@ memorizza il risultato
sum1_end:
b
sum1_end
@ trappola
vec:
out:
.data
.long
.bss
.align
.space
.end
10,56,89,35,67,54,9,29,37,72 @ elementi
4
4
@ spazio per il risultato
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
81
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.
82
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/*sum2.s***************************************************/
/* somma di un vettore di numeri
*/
/**********************************************************/
.text
.equ
ELEM, 10
@ numero di elementi del vettore
.global _start
_start:
.global sum2_func
sum2_func:
ldr
sp, =stack
mov
r2, #ELEM
ldr
r1, =vec
bl
somma
ldr
r8, =out
str
r0, [r8]
sum2_end:
b
sum2_end
@ chiama la subroutine somma
@ indirizzo del risultato
@ memorizza il risultato
@ trappola
.func somma
/*--------------------------------------------------------*/
/* calcola la somma degli elementi di un vettore
*/
/* input:
*/
/* R1: indirizzo del primo elemento del vettore
*/
/* R2: numero di elementi del vettore
*/
/* output:
*/
/* R0: somma
*/
/*--------------------------------------------------------*/
somma:
stmfd sp!, {r2,r3}
@ salva nello stack i registri
bic
r0, r0, r0
sub
r2, r2, #1
@ indice dell’ultimo elemento
somma_l: ldr
r3, [r1, r2, lsl #2] @ carica un elemento
add
r0,r0,r3
@ lo somma in r0
subs r2, r2, #1
@ decrementa l’indice
bge
somma_l
@ se >=0, carica il prossimo
ldmfd sp!, {r2,r3}
@ ripristina i registri salvati
mov
pc, lr
@ ritorna al programma chiamante
.endfunc
.data
vec:
.long
1,2,3,4,5,6,7,8,9,10 @ elementi
.bss
.align 4
out:
.space
.space
stack:
.space
4
4096
4
@ spazio per il risultato
@ spazio per lo stack
@ base dello stack
.end
83
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
84
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.
85
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
86
/*sum3.s***************************************************/
/* somma di un vettore di numeri
*/
/**********************************************************/
.text
.equ
ELEM, 10
@ numero di elementi del vettore
.global _start
_start:
.global sum3_func
sum3_func:
ldr
sp, =stack
mov
r2, #ELEM
ldr
r1, =vec
bl
somma
ldr
r8, =out
str
r0, [r8]
sum3_end:
b
sum3_end
@
@
@
@
@
@
inizializza lo stack pointer
numero di elementi
indirizzo del primo elemento
chiama la subroutine somma
indirizzo del risultato
memorizza il risultato
@ trappola
.func somma
/*--------------------------------------------------------*/
/* funzione ricorsiva per la somma degli el. di un vettore*/
/* input:
*/
/* R1: indirizzo del primo elemento del vettore
*/
/* R2: numero di elementi del vettore
*/
/* output:
*/
/* R0: somma
*/
/*--------------------------------------------------------*/
somma:
stmfd sp!, {r1-r3,lr} @ salva i registri (anche lr)
cmp
r2, #1
@ c’è un solo elemento?
bhi
somma_split
@ no: procede con l’algoritmo
ldr
r0, [r1]
@ si: la somma è l’elemento e ...
b
somma_end
@ la subr. termina restituendolo
somma_split:
@ divide a metà il vettore
mov
r3, r2, lsr #1
@ n. di elem. della seconda metà
sub
r2, r2, r3
@ n. di elem. della prima metà
bl
somma
@ somma della prima metà in r0
add
r1, r1, r2, lsl #2 @ indirizzo della seconda metà
mov
r2, r3
@ n. di elem. della seconda metà
mov
r3, r0
@ in r3 la somma della prima metà
bl
somma
@ somma della seconda metà in r0
add
r0, r0, r3
@ somma delle due metà
somma_end:
ldmfd sp!, {r1-r3,pc} @ ripristina i registri (lr->pc)
.endfunc
.data
vec:
.long
1,2,3,4,5,6,7,8,9,10 @ elementi
.bss
.align 4
out:
.space 4
.space 4096
stack:
.space 4
@ spazio per il risultato
@ spazio per lo stack
@ base dello stack
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
87
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
88
/*strrev.s*************************************************/
/* rovesciamento di una stringa di caratteri
*/
/**********************************************************/
.text
.global _start
_start:
.global strrev_func
strrev_func:
ldr
sp, =stack
@ inizializza lo stack pointer
ldr
r1, =str
@ indirizzo del primo elemento
bl
strrev
@ chiama la subroutine strrev
strrev_end:
b
strrev_end
@ trappola
.func strrev
/*--------------------------------------------------------*/
/* funzione che rovescia una stringa: la stringa sia in
*/
/* input che output è terminata da un byte nullo:"\0"(EOS)*/
/* input/output:
*/
/* R1: indirizzo primo carattere della stringa
*/
/*--------------------------------------------------------*/
strrev:
stmfd sp!,{r0-r3,lr} @ salva i registri (anche lr)
mov
r3, r1
@ salva l’indirizzo della stringa
mov
r2, sp
@ salva sp
strrev_l:
ldrb r0, [r1], #+1 @ preleva un carattere, post-inc
strb r0, [sp, #-1]! @ lo inserisce nello stack, pre-dec
tst
r0,#0xff
@ si tratta di EOS?
bne
strrev_l
@ no: preleva il car. successivo
mov
r1,r3
@ si: ripristina r1 (ind. stringa)
add
sp,sp, #1
@ elimina dallo stack l’EOS
strrev_l2:
ldrb r0, [sp], #+1 @ pop un car. dallo stack, post-inc
strb r0, [r1], #+1 @ lo inserisce nella str., post-inc
cmp
sp,r2
@ sp è tornato al val. originario?
blo
strrev_l2
@ no: pop dallo stack il prossimo
ldmfd sp!,{r0-r3,pc} @ ripristina i registri (lr->pc)
/*--------------------------------------------------------*/
.endfunc
.data
str:
.ascii
"Architettura degli elaboratori \0"
.bss
.align 4
.space
4096
@ spazio per lo stack
stack:
.space 4
@ base dello stack
.end
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).
89
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
90
/*strrev2.s************************************************/
/* rovesciamento di una stringa di caratteri
*/
/**********************************************************/
.text
.global _start
_start:
.global strrev2_func
strrev2_func:
ldr
sp, =stack
@ inizializza lo stack pointer
ldr
r1, =str
@ indirizzo del primo elemento
bl
strrev
@ chiama la subroutine strrev
strrev2_end:
b
strrev2_end
@ trappola
.func strrev
/*--------------------------------------------------------*/
/* funzione che rovescia una stringa: la stringa sia in
*/
/* input che output è terminata da un byte nullo:"\0"(EOS)*/
/* input/output:
*/
/* R1: indirizzo primo carattere della stringa
*/
/*--------------------------------------------------------*/
strrev:
stmfd sp!,{r0-r4,lr} @ salva i registri (anche lr)
mov
r2, r1
@ in r2 l’indirizzo della stringa
strrev_l0:
ldrb r0, [r2], #+1 @ preleva un carattere, post-inc
tst
r0, #0xff
@ si tratta di EOS?
bne
strrev_l0
@ no: preleva il car. successivo
sub
r2, r2, r1
@ si: in r2 n. car. str. (con EOS)
bic
r4, r2, #3
@ azzera i bit 0 ed 1
cmp
r2, r4
@ se uguali: r2 è un multiplo di 4
addne r4, r4, #4
@ se no: in r4 il mult. successivo
mov
r3, sp
@ in r3 lo sp
sub
sp, sp, r4
@ alloca area riservata sullo stack
@ r3 punta alla base di quest’area
@ sp rimane allineato a mult. di 4
strrev_l1:
ldrb r0, [r1], #+1 @ preleva un carattere, post-inc
strb r0, [r3, #-1]! @ lo inserisce nello stack, pre-dec
tst
r0, #0xff
@ si tratta di EOS?
bne
strrev_l1
@ no: preleva il car. successivo
add
r3, r3, #1
@ elimina l’EOS
sub
r1, r1, r2
@ r1 punta di nuovo al primo car.
sub
r2, r2, #1
@ n. di car. da copiare (senza EOS)
strrev_l2:
ldrb r0, [r3], #+1 @ preleva car. da stack, post-inc
strb r0, [r1], #+1 @ lo inserisce nella str., post-inc
subs r2, r2, #1
@ decrementa il contatore di car.
bne
strrev_l2
@ preleva il prossimo se cont ? 0
add
sp, sp, r4
@ rimuove l’area dallo stack
ldmfd sp!,{r0-r4,pc} @ ripristina i registri (lr->pc)
/*--------------------------------------------------------*/
54
55
56
57
58
59
60
61
62
.endfunc
.data
str:
.ascii
.bss
.align 4
.space
stack:
.space
.end
"Architettura degli elaboratori \0"
4096
4
@ spazio per lo stack
@ base dello stack
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.
91
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
92
/*msort_b.s************************************************/
/* merge-sort di un vettore di naturali (da un byte)
*/
/**********************************************************/
.text
.global _start
.equ
ELEM, 10
@ numero di elementi del vettore
ldr
sp, =stack
@ inizializza lo stack pointer
ldr
mov
bl
b
r1, =vec
r2, #ELEM
sort
_end
@
@
@
@
_start:
_end:
indirizzo del vettore
numero di elementi del vettore
chiama la subroutine sort
trappola
.func sort
/*--------------------------------------------------------*/
/* funzione che ordina i componenti di un vettore
*/
/* input/output:
*/
/* R1: indirizzo del primo elemento del vettore
*/
/* R2: lunghezza del vettore
*/
/*--------------------------------------------------------*/
sort:
stmfd sp!, {r1-r6,lr} @ salva i registri (anche lr)
cmp
r2, #1
@ c’è un solo elemento?
beq
sort_end
@ si: fine subr. (il vet. è ordinato)
sort_split:
@ no: procede con l’algoritmo
@ r1,r2 indirizzo e lunghezza della prima metà del vettore
@ r3,r4 indirizzo e lunghezza della seconda metà del vettore
mov
r4, r2
mov
r2, r2, lsr #1 @ r2=r2/2 (n. el. della prima metà)
sub
r4, r4, r2
@ n. di elementi della seconda metà
add
r3, r1, r2
@ ind. del primo el. seconda metà
@ ordina la prima metà:
bl
sort
mov
r5, r1
@ salva r1, r2 in r5, r6
mov
r6, r2
mov
r1, r3
@ copia r3, r4 in r1, r2
mov
r2, r4
@ ordina la seconda metà:
bl
sort
mov
r1, r5
@ ripristina r1, r2 (prima metà)
mov
r2, r6
@ fonde le due metà già ordinate
bl
merge
sort_end:
ldmfd sp!, {r1-r6,pc} @ ripristina i registri (lr->pc)
/*--------------------------------------------------------*/
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
.endfunc
.func merge
/*--------------------------------------------------------*/
/* funzione che fa il merge di due vettori contigui gia` */
/* ordinati mantenendo l'ordinamento
*/
/* input:
*/
/* R1: indirizzo del primo elemento del primo vettore
*/
/* R2: lunghezza del primo vettore
*/
/* R3: indirizzo del primo elemento del secondo vettore */
/* R4: lunghezza del secondo vettore
*/
/* output:
*/
/* R1: indirizzo del primo elemento del vettore ordinato */
/* R2: lunghezza del vettore ordinato
*/
/*--------------------------------------------------------*/
merge:
stmfd sp!,{r0-r7,lr} @ salva i registri (anche lr)
sub
r0, sp, #1
@ r0: ind. primo byte sopra lo stack
add
r7, r2, r4
@ lunghezza del vettore ordinato
sub
sp, sp, r7
@ area di lavoro sullo stack
bic
sp, sp, #3
@ allinea sp al multiplo di 4
@ immediatamente inferiore o uguale
sub
r2, r2, #1
@ indice dell’ultimo el. del 1° vett.
sub
r4, r4, #1
@ indice dell’ultimo el. del 2° vett.
ldrb r5, [r1, r2]
@ preleva l’ultimo el. del 1° vett.
ldrb r6, [r3, r4]
@ preleva l’ultimo el. del 2° vett.
merge_loop:
cmp
r5, r6
@ confronta i due elementi prelevati
blo
merge_v2
@ salta se r5 < r6 (cmp tra naturali)
merge_v1:
@ copia nell’area di lavoro l’elemento del 1° vettore
strb r5, [r0], #-1
@ copia, post-dec
subs r2, r2, #1
@ indice el. precedente del 1° vett.
ldrgeb
r5, [r1, r2] @ lo preleva, posto che ci sia
blt
copy_v2
@ se non c’è, copia il resto del 2°
b
merge_loop
@ confrontalo con quello del 2° vet.
merge_v2:
@ copia nell’area di lavoro l’elemento del 2° vettore
strb r6, [r0], #-1
@ copia, post-dec
subs r4, r4, #1
@ indice el. precedente del 2° vett.
ldrgeb
r6, [r3, r4] @ lo preleva, posto che ci sia
blt
copy_v1
@ se non c’è, copia il resto del 1°
b
merge_loop
@ confrontalo con quello del 1° vet.
copy_v1:
@ copia nell’area di lavoro gli elementi restanti del 1°
strb r5, [r0], #-1
@ copia, post-dec
subs r2, r2, #1
@ indice el. precedente del 1° vett
ldrgeb
r5, [r1, r2] @ lo preleva, posto che ci sia
bge
copy_v1
@ se c’è, prova a copiarne un altro
b
merge_end
@ altrimenti il merge è finito
copy_v2:
@ copia nell’area di lavoro gli elementi restanti del 2°
strb r6, [r0], #-1
@ copia, post-dec
subs r4, r4, #1
@ indice el. precedente del 2° vett
ldrgeb
r6, [r3, r4] @ lo preleva, posto che ci sia
bge
copy_v2
@ se c’è, prova a copiarne un altro
b
merge_end
@ altrimenti il merge è finito
merge_end: @ ricopia il vett. ordinato al posto del vett. di input
add
r2, r0, #1
@ r2 punta all’ultimo el. inserito
mov
r3, #0
@ r3 è l’offset
mcopy_l: ldrb r0, [r2, r3]
@ carica l’elem. Dall’area di lavoro
strb r0, [r1, r3]
@ lo ricopia nel vettore di input
add
r3, r3, #1
@ incrementa l’offset
cmp
r3, r7
@ ricopiato tutto?
blo
mcopy_l
@ no: ricopia il prossimo
add
sp, sp, r7
@ si: rimuove l’area di lavoro
tst
sp, #3
@ sp è allineato a multiplo di 4?
93
114
115
116
117
118
119
120
121
122
123
124
125
126
127
bicne sp, sp, #3
@ no: multiplo immediatamente infer.
addne sp, sp, #4
@ adesso è quello giusto
ldmfd sp!,{r0-r7,pc}
@ ripristina i registri (lr->pc)
/*--------------------------------------------------------*/
.endfunc
.data
vec:
.byte
8,6,5,9,7,3,4,1,10,2 @ elementi
.bss
stack:
.end
.align 4
.space 4096
.space 4
@ spazio per lo stack
@ base dello stack
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.
94
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
2
3
4
5
6
7
8
9
10
11
12
13
14
/*led1.s***************************************************/
.include "front.inc"
.global _start
_start:
.global led1_func
led1_func:
mov
r1, #LED_ADDR @ indirizzo dei led e degli switch
loop:
ldrh r0, [r1]
@ legge lo stato degli switch
strh r0, [r1]
@ lo scrive sui led
b loop
@ indefinitamente
led1_end:
b led1_end
@ trappola
.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
2
3
4
5
6
7
8
9
10
11
12
13
14
/*front.inc*************************************************/
LED_ADDR = 0x28000000 @ indirizzo dei led e switch (halfword)
BUT_CONF = 0x56000050 @ indirizzo del registro di configurazione pin GPIO
S3_CONF_MASK = 0x000C @ maschera per la configurazione del pulsante S3
S4_CONF_MASK = 0x00C0 @ maschera per la configurazione bit del pulsante S4
BUT_DATA = 0x56000054 @ indirizzo del registro dati pin GPIO
S3_DATA_MASK = 0x0002 @ maschera per leggere lo stato del pulsante S3
S4_DATA_MASK = 0x0008 @ maschera per leggere lo stato del pulsante S4
BUT_PULL = 0x56000058 @ indirizzo del registro di configurazione resistenze
S3_DATA_MASK = 0x0002 @ maschera per disabilitare la resistenza di pullup S3
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.
95
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.
96
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/*led2.s***************************************************/
.include "front.inc"
.global _start
_start:
.global led2_func
led2_func:
ldr
sp, =stack
@ inizializza lo stack pointer
ldr
r1, =BUT_CONF
@ configura il pin del GPIO in input
ldr
r0, [r1]
mov
r2, #S3_CONF_MASK
orr
r2, r2, #S4_CONF_MASK
mov
r3, #0
sub
r3, r3, #1
eor
r2, r2, r3
and
r0, r0, r2
str
r0, [r1]
mov
r1, #LED_ADDR
@ indirizzo di led e switch
movs
r0, #1
strh
r0, [r1]
@ illumina il led di sinistra
ldr
r3, =BUT_DATA
@ indirizzo dei pin GPIO
loop:
bl
delay
@ lascia passare un po' di tempo
ldr
r2, [r3]
@ legge lo stato dei pulsanti
tst
r2, #S3_DATA_MASK
@ è premuto S3?
moveq
r0, r0, ror #31 @ si: ruota verso sinistra
tst
r2, #S4_DATA_MASK
@ è premuto S4?
moveq
r0, r0, ror #1 @ si: ruota verso destra
@ azzera i bit che escono dall'area visibile (word meno signif.)
bics
r0, r0, #0x00010000 @ bit uscito a sinistra
bicnes r0, r0, #0x80000000 @ bit uscito a destra
strh
r0, [r1]
@ riscrive sui led
bne
loop
@ continua se non nullo
led2_end:
b led2_end
@ trappola
.func delay
delay:
stmfd
mov
d_loop: subs
bne
ldmfd
mov
.endfunc
.bss
.space
stack:
.space
@ introduce un ritardo (~ 0.1 s)
sp!, {r0}
@ 20.185.088 * 2 / 400MHz ~= 0.1 sec
r0, #0x01340000 @ 0x01340000 = 20.185.088
r0, r0, #1
d_loop
sp!, {r0}
pc, lr
4096
4
97
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.
98
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/*lcd.s****************************************************/
.global lcd_init_16bpp, lcd_enable, lcd_disable, lcd_gpio_init
.text
.func lcd_init_16bpp
/**********************************************************/
/* lcd_init_16bpp: inizializza il display per 16M colori */
/*
*/
/* input: r8 indirizzo del framebuffer
*/
/* output: r8 indirizzo del framebuffer
*/
/**********************************************************/
WIDTH = 800
HEIGHT = 480
LCDCON0
LCDCON1
LCDCON2
LCDCON3
LCDCON4
=
=
=
=
=
0x4d000000
0x4d000004
0x4d000008
0x4d00000c
0x4d000010
LCDSADDR1 = 0x4d000014
LCDSADDR2 = 0x4d000018
LCDSADDR3 = 0x4d00001c
DITHMODE = 0x4d00004c
lcd_init_16bpp:
stmfd sp!, {r0, r1, r2, r3, lr}
@ configuriamo il framebuffer
mov
r0, r8
bic
r0, r0, #0xC0000000
mov
r0, r0, lsr #21
mov
r0, r0, asl #21
mov
r0, r0, lsr #1
mov
r1, r8
mov
r2, #0xFF000000
bic
r1, r1, r2, asr #3
mov
r1, r1, lsr #1
orr
r2, r0, r1
ldr
r3, =LCDSADDR1
str
r2, [r3]
mov
mov
mul
add
mov
bic
mov
mov
r1,
r2,
r2,
r0,
r1,
r0,
r0,
r0,
#WIDTH
@ calcolo l'end address
#HEIGHT
r1, r2
r8, r2, lsl #1
r0
r0, #0xC0000000
r0, lsr #21
r0, asl #21
99
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
100
mov
mov
bic
mov
orr
add
str
r0,
r2,
r1,
r1,
r2,
r3,
r2,
r0, lsr #1
#0xFF000000
r1, r2, asr #3
r1, lsr #1
r0, r1
r3, #4
[r3]
add
mov
str
r3, r3, #4
r2, #0
r2, [r3]
@ impostazione dei valori di timing TFT
ldr
r0, =lcdcon_16bpp
ldr
r1, =LCDCON0
ldmia r0, {r2, r3, r4, r5, r6}
stmia r1, {r2, r3, r4, r5, r6}
@ clear dithering mode for TFT
mov
r0, #0
ldr
r1, =DITHMODE
str
r1, [r0]
ldmfd sp!,{r0, r1, r2, r3, lr}
mov
pc,lr
.endfunc
.func lcd_enable
/**********************************************************/
/* lcd_enable: attiva il controller LCD
*/
/**********************************************************/
LCDCON0 = 0x4d000000
lcd_enable:
ldr
ldr
tst
bne
orr
str
enabled:
mov
.endfunc
r1, =LCDCON0
r0, [r1]
r0, #0x0001
enabled
r0, r0, #0x0001
r0, [r1]
pc, lr
.func lcd_disable
/**********************************************************/
/* lcd_disable: disattiva il controller LCD
*/
/**********************************************************/
LCDCON0 = 0x4d000000
lcd_disable:
ldr
ldr
tst
beq
orr
str
disabled:
mov
.endfunc
r1, =LCDCON0
r0, [r1]
r0, #0x0001
disabled
r0, r0, #0x0001
r0, [r1]
pc, lr
@ se non è già disattivo
@ imposta il campo ENABLE
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
.func lcd_gpio_init
/**********************************************************/
/* Setup GPIO to act as LCD interface
*/
/* using as LCD interface
*/
/* alternate function 2
*/
/**********************************************************/
GPCCON = 0x56000020
GPCCONF = 0xAAAAAAAA
GPDCON = 0x56000030
GPDCONF = 0xAAAAAAAA
lcd_gpio_init:
ldr
r1, =GPCCON
ldr
r0, =GPCCONF
str
r0, [r1]
ldr
ldr
str
r1, =GPDCON
r0, =GPDCONF
r0, [r1]
mov
pc,lr
.endfunc
.data
lcdcon_16bpp:
.long 0x00000078
.long 0x0477c102
.long 0x00631f07
.long 0x00000002
.long 0x00000b01
.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:
101
colori
2
4
16
256
65536
16777216
bpp
1
2
4
8
16
24
palette
SI
SI
SI
SI
NO
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.
102
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/*color1.s*************************************************/
WIDTH = 800
HEIGHT = 480
.text
.global _start
_start:
.global color1_func
color1_func:
ldr
sp, =stack
@ inizializza lo stack pointer
ldr
r8, =frame_buffer @ inizializza il puntatore al FB
bl
bl
lcd_init_16bpp
lcd_enable
@ in r8: ind. del FB
@ enable TFT display
mov
mov
mov
r3, #WIDTH/4
r4, #HEIGHT/4
r0, #0
@ 1/4 della larghezza dello schermo
@ 1/4 della altezza dello schermo
@ colore (16bpp)
bic
mul
mov
mul
bl
add
cmp
blt
r5, r0, #0x0C
r1, r5, r3
r5, r0, lsr #2
r2, r5, r4
draw_box_16bpp
r0, r0, #1
r0, #16
m_loop
@
@
@
@
@
@
@
@
b
color1_end
@ trappola
m_loop:
2 LSb dell'indice colore: 0,1,2,3
ascissa vertice del rettangolo
2 MSb dell'indice colore: 0,1,2,3
ordinata vertice del rettangolo
disegna il rettangolo
incrementa l'indice colore
era l'ultimo colore?
no: disegna un altro rettangolo
color1_end:
.func draw_box_16bpp
/*************************************************************/
/* r0 : indice colore
*/
/* r1 : ascissa x (in pixel) del vertice superiore sinistro */
/* r2 : ordinata y (in pixel) del vertice superiore sinistro */
/* r3 : larghezza (in pixel) del rettangolo
*/
/* r4 : altezza (in pixel) del rettangolo
*/
/* r8 : indirizzo del framebuffer (FB)
*/
/*************************************************************/
draw_box_16bpp:
stmfd sp!,{r0,r3-r8}
ldr
r7, =palette_4bpp
@ scelgo il colore nella palette
add
r7, r7, r0, lsl #1
ldrh
r0, [r7]
mov
r7, #WIDTH
@ n. pixel in 1 riga dello schermo
mul
r5, r2, r7
@ n. pixel in y righe (da saltare) (short)
add
r5, r5, r1
@ + x pixel della riga y
add
r8, r8, r5, lsl #1 @ r8 = indirizzo di memoria del
@ vertice superiore sinistro del
@ rettangolo (nel framebuffer)
add
r0, r0, r0, lsl #16 @ 2 pixel uguali nel LSB di r0
sub
r3, r3, #1
@ contatore di colonne (pixel/riga
sub
r4, r4, #1
@ contatore di righe del box
mov
r6, r3
@ salva il contatore di colonne
103
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
db_loop:
mul
add
mov
strh
r5,
r5,
r5,
r0,
r7, r4
r5, r3
r5, lsl #1
[r8, r5]
subs
bpl
mov
subs
bpl
r3, r3, #1
db_loop
r3, r6
r4, r4, #1
db_loop
@ scrive nel FB: parte dal vertice inf dx
@ indice (pxl) di riga (parte dall'ultima)
@ indice (pxl) di colonna (dall'ultima)
@
@
@
@
@
decrementa l'indice di colonna
scrivi altri pxl della riga
fine riga: ripristina il cont. di col.
decrementa l'indice di riga
scrivi un'altra riga
ldmfd sp!,{r0,r3-r8}
mov
pc,lr
.endfunc
.data
.align 4
palette_4bpp:
.short
0x0000
.short
0x7800
.short
0x01e0
.short
0x79e0
.short
0x000f
.short
0x780f
.short
0x03ef
.short
0x39e7
.short
0x7bef
.short
0xf800
.short
0x07e0
.short
0xffe0
.short
0x001f
.short
0xf81f
.short
0x07ff
.short
0xffff
@
@
@
@
@
@
@
@
@
@
@
@
@
@
@
@
black
dark red
dark green
dark yellow
dark blue
dark magenta
dark cyan
dark grey
gray
red
green
yellow
blue
magenta
cyan
white
.bss
.align
frame_buffer:
.space
.space
stack:
.space
.end
9.12.1
8
800*480*2
4096
@ spazio per lo stack
4
@ base dello stack
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.
104
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.
105
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
106
/*color2.s*************************************************/
WIDTH = 800
HEIGHT = 480
.text
.global _start
_start:
.global color2_func
color2_func:
ldr
sp, =stack
@ inizializza lo stack pointer
ldr
r8, =frame_buffer @ inizializza il puntatore al FB
bl
bl
lcd_init_16bpp
lcd_enable
@ in r8: ind. del FB
@ enable TFT display
mov
mov
mov
r3, #WIDTH/16
r4, #HEIGHT/16
r0, #0
@ 1/16 della larghezza dello schermo
@ 1/16 della altezza dello schermo
@ colore (16bpp)
bic
mul
mov
mul
bl
add
cmp
ble
r5, r0, #0xf0
r1, r5, r3
r5, r0, lsr #4
r2, r5, r4
draw_box_16bpp
r0, r0, #1
r0, #256
m_loop
@
@
@
@
@
@
@
@
b
color2_end
@ trappola
m_loop:
4 LSb dell'indice colore: 0..15
ascissa vertice del rettangolo
4 MSb dell'indice colore: 0..15
ordinata vertice del rettangolo
disegna il rettangolo
incrementa l'indice del colore
era l'ultimo colore?
no: disegna un altro rettangolo
color2_end:
.func draw_box_16bpp
/*************************************************************/
/* r0 : indice colore
*/
/* r1 : ascissa x (in pixel) del vertice superiore sinistro */
/* r2 : ordinata y (in pixel) del vertice superiore sinistro */
/* r3 : larghezza (in pixel) del rettangolo
*/
/* r4 : altezza (in pixel) del rettangolo
*/
/* r8 : indirizzo del framebuffer (FB)
*/
/*************************************************************/
draw_box_16bpp:
stmfd sp!,{r0,r3-r8}
ldr
r7, =palette_8bpp
@ scelgo il colore nella palette
add
r7, r7, r0, lsl #1
ldrh
r0, [r7]
mov
r7, #WIDTH
@ n. pixel in 1 riga dello schermo
mul
r5, r2, r7
@ n. pixel in y righe (da saltare) (short)
add
r5, r5, r1
@ + x pixel della riga y
add
r8, r8, r5, lsl #1 @ r8 = indirizzo di memoria del
@ vertice superiore sinistro del
@ rettangolo (nel framebuffer)
add
r0, r0, r0, lsl #16 @ 2 pixel uguali nel LSB di r0
sub
r3, r3, #1
@ contatore di colonne (pixel/riga
sub
r4, r4, #1
@ contatore di righe del box
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
mov
r6, r3
mul
add
mov
strh
r5,
r5,
r5,
r0,
subs
bpl
mov
subs
bpl
r3, r3, #1
db_loop
r3, r6
r4, r4, #1
db_loop
db_loop:
r7, r4
r5, r3
r5, lsl #1
[r8, r5]
@ salva il contatore di colonne
@ scrive nel FB: parte dal vertice inf dx
@ indice (pxl) di riga (parte dall'ultima)
@ indice (pxl) di colonna (dall'ultima)
@
@
@
@
@
decrementa l'indice di colonna
scrivi altri pxl della riga
fine riga: ripristina il cont. di col.
decrementa l'indice di riga
scrivi un'altra riga
ldmfd sp!,{r0,r3-r8}
mov
pc,lr
.endfunc
.bss
.align
frame_buffer:
.space
.space
stack:
.space
.end
8
800*480*2
4096
@ spazio per lo stack
4
@ base dello stack
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
2
3
4
5
6
7
8
9
10
11
12
13
14
/*palette256.s***********************************************/
.global palette_8bpp
.data
.align 4
palette_8bpp:
.short 0x0420
@
0 33
0
.short 0x0380
@
0 28
0
.short 0x02c0
@
0 22
0
.short 0x0200
@
0 16
0
.short 0x0160
@
0 11
0
.short 0x28a0
@
5
5
0
.short 0x5800
@ 11
0
0
.short 0x8000
@ 16
0
0
.short 0x03e0
@
0 31
0
...
153
154
155
156
157
158
259
260
261
262
263
.short
.short
.short
.short
.short
.short
.short
.short
.short
0xfb3f
0x0fdf
0x3f1f
0x667f
0x95bf
0xc4ff
0xec5f
0xfb9f
0xfadf
@
@
@
@
@
@
@
@
@
31
1
7
12
18
24
29
31
31
25
62
56
51
45
39
34
28
22
31
31
31
31
31
31
31
31
31
.end
107
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.
108
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
2
3
4
/*font_tab.inc*********************************************/
FONT_LINE = 160
@ full pixmap lenght in pixel
FONT_H = 12
@ altezza, in pixel, di un carattere
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/*font1.s**************************************************/
WIDTH = 800
HEIGHT = 480
.global _start
.include "font_tab.inc"
.text
_start:
.global font1_func
font1_func:
ldr
sp, =stack
@ inizializza lo stack pointer
ldr
r8, =frame_buffer @ r8=0 (framebuffer predefinito)
bl
bl
lcd_init_16bpp
lcd_enable
@ in r8: indirizzo del FB
mov
mov
r0, #0
r2, #0
@ cifra esadec. (nei 4 LSb di r0)
@ posiz. verticale (indice di riga)
mov
r1, #0
@ posiz. orizzont. (indice di col.)
add
bic
bl
add
cmp
ble
add
cmp
ble
r0, r1, r2
@ incrementa la cifra esadecimale
r0, r0, #0x0ff0
@ isola i 4 bit (nibble) meno sign.
putnibble
@ scrive il carattere sullo schermo
r1, r1, #1
@ incrementa la posizione orizzont.
r1, #WIDTH/FONT_W-1 @ oltre l'ultima pos. nella riga?
l2
@ no: prosegue con car. successivo
r2, r2, #1
@ si: incrementa la posizione vert.
r2, #HEIGHT/FONT_H-1 @ oltre l'ultima pos. nella col.?
l1
@ no: prosegue con riga successiva
l1:
l2:
font1_end:
b
_end
@ trappola
.func putnibble
109
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
110
/******************************************************************/
/* calcola la pos. nel FB (480 righe × 800 colonne) ove scrivere */
/* input : R0 valore (esadecimale) da scrivere
*/
/*
R1 posizione orizzontale [0..799] (indice di col.)
*/
/*
R2 posizione verticale [0..479] (indice di riga)
*/
/*
R8 indirizzo del framebuffer
*/
/******************************************************************/
putnibble:
stmfd sp!,{r1-r4,lr}
cmp
r1, #WIDTH/FONT_W-1 @ verifica se ci sta nella riga
bhi
pnb_end
@ fuori schermo (lateralmente)
cmp
r2, #HEIGHT/FONT_H-1 @ verifica se ci sta nella col.
bhi
pnb_end
@ fuori schermo (verticalmente)
cmp
r0, #FONT_LINE/FONT_W-1
bhi
pnb_end
@ valore fuori range
mov
ldr
mul
r3, r1
r4, =FONT_W
r1, r3, r4
@
@
@
@
ldr
mul
r4, =FONT_H*WIDTH
r3, r2, r4
@
@
add
r1, r1, r3
@
add
r1, r8, r1, lsl #1 @
@
bl
drawnibble
pnb_end: ldmfd sp!,{r1-r4,pc}
.endfunc
posiz. oriz. (in larghezze car.)
larg. dei caratteri (in pixel)
r1 = offset orizz. (in pixel)
del carattere da scrivere
@ n. di pixel in una riga di car.
r3 = offset verticale (in pixel)
dell'inizio riga in cui scrivere
pos. (pixel) del car. da scriver
indir. di mem. in cui scrivere
il carattere (2 byte/pixel)
.func drawnibble
/**************************************************************/
/* disegna un carattere sullo schermo
*/
/* input : R0 valore (esadecimale) da scrivere
*/
/*
R1 indirizzo di mem. in cui scrivere il carattere */
/**************************************************************/
drawnibble:
stmfd sp!,{r0,r2-r6,lr}
ldr
r3, =FONT_W*2 @ larg. car. (in byte: 565 a 16 bit)
mul
r2, r0, r3
@ offset orizz.(byte) del car. in TAB
ldr
add
ldr
r3, =gimp_image @ inizio della tabella (TAB)
r2, r2, r3
@ indir. I riga del car. (in TAB)
r3, =FONT_H-1 @ indice ultima riga car (di pixel)
ldr
r4, =FONT_W-1
ch_1:
@ indice ultima colonna (di pixel)
ch_2:
ldr
r6, =FONT_LINE @ n. pixel di una riga dei 16 car
mul
r5, r3, r6
@ offset vert.(pixel) dell'ultima riga
@ r5=offset vert. (pxl in TAB) dell'inizio ultima riga dei 16 car.
mov
r6, #WIDTH
mul
r6, r3, r6
@ r6=offset vert. (pxl su schermo) inizio ultima riga dei 16 car.
add
r6, r6, r4 @ r6=offset su sch. r4-esimo pxl del car.
add
r5, r5, r4 @ r5=offset in TAB r4-esimo pxl del car.
@ inizia dal vertice inferiore destro (VID) del carattere
mov
ldrh
mov
mov
r5,
r0,
r5,
r6,
r5, lsl #1
[r2, r5] @ carica pixel del car. da TAB
r5, lsr #1
r6, lsl #1
@ offset su schermo (2byte/pxl)
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
strh
subs
bge
subs
bge
ldmfd
r0, [r1, r6]
r4, r4, #1
ch_2
r3, r3, #1
ch_1
sp!,{r0,r2-r6,pc}
@
@
@
@
@
lo scrive nel FB
decrementa indice di colonna
cicla se non era l'ultima
decrementa indice di riga
cicla se non era l'ultima
.endfunc
.bss
.align 8
frame_buffer:
.space 800*480*2
.space
4096
stack:
.space
4
.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
111
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.
112
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
113