Il linguaggio VHDL

Il linguaggio VHDL consente di descrivere ed implementare circuiti complessi utilizzando tecnologie quali FPGA, CPLD, ASIC e structured ASIC. In questo articolo verrà fornita un’introduzione al linguaggio, ne verranno descritti gli elementi fondamentali e saranno presi in considerazione diversi esempi di codice sintetizzabile che potranno servire da base per la descrizione di circuiti più complessi.

Introduzione

Il linguaggio VHDL (Very high speed integrated circuit Hardware Description Language) è stato creato più di 20 anni fa come linguaggio di descrizione dell’hardware, in particolare di circuiti digitali. Da allora ha assunto un’importanza sempre maggiore in campo industriale tanto da essere standardizzato in diverse riprese dalla IEEE. La sua importanza deriva dal fatto che oltre alle applicazioni iniziali legate principalmente alla descrizione, modellizzazione e simulazione di circuiti digitali, esso è attualmente utilizzato soprattutto per la sintesi automatica dei circuiti. In pratica a partire da una descrizione VHDL del comportamento di un circuito è possibile ricavare automaticamente (utilizzando degli appositi programmi, chiamati “sintetizzatori logici”) la sua implementazione in termini di porte logiche, o blocchi funzionali elementari.

Questo approccio risulta molto vantaggioso da diversi punti di vista: in primo luogo permette di realizzare velocemente circuiti molto complessi ed ottimizzati a partire da una loro descrizione comportamentale. In secondo luogo il circuito descritto tramite VHDL piuttosto che con metodi tradizionali (schemi elettrici, funzioni logiche, etc.) risulta facilmente documentabile e verificabile. Inoltre la descrizione VHDL può essere del tutto indipendente dalla tecnologia che si utilizzerà per implementare il circuito, questo rende possibile ad esempio implementare su FPGA circuiti descritti per ASIC o viceversa. Data la sua versatilità e le sue caratteristiche il VHDL risulta un linguaggio piuttosto complesso. Esso infatti non è un linguaggio di programmazione, e non funziona ne va utilizzato come tale. Il codice VHDL non viene infatti “eseguito” sequenzialmente come ad esempio un programma scritto in C o Basic, ma piuttosto descrive qualcosa (tipicamente un circuito) che viene simulato o implementato. Per questo motivo il VHDL permette di gestire eventi o elaborazioni che avvengono anche in parallelo tra loro. È conveniente fare da subito una distinzione: come già detto il VHDL può servire per modellare e simulare dei sistemi, o per sintetizzare dei circuiti. Mentre qualsiasi descrizione VHDL formalmente corretta può essere simulata, non tutte le descrizioni possono essere sintetizzate.

Questo è dovuto al fatto che per potere sintetizzare un circuito, esso deve essere fisicamente realizzabile e coerente dal punto di vista logico e funzionale, e la descrizione deve essere “sufficientemente chiara” e univoca per il sintetizzatore. Per questo motivo il VHDL sintetizzabile è un sottoinsieme semplificato  dell’intero  linguaggio,  ma  richiede  uno stile più curato e preciso per ottenere buoni risultati. La possibilità di sintetizzare una certa descrizione dipende anche dal sintetizzatore utilizzato: non tutti i software infatti implementano le stesse funzionalità (anche se recentemente si ha una maggiore omogeneità rispetto a  qualche  anno fa). Ovviamente anche il VHDL sintetizzabile è simulabile, e questo permette di testare i circuiti descritti, simulandoli appunto. In questo tutorial verranno forniti gli elementi di base per iniziare a scrivere delle semplici descrizioni VHDL sintetizzabili. Considerando la complessità e la vastità dell’argomento, il linguaggio sarà introdotto  principalmente utilizzando  degli  esempi  pratici,  tralasciando  per brevità molti particolari che tuttavia risulta indispensabile conoscere per sfruttare il linguaggio a pieno. Per una trattazione più approfondita  si rimanda ai testi riportati in bibliografia.

ELEMENTI DEL LINGUAGGIO

Una descrizione VHDL è composta essenzialmente da due parti: una in cui si dichiarano gli ingressi e le uscite del circuito, e l’altra in cui viene descritto il funzionamento del circuito. La prima sezione si chiama “entity”,  la seconda “architecture”.  Il fatto di potere dividere le due parti è utile sia per ragioni di chiarezza (l’entity infatti fornisce una descrizione “black-box” del  circuito,  cioè  la sua interfaccia verso l’esterno), sia perché per una data entità possono esistere diverse architecture (ad esempio una per la sintesi e un’altra per la simulazione). Spesso  prima  dell’entity  è  presente  una  terza parte dichiarativa che ha la funzione di includere delle librerie esterne. L’aspetto complessivo è quello mostrato nel Listato 1, in cui è visibile la descrizione VHDL di una porta AND.

-- Porta AND a due ingressi
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;

entity Porta_AND is
   Port (A : in std_logic;
         B : in std_logic;
         Y : out std_logic);
end Porta_AND;


architecture Arch1 of Porta_AND is
begin
 Y <= A and B;
end Arch1;
Listato 1

Come si può vedere all’inizio del codice è stato incluso il package STD_LOGIC_1164 della libreria IEEE. Per fare questo sono state utilizzate le key-word “library” e “use”. Il package incluso contiene la definizione dei più comuni tipi di dati, costanti e funzioni, e per questo la sua inclusione si trova all’inizio della maggior parte delle entità. La sezione entity dichiara un unità chiamata “Porta_AND” che è dotata di due porte d’ingresso (A e B) e di una d’uscita (Y), tutte e tre del tipo std_logic, che può essere schematizzata come in Figura 1.

Figura 1. Entity della porta AND a due ingressi

Figura 1: Entity della porta AND a due ingressi

Il tipo std_logic è utilizzato per descrivere segnali logici binari come spiegato meglio di seguito. La sezione architecture specifica il funzionamento dell’entità Porta_AND. Ogni architecture ha un nome arbitrario, che in questo caso è “Arch1”,  e che serve per distinguerla dalla altre eventualmente presenti per la stessa entità. Il funzionamento del circuito è specificato utilizzando l’operatore “and” del VHDL. Si possono notare alcuni dettagli sulla sintassi del linguaggio:

  • il VHDL non è case sensitive: possono essere usati indifferentemente caratteri maiuscoli e minuscoli;
  • ogni linea di codice termina con un punto e virgola (“;”);
  • le varie sezioni sono delimitate da un “end”;
  • i commenti si ottengono con un doppio meno (“--“).

I principali tipi di dati utilizzabili sono riportati in Tabella 1, mentre gli operatori sono elencati in Tabella 2.

Tabella 1. Principali tipi VHDL

Tabella 1: Principali tipi VHDL

Tabella 2. Principali operatori VHDL

Tabella 2: Principali operatori VHDL

Come si può vedere oltre al tipo std_logic,  esiste  anche  il  tipo  std_logic_vector, che può essere considerato  come un array di std_logic,  e  che  è  adatto  per  rappresentare grandezze composte da più bit o bus. È interessante notare che per gli std_logic_vector è possibile specificare l’intervallo di bit che si vuole selezionare, e la direzione in cui li si indica. Ad esempio, se BUS è stato dichiarato come un bus ad 8 bit, cioè come std_logic_vector(7 downto 0), è possibile selezionare i 4 bit meno significativi scrivendo BUS(3 downto 0), o solo il sesto bit scrivendo BUS(5). Questo meccanismo è molto versatile, e permette di assegnare elementi di uno std_logic_vector ad uno std_logic e viceversa, o di inizializzare in maniera flessibile una variabile, come mostrato negli esempi seguenti:

BUS<=”0000000”;
BUS (3 downto 0)<=”0000”;
BUS (5)<=A; -- A è uno
std_logic
BUS (2)<=’1’;
BUS <=(1=> ‘1’, 2=>’1’,
others => “0”);

Da questi esempi si può notare come gli std_logic_vector si possono indicare numericamente ponendo il valore tra doppie apici, mentre gli std_logic usando le apici singole. Inoltre è possibile riempire automaticamente singoli valori elencandoli per posizione, o gruppi di valori usando la keyword “others”.

ISTRUZIONI E COSTRUTTI DI BASE

Combinando i vari operatori mostrati in Tabella 2 è possibile realizzare dei semplici circuiti combinatori (va notato comunque che tra gli operatori aritmetici solo “+”, “-” ed in alcuni casi “*” sono sintetizzabili). Per realizzare dei circuiti più complessi comunque uno dei costrutti più utili è il when-else, che permette di effettuare delle assegnazioni condizionate. La sintassi più generale è la seguente:

A <= B when condizione
else
      C when condizione
else
      …
      D;

Utilizzando questo costrutto è possibile implementare facilmente circuiti con funzione di selezione, come i multiplexer. L’esempio seguente (Listato 2) riporta proprio la descrizione di un multiplexer 2 a 1 per bus ad 8 bit.

-- Multiplexer 2 a 1 per bus ad 8 bit
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
entity MUX is
-- Ingresso bus A (8 bit)
port(A : in std_logic_vector(7 downto 0);
-- Ingresso bus B (8 bit)
B : in std_logic_vector(7 downto 0);
-- Ingresso di selezione
S : in std_logic;
-- Uscita (8 bit)
Y : out std_logic_vector(7 downto 0));
end MUX;
architecture Arch1 of MUX is
begin
Y <= A when (S=’0’) else B;
end Arch1;
Listato 2

Un costrutto simile al when-else è il with-select, che permette di elencare il valore delle uscite in corrispondenza di determinati ingressi. In questo caso l’unica condizione possibile è quella di uguaglianza. La sintassi è la seguente:

with X select
  A<=A0 when X0
     A0 when X0
…
     An when Xn;

Questo costrutto è particolarmente adatto a descrivere tabelle di valori, ROM, o decoder. Nel Listato 3 è riportato proprio la descrizione di un decoder BCD-7 Segmenti.

-- Decoder BCD-7Seg (anodo comune)
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;

entity BCD7SEG is
  port(BCD : in std_logic_vector(3 downto 0);
       LED : out std_logic_vector(6 downto 0));
end BCD7SEG;

architecture Arch1 of BCD7SEG is
begin
 with BCD select

  LED<=”1000000” when “0000”, --0
       “1111001” when “0001”, --1
       “0100100” when “0010”, --2
       “0110000” when “0011”, --3
       “0011001” when “0100”, --4
       “0010010” when “0101”, --5
       “0000010” when “0110”, --6
       “1111000” when “0111”, --7
       “0000000” when “1000”, --8
       “0010000” when “1001”, --9
       “1111111” when others; --?
end Arch1;
Listato 3

Si può notare che nell’ultima riga è stato specificato un valore da utilizzare nei casi diversi da quelli specificati (“others”). Questa riga rappresenta una specie di condizione di default, e tiene conto dell’immissione di codici non BCD. Più in generale comunque è necessario specificare l’ultima condizione come “others” anche quando vengano elencate tutte le combinazioni di bit, poiché i tipi std_logic non comprendono solamente i valori 0 ed 1, ma anche alcuni valori speciali che rappresentano condizioni “non logiche”, ad esempio “Z” per indicare l’alta impedenza, “X”  il  conflitto  tra  due  valori, “U” la non definizione, etc. Anche se questi valori non si avranno nel normale funzionamento è bene tenerli in conto in questo modo. Va notato che tutte le istruzioni scritte all’interno dell’architecture non vengono eseguite sequenzialmente, ma contemporaneamente, per questo motivo scrivendo:

A <= “0001”;
A <= “1101”;

Il segnale A non assumerà il secondo valore (“1101”), ma entrambi contemporaneamente, creando così una condizione di contesa (il valore assunto da A sarà “XX01”). Lo stesso vale quando si utilizzano più costrutti diversi all’interno dell’architecture. Per potere gestire delle istruzioni in qualche  modo  “sequenziali”  occorre  utilizzare  i  process, come discusso nel successivo paragrafo.

COSTRUTTI ED ISTRUZIONI SEQUENZIALI

Un “process” è un blocco funzionale che rappresenta una singola unità concorrente (rispetto ad altri elementi presenti nell’architecture), ma che può contenere costrutti sequenziali. La sintassi dei process è la seguente:

nome: process (a,b,c…)
begin
  …
  istruzioni
  …
end process;

La struttura del blocco process rispecchia quella dell’architecture, ma più è presente un elenco di variabili chiamato
“sensitivity list”. Il process verrà attivato solo quando si verificherà un cambiamento in una delle variabili contenute
nella sensitivity list, che rappresentano in qualche modo i segnali di ingresso del process. Nella sintesi spesso la sensitivity list è del tutto ignorata, però essa rimane importante in simulazione, per potere fornire dei risultati coerenti col funzionamento effettivo del circuito. Scrivendo le due assegnazioni viste prima all’interno del process, il valore assegnato ad A sarebbe stato effettivamente il secondo. Tuttavia non bisogna pensare che le istruzioni vengono eseguite sequenzialmente come accade nei linguaggi di programmazione, piuttosto vengono valutate tutte le espressioni e vengono effettuate le assegnazioni usando come precedenza l’ordine in cui sono scritte le istruzioni. All’interno di un blocco process possono essere utilizzate delle istruzioni dette appunto sequenziali, quali if e case. Il costrutto if-else ha la seguente sintassi:

if condizione then
   …
elsif condizione then
   …
elsif …
   …
else
   …
end if;

Il costrutto if permette di eseguire determinate sezioni di codice al verificarsi di una o più condizioni. Le sezioni elsif ed else indicano delle condizioni alternative e di default rispettivamente, e possono non essere presenti. Un esempio di descrizione sequenziale è mostrato nel Listato 4, in cui è descritto un encoder 7 a 3 con priorità.

-- Encoder a 8 bit con priorita’
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;

entity ENC7 is
  port(DIN : in std_logic_vector(6 downto 0);
       DOUT : out std_logic_vector(2 downto 0));
end ENC7;
architecture Arch1 of ENC7 is
begin

process (DIN)
begin
  DOUT <= “000”;
  if DIN(0)=’1’ then DOUT<=”001”; end if;
  if DIN(1)=’1’ then DOUT<=”010”; end if;
  if DIN(2)=’1’ then DOUT<=”011”; end if;
  if DIN(3)=’1’ then DOUT<=”100”; end if;
  if DIN(4)=’1’ then DOUT<=”101”; end if;
  if DIN(5)=’1’ then DOUT<=”110”; end if;
  if DIN(6)=’1’ then DOUT<=”111”; end if;

end process;

end Arch1;
Listato 4

In pratica l’uscita rappresenta in binario la posizione dell’ingresso che si trova ad 1. Se più ingressi sono portati ad 1 è necessario stabilire una priorità per decidere cosa dare in uscita. Si è scelto di presentare il valore più alto, e questo è ottenuto proprio sfruttando la sequenzialità delle istruzioni if. Se più di un ingresso è a livello 1, la condizione che sarà considerata valida tra quelle verificate sarà quella scritta per ultima. Questo implementa il meccanismo di priorità voluto. Il costrutto if è importante in quanto è utilizzato di solito per creare circuiti sincroni. È possibile infatti eseguire determinate sezioni di codice quando si verifica il fronte di clock. Questo è mostrato nel listato 5 in cui è riportata la descrizione di un registro a caricamento parallelo.

library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
entity REG8 is
-- Ingresso (8 bit)
port(DIN : in std_logic_vector(7 downto 0);
-- Uscita (8 bit)
DOUT : out std_logic_vector(7 downto 0);
LOAD : in std_logic; -- Caricamento
CLOCK : in std_logic; -- Clock
RESET : in std_logic); -- Reset
end REG8;
architecture Arch1 of REG8 is
begin
process (DIN, CLOCK, RESET, LOAD)
begin
  if RESET=’1’ then
    DOUT <= (others => ‘0’);
  elsif (CLOCK’event and CLOCK=’1’) then
    if LOAD=’1’ then
      DOUT <= DIN;
     end if;
  end if;
end process;
end Arch1;
Listato 5

Il codice descrive un registro ad 8 bit con ingresso di caricamento (LOAD) e reset asincrono. Il funzionamento è il seguente: se il  RESET viene attivato,  l’uscita  deve essere portata a 0, altrimenti, in caso di fronte di clock positivo (l’attributo “event” viene utilizzato per rilevare la transizione, mentre “=’1’” specifica  che si tratta  del frotne positivo), se il segnale di caricamento vale 1, l’uscita deve assumere il valore presente in ingresso. Se queste condizioni non sono verificate l’uscita rimane immutata, e per questo viene inferito un registro dal sintetizzatore. Si può notare che la condizione di clock e quella di reset sono mutuamente esclusive ed indipendenti, questo descrive il funzionamento di un reset asincrono. Per ottenere un reset sincrono la condizione di reset doveva essere posta all’interno di quella di clock. Con gli elementi visti è possibile descrivere anche un contatore. I contatori sono importanti quando si descrive un circuito da sintetizzare poiché possono svolgere la funzione dei cicli for dei linguaggi di  programmazione. L’istruzione  for-loop presente nel VHDL non permette infatti di implementare cicli sintetizzabili (permette di farlo in simulazione). Nel Listato 6 è presente il codice che descrive un contatore a 16 bit sincrono, con reset, abilitazione e caricamento parallelo.

library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;
use IEEE.STD_LOGIC_ARITH.ALL;
entity CONT16 is
-- Ingresso (16 bit)
port(DIN: in std_logic_vector(15 downto 0);
-- Uscita (16 bit)
DOUT : out std_logic_vector(15 downto 0);
LOAD : in std_logic; -- Caricamento
EN : in std_logic; -- Abilitazione
CLK : in std_logic; -- Clock
RESET : in std_logic); -- Reset
end CONT16;
architecture Arch1 of CONT16 is
signal cnt : std_logic_vector(15 downto 0);
begin
process (DIN, CLOCK, RESET, LOAD, EN)
begin
  if RESET=’1’ then
    cnt <= (others => ‘0’);
  elsif (CLK’event and CLK=’1’) then
    if EN=’1’ then
      if LOAD=’1’ then
        cnt <= DIN;
      else
        cnt <= cnt + 1;
      end if;
     end if;
    end if;
end process;
DOUT <= cnt;
end Arch1;
Listato 6

Nel listato si possono notare alcuni dettagli interessanti. Innanzi tutto  è stato dichiarato un “segnale” all’inizio dell’architecture. I segnali funzionano quasi come delle variabili, contengono dei valori del tipo specificato, e dal punto di vista della sintesi possono rappresentare o dei semplici collegamenti interni (che non sono visibili dall’esterno) o dei registri Il segnale dichiarato è di tipo std_logic_vector a 16 bit, ed è utilizzato per contenere il valore del contatore. Non è possibile utilizzare direttamente il valore dell’uscita DOUT perché questo è di modo out, e quindi non può essere letto (cosa necessaria per incrementare il valore). Anche in questo caso è stato utilizzato un reset asincrono, mentre il funzionamento vero e proprio del circuito è completamente sincrono e quindi descritto dentro la condizione di clock. In particolare si può notare l’ordine in cui le istruzioni sono state scritte: la verifica del segnale di abilitazione è posta a monte delle altre, e quindi ha precedenza su queste: se l’abilitazione non è attiva non sarà possibile compiere nessun’altra operazione.

Le operazioni di caricamento e di incremento  invece sono state poste allo stesso livello, ma sono mutuamente esclusive: se avviene un caricamento il contatore non viene incrementato e viceversa. L’incremento del contatore viene ottenuto sommando 1 al valore precedente  del  contatore.  Va notato che  la somma  tra  std_logic_vector e con interi non è un’operazione banale, perché occorre definirne il significato preciso e le regole da adottare. Queste non sono presenti nativamente nel linguaggio VHDL, per questo è stato necessario includere delle librerie apposite. L’ultima istruzione contenuta nell’architecture assegna semplicemente il valore del contatore all’uscita. Un altro costrutto sequenziale utilizzabile all’interno dei process è il case-when, che permette di selezionare un segmento di codice da eseguire quando una variabile di controllo assume un certo valore. Un esempio di utilizzo di questo costrutto è mostrato nel Listato 7, in cui viene descritta un’unità che esegue un’operazione logica a scelta tra AND, OR, NAND o XOR sui due operandi ad 8 bit in ingresso.

library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
entity ALU8 is
port(OPA : in std_logic_vector(7 downto 0);
OPB : in std_logic_vector(7 downto 0);
DOUT: out std_logic_vector(7 downto 0);
SEL : in std_logic_vector(1 downto 0));
end ALU8;
architecture Arch1 of ALU8 is
begin
process (OPA, OPB, SEL)
begin
  case SEL is
    when “00” =>
      DOUT <= OPA and OPB;
    when “01” =>
      DOUT <= OPA or OPB;
    when “10” =>
      DOUT <= OPA nand OPB;
    when others =>
      DOUT <= OPA xor OPB;
  end case;
end process;
end Arch1;
Listato 7

L’operazione eseguita è scelta in base al valore presente su un ingresso di controllo.

DESCRIZIONI STRUTTURALI

I tipi di descrizione utilizzati fino a qui vengono definiti comportamentali, dataflow o RTL (Registers Transfert Level) dal momento che descrivono il funzionamento del circuito,  anche dal punto  di vista delle operazioni compiute. Il VHDL permette anche di creare descrizioni strutturali, cioè di descrivere dei circuiti come connessioni di blocchi funzionali preesistenti. Questa modalità di descrizione è utilizzata di solito per collegare tutte le parti di un progetto complesso (realizzando il cosiddetto “top level”), per rendere modulare un progetto, utilizzando un approccio top-down  o per realizzarlo mettendo assieme componenti  già  disponibili  (approccio  bottom-up). I moduli che si intendono usare devono essere prima dichiarati, utilizzando l’istruzione “component”,  quindi instanziati, specificandone i collegamento con i segnali presenti nell’architettura o nell’entità che li richiama. Nel listato 8 è mostrato un esempio di  descrizione strutturale relativa ad un half-adder.

library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
entity HALFADD is
  port(AIN : in std_logic;
       BIN : in std_logic;
       SOUT: out std_logic;
       COUT: in std_logic);
end HALFADD;
architecture Struttura of HALFADD is
  component Porta_AND
    port (A : in std_logic;
          B : in std_logic;
          Y : out std_logic);
   end component;
   component Porta_XOR
     port (A : in std_logic;
           B : in std_logic;
           Y : out std_logic);
    end component;
begin
XOR1: Porta_XOR
  port map (A => AIN,
            B => BIN,
            Y => SOUT);
AND1: Porta_AND
  port map (A => AIN,
            B => BIN,
            Y => COUT);
end Struttura;
Listato 8

Il circuito esegue la somma (aritmetica) di due bit, riportando in uscita il valore della somma e quello il riporto. Il circuito  può essere realizzato collegando le porte logiche AND e XOR come mostrato in Figura 2.

Figura 2. Entity del multiplexer 2 a 1 a 8 bit

Figura 2: Entity del multiplexer 2 a 1 a 8 bit

 

Figura 3. Entity del decoder BCD-7 Segmenti

Figura 3: Entity del decoder BCD-7 Segmenti

 

Figura 4. Entity dell’encoder 7 a 3 con priorità

Figura 4: Entity dell’encoder 7 a 3 con priorità

 

Figura 5. Entity del registro ad 8 bit

Figura 5: Entity del registro ad 8 bit

 

Figura 6. Entity del contatore caricabile a 16 bit

Figura 6: Entity del contatore caricabile a 16 bit

 

Figura 7. Entity dell’unità logica programmabile

Figura 7: Entity dell’unità logica programmabile

 

Figura 8. Entity e struttura dell’half-adder

Figura 8: Entity e struttura dell’half-adder

La porta AND è già stata descritta nel Listato 1, la XOR ha una struttura  identica, ma utilizza il rispettivo operatore. Come si può vedere i moduli relativi alle due porte sono stati richiamati con l’istruzione component, posta prima del begin dell’architecture, dove di solito sono dichiarati i segnali. La sintassi è molto simile a quella di un’entity. I due componenti sono poi stati instanziati, specificando il loro nome ed i collegamenti all’interno della sezione “port  map”.  Gli identificativi a sinistra si riferiscono alle porte del componenti richiamato, quelli sulla destra possono essere segnali o porte dell’entità che li richiama.

 

Scrivi un commento

EOS-Academy
Abbonati ora!