Fino ad adesso abbiamo usato segnali di tipo bit che possono propagare solo due valori '0' e '1'. E' circuitalmente scorretto in un progetto reale collegare ad una stessa linea piu' dispositivi. Se si fa una cosa del genere:
tutte le volte che una porta da' in uscita '1' e l' altra da' '0', sulla linea che collega le due uscite, scorrera' una corrente eccessiva. La tensione della linea poi si portera' ad un valore intermedio tra alto e basso e non esprimera' un valore logico ben definito. In VHDL semplicemente non si possono collegare due uscite di tipo bit o bit_vector ad una stessa linea.
Se pero' pensiamo al bus del nostro PC, su cui colleghiamo i vari banchi di memoria, dobbiamo ammettere che qualcosa di diverso deve pur accadere. Infatti ciascun banco di memoria manda i dati sfruttando le stesse piste (bus) della motherboard. Quindi le uscite dei banchi di memoria sono collegate tutte allo stesso segnale. In generale ogni dispositivo progettato per interfacciarsi con un bus deve essere in grado di imporre '1' o '0' sul bus quando e' il suo turno ma deve anche essere in grado di scollegarsi per permettere ad altri dispositivi attaccati al bus di imporre '0' o '1'. Questo si ottiene collegandosi al bus con un dispositivo a tre stati. Nell' elettronica digitale, quindi, si incontrano spesso dei dispositivi, detti a tre stati, la cui uscita si comporta in questo modo. A seconda dei casi il dispositivo pilota il pin dell' uscita portando l' uscita o a una tensione alta (livello logico '1') o a una tensione bassa (livello logico '0') oppure non pilota affatto il pin che risulta staccato, come se fosse stato tagliato via dal dispositivo. La linea collegata a questo pin risulta staccata e ogni altro dispositivo attaccato a questa linea puo' imporre un valore sulla linea stessa. Un dispositivo che funziona in questo modo si dice a tre stati. Un dispositivo a tre stati fornisce in uscita '0', '1' e un' altro valore detto ad alta impedenza. Se tra i dispositivi attaccati al bus uno solo impone il valore logico e gli altri sono ad alta impedenza non vi saranno problemi. Se nessuno impone un valore il bus stesso sara' ad alta impedenza. Se piu' d' uno pretende di imporre un valore logico avremo una situazione d' errore.
In VHDL tutto questo si modella dicendo che il segnale di uscita di un tre stati e' di tipo std_logic. I segnali std_logic possono assumere valori '0', '1' e 'Z' o alta impedenza (vi sono anche altri valori come 'X' detto unknown che denota una situazione di errore). Questi valori in caso di conflitto si compongono secondo regole precise che dicono come risolvere il conflitto. Per questa ragione il tipo std_logic, nella terminologia VHDL, e' detto resolved type. Senza entrare nei dettagli dei resolved type VHDL possiamo dire che un tipo resolved si definisce dando l' insieme dei possibili valori e una tabella per risolvere i conflitti tra valori di quel tipo. Nel caso del tipo std_logic la tabella (parziale) di risoluzione dei conflitti e' la seguente:
0 | 1 | Z | X | |
X | X | X | X | X |
0 | 0 | X | 0 | X |
1 | X | 1 | 1 | X |
Z | 0 | 1 | Z | X |
La definizione del tipo std_logic e' contenuta nel package std_logic_1164 che si trova nella libreria IEEE.O. Bisognera' includere questa libreria tutte le volte che si usino dei segnali di tipo std_logic. In questa libreria sono anche definiti i normali operatori che estendono quelli che abbiamo visto per i segnali di tipo bit. L' estensione e' fatta imponendo una uscita erronea ('X') tutte le volte che un operando e' ad alta impedenza ('Z') o e' erroneo ('X'). Fa eccezione l' or che da' come risultato '1' quando un suo operando e' a '1' qualunque sia il valore dell' altro operando. I tipi bit e std_logic non sono affatto compatibili tra di loro per cui per passare da uno all' altro sono previste delle funzioni di conversione:
function To_bit(s:std_ulogic; xmap:bit:='0') return bit; function To_StdULogic(b:bit)return std_ulogic;
Nella funzione To_bit il parametro xmap, e' il valore fissato per la conversione di tutti i valori diversi da '0' e '1'. Se xmap non viene fornito, viene preso il valore di default '0' e tutto quello diverso da '1' sara' messo a '0' (per una certa impostazione del VHDL nel package IEEE 1164 si trova talvolta std_ulogic anziche' std_logic ma per i nostri scopi e' meglio sempre usare std_logic). Vediamo di riepilogare quanto fin qui visto con un esercizio:
Usando la libreria IEEE.O si e' realizzato un driver di linea three-state monodirezionale che ha come interfaccia:
entity ts_driver is generic(delay:time); port (CE: bit;in_b: bit; out_bz: out std_logic ); end ts_driver;
Questo dispositivo, a livello circuitale, e' costituito da un interruttore posto tra l' ingresso in_b di tipo bit e l' uscita out_bz di tipo std_logic. Un altro ingresso CE comanda l' interruttore. Quando CE=1 viene stabilito il collegamento e in_b sara' collegato a out_bz. Quando CE=0 invece la linea di uscita e' portata allo stato di alta impedenza. Tutte le commutazioni, sia quelle causate da CE, sia quelle causate da variazioni su in_b, si propagano all' uscita con un ritardo delay.
Purtroppo il GMVHDL non permette di simulare una entita' le cui porte, di ingresso e/o di uscita siano di tipo resolved come e' il tipo std_logic, pertanto, come esercizio, si costruisca quindi una entita' con interfaccia:
entity ts_tst is port(ts_tst_in:bit;cs:bit; ts_tst_out:out bit); end ts_tst;
che colleghi ts_tst_in a in_b, ts_tst_out a out_bz e includa ts_driver fissando il suo ritardo a 1ns. Si simuli il comportamento di questa entita'. [soluzione e test 006]
Come gia' visto per i bit_vector anche per i tipi std_logic e' possibile definire una sequenza di valori di tipo std_logic che avranno tipo std_logic_vector. L' elencazione di tutte le proprieta' del tipo std_logic_vector e' tanto noiosa quanto inutile e si rimanda il lettore al package IEEE 1164. Anche per gli oggetti di tipo std_logic_vector sono disponibili in IEEE.O le operazioni normali logiche su tutto il vettore, bit per bit. Sono poi disponibili le funzioni di conversione:
function To_bitvector(s:std_logic_vector;xmap:bit:='0')return bit_vector; function To_StdLogicVector(b:bit_vector)return std_logic_vector;
Per il lettore che abbia qualche dubbio sul tipo std_logic_vector proponiamo un esercizio che ci permette di presentare un altro componente predefinito, il driver three-state ad n linee.
Utilizzando il driver generico a n linee:
entity ts_driver is generic(n:positive; delay:time); port (CE: bit;in_bus:bit_vector(n-1 downto 0); out_bus:out std_logic_vector(n-1 downto 0)); end ts_driver;
Si sviluppi una entita' con interfaccia:
entity ts_tst is port(ts_tst_in:bit_vector(6 downto 0);cs:bit;ts_tst_out:out bit_vector(6 downto 0)); end ts_tst;
che incapsuli il driver collegando ts_tst_in a in_bus e ts_tst_out a out_bus imponendo un ritardo delay nullo [soluzione e test 007]
Per concludere presentiamo un componente di media complessita' che puo' permetterci di costruire architetture di un qualche interesse assemblando tutti i componenti fin qui presentati. Il modello dettagliato di seguito e' quello di una RAM parametrizzata sulle linee di indirizzo (addr_lines) e sulle linee di uscita (word_size). Altri parametri generici sono il tempo di ritardo con cui i dati vengono forniti in lettura (delay) e il tempo (turn_off_delay) che impiega la RAM a sconnettersi dal bus dei dati (data). Piu' precisamente diremo che:
L' interfaccia della nostra entita' sara' dunque la seguente:
entity ram_mem is generic(delay,turn_off_delay:time; addr_lines:positive; word_size:positive; ramfile:string; port(addr: std_logic_vector(addr_lines-1 downto 0) ;CS,RnW:bit; data:inout std_logic_vector(word_size-1 downto 0) bus); end ram_mem;
Come si vede e' presente anche un parametro generico di tipo string (deve essere una stringa tra doppi apici) che da il nome del file in cui si trova il contenuto iniziale della RAM. Questo file viene letto quando parte la simulazione. I valori contenuti nel file vengono trasferiti nella RAM. Vedremo tra un attimo come scrivere questo tipo di file di inizializzazione.
Si puo' anche descrivere il funzionamento di questa RAM dicendo che vi sono tre stati fondamentali per la RAM La RAM puo' trovarsi infatti o in un ciclo di lettura o in un ciclo di scrittura o essere in posizione di attesa (standby). Vediamo le peculiarita' di ciascun stato:
Il contenuto della RAM viene caricato all' inizializzazione dal file il cui nome e' dato dalla stringa ramfile. Ogni accesso alla RAM durante questa fase di caricamento viene registrato nel file debug.out. Anvhe ogni altro accesso alla RAM prodotto tanto in lettura che in scrittura, viene registrato su tale file. Ogni accesso produce una linea del tipo:
N BBBBBBBBBBBBBBBBBBBBBBBB [L|R|W]
Dove N e' un numero in base 10 maggiore o uguale a 0 che indica la locazione della RAM a cui si riferisce l' operazione. La stringa di bit BBBBBBBBBBBBB indica il dato in ingresso o uscita. La lettera finale L o R o W indica il tipo di accesso L (loading) per gli accessi in fase di inizializzazione, R per la lettura e W per la scrittura. Se vengono istanziate piu' copie di questa entita' i tracciati dei vari accessi finiranno tutti in debug.out e risulteranno quindi mischiati. Questo, molto spesso, e' piu' utile di avere tracciati separati. Il file debug.out conterra' infatti la storia di tutti gli accessi a tutte le memorie dando informazioni sull' ordinamento temporale reciproco dei vari accessi.
Le word da caricare in memoria sono date usando un file ASCII con una parola per ogni linea. Tali linee hanno il seguente formato:
N BBBBBBBBBBBBB...BBBBBBBBBBB
N e' un numero in base 10 maggiore o uguale a 0 che indica la locazione in cui collocare i bit che seguono. Si assume che N non sia troppo grande, rispetto alla memoria che stiamo usando e che il numero di bit sia quello esatto. Se questo non accade i risultati sono impredicibili. L' utente puo aggiungere una stringa di commento dopo aver completato la stringa di bit. La stringa di bit BBBBB..BBBB finisce nella locazione indicata da N con i bit che vengono caricati leggendoli da sinistra a destra con il piu' significativo a sinistra Se piu' righe hanno lo stesso N l' ultima riga (che si legge) determinera' il contenuto della locazione N. Nel file non e' detto che le varie righe debbano avere N ordinato in qualche modo speciale. E' possibile avere righe con N negativo ma esse vengono scartate. Se in una riga N e' negativo il contenuto della riga viene ignorato, quindi righe con N negativo possono essere usate per inserire commenti Si consiglia, per motivi di bufferizzazione dell' input, di terminare il file con la linea: -1.
Si noti che:
Il modello proposto si trova nel file RAM_____.O che a sua volta utilizza le librerie in IEEE.O e RAM_PKG.O. Quest' ultima a sua volta utilizza la libreria in DEBUG_PK.O e in VECTOR.O. Per compilare la RAM bisogna dunque compilare prima tutte queste librerie partendo dalle ultime. Vediamo ora il funzionamento della RAM con un esempio semplice semplice.
Come abbiamo gia' visto e' impossibile, nel nostro simulatore GMVHDL, simulare una entita', come la ram_mem, avente dei segnali di tipo std_logic nell' interfaccia. Scopo di questo semplice esercizio e' quello di costruire una entita' che, istanziando l' entita' ram_mem, produca l' entita':
entity ram_tst is port(ram_tst_addr:bit_vector(6 downto 0);ce,rd:bit;ram_tst_data:in bit_vector(15 downto 0)); end ram_tst;
che implementa una memoria di 128 parole di 16 bit. Decidiamo di fissare il ritardo in lettura a 10ns ed il tempo di spegnimento della RAM (turn_off_delay) a 2ns [soluzione e test 001].