LOGICHE E FUNZIONAMENTI DEL MIPS
Maggiore, Minore,…: tra <= >= < >, l’unico che esiste realmente è il simbolo <. Gli
altri possono essere comunque usati ma sono in realtà tutti simulati grazie a <
(pseudo istruzioni).
Somma in virgola mobile: esempio: programma che somma 0,2 e quando arriva a 10
termina -> non arriverà mai a 10 perché 0,2 in virgola mobile non è potenza di 2,
quindi è periodico. Se sommi 0,25 allora arriverà a 10.
Gestione Overflow: Generalmente useremo il complemento a2 per le operazioni
(tranne rari casi). Normalmente si usa il flag dell’overflow per capire se l’operazione
è riuscita, ma nel nostro processore non è presente il flag dell’overflow. Quindi
useremo un’altra tecnica: procediamo senza verificare se c’è l’overflow, quindi se
non avviene siamo apposto e abbiamo anche evitato di controllare il flag, se invece
l’overflow accade, allora la alu comunicherà con il controllore il quale fermerà la
prosecuzione del programma per dare modo di gestire la situazione per poi
riprendere. E’ vantaggioso perché si evita ogni volta di controllare il flag, ma
svantaggioso perché in caso di overflow bisogna ricordarsi in che punto
dell’esecuzione eravamo rimasti (per poi poter riprendere). Quindi questo metodo è
vantaggioso solo nel caso l’overflow si verifichi raramente. Nel nostro processore c’è
un bus dei dati di 32 bit quindi è raro che i bit non bastino, allora ci va bene. Inoltre,
quando il PC serve per andare alle routine che gestiscono l’overflow, esso viene
salvato nel EPC (Exception Program Counter) in modo da non andare perso, verrà
poi recuperato quando saranno terminate le routine per la gestione dell’overflow (per
copiare l’EPC in un registro k si usa mfc0 [move from core processor 0]). Sub
$8,$zero,$9 può dare overflow se $9 contiene il minimo rappresentabile in a2.
Gestione sottoprogrammi Program Counter e Stack: Visto che raramente un
sottoprogramma viene chiamato in un altro sottoprogramma (può succedere ma non
spesso), non viene utilizzato lo stack pointer e lo stack inizialmente. L’indirizzo da
ricordare per riprendere da dove eravamo rimasti non va nello stack ma in un
program counter di riserva (Return Address)(jump and link è saltare e salvare nel RA
lo stato). In questo modo, se c’è un'unica nidificazione (come spesso accade) è tutto
più veloce perché viene utilizzato un registro interno alla cpu e non lo stack.
Se dovesse avvenire un’altra nidificazione, prima di farla, verrà salvato nello stack il
valore del return address. Quando la seconda nidificazione è terminata, l’indirizzo
che era stato salvato nello stack, viene rimesso nel return address, per permettere,
al termine della prima nidificazione, di tornare nel main.
L’utilizzo del return address per tornare al punto in cui ero rimasto è automatico,
mentre il salvataggio del return address nello stack (e viceversa il riportare il valore
dello stack nel return address) deve essere fatto da noi scrivendo le istruzioni.
“PUSH”= addi $sp,$sp,-4 Sw $ra,0($sp) [-4 perché lo stack è da alto a basso]
“POP”= lw $ra,($sp) Addi $sp,$sp,4
Dimensione celle di memoria: Nella memoria (RAM) ogni cella è grande 4 byte,
quindi gli indirizzi vanno di 4 in 4 (per le word)
Formato delle istruzioni: Ogni istruzione è formata da 32 bit (4 byte). Ma come
vengono utilizzati e suddivisi questi 32 bit? Esistono 3 tipi di formato:
● Formato R: usato da add,sub,shift,… 6 bit Codice Operativo, 5 bit per ciascun
“operando” per identificare uno dei 32 registri (sono 3 operandi quindi 15 bit),
5 bit di shift amount
(usato in caso di
istruzione shift), 6 bit di
func (specifica meglio
l’istruzione, ad esempio
ci fa distinguere add e
addi…)
● Formato I: usato da lw,sw,beq,bne,… 6 bit Codice Operativo, 5 bit per ciascun
“operando” per identificare uno dei 32 registri (sono 2 operandi quindi 10 bit),
16 bit per il numero da aggiungere al PC per il salto di beq/bne (si utilizza un
indirizzamento relativo al PC, il numero sarà in complemento A2 in modo da
poter fare anche spostamenti negativi. Questo numero verrà moltiplicato per 4
in automatico prima di
essere sommato al PC) o
per il numero per lo
spostamento dell’indirizzo di
lw/sw (es 32($s2)).
[(pc+4)+100] pc +4 in fetch
PS: Visto che lo spostamento del bne/beq è limitato, si può anche sostituire
un beq con un bne e un jump, e il jump farà il grosso salto che bne non
poteva fare. ES: beq $t0,$t1,Poi Poi: → bne $t0,$t1,L1 j Poi L1:... Poi:
● Formato J: usato da j (jump incondizionato). 6 bit Codice Operativo, 26 bit per
l’indirizzo. 26 bit non basterebbero per un indirizzo a cui saltare
(mancherebbero 4 bit per avere un indirizzo completo [non 6 perché gli ultimi
due bit sono 0 visto che è multiplo di 4]), allora si fa un indirizzamento semi
diretto (non sicuro che si chiami così) cioè vengono tenuti i primi 4 bit del PC
poi vanno i 26 bit presi dall’istruzione e infine due zeri. Usando questa tecnica
però, non si può andare da
qualunque luogo a qualunque
altro, ma posso muovermi
ovunque entro 256mb (2^28 [26
bit + due zeri]) che è comunque
tanto. [Se proprio servisse andare oltre questo range, si usa un salto ad un
indirizzo che è contenuto in un registro {per mettere l’indirizzo in un registro si
usa la}].
Registro d’appoggio per pseudo istruzioni: per realizzare pseudo istruzioni,
l’assemblatore sfrutta istruzioni realmente esistenti: per esempio usa slt per
realizzare i salti con < >, ma necessita di un registro d’appoggio dove mettere il
registro settato di slt. Quindi esiste uno specifico registro che l’assemblatore utilizza
per questi scopi, in modo da non utilizzare altri registri che servono al
programmatore, non possono essere modificati dal programmatore. Il registro in
questione è il numero 1 ed è chiamato AT.
Gestione dello switch: Si pone un esempio di istruzione switch in C (4 casi). Come
gestirlo in modo semplice, senza verificare ogni condizione?
Es.
slt $t3, $s5, $zero # k <0?
bne $t3, $zero, Exit
slt $t3, $s5, $t2 # k >3?
beq $t3, $zero, Exit
add $t1, $s5, $s5
add $t1, $t1, $t1 # $t1=4*k
add $t1, $t1, $t4
lw $t0, 0($t1)
jr $t0 #vai a indir. letto →
L0: add $s0, $s3, $s4 #k=0, f=i+j
j Exit
L1: add $s0, $s1, $s2 #k=1, f=g+h
j Exit
L2: sub $s0, $s1, $s2 #k=2, f=g-h
j Exit
L3: sub $s0, $s3, $s4 #k=3, f=i-j
Exit:
Prima parte: controllo se k rientra nel range dei casi (tra 0 e 3), altrimenti salto tutto e
vado direttamente a Exit.
Seconda parte: questa è la parte che mi evita di controllare ogni condizione dello
switch: in precedenza è stata preparata una “tabella” in memoria nella quale, in ogni
cella, c’è l’indirizzo a cui saltare per ogni caso dello switch. Quindi, k viene
moltiplicato per 4 grazie alle due add e poi lo sommo a $t4 che è l’indirizzo base
della tabella. Ora carico il contenuto della cella della mia tabella in un registro (che è
l’indirizzo a cui devo saltare) e ci salto con jr.
Terza parte: grazie alla seconda parte, sarò saltato ad uno di questi “casi”.
Con poche righe evito di verificare tanti casi, la velocità è efficiente
indipendentemente dal numero di casi dello switch. L’unico “costo” è quello di
costruire la tabella. Il programma completo di creazione della tabella è sul sito del
prof denominato Case Switch (mi sembra).
I registri: per le operazioni vengono generalmente utilizzati i registri. I registri che
abbiamo a disposizione sono 32.
Quasi tutti sono modificabili, eccetto:
● Il return address menzionato prima ($ra)
● Il registro zero che contiene il valore zero, utile per le somme ($zero)
● Il registro numero 1 che è utilizzato dall’assemblatore come registro
d’appoggio per le pseudo-istruzioni
Gli altri registri possono essere richiamato con un nome secondo questa tabella (di
convenzioni)
I registri non preservati sono quelli utilizzabili liberamente dai sottoprogrammi. I
registri v sono usati per esempio per salvare un risultato di un sottoprogramma che
andrebbe ritornato al main.
Se un sottoprogramma sta usando i registri t ma gliene servono degli altri, può
utilizzare i registri s (che sarebbero modificabili solo dal main) ma prima di modificarli
deve salvare i valori che ci sono dentro agli s nello stack (con push) e prima di
tornare al main deve ripristinarli in modo che siano rimasti invariati per il main (con
pop).
Se il main sta usando i registri s ma gliene servono degli altri, può utilizzare i registri
t MA prima di passare ad un sottoprogramma dovrà LUI salvare il contenuto dei
registri t che ha usato nello stack perché il sottoprogramma potrà liberamente
utilizzarli e potrebbe sovrascrivere ciò che il main ha fatto. Il main li ripristinerà una
volta terminato il sottoprogramma.
Se da un sottoprogramma che usa solo registri t si passa ad un altro
sottoprogramma che usa solo registri t, il primo sottoprogramma dovrà salvare nello
stack i registri t che ha utilizzato prima di passare al secondo sottoprogramma
perché il secondo sottoprogramma può utilizzare i registri t liberamente e potrebbe
modificare ciò che ha fatto il primo sottoprogramma.
Problema Load/Store Byte: Pare esserci un problema quando si effettua la store
byte: si salva un solo byte (8 bit), quindi gli altri 26 bit rimangono aleatori. In realtà
questo problema non ha nessun effetto, perché quando si utilizzerà la load byte per
prelevare gli 8 bit dalla memoria, verrano appunto presi solo gli 8 bit che ci
interessano, ignorando i restanti 24 bit “sbagliati”. (Se usi lb nei 24 bit restanti si fa
l’estensione del segno, cioè si ripete il bit più significativo degli 8 validi, con lbu
vengono riempiti di 0)
Passare da lettera maiuscola a minuscola in ASCII: Generalmente si può sommare
20 visto che lettere maiuscole e minuscole sono a 20 di distanza nel codice ASCII (in
decimale o esa), ma questo richiede un po’ di istruzioni quindi si può utilizzare
un'altra tecnica: in binario, la lettera maiuscola e minuscola differiscono di solo un bit
(che nella maiuscola è 0 e nella minuscola è 1) [nella stessa posizione] allora si può
fare una or di quello specifico bit con 1 in modo da cambiarlo (oppure and per fare il
contrario).
Caricare una costante più grande di 16 bit: Con addi si potrebbe caricare massimo
un numero di 16 bit in un registro (perché è lo spazio che c’è nell’istruzione per quel
numero), e se si vuole caricare un numero di 32 bit? Si usa la lui per caricare la
parte alta del numero nel registro (e il resto diventa 0). Successivamente, con
l’istruzione di ori tra il contenuto attuale del registro (cioè solo la parte alta riempita e
il resto a zero) e la parte restante del numero, si vanno a riempire i 16 bit meno
significativi.
Es.
Parte 1) Riempio i bit più
significativi del registro con la
parte alta del numero, con lui, e il
resto viene settato a 0
Parte 2) Facendo ori tra il contenuto del registro e la parte restante del numero,
mettendo il risultato sempre nel registro,
riempio la parte restante
Nel codice basta anche fare li
$t0,numero anche se il numero è
maggiore di 16 bit, poi ci penserà l’assemblatore a trasformare il li in lui e ori.
Estensione del segno: Se si vuole “estendere” un numero in complemento a2, cioè
allungarlo, si estende il suo bit più significativo. Es: 0010 -> 0000 0010 sono
entrambi -2 oppure 1010 -> 1111 1010 è sempre -6. LO FANNO TUTTI QUELLI CON
FORMATO I TRANNE ORI ANDI XORI ECC… (OPERAZIONI SINGOLO BIT). E
ANCHE LBU CHE LO FA PER IL NUMERINO, E LW ECC….
Capire se c’è overflow con codice:
Per gestire l’overflow in autonomia, si usa la addu in modo tale che venga inibito il
segnale di overflow. Poi si applica un certo codice per capire se c’è stato overflow:
Se i segni dei due operandi sono diversi, sicuramente non c’è stato overflow, quindi
controllo se i segni dei due operandi sono uguali e se lo sono allora se il segno del
risultato è diverso da quello dei due operandi, c’è stato overflow. Per controllare se i
segni dei due operandi sono uguali, posso fare un'operazione di xor tra i due e poi
traslo di 31 il risultato in modo che mi rimanga solo il risultato della xor tra i due bit
più significativi (il segno) [lo xor da 1 se i due bit sono uguali mi sembra]. Un altro
modo per farlo è usare slt per controllare il segno degli operandi, di questo si può
vedere il codice:
li $t1, 0xf0001000
li $t2, 0xf0001000
#Rivelazione Overflow
addu $t0, $t1, $t2 #t0 = somma senza segnalare ow
xor $t3, $t1, $t2 #check dei segni di t1 e t2
slt $t3, $t3, $zero #segno < 0? t3 = 1 se segni diversi
bne $t3, $zero, no_ow #se t3 = 1 no overflow
xor $t3, $t0, $t1 #check dei segni di t1 e t0
slt $t3, $t3, $zero #segno < 0?
bne $t3, $zero, ow #t3 = 1 se segno somma diverso
no_ow: .....
ow: .....
Come rilevare il Carry con codice:
A+B si può fare? (Se non si può fare ci sarà riporto [valore assoluto (?)]). Posso
sommare ad A massimo (2^(32))-1-A <— ottengo quello scambiando 0 con 1.
li $t1, 0xf0001000
li $t2, 0xf0001000
#Rivelazione riporto
nor $t3, $t1, $zero #t3 = /(t1) = 2^32-1-t1
sltu $t3, $t3, $t2 #2^32-1-t1 < t2 ?
#->2^32-1 < t2 + t1?
bne $t3, $zero, carry
addu $t0, $t1, $t2 #no Carry: si può fare +
j poi
carry: .....
poi: ....
Problema memoria RAM continua a ROM:
Nel nostro simulatore la “””ROM”””, cioè lo spazio dove stanno le costanti è attaccata
alla RAM dove stanno i risultati, ma nel caso reale ci sarà la ROM poi dello spazio
vuoto non usato della ROM e poi la RAM. Non saranno attaccate.
Emulazione in tempo reale:
Per testare un processore, sarebbe
opportuno poter vedere come
cambia il contenuto dei suoi registri
interni, normalmente non si può fare.
Quindi si estrae il processore dalla
sua circuiteria e lo si inserisce in una
ICE (rimanendo collegato alla
circuiteria sua), il tutto è collegato al
sistema di sviluppo con periferiche
I/O.
Noi in realtà utilizzeremo un simulatore.
Interfaccia PC Spim:
In alto ci sono i registri numerati con il loro contenuto. Poi ci sono le istruzioni divise
in: indirizzo in cui sta l’istruzione, istruzione in esadecimale/binario, istruzione vera,
pseudo istruzione. Poi ci sono i dati della memoria centrale RAM. Poi c’è l’interfaccia
utente che segnala eventuali errori.
Simulator->settings->disabilita load exception file
File->open file .asm Simulator->set value e setti il PC con l’indirizz
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
-
Appunti Architettura dei calcolatori
-
Appunti
-
Appunti Architettura dei Calcolatori Elettronici
-
Appunti completi corso Architettura dei Calcolatori