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