Appunti VHDL
Simone Battisti
Contents
1 Introduzione 2
2 VHDL in pratica 3
3 Tipi del VHDL 4
4 Stile Comportamentale 6
5 Aritmetica di VHDL 7
6 VHDL per circuiti sequenziali 7
7 Processi sequenziali 8
8 Pattern sequenziali in VHDL 8
1
1 Introduzione
Il VHDL definisce quello che un sistema andrà a fare con del vero e proprio codice. Ad esempio
prendiamo un multiplexer e lo definiamo come un semplice if e elseif.
In VHDL i circuiti sono fatti da dei blocchi, detti entità, che sono delle vere e proprie funzioni
o classi dei vari linguaggi di programmazione. Un’entità può avere più ingressi e più uscite,
dette proprietà dell’entità:
entity entity_name is
port (a ,b , cin : in bit ;
s , cout : out bit );
end entity_name
Per definire cosa fa il blocco ne scrivo la sua architettura, introdotta con la keyword archi-
tecture e si definisce di che entità si sta definendo cosa deve fare:
architecture architecture_name of entity_name is
[ dichiarazioni ]
begin
[ codice vero e proprio dell ’ architettura ]
end
Per descrivere cosa fa si usano vari modi:
• data flow: descrivo il funzionamento con equazioni booleane che vengono risolte tutte
assieme, differentemente dai vari linguaggi di programmazione che ne eseguono a cascata
in base all’ordine che decidiamo.
• strutturale: definiamo una gerarchia delle cose che mi servono per finire il mio circuito,
dal più semplice al più complesso.
• comportamentale: tutto eseguito allo stesso istante tramite un algoritmo che descrive
il valore di uscita in base agli ingressi.
All’interno dell’architettura possiamo usare tutti i metodi che vogliamo, solitamente usiamo le
cose semplici col data flow, cose difficili con il comportamentale e poi metto tutto assieme
in un processo strutturale. Definiamo ora per esempio una porta and:
entity AND2 is
port (x , y : in bit ;
s : out bit ;)
end entity AND2
architecture ex1 of AND2 is
begin
s <= x and y ;
end architecture ex1 ;
Notiamo l’operatore ¡= che indica l’assegnazione ad un valore e l’operatore and che ci
definisce la relativa funzione logica.
Per provare il nostro codice sfruttiamo il testbench che definisce gli ingressi e contiene l’entità
del nostro codice.
Lo definiamo come una vera e propria nuova entità, dei segnali di ingresso e le sue uscite, che
andranno a collegarsi al nostro circuito. Creiamo un testbench per la nostra and:
entity TestAnd2 is
end entity TestAnd2 ;
architecture simple of TestAnd2 is
signal a , b , c : bit ;
2
begin
g1 : And2 port map ( x = > a , y = > b , s = > c )
a <= ’0 ’ , ’1 ’ after 100 ns , ’0 ’ after 200 ns ;
b <= ’0 ’ , ’1 ’ after 150 ns , ’0 ’ after 200 ns ;
end architecture simple ;
Come possiamo notare il testbench definisce tutti i suoi ingressi che vengono collegati nella
port map ai segnali di ingresso e uscita della nostra porta and2.
Definiamo poi i valori dei vari segnali che mi definiscono i loro valori in bit (fra apici) e con i
relativi cambi nel tempo dopo un tot di ritardo.
In VHDL ci sono tutti gli operatori logici che conosciamo. Tutti questi hanno la stessa prece-
denza, tranne not che viene fatto prima di tutti. Per risolvere questo problema si devono
usare bene le parentesi.
In un’architettura possiamo definire più espressioni sfruttando gli ingressi di un’espressione
in un’altra:
entity dual_port is
port (a , b , c : in bit ;
d , z : out bit ;)
end entity dual_port
architecture ex1 of dual_port is
begin
d <= a and b ;
z <= c or d ;
end architecture ex1 ;
Anche nelle espressioni possiamo aggiungere dei ritardi con after, soprattutto nel caso prece-
dente perché, dato che VHDL fa tutte le espressioni assieme, dobbiamo aspettare che d venga
calcolato prima di poter fare la seconda espressione.
2 VHDL in pratica
Cerchiamo di rappresentare un full adder:
3
entity full_adder is
port (a , b , cin : in bit ;
s , cout : out bit ;)
end entity full_adder
architecture adder of full_adder is
begin
s <= a xor b xor cin ;
cout <= ( a and b ) or ( a and cin ) or ( b and cin )
end architecture adder ;
Proviamo ora un addizionatore a 4 bit, dove gli ingressi saranno dei vettori:
entity full_adder4 is
port (A , B : in bit_vector (3 downto 0);
cin : in bit ;
s : out bit_vector (3 downto 0);
cout : out bit ;);
end entity full_adder4 ;
architecture adder4 of full_adder4 is
signal C : bit_vector (3 downto 1);
begin
FA0 : full_adder port map ( A (0) , B (0) , cin , C (1) , S (0));
FA1 : full_adder port map ( A (1) , B (1) , C (1) , C (2) , S (1));
FA2 : full_adder port map ( A (2) , B (2) , C (2) , C (3) , S (2));
FA3 : full_adder port map ( A (3) , B (3) , C (3) , cout , S (3));
end architecture adder4 ;
Per costruire il full adder a 4 bit posso sfruttare il metodo strutturale e prendere il full adder
fatto prima, istanziarne 4 e mettere assieme il risultato.
Da notare che utilizziamo i bit vector, dei vettori di bit a cui indichiamo la lunghezza con n
downto m.
Tra parentesi dopo il port map del full adder sono i parametri che gli passiamo, quasi come
se fosse un costruttore della classe.
I segnali possono essere solamente letti, solo a destra del segno ¡=, mentre quelli di uscita
possono comparire solo a sinistra del ¡= e possono essere solo scritti.
Per poter usare degli oggetti sia a destra che a sinistra lo definiamo come buffer nell’entità.
Come i buffer esistono i signal che sono dei segnali interni leggibili e scrivibili, possono
essere scritti una volta sola.
Possiamo definire poi dei segnali di ingresso e uscita, gli inout, che si comportano come
buffer, ma possono essere scritti da più parti. VHDL decide che valore dare all’inout per non
dare conflitto.
Le porte, come sappiamo, possono definire un ritardo con after, ma più semplice è definire un
generic che prende come parametro tra parentesi il tempo di delay.
In VHDL possiamo definire delle costanti (constant) che aiutano nella lettura del codice.
3 Tipi del VHDL
Per ora abbiamo visto vari tipi, come bit e bit vector, ma ne esistono molti altri:
• bit
• bit vector: come visto in precedenza possiamo definire la sua grandezza con il downto,
ma ne esiste un altro modo, il metodo to. La differenza tra i due è che quando vado ad
aggiungere valori nei campi all’interno del vettore col downto li metto dal primo posto
4
all’ultimo (v[n] a v[0]), mentre col to li vado a posizionare dall’ultimo posto al primo
(v[0] a v[n]).
• integer: valore intero a 32 bit.
• time: valore del tempo per i delay in ns.
Su VHDL possiamo fare dei tipi creati da noi, un po’ come il typedef del C, usiamo la
keyword type per definirlo, seguito dal nome della variabile e poi is per definire cosa è quel
valore.
• tipi composti: uguale alla enumerazione del C.
• array: come per i vettori, stesso tipo di istanza dei vector.
Un tipo particolare definito in una libreria esterna, il std logic, un tipo non numerico che
va a sostituire il bit:
La maggior parte dei linguaggi permette di definire degli operatori per fare delle operazioni in
base ad una condizione:
int z = ( b < 10) ? a : a * b
Questa operazione ”calcola” b ¡ 10, se è vera la condizione assegna a, altrimenti esegue a * b.
La libreria per la std logic è la ieee, implementata con library ieee use [Link] logic [Link]
e ci aggiunge anche molte altre keyword che semplificano la leggibilità del codice, come il when
che applica una assegnazione condizionata quando assegniamo un valore alle uscite col ¡=,
in coppia con else quando la condizione non è vera.
Le espressioni possono essere annidate, con altri when else. Creiamo ad esempio un decoder
2 a 4 (00 → 0001, 01 → 0010, 10 → 0100, 11 → 1000):
library ieee use std_logic_1164 . all
entity decoder is
port ( a : in std_logic_vector (1 downto 0);
y : out std_logic_vector (3 downto 0););
end entity decoder
architecture decode of decoder is
begin
y <= ’0000 ’ when a = ’00 ’ else
’0010 ’ when a = ’01 ’ else
’0100 ’ when a = ’10 ’ else
’1000 ’ when a = ’11 ’ else
’ XXXX ’;
end architecture decode ;
Per evitare tutti questi a= posso scrivere all’inizio della nostra condizione with a selected e
poi pongo y ¡= e poi i when come prima ma senza dover mettere a= tutte le volte e senza
gli else.
5
Per prendere dentro tutti i casi nell’else finale posso anche aggiungere un when others e lui
prende tutti i valori che non sono tra quelli scritti nei when.
library ieee use std_logic_1164 . all
entity decoder is
port ( a : in std_logic_vector (1 downto 0);
y : out std_logic_vector (3 downto 0););
end entity decoder
architecture decode of decoder is
begin
with a select
y <= ’0000 ’ when ’00 ’
’0010 ’ when ’01 ’
’0100 ’ when ’10 ’
’1000 ’ when ’11 ’
’ XXXX ’ when others ;
end architecture decode ;
4 Stile Comportamentale
Ultimo tipo di stile in cui gli oggetti sono rappresentati come degli algoritmi. È proprio come
scrivere in un linguaggio di programmazione normale, permettendo l’utilizzo di if, ciclo for e
l’uso di while. Il linguaggio è sequenziale, cioè come tutti a cascata.
Tramite questo stile possiamo costruire sia circuiti combinatori che sequenziali. Dividiamo il
codice in processi, delle sorte di equazioni che però possono gestire più segnali. Definiamo
una sensitivity list che indica i valori che possono cambiare durante il tempo del circuito e
nel codice. Un processo cambia ogni volta che un segnale nella sensitivity list viene cambiato.
Quando il processo parte viene eseguito dall’inizio alla fine, e durante la sua esecuzione il
tempo di simulazione non avanza.
Vediamo ad esempio il priority encoder visto prima, ma con lo stile comportamentale (im-
portiamo da prima l’entità):
architecture sequential of priority is
begin
process ( a ) is
begin
if a (3) = ’1 ’ then
y <= ’11 ’;
valid <= ’1 ’;
elseif a (2) = ’1 ’ then
y <= ’10 ’;
valid <= ’1 ’;
elseif a (1) = ’1 ’ then
y <= ’01 ’;
valid <= ’1 ’;
elseif a (0) = ’1 ’ then
y <= ’11 ’;
valid <= ’1 ’;
else
y <= ’00 ’;
valid <= ’0 ’;
end process ;
end architecture sequential ;
6
Quando un segnale cambia nel tempo si dice che un segnale ha un evento.
Quindi quando c’è un evento su un segnale in sensitivity list comporta l’esecuzione di una
espressione che contiene quelle variabili.
Ricordarsi sempre che l’esecuzione viene fatta senza ritardi, quindi se mettiamo un ciclo
for ad esempio, e volessimo sfruttare il ciclo di clock per fare qualcosa, il segnale c sarà sempre
lo stesso perché il codice comportamentale viene eseguito tutto subito.
Possiamo assegnare il valore ad un segnale in momenti diversi durante il processo, vale sempre
l’ultimo.
Per fare operazioni con valori calcolati precedentemente o degli algoritmi iterativi non pos-
siamo usare i segnali, ma sfruttiamo le variabili, usabili solo dentro i processi e vengono
assegnate immediatamente. Tra un’attivazione e l’altra le variabili ricordano il valore.
Per assegnare un valore ad una variabile uso l’operatore :=:
library ieee ;
use std_logic_1164 . all ;
entity parity is
port (
a : in std_logic_vector ; -- vettore no range
y : out std_logic ;
);
end entity parity ;
architecture iterative of parity is
begin
process ( a ) is
variable even : std_logic ;
begin
for i in a ’ range loop
if a ( i ) = ’1 ’ then
even := not even ;
end if ;
end loop ;
y <= even ;
end process ;
end architecture iterative ;
Utile il for di std logic per creare testbench, dato che fornisce un comando wait for x ns, dove
x è un numero intero che rappresenta il tempo che serve tra il passaggio da un segnale ad un
altro, semplificando la lettura dei testbench.
5 Aritmetica di VHDL
VHDL non implementa le somme, sottrazioni, ecc, ma per farlo possiamo utilizzare una libreria
esterna, la numeric std che implementerà tutte le operazioni matematiche tra numeri in-
teri. Questi valori interi vengono implementati sotto forma di signed e unsigned, simili agli
std logic vector che finora abbiamo utilizzato per definire i numeri a 32 bit.
6 VHDL per circuiti sequenziali
Per scrivere circuiti sequenziali definiamo degli strumenti che definiscono la memoria. Creiamo
ad esempio un latch di tipo d:
entity latch is
port (
7
d , c : in std_logic ;
q : buffer std_logic ;
);
end entity latch ;
architecture memory of latch is
begin
q <= d when c = ’1 ’ else q ;
end architecture memory ;
Il latch lo possiamo fare anche edge triggered tramite gli eventi di un segnale. Questo viene
definito con un ’event dopo il nome del segnale in architettura, aggiornando il flip flop solo
al fronte del clock.
Meglio di questo è usare il rising edge(a) che è vero quando il clock passa da 0 a 1 (per il
contrario usiamo il falling edge).
7 Processi sequenziali
Solitamente si sfruttano i processi, dove però bisogna fare attenzione perché possiamo porre
delle memorie quando non le vogliamo. Questo aggiunge latch o clock in più che noi non
vogliamo.
Ad esempio basta semplicemente dimenticare una situazione in un if else; se capita quel valore,
il VHDL aggiunge automaticamente un latch che mette quel valore uguale a quello precedente,
avvisandoci con un semplice warning.
Per evitare ciò è sempre meglio mettere all’ingresso dei valori di default e correggerli a lungo
andare all’interno del circuito.
8 Pattern sequenziali in VHDL
Cominciamo ora a vedere tutti i diversi pattern visti dai circuiti sequenziali.
• Registri: sfruttiamo i processi per farlo, costruendo un processo sensibile sia alle variazioni
del clock, sia al cambio del valore d di ingresso. Se clk è a 0 e d cambia, allora si fornisce
il valore precedente, altrimenti si aggiorna:
library ieee ;
use std_logic_1164 . all ;
entity latch_D is
port (
d , clk : in std_logic ;
q : out std_logic ;
);
end entity latch_D ;
architecture memory of latch_D is
begin
process (d , clk ) is
begin
if clk = ’1 ’ then
q <= d ;
end if ;
end process ;
end architecture memory ;
8
• Flip flop edge triggered: cambia il flip flop solo al fronte del clock, semplicemente tolgo
dalla sensitivity list del processo il valore d e lascio solo il clk.
• Flip flop reset asincrono: prendiamo sempre un flip flop edge triggered, ma aggiungiamo
in sensitivity un reset asincrono per resettare il valore. Il reset può essere sincrono
definito come init, sempre al fronte del clock.
• Registro a più bit: esattamente come un registro normale, ma le uscite e gli ingressi
sono fatti da soli vettori.
library ieee ;
use std_logic_1164 . all ;
entity reg is
generic ( n : natural := 4);
port (
d : in std_logic_vector (n -1 downto 0);
clk , res : in std_logic ;
q : out std_logic_vector (n -1 downto 0);
);
end entity reg ;
architecture memory of reg is
begin
process ( clk , res ) is
begin
if res = ’0 ’ then
q <= ( others = > ’0 ’); -- metodo assegnare valori ad un
-- vettore senza saperne la dimensione
elseif rising_edge ( clk ) then
q <= d ;
end if ;
end process ;
end architecture memory ;
• Registro a scorrimento: abbiamo ingressi e uscite ad 1 bit, per farlo sfruttiamo dei
segnali interni come un vettore e, al fronte del clock, trasliamo di 1 i valori all’interno del
registro precedente e mandiamo in uscita l’ultimo bit.
• Contatori: abbiamo bisogno di valori numerici, quindi sfruttiamo la libreria numeric.
Semplicemente, al cambio di clock (clk in sensitivity), aggiungiamo 1 alla nostra us-
cita. Aggiungiamo poi un reset asincrono e, se vogliamo, possiamo aggiungere l’init
sincrono.