Sei sulla pagina 1di 68

PIC Intel 8259A

Studieremo un PIC prodotto dall’Intel stessa, vediamo com’è fatto. Il PIC è un manager: accetta
richieste dalle periferiche, facendo riferimento alle priorità delle richieste e inoltrando quella che
ritiene più urgente al processore.

•D7-D0: (data
bus) alla CPU,
anche se
l’8086 ne ha
16 si usano gli
8 più bassi,
vengono usati
per
trasmettere
control, status
e interrupt
vector
informations.
•INT:
interrompe
•INTA: ack

dell’interruzione, 2 impulsi negativi


•RD: CPU chiede lo stato (IMR/ISR/IRR/Priorità) su data bus
•WR: CPU ha inviato comando su data bus
•CS: chip select (insieme con S2 fornisce il ready). Abilita RD, WR da verso il processore
•A0: fa parte del bus indirizzi, è utilizzato insieme ad altri pin dal PIC per “decifrare” control words
mandate dal processore o status words che il processore vorrebbe leggere.
•IR7/IR0: linee interrupt da periferiche
•CAS2-CAS0: identificazione slave
•SP/EN: non in buffered mode, 1=master, 0=slave; in buffered mode=disabilita bus dati
L’Interrupt request register memorizza tutti i livelli che stanno richiedendo attenzione, è un byte,
l’in service register invece memorizza tutti i livelli di interruzione che vengono soddisfatti in quel
1
momento, sempre uno alla volta. Il priority resolver decide a chi tocca in base a queste
informazioni e abbassa il bit corrispondente in IRR e lo alza in ISR. L’ interrupt mask register
(OCW1) conserva i bit che indicano quali linee sono mascherate.
Vediamo il flusso di gestione di un interruzione: una o più IRQ vengono alzate, il PIC valuta le
richieste e se possibile alza INT verso il processore; il processore abbassa per due volte INTA ad
indicare alla logica precedentemente descritta che nel primo impulso il dispositivo a priorità più
alta deve avere ISR a uno e IRR a zero, mentre dopo il secondo, sul fronte di discesa, sui D viene
caricato ICW2, che ci manderà alla routine di gestione. Questo è ciò che avviene in un 8086. Se
arriva un interruzione a priorità superiore quando se ne sta servendo un'altra, è possibile un
cambio di contesto.
Il processore viene interrotto sulla linea di interruzione e questo è il primo collegamento, base.
Dopo di che dall’indirizzo si va a capire se stiamo parlando proprio di questo dispositivo, perché i
dispositivi hw vengono cablati sulla scheda madre; il PIC avrà un indirizzo particolare, quindi se il
decoder riconosce questo specifico indirizzo, accende il chip. Il SO già sa che se vuole scrivere o
leggere con il PIC dovrà farlo su quel particolare indirizzo, è una cosa che fa parte dello standard
IBM compatibile.
In base alla combinazione di valori su questa linea A0 e una certa combinazioni di questi bit (8D) è
possibile accedere ai 7 registri, 4 di inizializzazione e 3 di operazione. I secondi possono essere
scritti a qualsiasi punto, i primi in fase di inizializzazione.
Parliamo dei registri di inizializzazione (da ICW1 fino a ICW4): quando il so sta per partire una
delle cose che fa è mandare il reset al pic, inizializzando durante il bootstrap dell’elaboratore i
suddetti. La configurazione del pic verrà quindi scritta qua dentro. In particolare, in ICW1 (a cui vi si
accede con A0=0), nel primo registro troviamo il bit SNGL che gli dice se il pic è solo o no, se sta
lavorando da solo o con altri, se IC4 è a uno bisogna leggere ICW4, altrimenti no.
Se sta lavorando insieme ad altri allora verrà scritto qualcosa anche in ICW2, costituito da 8 bit, 3
non ci interessano per il momento e 5 dove si va a scrivere, si scrive i bit più significativi
dell'indirizzo di memoria, i bit più significativi del vettore delle interruzioni. I 3 bit saranno 0 se
abbiamo l'interruzione sulla linea 0, se abbiamo l'interruzione 7 invece saranno 111 e poi verranno
completati dai 5 bit. Quando interrompo il processore, se questo è pronto a gestire l’interruzione,
il PIC mette sul BUS dati proprio questo numero qui che sarà proprio l'indirizzo della periferica e
sarà il so a prendersi il vettore nella memoria.
ICW3 non viene utilizzato se lavora da solo, serve a fare slave control. ICW4 indica una serie di
configurazioni fatte nella fase iniziale, un bit è usato per capire se è un master o uno slave, e un
altro bit che gli dice se sta funzionando con un 8088 o un 8086, un altro se generare AEOI oppure
aspettare EOI, il primo rimette il PIC in ascolto subito dopo la sequenza di INTA, il secondo invece
aspetta la fine dalla subroutine.
OCW1-2-3 vengono usati in fase di funzionamento (Operation Control Word). Il più facile OCW1,
detto maschera, perchè se il processore o il SO vuole mascherare un’interruzione, o vuole che non
passi mai, mette il rispettivo numero a 1 e non passerà mai. Se la vuole riabilitare scrive 0. Ci si
accede con A0=1, ma non c’è ambiguità con ICW2,3,4 poiché essi vengono settati prima di ocw1 e

2
dopo icw1. OCW2,3 vi si accede con A0=0, per non essere ambigui si utilizzano alcuni bit tra gli 8D
per capire da chi andare (ICW1 anche è in lizza). OCW1 serve a settare o a pulire IMR.
In OCW2 R e SL servono a settare dinamicamente le priorità per non permettere ad esempio ad
una periferica di essere eccessivamente prepotente. L2, L1, L0 sono il livello dell’interruzione,
servono a settare il livello in modo dinamico durante l’esecuzione. Con AEOI ad uno a seconda dei
due bit R/SL posso dire se voglio che tale interruzione vada a priorità normale, se deve diventare
minima (meccanismo di rotazione delle priorità) o altro. Con AEOI=0 si fanno cose simili. In OCW3
degno di nota c’è il bit P che manda in modalità polling il PIC, oppure un bit che ci permette di
entrare nella special mask mode. Le priorità iniziali vengono settate nella fase di bootstrap (si parla
con il priority resolver).
Supponiamo che la periferica IR1 mi interrompa, per prima cosa si va a controllare se il bit in
OCW1 sia ad 1, se è alto allora si ignora, altrimenti si procede ad alzare il corrispettivo bit
all’interno del registro IRR, che risulta essere un registro ad 8 bit con un po’ di logica; qui si segna
che IR1 ha interrotto. Poi Il Priority Resolver determina la priorità della richiesta usando IRR e IMR
e va a vedere se ci sono altri bit uguali ad 1, e decide se l’interruzione può passare; il processore
viene interrotto poiché si alza il bit di interruzione, il PIC inibisce le altre linee di interruzione per
non essere disturbato.
A questo punto il processore sta funzionando, decide di soddisfare l’interruzione e manda un
segnale INT ACK: vengono mandati due impulsi negativi, al primo alza il bit corrispondente nel
registro ISR e lo abbassa nell’IRR; al secondo il PIC mette ICW2 sugli 8D e dopo questo se AEOI è
uno il bit corrispondente in ISR si abbassa, altrimenti si aspetta EOI in OCW2. Se ci sono più
interruzioni in base alla priorità si decide quale far passare. Gli dobbiamo dire con quale
processore sta lavorando perché nell'8088 gli indirizzi sono consecutivi. C'è uno schema diverso di
come mappare la memoria.
Per inizializzare il PIC si va a scrivere un byte su un particolare indirizzo.

PIC a cascata

Nelle architetture IBM compatibili venivano utilizzati due PIC. Lo schema di come si montano è
questo.

3
Per via delle 8 linee, sono possibili 64 periferiche. SP/EN è zero per gli slave, SNGL deve essere 0 e
icw3 deve avere gli uni giusti là dove si trovano gli slave (master) oppure deve avere gli ultimi 3 bit
che indicano a che linea sono connessi (slave). Quando lo slave interrompe, chiama il master che a
sua volta interrompe il processore; quando arriva INTA il master mette sui CAS0,1,2 l’identificativo
dello slave che deve rispondere. Lo slave mette sul bus dati ICW2 che contiene in qualche modo
l’indirizzo dell’opportuna ISR. Se AEOI è zero, l’EOI è mandato sia a master che slave.
La fase di inizializzazione deve essere fatta dal sistema operativo conoscendo come è fatta la
scheda madre. Basta pensare che ICW3 in uno slave negli ultimi 3 bit contiene a quale linea IRQ
del master ha INT collegato.
Supponiamo che un pic slave interrompa il master sulla line IR3, questo fa la solita gestione delle
interruzione, supponiamo che deve gestire un interruzione lv3, interrompe il processore, questo
manda un INTA a tutti i pic, il pic master metterà sulle linee CAS la codifica 011, cioè identifica che
deve rispondere il PIC3. Tutti i PIC stanno guardando su quella linea, quindi riceve INTA, e va a
confrontare il proprio registro ICW3 (ultimi 3 bit) con la codifica ricevuta dal CAS, capendo se deve
caricare lui qualcosa sui D. In caso positivo sarà lui a mettere l’identificativo ICW2 sul BUS dati.
In questo modo quando arriva un altro INT ACK, sia il master che lo slave abbassano il bit
dell’interruzione, sapendo che la ISR è finita ed è stata servita.
N.b Le priorità vengono settate dal sistema operativo al momento della configurazione, vedendo
che tipo di periferica è.

DMA Intel 8237


DMA sta per direct memory access, ed è un modo con cui trasferire i dati con le periferiche, viene
utilizzato con periferiche veloci, quindi per lo più memorie di massa. Ha più o meno lo stesso
scopo del PIC, ma si occupa del trasferimento dati memoria-memoria o memoria-periferica, in

4
modo trasparente al processore. Il DMA insieme al processore, su una motherboard, è l’unico che
può autonomamente indirizzare la memoria. Il DMA occupa 16 indirizzi e può gestire 4 periferiche
(4 canali).
Ha dei registri generali:

 Control register
 Status register
 Temporary register
Possono esserne inseriti due a cascata ma non lo vedremo. Possiamo vedere in figura 3 registri
isolati che sono quelli detti in precedenza, poi le 4 parti sono tutte uguali e rappresentano i 4
canali con le quali le periferiche possono interagire. Con i bit più significativi un decoder lo abilita
poiché si utilizza un CS.
Il DMA si occupa del trasferimento tra memoria e periferiche, e quindi quello che deve fare a
differenza del PIC non è solo essere indirizzato per essere programmato, ma deve essere in grado
di prendere anche lui il controllo del BUS per indirizzare la memoria.

Dei 16 bit del bus indirizzi dell'8088, i bit da 0 a 3 (A0, A1, A2, A3), sono in ingresso e uscita,
perchè con essi dobbiamo essere in grado di leggere e scrivere quello che sta là dentro (devo
riuscire a programmare il DMA). Però allo stesso tempo deve essere in grado di far uscire delle
linee per indirizzare la memoria: quindi le prime 4 indirizzano e sono indirizzate, le altre indirizzano
solo (un totale di 8 linee indirizzo). Siccome sono pochi bit, in un latch mette la parte alta
dell'indirizzo attraverso il bus dati (stiamo parlando di un indirizzo a 16 bit quindi potrebbe essere
Ax-Bx essendo due byte); siccome il DMA non esce con 8 linee dati e 16 linee indirizzo, ma esce
con 8 dati ed 8 indirizzo, potrà usare Bx sulla linee indirizzi ed Ax sulle linee dati. Le linee dati

5
verranno memorizzate su un latch che a sua volta sarà memorizzato sulle 8 linee indirizzi: usa il
registro temporaneo per appoggiare una parte degli indirizzi, in modo da essere libero di usare il
bus dati.
ADSTB, serve ad indicare che l’indirizzo è pronto, ovvero per scrivere la parte alta dell’indirizzo nel
latch. Il latch sarà attivo quando il DMA vorrà prendere il BUS.
Le due linee HRQ e HLDA, servono a chiedere l'utilizzo del bus indirizzi, e ricevere la risposta: se
DMA e processore vogliono usare il bus, il controller del bus dirà quale dei due può.
Sulla destra abbiamo le linee di lettura e scrittura IOR e IOW sia in ingresso che in uscita: la prima
è usata in ingresso come controllo per ricevere richieste di lettura da parte della CPU ad esempio
dei registri di controllo; in uscita è usata per accedere ai dati delle periferiche; IOW è usato in
ingresso come controllo per permettere alla CPU di scrivere i registri interni, in uscita per scrivere
verso le periferiche. Essendo che il DMA scrive e legge anche dalla memoria, si utilizzano altre due
linee non bidirezionali MEMW e MEMR. Abbiamo poi anche un ready e reset dalla CPU.
Per ogni canale abbiamo un DREQ e un DACK, che servono per fare l'handshake e comunicare con
una periferica. EOP è il segnale di interruzione, blocca il completamento dell’operazione; è una
linea bidirezionale.
Il processore indirizza il DMA, un canale particolare, e gli dice di trasferire dei byte dalla memoria
alla periferica, il DMA comincia questo trasferimento e poi avvisa il processore che ha finito. Nel
frattempo, il processore farà altro. Il processore scrive l'indirizzo di partenza della memoria dal
quale leggere e quanti byte leggere. Il DMA inizia il trasferimento e man mano che lo fa
incrementa l'indirizzo e decrementa il contatore, quando ha finito avverte il processore che ha
finito il trasferimento. E’ importante anche il temporary register, perché il DMA non si occupa solo
del trasferimento memoria periferica, ma anche di trasferimenti da memoria a memoria, e lo fa
usando solamente i canali 0 e 1, in particolare un canale saprà che deve solo leggere dalla
memoria e uno che deve solo scrivere. E questo registro temporaneo serve per bufferizzare il byte
da trasferire (holding del byte). Ci sono due modalità di lavoro:
Idle cycle, quando nessun canale ha richieste il DMA entra nell’Idle cycle e campiona le linee di
DREQ ogni colpo di clock per capire se c’è un canale che richiede i servizi del DMA. Oltre a
campionare DREQ, campiona anche il segnale Chip Select (CS). Se CS è basso ed HLDA è basso
allora il DMA entra in una fase di Program Condition. In questa fase la CPU può leggere o scrivere i
registri interni del DMA. Per selezionare un registro interno e l’operazione da effettuare (lettura o
scrittura) si utilizzano i segnali di IOW e IOR e le linee A0-3.
Active cycle (handshake): quando ci sono delle richieste (DREQ alto) il DMA mette in uscita HRQ
alto per richiedere il controllo del bus. Quando il DMA riceve il segnale HLDA, che indica che ha
ricevuto il controllo del bus, entra nella fase di Active cycle. Quest’ultimo può lavorare in 4
modalità.

6
Vediamo ora il funzionamento interno per ogni canale, dobbiamo vedere come sono fatti i
registri. Il mode register (è uno per ogni canale) viene utilizzato nel canale per dire se, ad esempio,
il trasferimento deve essere da periferica a memoria o viceversa; è possibile anche disattivare
l'interruzione. Bit 7-6: modo di funzionamento (sotto); Bit 5: se 1 indirizzi a scendere; Bit 4: se 1
autoinizializzazione a fine blocco; Bit 3-2: tipo di operazione: se 00 verifica, se 10 lettura, se 01
scrittura; Bit 1-0: definiscono a quale canale ci si riferisce scrivendo sull’unico indirizzo relativo ai 4
mode register. E’ possibile far funzionare ogni canale in 3 modalità diverse (bit 7-6):

 Signle trasfer mode (01), I/O di un byte alla volta: se la periferica non è troppo veloce, per
evitare che il dma si impossessi del bus e poi il processore non lo può utilizzare, non
potendo comunicare con la RAM, allora ad ogni trasferimento di un byte rilascia il bus e
ogni volta rifà l'handshake. Però sta
sempre allerta guardando DREQ sul
channel, così da rubare un nuovo ciclo di
clock.
 Block trasfer mode (10), si usa se la
periferica è molto veloce, ogni volta
ottenuto il bus lo rilascia solo una volta
trasferito tutto il blocco.
 Demand trasfer mode (00), il dma quando
ha il controllo del bus continua il
trasferimento finchè il dreq è alto, quando
è basso allora rilascia il bus, perché
quando è basso il dato non è pronto, come
il single, ma non fa handshake.
 Cascade (11), non lo trattiamo.
N.b E’ il driver che programma il DMA e tutti i registri. N.b Oltre lettura e scrittura i canali possono
anche fare un controllo.

Il control register e lo status register invece sono proprio del DMA e non del singolo canale, i
singoli bit hanno il significato riportato nelle slide appena sopra. Una combinazione dei primi bit
address e di IOW, IOR permette di configure il dispositivo:

7
Durante la fase di programmazione (DMA slave),
AEN=0 e scriveremo nei registri interni del dispositivo; in fase di funzionamento (master) AEN=1
quando scrive sul bus indirizzi, per indirizzare la memoria.
Ogni canale è dotato di un base address, che ci dice da dove partire, base byte count, quanti byte
leggere, current address register, che quando si parte è uguale al base address, current byte
count, si incrementa finché non si finisce. Per i DMA in cascata i collegamenti sono fatti tra
HRQ,HLDA e DREQ e DACK. Da chiarire la questione sull’Isolated I-O e sui bus.

Trasmissione
Quando dobbiamo far comunicare due dispositivi o usiamo una trasmissione parallela, che si basa
su più linee, poco economica ma piuttosto veloce, e per questo motivo abbandonata come
soluzione, oppure si può usare una singola linea poiché è possibile comunque raggiungere velocità
molto elevate.
Per trasmissione seriale si intende un trasferimento su singola linea di un bit alla volta, possiamo
immaginare un ricevitore e un trasmettitore, per trasmettere un segnale abbiamo almeno due
linee, una per il riferimento e una con un potenziale da far variare. Realizzando la trasmissione con
solo due linee la trasmissione è half-duplex, ovvero trasmettitore e ricevitore possono mettersi
d’accordo e trasmettere o in un senso o nell’altro. Se invece abbiamo 3 linee possiamo avere una
trasmissione del tipo full-duplex, ovvero i bit possono viaggiare sia in una direzione che nell’altra
contemporaneamente.
I vantaggi di questa realizzazione (seriale) sono sicuramente che è economica, difatti bastano 2 o 3
linee, il che nelle lunghe distanze è conveniente.
Il limite di questa trasmissione è che all’aumentare della distanza diventa problematica la
trasmissione, perché per via di fenomeni di aliasing non si riesce a distinguere il valore alto da
quello basso.
Questo ci costringe ad aumentare la distanza tra gli impulsi per non avere sovrapposizione, il che
porta ad una diminuzione della banda.
Su linea seriale abbiamo due tipi di
trasmissione:

 Sincrona, con il clock condiviso, stesso


riferimento temporale. Il trasmettitore
emette una sequenza che il ricevitore
riconosce, deducendo che stanno per
arrivare i dati;

8
 Asincrona, ognuno col suo clock, si mandano prima e dopo i bit di start e di stop.
Trasmissione Asincrona
Nella trasmissione asincrona ricevitore e trasmettitore hanno ognuno il proprio clock, questo non
viene trasmesso, ovviamente devono mettersi d’accordo sulla frequenza.
Quello che succede è che questi clock possono essere sfasati, quindi durante la trasmissione
occorre recuperare la sincronizzazione ad ogni carattere, la codifica prevede che ogni trasmissione
inizi con un bit di start dopo di che c’è un numero di bit variabile che sono dati ed un bit di stop
per indicarne la fine. Per ogni carattere abbiamo quindi delle informazioni in più, una ridondanza
presente in ogni byte (ogni byte che mando richiede questa procedura)!
Quello che succede è che il trasmettitore invia questi bit e il ricevitore campiona: quando rileva il
bit di start allora capisce che è iniziata la trasmissione e sa che deve prendere il valore medio di
ogni valore.

Trasmissione Sincrona
Nella trasmissione sincrona invece abbiamo un unico clock, che può essere quello del ricevitore o
trasmettitore; esso viene trasmesso insieme ai dati. Il clock è una linea in più che fa parte del BUS,
ed è condivisa. In questo modo non c’è bisogno di sincronizzazione!
Quello che di solito si ha è una serie di byte, che ci indica l’inizio della trasmissione, e allo stesso
modo una che ne indica il termine. Il ricevitore campiona la linea dati in ingresso, quando
riconosce i byte allora si accorge che c’è una nuova sequenza di dati da leggere.
Ad esempio:
100101 10100101 01001010 10100100 10101001
START BYTE 1 BYTE2 BYTE3 STOP
Confronto
Nella prima soluzione il vantaggio è che ho una linea in meno, lo svantaggio è che ho molti bit di
ridondanza.
Nella seconda soluzione il vantaggio è che ho un tasso d’informazione molto elevato perché ho
solo due caratteri (in alcuni casi possono essere di più) di sincronizzazione; lo svantaggio è che
trasmetto il clock.
Ma soprattutto sulle corte distanze la sincrona va molto bene perché il clock non degenera,
mentre su lunga distanza avrei che degenererebbe e quindi non potrei utilizzarlo.

USART Intel 8251A


È un dispositivo per la trasmissione seriale, che effettua una conversione parallela da una parte
mentre è seriale dall’altra.

9
Questo può funzionare sia per instaurare una trasmissione di tipo sincrono sia di tipo asincrono,
permette una comunicazione full duplex. Con una sola linea vado a scrivere e leggere nei registri di
questa interfaccia (A0).
L’ interfaccia di programmazione è costituita, sommariamente, da:
 Due registri per i dati in ingresso e in uscita
 Un registro di controllo
 Un registro di stato
 Un registro di modo
 Due registri per memorizzare i caratteri SYNC

I bit CD, RD, WR sono utilizzati in particolari combinazioni per accedere allo status reg, control reg,
registro dati ricevuti e registro dati da trasmettere. Tutte le altre combinazioni non sono permesse
e mettono le linee dato in alta impedenza.
Il mode register serve a configurare il dispositivo , ci scrivo la prima volta che accedo per
configurarlo per dire se funzionare in modo sincrono o asincrono (i due bit meno significativi). Vi si
accede con A0=1. Se lo configuro in modo sincrono allora i prossimi accessi andranno ad
indirizzare i registri sync character 1 e 2, per dire quali sono i caratteri di sincronizzazione (che
vengono mandati automaticamente, non c’è bisogno di metterli nel transmitter buffer). E’
possibile anche una soluzione con un solo carattere di sincronizzazione. Se è asincrona allora salto
la configurazione di questi registri, visto che non mi servono. In questo registro troviamo anche il

10
numero di bit di dati, se usiamo parità o meno e, nei due bit più significativi, abbiamo le seguenti
alternative:

 Asincrono: numeri di bit di stop;


 Sincrono: numero di sync e se si utilizza o meno il segnale SYNDET nel caso di presenza di
un signal detector.
Control register serve a mandare i comandi al dispositivo, vi si accede con A0=1. In questo registro
troviamo bit che abilitano trasmissione e ricezione, un bit di reset e uno di pulizia errori nello
status register.
Status register serve per sapere lo stato, errori, ecc.
Dopo la configurazione il programmatore può accedere al control register in scrittura, lo status
register in lettura e ai registri dati, data in buffer e data out buffer. Quando il dispositivo comincia
a ricevere i dati arrivano sulla linea ‘Serial Input’ e vanno nello shift-register; se il dato è arrivato
senza errori viene copiato nel buffer e viene interrotto il processore per avvertire la presenza del
dato: ci sono infatti due linee di interruzione che vanno dal dispositivo al processore (TxRDY e
RxRDY) che indicano rispettivamente che si è pronti a trasmettere o che è arrivato qualcosa.
Se il processore vuole mandare un dato può scrivere un byte lì dentro, quando è finita la
trasmissione nello shift register, i bit uno a uno vengono inviati sulla linea TxD. Quando vogliamo
spedire un dato sarà il componente a mettere la parola di start e di stop, non dobbiamo
preoccuparcene noi. Dall’altra parte quando ho terminato la ricezione avverto. C’è il reset e ci
sono RD e WR che indicano quando la CPU sta leggengo o scrivendo i registri interni del dispositivo
(li scrive in fase di programmazione).
Il clock viene preso localmente oppure può essere trasmesso dall'altro usart, in base al tipo di
trasmissione. C’è anche un clock interno CLK che però è coinvolto solo in meccanismi interni.
Ci sono delle linee del protocollo full modem, DSR,DTR,CTS,RTS,SYNDET (il dispositivo nacque per
interfacciarsi con un modem), e c'è una linea che viene utilizzata dal modem per avvertire quando
è stata agganciata la sincronizzazione. In una comunicazione le prima 4 linee sono messe a croce.
Con la linea TxE il dispositivo dice se il buffer di trasmissione è vuoto, ovvero se non ha nulla da
trasmettere.
Mode Register nel dettaglio
Quando faccio il reset io devo andare a dire al dispositivo se funzionare in modo sincro o asincro,
quindi in base agli ultimi due bit capiamo come funzionare. In base alle due modalità i due bit
iniziali hanno significato diverso.
Nella modalità sincrona (con 00 alla fine) il primo bit dice se ci sono 1 o 2 caratteri di sincronismo.
ESD ci dice se quel segnale SYNDET è in uscita o in ingresso, cioè chi dei due deve rilevare la
sincronizzazione.
EP dice se il bit di parità deve essere usato per settare una parità dispari o pari.
PEN ci dice se ci deve essere o meno il bit di parità, se 0 non viene usata, se 1 si.

11
L1 e L2 ci dicono di quanti byte l'informazione deve essere formata. Ad esempio per linee poco
affidabili mandiamo 8 bit dati e uno di parità.
In modalità asincrona i primi due bit ci dicono quanti sono i bit di stop, se uno, uno e mezzo o due;
poi EP bit di parità attivato o meno, e PEN se parità pari o dispari come nel caso precedente, il
numero di bit dati, poi il baud rate cioè ammesso che il clock funziona con una certa frequenza,
questo ci dice ogni quanti colpi di clock ci manda un bit.

Control Register nel dettaglio


Nel control register possiamo andare dei comandi, possiamo controllare le linee del modem,
cancellare gli errori o mettere il modem in trasmissione.

12
Status Register nel dettaglio
È interessante perchè contiene prima di tutto 3 bit (3-4-5) , che quando vengo interrotto devo
andare a controllare per capire se il dato è arrivato correttamente. Il parity error non ha bisogno
di presentazioni.
Un’altra cosa che può succedere è il framing error, ovvero sto ricevendo il bit di start ma non
ricevo il bit di stop, non troviamo parte iniziale o finale in una trasmissione, anche in questo caso
segnaliamo l'errore.
Il terzo tipo di errore che può esserci è l'overrun error, ho ricevuto i bit correttamente e dallo
shifter register li copio nel buffer in, nel frattempo il processore è troppo impegnato e arrivano
ancora byte: l’effetto è che vengono sovrascritti quelli di prima (questo in fase di ricezione, ma
anche in trasmissione potremmo sovrascrivere il buffer out).
Il bit 0 ci dice se il buffer di scrittura è vuoto, il bit 1-2-6 riflendo TxE, RxRDY e SYNDET. Il bit 7
riflette il segnale DSR.
TxRDY e RxRDY vengono automaticamente portati a 0, questo perché il driver deve sapere che
quando è arrivato un nuovo byte si alzano TxRDY e RxRDY, si vanno a controllare gli errori, si
vanno a leggere i dati, e automaticamente si devono abbassare.
Il dispositivo visto è un dispositivo ad interfaccia, il bus USB non è vicino a questa concezione.
N.b. Spesso questo dispositivo è utilizzato in coppia con un PIC che gli assegna una linea di
interruzione.

Driver Linux
Innanzitutto parliamo di una parte del sistema operativo, che è composto da diversi componenti
tra i quali: gestore utenti, memoria, processi, file system, periferiche attraverso software detti
driver, di cui ci occupperemo in questa parte.
13
Sono dei meccanismi che consentono ad uno sviluppatore di utilizzare le periferiche nei propri
programmi, facendo in modo che lui si preoccupi di realizzare quelle che sono dette le politiche,
mascherando l’hardware al programmatore. Molto vicino è il concetto di socket. Molto famoso è
XServer (gestore delle finestre) che utilizza driver per parlare con display e altro. Quindi i driver
sono dei meccanismi del SO che servono a gestire le periferiche.
Kernel Linux
Il kernel linux è colui che si occupa della schedulazione dei thread, dei processi. Questo nucleo è
opensource, scaricabile e persino ricompilabile. Il kernel contiene una serie di driver, e una parte
di esso è caricato in RAM
all’avvio.
È possibile poi installare
delle estensioni del SO e
installarle come moduli,
come pezzi di codice che si
caricano dinamicamente,
che possono essere caricati
anche in base ad un certo
evento, come l'accensione di
una periferica. Da linea di
comando possiamo caricare
la libreria, inserendo il driver
dinamicamente senza
riavviare il sistema. Le
primitive a linea di comando
usate sono insmod e
rmmod. Il gestire il sistema operativo in moduli si contrappone ad un’ altra soluzione detta
monolitica. Ma ora sorge una domanda: perché le ISO del sistema operativo vengono indicate per
architettura x86 o altro, piuttosto che per il preciso processore? Perché l’ISA è la stessa!
Chiaramente possono essere presenti anche driver che non servono e che potrebbero essere
cancellati: in seguito bisogna ricompilare il kernel, anche apposta per il nostro processore.

In linux abbiamo tre tipi di device:

 Quelli a caratteri: vuol dire che possiamo scrivere un byte alla volta. Esempi: tastiera,
mouse, seriali.
 Quelli a blocchi possiamo scrivere blocchi di dati. Esempi: memorie di massa, penne USB.
 Quelli a rete parlano con i pacchetti. Esempi: scheda di rete, non li vedremo.
Questa figura rappresenta un sistema linux rappresentato a blocchi: quasi tutti i dispositivi con cui
scambiamo grandi quantità di dati sono dispositivi a blocchi, come CD-ROM, penne USB ecc. la cui
gestione deve essere molto efficiente, solitamente il SO linux ce li fa vedere come directory. Ne
parleremo non troppo nel dettaglio più avanti. Le operazioni non sono come quelle che si fanno su
quelle a caratteri, per un motivo prestazionale. Viene usata una sorta di coda, e si effettua la
14
richiesta per un operazione di lettura scrittura, e nel driver avremo uno scheduler che decide quale
richiesta schedulare. Essi contengono un file system.
I dispositivi a caratteri vengono astratti come file dal SO, in windows invece sono i COM.
Infine i dispostivi di rete in linux vengono visti come etichette, vengono gestiti in modo
particolare, assumendo dei nomi logici.
Se il device a caratteri viene astratto come un file allora quello che si può fare è scrivere, leggere,
aprire, chiudere, ovviamente non verrà scritto come facciamo su un file, ma ogni funzione
richiamerà una funzione del driver. In genere non può essere fatto proprio tutto quello che si fa
con un file, ad esempio non sempre è possibile spostarsi all’interno di esso, perché raramente
sono mappati in memoria. Il driver dovrà implementare tutte le operazioni tipiche che si fanno su
file. I dispositivi a caratteri in linux come file si trovano nella directory dev. Sotto dev per i device a
blocchi trovo directory.
Abbiamo dei dispositivi diversi che non fanno parte di questa categorizzazione che possono
funzionare anche con una serie di moduli, l'applicazione userà comunque il modulo più esterno.
Anche questi non li tratteremo; ma avremo una catena di moduli che ci permette di usarli. Avremo
diversi livelli di operazioni costruiti uno sopra l’altro;
l’applicazione userà direttamente il modulo più
esterno.
Ma come si realizza un dirver? Esso è un file C (linux è
scritto in C) in cui includiamo due librerie di sistema.
Questo file (driver) include tre macro:

 MODULE_LICENSE che specifica con quale


licenza abbiamo sviluppato il driver: una licenza
lasca permette a chiunque di modificare il
programma, a patto di rilasciare il modificato.
 MODULE INIT ci dice che quando viene
installato il driver chiama il metodo
hello init, è una macro alla quale passo
una funzione che sarà eseguita al
momento dell’inserimento del device.
 MODULE EXIT chiama il metodo hello
exit, stesso ragionamento ma sarà
eseguito al disinserimento.
Il metodo che viene usato per stampare è
printk, che ha due parametri, il primo ci
segnala la priorità della stringa, l'altro la
stringa stessa. Si usa printk perchè è già nel kernel questo metodo, non si usa printf perchè
altrimenti dovremmo includere std.io, quindi il modulo dovrebbe portarsi indietro una libreria C,
portando a dimensioni eccessive.

15
Il driver può poi essere compilato anche se non è in esecuzione. I driver sono eseguiti in kernel
mode in genere, ma anche in user mode. Per compilare il driver bisogna essere forniti degli
headers del kernel (comando per averli sul quaderno), più altri check. Con un makefile genero un
file .ko. Poi bisogna anche ricompilare il kernel. Se un modulo dipende da simboli di altri moduli
(moduli in stack come porta seriale ->stampante) insmod è sostituito da modeprobe.
Possibili errori nella fase di registrazione: creato il driver con la fase di init io magari vorrei che
faccia anche altre cose, come magari l’allocazione di memoria. Se abbiamo un errore dobbiamo
anche sapere quanti puntatori sono stati allocati, per poterli deallocare, altrimenti si sporca tutta
la memoria!
Oltre al caricamento del driver potrebbero essere caricate anche delle variabili per il suo corretto
funzionamento, il che è importante perché queste variabili globali servono per far comunicare i
metodi del driver tra loro.
Si possono anche passare parametri al driver. I parametri possono essere passati attraverso la
linea di comando, definendo un comando, nome parametro, tipo, e il valore. In questo modo si
scambiano i parametri con il modulo. Un parametro spesso passato è l’indirizzo del porto (credo si
riferisca alle vere e proprie uscite).

Sviluppare un driver

Noi possiamo sviluppare un driver come applicazione lato utente: quest’applicazione sviluppata
come utente diventa un qualcosa che possiamo debbugare facilmente, se faccio la kill non ne
risente il SO, quando non è necessario viene mandato via dalla la RAM, è possibile usare la
libreria .C, non si hanno problemi di licenze se cambia qualcosa nel kernel. Gli svantaggi sono che
da alcune applicazioni potremmo non vedere il driver, abbiamo un problema di performance se si
è fatto swapping sul disco, non possiamo usare le interruzioni, non possiamo usare il DMA, se non
facendolo girare come utente supervisore e così via.
La prima cosa che un driver dovrebbe fare è diventare proprietario degli indirizzi della porta (ad
esempio parallela), ovvero allocare un porto, e lo farà chiamando dei metodi, dando degli indirizzi
dal primo ad n e assegnando un nome. Prima di essere disinstallato il driver deve liberare la porta
parallela, che tecnicamente significa deallocare il porto. Va controllato che gli indirizzi della porta
non siano usati da qualcun'altro, perchè non ci possono essere due driver per lo stesso indirizzo. In
linux nella cartella /proc c’è l’astrazione dei processi, di tutte le cose dinamiche. Attraverso
proc/ioports si può accedere alla lista dei porti.
Solitamente per semplicità i dispositivi di IO vengono mappati in memoria (memory mapped)
nonostante si usi isolated I/O e si scrive come se fosse un normale indirizzo di memoria RAM.
Esistono poi dei istruzioni dette barrier che garantiscono che le scritture non restino in cache. Le
barrier servono ad essere sicuri che tutto sia terminato, a questo servono i metod WMD o RMB.
Il driver lo possiamo pensare come una doppia interfaccia: da una parte conosce il protocollo della
periferica e i suoi registri, mentre dall’altra deve implementare l'astrazione del SO verso le
applicazioni.
16
Abbiamo detto che le periferiche sono astratte come file e cartelle: il SO deve capire quale driver
far partire e per questo di fianco al file speciale ci sono due numeretti, major number e minor
number.
Attraverso il major number il SO sa che deve
chiamare il driver associato al numero; il minor
number viene usato per identificare in maniera
univoca il dispositivo, ma è utilizzato dalle user
application per indirizzare i devices, non dal
sistema operativo.
Quando il driver parte si registra con un major
number, e può poi allocare un minor number.
Alcuni major number sono riservati, ci sono una serie di device che hanno i loro numeri, questo
elenco lo troviamo in un file in Documentation/devices.txt. Troviamo anche i permessi
accompagnati dalla lettera c che ci fa capire che si tratta di un device a caratteri (sarà d per i device
a blocchi).

Se vogliamo creare noi dei file speciali (le periferiche sono file speciali) lo possiamo fare usando il
comando mknod, specificando i permessi, la cartella, il tipo di device(carattere ecc) il minor
number 0 e major number 20. Se scriviamo in questo file però non succede niente perchè non ci
sarà un driver che si è registrato con quel number. Quindi ora dobbiamo andare a creare il driver.
Noi scriveremo dei metodi che saranno quelli che il SO dovrà chiamare quando viene effettuata
una particolare operazione come write o read; deve essere fatta poi l'associazione tra questi
metodi e le operazioni: va creata quindi una variabile globale che effettua queste associazioni, che
rappresenta l'interfaccia verso il sistema operativo del nostro driver. In questo corrisponde la
dichiarazione. Nel metodo di inizializzazione il driver si registrerà con un major number (20 ad es),
il SO vede chi si è registrato con il major number 20 e sa quindi che driver chiamare quando si
richiede un operazione di read e write verso quella periferica. Per capire il mapping tra gli eventi e
le operazioni si passa l’indirizzo di una particolare struct definita in “linux/fs.h” precedentemente
definita. In questo modo il SO sa chi è 20 e i metodi da chiamare quando deve usare il driver con
questo major number. Un driver quindi si crea definendo questi metodi. In particolare un modulo
di un driver a caratteri è formato dalla definizione di open, close, write e read. Un po di codice:

17
BUS
I dispositivi sulla scheda madre sono collegati tra loro tramite i cosiddetti BUS. Esistono diversi tipi
di connessioni, a seconda del caso (esempio dati, indirizzi, controllo). Vediamo ora come sono
organizzati. I componenti che dobbiamo interconnettere sono processori, memoria e dispositivi
I/O.
I componenti vengono schematizzati come in figura (sotto). Un I/O module ha anch'esso indirizzi,
ma ha anche il bus dati verso il processore e un bus dati verso la periferica, oltre che diverse linee
di interruzione e di controllo verso entrambe le parti. La CPU ha anche essa delle linee: istruzioni e
dati sono separati perchè dipende dal tipo di architettura che abbiamo (Harvard o Von Neumann).
Deve indirizzare la memoria ma anche le perferiche, ha dei segnali di controllo per controllare la
periferica e ha poi dati in ingresso e in uscita.
Tutte queste linee devono essere collegate ad uno o più bus. Esistono diverse soluzioni per
l’interconnessione dei nostri componenti. Queste sono quelle dei primi sistemi.
18
Una prima soluzione aveva due bus separati, uno con la memoria e uno
di I/O; oppure abbiamo un’architettura con un singolo bus. Unibus fu il
primo esempio di bus di sistema. Siccome esistono tanti bus, dovremmo
stabilire qual è lo standard: attualmente lo standard de facto è PCI.
Occorre definire delle caratteristiche di esso:

 Caratteristiche meccaniche: inserzione e dimensione


 Caratteristiche elettriche: a quale tensione deve funzionare,
quanta corrente gli deve passare, tempi di salita e discesa
 Caratteristiche logico funzionali: struttura, protocollo (per
protocollo si intende l’insieme di regole adoperato per lo
scambio di informazioni).
Solitamente si parla di Address BUS, Control BUS e Data BUS; in realtà
c’è un solo BUS, dove alcune linee sono dedicate ai dati, altri agli indirizzi
ed altre ai controlli. I bus dati trasporta istruzioni e dati, che per noi
sono la stessa cosa in von neumann; il bus indirizzi serve ad accedere alla memoria, la sua capienza
indica lo spazio di indirizzamento. Se usassi un bus unico per i 3 compiti perderei troppo tempo,
per questo si adottano bus multipli.

19
Solitamente ci sono diversi bus e il traffico tra loro è separato da degli switch o meglio da dei
bridge che possono fare semplicemente da espansione oppure effettivamente separare il traffico.
In questo caso il processore legge direttamente dalla cache e mai dalla RAM, la RAM è collegata
alla cache con il bus di sistema a sua
volta collegato e un bridge che ci
permette di accedere ad un I/O bus.
In questo caso invece abbiamo BUS di
I/O separati, uno per periferiche ad
alta velocità uno per periferica a bassa
velocità. Solitamente ad alta velocità
sono schede video, lan mentre bassa
velocità sono ad esempio quelle che
leggono un byte alla volta.

Le linee del BUS possono essere


dedicate (linee separate per dati e
indirizzi) o condivise (le stesse linee
potrebbero funzionare sia come
indirizzi che dati, ma si prevede un
controllo più complesso). Importante
è l’arbitraggio poiché un bus può
essere usato da più dispositivi come,
ad esempio, CPU e DMA o più CPU
nelle architetture multiprocessore
Ci deve essere quindi un arbitro che può o essere un dispositivo hardware oppure un meccanismo
distribuito. Nel primo caso c’è un modulo a parte (bus controller), facente parte del processore,
che “schedula”, mentre nel secondo caso ogni modulo può richiedere il bus, ma c’è bisogno di una
logica di assegnazione. Inoltre un dispositivo si dice master se impone una tensione sulle linee,
slave se le legge (chiaramente il ruolo di un dispositivo varia nel tempo).

Un

esempio di
arbitraggio ditribuito prevede che i diversi master utilizzino delle linee per chiedere il bus, e per
vedere se il bus è occupato o no. Abbiamo tre soluzioni possibili per l’arbitraggio:
20
 Daisy chain: il master che intende utilizzare la linea controlla BUSY, se libera la alza e non
permette a nessun altro di accedervi (disegno sopra); Il dispositivo collegato più a sinistra
ha una priorità maggiore.
 Centralizzato parallelo: le linee di richiesta vanno all’arbitro che decide e setta le priorità;
 Centralizzato daisy chain: simile al daisy chain, ma con un meccanismo che assegna anche
le priorità;

Vediamo ora i meccanismi di tempificazione per i bus,


vedremo due soluzioni: la prima è di tipo sincrono, con clock, cioè tutte le operazioni prendono
come riferimento i fronti di salita e discesa del clock. Il clock ci dà il tempo, poi avremo linee di
status, linee di indirizzo e una linea che ci dice se l'indirizzo è valido. Da una parte abbiamo il
master ed è lui che decide l'indirizzo sul quale leggere. Dopo aver posto l'indirizzo sul bus, da quel
momento in poi l'indirizzo è valido. Per dire che è valido viene alzato l AdressEnable, che ci dice
che l'indirizzo è valido. Visto che il tutto è sincrono vuol dire che il processore sa che la periferica al
fronte di salita ha letto l'indirizzo, quindi può abbassarlo.
Se il processore vuole leggere alza la linea Read
per segnalare che vuole leggere e sa che al
prossimo fronte di salita la periferica avrà messo i
dati sul BUS, quindi dopo un colpo di clock avrà i
dati perchè è sicuro che la periferica l'ha letto. Se
vuole scrivere dovrà prima preparare i dati ed
alzare la linea Write, e sarà sicuro che la periferica
sul fronte di salita leggerà i dati.

21
In un protocollo asincrono servono dei segnali di
handshaking per gestire la sincronizzazione, una
volta messo l'indirizzo, la periferica metterà i dati
sul bus, il processore deve aspettare quindi che la
periferica gli dia un segnale specifico.
Nella scrittura la periferica in maniera asincrona gli
da un riferimento esplicito. Dopo aver letto il dato
il processore deve avvertire che ha finito togliendo
i dati dal bus. Nel caso di sopra vediamo una read,
nel caso di sotto una write.

BUS PCI (Peripheral component interconnection)


È il bus per il collegamento processore-periferiche: a questo bus troviamo collegate le schede
audio, video, il dma le usa per parlare con le periferiche, ecc. È un bus a trasmissione parallela a 32
linee o 64 linee per i dati (vengono multiplexate per usare anche indirizzi), più altre 50 linee. Lo
standard è realizzato dall’Intel e rilasciato opensource.
Possiamo raggruppare le linee del bus secondo la logica di funzionamento. Abbiamo delle linee:

 Generali di sistema, che portano ad esempio il clock e il reset, il clock sarà necessario
perché si usa un protocollo sincorno.
 Le linee indirizzi e dati nella versione base sono almeno 32, quindi con 32 linee possiamo
accedere a numerosi indirizzi; queste linee sono multiplexate nel tempo ovvero sullo stesso
indirizzo prima mettiamo l’indirizzo della periferica e poi possiamo mettere anche i dati.
 Linee di interruzione, per fare un check, per validare i dati.
 Linee per il controllo delle diverse interfacce .
 Linee per l'arbitraggio, ogni master del BUS , come DMA e CPU, ha delle linee dedicate
all'arbitraggio, abbiamo quindi un numero max di dispositivi master che possiamo
collegare. Ci sarà un solo arbitro centralizzato, difatti queste linee servono a richiedere il
controllo del BUS.
 Linee di errore.
 Linee di interruzione non condivise, opzionali.
 Linee per interfacciarsi con la cache, opzionali.
 Nel caso 64 bit altre 32 linee, per il multiplex e 2 che servono a richiedere agli slave se
supportano o meno il trasferimento a 64 bit, opzionali.
 Linee per il test, opzionali.

22
La comunicazione avviene sempre tra un master e uno slave, generalmente DMA e CPU con una
periferica, il master è detto initiator e lo slave è detto target.
Il master deve chiedere prima il bus all'arbitro e poi lo può usare, iniziando la trasmissione,
selezionando il target, deve definite che tipo di operazione vuole fate (R o W), e poi cominciare la
comunicazione che si divide in due fasi, l'indirizzamento e poi il trasferimento dati.
Protocollo trasferimento
Questo è il protocollo utilizzato:

Questo è timing diagram per la fase di lettura.


La prima linea che vediamo è il clock, questo detta il tempo per le diverse operazioni, ogni
operazione sul fronte di salita.
La linea frame è condivisa da tutti gli elementi ed è controllata dal master, il quale l’abbassa ,
ovvero l'attiva per prendere il controllo del bus all'inizio della transazione; se gli altri la vedono
bassa allora non possono occupare il bus.
Contemporaneamente all'abbassare del frame inserisce l'indirizzo (sulle linee indirizzo-dati) della
periferica con cui vuole comunicare e sulla Control/ByteEnable (contiene metadati, come ad
esempio su quali linee farò viaggiare i dati) mette l'operazione che vuole fare, Write, Read, Verifica
o altro comando.
Ci si aspetta che una periferica riconosca il proprio indirizzo, e in particolare se ne renderà conto
sul prossimo fronte di salita, quindi un colpo di clock dopo, il master abbassa l'IRDY che dice "Io
sono pronto a leggere i dati".
Si aspetta che la periferica legga il byte enable e poi essa mette i dati sul bus, se non accade si
aspetta. Il ByteEnable ci dice su quali linee ci deve mandare il dato, le prime 8, le prime 16, le
seconde 16 ecc.

23
Quando la periferica è pronta mette i dati sulle stesse linee indirizzo-dato, e poi lo avverte
abbassando il Target Ready TRDY; dopo averlo abbassato è sicuro che dopo un colpo di clock il
dato è stato letto dal processore, e quindi lo rialza, e prepara i prossimi dati.
Il processore deve aspettare che il dato deve essere preparato e si accorge che è pronto perchè la
periferica riabbassa il target ready. Sul fronte di salita vede che il dato è stato preparato e quindi
può leggerlo.
In questo esempio se la periferica prepara il nuovo dato, e il processore non è pronto alla ricezione
perché magari sta facendo altro, allora questo alzerà l’IRDY, allora la periferica non cambia il dato,
questo rimane lo stesso. Ad un certo punto l’iniziator fa due cose, dice che sono di nuovo pronto a
leggere il dato e dall’altra parte lascio il frame per dire che non ci saranno più trasferimenti dopo
questo. La periferica sul fronte di salita si accorge che l’IRDY è asserito, ovvero è velido, capisce
anche che il frame è alto e che è finita la trasmissione, allora la periferica alza DEVSELECT e TRDY
per segnalare la fine della trasmissione; l’iniziator alza l’IRDY. E’ da notare che sebbene il frame si
alzi prima che i byte siano arrivati tutti, comunque l’IRDY e il TRDY aspettano che la comunicazione
termini, per essere alzati.
Come abbiamo notato abbiamo una fase di indirizzamento e più fasi di trasferimento.
Se ricordiamo il DMA, questo parlava con la periferica attraverso due linee, ed è lui a preoccuparsi
di fare l'inizializzazione.Il protocollo di scrittura visto è lo stesso, l’unica differenza è che è il
processore a mettere i dati ed è il processore a dire che il dato è pronto o non è pronto.
Protocollo di arbitraggio
Dobbiamo però vedere il protocollo di arbitraggio. Quello che non è definito dal protocollo è la
priorità della periferiche: essa viene lasciata allo sviluppatore del bus, se round robin, fissa o
altro.
Ogni dispositivo, master, è collegato ad un arbitro centralizzato ed ogni dispositivo ha due linee:
una linea per richiedere il controllo del bus ed una per ricevere il controllo, non sono linee
condivise quindi per ogni dispositivo c'è una coppia di linee sul bus.

Vediamo come
funziona il
protocollo:

24
Il clock è sempre lo stesso. Supponiamo in questo esempio ci troviamo in una situazione con la
linea di richiesta da parte dell’iniziator A già asserita (riservo sempre il bus al dispositivo A),
mentre B l'abbassa un po' dopo.
Sul fronte di salita del clock l'arbitro si accorge che ad A serve il bus, glielo concede abbassando
GNTA, REQ-B nel frattempo si è abbassato. Sul fronte di salita A si accorge che gli è stato concesso
il bus e lo occupa con il frame e mette l’indirizzo; al colpo di clock successivo, la periferica
riconoscerà il proprio indirizzo e a quello ancora successivo si abbassano IRDY e TRDY e comincia
il trasferimento come visto prima.
Però mentre avviene il trasferimento l'arbitro ha visto una richiesta da B, e decide di concedere il
bus a B e toglierlo ad A; al prossimo colpo di clock, il dispositivo A disattiverà il frame, perché vede
che il BUS non è più suo, oppure perché ha finito il trasferimento. Quando il trasferimento è
veramente finito alza anche IRDY e TRDY (solo in questo momento ha liberato completamente il
BUS). Appena vengono alzati questi due segnali, siccome il BUS è libero il dispositivo B abbasserà il
frame; tuttavia quando inizierà la trasmissione al colpo di clock successivo si accorge che gli è stato
tolto il BUS, quindi aspetta di completare la transazione e rilascia il BUS. Ricomincerà A. L'arbitro
abbassa un solo GRANT alla volta, quale è deciso dal produttore.
Quando è tolto un bus all'iniziator, questo alza il frame e manda l'ultimo dato, completa la
trasmissione dell’ultimo dato, e solo quando è completato allora rilascia il bus, e solo da quel
momento l'unico che ha il grant attivo occupa il bus, abbassando il frame. Quinidi, l’occupazione
del BUS inizia col frame e finisce con IRDY e TRDY.

25
BUS USB

Questo è un bus per la gestione delle periferiche. Esistono diverse versioni (in particolare ad oggi
siamo arrivati alla versione 3.0). La versione 1.1 prevedeva low speed e full speed; la versione 2.0
aggiunge l’high speed. L’usb è, innanzitutto, seriale (1 bit alla volta); è host controlled, ovvero c’è
un solo master ed è l’unico che può iniziare la trasmissione (solo dalla versione 2.0 viene
introdotta la funzionalità di negoziazione, più host possono fare da master); la topologia è a multi-
stella (a livelli con più di un hub); fino a 127 dispositivi possono essere connessi a un bus USB ( ci si
arriva ponendo diversi hub in cascata) ; codifica NRZI su due linee; asincrono. Attualmente gli hub
sono già installati internamente nelle periferiche.
Ogni porta ha 4 fili:

 Uno per per la massa

 Uno per l’alimentazione, anche per alimentare.

 Gli ultimi due D+ e D- vanno a codificare la ddp, regolare la differenza di potenziale,


invertendosi iniziano le transazioni: se rimane costante trasmettiamo 0, se varia
trasmettiamo 1 (questo significa codifica nrzi). La D- è negativa rispetto al GND.

Supporta il plug-n-play, quando viene collegato qualcosa, l’host deve riconoscerla e caricare
l’opportuno driver. Viene riconosciuto perché ogni periferica è dotata di un Product ID e Vendor
ID: il consorzio USB vende questi identificativi, un produttore deve quindi farsi fornire un Vendor
ID; quando viene collegata la periferica il SO riconosce il Vendor ID e Product ID. Molti produttori
lasciano libero qualche Product ID per permettere lo sviluppo di un driver opensource. Molte
aziende subaffittano un lotto di Vendor ID.

Abbiamo due esempi di porte usb che hanno 4 connettori diversi,


D- e D+ vanno a codificare la differenza di potenziale.

Quando è la periferica che vuole trasmettere allora forza la differenza di potenziale, la


trasmissione non è full duplex ma half duplex, si trasmette solo in un verso alla volta.
È importante come elettricamente siano connessi i dispositivi, poiché quando connettiamo la
nostra periferica il chip host riconosce il carico, riconoscendone uno su D+ e D-; a seconda del
carico che ha riconosciuto capisce a che velocità mandare i bit, se è un low speed device o un high
speed device. I D+ e i D- vengono portati a una certa tensione (a seconda di high o low), anche se
sono alimentati a 5V, quindi ci sarà qualche resistenza che farà questo, un regolatore di carica che
abbassa la tensione sui segnali.
I dispositivi USB sono dispositivi che possono essere alimentati direttamente dall’host, difatti
quando il dispositivo si collega fornisce un numero all’host e questi (facendo 2mA*numero poiché
26
2mA è l’unità di potenza per l’usb) riconosce la corrente con cui alimentarlo. Abbiamo quindi tre
tipi di dispositivi:

 Low power che richiedono poca potenza per funzionare e la prendono tutta dal BUS,
quando collegato sarà lui a dire all’host di quanta potenza ha bisogno, e lo farà fornendo
un numero; il massimo è 100mA. La massima tensione alla quale possono essere alimentati
è 5,25V. Per quelli che hanno bisogno di 3,3V gli viene fornito sempre 5V, dovranno
regolarla loro.

 High power, possono assorbire più di 100mA, al massimo 500mA, all’inizio ne riceve
100mA e poi ne richiede di più al momento della configurazione.

 Self power, possono ricavare un minimo di potenza dall’USB e il resto da un’alimentazione


esterna.

Tutti i dispositivi devono supportare il suspend mode, ovvero una condizione in cui sono messi in
standby: essi comunicano sempre la corrente con un numero, la cui unità sarà 500 uA, questa
deve consumare al massimo 2,5 mA. Quando non c’è attività per 3ms allora il dispositivo va in
standby, e ne ha 7ms per spegnere i suoi pezzi e non prendere più della corrente fornita in
sospensione. Per questo motivo il dispositivo ogni tanto manda alle diverse periferiche dei frame
(keep alive), per tenerle sveglie. Più è lenta la periferica più sarà lento il keep alive. Esiste il global
suspend, in cui tutti vanno in sospensione collettivamente, e può essere anche forzata una
periferica ad andare in standby, appena riceve qualcosa viene riacceso.

Protocollo USB
Ci tocca adesso capire come viene usato il bus. La maggior parte di questi protocolli sono semplici,
la maggior parte delle cose sono fatte dall’hardware.
Ci sono delle regole molto definite per far parlare i dispositivi, in particolare ogni dialogo è detto
transazione e in ogni transazione avremo: un token packet, che generalmente manda l’host per
iniziare la transizione, contiene con chi parlare, comandi e bit; un data packet (opzionale), porta il
payload; uno status packet (il ricevitore ha accolto il pacchetto, o segnalazioni di errore). Quando
l’host manda il token, l’hub lo riceve e lo rimanda la periferica.
Pacchetto di token

Pacchetto Dati

Pacchetto Handshake

Pacchetto Start frame


27
Vediamo come è fatto un pacchetto. C’è sempre un primo campo sync: è una sequenza che serve
a far sincronizzare le parti, 8 bit per low e full speed, 32bit per gli high speed. Gli ultimi due bit del
sync indicano dove comincia il campo PID. La parte PID (è uguale per ogni tecnologia) identifica
che tipo di pacchetto è: può essere out, in, tipo dati, tipo handshake, acknowledge ecc. Per evitare
errori il pid viene scritto due volte: sarà di un byte quindi, poiché il PID è 4 bit. Ogni gruppo di
pacchetti (token, dati, ecc) a seconda di quei 4 bit ha tipi diversi. Il campo indirizzo è di 7 bit,
identifica con quale periferica voglio parlare (massimo 127, poiché l’indirizzo 0 è assegnato a
quelle periferiche che non ne hanno ancora uno). Il campo end point esiste poiché ogni periferica
ha uno o più end point (sulla stessa periferica usb potremmo avere più periferiche). Noi potremmo
voler parlare con un end point specifico della periferica, come se l’end point fosse il numero di
porta. CRC è un codice di correzione e serve per la rilevazione di errori. EOP indica la fine del
pacchetto.
N.b Una periferica non può mai iniziare una comunicazione.
Un token packet, che serve ad inviare una richiesta, può essere IN per leggere, out per scrivere,
setup per iniziare una transazione di controllo. Dopo i token packet che servono a dare questi
comandi ci sono i data packet: questo pacchetto avrà un PID che sarà data0, data1 e così via. La
dimensione massima del campo data può essere 8 bytes per low speed, 1023 bytes per full speed
e 1024 bytes per high speed. Generalmente noi trasferiamo dati di dimensioni maggiori e per
questo motivo usiamo tanti pacchetti data.
C’è il pacchetto di tipo handshake (gruppo status) che può essere un ACK, per dire che ho ricevuto
tutto correttamente, se il dispositivo non può temporaneamente mandare o ricevere pacchetti è
un NAK, se c’è un errore invece è uno STALL.
Start of frame è un pacchetto particolare che viene mandato per dire al dispositivo che deve stare
sveglio. La grandezza dei pacchetti dipende da low, high o full.

USB Function

Su ogni porta usb abbiamo più dispositivi e ogni dispositivo avrà un indirizzo diverso. Un
dispositivo può avere più funzioni, per ognuna di esse avremo uno o più endpoint. Ogni endpoint

28
(funzione) ha dei buffer in cui scrivere e leggere, che sarà endpoint 0 in, endpoint 0 out ecc. Ogni
buffer tipicamente è lungo 8 byte.
Quello che succede è che noi colleghiamo la periferica, questa viene riconosciuta, l’host manda un
pacchetto all’indirizzo 0, il primo pacchetto richiede il device descriptor, la periferica si riconosce e
copia il pacchetto nel buffer del giusto endpoint, essa poi manda un ack e genera una interrupt
interna per gestire la particolare richiesta data dal pacchetto.
Gli endpoint sono l’interfaccia tra il software del driver e la periferica; viene aperta una pipe tra
essi e il driver per la gestione della periferica. Il driver comunicherà con il firmware del dispositivo,
scriverà e leggerà nei buffer. Tutti i device devono avere un endpoint 0, che è quello che riceve
control e status requests del dispositivo.
Abbiamo più tipi di endpoint che ci identificano anche il tipo di trasmissione che possiamo avere:

 Control Transfers
 Interrupt Transfers
 Isochronous Transfers
 Bulk Transfers
Control Trasfers
E’ il tipo di transazione per comandi e stato (setup, assegno l’indirizzo ecc). Per fare una control
trasfer parliamo con l’endpoint 0. Il trasferimento dei pacchetti avviene in tre fasi:

 La prima è setup stage.


 La seconda è il data stage.
 La terza status stage.
Nella prima fase ho tre pacchetti e dico che
voglio fare, ad esempio “voglio leggere il
descrittore”, tipicamente il primo è un setup
token; nella seconda invio i dati di setup (qua
c’è la richiesta di assegnazione dell’indirizzo, è
nel data stage che effettivamente mando) e
nella terza capisco se tutto è andato bene. Il
Data packet non contiene veramente dati utili,
ma dettagli sulla control word. Due saranno inviati dall’host e uno dalla periferica. Il pacchetto di
ACK può essere di effettivo acknowledgement, oppure non ci sarà se la procedura non è andata a
buon fine. Dopo lo stage di setup c’è il data stage che può essere o di IN o di OUT:

 Nel Caso IN l’host deve dire mandami i dati e la periferica può rilevare un errore e non fare
niente, inviarli correttamente, mandare un NAK se non può comunicare, mandare uno
STALL in caso di errore. Se l’host ha ricevuto i dati correttamente gli ritorna un pacchetto di
ACK.

 Un altro caso è quello di OUT, ad esempio potrebbe essere l’assegnazione di un indirizzo


alla periferica (l’host dice nella fase di setup che vuole fornire l’indirizzo); la periferica
conferma che è pronta a riceverlo e l’host gli manda i dati o il pacchetto di configurazione,
29
poi risponde o con ACK, o con STALL oppure con NAK se il buffer è pieno perchè non può
ancora scrivere.

I dati possono essere anche mandati in più data stage se il pacchetto è effettivamente grande.
Infine abbiamo lo status stage, dobbiamo chiudere la transazione. Questa può essere chiusa in
due modi a seconda di quello fatto in precedenza:

 IN, ovvero se l’operazione era di lettura, la periferica deve sapere se l’ha ricevuto
correttamente e l’host lo fa dicendo di voler mandare un informazione; manda un
pacchetto di dati di lunghezza 0, e aspetta una risposta dalla periferica; che gli dice se ha
capito che la transazione è chiusa ed è andata a buon fine (ACK), oppure stava facendo
altro e non può prendere quei dati (NAK), oppure sono in errore(STALL).

 OUT, se volevo scrivere, deve essere la periferica a dire se è andata a buon fine la
trasmissione, però la trasmissione viene cominciata sempre dall’host. Quindi l’host effettua
una richiesta IN per lo stato e la periferica risponde o con un pacchetto di dati di lunghezza
0 o con stall o con nak; da questo punto l’host capisce che è andato tutto bene e risponde
con un ACK.

30
Questo è il tipo di transfer più complicato perché è formato da queste tre fasi: per iniziare il
trasferimento, lo scambio dei dati, e la chiusura con la fase di status.
Interrupt Transfers
In USB se un dispositivo richiede attenzione, deve aspettare finché non viene interrogato dall’host
che è l’unico che può avviare la comunicazione (nel frattempo il dispositvo mette in una piccola
coda tali richieste). L’host ogni tanto andrà a chiedere all’end point se c’è stata un interruzione
(con una certa frequenza definita dal descrittore); ancora una volta il payload ha dimensioni
variabili a seconda della velocità, va da 8 a 1024 bytes.
La funzione può essere interrupt in o interrupt out:

 Nel caso IN l’host manda un pacchetto in all’endpoint, se ha un interruzione in coda questo


manda un dato. Se invece non ho interruzioni pronte allora gli da un ACK. Se hai qualcosa
in coda mandamelo.

 Se invece voglio interrompere il device, ovvero nel caso OUT, gli devo mandare un OUT e il
tipo di interruzione, la funzione a cui mando l’interruzione poi risponde se l’ha ricevuta, se
ha buffer pieno o se si trova in errore.

Isochronous Transfers
Alcune periferiche potrebbero avere un endpoint di tipo isocrono, ovvero una funzione che
funziona continuamente e periodicamente (dispositivi per streaming audio e video), non ha
bisogno dell’ack, né di ritrasmissione. Questo garantisce la velocità che deve essere un must per

31
questo tipo di dispositivi. Ci sono anche qui dei codici di correzione errore, ma se si accorge che c’è
un errore semplicemente prende il pacchetto e lo scarta. Supporta solo la modalità full e high

speed e ancora la lunghezza del payload dipende da ciò.


Bulk Transfers
Questi sono i trasferimenti che riguardano i dati veri e propri. Sono supportati solo da full e high
speed. Sono trasferimenti di grosse quantità di dati dove è importante la correttezza, senza curarsi
della banda o della latenza. Ogni trasferimento ha 3 fasi; per garantire la correttezza, se non è
stato ricevuto il dato allora lo rimandiamo. (immagine uguale a data stage in control transfer).
Questo è un trasferimento utilizzato per dispositivi dove trasferiamo grandi file, come ad esempio
streaming su rete, ecc.
Bandwidth managment
In questo caso per banda si intende quanti transfer sono di un certo tipo in una generica
comunicazione. Vediamo come viene gestita la banda del dispositivo: di questa ne è responsabile il
controller, e la gestisce nel momento della connessione dei dispostivi (per ogni dispositivo
connesso va ad allocare una certa banda). Quindi alloca questa banda e ne può dare fino al 90% su
un bus full speed e 80% per gli high speed, mentre il restante 10% (o 20%) lo deve lasciare libero
per i control transfer; per quelli di tipo bulk non c’è bisogno di banda e vengono inviate quando si
hanno spazi liberi.
USB descriptors
Vediamo com’è organizzato il dispositivo. Ogni dispositivo ha un suo Device Descriptor in cui
possono essere elencate un certo numero di configurazioni (info sull’alimentazione, massima
potenza assorbita, numero di interfaccie), ad esempio per un dispositivo che può funzionare come
memoria di massa, modem, ecc. Ogni configurazione contiene la lista delle sue interfacce ed
ognuna di essa con un certo numero di endpoint. Ad esempio lo stesso disco potrebbe funzionare
come selfpower oppure alimentato!
Nel device descriptor ( in cui si trovano anche product id e vendor id),troviamo un numero che
indica quanto è lungo il descriptor in bytes, poi una costante che indica che siamo un device
descriptor, che tipo di USB supporto e che tipo di dispositivo sono, come ad esempio un
dispositivo standard che non ha bisogno di driver proprietario e poi altre informazioni tra cui il
numero di possibili configurazioni. Ricevuto questo numero allora chiedo ognuna delle
configurazioni ed ognuna di esse mi dice quali sono le interfacce.

32
33
L’Endpoint Description ci dice che tipo di endpoint è ovvero il tipo di trasferimento che supporta.
Mostro un esempio:

Da USB 3.0 si è passato a 8 connettori, è retrocompatibile, e la connessione è diventata full duplex.


Poi l’USB di tipo C ancora più veloce, inoltre supporta diversi livelli di alimentazione, che permette
anche di caricare o essere caricati.

Gestione cache distribuite


Siccome il pentium ha una cache di 16K 8 dati e 8 istruzioni, siccome il processore può essere
montato sulla scheda madre con altri processori, quindi queste cache tra di loro devono parlare, se
un processore scrive sulla cache gli altri non se ne accorgeranno avendo dei problemi sulla RAM, si
avranno problemi di coerenza.

34
Per questo motivo il processore ha delle cache con più porte, alcune vengono usate per parlare
con le altre cache e altri processori, altre per lavorare come se fosse da solo.
Le cache che abbiamo sono set associative a due vie, questo significa che in ogni gruppo indirizzato
da multiplexer si possono mettere due linee, occupare due posti in ogni gruppo della cache.
Abbiamo 128 set e ciascuno ha due blocchi. Ad ogni set è associato un bit per vedere se il blocco è
valido o no ed ogni blocco è
composto da 32 byte.
Difatti ogni indirizzo usa i bit da 31
a 12 che sono il TAG, i bit da 11 a
5 il SET, i bit da 4 a 0 ci vanno ad
indicare quale segmento di byte
voglio andare a leggere.
Importante è che i data cache
sono a tripla porta, 2 per le
pipeline, per scrivere e leggere
dalla cache, se si tratta della
floating point le usa tutte e 64 assieme, la terza porta serve diverse cache per parlare tra di loro. È
a tre porte perché si perde la coerenza nelle operazioni di scrittura.
Politiche di rimpiazzamento
C'è una WB (il blocco viene scritto in memoria centrale solo quando è scaricato) e WT (la memoria
viene riscritta ad ogni acceso in scrittura della cache) per la scrittura, sono entrambe utilizzare,
quando il processore è da solo allora la politica viene scelta in base al sistema operativo, quando
lavora in multiprocessore esiste un protocollo per la gestione della politica di scrittura per la
gestione delle coerenze.
Con questo chiudiamo il pentium, da questo in poi quello che avanzerà sarà il multithreading, che
permette di far avanzare più processi contemporaneamente.

Architetture Multiprocessore SMP


Parliamo di architetture con diversi processori che interagiscono con la stessa RAM, il tutto sulla
stessa scheda madre. Un solo processore può accedere alla memoria, ma ognuno di essi
chiaramene vorrebbe! Per andare più veloci si aggiungono delle cache, quando il processore porta
il blocco in cache poi lavora localmente, con lo stesso principio dell’architettura a singolo
processore: in questo modo gli n processori possono evitare talvolta di accedere alla RAM e
richiedere il BUS.
Però c’è un problema, ovvero quando scrivo nella cache perdo coerenza con ciò che sta scritto
nella RAM se uso una politica write back, ma anche se uso una write through comunque la perdo
anche con le altre cache. Sappiamo come si gestisce la coerenza con la RAM, ma che succede
rispetto alle altre cache?
35
Se abbiamo un informazione che viene condivisa tra più
processori allora tutti gli effettueranno delle modifiche,
ognuno modificherà la propria copia in cache.
Dobbiamo tener conto della politica di coerenza utilizzata
con la RAM, ne avevamo 3:

 Write Back , scrivo solo quando sostituisco il blocco,


allo svuotamento.
 Write Through, scrivo nella RAM ogni volta che modifico nella cache.
 Write Once, che effettua una prima scrittura sia in cache che in RAM, se ce ne saranno
altre allora le farà solo in cache.
Se uso WT ho sempre la coerenza nella memoria condivisa, e quando vado a leggere trovo subito il
dato pronto, non devo recuperare la coerenza, gli altri processori che andranno a leggere
troveranno il dato coerente. Lo svantaggio è che andiamo lenti perchè scriviamo sempre in ram.
Per migliorare spesso vengono usati dei buffer che permettono di eseguire mentre viene
aggiornata la memoria. Se usiamo WB andiamo molto veloci, ma non abbiamo mai la coerenza con
la RAM, a meno che non abbiamo fatto lo svuotamento; quindi se ad un altro processore serve il
dato, il processore che lo ha sporcato lo deve salvare e poi può essere letto. Si perde la coerenza
in diversi casi, ad esempio quando abbiamo:

 Una condivisione dei dati, i diversi processi condividono gli stessi dati, effettuano
operazioni su di essi, quindi senza un meccanismo che mantiene la coerenza, essa non è
assicurata. Nel caso di WB succederebbe che i processori p1 e p2 leggono il dato, poi se uso
una scrittura in c1, il dato nell'altro processore non è più valido. In entrambi i casi se il dato
è condiviso non abbiamo coerenza tra le due cache, ma solo con la ram nel caso WT poiché
la cache due avrà comunque il dato vecchio.

 Operazioni di I/O, i processori sono collegati con delle periferiche o sullo stesso BUS o su
spazi di indirizzamento diversi. Quello che succede è che se il DMA ha aggiornato il dato in
memoria RAM, ed entrambi i processori hanno il dato in cache, esso sarà obsoleto! Nel
caso della lettura avviene lo stesso, poiché se il dispositivo di I/O legge il dato nella RAM
che era stato modificato da un processore nella cache, ne leggerà il vecchio valore.

 Migrazione dei processi, ad esempio se avessimo due processori che eseguono lo stesso
processo, il dato lo sto usando sul primo processore, che ne effettua una modifica in locale
senza aggiornare la memoria condivisa; il processo viene migrato in un secondo momento
(rischedulazione) sul secondo processore (che non ha copia locale) e questo non si accorge
che nel frattempo il dato è cambiato; questo vale solo se si usa WB, nel caso di WT nella
memoria condivisa abbiamo sempre la coerenza e quindi non ci sono problemi. Un altro
caso che vale per qualsiasi politica di scrittura utilizzata è che entrambi i processori abbiano
una copia in locale, quindi quando il processo migra uno userà la sua copia non aggiornata.
Per ottimizzare le si può usare un parametro detto affinità in modo da schedulare lo stesso
processo sullo stesso processore, in modo da non dover migrare il dati e non dover
allineare la memoria.

36
Protocolli mantenimento della coerenza
Per gestire la coerenza esistono due tecniche:

 Alcune software, che per essere molto efficienti vengono applicate al tempo di esecuzione,
sono delle istruzioni particolati utilizzate appositamente e staticamente per mantenere la
coerenza (poco utilizzata).

 Altre hardware, per ogni processore sulla scheda madre c’è un organo che consente la
gestione della coerenza delle cache in modo dinamico: due soluzioni, usando dei cataloghi
o usando tecnologie snoopy(sniffing). Sono queste ultime che approfondiremo.

Protocolli Hardware
Quelle su catalogo si basano su una tabella condivisa, dove viene scritto quale blocco di cache è
valido o meno e qual è modificato; e i processori vedendo in questa tabella sanno se va
recuperata o meno la coerenza per un determinato blocco. Non le vedremo.
Le snoopy cache hanno (ognuna) un controllore che osserva su un bus quello che succede (bus
unico); osservando i pacchetti che transitano su questo bus capisce quali sono le linee che hanno
perso coerenza e quali quelle coerenti, nel caso in cui non fossimo coerenti avviano un processo di
recupero della coerenza. Sono quindi delle sezioni hardware che consentono la gestione della
coerenza in sistemi multiprocessore a memoria condivisa. Già pentium permette questa soluzione.
Esistono due politiche snoopy per gestire la coerenza, la prima (write invalidate) ci invalida il dato
nella cache quando si è persa la coerenza, la seconda (write update) garantisce l'aggiornamento,
recuperando il dato appena modificato. Noi vedremo solo la prima.
Noi andremo a vedere quindi 3 tecniche, che consistono nella combinazione di tutte quelle della
coerenza con la memoria e la Write Invalidate, più una quarta soluzione adottata dall’Intel.

Write invalidate write through


La prima tecnica è molto semplice, il processore scrive il dato sempre in cache e in ram usando
una logica WT. Il controllore per ogni linea di cache utilizza un bit per linea, il quale gli dice se
quella linea è valida o meno, ogni linea del processore può trovarsi in due soli stati. Se io
processore sto leggendo o scrivendo il dato allora vuol dire che ne ho la copia aggiornata del dato.

37
W(i)/R(i) rappresentano le azioni da parte del processore stesso che sta gestendo questo automa
(che si riferisce all’i-esimo processore, con k ci riferiremo a uno qualsiasi degli altri); se sostituisco
la linea (Z(i)) allora so che quel blocco non è più valido e cambio stato. Se lo sostituisco di nuovo
allora rimango qui. Se vado a leggere il dato allora il dato è di nuovo valido. R(k) e Z(k) vogliono
dire che un altro processore ha letto il dato o ha svuotato la line di cache perché non lo usa più.
Se qualche altro processore ha scritto quel dato allora non è più valido, perchè noi abbiamo la
versione vecchia, il controllore se ne accorgerà e alla lettura di quella variabile si genererà un
cache miss, forzando i a prendere il dato
dalla RAM; il protocollo in sè è molto
semplice, dovremmo aggiungere un bit
ad ogni linea di cache e cambiare stato
della linea in base alle operazioni.
Write invalidate write back
Adesso usiamo come politica di gestione
di coerenza con la RAM la WB, ogni
processore non scrive mai nella RAM, ma
lo farà solo alla fine quando dovrà fare lo
svuotamento.
Succede che se un processore va a scrivere, tutti gli altri devono invalidare il dato, quindi il
processore può trovarsi in 3 stati; nello stato non valido, nello stato read dove il dato è valido
perché k ha effettuato solo una lettura, nello stato read e write dove il dato è ancora valido,
perché è quello più aggiornato, ma non è coerente con la RAM. Partendo da Read e Write,
qualsiasi cosa faccio è chiaro che questo dato rimarrà sempre valido perché sono l’unico ad averlo
(tranne se lo tolgo dalla cache). In questo caso ci serviranno due bit.
Supponiamo che i abbia il dato aggiornato in cache: se qualcun'altro lo vuole leggere allora
bisogna avviare un protocollo di recupero della coerenza, perchè per lui il dato non è valido.
Quindi lo bloccherò un attimo, recupero la coerenza e poi ne permetto la lettura, e io mi porto
nello stato Read. Tornerò in R&W sempre quando io faccio una scrittura, poiché tale stato significa
che io detengo l’unica copia aggiornata del dato. Allo stato non valido ci vado sostanzialmente
quando qualcun’ altro scrive quel dato nella propria cache: da qui posso andare in R&W se lo
scrivo e in Read se comando una lettura, poiché k sarà forzato a ripristinare la coerenza e a forzare
la lettura di i dalla MC. In read invece io sono
coerente con la memoria condivisa, se altri
leggono non mi interessa ma non appena
qualcuno scrive o io scrivo vado o in non
valido o nello stato in cui devo preoccuparmi di
invalidare le operazioni (R&W)
rispettivamente.
Write invalidate write once
L'ultima soluzione è quella che usa la write once
con la ram, l'unica diversità rispetto a prima è che
38
quando scriviamo la prima vota dobbiamo ricordarci di mantenere la coerenza con la ram.
Abbiamo quindi 4 stati: valido, reserved, dirty, non valido. Partiamo sempre dallo stato non valido
(anche negli altri esempi). Lo stato valido significa che il dato l'ho letto dalla ram, è coerente con
essa e con tutte le altre cache, a questo punto se leggo rimango qui dentro ed anche se altri
leggono. Se scrivo la prima volta vado in reserved, perchè ho scritto quel dato, l'ho cambiato ed è
uguale a quello della ram perchè è stata la prima scrittura; a questo punto succede che se
qualcun'altro legge sarà forzato ad andare in RAM e ritorno nello stato valido, è di nuovo coerente
con la ram e con la cache, è come se azzerassi il protocollo write once, quindi faccio finta che la
prima scrittura non sia mai avvenuta!

Nel caso mi trovo ancora in reserved e scrivo di nuovo allora vado nel dirty, dove io sono l'unico ad
avere il dato corretto, se qualcun altro deve leggere, quindi mi fermo, recupero la coerenza con la
ram e rivado in valido; ovviamente se qualcuno scrive io vado sempre nel dato non valido. Perché
qualsiasi scrittura non influenza la mia cache. Ogni volta che gli altri svuotano rimango dove sto
perché non cambia niente. Quello che dobbiamo ricordarci è che se qualcuno legge dobbiamo
ricominciare da capo il protocollo.
Abbiamo visto quindi 3 soluzioni, tutte e tre che usano write invalidate per recuperare la coerenza
tra le cache e usano una delle 3 soluzioni già viste per recuperare coerenza con la memoria. La
complessità aumenta perché cresce l’automa da implementare, questo deve essere implementato
per ogni blocco della cache!
Protocollo MESI
Vediamo un’implementazione reale di un protocollo snoopy, ad esempio quella utilizzata dall'Intel.
Il protocollo si chiama MESI, usa una politica WB Write Invalidate. Due bit per ogni linea di cache,
quindi 4 stati:

 Modified, dato nella mia cache il solo valido, incoerente con la RAM.

39
 Exclusive, dato valido e presente solo nella mia cache, coerente con RAM, non modificato
ancora (lo stato aggiunto).
 Shared, dato valido e presente anche in altre cache, coerente con RAM.
 Invalid, dato invalido.
Tale protocollo fa differenza tra lettura e scrittura in caso di miss e in caso di hit: se ho una miss
con S allora il dato è valido nella memoria principale e almeno in un’altra cache, se ho la miss con
E è valido sono nella memoria principale. ShW è la scrittura da parte di qualcuno.

Supponiamo di essere in uno stato in cui il dato lo sto utilizzando ed è valido, qualunque lettura io
faccia, rimango nello stesso stato. Se sto nello stato INVALID, può essere che io faccio una lettura e
sono l’unico ad avere quel dato nella mia cache e quindi vado in EXCLUSIVE, se faccio la lettura e il
dato ce l’ha qualcun altro vado nello stato SHARED. Se sto nello stato SHARED e c’è una SHARED
WRITE, allora il mio dato non è più buono, passo ad INVALID e questo vale anche in qualsiasi stato
mi trovo. Se sono in SHARED o sto in EXCLUSIVE se faccio una scrittura il dato è valido ma
modificato, quindi passo allo stato MODIFIED. Difatti questo stato mi rappresenta che il dato è
valido ma non c’è coerenza né con cache né con RAM, mentre in EXCLUSIVE il dato è valido ma
non è presente in altre cache; in SHARED è valido per tutti. In qualsiasi di questi tre stati se
effettuo un’operazione di scrittura passo allo stato MODIFIED. Mi porto dallo stato EXCLUSIVE a
SHARED se qualcun altro legge. Lo stesso in MODIFIED, ma stavolta devo prima bloccarlo,
recuperare coerenza con la RAM e poi può leggere.
La differenza rispetto il WO è che non si anticipa il passo degli altri, lì si diceva se qualcun altro ce
l’ha allora quando vado a scrivere la prima volta allora scrivo pure in RAM, mentre qui se qualcun
altro ce l’ha non scrivo in RAM, non faccio quel gioco di recuperare la prima volta in RAM e poi
farlo sempre.
40
Questo che implementa il protocollo snoopy non è lo stesso BUS che accede alla memoria
centrale, ma sarà un BUS apposito.

Tecniche di controllo
L’unità di controllo è quella parte del processore che si occupa, dato un data path e un’istruzione
da eseguire, di mandare i giusti segnali alle diverse componenti in modo da portare a termine
l’esecuzione di tale istruzione. Essa può essere realizzata in logica cablata o microprogrammata. La
logica cablata è quella che usano ad esempio le architetture RISC, la microprogrammata per
architetture CISC che hanno istruzioni un po’ più complesse (parleremo di microcodice).
Per la logica cablata, basta un instruction register, codici di condizione, flags di stato per definire
una serie di messaggi per il data path. Il contatore scandisce i passi necessari per l’esecuzione di un
istruzione, i valori dei segnali di controllo varieranno a seconda di quale fase ci indica il suddetto.

Il principio alla base della logica microprogrammata invece è che ogni istruzione complessa del
codice assembler viene eseguita con un microprogramma (scritto in simil assembly) con
microistruzioni nel processore, che corrispondono ad una sequenza di segnali da mandare al
datapath. Ci sarà poi una piccola memoria rom che contiene le microistruzioni per ogni istruzione
assembly. In questo modo noi possiamo cambiare il set di istruzioni oppure estenderlo, andando a

41
cambiare il contenuto di questa memoria rom.

A partire dalla particolare istruzione si può ricavare un puntatore ad un microprogramma, che


eseguirò scandendo le operazioni tramite un microPC. All’uscita della memoria di controllo ci sono
i segnali di controllo. Ci sono casi in cui abbiamo che la sequenza di microistruzioni da eseguire
non dipende solo dall’istruzione ma anche dai flag di stato e codici di condizione (ad esempio
istruzioni di salto). Inoltre se dovessimo avere un microprogramma per ogni istruzione, la
memoria sarebbe troppo grande, quindi quello che si fa è riutilizzare parti del microprogramma
per altre istruzioni utilizzando salti a microistruzioni utili a più istruzioni, realizzando un vero e
proprio microprogramma. Questa soluzione ci permette di realizzare applicazioni con poche
istruzioni complesse, ma occupa spazio sul chip, togliendolo quindi ai registri, alla cache, la
pipeline, ecc. Oltre al micro program counter esiste anche il micro instruction register.
Per ogni colpo di clock ci sono 4 fasi per ogni microistruzione: caricamento della uInstructione
nella uIR; caricamento dei registri A-Latch, B-Latch; abilata ALU; write back

Un esempio di microprogrammed control unit:

42
Parallelismo
Questo argomento è di natura del tutto generale e ci porterà in seguito a parlare del Pentium 4.
Innanzitutto c’è da dire che abbiamo già studiato diversi tipi di parallelismo: per i dispositivi di IO
abbiamo visto il DMA, per il processore le pipeline e cosi via. La prima nuova forma di parallelismo
di cui parleremo ora è la superscalarità, una strategia che consiste nel replicare all’interno di un
unico processore le unità di calcolo. In questo ambito abbiamo due soluzioni: la prima è detta
scheduling statico, che viene preposta dal programmatore, è lui a dover indicare al compilatore
sul come mandare in esecuzione, come tradurre in linguaggio macchina, ad esempio dicendo di
fare iterazioni di un ciclo in parallelo ecc. Ci sono proprio delle istruzioni per dare delle
informazioni al compilatore su come tradurre poi in linguaggio macchina; la seconda è detta
scheduling dinamico il programmatore non dirà niente, sarà il processore a prendere le istruzioni
e sfruttare le risorse hardware dedicate, capendo quali istruzioni possono essere eseguite in
parallelo e come schedularle. Un esempio del primo caso è il processore IA64 che portava con sé
l’esecuzione speculativa, ovvero l’eseguire entrambi i rami di un branch per non perdere cicli utili.

43
Ci occuperemo maggiormente di quella dinamica, dove in genere le istruzioni verranno eseguite in
ordine diverso da come fornito. Il fetch sarà sempre unico, ma il processore sarà l’organo che
deciderà, con una certa logica, l’ordine di esecuzione, riordinando chiaramente i risultati
nell’ordine sequenziale previsto. Il programmatore non sà nulla. E’ la soluzione adottata in parte
dal Pentium 4.
Nel caso del pentium4 viene prelevata l'istruzione, decodificata e trasformata in tante
miscroistruzioni, più semplici, queste vengono eseguite con un ordine casuale. L’ordine è dettato
da quale elemento sia libero; se si libera un componente rispetto ad un altro, allora mandiamo
quella microistruzione, senza badare all'ordine, quando finiamo tutte le microistruzioni allora
riordino i risultati e scrivo in memoria, riformando l'istruzione assembler più complessa che
avevamo in precedenza.
Multithreading
Per multithreading si intende una forma di pseudo parallelismo per una o più CPU in cui alcuni
entità, dette appunto thread, cooperano sullo stesso processo in modo concorrente. Si potrebbero
addirittura avere situazioni in cui i thread agiscono su processi diversi. Un processo ha la sua area
codice, area dati e variabili, mentre i thread in genere sono due flussi di esecuzione dello stesso
processo, condividono lo stesso codice, stessa area dati. Istruzioni di due processi diversi o thread
diversi entrano nel processore, però per avanzare concorrentemente devono puntare a due
indirizzi diversi, quindi il processore deve avere duplicato il PC, i registri, deve etichettare le
istruzioni per capire a quale thread appartengono e deve avere un piccolo hardware dedicato per
distinguere le due cose. Inoltre è palese che il thread switch debba essere più rapido di un process
switch, che richiede centinaia di cicli di clock! N.b. parliamo di thread implementati già
dall’hardware! Ora la domanda è: come entrano nella pipeline?
Abbiamo tre tecniche principali:

 Coarse-grained multithreading, funziona che nella pipeline nello stesso istante possono
essere presenti le istruzioni di un solo thread alla volta; quando un thread va in stallo
perchè deve aspettare un componente o una periferica allora viene svuotata la pipeline,
congelando lo stato del thread, e faccio avanzare le istruzioni del thread che è libero, ad
ogni cambio svuoto la pipeline e faccio entrare le istruzioni del thread libero. In questo
modo non ho mai nessun’istruzione in attesa di risorse. Svuotare la pipeline è comunque
un’operazione in più.

 Medium-grained multithreading, posso andare a switchare le istruzioni che entrano nella


pipeline quando prevedo uno stallo, lo scambio tra thread avviene solo quando uno sta in
stallo.

 Fine-grained multithreading, è la più prestante, permette ad ogni colpo di clock di inserire


un istruzione di un thread piuttosto che un altro, in questo modo possono alternarsi anche
in maniera perfetta, magari in funzione di quale core devono usare, quale adder e così via.

Come si fa a sapere a quale thread “appartiene” un’istruzione? Nella logica fine grained è chiaro
che l’unica soluzione è quella di etichettarle con l’ID del thread, ma nel coarse essendo che tutte le
44
istruzioni presenti in pipeline sono del medesimo thread potrei accorgermene molto più
facilmente.

Supponiamo che le colonne siano tante pipeline, è superscalare perchè ha le risorse dedicate.

 Nel primo caso ho un solo processo perché ho un solo colore, metto gli stalli e continuo.

 Nel secondo caso congelo le istruzioni in stallo e faccio entrare le istruzioni di un altro
thread.

 Nel terzo caso mi permette di cambiare le istruzioni (immagina le righe tutte dello stesso
colore). In una fase della pipeline posso trovare istruzione di un solo thread, però posso
cambiare mettendo le istruzioni di un altro thread quando si libera una risorsa, se uno è in
stallo.
 Ultimo caso, parliamo di simultaneous multithreading (SMT), usa sia il fine grained
multithreding che la super scalarità, ovvero nella stessa fase posso trovare istruzioni
diverse, ad esempio il thread 1 usa l’adder mentre il thread 2 usa l’unità floating point o
altro.

Hyperthreading

45
L’implementazione dell’SMT di intel si
chiama hyperthreading. Fu introdotta
nel processore XEON e poi a seguito nel
P4. L’inserimento di una logica
multithread comportò l’aumento del 5%
delle dimensioni a fronte del 30 % circa
sulle performance, poiché si andavano a
sfruttare i tempi morti che una cpu
lasciava. E’ un meccanismo che
permette di schedulare ad ogni colpo di
clock istruzioni di thread diversi, e
questi thread possono
contemporaneamente sfruttare l’
architettura super scalare. Quello che
ovviamente non è concesso è che i due
thread usino la stessa unità contemporaneamente.
Multicore
Per avere processori sempre più potenti si pensò di farli girare sempre più velocemente (si parla di
frequenze). Il problema fù che l'aumento della frequenza comportò la necessità di grosse capacità
di dissipazione, addirittura nacquero sistemi di raffredamento a liquido! Il concentrasi
sull’aumento della potenza della CPU non era, evidentemente, la strada giusta verso il progresso
tant’è che si riprese la strada del
parallelismo (per un periodo abbandonata)
esasperandone le potenzialità. Questo portò
alla nascita di soluzioni multicore. Un core si
definisce come un vero e proprio processore
logico, montato sulla stessa SoC insieme a
tutti gli altri core.
Il multicore permette di far andare istruzioni
in parallelo su più unità; non stiamo parlando più di multithreading, qui il parallelismo è reale,
però comunque tale soluzione fu unita all’idea del multicore negli anni successivi. L’immagine, per
un sistema singolo core, prevederebbe una sola di quelle unità! Non è superscalare, qui tutto è
duplicato: ogni core ha un’ alu, i suoi registri, una sua cache, interfaccia verso il bus, tutti i core
accedono allo stesso bus ed arrivano alla memoria che solitamente è una cache di livello 2 o livello
3.
Ogni core può eseguire un solo thread o anche più thread nelle ultime versioni, avendo
l’hyperthreading per ogni core. Un thread non è assegnato a priori ad un core, però si può in
qualche modo aumentare il cosiddetto grado di affinità, facendo in modo che un certo thread giri
sempre sullo stesso core e che non si richiedano pesanti campi di contesto (svuotamento cache e
cosi via) che rallenterebbero di molto l’esecuzione.

46
Il SO vede i core come un processore. Questi
servono quando abbiamo più richieste
contemporaneamente, come l'accesso ad una
web server, dove molti accedono alla stessa
risorsa, ma nel frattempo dobbiamo anche
rispondere.
Multicore con hyperthreading:

GPU
Le gpu hanno tantissimi core, hanno una loro ram, con una cache e dei gruppi di processori
ognuno con tante alu, quindi multiscalarità, che condividono la ram ed eseguono più o meno le
stesse istruzioni. Per questo motivo tale unità di calcolo è detta ad array di multiprocessori. SI
parla di stream computing, dove arriva un gruppo di dati e il primo libero fa la stessa operazione
su ogni dato.
Il problema è che il processore che ha la memoria ddr , la ram, se vuole fare un’operazione con la
scheda video allora deve passare i dati dalla ram all GPU, la gpu fa i calcoli e li rimette nella ram,
quest'operazione richiede una grande banda; per questo motivo si adotta un bus pci express a più
o meno linee, ed è questo il collo di bottiglia che va ottimizzato. Attraverso l’ambiente CUDA è
possibile dividire il codice in parti che vanno eseguite in modo sequenziale (CPU) e parti che vanno
eseguite in parallelo (GPU). Tali parti prendono il nome di kernel; un kernel non può essere
assegnato a più di un elemento multiprocessore.

Pentium 4
Pentium 2 e Pentium 3 non si differenziano dal Pentium studiato e per questo non ci
soffermeremo: le uniche novità riguardano le dimensioni delle cache e i miglioramenti degli
algoritmi per avere la pipeline sempre piena. Il Pentium 4 introduce l'hyperthreading per gestire
più thread contemporaneamente; per questo motivo viene visto come più processori dal SO. Così
come il Pentium, è un processore a 32 bit, singolo core.
Noi per avere performance più elevate conosciamo le seguenti tecniche: aumenta il throughput
con la pipeline, granularità più fine , brench prediction per avere la pipeline sempre piena, super
scalarità duplicando le risorse, esecuzione fuori ordine per migliorare i brench anticipando
istruzioni che ad esempio non usano input output ecc. E poi memorie veloci, cache che aumentano

47
in dimensione. Sono tutte tecniche che mirano a migliorare le prestazioni e le abbiamo viste già

tutte.
Dal seguente grafico si evince come con il progredire dei modelli, si è andati verso l’aumento delle
dimensioni, corrispondente all’aumento della potenza richiesta. Le prestazioni sono misurate con i
cosiddetti benchmark, programmi appositi che vanno a sollecitare certe dinamiche con lo scopo di
misurare alcune grandezze. Il risultato finale (a cui ci fermiamo per l’Intel) sarà il Pentium 4 che
sarà 5 volte più potente dell’intel 486. Il multithreading è stato un innovazione perchè è
l’alternativa ad inserire due processori sulla stessa scheda madre, che hanno sicuramente un costo
superiore. Un processore che permette l’esecuzione multithread non necessità di duplicare tutte
le risorse ma solo una parte, proprio per garantire che il context switch sia corretto (flags di stato,
PC, registri interni, gestore delle interruzioni, registri di appoggio e cosi via); altre risorse invece
sono partizionate più che duplicate, mentre ancora altre sono condivise (bus dati, memoria
interna).
Organizzazione interna Pentium 4
Vediamo come viene realizzato il pentium 4 dal
punto di vista del layout, l'architettura (intesa
come organizzazione, le istruzioni assembly sono
sempre le stesse).
All'interno ho una pipeline molto lunga (20 stadi),
con un modulo di esecuzione Rapid Execution
Engine (REE) che contiene tanti moduli replicati,
ha una cache di primo livello che si chiama
Execution Trace Cache, un sistema che permette
l'esecuzione speculativa delle istruzioni,
eseguendole fuori ordine, anche se c’è una certa dipendenza, e poi un bus molto veloce, detto
Quad Pumped Bus, ovvero due linee di clock sfasate di mezzo periodo, le operazioni sono svolte
sui fronti di salita e discesa delle due.
A partire dalla memoria ram abbiamo una cache di secondo livello che funziona come abbiamo
visto fin'ora, ma conterrà sia dati che istruzioni; se mi servono dei dati o delle istruzioni me ne
faccio una copia vicino al processore, nella cache di primo livello: le istruzioni però vengono
tradotte in qualcosa di semplice (RISC) e la cache istruzioni di primo livello, che conterrà tali
microistruzioni (almeno 4), la chiameremo trace cache. Dalla trace cache il processore lavora
come se fosse un processore risc, con delle istruzioni più semplici, e unità di controllo più
48
semplice, ma una pipeline molto lunga. Al completamento delle istruzioni ci sarà poi un
meccanismo che salverà il risultato in maniera ordinata. L’unità di elaborazione (Rapid Execution
Engine) è costituita da una serie di moduli duplicati, una cache di secondo livello indirizzata
attraverso segmento+spiazzamento, 2 moduli che si preoccupano di calcolare l’indirizzo, 2 ALU per
le istruzioni semplici, un’ALU per le istruzioni complesse, un FP unit per operare con i floating point
e un modulo che si occupa solo di spostare i FP nella cache. E’ facile notare che abbiamo una
grande
superscalarità.
Vediamo come
funziona la
Pipeline: nella
figura vedremo in
giallo e rosso i
due thread
diversi, i PC
puntano
entrambi alla
prossima microistruzione da eseguire nella Trace Cache, ad ogni colpo di clock una volta si preleva
l’istruzione di un thread una volta dell’altro, le istruzioni vanno in due code separate e vengono
taggate per indicare di che thread sono, così il processore sa a chi appartengono.
Nel caso in cui ci fosse una miss, per arrivare alla Trace Cache c'è un accesso alla cache di secondo
livello, con due table (TLBs separate) per fare la conversione da indirizzo virtuale a indirizzo fisico e
accedere alle L2, dopo di che vengono
decodificate, tradotte in microistruzioni e
viene riempito la cache di primo livello.
Qui abbiamo i PC che indicano la
sequenza di microistruzioni da eseguire,
le quali sono posizionate in code distinte
(per i due thread) di microistruzioni.

A questo punto le istruzioni RISC devono essere eseguite, ogni istruzione risc indirizza un registro;
dopo di che vengono portate ad una coppia di code (una per le operazioni logico aritmetiche, una
per i load e store); abbiamo in seguito degli scheduler, che non si preoccupano di quale thread
stanno eseguendo, vedono solo una microistruzione (si perde l'associazione con il thread): vedono
la prima disponibile (in base alla disponibilità dei dati e delle ALU) e la mandano in esecuzione, in
questo modo si cerca di tenere sempre occupato tutto il chip. Dopo eseguita si va a fare il write
back dei valori nei registri temporanei; quando hanno finito tutte le micro istruzioni, un’unità
riordina il risultato. Perché è necessario il riordino? Perché le microistruzioni RISC arrivano in
modo completamente disordinato! N.b la trace cache è una 8 way set associative, LRU come
algoritmo di sostituzione. C’è quindi una sorta di Istruzione Assembler->Traduzione->Tante

49
microistruzioni->Esecuzione disordinata->Ricompilazione del risultato.

Potrebbe succedere anche che ci troviamo in un ciclo in cui il processore suppone che il salto
debba essere fatto: quindi preleva le istruzioni, comincia ad eseguirle e poi si accorge che non
avrebbe dovuto; a questo punto cancella tutto, senza fare il retire e ricomincia a prelevare dal pc
+4. Si svuota solo la parte del thread che ha sbagliato. Le strutture per la branch prediction sono o
duplicate o condivise. Quando c’è una cache miss, dalla cache di secondo livello si prelevano le
istruzioni che mi servono, traduco attraverso un microcodice nella ROM e quindi quella di primo
livello viene riempita con queste istruzioni e non con quella di secondo livello.

Per tenere lo stato di tutte queste microistruzioni abbiamo 126 righe per i buffer di ordino, 128
registri interi (renaming) e floating point, 48 registri di appoggio per le load e 24 per le store (tutta
una serie di memorie per memorizzare gli stadi intermedi). La fase di rename serve a mappare in
qualche modo i registri condivisi per i due processori logici (si usano due RAT registers alias table,
una per thread). Abbiamo 5 scheduler con code da 8-12 posti. La memoria dati è fully associative
da 64 entry ed ogni entry può mappare pagine da 4k o 4 MB. È una memoria condivisa, tutta la
parte di memoria non sa che esistono due thread diversi, tranne quella di livello 1.
Ricorda: sarà sempre il programmatore che predisporrà un programma al multithtreading, ma di
certo non scenderà nei dettagli descrivendo cosa andrà dove e in che momento.

50
Spectre e Meltdown
Entambi gli attacchi mirano a rubare informazioni dal calcolatore, in particolare dalla RAM:
ricordiamo che i processi che sono in esecuzione si trovano fisicamente in RAM. In particolare
Spectre cerca di rubare dati da un’applicazione utente (come una password del browser)
rompendo l’isolamento che c’è tra processi e non c’è bisogno che l’applicazione abbia un bug, anzi
più cerchiamo di proteggerci con controlli e più siamo vulnerabili. Meltdown ruba sempre dati
dalla RAM, ma generalmente invade lo spazio riservato al sistema operativo: difatti
un’applicazione utente potrebbe accedere a tutta la memoria fisica rubando informazioni utili.
Entrambi gli attacchi si basano sul concetto di side channel (canale secondario).
Quando il processo sta girando indirizza una locazione di memoria, questo viene tradotto in
indirizzo fisico da un certo apparato, quindi il processo potrà accedere solo ai propri indirizzi. Nello
spazio di indirizzamento virtuale si trova sia una user part, sia una kernel part (questa può essere
acceduta solo in modalità privilegiata). Il SO mappa gli indirizzi dell’intera memoria in una certa
sezione, ma mappa anche pagine degli altri processi, alcuni indirizzi virtuali corrispondono ad
indirizzi di altri processi, perché magari deve farne il trasferimento della pagina. Quindi accedendo
a quest’area posso accedere a tutta la memoria.
Il concetto di esecuzione speculativa è fondamentale: se si fa partire un’eccezione,
quest’istruzione viene eseguita in ogni caso perché il processore esegue tutte le istruzioni non in
ordine; quindi si va al catch, si esegue, poi si ritorna indietro, ma comunque l’esecuzione è
avvenuta! Per motivi di performance essendo stata eseguita l’istruzione i dati rimangono nella
cache, anche se non servono. Se quindi leggiamo tutti gli indirizzi di memoria e calcoliamo il tempo
tra prima e dopo la lettura (posso farlo nell’handler), possiamo verificare se in quella linea c’è un
hit, e quella sarà la linea in cui si trovano dati utili (chiaramente devo prima svuotare la cache).
Quindi in questo modo un’istruzione del programma che non doveva essere eseguita, ha cambiato
lo stato del processore cambiando la cache e non ce ne possiamo accorgere, dato che il codice è
lecito. Meltdown inserisce del codice che cerca di accedere ad aree del sistema operativo e ci
riuscirà perché in quel momento non viene controllato se è permesso o no (se devo generare un
eccezione o no) non dovendo essere eseguito; poi cercherà di capire in quale blocco della cache si
trovano i dati utili, in modo da trovare un hit per ogni byte che vogliamo rubare. Mentre per
meltdown è stata rilasciata una patch correttiva denominata KAISER (permette di mappare in
maniera random il sistema operativo all’avvio, così da non sapere a priori dove si trova), spectre
non può essere patchato. Ricorda che in windows solo una parte del kernel è mappata nello spazio
virtuale; in linux la sua interezza. Come si potrebbe effettuare un attacco? Eseguendo del codice
che fa proprio questo, innestato in altro codice che stiamo usando appartenente ad un
programma che potremmo aver scaricato dalla rete.

Attacco Buffer Overflow


Mentre Meltdown e Spectre sono delle vulnerabilità che fondamentalmente vengono fuori
dall’hardware, l’attacco a buffer overflow sfrutta una vulnerabilità software. Prima di andare oltre
bisogna ben capire sia di cosa stiamo parlando, sia qualche concetto base. Innanzitutto, cos’è uno
shellcode? E’ un programma assembly il cui fine ultimo è generalmente eseguire una shell sul
calcolatore della vittima. Sappiamo che quando viene eseguito un programma C, in particolare uno
che utilizzi delle chiamate a funzione, alcune informazioni utili come indirizzo di ritorno e variabili
locali, oltre che base pointer, sono poste sullo stack. In cosa consiste il buffer overflow? Sfondare
51
un buffer di una certa capienza e riversare i dati in aree di memoria adiacenti a quella giusta. Se il
buffer si trovasse sullo stack, allora uno sfondamento potrebbe compromettere l’indirizzo di
ritorno (e si manda anche lo shellcode stesso, però essendo che metto il codice nello stack, lo
stack deve essere eseguibile e c’è un modo per farlo), generando due effetti: segmentation fault,
provo a ritornare ad un indirizzo non consentito; ritorno ad un indirizzo di memoria ben preciso,
dove si trova del codice che l’attaccante vorrebbe fosse eseguito. In questo secondo caso
rientrano gli attacchi basati su shellcode, in cui viene eseguita una shell (e qui l’attaccante può far
quello che vuole). Alcuni attacchi non si basano sulla modifica dell’indirizzo di ritorno, ma del base
pointer, così da puntare a stack fasulli con l’ indirizzo di ritorno allo shellcode. Come ci si difende?
Beh, la miglior difesa sta nella consapevolezza: bisogna conoscere bene quali sono le funzioni del
linguaggio (ad esempio C) che permettono questo tipo di operazioni in modo non controllato
(esempio banale è la funzione gets). Esistono infatti librerie sicure con funzioni che in qualche
modo cercano di imitare gli stessi comportamenti di quelle più semplici, ma in modo protetto.

SSD
Durante il corso abbiamo parlato di memoria in molte occasioni: cominciammo con la memoria
centrale, poi con le cache e le diverse politiche di gestione, ma non ci siamo mai soffermati sui
dispositivi di memorizzazione di massa, quali HDD e SSD. Noi parleremo degli SSD, poiché
all’unisono reputati come il futuro delle memorie secondarie, ma perché sono così acclamati?
Sebbene gli HDD siano ancora quelli più usati, essi sono soggetti ad usura, sono meccanici,
richiedono potenza e sono generalmente meno costosi; gli SSD viceversa sono robusti, più veloci in
genere, compatti. Vediamo come sono fatti gli SSD.
La cella elementare di un SSD è un transistor particolare, poiché possiede due gate: uno di
controllo (classico) e un altro che invece è detto fluttuante. Sappiamo che lo stato del transistor è
dettato dal fatto se abbiamo superato o meno la cosiddetta tensione di soglia: per gli FGMOS essa
è variabile, poiché lo stato logico di quest’ ultimo è dettato da quanta carica è depositata
all’interno della floating gate e questo fa variare la soglia. Una cella dunque può essere erased
(vuoto) oppure programmed (pieno) a seconda di cosa c’è nella FG. Applicando una tensione al
gate di 0.8V posso capire se la cella è programmata o no: se il circuito si chiude non ci sono cariche
nella FG, questo stato è l’uno; se invece ciò non avviene, significa che la soglia è molto più alta,
ovvero ci sono cariche nella FG! In questo caso diremo che c’è uno zero. Solitamente le celle si
programmano attraverso il tunneling e si svuotano attraverso diverse tecniche (se è possibile
svuotare elettricamente parleremo di EEPROM).
Per le memorie FLASH abbiamo due tecnologie possibili:
 Nor Flash: Usata maggiormente per delle piccole ROM o all’interno degli smartphone. Sono
più costose. Ogni cella ha un terminale connesso alla massa e uno alla bitline. Nel
momento in cui si alza la wordline (gate), lo stato logico del transistor si rifletterà sulla
bitline, alzando o abbassando la linea. Per memorie che contengono codice che deve
essere eseguito, le NOR sono la soluzione migliore data la velocità di lettura superiore.
52
 Nand Flash: Più piccole e meno costose delle precedenti, solitamente le troviamo nei drive
USB. Tutte le word line staranno ad una tensione maggiore della soglia per la cella
programmata, tranne la word line che voglio leggere che è messa ad una tensione
maggiore della soglia in stato erased. Se si switchano tutti, la cella scelta ha un uno,
altrimenti uno zero. Consumano meno se l’applicazione richiede molte scritture, che sono
molto veloci.

E le prestazioni in termini di throughput? Se le letture o scritture sono semplici, gli SSD di qualsiasi
tipo battono gli HDD e non di poco, ma se si parla di dati sequenziali in spazio gli HDD sono ancora
più veloci (a meno che gli SSD non adottino la soluzione interleaving: fare in modo da sfruttare
diversi dispositivi di memorizzazione sul quale distribuisco dati in maniera ordinata per accedere
velocemente in parallelo, come facevamo con i diversi banchi di RAM) : è anche per questo che i
film o i videogame sono venduti in CD-ROM. Ad una memoria flash ci si può riferire con il termine
package (ad esempio da 4 GB). Ogni package può essere costituito da uno o più dies, che
condividono tra loro diverse linee (non tutte) e che possono operare in parallelo.

53
A loro volta i dies sono divisi in plane, ognuno dei quali ha un certo numero di blocchi con un certo
numero di pagine. La cancellazione avviene nell’ordine dei blocchi! Se devo scrivere una singola
cella del blocco (metto uno zero) allora posso farlo molto velocemente, ma se dovessi inserire un
uno allora dovrei fare l’erase di tutto e riprogrammare ogni cella di quel blocco; questo è un
grosso difetto delle SSD che li rende forse peggiore degli HDD in certi casi per quanto riguarda le
scritture. Ogni blocco può trovarsi in tre stati possibili: clean (liberi), data(appena scritto), dirty
(dati non validi). Questi ultimi sono ripuliti da una sorta di garbage collector in un secondo
momento. C’è da tener conto di una cosa importante, ovvero del fatto che i cicli di erase/program
accorciano la vita delle celle di memoria: l’idea è quella quindi di non sfruttare sempre le stesse
pagine, sempre gli stessi blocchi, ma di distribuire il “carico”, attuando una strategia denominata
wear leveling. Il controller del dispositivo tiene conto della vita di ogni blocco con un contatore: se
si attua la strategia del dynamic wear leveling i blocchi che contengono codice statico
(sostanzialmente SO) non vengono considerati nella distribuzione; nello static wear leveling tutti i
blocchi sono considerati e i count di tutti i blocchi sono resi i più simili possibile.
Ad oggi più che alle performance si pensa ai consumi e gli SSD hanno un punto a proprio favore,
ma sono ancora meno capienti, costosi, e hanno una vita breve. L’unica cosa che ci resta da capire
è come fa un SSD a parlare con il resto del nostro calcolatore, che interfaccia espone? Ricorda che
un dispositivo di massa è pur sempre una periferica di I/O! Abbiamo visto due interfacce: l’ eMMC
che consiste in alcune linee sulla quale si va in half duplex; UFS, diverse coppie di interfacce seriali
(tx-rx) sulla quale comunicare.

54
ARM
E’ da un po’ di anni che ARM è il riferimento architetturale per tutti i processori che troviamo
all’interno dei dispositivi mobili. ARM è una architettura, l’azienda che l’ha sviluppata non produce
davvero prodotti tipici; i veri produttori sono ad esempio SnapDragon e altri che sfruttano tale
core per sviluppare dispositivi, un po’ come fa AMD con l’architettura Intel.
L’ARM è a metà tra un’ architettura per un processore embedded e un general purpose:
embedded perché deve consumare poco e deve essere piccolo, general purpose poiché è sempre
più potente (ultimamente si trovano chip di questo tipo anche a bordo dei calcolatori). I processori
ARM sono processori RISC, poche istruzioni tutte della stessa lunghezza, delle quali solo due per
accedere in memoria (load e store). I grandi produttori possono sviluppare sotto ARM acquistando
le cosiddette licenze. ARM, oltre che a bordo degi smartphone, viene usato anche a bordo di
schede per elettrodomestici e altre applicazioni.
L’ARM nasce come un’architettura a 32 bit, ma molte versioni possono funzionare sia con set di
istruzioni a 32 bit sia con istruzioni a 16 bit (thumb) passando da un set all’altro; dalla versione 5 in
poi introduce un terzo set di istruzioni a 8 bit che si chiama jazelle, che sono utili per eseguire
istruzioni bytecode provenienti dalle applicazioni. Questa è stata la chiave del successo, che
permette di avere una java virtual machine realizzata in hardware, detta dalvik: Il bytecode java,
il .class, è formato da molte istruzioni a 8 bit e molte di queste vengono eseguite direttamente in
hw!
È un processore a risparmio energetico, che spegne dei componenti sotto una certa soglia di idle.
Cosa importante da sottolineare è che se avessimo un programma a 16 bit, con un bus a 32 bit
potrei comunque prelevare 2 istruzioni alla volta, ma questo non è completamente un vantaggio
poiché tali istruzioni saranno sicuramente più semplici.
Off topic: metodi nativi, quei metodi java che nascono per generare codice non portabile (in
qualche modo legato al linguaggio, sistema operativo, architettura) che rappresenterà una libreria
dinamica verso un certo dispositivo.
Quando il processore parte questo può funzionare in 7 modalità:

55
 Supervisore: lo stato nell quale viene eseguito il sistema operativo, accesso completo
all’hardware.
 User: accesso limitato all’hardware.
 FIQ,IRQ: metodi diversi in cui passa il processore se arriva un interruzione, se questa è ad
alta priorità allora va in fast interrupt, bassa priorità IRQ.
 Abort: quando c’è una violazione di un indirizzo di memoria.
 Undef: quando non si riconosce l’ istruzione.
 System: è uno stato privilegiato che usa gli stessi registri dello stato utente.
A seconda di quale modalità si adoperà, il processore riutilizzerà certi registri oppure ne avrà altri a
disposizione.

Partiamo dalla modalità utente: abbiamo 13 registri generali più altri registri dedicati; si può
notare come tutte le altri modalità riutilizzino gran parte dei registri che si usano di solito nella
modalità utente, tranne lo stack perché ce ne è uno per ogni modalità. Però si nota come in
modalità FIQ si hanno a disposizione dei registri (dall’8 al 12) completamente dedicati; perché? Noi
sappiamo che ogni qual volta viene richiamata una funzione, su uno stack dedicato vengono
salvati alcuni registri come quelli che si andranno a sporcare, l’indirizzo di ritorno, più alcune
variabili locali: stiamo però parlando di funzioni che vengono eseguite in modalità utente, se c’è
bisogno di gestire un interruzione con una certa velocità ci rendiamo conto che questo passaggio
di preparazione è piuttosto tedioso. Ecco perché in modalità FIQ si evita di utilizzare i registri
utente per fare operazioni, ma si usano registri dedicati. N.b. spsr sta per saved previous status
register, mentre cpsr sta per current program status register.

Quando passiamo in modalità Thumb non possiamo usare tuta la grandezza dei i registri ma metà
di questa, ricordiamo che si lavora a 16 bit. Per quanto riguarda lo status register:

56
A sinistra abbiamo i flag di stato, poi il bit Q che ci dice se ci sono delle operazioni di saturazione,
ovvero un tipo di operazione aritmetica che sfonda il massimo ma non ritorna indietro, fornendo il
valore di saturazione. J e T ci dicono se sta prelevando codice java o codice thumb ( in questo
modo l’unità di controllo sa i 32 bit che preleva dalla RAM cosa sono), I ed F per abilitare e
disabilitare le interruzioni; infine c’è il mode che specifica uno dei 7 stati che il processore può
assumere.
Se il processore funziona in modalità ARM le istruzioni sono tutte a 32 bits, quindi del program
counter vanno considerati solo i primi 30 bit (vado di 32 in 32); se funziona in modalità Thumb, le
istruzioni sono tutte a 16 bit, il valore del PC si trova nei bit dal 31 all’ 1. In modalità Jazelle le
istruzioni sono di 8 bits e il PC occupa tutti i bit.
Se arriva un’interrupt lo stato viene copiato nel spsr della modalità in cui si va a finire (IRQ o FIQ);
in questo modo l’interruzione oltre ad inseguire lo stato dello stack, allo stesso modo sa lo stato
dove è stato interrotto prendendo decisioni in maniera appropriata; l’indirizzo di ritorno viene
salvato nel link register (lr), quindi non vengono salvati nello stack, sempre per migliorare la
velocità del processore. Salvati questi due elementi nei registri viene inizializzato il pc all’inizio
dell’ISR andando a prendere l’indirizzo dal vettore delle interruzioni (si utilizza un vettore delle
interruzioni). Tutta le gestione avviene in ARM state.
Esistono varie versioni di ARM, questo perchè si è andati ad aggiungere sempre più componenti
che vengono usate per effettuare operazioni aggiuntive, come il supporto alla parte multimediale
o al floating point. Spesso nel nome della versione si capiscono molte cose. ARM nelle ultime
versioni definisce tre famiglie di prodotti (cortex-A-M-R): quelli che finiscono per A sono quelli
pensati per far girare le applicazioni, quindi adatti agli smartphone: questo si comporta come un
processore per portatili avendo un TLB, unità floating point, ecc. supporta delle interruzioni
vettorizzate ma anche quelle autovettorizzate.
Il cortex M invece è un microcontrollore che usa un modello Harvard, non avrà tutto il set ARM a
32 bit ma un set Thumb a 16 bit; li troviamo spesso a bordo di schede a microcontrollore. Il cortex
R è un processore Von Neumann pernsato per applicazioni real time e ad alta affidabilità.
Rispondono velocemente agli eventi, hanno diverse cache e codici di correzione. Una particolarità
è che i core sono disposti ortogonalmente ed eseguono le stesse istruzioni; nel caso in cui uno dei
2 core subisse un’interferenza elettromagnetica e non fosse in grado di eseguire correttamete
l’istruzione, l’altro riuscirà ad eseguirla in modo correttob.
57
Un’altra differenza tra cortex A e quelli R per realtime è il supporto alle interruzioni vettorizzate e
autovettorizzate, presente nel primo.
Instruction Set
Noi non studieremo tutta l’architettura esattamente nel dettaglio, ma più che altro ci
focalizzeremo su diverse particolarità e idee che i realizzatori si sono inventati. Ad esempio
esistono istruzioni che integrano al loro interno dei salti, cosi da scrivere meno codice e cercare di
prevenire in qualche modo i control hazard (esempio ADDNE). Ancora, aggiungendo una S alla fine
del comando possiamo cambiare i flag del registro di stato (normalmente le istruzioni aritmetiche
non lo permettono): mediante queste desinenze non c’è bisogno di passaggi ulteriori utilizzando,
ad esempio, dei compare per capire se bisogna saltare o no, perché il bit nel registro di stato già l’
ho cambiato! CMP non ha la possibilità di usare S.
Nel primo esempio abbiamo la prima istruzione che
effettua il controllo sulla condizione, solo se si è
verificato, mi preparo il parametro da passare alla
funzione, siccome non c’è S alla fine non vado a cambiare
il flag di stato.
Nel secondo caso comparo a con 0 (ovvero r0 con 0), se
l’operazione è 0 allora metto 0 in r1, se a=0; se a>0 metto
1 in r1. Quindi con solo tre istruzioni faccio una sola volta
il confonto e poi uso le due istruzioni condizionate.
Nel terzo caso abbiamo una OR, ovvero comparo r0 con
4, se è diverso lo comparo con 10; se una delle due viene verificata allora metto 0 in r1.
L’istruzione di branch è fatta in questo modo: su 4 bit c’è la condizione, abbiamo un codice 101
che ci dice che è branch, e poi un bit che ci dice se saltare utilizzando il registro link o lo stack, se il
salto è innestato o meno, il resto è lo spiazzamento rispetto al program counter.

58
Abbiamo una serie di istruzioni aritmetico
logiche, tutte a tre operandi, e tutte hanno la
possibilità di essere eseguite in maniera
condizionata. Tali istruzioni lavorano su
registri, NON SULLA MEMORIA.

L’ALU è fatta in questo modo: il secondo


operando va nella ALU passando per un barrel
shifter. Noi sappiamo che gli operandi sono a 32
bit, indichiamo i registri; se utilizzassimo dati di
tipo immediato dove questo è una costante, non
avremmo abbastanza bit nell’istruzione per
indicarlo! .

L’immediato è codificato su 12 bit, che sono molti meno di quelli normali a 32 bit: 8 bit vengono
codificati normalmente, mentre gli altri 4 bit vengono usati per shiftare gli 8 bit all'interno dei 32
bit, in questo modo possiamo rappresentare tutti i numeri tra 0 a 255 se non shiftiamo niente;
spostando di due posti avrò altre rappresentazioni, e cosi via. Il problema diventa che alcune
costanti non possiamo rappresentarle, come ad esempio tutti 1 non possiamo rappresentarli. Per
poter risolvere questa situazione l’assemblatore in automatico trasformerà il codice facendo in
59
modo da prelevare quell’immediato dalla memoria e metterlo in un registro: la costante si troverà
qualche parte nel codice e la si fa a prelevare a partire dal PC e salvata in un registro.
L’indirizzamento può avvenire con offset, con registro base, ecc. MRS e MSR, ci permettono di
leggere o scrivere nei registri di stato.
Nel salto a funzione, se ho una chiamata innestata in un sottoprogramma, la prima volta devo
salvare il link register all’interno dello stack; quando salto, siccome è l’ultima e non c’è più innesto,
allora dovrò semplicemente ripristinare il link register nel PC, mentre per le non foglie il link
register (return address) dovrà necessariamente stare sullo stack.

Quando passiamo dall’istruction set ARM a quello thumb perdiamo delle caratteristiche. Perdiamo
le operazioni a 3 operandi, ne abbiamo solo a 2, perdiamo il modo per settare il flag di stato,
abbiamo istruzioni più semplici. Il set thumb è nato perchè il codice a 32 era troppo denso e non
c'era bisogno in alcuni casi di tutta quest'espressività.

System design

60
Contiene l’ARM Core, una parte per la
gestione della memoria, un bus verso le
periferiche, una ROM e una cache o
una RAM a seconda del tipo.
Solitamente il bus viene utilizzato in
due versioni, una più veloce e una più
lenta che porta verso la memoria. Se è
cortex A allora ha una memoria
esterna.

Il processore almeno in versione A ed R, nello spazio di indirizzamento mette sia istruzioni che dati,
è un modello Von Neumann. La maggior parte delle ultime versioni dell’ARM ha una cache
separata per istruzioni e dati, e un bus interfaccia verso le due cache, un po’ come un Harvard.
Le istruzioni arrivano dalla memoria, se sono istruzioni ARM a 32 bit vengono decodificate
dall’unità di controllo, la quale gestisce il datapath. Se sono Thumb vengono tradotte in istruzioni
ARM e vengono mandate all’unità di controllo, quindi alla fine cambia la codifica delle istruzioni,
ma il corpo è sempre lo stesso.
Abbiamo una pipeline a 5 stati nelle
architetture più moderne. L’esecuzione delle
istruzioni sono diventate fuori ordine quando
hanno iniziato a sviluppare la superscalarità,
così come quelle dell’intel. Se in pipe ci sono
istruzioni semplici ad ogni ciclo verrà
completata un’istruzione, se invece già ne
troviamo una un po più complessa, il throughput potrebbe non essere massimo.

61
Arm ISA approfonditimento
L’ARM è molto cambiato nel tempo, potremmo riconoscere otto versioni: la prima a sfruttare un
architettura a 32 bit fu l’ARM v3, addirittura oggi si è arrivati a 64 con l’ARM v8 (v1 e v2 furono
comunque architetture a 32 bit, ma sfruttavano solo 26 bit per l’indirizzamento). I registri sono
cambiati davvero poco. Lo sviluppo, nel tempo, si è spostato in tre direzioni: le performance,
ottimizzazione code size and execution of java bytecode, sicurezza. Muoviamoci verso le prima
direzione, prenderemo in considerazione due set di registri: i classici GPR con le estensioni e gli FP
+ SIMD che vennero aggiunti dalla versione 5 per operazioni in virgola mobile e quelle su dati
multimediali, rispettivamente. Quando si parla di estensioni nell’utilizzo dei GPR, ci basta pensare
all’ARM v6 che permetteva di contenere all’interno di registri di 32 o 2 operandi da 16 o 4 da 8.
Questo serviva a implementare il cosiddetto SIMD (single instruction multiple data). Dall’ altra
parte invece abbiamo lo sviluppo di unità apposite per il calcolo in virgola mobile e vettoriale:
parliamo del VFP o anche vector floating point. Ne esistono diverse versioni, il principio è il
medesimo, cambia soltanto il numero di registri a disposizione e la loro grandezza.

L’advanced SIMD NEON nacque in coppia con la VFP, utilizzando i medesimi registri per
applicazioni multimediali: SIMD era capace di passare da 32 registri da 64 bit ad esempio a 16
registri a 128 bit, poiché magari c’era bisogno di tale configurazione. L’ultima versione di ARM, la
v8, monta a bordo molti più GPR (prima 13, ora 31) che possono essere utilizzati sia a 32 che a 64
bit. Alla fine tutti i miglioramenti sono stati sempre indirizzati verso l’ampliamento della capienza e
del numero dei registri, più qualche unità apposita per il calcolo speciale.
62
La seconda strada riguardava i miglioramenti sulla densità del codice e sull’esecuzione del
bytecode: su thumb abbiamo già parlato abbastanza, dicendo che ho istruzioni ridotte e meno
GPR a disposizione. Abbiamo poi parlato di Jazelle, un instruction set nato per l’esecuzione del
bytecode. Ricorda: il bytecode è tradotto in codice nativo dal JIT compiler, e il codice nativo
sappiamo viene eseguito molto più velocemente. Informati su Thumb EE. Sui miglioramenti in
sicurezza diciamo ancor meno, poiché gli unici miglioramenti sono stati: l’introduzione di una
trustzone, ovvero parti hardware e software maggiormente protette, e unità di calcolo apposite
per la crittografia.
Ricorda: Android è compilato per l’architettura, non per il singolo processore!
Qualche informazione: il nominativo cortex è nato con ARMv7; le pipeline non fanno parte
dell’architettura, ma del singolo processore (ARM 4 e StrongARM1 sono entrmabi v4 ma ce
l’hanno diversa). In particolare l’ARM 11 (fa parte di v6) ha 3 pipeline, ne sarà selezionata una sola
a seconda di quale istruzione arrivi: in particolare c’è la ALU pipe, la MAC pipe per multiply, e la LS
pipe per load e store. Con ARM 11 ho anche la possibilità di multicore, con MESI snoopy protocol
per la coerenza.
Completeremo lo studio dell’architettura scendendo nel dettaglio di due processori particolari, un
minimo più recenti: parliamo del Cortex A53 (low performance, più piccolo, consuma meno) è del
Cortex A57 (praticamente l’opposto). Non è raro trovare a bordo dello stesso smartphone
soluzioni che montano entrambe le filosofie, cosi da chiedere supporto alla potenza quando ce n’è
bisogno (questo tipo di soluzione è chiamato big little). Si basano entrambi sulla versione 8, quindi
parliamo di processori a 64 bit. Cortex A in generale supporta la versione thumb 2, ha una parte di
esecuzione affidabile, di gestione della memoria, interfaccia a 64 o 128bit, una cache l1 di 16-32 kb
e una di l2 di 2 mb, può avere un’unità floating point vettoriale, ha una pipeline a molti stati con
brench prediction spesso. In particolare l’A57 può lavorare insieme ad una GPU; inoltre può
trovarsi in uno schema quad-core, ogni core avra una cache di primo livello e poi ce ne sarà
un'altra condivisa. In un confrontro tra le due pipeline, si nota come l’A57 ce l’abbia più lunga e
complessa. Non sono mostrate le pipeline di load e store, che ci sono sempre.

63
Molto di ciò che ho detto sull’ARM 57 si riflette sull’ARM53 (beh chiaro, sono la stessa
architettura) tranne la pipeline (a maggior ragione). L’unica cosa che ci manca capire è come si
capisce in uno schema Big-Little su quale cluster mandare le istruzioni. Innanzitutto, c’è da dire che
si può schedulare in tre modi diversi:

 Migrazione cluster: vedo i due cluster come due unità operative differenti, o sta acceso uno
o l’altro.
 Migrazione CPU: vengono considerati 1:1 core del cluster piccolo e del cluster grande, si
schedula per ogni coppia verso uno dei due.
 Global task scheduling: tutto è possibile.
E come si decide quando migrare dal grande al piccolo o viceversa? Esistono due possibilità:
awake migration e forced migration.

Awake: sul fronte di discesa della task line, si tiene conto di quale cluster stesse eseguendo in quel
momento; al prossimo fronte di salita del task sveglio sul processore salvato; il procedimento si
ripete;
Forced: quando supera la soglia, si passa sul big

Monitoring delle prestazioni di un calcolatore


Come si misurano le prestazioni di un calcolatore? Solitamente la tattica più utilizzata consiste
nell’utilizzo di benchmark, ovvero una sorta di programma scritto appositamente per sollecitare
alcune caratteristiche del calcolatore: esso restituirà risultati e metriche. Di benchmark ne esistono
tanti, ognuno atto a sollecitare una caratteristica. Un'altra idea sarebbe quello di andare a
monitorare come le risorse sono sfruttate durante l’esecuzione dei programmi. Ma su cosa si
basano queste tecniche a livello software? Molto spesso ci si riferisce a PAPI, alcune librerie
portabili che permettono di collezionare statistiche. Le librerie sono usati da tool, tra i più famosi
in linux ricordiamo nmon.
Un'altra strada in Linux sarebbe quella di ricorrere ad un modulo apposito del Kernel atto alla
lettura di importanti statistiche: tale modulo si chiama perf, una collezione di probe software. Ma
non tutte le caratteristiche e statistiche possono essere lette via software, come ad esempio cache
miss, pipeline stats e cosi via. Per fare ciò perf si basa su alcuni contatori hardware interni al
processore (i cosiddetti PMU nelle architetture ARM), alla quale si può accedere attraverso un
semplice assembly. In aula abbiamo cercato di riimitare ciò che fa normalmente perf, senza però
64
sfruttare l’ astrazione della libreria. Il PMU mostra due tipi di interfacce: una esterna a bus, l’altra
interna attraverso assembly; ci sono 6 contatori che vengono modificati a seconda di particolari
eventi, più un ultimo che scandisce il clock di sistema (sulla rete si trovano tutta una serie di guide
che permettono di configurare, settare, leggere i contatori a partire da processi che girano
modalità utente).
E allora, come si parte da un’ applicazione scritta in C? E’ importante sapere che il compilatore gcc
permette anche di compilare del codice assembly che si trova all’interno di un programma C. C’è
una particolare sintassi, si comincia con asm asm-qualifiers e poi tra parentesi si elencano
l’assembler template, quali sono i registri di output e quali quelli di input. Distringueremo tre
moduli del programma che devono essere in qualche modo inseriti all’interno del Kernel: un
sottoprogramma che abilità la lettura dei contatori e quali; un modulo di init; un modulo di exit.
Attraverso un secondo programma C esterno useremo questa sorta di interfaccia per fare ciò che
ci piace.
Quali sono i vantaggi di adottare una soluzione utente piuttosto che perf stesso? Sostanzialmente
in modalità utente abbiamo un piccolo overhead di poche istruzioni assembly, mentre utilizzare
perf significa fare utilizzo di system call, quindi c’è da aspettarsi un cambio di contesto e diverse
altre operazioni che possono rallentarci. E qual è invece lo svantaggio più grosso? In sostanza, se
sto in modalità utente, è facile andare in conflitto con altri procesi che utilizzano perf, cosa che
non accade se tutti utilizzassimo perf. Quindi i registri del PMU possono o essere letti via utente
utilizzando dell’apposito assembly, oppure dal sistema operativo utilizzando la collezione di
system call data da perf.

Sistemi Embedded
I microcontrollori sono dei sistemi embedded poiché non progettati per il calcolo, ma piuttosto per
lavorare insieme a qualcosa da controllare. Sono soluzioni sviluppate su singolo chip, dov’è
contenuto tutto il necessario. La caratteristica principale di un microcontrollore è che tutte le
architetture di questo tipo non hanno una sola memoria che contiene sia dati che istruzioni ma
due memorie diverse, due bus divisi cosi come gli spazi di indirizzamento, dividendo memoria dati
e memoria programmi e seguendo quindi il modello Harvard. I microcontrollori ricadono in
qualche modo nella classe dei microprocessori, ma non sono semplicemente questo, hanno anche
altro.

65
All’interno di questi microcontrollori abbiamo dei microprocessori RISC, poche istruzioni della
stessa lunghezza, con all’interno (potrebbe avere) una memoria RAM, una memoria ROM e una
EEPROM
Sul silicio tutta la parte che mi avanza la vado ad utilizzare per
realizzare le interfacce.
PIC (Peripheral interface controller) è una famiglia di
microcontrollori, molto diffusa perché economici, con un
numero di istruzioni limitato e uno stack piccolo, istruzioni
brevi e interruzioni che possono essere anche non presenti, o
se presenti solitamente vettorizzate.
Al controllore noi connetteremo: l’alimentazione, un pin a cui
dare il reset essendo una macchina asincrona, il clock (che
potrebbe però essere implementato anche internamente), e poi dei pin con cui collegare delle
periferiche. Dovremo poi avere un interfaccia per programmarlo. La lunghezza delle istruzioni
dipende dal modello adottato, ma solitamente i classici non vanno oltre i 16 bit. Vediamo
l’architettura di un PIC e capiamo cosa troviamo al suo interno.

66
La parte centrale tratteggiata in nero è il nostro processore, dove abbiamo uno status register,
un’ALU; in questo caso abbiamo una Ram contenente dati e i file register tutto in un blocco,
realizzata come una cache (una static ram). Lo stack che è dello stesso tipo di memoria della RAM,
un program counter che deve indirizzare una memoria diversa dove si trova il programma (una
flash che si va a programmare ogni qual volta si voglia riscrivere il codice che viene flashato
all’interno), che è non volatile. Nel datapath non manca l’instruction register (anche se non
prettamente parte del core), quindi prelevo l’istruzione dalla memoria programma, lo devo
decodificare e devo mandare segnali al datapath. Non ho superscalarità ne pipeline lunghe.
Rispetto ad un calcolatore manca l’HDD; la maggior parte dei microcontrollori ha una EEPROM che
però non è mostrata in figura, non volatile riscrivibile a tempo di esecuzione. In questa memoria
possiamo scrivere dei byte, non dei file perché non abbiamo un file system, non c’è un sistema
operativo. C’è poi un circuito di RC interno per avere un clock, oppure possiamo collegare un
oscillatore esterno per averne uno più preciso, ad esempio al quarzo.

AVR
Avr è la casa che produce la maggior parte dei microcontrollori che si possono trovare a bordo di
schede come Arduino, oltre che lo standard ISA per questo tipo di architetture. Essi sono dotati di
un numero variabile di pin (si può anche superare la centinaia), si adotta una single cycle execution
per tutte le istruzioni, che sono inizialmente CISC ma vengono tradotte per girare su
un’architettura RISC.

Questo microcontrollore in particolare ha una


parte di elaborazione dove troviamo l’ALU, i
registri, la SRAM, l’intruction register, PC, il
decoder delle istruzioni dell’unità di controllo e una
parte di flash dove c’è il programma. C’è un
interfaccia JTAG che è uno standard e serve per il
67
debugging della programmazione; l’EEPROM è il nostro hard disk; poi abbiamo una periferica
seriale per la programmazione o per il debbugging, più una serie di interfacce digitali che servono
a comunicare con le periferiche. Un watchdog è un timer che ci interrompe quando scatta lo zero
ma di solito viene resettato al valore iniziale prima che scatti: esso servirà a capire se ci sono ritardi
nell’esecuzione per qualche ragione, generando un Alert in tal caso. Per paragonare i
microcontrollori si fa eseguire un codice C e si confrontano i parametri in questo modo:

La memoria flash è divisa in due parti, una non viene programmata e al suo interno abbiamo il
bootloader, ovvero le istruzioni per inizializzare il dispositivo. Poi abbiamo la parte dove va il
nostro programma compilato, il bootloader caricherà la prima istruzione del programma che verrà
eseguita.
Rispetto a molte altre architetture AVR si vanta di introdurre diversi moduli innovativi, come ad
esempio il brown out detector (verifica se la tensione di alimentazione sta scendendo oltre una
certa soglia) e il pull up on demand. Pipeline a due stati, non c’è branch prediction, né MMU
(richiederebbe troppo spazio l’inserimento di questi due moduli).
Arduino
Fin’ora abbiamo visto solo il chip, per avere qualcosa di completo possiamo rivolgerci ad Arduino.
Arduino è una compagnia che rilascia degli schemi hardware per microcontrollori, usa come
microcontrollore l’AVR, ci fornisce lo schema open-source, quali componenti ha, come sono fatte
le piste, ecc.
Generalmente questo funziona a 3,3V, avendo un regolatore di tensione interno che l’abbassa da
5 a 3,3. I Pin A sono analogici , i pin D sono digitali. Dà la possibilità di utilizzare UART,SPI,I2C.
Per programmare arduino scriviamo o in assemebly o c; il compilatore è quello sviluppato da AVR,
ma arduino ci fornisce anche un elemento opensource con delle librerie e un loader che sa come
flashare il programma in base anche al modello.

68

Potrebbero piacerti anche