CAPITOLO 4

Transcript

CAPITOLO 4
CAPITOLO 4
Questo capitolo contiene una descrizione dettagliata di tutti i passi
relativi alla progettazione del processore HEPE97 dalla descrizione VHDL di
tutti i singoli blocchi alla realizzazione del layout finale.
Di fronte all’esigenza di progettare un circuito molto complesso la
strategia migliore da adottare è quella di suddividerne le funzionalità globali in
vari blocchi logici più semplici ognuno dei quali assolva ad una singola
funzione. Per questo motivo uno schema a blocchi come quello di fig. 3.13 del
capitolo precedente costituisce una buona base di partenza: da questo si può
poi cominciare a pensare ad una struttura operativa come quella presentata in
fig. 4.1. In questo schema sono presentati i 5 blocchi fondamentali che sono
stati usati come base per la progettazione del processore. Più in particolare:
• l’Int_Selector è il selettore degli intervalli;
• il Rule_Add_Gen è il generatore degli indirizzi delle regole attive in base ai
codici prodotti dall’Int_Selector;
P r o c esso r
In t_ S e le c to r
R u le _ A d d _ G e n
C ore
A d d re ss_ G e n e r a to r
C o n se q u e n t
R a m ru le
M u lt7 x 4 fa st
R a m m f_ X 0
R a m m f_ X 1
M in im u m
D iv id e r
A lp h a _ P ro d u c t
W a lla c e
Fig. 4.1: Suddivisione gerarchica del processore HEPE97 in vari blocchi.
93
• l’Address_Generator è il generatore degli indirizzi delle tre memorie RAM
usato durante la fase di carica del sistema fuzzy all’interno del chip;
• il Core è il nucleo centrale di tutto il microprocessore in quanto contiene al
proprio interno le tre memorie RAM ed i blocchi di minimo e di prodotto
per il calcolo di θ;
• il Consequent è il blocco che esegue la defuzzificazione secondo il metodo
del centro di gravità e contiene quindi al proprio interno i due sommatori
paralleli, un moltiplicatore veloce ed il divisore finale.
I seguenti paragrafi trattano dettagliatamente della struttura interna di
ognuno di questi blocchi dalla descrizione dell’entità e architettura in VHDL
allo schematic e alla relativa simulazione. In seguito verrà delineata la struttura
che contiene al proprio interno questi 5 blocchi e che si trova al livello
gerarchico superiore, il blocco Processor.
4.1 Note alla lettura del codice VHDL
Di ogni blocco del processore è riportata l’entità con accanto il simbolo
per facilitarne la comprensione e solo alcune parti della descrizione
dell’architettura per ragioni di brevità. La versione integrale del codice VHDL
del processore HEPE97 è riportata in Appendice B. Per una chiara
comprensione di ogni parte del codice può risultare utile sottolineare i seguenti
punti:
• nell’entità dei vari blocchi gli ingressi e le uscite dichiarate sono di due tipi:
std_logic e my. Il tipo std_logic è un tipo fondamentale definito dallo
standard IEEE per tutti gli ingressi e le uscite ad un solo bit. Il tipo my
invece non è un termine standard ma è stato introdotto per dichiarare con
maggiore chiarezza i bus d’ingresso e uscita. Ad esempio l’espressione my7,
che indica un bus di 7 bit, nella notazione standard sarebbe scritta come
std_logic_vector(6 downto 0) in maniera molto meno elegante.
• la maggior parte delle istruzioni processo è sincrona con un segnale, ad
esempio un clock, un memory enable, ecc. L’istruzione “wait until
clock’event and clock=’1’ “significa che il processo è sincronizzato con il
fronte di salita del segnale clock, mentre l’istruzione “wait until clock’event
and clock=’0’ “ significa che il processo è sincrono con il fronte di discesa
di tale segnale.
• all’interno di una istruzione processo possono essere definite delle variabili
dove memorizzare il valore assunto dai segnali; è necessario però
distinguere le istruzioni di assegnamento a segnale (contraddistinte dal
simbolo <=) e le istruzioni di assegnamento a variabile (simbolo :=) in
94
quanto hanno un comportamento completamente diverso. In un
assegnamento a variabile del tipo var := espressione il risultato della
valutazione dell’espressione è assegnato immediatamente alla variabile var;
in una istruzione di assegnamento a segnale del tipo segnale <= espressione
il valore di espressione è assegnato a segnale solo dopo che tutte le
istruzioni sequenziali del processo sono state eseguite. E’ per questo motivo
che all’interno di un processo non possono essere eseguiti multipli
assegnamenti allo stesso segnale.
4.2 Il selettore degli intervalli Int_Selector
La fig. 4.2 evidenzia con chiarezza che, nell’ipotesi assunta di una
massima sovrapposizione consentita fra due insiemi fuzzy adiacenti e
nell’ipotesi che vi siano 8 insiemi fuzzy per variabile, la variabile in ingresso
X0 o X1 ha un grado di appartenenza non nullo rispetto a due soli fuzzy set.
Lo scopo del selettore di intervalli è determinare il codice di 3 bit del primo fra
i due insiemi fuzzy coinvolti per ognuna delle due variabili di ingresso, il
codice del secondo insieme fuzzy verrà poi determinato dal blocco successivo
Rule_Add_Gen. Per raggiungere questo obiettivo all’interno del blocco
devono essere caricate 6 parole da 7 bit che rappresentano i punti intervallo sia
per X0 sia per X1: 6 punti infatti dividono l’universo del discorso delle due
variabili in un numero di intervalli sufficiente a discriminare qualsiasi coppia
di fuzzy set adiacenti nel caso di 8 MF (in HEPE96 ci sono solo 7 MF per
variabile e quindi bastano 5 punti intervallo). Per memorizzare questi punti
intervallo, in totale 12 parole da 7 bit, si possono usare due blocchi di memoria
RAM ciascuno composto da 6 parole di 7 bit oppure si possono usare dei
buffer composti da 42 flip-flop di tipo D.
z 00
00 0
z 01
0 01
z0 2
010
z0 3
0 11
95
z0 4
10 0
z05
101
11 0
Fig. 4.2: I sei punti intervallo per la variabile di ingresso X.
La soluzione che è stata adottata nel processore fuzzy è quella dei registri
in quanto sono più veloci (il tempo d’accesso ad una parola di un buffer si
riduce al tempo di commutazione di un singolo flip-flop che in tecnologia
CMOS 0.7 µm è di 1.98 ns contro gli 8-10 ns di tempo d’accesso di una
memoria RAM) e occupano un’area minore. Per questi motivi in genere
quando si ha un basso numero di parole da memorizzare è preferibile usare dei
registri piuttosto che delle memorie RAM.
Ecco l’entità dell’Int_Selector:
entity Int_Selector is
port(me_ff, reset
x0, x1, di_int
intout0, intout1
end Int_Selector;
3
: in std_logic;
: in my7;
: out my3);
7
7
7
3
In ingresso al selettore degli intervalli entrano in parallelo i due ingressi
x0 e x1 di 7 bit (my7) e la parola di_int tramite la quale vengono caricati i
punti intervallo. Il segnale reset abilita o meno il funzionamento del circuito,
mentre il me_ff temporizza l’acquisizione degli ingressi.
Logicamente l’architettura del selettore, che è di tipo behavioral, è
scomposta in due processi ben distinti: il primo, chiamato Load_FF, si occupa
della fase di carica dei punti intervallo nei registri interni, il secondo, chiamato
Ric_intout, (in realtà sono due, uno per x0 per x1) si occupa di fornire in uscita
il codice voluto in base al valore degli ingressi.
La fase di carica viene descritta in VHDL nel seguente modo:
Load_FF : process
wait until me_ff’event and me_ff=’0’;
if(reset=’1’) then
z00 <= di_int;
wait until me_ff’event and me_ff=’0’ ;
z01 <= di_int ;
wait until me_ff’event and me_ff=’0’ ;
z02 <= di_int ;
…
end process Load_FF;
96
--fronte di discesa
--fronte di discesa
--fronte di discesa
Questa istruzione processo significa che quando si ha un fronte di discesa
del segnale me_ff, cioè quando il suo valore logico passa da 1 a 0, se il segnale
reset si trova a livello logico alto, e quindi se il funzionamento del blocco è
abilitato, allora l’ingresso di_int viene trasferito al segnale interno di 7 bit di
nome z00. Ai successivi fronti di discesa del me_ff il dato in ingresso di_int
viene trasferito ai segnali interni z01, z02,…, z05, z10, ..., z15 fino a quando
tutti e dodici i punti intervallo sono stati memorizzati.
Il processo di selezione degli intervalli è invece così descritto:
ric_intout0: process (x0)
begin
if(x0 <= z02) then
if(x0 <= z00) then
intout0 <= “000”;
elsif(x0 <= z01) then
intout0 <= “001”;
end if;
…
end process ric_intout0;
--se x0 è minore di z02
--se x0 è minore di z00
--all’uscita e assegnato il codice 000
--se x0 è minore di z01
--all’uscita e assegnato il codice 001
Quando si ha una variazione sul segnale d’ingresso x0 vengono svolti
tutti i possibili confronti fra x0 ed i 6 punti intervallo per determinare il codice
dell’intervallo a cui appartiene l’ingresso. La descrizione VHDL si basa
semplicemente su una serie
di istruzioni IF-THEN-ELSE,
istruzioni che verranno poi
implementate in hardware
tramite
una
serie
di
comparatori digitali. Da
notare che il confronto è
iniziato con il punto z02 per
ottimizzare i ritardi.
La descrizione VHDL di cui
è stata qui riportata una
piccola parte (la versione
integrale
si
trova
in
Appendice) è stata simulata
con Leapfrog per verificarne
la funzionalità e sintetizzata
con Synergy fino ad ottenere
Fig. 4.3 : Schematic del blocco Int_Selector.
97
lo schematic in fig. 4.3. La figura ottenuta contiene un numero molto alto di
standard cell (669 porte logiche elementari) e quindi può risultare molto
complicata a prima vista; comunque la parte sinistra dello schematic contiene
tutti i flip-flop mentre la parte destra contiene i comparatori digitali
rappresentati al livello di porte logiche elementari. La fig. 4.4 riporta i risultati
della simulazione circuitale effettuata con Verilog sia nella fase di carica sia
nella fase di running vera e propria. Si può notare (vedi fig. 4.4a) che al
momento del fronte di discesa del me_ff la parola da memorizzare di_int è già
pronta da qualche ns per non causare problemi di setup / hold con i flip-flop
interni e per questo motivo la temporizzazione degli ingressi della fase di
carica è il seguente:
defstrobe in window ".....%%%%%"
defstrobe in edge ".......%%%"
defstrobe in edge ".......%%%"
me_ff
reset
di_int
L’istruzione “defstrobe in window” indica che il me_ff oscilla fra gli stati
0 e 1 come un clock e come tale viene usato durante la fase di carica; il
segnale di_int ha esattamente la stessa temporizzazione in modo che durante
una transizione di fase negativa del me_ff i dati in ingresso sono stabili. Nella
fase di selezione degli intervalli (fig. 4.4b) il me_ff si trova sempre al livello
logico basso ed il ritardo dal momento in cui gli ingressi sono pronti all’istante
in cui le uscite intout0 e intout1 si trovano ad un valore definito è di circa 6 ns.
(a)
(b)
98
Fig. 4.4: Forme d’onda della simulazione di Int_Selector nella fase di carica
(a) e nella fase di selezione vera e propria (b).
4.3 Il generatore di indirizzi Rule_Add_Gen
I due codici di 3 bit prodotti dal selettore di intervalli devono essere
combinati opportunamente in modo da fornire gli indirizzi delle 4 regole attive
della memoria delle regole Ramrule. Il blocco circuitale progettato per
adempiere a questo compito è il Rule_Add_Gen, che è il generatore degli
indirizzi delle regole attive. Partendo da intadd0 e intadd1 gli indirizzi delle
regole attive sono generati nel seguente modo :
•
•
•
•
I regola : intadd0 & intadd1 ;
II regola : (intadd0 + 1) & intadd1 ;
III regola : intadd0 & (intadd1 + 1) ;
IV regola : (intadd0 + 1) & (intadd1 + 1) ;
dove il simbolo “&” indica il concatenamento delle due parole di 3 bit in
un’unica parola di 6 bit. Se ad esempio intadd0 e intadd1 valgono
rispettivamente 010 e 101 le quattro parole generate sono 010-101, 011-101,
010-110 e 011-110.
Ecco l’entità VHDL del generatore di indirizzi:
entity Rule_Add_Gen is
port( intadd0, intadd1: in my3;
ck, enable
: in std_logic;
rule_address : out my6);
end Rule_Add_Gen;
3
3
Il funzionamento di questo circuito è
molto semplice: appena si ha un picco sul
segnale enable, quando cioè questo segnale rimane al livello logico 1 almeno
per un periodo di clock, vengono forniti in uscita per i 4 periodi di clock
successivi i quattro indirizzi desiderati. Quindi per 4 periodi di ck gli ingressi
intadd0 e intadd1 devono rimanere costanti. La descrizione VHDL
dell’architettura behavioral è costituita da un unico processo chiamato chain
(che in inglese vuol dire concatenare) di questo tipo:
chain : process
begin
wait until ck’event and ck=’1’;
--attivo sul fronte di salita del clock
99
if(enable = ’1’) then
case intadd0 is
--incremento intadd0 di una unità
when “000” => var0 := “001”;
when “001” => var0 := “010”;
when “010” => var0 := “011”;
when “011” => var0 := “100”;
when “100” => var0 := “101”;
when “101” => var0 := “110”;
when “110” => var0 := “111”;
end case;
--primo indirizzo
rule_address <= intadd0 & intadd1;
add <= var0 & intadd1;
--secondo indirizzo
…
end process chain;
Al primo colpo di clock in cui l’enable si trova a livello logico alto viene
eseguita l’istruzione “case” che dà alla variabile var0 il valore dell’ingresso
intadd0 incrementato di 1, viene fornito in uscita il primo indirizzo ottenuto
concatenando, tramite l’operatore di concatenazione “&”, gli ingressi intadd0
e intadd1 e, infine, viene prodotto il nuovo indirizzo add che è l’indirizzo
della seconda regola attiva: tutto questo in un unico periodo di clock. In questo
modo nel periodo successivo il valore add viene direttamente portato in uscita
con l’istruzione:
rule_address <= add;
100
Fig. 4.5: Schematic del Rule_Add_Gen.
ed in parallelo viene generato il valore dell’indirizzo della terza regola attiva
incrementando il valore dell’ingresso intadd1.
La vista schematic (vedi fig. 4.5) ottenuta tramite sintesi contiene 128
celle per un’area complessiva di circa 90000 µm2, cioè 0.09 mm2.
La temporizzazione degli ingressi e delle uscite è riportata in fig. 4.6
attraverso le forme d’onda ottenute dalla simulazione con Verilog: come si
vede l’uscita rule_address si trova ad un livello logico indefinito (zona nera)
fino a quando non si ha un fronte di salita del clock con l’enable a livello
logico alto. Da questo momento in poi il bus d’uscita, con il ritardo dovuto alla
commutazione dei flip-flop d’uscita, contiene i 4 indirizzi desiderati uno per
ogni periodo di clock successivo. In seguito gli ingressi intadd0 e intadd1
potranno cambiare e potranno così essere generati 4 nuovi indirizzi di
memoria.
Fig. 4.6 : Forme d’onda ottenute dalla simulazione del blocco Rule_Add_Gen.
4.4 Il generatore di indirizzi Address_Generator
Oltre al Rule_Add_Gen c’è un altro generatore di indirizzi,
l’Address_Generator, il quale a differenza dal precedente non viene usato nella
fase di running del processore ma nella fase di carica. Durante la fase di carica
del chip vengono memorizzate tutte le parole della Ramrule e le parole delle
due look-up table Rammf_X0 e Rammf_X1, oltre ai punti intervallo Zij.
Affinché ciò sia possibile occorre che un blocco del chip sia dedicato alla
generazione di ognuno di questi indirizzi in modo che le parole in ingresso
vengano memorizzate nella giusta locazione di memoria.
101
L’entità dell’Address_Generator è:
entity Address_Generator is
port(setram:
in my3;
me_load:
in std_logic;
reset:
in std_logic;
address_loading:
out my7);
end Address_Generator;
3
Set_ram è un bus di 3 bit che entra in ingresso al processore e che prende
il nome setram in ingresso all’Address_Generator. Il valore di set_ram
contraddistingue tutte le possibili fasi di funzionamento del processore, in
particolare:
set_ram=000
set_ram=001
set_ram=010
set_ram=011
set_ram=111
Î fase di carica della Ramrule;
Î fase di carica della Rammf_X0;
Î fase di carica della Rammf_X1;
Î fase di carica dei punti intervallo;
Î fase di running.
L’ingresso set_ram del microprocessore viene posto direttamente in
ingresso al blocco Address_Generator, così come l’ingresso reset, con il nome
setram in modo che vengano generati gli indirizzi corretti. Quando setram vale
0 (in decimale) l’Address_Generator deve generare gli indirizzi da 0 a 63, uno
per ogni periodo del segnale me_load; allo stesso modo quando vale 1 o 2
deve generare gli indirizzi da 0 a 127 per potere indirizzare ogni parola delle
look-up table Rammf_X0 e Rammf_X1; ogni altro valore di setram in ingresso
è privo di significato all’interno di questo blocco e non dà luogo ad alcuna
uscita.
L’architettura VHDL del generatore di indirizzi è descritta da un unico
processo che contiene istruzioni di questo tipo:
carica : process
variable add: my7;
begin
wait until me_load’event and me_load=’0’;
--fronte di discesa
if(reset=’0’) then
if(setram=”000) then
--fase di carica della Ramrule
address := “0000000”;
for i in 1 to 64 loop
wait until me_load’event and me_load=’0’;
102
address_loading <= address;
address := address + ”0000001”;
end loop;
elsif …
--fase di carica delle altre due memorie
end process carica;
Al primo fronte di discesa del me_load la variabile address viene
inizializzata a 0 e dal periodo successivo e per 64 periodi il suo valore è
assegnato all’uscita address_ loading e poi incrementato di 1 : in questo modo
vengono generati in ordine i 64 indirizzi per la carica della memoria Ramrule.
Lo schematic ottenuto con la sintesi (vedi fig. 4.7) è piuttosto complicato
per l’alto numero di porte logiche elementari che lo costituiscono : ve ne sono
infatti 313 per un’area totale di 0.2 mm2. Al contrario il funzionamento
circuitale è molto semplice come si può vedere nelle forme d’onda di fig. 4.8:
il bus setram è fisso al valore 0 (fase di carica della memoria Ramrule), il reset
è al valore 1 cioè inattivo e quindi ad ogni fronte di discesa del me_load
l’uscita address_loading si incrementa di una unità. Il tempo di ritardo
necessario dal fronte di discesa del me_load all’istante in cui l’uscita è valida è
di circa 3-4 ns.
103
Fig. 4.7: Vista schematic del blocco circuitale Address_Generator.
4.5 Il blocco circuitale Core
Il blocco Core è il nucleo centrale di tutto il microprocessore : esso
comprende al proprio interno il blocco della fuzzificazione, e cioè le due lookup table Rammf_X0 e Rammf_X1, il blocco dell’inferenza, quindi i due
circuiti che calcolano il minimo ed il prodotto fra gli α, e la memoria delle
regole Ramrule. Esso gestisce la temporizzazione di tutti i segnali interni e la
comunicazione fra i vari sottoblocchi che lo compongono in modo tale che al
variare degli ingressi siano prodotti in uscita i valori dei θ e degli Z
corrispondenti.
I prossimi paragrafi descrivono ognuno di questi sottoblocchi per poi
passare alla descrizione della vista Core e di tutti i segnali interni.
Fig.
4.8:
Forme d’onda relative
Address_Generator.
alla
simulazione
del
blocco
4.5.1 La memoria delle regole Ramrule
La memoria Ramrule è stata generata con il programma ES2generate
come una megacella con le seguenti caratteristiche :
•
•
•
•
•
64 parole di 9 bit ciascuna ;
tempo di accesso di 8.03 ns;
dimensioni laterali di 0.903 x 0.391 mm x mm;
area di 0.35 mm2;
potenza assorbita di 0.64 mW/MHz.
Gli ingressi e le uscite della memoria Ramrule, di cui è riportata una
schematizzazione in fig. 4.9, sono i seguenti :
• il bus degli indirizzi Add di 6 bit ;
• il bus dei dati in ingresso Di di 9 bit ;
• il segnale di controllo We (write enable) ;
104
• il segnale di temporizzazione Me (memory enable);
• il bus dei dati in uscita Do sempre di 9 bit.
L’ingresso Me temporizza tutte le fasi di lettura e scrittura come se fosse
un clock mentre l’ingresso We stabilisce la direzione del flusso di dati: quando
si trova a livello logico alto il dato viene estratto dalla memoria (fase di
lettura), quando invece è a livello logico basso una parola può essere
memorizzata in una cella interna (fase di scrittura). Per un corretto
funzionamento la memoria RAM deve rispettare una serie di vincoli temporali
che vengono poi accuratamente controllati dai simulatori Verilog e Veritime.
In particolare la fig. 4.10 mostra il diagramma temporale di un ciclo di lettura
della memoria e la fig. 4.11 mostra le temporizzazioni da rispettare in un ciclo
di scrittura. Per ognuno dei parametri temporali contraddistinto da una sigla,
come tads o tacc, la memoria possiede i valori minimo, tipico e massimo
all’interno dei quali il funzionamento è garantito. Quando la temporizzazione
dei segnali di ingresso è tale da violare anche uno solo di questi parametri il
funzionamento dell’intera memoria è compromesso. Vediamo il significato di
tutte le sigle riportate nelle due figure:
• tacc: tempo che passa dal
VDD
fronte di salita del me
all’uscita del dato;
• macc: ritardo dipendente dal
Ramrule 64 x 9 bit
carico capacitivo da pilotare
Me
(ns / pF);
Logica di controllo
• tpre: tempo minimo in cui il
We
me deve rimanere a 0 fra un
ciclo e il successivo;
• trds: setup del segnale we;
Indirizzi
Add<5:0>
• trdh: hold del segnale we;
• trdw:
larghezza
minima
dell’impulso di me;
• twds: tempo di setup in cui il
Di<8:0> Dati Do<8:0>
dato da scrivere in memoria
deve rimanere valido prima del
GND
fronte di discesa del me;
• twdh: tempo di hold del dato
dopo la transizione del me.
Fig. 4.9: Simbolo della Ramrule.
In fase di lettura viene prima
fornito un indirizzo valido, poi dall’istante in cui il Me commuta da 0 a 1
comincia la fase di decodifica interna che porta il dato in uscita sul bus Do
105
dopo il tempo tacc*. Questo tempo è dato dalla somma del tempo di accesso
standard (in assenza di carico) e del ritardo dovuto al carico secondo la
formula:
tacc*
=
tacc
+
macc
*
Cout
(4.1)
dove Cout è la somma delle capacità in parallelo da pilotare.
La fase di scrittura è invece un poco più difficile da gestire in quanto
l’indirizzo valido deve essere fornito sul fronte di salita del Me mentre il dato
da memorizzare deve essere fornito sul fronte di discesa dello stesso segnale.
Solo in questo modo il dato può venire scritto all’interno della cella di
memoria desiderata.
Visto che la megacella Ramrule è stata generata automaticamente pronta
per essere inserita all’interno di uno schema circuitale potrebbe sembrare
inutile una descrizione VHDL della stessa. Al contrario questa può risultare
molto utile in fase di simulazione logica con Leapfrog del blocco gerarchico
che contiene questa e le altre memorie per verificare la correttezza del
funzionamento globale del circuito. Ecco quindi l’entità della memoria
Ramrule:
entity Ramrule is
port( do : out my9;
add : in my6;
di : in my9;
me : in std_logic;
we : in std_logic);
end Ramrule;
--bus dei dati in uscita a 9 bit
--bus degli indirizzi a 6 bit
--bus dei dati in ingresso a 9 bit
--memory enable
--write enable
106
in d i riz z o
v a lid o
ADD
tad s
tp re
ta d h
ME
trd s
WE
tr d w
trd h
ta c c *
DO
u s c ita
v a lid a
Fig. 4.10: Diagramma temporale della fase di lettura della Ramrule.
L’architettura è costituita da due processi concorrenti uno per la fase di lettura
e uno per la fase di scrittura costruiti in modo tale da modellare adeguatamente
il comportamento della memoria Ramrule:
Read_Rule_Mem : process
--fase di lettura
begin
wait until me’event and me = ‘1’;
--fronte di salita
if (we = ‘1’) then
do <= ram_data(add) after 8 ns;
end if;
end process Read_Rule_Mem;
Write_Rule_Mem : process
--fase di scrittura
begin
wait until me’event and me = ’0’;
--fronte di discesa
if (we = ‘0’) then
ram_data(add) <= di after 8 ns;
end if;
end process Write_Rule_Mem;
107
In fase di lettura il dato viene fornito in uscita con un certo ritardo (qui
schematizzato come 8 ns con l’istruzione “after”) dopo il fronte di salita del
me. E’ stato scelto un ritardo di 8 ns in quanto l memoria Ramrule ha un tempo
d’accesso di circa 8 ns. In fase di scrittura invece il dato viene memorizzato
all’interno con lo stesso ritardo dopo un fronte di discesa del me. Con queste
semplici istruzioni si è cercato di modellare al meglio i diagrammi temporali di
fig. 4.10 e 4.11.
in d iriz z o
v a lid o
ADD
ta d s
tp re
ta d h
ME
trd w
trd h
WE
tw d s
tw d h
DI
in g re s s o
v a lid o
Fig. 4.11: Diagramma temporale della fase di scrittura nella memoria
Ramrule.
4.5.2 Le look-up table Rammf_X0 e Rammf_X1
Le due memorie Rammf_X0 e Rammf_X1 generate come megacelle allo
stesso modo della Ramrule presentano le seguenti caratteristiche:
•
•
•
•
•
128 parole di 8 bit ciascuna;
tempo d’accesso di 8.55 ns;
dimensioni laterali di 0.826 x 0.561 mm x mm;
area di 0.46 mm2;
potenza assorbita di 0.96 mW / MHz.
Queste memorie sono più grandi della Ramrule e quindi hanno un tempo
di accesso superiore e assorbono una potenza maggiore. Tutti i discorsi sulla
temporizzazione e sulla descrizione VHDL fatti per la Ramrule valgono anche
per queste due memorie RAM e quindi non verranno ripetuti.
4.5.3 Il blocco di minimo Minimum
108
Questo è il blocco più piccolo e più semplice fra tutti i vari componenti
del processore. Il suo funzionamento si riduce infatti a prendere in ingresso
due parole da 4 bit a e b e a fornire in uscita il minimo theta fra i due valori.
La logica di controllo della vista Core provvede a fornire in ingresso al blocco
Minimum i valori di α corretti su cui operare l’inferenza.
Ecco l’entità del blocco di minimo:
M in im u m
entity Minimum is
port ( a, b :
in my4;
ck :
theta :
4
a
4
4
b
th eta
ck
in std_logic;
out my4);
end Minimum;
Il comportamento del circuito è molto semplice: quando si ha un fronte di
salita del clock il minimo fra i due valori a e b viene posto nell’uscita theta. La
descrizione dell’architettura di tipo comportamentale è composta da un unico
processo:
mini : process
begin
wait until ck’event and ck = ’1’;
if(a > b) then
theta <= b;
else
theta <= a;
end if;
end process mini;
109
--fronte di salita del clock
--se a è maggiore di b allora ...
--il valore di b è assegnato a theta
--altrimenti
--il valore di a è assegnato a theta
La sintesi di questa descrizione VHDL ha prodotto una vista schematic
(vedi fig. 4.12) composta da un numero di porte molto basso: questo schema
circuitale è quindi molto più facilmente comprensibile degli altri.
Fig. 4.12: Schematic del circuito di minimo.
Il circuito ottenuto impiega un tempo di soli 3-4 ns dal fronte di salita del
clock all’istante in cui theta assume il valore corretto e quindi può svolgere
con ampi margini il proprio compito all’interno di un unico periodo di clock.
4.5.4 Il blocco moltiplicatore Alpha_Product_Wallace
Il processo di inferenza alternativo al minimo è il prodotto. In pratica il
blocco Alpha_Product_Wallace prende in ingresso due valori Alpha_a e
Alpha_b di 4 bit e ne esegue la moltiplicazione fornendo in uscita il prodotto
theta anch’esso rappresentato come numero di 4 bit. È ovvio che il prodotto
fra due numeri di 4 bit produce un numero di 8 bit e quindi il risultato della
moltiplicazione deve essere troncato alle prime 4 cifre significative.
Il metodo più usato comunemente per implementare una moltiplicazione
in hardware è quello detto “add & shift”: dati i due numeri a e b di cui si vuole
eseguire la moltiplicazione il valore a viene sommato a se stesso tante volte
quanto è il valore del fattore b. Questo metodo è molto semplice da
implementare ma anche molto dispendioso in termini di tempo. Per riuscire ad
eseguire l’intera moltiplicazione in un tempo breve e quindi chiuso all’interno
110
di un unico periodo di clock la scelta è caduta su un algoritmo diverso :
l’algoritmo di Wallace [ 16].
A lph a_ a(3 ) A lp ha_a(2) A lpha _a(1) A lpha_a (0) x
A lph a_ b(3 ) A lph a_ b(2) A lph a_b(1) A lph a_b (0) =
X (2)
Y (1)
Z (0)
X (1)
Y (0 )
X (0)
Y (3)
Z (3) Z (2 )
K (2) K (1)
X (3)
Y (2)
Z (1)
K (0)
K (3 )
R (3) R (2)
Z (3) S (3)
K (2)
R (1)
S (2 )
K (0)
R (0)
S (1)
S (0)
X (0)
R (7)
K (3)
R (6)
S (7)
R (5)
S (6)
R (4 )
S (5)
S (4)
S (0)
X (0)
Θ(4)
Θ(3)
Θ(2)
Θ(1)
Θ(0)
I
K (3 )
II
III
Θ(7) Θ(6) Θ(5)
Fig. 4.13: Moltiplicazione effettuata con l’algoritmo di Wallace.
Il funzionamento dell’algoritmo è visualizzato in fig. 4.13: per prima
cosa viene effettuata la moltiplicazione bit a bit fra ogni bit di Alpha_a ed ogni
bit di Alpha_b. In questo modo vengono prodotte le 4 parole di 4 bit X, Y, Z e
K (vedi fig. 4.13) dove X è data dalla moltiplicazione bit a bit di Alpha_b(0)
ed i quattro bit di Alpha_a, Y è data dalla moltiplicazione di Alpha_b(1) ed i
bit di Alpha_a e così via. Occorre precisare che la moltiplicazione fra bit è
equivalente all’operazione logica di AND : la generazione dei valori di X, Y,
Z e K sarà quindi realizzata attraverso una serie di porte logiche AND che
lavorano in parallelo. Successivamente devono essere effettuate in parallelo le
somme dei bit evidenziati in figura da un cerchio che si trovano nel livello I :
allo scopo vengono usati cinque full-adder, celle della libreria che eseguono la
somma fra due bit più un eventuale riporto. I valori della somma e del riporto
di ogni operazione sono chiamati S e R. Successivamente nel livello II
vengono eseguite le quattro somme evidenziate da altre quattro celle fulladder. Giunti al terzo livello i 3 bit meno significativi (S(4), S(0) e X(0)) sono
già pronti per l’uscita mentre i primi 5 sono ottenuti con un sommatore carry
look ahead veloce che esegue la somma dei 4 bit in parallelo fino a fornire in
uscita i bit più significativi dell’uscita theta. Il circuito che si ottiene usando
l’algoritmo di Wallace è una rete interamente combinatoria costituita da
111
diversi livelli: il primo livello di celle AND, due livelli di full-adder ed il
livello finale contenente un carry look ahead. La velocità raggiunta con
un’architettura simile è sufficiente per i nostri scopi.
La descrizione VHDL di questo moltiplicatore contiene istruzioni di
diverso tipo. Ad esempio il primo livello combinatorio viene descritto
semplicemente con istruzioni quali:
X(0) <= Alpha_b(0) and Alpha_a(0);
X(1) <= Alpha_b(0) and Alpha_a(1) ;
...
Il primo livello di sommatori (I in figura 4.13) viene descritto con
istruzioni di configurazione di celle full-adder, della libreria ES2 o generate in
precedenza, di questo tipo:
full_adder port map(a => X(1), b => Y(0), ci => zero, s => S(0), co => R(0));
Questa istruzione può essere letta nel seguente modo: viene generata una
copia del componente full_adder i cui ingressi a e b vengono collegati ai
segnali X(1) e Y(0) mentre all’ingresso ci (l’ingresso del riporto carry in) entra
il livello logico 0; le uscite s e co (il riporto in uscita carry out) vanno poi
collegate ai segnali S(0) e R(0). Per quanto riguarda il sommatore carry look
ahead può essere descritto nel seguente modo:
theta(7 downto 3) <= R(7 downto 4) + (K(3) & S(7 downto 5));
In fase di sintesi occorre però ricordarsi di specificare al sintetizzatore di
implementare questa somma con un carry look ahead, altrimenti la somma
verrà implementata con una serie di full-adder in cascata.
L’operazione di troncamento del risultato finale può non essere priva di
problemi. Infatti eseguendo la moltiplicazione 1111 x 1111 e troncando gli
ultimi 4 bit del risultato porta alla conclusione assurda:
1111 x 1111 = 11100001 = 1110 e non 1111 !
In altre parole il valore 1111, che dovrebbe essere l’elemento neutro della
moltiplicazione, non si comporta come tale. Il problema sta nel fatto che con 4
bit non è possibile rappresentare l’unità: infatti 1111b = 15d e non 16d, in modo
tale che si commette un errore di una parte su 16. Per evitare questo problema
viene eseguita una conversione di scala [0,225]
[0,255] sul risultato della
Î
112
moltiplicazione, in modo che se theta vale 11100001 (=225) il suo valore sia
convertito in 11111111 (=255) come desiderato. Per ottenere la conversione di
scala desiderata è sufficiente applicare la seguente formula al valore di theta
ottenuto con la moltiplicazione:
(4.2)
theta corretto = (255/225) * theta prodotto ≅
≅ 1.13 * theta prodotto ≅
≅ (1 + 1/8) * theta prodotto.
La frazione 0.13 è approssimata ad 1/8 in quanto in hardware una
divisione per un numero multiplo di 2 è molto semplice ed equivale al
troncamento delle ultime 3 cifre. La divisione per un numero qualsiasi invece
richiede un hardware molto più complicato, quindi meno conveniente dal
punto di vista dell’area e dei ritardi. Quindi se per esempio Alpha_a vale 1111
e Alpha_b vale 0101 allora il risultato della moltiplicazione vale 01001011 ed
il risultato finale viene calcolato come:
01001011 + 00001001 = 01010100 = 0101
che è esattamente il risultato desiderato.
Lo schema circuitale ottenuto con la sintesi è costituito da 217 celle per
un’area totale di 0.12 mm2. I risultati della simulazione con Verilog sono
quelli attesi: il circuito riesce ad operare la moltiplicazione dei due numeri da
4 bit all’interno del periodo di clock usato di 16 ns e quindi l’obiettivo velocità
è stato pienamente raggiunto.
4.5.5 Funzionamento del blocco Core vero e proprio
Partiamo direttamente dall’entità
della vista Core:
entity Core is
port(me, min_prod:
we, ck:
set_ram :
set_test :
stand_by:
stand_by1:
add_rule :
in std_logic;
in std_logic;
in my3;
in my4;
in std_logic;
in std_logic;
in my6;
3
4
7
7
9
8
113
4
7
7
add_code_0:
add_code_1:
data_rule :
data_mf :
en_assign :
test :
theta :
zeta :
end Core;
in my7;
in my7;
in my9;
in my8;
in std_logic;
out my7;
out my4;
out my7);
I segnali che regolano la logica di controllo e la temporizzazione sono il
memory enable me, il bit per la selezione del metodo di inferenza min_prod, il
write enable we, il clock ck ed il selettore delle fasi di funzionamento del chip
set_ram. I segnali stand_by e stand_by1 fanno in modo che le memorie siano
abilitate solo quando ciò è realmente necessario per diminuire così la potenza
totale assorbita. Tutti questi segnali vengono forniti direttamente dal livello
superiore della gerarchia e cioè dalla vista Processor vera e propria. Compito
del blocco Core è fare in modo che i segnali che provengono dall’esterno
opportunamente temporizzati sappiano governare il funzionamento interno di
tutti i sottoblocchi.
L’ingresso add_rule è la parola di 6 bit usata per indirizzare una cella
della memoria Ramrule: durante la fase di carica della Ramrule (set_ram = 0)
add_rule viene fornito esternamente dal blocco Address_Generator, mentre in
fase di running (set_ram = 7) add_rule viene fornito dal blocco
Rule_Add_Gen e corrisponde all’indirizzo di una regola attiva. Un discorso
analogo vale per gli ingressi add_code_0 e add_code_1 che entrano in
ingresso nel bus indirizzi delle memorie Rammf_X0 e Rammf_X1 : in fase di
carica sono forniti dall’Address_Generator mentre in fase di running sono
forniti direttamente dagli ingressi X0 e X1 del processore.
Gli ingressi data_rule e data_mf sono rispettivamente la parola di
memoria di 9 bit per la Ramrule e di 8 bit per le Rammf_X0 e Rammf_X1 :
queste parole vengono fornite già pronte per essere memorizzate all’interno
delle memorie.
L’ingresso set_test permette di portare in output all’uscita test una serie
di segnali interni al blocco Core che normalmente non dovrebbero comparire
in uscita : avere però la possibilità di testare ciò che avviene all’interno del
blocco, ad esempio conoscere ogni parola estratta dalle memorie o i valori dei
singoli α, può essere molto utile in fase di test del chip. Se infatti le uscite non
corrispondono al valore voluto si può in questo modo risalire alle cause
114
dell’errore e rintracciare il blocco incriminato. Le altre due uscite standard del
blocco Core sono zeta e theta rispettivamente codificate con 7 e 4 bit.
Vediamo di capire cosa succede internamente al Core in ogni fase di lavoro.
Durante la fase di carica delle memorie, cioè quando set_ram ha un
valore compreso fra 0 e 2, vengono forniti dall’esterno tutti i segnali di
controllo, gli indirizzi ed i dati. Poiché vi è un unico me in ingresso la logica di
controllo interna provvede ad inviarlo solo alla memoria interessata. Per
descrivere in VHDL questa funzionalità occorre una istruzione processo di
questo tipo:
add _code_0:
add _code_1:
ck:
m e:
alpha0_alpha1:
alpha2_alpha3:
alpha0, alpha1,
alpha2, alpha3:
a, b:
alpha0_ok,
alpha1_ok
theta_ m in,
theta_ prod
theta
add_rule:
data_ out:
set_alpha,
zeta0 :
zeta1:
zeta2:
zeta:
Fig. 4.14 : Fasi di pipeline e temporizzazione interna del blocco Core.
115
setmemorieme : process (me,set_ram)
begin
case set_ram is
when “000” =>
--fase di carica della Ramrule
me0 <= me; me1 <= ‘0’; me2 <= ‘0’;
when “001” =>
--fase di carica di Rammf_X0
me0 <= ‘0’; me1 <= me; me2 <= ‘0’;
when “010” =>
--fase di carica di Rammf_X1
me0 <= ‘0’; me1 <= ‘0’; me <= me;
…
end case
end process setmemoriesme;
me0 è il segnale in ingresso alla Ramrule, me1 va in ingresso alla
Rammf_X0 e me2 alla Rammf_X1. In questo modo durante la fase di carica
delle memorie solo una delle tre è attiva mentre le altre due sono in modalità
stand-by.
La fig. 4.14 può aiutare a capire cosa succede nella fase di running in
quanto riporta il flusso dei dati dall’ingresso alle uscite periodo clock dopo
periodo. Nel primo periodo di clock con un certo ritardo rispetto al fronte di
salita di ck vengono forniti dall’esterno i due indirizzi add_code_0 e
add_code_1 delle look-up table. La lettura dalle due memorie, che avviene in
parallelo, termina circa 8 ns dopo il fronte di salita del me e da questo istante
le parole alpha0_alpha1 e alpha2_alpha3 contenenti i due valori di α per ogni
ingresso si trovano ad un livello stabile. Per sincronizzarsi con il clock sul
fronte di discesa successivo di ck alpha0_alpha1 è scomposto nelle due parole
di 4 bit alpha0 e alpha1, lo stesso per alpha2_alpha3 che viene diviso in
alpha2 e alpha3. In questo istante i quattro gradi di verità α0, α1, α2, α3 sono
noti : il passo successivo consiste nell’accoppiarli a due a due per procedere
con il calcolo del grado di verità della premessa. Sul fronte di salita del clock
immediatamente successivo gli α vengono assegnati due alla volta ai segnali a
e b: in questo primo periodo ad a e b vengono assegnati i valori di α0 e α2, nel
periodo successivo i valori α0 e α3 e così via fino ad ottenere tutte le quattro
combinazioni. Ora queste coppie di valori a e b devono essere inviate in
ingresso ai blocchi Minimum e Alpha_Product_Wallace non prima però di
avere modificato questi valori in base al valore dell’FRPC della regola
corrispondente. L’indirizzo add_rule della prima regola attiva arriva in
ingresso al blocco Core al secondo periodo di clock, un periodo dopo l’arrivo
degli indirizzi add_code_0 e add_code_1. La parola in uscita dalla Ramrule
chiamata data_out è pronta 8 ns dopo il fronte di salita del me e, al successivo
116
fronte di discesa del clock, viene scomposta in due parole distinte: la parola
zeta0 di 7 bit corrispondente al CRC e la parola set_alpha di 2 bit
corrispondente all’FRPC.
Quindi nell’istante in cui sono pronti i valori a b dei gradi di
appartenenza relativi alla prima regola è pronto anche l’FRPC corrispondente,
perciò possono essere prodotti i valori alpha0_ok e alpha1_ok che tengono
conto della presenza o meno di una variabile all’interno della regola. La
generazione di questi valori è schematizzata in VHDL con il seguente
processo:
setalphaok : process
begin
wait until ck’event and ck = ‘1’;
if(set_alpha(1 downto 0) = “00”) then
alpha1_ok <= “0000”;
else
if(set_alpha(1) = ‘0’) then
alpha1_ok <= “1111’;
else
alpha1_ok <= a;
end if;
end if;
if(set_alpha(0) = ‘0’) then
alpha0_ok <= “1111”;
else
alpha0_ok <= b;
end if;
end process setalphaok;
--fronte di salita del clock
--la regola non è presente
--solo X1 non è presente
--solo X0 non è presente
In questo modo dopo 4 periodi di clock dall’inizio della fase di running i
gradi di verità alpha0_ok e alpha1_ok possono entrare nei blocchi di minimo e
di prodotto: al V colpo di clock i due valori theta_min e theta_prod sono
pronti e nel periodo successivo sarà pronto in uscita in valore theta in base al
valore del bit min_prod secondo le modalità definite nel seguente questo
processo:
inference : process
begin
wait until ck’event and ck = ‘1’;
if (min_prod = ‘0’) then
117
--fronte di salita del clock
theta <= theta_min;
--viene scelto il minimo
theta <= theta_prod;
--viene scelto il prodotto
else
end if;
end process inference;
L’uscita theta è valida quindi dal sesto periodo di clock mentre l’uscita
zeta è già pronta dal fronte di discesa del terzo periodo del clock con il nome
zeta0, occorre quindi prevedere una linea di ritardo per zeta0. Per questo
motivo si usa uno shift register con il quale zeta0 viene trasferito a zeta1,
zeta1 a zeta2 e zeta2 a zeta ad ogni fronte di salita del clock in modo che le
uscite theta e zeta siano perfettamente sincronizzate.
Fig. 4.15: Schema circuitale del blocco Core.
L’uscita test può contenere i valori di diversi bus interni a seconda del
valore assunto dall’ingresso set_test : ad esempio se set_test vale 2 in uscita
andrà il valore del bus alpha0_alpha1, mentre se vale 6 l’uscita test conterrà
alpha0_ok.
118
La sintesi del blocco Core deve essere effettuata avendo cura di
preservare i 5 blocchi prodotti a priori : in questo modo lo schematic ottenuto
(vedi fig. 4.15) presenta circa 200 porte logiche elementari oltre i 5 blocchi
complessi generati in precedenza. La simulazione circuitale dell’intero blocco
non dà problemi fino al valore massimo di frequenza di 62.5 MHz ed in ogni
caso il blocco più lento a svolgere il suo compito in 16 ns è
l’Alpha_Product_Wallace: tutti gli altri blocchi funzionerebbero senza
problemi anche a frequenze superiori.
Come ben visibile dalla fig. 4.14 la struttura interna del Core è di tipo
pipeline: due nuovi valori add_code_0 e add_code_1 possono essere presentati
in ingresso al Core ogni 4 periodi clock: una volta riempite le 5 fasi di pipeline
necessarie per dare in uscita i valori voluti di theta e zeta una nuova coppia
di valori è presentata in uscita ad ogni periodo di clock.
4.6 Il blocco di defuzzificazione Consequent
Il blocco Consequent riceve in ingresso i due valori di Z e θ prodotti dal
Core ed esegue la formula di Sugeno di ordine 0 per la composizione delle
varie regole:
4
zeta _ out =
∑ (Θi * Zi)
i =1
4
∑ Θi
(4.3)
i =1
Per questo motivo contiene al proprio interni due blocchi specifici: il
blocco Mult7x4fast esegue in modo veloce la moltiplicazione fra θi e Zi,
mentre il blocco Divider si occupa della divisione finale. I prossimi paragrafi
si occupano di questi due blocchi e della vista Consequent che li gestisce e li
temporizza adeguatamente.
4.6.1 Il blocco per la moltiplicazione Mult7x4fast
Il blocco Mult7x4fast deve eseguire in modo veloce la moltiplicazione
fra i due ingressi theta e zeta. Per questo motivo anche questa moltiplicazione
è implementata tramite l’algoritmo di Wallace che garantisce un ritardo dagli
ingressi all’uscita di 14-15 ns. L’uscita zetatheta prodotta è un numero a 11 bit
che non deve essere troncato, quindi non occorre eseguire la conversione di
scala che era stata necessaria all’interno del blocco Alpha_Product_Wallace.
Questo blocco, una volta sintetizzato, presenta un’area totale di circa
127000 µm2 ed è costituito da 250 porte logiche elementari.
119
4.6.2 Il blocco per la divisione Divider
L’entità del divisore è la seguente:
entity Divider is
port ( num :
in my13;
den :
in my6;
ck :
in std_logic;
godiv :
in
std_logic;
zetaout :
out my7);
D ivid er
13 n um
6 d en
7
zetao
ut
ck
g odiv
end Divider;
Il divisore prende in ingresso le due parole num e den, rispettivamente di
13 e 6 bit, e comincia ad eseguire la divisione dall’istante in cui il bit godiv
passa dal livello logico 0 a 1. Da questo istante in poi il divisore va ad
effettuare un confronto fra il numero costituito dai 7 bit più significativi di
num con il denominatore den: nel caso risulti maggiore il bit più significativo
del quoziente zetaout è posto ad 1 e viene eseguita la differenza fra i 7 bit del
numeratore ed il denominatore, altrimenti il primo bit del quoziente è posto a
0. Ai sei bit ottenuti dalla differenza viene aggiunto il bit più significativo del
numeratore non ancore usato e viene ripetuto lo stesso calcolo fino a quando
non si sono usati tutti i bit del numeratore ed ottenuti tutti i 7 bit del quoziente
zetaout. Il resto ottenuto viene poi confrontato con la metà del denominatore:
nel caso in cui risulti maggiore viene eseguita una approssimazione per
eccesso, cioè zetaout viene incrementata di una unità, altrimenti nessuna
variazione verrà eseguita.
La descrizione VHDL del processo di divisione ricalca esattamente tutti i
passi descritti, perciò è molto semplice sia da scrivere sia da capire. La sintesi
del codice VHDL ha prodotto una rete combinatoria molto grande: contiene
infatti al proprio interno più di 750 celle per un’area totale di quasi 0.5 mm2. Il
tempo totale richiesto per la divisione, esaminato attraverso una serie di
simulazioni su vari valori di test in ingresso con Verilog, richiede circa 45-50
ns. Questo ritardo non costituisce comunque un problema vista l’architettura
generale del chip in quanto la divisione è eseguita in parallelo ai processi di
fuzzificazione e inferenza di un nuovo insieme di valori in ingresso. La cosa
importante è che la divisione sia completa dopo almeno 4 periodi di clock,
cioè 64 ns se si usa un clock alla frequenza di 62.5 MHz ; infatti ogni 4 periodi
120
di clock deve essere eseguita la divisione su una nuova coppia di ingressi num
e den.
4.6.3 Il blocco Consequent vero e proprio
Il blocco Consequent deve gestire la temporizzazione dei due
sottoblocchi Mult7x4fast e Divider ed eseguire le due sommatorie dei θ*Z e
dei θ. Ecco l’entità VHDL del blocco Consequent:
entity Consequent is
port( theta:
in my4;
4
7
zeta:
ck:
std_logic;
stand_by :
std_logic;
goout:
zetaout:
end Consequent;
in my7;
7
in
in
out std_logic;
out my7);
Il blocco Consequent viene attivato dal segnale stand_by: quando questo
si porta a livello logico alto il Consequent comincia ad elaborare gli ingressi,
altrimenti rimane in uno stato di attesa. Quando il blocco viene attivato gli
ingressi theta e zeta sincroni con il clock ck vengono mandati in ingresso al
blocco moltiplicatore per calcolare il prodotto zetatheta. Appena prodotti i
valori zetatheta sono sommati alla variabile sum di 13 bit in modo che dopo 4
periodi di clock sum contenga il valore della sommatoria pronto per essere
mandato in ingresso al divisore. Dopo questi 4 periodi la variabile sum viene
re-inizializzata a 0 in modo che possa ricominciare la somma di 4 nuovi
prodotti. Stesso discorso vale per i theta in ingresso che ad ogni colpo di clock
vengono sommati alla variabile den. Sorge però un problema riguardo la
somma dei zetatheta: l’addizione fra due numeri di 13 bit (sum e zetatheta)
non può essere operata in un unico periodo di clock, infatti il tempo richiesto
per un’operazione simile è superiore ai 20 ns. È necessario perciò suddividere
questa operazione in due fasi di pipeline separate: in questo modo ogni singola
operazione di somma richiede due periodi di clock ma, una volta riempiti
entrambi i blocchi della pipeline, ad ogni colpo di clock si ha una uscita valida
121
e si può così tenere il passo del sommatore dei theta. Vediamo più in concreto
come è possibile scomporre una somma di due numeri di 13 bit in 2 fasi di
pipeline: al primo colpo di clock sono sommati i 6 bit meno significativi di
sum e zetatheta fino a produrre una somma di 6 bit (sum_lsb) ed un eventuale
carry out. Al secondo colpo di clock viene eseguita la somma dei 7 bit più
significativi compreso il carry out della somma precedente fino all’uscita
sum_msb di 7 bit e, contemporaneamente, viene eseguita la somma di sum_lsb
con i bit meno significativi di un nuovo zetatheta da sommare e il risultato è
memorizzato al posto del sum_lsb precedente (il contenuto del precedente
sum_lsb viene passato alla variabile sum_lsb0). Perciò il valore reale della
somma sum ad ogni colpo di clock si ottiene con l’istruzione VHDL:
sum <= sum_msb & sum_lsb0;
Ad ogni istante la somma è data dal valore attuale della somma dei bit
più significativi e dal valore del periodo precedente della somma dei bit meno
significativi: questo perché le due somme corrispondenti sono sfalsate di un
periodo di clock.
Questa è l’unica caratteristica concettualmente complicata del blocco
Consequent: per il resto non appena sono passati 4 periodi clock la logica di
controllo fa commutare il segnale godiv da 0 a 1 in modo che la divisione
possa avere inizio. Dopo altri 4 periodi di clock il segnale di uscita goout si
porta al livello 1 per segnalare che il processo di divisione ha avuto termine: 4
periodi sono sufficienti anche se il processore viene pilotato ad una frequenza
di 62.5 MHz in quanto la divisione richiede al massimo 50 ns.
122
Fig. 4.16: Forme d’onda relative alla simulazione del Consequent.
Il blocco Consequent da solo, senza considerare anche i 2 sottoblocchi, è
costituito da 370 celle per un’area totale di 0.3 mm2. Le forme d’onda della
simulazione con Verilog riportate in fig. 4.16 mostrano il flusso di dati dagli
ingressi alle uscite. Come si vede l’arrivo dei dati theta e zeta in concomitanza
con il frontedi salita del segnale stand_by dà inizio al calcolo di zetatheta e
delle somme sum e den. I segnali clear_num e clear_den azzerano il
numeratore ed il denominatore ogni 4 periodi di clock in modo che le somme
possano sempre ricominciare da 0.
Il numero totale di periodi clock richiesti dall’attivazione del blocco
all’inizio della divisione è 6 di cui 2 sono dovuti alla moltiplicazione e alla
divisione in due blocchi della somma dei zetatheta, mentre gli altri 4 sono
dovuti al numero di valori zeta e theta che devono essere sommati e quindi al
numero di regole attive.
4.7 Il blocco Processor
Il blocco chiamato Processor riguarda il blocco più alto di tutti nella
gerarchia dei livelli: la vista Processor include al proprio interno tutti i blocchi
e sottoblocchi creati in precedenza. Nella descrizione VHDL dell’architettura
tutti i blocchi sono istanziati e collegati fra loro tramite opportuni segnali;
inoltre una opportuna logica di controllo implementata grazie a varie istruzioni
processo gestisce una corretta temporizzazione dei segnali all’interno del
processore.
Partiamo dall’entità della vista Processor:
entity Processor is
port( min_prod: in std_logic;
me_load:
in std_logic;
ck, we:
in std_logic;
reset:
in std_logic;
load_input: in std_logic;
set_ram:
in my3;
set_test:
in my4;
x1, x0:
in my7;
input_ready: out
std_logic;
output_ready:
out
P rocessor
7
3
4
7
7
123
std_logic;
zeta_out:
end Processor;
out my7);
I piedini di ingresso / uscita del processore qui elencati sono 36: i
rimanenti 8 sono tutti pad di alimentazione e di massa. Tutti i segnali di
controllo quali me_load (il memory enable delle memorie), ck, we e reset sono
forniti al processore dall’esterno: compito del processore è temporizzare
adeguatamente questi segnali all’interno in modo che tutti i blocchi si
comportino come voluto. Vediamo ad esempio come il processore riesce a
fornire al blocco Core gli indirizzi ed i dati richiesti per scrivere nelle memorie
RAM durante la fase di carica del processore. Il seguente processo fornisce in
ingresso al Core le parole data_rule e data_mf che saranno poi scritte nelle
memorie:
data_template : process
begin
wait until me_load’event and me_load=’1’;
--fronte di salita
if(set_ram >= “000” and set_ram <= “010”) then
data_rule <= x1 & x0(6 downto 5);
data_mf <= x0 & x1(0);
else
data_rule <= “000000000”;
data_rule <= “00000000”;
end if;
end process data_template;
Le parole con cui caricare le memorie vengono prese dagli ingressi x0 e
x1: in particolare la parola di memoria Ramrule è costituita dai 7 bit di x1 ed i
due bit più significativi di x0, mentre la parola della Rammf_X0 o Rammf_X1
è costituita da x0 e dal bit meno significativo di x1. In questo modo la fase di
carica delle memorie viene effettuata in parallelo, cioè in ogni periodo del
me_load viene memorizzata una parola intera di memoria. Il seguente processo
dovrà poi fornire al Core gli indirizzi corretti add_rule, add_code_0 e
add_code_1:
address_template: process(set_ram, address_loading)
begin
case set_ram is
--fase di carica della Ramrule
when “000” =>
124
add_rule <= address_loading(5 downto 0);
add_code_0 <= “0000000”;
add_code_1 <= “0000000”;
--fase di carica della Rammf_X0
when “001” =>
add_rule <= “000000”;
add_code_0 <= address_loading;
add_code_1 <= “0000000”;
--fase di carica della Rammf_X1
when “010” =>
add_rule <= “000000”;
add_code_0 <= “000000”;
add_code_1 <= address_loading;
…
end process address template;
A seconda del valore assunto dal bus set_ram l’indirizzo
address_loading di 7 bit prodotto dall’Address_Generator viene inviato o ad
add_rule (tranne il bit più significativo visto che add_rule è di 6 bit) o ai
segnali add_code_0 e add_code_1. Ricevuti in questo modo dati e indirizzi la
fase di carica delle memorie può avvenire correttamente all’interno del Core.
125
Fig. 4.17: Forme d’onda relative alla simulazione della vista Processor.
In fase di running (set_ram = 7) gli ingressi x0 e x1 devono essere
caricati all’interno del processore e inviati contemporaneamente al selettore
degli intervalli Int_Selector e al Core come indirizzi delle due look-up table.
Durante questa fase gli ingressi entrano nel processore solo quando il segnale
load_input transita da 0 a 1 come descritto nel seguente processo:
data_input : process
begin
wait until load_input’event and load_input=’1’;
x0_local0 <= x0; x1_local0 <= x1;
end process data_input;
126
--fronte di salita
I segnali interni x0_local0 e x1_local1 di 7 bit vengono poi posti
direttamente in ingresso ai blocchi Int_Selector e Core dove vengono elaborati.
Per avere una visione più completa del flusso dei dati all’interno del
processore occorre dare un’occhiata alle forme d’onda riportate in fig. 4.17
ottenute dalla simulazione circuitale con Verilog dell’intero blocco Processor.
Gli ingressi x0 e x1 vengono caricati all’interno del processore sul fronte
di salita del segnale load_input e vengono inviati direttamente in ingresso
all’Interval Selector che genera i codici dei due intervalli selezionati intout0 e
intout1. Al colpo di clock successivo un picco sull’enable del Rule_Add_Gen
abilita il blocco a generare gli indirizzi delle 4 regole attive rule_address, una
per ogni periodo di clock, che sono inviati subito in ingresso al Core con il
nome di add_rule. Nel frattempo gli indirizzi add_code_0 e add_code_1 sono
disponibili al Core già da un periodo di clock visto che provengono dagli
ingressi x0 e x1.
Il blocco Core impiega 5 periodi di clock per fornire in uscita la coppia
di valori zeta e theta. Quando ciò avviene il blocco Consequent viene attivato
dal segnale stand_by: due periodi di clock dopo sono pronte le prime somme e
dopo 6 periodi le prime sommatorie complete Σθ*Z e Σθ. In questo istante i
due valori ottenuti vengono inviati al blocco Divider e viene attivata l’uscita
input_ready per avvertire il dispositivo esterno che due nuovi ingressi possono
essere caricati all’interno del processore. Un picco sull’uscita output_ready
poi segnala al dispositivo esterno che l’uscita zeta_out è pronta.
Il numero totale di periodi clock che passano dall’ingresso di x0 e x1 al
momento in cui le due sommatorie sono concluse è di 12, di cui 8 sono dovuti
alle fasi di pipeline in cui il flusso di dati è stato suddiviso e 4 sono dovuti al
numero di regole attive da processare. A questi tempi va poi aggiunto il tempo
dovuto alla divisione che è di circa 50 ns. Il tempo globale di elaborazione di
una coppia di ingressi x0 e x1 si compone quindi nel seguente modo:
• il tempo dovuto al numero reale di fasi di pipeline che è 8 per un tempo
totale di 8 x 16 ns = 128 ns;
• il tempo dovuto al numero di regole attive da processare che è 4 per un
ritardo totale di 4 x 16 ns = 64 ns;
• il tempo dovuto alla divisione che è di circa 50 ns.
Il tempo globale di elaborazione è quindi di 240 ns se si usa un clock alla
frequenza massima di 62.5 MHz. Ciononostante data la struttura interna di tipo
pipeline il numero veramente importante è la frequenza con cui si possono
presentare due nuovi ingressi : nel caso di HEPE97 due nuovi ingressi possono
essere caricati ogni 4 periodi clock e quindi ogni 64 ns.
127
La vista schematic ottenuta dalla sintesi del codice VHDL (vedi fig. 4.18)
ha prodotto una rete costituita da 223 celle per un’area totale di 0.2 mm2 senza
considerare ovviamente tutti i blocchi e i sottoblocchi inclusi all’interno.
Questa area di 0.2 mm2 costituisce in pratica la logica di controllo che governa
il funzionamento di tutto il chip. Le simulazioni effettuate non danno alcun
problema fino alla frequenza di lavoro di 62.5 MHz. Oltre questa frequenza il
mancato funzionamento di alcuni blocchi compromette irrimediabilmente il
funzionamento dell’intera rete.
L’ultima simulazione effettuata sulla vista schematic del blocco
Processor è stata effettuata con Verifault con lo scopo di avere un set di dati in
ingresso capace di rivelare una percentuale abbastanza alta di difetti posti sullo
schematic da Verifault. Ponendo in ingresso tutti le possibili combinazioni
degli ingressi x0 e x1 (e le combinazioni possibili sono molte cioè 1282 =
16384) il numero di difetti rivelati è prossimo all’80 % e quindi rappresenta un
valore accettabile.
4.8 Dallo schematic al layout
Terminata la vista schematic del processore e le relative simulazioni si
può cominciare a pensare alla realizzazione del layout del processore, cioè alla
sua implementazione fisica su silicio. Il primo passo consiste nell’inserire i
pad di I/O negli ingressi e nelle uscite della vista schematic del processore: per
quasi tutti gli ingressi, tranne per il clock ed il reset, è stato usato il pad IPS8B
di tipo CMOS non invertente che è uno dei pad più comuni e usati
normalmente. Per l’ingresso reset è stato adottato il pad IPS4E di tipo CMOS
non invertente dotato di un trigger di Schmitt. Per il clock va fatto un discorso
a parte: poiché il clock deve pilotare contemporaneamente tutte le componenti
sincrone del microprocessore, cioè tutti i flip-flop ed i registri, presenti nel
progetto in un numero molto alto (qualche centinaia), il carico capacitivo da
pilotare è il più alto di qualsiasi altro segnale, se si escludono i segnali di
alimentazione e di massa che per ovvie ragioni devono pilotare tutte le
standard cell e le macrocelle. Il pad di ingresso del clock deve avere un fanout
tale da sopportare questo carico che è di circa 10 pF. Per questo motivo ho
scelto per il pad del clock un normale pad d’ingresso IPS8B seguito
direttamente dalla cella inverter INV8 che possiede un fanout di 11 pF. Un
altro punto molto interessante che riguarda il clock è la conformazione fisica
della pista che connette tutte le parti sincrone del chip: tale conformazione
deve essere tale da minimizzare gli skew, cioè le differenze di tempo con cui il
segnale del clock arriva a fare commutare ogni singola porta.
128
Fig. 4.18: Vista schematic del blocco Processor.
Per tutte le uscite del processore ho adottato il pad OPR2T, un pad di
tipo CMOS non invertente abastanza comune. La caratteristica principale di
questo pad è il suo fanout che è di circa 100 pF.
Rimane il problema della scelta del numero dei pad di alimentazione e di
massa del nucleo e della periferia. La scelta del numero di pad di
alimentazione e di massa del core, VDD_Core e GND_Core, è vincolata a
quanta corrente assorbono le standard cell e le megacelle all’interno del chip.
Occorre quindi fare dei calcoli sul consumo del chip prima di potere fare delle
scelte ragionate.
La memoria Ramrule assorbe 0.64 mW / MHz, mentre le memorie
Rammf_X0 e Rammf_X1 assorbono ciascuna 0.96 mW / MHz. Per questo il
consumo totale dovuto alle memorie è di:
0.64 + 2 * 0.96 = 2.56 mW / MHz
129
A 62.5 MHz la potenza assorbita dalle memorie è dunque di 0.16 W.
Ogni standard cell consuma in media 7 µW / MHz e quindi 440 µW alla
frequenza di 62.5 MHz. Poiché il chip contiene circa 3470 celle il consumo
totale dovuto alle standard cell è di 1.53 W. Questo valore si riferisce però ad
una situazione in cui tutte le 3470 celle commutino ad ogni periodo di clock
ma questa è una situazione poco realistica. Supponendo che ad ogni istante
solo il 50 % delle celle siano coinvolte e di queste solo il 50 % commutino la
potenza assorbita dalle standard cell si riduce ad un quarto cioè a 0.38 W.
Perciò il consumo totale è:
0.16 + 0.38 = 0.54 W
Poiché il chip è alimentato con una tensione di riferimento di 5 V la
corrente assorbita è:
corrente =
0.54W
= 108mA
5V
(4.4)
La corrente che i pad di alimentazione devono fornire e i pad di massa
devono assorbire è di 108 mA: a tale scopo potrebbe bastare un’unica coppia
di pad VDD_Core e GND_Core ma per sicurezza ne sono state inserite due
coppie.
Per quanto riguarda la scelta del numero di pad VDD_Pery e GND_Pery
questa è vincolata al numero totale di pad presenti: ogni coppia di pad di
alimentazione e di massa della periferia dovrebbe garantire un perfetto
funzionamento di circa 30 pad di I/O. Nel nostro caso ho scelto due coppie di
alimentazione e di massa anche per la periferia : il numero totale di pad del
chip diventa quindi di 44 e ciò va molto bene perché esiste un package di tipo
JLCC esattamente a 44 piedini. In questo modo non verrà sprecato nemmeno
un piedino di I/O del package utilizzato. La fig. 4.19 riporta la disposizione di
tutti i pad di I / O attorno al nucleo del processore.
4.9 Fase di placement e di routing
Una volta decisa la disposizione dei piedini si può cominciare a pensare a
come posizionare nello spazio interno le 3 megacelle e le 3470 standard cell
contenute nel processore. La cosa migliore per quanto riguarda la distribuzione
delle standard cell è suddividerne il numero totale in più sottoblocchi in modo
tale che una loro distribuzione risulti molto più semplice. In questo modo
risulta poi anche più agevole progettare le piste del clock, dell’alimentazione e
130
S et_ Test(2 )
S et_ test(3 )
S et_ R am (0 )
V D D _ C o re2
S et_ R am (1 )
Ck
S et_ R am (2 )
L o ad _ In p u t
We
M in _ P ro d
V D D _ C o re1
della massa con una struttura ad albero o ad H. Sempre in fig.
4.19
R am ru le
C on seq uent
A ddress
V D D _ P ery 2
Z eta_ O u t(0 )
Z eta_ O u t(1 )
Z eta_ O u t(2 )
Z eta_ O u t(3 )
Z eta_ O u t(4 )
M e_ L o ad
Z eta_ O u t(5 )
Z eta_ O u t(6 )
O u tp u t_ R ead y
G N D _ P ery 2
R eset
G N D _ C o re2
X 0 (2 )
X 0 (1 )
X 0 (0 )
In p u t_ R ead y
R am m f
X0
R am m f
X1
C o re
G N D _ C o re1
X 0 (6 )
X 0 (5 )
X 0 (4 )
X 0 (3 )
S et_ Test(1 )
S et_ Test(0 )
V D D _ P ery 1
X 1 (6 )
X 1 (5 )
X 1 (4 )
X 1 (3 )
X 1 (2 )
X 1 (1 )
G N D _ P ery 1
X 1 (0 )
Fig. 4.19: Disposizione dei pad di I/O e geometria del layout.
sono riportati i 3 blocchi in cui sono state divise le oltre 3000 standard cell :
il
blocco Core, il blocco Consequent ed il blocco Address. Questi sono blocchi
fisici, cioè contengono al loro interno diverse file di standard cell
rappresentate nella loro vista abstract, la vista che viene usata nel layout in
quanto tiene conto delle dimensioni fisiche delle celle. Ad esempio una porta
logica NOT nella vista abstract non viene rappresentata come un triangolo
seguito da un pallino ma come un rettangolo lungo 7.2 µm e alto 38 µm. Il
blocco Core contiene al proprio interno tutte le standard cell dei blocchi logici
Core (escluse le memorie) e Rule_Add_Gen disposti su 14 righe, il blocco
131
Consequent contiene i blocchi logici Consequent e la logica di controllo del
blocco Processor su 20 righe e il blocco Address contiene i sottoblocchi
Address_Generator e Int_Selector su 13 righe. Le tre memorie RAM sono state
disposte come in fig. 4.19: le due memorie Rammf_X0 e Rammf_X1 una
accanto all’altra in basso a sinistra mentre la Ramrule disposta in orizzontale
in altro a sinistra. Alla luce di questa disposizione può ora risultare chiaro
perché fra le configurazioni possibili della megacella Ramrule (vedi paragrafo
2.4) sia stata scelta quella con le dimensioni laterali di 0.903 x 0.391 mm.
Questa infatti è quella che si adatta meglio allo spazio “rimasto” in alto a
sinistra una volta che sono stati disposti tutti gli altri blocchi.
La suddivisione in questi blocchi è stata studiata con cura soprattutto per
ottimizzare la pista del clock: il pad del clock si trova in alto al centro e la
pista a cui dà origine deve alimentare tutte le componenti sincrone del
processore. Per questo motivo tutti blocchi logici pilotati dal clock sono stati
messi nei blocchi fisici Core e Consequent che si trovano più vicini al pad del
clock, mentre nessuna standard cell del blocco Address viene pilotata dal
clock. La pista del clock che si dirama ad albero dal pad del clock verso tutti i
flip-flop è quindi più compatta e gli skew risultanti, cioè le differenze di tempo
in cui il segnale del clock giunge in ingresso ad ogni flip-flop, dovrebbero
essere minimizzati. Secondo le direttive della fonderia ES2 una pista del clock
larga 3 µm è sufficiente per pilotare un carico capacitivo di 10 pF fino alla
frequenza di lavoro di 62.5 MHz senza che si abbiano dei problemi.
Passiamo alle piste di alimentazione e di massa: la pista di alimentazione
parte dai due pad VDD_Core che si trovano in alto e si divarica in due rami
esterni da cui raggiunge l’inizio di ogni riga di standard cell e tutte e tre le
memorie. È sufficiente che la pista di alimentazione raggiunga la prima cella
di una riga affinché vengano effettuati anche i collegamenti di tutte le altre
celle. Allo stesso modo la pista di massa parte dai due pad GND_Core che si
trovano in basso e si collega a tutte le celle e megacelle attraverso il solo
canale centrale.
Una questione tecnica molto importante riguarda la larghezza che devono
avere le piste VDD e GND. È ovvio che il dimensionamento di queste piste
dipende dalla corrente assorbita dal chip, dallo strato metallico che esegue le
connessioni (metallo1 o metallo2), dalla temperatura, ecc. Per compiere questo
calcolo la fonderia ES2 fornisce le seguenti formule:
metallo1: corrente = 1.1 * (w - 0.16) (mA) a 100°C
metallo2: corrente = 1.3 * (w - 0.16) (mA) a 100°C
132
(4.4)
dove w è la larghezza della pista in µm. Invertendo queste formule si ricava
che, in base ad un consumo di corrente di 108 mA, se la pista di alimentazione
è realizzata con lo strato metallo1 la larghezza deve essere di 98 µm, mentre
con lo strato metallo2 la larghezza dovrebbe essere di 83 µm. Poiché il routing
dettagliato provvede a gettare le piste di alimentazione e di massa con lo strato
metallo1, cioè quello dei 2 che si trova a livello inferiore, la dimensione della
larghezza di tali piste deve essere di almeno 100 µm.
R am ru le
R a m ru le
C o n se q u en t
C o n s eq u e n t
: VDD
A d d r es s
: GND
Ram m f
X1
Ram m f
X0
C o re
Ram m f
X1
Ram m f
X0
C o re
A d d res s
: CLOCK
(a)
(b )
Fig. 4.20 : Disposizione delle piste di alimentazione e di massa (a) e della
pista del clock (b).
Un altro particolare tecnico interessante riguarda l’alimentazione delle
memorie RAM. Ognuna di queste memorie è dotata di due ingressi per
l’alimentazione e due per la massa: in questo modo una memoria può avere
una alimentazione doppia oppure singola. Il progettista deve sapere decidere se
può bastare una singola alimentazione oppure no. Vediamo il caso della
memoria Ramrule: essa assorbe 40 mW alla frequenza di lavoro, perciò
assorbe una corrente di 8 mA; secondo le formule (4.4) la larghezza del
collegamento metallo1 deve essere almeno di 7 µm, mentre la larghezza del
collegamento metallo2 deve essere almeno di 6 µm. Questo significa che se
l’alimentazione della memoria è singola il collegamento deve essere largo
almeno 7 µm (metallo1) o 6 µm (metallo2); se invece l’alimentazione è doppia
la dimensione dei singoli collegamenti può anche ridursi alla metà. Poiché nel
133
chip HEPE97 la larghezza di tali collegamenti è 12 µm ognuna delle memorie
è alimentata solo da una parte.
La disposizione delle piste di alimentazione e di massa che partono dai
pad VDD_Pery e GND_Pery viene svolta in modo completamente automatico
dal programma Cell Ensemble: in ogni caso queste piste si dispongono ad
anello attorno al chip passando attraverso tutti i pad di I / O.
La disposizione finale delle piste di alimentazione, di massa e del clock è
riportata in fig. 4.20, mentre la fig. 4.21 riporta un immagine del layout finale
come visualizzata da Cadence al termine della fase del routing dettagliato.
Fig. 4.21 : Layout finale del processore HEPE97 come visualizzato con
Cadence.
Sul layout ottenuto sono state fatte tutte le verifiche DRC, ERC e LVS
per verificare che la conversione da schematic a layout fosse avvenuta in modo
corretto e nel rispetto dei parametri tecnologici forniti dalla fonderia ES2.
Inoltre le successive simulazioni post-layout con Verilog e Veritime,
tenendo conto delle capacità parassite introdotte, non hanno mai dato né il
minimo problema di timing check né di overload. Il layout così ottenuto ha
134
quindi superato tutte le prove ed è stato quindi considerato pronto per essere
finalmente realizzato su silicio. L’intero progetto verrà spedito alla fonderia
ES2 nei prossimi giorni e si stima che verrà realizzato completamente entro la
fine di Settembre di quest’anno.
135