Architettura dei Calcolatori 07 Architettura Di Esempio 16
1. Architettura di esempio
Ing. Costantino Grana
Università degli Studi di Modena e Reggio Emilia
(Stampato il 07/04/2008 - architettura di esempio_16.doc)
Introduzione ......................................................................................................................................... 1
Struttura................................................................................................................................................ 2
Formato delle istruzioni ....................................................................................................................... 3
Microistruzioni ..................................................................................................................................... 4
Ciclo delle istruzioni ............................................................................................................................ 5
Fetch ................................................................................................................................................. 5
Execute ............................................................................................................................................. 5
Set (SET) ...................................................................................................................................... 5
Load (LD) .................................................................................................................................... 5
Store (ST) ..................................................................................................................................... 5
Add (ADD) .................................................................................................................................. 6
Subtract (SUB) ............................................................................................................................. 6
Increment (INC) e Decrement (DEC) .......................................................................................... 6
Unità di controllo ................................................................................................................................. 6
Implementazione cablata.................................................................................................................. 7
Versione semplificata (solo fetch) ............................................................................................... 7
Versione completa (fetch e execute) ............................................................................................ 7
Parallelizzazione del caricamento del MAR ........................................................................................ 9
SET (000) ................................................................................................................................... 10
LD (001)..................................................................................................................................... 10
ST (002) ..................................................................................................................................... 10
ADD (003) ................................................................................................................................. 10
SUB (004) .................................................................................................................................. 10
INC (005) ................................................................................................................................... 10
DEC (006) .................................................................................................................................. 10
Le istruzioni di salto........................................................................................................................... 11
Codice operativo e tempi di clock...................................................................................................... 12
Esempi di programmi per l’architettura ............................................................................................. 13
Somma di due numeri .................................................................................................................... 13
Prodotto di due numeri................................................................................................................... 14
Implementazione micro programmata dell’unità di controllo ........................................................... 15
Versione senza salti........................................................................................................................ 15
Codifica delle istruzioni ................................................................................................................. 17
Versione completa con i salti ......................................................................................................... 17
Introduzione
Il processore di esempio che considereremo ha una struttura interna a singolo bus a 32 bit per lo
scambio di informazioni tra i registri. Le connessioni esterne avvengono tramite un bus dati a 32 bit
e un bus indirizzi a 20 bit. La scelta di avere un bus indirizzi così piccolo (consente di indirizzare
220 (=1 Mega) parole di memoria) è dovuta alla volontà di evitare istruzioni con lunghezza
variabile. Inizialmente considereremo che la memoria abbia un parallelismo di 32 bit, cioè ad ogni
indirizzo corrisponde una diversa parola di 4 Byte, mentre solo successivamente vedremo come
modificare questa architettura per indirizzare i singoli byte.
1
2. Struttura
Internamente il processore è dotato di un registro accumulatore da 32 bit (ACC), di un Memory
Data Register da 32 bit (MDR), di un Memory Address Register da 20 bit (MAR), di un Program
Counter da 20 bit (PC), di un Instruction Register da 32 bit (IR), di due registri relativi alla ALU da
32 bit (ALUA e ALUOUT) e da un registro di FLAG che contiene 4 bit di condizione (Zero, Carry,
Negativo e Overflow). Le connessioni sono illustrate in figura:
ZA
0
0
Cin
ACC
1
IR ALUA A
OUT
MDR B
D0-31
MAR ALUOUT
A0-19
PC FLAG
I registri connessi al bus hanno tutti un segnale di abilitazione in lettura (OE), cioè abilitano i buffer
tri-state che li connettono al bus, e uno in scrittura (IE), tranne ALUA che è abilitato sempre in
lettura verso la ALU (l’uscita dei flip-flop non passa per un buffer). Inoltre il MDR dispone di due
segnali MDRBUSOE e MDRBUSIE che consentono di attivare l’output e l’input dal bus dati. MDRIE e
MDRBUSIE non possono essere attivi contemporaneamente. MDRBUSOE viene portato all’esterno
come segnale di Write (WR) per la memoria e MDRBUSIE è connesso al Read (RD) della memoria
(la memoria è in lettura e pone i dati sul data bus ed MDR è in scrittura e memorizza tali dati).
Il segnale ZA comanda il multiplexer posto all’ingresso A della ALU e consente di inviare uno zero
alla porta A oppure l’output di ALUA. La porta B è sempre connessa al bus. Il registro FLAG
dispone di un segnale di abilitazione FLAGIE per campionare lo stato della ALU. La ALU è molto
semplice e consente di eseguire le seguenti operazioni:
S1 S0 Operazione
0 0 A and B
0 1 A or B
1 0 A + B + Cin
1 1 A + B’ + Cin
La ALU è costituita da 32 blocchi come il seguente:
2
3. I segnali di Carry sono connessi in cascata tra di loro, i segnali di zero (Z) e negato (N) vengono
generati rispettivamente dal negato della somma logica di tutti i bit e dal bit più significativo di
ALUOUT. Il Carry della ALU (C) viene generato dal carry out del blocco corrispondente ai bit più
significativi (il bit 31), mentre l’overflow è dato dall XOR dei carry out 30 e 31.
Concludendo sono necessari i seguenti segnali di controllo:
0 ACCOE
1 ACCIE
2 MDROE
3 MDRIE
4 MDRBUSOE
5 MDRBUSIE
6 MAROE
7 MARIE
8 IROE
9 IRIE
10 PCOE
11 PCIE
12 ALUAIE
13 ALUOUTOE
14 ALUOUTIE
15 FLAGIE
16 ZA
17 Cin
18 S0
19 S1
Formato delle istruzioni
Per avere una architettura semplice, vale la pena sacrificare un po’ di praticità dal set di istruzioni,
quindi avere meno possibilità, ma ridurre lo sforzo nella realizzazione dell’unità di controllo.
Le istruzioni sono tutte composte da un primo campo a 12 bit che identifica il codice operativo e un
secondo campo a 20 bit che contiene un indirizzo. La funzione dell’indirizzo dipende dall’istruzione
implementata. Questa architettura semplice consente di realizzare numerose funzioni, ma molto
difficilmente si potrà lavorare con dati di dimensione inferiore a 32 bit (come dei caratteri per fare
3
4. un esempio) o utilizzare indirizzi negativi (come spostamento per indirizzamenti indiretti). Quindi
la semplicità viene poi pagata in termini di realismo del processore.
31 20 19 0
Codice operativo (opcode) Indirizzo (addr)
Microistruzioni
Tre sono le possibili tipologie di microistruzioni che si possono realizzare: i trasferimenti tra
registri, i trasferimenti con la memoria e le operazioni di ALU. Per i trasferimenti tra registri è
necessario abilitare in scrittura il registro destinazione e in lettura il registro sorgente. È necessario
verificare che con questo schema il campionamento avvenga al momento giusto, per esempio sul
fronte di salita del clock, in modo che il dato venga campionato alla fine del periodo di clock. Ad
esempio la microistruzione
MAR PC
viene realizzata con MARIE=1 e PCOE=1. I dati scambiati con la memoria richiedono un proprio
ciclo di clock per il campionamento dei dati da parte della memoria (in scrittura) o del MDR (in
lettura). Durante questi trasferimenti il bus locale (interno al processore) non è utilizzato ed è quindi
possibile sfruttarlo per altre operazioni. Siccome il MAR è sempre attivo sul bus indirizzi, è
sufficiente utilizzare i due segnali di controllo del MDR per eseguire le microoperazioni relative
alla memoria:
Read MDR M[MAR] MDRBUSIE=1
Write M[MAR] MDR MDRBUSOE=1
Le operazioni di ALU hanno bisogno di tutti e quattro i segnali ZA, Cin, S1 e S0 e quindi la tabella
di verità per le operazioni di ALU diventa:
S1 S0 ZA Cin Operazione Risultato
0 00 - 0 and B Sempre 0 (può servire per avere uno zero da
usare con il decremento)
0 0 1 - A and B AND
0 1 0 - 0 or B B (inutile)
0 1 1 - A or B OR
1 0 0 0 0+B B (inutile)
1 0 0 1 0+B+1 Incremento
1 0 1 0 A+B Somma
1 0 1 1 A+B+1 Somma e incrementa (inutile)
1 1 0 0 0 + B’ NOT
1 1 0 1 0 + B’ + 1 Negativo, cioè –B
1 1 1 0 A + B’ Sottrae e decrementa (può servire per fare un
decremento)
1 1 1 1 A + B’ + 1 Sottrazione
Ogni operazione di ALU necessita inoltre di un registro abilitato in lettura per avere un dato nel
canale B.
Le microistruzioni della ALU vengono sempre scritte specificando prima il canale A, cioè ALUA o
0, poi indicando l’operazione (and, or, +), poi il canale B, cioè il registro abilitato in lettura in forma
vera o negata e infine, se necessario, il valore del Cin. Il risultato viene sempre portato in
ALUOUT. Ad esempio volendo sottrarre il MDR all’accumulatore si indica:
ALUA ACC
ALUOUT ALUA + MDR’ + 1
4
5. Ciclo delle istruzioni
Ogni operazione, come avviene tradizionalmente, è composta da due fasi: Fetch e Execute. La fase
di fetch è seguita sempre da una sottofase che a volte viene indicata come Decode, ma che in questo
caso non è altro che l’incremento del program counter, dato che la decodifica non coinvolge alcun
registro di stato.
Fetch
Le microistruzioni di questa fase sono comuni a tutte le istruzioni:
MAR PC
MDR M[MAR]
IR MDR
È inoltre necessario, come detto, incrementare il PC:
ALUOUT 0 + PC + 1
PC ALUOUT
Dal momento che non vi è necessità di tenere separate le due operazioni, possiamo sfruttare il fatto
che durante la lettura il bus locale non viene utilizzato e che durante la prima microistruzione PC sta
già pilotando il bus e possiamo sfruttarlo per effettuare la somma.
Quindi la fase di fetch definitiva diventa:
MAR PC, ALUOUT 0 + PC + 1
MDR M[MAR], PC ALUOUT
IR MDR
Execute
Per delineare le possibili tipologie di istruzioni bisogna considerare che l’architettura ad
accumulatore lavora sempre con la memoria, quindi saranno certo necessarie due operazioni di
lettura e scrittura dell’accumulatore, poi ci saranno alcune operazioni aritmetiche che riguardano il
solo accumulatore. Prenderemo in considerazione in seguito le istruzioni di controllo.
Set (SET)
Volendo impostare in modo immediato il contenuto dell’accumulatore, è possibile realizzare una
istruzione molto semplice che prende il campo ADDR dell’istruzione e lo copia in ACC. Ad
esempio si potrebbe fare:
MAR IR(addr)
ACC MAR
Il passaggio per il MAR ci permette di “eliminare” l’opcode, e di trasferire in ACC solo i 20 bit
meno significativi. Una soluzione più brutale potrebbe essere questa:
ACC IR(addr)
Purtroppo sappiamo che in questo modo in ACC andrebbe a finire anche l’opcode dell’istruzione
SET. La soluzione semplice per questo problema è quindi far sì che questo opcode sia 0!
Ovviamente questo impone dei vincoli progettuali, quindi in generale non sembra una buona idea.
Load (LD)
L’operazione di load carica il contenuto della memoria all’indirizzo specificato nell’accumulatore:
MAR IR(addr)
MDR M[MAR]
ACC MDR
Store (ST)
L’operazione di store scrive il contenuto dell’accumulatore in memoria all’indirizzo specificato:
5
6. MAR IR(addr)
MDR ACC
M[MAR] MDR
Add (ADD)
La somma esegue l’operazione concettuale ACC = ACC+M[addr]:
MAR IR(addr)
MDR M[MAR], ALUA ACC
ALUOUT ALUA + MDR
ACC ALUOUT
Vale la pena notare che si può sfruttare il momento di lettura dell’operando per caricare il registro
ALUA con il contenuto dell’accumulatore.
Subtract (SUB)
La differenza non gode della proprietà commutativa, quindi è necessario specificare quale dei due
operandi sia il minuendo e quale il sottraendo. SUB esegue l’operazione ACC = ACC-M[addr]:
MAR IR(addr)
MDR M[MAR], ALUA ACC
ALUOUT ALUA + MDR’ + 1
ACC ALUOUT
Sarebbe possibile definire anche l’operazione inversa, cioè ACC = M[addr]-ACC. E’ comunque
possibile ottenere lo stesso risultato utilizzando questa sottrazione e negando (in senso aritmetico)
poi il risultato.
Increment (INC) e Decrement (DEC)
Solo come ulteriore esempio presentiamo le funzioni di incremento e decremento. Per l’incremento
non ci sono particolari problemi, dato che è una classica istruzione di ALU priva di parametri:
ALUOUT 0 + ACC + 1
ACC ALUOUT
Per quanto riguarda il decremento è possibile sfruttare la sottrazione con decremento, cioè sommare
all’accumulatore il negato di zero che in complemento a due è -1. Per fare questo però bisogna
caricare il canale B della ALU con uno zero che generiamo usando l’AND con uno zero.
ALUA ACC, ALUOUT 0 and ACC
ALUOUT ALUA + ALUOUT’
ACC ALUOUT
Questo esempio è un po’ più complesso, dato che si utilizza il fatto che non è necessario attendere il
corretto trasferimento di ACC in ALUA per preparare ALUOUT con uno 0, dato che il canale B è
assolutamente ininfluente e quindi possiamo in parallelo usare ACC per pilotare la porta B della
ALU e caricare ALUA. Inoltre si vede come possiamo utilizzare ALUOUT come registro sorgente
e come registro destinazione.
Unità di controllo
Per progettare l’unità di controllo è necessario identificare il valore di ogni segnale di controllo (i 19
che abbiamo visto precedentemente) in ogni “istante”. Gli istanti in realtà sono quantizzati dal
segnale di clock che quindi identifica una serie di fasi corrispondenti alle singole microistruzioni.
Chiameremo queste fasi con T seguito da un numero (cominciando da T0), per indicare il periodo di
clock a cui ci stiamo riferendo. È possibile vedere ogni periodo di clock come un indicatore dello
stato presente in un automa a stati finiti che secondo la rappresentazione di Moore associa as ogni
stato un valore delle uscite. Lo stato futuro dipenderà allora dallo stato presente e dagli ingressi. Gli
ingressi in questo caso sono identificati unicamente dal contenuto del campo OPCODE dell’IR. Per
6
7. chiarezza sul significato dei T, quindi continueremo a indicare T5 la sesta microistruzione
indipendentemente dall’istruzione di cui fa parte.
Implementazione cablata
Versione semplificata (solo fetch)
Riprendiamo in considerazione la fase di fetch e limitiamoci a specificare i segnali interessati a
questa fase, pensiamo quindi ad un processore che faccia solo e unicamente il fetch delle istruzioni
senza eseguirle. Indichiamo poi accanto ad ogni microistruzioni i segnali che devono essere posti a
1 e il valore dei segnali dell ALU.
T0: MAR PC, ALUOUT 0 + PC + 1 MARIE, PCOE, ALUOUTIE,
ZA=0, Cin=1, S1S0=10
T1: MDR M[MAR], PC ALUOUT MDRBUSIE, PCIE, ALUOUTOE
T2: IR MDR IRIE, MDROE
Da questo schema risulta molto semplice associare ad ogni segnale la sua funzione combinatoria
dipendente solo da T0,T1 e T2 che per la verità hanno solo tre possibili combinazioni, dato che
sono stati mutuamente esclusivi. Lo schema completo dei segnali per questo processore che fa solo
fetch, è il seguente:
MARIE = T0
PCOE = T0
ALUOUTIE = T0
ZA = T0’
Cin = T0
S1 = T0
S0 = T0’
MDRBUSIE = T1
PCIE = T1
ALUOUTOE = T1
IRIE = T2
MDROE = T2
Bisogna poi specificare la funzione di stato futuro per i T e in questo caso è veramente semplice:
T0f = T2
T1f = T0
T2f = T1
Versione completa (fetch e execute)
Passiamo invece ora a considerare l’unità di controllo necessaria a implementare un processore con
tutte le sei istruzioni viste in precedenza. La fase di fetch è esattamente quella vista in precedenza,
quindi andiamo a dettagliare solo le varie possibilità per l’execute.
Il campo OPCODE di IR è direttamente connesso (prima dei buffer di OE) ad un decoder nell’unità
di controllo che consente per ogni istruzione di generare un segnale corrispondente, ad esempio
SET potrebbe essere generato dall’opcode (in esadecimale) 000, LD da 001, ST da 002 e così via.
Non ci sono particolari motivi per avere una codifica o un’altra in questo caso.
7
8. SET:
T3,SET: MAR IR(addr) MARIE, IROE
T4,SET: ACC MAR ACCIE, MAROE
LD:
T3,LD: MAR IR(addr) MARIE, IROE
T4,LD: MDR M[MAR] MDRBUSIE
T5,LD: ACC MDR ACCIE, MDROE
ST:
T3,ST: MAR IR(addr) MARIE, IROE
T4,ST: MDR ACC MDRIE, ACCOE
T5,ST: M[MAR] MDR MDRBUSOE
ADD:
T3,ADD: MAR IR(addr) MARIE, IROE
T4,ADD: MDR M[MAR], ALUA ACC MDRBUSIE, ALUAIE, ACCOE
T5,ADD: ALUOUT ALUA + MDR ALUOUTIE, MDROE
ZA=1, Cin=0, S1S0=10
T6,ADD: ACC ALUOUT ACCIE, ALUOUTOE
SUB:
T3,SUB: MAR IR(addr) MARIE, IROE
T4,SUB: MDR M[MAR], ALUA ACC MDRBUSIE, ALUAIE, ACCOE
T5,SUB: ALUOUT ALUA – MDR’ + 1 ALUOUTIE, MDROE
ZA=1, Cin=1, S1S0=11
T6,SUB: ACC ALUOUT ACCIE, ALUOUTOE
INC:
T3,INC: ALUOUT 0 + ACC + 1 ALUOUTIE, ACCOE
ZA=0, Cin=1, S1S0=10
T4,INC: ACC ALUOUT ACCIE, ALUOUTOE
DEC:
T3,DEC: ALUA ACC, ALUOUT
0 and ACC ALUAIE, ACCOE, ALUOUTIE,
ZA=0, S1S0=00
T4,DEC: ALUOUT ALUA + ALUOUT’ ALUOUTIE, ALUOUTOE,
ZA=1, Cin=0, S1S0=11
T5,DEC: ACC ALUOUT ACCIE, ALUOUTOE
Per sintetizzare i segnali di controllo, è sufficiente mettere in AND l’indicatore dell’istante in cui il
segnale si deve attivare (deve essere posto a 1) con il segnale relativo all’istruzione corrente. Si
possono poi usare le varie leggi dell’algebra di Boole per semplificare l’espressione, risparmiando
sul numero di porte logiche necessarie:
0 ACCOE = T3·(INC + DEC) + T4·(ST + ADD + SUB)
1 ACCIE = T4·(SET + INC) + T5·(LD + DEC) + T6·(ADD + SUB)
2 MDROE = T2 + T5·(LD + ADD + SUB)
3 MDRIE = T4·ST
4 MDRBUSOE = T5·ST
5 MDRBUSIE = T1 + T4·(LD + ADD + SUB)
8
9. 6 MARIE = T0 + T3·(SET + LD + ST + ADD + SUB)
7 MAROE = T4·SET
8 IROE = T3·(SET + LD + ST + ADD + SUB)
9 IRIE = T2
10 PCOE = T0
11 PCIE = T1
12 ALUAIE = T3·DEC + T4·(ADD + SUB)
13 ALUOUTOE = T1 + T4·(INC + DEC) + T5·DEC + T6·(ADD + SUB)
14 ALUOUTIE = T0 + T3·(INC + DEC) + T4·DEC + T5·(ADD+SUB)
15 FLAGIE = 0
16 ZA = T4·DEC + T5·(ADD+SUB)
17 Cin = T0 + T3·INC + T5·SUB
18 S1 = T0 + T3·INC + T5·(ADD + SUB)
19 S0 = T4·DEC + T5·SUB
La codifica sopra riportata è certamente non minima, dato che assomiglia fortemente ad una sintesi
canonica SP. Non vengono assolutamente utilizzate le indifferenze anche banalmente verificabili.
Per esempio S0 può essere messo ad 1 in T4 sempre, dato che nessun’altra istruzione utilizza S0 in
T4, quindi si potrebbe risparmiare l’AND con il segnale DEC.
Nella realizzazione di una unità di controllo cablata, il numero di segnali cresce enormemente
rispetto a questo esempio, quindi riduzioni di questo tipo diventano indispensabili per evitare
dimensioni eccessive. Inoltre sostituire i segnali T0-6 con una loro codifica, ridurrebbe
notevolmente il numero di segnali da trasportare nell’unità di controllo.
La sintesi SP è piuttosto naturale per questo tipo di sistema, dato che abbiamo supposto i segnali
attivi ad uno. Dal momento che solo pochi di questi segnali si attivano contemporaneamente,
possiamo supporre che per la maggior parte dei casi l’uscita debba essere a 0, riducendo il numero
di mintermini da utilizzare. Parlare di mintermini in questo caso non è realmente appropriato, dato
che le varie configurazioni sono già decodificate, quindi dire ad esempio T3·INC sottointende
chiaramente che T0, T1, T2, T4, T5, T6, LD, ST, ADD, SUB e DEC siano a zero (evitandoci
dunque di specificarli nell’espressione).
È da notare inoltre che FLAGIE è stato volutamente ignorato, dato che in realtà nessuna istruzione
ha bisogno dei flag, anche se certamente dovrebbero impostarli tutte le istruzioni aritmetiche.
La sintesi dello stato futuro diventa leggermente più complessa visto che ci sono istruzioni con
lunghezza variabile. Quindi ad esempio se sono nello stato T4 e l’istruzione è una INC devo andare
allo stato T0, mentre se sono in una DEC devo andare in T5. Proseguendo con la sintesi intuitiva
fatta finora, si ottiene questo risultato:
T0f = T4·(SET + INC) + T5·(LD + ST + DEC) + T6·(ADD + SUB)
T1f = T0
T2f = T1
T3f = T2
T4f = T3
T5f = T4·(LD + ST + ADD + SUB + DEC)
T6f = T5·(ADD + SUB)
Parallelizzazione del caricamento del MAR
L’architettura appena illustrata è ancora incompleta, dato che manca di un aspetto fondamentale,
cioè la gestione dei salti che consente di eseguire istruzioni diverse a seconda di risultati o output.
Però è comunque utile considerare una otimizzazione a quanto illustrato finora.
Abbiamo già visto che se un registro sta pilotando il bus, più di un dispositivo può leggere e
memorizzare il contenuto. Questo può essere utilizzato per eliminare la prima microistruzione di 5
9
10. delle istruzioni già viste. Infatti SET, LD, ST, ADD e SUB sfruttano l’indirizzo presente nell’IR per
leggere dalla memoria e devono trasferirlo nel MAR. Si deve altresì notare che il contenuto del
MAR è ininfluente sulle altre microperazioni (a parte l’accesso in memoria). Quindi si potrebbe
sfruttare la terza istruzione del fetch per preparare già il MAR con il contenuto dell’IR. Nel MAR si
dovrebbe trasferire solo il campo Address dell’IR, ma il MAR è collegato ai 20 bit meno
significativi del bus, cioè proprio al campo richiesto, quindi qualsiasi cosa ci sia negli altri bit è
completamente ininfluente. Facendo l’esempio dell’istruzione LD, il risultato sarebbe il seguente:
MAR PC, ALUOUT 0 + PC + 1
MDR M[MAR], PC ALUOUT
IR MDR, MAR MDR <<< Qui viene fatta l’aggiunta
MAR ← IR(addr) <<< Questa μ-istruzione è eliminata
MDR M[MAR]
ACC MDR
Questa modifica non influenza in alcun modo le altre istruzioni, dato che non aggiunge nulla alle
istruzioni che non utilizzano la memoria (INC e DEC).
In sostanza le microistruzioni delle varie istruzioni diventano (tra parentesi è indicato il codice
operativo che useremo come riferimento):
SET (000)
ACC MAR
LD (001)
MDR M[MAR]
ACC MDR
ST (002)
MDR ACC
M[MAR] MDR
ADD (003)
MDR M[MAR], ALUA ACC
ALUOUT ALUA + MDR
ACC ALUOUT
SUB (004)
MDR M[MAR], ALUA ACC
ALUOUT ALUA + MDR’ + 1
ACC ALUOUT
INC (005)
ALUOUT 0 + ACC + 1
ACC ALUOUT
DEC (006)
ALUA ACC, ALUOUT 0 and ACC
ALUOUT ALUA + ALUOUT’
ACC ALUOUT
10
11. I segnali di controllo da generare diventano allora:
ACCOE = T3·(ST + ADD + SUB + INC + DEC)
ACCIE = T3·SET + T4·(LD + INC) + T5·(ADD + SUB + DEC)
MDROE = T2 + T4·(LD + ADD + SUB)
MDRIE = T3·ST
MDRBUSOE = T4·ST
MDRBUSIE = T1 + T3·(LD + ADD + SUB)
MARIE = T0 + T2
MAROE = T3·SET
IROE = 0
IRIE = T2
PCOE = T0
PCIE = T1
ALUAIE = T3·(ADD + SUB + DEC)
ALUOUTOE = T1 + T4·(INC + DEC) + T5·(ADD + SUB + DEC)
ALUOUTIE = T0 + T3·(INC + DEC) + T4·(ADD + SUB + DEC)
FLAGIE = 0
ZA = T4·(ADD + SUB + DEC)
Cin = T0 + T3·INC + T4·SUB
S1 = T0 + T3·INC + T4·(ADD + SUB)
S0 = T4·(SUB + DEC)
T0f = T3·SET + T4·(LD + ST + INC) + T5·(ADD + SUB + DEC)
T1f = T0
T2f = T1
T3f = T2
T4f = T3·(LD + ST + ADD + SUB + INC + DEC)
T5f = T4·(ADD + SUB + DEC)
Le istruzioni di salto
Parte integrante della programmazione sono le istruzioni di salto che consentono di modificare il
punto di esecuzione del programma. In particolare, fondamentali per implementare funzioni logiche
complesse servono istruzioni di salto condizionato, istruzioni che eseguono la modifica al PC solo
se si verifica una determinata situazione. Le “situazioni” che tipicamente si prendono in
considerazione sono i risultati delle operazioni aritmetiche precedenti. Per tenerne conto abbiamo
già accennato alla presenza di un registro FLAG che abbiamo ignorato finora. Questo registro deve
essere aggiornato con i segnali provenienti dalla ALU ad ogni operazione richiesta dall’utente, cioè
se consideriamo l’istruzione ADD comprensiva di Fetch ed Execute (nella prima versione senza
caricamento parallelo di IR e MAR in T2):
T0: MAR PC, ALUOUT 4 + PC MARIE, PCOE, ALUOUTIE,
ZA=0, QA=1, Cin=0, S1S0=10
T1: MDR M[MAR], PC ALUOUT MDRBUSIE, PCIE, ALUOUTOE
T2: IR MDR IRIE, MDROE
T3: MAR IR(addr) MARIE, IROE
T4,ADD: MDR M[MAR], ALUA ACC MDRBUSIE, ALUAIE, ACCOE
T5,ADD: ALUOUT ALUA + MDR ALUOUTIE, MDROE
ZA=1, Cin=0, S1S0=10, FLAGIE
T6,ADD: ACC ALUOUT ACCIE, ALUOUTOE
11
12. vediamo che vengono eseguite due operazioni di ALU, ma l’unica che deve essere memorizzata nei
FLAG è la seconda, cioè quella relativa ai dati dell’utente, cioè la ADD. I segnali provenienti dai
flag arrivano direttamente alla unità di controllo che li utilizza per realizzare le funzioni logiche
opportune. Iniziamo considerando la struttura di un salto incondizionato:
JMP
T3,JMP: PC IR(addr) PCIE, IROE
Questo è tutto. Insomma si prende il contenuto del campo ADDR di IR e lo si mette in PC. Al fetch
successivo il PC è già stato modificato opportunamente. Che cosa cambia in un salto condizionato?
semplicemente il trasferimento in PC avviene solo se si verifica la condizione richiesta, quindi se
per esempio vogliamo considerare un salto in caso di risultato Zero dell’operazione precedente:
JZ
T3,JZ,Z: PC IR(addr) PCIE, IROE
Aggiungiamo alla condizione da verificare anche il flag interessato.
I salti disponibili in questa architettura saranno (vista la ALU a disposizione):
JZ salta se il flag Z vale 1
JNZ salta se il flag Z vale 0
JO salta se il flag O vale 1
JNO salta se il flag O vale 0
JN salta se il flag N vale 1
JNN salta se il flag N vale 0
JC salta se il flag C vale 1
JNC salta se il flag C vale 0
Come si realizza questa condizione nell’unità di controllo? Nel caso cablato la soluzione è
particolarmente semplice, infatti il segnale Zero è disponibile già durante T2 (e anche prima) quindi
è sufficiente evitare di andare in T3 se l’istruzione è JZ e Z=0. Purtroppo però JZ non è disponibile
fino a T3, dato che viene calcolato da IR e quindi aspettiamo T3, non facendo niente nel caso di
salto non eseguito (condizione falsa). Consideriamo quindi di aggiungere l’istruzione JZ
all’implementazione cablata vista in precedenza, occupandoci solo dei segnali interessati
dall’istruzione. Queste sono le modifiche necessarie per una realizzazione letterale, cioè verificando
la condizione in T3 (le modifiche sono evidenziate):
IROE = T3·(JMP + JZ·Z + JNZ·Z’ + JO·O + JNO·O’ + JN·N + JNN·N’ + JC·C + JNC·C’)
PCIE = T1 + T3·(JMP + JZ·Z + JNZ·Z’ + JO·O + JNO·O’ + JN·N + JNN·N’ + JC·C + JNC·C’)
T0f = T3·(SET + JMP+JZ+JNZ+JO+JNO+JN+JNN+JC+JNC) + T4·(LD + ST + INC) + T5·(ADD
+ SUB + DEC)
Codice operativo e tempi di clock
Nella tabella seguente sono riportati, per ogni istruzione il codice operativo in decimale e
esadecimale, il codice mnemonico identificativo e il tempo di clock necessario all’esecuzione,
tenendo conto del caricamento anticipato del MAR, con e senza i cicli di clock necessari alla fase di
fetch.
12
13. Dec Hex Istruzione Clock Con Fetch
0 000 SET 1 4
1 001 LD 2 5
2 002 ST 2 5
3 003 ADD 3 6
4 004 SUB 3 6
5 005 INC 2 5
6 006 DEC 3 6
7 007 JMP 1 4
8 008 JZ 1 4
9 009 JNZ 1 4
10 00A JO 1 4
11 00B JNO 1 4
12 00C JN 1 4
13 00D JNN 1 4
14 00E JC 1 4
15 00F JNC 1 4
Esempi di programmi per l’architettura
Con la struttura delineata finora è possibile realizzare alcuni programmi molto semplici utilizzando
una sintassi nota come linguaggio Assembly. In questo caso non esistono regole sulla sintassi, dato
che ogni costruttore ne inventa di sue. Senza volere entrare nei dettagli delle caratteristiche
dell’Assembly, basti sapere che su ogni riga si specifica il codice operativo simbolico (detto anche
mnemonico) e gli eventuali operandi che possono essere numerici o simbolici, nel senso che non è
necessario calcolare l’indirizzo delle variabili in memoria, dato che può farlo un programma per
noi. Questo programma si chiama Assemblatore (Assembler).
Somma di due numeri
Vediamo un semplicissimo programma di esempio che somma due valori numerici a 32 bit A e B
impostati in memoria dal caricamento del programma e mette il risultato in una variabile C:
ld A
add B
st C
A: 00001234h
B: 0056789Ah
C: 0
Solitamente i valori esadecimali nei programmi assembly vengono terminati con una h. Inoltre vale
la pena notare che si è utilizzata una sintassi specifica per indicare che una linea non contiene
un’istruzione, ma un valore numerico che non deve essere interpretato.
Supponiamo che questo programma venga convertito in codice macchina e caricato in memoria a
partire dall’indirizzo 0. Utilizzando la codifica degli opcode illustrata precedentemente e scrivendo i
valori in esadecimale, quello che troveremmo sarebbe:
13
14. 32 bit
12 bit 20 bit
00000 001 0000C
00004 003 00010
00008 002 00014
0000C 000 01234
00010 005 6789A
00014 000 00000
Caricato il programma in memoria, l’esecuzione potrebbe iniziare a partire dall’indirizzo 00000. Il
problema è che terminata la terza operazione il processore eseguirebbe il fetch di una ulteriore
istruzione, in particolare di 00001234, che verrebbe interpretata come SET 01234! Insomma, il
problema è che non disponiamo di una istruzione per interrompere il fetch, cioè per fermare il
processore. Nella realtà questo non è un problema, dato che i processori continuano ad eseguire un
programma di base che solitamente è chiamato sistema operativo. Ma per non dilungarci e andare
fuori tema, limitiamoci a considerare come sia possibile interrompere il ciclo fetch/execute. Questo
effetto si può ottenere con un JMP all’indirizzo dell’istruzione. Il nostro programma a questo punto
diventa:
ld A
add B
st C
Fine: jmp Fine
A: 00001234h
B: 0056789Ah
C: 0
Si è utilizzato il simbolo Fine per definire un indirizzo, e in particolare l’indirizzo dell’istruzione
JMP. Anche negli esempi successivi non specificheremo gli indirizzi, ma sfrutteremo i due punti
per definire etichette simboliche da utilizzare come argomenti delle istruzioni. A questo punto
possiamo passare ad un esempio più complesso.
Prodotto di due numeri
Il prodotto di due numeri può essere eseguito in diversi modi, ad esempio con la tecnica che ci è
stata insegnata alle elementari che consiste nell’imparare delle moltiplicazioni fondamentali (le
tabelline) ed estenderle poi opportunamente. La versione più semplice però è l’utilizzo della
definizione, cioè sommare il moltiplicando tante volte quante indicate dal moltiplicatore. È da
sottolineare anche che questa tecnica è anche la più lenta e sconsigliabile in applicazioni reali. Ecco
come potrebbe apparire un programma che moltiplica A e B scrivendo il risultato in C:
Ancora: ld C
add A
st C
ld B
dec B
jnz Ancora
Fine: jmp Fine
A: 5
B: 6
C: 0
14
15. Alla fine di questo programma, il valore di B è stato “rovinato”, dato che lo si è utilizzato come
contatore per verificare quando terminare il ciclo. È inoltre importante notare come sia
fondamentale avere a disposizione una locazione di memoria inizializzata a zero, dato che questa
viene subito utilizzata come raccoglitore iniziale del risultato della moltiplicazione.
Implementazione micro programmata dell’unità di controllo
Versione senza salti
Un approccio alternativo alla realizzazione dell’unità di controllo, consiste nel memorizzare ogni
microistruzione come l’insieme di tutti i segnali di controllo con il loro valore. Questo necessita di
una struttura di memorizzazione (RAM, EEPROM, ROM) alla quale si potrà accedere con un
indirizzo. Al termine del ciclo di clock sarà necessario modificare opportunamente l’indirizzo
prelevando i segnali corretti. Tutto questo lo si può realizzare con un micro Program Counter (μPC)
che può essere incrementato per passare alla microistruzione successiva, può essere azzerato al
termine dell’istruzione, per tornare alla fase di fetch (comune a tutte le istruzioni) e può essere
caricato con il contenuto dell’OPCODE, che diventa un indirizzo nella memoria delle
microistruzioni. Lo schema seguente dovrebbe chiarire il concetto:
0
0
micro control
op code µPC address
1
control signals
2
Incremento microcode memory
Secondo questo schema sono necessari in ogni microistruzione anche i segnali di controllo per il
μPC che si limitano a selezionare quale dei tre ingressi del multiplexer deve essere utilizzato per il
passo successivo. Sono quindi necessari due bit aggiuntivi MC0 e MC1 che selezioneranno
l’ingresso:
MC1 MC0 Selezione
0 0 0 (μsalto al fetch)
0 1 μsalto all’opcode
1 0 prossima μistruzione
1 1 X (non valido)
Ci sono vari modi per evitare si avere un salto specifico al fetch, ma li vedremo in seguito.
Passiamo invece alla implementazione della architettura che abbiamo illustrato. Sono necessari 19
bit per i segnali di controllo più due per il microcontrollo. Utilizzando la numerazione della prima
tabella dei segnali di controllo possiamo assegnare 19 a MC0 e 20 a MC1 ottenendo uno schema a
21 colonne come il seguente:
15
16. ALUOUTOE
ALUOUTIE
Operazione
MDRBUSOE
MDRBUSIE
Indirizzo
ALUAIE
FLAGIE
MDROE
ACCOE
MARIE
MDRIE
ACCIE
PCOE
MC1
MC0
IROE
PCIE
IRIE
Cin
ZA
S1
S0
20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
101 010 1 1 1
fetch 0
10 1 1 1
1
01 1 1
2
10 1 1
LD 3
10 1
4
00 1 1
5
10 1 1
ST 6
10 1 1
7
00 1
8
10 1 1
ADD 9
10 1 1 1
10
101 0 0 1 1 1
11
00 1 1
12
10 1 1
SUB 13
10 1 1 1
14
101 1 1 1 1 1
15
00 1 1
16
101 0 1 0 1 1
INC 17
00 1 1
18
100 0 0 1 1 1
DEC 19
101 1 0 1 1 1
20
00 1 1
21
Gli spazi non riempiti contengono degli zeri. Questa soluzione prevede che il codice operativo sia
esattamente l’indirizzo nella memoria di controllo della prima microistruzione corrispondente
all’istruzione selezionata. Quindi ad esempio l’istruzione LD non sarebbe più 000 come
nell’esempio precedente, ma obbligatoriamente 003.
Purtroppo questa implementazione così come è stata presentata ha un bug sulla fase di esecuzione
del salto all’opcode. Infatti, nella terza microistruzione si trasferisce il contenuto dell’MDR in IR e
contemporaneamente si vorrebbe trasferire questo dato in μPC. Il problema è che i dati vengono
campionati sul fronte di salita del clock, quindi se si legge il contenuto di IR (anche solo il campo
OPCODE) nell’istante in cui questo campiona, si ricava il contenuto precedente dell’IR, quindi
l’indirizzo sbagliato. In realtà vuol dire che si fa il fetch di un’istruzione, ma si esegue la precedente
e questo potrebbe causare molti problemi. Sono possibili varie soluzioni, ma la più semplice –valida
solo per questo specifico caso– è quella di connettere il bus locale all’ingresso 1 del multiplexer di
selezione. In tal modo al termine della fase di fetch, quando viene eseguita la microistruzione (IR
MDR), sullo stesso fronte di clock viene caricato il codice istruzione (proveniente da MDR) in
IR ed il solo campo OPCODE in uPC. È chiaro che la soluzione alternativa sarebbe inserire un’altra
microistruzione dopo il trasferimento all’IR, ad esempio posticipando PC ALUOUT, purtroppo
in questo modo si aggiunge un ciclo di clock alla fase di fetch.
16
17. Codifica delle istruzioni
Abbiamo sottolineato come nel caso di microprogrammazione le istruzioni debbano essere
codificate in modo specifico per puntare all’indirizzo della microroutine corrispondente.
Effettivamente sarebbe più comodo poter utilizzare la stessa codifica utilizzata nel caso di
microarchitettura cablata. Una soluzione semplice si può realizzare specificando con l’op-code non
l’indirizzo, ma piuttosto il blocco di microistruzioni di interesse. Allineando i blocchi a indirizzi
multipli di una potenza di due e riservando loro uno spazio potenza di due, è sufficiente uno shift
cablato per ottenere il risultato voluto. Ad esempio, nel caso precedente sarebbe possibile utilizzare
blocchi da 4 microistruzioni, ristrutturando la memoria così:
Istruzione opcode Celle utilizzate Lunghezza
Fetch - 0,1,2 3
LD 001 4,5,6 3
ST 002 8,9,10 3
ADD 003 12,13,14,15 4
SUB 004 16,17,18,19 4
INC 005 20,21 2
DEC 006 24,25,26 3
In questo modo è possibile connettere il bit 2 dell’opcode al bit meno significativo del μPC
realizzando quindi uno shift a sinistra di due bit (moltiplicazione per 4, dimensione del blocco).
Sostanzialmente è come se si inserissero due zeri (con una codifica binaria) a destra del codice
operativo dell’istruzione, producendo così l’indirizzo del μPC.
Memoria delle microistruzioni
LD 0000|0000|0001 0000|0000|0000
ST 0000|0000|0010 0000|0000|0001
ADD 0000|0000|0011 0000|0000|0010
SUB 0000|0000|0100 0000|0000|0011
INC 0000|0000|0101 0000|0000|0100
DEC 0000|0000|0110 0000|0000|0101
0000|0000|0110
0000|0000|0111
0000|0000|1000
0000|0000|1001
0000|0000|1010
0000|0000|1011
0000|0000|1100
...
Il difetto di questa soluzione è chiaramente lo spreco di memoria, evidente dall’indirizzo dell’ultima
microistruzione di DEC che è a 26 rispetto al 21 del caso precedente. È anche possibile pensare ad
una rom che si occupi di tradurre gli opcode in microindirizzi, ma ovviamente si complica
ulteriormente lo schema architetturale.
Versione completa con i salti
Il problema più grosso nella gestione dei segnali di controllo lo si ha con l’implementazione
microprogrammata che non consente di inviare direttamente in uscita il contenuto di un registro, ad
esempio Carry-out. Se alcuni eventi devono essere condizionati al valore dei registri di stato diventa
necessario prevedere l’hardware (multiplexer solitamente) per inviare i vari segnali e poi codificare
nel microcodice i segnali di controllo per decidere quale valore verrà effettivamente selezionato.
17
18. Nel caso microprogrammato la questione è più complessa, dato che bisogna dotare il sequencer,
ovvero la parte di architettura che seleziona il prossimo valore del μPC, di ingressi dedicati ai Flag e
nella memoria delle microistruzioni prevedere dei selettori per questi segnali. Le soluzioni possibili
sono molteplici e noi ne vedremo solo una di esempio molto banale e probabilmente non delle
migliori, ma sicuramente semplice e di facile comprensione. Questa struttura si basa proprio sul
concetto che abbiamo visto sopra, cioè di passare da T2 a T3 in un salto condizionato solo se la
condizione specificata è vera. Per questo motivo si possono separare le tre possibilità previste nello
schema base (selezionate con MC1 e MC0) in due blocchi in cascata così:
MC0
0
0
MC1
1
0
µPC
op code 1
Incremento
In questo modo abbiamo separato in due livelli concettuali la selezione, ovvero abbiamo un segnale
che ci dice se dobbiamo eseguire il decode oppure se dobbiamo usare l’altro multiplexer. Questo
multiplexer ci consente di procedere alla microistruzione successiva o di tornare al fetch. Per
comandare il multiplexer di incremento o fetch possiamo decidere in maniera secca 0 o 1 fissi
oppure possiamo lasciare che siano i FLAG a comandarlo. Nasce quindi l’esigenza di inserire un
ulteriore multiplexer (questa volta a 1 bit) per passare i segnali di controllo opportuni:
0
1
C
C’
0 Z
Z’
N
0 N’
MC3
1
MC2-0
0
µPC
op code 1
Incremento
I segnali di controllo MC0, MC1 e MC2 selezionano quale segnale deve decidere se eseguire il
Fetch o se incrementare μPC quindi volendo riprodurre il comportamento della microarchitettura
precedente è possibile utilizzare i segnali come in tabella:
MC3 MC2 MC1 MC0 Funzione
1 x x x μsalto all’opcode
0 0 0 0 0 (μsalto al fetch)
0 0 0 1 prossima μistruzione (incremento)
0 0 1 0 se C incremento, altrimenti fetch
0 0 1 1 se C’ incremento, altrimenti fetch
0 1 0 0 se Z incremento, altrimenti fetch
0 1 0 1 se Z’ incremento, altrimenti fetch
18
19. 0 1 1 0 se N incremento, altrimenti fetch
0 1 1 1 se N’ incremento, altrimenti fetch
A questo punto non è difficile realizzare i salti condizionati e non. Nella prossima tabella vediamo
la fase di fetch e la realizzazione dei salti. In questo caso si è utilizzata l’architettura con
indirizzamento al byte e il caricamento parallelo di IR e MAR in T2. Resta l’accortezza di ricordare
che il μPC carica i dati dal bus quando si legge l’opcode e non dal IR per quanto detto
precedentemente:
ALUOUTOE
ALUOUTIE
Operazione
MDRBUSOE
MDRBUSIE
Indirizzo
ALUAIE
FLAGIE
MDROE
ACCOE
MARIE
MDRIE
ACCIE
PCOE
MC3
MC2
MC1
MC0
IROE
PCIE
IRIE
Cin
QA
ZA
S1
S0
23 22 21 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
0 0 0 110010 1 1 1
fetch 0
0 0 0 1 1 1 1
1
1 0 0 0 1 1 1
2
… …
0 0 0 0 1 1
JMP x
0 0 1 0
JC x+1
0 0 0 0 1 1
x+2
0 0 1 1
JNC x+3
0 0 0 0 1 1
x+4
0 1 0 0
JZ x+5
0 0 0 0 1 1
x+6
0 1 0 1
JNZ x+7
0 0 0 0 1 1
x+8
0 1 1 0
JN x+9
0 0 0 0 1 1
x+10
0 1 1 1
JNN x+11
0 0 0 0 1 1
x+12
…
Il grosso difetto di questa soluzione è che diventa necessario allungare di un ciclo di clock ogni
istruzione di salto. Infatti non è possibile caricare la nuova istruzione e contemporaneamente sapere
già quale condizione verificare. Viene quindi introdotta una microistruzione nulla che serve solo per
verificare lo stato dei segnali ed eseguire un μsalto al fetch in caso di condizione falsa.
Una alternativa a questo schema che non necessiti di una microistruzione vuota, si può realizzare
utilizzando il selettore del multiplexer per comandare direttamente PCIE, ritornando quindi allo
schema precedente per decidere riguardo all’incremento o al fetch.
19