Fin qui abbiamo esaminato l' approccio data flow che permette di codificare una rete combinatoria una volta nota la relazione tra ingressi e uscite. In generale, data una rete combinatoria qualsiasi, la si puo' sempre codificare con una architettura data-flow a patto di conoscere l' espressione delle relazioni di input-output che la regolano. Se consideriamo la generica rete combinatoria RC con n ingessi ed m uscite per cui conosciamo l'espressione delle relazioni di ingresso-uscita U1=F1(I1,...,In); ... Um=Fm(I1,...,In); possiamo ottenere un modello VHDL con una procedura standard che vediamo subito
Un modello di una generica rete sequenziale si ottiene semplicemente adattando il codice VHDL:
entity RC is port(i1,...,in:in bit;u1,...,um:out bit); end RC; architecture RC_df of RC is begin u1<=F1(i1,..,in);....;um<=Fm(i1,..,in); end RC_df;
dove gli spazi bianchi, i puntini di sospensione e i simboli F1 ed Fn vanno sostituiti con le opportune espressioni che variano a secondo della rete combinatoria. Se poi si vuole ottenere un modello che simuli anche il fatto che l'uscita u1 risente delle variazioni negli ingressi con un ritardo r1, l'uscita u2 si modifica con ritardo r2 e cosi' via, potremo descrivere la nostra rete con una architettura del tipo:
architecture RC_df_ritardata of RC is begin u1<=F1(i1,..,in)after r1;...;um<=Fm(i1,..,in)after rm; end RC_df_ritardata;
dove r1 e rm saranno le opportune espressioni di tempo.
Come esempio consideriamo la rete combinatoria vista in precedenza:
L' architettura di questo demultiplexer secondo questo schema e' data dal frammento VHDL:
architecturedmpx_df of dmpx is begin u00<=(not c0) and i0;u01<=(not c0) and i1;u10<=c0 and i0;u11<=c0 and i1; end dmpx_df;
Esercizio:
Si scriva in VHDL, usando solo AND, OR, e NOT, un modello per una rete combinatoria compare3 con sei ingressi A0,A1,A2 e B0,B1,B2 che dia in uscita un bit U che sara' a 1 se e solo se risultano ordinatamente uguali i tre bit A0,A1,A2 ai tre bit B0,B1,B2 ovvero se A0=B0 e A1=B1 e A2=B2. Si risolva l' esercizio scrivendo la forma normale disgiuntiva per U, ovvero si scriva U come OR di AND presi mattendo in AND o gli ingressi o le loro negazioni. Per costruire tale forma non costruire tutta la tavola di verita per U (sono 64 righe) ma trovare solo le righe della tavola di verita' per cui U e' 1. [soluzione e simulazione]
La maggior parte delle reti combinatorie di una qualche utilita' prendono in ingresso molti bit e producono molte uscite. Per questa ragione risulterebbe un po' difficile e lungo scrivere i modelli VHDL con gli strumenti visti fino ad ora. Ad esempio se volessimo scrivere il modello per un demultiplexer con un bit di selezione ma otto bit in ingresso ci troveremmo con 16 bit in uscita.
e quindi per costruire un modello in VHDL dovremmo scrivere l' interfaccia:
entitydmpx_x8 is port(i0,i1,i2,i3,i4,i5,i6,i7,c0:in bit; u00,u01,u02,u03,u04,u05,u06,u07,u10,u11,u12,u13,u14,u15,u16,u17:out bit); end dmpx_x8;
Sarebbe quindi interessante poter descrivere i segnali i,u0,u1 come delle collezioni di 8 bit. In VHDL e' effettivamente possibile dichiarare segnali multipli o di tipo array atti a propagare una collezione di valori tutti dello stesso tipo. In particolare in VHDL e' predefinito il tipo di array di bit o bit_vector. Possiamo definire segnali di tipo bit_vector per registrare sequenze di bit. Ad es. un byte puo' essere propagato con il segnale :
signalbyte_out:bit_vector(7 downto 0);
quindi possiamo scrivere l' interfaccia del nostro demultiplexer in maniera piu' compatta e ordinata come:
entitydmpx_x8 is port(i:in bit_vector(7 downto 0);c0:in bit;u0,u1:out bit_vector(7 downto 0)); end dmpx_x8;
Per i segnali di tipo bit_vector sono possibili tutta le operazioni che sono possibili tra i bit. Tali operazioni verranno eseguite bit a bit. Inoltre sono previste anche delle operazioni numeriche che troverete definite nel package contenuto nella libreria vector.vhd.
Per definire un bit_vector bisogna dire quanti elementi vi sono in quel bit_vector ed in quale ordine vanno presi. Le possibili definizioni sono:
nome: bit_vector(limite-inferiore to limite-superiore); nome: bit_vector(limite-superiore downto limite-inferiore);
Il vettore dichiarato con il to avra' nella prima locazione l' elemento di indice piu' basso. Viceversa se la dichiarazine e' fatta con il downto il primo elemento e' l' elemento del vettore con indice piu' alto. Spesso scambiare to con downto porta ad errori difficili da scovare. Per evitare problemi basterebbe scegliersi uno stile e non mischiare i due tipi di definizione. Tuttavia to e downto non hanno, per noi, realmente pari dignita'. Infatti il sistema di sviluppo GM VHDL ha adottato la convenzione di considerare le stringhe di bit come se fossero state dichiarate con il downto. Questo ha due conseguenze:
Per queste ragioni e' bene dichiarare sempre i vettori di bit con il downto.
Dopo aver definito un vettore e' possibile indicare un singolo elemento alla posizione n con la sintassi: nome_vettore(n). Ad esempio l' architettura del demultiplexer dove sono presenti tre vettori i,u0,u1 a 8 bit puo' essere scritta come:
architecture dmpx_df of dmpx_x8 is signal nc0:bit; begin nc0<=not c0; u0(0)<=nc0 and i(0); u0(1)<=nc0 and i(1); u0(2)<=nc0 and i(2); u0(3)<=nc0 and i(3); u0(4)<=nc0 and i(4); u0(5)<=nc0 and i(5); u0(6)<=nc0 and i(6); u0(7)<=nc0 and i(7); u1(0)<=c0 and i(0); u1(1)<=c0 and i(1); u1(2)<=c0 and i(2); u1(3)<=c0 and i(3); u1(4)<=c0 and i(4); u1(5)<=c0 and i(5); u1(6)<=c0 and i(6); u1(7)<=c0 and i(7); end dmpx_df;
Non bisogna cercare di accedere a elementi inesistenti. Cosi' se in un modello abbiamo un segnale i definito come sopra e se tentiamo di assegnare un valore a i(8) il nostro modello, quando viene simulato, dara' dei risultati impredicibili o blocchera' il computer.
E' possibile isolare una fetta (slice) di un vettore per poi usarlo in tutti i contesti in cui e' richiesto un vettore piu' piccolo di quello di partenza. Ad esempio per isolare i 4 bit meno significativi di i scriveremo: i(3 downto 0)
Abbiamo visto come sia possibile indicare un singolo elemento di un vettore o una fetta di esso. Il nome del vettore senza alcuna altra indicazione sta' invece ad inidicare tutto il vettore. Ad esso puo' essere assegnata una sequenza di valori, tutti dello stesso tipo, tanti quanti sono gli elementi del vettore in questione. Questa sequenza prende il nome di aggregato. Un aggregato puo essere definito con due diverse notazioni: la notazione posizionale e quella nominale. Vi e' poi la possibilita' di dare delle notazioni sintetiche con il costrutto others.
Nella notazione posizionale gli elementi da assegnare sono dati nell' ordine in cui verranno inseriti nel vettore ad esempio:
i<=('1','0','1','0','1','0','1','1');
questa assegnazione ha lo stesso effetto della assegnazione nominale
i<=(7=>'1',6=>'0',5=>'1',4=>'0',3=>'1',2=>'0',1=>'1',0=>'1');
Si noti che se i valori forniti dall' aggregato posizionale sono meno di quelli necessari per riempire il vettore gli ultimi valori verso destra non saranno cambiati.Nella asseganzione per nome ogni valore e' preceduto dall' indice che indica il posto in cui depositare il valore. Se i valori forniti dall' aggregato nominale sono meno di quelli necessari per riempire il vettore le posizioni non nominate non saranno cambiate. Infine se si vuole mettere lo stesso valore in quasi tutte le celle del vettore possiamo specificare questo valore comune con la notazione nominale usando il nome collettivo others.
i<=(7=>'1',5=>'1',3=>'1',1=>'1',0=>'1',others=>'0');
Altra forma particolare di un aggregato e' quella per le stringhe di bit che possono essere date in notazione binaria (B) esadecimale (X) ed ottale (O). Allora saranno equivalenti alle precedenti le tre assegnazioni date di seguito:
i<=B"1010_1011";
i<=O"253";
i<=X"AB";
Esercizio:
Si realizzi il demultiplexer per 32 bit:
entity dmpx is port (c0:bit;i:bit_vector(31 downto 0);u0,u1:out bit_vector(31 downto 0)); end dmpx;
(suggerimento si usino gli aggregati e l' AND tra bit_vectors, ovvero, si puo scrivere u0 and "000000000000000....") [soluzione e simulazione]
La libreria vector.vhd contiene il package functions che e ' un esempio di un package VHDL. Un package, in VHDL, e' una collezione di procedure e/o funzioni che sono utili per maneggiare un certo tipo di dato. La libreria vector.vhd contiene il package functions che e' una collezione di procedure e funzioni utili per maneggiare i dati di tipo bit_vector. Anche se non ci capitera' spesso di definire package, procedure o funzioni puo' capitare di doverne usare qualcuna contenuta in librerie di tipo generale, come vector.vhd , vediamo allora brevemente come usare packages, procedure e funzioni.
Le procedure e le funzioni sono pezzi di codice che compiono delle trasformazioni su dei dati detti parametri restituendo degli altri dati (parametri) modificati. Le funzioni e le procedure hanno ciascuna un nome e possono essere usate in un punto qualsiasi di un programma semplicemente citando il loro nome con una invocazione. Il programmatore che vuole definire una procedura deve prima definire il tipo di comunicazione che essa scambia con il resto del programma definendo numero, tipo e direzione (in, out, inout) dei parametri formali. Ad esempio la scrittura:
procedure read(variable line:inout string; variable message: out string);
dice che la procedura read ha 2 parametri formali di nome line e message. Il primo parametro prende un valore di tipo string in input. Tale valore verra' ritornato modificato (inout). Il secondo parametro message e' usato solo per comunicare un valore (out) di tipo string. Le funzioni possono essere viste, a livello elementare, come particolari procedure dove un solo parametro e' di modo out ed e' usato per ritornare un risultato mentre tutti gli altri sono di modo in e sono detti argomenti della funzione. Una volta definita, una funzione, puo' essere invocata una o piu' volte all' interno di un programma, passandogli dei parametri che in questo caso si chiameranno parametri attuali. L' invocazione di una funzione si fa con una sintassi del tipo:
c<=negate("0010");
dove la funzione negate, contenuta in vector.vhd e' definita come:
function negate (a: bit_vector) return bit_vector;
I parametri attuali dovranno essere delle espressioni che, una volta valutate, producano dei valori del tipo indicato nei parametri formali. L' invocazione di una procedura e' una istruzione a se, l'invocazione di una funzione produce invece un valore e in quanto tale puo' essere usata all' interno di una espressione. Per le funzioni e' anche possibile una invocazione che faccia figurare la funzione come un operatore. Se ad esempio a,b,c sono dei bit_vectors possiamo mettere in c la somma di a e b con la scrittura c<="+"(a,b). Questa scrittura rappresenta l' invocazione della funzione + inserita in vector.vhd. Essendo poi il simbolo "+" della funzione anche un simbolo di operatore e' possibile scrivere anche c<=a + b. Le funzioni che possono essere usate come operatori quando hanno nella definizione il loro nome inserito tra virgolette come in:
function "+" (a,b: bit_vector) return bit_vector
Piu' procedure possono essere ordinatamente raccolte in una struttura detta package. E' utile formare un package tutte le volte che si ha a che fare con un gruppo di procedure e/o funzioni che compiono delle operazioni che hanno un denominatore comune come ad esempio il fatto di operare su uno stesso tipo di struttura dati. Ad esempio tutte le procedure della libreria vector hanno in comune il fatto di operare su dei bit_vector. Questo rende naturale il fatto di raccogliere queste procedure in un package. La necessita' di raggruppare funzioni e procedure e' cosi' frequente che i linguaggi di programmazione forniscono dei costrutti per organizzare questo tipo di raggruppamento. VHDL seguendo l' esempio di Ada cerca di incanalare la creativita' del programmatore in una direzione ben precisa al fine di stimolare una chiara ed ordinata organizzazione dei programmi. In VHDL tutte le volte che si vogliono raggruppare in un modulo un insieme di procedure e funzioni ci si deve chiedere quale sia l'interfaccia del modulo ovvero quale sia l' insieme di funzioni che verranno usate fuori del modulo. Infatti VHDL prescrive che se si vogliono raggruppare delle funzioni in un modulo, che in VHDL si chiama package, si devono scrivere due cose:
Vediamo ad esempio la struttura del package functions nella libreria vector.vhd Avremo prima l' intestazione dove vengono elencati nome e parametri di ogni funzione
package functions is -- overloaded functions for bit_vector operations ...................... function negate (a: bit_vector) return bit_vector; ...................... -- conversion functions for bit_vector operations function to_bit (size: in integer; num: in integer) return bit_vector ...................... end functions;
Seguira' poi un body dove viene dato il codice per ogni funzione:
package body functions is ....... -- qui, per ogni funzione, ripeto l' intestazione della funzione, -- aggiungo un is e poi metto il codice che realizza la funzione in questione function to_bit (size: in integer; num: in integer) return bit_vector is variable ret: bit_vector (1 to size); variable a: integer; begin a:=abs num; -- prendo il valore assoluto for i in size downto 1 loop if ((a rem 2)=1) then ret(i):='1'; else ret(i):='0'; end if; a:=a/2; end loop; if num < 0 then return negate(ret);a -- esegue il complemento a due else return ret; end if; end; ........ -- codice per le altre funzioni ........ end; -- fine del body del package
A questo punto per utilizzare queste funzioni nella stesura di una architettura (in un altro file .vhd) dovro' scrivere:
library vector; use vector.functions.all; architecture xxx of .....................
che si legge: esiste una libreria di nome vector, nella architettura xxx usero tutto il contenuto del package functions che si trova nella libreria vector.
Per utilizzare il package vector.vhd devo prima averlo compilato con il comando vh (o vhdl o vhdl14) oppure deve essere presente nella directory dove opero il file vector.o.
Vedremo nella prossima sezione i meccanismi generali che presiedono alla strutturazione di un modello VHDL e come si possano utilizzare entita' e package contenuti in altri files .vhd.
Qui di seguito introduciamo invece i packages che useremo qualche volta in seguito.
Il package Ieee contiene alcune definizioni che ci torneranno utili per trattare i dispositivi a tre stati. Lo stesso tipo di approccio usato per la libreria vector e' necessario per utilizzare i package ieee.vhd dovro' quindi scrivere:
library ieee; use ieee.std_logic_1164.all; architecture xxx of .....................
Per utilizzare il pakage ieee.vhd dovro prima compilarlo con il comando vh (o vhdl o vhdl14) oppure deve essere presente nella directory dove opero il file vector.o. .
Esistono delle procedure predefinite di cui viene solo fornita l' interfaccia ovvero nome e tipo dei parametri e la loro direzione. Tra queste procedure predefinite un esempio importante e' dato delle procedure standard per l' I/O del VHDL. Per utilizzare il package standard per l' I/O non e' necessario compilare nulla bisogna solo utilizzare la sintassi:
library ieee; use std.textio.all; architecture xxx of .....................
E' bene, tutte le volte che si ha a che fare con una rete di una certa complessita', concepire il progetto dell' hardware dividendo quanto si deve realizzare in una serie di entita'. Resta tutta da scoprire la maniera migliore di ordinare piu' entita' secondo le relazioni che intercorrono tra essi. Vi potranno essere progetti in cui tra le varie entita' vi e' una chiara gerarchia basata sulle funzionalita' offerte per cui vi saranno entita' piu' grandi complesse collegate tra loro e realizzate a partire da entita' piu' semplici. Vi saranno progetti in cui la relazione tra alcune entita' richiama una organizzazione di tipo classificatorio o una tassonomia, per cui alcune entita' sono una versione specializzata di una entita' generica piu' generale. Piu' che entita' si parlera' allora di classi ed oggetti. In generale in un modello VHDL questi due aspetti coesistono, quindi di seguito impareremo come stratificare gerarchicamente un modello collegando entita' che gia' hanno una architettura composta da piu' entita'. Passeremo poi a scrivere ed utilizzare entita' generiche cioe' parametrizzate.