Sei sulla pagina 1di 53

Guida VHDL

Introduzione
Circuiti integrati digitali
I circuiti digitali comunemente richiesti nei progetti elettronici sono disponibili sul mercato come
circuiti integrati già pronti, per questo detti anche componenti off-the-shelf. La realizzazione di un
nuovo circuito integrato si rende necessaria quando non esiste sul mercato un dispositivo per
l'applicazione richiesta. In questo caso il dispositivo è detto ASIC (Application Specific Integrated
Circuit). Il progettista disegna un ASIC e lo fa realizzare da un produttore di dispositivi; oppure
acquista dal produttore un dispositivo che poi dovrà configurare per ottenere le funzioni richieste.
Gli ASIC si classificano in base alla metodologia di progettazione adoperata. L'approccio Full
Custom fornisce il massimo grado di libertà al progettista, che può disegnare i dettagli a livello dei
transistors nelle maschere di produzione.
Con il metodo Standard Cell, il produttore fornisce una libreria di celle logiche già disegnate, tra
cui scegliere quelle adatte. Le celle scelte possono essere posizionate nel dispositivo, e poi
configurate e collegate intervenendo solo sulle maschere di metallizzazione.
Il metodo Gate Array è una variante del precedente. Tutte le celle utilizzabili sono già posizionate
in una matrice nel dispositivo e si possono solo stabilire le connessioni negli strati di metallo.
Infine è posssibile acquistare un dispositivo già fisicamente realizzato prima che il progetto
applicativo specifico abbia inizio. In questo caso il produttore vende un dispositivo programmabile
(PLD Programmable Logic Device), cosiddetto perchè dotato di un set di funzioni elementari che,
opportunamente configurate attraverso un'operazione di "programmazione", permettono di
realizzare un'applicazione specifica richiesta dal progettista. (Notare che l'acronimo PLD viene
spesso utilizzato al posto di PAL per indicare un particolare tipo di PLD, il Programmable Array
Logic).
Tra i diversi tipi di PLD, attualmente sono molto diffusi gli FPGA (Field Programmable Gate
Array). L'acronimo indica che il dispositivo è fondamentalmente un gate array, solo che le
connessioni tra le celle non si eseguono una volta per tutte col disegno delle metallizzazioni, ma si
programmano sul campo, anche più di una volta, ossia non si programma in fabbrica ma nello
stesso luogo dell'utente finale. Un FPGA contiene un insieme di celle logiche, celle di I/O e linee di
interconnessione. Per definire un circuito si configura ogni cella logica affinchè realizzi una
particolare funzione combinatoria o sequenziale tra quelle elementari disponibili e si definiscono le
interconnessioni tra le celle. Questa configurazione è rappresentata da un file, una sequenza di bit
(risultato finale del progetto) che può essere inserita in una memoria del dispositivo quando questo
viene alimentato e può essere conservata in una memoria esterna non volatile, ad esempio Flash.
Questo perchè la loro configurazione è volatile. I CPLD (Complex Programmable Logic Device)
sono dispositivi che contengono l'equivalente di molte PAL collegate fra di loro mediante
interconnessioni programmabili ed incapsulate in un unico circuito integrato. A differenza degli
FPGA le CPLD mantengono la programmazione anche quando non sono alimentate perché
contengono delle memorie non volatili.

Flusso di progettazione
Lo sviluppo di un sistema parte dalla sua specifica iniziale con l'obiettivo di realizzare fisicamente
un dispositivo oppure solo di simularlo o documentarlo. Le attività coinvolte nel progetto
dipendono dal tipo di dispositivo, ma si possono individuare delle fasi fondamentali tipiche di tutti i
progetti:
1. Modellazione. Si disegna il sistema costruendone un modello software, ovvero una sua
rappresentazione fatta con opportuni strumenti grafici o linguaggi testuali (il termine
disegno viene usato spesso come sinonimo di progettazione e non necessariamente di

1
disegno grafico). Inizialmente il modello può anche essere molto astratto ovvero privo di
dettagli sull'implementazione.
2. Simulazione. Il simulatore, esegue il modello ovvero ne simula il comportamento affinchè
il progettista possa verificarne la correttezza. Se l'esito è negativo si torna a correggere il
modello nella fase precedente. Se la modellazione non è orientata alla realizzazione di un
dispositivo, ma solo alla documentazione e/o simulazione, il progetto può terminare qui.
3. Sintesi. Per implementare fisicamente il sistema, è necessario descriverne la struttura,
qualunque sia il tipo di descrizione iniziale fatta. La sintesi traduce il modello in una netlist,
che è la rappresentazione diretta in forma di rete di porte logiche. In questa fase il modello,
benchè simulato correttamente, potrebbe rivelarsi inadeguato, perchè alcuni costrutti del
linguaggio utilizzato potrebbero essere non orientati alla sintesi o non supportati dal
sintetizzatore.
4. Implementazione. La netlist viene ulteriormente elaborata, questa volta allo scopo di
produrne un formato che dipende dal tipo di dispositivo da realizzare. Si può intervenire sui
dettagli del layout nelle maschere di produzione. Si possono calcolare esattamente i ritardi di
propagazione delle linee di interconnessione, per inserirli nel modello ed eseguire una
simulazione più completa.
Per un ASIC non programmabile, i files risultanti dall'implementazione servono alla foundry
che dovrà produrre fisicamente il dispositivo. Per un programmabile, tali files vengono
utilizzati dal progettista per programmare il dispositivo.

Strumenti di progettazione
Ogni fase della progettazione è supportata da opportuni tools. Gli strumenti di modellazione
giocano un ruolo fondamentale perchè devono essere adeguati alla complessità del sistema da
progettare.
Nei progetti che implicano il disegno di dettagli implementativi dei transistors e/o delle celle
logiche nelle maschere di produzione, si utilizzano opportuni strumenti CAD. Nella scala dei livelli
di astrazione, questo tipo di descrizione appartiene al livello più basso, il livello fisico.
Il disegno schematico è il tipico metodo per definire il sistema come rete di porte logiche
direttamente in forma strutturale (astrazione gate-level). È uno strumento comodo perchè lo schema
viene creato in modo grafico.
Gli HDL (Hardware Description Languages) sono linguaggi per descrivere il sistema in forma
testuale. Rispetto al disegno schematico, gli HDL permettono di utilizzare anche livelli di astrazione
più alti, tipici dei progetti complessi. È anche possibile disegnare diagrammi di macchine a stati,
utilizzando direttamente il formalismo grafico, che poi vengono tradotti in una forma HDL.
Il primo HDL è stato PALASM (PAL ASseMbler) che serviva a specificare reti logiche per
dispositivi programmabili tipo PAL. Un linguaggio simile è ABEL (Advanced Boolean Equation
Language). Questi linguaggi permettono di descrivere il sistema attraverso equazioni logiche,
tabelle della verità e diagrammi a stati (in forma testuale).
Tra gli HDL, quelli considerati di alto livello, principalmente VHDL e Verilog, permettono anche
rappresentazioni astratte del sistema, e descrizioni algoritmiche simili a quelle dei linguaggi di
programmazione software.
È opportuno osservare che un flusso di progettazione, trasforma descrizioni più astratte e meno
dettagliate in descrizioni meno astratte e più dettagliate. Queste trasformazioni possono essere
eseguite in parte dai tools automatici e in parte dal progettista.

2
VHDL
VHDL (VHSIC-HDL, Very High Speed Integrated Circuit Hardware Description Language)
è il risultato del progetto di ricerca VHSIC sui circuiti integrati iniziato nei primi anni '80 negli Stati
Uniti. La ricerca evidenziò come gli strumenti di progetto gate-level fossero inadeguati alla
crescente complessità dei circuiti integrati a larghissima scala di integrazione. Si studiò allora un
linguaggio descrittivo più astratto, capace di aumentare l'efficienza nella progettazione di sistemi
digitali complessi.
Il VHDL è un linguaggio di descrizione dell'hardware adatto a modellare sistemi digitali sia per
scopi di sola documentazione o simulazione, che per scopi di sintesi per implementare dispositivi
reali. La sua principale caratteristica è l'indipendenza tecnologica, ovvero la capacità di descrivere
qualsiasi sistema digitale senza vincolare la descrizione a dispositivi reali particolari. Questa è la
base per costruire codice portabile e riusabile e quindi per una progettazione più efficiente.
La prima versione standard del VHDL fu definita nel 1987 da IEEE (Institute of Electrical and
Electronics Engineers) con il nome IEEE-1076-1987. Nel 1993 è stata aggiornata in IEEE-1076-
1993. Le due versioni principali del linguaggio definite da questi standard sono pubblicate nel
Language Reference Manual (LRM). Altri standard, tra cui IEEE-1164, introducono alcuni
importanti miglioramenti, come la logica a 9 valori che è più adatta della logica a due valori nella
modellazione di sistemi complessi (come i bus).

Concetti Chiave
Il linguaggio VHDL serve a descrivere sistemi hardware digitali. È simile ai linguaggi di
programmazione per la costruzione di programmi software, ma è dotato di caratteristiche nuove,
necessarie alla diversa natura dell'oggetto da descrivere.

Descrizione del sistema


Un sistema hardware si può sempre rappresentare in questi termini: riceve degli input, esegue delle
operazioni, produce degli output. In generale sarà costituito da uno o più sottosistemi
rappresentabili in questi termini. Allora per descrivere un sottosistema si devono definire:
• la sua interfaccia esterna, ovvero gli
ingressi e le uscite che rappresentano le sue
relazioni con gli altri sottosistemi o con
l'esterno;
• il suo funzionamento interno, ovvero che
cosa fa il sistema e/o come lo fa.

In VHDL il modello di un sottosistema è detto


design entity ed è costituito da due parti: una entity
(che descrive l'interfaccia) e una architecture (che
descrive il funzionamento).
Nella descrizione hanno un ruolo fondamentale i segnali, rappresentazione dei wires (conduttori),
che spostano i dati tra parti diverse del sistema. Le porte dell'interfaccia esterna sono segnali
particolari perchè spostano i dati da e verso l'esterno.
I packages e le librerie sono moduli che possono contenere porzioni di codice isolate dal resto della
descrizione, allo scopo di rendere il codice riusabile.

3
Le entity, le architecture, i package (e i package body) sono detti design units. Un modello
praticamente è fatto di design units al cui interno si costruisce la descrizione dettagliata del sistema.
Il modello di un sistema si descrive in un file sorgente che contiene istruzioni VHDL in formato
testuale. Ogni istruzione si può estendere su più righe e deve terminare in un carattere di punto e
virgola ( ; ). Alcune istruzioni possono essere precedute da un'etichetta (label), una stringa seguita
da un carattere di due punti ( : ). L'etichetta serve a identificare l'istruzione nel sorgente o a dare il
nome a elementi definiti da alcune istruzioni. In genere non c'è distinzione tra maiuscole e
minuscole (case insensitive).
Il sorgente che segue è la design entity di una porta and a 4 ingressi e serve a dare un'idea di come
si presenta un modello VHDL:
entity andgate4 is
port(X1, X2, X3, X4: in bit;
Y: out bit);
end entity andgate4;

architecture arc1 of andgate4 is


begin
Y <= X1 and X2 and X3 and X4;
end architecture arc1;

Livelli di astrazione
Due esigenze contrapposte accompagnano il processo di modellazione: l'efficacia, esigenza che il
modello possa essere interpretato correttamente dallo strumento di sviluppo in fase di sintesi;
l'efficienza, esigenza del progettista di poter utilizzare un linguaggio descrittivo il più possibile
vicino alla propria visione del sistema da modellare. Questo ha a che fare con il concetto di
astrazione. Una descrizione più astratta dice solo cosa il sistema deve fare e non come, quindi è più
vicina alle esigenze descrittive del progettista. Una meno astratta invece contiene i dettagli
implementativi e dice come è fatto il sistema.
Il VHDL supporta tre livelli di astrazione. Il più basso è quello della Descrizione Strutturale: il
sistema viene rappresentato direttamente nella sua struttura come rete di porte logiche. Si indicano i
componenti del sistema e le interconnessioni tra di essi. Questa descrizione è equivalente a quella
che si ottiene con un disegno schematico, solo che non è grafica ma testuale.
La Descrizione Funzionale o Comportamentale (behavioural) è il livello di astrazione più alto.
Nel definire cosa il sistema deve fare, il progettista utilizza descrizioni algoritmiche fatte con
istruzioni procedurali simili a quelle dei linguaggi di programmazione software. In questo caso si
esprime il funzionamento del sistema come una funzione degli ingressi.
Ad un livello intermedio di astrazione c'è la Descrizione Dataflow, cosiddetta perchè contiene una
rappresentazione del flusso dei dati, fatta per mezzo di equazioni logiche in cui si descrive l'effetto
su segnali in uscita da un flusso in risposta a eventi su segnali in ingresso al flusso, ossia come i
segnali si propagano dai pin d'ingresso a quelli di uscita. La design entity dell'esempio visto prima è
una descrizione dataflow. Ci sono importanti differenze fra una descrizione strutturale e una data-
flow:
➢ La descrizione strutturale:
✔ descrive il circuito come una netlist di porte fissate e disponibili in una libreria;
✔ avendo disponibili una implementazione delle porte necessarie (and, or, nor) si potrebbe
realizzare il circuito esattamente secondo lo schematico;
➢ La descrizione data-flow:

4
✔ descrive il legame ingresso-uscita (la tabella di verità) senza far riferimento ad una sua
implementazione concreta (e quindi non viene fatto alcun riferimento ad una libreria di
porte);
✔ si potrebbe realizzare con and-or-not, oppure con sole nand, o sole nor.
In generale è necessario un passo di sintesi per passare da una descrizione dataflow ad una
descrizione strutturale con componenti di libreria (descrizione gate level) che possa essere
implementata. Praticamente, la descrizione Gate Level, è una descrizione strutturale riferita ad una
specifica tecnologia. Tutte le porte usate fanno riferimento ad una tecnologia target in cui sono
accuratamente descritte ulteriori informazioni sulle porte (area, propagation delay, capacità di
ingresso, capacità di pilotaggio, ...).Una descrizione gate level è la descrizione con il minimo livello
di astrazione in VHDL, perché fornisce indicazioni estremamente dettagliate sul circuito, quali
massima frequenza di clock, area occupata, potenza dissipata, etc.
Un'altra descrizione è quella RTL (Register Transfer Level). A questo livello di astrazione il
design è diviso in 2 parti:
● una parte di logica combinatoria, che calcola il prossimo stato e le uscite a partire dallo stato
corrente e dagli input
● elementi di memoria controllati dai clock di sistema
Pertanto, si effettua una divisione tra i componenti che memorizzano i dati da quelli che li
elaborano.

Le simulazioni di circuiti descritti in maniera strutturale sono più onerose in termini di tempo e di
memoria perchè ci sono molto più dettagli da simulare rispetto ad un circuito descritto in maniera
comportamentale.

Il modello può contenere descrizioni di tipo diverso per parti diverse del sistema. Inoltre è evidente
che, se di una parte del sistema viene data una descrizione strutturale, il funzionamento interno dei
sottosistemi corrispondenti deve comunque essere definito da qualche parte nel progetto o in moduli
esterni.
Per poter realizzare il dispositivo, una descrizione funzionale o dataflow deve essere sintetizzata,
ovvero tradotta in una strutturale. La sintesi automatica dei costrutti funzionali però non sempre è
possibile. Quello che si può fare dipende dal supporto offerto dal tool di sintesi utilizzato.
I due esempi che seguono descrivono lo stesso dispositivo, uno shift register a 8 bit, in due modi
diversi:

entity shiftreg8 is
port (CLK, LOAD: in bit;
DATAin: in bit_vector (0 to 7);
DATAout: out bit_vector (0 to 7) );
end entity shiftreg8;

architecture arc1 of shiftreg8 is


signal DATAreg: bit_vector (0 to 7);
begin

shiftleft1: process
begin
wait until CLK'event and CLK = '1';
if (LOAD = '1') then
DATAreg <= DATAin;
else
DATAreg <= DATAreg (1 to 7) & '0';

5
end if;
end process shiftleft1;

DATAout <= DATAreg;

end architecture arc1;

-----------------------------
entity shiftreg8 is
port (CLK, LOAD: in bit;
DATAin: in bit_vector (0 to 7);
DATAout: out bit_vector (0 to 7) );
end entity shiftreg8;

architecture arc1 of shiftreg8 is


signal DATAreg: bit_vector (0 to 7);
begin

shiftleft1: sl1
port map (LD=>LOAD,
CK=>CLK,
Din=>DATAin,
Dreg=>DATAreg);

DATAout <= DATAreg;

end architecture arc1;

Nella prima design entity, shiftleft1 è una descrizione funzionale. Nella seconda essa è stata
sostituita con una descrizione strutturale: shiftleft1 richiama il componente sl1, che si suppone
definito da qualche altra parte, e mappa le sue porte con i segnali locali.

Istanza e Inferenza
Quando una parte del modello viene descritta in modo non astratto, ovvero esplicitandone la rete
logica corrispondente, essa verrà implementata così come descritta, con i componenti richiesti.
L'istanza è la richiesta diretta di un componente in un modello.
Quando invece una parte della descrizione è fatta in modo astratto, ovvero senza esplicitarne la rete
logica corrispondente, questa rete viene implicitamente creata dal sintetizzatore con i componenti
che esso ritiene opportuni a realizzare la funzione richiesta. Una sintesi di componenti fatta in
questo modo è detta inferenza.
L'inferenza è un supporto alla indipendenza tecnologica, l'esigenza di mantenere il modello
svincolato da riferimenti a particolari dispositivi, affinchè lo stesso modello possa essere riutilizzato
ed implementato con tecnologie diverse, o semplicemente perchè possa essere facilmente letto e
interpretato.
Si usa l'inferenza quando ad esempio si vuole generare automaticamente logica sequenziale senza
istanze dirette di registri, a partire da una descrizione funzionale.
È evidente che il risultato di un'inferenza deve essere chiaro al progettista già in fase di disegno, per
evitare errori o configurazioni di componenti indesiderati. Per questo si devono adottare particolari
accorgimenti nella codifica affinchè la sintesi non produca un latch o un flip flop dove si prevedeva
una pura rete combinatoria.

6
Nel primo esempio dello shift register, DATAreg viene sintetizzato per inferenza con 8 flip flop. Nel
secondo shift register, shiftleft1 è una istanza di componente.

Concorrenza e Sequenzialità
Per evitare confusione è meglio chiarire subito che la sequenzialità è una cosa diversa dalla logica
sequenziale.
Quando si descrive come è fatto un
sistema si mettono insieme delle parti
che in genere operano
contemporaneamente. Il fatto che un
linguaggio testuale imponga
l'indicazione delle parti in sequenza,
non significa necessariamente che
esse debbano agire nella sequenza
indicata. Invece una descrizione
algoritmica del funzionamento di una
parte è fatta di operazioni eseguite in
sequenza.

In un sorgente VHDL le aree di


codice disponibili a descrizioni
concorrenti o sequenziali sono dette
rispettivamente aree concorrenti e
aree sequenziali. Nelle prime si usano istruzioni concorrenti, la cui esecuzione è indipendente
dall'ordine in cui vengono scritte. Nelle seconde si usano istruzioni sequenziali che vengono
eseguite nell'ordine in cui sono scritte.
Le istruzioni da utilizzare dipendono dal tipo di astrazione desidarata. Tra le istruzioni concorrenti,
le istanze di componenti producono descrizioni strutturali perchè richiamano, in un punto del
modello, copie di componenti riusabili definiti da un'altra parte; gli assegnamenti di segnali
producono descrizioni dataflow perchè rappresentano equazioni logiche. I processi sono istruzioni
concorrenti ma il loro scopo è quello di contenere istruzioni sequenziali per produrre descrizioni
funzionali. Le principali istruzioni sequenziali sono le selezioni, le iterazioni e gli assegnamenti.
Nell'esempio dello shift register, il process e la successiva istruzione di assegnamento a DATAout,
sono istruzioni concorrenti. Le istruzioni contenute nel process sono sequenziali.
Design Entity
La minima descrizione di un sistema in VHDL è la design entity, rappresentazione di un
sottosistema fatta in due sezioni separate: l'entity per l'interfaccia e l'architecture per il
funzionamento.
Entità
L'interfaccia della design entity rappresenta il sottosistema esternamente per mezzo dei canali che
servono a interagire con altri sistemi. L'istruzione entity dichiara una nuova design entity, ne
definisce il nome e, al suo interno, l'istruzione port definisce le porte di ingresso e uscita
dell'interfaccia.
Le porte sono segnali disponibili all'interno, per l'architettura, e all'esterno, per il collegamento con
altre entità. Per ogni porta si devono definire: il nome del segnale, il modo o direzione e il tipo di

7
dato. I modi principali sono:
• in: direzione in ingresso, i dati entrano nel sistema; l'architettura interna può solo leggere
questo segnale, mentre un'altra entità esterna può solo scriverlo;
• out: direzione in uscita, i dati escono dal sistema; l'architettura interna può solo scrivere
questo segnale, mentre un'altra entità esterna può solo leggerlo.
• inout: il segnale può essere utilizzato sia come ingresso che come uscita (es. bus dati
bidirezionale)
• buffer: la porta è di uscita ma il suo valore può essere letto all'interno dell'entità.

Sintassi
entity nome_entità is
[port ( nome_porta {, nome_porta} : modo tipo {;
nome_porta {, nome_porta} : modo tipo } ) ;]
{dichiarazione}
end [entity] [nome_entità] ;

Esempi
Nell'esempio seguente l'entity ha descrive l'interfaccia di un half-adder con gli addendi A e B in
ingresso, la somma S e il carry (riporto) C in uscita:
entity ha is
port (A, B: in bit;
S, C: out bit);
end;

Oltre alle porte, l'entity può anche dichiarare segnali che l'architettura userà internamente. Ad
esempio qui S1 è un segnale di tipo bit, ma non è una porta, quindi non è disponibile all'esterno,
ovvero non fa parte dell'interfaccia:
entity ha is
port (A, B: in bit;
S, C: out bit);
signal S1: bit;
end;

Architettura
Dopo aver definito l'interfaccia di una design entity, si passa a descrivere il suo effettivo
funzionamento interno con un'architecture. Il corpo (body), che inizia con la parola chiave begin, è
la parte dell'architettura dove si inseriscono le istruzioni descrittive ed è un'area concorrente. Il
corpo può essere preceduto da un'area dichiarativa. Nell'architettura si possono utilizzare come
segnali le porte dichiarate nell'entità.

Sintassi
architecture nome_architettura of nome_entità is
{dichiarazione}
begin
{istruzione_concorrente}
end [architecture] [nome_architettura] ;

8
Esempi
In questo esempio l'architecture arc1 descrive il funzionamento della design entity ha, la cui entity
è stata già descritta. Il body contiene due istruzioni concorrenti che operano sui segnali A, B, S, C,
che sono stati dichiarati come porte nell'entity e non necessitano di ulteriore dichiarazione
nell'architecture.
architecture arc1 of ha is
begin
S <= A xor B;
C <= A and B;
end;

La prima istruzione assegna a S la somma logica esclusiva di A e B, realizzando la prima parte della
funzione di un half-adder. La seconda istruzione si occupa del riporto, assegnando a C il prodotto
logico degli ingressi. La descrizione è di tipo dataflow perchè le istruzioni rappresentano equazioni
logiche in ognuna delle quali i segnali A e B rappresentano gli ingressi di un flusso di dati, i segnali
S o C rappresentano l'uscita dal flusso.
La design entity completa dell'half-adder è questa:
library ieee;
use ieee.std_logic_1164.all;

entity ha is
port (A, B: in std_logic;
S, C: out std_logic);
end entity ha;

architecture arc1 of ha is
begin
S <= A xor B;
C <= A and B;
end architecture arc1;

Istruzioni Concorrenti
Le aree concorrenti sono i corpi delle architetture. Le istruzioni concorrenti ammesse in queste aree
sono eseguite contemporaneamente, indipendentemente dall'ordine in cui sono scritte.
Istanze di componenti
Un componente è una design entity che viene riusata più volte nel modello. Creare un'istanza di un
componente significa usare una copia di una design entity che è già stata definita da qualche altra
parte.
Un'istanza è un'istruzione concorrente che crea la copia del componente nel punto desiserato del
modello e collega le sue porte ad altri segnali dell'architettura. In questo modo si ottiene una
descrizione strutturale gate-level equivalente a quella che si disegnerebbe in uno schematico.
Si può procedere in due modi:
• istanza diretta: l'istruzione fa riferimento direttamente alla entity da usare come
componente;
• istanza indiretta: prima dell'istanza si deve dichiarare un component, che fa riferimento
direttamente alla entity da usare come componente; poi l'istanza farà riferimento al
component.

9
Il metodo diretto è più semplice. D'altra parte il metodo indiretto è adatto ai progetti gerarchici
complessi dove si procede dall'alto verso il basso e gli elementi terminali si disegnano alla fine
(l'interfaccia però deve essere già definita al momento dell'istanza).
Nell'istruzione di istanza si deve definire il nome da assegnare all'istanza, indicare il nome
dell'elemento istanziato e mappare le porte. L'istruzione da utilizare nei due casi è la stessa, salvo
che il nome dell'elemento istanziato indica due cose diverse.
Nella port map (mappatura delle porte) si associano le porte (parametri formali) del componente
istanziato, a segnali (parametri attuali) dell'architettura corrente. La mappatura si può definire in
due modi:
• associazione posizionale: si inserisce la lista dei segnali attuali nello stesso ordine in cui
sono definite le porte del componente istanziato;
• associazione per nome: si inserisce la lista delle coppie di oggetti formale e attuale (separati
dal simbolo =>) in un ordine qualsiasi.
Una porta non connessa può essere semplicemente omessa nella port map, ma questo è sconsigliato.
Invece si può mappare esplicitamente la porta alla parola chiave open.
La dichiarazione del component va fatta nell'area dichiarativa di un'architettura o di un package. Il
nome del component e la sua dichiarazione delle porte devono essere quelli dell'entity a cui si
riferisce affinchè un component faccia riferimento implicitamente a una precisa entity. Quando si
utilizza un componet occorre specificare l'architettura a cui dovrà riferirsi, tramite l'istruzione di
configuration specification.

Sintassi
Dichiarazione di un component:
component nome_componente [is]
[port ( nome_porta {, nome_porta} : modo tipo {;
nome_porta {, nome_porta} : modo tipo } ) ;]
end component

Specificazione della configurazione di un component:

FOR ALL | elenco di label : nome_componente


USE ENTITY nome_libreria.nome_entity(nome_architecture);

Istanza di component o di entity:


nome_istanza :
([component] nome_componente)
| (entity nome_entity [ ( nome_architettura ) ])
[port map ( [formale =>] attuale {,
[formale =>] attuale } )] ;

Esempi
Un full-adder serve a sommare due cifre binarie in
una posizione successiva alla prima in un numero
binario a più cifre. Si può realizzare con due half-
adder: il primo si occupa della somma corrente; il
secondo aggiunge il carry proveniente dalla somma

10
delle cifre della posizione precedente. La seguente descrizione strutturale definisce un full-adder,
supponendo di aver precedentemente definito la design entity ha di un half-adder:
entity fa is
port(X, Y, Cin: in bit;
Z, Cout: out bit);
end entity fa;

architecture afa of fa is
signal S1, C1, C2: bit;
begin

ha1: entity ha
port map (A=>X, B=>Y, S=>S1, C=>C1);

ha2: entity ha
port map (A=>S1, B=>Cin, S=>Z, C=>C2);

Cout <= C1 or C2;

end architecture afa

Il full-adder fa dispone di 5 porte: in ingresso X e Y sono le cifre da sommare e Cin è il carry


proveniente da una somma precedente. In uscita Z è la somma e Cout è il carry. Inoltre sono stati
dichiarati i segnali interni S1, C1 e C2 che serviranno alle connessioni interne con le istanze.
La design entity di ha con ingressi A, B e uscite S, C è stata già definita. ha1 e ha2 sono istanze
dirette dell'entity ha. Le mappature sono eseguite per nome. La porta A della prima istanza dell'half-
adder è mappata sul segnale X del full-adder, la porta B a Y, ecc. L'ordine delle mappature per
nome non è importante, infatti la seguente istanza ha1 sarebbe stata ugualmente valida:
ha1: entity ha
port map (S=>S1, A=>X, C=>C1, B=>Y);

Volendo utilizzare una mappatura posizionale si dovrebbe rispettare l'ordine dei parametri:
ha1: entity ha
port map (X, Y, S1, C1);

L'esempio seguente mostra lo stesso full-adder ottenuto con istanze indirette:


entity fa is
port(X, Y, Cin: in bit;
Z, Cout: out bit);
end entity fa;

architecture afa of fa is

component ha is
port (A, B: in bit;
S, C: out bit);
end component;

signal S1, C1, C2: bit;

begin

ha1: component ha
port map (A=>X, B=>Y, S=>S1, C=>C1);

11
ha2: component ha
port map (A=>S1, B=>Cin, S=>Z, C=>C2);

Cout <= C1 or C2;


end architecture afa

L'area dichiarativa dell'architettura fa contiene la dichiarazione del component ha che fa riferimento


all'entity con lo stesso nome definita precedentemente. Le istanze ha1 e ha2 questa volta fanno
riferimento al component.
Assegnamenti concorrenti di segnali
In un assegnamento concorrente un segnale indicato alla sinistra di un'equazione logica riceve un
valore risultante da un'espressione indicata a destra che elabora eventualmente altri segnali.
Istruzioni di assegnamenti concorrenti formano descrizioni di tipo dataflow.
Se i segnali sono porte dell'interfaccia esterna, allora devono essere di modo in o inout quando
vengono letti per essere valutati in un'espressione a destra nell'istruzione, di modo out o inout
quando ricevono un assegnamento a sinistra nell'istruzione.
Un assegnamento concorrente viene eseguito immediatamente ogni volta che si verifica un evento
su un segnale del lato destro dell'istruzione.
Gli assegnamenti concorrenti sono di tre tipi:
• assegnamento semplice: un'espressione viene assegnata ad un segnale; è un caso particolare
dell'assegnamento condizionale;
• assegnamento condizionale: una sequenza di condizioni decide quale espressione assegnare
ad un segnale; è equivalente a inserire gli assegnamenti in un'istruzione di selezione
prioritaria (if); In generale le condizioni possono sovrapporsi ed in questo caso l'ordine di
valutazione delle condizioni (dal primo fino all'else finale) è decisivo: vengono valutate
sequenzialmente le condizioni e la prima che è vera determina l’assegnazione. Una
assegnazione condizionata deve terminare con una else senza condizione.
• assegnamento selettivo: il valore di un'espressione di controllo decide quale tra diverse
espressioni assegnare ad un segnale; è equivalente a inserire gli assegnamenti in
un'istruzione di selezione parallela (case).I casi devono essere esaustivi (a meno che non si
usi un caso others). I casi non si possono sovrapporre, ovvero devono essere mutuamente
esclusivi.

È possibile definire un ritardo nella propagazione della forma d'onda sul segnale, con la parola
chiave after. Si può anche specificare il meccanismo di ritardo:
• transport, ritardo di propagazione: tutte le parti della forma d'onda si propagano sul segnale
dopo il ritardo definito;
• inertial, ritardo inerziale: si propagano sul segnale (dopo un ritardo di propagazione
definito) solo le parti della forma d'onda di durata non inferiore a un valore definito, che può
essere uguale al ritardo di propagazione; questo meccanismo serve a sopprimere dal segnale
gli spikes di durata inferiore a un certo valore.
Gli assegnamenti ritardati sono inerziali se non specificato diversamente. Inoltre è possibile definire
più espressioni da assegnare con ritardi diversi.

12
Sintassi

Assegnamento condizionale (include l'assegnamento semplice come caso particolare):


[etichetta :]
nome_segnale <= [([reject tempo] inertial) | transport]
espressione [after ritardo] {, espressione [after ritardo]}
[when condizione] { else
[([reject tempo] inertial) | transport]
espressione [after ritardo] {, espressione [after ritardo]}
[when condizione | others] } ;

Assegnamento selettivo:
[etichetta :] with espressione select
nome_segnale <= [([reject tempo] inertial) | transport]
espressione [after ritardo] {, espressione [after ritardo]}
when valore { ,
[([reject tempo] inertial) | transport]
espressione [after ritardo] {, espressione [after ritardo]}
when valore | others } ;

Esempi
Un assegnamento semplice (notare l'operatore <= usato per l'assegnamento di soli segnali):
A <= '1';

Assegnamento ritardato, dopo 5 ns qualunque segnale in B si propaga su A:


A <= transport B after 5 ns;

Assegnamento ritardato inerziale, B si propaga su A dopo 5 ns e tutti gli spikes su B più brevi di 5
ns sono soppressi:
A <= B after 5 ns;

Assegnamento ritardato inerziale, B si propaga su A dopo 5 ns e tutti gli spikes su B più brevi di 2
ns sono soppressi:
A <= reject 2ns inertial B after 5 ns;

Nell'assegnamento seguente A diventa 1 dopo 5 ns, poi dopo essere stato ad 1 per 3 ns, diventa 0:
A <= '1' after 5 ns, '0' after 3 ns;

L'esempio seguente modella un multiplexer con un assegnamento condizionale. Il segnale di


selezione SEL decide quale dei 4 segnali di ingresso deve essere propagato sul segnale di uscita.
Quando SEL vale "00", su Y si propaga X0, ecc.:
Y <= X0 when SEL = "00" else
X1 when SEL = "01" else
X2 when SEL = "10" else
X3 when SEL = "11";

In generale:
segnale <= waveform_1 WHEN condizione_1 ELSE
waveform_2 WHEN condizione_2 ELSE

waveform_(n-1) WHEN condizione_(n-2) ELSE

13
waveform_n;

Anche quest'ultimo esempio è un multiplexer, ma è modellato con un assegnamento selettivo.


Inoltre qui SEL è largo 3 bit e può assumere 8 valori mentre ci sono solo 4 ingressi. La parola
chiave others serve ad indicare in una volta sola tutti i valori di SEL non ancora elencati:
with SEL select
Y <= X0 after 2ns when "000",
X1 after 2ns when "001",
X2 after 2ns when "010",
X3 after 2ns when others;

In generale:
WITH segnale_di_selezione SELECT
segnale <= waveform_1 WHEN caso_1,
waveform_2 WHEN caso_2,

waveform_n WHEN caso_n;

Processi

Un process è un'istruzione concorrente che contiene un'area sequenziale. Si usa nel corpo di
un'archittetura. Le istruzioni sequenziali che può contenere si inseriscono nel suo body, che inizia
con la parola chiave begin e può essere preceduto da un'area dichiarativa. La descrizione fatta in un
processo è di tipo funzionale.

Un processo viene eseguito parallelamente alle altre istruzioni concorrenti. L'esecuzione del suo
body può essere condizionata da una sensitivity list, una lista di segnali. Il processo viene eseguito
una volta e poi rimane sospeso. L'esecuzione riparte solo se si verificano eventi sui segnali della
sensitivity list. La sensitivity list non si usa quando il body contiene un'istruzione wait. In questo
caso l'esecuzione viene avviata e si sospende nel modo indicato dall'istruzione wait. Solo statement
sequenziali sono leciti nel corpo di un process. Le assegnazioni '<=' sono lecite (come vedremo
l’assegnazione di segnale '<=' è considerata sequenziale all’interno di un process); le assegnazioni
selezionate e condizionate non sono considerate concorrenti.
Un processo ha visibilità di tutti gli oggetti definiti nella sua architettura: in altre parole, lo scope di
un process è lo stesso della architettura che lo contiene. Le dichiarazioni nella parte dichiarativa di
un process sono invece locali allo stesso. Da notare che i segnali assegnati all'interno di un processo
ricevono il valore solo dopo la sua sospensione e che le variabili locali conservano il proprio valore
nel tempo tra un'esecuzione e l'altra. Inoltre, se si incontrano più assegnazioni di segnale all'interno
dello stesso process, solo l'ultima è valida. Se ad un segnale è assegnato un valore in un process, ed
esso compare nella sensitivity list dello stesso process, allora un evento su tale segnale può
riattivare il processo. L'unico modo che ha un processo quindi per comunicare con l'esterno è
tramite i segnali di cui ha visibilità e che assegna e legge. Non è possibile dichiarare segnali
all'interno di un process, ma solo variabili; queste ultime non possono essere condivise fra processi.

Sintassi
[nome_processo :] process [(sensitivity_list)]
{dichiarazione}
begin
{istruzione_sequenziale}
end process [nome_processo] ;

14
Esempi
Nel primo esempio il processo p1 esegue due assegnamenti, il primo sul segnale A e il secondo sul
segnale C, in sequenza nell'ordine indicato. La sensitivity list, che segue la parola chiave process,
indica che l'esecuzione si riavvia ogni volta che si verifica un evento sui segnali B o C. Notare che
A non compare nella sensitivity list:
p1: process (B, C)
begin
A <= B and C;
C <= '0';
end;

Una descrizione equivalente si ottiene sostituendo la sensitivity list del processo con un'istruzione
wait:
p1_2: process
begin
A <= B and C;
C <= '0';
wait on B, C;
end;

Il processo p2 scambia i valori dei segnali A e B servendosi della variabile TMP. Questa variabile è
dichiarata come array di 4 bit, quindi si suppone che i due segnali siano stati dichiarati dello stesso
tipo, fuori dal processo:
p2: process (A, B)
variable TMP: bit_vector (3 downto 0);
begin
TMP := A;
A <= B;
B <= TMP;
end;

Il processo p3 simula un clock con ciclo di 10 ns. Non ha sensitivity list e la sospensione è
controllata dall'istruzione wait:
p3: process
begin

CLK <= '1';


wait for 5 ns;
CLK <= '0';
wait for 5 ns;

end;

L'esempio seguente è la design entity completa di un flip-flop. Il processo p4 non ha sensitivity list
ed è attivato dai fronti di salita del segnale CLK attraverso l'istruzione wait. Quando questo si
verifica, l'uscita Q riceve il valore dell'ingresso D.
library ieee;
use ieee.std_logic_1164.all;

entity eff is
port (D, CLK: in std_logic;
Q: out std_logic);
end entity eff;

15
architecture aff of eff is
begin

p4: process
begin
wait until CLK'event and CLK = '1';
Q <= D;
end process p4;

end architecture aff;

Questa configurazione di un processo, dove si analizza il fronte di un segnale, genera per inferenza
un flip-flop. In generale, utilizzando in modo opportuno un processo con istruzioni di selezione o
con l'istruzione wait si può ottenere l'inferenza di latch, flip-flop, o buffer Tri-State.
Notare che negli esempi precedenti l'attenzione è rivolta al contenuto sequenziale del processo.
Comunque ogni processo è un'istruzione concorrente e quindi, se ci sono più processi, questi sono
attivati in modo concorrente, e non in sequenza. Nell'esempio seguente, ognuno dei tre processi pa,
pb, pc, è attivato dalla propria sensitivity list o da istruzioni wait interne, indipendentemente dagli
altri processi. Poi all'interno del singolo processo le istruzioni sono eseguite in sequenza:
architecture aaa of eee is
begin

pa: process
begin
-- istruzione sequenziale
-- ...
end;

pb: process
begin
-- istruzione sequenziale
-- istruzione sequenziale
-- ...
end;

pc: process
begin
-- istruzione sequenziale
-- istruzione sequenziale
-- ...
end;

end;

La simulazione VHDL di un processo prevede due fasi:

● Inizializzazione → Al tempo 0+0δ della simulazione ai segnali vengono assegnati i loro


valori iniziali, valori esplicitamente assegnati oppure i valori di default per il loro tipo.
Vengono eseguiti tutti i processi finchè tutti non raggiungono lo stato di sospensione.
● Esecuzione → Terminata la fase di inizializzazione, la simulazione è pilotata dagli eventi
esplicitamente indicati: un process esegue tutte le istruzioni sequenziali e poi le ripete
ripartendo dall'inizio.

16
Aree Sequenziali

Le aree sequenziali sono il corpo di un processo e il corpo di un sottoprogramma. Le istruzioni


sequenziali ammesse in queste aree vengono eseguite nell'ordine in cui sono scritte e la descrizione
che ne deriva è di tipo funzionale.

Assegnamenti sequenziali di segnali e variabili

In un assegnamento sequenziale un oggetto segnale o variabile, indicato a sinistra di un'equazione


logica riceve un valore risultante da un'espressione indicata a destra che elabora eventualmente altri
segnali o variabili.

Se i segnali sono porte, allora devono essere di modo in o inout quando vengono letti per essere
valutati in un'espressione a destra nell'istruzione, di modo out o inout quando ricevono un
assegnamento a sinistra nell'istruzione.
Lo scopo di un assegnamento di segnale è quello di trasportare i dati da una parte all'altra del
sistema (e questo vale anche in aree concorrenti, anche se con forme diverse dell'istruzione). Una
variabile invece serve a conservare il valore di un dato in una sequenza di istruzioni, quindi per
loro natura gli assegnamenti di variabili sono ammessi solo in aree sequenziali.
Diversamente dagli assegnamenti concorrenti (che sono indipendenti dal tempo ed eseguiti
parallelamente), quelli sequenziali sono eseguiti appunto in sequenza. Ma, mentre una variabile
viene assegnata nel momento in cui l'istruzione è valutata, un assegnamento di segnale viene prima
valutato e poi l'assegnamento vero e proprio viene eseguito quando il processo o il sottoprogramma
corrente è sospeso.
È possibile definire un ritardo nella propagazione della forma d'onda su un segnale, con gli stessi
meccanismi degli assegnamenti concorrenti.

Sintassi
Assegnamento sequenziale di segnale:
[etichetta :]
nome_segnale <= [([reject tempo] inertial) | transport]
espressione [after ritardo] {, espressione [after ritardo]} ;

Assegnamento sequenziale di variabile:


[etichetta :] nome_variabile := espressione ;

Esempi
Gli assegnamenti sequenziali di segnali sono identici agli assegnamenti concorrenti semplici di
segnali. Per gli assegnamenti di variabili c'è solo da notare il simbolo di assegnamento := :
a := 1;

Selezioni
Una selezione permette a un flusso di istruzioni sequenziali di decidere quale direzione prendere fra
più alternative. In base alla selezione, viene eseguita una tra più sequenze di istruzioni. Le selezioni
sono di due tipi:

17
• selezione prioritaria, if: una condizione viene valutata e, in base al suo risultato booleano,
viene selezionata una tra due alternative; quindi nella selezione di più di due alternative
(ottenuta con if annidati) le prime condizioni avranno la priorità;
• selezione parallela, case: un'espressione viene valutata e, in base al suo risultato, viene
selezionata una tra più alternative; quindi si possono utilizzare allo stesso livello prioritario
più di due alternative.

Sintassi
Selezione if:
[etichetta :]
if condizione then
{istruzione_sequenziale}
{elsif condizione then
{istruzione_sequenziale}}
[else
{istruzione_sequenziale}]
end if [etichetta] ;

Selezione case:
[etichetta :]
case espressione is
when valore =>
{istruzione_sequenziale}
{when valore =>
{istruzione_sequenziale}}
[when others =>
{istruzione_sequenziale}]
end case [etichetta] ;

Esempi
Una selezione prioritaria. Se la condizione A = B è verificata allora a C viene assegnato il valore di
D, altrimenti a C viene assegnato il valore di E:
if A = B then
C <= D;
else
C <= E;
end if;

Un multiplexer a 4 ingressi modellato con una selezione prioritaria a 4 alternative. Se S = "00"


sull'uscita Y si propaga l'ingresso A, ecc.:
if S = "00" then
Y <= A;
elsif S = "01" then
Y <= B;
elsif S = "10" then
Y <= C;
else
Y <= D;
end if;

Lo stesso multiplexer, ma modellato con una selezione parallela:

18
case S is
when "00" => Y <= A;
when "01" => Y <= B;
when "10" => Y <= C;
when "11" => Y <= D;
end case;

Notare che tutti gli esempi precedenti descrivono selezioni complete perchè trattano tutte le
possibili alternative. Come risultato la logica generata è sempre combinatoria perchè si genera
sempre un multiplexer. Per capire quale sarebbe l'effetto di una selezione incompleta consideriamo
questo esempio:
if SEL = '0' then
Y <= A;
end if;

L'effetto sarà sul valore di Y, per questo l'istruzione viene detta assegnamento incompleto.
Supponiamo che SEL sia di tipo bit e che quindi possa assumere i due valori '0' e '1'. Quando SEL =
'0', Y riceve il valore di A. Poichè non è specificato nulla per il caso SEL = '1', si suppone che Y
debba conservare il valore che contiene e quindi sarà sintetizzato come un latch. Questa istruzione
quindi genera logica sequenziale per inferenza. Un esempio completo di latch è questo:
library ieee;
use ieee.std_logic_1164.all;

entity elatch is
port (D, EN: in std_logic;
Q: out std_logic);
end entity elatch;

architecture alatch of elatch is


begin

platch: process (D, EN)


begin
if EN = '1' then
Q <= D;
end if;
end process platch;

end architecture alatch;

Per evitare di generare un latch quando si vuole generare logica combinatoria, si devono specificare
tutte le alternative della selezione, come nei primi tre esempi.
Un'altra situazione in cui una selezione genera logica sequenziale per inferenza si verifica quando
nell'espressione condizionale si analizza il fronte di un segnale:
library ieee;
use ieee.std_logic_1164.all;

entity eff is
port (D, CLK: in std_logic;
Q: out std_logic);
end entity eff;

19
architecture aff of eff is
begin

pff: process (CLK)


begin
if (CLK'event and CLK = '1') then
Q <= D;
end if;
end process pff;

end architecture aff;

Questo caso è simile al precedente perchè si deve generare un elemento di memoria. Però questa
volta l'elemento è un flip-flop perchè la condizione analizza il fronte del segnale CLK: l'espressione
CLK'event risulta vera quando CLK cambia; quindi (CLK'event and CLK = '1') è vera quando su
CLK si verifica un fronte di salita.
Si può ottenere lo stesso risultato anche con una wait, eliminando la sensitivity list del processo:
pff: process
begin
wait until CLK'event and CLK = '1';
Q <= D;
end process pff;

Iterazioni
Per eseguire ripetutamente una serie di istruzioni sequenziali si usa l'istruzione loop (iterazione). Ci
sono tre tipi di loop:
• loop con schema di iterazione while: l'esecuzione è controllata dal verificarsi di una
condizione specificata;
• loop con schema di iterazione for: l'esecuzione è controllata dalla variazione del valore di
un parametro specificato;
• loop senza schema di iterazione: l'esecuzione è controllata solo dalle stesse istruzioni
interne;
Inoltre ci sono due istruzioni sequenziali particolari che si usano all'interno di un ciclo: next termina
l'esecuzione delle istruzioni e salta alla ripetizione successiva; exit termina ed esce dal loop. Queste
istruzioni sono l'unico modo di uscire da un loop del terzo tipo.

Sintassi

Loop:
[etichetta :]
[while condizione] | [for nome in range] loop
{istruzione_sequenziale}
end loop [etichetta] ;

Istruzione next in un loop:


[etichetta_next :] next [etichetta_loop] [when condizione] ;

Istruzione exit in un loop:


[etichetta_exit :] exit [etichetta_loop] [when condizione] ;

20
Esempi
Un esempio di clock, con un loop infinito, senza schema di iterazione. Il segnale CLK viene
invertito e l'istruzione wait sospende il loop per un tempo pari alla metà del ciclo di clock definito
esternamente dalla costante CLK_CYCLE. L'istruzione exit termina ed esce dal loop se il segnale
STOP è '1', altrimenti il loop ricomincia:
loop
CLK <= not CLK;
wait for CLK_CYCLE / 2;
exit when STOP = '1';
end loop;

Lo stesso clock, realizzato con un loop di tipo while. Il loop continua finchè il segnale STOP è
diverso da 0:
while STOP /= '0' loop
CLK <= not CLK;
wait for CLK_CYCLE / 2;
end loop;

Un ciclo for per sommare i 100 elementi di un vettore X, escluso il decimo. La variabile I è il
parametro che controlla la ripetizione del ciclo 100 volte (da 0 a 99) e vale come indice per il
vettore X. La variabile S somma l'elemento corrente alla somma accumulata (che all'inizio è 0).
Quando I indica la posizione 9, l'istruzione next salta alla ripetizione successiva, evitando
l'esecuzione della somma:
for I in 0 to 99 loop
next when I := 9;
S := S + X(I);
end loop;

Sospensioni dell'esecuzione
Un flusso di istruzioni sequenziali può essere sospeso in tre modi:
• specificando la durata della pausa;
• specificando una condizione che deve essere verificata per poter continuare;
• specificando una lista di segnali (sensitivity list) su cui si deve verificare un evento per poter
continuare;
L'istruzione wait sospende l'esecuzione in un modo definito da una combinazione dei tre modi
precedenti.
Un processo non deve avere la sensitivity list se contiene una wait.

Sintassi
wait [on sensitivity_list] [until condizione] [for tempo] ;

Esempi
Esecuzione sospesa finchè non cambia uno dei segnali EN, X, Y:
wait on EN, X, Y;

Attende che il segnale di clock CLK torni ad '1':


wait until CLK = '1';

21
Si ferma per 8 ns:
wait for 8 ns;

L'esecuzione è sospesa e riprende se si verifica un evento su uno dei segnali EN, X, Y oppure se
CLK diventa '1', ma riprende comunque dopo 8 ns:
wait on EN, X, Y until CLK = '1' for 8 ns;

Sospensione permanente:
wait;

Una sensitivity list è equivalente ad una istruzione del tipo wait on sensitivity list; posta alla fine del
processo.

Dati ed elementi lessicali


Segnali, variabili e costanti
Gli elementi del linguaggio che servono a rappresentare i dati trattati nel modello sono di tre tipi e,
nel loro insieme, sono detti oggetti. Indipendentemente dal tipo, ogni oggetto è definito da un
nome, dal tipo di dato che può rappresentare e dal valore del dato. Le prime due informazioni
vengono date nella dichiarazione dell'oggetto che si deve fare in un'area dichiarativa prima di usare
l'oggetto. Il valore viene dato in un'istruzione di assegnamento oppure nella inizializzazione che è
un assegnamento iniziale incluso nella dichiarazione.
I segnali servono a modellare i wires (i conduttori) che portano i dati da una parte all'altra del
sistema. Le dichiarazioni di segnali sono ammesse in tutte le aree dichiarative tranne quelle di
funzioni e processi.
Le variabili hanno la funzione di conservare i dati nelle sequenze di istruzioni (cosa che un segnale
non può fare da solo) e sono l'equivalente delle variabili dei linguaggi di programmazione software.
L'utilizzo di una variabile ha senso solo nell'ambito di un algoritmo e quindi è ammesso solo in
un'area sequenziale.
La dichiarazione in genere si fa nell'area dichiarativa del processo o del sottoprogramma
corrispondente. Tuttavia sono ammesse dichiarazioni globali nelle aree dichiarative di entità,
architetture e packages che permettono ad una variabile di subire modifiche anche fuori dai confini
di una stessa area sequenziale. Questa funzionalità si ottiene con la parola chiave shared.
Le costanti sono uno strumento per rendere più comoda e più leggibile la descrizione. Ad una
costante si assegna un valore una volta per tutte e poi il nome viene usato al posto del valore. Si
possono dichiarare in tutte le aree dichiarative.

Sintassi
Dichiarazione di signal:
signal nome {, nome} : tipo_dato [range] [:= espressione] ;

Dichiarazione di variable:
[shared] variable nome {, nome} : tipo_dato [range] [:= espressione] ;

Dichiarazione di constant:

22
constant nome {, nome} : tipo_dato [range] [:= espressione] ;

Esempi
Questa design entity contiene esempi di dichiarazioni dei tre tipi di oggetti:
entity eee is
port (A, B: inout std_logic);
shared variable X: boolean;
constant BUSW: integer := 16;
end entity eee;

architecture aaa of eee is


signal C: std_logic_vector (7 downto 0);
signal D: std_logic_vector range 7 downto 0;
signal E: std_logic_vector (1 to 8);
begin

-- ...

prc1: process
variable Y: bit := false;
begin
-- ...
end process prc1;

-- ...

end architectire aaa;

Le porte A e B sono segnali particolari, perchè dichiarati implicitamente come tali nella
dichiarazione delle porte dell'entity e perchè a differenza dei semplici signal, sono accessibili anche
dall'esterno della design entity.
C, D ed E sono segnali dichiarati nell'architettura aaa. Il range (7 downto 0) indica che i valori
dell'array sono 8 e gli indici si estendono da 7 a 0. Il range range 7 downto 0 è equivalente. Il terzo
range ha direzione opposta. Il tipo è std_logic_vector.
Y è una variabile di tipo bit dichiarata e inizializzata al valore false nel processo prc1. X è una
variabile di tipo boolean dichiarata nell'entità eee. Infine BUSW è una costante di tipo integer
dichiarata e inizializzata al valore 16 nell'entità eee.
Un segnale e una variabile sono oggetti completamente diversi dal punto di vista della
tempificazione (anche se entrambi portano una informazione). Infatti, un'assegnazione di segnale
comporta la schedulazione di un evento, ma non ha mai l'effetto di una assegnazione immediata.
L'assegnazione di un valore ad una variabile non comporta lo scheduling di nessun evento ed ha
l'effetto di una assegnazione immediata. Consideriamo i due processi in figura.

23
Supponiamo di lavorare con il primo processo e che ci sia un evento sul segnale ingr all'istante T,
ossia ingr = 1 → 0 @T. In tale istante il process viene attivato, si schedula l'assegnazione del valore
0 a tmp per l'istante di simulazione T+δ, ossia tmp = 1 → 0. Nello stesso istante T il flusso di
esecuzione del process incontra la seconda assegnazione su out che viene anch'essa schedulata per
l'istante T+δ. Il valore che viene assegnato ad out all'istante T+δ è not tmp, ma tmp vale ancora 1 e
quindi ad out sarà assegnato 0. Il processo raggiunge la fine e viene sospeso, ossia viene eseguito
per intero all'istante T. Da notare che le assegnazioni sono incontrate nello stesso istante di
simulazione T e schedulate entrambe all'istante T+δ, ma sequenzialmente. Il secondo processo
viene attivato dallo stesso evento su ingr all'istante T. L'esecuzione incontra l'assegnazione di
variabile tmp:=in che viene eseguita istantaneamente e quindi tmp vale 0 già all'istante T. La
successiva assegnazione su out, quindi, comporta che il valore di out divenga 1, poiché tmp è già 0
all'istante T.

Letterali
I letterali sono i valori assegnati agli oggetti o usati direttamente nelle espressioni senza l'ausilio di
nomi di oggetti. Si differenziano in 6 categorie che devono essere usate coerentemente con il tipo di
dato richiesto dal contesto.
Un valore carattere è costituito da un singolo carattere racchiuso tra due virgolette semplici ( ' ),
come 'A', '3', '%'.
Una stringa è una sequenza di caratteri racchiusa tra due caratteri di virgolette doppie ( " ) oppure
percento ( % ), come "ABC" e %ABC%. Nel primo caso, se la stringa deve contenere il carattere di
virgolette doppie, questo si inserisce raddoppiato, come in """". La stessa cosa vale per il secondo
caso con il carattere percento, come in %%%%. Non si può inserire una stringa su più righe.
Bisogna spezzarla in più stringhe e concatenarle con l'operatore di concatenazione ( & ), come:
str := "abc" &
"def" ;

Una stringa di bit è una rappresentazione di un numero in base binaria, ottale o esadecimale,
costituita da una sequenza di cifre della rispettiva base racchiusa tra virgolette doppie ( " ) o
caratteri percento ( % ) e preceduta rispettivamente dal carattere B, O, X. Per la base binaria, la B
può essere omessa.
Esempi:
B"01111011" forma binaria del decimale 123
equivalente al precedente; il carattere di sottolineatura ( _ ) si usa per migliorare la
B"0111_1011"
leggibilità e viene ignorato.
O"173" forma ottale del decimale 123
X"7B" forma esadecimale del decimale 123
X%7B% forma esadecimale del decimale 123

Un valore numerico è un numero intero o reale in base 10. Per i numeri reali è sempre necessario il
punto frazionario. Si può usare la notazione scientifica con la mantissa alla sinistra del carattere 'E' e
l'esponente a destra.
Esempi:
1234567 intero

24
1_234_567 equivalente al precedente
-45.0 reale negativo
67.8E12 reale 67.8·1012
91.2E-12 reale 91.2·10-12

Un valore numerico con base è un numero intero o reale in base 2, 8 o 16. È racchiuso tra due
caratteri cancelletto ( # ) oppure due punti ( : ) e preceduto dal valore della base. I valori reali
devono avere sempre il punto frazionario e possono essere seguiti dal carattere 'E' della notazione
scientifica e dall'esponente (per la relativa base 2, 8 o 16).
Esempi:
2#101101# intero binario
2:101101: intero binario
8#55# intero ottale
16#2D# intero esadecimale
2#1101.0#E3 reale binario (1101.02)·23

Un valore fisico rappresenta una misura di una grandezza fisica espressa come un numero intero
seguito da un'unità di misura. Esempi: 250 ns, 15 ma.
Tipi di dati
Il tipo di un dato è la definizione dei valori che il dato può assumere e delle operazioni ammesse su
di esso. Quando si dichiara un oggetto, si deve indicare il tipo di dato. Tutti i tipi si classificano
principalmente come tipi scalari (costituiti da un solo valore) o tipi composti (costituiti da insiemi
di valori). Un tipo scalare contiene un valore che deve appartenere a un range o a un insieme
enumerativo oppure un valore fisico. Un tipo composto contiene più valori di tipo scalare; se sono
dello stesso tipo è un array, se di tipo diverso è un record. I valori possibili rientrano sempre in una
delle categorie di letterali visti prima. Il VHDL mette a disposizione diversi tipi predefiniti ma
permette di definire propri tipi, sia scalari che composti.
Principali tipi predefiniti:
bit
Uno scalare enumerativo che può assumere uno dei due valori '0' e '1' ed è diverso dal tipo
boolean. Rappresenta un dato di 1 bit e si usa nelle operazioni logiche. È sconsigliato a favore
del tipo std_logic.

std_logic
Uno scalare enumerativo che rappresenta un dato di 1 bit e può assumere uno dei 9 valori: 'U'
(uninitialized), 'X' (strong unknown), '0' (strong 0), '1' (strong 1), 'Z' (high impedance), 'W'
(weak unknown), 'L' (weak 0), 'H' (weak 1), '-' (don't care). È consigliato come sostituto del
tipo bit perchè la logica a 9 valori permette di affrontare meglio le situazioni più complesse
(come il disegno dei bus). Esempi:

X <= '1'; -- X ha il valore di 1 logico


Y <= 'Z'; -- Y ha il valore di alta impedenza

boolean

25
Uno scalare enumerativo che può assumere uno dei due valori true e false. Questi valori sono
diversi da '1' e '0' del tipo bit. Boolean è il tipo dei risultati dei test nelle espressioni
condizionali.

integer
Uno scalare con valore numerico intero nel range da -2147483647 a +2147483647.

real
Uno scalare con valore numerico reale (floating point) nel range da -1.0E38 a +1.0E38 .
Esempio:

A := 3.0E-5; -- valore reale 0.00003

character
Uno scalare enumerativo con valore carattere.

time
Un valore fisico per la misura del tempo, ovvero un valore numerico intero seguito da una
delle unità fs, ps, ns, us, ms, sec, min, hr. Esempio:

DELAY1 := 5 ns; -- un ritardo di 5 nanosecondi

bit_vector
Un array di tipi bit. Esempi:

X <= "1011"; -- un array di 4 bit 1011


Y <= ('1','0','1','1'); -- come X

std_logic_vector
Un array di tipi std_logic.

string
Un array di tipi carattere. Esempio:

S := "stringa test";

Per usare i tipi std_logic e std_logic_vector si deve prima rendere visibile il package
ieee.std_logic_1164. La design entity seguente mostra l'utilizzo del tipo std_logic in un buffer Tri-
State (o three-state):
library ieee;
use ieee.std_logic_1164.all;
entity etsb is
port(Din, EN: in std_logic;
Dout: out std_logic);
end entity etsb;

architecture atsb of etsb is


begin

ptsb: process (Din, EN)


begin

26
if EN = '1' then
Dout <= Din;
else
Dout <= 'Z';
end if;
end process ptsb;

end architecture atsb;

Il buffer Tri-State etsb legge il dato in ingresso Din e lo manda su Dout quando è abilitato
dall'ingresso EN (enable). Se invece EN vale '0', Dout viene posto in alta impedenza 'Z'. Questo tipo
di dispositivo si usa nell'interfacciamento dei sistemi sui bus. Se più segnali devono essere connessi
allo stesso bus, questi devono essere abilitati solo uno per volta, mentre gli altri sono in alta
impedenza.
Le porte logiche dei dispositivi elettronici digitali si dicono three state, tri-state o 3-state se la loro
uscita può trovarsi in un terzo stato, l'alta impedenza, spesso indicato con il simbolo Z, oltre ai due
livelli logici già presenti nella logica binaria, indicati convenzionalmente con 1 (o alto) e 0 (o
basso).Quando un dispositivo pone in alta impedenza una porta, generalmente collegata ad un bus
dati o di indirizzi, questa porta non impone alcun valore logico sul dispositivo ad essa collegato,
ovvero risulta virtualmente scollegata dalla linea di comunicazione verso l'esterno. Una porta in alta
impedenza, come dice il nome, offre una elevata resistenza al passaggio della corrente e quindi dei
segnali elettrici.
La necessità di poter mettere il dispositivo in alta impedenza è dovuto al fatto che nei circuiti
digitali complessi, molti dispositivi con funzioni diverse tra loro, coabitano sullo stesso bus, quando
serve attivare la funzione di uno, è necessario scollegare gli altri, altrimenti andrebbero in conflitto
tra loro. Questa condizione può essere ottenuta facendo lavorare un transistor (ad esempio MOS)
nella regione di interdizione (o cut-off).
Un array in VHDL è una collezione di elementi indicizzati tutti dello stesso tipo. È possibile
definire nuovi tipi di array, ad esempio:
TYPE word IS ARRAY (31 DOWNTO 0) of bit;
TYPE memory IS ARRAY (address) of word;
Gli array possono essere monodimensionali (vettori) o multidimensionali.
TYPE transform IS ARRAY (1 to 4, 1 to 4) of real;
Un array può essere constrained, ovvero i limiti degli indici sono stabiliti, oppure unconstrained se
i limiti sono stabiliti all'atto della dichiarazione di una variabile di quel tipo.
TYPE vector IS ARRAY (integer range <>) of real;
La direzione degli array ha effetto sull'ordine di assegnamento dei valori. Gli elementi sono
ordinati per posizione da sinistra a destra e non secondo gli indici. Se il range è crescente, l'indice
dell'elemento più significativo a sinistra è il più basso. Se il range è decrescente, l'indice
dell'elemento più significativo a sinistra è il più alto. Ad esempio, se il segnale A è dichiarato e
inizializzato così:
signal A: bit_vector (0 to 2) := ('1','0','0');

gli elementi di A ordinati da sinistra a destra sono A(0)=1, A(1)=0, A(2)=0. Se adesso per il segnale
B cambiamo solo la direzione del range:
signal B: bit_vector (2 to 0) := ('1','0','0');

27
gli elementi di B ordinati da sinistra a destra sono B(2)=1, B(1)=0, B(0)=0. È importante tenere
presente questo fatto perchè, se si assegna B ad A con:
A <= B;

questo equivale a:
A(0) <= B(2);
A(1) <= B(1);
A(2) <= B(0);

che è qualcosa di diverso da quello che ci si potrebbe aspettare. Per questo è consigliabile usare
sempre la stessa direzione negli arrays.
Notare che gli arrays di diversa lunghezza vengono allineati a sinistra. Ad esempio nel confronto
tra i due bit_vector 1011 e 110, viene aggiunto uno zero non a sinistra ma a destra del più corto,
quindi il primo risulta minore del secondo:
A := "1011";
B := "110";
C := A > B;

Nonostante A sia maggiore di B, nel confronto B diventa maggiore di A, per cui C risulta false.
Un elemento di un array è riferito usando gli indici ed il nome dell'oggetto: a(1), b(1, 1).Una
slice di elementi contigui di un array monodimensionale può essere riferita usando un range degli
indici: a(8 TO 15) è un vettore di 8 elementi parte dell'array a. Si supponga di aver definito un
array di caratteri in questo modo:
TYPE c IS ARRAY (1 TO 4) of bit;
È possibile scrivere un valore in ogni posizione di un oggetto (ad es. segnale) di tipo c usando una
notazione posizionale
segnale_c <= ('1','0','1','1');
oppure usando una notazione con associazione per nome:
segnale_c <= (1 => '1', 3 => '1', 4 => '1', 2 => '0');
segnale_c <= (1 => '1', 2 => '0', 3 => '1', 4 => '1');
Le notazioni posizionale e per associazione nominale possono essere mischiate, usando la keyword
others per assegnare gli elementi non specificati con un unico valore:
segnale_c <= ('1', 2 => '0', OTHERS => '1');

I record sono una collezione di elementi anche di diverso tipo. Una definizione di record è utile, ad
esempio, per rappresentare il formato e la codifica di una istruzione di un processore:
TYPE opcode IS (sta, lda, add, sub, and, nop, jmp, jsr);
TYPE mode IS RANGE 0 TO 3
TYPE address IS BIT_VECTOR (10 DOWNTO 0);
TYPE instruction_format IS RECORD
opc : opcode;
mde : mode;
adr : address;
END RECORD;
SIGNAL instr : instruction_format := (nop, 0, "00000000000");
instr.opc <= lda;
instr.mde <= 2;
instr.adr <= "00011110000";
instr <= (lda, 2, "00011110000");

28
Nell'esempio si vede come l'assegnazione di un segnale di tipo record può essere fatta per gli
elementi del record individualmente oppure per tutti gli elementi assieme.
In VHDL possono essere definiti tipi propri come il tipo physical, che permette di rappresentare
alcune quantità fisiche come la resistenza, la tensione, ecc. La dichiarazione di un tipo fisico include
la specifica di una unità base e opzionalmente un insieme di unità secondarie, come multipli e
sottomultipli. Ad esempio il tipo lunghezza:
TYPE length IS RANGE 0 to 1E9
UNITS um; -- unità base
mm = 1000 um; cm = 10 mm; m = 1000 mm; in = 25.4 mm; ft = 12 in;
yd = 3 ft; rod = 198 in; chain = 22 yd; furlong = 10 chain;
-- unità secondarie
END UNITS;

La definizione del tipo predefinito time invece è:


TYPE time IS RANGE implementation_defined
UNITS fs; -- unità base
ps = 1000 fs; ns = 1000 ps; us = 1000 ns; ms = 1000 us; sec = 1000 ms;
min = 60 sec; hr = 60 min;
-- unità secondarie
END UNITS;
Un tipo enumerazione è un insieme ordinato di identificatori di caratteri, tutti distinti tra loro, anche
se in tipi di enumerazione distinti lo stesso identificativo può essere usato più volte. Ad esempio, il
tipo TYPE qit IS ('0', '1', 'Z','X') definisce un nuovo tipo di logica multivalore, in cui:
➢ qit è l'identificatore di tipo
➢ '0', '1', 'Z', 'X' sono gli elementi del tipo
➢ '0' è il valore assegnato per default

Alcuni tipi enumerazione sono predefiniti in VHDL come:


TYPE boolean IS (false, true);
TYPE bit IS ('0', '1');

Una volta definito un tipo, è possibile assegnare un nome alternativo al tipo o ad una sua parte,
usando la dichiarazione alias, in modo da potervi fare riferimento più comodamente. L'aliasing
chiaramente è semplicemente un renaming e non definisce nessun oggetto. Ad esempio:
dichiara op_code come alias per gli 8 bit più significativi di instr;
VARIABLE instr : BIT_VECTOR(31 DOWNTO 0);
ALIAS op_code : BIT_VECTOR(7 DOWNTO 0) IS instr(31 DOWNTO 24);
In questo modo è possibile lavorare sulla porzione di bit relative al codice operativo assegnando il
valore opportuno all'alias op_code:
op_code <= B"1111_0001";
Un altro esempio dell’utilizzo degli alias, può essere nell’assegnazione di nomi simbolici ai bit di
un registro di stato:
ALIAS c_flag : BIT IS flag_register(3);
ALIAS v_flag : BIT IS flag_register(2);
ALIAS n_flag : BIT IS flag_register(1);
ALIAS z_flag : BIT IS flag_register(0);

Espressioni, operatori e attributi


Gli operatori sono gli strumenti per eseguire operazioni sui dati rappresentati negli oggetti o nei
letterali. Un operatore agisce su uno o due valori o oggetti, detti operandi in questo contesto. Un
costrutto formato da oggetti, letterali, operatori e attributi, è un'espressione e produce come

29
risultato un valore.
Gli operatori predefiniti sono:
Operatore: Descrizione:
negazione
not
logica
and prodotto logico
or somma logica
prodotto logico Operatori Logici. Accettano due operandi (tranne not) di tipo bit,
nand boolean, bit_vector e restituiscono un dato dello stesso tipo. Esempi:
negato
somma logica A := B and C;
nor
negata D <= not E;
somma logica
xor
esclusiva
somma logica
xnor negata
esclusiva
< minore Operatori Relazionali. Accettano due operandi dello stesso tipo
minore o boolean, bit, character, integer, real, bit_vector, string o time e
<= restituiscono un tipo boolean. Il simbolo minore o uguale è identico
uguale
= uguaglianza al simbolo di assegnamento di segnale, ma il significato cambia con
il contesto. Esempio:
/= disuguaglianza
maggiore o A <= B <= C;
>=
uguale
significa assegnare ad A il valore true se B è minore uguale a C; il
> maggiore
valore false altrimenti.
+ addizione Operatori Aritmetici. Accettano due operandi (tranne abs) dello
- sottrazione stesso tipo integer, real o time. Esempi:
* moltiplicazione
X := abs(A - B); -- valore assoluto di A - B
/ divisione
mod modulo A := B rem C; -- resto della divisione
-- intera di B per C
rem resto
** potenza X := B ** E; -- potenza BE
abs valore assoluto
scorrimento Operatori di Scorrimento. Accettano due operandi. Il primo è un
sll logico a array monodimensionale di bit o boolean su cui eseguire lo
sinistra scorrimento. Il secondo è il numero di posizioni da spostare. Il
scorrimento risultato è del tipo di sinistra. Esempi:
srl
logico a destra
A := "01101001";
scorrimento
sla aritmetico a B := A sll 1; -- B coniene "11010010"
sinistra C := A srl 1; -- C coniene "00110100"
sra scorrimento
D := A sla 1; -- D coniene "11010011"
aritmetico a E := A sra 1; -- E coniene "00110100"
destra

30
rotazione a
rol
sinistra
rotazione a
ror F := A rol 1; -- F coniene "11010010"
destra
G := A ror 1; -- G coniene "10110100"
Esempi:

variable A: bit_vector (0 to 3) := "1001";


variable B: bit_vector (0 to 3) := "0011";
& concatenazione -- ...

C <= A & B; -- C contiene "10010011"


D <= B(2 to 3) & A; -- D contiene "111001"

Un attributo serve ad estrarre da un oggetto informazioni che non sono disponibili attraverso gli
operatori. Gli attributi iniziano tutti con il carattere di virgoletta semplice ( ' - tick ) e si applicano
alla destra del nome dell'oggetto (il prefisso). Ci sono un gran numero di attributi predefiniti per i
tipi, gli array ed i segnali, ed altri attributi possono essere definiti dall’utente (raramente usati).
Alcuni attributi predefiniti sono:
'last_event
L'intervallo di tempo dall'ultimo cambiamento del segnale.
'stable(x ns)
Restituisce True se il segnale non ha subito cambiamenti da x ns.
'delayed(x ns)
Restituisce una copia del segnale ritardata di x ns.
'quite(x ns)
Restituisce True se nessuna transazione è stata effettuata sul segnale da x ns.
'structure
Vale True se il prefisso è un'architettura con riferimenti a componenti.
'active
Restituisce True se il segnale ha avuto una transazione nell'ultimo ciclo di simulazione.
'last_active
L'intervallo di tempo dall'ultima transazione effettuata sul segnale.
'transaction
È un segnale che commuta ogni volta che sul segnale di riferimento avviene una transazione.
'behavior
Vale True se il prefisso è un'architettura senza riferimenti a componenti.
'event
Vale True se il segnale prefisso cambia. Esempio:
if CLK'event then -- ...

31
-- la condizione vale true se il segnale CLK è cambiato
'last_value
Valore che il segnale prefisso aveva prima dell'ultimo evento.

Un tipico esempio di utilizzo degli attributi sui segnali è nella individuazione di un fronte attivo di
un clock. Ad esempio un flip-flop D attivo sul fronte di discesa:
ENTITY brief_d_flip_flop IS
PORT (d, c : IN BIT; q : OUT BIT);
END brief_d_flip_flop;
ARCHITECTURE falling_edge OF brief_d_flip_flop IS
SIGNAL tmp : BIT;
BEGIN
tmp <= d WHEN (c = '0' AND NOT c'STABLE) ELSE tmp;
q <= tmp AFTER 8 NS;
END falling_edge;
Identificatori
Un identificatore è una stringa che si crea per dare un nome a un oggetto, un sottoprogramma, una
design unit o ad un'etichetta. Il tipo di identificatore normale segue queste regole:

• deve iniziare con un carattere alfabetico


• gli altri caratteri possono essere alfabetici, numerici o di sottolineatura ( _ )
• due caratteri di sottolineatura consecutivi non sono ammessi
• non sono ammessi spazi
• deve essere diverso dalle parole chiave del linguaggio
• non c'è distinzione tra maiuscole e minuscole
Notare che in un identificatore il carattere di sottolineatura non viene ignorato, diversamente da ciò

32
che avviene nei letterali numerici.
Commenti
Un commento è un elemento lessicale che viene totalmente ignorato, il cui testo ha il solo scopo di
documentare il sorgente al lettore. Inizia con due trattini ( -- ) e termina alla fine della riga.

Esempi:
-- Questo e' un commento, non ha punto e virgola
--------Anche questo e' un commento

entity ee is
port (x, y: in bit;
z: out bit);
end;

-- Per continuare su una nuova riga ...


-- ... si deve inserire un nuovo commento

architecture aa of ee is
begin
z <= x and y; -- Commento in coda a un'istruzione
end ;

Elementi modulari
Allo scopo di permettere il riuso di porzioni di codice, di usare porzioni già pronte, o
semplicemente di rendere il sorgente più leggibile, è necessaria una gestione modulare del progetto.
Questa si ottiene attraverso sottoprogrammi (funzioni e procedure) e packages.
Funzioni e procedure
Un supporto alla modularità è offerto dai sottoprogrammi, moduli che isolano gruppi di istruzioni
sequenziali dal resto del sorgente. Un sottoprogramma viene definito una volta sola e poi viene
utilizzato dove serve attraverso le chiamate.
Un sottoprogramma può ricevere dati dalla chiamata attraverso la lista di argomenti e può
restituirne alla fine. Nella definizione di un sottoprogramma gli argomenti sono detti parametri
formali. I corrispondenti valori usati nella chiamata sono detti parametri attuali.
Le definizioni di sottoprogrammi sono ammesse nelle aree dichiarative di architetture, processi, di
altri sotoprogrammi e nei packages. La visibilità del sottoprogramma dipende da dove è stata
inserita la definizione. Le chiamate di sottoprogrammi sono ammesse sia in aree concorrenti che in
aree sequenziali.
I sottoprogrammi sono di due tipi:
• le funzioni hanno una lista di argomenti di solo ingresso e devono restituire un valore alla
fine attraverso l'istruzione sequenziale return; le chiamate di funzione sono espressioni
all'interno di altre istruzioni;
• le procedure hanno una lista di argomenti sia di ingresso che di uscita e non restituiscono
alcun valore, anche se possono contenere l'istruzione return per terminare; le chiamate di
procedura sono vere e proprie istruzioni.
In un sottoprogramma le variabili locali non conservano il proprio valore nel tempo tra
un'esecuzione e l'altra.
Nella lista di argomenti della chiamata, la mappatura tra parametri formali e parametri attuali si può

33
fare in due modi:
• associazione posizionale: si inserisce semplicemente la lista dei parametri attuali nello
stesso ordine in cui sono definiti i parametri formali;
• associazione per nome: si inserisce la lista delle coppie di parametri formale e attuale
(separati da una freccia a destra) in un ordine qualsiasi.

Sintassi

Definizione di una function:


function nome_funzione (
constant | signal parametro_formale : tipo {,
constant | signal parametro_formale : tipo }
) return tipo is
{dichiarazione}
begin
{istruzione_sequenziale}
end [function] [nome_funzione] ;

Definizione di una procedure:


procedure nome_procedura (
constant | signal | variable parametro_formale : modo tipo {,
constant | signal | variable parametro_formale : modo tipo }
) is
{dichiarazione}
begin
{istruzione_sequenziale}
end [procedure] [nome_procedura] ;

Istruzione return in un sottoprogramma:


[etichetta :] return [espressione] ;

Chiamata di una function:


nome_funzione [ ( [parametro_formale =>] parametro_attuale {,
[parametro_formale =>] parametro_attuale } ) ] ;

Chiamata di una procedure:


[etichetta :]
nome_procedura [ ( [parametro_formale =>] parametro_attuale {,
[parametro_formale =>] parametro_attuale } ) ] ;

Esempi
La funzione seguente è un generatore di parità. Legge un dato di 8 bit in ingresso (il segnale D) e
restituisce un dato di 9 bit ottenuto concatenando al dato il bit di parità (la variabile P).
function fpari(signal D: bit_vector(0 to 7),
) return bit_vector(0 to 8) is

variable I: integer;
variable P: bit := '0';

begin

34
for I in 0 to 7 loop
if D(I) = '1' then
P := not P;
end if;
end loop;

return D & P;

end;

All'inizio P è 0. Il ciclo for legge gli 8 bit del dato dal primo all'ultimo. Ad ogni ripetizione, P viene
negato solo quando D(I) vale 1. In questo modo, dopo ogni ripetizione l'insieme degli 1 di D e P è
sempre pari. Alla fine P viene concatenato a D e restituito da return.
L'architettura seguente contiene un esempio di chiamata della funzione fpari. Ad Y viene assegnato
il risultato della chiamata fatta con associazione per nome. L'assegnamento a Z è uguale solo che
l'associazione è posizionale:
architecture aaa of eee is

signal X: bit_vector (0 to 7);


signal Y, Z: bit_vector (0 to 8);

begin
-- ...
Y <= fpari(X);
Z <= fpari(D => X);
-- ...
end;

L'esempio che segue è una variante dell'applicazione precedente, realizzata con una procedura.
L'argomento D riceve il dato in ingresso e contiene l'uscita alla fine.
procedure ppari(signal D: inout bit_vector(0 to 8) ) is

variable I: integer;

begin

D(8) <= 0;
for I in 0 to 7 loop
if D(I) = '1' then
D(8) := not D(8);
end if;
end loop;

end;

La chiamata alla procedura ppari in questo caso è un'istruzione:


architecture aaa of eee is
signal X: bit_vector (0 to 8);
begin
-- ...
ppari(X);
-- ...
end;

35
Packages e librerie
Un modo per ottenere un progetto modulare è quello di isolare fuori dalle altre design units le
porzioni riusabili di codice, ovvero usare porzioni di codice definite in moduli esterni al modello.
Un package è una design unit che può contenere, isolatamente dal resto del sorgente, dichiarazioni
di oggetti, sottoprogrammi e componenti. In particolare per i sottoprogrammi è necessasaria anche
una seconda design unit, il package body che deve contenere la definizione completa del
sottoprogramma, mentre nel package si inserisce solo una dichiarazione parziale, il prototipo.

Una library è un contenitore di design units


analizzate (compilate). Le librerie contengono
elementi esterni riusabili precompilati. Ma anche
tutto ciò che si scrive nel modello corrente viene
inserito in una libreria. Tutte le design units create
nel sorgente (non solo i packages), sono
automaticamente inserite dal compilatore nella
libreria corrente il cui nome di default è work.
Un modello VHDL è un insieme di librerie
contenenti design units che a loro volta contengono
altri elementi con nome. Questa struttura gerarchica
implica la creazione di name spaces (spazi dei
nomi) e di regole di visibilità degli elementi. Un
elemento dichiarato in un processo o in un
sottoprogramma, sarà vivibile solo in quel processo
o sottoprogramma. Un elemento dichiarato in un'architettura o in un'entità sarà visibile
rispetivamente solo sotto quell'architettura o sotto tutte le architetture dell'entità, compresi i processi
interni. Un elemento dichiarato in un package sarà visibile sotto tutte le design units che usano quel
package. Questo significa anche che ci possono essere elementi con lo stesso nome, purchè
appartenenti a spazi di nomi diversi.
Se dal sorgente di una design unit si vuole fare riferimento a un'altra design unit o ad un elemento
contenuto in un'altra design unit, si deve utilizzare il nome selettivo dell'elemento. Nel nome
selettivo il nome dell'elemento è preceduto dal nome della sua design unit e dal nome della sua
libreria, separati da un punto. Inoltre la design unit che fa uso di oggetti di librerie esterne deve
essere preceduta dall'istruzione library che specifica la libreria esterna a cui si fa riferimento. La
libreria corrente work è sempre visibile automaticamente.
Per evitare l'utilizzo del nome selettivo completo, la design unit che contiene riferimenti all'esterno
deve essere preceduta da istruzioni use che specificano i nomi selettivi o gli spazi dei nomi che si
vogliono utilizzare. Si può usare la parola chiave all al posto del nome della design unit o del nome
dell'elemento per indicare tutti gli elementi di quello spazio.
I sistemi di sviluppo sono forniti con alcune librerie già pronte che contengono gli elementi
standard del linguaggio ed eventuali elementi proprietari. Gli elementi standard principali (come i
tipi bit e integer) sono contenuti nella libreria std e sono automaticamente visibili senza la necessità
di una use. Invece gli elementi IEEE-1164 (come il tipo std_logic) sono inseriti nel package
std_logic_1164 della libreria ieee e devono essere resi visibili esplicitamente con una use.

Sintassi
Definizione di un package:

36
package nome_package is
{dichiarazione}
end [package] [nome_package] ;

Definizione di un package body:


package body nome_package is
{dichiarazione}
{definizione}
end [package body] [nome_package] ;

Riferimenti agli elementi riusabili:


library nome_libreria {, nome_libreria} ;
use nome_selettivo {, nome_selettivo} ;

Esempi
Il package pack1 contiene solo dichiarazioni di oggetti e di un componente, quindi non necessita di
package body:
package pack1 is
constant CLK_CYCLE: time;
variable COUNT: integer;
signal A, B: bit;
component ff
port (D, CLK: in bit;
Q: out bit);
end component ff;
end package pack1;

Invece qui il package pack1 dichiara due sottoprogrammi, quindi deve essere seguito da un package
body con lo stesso nome pack1, che contiene la definizione completa dei sottoprogrammi (fpari e
ppari sono gli stessi già visti, quindi il testo completo non è riportato per brevità):
package pack1 is
function fpari(signal D: bit_vector(0 to 7),
) return bit_vector(0 to 8);
procedure ppari (signal D: inout bit_vector (0 to 8));
end package pack1;

package body pack1 is

function fpari(signal D: bit_vector(0 to 7),


) return bit_vector(0 to 8) is
begin
-- ...
end function fpari;

procedure ppari (signal D: inout bit_vector (0 to 8)) is


begin
-- ...
end procedure ppari;

end package body pack1;

La definizione di pack1 ha creato implicitamente un name space. Infatti se adesso si vuole chiamare

37
la fpari dall'esterno, si dovrà usare il nome selettivo. Supponendo che pack1 si trovi nella libreria
lib1, il nome selettivo di fpari è lib1.pack1.fpari:
library lib1;
architecture aaa of eee is
-- ...
begin
-- ...
Y <= lib1.pack1.fpari(X);
-- ...
end;

Aggiungendo l'istruzione use lib1.pack1.fpari; si evita l'uso del nome selettivo. Inoltre, se nella use
si sostituisce il nome fpari con la parola chiave all, si potrà evitare il nome selettivo per tutti gli
elementi contenuti in pack1:
library lib1;
use lib1.pack1.all;
architecture aaa of eee is
-- ...
begin
-- ...
Y <= fpari(X);
ppari(X);
-- ...
end;

Le stesse regole valgono per tutte le design units, non solo per i packages. Ad esempio, qui la prima
use integra il segnale sig1 dell'architettura arc1 della libreria lib2. La seconda use integra l'entità
ent1 della libreria lib3:
use lib2.arc1.sig1;
use lib3.ent1;

Infine l'esempio seguente dà una visione della struttura a library e design units dei sorgenti VHDL.
Il sorgente è l'insieme delle design units che formeranno la libreria work: l'entità ent1, l'architettura
arc1, il package pack1 e il suo package body. Inoltre, perchè work funzioni, è necessaria la
disponibilità di design units esterne che si trovano nelle tre librerie ieee, lib1 e lib2:
library ieee, lib1, lib2;

use ieee.std_logic_1164.all, lib1.pack5.all;


entity ent1 is
-- ...
end entity ent1;

architecture arc1 of ent1 is


-- ...
begin
-- ...
end architecture arc1;

use lib2.all;
package pack1 is
-- ...
end package pack1;

38
package body pack1 is
-- ...
end package body pack1;

Ciclo di sviluppo VHDL

Per sviluppare un sistema in VHDL occorre passare attraverso il ciclo di sviluppo presentato in
figura. Oltre alla sintesi del sistema in una forma circuitale, il VHDL si presta ad un altro
importante utilizzo: la simulazione. Una parola chiave molto importante sotto questo aspetto è
after.

Infatti tramite essa si specifica il ritardo di simulazione da assegnare ad un certo evento (ad esempio
l'assegnazione di un valore logico ad un segnale). Se in un caso del genere non si specificasse il
ritardo questo sarebbe considerato nullo. Per avere ritardi relativi ad una certa famiglia tecnologica
occorre importare, tramite librerie, i componenti realizzati con quella famiglia.

Tecniche di simulazione dell'hardware

Una fase fondamentale nello sviluppo di circuiti è la simulazione, in quanto consente di rilevare il
funzionamento del sistema ed individuare problemi prima che si passi alla realizzazione fisica del
circuito stesso. Ciò consente di eliminare costi economici legati al testing del sistema. Una
caratteristica molto importante del testing è che deve essere efficiente in termini di tempo, ossia la
simulazione di un circuito deve avvenire in un tempo accettabile. Esistono diverse tecniche di
simulazione:

● Simulazione a tempo quantizzato: viene fissato un quanto di tempo, detto passo di


quantizzazione. Ogni passo rappresenta l'intervallo apprezzabile in simulazione dopo il
quale si verifica lo stato dell'intero sistema. Questa è una tipologia di simulazione statica in
quanto, indipendentemente dalla presenza o meno di variazioni nel sistema, si effettua un

39
controllo dello stesso. Questa tecnica è quindi incapace di adattarsi alla dinamica del
sistema. Infatti esisteranno molti passi di quantizzazione all'interno dei quali non accadrà
nulla.

● Simulazione ad eventi: questa tecnica è altamente dinamica in quanto valuta il nuovo stato
del sistema solo quando si verifica un evento che determina un cambiamento. Ciò evita di
eseguire calcoli di simulazione inutili all'interno di periodi in cui non succede nulla.

Consideriamo un esempio per entrambe le tipologie di simulazione, a partire da quella a tempo


quantizzato. Il circuito viene rappresentato come una netlist in forma tabellare e il suo stato non è
altro che l'insieme delle tensioni ad ogni nodo. Il simulatore si attiva ad intervalli pari al passo di

quantizzazione e aggiorna lo stato del circuito. In particolare, ad ogni passo campiona i valori di
tutti gli ingressi e calcola il valore del potenziale di ogni nodo secondo le gate e le loro
interconnessioni, ispezionando la tabella. Questa tecnica sebbene sia di semplice implementazione
ha gli svantaggi presentati in precedenza. La simulazione ad eventi invece, anche se più
difficilmente implementabile, permette una simulazione veloce dei circuiti digitali ed è quella
utilizzata dal VHDL. Questo perchè valuta il
circuito solo quando si verifica un evento ovvero
un cambiamento del potenziale di un nodo; se non
ci sono eventi la rete non evolve ma è stabile.
Supponiamo di avere un circuito in cui tutte le
porte introducono un ritardo di 12 ns. Le
variazioni si un nodo si propagano lungo il
circuito. Ad esempio, la commutazione
sull'ingresso a da 1→0 @0ns provoca, tramite la
porta NOT g1, una commutazione su w da 0
→1@12ns, e così via.

Il VHDL utilizza dunque la simulazione ad


eventi. Un evento è individuabile da una
quaterna:

1. da cosa è generato

2. su cosa ha effetto

3. a che tempo avviene

4. qual è il valore che cambia

40
Una assegnazione concorrente su un segnale
comporta una transazione. Questa è
caratterizzata da una coppia (value, time):

● value rappresenta il valore che verrà


assegnato al segnale e viene
calcolato nell'istante in cui si genera
la transazione;

● time rappresenta l'intervallo di


tempo trascorso il quale il valore
value verrà assegnato al segnale.

Non sempre una assegnazione comporta una variazione del valore di un segnale. Si parla di evento
quando questa variazione avviene, altrimenti di semplice transazione. Ad esempio, se il segnale a
varia da 1 → 0 @0ns, l'assegnazione w <= NOT a AFTER 12 NS; genera una transazione (1,
12ns). Questo significa che dopo 12ns dall'istante 0, ci sarà l'evento 0 → 1 sul segnale w.

Un parametro fondamentale nella simulazione dei circuiti è il delta cycle. Esso rappresenta un
tempo fittizio che viene utilizzato dallo scheduler degli eventi, ossia l'entità preposta alla gestione
degli eventi, per gestire la concorrenza degli eventi che incidono in un certo istante. In altre parole è
un semplice ciclo di simulazione che non corrisponde ad un tempo reale. In particolare, se in un
certo istante si verifica una transazione il simulatore VHDL la eseguirà nel delta cycle successivo.
Questo concetto è fondamentale perchè consente al simulatore di andare a determinare cosa accade
in un certo istante; ossia consente di aprire su ciò che accade in un istante. Supponiamo di avere

ARCHITECTURE concurrent OF es_delta IS

SIGNAL a, b, c : BIT := '0';


BEGIN
a <= '1';
b <= NOT a;
c <= NOT b;
END concurrent;

All'inizio i segnali sono tutti posti al valore logico 0, con l'istruzione di


assegnazione. Questo particolare stato però non è stabile per il circuito
(poiché incompatibile con le assegnazioni presenti), e quindi la rete
comincia ad andare a regime, passando per i transitori in figura.

Un caso limite è costituito dal seguente esempio

ARCHITECTURE concurrent OF timing_demo IS

SIGNAL x, y : BIT := '0';


BEGIN
x <= y;
y <= NOT x;
END concurrent;

41
In questo caso abbiamo che i segnali x, y
oscillano indefinitamente all'istante zero,
senza che il tempo reale della simulazione
avanzi.

Assert

Le istruzioni ASSERT possono essere usate


sia come statement sequenziali che concorrenti. La sintassi è: ASSERT condition REPORT
message SEVERITY severity level; in cui, il messaggio message viene riportato dal simulatore
quando si verifa la condizione not(condition);il severity_level può essere note, warning, error,
failure e può fermare o meno la simulazione.

I flip-flop e gli altri elementi di memoria (e quindi anche i sistemi sequenziali) per funzionare
correttamente hanno bisogno che siano verificate delle opportune relazioni fra segnali da
campionare e segnali di clock:
● vincolo di set-up: il segnale da campionare deve essere stabile per un certo tempo (tempo di
set-up) prima del fronte attivo del clock
● vincolo di hold: il segnale di uscita deve essere stabile per un certo tempo (tempo di hold)
dopo il fronte attivo del clock
Se uno di questi vincoli non è verificato non si può essere certi che il valore campionato sia quello
presente quando c’è il fronte attivo del clock.
Tempo di setup: quando (il clock commuta da 0 a 1), se (il data input non è stato stabile per almeno
un tempo pari a setup-time) allora c'è una violazione del tempo di setup.
ASSERT
NOT( (clock='1' AND clock'EVENT) AND (NOT data'STABLE(setup_time)) )
REPORT
“Violazione del tempo di setup”
SEVERITY
WARNING;
Tempo di hold: quando c'è una commutazione sul data input, se il valore del clock è '1' ed il clock
ha commutato meno di un hold-time, allora c'è una violazione del tempo di hold;
ASSERT
NOT( (data'EVENT) AND (clock='1') AND (NOT clock'STABLE(hold_time)) )
REPORT
“Violazione del tempo di hold”
SEVERITY
WARNING;
Un esempio di istruzione assert utilizzata sia come istruzione sequenziale che concorrente è
riportato di seguito.

42
Generics

I generics sono specificati nell’interfaccia di una entity e permettono di parametrizzarne la


descrizione. Il valore dei generics devono essere specificati in almeno uno dei seguenti modi:
1. come valori di default (nella dichiarazione di entity);
2. all’atto della dichiarazione del component;
3. all’atto dell’instanziazione del component;
In caso di uso di diversi modi di specificazione dei generics, il valore usato è quello che ha priorità
maggiore. La lista precedente è espressa in senso crescente di priorità. Si possono usare come
generics solo quantità costanti (appartenenti alla classe constant) o espressioni statiche, ovvero che
coinvolgono solo quantità costanti.
Un esempio di utilizzo dei generics è presentato nel codice che segue.
ENTITY rom IS
GENERIC( width : positive := 32;
depth : positive := 4);
PORT( enable : IN std_logic;
address : IN std_logic_vector(depth–1 downto 0);
data : OUT std_logic_vector(width–1 downto 0) );
END rom

COMPONENT rom
GENERIC (width : positive := 8;
depth : positive := 4);
PORT ( enable : IN std_logic;

43
address : IN std_logic_vector(depth–1 downto 0);
data : OUT std_logic_vector(width–1 downto 0) );
END COMPONENT;

rom_0 : rom GENERIC MAP (16, 4) PORT MAP (…);
-- rom di 16 word di 16 bit
rom_1 : rom PORT MAP (…);
-- rom di 16 word di 8 bit
rom_2 : rom GENERIC MAP (depth => 5, width => 32) PORT MAP (…);
-- rom di 32 word di 32 bit

Nel primo caso di istanziazione (rom_0) il valore 16 sovrascrive il valore di width 8 assegnato nella
dichiarazione del component (che a sua volta ha sovrascritto il valore di width 32 del generic di
default).
I generics possono anche essere usati per passare dei parametri che rappresentano costanti fisiche
che possono variare in diverse istanzazioni dello stesso modulo.

ENTITY processor IS
GENERIC (max_clock_freq : frequency := 30 MHz);
PORT (clock : IN std_logic;
address : OUT integer;
data : INOUT word_32;
control : OUT proc_control;
ready : IN std_logic);
END processor;

I blocchi logici dei sistemi digitali spesso sono composti da un solo blocco più semplice istanziato
molte volte (ad es. un addizionatore carry-ripple, una porta logica che operi su word, …). Per
descrivere efficacemente questo tipo di blocchi il VHDL mette a disposizione il costrutto generate
che permette di istanziare iterativamente un insieme di blocchi. Lo statement generate può essere
usato in combinazione sia con il costrutto FOR che con un costrutto IF.

Testbench

Per verificare il funzionamento e la tempificazione di un progetto VHDL, si usano i testbench. Un


testbench è una design unit VHDL che:
● dichiara e istanzia il blocco da testare (unit under test, UUT);
● applica una opportuna sequenza di stimoli;
● verifica che le uscite siano quelle corrette secondo le specifiche;
In pratica un testbench è una entità VHDL che non ha segnali di ingresso né di uscita, poiché in
pratica non può essere istanziato da nessun altro blocco. Il più semplice dei testbench applica le
sequenza degli ingressi, senza verificarne l’esattezza; sarà l’utente a verificare se le uscite sono
quelle corrette.

Esempi VHDL
Un latch SR può essere realizzato usando 4 NAND
incrociate.

ENTITY nand2 IS PORT (


i1, i2: IN std_logic; o1: OUT
std_logic );
END nand2;
--
ARCHITECTURE single_delay OF nand2 IS

44
BEGIN
o1 <= i1 NAND i2 AFTER 4 NS;
END single_delay;
ENTITY nand2 IS PORT (
i1, i2: IN std_logic; o1: OUT std_logic );
END nand2;
--
ARCHITECTURE single_delay OF nand2 IS
BEGIN
o1 <= i1 NAND i2 AFTER 4 NS;
END single_delay;
ENTITY sr_latch IS PORT (
s, r, c: IN std_logic; q : OUT std_logic );
--
ARCHITECTURE gate_level OF sr_flipflop IS
COMPONENT n2 PORT (i1, i2: IN std_logic; o1: OUT std_logic); END COMPONENT;
FOR ALL : n2 USE ENTITY WORK.nand2 (single_delay);
SIGNAL im1, im2, im3, im4 : std_logic;
BEGIN
g1 : n2 PORT MAP (s, c, im1);
g3 : n2 PORT MAP (im1, im4, im3);
g2 : n2 PORT MAP (r, c, im2);
g4 : n2 PORT MAP (im3, im2, im4);
q <= im3;
END gate_level;
Il latch fatto nel modo precedente non funziona perché nel caso in cui i segnali im3 e im4 assumano
lo stesso valore nello stesso istante, il circuito oscilla indefinitamente (come nel caso di 2 NOT con
lo stesso ritardo). Questo, in realtà, non è un limite per il simulatore VHDL perché è chiaro che
questo comportamento deriva da troppe assunzioni ideali, quali interconnessioni senza ritardo e
l'utilizzo di 4 porte perfettamente identiche. La soluzione sta nell’usare 2 NAND più veloci in uno
dei 2 rami.
ENTITY nand2 IS PORT (
i1, i2: IN std_logic; o1: OUT std_logic );
END nand2;
--
ARCHITECTURE single_delay OF nand2 IS
BEGIN o1 <= i1 NAND i2 AFTER 4 NS;
END single_delay;
--
ARCHITECTURE fast_single_delay OF nand2 IS
BEGIN o1 <= i1 NAND i2 AFTER 3 NS;
END fast_single_delay;
ARCHITECTURE gate_level OF sr_flipflop IS
COMPONENT n2 PORT (i1, i2: IN std_logic; o1: OUT std_logic);
END COMPONENT;
FOR g1, g3 : n2 USE ENTITY WORK.nand2(fast_single_delay);
FOR g2, g4 : n2 USE ENTITY WORK.nand2(single_delay);
SIGNAL im1, im2, im3, im4 : std_logic;
BEGIN
g1 : n2 PORT MAP (s, c, im1);
g3 : n2 PORT MAP (im1, im4, im3);
g2 : n2 PORT MAP (r, c, im2);
g4 : n2 PORT MAP (im3, im2, im4);
q <= im3;
END gate_level;
ARCHITECTURE gate_level OF sr_flipflop IS
COMPONENT n2 PORT (i1, i2: IN std_logic; o1: OUT std_logic);
END COMPONENT;

45
FOR g1, g3 : n2 USE ENTITY WORK.nand2(fast_single_delay);
FOR g2, g4 : n2 USE ENTITY WORK.nand2(single_delay);
SIGNAL im1, im2, im3, im4 : std_logic;
BEGIN
g1 : n2 PORT MAP (s, c, im1);
g3 : n2 PORT MAP (im1, im4, im3);
g2 : n2 PORT MAP (r, c, im2);
g4 : n2 PORT MAP (im3, im2, im4);
q <= im3;
END gate_level;

Adesso possiamo usare il nostro SR latch per realizzare un D latch.


ENTITY d_latch IS PORT(
d, c : IN std_logic; q: OUT std_logic);
END d_latch;
--
ARCHITECTURE sr_based OF d_latch IS
COMPONENT sr_latch PORT (s, r, clk : IN std_logic; q : OUT std_logic);
END COMPONENT;
FOR ALL: sr_latch USE ENTITY WORK.sr_latch(gate_level);
COMPONENT inv_t PORT (i1 : IN std_logic; o1 : OUT std_logic); END COMPONENT;
FOR ALL: inv_t USE ENTITY WORK.inv_t(dataflow);
SIGNAL not_d: std_logic;
BEGIN
c1 : sr_latch PORT MAP (d, not_d, c, q);
c2 : inv_t PORT MAP (d, not_d);
END sr_based;

Un D latch 1-attivo è un blocco con memoria che memorizza il dato quando il livello del “clock” è
alto (basso nel caso 0-attivo) → trasparente, e propaga in uscita l’ingresso → in hold. Una
descrizione di un flip-flop trasparente alto, con una assegnazione condizionata:
LIBRARY ieee;
USE ieee.std_logic_1164.all;
ENTITY d_latch IS PORT (
d, clk : IN std_logic;
q : OUT std_logic );
END d_latch;
ARCHITECTURE dataflow of d_latch IS
SIGNAL q_tmp : std_logic;
BEGIN
q <= q_tmp;
q_tmp <= d WHEN clk = ‘1’ ELSE q_tmp;
END dataflow;

Con una assegnazione selettiva invece,


LIBRARY ieee;
USE ieee.std_logic_1164.all;
ENTITY d_latch IS PORT (
d, clk : IN std_logic;
q : OUT std_logic );
END d_latch;
ARCHITECTURE dataflow of d_latch IS
SIGNAL q_tmp : std_logic;
BEGIN
q <= q_tmp;
WITH clk SELECT
q_tmp <= d WHEN ‘1’;
q_tmp WHEN OTHERS;

46
END dataflow;

Un flip-flop D attivo sul fronte di salita (risp. di discesa) è un blocco con memoria che memorizza
(campiona) il dato immagazzinandolo nel suo stato quando il livello del clock commuta da 0 a 1 e
mostra in uscita il suo stato. Una descrizione con una assegnazione condizionata si ottiene con
LIBRARY ieee;
USE ieee.std_logic_1164.all;
ENTITY d_ff IS PORT (
d, clk : IN std_logic;
q : OUT std_logic );
END d_ff;
ARCHITECTURE dataflow of d_ff IS
SIGNAL q_tmp : std_logic;
BEGIN
q <= q_tmp;
q_tmp <= d WHEN (clk = ‘1’ and clk’event) ELSE q_tmp;
END dataflow;

Con assegnazione selettiva,


LIBRARY ieee;
USE ieee.std_logic_1164.all;
ENTITY d_ff IS PORT (
d, clk : IN std_logic;
q : OUT std_logic );
END d_ff;
ARCHITECTURE dataflow of d_ff IS
SIGNAL q_tmp : std_logic;
BEGIN
q <= q_tmp;
WITH (clk = ‘1’ and clk’event)
q_tmp <= d WHEN true
q_tmp WHEN false;
END dataflow;

Un flip-flop D attivo sui fronti di discesa, con reset asincrono 1-attivo potrebbe essere descritto
come segue:
LIBRARY ieee;
USE ieee.std_logic_1164.all;
ENTITY d_ff IS PORT (
d, clk : IN std_logic;
q : OUT std_logic );
END d_ff;
ARCHITECTURE dataflow of d_ff IS
SIGNAL q_tmp : std_logic;
BEGIN
q <= q_tmp;
q_tmp <= ‘0’ WHEN rst = ‘1’ ELSE
d WHEN (clk = ‘0’ and clk’event) ELSE
q_tmp;
END dataflow;
LIBRARY ieee;
USE ieee.std_logic_1164.all;
ENTITY d_ff IS PORT (
d, clk : IN std_logic;
q : OUT std_logic );
END d_ff;
ARCHITECTURE dataflow of d_ff IS
SIGNAL q_tmp : std_logic;

47
BEGIN
q <= q_tmp;
q_tmp <= ‘0’ WHEN rst = ‘1’ ELSE
d WHEN (clk = ‘0’ and clk’event) ELSE
q_tmp;
END dataflow;

Descrizione di macchine a stati finiti


Una macchina sequenziale è un sistema a tempo discreto in cui il valore dell'uscita dipende dai
valori assunti dall'ingresso in tutti gli istanti di tempo (non da quelli successivi all'istante corrente se
il sistema è fisicamente realizzabile). Il modello matematico di una macchina sequenziale è il
seguente:
M = {I, U, S, F, G}dove:
I: {i1, i2,…, in,…}è l'alfabeto di ingresso,
U: {u1, u2,…, un,…}è l'alfabeto di uscita,
S: {s1, s2,…, sn, …}è l'insieme degli stati,
F : S x I → U è la funzione di uscita
G : S x I → S è la funzione di aggiornamento dello stato interno
Un opportuno blocco di ritardo T mantiene il vecchio stato s fino a quando non è necessario
sostituirlo con il nuovo stato s*. Le funzioni di uscita F e di aggiornamento dello stato interno G
sono spesso rappresentate mediante grafi oppure tabelle e sono semplici reti combinatorie. Nei
calcolatori elettronici, ingresso, uscita e stato sono codificati attraverso una rappresentazione
digitale e pertanto possono assumere un numero finito di valori. Si parla dunque di FSM (Finite
State Machine). Esistono modelli matematici di macchine non-FS, generalmente composti da una
FSM che realizza l'unità di controllo e da una memoria virtualmente infinita. La macchina di Turing
ne è un esempio.
Esistono due modelli fondamentali di FSM: quello di Mealy e quello di Moore. Essi si
differenziano per la funzione di uscita F:
● nel modello di Mealy l'uscita dipende sia dallo stato corrente che dall'ingresso, ossia
u=F(s,i);
● nel modello di Moore l'uscita è funzione solo dello stato corrente, mentre non c'è dipendenza
dell'uscita rispetto all'ingresso corrente, ossia u=F(s).

In un grafo degli stati di Mealy l'uscita viene rappresentata sugli archi, mentre in quello di Moore è
rappresentata all'interno degli stati.Nella loro implementazione elettronica, le macchine sequenziali
richiedono evidentemente degli elementi di memoria.Precisamente, le funzioni F e G sono
realizzate mediante blocchi combinatori, mentre l'elemento di ritardo è costituito da flip-flop. Nelle
macchine sequenziali sincrone un unico segnale (il segnale di clock) controlla gli elementi di
memoria determinando l'istante di cambio dello stato.
Il VHDL consente di descrivere molto agilmente macchine a stati finiti;gli stati della macchina
possono essere elencati definendo un tipo enumerato e quindi indicando con un nome mnemonico
ogni valore dello stato.
--definizione dello stato
TYPEstate IS(idle, read, post_read);
SIGNALcurrent_state, next_state : state;

La descrizione della FSM può essere effettuata con diverse tecniche. In particolare è possibile
destinare due process distinti alla descrizione della parte combinatoria e quella con memoria della
FSM. In alternativa, è possibile descrivere in un unico process il comportamento complessivo della
macchina. Utilizzare costrutti sequenziali può facilitare la scrittura del codice ma rende più
difficoltoso il mapping di quella macchina in un circuito.

48
Un process è utilizzato per descrivere la parte combinatoria della FSM, ovvero la funzione di
aggiornamento dello stato G e la funzione di uscita F. Pertanto, nella sensitivity list del process
saranno certamente contenuti i segnali relativi agli ingressi della FSM ed allo stato corrente, mentre
tra i segnali aggiornati ci sarà certamente quello che rappresenta lo stato prossimo. Un costrutto
case…when sul segnale che rappresenta lo stato corrente consentirà di decidere, anche in funzione
dell'ingresso, qual è l‟uscita attuale e quale sarà il prossimo stato. L'altro process descrive
l'elemento di ritardo della FSM. Dunque, avrà nella sensitivity list il segnale di clock, mentre
aggiornerà il segnale che rappresenta lo stato corrente. Ad esempio,
TYPEstate IS(S1,S2, );
SIGNAL current_state, next_state : state;
BEGIN
Reg: PROCESS(clock )
BEGIN
IF rising_edge(clock) THEN
current_state <=next_state; END IF;
END PROCESS reg;
Comb: PROCESS(i1, i2, ,current_state)
BEGIN
CASE current_state IS
WHEN S1 => IF (i1=1) THEN u1<=1;
next_state <=s3;
WHENS2 =>
END CASE;
END PROCESS Comb;
È comunque possibile descrivere l'intera FSM entro un solo processo, peraltro utilizzando un unico
segnale di tipo stato. Ne risulta una descrizione più compatta ma meno aderente alla struttura della
FSM. L'uso del costrutto case…when è sostanzialmente lo stesso rispetto al caso precedente. Ad
esempio,
TYPE state IS(S1,S2 );
SIGNAL current_state: state;
BEGIN
fsm: PROCESS(clock )
BEGIN
IF rising_edge(clock) THEN
CASE current_state IS
WHENS1 => IF (i1=1) THEN u1<=1;
current_state <=s3;
WHENS2 =>
END CASE;
END IF;
END PROCESS fsm;

Per descrivere una FSM elaborata in VHDL può spesso essere comodo ricorrere ad una descrizione
comportamentale della macchina. Particolarmente utile a tal fine è lo statement wait, che permette
di introdurre gli elementi sequenziali del circuito descritto. Sostanzialmente si elimina la sensitivity
list del processo inserendo l'attesa sul segnale di clock. Ad esempio, una macchina di Moore che
riconosce la stringa 1011 è la seguente:
ENTITY moore_detector IS
PORT( x, clk : IN BIT;
z : OUT BIT);
END moore_detector;
ARCHITECTURE behavioral_state_machine OF moore_detector IS
TYPE state IS (reset, got1, got10, got101, got1011);
SIGNAL current : state := reset;
BEGIN

49
PROCESS
BEGIN
CASE current IS
WHEN reset=>
WAIT UNTIL clk = '1';
IF x = '1' THEN current <= got1; ELSE current <= reset;
END IF;
WHEN got1=>
WAIT UNTIL clk = '1';
IF x = '0' THEN current <= got10; ELSE current <= got1;
END IF;
WHEN got10=>
WAIT UNTIL clk = '1';
IF x = '1' THEN current <= got101; ELSE current <= reset;
END IF;
WHEN got101=>
WAIT UNTIL clk = '1';
IF x = '1' THEN current <= got1011; ELSE current <= got10;
END IF;
WHEN got1011=> z <= '1';
WAIT UNTIL clk = '1';
IF x = '1' THEN current <= got1; ELSE current <= got10;
END IF;
END CASE;

Come si è visto il VHDL permette di rappresentare diverse versioni di una macchina a stati finiti, a
diversi livelli di astrazione. Nella descrizione VHDL, per modellare il riconoscitore di stringhe
attraverso una macchina di Mealy, si può ricorrere ai costrutti di assegnazione con guardia e
funzione di risoluzione. Un'assegnazione con guardia è un'istruzione di assegnamento che è
eseguita solo quando l'espressione booleana ad essa associata è valutata come vera. Il segnale che
compare alla sinistra di un'assegnazione con guardia è detto quindi segnale con guardia (guarded
signal). La condizione di guardia può essere definita implicitamente all'interno di un modulo block:
tale blocco conterrà nella prima riga della sua definizione la condizione che costituisce la guardia.
Ad esempio,
label_b: BLOCK(Condizion_Guard)
BEGIN
w_signal <= GUARDED x_signal AFTER delay_a;
z_signal <= y_signal AFTER delay_b;
END
All'interno del Block, le istruzioni di assegnazione con guardia sono ottenute ricorrendo alla parola
chiave GUARDED. Se tale parola è presente, la singola istruzione di assegnazione è eseguita solo
quando l'espressione di guardia viene valutata come vera .Quando la condizione di guardia è falsa,
l'assegnamento non viene eseguito, qualunque sia la forma d'onda dei segnali sul lato destro
dell'assegnamento. In particolare, quando la guardia è falsa i segnali sul lato sinistro
dell'assegnamento sono detti disconnessi dai loro rispettivi driver. Nel nostro caso, fino a che la
guardia è bassa, il segnale w_signal è come se si trovasse in alta impedenza. Il segnale z_signal
invece, sebbene si trovi nel blocco è indipendente dalla condizione di guardia.
Un blocco può contenere al suo interno la definizione di un altro blocco innestato, che a sua volta,
può contenere un altro blocco, e così via. Se i blocchi innestati sono con guardia allora, dato un
assegnamento con guardia, la guardia fa riferimento a tutti i blocchi in cui l'assegnamento è
contenuto. Ad esempio,
Outer_b: BLOCK (Outer_Guard)
BEGIN
w <= GUARDED x AFTER delay_a;
Inner_b: BLOCK (Inner_Guard)
BEGIN

50
z <= GUARDED y AFTER delay_b;
END
END
L'espressione guard è ottenuta ponendo in AND le condizioni di guardia di tutti i blocchi nello
scope. In particolare, l'assegnamento su w ha effetto solo se Outer_Guard risulta vera; mentre
l'assegnamento su z ha effetto solo se la condizione booeleana Outer_Guard and Inner_Guard
risulta vera.
Quando si hanno diverse sorgenti per lo stesso segnale, il valore che tale segnale assumerà non è
determinato. Ad esempio, date le seguenti assegnazioni concorrenti, in cui si hanno più sorgenti per
lo stesso segnale USignal
USignal<= a;
USignal<= b;
USignal<= c;
USignal<= d
si otterrebbe un messaggio di errore. Infatti, supponiamo il caso in cui, ad un determinato istante, a
valga '0' e b '1' ; quale valore dovrebbe assumere USignal? Per risolvere questo problema si ricorre
alla Funzione di Risoluzione. La funzione di Risoluzione è applicata ad un segnale, ed ha il compito
di definire il valore che esso dovrà assumere. Essa è definita nella parte dichiarativa del segnale a
cui è applicata. La sintassi generale è
FUNCTION Resolv_fun_Name(Drivers: Signal_Type_Vector) RETURN Signal_Type
Tale funzione, è invocata ogni volta che occorre un evento su uno qualsiasi degli input per il
segnale a cui è applicata. Quando invocata, restituisce un valore dello stesso tipo del segnale, tale
valore sarà assegnato al segnale. I parametri di input della funzione devono essere di tipo vettore, i
cui elementi sono dello stesso tipo del segnale a cui è associata.
Consideriamo la funzione di risoluzione che al singolo segnale associa la concatenazione in AND di
tutte le sue sorgenti multiple.
--USE qit, qit_vector, AND from basic_utilities
FUNCTION Anding(drivers: qit_vector) RETURN qit IS
VARIABLE accumulate: qit:=‘1’;
BEGIN
FORI IN drivers'RANGE LOOP
accumulate:=accumulate AND drivers(i);
ENDLOOP;
RETURN accumulate;
END Anding;
Utilizziamo tale funzione per la risoluzione dell'assegnamento concorrente sul segnale Usignal.
L'attributo range indica che il ciclo avviene su tutti gli elementi del vettore. Un esempio completo è
riportato di seguito:
USEWORK.basic.utilities.ALL
ARCHITECTURE wired_andOF circuit_component
FUNCTION Anding (drivers: qit_vector) RETURN qit IS
VARIABLE accumulate: qit:=‘1’;
BEGIN
FORI IN drivers’RANGELOOP
accumulate:=accumulate AND drivers(i);
ENDLOOP;
RETURNaccumulate;
ENDAnding;
SIGNAL USignal:Anding qit;
USignal<= a;
USignal<= b;
USignal<= c;
USignal<= d;

ENDwired_and

51
I 4 assegnamenti concorrenti, applicati allo stesso segnale USignal, sono risolti tramite la funzione
Anding: un evneto su uno dei segnali in input provoca l'invocazione della funzione Anding, che
assegnal ad USignal il valore determinato dalla And di a,b,c,d. Il VHDL non specifica l'ordine con
cui le diverse sorgenti sono concatenate. La funzione è invocata ad ogni ciclo di simulazione in cui
il segnale risolto è attivo. Ad ogni invocazione riceve in input un array di valori, contenente un
elemento per ogni sorgente del segnale. Nello specifico, gli elementi di tale vettore sono tutte le
sorgenti del segnale per cui la guardia è vera. Se per taluni segnali la guardia fosse falsa, questi non
verrebbero inseriti nell'array: in questo caso il driver relativo a quella sorgente viene considerato
disconnesso oppure, la transazione relativa viene considerata come nulla. Può capitare quindi che
tutti gli assegnamenti con guardia siano disconnessi e che, alla funzione di risoluzione, sia passato
un vettore vuoto.
In questo caso, la funzione avrà un comportamento definito dal tipo di segnale a cui è associata al
momento della dichiarazione:
signalsignal_name :resol_function_name [ signal_kind ][ := expressione_default ]
;
signal_kind ::= bus | register
Se il segnale è di tipo bus, ovvero se la connessione multipla ad esso viene modellata come un bus,
la funzione di risoluzione, se invocata con un vettore vuoto, restituisce il valore che rappresenta
l'output di default del bus quando non c'e' nessun segnale che lo pilota. Se il segnale è di tipo
register, allora la funzione di risoluzione lascia inalterato il valore che il segnale aveva prima che
essa fosse invocata.

Consideriamo un riconoscitore di stringa modellato mediante una macchina di Mealy.


LIBRARY ieee;
USE ieee.std_logic_1164.ALL;
entity detector is
port(x, clk: in bit; z: out bit);
end detector;
architecture singular_state_machine ofdetector is
typestate is(reset, got1, got10, got101);
typestate_vector isarray(natural range <>) of state;
functionone_of (sources: state_vector) return state is
begin
return sources(sources'left);
endone_of;
signal current: one_of state register := reset;
begin
clocking: block(clk='1' and not clk'stable)
begin
s1: block (current=reset and guard)
begin
current<=guardedgot1 when x='1'else reset;
end block s1;
s2: block (current=got1 and guard)
begin
current<=guardedgot10 when x='0'else got1;
end block s2;
s3: block (current=got10 and guard)
begin
current<=guardedgot101 when x='1'else reset;
end block s3;
s4: block (current=got101 and guard)
begin
current<=guardedgot1 when x='1'else got10;
z<='1' when(current=got101 and x='1')else '0';

52
end block s4;
end block clocking;
end singular_state_machine;

Mediante la funzione di risoluzione il segnale Current ha in ogni istante un unico valore di


pilotaggio. Essa accetta in input un vettore di stati di dimensione non prefissata: l'operatore left
restituisce il primo indice del vettore. Pertanto, sources(sources'left) è quindi il primo elemento del
vettore. Notiamo inoltre due blocchi innestati. La guardia è costituita da current=reset AND
(clk='1' and not clk'stable).
Negli esempi presentati si è sempre fatto ricorso ad un tipo enumerato per introdurre i possibili stati
del sistema descritto. Qualora il codice VHDL sia sottoposto ad un programma di sintesi per
derivarne una netlist, lo stato sarà automaticamente codificato attraverso opportuni valori binari. In
generale, ci si può aspettare che la codifica risponda a criteri di ottimizzazione. In qualche caso,
comunque, può essere necessario controllare esplicitamente la codifica dello stato.
In generale, per definire esplicitamente la codifica degli stati, occorre intervenire sul programma di
sintesi, con modalità che non sono generalizzabili. Un modo molto diffuso per comunicare col
programma di sintesi consiste nel fornire indicazioni tramite attributi VHDL. Un esempio può
essere il seguente:
TYPE state IS(reset, got1, got10, got101, got1011);
ATTRIBUTE enum_encoding OF state IS: TYPE IS 000 001 011 101 111;
Nell'esempio si usa un attributo su un tipo (il tipo state) consistente in una stringa che elenca la
codifica richiesta per gli elementi del tipo. Il sintetizzatore che metta a disposizione questa tecnica
riconoscerà l'attributo e si comporterà di conseguenza. Sottolineiamo ancora che una tale modalità e
il riconoscimento stesso dell'attributo dipende dal particolare programma di sintesi usato.
Un modo più generale che è possibile usare consiste nel definire esplicitamente i valori dello stato
come vettori di bit. È poi possibile definire dei vettori costanti cui è possibile dare il nome degli
stati, per un immediato riferimento.

SUBTYPE state IS std_logic_vector(2 DOWNTO 0);


CONSTANT reset: state := 000;
CONSTANT got1: state := 001;
CONSTANT got10: state := 011;
CONSTANT got101 : state := 101;
CONSTANT got1011: state := 111;
SIGNAL current : state;
Current<= got10;

53

Potrebbero piacerti anche