Sei sulla pagina 1di 163

UNIVERSITA’ DEGLI STUDI DI GENOVA

FACOLTA’ DI INGEGNERIA

Corso di Laurea Specialistica in Ingegneria Elettronica

Dispense per le esercitazioni del corso


Sistemi Elettronici Programmabili 1

Prof. Davide Anguita

A cura di: Alessandro Ghio


Basato sul lavoro di: Stefano Pischiutta

Anno Accademico 2006 - 2007


Documento realizzato in LATEX
Indice

1 Progetto di un sistema digitale 7


1.1 Introduzione . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
1.2 Rappresentazione di un sistema digitale . . . . . . . . . . . . . 8
1.3 Development flow . . . . . . . . . . . . . . . . . . . . . . . . . 12
1.4 Organizzazione della dispensa . . . . . . . . . . . . . . . . . . 15

2 Hardware Description Languages 17


2.1 Introduzione . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
2.2 VHDL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
2.2.1 Strutture . . . . . . . . . . . . . . . . . . . . . . . . . 19
2.2.2 Elementi lessicali . . . . . . . . . . . . . . . . . . . . . 26
2.2.3 Oggetti . . . . . . . . . . . . . . . . . . . . . . . . . . 28
2.2.4 Tipi di dati e operatori . . . . . . . . . . . . . . . . . . 30
2.2.5 Consigli pratici . . . . . . . . . . . . . . . . . . . . . . 37

3 Istruzioni concorrenti 39
3.1 Circuiti combinatori vs. Circuiti sequenziali . . . . . . . . . . 39
3.2 Istruzioni concorrenti semplici . . . . . . . . . . . . . . . . . . 40
3.3 Istruzioni concorrenti condizionali . . . . . . . . . . . . . . . . 41
3.3.1 Sintassi . . . . . . . . . . . . . . . . . . . . . . . . . . 41
3.3.2 Un esempio: multiplexer a 4 ingressi . . . . . . . . . . 41
3.4 Istruzioni concorrenti di selezione . . . . . . . . . . . . . . . . 43
3.4.1 Sintassi . . . . . . . . . . . . . . . . . . . . . . . . . . 43
3.4.2 Un esempio: multiplexer a 4 ingressi (con istruzioni di
selezione) . . . . . . . . . . . . . . . . . . . . . . . . . 44
3.4.3 Un secondo esempio: tabella di verità generica . . . . . 44

4 Istruzioni sequenziali 47
4.1 Il processo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
4.1.1 Introduzione . . . . . . . . . . . . . . . . . . . . . . . . 47
4.1.2 Processi con sensitivity list . . . . . . . . . . . . . . . . 48

1
4.1.3 Processi con istruzioni wait . . . . . . . . . . . . . . . 49
4.2 Istruzioni di assegnazione sequenziale per segnali . . . . . . . . 51
4.3 Istruzioni di assegnazione sequenziale per variabili . . . . . . . 52
4.4 Costrutto if...then...else . . . . . . . . . . . . . . . . . . . . . . 54
4.4.1 Sintassi . . . . . . . . . . . . . . . . . . . . . . . . . . 54
4.4.2 Un esempio: multiplexer a 4 ingressi . . . . . . . . . . 54
4.4.3 Incomplete branch . . . . . . . . . . . . . . . . . . . . 56
4.4.4 Incomplete assignment . . . . . . . . . . . . . . . . . . 56
4.5 Costrutto case...when . . . . . . . . . . . . . . . . . . . . . . . 57
4.5.1 Sintassi . . . . . . . . . . . . . . . . . . . . . . . . . . 57
4.5.2 Un esempio: multiplexer a 4 ingressi . . . . . . . . . . 58
4.6 Costrutto for...loop . . . . . . . . . . . . . . . . . . . . . . . . 59
4.6.1 Sintassi . . . . . . . . . . . . . . . . . . . . . . . . . . 59
4.6.2 Un esempio: XOR bit–a–bit a 8 bit . . . . . . . . . . . 59

5 Progetto di circuiti combinatori 61


5.1 Operator Sharing . . . . . . . . . . . . . . . . . . . . . . . . . 62
5.2 Functionality Sharing . . . . . . . . . . . . . . . . . . . . . . . 63
5.3 Ottimizzazioni di layout . . . . . . . . . . . . . . . . . . . . . 65

6 Progetto di circuiti sequenziali 71


6.1 Introduzione . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
6.1.1 Circuiti sequenziali . . . . . . . . . . . . . . . . . . . . 71
6.1.2 Elementi di memoria . . . . . . . . . . . . . . . . . . . 71
6.1.3 Classificazione dei circuiti sequenziali . . . . . . . . . . 73
6.2 Circuiti sequenziali sincroni . . . . . . . . . . . . . . . . . . . 74
6.3 Programmazione di elementi di memoria elementari . . . . . . 75
6.3.1 Positive–Edge–Triggered D Flip–Flop . . . . . . . . . . 76
6.3.2 FF D con reset asincrono . . . . . . . . . . . . . . . . . 76
6.3.3 Registro a 8 bit . . . . . . . . . . . . . . . . . . . . . . 78
6.3.4 RAM . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
6.3.5 ROM . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
6.4 Un esempio: contatore sincrono modulo–m programmabile . . 80
6.4.1 Prima soluzione . . . . . . . . . . . . . . . . . . . . . . 80
6.4.2 Seconda soluzione . . . . . . . . . . . . . . . . . . . . . 81
6.5 Analisi temporale di circuiti sincroni . . . . . . . . . . . . . . 83
6.5.1 Massima frequenza di clock . . . . . . . . . . . . . . . 84
6.5.2 Altre informazioni . . . . . . . . . . . . . . . . . . . . 85
6.5.3 Output del sintetizzatore Xilinx . . . . . . . . . . . . . 86
6.6 Utilizzo di variabili in circuiti sequenziali . . . . . . . . . . . . 89

2
6.6.1 Progettazione del contatore modulo–m utilizzando vari-
abili . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91

7 Macchine a stati finiti 93


7.1 Introduzione . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
7.2 Rappresentazione di una FSM . . . . . . . . . . . . . . . . . . 94
7.2.1 Pallogrammi . . . . . . . . . . . . . . . . . . . . . . . . 94
7.2.2 ASM . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
7.3 Alcune considerazioni sulle FSM . . . . . . . . . . . . . . . . . 101
7.3.1 Considerazioni temporali . . . . . . . . . . . . . . . . . 101
7.3.2 Macchine di Moore e macchine di Mealy: pro e contro . 101
7.4 Descrizione VHDL di una FSM . . . . . . . . . . . . . . . . . 103
7.5 Codifica degli stati . . . . . . . . . . . . . . . . . . . . . . . . 110

8 Look–Up Tables (LUT) 113


8.1 Che cos’è una LUT? . . . . . . . . . . . . . . . . . . . . . . . 113
8.2 Pro e contro . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114
8.3 Codice VHDL per una LUT . . . . . . . . . . . . . . . . . . . 114
8.4 Codice per la generazione automatica del VHDL . . . . . . . . 116

9 Hierarchical Design 119


9.1 Introduzione . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119
9.1.1 Hierarchical Design: pro e contro . . . . . . . . . . . . 120
9.1.2 Costrutti VHDL per il Hierarchical Design . . . . . . . 120
9.2 Component . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121
9.3 Generic . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123
9.4 Configuration . . . . . . . . . . . . . . . . . . . . . . . . . . . 125
9.5 Library . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126
9.6 Subprogram . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126
9.7 Package . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127

10 Parameterized VHDL 129


10.1 Attributi di un array . . . . . . . . . . . . . . . . . . . . . . . 129
10.2 Array con e senza range . . . . . . . . . . . . . . . . . . . . . 130
10.3 Costrutto For...Generate . . . . . . . . . . . . . . . . . . . . . 131
10.4 Costrutto if...generate . . . . . . . . . . . . . . . . . . . . . . 132
10.5 Array bidimensionali . . . . . . . . . . . . . . . . . . . . . . . 133
10.5.1 Array bi–dimensionale effettivo . . . . . . . . . . . . . 133
10.5.2 Array bi–dimensionale emulato . . . . . . . . . . . . . 134

3
11 Testbench 135
11.1 Test di dispositivi digitali . . . . . . . . . . . . . . . . . . . . 135
11.2 Creazione di un testbench . . . . . . . . . . . . . . . . . . . . 136

12 Esempi finali e guida a Xilinx ISE Web–Pack 143


12.1 Introduzione a Xilinx ISE Web Pack 8.2i . . . . . . . . . . . . 143
12.2 Esempi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 146
12.2.1 Half–Adder asincrono . . . . . . . . . . . . . . . . . . . 146
12.2.2 Full–Adder . . . . . . . . . . . . . . . . . . . . . . . . 148
12.2.3 Decimal Counter a 2 cifre . . . . . . . . . . . . . . . . 149
12.2.4 Calcolo con LUT di funzione trigonometrica . . . . . . 152
12.2.5 Generatore di sequenze con FSM . . . . . . . . . . . . 157
12.3 Esempio completo . . . . . . . . . . . . . . . . . . . . . . . . . 157
12.3.1 Testo . . . . . . . . . . . . . . . . . . . . . . . . . . . . 160
12.3.2 Soluzione . . . . . . . . . . . . . . . . . . . . . . . . . 160

4
Capitolo 1

Progetto di un sistema digitale

1.1 Introduzione
I dispositivi digitali sono diventati, negli ultimi 40 anni, sempre più dif-
fusi ed utilizzati in un gran numero di svariate applicazioni. A partire dalla
loro prima comparsa, la quantità di transistor in ogni chip è cresciuta espo-
nenzialmente, fino a raggiungere anche centinaia di milioni di transistor in un
singolo componente. Se agli albori della storia dei dispositivi hardware essi
erano prevalentemente usati nei cosiddetti computational systems, ora, grazie
alla loro sempre maggiore economia di esercizio ed acquisto e alle notevoli
capacità di elaborazione, molti sistemi meccanici, comunicazionistici, elet-
tronici e di controllo vengono digitalizzati ed introdotti in strutture quali
DSP e FPGA.
Ovviamente, al crescere delle risorse disponibili e delle capacità di elab-
orazione, cresce anche la complessità di progetto di tali strutture hw. A tal
scopo sono nati linguaggi di programmazione che consentono un’ottima as-
trazione ad alto livello, in modo da confinare operazioni poco user–friendly
al solo range di azione del cosiddetto sintetizzatore, ovvero quel modulo soft-
ware che tradurrà le istruzioni dell’utente nello schema logico vero e proprio
(sfruttando strutture e porte logiche presenti sulla scheda di destinazione).
Sebbene, quindi, un software possa automatizzare alcune operazioni, esso è
comunque in grado di svolgere solo un numero limitato di ottimizzazioni: per
buono che sia, un sintetizzatore non potrà mai rendere efficiente uno schema
mal progettato o, peggio ancora, errato e mal testato. Pertanto, il progetto e
la sintesi di una struttura hardware sono solo due dei molti step che portano
alla creazione di un sistema digitale efficace ed efficiente.

5
1.2 Rappresentazione di un sistema digitale
Progettare un sistema digitale complesso non è per nulla semplice. Og-
ni passo del processo di produzione richiede informazioni sul sistema, che
spaziano dalle specifiche di I/O al layout fisico sulla scheda. Per questo sono
nate diverse rappresentazioni (views) di un sistema, le quali rappresentano
diverse prospettive attraverso le quali vedere l’oggetto in analisi. Vengono
anche dette livelli di astrazione, in quanto consentono di concentrarsi di volta
in volta solo su quelle caratteristiche vitali, tralasciando per analisi successive
problematiche secondarie. Esistono sostanzialmente tre views, qui presentate
in ordine di astrazione decrescente:

• Behavioral view

• Structural view o Register Transfer Level (RTL)

• Logic view

• Layout view.

La behavioral view descrive il comportamente e la funzionalità di un sis-


tema. Non si occupa di come possa essere implementata una certa operazione
all’interno dell’FPGA, semplicemente definisce gli input e output trattando
il blocco di elaborazione come una black box. Si definisce come il sistema
in analisi dovrà reagire a determinati input, quali output dovrà fornire, se
gli I/O dovranno essere sincroni con un clock o potranno essere asincroni, e
altro ancora.
La structural view permette di descrivere la struttura interna di un sis-
tema. In essa, vengono presentati i blocchi di elaborazione e come essi sono
interconnessi fra loro. La structural view è spesso descritta attraverso una
netlist, ovvero una lista di nodi ed interconnessioni.
Le logic e layout view (spesso unite sotto un’unica rappresentazione, chia-
mata physical view ) descrivono le caratteristiche fisiche di basso livello del
sistema. Si specificano la dimensione dei componenti fisici, il numero di tran-
sistor e porte logiche necessarie per le varie operazioni, l’ubicazione di esse
all’interno della scheda, il tracciamento e le caratteristiche dei path di con-
nessione tra i vari blocchi. Un esempio di physical view è il layout di una
scheda. Si parte da un’astrazione a livello di porta logica (AND/OR) fino ad
arrivare anche alla scelta di quali buffer utilizzare per l’I/O (layout level).
In Figg. 1.1, 1.2, 1.3 e 1.4 viene mostrato un semplice esempio di un
half–adder a un bit.

6
Figura 1.1: Blocco per un half–adder a un bit

Figura 1.2: Blocco strutturale per un half–adder a un bit

7
Figura 1.3: Layout di una Xilinx Virtex–4 sulla quale è presente (nel cerchio)
l’implementazione fisica dell’half–adder

8
Figura 1.4: Ingrandimento del placing su FPGA dell’half–adder

9
1.3 Development flow
Il Development Flow (DF ) rappresenta il diagramma di flusso di riferi-
mento per la progettazione di componenti hw su FPGA. Innanzitutto, esso
varia notevolmente sulla base della grandezza del progetto che si vuol realiz-
zare, ove per “grandezza” si intende il numero di porte logiche che verranno
utilizzate dal modulo progettato e se esso sfrutterà IP–core già realizzati
precedentemente. Di solito, si organizzano i progetti secondo la seguente
classificazione:

• small size design: vengono impiegate meno di 10mila porte logiche e si


basano su eventuali IP–core precedentemente realizzati, ma di piccole
dimensioni e limitate funzionalità;

• medium size design (quello di nostro interesse): vengono impiegate tra


10 e 50mila porte logiche e si basano su eventuali IP–core preceden-
temente realizzati, ma di piccole/medie dimensioni e limitate funzion-
alità;

• large size design: vengono impiegate più di 50mila porte logiche e si


basano su eventuali IP–core precedentemente realizzati, completi, com-
plessi e spesso a scatola nera. Possono avere come target sia FPGA ma
anche ASIC.

Concentriamoci su progetti di medio/piccole dimensioni. Il flusso di pro-


getto inizia con la descrizione comportamentale, in cui non specifichiamo
come un sistema debba essere implementato, ma solo cosa vogliamo che es-
so faccia e quali interfacce abbia con l’esterno. Passiamo alla structural
view, implementando (per esempio, in VHDL) i blocchi che ci permettono di
garantire che a determinati input corrispondano certi output. Genereremo,
quindi, una netlist RTL che descriverà il sistema. Prima di procedere alla
traduzione di tale netlist di alto livello in un insieme di porte logiche per la
physical view, dobbiamo verificare che il codice da noi scritto non contenga
errori di alto livello: per esempio, se vogliamo realizzare una macchina a sta-
ti finiti (FSM), dobbiamo controllare che il diagramma ASM che la descrive
sia corretto nel suo progetto e nella sua implementazione. A tale scopo, si
utilizzano i testbench, che sono dei semplici file (per esempio, in VHDL) in
cui vengono preparati appositi stimoli di input per il controllo delle uscite.
In pratica, è come il test di un dispositivo (detto DUT, ovvero Device Under
Test) al classico banco hardware: dobbiamo disporre un generatore di forme
d’onda (word generator, per esempio) cn gli ingressi che vogliamo, connet-
tervi il DUT e osservare le uscite su un Logic State Analyzer. Nel nostro

10
caso, il generatore di stimoli è scritto dall’utente nel file VHDL, il DUT è
il nostro progetto e l’output verrà visualizzato su un apposito software di
test (ModelSim, per esempio). Si veda anche la Fig. 1.5. Dovremo, inoltre,
preparare eventuali file di constraint per quanto riguarda eventuali vincoli
temporali o di posizionamento del dispositivo sulla FPGA.

Figura 1.5: Esempio di schema a blocchi per il test di un DUT. Nel nostro
caso, il generatore di stimoli è il file di testbench, mentre l’oscilloscopio è il
simulatore (es. ModelSim)

A questo punto, il secondo passo è rappresentato dalla simulazione vera


e propria del sistema: presi i sorgenti RTL e i testbench preparati, si passa
alla verifica che tutto funzioni correttamente. In questo primo passo, non si
considerano i ritardi delle porte logiche, ma si fa semplicemente un test di
altissimo livello per controllare la bontà sintattica del codice generato.
Il passo successivo è rappresentato dalla sintesi: a partire dalla netlist
RTL, si genera una netlist di sintesi, in cui le funzioni di alto livello vengono
tradotte in connessioni fra porte logiche, Look–Up–Tables (LUT), buffer e
altri componenti elementari.
Si effettua una seconda simulazione, questa volta più realistica, in cui
si tiene conto dei ritardi delle porte logiche. Attenzione: non è ancora la
simulazione definitiva, in quanto semplicemente le funzioni di alto livello
sono state tradotte in equazioni booleane. In questa fase si tiene conto solo
dei tempi di setup dei flip–flop, delle porte logiche, ecc. ma non dei ritardi
dovuti, ad esempio, ad interconnessioni fisiche lunghe.
Se il dispositivo passa brillantemente la cosiddetta timing simulation,
si passa al quinto passo, che consiste nel place–and–route: a partire dalla
netlist di sintesi e dagli eventuali constraint, le porte logiche trovate vengono
fisicamente piazzate su uno schema della FPGA che si vuole usare. Ora si
ha a disposizione un design completo: le funzioni sono state sintetizzate,
e abbiamo anche una traccia di piazzamento (spesso non ottimale) su hw.
Viene generata una netlist finale, detta post par–netlist (post place–and–route
netlist).
A questo punto, si simula nuovamente il comportamento del sistema in
condizioni quasi reali (simulazione post place–and–route). I risultati tengono

11
Figura 1.6: Development flow

12
conto (attenzione, in modo approssimato e simulato) di tutti i ritardi, dovuti
sia a porte logiche sia a piazzamenti e interconnessioni all’interno dell’FPGA.
Se il DUT supera brillantemente anche quest’ultimo step, si passa final-
mente alla generazione del bitstream, ovvero del file di programmazione vero
e proprio della FPGA. Dato che una FPGA può essere vista come un array di
unità di calcolo elementari, che possono venire connesse ed attivate o meno
attraverso dei fuse, semplicemente il bitstream è una matrice di 0 e 1, i quali
rappresentano le locazioni ove effettuare i fuse.
L’ultimo step è la verifica del corretto funzionamento del nostro DUT
sulla FPGA programmata attraverso il bitstream. Lo schema generale è
presentato in Fig. 1.6.
Possiamo, quindi, pensare di dividere idealmente lo sviluppo di un dis-
positivo digitale in tre sezioni:

1. Synthesis: comprende tutta la fase che porta dalla stesura del codice
(o schematic) sorgente alla generazione della netlist RTL e di sintesi;

2. Verification: comprende lo sviluppo del file di testbench e le varie fasi


di test, a partire da quella comportamentale fino alla timing analysis
dopo il place–and–route;

3. Physical Design: comprende tutti gli step che portano all’implemen-


tazione fisica e al piazzamento dei componenti necessari sulla FPGA. Di
solito, si parte da una netlist post–sintesi e si giunge fino al bitstream
di programmazione.

1.4 Organizzazione della dispensa


Queste pagine vogliono fornire un’introduzione generale al VHDL1 , ap-
profondendo aspetti importanti nell’ambito dello sviluppo di sistemi anche
complessi. In particolare:

• nel Cap. 2, introdurremo i linguaggi orientiati alla descrizione hard-


ware, descrivendo brevemente alcune caratteristiche del VHDL, oggetto
degli approfondimenti dei successivi capitoli;

• nel Cap. 3, analizzeremo i costrutti concorrenti nella programmazione


VHDL, distinguendo fra circuiti combinatori e sequenziali;
1
Alcuni spunti sono tratti dal libro P.P. Chu, “RTL Hardware Design Using VHDL”,
J. Wiley & Sons.

13
• nel Cap. 4, introdurremo le istruzioni sequenziali, il concetto di process
e alcune strutture basilari quali if, case e for ;

• nel Cap. 5, studieremo alcuni aspetti avanzati della progettazione di


circuiti combinatori e della loro ottimizzazione;

• nel Cap. 6, ci concentreremo sui circuiti sequenziale e sull’analisi di quei


componenti fondamentali che li costituiscono: per esempio, elementi
di memoria. Vedremo la differenza nell’uso di segnali e variabili, e
proveremo a progettare qualche semplice circuito;

• nel Cap. 7, richiameremo alcuni concetti sulle macchine a stati finiti,


la loro rappresentazione, la loro codifica e implementazione in VHDL;

• nel Cap. 8, vedremo cosa sono le Look–Up Tables, i vantaggi e gil


svantaggi nell’uso di tale costrutto, come implementare una LUT in
VHDL e come generare tale codice in maniera automatizzata;

• il Cap. 9 è dedicato ad aspetti avanzati della programmazione gerar-


chica, utile per dispositivi complessi;

• nel Cap. 10 approfondiremo un altro aspetto di progettazione modu-


lare, ovvero il VHDL basato su parametri, in modo da personalizzare
strutture molto generali;

• nel Cap. 11 vedremo come effettuare test e simulazione di dispositivi


digitali;

• infine, nel Cap. 12, verrà proposta una brevissima guida all’ambiente
di sviluppo Xilinx ISE Web Pack 8.2i e verranno svolti alcuni esercizi
significativi.

14
Capitolo 2

Hardware Description
Languages

Introdurremo in questo capitolo molti degli aspetti fondamentali dei lin-


guaggi per la descrizione hardware, concentrandoci ovviamente sul VHDL.
Questo capitolo vuole essere una panoramica generale: maggiori dettagli
verranno presentati nei prossimi capitoli.

2.1 Introduzione
Molti linguaggi di programmazione classici (C, Java, C++,...) non si
addicono ad una descrizione hardware efficiente. Una delle caratteristiche
principali nonchè peculiarità dei circuiti hardware e la capacità di esecuzione
di processi in parallelo, normalmente non contemplata da linguaggi quali il C:
in essi, le istruzioni sono eseguite in maniera sequenziale seguendo il flow di
scrittura. Non solo: di solito, le varie subroutine utilizzano risultati ottenuti
da subroutine precedentemente eseguite, il che comporta anche la necessità
di manterere non solo l’ordine delle istruzioni all’interno di una funzione, ma
anche l’ordine nel quale tali funzioni vengono chiamate. Nel momento in
cui si vuole implementare in hardware una certa funzionalità, spesso molti
processi possono essere eseguiti in parallelo: non solo, in generale è anche
buona norma mantenere una buona modularità ed indipendenza fra processi,
in modo anche da limitare lo scambio dati, spesso problematico a causa di
ritardi ed interconnessioni. Ciò non toglie che esistano degli adattamenti del
C all’hw (i cosiddetti HLL, High Level Languages): System–C è stato un
(pressochè fallimentare) esempio. Il nuovo trend in quest’ottica riguarda la
ricerca di traduttori C–to–VHDL, i quali partono da un codice C “spolpato”

15
delle funzioni che meno si addicono all’hardware e forniscono in uscita un file
VHDL.
E’ palese che, nel momento in cui esiste un linguaggio (per quanto verbose,
pesante e poco intuitivo quale il VHDL) nato ad hoc per l’hardware, ogni
tentativo di riportare linguaggi nativamente sequenziali quali il C ad avere
le caratteristiche di un HDL (Hardware Description Language) snatura il
codice stesso. Pertanto, spieghiamo meglio cosa sia un linguaggio orientato
alla descrizione hw.

2.2 VHDL
I primi Hardware Description Languages (HDL) nacquero negli anni ’80
per consentire un’agevole descrizione di schemi hw attraverso un linguaggio
che fosse un buon trade–off tra esigenze di basso livello (hardware) e alto
livello (programmabilità). Infatti, nel momento in cui si svilupparono chip
contenenti sempre una densità maggiore di transistor a parità di superficie,
descrivere schemi attraverso netlist scritte a mano divenne, di fatto, impro-
ponibile anche per il più paziente degli implementatori. Fu cosı̀ che ebbero
immediata diffusione VHDL e Verilog. Entrambi hanno frecce al proprio
arco, ed entrambi hanno difetti. Una delle caratteristiche molto usate del
VHDL è la sua capacità di generazione parametrizzata di entità (semplice-
mente, se abbiamo bisogno, ad esempio, di inserire in un chip 16 sommatori,
essi possono essere istanziati attraverso un semplice ciclo FOR...GENERATE),
ed è per questo che, al momento, forse il VHDL ha un piccolo vantaggio sul
Verilog.
VHDL è un acronimo, in verità, di un acronimo: infatti, ‘V’ equivale a
VHSIC, acronimo di Very High Speed Integrated Circuit. Nato nei primissimi
anni ’80, venne diffuso inizialmente dal Dipartimento della Difesa degli USA
come standard per la documentazione hw, ed è stato più volte modificato nelle
sue caratterizzazioni attraverso diversi standard. Nel 1987, venne definito
dall’IEEE lo standard definitivo, che tutt’oggi utilizziamo. E’ un linguaggio
case–insensitive, ma molti programmi di sintesi sono case–sensitive (!), quindi
attenzione. Esistono poi 7 packages IEEE di VHDL, ovvero 7 librerie che
aggiungono funzionalità allo standard base. I principali e più usati sono:
• 1076.3, in cui vengono definiti alcuni standard per la sintesi hw;
• 1076.6, in cui vengono definiti gli standard per la generazione della
netlist RTL;
• 1164, il più usato, in cui vengono definiti molti tipi di dati, utili in
operazioni elementari e bit–a–bit (i cosiddetti tipi std_logic_1164).

16
2.2.1 Strutture
Il VHDL si basa su alcune strutture chiave, qui disposte in ordine gerar-
chico, e che andremo a breve a descrivere in maniera più approfondita:

• Library: sono le librerie di sistema che contengono i costrutti chiave


del linguaggio, come i tipi di dati;

• Package: sono i pacchetti di strutture dati e hw contenuti all’interno


delle librerie (per intenderci, una struttura simile al Java);

• Entity: fornisce la descrizione dell’interfaccia del blocco in analisi,


ovvero numero e tipo di I/O;

• Architecture: fornisce la descrizione della funzionalità dell’entity a cui


è associata;

• Process: sono i processi in esecuzione concorrente e parallela, contenuti


all’interno dell’architecture;

• Configuration: in strutture hw molto grandi, è spesso utile definire più


architetture aventi la stessa interfaccia. A quel punto, la configuration
permette di settare per ogni particolare istanza l’architettura voluta.

Partiamo da questo codice elementare, in modo da andare ad analizzarlo


in tutte le sue parti:

library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_ARITH.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;

entity sum is
Port ( a : in STD_LOGIC;
b : in STD_LOGIC;
s : out STD_LOGIC;
c : out STD_LOGIC);
end sum;

architecture Behavioral of sum is

begin

17
s <= a xor b;
c <= a and b;

end Behavioral;

Entity
Come detto, la entity fornisce la descrizione generale dell’interfaccia di
un dispositivo hardware. La dichiarazione di un’entità è la seguente:

entity entity_name is
Port ( port_1 : direction type;
port_2 : direction type;
...
port_n : direction type);
end entity_name;

In pratica, l’entità definisce il nome del componente che vogliamo proget-


tare (nel caso precedente, sum), il numero di porti di I/O (nel caso precedente
4, di cui due input e due output), la direzione di ognuno di essi e il tipo (per
quest’ultimo aspetto, si rimanda ai prossimi paragrafi). In particolare, un
porto può avere le seguenti configurazioni:

• in, porto di sola lettura e di ingresso per il nostro sistema;

• out, porto di sola scrittura e di uscita per il nostro sistema (attenzione,


una volta che si è scritto un valore NON può più essere riletto);

• inout, porto di ingresso o uscita, a seconda delle condizioni (attenzione,


NON può essere contemporaneamente di ingresso e uscita);

• buffer, analogo a in e out, ma, in quest’ultimo caso, il valore even-


tualmente scritto può anche essere riletto internamente.

Architecture
L’architettura descrive il comportamento dell’entity a cui è associata,
ne determina la funzionalità o gli opportuni legami tra ingressi e uscite (ne
definisce l’implementazione). La struttura classica di una architettura è la
seguente:

18
architecture nome of nome_entity is

{parte dichiarativa}

begin

{istruzioni concorrenti}

end architecture nome;

Quindi, abbiamo che la struttura di un’architettura è divisa dalla parola


chiave begin in due sezioni:

• parte dichiarativa, per segnali, tipi, variabili, costanti, sottoprogram-


mi e component (si veda il capitolo 9) da utilizzare localmente nella
architettura;

• istruzioni concorrenti, le quali possono essere:

– istruzioni di assegnazione di un valore ad un segnale (attenzione,


SOLO ad un segnale);
– processi;
– istanze, ovvero connessioni di blocchi dichiarati come componenti
nella parte dichiarativa e che vengono a tutti gli effetti istanziati.

Le istruzioni comunicano attraverso segnali e sono eseguite in parallelo,


e, più precisamente, in pipeline. Attenzione: non è detto che esse
vengano eseguite nello stesso ordine in cui sono state scritte. E’ il
sintetizzatore che sceglie come eseguire ogni processo e/o istruzione, e
tale ordine non è in alcun modo modificabile.

L’architettura di una entità può essere descritta in tre modi differenti:

1. stile comportamentale: prevede la descrizione della logica attraverso


uno o più processi dove si modellizza il comportamento del circuito
senza fornire dettagli dell’implementazione;

2. stile data flow: descrive la funzionalità di un circuito in base alle


elaborazioni concorrenti e parallele che subiscono i dati;

19
3. stile strutturale: utilizza interconnessioni fra componenti e istanze dif-
ferenti di vari blocchi, ognuno con una propria architecture e una pro-
pria interfaccia. Il componente dev’essere dichiarato con la propria
interfaccia come una qualsiasi altra variabile nella parte dichiarativa,
e dev’essere quindi istanziato in maniera concorrente tra le istruzioni
dell’architettura. Nella creazione dell’istanza, vi sarà anche il cosiddet-
to port mapping, in cui i porti di I/O del componente vengono connessi
con i porti e/o segnali dell’entità in cui vengono istanziati. Appro-
fondiremo meglio questi aspetti nel capitolo 9. A livello puramente
introduttivo, proponiamo un brevissimo esempio:

architecture arch of TOP is


signal n : bit;
component AND
port ( A,B : in bit;
C : out bit);
end component;
begin
istanza_1: AND port map(A => ingr1, B => ingr2, C => n);
end architecture arch;

In questo caso, inseriamo all’interno dell’entità TOP un componente AND,


avente due bit di ingresso e uno di uscita. Nel momento in cui istanzi-
amo l’oggetto AND all’interno della nostra architettura, connettiamo i
pin di ingresso A e B con gli ingressi di TOP, mentre creiamo un link fra
l’uscita C e il segnale n interno alla nostra architettura.

Configuration
Come già precedentemente anticipato, una entity può avere diverse ar-
chitectures ad essa associate: può essere utile qualora vi siano più dispositivi
aventi la stessa interfaccia. Attenzione, però, alla confusione che ne può
nascere: il nome al blocco viene dato dalla entity. Pertanto, si corre il rischio
di avere due istanze di una certa entità ENT che svolgono diverse elaborazioni
sui dati a seconda della configurazione data ma con lo stesso nome.
In generale, una configuration viene utilizzata solo in fase di test pre–
sintesi: supponendo di voler testare due possibili architetture per una entità,
la configuration permette un cambio veloce tra più possibili architetture. Non
è praticamente mai considerato un supporto contemplato dai sintetizzatori:
l’utilizzo di una configuration porta spesso al fallimento di una sintesi.
La sintassi del comando è la seguente:

20
configuration nome_configuration of nome_entity is
for nome_architecture
{operazioni di assegnazione}
end for;
end nome_configuration;

Process
Un processo è costituito da un insieme di istruzioni sequenziali, eseguite
poi nell’ordine in cui sono state scritte dall’utente. Il process comunica con
il resto del progetto leggendo e/o aggiornando determinati segnali e porti di
entity dichiarate al di fuori di esso. Più processi sono fra loro concorrenti,
ovvero vengono eseguiti in parallelo (più precisamente, in pipeline), ma, in
ogni istante, è attiva una sola istruzione per processo. E’ possibile anche
vedere un insieme di processi come tante istruzioni concorrenti.
La sintassi è la seguente:

[nome_etichetta:] process [sensitivity_list]

{dichiarazioni}

begin

{istruzioni sequenziali}

end process [nome_etichetta];

I campi tra parentesi quadre [] sono facoltativi. La sezione dichiarati-


va definisce elementi locali, visibili solo all’interno del particolare processo.
Possono essere dichiarate:

• costanti

• variabili

• tipi

• sottotipi

• elementi di sottoprogrammi.

Il processo si comporta come un loop infinito di un determinato gruppo di


istruzioni sequenziali. Un processo può avere una sensitivity list: in pratica, è

21
una lista di segnali presenti nella entity e/o architecture, e la variazione di uno
di questi segnali provoca l’attivazione del loop del processo. In alternativa,
si può evitare la sensitivity list e porre una delle seguenti istruzioni subito
dopo il begin o subito prima della fine del processo:

• wait on sensitivity_list;

• wait for specific_time;

• wait until condition;

Bisogna prestare attenzione, però, nell’utilizzo della seconda e terza i-


struzione: non tutti i sintetizzatori supportano (spesso, anche perchè non
sarebbe realistico...) condizioni per quanto riguarda l’attesa di un certo in-
tervallo temporale. Il consiglio è quello di utilizzare, qualora fosse possibile,
la sensitivity list.
Un processo senza sensitivity list (o, in alternativa, senza condizioni
wait) non verrà mai eseguito. E’ inoltre importante sottolineare come non
si possano utilizzare istruzioni wait nel momento in cui è presente una sensi-
tivity list. L’eventuale etichetta consente di identificare meglio un processo:
all’interno della stessa architettura dev’essere univoco e non deve richiamare
delle parole chiave del VHDL.
Per finire, un piccolo esempio di processo con sensitivity list:

esempio: process(clk)

begin

if rising_edge(clk) then
o <= i;
end if;

end process esempio;

In questo caso, abbiamo implementato il processo per un’architettura di


un flip–flop D: sul fronte di salita del clock (il comando rising_edge ha lo
scopo di controllare il fronte di salita), il segnale sull’uscita o assume il valore
del segnale sull’ingresso i. Il simbolo <= assume il significato di assegnazione,
e si legge “gets”. Attenzione, perchè il simbolo di assegnazione di un segnale
ha un significato ben diverso dal simbolo di assegnazione di una costante o
variabile: approfondiremo meglio questi aspetti nel paragrafo 2.2.3.

22
Package
Il package è una collezione di definizioni che possono essere condivise fra
due o più unità di progetto. Tali unità condivise possono essere:
• tipi di dato
• costanti
• componenti
• sottoprogrammi.
Un package si suddivide in:
• header : contiene le dichiarazioni di tutti gli elementi che potranno es-
sere visti dalle entity che utilizzano quel package. Sono quindi presenti
dichiarazioni di sottoprogrammi, costanti, tipi di dato e componenti;
• body: contiene gli eventuali dettagli implementativi degli elementi del-
l’header. Qualora il package non contenesse sottoprogrammi, potrebbe
essere formato anche dal solo header.
Esistono package di tipo standard inclusi nel VHDL, e sono quelli di cui
abbiamo già parlato in precedenza. Per esempio, molti costrutti definiti nello
standard 1164 sono contenuti nel package IEEE.std_logic_1164, il quale è
molto utilizzato per la maggiore flessibilità rispetto allo standard classico
bit.
La sintassi del package è la seguente:
package nome is

{header}

end;
package body nome is

{body}

end;
Per aggingere un package ad un modulo VHDL, si usa la keyword use.
Ad esempio, l’inclusione dello standard 1164 si effettua nel seguente modo:
use IEEE.STD_LOGIC_1164.ALL;
Analogamente al Java, si può importare tutto il package o solo parte di
esso: la parola chiave ALL svolge le funzioni del simbolo *.

23
Library
La prima istruzione in un modulo VHDL (ovvero entity più la/e architet-
tura/e) dev’essere l’importazione di una libreria, qualora poi si intendano uti-
lizzare dei package e dei componenti già realizzati. Per esempio, se (come di
solito avviene) si intendono importare package dello standard IEEE, si deve
prima di tutto caricare la libreria dell’IEEE stesso. In pratica, la gerarchia
è:

Library IEEE;
use IEEE.STD_LOGIC_1164.ALL;

{entity}

{architecture(s)}

2.2.2 Elementi lessicali


Presentiamo qui una serie di elementi lessicali caratterizzanti il VHDL.

Commenti
I commenti devono essere preceduti in VHDL dal simbolo --, e tut-
to ciò che segue tale simbolo fino al terminatore di riga viene ignorato dal
compilatore. Ad esempio, un commento può essere:

a <= b; -- al segnale a viene connesso il segnale b

Identificatori
Un identificatore rappresenta il nome di un oggetto in VHDL. Affinchè
possa essere considerato tale, un identificatore deve rispettare alcune semplici
regole:

• dev’essere composto solo di lettere, numeri e underscore;

• non può cominciare con un numero nè un underscore;

• non può terminare con un underscore;

• non sono permessi due o più underscore consecutivi.

24
Ad esempio, X10 è un nomevalido, mentre 1A non lo è. Come anticipa-
to, il VHDL è (o, meglio, sarebbe) case–insensitive. E’ altrettanto vero che
è buona norma essere consistenti con la propria definizione: se dichiariamo
una variabile Anno, non è stilisticamente e praticamente molto comodo mod-
ificarla di volta in volta in ANNO o anno o quant’altro. Senza contare che
alcuni sintetizzatori sono case–sensitive...

Parole chiave
In Fig. 2.1 presentiamo alcune parole chiave del linguaggio VHDL, che
non possono essere utilizzate come nome nè etichetta per segnali, processi,
ecc.

Figura 2.1: Parole chiave del VHDL

Numeri, caratteri e stringhe standard


Un numero in VHDL può essere un intero (come 37 o 98E+7) o un numero
reale (come 1,2345), entrambi supportati dallo standard di definizione. E’
altresı̀ possibile rappresentare un numero in altre basi, differenti dalla classica
base 10. Lo standard di definizione per una base differente è il seguente:

base # numero_nella_base_specificata #

Ad esempio, se vogliamo rappresentare 18, possiamo anche esprimerlo in


base 2 come 2#10010# o in base esadecimale come 16#12#. Per facilitare la
lettura e la comprensione di numeri lunghi, possiamo suddividerli attraverso
un underscore: ad esempio, il numero in base 2 precedente può essere scritto
come 2#1_0010# senza problemi nè errori.

25
Un carattere in VHDL è compreso all’interno di singoli apici. Ad esem-
pio, caratteri sono ’A’ e ’1’. Attenzione: scrivere 1 e ’1’ sono due cose
profondamente differenti. Il primo è un numero, mentre il secondo è un
carattere (quindi, con proprietà ben differenti).
Una stringa è invece compresa fra doppi apici, ed è sempre considerata
una stringa di caratteri: una stringa è, per esempio, "Hello". Ovviamente, è
possibile anche esprimere una stringa di bit sotto forma di stringhe di carat-
teri, ma, a questo punto, diviene impossibile utilizzare il carattere underscore
per rendere più leggibile la stringa stessa.

2.2.3 Oggetti
Ci sono quattro tipi di oggetti in VHDL, e sono: costanti, variabili, seg-
nali e file. Gli oggetti di tipo file non sono però sintetizzabili, e permangono
nello standard solo per una questione di continuità con le versioni precedenti.
Gli alias sono spesso considerati il quinto tipo di oggetto del VHDL.

Costanti
Una costante contiene un valore assegnatole in fase di dichiarazione e che
non può più venire modificato in seguito. La definizione di una costante può
avvenire nella sezione apposita di processi, architetture, package,... secondo
la seguente sintassi:

constant nome : tipo_di_dato := valore;

Variabili
La definizione dell’IEEE è “symbolic memory location”, ed è abbastan-
za rappresentativa: sostanzialmente, rappresenta una locazione di memoria
locale per un processo, in cui, per comodità di programmazione, si effettua
uno storage di un certo valore. La sintassi della dichiarazione (da effettuarsi
nelle dichiarazioni di un processo) è la seguente:

variable nome : tipo_di_dato [:= valore_iniziale];

I campi fra parentesi quadre sono facoltativi. L’assegnazione all’interno


di un processo a runtime è da considerarsi senza ritardo, perciò effettuato
con l’utilizzo del simbolo :=:

nome := 2;

26
Segnali
Un segnale è uno degli oggetti più comuni del VHDL. Va dichiarato nella
sezione apposita di una architettura, e ha la seguente sintassi:

signal nome : tipo_di_dato [:= valore_iniziale];

Come al solito, le parti fra parentesi quadre sono facoltative. L’inizializ-


zazione avviene senza ritardo, perciò con il simbolo :=. Un segnale assume
un determinato valore se viene connesso ad un porto di input, ad un altro
segnale, ad una variabile o ad una costante. Il simbolo di assegnazione è
il già citato <=. In tal caso, il segnale modifica il proprio valore SOLO al
termine del ciclo del processo, e non istantaneamente: se questo fatto ricalca
ciò che poi avviene in un circuito reale, è vero però che obbliga a prestare
attenzione. Supponiamo di avere due segnali, a,b, e di inizializzare a:=0.
Consideriamo il seguente codice:

a <= a+1;
b <= a;

L’assegnazione del valore 1 al primo passo ad a avviene solo alla fine


del ciclo. Pertanto, quando assegnamo b<=a, a ha ancora valore 0. Alla
fine del ciclo, quindi, b varrà 0. Se, invece, dichiariamo a come variabile, e
modifichiamo il codice precedente nel seguente modo:

a := a+1;
b <= a;

Tutto andrebbe come vorremmo. Attenzione però: l’utilizzo sconsider-


ato e poco ponderato di variabili può portare ad un codice non sintetizz-
abile. Senza contare che in molti processi può essere utile sfruttare questa
peculiarità del VHDL.

Alias
L’alias non è un vero e proprio oggetto, ma semplicemente un nome alter-
nativo per chiamare un oggetto o parte di esso. Viene usato per semplicità di
programmazione quando si trattano moduli complessi. La maniera più sem-
plice per comprendere l’alias è passare attraverso un esempio. Supponiamo
di voler progettare un processore, avente istruzioni a 16 bit. Supponiamo
che, per esempio, tali istruzioni siano strutturate nel seguente modo:

• un codice dell’operazione da 8 bit;

27
• due operandi da 4 bit ciascuno.

Può essere comodo definire tre alias (non si badi alla definizione dei tipi,
sarà chiara dopo la lettura del paragrafo 2.2.4) nel seguente modo:

-- istruzione a 16 bit
signal word : std_logic_vector(15 downto 0);
-- alias per l’operazione a 8 bit
alias op : std_logic_vector(7 downto 0) is word(15 downto 8);
-- alias per i due operandi
alias reg1 : std_logic_vector(3 downto 0) is word(7 downto 4);
alias reg2 : std_logic_vector(3 downto 0) is word(3 downto 0);

E’, ovviamente, nettamente più chiaro l’utilizzo di reg1 piuttosto che


considerare di volta in volta sottostringhe di word. Purtroppo, molti sinte-
tizzatori ancora non supportano gli alias, quindi bisogna prestare attenzione
nell’utilizzo.

2.2.4 Tipi di dati e operatori


In VHDL ogni oggetto ha un suo tipo, definito attraverso:

• un range o set di valori limitato che tale oggetto può assumere;

• un set di operazioni che possono essere eseguite dall’/sull’oggetto con-


siderato.

Il VHDL è strong typed, ovvero possono essere eseguite su un oggetto


di un determinato tipo solo e soltanto quelle operazioni definite per quel
determinato tipo. Per adattare (qualora sia possibile e consentito) un deter-
minato oggetto al tipo più adatto per una certa operazione, esistono funzioni
di conversione di tipo, dette anche funzioni di casting, definite nelle librerie
standard. Questa scelta per il VHDL è stata effettuata per semplicità di pro-
grammazione e sintesi; d’altra parte, però, l’utilizzo di operatori di casting
rende spesso il codice pesante e difficile da leggere.
Il VHDL è ricco di tipi: accanto ai tipi predefiniti dallo standard orig-
inale, si sono affiancati i tipi IEEE, molto utilizzati per l’enorme quantità
di operazioni e valori ad essi applicabili. Partiamo con un’analisi dei tipi
originari, per poi passare ai tipi IEEE.

28
Tipi e operatori predefiniti
Esistono circa 12 tipi originari di VHDL. I principali sono:

• integer: sono i numeri interi. Lo standard non definisce un range di


valori preciso, ma garantisce che essi debbano avere almeno un range
pari a [−(231 − 1), (231 − 1)], ovvero almeno 32 bit con segno. Esistono
sottotipi (come i positive) per rappresentare, per esempio, numeri
solo positivi;

• boolean: definiti nel solo range {true, false};

• bit: definiti nel solo range {’0’, ’1’};

• bit_vector: definiti come array mono–dimensionale di elementi di tipo


bit. La sintassi per la definizione è: bit_vector(a downto b), dove a
rappresenta il bit più significativo (MSB) e b il bit meno significativo
(LSB), e la lunghezza totale dell’array è pari a (a − b) + 1;

• time: utilizzato per le variabili di tempo, può essere misurato in s, ms,


ns, ps e fs;

• real: rappresenta i numeri reali, ma è un tipo poco utilizzato perchè


molto raramente sintetizzabile;

• character e string: caratteri ASCII e array di questi ultimi.

Originariamente, il tipo bit era nato per rappresentare quantità booleane,


limitandole ai soli due casi 1 e 0. In verità, nella pratica esistono molte al-
tre necessità: si pensi al caso di indeterminazione, o, ancora, all’alta im-
pedenza su un pin. Nacquero cosı̀ i tipi std_logic e il corrispondente
std_logic_vector, definiti nello standard IEEE 1164. Molto utilizzata è
la definizione di un nuovo tipo di dato, attraverso la parola chiave type: per
esempio, è utile definire dei nomi per identificare gli stati di una FSM. La
sintassi del comando è la seguente:

type nome_tipo is { range_valori }

Ad esempio, potremmo definire nuovi tipi per diversi scopi:

type stato is {countup, countdown}

29
Passiamo a parlare degli operatori standard del VHDL. Sono definiti
moltissimi operatori, ed ognuno di essi è applicabile solo a determinati tipi
di operandi: in Fig. 2.2 vengono mostrati i principali operatori standard.
Attenzione, però, in quanto un’operazione come l’esponenziale, per quanto
presente ed utilizzabile, non sempre viene ottimizzata dal sintetizzatore: per-
tanto, una semplice istruzione potrebbe portare ad un’occupazione di scheda
enorme, se non addirittura ad un codice non sintetizzabile. Gli operatori
sono utili, ma vanno usati con criterio nel momento in cui si voglia realizzare
un progetto serio: spesso, è meglio ottimizzare manualmente le operazioni da
eseguire sui dati, in modo da usare quasi esclusivamente operatori booleani.
Per quanto riguarda l’ordine di precedenza, esistono regole molto com-
plesse per quanto riguarda i vari tipi di operatori: la cosa migliore e che si
consiglia SEMPRE (anche per chiarezza nei confronti di chi poi dovrà riu-
tilizzare o anche solo leggere e capire il vostro codice) di fare è utilizzare le
parentesi per definire manualmente come operare sui dati. In alcuni casi è
d’obbligo. Vediamo il seguente esempio. Vogliamo calcolare y = a · b + c · d.
Il seguente codice è sbagliato:

y <= a and b or (not c) and d; -- NO!!

Infatti, molti sintetizzatori non saprebbero come compilare il precedente


codice. Il VHDL prevede una gerarchia di operatori, ma molti sintetizzatori
non contemplano una sub–gerarchia fra operatori con la stessa priorità. Il
risultato è che dovrete riscrivere il codice. Molto più chiara e corretta è la
seguente scrittura:

y <= (a and b) or ((not c) and d); -- Sı̀!!

Tipi e operatori del package IEEE std logic 1164


I tipi definiti nello standard IEEE 1164 nacquero e vennero definiti per
avvicinare la rappresentazione hw/sw del VHDL a quelle che sono le carat-
teristiche elettriche dei circuiti reali.
I tipi e gli operatori definiti in questo standard possono essere inclusi
semplicemente con le due seguenti righe di codice, da porre in cima ad un
modulo VHDL:

Library IEEE;
use IEEE.STD_LOGIC_1164.ALL;

I tipi più usati sono:

30
Figura 2.2: Tabella dei principali operatori standard

31
• std_logic: rappresenta un’alternativa al tipo bit, completandolo con
tutti gli ulteriori possibili valori che può assumere un segnale reale;

• std_logic_vector: è un vettore di bit di tipo std_logic.

I tipi std_logic possono variare in un range di 9 possibili valori. In


particolare:

• ’0’ e ’1’: il segnale è forzato a valere 0 oppure 1, con un valore di


tensione corrispondente sulla base della logica usata e stabilita dal sin-
tetizzatore. Il segnale è forzato da una corrente (detta driving current)
che viene fatta materialmente scorrere nel pin;

• ’Z’: alta impedenza, utile nel caso di buffer tri–state;

• ’L’ e ’H’: come ’0’ e ’1’, ma qui la corrente è debole e il segnale viene
forzato ai diversi stati logici sulla base della cosiddetta wired logic;

• ’X’ e ’W’: rappresentano lo stato di indeterminazione nel caso di driving


current e wired logic. Può essere utilizzato dall’utente e dal simulatore
in fase di debug per evidenziare eventuali conflitti o collegamenti non
precisi;

• ’U’: significa che un segnale non è stato inizializzato nè è stato in


alcun modo forzato ad assumere un certo valore. In questo caso, è
vietata l’assegnazione da parte di un utente, ma è utilizzato in fase di
simulazione;

• ’-’: è il simbolo don’t care.

Per quanto riguarda gli operatori, è stato effettuato un overload di molti


dei principali operatori presentati in Fig. 2.2. In Fig. 2.3 viene mostrata la
tabella degli operatori più usati. Esistono, inoltre, operatori per la conver-
sione di tipo, per passare da tipi dello standard 1164 a tipi dello standard
VHDL classico. In Fig. 2.4 vengono presentati questi ultimi.
I tipi std_logic_vector sono semplicemente array di std_logic, ovvero
di bit. Analogamente a quanto visto per i bit_vector, si può definire un
vettore nel seguente modo:

signal y : std_logic_vector(7 downto 0);

In questo caso, generiamo un segnale y a 8 bit, il cui MSB è il bit più


a sinistra. Se volessimo seguire una rappresentazione opposta, potremmo
scrivere:

32
Figura 2.3: Operatori di overload per lo standard IEEE 1164

Figura 2.4: Operatori di conversione per lo standard IEEE 1164

33
signal y : std_logic_vector(0 to 7);

Di solito, la prima è la più usata. Analogamente a quanto visto in Figg.


2.3 e 2.4, esistono operatori di conversione e overload anche per gli array.
Peculiarità di questi ultimi sono gli operatori relazionali, di concatenazione
e di aggregazione.
Per operatori relazionali, si intendono quegli operatori di confronto per
verificare, ad esempio, l’uguaglianza di due vettori. I principali sono: =,>,<.
Escluso l’operatore di uguaglianza, che richiede anche egual lunghezza per
i due vettori, gli operatori di confronto possono essere utilizzati anche con
array aventi un numero diverso di bit. Per esempio, i seguenti operatori
restituiscono true:

"011" = "011";
"011" > "010";
"0110" < "11";

L’operatore di concatenazione & è molto utilizzato per generare array più


grandi a partire da vettori più piccoli. Un esempio semplice è il seguente.
Supponiamo di avere due vettori, a e b, entrambi a 8 bit, e di voler generare
un vettore c a 16 bit. Semplicemente:

c <= a & b;

Si possono eseguire svariati tipi di operazioni. Per esempio, replicare il


MSB due volte:

c <= a(7) & a(7) & a(7 downto 2);

O effettuare uno shift di due posizioni (senza utilizzare l’apposito oper-


atore):

c <= a(1 downto 0) & a(7 downto 2);

Infine, gli operatori di aggregazione vengono spesso utilizzati in fase di


assegnazione manuale di un certo valore ad un certo segnale. Supponiamo
di voler assegnare ad un certo segnale a un valore "10100000". La maniera
più immediata è la seguente:

a <= "10100000";

Esistono delle alternative, talvolta utili. La prima è l’utilizzo della cosid-


detta notazione positional association:

34
a <= (’1’,’0’,’1’,’0’,’0’,’0’,’0’,’0’);

Altra alternativa è la notazione index/value, in cui i bit possono essere


assegnati in ordine sparso:

a <= (7=>’1’, 2=>’0’, 1=>’0’, 4=>’0’,


3=>’0’, 6=>’0’, 5=>’1’, 0=>’0’);

Più comoda è la notazione piped index/value:

a <= (7|5=>’1’, 6|4|3|2|1|0=>’0’);

La parola chiave others permette di assegnare un certo valore a tutti gli


indici non utilizzati fino a quel momento nell’espressione in cui si trova. Ad
esempio, la precedente assegnazione può divenire:

a <= (7|5=>’1’, others =>’0’);

La parola chiave others è spesso utilizzata nel momento in cui si vuole


inizializzare un intero vettore a 0 o a 1:

a <= (others =>’0’);


-- equivale a
a <= "00000000";

2.2.5 Consigli pratici


Ricapitolando, in VHDL esistono tantissimi operatori, tipi di dato, ogget-
ti di svariato tipo, per non parlare dei mille modi in cui si possono definire
ed assegnare vettori e segnali. Attenzione, però, all’uso che si fa di questa
libertà: non scordiamoci mai che ciò che noi programmiamo non è un sem-
plice toy dimostrativo sulla completezza del linguaggio, bensı̀ è un qualcosa
che deve poi essere tradotto in un array di 0 e 1 nel bitstream per program-
mare un oggetto composto per lo più da AND, OR e qualche LUT. Quando
possibile, il consiglio è sempre quello di utilizzare tipi standard IEEE 1164,
al più interi o caratteri, e di mantenere sempre una certa chiarezza nel mo-
mento in cui vengono inizializzati e assegnati valori e segnali. Il consiglio è
quello di scrivere, se necessario, anche un po’ di codice in più, ma che il tutto
sia più chiaro e immediato. Attenzione, infine, a non usare costrutti troppo
complessi: il sintetizzatore potrebbe riservarvi brutte sorprese...

35
Capitolo 3

Istruzioni concorrenti

Le istruzioni concorrenti in VHDL sono semplici, eppure molto poten-


ti. Se utilizzate in modo efficiente, il collegamento tra le elaborazioni da
esse effettuate e lo schema logico e fisico del circuito è diretto. Questo può
aiutare a progettare in maniera più efficace circuiti anche complessi. Per
“modo efficiente” si intende l’utilizzo di costrutti elementari, quali and op-
pure or, tralasciando operatori quali l’elevazione a potenza, di difficile ed
incerta sintesi. Si dividono in 3 gruppi, ovvero istruzioni concorrenti:

1. semplici;

2. condizionali;

3. di selezione;

Vedremo in questo capitolo come le istruzioni semplici siano delle istruzioni


concorrenti condizionali, ma senza condizioni.

3.1 Circuiti combinatori vs. Circuiti sequen-


ziali
Prima di addentrarci nelle istruzioni concorrenti, vediamo come possono
essere classificati i circuiti digitali.
Un circuito combinatorio o circuito basato su logica combinatoria non ha
memoria interna nè può essere diviso in stati di funzionamento. L’output è
funzione solo e soltanto degli input, e ad uguali ingressi corrispondono uguali
uscite. Sebbene in un circuito reale vi possa essere un periodo di transitorio,
il valore di regime, ottenuto dopo un breve lasso di tempo, sarà quello atteso.
In termini di implementazione, un circuito combinatorio non presenterà nel

36
proprio schema elementi di memoria (quali flip–flop o latch) o un loop in
retroazione.
Un circuito sequenziale, viceversa, è un circuito che al proprio interno
prevede la presenza di registri di stato e/o elementi di memoria. L’output di
un circuito sequenziale è funzione sia degli input che dello stato attuale del
circuito stesso.
Sebbene le istruzioni concorrenti possano essere utilizzate anche per de-
scrivere circuiti sequenziali, in quest’ultimo ambito è preferibile l’utilizzo dei
processi, che rendono più chiara e semplice anche il debug del circuito stesso
(si veda il Cap. 6 per maggiori dettagli). Le istruzioni concorrenti vengono,
invece, ampiamente utilizzate nell’ambito dei circuiti combinatori.

3.2 Istruzioni concorrenti semplici


In generale, un’istruzione concorrente semplice per l’assegnazione di un
certo valore ad un certo segnale segue questa sintassi:

destinazione <= nuovo_valore ritardo;

Quanto appena riportato è la definizione di istruzione di assegnazione


semplice cosı̀ come viene fornita dal VHDL. Un esempio semplice può essere:

y <= a + b after 10 ns;

Il segnale y assumerà un valore pari alla somma di a e b dopo 10 ns: per


esempio, potremmo voler forzare i ritardi interni dovuti alla logica combina-
toria ad essere pari a 10 ns, o ancora potremmo volerli simulare in fase di
progettazione. Purtroppo, tutto ciò non è possibile, ma non a causa di un
errore sintattico, ma di sintesi: non esiste sintetizzatore al mondo in grado
di progettare una logica tale da avere un ritardo voluto a priori. La sintassi
corretta anche per la sintesi diviene:

y <= a + b;

Sarà il progettista a dover poi, eventualmente, tener conto dei ritardi


interni nel momento in cui il blocco realizzato comunicherà con altri blocchi
hw. Il ritardo dovuto alla logica combinatoria è spesso definito δ-delay. Altra
nota: in quest’ottica, nulla ci vieta di introdurre un loop in retroazione. Ad
esempio:

y <= (not y) and b;

37
Il fatto è che la porta NOT comporterà un ritardo nell’elaborazione (an-
che se semplice...) del segnale. Se y cambia velocemente a causa, per esem-
pio, di frequenti variazioni di b, la porta NOT come si comporterà? Le uscite
saranno prevedibili e verificabili a priori? In generale, introdurre feedback
è sempre pericoloso, specie in circuiti combinatori, e l’uso in questi casi è
fortemente sconsigliato.

3.3 Istruzioni concorrenti condizionali


3.3.1 Sintassi
La sintassi per un’istruzione di assegnazione concorrente condizionale è
piuttosto semplice, ed è sostanzialmente l’unione di più istruzioni semplici
attraverso condizioni di attivazione. Vediamo:

segnale <= val_1 when condizione_1 else


val_2 when condizione_2 else
...
val_n when condizione_n else
val_default;

Le varie condizioni sono espressioni booleane che restituiscono valore true


o false. A segnale viene attribuito un valore val_i, dove la i-esima è la prima
condizione in ordine che ha restituito valore true. In parole povere, si partirà
controllando la prima condizione; se essa non restituisce true, si passa a
valutare la seconda; si prosegue, fino all’i-esima, che supponiamo restituisca
true: in tal caso, al nostro segnale attribuiremo valore pari a val_i. Se
nessuna condizione è verificata, dev’essere sempre presente un termine di
assegnazione di default. Nei circuiti combinatori, è uno dei costrutti più
utilizzati, ma va prestata attenzione all’ordine di scrittura delle condizioni in
caso di condizioni e segnali di selezione complessi.

3.3.2 Un esempio: multiplexer a 4 ingressi


Un multiplexer è semplicemente una specie di interruttore fisico hardware
in grado di connettere l’uscita con uno dei 4 ingressi, ed in particolare con
l’input scelto attraverso appositi segnali di pilotaggio. Un multiplexer a 4
ingressi e un’uscita (detto anche mux 4–1) in particolare avrà:

• 4 segnali di ingresso, supponiamo a 8 bit ciascuno;


• 2 segnali di pilotaggio, per selezionare l’ingresso 1, 2, 3 oppure 4;

38
Tabella 3.1: Tabella di funzionamento per il multiplexer 4–1
Input Output
sel o
0 0 a
0 1 b
1 0 c
1 1 d

• un segnale di uscita.

La tabella di funzionamento per il segnale di selezione è riportata in Tab.


3.1. Il seguente codice VHDL è sbagliato. Vediamo perchè:

library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_ARITH.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;

entity mux4 is
Port ( a,b,c,d : in std_logic_vector(7 downto 0);
sel : in std_logic_vector(1 downto 0);
o : out std_logic_vector(7 downto 0));
end mux4;

architecture Behavioral of mux4 is

begin

o <= a when (sel = "00") else


b when (sel = "01") else
c when (sel = "10") else
d;

end Behavioral;

Apparentemente, sembrerebbe tutto ok. Però attenzione: stiamo utiliz-


zando dei segnali dello standard IEEE 1164, e ricordiamo che essi possono
assumere ben 9 valori! Se noi avessimo in ingresso sul segnale di selezione
una stringa "X0" per un qualche motivo, noi porremmo in uscita il canale d,
il che non è corretto! Il codice precedente può essere modificato in modo da
contemplare anche questi casi patologici:

39
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_ARITH.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;

entity mux4 is
Port ( a,b,c,d : in std_logic_vector(7 downto 0);
sel : in std_logic_vector(1 downto 0);
o : out std_logic_vector(7 downto 0));
end mux4;

architecture Behavioral of mux4 is

begin

o <= a when (sel = "00") else


b when (sel = "01") else
c when (sel = "10") else
d when (sel = "11") else
"XXXXXXXX";

end Behavioral;

Nel caso di segnale di selezione non noto precisamente, poniamo in uscita


un segnale non determinato. Ogniqualvolta cambierà il segnale di selezione
o l’ingresso attualmente selezionato, l’istruzione verrà ricontrollata e l’uscita
opportunamente modificata.

3.4 Istruzioni concorrenti di selezione


3.4.1 Sintassi
Nel caso di istruzioni condizionali, un certo segnale di uscita assume un
certo valore sulla base di una o più condizioni booleane, che possono essere
molto semplici (come nel caso del multiplexer del paragrafo 3.3.2) o anche
molto complesse. Nel caso di condizioni su un singolo segnale, è spesso una
valida alternativa l’utilizzo di istruzioni di assegnazione concorrente di tipo
selettivo, ovvero aventi la seguente sintassi:

with segnale_controllo select


segnale <= val_1 when scelta_1,

40
val_2 when scelta_2,
...
val_n when scelta_n;

Come in precedenza, o si contemplano tutti i casi possibili o è neces-


saria una condizione di default. Di fatto, rispetto a quanto visto nel prece-
dente paragrafo non cambia nulla, se non per il fatto che si può controllare
un segnale invece che un’espressione booleana anche complessa. La strut-
tura di questo tipo di istruzioni ricalca quella di un select/case dei linguaggi
tradizionali, ed effettua un mapping pressochè immediato di una tabella di
verità.

3.4.2 Un esempio: multiplexer a 4 ingressi (con istru-


zioni di selezione)
Riprendiamo l’esempio del mux 4–1 del paragrafo 3.3.2. Modifichiamo
l’architecture in modo da utilizzare le istruzioni concorrenti di selezione.

architecture Behavioral of mux4 is

begin

with sel select


o <= a when "00",
b when "01",
c when "10",
d when "11",
"XXXXXXXX" when others;

end Behavioral;

Abbiamo utilizzato la keyword others per evitare di scrivere tutte le


possibili combinazioni e offrire una soluzione di default.

3.4.3 Un secondo esempio: tabella di verità generica


Supponiamo di voler implementare il circuito che generi gli output di
Tab. 3.2. Tale circuito avrà un ingresso da 3 bit e un output da 1 bit. Il
codice corrispondente sarà:

library IEEE;

41
Tabella 3.2: Tabella di verità
Input Output
x o
0 0 0 0
0 0 1 1
0 1 0 1
0 1 1 0
1 0 0 1
1 0 1 1
1 1 0 1
1 1 1 1

use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_ARITH.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;

entity tv is
Port ( x : in std_logic_vector(2 downto 0);
o : out std_logic);
end tv;

architecture Behavioral of tv is

begin

with x select
o <= ’0’ when "000",
’1’ when "001",
’1’ when "010",
’0’ when "011",
’1’ when "100",
’1’ when "101",
’1’ when "110",
’1’ when "111",
’X’ when others;

end Behavioral;

Un’alternativa più compatta è la seguente, utilizzando la notazione pipe:

architecture Behavioral of tv is

42
begin

with x select
o <= ’0’ when "000"|"011",
’1’ when "001"|"010"|"100"|"110"|"101"|"111",
’X’ when others;

end Behavioral;

Notiamo, però, una peculiarità: quando il primo bit di x è pari a 1, a


prescindere da quanto valgono gli altri due bit l’uscita è sempre 1. Potremmo
semplificare la scrittura precedente con il simbolo don’t care:

architecture Behavioral of tv is

begin

with x select
o <= ’0’ when "000"|"011",
’1’ when "001"|"010"|"1--",
’X’ when others;

end Behavioral;

Attenzione però: se in input avessimo una stringa "11X" in uscita ot-


terremo 1, anche se non è ciò che vogliamo. Quindi attenzione all’utilizzo
dei simboli don’t care, specie se si vogliono contemplare anche casi patologici
quali quelli di segnali indeterminati. E’ possibile scrivere l’espressione prece-
dente in forma ancora più compatta calcolando l’equazione booleana, per
esempio, attraverso l’utilizzo delle mappe di Karnaugh. Si verifica, infatti,
che o = x2 +x1 x0 +x1 x0 , dove x2 è il MSB e x0 il LSB. Attenzione, però, nuo-
vamente al fatto che, in questo caso, non gestiamo i casi patologici: talvolta
questo può essere un problema.

43
Capitolo 4

Istruzioni sequenziali

Come può suggerire il nome stesso, le istruzioni sequenziali vengono es-


eguite “in sequenza”, e la loro semantica è ancor più simile alle classiche
istruzioni di un linguaggio di programmazione tradizionale. Molti costrutti
sequenziali, peraltro, sono incompatibili con la natura concorrenziale propria
del VHDL, e devono perciò essere incluse all’interno di un guscio concorrente
detto process e che abbiamo presentato nel paragrafo 2.2.1. Se le istruzioni
concorrenti fornivano, di fatto, un mapping diretto (o quasi...) del codice
nelle caratteristiche più schiettamente hardware, le istruzioni sequenziali,
in generale, non possiedono questa proprietà, ma tendono a descrivere in
maniera talvolta anche astratta il comportamento del sistema (in partico-
lare, di una entity). Proprio per questa natura quasi astratta, le istruzioni
sequenziali vanno utilizzate con molta cautela, al fine di ottenere un codice
corretto sotto tutti i punti di vista, non ultima la capacità di essere poi
sintetizzato.
In questo capitolo, rivedremo molti concetti introdotti sommariamente
nel Capitolo 2, e approfondiremo meglio alcuni aspetti fondamentali.

4.1 Il processo
4.1.1 Introduzione
Un process o processo, come già più volte detto, è un costrutto che con-
tiene un insieme di azioni, le quali devono essere eseguite in maniera sequen-
ziale. Tali azioni sono le istruzioni sequenziali, mentre il processo in sè può
essere considerato come un’unica istruzione (o costrutto) concorrente.
Le istruzioni sequenziali racchiudono una vasta gamma di costrutti, i
quali possono essere utilizzati solo all’interno di un processo. Al contrario di

44
quanto visto per le istruzioni concorrenti, le istruzioni sequenziali vengono
eseguite nell’ordine in cui sono state scritte. Molti di questi costrutti sono
utilissimi in fase di programmazione, ma non sintetizzabili, perciò bisogna
prestare attenzione. In questo capitolo, analizzeremo alcuni costrutti:

• wait;

• istruzioni di assegnazione di un certo valore ad un certo segnale;

• istruzioni di assegnazione di un certo valore ad una certa variabile;

• if...then...else;

• case;

• for...loop.

Esistono strutture anche molto più complesse e potenti, facenti parte del
cosiddetto stile di programmazione parametrizzato (si veda il Cap. 10).
Attenzione a non far confusione: una istruzione sequenziale è un’istruzione
VHDL all’interno di un processo; un circuito sequenziale è un circuito dotato
di elementi di memoria e registri di stato. In generale, mentre le istruzioni
concorrenti possono essere utilizzate quasi solo nella descrizione di circuiti
combinatori, le istruzioni sequenziali e i processi possono essere usati sia per
circuiti combinatori che sequenziali. Per il momento, ci concentreremo sui
circuiti combinatori. Per maggiori dettagli sui circuiti sequenziali, si veda il
Cap. 6.
Un processo, abbiamo visto, deve avere un evento di attivazione, ovvero
una causa scatenante. Tale causa può essere settata sia attraverso l’utilizzo
di una sensitivity list, sia con costrutti come il wait. Procediamo con ordine.

4.1.2 Processi con sensitivity list


La sintassi di un processo con sensitivity list è il seguente:

process(sensitivity_list)
{dichiarazioni}
begin
{istruzioni sequenziali}
end process;

45
La sensitivity list nient’altro è che una lista di segnali alla variazione dei
quali il processo è sensibile. Infatti, analogamente a quanto succede in hw,
un processo può avere due stati: attivato o sospeso. Se è sospeso, il processo
è in stato di idle, ovvero non fa nulla, e attende che uno dei segnali della
propria sensitivity list vari. Nel momento in cui tale transizione arriva, il
processo si attiva, e vengono eseguite le istruzioni sequenziali nell’ordine in
cui sono state scritte dal programmatore. Una volta che tali istruzioni sono
terminate, il processo si sospende in attesa di una nuova variazione di un
segnale della sensitivity list.
Esistono due tipi di processi con sensitivity list: a lista completa ed
incompleta. Supponiamo di avere 3 segnali:

signal a, b, y : std_logic;

Un processo con sensitivity list completa è il seguente:

process(a,b)
begin
y <= a and b;
end process;

Un qualsiasi variazione di a o b causa l’attivazione del processo e, quindi,


un cambio di valore per y. Viceversa, un processo avente sensitivity list
incompleta è del tipo:

process(a)
begin
y <= a and b;
end process;

In questo caso, il processo viene attivato solo per variazioni di a. Questo


significa che se anche b cambia valore, e con esso dovrebbe cambiare anche
y, in verità finchè non cambia a l’uscita non subisce variazioni.

4.1.3 Processi con istruzioni wait


Un processo che presenta una o più istruzioni di tipo wait al suo interno
non ha di sensitivity list. Come anticipato nel paragrafo 2.2.1, le istruzioni
sono:

• wait on {segnali}: il processo si sospende in quel punto in attesa di


una variazione su uno dei segnali selezionati;

46
• wait until {condizione}: il processo si sospende in attesa che la
condizione divenga true;

• wait for {intervallo_di_tempo}: il processo si sospende per un


determinato periodo di tempo.

Per esempio, il circuito con sensitivity list completa del precedente para-
grafo può essere scritto come:

process
begin
y <= a and b;
wait on a, b;
end process;

Teoricamente, è sintatticamente corretto anche il seguente codice:

process
begin
y <= a and b;
wait on b;
y <= b;
wait on a, b;
end process;

Nella pratica, un utilizzo dei wait di questo tipo è fortemente sconsiglia-


to, perchè facilmente porta a codice non sintetizzabile. In generale, è sempre
preferibile utilizzare una sensitivity list, perchè:

• il costrutto wait on è sostanzialmente equivalente e complica la leggi-


bilità del codice;

• se le condizioni del costrutto wait until fossero troppo complicate,


porterebbero facilmente alla mancata sintesi del circuito;

• in generale, a meno di casi molto patologici, il costrutto wait for non


è mai sintetizzabile.

47
4.2 Istruzioni di assegnazione sequenziale per
segnali
Abbiamo affrontato in parte questi argomenti già nel paragrafo 2.2.3.
Ricordiamo che un segnale all’interno di un processo assume il suo valore
definitivo solo al termine del processo. Pertanto, se noi inizializziamo un
segnale tmp:

signal tmp : integer := 0;

Al primo passo, con il seguente processo, otteniamo y=0 e tmp=1:

process(a)
begin
z <= a;
tmp <= tmp + 1;
y <= tmp;
end process;

Di fatto, y sarà in ritardo di un passo rispetto a tmp. Questo permette


di effettuare (per quanto non sia buona norma farlo...) più assegnazioni
contemporanee:

...
signal a,b,c,d : std_logic;
...
process(a,b,c,d)
begin
y <= ’1’;
y <= a and b;
y <= c and d;
y <= a or c;
end process;

Il precedente codice equivale a calcolare direttamente:

...
signal a,b,c,d : std_logic;
...
process(a,b,c,d)
begin
y <= a or c;
end process;

48
Proprio in questa peculiarità si evidenziano le differenze con le variabili
e con le istruzioni concorrenti. Un codice come questo, per quanto insensato,
è sintetizzabile:
...
signal a,b,c,d : std_logic;
...
process(a,b,c,d)
begin
y <= ’1’;
y <= a and b;
y <= c and d;
y <= a or c;
end process;
Se non utilizzassimo i processi, ma le istruzioni concorrenti:
...
signal a,b,c,d : std_logic;
...
y <= ’1’;
y <= a and b;
y <= c and d;
y <= a or c;
...
Questo codice non sarebbe nemmeno sintetizzabile, in quanto contempo-
raneamente vorremmo connettere y al segnale di high, ma anche all’uscita di
due AND e di un OR, con evidente conflitto.

4.3 Istruzioni di assegnazione sequenziale per


variabili
Passiamo alla descrizione delle istruzioni per attribuire un certo valore ad
una variabile. Sintatticamente, rispetto ai segnali cambia poco: si utilizza
il simbolo := invece di <=. Concettualmente, invece, cambia moltissimo:
una variabile varia il proprio valore in modo istantaneo nel momento in cui
effettuiamo l’assegnazione. In certi casi, questa può essere una comodità, ma
riempire un processo di variabili può portare all’utilizzo di molte unità di
memoria nonchè spesso porta anche a codice non sintetizzabile (nella realtà,
una variabile non può cambiare in maniera immediata, ma avrà sempre il
delta–delay). Vediamo un semplice codice, in cui utilizziamo solo segnali:

49
...
signal a,b,c,d : std_logic;
signal tmp : std_logic;
...
process(a,b,c,d)
begin
tmp <= a and b;
tmp <= tmp or c;
y <= tmp and d;
end process;
In questo caso, introduciamo dei loop, in quanto questo codice equivale
al seguente:
...
signal a,b,c,d : std_logic;
signal tmp : std_logic;
...
process(a,b,c,d)
begin
tmp <= tmp or c;
y <= tmp and d;
end process;
Ricordiamo, inoltre, che y assume valore pari all’AND fra il valore di tmp
prima dell’aggiornamento e il valore di d. Se utilizziamo una variabile, le
cose cambiano molto:
...
signal a,b,c,d : std_logic;
...
process(a,b,c,d)
variable tmp : std_logic;
begin
tmp := a and b;
tmp := tmp or c;
y <= tmp and d;
end process;
In questo caso, avremo y = ((a · b) + c) · d), perchè la variabile tmp
viene aggiornata nel proprio valore ad ogni passo. Bisogna prestare atten-
zione, perchè un uso poco attento di variabili e segnali può portare a malfun-
zionamenti comportamentali. D’altro canto, un utilizzo troppo spinto delle
variabili può portare a problemi in fase di sintesi o post place–and–route.

50
4.4 Costrutto if...then...else
4.4.1 Sintassi
La sintassi di questo costrutto è analoga a quella di un qualsiasi altro
linguaggio di programmazione:

if condizione_1 then
{istruzioni sequenziali};
elsif condizione_2 then
{istruzioni sequenziali};
...
else
{istruzioni sequenziali};
end if;

Ogni gruppo di istruzioni sequenziali è detto branch, e, in particolare, si


hanno i:

• then branch: gruppo di istruzioni che segue l’if iniziale;

• elsif branch: gruppi di istruzioni che seguono gli elsif ;

• else branch: gruppo di istruzioni che segue l’else finale.

4.4.2 Un esempio: multiplexer a 4 ingressi


Riprendiamo l’esempio, presentato nel paragrafo 3.3.2, del multiplexer
4–1. Come detto, può essere implementato attraverso semplici istruzioni con-
correnti. Altrettanto semplicemente può essere implementato con istruzioni
sequenziali in un processo. Il codice è il seguente:

library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_ARITH.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;

entity mux4 is
Port ( a,b,c,d : in std_logic_vector(7 downto 0);
s : in std_logic_vector(1 downto 0);
o : out std_logic_vector(7 downto 0));
end mux4;

51
architecture Behavioral of mux4 is

begin

process(a,b,c,d,sel)
begin
if(s="00") then
o <= a;
elsif(s="01") then
o <= b;
elsif(s="10") then
o <= c;
elsif(s="11") then
o <= d;
else
o <= "XXXXXXXX";
end if;
end process;

end Behavioral;

Le differenze con i costrutti analizzati nel Cap. 3 sono, per casi semplici,
davvero piccole, ed identificare una scelta ottima tra if e with...select...when
è davvero complesso. In generale, è buona norma, mano a mano che si
complicano le condizioni da analizzare, cercare di utilizzare il costrutto if
assieme ad un processo: rende il tutto più leggibile. Ad esempio, nel caso di
una doppia condizione annidata, l’utilizzo dell’if è immediato ed intuitivo:

...
signal a,b,c,d : std_logic;
...
process(a,b,c,d)
begin
if(a=’1’) then
if(b=’0’) then
o <= c;
else
o <= d;
end if;
else

52
if(b=’1’) then
o <= c;
else
o <= d;
end if;
end process;

4.4.3 Incomplete branch


Viene definito branch incompleto un costrutto if in cui non vengono
contemplate tutte le possibili alternative. In parole povere, il seguente codice
è un incomplete branch:

...
signal a,b,c : std_logic;
...
process(a,c)
begin
if(a=’1’) then
b <= c;
end if;
end process;

Il segnale b cambia il proprio valore solo se:

• vi è stata una variazione di a o c;

• a ha valore pari a 1.

In caso contrario, b non varia. Se b non è stato ancora mai assegnato ad


un certo valore, il suo stato è ’U’.

4.4.4 Incomplete assignment


Un costrutto if può avere molti branch, e un’entità può avere molti
segnali. In alcuni branch alcuni segnali potrebbero non venire assegnati
nuovamente. Facciamo un esempio:

...
signal a,b,c,d : std_logic;
...
process(a,c)

53
begin
if(a>c) then
b <= c;
elsif (a=c) then
d <= a;
else
d <= ’0’;
end if;
end process;

In questo caso, abbiamo un complete branch, ma nel then branch non


assegnamo alcun valore a d, mentre nei due casi successivi non assegnamo
alcun valore a b. Se non modificato, un segnale mantiene il proprio valore.
Però la programmazione è poco precisa. Si consiglia di utilizzare molto rara-
mente gli incomplete branch (limitarli ai casi di controllo su fronte di salita di
un clock, per esempio) e ancor più di rado gli incomplete assignment, perchè
alcuni segnali potrebbero diventare poco gestibili.

4.5 Costrutto case...when


4.5.1 Sintassi
La sintassi del costrutto è la seguente:

case espressione is
when scelta_1 =>
{istruzioni sequenziali};
when scelta_2 =>
{istruzioni sequenziali};
...
when scelta_n =>
{istruzioni sequenziali};
end case;

Si valuta, quindi, il risultato di una certa espressione (booleana o comp-


lessa) e si esegue un certo branch (insieme di istruzioni sequenziali) sulla base
di tale risultato. Sono possibili, anche in questo costrutto, casi di incomplete
branch o assignment, anche se sconsigliabili. E’ sempre consigliabile l’intro-
duzione di una scleta di default, al più utilizzando la parola chiave others.
Questo costrutto è simile a quanto visto nel Cap. 3: come già detto nel prece-
dente paragrafo, costrutti sequenziali in processi sono più flessibili, semplici

54
ed affidabili (in termini di sintesi hw) rispetto a costrutti concorrenti, e,
quindi, sono in generale preferibili.

4.5.2 Un esempio: multiplexer a 4 ingressi


Riprendiamo per l’ennesima volta il nostro mux 4–1. Possiamo proget-
tarlo anche nel seguente modo:

library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_ARITH.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;

entity mux4 is
Port ( a,b,c,d : in std_logic_vector(7 downto 0);
s : in std_logic_vector(1 downto 0);
o : out std_logic_vector(7 downto 0));
end mux4;

architecture Behavioral of mux4 is

begin

process(a,b,c,d,sel)
begin
case s is
when "00" =>
o <= a;
when "01" =>
o <= b;
when "10" =>
o <= c;
when "11" =>
o <= d;
when others =>
o <= "XXXXXXXX";
end case;
end process;

end Behavioral;

55
4.6 Costrutto for...loop
Introduciamo il costrutto for...loop. In verità, in questa forma è rara-
mente utilizzato, perchè spesso porta a codice non sintetizzabile. Appro-
fondiremo questi aspetti e formulazioni alternative per il costrutto for nel
Cap. 10.

4.6.1 Sintassi
La sintassi è la seguente:

for indice in {range} loop


{istruzioni sequenziali};
end loop;

La variabile indice tiene traccia del numero di iterazioni, che varier-


anno all’interno di un range fissato. Il branch verrà eseguito, quindi, un
numero determinato di volte. Nè la variabile indice nè il range devono essere
preventivamente dichiarati. Vediamo un esempio.

4.6.2 Un esempio: XOR bit–a–bit a 8 bit


Premesso che esiste un operatore XOR anche per vettori di bit, supponi-
amo di volerne realizzare uno nostro. Il codice può essere:

library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_ARITH.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;

entity xor8 is
Port ( a,b : in std_logic_vector(7 downto 0);
o : out std_logic_vector(7 downto 0));
end xor8;

architecture Behavioral of xor8 is


constant WIDTH : integer := 8;
begin

process(a,b)
begin
for i in (WIDTH-1) downto 0 loop

56
o(i) <= a(i) xor b(i);
end loop;
end process;

end Behavioral;

Dichiariamo una costante WIDTH per il numero di bit dei vettori, quindi
iniziamo il loop avente come indice i e come range 7 (cioè WIDTH-1) fino a
0. In tutto, quindi, effettueremo 8 step.

57
Capitolo 5

Progetto di circuiti combinatori

Progettare circuiti combinatori non è cosı̀ semplice come potrebbe sem-


brare ad un primo approccio. Innanzitutto, come già appare chiaro dai
precedenti capitoli, bisogna porre attenzione a scrivere un codice che sia
sintetizzabile: per esempio, è necessario evitare condizioni di attesa o ritar-
do temporale. Mano a mano che un progetto si fa più ampio e complesso,
è necessario intervenire anche sulla struttura stessa del circuito: sebbene il
VHDL spesso non ricordi molto da vicino uno schematico, bisogna lavorare
in modo da rendere più efficace ed efficiente lo schema conclusivo. In parole
povere, modificare l’input per migliorare l’output.
Da un punto di vista operativo, tutto ciò si traduce in alcuni semplici
ma importanti step:

• progetto iniziale del sistema;

• ottimizzazione del progetto;

• implementazione del progetto ottimizzato;

• ottimizzazione del codice.

Per ottimizzazione del codice VHDL si intende, per esempio, l’utilizzo di


costrutti semplici, al più scrivendo operatori complessi manualmente, o l’e-
liminazione di elementi quali le variabili. In parte, per semplicità in questo
capitolo verremo meno a queste idee per quanto riguarda, soprattutto, l’im-
plementazione manuale di alcuni operatori quali la somma o la sottrazione,
supponendo che tali miglioramenti vengano effettuati dal sintetizzatore.

58
5.1 Operator Sharing
Una delle più immediate ottimizzazioni applicabili ad un codice riguar-
da la diminuzione del numero di strutture pesanti dal punto di vista com-
putazionale e di occupazione di scheda. Non scordiamoci mai che l’output
del nostro lavoro è pur sempre il bitstream di programmazione, che andrà
poi a creare (o non creare) dei fuse su una FPGA. Le FPGA, normalmente,
hanno un numero molto grande, ma pur sempre limitato, di porte logiche.
Un circuito può essere considerato migliore di un altro circuito che esegua
analoghe operazioni se offre una precisione maggiore e/o se occupa un nu-
mero di porte logiche e LUT inferiore e/o (nel caso di circuiti sequenziali,
che tratteremo nel prossimo capitolo) se fornisce un thorughput maggiore
(ovvero, ogni quanti colpi di clock otteniamo un risultato valido).
Ogni componente, sia esso un adder o un multiplexer, ha una sua occu-
pazione di scheda che lo caratterizza. E’ ovviamente improponibile pensare
di ricordare l’occupazione di tutti i possibili core hardware. In linea di massi-
ma, è comunque intuibile che un multiplexer occuperà meno porte logiche di
un sommatore, e ancora meno di un moltiplicatore. Anzi, su alcune schede un
moltiplicatore non è neppure presente: pertanto, il sintetizzatore provvederà
ad usare strutture alternative (ancora più pesanti sotto tutti i punti di vista).
E’ buona norma, quindi, tenere d’occhio anche le risorse hw disponibili sulla
scheda su cui poi vorremo implementare il nostro circuito.
In particolare, l’operator sharing prevede l’individuazione di risorse che
posssono essere utilizzate da diverse operazioni. Talvolta, ciò comporta un
aumento del numero totale di componenti: questo è, però, accettabile se
aumentano i core “leggeri” a fronte di una drastica diminuzione di quelli
“pesanti”.
Consideriamo il seguente esempio:
process(a,b,c,d)
begin
if(a<c) then
r <= a+b;
else
r <= a+c;
end if;
end process;
In tutto, necessitiamo di due sommatori, un multiplexer (per l’if ) e un
comparatore. Notiamo, però, come in entrambi i casi sia calcolata una somma
tra due quantità. Inoltre, uno dei due operandi è in ambo i casi a. Pertanto,
possiamo scrivere il seguente codice, più efficiente:

59
process(a,b,c,d)
signal src : std_logic_vector(7 downto 0);
begin
if(a<c) then
src <= b;
else
src <= c;
end if;
r <= src + a;
end process;

In questo modo, utilizziamo un sommatore solo, un comparatore, un


multiplexer per l’if e uno per la selezione di b o c. Abbiamo, quindi, un
sommatore in meno e un multiplexer in più: dato che un mux è molto meno
“ingombrante” di un sommatore, questa seconda soluzione è preferibile. In
questo caso, l’operatore a cui abiamo applicato lo sharing è la somma. Ovvi-
amente, all’aumentare del numero di operazioni che risparmiamo aumentano
anche i vantaggi che riusciamo a trarre dall’operator sharing. Spesso, essendo
abbastanza facili da individuare, un sintetizzatore è in grado di effettuare in
automatico queste ottimizzazioni.

5.2 Functionality Sharing


La functionality sharing prevede l’ottimizzazione di un circuito sfruttan-
do le proprietà delle funzioni e dei core che vengono utilizzati nel circuito
stesso. Per esempio, se dobbiamo progettare un microprocessore, non si può
pensare di implementare un singolo blocco per ogni possibile operazione: la
ricerca consta nel voler trovare quelle affinità tra funzioni che permettano di
semplificare il numero totale di blocchi da utilizzare. E’ molto più difficile
individuare la possibilità di functionality sharing rispetto all’operator shar-
ing, tanto è vero che non esiste sintetizzatore al mondo in grado di effettuare
questo tipo di considerazioni (se non in casi banali).
Con un esempio tutto risulterà più chiaro. Supponiamo di voler progettare
un core in grado di implementare la somma e la sottrazione tra due operandi.
Verrà calcolata una piuttosto che l’altra quantità sulla base del valore di un
segnale di controllo ctrl. Supponiamo che gil addendi siano del tipo signed,
ovvero in complemento a 2. Una soluzione è la seguente:

library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_ARITH.ALL;

60
use IEEE.STD_LOGIC_UNSIGNED.ALL;
use IEEE.NUMERIC_STD.ALL;

entity addsub is
Port ( a,b : in std_logic_vector(7 downto 0);
ctrl : in std_logic;
r : out std_logic_vector(7 downto 0));
end addsub;

architecture Behavioral of addsub is


signal src0, src1, sum : signed(7 downto 0);
begin

-- trasformiamo a e b in signed
src0 <= signed(a);
src1 <= signed(b);
sum <= src0 + src1 when ctrl=’0’ else
src0 - src1;
-- trasformiamo sum in std_logic_vector
r <= std_logic_vector(sum);

end Behavioral;

In tutto, utilizziamo un sommatore, un sotrattore e un multiplexer. Dato


che sommatore e sotrattore non sono lo stesso componente, non possiamo
applicare le tecniche viste nel precedente paragrafo. E’ altrettanto vero,
però, che in complemento a 2 vale la seguente relazione:

a−b=a+b+1
Dato che un adder ha sempre un ingresso per il carry–in, ovvero il resto
in ingresso, l’operazione precedente implica l’utilizzo di un solo sommatore,
anche se dobbiamo sommare 3 quantità. A questo punto, possiamo applicare
l’operator sharing ed ottenere:

library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_ARITH.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;
use IEEE.NUMERIC_STD.ALL;

entity addsub is

61
Port ( a,b : in std_logic_vector(7 downto 0);
ctrl : in std_logic;
r : out std_logic_vector(7 downto 0));
end addsub;

architecture Behavioral of addsub is


signal src0, src1, sum : signed(7 downto 0);
signal cin : signed(0 downto 0);
begin

-- trasformiamo a e b in signed
src0 <= signed(a);
src1 <= signed(b) when ctrl=’0’ else
signed(not(b));
-- calcolo del carry-in
cin <= "0" when ctrl=’0’ else
"1";
sum <= src0 + src1 + cin;
-- trasformiamo sum in std_logic_vector
r <= std_logic_vector(sum);

end Behavioral;
Abbiamo 2 multiplexer, un inverter (per il NOT) e un sommatore: 4 com-
ponenti invece di 3, ma risparmiamo un sotrattore (pesante) per introdurre
un inverter e un multiplexer (leggeri).

5.3 Ottimizzazioni di layout


Un buon progetto passa anche attraverso l’attenzione che si presta a come
viene generato un circuito alla conclusione del processo di sintesi. Ovvia-
mente, l’utente non può agire sul sintetizzatore. Però può utilizzare un po’
di furbizia nel codice che si passa al sintetizzatore.
Uno degli aspetti peggiori dei circuiti, siano essi combinatori o sequen-
ziali, è rappresentato dalla capacità di prevedere quanto tempo (nel caso di
circuiti combinatori) o quanti cicli di clock (per i sequenziali) passeranno
prima di ottenere gli output, una volta forniti gli input. Questa fase del
progetto è molto noiosa, ma altrettanto importante, specie se il blocchetto
che si considera è parte integrante di un circuito più grande, dal quale si
otterranno gli ingressi e si forniranno le uscite. Ci sarebbero migliaia di con-
siderazioni da fare, ma concentriamoci sull’aspetto temporale attraverso un

62
esempio semplice. Vogliamo implementare un circuito che fornisca un’uscita
y tale che:

(a · b) + c se ctrl = 0
y=
((a ⊕ b) ⊕ c) ⊕ d se ctrl = 1
dove ⊕ è lo XOR bit–a–bit. Un’implementazione diretta è:

library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_ARITH.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;

entity esempio is
Port ( a,b,c,d : in std_logic;
ctrl : in std_logic;
r : out std_logic);
end esempio;

architecture Behavioral of esempio is


begin

process(a,b,c,d,ctrl)
begin
if(ctrl=’0’) then
y <= (a and b) or c;
else
y <= ((a xor b) xor c) xor d;
end if;
end process;

end Behavioral;

Lo schema circuitale è rappresentato in Fig. 5.1. Nasce un problema: se


ctrl vale 0, abbiamo due stadi di logica di ritardo; altrimenti, ne abbiamo
3. Come risultato otteniamo un diverso δ–delay, e quindi un circuito poco
preciso e prevedibile. Come sistemarlo? La prima soluzione è quella di
aggiungere un buffer di ritardo sulla prima soluzione. Dato che questa scelta
non è proprio il massimo della vita, si può sfruttare la proprietà associativa
dello XOR. Pertanto, si ha:

((a ⊕ b) ⊕ c) ⊕ d = (a ⊕ b) ⊕ (c ⊕ d)

63
Quindi, scriviamo il nostro codice come:

library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_ARITH.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;

entity esempio is
Port ( a,b,c,d : in std_logic;
ctrl : in std_logic;
r : out std_logic);
end esempio;

architecture Behavioral of esempio is


begin

process(a,b,c,d,ctrl)
begin
if(ctrl=’0’) then
y <= (a and b) or c;
else
y <= (a xor b) xor (c xor d);
end if;
end process;

end Behavioral;

In uscita otterremo uno schema come quello di Fig. 5.2, ovvero bilanciato.

64
Figura 5.1: Circuito sbilanciato nei δ–delays

65
Figura 5.2: Circuito bilanciato nei δ–delays, sfruttando la proprietà
associativa dello XOR

66
Capitolo 6

Progetto di circuiti sequenziali

6.1 Introduzione
6.1.1 Circuiti sequenziali
Come detto nel paragrafo 3.1, un circuito sequenziale è caratterizzato
da un proprio stato interno e da elementi di memoria. Quindi, mentre in
un circuito combinatorio gil output, istante per istante, sono funzione solo
di quel singolo input, in un circuito sequenziale la funzione che genera le
uscite è funzione non solo dell’ingresso istantaneo, ma di tutti gli ingressi
precedenti, i quali vengono memorizzati nello stato interno del circuito. Il
nome “sequenziale” deriva proprio dal fatto che l’output è funzione della
sequenza di input.
I circuiti sequenziali si dividono in asincroni e sincroni : in questi ulti-
mi, tutti gli elementi di memoria sono controllati da uno o più segnali di
sincronizzazione (clock ). I circuiti sequenziali sincroni sono largamente i più
utilizzati e rappresentano l’argomento di interesse per quanto riguarda la
nostra trattazione.

6.1.2 Elementi di memoria


Elementi di memoria possono essere aggiunti ad un circuito in due modi:

• la prima tecnica è l’utilizzo di retroazione in un circuito combinato-


rio classico. Questa tecnica però è pericolosa e sconsigliabile a causa
dei ritardi di propagazione, che possono anche portare ad uscite non
desiderate nè previste;

• la seconda soluzione è rappresentata dall’uso di flip–flop e latch.

67
In Fig. 6.1 sono presenti i principali tipi di latch e flip–flop (FF). Dato
che, sostanzialmente, il latch non è sincrono e, se c è 1 copia l’ingresso d,
l’utilizzo di questo elemento può portare al cosiddetto fenomeno dei running
delays, ovvero propagazione di ritardi. L’utilizzo di elementi sincroni, quali i
flip–flop D in figura, è consigliato, in quanto crea una sorta di cuscinetto, per
esempio, nell’I/O di un componente per ammortizzare i ritardi e favorire la
propagazione del dato nel circuito. Non si entra nei particolari dei flip–flop,
che dovrebbero essere noti, se non per una notazione: q* rappresenta lo stato
successivo che assumerà il FF.

Figura 6.1: I vari tipi di flip–flop D–type

E’ importante approfondire, invece, le caratteristiche temporali dei flip–


flop, mostrate graficamente in Fig. 6.2. Analizziamo le diverse voci:

• Tcq : clock–to–Q delay, ovvero il tempo di propagazione necessario al


segnale d per propagarsi sull’uscita q dopo il sampling del fronte di
clock;

• Tsetup : è il setup time, ovvero l’intervallo di tempo per il quale il segnale


d deve rimanere stabile prima del fronte del clock;

68
• Thold : hold time, ovvero per quanto tempo dopo il fronte del clock il
segnale d deve rimanere stabile.

Figura 6.2: Timing diagram di un flip–flop D

Quindi, mentre Tcq rappresenta il classico ritardo di propagazione, gli al-


tri due sono vincoli temporali, i quali entrano in gioco in un circuito sincrono
per la stima della massima frequenza di clock ammessa: infatti, se i segnali
dovessero variare troppo rapidamente (o il periodo di clock fosse troppo pic-
colo) violeremmo uno di questi constraints. In particolare, tale violazione
porta il FF in uno stato cosiddetto metastabile.

6.1.3 Classificazione dei circuiti sequenziali


A seconda di come viene organizzata la rete di distribuzione interna del
clock per i vari FF, possiamo classificare i circuiti sequenziali in:

• circuiti totalmente sincroni (o, più semplicemente, circuiti sincroni ):


tutto il circuito utilizza FF sincronizzati da un unico segnale di clock co-
mune a tutto il dispositivo. E’ la soluzione più utilizzata, in quanto fa-
cilita la progettazione, l’interfacciamento, il test e la verifica dei proget-
ti. Non solo, è utile sia in sistemi di grandi dimensioni (es. processori)
sia in dispositivi elementari (es. contatori);

• circuiti asincroni localmente sincroni: a causa di vincoli progettuali


(ad esempio, il piazzamento di certi componenti in parti distanti della
scheda), potrebbe essere difficile far condividere a tutto il sistema lo
stesso clock. Si può, però, dividere il tutto in tanti sotto–sistemi più

69
semplici, all’interno dei quali creare circuiti sincroni con un proprio
clock. A questo punto, il vero problema consiste nella sincronizzazione
tra tali sotto–sistemi: esistono tecniche ad hoc, ma che non tratteremo
in quanto non di nostro diretto interesse;

• circuiti totalmente asincroni (o, più semplicemente, circuiti asincroni ):


nessun componente del circuito utilizza un clock globale, quindi o i FF
vengono auto–gestiti attraverso una politica basata su comuni segnali
usati come clock, oppure ogni flip–flop utilizza un clock proprio e non
legato a nessun altro segnale di sincronizzazione. Circuiti di questo
tipo sono di difficile gestione e progettazione, pertanto non verranno
trattati.

6.2 Circuiti sequenziali sincroni


Passiamo ad analizzare i circuiti sequenziali sincroni, ovvero quelli di
maggiore interesse dal nostro punto di vista. Innanzitutto, descriviamo lo
schema generale di circuito sincrono sequenziale di Fig. 6.3. Il blocco di
elaborazione next–state logic riceve in ingresso i segnali di input e, eventual-
mente, lo stato del sistema (in pratica, la traccia degli input e output agli
istanti precedenti): il suo compito è quello, sulla base di tali informazioni,
di generare lo stato successivo per il blocco di calcolo in uscita dal circuito.
In pratica, ricevuti gli input, genera i dati per il calcolo dell’output vero e
proprio, eseguito poi dalla output logic. Nel mezzo, si introduce un FF di
tipo D per la sincronizzazione. Questa è una struttura a stato sincronizzato:
esistono varianti a ingressi/uscite registrate, che non prevedono il FF per lo
stato ma l’aggiunta di un flip–flop in ingresso e/o uscita per sincronizzare gli
I/O.

Figura 6.3: Schema a blocchi generale per un circuito sequenziale sincrono

70
Possiamo identificare 3 tipi di circuiti sequenziali:

1. Regular Sequential Circuits: la rappresentazione dello stato e la tran-


sizione fra essi è semplice da gestire, come nel caso di contatori o
shift–registers. Spesso, lo stesso stato della macchina è l’output del
circuito;

2. Random Sequential Circuits: la gestione della transizione fra stati è


più complessa, in quanto l’assegnazione di essi è randomica rispetto
all’output della macchina. E’ il caso delle macchine a stati finiti (FSM)
semplici, in cui l’uscita è determinata solo dallo stato del sistema;

3. Combined Sequential Circuits: è l’unione dei due casi precedenti. Una


FSM è utilizzata per la gestione degli stati, ma l’output è generato sulla
base di una elaborazione su uno o più input. Vengono dette macchine
a stati finiti con datapath (FSMD), anche se spesso, per semplicità, si
usa anche per esse il nome FSM.

Approfondiremo le FSM e le FSMD nel Cap. 7.

6.3 Programmazione di elementi di memoria


elementari
Tutte le schede che si utilizzano sono composte da: slice, ovvero celle di
calcolo elementari (per lo più AND e OR); slice flip–flop, ovvero celle cont-
nenti flip–flop per la sincronizzazione; LUTs, ovvero piccole celle di memoria,
in cui l’ingresso è costituito dall’indirizzo della cella di memoria e l’output
è il dato in essa memorizzato (le analizzeremo più approfonditamente nel
Cap. 8). Nel momento in cui sintetizziamo un codice VHDL, il sintetizza-
tore è in grado di tradurre le nostre righe di codice in componenti presenti
on–board sulla scheda di detinazione del nostro lavoro: è per questo che non
ha senso pensare di scrivere un codice VHDL senza conoscere bene su quale
device vogliamo implementare il nostro circuito. Quindi, se implementiamo
una architettura avente il comportamento di un flip–flop D, in automatico
il sintetizzatore trasformerà il componente in un elemento della libreria e lo
piazzerà nelle apposite slice.
Fatta questa premessa, vediamo nei prossimi paragrafi come implementare
alcune semplici unità di memorizzazione.

71
6.3.1 Positive–Edge–Triggered D Flip–Flop
La tabella di attivazione del FF D positive–edge è presente in Fig. 6.1.
Vediamo come potrebbe essere implementato:

library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_ARITH.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;

entity ffd is
Port ( d : in STD_LOGIC;
clk : in STD_LOGIC;
q : out STD_LOGIC);
end ffd;

architecture Behavioral of ffd is


begin
process(clk)
begin
if rising_edge(clk) then
q <= d;
end if;
end process;
end Behavioral;

Sul fronte di salita del clock (controllato dal comando rising_edge


facente parte dello standard IEEE 1164), il segnale di ingresso viene copiato
sull’uscita. Altrimenti, non viene fatto nulla. Come mostrato in Fig. 6.4,
il sintetizzatore riconosce che il componente da noi progettato è proprio un
flip–flop di tipo D positive–edge triggered.

6.3.2 FF D con reset asincrono


Vediamo un secondo esempio. Modifichiamo la precedente struttura ag-
giungendo un reset (ipotizzandolo attivo basso), il quale agisce in maniera
totalmente asincrona rispetto al clock: quando vale 0 tale segnale (attivo
basso), l’uscita si deve annullare.

library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_ARITH.ALL;

72
Figura 6.4: Schema RTL di sintesi per un flip–flop D

use IEEE.STD_LOGIC_UNSIGNED.ALL;

entity ffdr is
Port ( d : in STD_LOGIC;
clk : in STD_LOGIC;
reset : in STD_LOGIC;
q : out STD_LOGIC);
end ffdr;

architecture Behavioral of ffdr is


begin
process(clk, reset)
begin
if reset=’0’ then
q <= ’0’;
elsif rising_edge(clk) then
q <= d;
end if;
end process;
end Behavioral;

Si notino la modifica nella sensitivity list e l’ordine dei branch nell’if : se


avessimo controllato il reset dopo il clock, avremmo avuto un comportamento
errato. Sintetizzando, si ottiene lo schema di Fig. 6.5: nella libreria della
scheda utilizzata, non era contemplato un flip–flop con reset attivo basso,
ma solo con un clear attivo alto. Pertanto, cosa accade? Il sintetizzatore
inserisce un inverter al fine di utilizzare il componente disponibile.

73
Figura 6.5: Schema RTL di un flip–flop D con reset asincrono

E se volessimo un reset sincrono? Modifichiamo l’architettura precedente


nel seguente modo:

architecture Behavioral of ffdr is


begin
process(clk)
begin
if rising_edge(clk) then
if reset=’0’ then
q <= ’0’;
else
q <= d;
end if;
end if;
end process;
end Behavioral;

In questo caso, la sensitivity list include solo il clock, e il segnale di reset


viene analizzato solo sui fronti di salita. Anche il sintetizzatore modifica il
componente utilizzato, come mostrato in Fig. 6.6.

6.3.3 Registro a 8 bit


Un registro a 8 bit non è nient’altro che un insieme di 8 FF D classici. Il
codice è piuttosto semplice:

library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_ARITH.ALL;

74
Figura 6.6: Sintesi RTL di un FF D con reset sincrono

use IEEE.STD_LOGIC_UNSIGNED.ALL;

entity reg8 is
Port ( d : in STD_LOGIC_VECTOR (7 downto 0);
clk : in STD_LOGIC;
q : out STD_LOGIC_VECTOR (7 downto 0));
end reg8;

architecture Behavioral of reg8 is


begin
process(clk)
begin
if rising_edge(clk) then
q <= d;
end if;
end process;
end Behavioral;

6.3.4 RAM
La Random Access Memory è uno dei tipi di memoria in assoluto più
utilizzati, ma anche più difficili da implementare in VHDL. Infatti, tecnica-
mente una RAM si può rappresentare come un insieme di latch, con molti
segnali di handshaking in fase di gestione. Di fatto, si consiglia vivamente
di fare riferimento alla documentazione della propria scheda e sintetizzatore
per verificare (qualora sia prevista...) l’eventuale presenza nelle librerie di
sistema di un blocco RAM già realizzato ed utilizzare quel componente.

75
6.3.5 ROM
La Read–Only Memory è un altro componente molto utilizzato, specie
per le LUT (vedi Cap. 8). Rispetto alle RAM, è molto meno efficiente,
ma più semplice da implementare in VHDL: in generale, a struttura case
con indirizzo e dato, il sintetizzatore fa corrispondere un componente ROM.
Attenzione: se in una scheda non sono disponibili ROM esterne alla FPGA,
l’implementazione delle celle di memoria avverrà on–board sulla scheda, con
grande occupazione di slice (a meno che, ovviamente, i dati da salvare non
siano pochi...). Tratteremo meglio in seguito questi aspetti.

6.4 Un esempio: contatore sincrono modulo–


m programmabile
Un contatore modulo–m è un componente hardware che, come dice il
nome stesso, effettua un conteggio, partendo da 0 e terminando quando rag-
giunge il valore m − 1, per iniziare nuovamente da 0. Per esempio, un con-
tatore modulo–3 effettuerà i seguenti passaggi: 0, 1, 2, 0, 1,... Nel nostro
esempio, supporremo che m venga espresso a 4 bit: pertanto, il range di
valori ammissibili per m varierà fra 2 (ovvero "0010") e 15 (ovvero "1111").
Nel seguente codice, si potrebbe anche introdurre una notazione mai vista
in precedenza, ma utile nel momento in cui si utilizzano i tipi IEEE 1164.
Possiamo inserire stringhe esadecimali aggiungendo un prefisso X alla stringa
di bit. Ogni carattere esadecimale rappresenta 4 bit: pertanto, il range di
m può essere espresso tra X"2" e X"F". Nel nostro caso, dovendo trattare
quantità aritmetiche senza segno, sfrutteremo invece i tipi di dato unsigned
dello standard IEEE 1664. La struttura generale ricalca quella di Fig. 6.3.

6.4.1 Prima soluzione


Vediamo una prima soluzione:

library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_ARITH.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;

entity contm is
Port ( clk : in STD_LOGIC;
reset : in STD_LOGIC;

76
m : in STD_LOGIC_VECTOR (3 downto 0);
q : out STD_LOGIC_VECTOR (3 downto 0));
end contm;

architecture Behavioral of contm is


signal r_reg : unsigned(3 downto 0);
signal r_next : unsigned(3 downto 0);
begin

-- state-register
process(clk, reset)
begin
if reset=’0’ then
r_reg <= (others => ’0’);
elsif rising_edge(clk) then
r_reg <= r_next;
end if;
end process;

-- next-state logic
r_next <= (others => ’0’) when r_reg=(unsigned(m)-1) else
r_reg + 1;

-- output logic
q <= std_logic_vector(r_reg);
end Behavioral;

La sintesi di questa soluzione in moduli hw di una FPGA Xilinx è presente


in Fig. 6.7.

6.4.2 Seconda soluzione


Notiamo che c’è un’opportunità di ottimizzazione attraverso sharing.
Infatti, la seguente operazione:

r_reg=(unsigned(m)-1)

richiede l’utilizzo di un decrementatore. Sela riscriviamo come:

r_reg+1=unsigned(m)

77
Figura 6.7: Sintesi della prima soluzione proposta per il contatore modulo–m

potremmo pensare di utilizzare il sommatore per il calcolo di r_next anche


per la condizione del when, eliminando cosı̀ un sottrattore. Possiamo scrivere,
quindi, il precedente codice anche nel seguente modo, con l’aggiunta di un
terzo segnale “di appoggio”:

architecture Behavioral of contm is


signal r_reg : unsigned(3 downto 0);
signal r_next : unsigned(3 downto 0);
signal r_inc : unsigned(3 downto 0);
begin

-- state-register
process(clk, reset)
begin
if reset=’0’ then
r_reg <= (others => ’0’);
elsif rising_edge(clk) then
r_reg <= r_next;
end if;
end process;

-- next-state logic
r_inc <= r_reg + 1;
r_next <= (others => ’0’) when r_inc=(unsigned(m)) else
r_inc;

-- output logic
q <= std_logic_vector(r_reg);

78
end Behavioral;
Anche dallo schema di sintesi RTL di Fig. 6.8 appare subito una maggiore
semplicità, con l’assenza del sotrattore.

Figura 6.8: Sintesi della seconda soluzione ottimizzata per il contatore


modulo–m

6.5 Analisi temporale di circuiti sincroni


Nel momento in cui sintetizziamo un circuito, sia esso combinatorio o
sequenziale, sincrono o asincrono, in output otteniamo alcune informazioni
molto importanti. Innanzitutto, la più importante è se il circuito è sintetizz-
abile o meno: per quanto possa sembrare banale, nel progetto di componenti
molto complessi potrebbe esserci sfuggita una struttura non sintetizzabile, o,
magari in qualche tentativo di sharing, potremmo aver creato qualche corto–
circuito o conflitto nei collegamenti. Altre informazioni sono l’occupazione
totale di porte logiche e le caratteristiche temporali dell’oggetto realizzato:
un buon progetto non deve occupare più slice di quante non siano effettiva-
mente necessarie, e (per circuiti sincroni) deve avere una frequenza massima
di funzionamento quanto più possibile elevata. Concentriamoci sui circuiti
sincroni.
Per quanto riguarda quest’ultimo aspetto, è necessaria qualche ulteri-
ore precisazione preliminare. Nel momento in cui progettiamo un sistema
hw complesso, al fine di incrementarne le prestazioni, possiamo strutturarlo
sostanzialmente in due modi:

79
• struttura parallela;

• struttura pipeline.

In generale, una struttura parallela è in grado di elaborare contempo-


raneamente molti input contemporaneamente, fornendo in uscita gli output.
Più le elaborazioni sui dati sono indipendenti fra loro, più risulta efficiente
tale strategia. In uscita si ha un circuito che occupa tendenzialmente molte
slide (a causa delle molte strutture di calcolo) e ha una frequenza di funzion-
amento non elevatissima a causa della necessaria sincronizzazione fra le varie
componenti. Tale struttura è anche caratterizzata da una latenza (ovvero,
il numero di cicli di clock intercorsi fra l’arrivo dell’input e la generazione
del corrispondente output) contenuta e da un throughput (numero di output
validi a fronte del numero di cicli di clock intercorsi) variabile a seconda del
problema.
Una struttura pipeline prevede l’elaborazione di un solo input a ciclo di
clock, e l’elaborazione successiva di esso su molti stage. L’occupazione totale
è di solito più bassa o in linea con un circuito parallelo, ma ha una frequenza
di clock consentita più elevata grazie alla sincronizzazione offerta, di fatto,
dal clock globale. La latenza iniziale è molto alta (di solito, molto più alta di
una struttura parallela), mentre per il thoroughput bisogna distinguere due
casi:

• a regime, di fronte ad una frequenza di arrivo degli input di circa un


ingresso valido ogni ciclo di clock, il thorughput è pari a 1 (efficienza
massima);

• se la frequenza di un nuovo ingresso valido è bassa, la pipeline non è


efficiente, è ha un throughput bassissimo.

Quando scegliere una struttura piuttosto che un’altra? Dipende dal prob-
lema. Se, a regime, abbiamo un ingresso ogni ciclo di clock e le elaborazioni
da eseguire sono complesse (e richiedono alcuni cicli di clock) è consigliabile
la struttura pipeline.
Approfondiamo, però, alcuni aspetti temporali per i circuiti sincroni.

6.5.1 Massima frequenza di clock


In Fig. 6.9, viene proposta l’analisi temporale generica per il circuito
sequenziale proposto in Fig. 6.3. Oltre ai classici vincoli dovuti al FF (già
presentati nel paragrafo 6.1.2), si aggiungono Tnextmin e Tnextmax , che rapp-
resentano i ritardi di propagazione nello schema per le variazioni dello stato

80
successivo, generate dall’apposito circuito. A questo punto, al fine di evitare
violazioni del setup time, è immediato verificare che il periodo minimo per

un ciclo di clock Tclk è dato dalla seguente formula:

Tclk = Tcq + Tnextmax + Tsetup (6.1)
Ovviamente, la frequenza massima di funzionamento è:

∗ 1
fclk = (6.2)
Tcq + Tnextmax + Tsetup

Figura 6.9: Analisi temporale di un circuito sequenziale sincrono

6.5.2 Altre informazioni


Oltre alla frequenza massima di funzionamento (di gran lunga la più
importante), un sintetizzatore è in grado di fornire altre informazioni, utili
soprattutto per l’interfacciamento tra dispositivi all’interno di progetti di
grandi dimensioni:

• quanto tempo prima, rispetto al fronte di clock, deve essere stabile


l’ingresso al nostro sistema;

81
• quanto tempo dopo, rispetto al fronte di clock, avremo a disposizione
l’output desiderato;
• l’eventuale ritardo di propagazione dovuto alla presenza di logica com-
binatoria all’interno di unn circuito sequenziale (soluzione raramente
utilizzata).

6.5.3 Output del sintetizzatore Xilinx


Premesso che il sintetizzatore messo a disposizione da Xilinx non è il
migliore disponibile su mercato, vediamo come leggere un output del sintetiz-
zatore. Recuperando l’esempio del contatore del paragrafo 6.4, andiamo, per
esempio, ad osservare l’occupazione di porte logiche su una Xilinx Virtex–4:

Macro Statistics
# Adders/Subtractors : 1
4-bit adder : 1
# Registers : 4
Flip-Flops : 4
# Comparators : 1
4-bit comparator equal : 1

Optimizing...

Device utilization summary:


---------------------------

Selected Device : 4vsx25ff668-12

Number of Slices: 6 out of 10240 0%


Number of Slice Flip Flops: 4 out of 20480 0%
Number of 4 input LUTs: 13 out of 20480 0%
Number of IOBs: 10 out of 320 3%
Number of GCLKs: 1 out of 32 3%

Il primo blocco di risultati è quello fornito dal sintetizzatore senza ot-


timizzazioni: in pratica, è una traduzione in forma testuale dello schema
RTL di Fig. 6.8. A questo punto, dopo ottimizzazioni varie (per esempio,
sul routing e di tipo sharing), troviamo il riassunto dell’occupazione. Per la
FPGA selezionata (identificata dal codice 4vsx25ff668-12), abbiamo un’occu-
pazione totale di 6 slice di logica, 4 flip–flop, utilizziamo 13 LUT (molte delle

82
quali per evitare l’utilizzo del sommatore, lento, sostituendolo con locazioni
di ROM, ad accesso pressochè istantaneo), 10 porti di I/O (abbiamo 4 bit
per m, 4 per l’output, il clock e il segnale di reset) e un segnale di clock (che
è stato riconosciuto e sintetizzato come tale). Per quanto riguarda l’analisi
temporale abbiamo:

Timing Summary:
---------------
Speed Grade: -12

Minimum period: 2.259ns (Maximum Frequency: 442.684MHz)


Minimum input arrival time before clock: 2.760ns
Maximum output required time after clock: 4.013ns
Maximum combinational path delay: No path found

Ritroviamo le voci presentate in precedenza. Attenzione: si noti come


il tempo di arrivo minimo per un segnale di input prima del fronte di clock
dev’essere 2.76 ns, ovvero addirittura superiore al periodo minimo del clock.
Com’è possibile? Che senso ha? Il problema nasce dal fatto che il peri-
odo minimo di clock è calcolato considerando nulli i tempi di routing dal
pad di ingresso/uscita al porto di I/O del nostro componente. Se, infatti,
approfondiamo l’analisi, per esempio, della seconda voce, si ha:

===============================================================
Timing constraint: Default OFFSET IN BEFORE for Clock ’clk’
Total number of paths / destination ports: 16 / 4
---------------------------------------------------------------
Offset: 2.760ns (Levels of Logic = 4)
Source: m<2> (PAD)
Destination: r_reg_2 (FF)
Destination Clock: clk rising

Data Path: m<2> to r_reg_2


Gate Net
Cell:in->out fanout Delay Delay
----------------------------------------
IBUF:I->O 3 0.754 0.581
LUT4:I0->O 1 0.147 0.529
LUT4_L:I1->LO 1 0.147 0.157
LUT4:I2->O 1 0.147 0.000

83
FDC:D 0.297
----------------------------------------
Total 2.760ns
(1.492ns logic, 1.268ns route)
(54.1% logic, 45.9% route)

In poche parole, se eliminiamo i ritardi dovuti al routing e consideriamo


i soli delay dovuti alla logica, abbia 1.492 ns, che, pertanto, è inferiore al
limite del clock. Per diminuire i ritardi dovuti alla logica (che, in caso di
componente unico sulla FPGA, possono essere molto fastidiosi), di solito si
usano i file di constraint per il sintetizzatore, che forzano l’implementazione
in particolari zone della FPGA (magari, in prossimità dei pad di I/O).
Cosa succede se aumentiamo il numero di bit? Magari passando da 4 bit
a 16 bit?

Device utilization summary:


---------------------------

Selected Device : 4vsx25ff668-12

Number of Slices: 21 out of 10240 0%


Number of Slice Flip Flops: 16 out of 20480 0%
Number of 4 input LUTs: 41 out of 20480 0%
Number of IOBs: 34 out of 320 10%
Number of GCLKs: 1 out of 32 3%

Timing Summary:
---------------
Speed Grade: -12

Minimum period: 4.340ns (Maximum Frequency: 230.420MHz)


Minimum input arrival time before clock: 3.410ns
Maximum output required time after clock: 3.935ns
Maximum combinational path delay: No path found

Vediamo come, complicandosi la logica, a fronte di un aumento piuttosto


contenuto dell’occupazione hardware, diminuisca di un fattore 2 la frequenza
massima di funzionamento del nostro device.

84
6.6 Utilizzo di variabili in circuiti sequenziali
Fino ad ora abbiamo sempre utilizzato i segnali, anche come elemento
di memoria. In generale, questa è la tecnica più sicura e più usata per
l’implementazione di circuiti sequenziali. Tuttavia, non è errato, specie in
strutture semplici, utilizzare anche le variabili all’interno dei processi. Data
la natura molto locale delle variabili, è conveniente utilizzarle soprattutto
come variabili di “appoggio”. Vediamo la differenza fra segnali e variabili
attraverso un semplice esempio. Ecco il codice:

library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_ARITH.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;

entity varsign is
Port ( a : in STD_LOGIC;
b : in STD_LOGIC;
clk : in STD_LOGIC;
q1 : out STD_LOGIC;
q2 : out STD_LOGIC;
q3 : out STD_LOGIC);
end varsign;

architecture Behavioral of varsign is


signal tmp_sig1 : std_logic;
begin
-- prima soluzione
process(clk)
begin
if rising_edge(clk) then
tmp_sig1 <= a and b;
q1 <= tmp_sig1;
end if;
end process;

-- seconda soluzione
process(clk)
variable tmp_sig2 : std_logic;
begin
if rising_edge(clk) then

85
tmp_sig2 := a and b;
q2 <= tmp_sig2;
end if;
end process;

-- terza soluzione
process(clk)
variable tmp_sig3 : std_logic;
begin
if rising_edge(clk) then
q3 <= tmp_sig3;
tmp_sig3 := a and b;
end if;
end process;
end Behavioral;

Questo modulo presenta tre uscite, corrispondenti alla stessa elaborazione


sul segnale (a · b) ma eseguita in 3 modi diversi. Nel primo caso, utilizziamo
il classico segnale per l’aggiornamento; nel secondo, utilizziamo una variabile
di appoggio (in modo da non creare ritardi nella propagazione del risultato);
nel terzo, la variabile viene aggiornata dopo l’assegnazione al segnale. Di
fatto, il primo e il terzo metodo sono equivalenti (come mostrato anche dalla
simulaizone temporale comportamentale di Fig. 6.10), e, di fatto, la variabile
in questo caso è utilizzata in maniera non utile. Nel secondo caso, la variabile
viene istantaneamente aggiornata, quindi otterremo in output su q2 il risul-
tato di a·b con un ciclo di clock di anticipo (Fig. 6.10). Questo è, ovviamente,
vantaggioso, ma altrettanto pericoloso: in circuiti molto complessi, potrebbe
non essere possibile tale aggiornamento istantaneo, e avremmo risultati non
voluti.

Figura 6.10: Simulazione temporale comportamentale per evidenziare le


differenze fra segnali e variabili

86
6.6.1 Progettazione del contatore modulo–m utilizzan-
do variabili
Modifichiamo l’architettura dell’esempio del paragrafo 6.4 in modo da
utilizzare variabili.

architecture Behavioral of contm is


signal r_reg : unsigned(3 downto 0);
begin

process(clk,reset)
variable q_tmp : unsigned(3 downto 0);
begin
if reset=’1’ then
r_reg <= (others => ’0’);
elsif rising_edge(clk) then
q_tmp := r_reg + 1;
if(q_tmp = unsigned(m)) then
r_reg <= (others => ’0’);
else
r_reg <= q_tmp;
end if;
end if;
end process;

q <= std_logic_vector(r_reg);

end Behavioral;

Il comportamento è identico a quanto visto in precedenza. Ma l’utilizzo di


variabili ha giovato anche ai risultati della sintesi? Vediamo le caratteristiche
di occupazione e temporali:

Device utilization summary:


---------------------------

Selected Device : 4vsx25ff668-12

Number of Slices: 6 out of 10240 0%


Number of Slice Flip Flops: 4 out of 20480 0%
Number of 4 input LUTs: 11 out of 20480 0%

87
Number of IOBs: 10 out of 320 3%
Number of GCLKs: 1 out of 32 3%

Timing Summary:
---------------
Speed Grade: -12

Minimum period: 2.445ns (Maximum Frequency: 408.956MHz)


Minimum input arrival time before clock: 3.048ns
Maximum output required time after clock: 3.971ns
Maximum combinational path delay: No path found

Utilizziamo un paio di LUT in meno, ma paghiamo questa leggera sem-


plificazione con un maggiore ritardo nella logica di connessione interna, e,
quindi, con una frequenza di picco leggermente inferiore alla precedente. Tale
effetto è amplificato se passiamo a 16 bit:

Timing Summary:
---------------
Speed Grade: -12

Minimum period: 6.019ns (Maximum Frequency: 166.150MHz)


Minimum input arrival time before clock: 5.102ns
Maximum output required time after clock: 3.933ns
Maximum combinational path delay: No path found

88
Capitolo 7

Macchine a stati finiti

7.1 Introduzione
Una macchina a stati finiti (finite state machine, FSM ), come dice il
nome stesso, è una macchina caratterizzata da un certo numero di possibili
stati interni. A contrario di quanto visto nel precedente capitolo per i circuiti
sequenziali più elementari (i Regular Sequential Circuits analizzati in alcuni
esempi nel precedente capitolo), la logica di controllo per lo stato interno nel
caso di FSM è più complesso, tanto da essere considerato randomico.
Una FSM è costituita da 5 entità:

• stati;

• segnali di input;

• segnali di output;

• funzione per il calcolo dello stato successivo (next–state function);

• funzione per il calcolo dell’output (output function).

Lo stato caratterizza per intero una FSM. Il funzionamento è definito


dalle transizioni da uno stato al successivo: se tali passaggi sono gestiti da
un segnale di clock, si parla di FSM sincrone, ed è il caso di gran lunga più
comune. La output function può essere funzione semplicemente dello stato,
oppure anche degli ingressi al sistema: nel primo caso si parla di macchina
di Moore, nel secondo di macchina di Mealy. Se la FSM svolge calcoli non
banali sugli input, più precisamente si parla di FSM con datapath o FSMD.
Per semplicità, nel seguito confonderemo FSM e FSMD. Di solito, una FSM
complessa è una FSMD con uscite di tipo Moore e Mealy.

89
7.2 Rappresentazione di una FSM
Il progetto di una macchina a stati finiti si basa su una descrizione
grafica astratta, che utilizzi una rappresentazione simbolica degli stati e in-
dichi sotto quali condizioni vengono posti in uscita determinati output. Le
rappresentazioni più comuni sono gli state diagram o pallogrammi e gli ASM.

7.2.1 Pallogrammi
Un pallogramma è caratterizzato da stati, indicati con dei bubbles o nodi,
e degli archi di transizione. All’interno di un nodo, vi può essere un’uscita
caratterizzante quel determinato stato: in tal caso si parla di uscite di Moore.
Se un arco non contiene una condizione, la transizione allo stato successivo
avviene senza incertezze al colpo di clock successivo; viceversa, se un arco
contiene una condizione, la transizione avverrà solo quando essa è verificata.
Se, su un arco, ad una condizione è associata anche un’uscita, si parla di
uscite di Mealy. Un esempio di nodo e archi è riportato in Fig. 7.1.

Figura 7.1: Notazione per un nodo di un pallogramma

Analizziamo un breve esempio. Si consideri la FSM di Fig. 7.2, in cui


viene presentato un ipotetico (e semplificato) schema per un controller di
memoria. Al reset, il sistema si pone in idle, e vi rimane fino a quando
il segnale mem vale 0 (condizione mem’ ). Quando mem vale 1, dobbiamo
controllare anche il valore di rw : se vale 0, abbiamo un’uscita di Mealy we me
pari a 1, e passiamo nello stato write; se vale 1, abbiamo la transizione allo
stato read1. Se ci troviamo in write, avremo un’uscita di Moore we pari a
1, e al colpo di clock successivo torneremo in idle. Se ci troviamo in read1,

90
avremo un output di Moore oe pari a 1, quindi dovremo controllare burst, e
cosı̀ via in maniera analoga.

7.2.2 ASM
Spesso, sono più utilizzati dei diagrammi aventi molto in comune cooi
pallogrammi, ma talvolta di più semplice interpretazione: gli Algorithmic
State Machine, o, più semplicemente, ASM. Gli ASM sono anche più semplici
da tradurre in VHDL, ed esistono molti generatori di codice automatici.
Inoltre, con gli ASM è facile descrivere anche le FSMD, più complesse da
descrivere con pallogrammi. Vediamo quali sono le strutture elementari.

Stato
Uno stato è rappresentato da un rettangolo simile a quello mostrato in
Fig. 7.3. Analizziamo brevemente i simboli che lo caratterizzano. La stringa
di caratteri sulla destra (in questo caso, st) rappresenta il nome associato
allo stato dall’utente: è un nome simbolico e non influisce sul funzionamen-
to generale del sistema. La stringa di bit in alto a destra (in questo caso,
01) rappresenta il codice identificativo dello stato, e sarà poi il codice che la
next–state logic dovrà opportunamente generare a runtime. E’ opportuno,
quindi, generarla con una certa logica: per facilitare la sintesi hw, può essere
una buona idea cercare (per quanto possibile) di mantenere piccola la dis-
tanza di Hamming1 fra i codici degli stati. In alto a sinistra, il rombo nero
semplicemente indica che lo stato mostrato è lo stato attivato al reset.

Condizione
Una condizione è rappresentata da un rombo, all’interno del quale è
presente il segnale da controllare. A seconda che tale segnale valga 0 o 1,
lo stato successivo può variare o si può avere una diversa uscita di Mealy
(analizzeremo il tutto a breve). Un esempio è mostrato in Fig. 7.4, in
cui controlliamo il valore di IN. Per implementare condizioni più complesse,
semplicemente di rappresentano una cascata di controlli.

Macchine di Moore
Per rappresentare le uscite in una macchina avente solo output di Moore,
semplicemente scriviamo all’interno degli stati il valore dell’uscita che vogliamo
1
Si ricorda che per distanza di Hamming si intende il numero di bit per i quali dif-
feriscono due stringhe binarie. Ad esempio, 001 e 010 hanno distanza 2, 001 e 011 hanno
distanza 1 (differiscono per un solo bit, il secondo per la precisione.

91
Figura 7.2: Esempio di pallogramma per un semplificato controller per una
memoria

92
Figura 7.3: Simbolo ASM di uno stato

Figura 7.4: Simbolo ASM per una condizione

93
forzare a 1. Facciamo un semplice esempio. Supponiamo di voler generare la
FSM per un contatore modulo–4 capace di contare in avanti e indietro. In
particolare, la direzione di conteggio viene controllata attraverso un segnale
DIR, il quale varia però solo nello stato iniziale (uscita a 0). In tutto, il
nostro componente avrà:

• un ingresso per il clock;

• un ingresso per il reset;

• un ingresso per il segnale DIR;

• un output a 2 bit per il risultato del contatore.

Inoltre, la nostra macchina dovrà essere caratterizzata da un certo nu-


mero di registri interni, per memorizzare le variabili caratterizzanti lo stato.
Un possibile ASM è quello presentato in Fig. 7.5, in cui C0 e C1 rappre-
sentano rispettivamente LSB e MSB per l’uscita del contatore. Lo schema
complessivo del componente generato è mostrato, invece, in Fig. 7.6. Sem-
plicemente, si nota come, ad ogni stato, si hanno le uscite che dipendono
solo dallo stato in cui ci si trova. In questo caso, abbiamo 7 stati, quindi il
numero di registri minimo è dato dalla formula:

numreg = log2 numstati  = log2 7) = 3 (7.1)

Macchine di Mealy
Per le macchine di Mealy si utilizzano le uscite condizionate, mostrate
in Fig. 7.7. Un’uscita condizionata va posta dopo lo stato per il quale
dev’essere generata l’eventuale output di Mealy. La cosa migliore è utilizzare
un esempio: modifichiamo lo schema del paragrafo precedente. Sfruttando
l’ipotesi che il segnale di controllo DIR vari solo all’istante iniziale, si può
proporre lo schema di Fig. 7.8.
Analizziamo questo ASM. Ad ogni passo, analizziamo il segnale DIR, e,
a seconda che esso valga 0 o 1, contiamo in avanti o indietro ponendo in
uscita i valori contenuto nelle uscite condizionate. Ad esempio, nello stato
d avremo come output 11 se stiamo contando in avanti, altrimenti 01 se
contiamo all’indietro. Lo stato c non necessita di uscite condizionate, in
quanto sia che si conti in avanti sia che si conti al contrario l’uscita è sempre
10. In questo caso usiamo un output di Moore.
Notiamo subito un grande vantaggio: utilizziamo 4 stati invece di 7. In
termini di registri, possiamo usarne uno in meno. Ciò significa una logica di

94
Figura 7.5: ASM di un contatore avanti/indietro modulo–4

95
Figura 7.6: Schematico per il contatore avanti/indietro modulo–4
96
Figura 7.7: Blocco ASM per l’uscita condizionata

controllo più semplice. Tutto ciò si paga con uno schema più complesso da
analizzare e meno immediato da tradurre in codice, nonchè con una logica
per gli output più complessa. Nell’ambito di progetti su grande scala, questo
fatto può rappresentare un problema.

7.3 Alcune considerazioni sulle FSM


Come già in precedenza, nel resto della trattazione, a meno di indicazioni
contrarie, ci riferiremo sempre a FSM sincrone, gestite da un clock globale.

7.3.1 Considerazioni temporali


Ricordiamoci che una macchina a stati finiti non è nient’altro che un caso
particolare di cicuito sequenziale. Pertanto, essa sarà sincronizzata attraverso
un clock, mentre la sua logica sarà costituita da flip–flop per la gestione dei
segnali. Possiamo, quindi, recuperare la trattazione del paragrafo 6.5.1. La
frequenza di funzionamento massima consentita si otterrà secondo la formula
(6.2).

7.3.2 Macchine di Moore e macchine di Mealy: pro e


contro
Vedremo meglio le differenze implementative fra una macchina pura-
mente di Moore e una puramente di Mealy (o mista Moore/Mealy) nel
prossimo paragrafo. A livello introduttivo si può dire che:

• una macchina di Moore in generale è caratterizzata da una next–state

97
Figura 7.8: ASM di un contatore avanti/indietro modulo–4 sfruttante output
di Mealy

98
logic abbastanza complessa e da una logica di uscita elementare (ad un
particolare stato corrisponde una particolare uscita);
• una macchina di Mealy (o mista) è caratterizzata da una next–state
logic più snella, ma da una logica di output più difficile (di fatto, i
controlli che nella macchina di Moore vengono eseguiti nella logica di
stato qui vengono implementati nella output logic).

Quale delle due soluzioni conviene usare? In generale, una soluzione


mista è la più vantaggiosa. Anche perchè spesso si può trovare qualche sem-
plificazione per ottenere una logica meno pesante. Tuttavia, ci sono dei casi
in cui una macchina di Moore può essere una buona soluzione (la logica di
controllo è comunque meno critica della logica di output). Ancora una volta,
non esiste una soluzione universalmente ottima, ma tante buone soluzioni a
seconda dei problemi che ci troviamo ad analizzare.

7.4 Descrizione VHDL di una FSM


Veniamo, ora, a come descrivere una FSM in VHDL. Come anticipato
in fase di introduzione, esistono molti software in grado di generare il codice
VHDL a partire dal pallogramma o dall’ASM. E’ importante, comunque,
sapere analizzare tali codici e, all’occorrenza, modificarli o scriverli per intero.
Il primo consiglio consiste nel creare un nuovo tipo di dato, in modo
da nominare gli stati con lo stesso nome presente nell’ASM, per esempio.
Ovvero, nell’architecture, introdurre:

TYPE states is ( state_a,


state_b,
state_d,
state_c );

A questo punto, è sufficiente dichiarare due segnali per la gestione interna


degli stati (attuale e futuro):

SIGNAL State,
Next_State: states;

Per quanto riguarda l’entity, nessun problema: gli I/O di una macchina a
stati sono esattamente gli stessi di un qualsiasi altro componente, e vengono
definiti in modo classico.
Per quanto riguarda l’architecture, dobbiamo prestare qualche attenzione
in più. Sappiamo che una FSM è composta, in sostanza, da 3 parti:

99
1. una next–state logic per il calcolo dello stato successivo;

2. un registro di stato;

3. una logica per le uscite.

E’ ovvio pensare, quindi, di creare 3 processi all’interno dell’architettura.


Riprendiamo l’esempio della FSM di Fig. 7.8. In questo caso, la logica di
controllo sullo stato è piuttosto semplice: utilizzando le uscite condizionate,
di fatto la transizione avviene sempre tra uno stato e il successivo senza
problemi. Per il registro di stato possiamo quindi scrivere:

-- Registro di stato
REG: process( Ck, Reset )
begin
if (Reset = ’0’) then
State <= state_a;
elsif rising_edge(Ck) then
State <= Next_State;
end if;
end process;

A questo punto, introduciamo la next–state logic per il calcolo dello stato


successivo. Utilizzando uscite di Mealy, essa risulta piuttosto snella:

-- Next State Logic


FSM: process( State )
begin
CASE State IS
when state_a =>
Next_State <= state_b;
when state_b =>
Next_State <= state_c;
when state_c =>
Next_State <= state_d;
when state_d =>
Next_State <= state_a;
END case;
end process;

Notiamo come questo processo non sia sensibile al clock: semplicemente,


in generale la next–state logic entra in gioco quando si modifica lo stato

100
attuale nell’apposito registro. A quel punto, si ha una variazione di del
segnale State e quindi si attiva il processo per il next–state, che modifica il
segnale Next_State. Concludiamo con l’output logic:
-- Output Logic
OUTPUTS: process( State )
begin
-- Valori di default:
C0 <= ’0’;
C1 <= ’0’;
-- Output funzione dell’input e dello stato:
CASE State IS
when state_b =>
if (DIR = ’1’) then
C0 <= ’1’;
else
C0 <= ’1’;
C1 <= ’1’;
end if;
when state_c =>
C1 <= ’1’;
when state_d =>
if (DIR = ’1’) then
C0 <= ’1’;
C1 <= ’1’;
else
C0 <= ’1’;
end if;
when OTHERS =>
C0 <= ’0’;
C1 <= ’0’;
END case;
end process;
Anche in questo caso, per semplificare la logica, possiamo pensare che
la sincronizzazione avvenga non attraverso una sensibilità ai fronti di clock,
bensı̀ alle variazioni del registro di stato. Settati i valori di default, anal-
izziamo lo stato e gli input per generare le uscite corrette. Attenzione: se
supponiamo che il segnale DIR possa variare in maniera asincrona e debbano
seguire tale standard anche gli output, è necessario aggiungere l’input al-
la sensitivity list. Se, invece, manteniamo valide le supposizioni fatte nella
generazione dell’ASM, DIR non dev’essre incluso nella list.

101
Il modulo nel suo complesso sarà:

LIBRARY ieee;
USE ieee.std_logic_1164.ALL;

ENTITY asm2es IS
PORT( -- Clock & Reset:
Ck: IN std_logic;
Reset: IN std_logic;
-- Inputs:
DIR: IN std_logic;
-- Outputs:
C0: OUT std_logic;
C1: OUT std_logic );
END asm2es;

ARCHITECTURE behave OF asm2es IS


TYPE states is ( state_a,
state_b,
state_d,
state_c );
SIGNAL State,
Next_State: states;
BEGIN

-- Next State Logic


FSM: process( State )
begin
CASE State IS
when state_a =>
Next_State <= state_b;
when state_b =>
Next_State <= state_c;
when state_c =>
Next_State <= state_d;
when state_d =>
Next_State <= state_a;
END case;
end process;

-- Registro di stato

102
REG: process( Ck, Reset )
begin
if (Reset = ’0’) then
State <= state_a;
elsif rising_edge(Ck) then
State <= Next_State;
end if;
end process;

-- Output Logic
OUTPUTS: process( State )
begin
-- Valori di default:
C0 <= ’0’;
C1 <= ’0’;
-- Output funzione dell’input e dello stato:
CASE State IS
when state_b =>
if (DIR = ’1’) then
C0 <= ’1’;
else
C0 <= ’1’;
C1 <= ’1’;
end if;
when state_c =>
C1 <= ’1’;
when state_d =>
if (DIR = ’1’) then
C0 <= ’1’;
C1 <= ’1’;
else
C0 <= ’1’;
end if;
when OTHERS =>
C0 <= ’0’;
C1 <= ’0’;
END case;
end process;

END behave;

103
Cosa cambia se utilizziamo lo schema di Moore? Riportiamo il codice
dell’architecture (l’unica a modificarsi):

ARCHITECTURE behave OF asm1es IS


TYPE states is ( state_0,
state_1i,
state_1a,
state_3a,
state_3i,
state_2i,
state_2a );
SIGNAL State,
Next_State: states;
BEGIN

-- Next State Combinational Logic


FSM: process( State )
begin
CASE State IS
when state_0 =>
if (DIR = ’1’) then
Next_State <= state_1a;
else
Next_State <= state_3i;
end if;
when state_3i =>
Next_State <= state_2i;
when state_2i =>
Next_State <= state_1i;
when state_1i =>
Next_State <= state_0;
when state_1a =>
Next_State <= state_2a;
when state_2a =>
Next_State <= state_3a;
when state_3a =>
Next_State <= state_0;
when OTHERS =>
Next_State <= state_0;
END case;
end process;

104
-- State Register
REG: process( Ck, Reset )
begin
if (Reset = ’0’) then
State <= state_0;
elsif rising_edge(Ck) then
State <= Next_State;
end if;
end process;

-- Outputs Combinational Logic


OUTPUTS: process( State )
begin
-- Set output defaults:
C0 <= ’0’;
C1 <= ’0’;

-- Set output as function of current state and input:


CASE State IS
when state_3i =>
C0 <= ’1’;
C1 <= ’1’;
when state_2i =>
C1 <= ’1’;
when state_1i =>
C0 <= ’1’;
when state_1a =>
C0 <= ’1’;
when state_2a =>
C1 <= ’1’;
when state_3a =>
C0 <= ’1’;
C1 <= ’1’;
when OTHERS =>
C0 <= ’0’;
C1 <= ’0’;
END case;
end process;

END behave;

105
Notiamo come la output logic sia molto più snella (a stato corrisponde
uscita), mentre la next–state abbia il controllo supplementare sul segnale
DIR. Ciò che rende, comunque, questa struttura svantaggiosa (in termini
implementativi) rispetto all’utilizzo di uscite condizionate è il fatto che il
numero di stati pressochè raddoppia, costringendo ad un numero elevato di
controlli.

7.5 Codifica degli stati


Per quanto possa sembrare un problema di poco conto, l’assegnazione
degli stati è una questione tutt’altro che da sottovalutare: l’utilizzo di una
tecnica “furba” può far risparmiare non poche porte logiche. L’assegnazione
degli stati si ripercuote nel codice VHDL nella dichiarazione del tipo states:
infatti, il sintetizzatore, ad ognuno nei termini nell’elenco, associa un codice
binario crescente. In pratica, se noi scriviamo:

TYPE states is ( state_a,


state_b,
state_d,
state_c );

abbiamo associato allo stato a il codice 0, a b il codice 1, a d il codice 2 e a


c il codice 3. Pertanto, la precedente scrittura e la seguente:

TYPE states is ( state_a,


state_b,
state_c,
state_d );

sono differenti, in quanto, nella seconda, abbiamo invertito i codici per c e d.


Supponiamo di considerare una FSM con n stati e r registri da un bit.
Le tecniche principali per la cosiddetta codifica degli stati sono:

• Assegnazione sequenziale binaria: in pratica, a stati contigui associ-


amo codici a distanza 1 decimale. In parole povere, la codifica avviene
in ordine 0, 1, 2, ..., n − 1. Il numero di registri necessari è dato da
r = log2 n. Il problema di questa tecnica consiste nelle difficoltà, per
n elevati, delle transizioni fra stati. Supponiamo n = 8: nel momento
in cui dovremo trasformare il segnale di stato da 111 a 000, per es-
empio, dovremo cambiare ben 3 bit. Spesso, questa tecnica non viene
utilizzata;

106
• Codifica a distanza di Hamming minima: stati contigui, per quanto
possibile, hanno distanza distanza di Hamming minima, e possibilmente
unitaria. Con 4 stati, in binario ciò equivale ad una codifica 00, 01,
11, 10, analogamente a quanto fatto nell’esempio di Fig. 7.8. Anche in
questo caso, sono necessari r = log2 n registri;
• Codifica One–Hot: ad ogni stato è associata una stringa di n bit, tutti
a 0 tranne 1. Ad esempio, con 4 stati, si ha 0001, 0010, 0100, 1000.
Quindi, si hanno r = n registri. In questo caso, la next–state logic è
molto semplice, e consiste in un semplice shifter (si paga tutto ciò però
con la proliferazione dei registri);
• Codifica Almost–One–Hot: è identica alla One–Hot, ma uno stato può
avere codifica pari a una stringa di tutti 0. Quindi, sono necessari r =
n − 1 registri. La next–state logic diviene leggermente più complessa, e
di fatto non si ha un grande guadagno in termini di registri. Pertanto,
è raramente utilizzata.
Abbiamo detto che nella dichiarazione del tipo per gli stati in VHDL ad
ogni nome è associato, in ordine, un numero crescente. E se, per esempio,
volessimo utilizzare una codifica One–Hot? La cosa più semplice è utilizzare
variabili dummy. Riprendiamo l’ASM di Fig. 7.8, e supponiamo di usare una
codifica One–Hot. Una possibile scrittura per il tipo di stato in VHDL è:
TYPE states is ( dummy_0000,
state_a,
state_b,
dummy_0011,
state_c,
dummy_0101,
dummy_0110,
dummy_0111,
state_d,
dummy_1001,
dummy_1010,
dummy_1011,
dummy_1100,
dummy_1101,
dummy_1110,
dummy_1111 );
Le variabili dummy non verranno mai utilizzate nel codice, ma servono
solo come “riempitivo” per il sintetizzatore. Per quanto sia inutile, di fatto,

107
nel codice precedente l’inserimento delle variabili dalla 1001 in poi, è spesso
consigliabile di inserire comunque tutte le possibilità, anche per facilitare il
sintetizzatore: cosı̀, avrà vita facile a capire che si tratta di una codifica
One–Hot.

108
Capitolo 8

Look–Up Tables (LUT)

8.1 Che cos’è una LUT?


Per Look–Up Table (LUT) si intende una struttura dati, generalmente
un array, usata per sostituire operazioni di calcolo a runtime con una più
semplice operazione di consultazione (Look–Up, in inglese). Il guadagno di
velocità può essere significativo, poiché recuperare un valore dalla memoria è
spesso più veloce che sottoporsi a calcoli con tempi di esecuzione dispendiosi.
Un tipico esempio di utilizzo di LUT è l’implementazione hardware di fun-
zioni trigonometriche: dato che la valutazione di un seno o coseno può es-
sere molto pesante computazionalmente, si salvano i valori (con la precisione
desiderata) nelle LUT, e a runtime sarà sufficiente leggere tali valori.
Supponiamo di voler implementare una generica y = f (x). Se decidiamo
di utilizzare le LUT, avremo le seguenti corrispondenze:

• x sarà l’indirizzo della locazione di memoria;

• y sarà il contenuto di tale locazione.

Il numero di celle totali dipende dalla precisione con cui si vuole definire
l’ingresso e calcolare l’uscita, sulla base anche del tipo di ROM (in generale)
che abbiamo a disposizione. Facciamo un esempio. Supponiamo di voler
calcolare una certa funzione con ingressi a 16 bit e uscite con precisione
pari a 16 bit. Supponiamo, inoltre, di avere a disposizione ROM da 16 bit.
Il calcolo è piuttosto semplice: necessiteremo di 216 = 65536 locazioni per
la memorizzazione. Se abbiamo a disposizione tale quantità di spazio, il
risultato della funzione sarà disponibile in un solo colpo di clock (il tempo
necessario alla lettura della ROM), a prescindere da quanto complicata la
funzione possa essere.

109
Supponiamo, ora, di aver a disposizione ROM da 8 bit: ciò significa
che per salvare un dato dovremo usufruire di 2 locazioni. Senza contare
che servirà una logica di gestione dell’inidirizzamento per le ROM. In tutto
ci serviranno 2 · 216 = 131072 locazioni, più altro spazio per la logica di
controllo.

8.2 Pro e contro


Le LUT hanno l’enorme vantaggio di essere indipendenti dalla comp-
lessità della funzione, e offrono sempre una soluzione velocissima anche per
operazioni che necessitano di alta precisione. Il problema è che, spesso, non
vi è abbastanza spazio su una FPGA per ospitare on–board tali LUT. Si può
far ricorso ad una memoria esterna, ma parte del vantaggio viene a perdersi a
causa della logica di interfacciamento e del tempo perso per via degli accessi
in lettura. Per complesso che sia, un CORDIC (il quale permette di calco-
lare funzioni trigonometriche) occupa molto meno spazio delle LUT. Senza
contare che, se si riesce ad usare la pipeline, il throughput a regime è uguale
per LUT e CORDIC.
Per diminuire il numero di LUT necessarie si può ricorrere ad approssi-
mazioni o interpolazioni sui valori salvati, ma si perde in precisione (senza
contare che si complica la logica e il throughput rischia di diminuire). Allora,
quando usare le LUT? Innanzitutto, se sappiamo che, per esempio, la nostra
FPGA dovrà sempre e solo calcolare un coseno, potrebbe essere utile usare
le LUT; oppure possono essere usate per salvare dei dati in memoria con alta
precisione (per esempio, per certe operazioni è necessario salvare il valore
del log 2, e si può utilizzare una LUT); più in generale, possono essere usate
quando l’utilizzo di un componente che esegua la corrispondente operazione
diventa troppo dispendioso.

8.3 Codice VHDL per una LUT


Una LUT è facilissima da implementare. Vediamo un esempio semplice
per il calcolo di y = x2 , supponendo l’ingresso a 4 bit e l’output a 16 bit:

library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_ARITH.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;

entity LUT1 is

110
Port ( addr : in STD_LOGIC_VECTOR (3 downto 0);
clk : in STD_LOGIC;
ris : out STD_LOGIC_VECTOR (7 downto 0));
end LUT1;

architecture Behavioral of LUT1 is


begin
process(clk)
begin
if rising_edge(clk) then
case addr is
when X"0" => ris <= X"00";
when X"1" => ris <= X"01";
when X"2" => ris <= X"04";
when X"3" => ris <= X"09";
when X"4" => ris <= X"0F";
when X"5" => ris <= X"19";
when X"6" => ris <= X"24";
when X"7" => ris <= X"31";
when X"8" => ris <= X"40";
when X"9" => ris <= X"51";
when X"A" => ris <= X"64";
when X"B" => ris <= X"79";
when X"C" => ris <= X"90";
when X"D" => ris <= X"A9";
when X"E" => ris <= X"C4";
when X"F" => ris <= X"E1";
when others => ris <= (others => ’X’);
end case;
end if;
end process;
end Behavioral;

In pratica, scriviamo un unico ciclo select...case per l’indirizzamento


in memoria. Come notiamo dallo schema di Fig. 8.1, il sintetizzatore ri-
conosce la struttura a Look–Up Tables, aggiungendo un FF in uscita per
sincronizzarci con il clock.

111
Figura 8.1: Schema RTL dell’implementazione attraverso LUT

8.4 Codice per la generazione automatica del


VHDL
Ovviamente, mano a mano che i circuiti da implementare attraverso LUT
diventano più complessi, appare improponibile la scrittura manuale di tutte
le righe di codice necessarie. Pertanto, è utile avere come riferimento un
codice piuttosto generale in C/C++/C#, in grado di adattarsi alle nostre
esigenze sia per quanto riguarda la funzione da implementare, ma anche i bit
di precisione di I/O. Quanto segue vuol essere un piccolo esempio–guida:

#include "stdafx.h"
#define bin 4
#define bout 8

#using <mscorlib.dll>

using namespace System;

int funzione(int x)
{
return(x*x);
}

char* toBool(int x, int b)


{
char* ris=new char[b];
for(int j=0;j<b;j++)
ris[j]=’0’;
if(x==0)
return ris;
else if(x==1)

112
{
ris[b-1]=’1’;
return ris;
}
int i=0;
while(x!=1)
{
int r=x%2;
x=(int)(Math::Floor((double)x/2.0));
ris[b-1-i]=(char)(r+48);
i++;
}
ris[b-1-i]=’1’;
return ris;
}

void main()
{
FILE* fp;
fp=fopen("lut.vhd","w");

//intestazione
fprintf(fp,"library IEEE;\n");
fprintf(fp,"use IEEE.STD_LOGIC_1164.ALL;\n");
fprintf(fp,"use IEEE.STD_LOGIC_ARITH.ALL;\n");
fprintf(fp,"use IEEE.STD_LOGIC_UNSIGNED.ALL;\n\n");

//entity
fprintf(fp,"entity LUT1 is\n");
fprintf(fp,"\tPort ( addr : in _
_ STD_LOGIC_VECTOR (%d downto 0);\n", (bin-1));
fprintf(fp,"\t\tclk : in STD_LOGIC;\n");
fprintf(fp,"\t\tris : out _
_ STD_LOGIC_VECTOR (%d downto 0));\n", (bout-1));
fprintf(fp,"end LUT1;\n\n");

//architecture
fprintf(fp,"architecture Behavioral of LUT1 is\n");
fprintf(fp,"begin\n");
fprintf(fp,"\tprocess(clk)\n");
fprintf(fp,"\tbegin\n");

113
fprintf(fp,"\t\tif rising_edge(clk) then\n");
fprintf(fp,"\t\t\tcase addr is\n");

for(int i=0;i<Math::Pow(2.0,bin);i++)
{
char* x=toBool(i,bin);
int ris=funzione(i);
char* y=toBool(ris,bout);
fprintf(fp,"\t\t\t\twhen \"");
for(int j=0;j<bin;j++)
fprintf(fp,"%c",x[j]);
fprintf(fp,"\" => ris <= \"");
for(int j=0;j<bout;j++)
fprintf(fp,"%c",y[j]);
fprintf(fp,"\";\n");
}
fprintf(fp,"\t\t\t\twhen others => _
_ ris <= (others => ’X’);\n");
fprintf(fp,"\t\t\tend case;\n");
fprintf(fp,"\t\tend if;\n");
fprintf(fp,"\tend process;\n");
fprintf(fp,"end Behavioral;");
fclose(fp);
}

E’ un codice C++, realizzato in Microsoft Visual Studio .NET 2003.


La generalità del codice è sottolineata dal fatto che, se si vogliono variare
i bit per l’I/O, è sufficiente modificare le definizioni iniziali, mentre, per
la funzione da implementare, bisogna modificare l’apposito metodo C++.
Il resto è sostanzialmente interfacciamento con file. Il metodo toBool è
stato realizzato ad hoc per trovare stringhe di caratteri da stampare su file.
Il codice cosı̀ realizzato può essere direttamente importato nell’ambiente di
sviluppo della FPGA.

114
Capitolo 9

Hierarchical Design

9.1 Introduzione
La metodologia Hierarchical Design prevede la divisione di un sistema
in sotto–moduli in maniera ricorsiva, in modo tale da poter progettare i
sotto–moduli in maniera indipendente. Con il termine “ricorsivamente” si
intende che, a loro volta, i sotto–moduli possono essere divisi in unità ancora
più elementari, fino ad arrivare a strutture molto semplici da progettare ed
utilizzare.

Figura 9.1: Esempio di progetto diviso ricorsivamente in unità elementari

115
Consideriamo l’esempio proposto in Fig. 9.1. Il sistema complesso (un
moltiplilcatore sequenziale, ovvero un dispositivo che calcola il risultato di
una moltiplicazione svolgendo una serie sequenziale di somme, utile quando
la FPGA utilizzata sia sprovvista di moltiplicatori) viene inizialmente diviso
in control path, ovvero un sistema di controllo, e data path per il calcolo vero
e proprio delle somme. Il primo dei due sotto–moduli è, di fatto, una FSM e
viene diviso sulla base dello schema proposto in Fig. 6.3. Il data path viene
anch’esso suddiviso in registri per la memorizzazione del dato, unità di rete
e componenti di routing ed interfacciamento. Le unità funzionali sono, poi,
dei semplici sommatori e sotrattori (che permettono di contare il numero di
somme ancora necessarie).

9.1.1 Hierarchical Design: pro e contro


La divisione di un progetto molto complesso in tanti sotto–moduli el-
ementari porta, di fatto, moltissimi vantaggi: un sistema modulare è più
semplice da progettare (specie se il progetto è in team) e molti ocmpo-
nenti possono poi essere riutilizzati in lavori successivi. Molto spesso, le
aziende di progettazione hardware creano delle proprie librerie di componen-
ti, progettati e migliorati nel corso degli anni. Ogni componente è detto
IP–core.
Progettare un sotto–modulo elementare è vantaggioso sia per chi proget-
ta, il quale può concentrarsi sugli aspetti critici dell’unità ottenendo un codice
migliore, sia per il sintetizzatore: è facile mostrare come l’ottimizzazione di un
sintetizzatore sia più efficace su sistemi semplici rispetto a moduli complessi.
Sembrerebbe non esistano difetti in un tale approccio. L’unico grande
problema di tale approccio è il fatto che l’interfacciamento tra i vari moduli
può divenire pesante: bisogna, quindi, stare attenti a non esagerare con la
modularità. In aiuto, spesso, al posto del codice VHDL possono essere us-
ati gli schematici: realizziamo i componenti elementari in VHDL, creiamo i
simboli circuitali, e utilizziamo schematics per le interconnessioni. Un appos-
ito compilatore provvederà all’istanziazione e alla sintesi del corrispondente
codice.

9.1.2 Costrutti VHDL per il Hierarchical Design


In questo capitolo analizzeremo i principali costrutti VHDL per il Hier-
archical Design. Come detto, in casi di interconnessione semplice, può essere
furbo anche utilizzare gli schematici. I principali costrutti (che abbiamo
presentato in maniera approssimativa nel Cap. 2) sono:

116
• Component;

• Generic;

• Configuration;

• Library;

• Package;

• Subprogram.

9.2 Component
Come detto, la metodologia Hierarchical Design si basa sulla creazione
di unità elementari, da includere in dispositivi più complessi. Tale inclusione
avviene attraverso l’utilizzo del costrutto component. La sintassi generale è:

component nome_component
generic (
{dichiarazioni}
...
);
port (
{porti di I/O}
);
end component;

Per quanto riguarda la parte generic, approfondiremo il discorso nel


prossimo paragrafo. Vediamo come passare da una generica entity ad un
componente. Si consideri la seguente entity:

ENTITY asm1es IS
PORT( Ck: IN std_logic;
Reset: IN std_logic;
DIR: IN std_logic;
C0: OUT std_logic;
C1: OUT std_logic );
END asm1es;

che è la struttura proposta per la FSM di Fig. 7.8. Un componente può


essere dichiarato nel seguente modo:

117
COMPONENT asm1es
PORT( Ck: IN std_logic;
Reset: IN std_logic;
DIR: IN std_logic;
C0: OUT std_logic;
C1: OUT std_logic );
END COMPONENT;

Se all’interno di un modulo VHDL complesso vogliamo utilizzare un com-


ponente, dobbiamo dichiararlo nella sezione dichiarativa di una architettura.
Una volta dichiarato, il componente va istanziato, ovvero dobbiamo dire al
sintetizzatore come pensiamo di connettere i porti del componente ai seg-
nali o porti del nostro dispositivo. Un’istanziazione generica ha la seguente
sintassi:

etichetta : nome_component
generic map (
{associazioni}
)
port map (
{associazioni}
);

Le associazioni possono essere effettuate in modo esplicito o implicito.


Nel primo caso, ad ogni segnale associamo manualmente un porto del nostro
componente, e la sintassi è del tipo:

porto => segnale

Nel secondo caso, scriviamo semplicemente l’elenco dei segnali da asso-


ciare, e il collegamento è fatto sull’ordine di dichiarazione nella entity e sulla
base dell’elenco fornito dal programmatore. Una dichiarazione implicita è
sempre sconsigliabile, perchè rende anche poco chiaro il codice. Supponen-
do, quindi, di usare una associazione esplicita, vediamo l’istanziazione per
un componente del tipo FSM visto in precedenza:

ARCHITECTURE behavior OF tfsm1_vhd IS

-- Dichiarazione
COMPONENT asm1es
PORT(
Ck : IN std_logic;

118
Reset : IN std_logic;
DIR : IN std_logic;
C0 : OUT std_logic;
C1 : OUT std_logic
);
END COMPONENT;

--Inputs
SIGNAL Clk : std_logic := ’0’;
SIGNAL Res : std_logic := ’0’;
SIGNAL D : std_logic := ’0’;

--Outputs
SIGNAL Tot : std_logic_vector(1 downto 0);

BEGIN

-- Instanziazione
dut: asm1es PORT MAP(
Ck => Clk,
Reset => Res,
DIR => D,
C0 => Tot(0),
C1 => Tot(1)
);
...
END;

9.3 Generic
Il costrutto generic è il metodo per creare e passare un’informazione
generica ad una entità e componente. Sostanzialmente, è un parametro che
permette di creare un componente molto generale. Supponiamo, ad esempio,
di voler scrivere il codice per un registro a W bit, ma senza precisare a priori
il valore di W . La sua entità potrebbe essere:

ENTITY reg IS
GENERIC ( W: natural );
PORT ( Clk: IN std_logic;
Reset: IN std_logic;

119
D: IN std_logic_vector(W-1 downto 0);
Q: OUT std_logic_vector(W-1 downto 0) );
END reg;

A questo punto, il modulo cosı̀ creato non può più essere utilizzato in
maniera indipendente, ma dovrà essere sempre istanziato. Per esempio,
generiamo due istanze di registri, uno a 4 e il secondo a 16 bit:

ARCHITECTURE behavior OF tfsm1_vhd IS

-- Dichiarazione
COMPONENT reg
GENERIC ( W: natural );
PORT ( Clk: IN std_logic;
Reset: IN std_logic;
D: IN std_logic_vector(W-1 downto 0);
Q: OUT std_logic_vector(W-1 downto 0) );
END COMPONENT;

--Inputs
SIGNAL Clk : std_logic := ’0’;
SIGNAL Reset : std_logic := ’0’;
SIGNAL D4 : std_logic_vector(3 downto 0) := X"0";
SIGNAL D16 : std_logic_vector(15 downto 0) := X"0000";

--Outputs
SIGNAL Q4 : std_logic_vector(3 downto 0);
SIGNAL Q16 : std_logic_vector(15 downto 0);

BEGIN

-- Instanziazione 1
dut4: reg
GENERIC MAP ( W => 4 )
PORT MAP(
Clk => Clk,
Reset => Reset,
D => D4,
Q => Q4
);
-- Instanziazione 2

120
dut16: reg
GENERIC MAP ( W => 16 )
PORT MAP(
Clk => Clk,
Reset => Reset,
D => D16,
Q => Q16
);
...
END;

9.4 Configuration
Talvolta, è possibile associare più architetture ad una stessa entità. Pren-
diamo il caso della FSM progettata nel paragrafo 7.2.2: abbiamo proposto
un’architettura basata su una macchina con output di Moore e una mista
Moore/Mealy, ma la entity era uguale. Si pensi ancora ad un contatore:
potremmo realizzare un’architettura per un contatore in avanti e una per un
contatore all’indietro. Nel momento in cui vogliamo dichiarare un compo-
nente, occorre però precisare quale configurazione si voglia usare. La sintassi
è la seguente:

CONFIGURATION nome_conf OF entità IS


FOR nome_architettura
FOR etichetta: nome_componente
USE ENTITY libreria.entità(architettura);
END FOR;
...
END FOR;
END;

Spesso, in maniera più semplice, può essere utilizzata una configurazione


diretta in fase di dichiarazione e non di istanziazione. Vediamo un esempio.
Supponiamo di aver realizzato un contatore, avente un’architettura su per
quando conta in avanti, e giu per contare all’indietro. Se volessimo introdurre
un componente contatore in avanti e uno all’indietro, potremmo scrivere:

ARCHITECTURE behavior OF tfsm1_vhd IS

-- Dichiarazione
COMPONENT contatore IS

121
PORT ( Clk: IN std_logic;
Q: OUT std_logic_vector(7 downto 0) );
END COMPONENT;

FOR av: contatore


USE ENTITY work.contatore(su);
FOR ind: contatore
USE ENTITY work.contatore(giu);
...
A questo punto, si procede tranquillamente utilizzando due istanze av e
ind di cui poi effettueremo l’istanziazione classica. La libreria work è quella
in cui vengono inseriti di default i moduli che generiamo all’interno di un
singolo progetto.

9.5 Library
Una libreria è una raccolta di moduli VHDL, siano essi entità, architet-
ture, configurazioni, package,... Spesso, in progetti di grandi dimensioni, è
utile raccogliere il nostro lavoro in librerie separate, ad esempio, in base alle
operazioni svolte dai moduli e cosı̀ via. Per creare una libreria, una volta
realizzati i moduli che vogliamo aggiungere, semplicemente si usa l’ambiente
di sviluppo e si impone la creazione di una nuova libreria, aggiungendo tutti
i file che desideriamo.
Per importare una libreria, si usa la parola chiave library. La struttura è
gerarchica, ovvero una libreria può contenere altre librerie al proprio interno.
In tal caso, si usa la seguente notazione per la parola chiave use:
library libreria;
use libreria.elemento.all;
La struttura è simile al Java, e la keyword all svolge il ruolo del sim-
bolo *. Ovviamente, si può decidere di importare un’intera libreria, o solo
alcune parti di essa: in tal caso vanno precisate con la precedente struttura
gerarchica. Nel momento in cui si implementa un progetto, viene sempre e
comunque realizzata una libreria work, importata di default.

9.6 Subprogram
Spesso, se le operazioni da svolgere in differenti punti della nostra ar-
chitettura su dati dello stesso tipo sono ripetitive ed identiche, può essere

122
comodo definire delle function e procedure: l’unica differenza fra le due è che
una funzione ritorna un valore di un certo tipo, mentre la procedura è void,
ovvero senza valore di ritorno.
Ci concentriamo sulle funzioni, perchè sono di gran lunga le più us-
ate. Esse vanno incluse nella parte dichiarativa di una architettura (tra la
dichiarazione della architettura e il BEGIN, per capirci), e hanno la seguente
sintassi:

FUNCTION funz(parametri : tipi) RETURN tipo IS


{dichiarazioni}
BEGIN
{istruzioni sequenziali}
RETURN risultato;
END;

FUNCTION funz(parametri : tipi) RETURN tipo; viene detta dichia-


razione della funzione. Attenzione però: spesso le funzioni utilizzano variabili
al proprio interno, e sappiamo che un uso sconsiderato di queste ultime può
portare alla fallita sintesi. Senza contare che anche funzioni troppo complesse
possono condurre ad un codice non implementabile in hw.
Vediamo un breve esempio. In questo caso, vogliamo trovare una funzione
che effettui la seguente operazione: y = (a · b) + c. Possiamo scrivere:

FUNCTION oper(a,b,c : std_logic) RETURN std_logic IS


variable tmp : std_logic;
BEGIN
tmp := (a and b) or (not c);
RETURN tmp;
END;

La variabile tmp è palesemente inutile, ma è stata introdotta con scopi


esemplificativi.

9.7 Package
Concludiamo questa carrellata con l’ultimo costrutto, il package. Mano
a mano che un sistema diviene sempre più complesso e racchiude in sè sem-
pre più componenti, dichiarazioni, costanti, tipi,..., la parte dicharativa di
una architettura può diventare effettivamente molto pesante, rendendo poco
leggibile il codice. Il package ha proprio lo scopo di racchiudere in sè una

123
collezione di tutti quegli elementi che altrimenti andrebbro a ridurre la leg-
gibilità. Un package creato entra a far parte di default della libreria work, o
può essere manualmente incluso in qualsiasi altra libreria user defined.
Un package è composto da 2 parti: una sezione didichiarazioni e un
body. Nella prima vengono incluse tutte le dichiarazioni generali, nel body le
implementazioni delle eventuali funzioni dichiarate nella sezione dichiarativa.
Se non è presente alcun sottoprogramma, non è necessaria la scrittura di alcun
body. La sintassi è la seguente:

-- parte dichiarativa (obbligatoria)


PACKAGE nome IS
{dichiarazioni}
END nome;
-- body (solo con soubprograms)
PACKAGE BODY nome IS
{sottoprogrammi}
END nome;

124
Capitolo 10

Parameterized VHDL

Progetti sempre più complessi possono necessitare di strutture molto gen-


erali, personalizzabili a seconda delle esigenze di progetto. Esistono costrutti
che permettono uno sfruttamento molto generale di strutture parametrizz-
abili : lo scopo del progettista diventa, in questi casi, realizzare un modulo
quanto più riutilizzabile.
Un primo esempio di VHDL caratterizzato da parametri è stato proposto
nel paragrafo 9.3: la keyword generic permette di rendere una entità molto
generale. In questo capitolo, viene proposta una panoramica generale su
alcuni costrutti avanzati, i quali però sono, al solito, armi a doppio taglio:
un uso non controllato può portare a codice illeggibile, non ottimizzato o
non sintetizzabile. Il VHDL con parametri è da utilizzare SOLO quando
effettivamente necessario e vantaggioso.

10.1 Attributi di un array


Un attributo di un segnale (sia esso un singolo valore o un array) for-
nisce informazioni sulle caratteristiche del segnale stesso. La sintassi di un
attributo è semplice:

(nome_segnale)’(attributo)

I tipi di attributi sono molti, specie per gli array. Essi spaziano dagli even-
ti riguardanti il segnale alle caratteristiche del segnale stesso. Elenchiamo i
più utilizzati:

• ’event: resituisce un boolean true se il segnale ha subito una modifica


in uno dei suoi valori;

125
• ’left e ’right: rappresentano estremo sinistro e destro del range di
un array;

• ’length: restituisce il numero di elementi totali contenuti nell’array;

• ’range: resituisce il range di indici valido per un certo segnale;

• ’reverse_range: restituisce il range di indici, ma in ordine invertito.

Per esempio, consideriamo il seguente segnale:

signal s1 : std_logic_vector(8 to 15);

Alcuni attributi sono:

• s1’left=8, s1’right=15;

• s1’length=8;

• s1’range=8 to 15;

• s1’reverse_range=15 downto 8.

Gli attributi possono essere utili per cicli for (per ottenere i range per
gli indici), ma anche in fase di dichiarazione di segnali. Per esempio:

signal s2 : std_logic_vector(s1’reverse_range);

è equivalente a:

signal s2 : std_logic_vector(15 downto 8);

10.2 Array con e senza range


Spesso, si può pensare di dichiarare un nuovo tipo di dato, per esempio
un array, ma di voler porre dei limiti alla libertà implementativa. Nel mo-
mento in cui vogliamo dichiarare un nuovo tipo di array, la dichiarazione ha
il seguente formato:

type nuovo_array is array(15 downto 0)


of std_logic;

126
Di fatto, dichiariamo un nuovo tipo di vettore di bit, il cui indice sarà
intero positivo (natural), ma che potrà avere al massimo un range per i
propri indici che varia fra 0 e 15. Pertanto, si hanno dichiarazioni consentite
e non permesse:

-- ok
signal s1 : nuovo_array(15 downto 8);
signal s2 : nuovo_array(13 downto 0);
-- ERRORE!!
signal s3 : nuovo_array(31 downto 0);

Allo stesso modo, possiamo dichiarare array senza range di valore per gli
indici. In tal caso, la dichiarazione diventa:

type nuovo_array_unbound is array(natural range <>)


of std_logic;

10.3 Costrutto For...Generate


I costrutti di generazione caratterizzati dalla keyword generate sono
gli unici in tutto il VHDL ad essere considerati quali istruzioni concorrenti
contenenti al proprio interni istruzioni anch’esse concorrenti.
In particolare, in questo paragrafo analizziamo il for...generate: questo
costrutto è molto utile qualora si debbano implementare molte operazioni di
egual tipo sui dati in maniera parallela, oppure nel caso in cui, all’interno di
un dispositivo gerarchicamente più importante, si debbano istanziare molti
componenti di un certo tipo. La sintassi è la seguente:

etichetta: FOR indice IN range GENERATE


{istruzioni concorrenti}
END GENERATE;

Facciamo un breve esempio. Supponiamo di voler implementare un de-


coder con un ingresso a di dimensioni settabili attraverso parametro generico,
e di voler generare tutti i possibili confronti con i possibili codici che taleinput
può assumere. Il VHDL corrispondente risulta:

library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.numeric_std.ALL;

127
entity bin_decoder is
Generic ( W : natural);
Port ( a : in STD_LOGIC_VECTOR (W-1 downto 0);
code : out STD_LOGIC_VECTOR (2**W-1 downto 0));
end bin_decoder;

architecture Behavioral of bin_decoder is

begin

es: for i in 0 to (2**W-1) generate


code(i) <= ’1’ when i=to_integer(unsigned(a)) else
’0’;
end generate;

end Behavioral;

Oppure, un ciclo for...generate può essere usato, come detto, per l’is-
tanziazione di componenti:

es: for i in (W-1) downto 0 generate


dff_array: dff
port map( clk => clk,
d => q_reg(i+1),
q => q_reg(i));
end generate;

10.4 Costrutto if...generate


Analogamente a quanto visto per il for...generate, anche questo costrutto
al suo interno contiene istruzioni concorrenti. L’uso è assolutamente analogo
a quanto visto per il for...generate. Un esempio può essere il seguente:

es: for i in (W-1) downto 0 generate


left_gen: if i=1 generate
tmp(i) <= a(i) xor a(0);
end generate;
else_gen: if i>1 generate
tmp(i) <= a(i) xor a(i-1);
end generate;
end generate;

128
10.5 Array bidimensionali
Un tipo nativo per descrivere una matrice in VHDL non esiste. Si pos-
sono, comunque, sfruttare le proprietà di dichiarazione di tipi per generare
array multi–dimensionali. Esistono, di fatto, due tecniche (in verità si può an-
che costruire un array di array, ma è una tecnica usata pochissimo), entrambe
valide.

10.5.1 Array bi–dimensionale effettivo


Concentriamoci su array bidimensionali (sopra le due dimensioni diventa
effettivamente un problema difficile da trattare). E’ possibile definire un tipo
matrice nel seguente modo:

TYPE matrice IS array(range_1, range_2)


OF tipo_dato;

Ovviamente, possiamo costruire matrici con limiti per il range o senza


limiti. A questo punto l’inizializzazione può avvenire nei 3 modi seguenti:

-- associazione in base alla posizione


t1 <= ("0000",
"0110",
"0001");
-- inizializzazione per righe
-- si inizializzano tutte a 0110
t2 <= (others => "0110");
-- inizializzazione completa a 0
t3 <= (others=>(others=>’0’));

L’accesso e la scrittura dati può avvenire attraverso la seguente notazione:

-- input
t1(1,0) <= ’0’;
-- output
e <= t(2,2);

E’ una notazione molto usata per emulare banchi di RAM onboard su


FPGA.

129
10.5.2 Array bi–dimensionale emulato
Un’alternativa è rappresentata dalla possibilità di rappresentare una ma-
trice attraverso un vettore. Se la matrice ha m righe e n colonne, la dimen-
sione dell’array sarà m · n e, se vogliamo leggere o scrivere l’elemento (i, j),
è sufficiente usare un indice k sull’intero array pari a:

k =i·n+j (10.1)
A questo punto, è sufficiente introdurre un classico vettore, e una funzione
per il cambio di indice:

constant R : natural := 4;
constant C : natural := 6;
signal s : std_logic_vector(R*C-1 downto 0);
function ix(i,j : natural) return natural is
begin
return(i*R+j);
end ix;
...
a <= s(2,1);

130
Capitolo 11

Testbench

11.1 Test di dispositivi digitali


Una volta progettato, un dispositivo digitale deve essere testato. I test e
le verifiche per sistemi basati su FPGA esistono a diversi livelli del Develop-
ment Flow (si veda il paragrafo 1.3 e la Fig. 1.6): si inizia con una simulazione
comportamentale, per verificare di aver scritto un codice coerente; si passa
alle varie simulazioni, con informazioni temporali sempre più realistiche; in-
fine, vi è il test post–implementazione, per verificare che la programmazione
sia andata a buon fine.
Nonostante le prime simulazioni siano “astratte”, nel senso che vengono
effettuate utilizzando un simulatore software (che, per perfetto che sia, non
è comunque una scheda in un ambiente reale...), mentre l’ultima verifica sia
molto concreta, le strutture sono molto simili. Ad un Device Under Test
(DUT ) viene connesso in input un generatore di stimoli, mentre le uscite del
DUT vengono analizzate da un oscilloscopio o un Logic–State–Analyzer (più
in generale, potremmo parlare di un visualizzatore di output). Lo schema
generale è quello di Fig. 1.5.
In un caso di test fisico di un componente, il generatore di stimoli può
essere, ad esempio, un Word Generator, il quale è un generatore di forme
d’onda digitali, ovvero di sequenze di ingresso per il DUT. Il Word Generator
fornisce gli input al DUT, e sul visualizzatore (ad esempio, un Logic–State–
Analyzer) potremo confrontare le uscite generate con quelle desiderate, e
controllare il corretto funzionamento del dispositivo.
Nel caso di test simulativo, l’input è fornito da un file VHDL particolare,
detto testbench module o testbench, il quale, di fatto, svolge il ruolo del gen-
eratore di forme d’onda. Il visualizzatore, invece, è un apposito simulatore:
i più comuni sono ModelSim e, in ambiente Xilinx, Xilinx ISE Simulator. In

131
particolare, i principali tipi di simulazione sono:

• simulazione comportamentale: non tiene conto dei ritardi dovuti al


routing, alla logica,..., ed è utile per controllare la correttezza e la
coerenza del codice scritto;

• simulazione post place–and–route: il sintetizzatore simula il piazzamen-


to su FPGA del nostro dispositivo, e simula il comportamento, tenendo
conto di tutti i possibili ritardi, glitch,...

11.2 Creazione di un testbench


Come detto, un testbench è un modulo VHDL che ha, però, alcune
peculiarità:

• non ha una sua entity, in quanto non dev’essere un componente vero e


proprio, ma solo un file che fornisce gli ingressi al simulatore;

• contiene sempre un’istanza del DUT;

• ha sempre dei segnali interni in egual numero rispetto ai porti di I/O


del DUT: in tal modo, vengono connessi gli ingressi e le uscite per
l’analisi simulativa;

• in generale, contiene un numeor di processi pari al numero di input:


infatti, in ogni processo viene fatto variare secondo una cadenza presta-
bilita un certo segnale;

• in generale, non contiene processi con sensitivity list, ma processi con


strutture wait...for ;

• tale modulo non dev’essere sintetizzato, pertanto si possono utilizzare


anche costrutti normalmente non sintetizzabili (quali, per esempio,
proprio il wait...for ).

In quasi tutti gli ambienti di sviluppo hardware, esistono strumenti per la


generazione automatica di un testbench per un certo dispositivo. Vediamo,
ad esempio, come potrebbe essere scritto un testbench per l’esempio di FSM
proposto nel paragrafo 7.2.2.

LIBRARY ieee;
USE ieee.std_logic_1164.ALL;
USE ieee.std_logic_unsigned.all;

132
USE ieee.numeric_std.ALL;

ENTITY tfsm_vhd IS
END tfsm_vhd;

ARCHITECTURE behavior OF tfsm_vhd IS

COMPONENT asm1es
PORT(
Ck : IN std_logic;
Reset : IN std_logic;
DIR : IN std_logic;
C0 : OUT std_logic;
C1 : OUT std_logic
);
END COMPONENT;

--Inputs
SIGNAL Ck : std_logic := ’0’;
SIGNAL Reset : std_logic := ’0’;
SIGNAL DIR : std_logic := ’0’;

--Outputs
SIGNAL C0 : std_logic;
SIGNAL C1 : std_logic;

BEGIN

-- Instantiate (DUT)
dut: asm1es PORT MAP(
Ck => Ck,
Reset => Reset,
DIR => DIR,
C0 => C0,
C1 => C1
);

res : PROCESS
BEGIN
reset <= ’0’;
wait for 70 ns;

133
reset <= ’1’;
wait; -- will wait forever
END PROCESS;

clk: process
begin
ck <= ’0’;
wait for 50 ns;
ck <= ’1’;
wait for 50 ns;
end process;

dir_proc: process
begin
dir <= ’0’;
wait for 450 ns;
dir <= ’1’;
wait;
end process;

END;

Dopo aver istanziato il DUT, abbiamo i 3 processi per gestire i 3 ingressi:

• il reset (attivo basso) viene portato inizialmente a 0 per 70 ns, quindi


viene forzato a 1 per il resto della simulazione;

• il segnale DIR viene forzato a 0 per i primi 450 ns, quindi viene portato
a 1 per il resto della simulazione;

• il clock dato in ingresso ha un periodo di 100 ns con un duty cycle del


50%, quindi ad una frequenza di 10 MHz.

A questo punto, lanciamo la simulazione comportamentale. Il risultato


è mostrato in Fig. 11.1. Il circuito si comporta correttamente. Possiamo,
quindi, passare alla simulazione post place–and–route. l’output di tale sim-
ulazione è presentato in Fig. 11.2: si può notare solo una piccola fase di
ritardo nella stabilizzazione dell’output, ma per il resto nessun problema.
Fin qui tutto bene, ma ci siamo limitati ad un clock di 10 MHz. Se
analizziamo i file di sintesi, i valori temporali ci dicono che la frequenza
massima di funzionamento è circa 750 MHz, quindi ben al di sopra di quella
da noi settata. Proviamo ad incrementare la frequenza del clock a 100 MHz,

134
Figura 11.1: Diagramma temporale per una simulazione comportamentale

Figura 11.2: Diagramma temporale per una simulazione post place–and–


route

modificando i periodi in 5 ns. il risultato è mostrato in Fig. 11.3: notiamo


come in fase di conteggio all’indietro nascano dei fastidiosi glitch dovuti ai
ritardi della logica. Nessun problema, comunque, se leggiamo gli output sul
fronte di salita del clock, come mostrato in Fig. 11.4. Questo, comunque, ci
fa già capire come i dati del sintetizzatore siano spesso molto indicativi: e
siamo ancora totalmente in fase simulativa!

Figura 11.3: Diagramma post place–and–route a 100 MHz

E se sforiamo la frequenza massima? Supponiamo di voler portare il


circuito a 1 GHz. Che succede nella post place–and–route? Lo si può vedere
in Fig. 11.5: una bella marea di glitch e ritardi, con output totalmente errati.
Tutto ciò a causa dei ritardi di propagazione.
Concludiamo questo capitolo con un utile consiglio pratico. Spesso, quan-
do si vogliono far variare molti segnali con frequenze multiple o sotto–multiple
della frequenza del clock, o anche solo per maggiore chiarezza, è utile definire
a livello di architecture una costante per il periodo (o semiperiodo) del clock
stesso. In parole povere, per esempio:

ARCHITECTURE behavior OF tfsm1_vhd IS

135
Figura 11.4: Ingrandimento di un glitch e un fronte di salita a 100 MHz

Figura 11.5: Diagramma post place–and–route a 1 GHz

136
-- Component Declaration for the Unit Under Test (UUT)
COMPONENT asm1es
PORT(
Ck : IN std_logic;
Reset : IN std_logic;
DIR : IN std_logic;
C0 : OUT std_logic;
C1 : OUT std_logic
);
END COMPONENT;

--Inputs
SIGNAL Ck : std_logic := ’0’;
SIGNAL Reset : std_logic := ’0’;
SIGNAL DIR : std_logic := ’0’;

--Outputs
SIGNAL C0 : std_logic;
SIGNAL C1 : std_logic;

-- Definizione del semiperiodo


CONSTANT T : time := 50 ns;

BEGIN
...

A questo punto, il processo per il clock, ad esempio, diviene:

clk: process
begin
ck <= ’0’;
wait for T;
ck <= ’1’;
wait for T;
end process;

137
Capitolo 12

Esempi finali e guida a Xilinx


ISE Web–Pack

12.1 Introduzione a Xilinx ISE Web Pack 8.2i


Vediamo come è possibile realizzare un nuovo progetto all’interno del-
l’ambiente di sviluppo Xilinx. Il software si presenta, in molti aspetti, simile
a Visual Studio, e di fatto le funzionalità non sono poi cosı̀ diversi (per quanto
profondamente differenti siano gli obiettivi dei due strumenti). In Fig. 12.1
viene mostrato l’ambiente di sviluppo, cosı̀ come si presenta al primo lancio.
La prima cosa da creare è un progetto: all’interno di esso andremo
a creare i vari dispositivi, scrivendo i moduli VHDL corrispondenti. Un
progetto è caratterizzato da:

• un nome. Attenzione: l’ambiente Xilinx richiede che nell’intero path


di creazione della cartella per il nuovo progetto NON vi siano spazi;

• un top–level source, ovvero un dispositivo che, nell’ordine gerarchico,


costituisca l’oggetto da implementare fisicamente su scheda;

• il tipo di top–level source: VHDL o schematico;

• il tipo di dispositivo di destinazione del nostro progetto:

– categoria della scheda (militare, general purpose,...);


– famiglia (Virtex–4, Spartan–2,...);
– tipo di device della particolare famiglia;
– package (ovvero, caratteristiche di produzione);

138
Figura 12.1: Interfaccia grafica iniziale dell’ISE Web Pack

– speed–grade, ovvero i nanosecondi di ritardo per ogni grado di


logica;
– sintetizzatore;
– simulatore.

Il tutto è riassunto in Fig. 12.2.

Un nuovo progetto può essere generato cliccando su File – New Project.


A questo punto, se vogliamo creare i file per il nostro progetto, andiamo con
il mouse nella finestra Sources, clicchiamo con il testo destro e selezioniamo
New Source. A questo punto dobbiamo selezionare il tipo di file da generare.
I principali sono:

• VHDL Module: è un modulo VHDL standard. Se lo selezioniamo, una


volta scleto il nome per il modulo, dovremo indicare l’I/O per il nostro
dispositivo, e il codice VHDL verrà generato in automatico;

• VHDL Test Bench: è un modulo per il test comportamentale e post


place–and–route del tipo analizzato nel Cap. 11. Al passo successivo,
ci viene chiesto quale modulo VHDL o schematico fra quelli disponibili
intendiamo testare;

139
Figura 12.2: Dialog box per le caratteristiche di un nuovo progetto

• Schematic: genera un nuovo schematico da implementare con blocchi


già disponibili o realizzati. Molto utile per l’interfacciamento gerar-
chico.

Supponiamo di aver creato un modulo o uno schematico, e supponiamo


che esso sia il nostro top–level source (se non lo è, supponiamo di settarlo
come tale cliccando sul file con il tasto destro e selezionando Set as Top mod-
ule, in quanto richiesto da Xilinx). In basso a sinistra, nella finestra Process-
es, sono disponibili molte utili voci. Basta cliccare due volte su ognuna di
esse per lanciare le funzioni messe a disposizione. Vediamo le principali:

• Synthesize – Check Syntax: permette di verificare che il VHDL scritto


sia corretto o le connessioni in uno schematico non creino pericolosi
corto–circuiti o conflitti;

• Design Utilities – Create Schematic Symbol: crea il simbolo corrispon-


dente al nostro modulo, il quale può essere poi utilizzato negli schemati-
ci;

• View Synthesis Report: permette di visualizzare i report di sinte-


si con tutte le caratteristiche temporali e di occupazione del modulo
realizzato;

140
• all’interno del menù Implement Design – Place–and–Route:

– cliccare due volte su Place–and–Route per generare il modello


piazzato su scheda;
– View Design permette di vedere il nostro progetto piazzato su
FPGA.

Esiste inoltre il menù Edit Constraints per creare i vincoli temporali e/o
spaziali, e il menù Generate Programming File per la creazione del bitstream.
Se, invece, abbiamo creato un testbench, ricordiamoci, per visualizzarlo,
si scegliere Behavioral (per la simulazione comportamentale) o Post–Route
(per la simulazione post–par) dal menù a tendina Sources. Dal menù Xilinx
ISE Simulator possiamo controllare la sintassi del testbench o lanciare la
simulazione.

12.2 Esempi
Nei prossimi esempi utilizzeremo come scheda di riferimento una Spartan–
3 XC3S200, package FT256, speed–grade -5. Vediamo alcune applicazioni,
in ordine di complessità crescente.

12.2.1 Half–Adder asincrono


Partiamo con un progetto semplicissimo: un half–adder a un bit asin-
crono. Il codice è semplicissimo:

library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_ARITH.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;

entity halfadder is
Port ( A : in STD_LOGIC;
B : in STD_LOGIC;
C : out STD_LOGIC;
O : out STD_LOGIC);
end halfadder;

architecture Behavioral of halfadder is

141
begin

O <= A XOR B;
C <= A AND B;

end Behavioral;

Generiamo un testbench per verificare che il tutto funzioni correttamente:

LIBRARY ieee;
USE ieee.std_logic_1164.ALL;
USE ieee.std_logic_unsigned.all;
USE ieee.numeric_std.ALL;

ENTITY test_ha_vhd IS
END test_ha_vhd;

ARCHITECTURE behavior OF test_ha_vhd IS

COMPONENT halfadder
PORT(
A : IN std_logic;
B : IN std_logic;
C : OUT std_logic;
O : OUT std_logic
);
END COMPONENT;

--Inputs
SIGNAL A : std_logic := ’0’;
SIGNAL B : std_logic := ’0’;

--Outputs
SIGNAL C : std_logic;
SIGNAL O : std_logic;

BEGIN

uut: halfadder PORT MAP(


A => A,
B => B,

142
C => C,
O => O
);

var : PROCESS
BEGIN
A <= ’0’;
B <= ’0’;
WAIT FOR 100 NS;
A <= ’0’;
B <= ’1’;
WAIT FOR 100 NS;
A <= ’1’;
B <= ’0’;
WAIT FOR 100 NS;
A <= ’1’;
B <= ’1’;
WAIT FOR 100 NS;
END PROCESS;

END;

Lanciando la simulazione, si può verificare il corretto funzionamento del


sistema.

12.2.2 Full–Adder
Sulla base dell’half–adder precedente, utilizziamo gli schematici per ot-
tenere il full–adder con riporto in ingresso. Dopo aver creato il simbolo
circuitale, si può disegnare lo schema di Fig. 12.3.

Figura 12.3: Esempio di full–adder a un bit realizzato con schematico

143
12.2.3 Decimal Counter a 2 cifre
Vediamo una struttura più complessa. Studiamo un contatore di tipo
BCD (Binary–Coded Decimal) a 2 cifre. In poche parole, un contatore BCD
interpreta ogni cifra decimale con 4 bit. Quindi, per esempio, 39 equivale a
0011 1001(ovvero la codifica binaria di 3 e 9). A questo punto, supponiamo
di voler implementare un circuito sequenziale per un contatore decimale a 2
cifre. Dovremo gestire la next–state logic, i registri di stato e la logica di out-
put. Supponendo di non usare una FSM (cosa sensata, dato che dovremmo
contemplare molti stati), i registri di stato e la logica di uscita è piuttosto
elementare: semplicemente avremo una trasferimento di dati con trasfor-
mazione dal comodo formato unsigned al classico std_logic_vector. Nella
next–state logic, invece, dovremo gestire le trasformazioni dei numeri: per-
tanto, quando arriveremo a 9 nel registro delle unità, dovremo tener traccia
dell’aumento nel registro delle decine, e riazzereremo il registro delle unità, e
cosı̀ via. Supponiamo, quindi, di avere 2 registri interni, implementati con i 4
classici segnali, ovvero stato attuale e futuro di: registro delle unità; registro
delle decine. La cosa migliore da fare è dare un’occhiata al codice, nel quale
si è supposta la presenza di un reset asincrono ed attivo basso:
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_ARITH.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;

entity DecimalCounter is
Port ( clk : in STD_LOGIC;
reset : in STD_LOGIC;
d1 : out STD_LOGIC_VECTOR (3 downto 0);
d10 : out STD_LOGIC_VECTOR (3 downto 0));
end DecimalCounter;

architecture Behavioral of DecimalCounter is


signal d1_r, d10_r : unsigned(3 downto 0);
signal d1_n, d10_n : unsigned(3 downto 0);
begin

-- register
process(clk, reset)
begin
if reset=’0’ then
d1_r <= (others=>’0’);

144
d10_r <= (others=>’0’);
elsif rising_edge(clk) then
d1_r <= d1_n;
d10_r <= d10_n;
end if;
end process;

-- next-state logic
d1_n <= X"0" when (d1_r=9) else
d1_r+1;
d10_n <= X"0" when (d1_r=9 and d10_r=9) else
d10_r+1 when (d1_r=9) else
d10_r;

-- output
d1 <= std_logic_vector(d1_r);
d10 <= std_logic_vector(d10_r);

end Behavioral;

Il testbench per questo dispositivo è altrettanto semplice:

LIBRARY ieee;
USE ieee.std_logic_1164.ALL;
USE ieee.std_logic_unsigned.all;
USE ieee.numeric_std.ALL;

ENTITY t_deccount_vhd IS
END t_deccount_vhd;

ARCHITECTURE behavior OF t_deccount_vhd IS

COMPONENT DecimalCounter
PORT(
clk : IN std_logic;
reset : IN std_logic;
d1 : OUT std_logic_vector(3 downto 0);
d10 : OUT std_logic_vector(3 downto 0)
);
END COMPONENT;

145
--Inputs
SIGNAL clk : std_logic := ’0’;
SIGNAL reset : std_logic := ’0’;

--Outputs
SIGNAL d1 : std_logic_vector(3 downto 0);
SIGNAL d10 : std_logic_vector(3 downto 0);

constant T : time := 5 ns;

BEGIN

-- Instantiate the Unit Under Test (UUT)


uut: DecimalCounter PORT MAP(
clk => clk,
reset => reset,
d1 => d1,
d10 => d10
);

res : PROCESS
BEGIN
reset <= ’0’;
wait for (T+T/2);
reset <= ’1’;
wait;
END PROCESS;

ck : process
begin
clk <= ’0’;
wait for T;
clk <= ’1’;
wait for T;
end process;

END;

Al solito, si è deciso di definire una costante T per il semiperiodo del


clock. La simulazione comportamentale offre il risultato corretto (si può
verificare per esercizio), mentre per la simulazione post place–and–route il

146
clock impostato a 100 MHz si è rivelato a frequenza troppo elevata (a causa
dei ritardi di propagazione, dato che il componente in sè può arrivare oltre i
200 MHz). Abbassando il clock a 40 MHz, si ottiene il risultato di Fig. 12.4.

Figura 12.4: Simulazione post–par a 40 MHz per il Decimal Counter a 2


cifre: si notano dei glitch, ma sul fronte di salita del clock il dato è pulito

12.2.4 Calcolo con LUT di funzione trigonometrica


Si implementi mediante LUT la seguente funzione:

y = sin(x) (12.1)
per x ∈ [0, 1). L’ingresso sia a 8 bit in binario puro, e l’output si richiede
con precisione pari a 8 bit.
Il grafico della funzione richiesta nell’intervallo è mostrato in Fig. 12.5.
La soluzione più semplice è quella, semplicemente, di rappresentare i bit di
ingresso e uscita con una classica rappresentazione:
 
valintero = valf loat · 2bit (12.2)
Ad esempio, possiamo rappresentare 0.534 a 8 bit come:

v = 0.534 · 256 = 13610 = 100010002

147
0.9

0.8

0.7

0.6

0.5

0.4

0.3

0.2

0.1

0
0 0.2 0.4 0.6 0.8 1

Figura 12.5: La funzione y = sin(x) nell’intervallo x ∈ [0, 1)

148
Ovviamente, è necessaria l’aggiunta di un codificatore apposito esterno
alla FPGA che trasferisca i dati in maniera adeguata, ma questo non fa
ovviamente parte delle specifiche di progetto. Si noti come non si possa
avere overflow, essendo il valore 1 non compreso nell’intervallo di validità di
x. Per gli output, possiamo usare la stessa notazione, ovvero:

valf loat = valintero /2bit (12.3)


Notiamo, però, come il valore massimo del seno sia:
 
ymax = sin 1 − 2−8 ≈ 0.8394
Si potrebbe pensare di normalizzare tutti i risultati per 0.84, per esempio,
complicando il decoder di uscita (esterno alla FPGA e non di nostro interesse)
ma sfruttando tutto il range dei bit che abbiamo a disposizione. Il codice
C++ di generazione del VHDL è il seguente:

#include "stdafx.h"
#define bin 8
#define bout 8

#using <mscorlib.dll>

using namespace System;

double seno(double in)


{
return Math::Sin(in);
}

char* toBool(int x, int b)


{
char* ris=new char[b];
for(int j=0;j<b;j++)
ris[j]=’0’;
if(x==0)
return ris;
else if(x==1)
{
ris[b-1]=’1’;
return ris;
}

149
int i=0;
while(x!=1)
{
int r=x%2;
x=(int)(Math::Floor((double)x/2.0));
ris[b-1-i]=(char)(r+48);
i++;
}
ris[b-1-i]=’1’;
return ris;
}

void main()
{
FILE* fp;
fp=fopen("lut.vhd","w");

//intestazione
fprintf(fp,"library IEEE;\n");
fprintf(fp,"use IEEE.STD_LOGIC_1164.ALL;\n");
fprintf(fp,"use IEEE.STD_LOGIC_ARITH.ALL;\n");
fprintf(fp,"use IEEE.STD_LOGIC_UNSIGNED.ALL;\n\n");

//entity
fprintf(fp,"entity seno is\n");
fprintf(fp,"\tPort ( addr : in _
_ STD_LOGIC_VECTOR (%d downto 0);\n", (bin-1));
fprintf(fp,"\t\tclk : in STD_LOGIC;\n");
fprintf(fp,"\t\tris : out _
_ STD_LOGIC_VECTOR (%d downto 0));\n", (bout-1));
fprintf(fp,"end seno;\n\n");

//architecture
fprintf(fp,"architecture Behavioral of seno is\n");
fprintf(fp,"begin\n");
fprintf(fp,"\tprocess(clk)\n");
fprintf(fp,"\tbegin\n");
fprintf(fp,"\t\tif rising_edge(clk) then\n");
fprintf(fp,"\t\t\tcase addr is\n");

for(int i=0;i<Math::Pow(2.0,bin);i++)

150
{
char* x=toBool(i,bin);
double tmp=(double)i/Math::Pow(2.0,bin);
double tmp2=seno(tmp);
int ris=Math::Floor(((tmp2/0.84)*256.0));
char* y=toBool(ris,bout);
fprintf(fp,"\t\t\t\twhen \"");
for(int j=0;j<bin;j++)
fprintf(fp,"%c",x[j]);
fprintf(fp,"\" => ris <= \"");
for(int j=0;j<bout;j++)
fprintf(fp,"%c",y[j]);
fprintf(fp,"\";\n");
}
fprintf(fp,"\t\t\t\twhen others => _
_ ris <= (others => ’X’);\n");
fprintf(fp,"\t\t\tend case;\n");
fprintf(fp,"\t\tend if;\n");
fprintf(fp,"\tend process;\n");
fprintf(fp,"end Behavioral;");
fclose(fp);
}

Analizziamo per prova un paio di uscite. Proviamo a calcolare il seno di


0.5, e analizziamone la precisione. In uscita otteniamo 146. A questo punto,
traduciamolo secondo lo schema introdotto:
146 ∗ 0.84
ris = ≈ 0.4791
256
Il seno di 0.5 vale:

sin(0.5) ≈ 0.4794
Calcoliamo i bit di precisione:

|log2 (|sin(0.5) − ris|)| ≈ 11bit


Proviamo a calcolare ora il seno di 0.95. I bit di precisione risultano
essere:

|log2 (|sin(0.95) − 0.8105|)| ≈ 8.4bit

151
12.2.5 Generatore di sequenze con FSM
Gli esempi con macchina a stati finiti sono stati realizzati utilizzando il
simulatore DEEDS1 , il quale contiene l’ambiente d–FsM per la progettazione
e la sintesi di macchine a stati finiti. Non solo: tramite il comando Export
VHDL, viene generato in automatico il codice per la FSM realizzata.
Supponiamo di voler realizzare un generatore di sequenze: esso ha, in
ingresso, un clock, un reset e due segnali, C0 e C1, tali da comandare l’at-
tivazione di un segnale di uscita sull’unico output T ad un preciso colpo di
clock. Lo schema è presentato in Fig. 12.6. Supponiamo che la sequenza dei
due ingressi di controllo possa variare solo durante il ciclo T0 e resti stabile
per i seguenti colpi di clock. A questo punto, possiamo generare una FSM
puramente di Moore o con uscite di Mealy (Figg. 12.7 e 12.8 rispettivamente).

Figura 12.6: Schema del generatore di sequenze

Da un’analisi temporale comportamentale si può verificare che entrambe


le soluzioni funzionano correttamente (è stata riportata in Fig. 12.9 l’analisi
per la soluzione come macchina di Mealy a titolo di esempio).

12.3 Esempio completo


Vediamo, in conclusione, un esempio completo, a partire dal testo per
finire con l’implementazione e lo schema su scheda.
1
Scaricabile all’indirizzo http://esng.dibe.unige.it/netpro/Deeds/Index.htm

152
Figura 12.7: FSM con uscite di Moore per il generatore di sequenze

153
Figura 12.8: FSM con uscite di Mealy

154
Figura 12.9: Analisi temporale comportamentale per la soluzione con FSM
Mealy

12.3.1 Testo
Si realizzi un Pulse Width Modulation Circuit (PWM). In pratica, esso
riceve in ingresso un clock ed un segnale (di dimensione variabile) che viene
codificato nel duty cycle del segnale di uscita. Il segnale di uscita avrà una
frequenza 2s volte inferiore, dove s è il numero di bit dell’ingresso, rispetto al
clock originario, e un duty cycle pari a 2ws , dove w è il segnale da modulare.
Scrivere il codice VHDL per l’implementazione, utilizzando tecniche basate
su generic, e realizzare un testbench per il sistema. Visualizzare simulazione
comportamentale e post–par, nonchè il piazzamento su FPGA e le principali
caratteristiche temporali e di occupazione, per un segnale di ingresso a 4 bit.

12.3.2 Soluzione
Viene qui proposta una possibile soluzione. Data la relativa semplicità,
dovrebbe essere intuitivo:

library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_ARITH.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;

entity pwm is
Generic ( S : natural );
Port ( clk : in STD_LOGIC;
reset : in STD_LOGIC;
w : in STD_LOGIC_VECTOR (S-1 downto 0);
pulse : out STD_LOGIC);
end pwm;

architecture Behavioral of pwm is


signal r_reg : unsigned(S-1 downto 0);
signal r_next : unsigned(S-1 downto 0);

155
signal buf_reg : std_logic;
signal buf_next : std_logic;
constant ref : std_logic_vector(S-1 downto 0)
:= (others => ’0’);
begin

-- registri e gestione buffer di output


process(clk, reset)
begin
if(reset=’0’) then
r_reg <= (others => ’0’);
buf_reg <= ’0’;
elsif rising_edge(clk) then
r_reg <= r_next;
buf_reg <= buf_next;
end if;
end process;

-- next-state
r_next <= r_reg + 1;

-- output logic
buf_next <= ’1’ when ((r_reg < unsigned(w)) or (w=ref)) else
’0’;
pulse <= buf_reg;
end Behavioral;

Si noti l’introduzione della costante ref per rendere il controllo indipen-


dente dai bit di ingresso. Realizziamo un blocco implementante il pwm a 4
bit. Semplicemente:

library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_ARITH.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;

entity pwm4 is
Port ( clk : in STD_LOGIC;
reset : in STD_LOGIC;
w : in STD_LOGIC_VECTOR (3 downto 0);
pulse : out STD_LOGIC);

156
end pwm4;

architecture Behavioral of pwm4 is


COMPONENT pwm
Generic ( S : natural );
Port ( clk : in STD_LOGIC;
reset : in STD_LOGIC;
w : in STD_LOGIC_VECTOR (S-1 downto 0);
pulse : out STD_LOGIC);
END COMPONENT;
begin

uut: pwm
generic map ( S => 4 )
PORT MAP(
clk => clk,
reset => reset,
w => w,
pulse => pulse
);

end Behavioral;

Testiamolo con un testbench:

LIBRARY ieee;
USE ieee.std_logic_1164.ALL;
USE ieee.std_logic_unsigned.all;
USE ieee.numeric_std.ALL;

ENTITY testpwm4_vhd IS
END testpwm4_vhd;

ARCHITECTURE behavior OF testpwm4_vhd IS

COMPONENT pwm4
PORT(
clk : IN std_logic;
reset : IN std_logic;
w : IN std_logic_vector(3 downto 0);
pulse : OUT std_logic

157
);
END COMPONENT;

--Inputs
SIGNAL clk : std_logic := ’0’;
SIGNAL reset : std_logic := ’0’;
SIGNAL w : std_logic_vector(3 downto 0)
:= (others=>’0’);

--Outputs
SIGNAL pulse : std_logic;

constant T : time := 5 ns;

BEGIN

-- Instantiate the Unit Under Test (UUT)


uut: pwm4 PORT MAP(
clk => clk,
reset => reset,
w => w,
pulse => pulse
);

-- Test Bench Statements


clock : PROCESS
BEGIN
clk <= ’1’;
wait for T;
clk <= ’0’;
wait for T;
END PROCESS clock;

res: process begin


reset <= ’0’;
wait for 5*T;
reset <= ’1’;
wait;
end process;

input: process begin

158
w <= X"4";
wait for 500 ns;
w <= X"8";
wait;
end process;

END;

Figura 12.10: Simulazione comportamentale del PWM a 4 bit

I risultati di tale simulazione (clock a 100 MHz) sono mostrati in Fig.


12.10, e si sono dimostrati corretti. Passiamo alla sintesi. Le principali
caratteristiche temporali ci dicono che:

Timing Summary:
---------------

Minimum period: 3.413ns (Maximum Frequency: 292.972MHz)

159
Minimum input arrival time before clock: 5.195ns
Maximum output required time after clock: 6.216ns
Maximum combinational path delay: No path found

Vediamo subito che, probabilmente, a causa dei ritardi di propagazione


sarà difficile raggiungere frequenze superiori a circa 100 MHz. Vediamo
l’occupazione di porte logiche:

Device utilization summary:


---------------------------

Selected Device : 3s200ft256-5

Number of Slices: 5 out of 1920 0%


Number of Slice Flip Flops: 5 out of 3840 0%
Number of 4 input LUTs: 9 out of 3840 0%
Number of IOBs: 7 out of 173 4%
Number of GCLKs: 1 out of 8 12%

Non ci rimane che simulare il comportamento post place–and–route. Lan-


ciamo la simulazione. Come mostrato in Fig. 12.11, tutto ok. Attraverso
prove iterative, a dispetto dei pessimistici dati (worst case) del sintetizzatore,
si può mostrare come il clock massimo raggiungibile (dato dalla simulazione
post–par) è pari circa a 200 MHz.
Mostriamo, infine, in Fig. 12.12 il piazzamento del nostro dispositivo sul-
la Spartan–3: tutto sommato, i dati pessimistici del routing fornito dal sin-
tetizzatore non si sono fatti eccessivamente sentire grazie ad un piazzamento
tutto sommato fortunato, in quanto non lontanissimo dai pin di I/O.

160
Figura 12.11: Simulazione post place–and–route del PWM a 4 bit

161
Figura 12.12: Piazzamento su Spartan–3 del PWM progettato

162