vhdl Tutorial
Iniziare con vhdl
Ricerca…
Osservazioni
VHDL è un acronimo composto per VHSIC (Very Integrated Speed Integrated Circuit) HDL (Hardware Description Language). Come linguaggio di descrizione dell'hardware, viene utilizzato principalmente per descrivere o modellare i circuiti. VHDL è un linguaggio ideale per descrivere i circuiti poiché offre costrutti linguistici che descrivono facilmente sia il comportamento simultaneo che sequenziale insieme a un modello di esecuzione che rimuove l'ambiguità introdotta durante la modellazione del comportamento concorrente.
Il VHDL viene tipicamente interpretato in due diversi contesti: per la simulazione e per la sintesi. Se interpretato per la sintesi, il codice viene convertito (sintetizzato) in elementi hardware equivalenti che vengono modellati. Solamente un sottoinsieme del VHDL è tipicamente disponibile per l'uso durante la sintesi e i costrutti linguistici supportati non sono standardizzati; è una funzione del motore di sintesi utilizzato e del dispositivo hardware di destinazione. Quando VHDL viene interpretato per la simulazione, sono disponibili tutti i costrutti di linguaggio per la modellazione del comportamento dell'hardware.
Versioni
Versione | Data di rilascio |
---|---|
IEEE 1076-1987 | 1988/03/31 |
IEEE 1076-1993 | 1994/06/06 |
IEEE 1076-2000 | 2000/01/30 |
IEEE 1076-2002 | 2002/05/17 |
IEEE 1076c-2007 | 2007-09-05 |
IEEE 1076-2008 | 2009-01-26 |
Installazione o configurazione
Un programma VHDL può essere simulato o sintetizzato. La simulazione è ciò che assomiglia di più all'esecuzione in altri linguaggi di programmazione. Synthesis traduce un programma VHDL in una rete di porte logiche. Molti strumenti di simulazione e sintesi VHDL fanno parte delle suite commerciali di Electronic Design Automation (EDA). Gestiscono spesso anche altre lingue di descrizione dell'hardware (HDL), come Verilog, SystemVerilog o SystemC. Esistono alcune applicazioni gratuite e open source.
Simulazione VHDL
GHDL è probabilmente il simulatore VHDL gratuito più aperto e open source. È disponibile in tre gusti diversi a seconda del back-end utilizzato: gcc
, llvm
o mcode
. I seguenti esempi mostrano come usare GHDL (versione mcode
) e Modelsim, il simulatore HDL commerciale di Mentor Graphics, sotto un sistema operativo GNU / Linux. Le cose sarebbero molto simili con altri strumenti e altri sistemi operativi.
Ciao mondo
Crea un file hello_world.vhd
contenente:
-- File hello_world.vhd
entity hello_world is
end entity hello_world;
architecture arc of hello_world is
begin
assert false report "Hello world!" severity note;
end architecture arc;
Un'unità di compilazione VHDL è un programma VHDL completo che può essere compilato da solo. Le entità sono unità di compilazione VHDL utilizzate per descrivere l'interfaccia esterna di un circuito digitale, ovvero le sue porte di input e output. Nel nostro esempio, l' entity
è denominata hello_world
ed è vuota. Il circuito che modelliamo è una scatola nera, non ha ingressi e non ha uscite. Le architetture sono un altro tipo di unità di compilazione. Sono sempre associati a entity
e vengono utilizzati per descrivere il comportamento del circuito digitale. Un'entità può avere una o più architetture per descrivere il comportamento dell'entità. Nel nostro esempio l' entità è associata a una sola architettura di nome arc
che contiene solo un'istruzione VHDL:
assert false report "Hello world!" severity note;
La dichiarazione verrà eseguita all'inizio della simulazione e stamperà Hello world!
messaggio sullo standard output. La simulazione finirà perché non c'è più nulla da fare. Il file sorgente VHDL che abbiamo scritto contiene due unità di compilazione. Potremmo averli separati in due file diversi ma non abbiamo potuto dividerli in file diversi: un'unità di compilazione deve essere interamente contenuta in un file sorgente. Si noti che questa architettura non può essere sintetizzata perché non descrive una funzione che può essere tradotta direttamente in porte logiche.
Analizza ed esegui il programma con GHDL:
$ mkdir gh_work
$ ghdl -a --workdir=gh_work hello_world.vhd
$ ghdl -r --workdir=gh_work hello_world
hello_world.vhd:6:8:@0ms:(assertion note): Hello world!
La directory gh_work
è dove GHDL memorizza i file che genera. Questo è ciò che dice l'opzione --workdir=gh_work
. La fase di analisi controlla la correttezza della sintassi e produce un file di testo che descrive le unità di compilazione trovate nel file sorgente. La fase di esecuzione in realtà compila, collega ed esegue il programma. Si noti che, nel mcode
versione di GHDL, nessun file binari vengono generati. Il programma viene ricompilato ogni volta che lo simuliamo. Le versioni di gcc
o llvm
comportano diversamente. Si noti inoltre che ghdl -r
non ghdl -r
il nome di un file sorgente VHDL, come ghdl -a
, ma il nome di un'unità di compilazione. Nel nostro caso, passiamo il nome entity
. Poiché ha una sola architecture
associata, non è necessario specificare quale deve essere simulato.
Con Modelsim:
$ vlib ms_work
$ vmap work ms_work
$ vcom hello_world.vhd
$ vsim -c hello_world -do 'run -all; quit'
...
# ** Note: Hello world!
# Time: 0 ns Iteration: 0 Instance: /hello_world
...
vlib
, vmap
, vcom
e vsim
sono quattro comandi che Modelsim fornisce. vlib
crea una directory ( ms_work
) in cui verranno archiviati i file generati. vmap
associa una directory creata da vlib
con un nome logico ( work
). vcom
compila un file sorgente VHDL e, per impostazione predefinita, memorizza il risultato nella directory associata al nome logico del work
. Infine, vsim
simula il programma e produce lo stesso tipo di output di GHDL. Nota ancora che ciò che vsim
chiede non è un file sorgente ma il nome di una unità di compilazione già compilata. L'opzione -c
dice al simulatore di funzionare in modalità riga di comando invece della modalità GUI (Graphical User Interface) predefinita. L'opzione -do
viene utilizzata per passare uno script TCL da eseguire dopo aver caricato il progetto. TCL è un linguaggio di scripting utilizzato molto frequentemente negli strumenti EDA. Il valore dell'opzione -do
può essere il nome di un file o, come nel nostro esempio, una stringa di comandi TCL. run -all; quit
istruire il simulatore per eseguire la simulazione fino a quando non termina naturalmente - o per sempre se dura per sempre - e poi per uscire.
Contatore sincrono
-- File counter.vhd
-- The entity is the interface part. It has a name and a set of input / output
-- ports. Ports have a name, a direction and a type. The bit type has only two
-- values: '0' and '1'. It is one of the standard types.
entity counter is
port(
clock: in bit; -- We are using the rising edge of CLOCK
reset: in bit; -- Synchronous and active HIGH
data: out natural -- The current value of the counter
);
end entity counter;
-- The architecture describes the internals. It is always associated
-- to an entity.
architecture sync of counter is
-- The internal signals we use to count. Natural is another standard
-- type. VHDL is not case sensitive.
signal current_value: natural;
signal NEXT_VALUE: natural;
begin
-- A process is a concurrent statement. It is an infinite loop.
process
begin
-- The wait statement is a synchronization instruction. We wait
-- until clock changes and its new value is '1' (a rising edge).
wait until clock = '1';
-- Our reset is synchronous: we consider it only at rising edges
-- of our clock.
if reset = '1' then
-- <= is the signal assignment operator.
current_value <= 0;
else
current_value <= next_value;
end if;
end process;
-- Another process. The sensitivity list is another way to express
-- synchronization constraints. It (approximately) means: wait until
-- one of the signals in the list changes and then execute the process
-- body. Sensitivity list and wait statements cannot be used together
-- in the same process.
process(current_value)
begin
next_value <= current_value + 1;
end process;
-- A concurrent signal assignment, which is just a shorthand for the
-- (trivial) equivalent process.
data <= current_value;
end architecture sync;
Ciao mondo
Esistono molti modi per stampare il classico "Hello world!" messaggio in VHDL. Il più semplice di tutti è probabilmente qualcosa come:
-- File hello_world.vhd
entity hello_world is
end entity hello_world;
architecture arc of hello_world is
begin
assert false report "Hello world!" severity note;
end architecture arc;
Un ambiente di simulazione per il contatore sincrono
Ambienti di simulazione
Un ambiente di simulazione per un progetto VHDL (Design Under Test o DUT) è un altro design VHDL che, come minimo:
- Dichiara i segnali corrispondenti alle porte di ingresso e uscita del DUT.
- Crea un'istanza del DUT e collega le sue porte ai segnali dichiarati.
- Crea un'istanza dei processi che guidano i segnali collegati alle porte di ingresso del DUT.
Opzionalmente, un ambiente di simulazione può istanziare altri progetti rispetto al DUT, come, ad esempio, generatori di traffico su interfacce, monitor per controllare i protocolli di comunicazione, verificatori automatici delle uscite DUT ...
L'ambiente di simulazione viene analizzato, elaborato ed eseguito. La maggior parte dei simulatori offre la possibilità di selezionare una serie di segnali per osservare, tracciare le loro forme d'onda grafiche, inserire punti di interruzione nel codice sorgente, inserire il codice sorgente ...
Idealmente, un ambiente di simulazione dovrebbe essere utilizzabile come un robusto test di non regressione, cioè dovrebbe rilevare automaticamente le violazioni delle specifiche DUT, riportare utili messaggi di errore e garantire una copertura ragionevole delle funzionalità di DUT. Quando tali ambienti di simulazione sono disponibili, possono essere rieseguiti ad ogni cambio del DUT per verificare che sia ancora funzionalmente corretto, senza la necessità di ispezioni visive noiose e soggette a errori delle tracce di simulazione.
In pratica, la progettazione di ambienti di simulazione ideali o anche solo buoni è impegnativa. È spesso come, o anche più, difficile rispetto alla progettazione del DUT stesso.
In questo esempio presentiamo un ambiente di simulazione per l'esempio di contatore sincrono . Mostriamo come eseguirlo usando GHDL e Modelsim e come osservare le forme d'onda grafiche usando GTKWave con GHDL e il visualizzatore di forme d'onda incorporato con Modelsim. Discutiamo quindi un aspetto interessante delle simulazioni: come fermarle?
Un primo ambiente di simulazione per il contatore sincrono
Il contatore sincrono ha due porte di input e una di output. Un ambiente di simulazione molto semplice potrebbe essere:
-- File counter_sim.vhd
-- Entities of simulation environments are frequently black boxes without
-- ports.
entity counter_sim is
end entity counter_sim;
architecture sim of counter_sim is
-- One signal per port of the DUT. Signals can have the same name as
-- the corresponding port but they do not need to.
signal clk: bit;
signal rst: bit;
signal data: natural;
begin
-- Instantiation of the DUT
u0: entity work.counter(sync)
port map(
clock => clk,
reset => rst,
data => data
);
-- A clock generating process with a 2ns clock period. The process
-- being an infinite loop, the clock will never stop toggling.
process
begin
clk <= '0';
wait for 1 ns;
clk <= '1';
wait for 1 ns;
end process;
-- The process that handles the reset: active from beginning of
-- simulation until the 5th rising edge of the clock.
process
begin
rst <= '1';
for i in 1 to 5 loop
wait until rising_edge(clk);
end loop;
rst <= '0';
wait; -- Eternal wait. Stops the process forever.
end process;
end architecture sim;
Simulazione con GHDL
Cerchiamo di compilare e simulare questo con GHDL:
$ mkdir gh_work
$ ghdl -a --workdir=gh_work counter_sim.vhd
counter_sim.vhd:27:24: unit "counter" not found in 'library "work"'
counter_sim.vhd:50:35: no declaration for "rising_edge"
Quindi i messaggi di errore ci dicono due cose importanti:
- L'analizzatore GHDL ha scoperto che il nostro design crea un'entità chiamata
counter
ma questa entità non è stata trovata nelwork
libreria. Questo perché non abbiamo compilato ilcounter
prima dicounter_sim
. Durante la compilazione di VHDL progetta che istanziano le entità, i livelli inferiori devono sempre essere compilati prima dei livelli principali (i progetti gerarchici possono anche essere compilati dall'alto verso il basso ma solo se creano un'istanza dicomponent
, non entità). - La funzione
rising_edge
utilizzata dal nostro design non è definita. Ciò è dovuto al fatto che questa funzione è stata introdotta in VHDL 2008 e non abbiamo detto a GHDL di utilizzare questa versione del linguaggio (per impostazione predefinita utilizza VHDL 1993 con tolleranza della sintassi VHDL 1987).
Risolviamo i due errori e lanciamo la simulazione:
$ ghdl -a --workdir=gh_work --std=08 counter.vhd counter_sim.vhd
$ ghdl -r --workdir=gh_work --std=08 counter_sim sim
^C
Si noti che l'opzione --std=08
è necessaria per l'analisi e la simulazione. Nota anche che abbiamo lanciato la simulazione su counter_sim
entità, sim
architettura, non su un file di origine.
Poiché il nostro ambiente di simulazione ha un processo senza fine (il processo che genera l'orologio), la simulazione non si ferma e dobbiamo interromperla manualmente. Invece, possiamo specificare un tempo di arresto con l'opzione --stop-time
:
$ ghdl -r --workdir=gh_work --std=08 counter_sim sim --stop-time=60ns
ghdl:info: simulation stopped by --stop-time
Come è, la simulazione non ci dice molto sul comportamento del nostro DUT. Scarichiamo le variazioni di valore dei segnali in un file:
$ ghdl -r --workdir=gh_work --std=08 counter_sim sim --stop-time=60ns --vcd=counter_sim.vcd
Vcd.Avhpi_Error!
ghdl:info: simulation stopped by --stop-time
(Ignora il messaggio di errore, questo è qualcosa che deve essere risolto in GHDL e che non ha conseguenze). È stato creato un file counter_sim.vcd
. Contiene in formato VCD (ASCII) tutte le modifiche di segnale durante la simulazione. GTKWave può mostrarci le forme d'onda grafiche corrispondenti:
$ gtkwave counter_sim.vcd
dove possiamo vedere che il contatore funziona come previsto.
Simulazione con Modelsim
Il principio è esattamente lo stesso con Modelsim:
$ vlib ms_work
...
$ vmap work ms_work
...
$ vcom -nologo -quiet -2008 counter.vhd counter_sim.vhd
$ vsim -voptargs="+acc" 'counter_sim(sim)' -do 'add wave /*; run 60ns'
Nota l' -voptargs="+acc"
passata a vsim
: impedisce al simulatore di ottimizzare il segnale data
e ci permette di vederlo sulle forme d'onda.
Simulazioni che terminano con grazia
Con entrambi i simulatori abbiamo dovuto interrompere la simulazione senza fine o specificare un tempo di arresto con un'opzione dedicata. Questo non è molto conveniente. In molti casi è difficile anticipare l'ora di fine di una simulazione. Sarebbe molto meglio interrompere la simulazione dall'interno del codice VHDL dell'ambiente di simulazione, quando viene raggiunta una particolare condizione, come, per esempio, quando il valore corrente del contatore raggiunge 20. Ciò può essere ottenuto con un'asserzione nel processo che gestisce il reset:
process
begin
rst <= '1';
for i in 1 to 5 loop
wait until rising_edge(clk);
end loop;
rst <= '0';
loop
wait until rising_edge(clk);
assert data /= 20 report "End of simulation" severity failure;
end loop;
end process;
Finché i data
sono diversi da 20, la simulazione continua. Quando i data
raggiungono 20, la simulazione si blocca con un messaggio di errore:
$ ghdl -a --workdir=gh_work --std=08 counter_sim.vhd
$ ghdl -r --workdir=gh_work --std=08 counter_sim sim
counter_sim.vhd:90:24:@51ns:(assertion failure): End of simulation
ghdl:error: assertion failed
from: process work.counter_sim(sim2).P1 at counter_sim.vhd:90
ghdl:error: simulation failed
Nota che abbiamo ri-compilato solo l'ambiente di simulazione: è l'unico progetto che è cambiato ed è il livello più alto. Se avessimo modificato solo counter.vhd
, avremmo dovuto ricompilare entrambi: counter.vhd
perché è cambiato e counter_sim.vhd
perché dipende da counter.vhd
.
Arrestare la simulazione con un messaggio di errore non è molto elegante. Può anche essere un problema quando si analizzano automaticamente i messaggi di simulazione per decidere se un test di non regressione automatico è passato o meno. Una soluzione migliore e più elegante è arrestare tutti i processi quando viene raggiunta una condizione. Ciò può essere fatto, ad esempio, aggiungendo un segnale boolean
End of Simulation ( eof
). Per impostazione predefinita, viene inizializzato su false
all'inizio della simulazione. Uno dei nostri processi lo imposterà su true
quando sarà giunto il momento di terminare la simulazione. Tutti gli altri processi monitoreranno questo segnale e si fermeranno con wait
eterna quando diventerà true
:
signal eos: boolean;
...
process
begin
clk <= '0';
wait for 1 ns;
clk <= '1';
wait for 1 ns;
if eos then
report "End of simulation";
wait;
end if;
end process;
process
begin
rst <= '1';
for i in 1 to 5 loop
wait until rising_edge(clk);
end loop;
rst <= '0';
for i in 1 to 20 loop
wait until rising_edge(clk);
end loop;
eos <= true;
wait;
end process;
$ ghdl -a --workdir=gh_work --std=08 counter_sim.vhd
$ ghdl -r --workdir=gh_work --std=08 counter_sim sim
counter_sim.vhd:120:24:@50ns:(report note): End of simulation
Ultimo ma non meno importante, c'è una soluzione ancora migliore introdotta in VHDL 2008 con il pacchetto standard env
e le procedure stop
and finish
che dichiara:
use std.env.all;
...
process
begin
rst <= '1';
for i in 1 to 5 loop
wait until rising_edge(clk);
end loop;
rst <= '0';
for i in 1 to 20 loop
wait until rising_edge(clk);
end loop;
finish;
end process;
$ ghdl -a --workdir=gh_work --std=08 counter_sim.vhd
$ ghdl -r --workdir=gh_work --std=08 counter_sim sim
simulation finished @49ns
Segnali contro variabili, una breve panoramica della semantica della simulazione di VHDL
Questo esempio tratta uno degli aspetti più fondamentali del linguaggio VHDL: la semantica della simulazione. È destinato ai principianti VHDL e presenta una vista semplificata in cui sono stati omessi molti dettagli (processi posticipati, interfaccia procedurale VHDL, variabili condivise ...) I lettori interessati alla semantica reale reale devono fare riferimento al manuale di riferimento linguistico (LRM).
Segnali e variabili
La maggior parte dei linguaggi di programmazione imperativi classici usano variabili. Sono contenitori di valore. Un operatore di assegnazione viene utilizzato per memorizzare un valore in una variabile:
a = 15;
e il valore attualmente memorizzato in una variabile può essere letto e utilizzato in altre dichiarazioni:
if(a == 15) { print "Fifteen" }
VHDL utilizza anche variabili e hanno esattamente lo stesso ruolo che nella maggior parte delle lingue imperative. Ma VHDL offre anche un altro tipo di contenitore di valori: il segnale. I segnali memorizzano anche i valori, possono anche essere assegnati e letti. Il tipo di valori che possono essere memorizzati nei segnali è (quasi) uguale a quello delle variabili.
Quindi, perché avere due tipi di contenitori di valore? La risposta a questa domanda è essenziale e al centro della lingua. Comprendere la differenza tra variabili e segnali è la prima cosa da fare prima di provare a programmare qualcosa in VHDL.
Cerchiamo di illustrare questa differenza su un esempio concreto: lo scambio.
Nota: tutti i seguenti frammenti di codice sono parti di processi. Vedremo più avanti quali sono i processi.
tmp := a;
a := b;
b := tmp;
scambia le variabili a
e b
. Dopo aver eseguito queste 3 istruzioni, il nuovo contenuto di a
è il vecchio contenuto di b
e viceversa. Come nella maggior parte dei linguaggi di programmazione, è necessaria una terza variabile temporanea ( tmp
). Se invece di variabili volessimo scambiare i segnali, scriveremmo:
r <= s;
s <= r;
o:
s <= r;
r <= s;
con lo stesso risultato e senza la necessità di un terzo segnale temporaneo!
Nota: l'operatore di assegnazione del segnale VHDL
<=
è diverso dall'operatore di assegnazione della variabile:=
.
Vediamo un secondo esempio in cui si assume che il sottoprogramma di print
stampi la rappresentazione decimale del suo parametro. Se a
è una variabile intera e il suo valore corrente è 15, eseguendo:
a := 2 * a;
a := a - 5;
a := a / 5;
print(a);
stamperà:
5
Se eseguiamo questo passo dopo passo in un debugger, possiamo vedere il valore di a
cambiamento dai 15 ai 30, 25 e infine 5 iniziali.
Ma se s
è un segnale intero e il suo valore attuale è 15, eseguendo:
s <= 2 * s;
s <= s - 5;
s <= s / 5;
print(s);
wait on s;
print(s);
stamperà:
15
3
Se eseguiamo questo passo dopo passo in un debugger non vedremo alcun cambiamento di valore di s
fino a dopo l' wait
. Inoltre, il valore finale di s
non sarà 15, 30, 25 o 5 ma 3!
Questo comportamento apparentemente strano è dovuto alla natura fondamentalmente parallela dell'hardware digitale, come vedremo nelle sezioni seguenti.
Parallelismo
VHDL essendo un linguaggio di descrizione dell'hardware (HDL), è parallelo per natura. Un programma VHDL è una raccolta di programmi sequenziali eseguiti in parallelo. Questi programmi sequenziali sono chiamati processi:
P1: process
begin
instruction1;
instruction2;
...
instructionN;
end process P1;
P2: process
begin
...
end process P2;
I processi, proprio come l'hardware che stanno modellando, non finiscono mai: sono infiniti cicli. Dopo aver eseguito l'ultima istruzione, l'esecuzione continua con la prima.
Come con qualsiasi linguaggio di programmazione che supporta una forma o l'altra di parallelismo, uno schedulatore è responsabile per decidere quale processo eseguire (e quando) durante una simulazione VHDL. Inoltre, il linguaggio offre costrutti specifici per la comunicazione e la sincronizzazione tra processi.
programmazione
Lo scheduler mantiene un elenco di tutti i processi e, per ciascuno di essi, registra il suo stato corrente che può essere in running
, run-able
o suspended
. C'è al massimo un processo in running
: quello attualmente eseguito. Finché il processo attualmente in esecuzione non esegue un'istruzione di wait
, continua a funzionare e impedisce l'esecuzione di qualsiasi altro processo. Lo scheduler VHDL non è preventivo: è responsabilità di ogni processo sospendere se stesso e lasciare che altri processi vengano eseguiti. Questo è uno dei problemi che i principianti VHDL incontrano spesso: il processo di corsa libera.
P3: process
variable a: integer;
begin
a := s;
a := 2 * a;
r <= a;
end process P3;
Nota: la variabile
a
è dichiarata localmente mentre i segnalis
er
sono dichiarati altrove, ad un livello superiore. Le variabili VHDL sono locali al processo che le dichiara e non possono essere viste da altri processi. Un altro processo potrebbe anche dichiarare una variabile denominataa
, non sarebbe la stessa variabile di quella del processoP3
.
Non appena lo scheduler riprende il processo P3
, la simulazione si bloccherà, il tempo corrente della simulazione non progredirà più e l'unico modo per fermarlo sarà uccidere o interrompere la simulazione. Il motivo è che P3
non ha un'istruzione di wait
e resterà quindi in running
per sempre, running
loop delle sue 3 istruzioni. Nessun altro processo avrà mai la possibilità di essere eseguito, anche se è run-able
.
Anche i processi che contengono un'istruzione wait
possono causare lo stesso problema:
P4: process
variable a: integer;
begin
a := s;
a := 2 * a;
if a = 16 then
wait on s;
end if;
r <= a;
end process P4;
Nota: l'operatore di uguaglianza VHDL è
=
.
Se il processo P4
viene ripreso mentre il valore del segnale s
è 3, verrà eseguito per sempre perché la condizione a = 16
non sarà mai vera.
Supponiamo che il nostro programma VHDL non contenga tali processi patologici. Quando il processo in esecuzione esegue un'istruzione di wait
, viene immediatamente sospeso e lo scheduler lo inserisce nello stato suspended
. L'istruzione di wait
porta anche la condizione affinchè il processo diventi nuovamente run-able
. Esempio:
wait on s;
significa sospendermi finché il valore del segnale s
cambia . Questa condizione è registrata dallo scheduler. Lo scheduler seleziona quindi un altro processo tra i run-able
, lo mette in running
e lo esegue. E gli stessi si ripete finché tutti i run-able
processi sono stati eseguiti e sospeso.
Nota importante: quando diversi processi sono
run-able
, lo standard VHDL non specifica in che modo lo schedulatore deve selezionare quale eseguire. Una conseguenza è che, a seconda del simulatore, della versione del simulatore, del sistema operativo o di qualsiasi altra cosa, due simulazioni dello stesso modello VHDL potrebbero, a un certo punto, effettuare scelte diverse e selezionare un processo diverso da eseguire. Se questa scelta avesse un impatto sui risultati della simulazione, potremmo dire che VHDL non è deterministico. Poiché il non-determinismo di solito è indesiderabile, sarebbe responsabilità dei programmatori evitare situazioni non deterministiche. Fortunatamente, VHDL si occupa di questo ed è qui che i segnali entrano nell'immagine.
Segnali e comunicazione tra processi
VHDL evita il non determinismo usando due caratteristiche specifiche:
- I processi possono scambiare informazioni solo attraverso i segnali
signal r, s: integer; -- Common to all processes
...
P5: process
variable a: integer; -- Different from variable a of process P6
begin
a := s + 1;
r <= a;
a := r + 1;
wait on s;
end process P5;
P6: process
variable a: integer; -- Different from variable a of process P5
begin
a := r + 1;
s <= a;
wait on r;
end process P6;
Nota: i commenti VHDL si estendono da
--
fino alla fine della riga.
- Il valore di un segnale VHDL non cambia durante l'esecuzione dei processi
Ogni volta che viene assegnato un segnale, il valore assegnato viene registrato dallo scheduler ma il valore corrente del segnale rimane invariato. Questa è un'altra grande differenza con le variabili che prendono il loro nuovo valore immediatamente dopo essere state assegnate.
Esaminiamo un'esecuzione del processo P5
sopra e assumiamo che a=5
, s=1
and r=0
quando viene ripreso dallo scheduler. Dopo aver eseguito l'istruzione a := s + 1;
, il valore della variabile a
cambia e diventa 2 (1 + 1). Quando si esegue l'istruzione successiva r <= a;
è il nuovo valore di a
(2) che è assegnato a r
. Ma r
essere un segnale, il valore corrente di r
è ancora 0. Quindi, durante l'esecuzione di a := r + 1;
, variabile a
prende (immediatamente) valore 1 (0 + 1), non 3 (2 + 1) come direbbe l'intuizione.
Quando il segnale r
prenderà davvero il suo nuovo valore? Quando lo schedulatore avrà eseguito tutti i processi eseguibili e saranno tutti sospesi. Questo è anche indicato come: dopo un ciclo delta . Solo allora lo scheduler controllerà tutti i valori assegnati ai segnali e aggiornerà effettivamente i valori dei segnali. Una simulazione VHDL è un'alternanza di fasi di esecuzione e fasi di aggiornamento del segnale. Durante le fasi di esecuzione, il valore dei segnali è congelato. Simbolicamente, diciamo che tra una fase di esecuzione e la successiva fase di aggiornamento del segnale è trascorso un delta di tempo. Questo non è tempo reale. Un ciclo delta non ha durata fisica.
Grazie a questo meccanismo di aggiornamento del segnale ritardato, il VHDL è deterministico. I processi possono comunicare solo con segnali e i segnali non cambiano durante l'esecuzione dei processi. Quindi, l'ordine di esecuzione dei processi non ha importanza: il loro ambiente esterno (i segnali) non cambia durante l'esecuzione. Mostriamolo nell'esempio precedente con i processi P5
e P6
, dove lo stato iniziale è P5.a=5
, P6.a=10
, s=17
, r=0
e dove lo schedulatore decide di eseguire prima P5
e P6
successivo . La seguente tabella mostra il valore delle due variabili, i valori attuali e successivi dei segnali dopo l'esecuzione di ciascuna istruzione di ciascun processo:
processo / istruzione | P5.a | P6.a | s.current | s.next | r.current | r.next |
---|---|---|---|---|---|---|
Stato iniziale | 5 | 10 | 17 | 0 | ||
P5 / a := s + 1 | 18 | 10 | 17 | 0 | ||
P5 / r <= a | 18 | 10 | 17 | 0 | 18 | |
P5 / a := r + 1 | 1 | 10 | 17 | 0 | 18 | |
P5 / wait on s | 1 | 10 | 17 | 0 | 18 | |
P6 / a := r + 1 | 1 | 1 | 17 | 0 | 18 | |
P6 / s <= a | 1 | 1 | 17 | 1 | 0 | 18 |
P6 / wait on r | 1 | 1 | 17 | 1 | 0 | 18 |
Dopo l'aggiornamento del segnale | 1 | 1 | 1 | 18 |
Con le stesse condizioni iniziali, se lo schedulatore decide di eseguire prima P6
e poi P5
:
processo / istruzione | P5.a | P6.a | s.current | s.next | r.current | r.next |
---|---|---|---|---|---|---|
Stato iniziale | 5 | 10 | 17 | 0 | ||
P6 / a := r + 1 | 5 | 1 | 17 | 0 | ||
P6 / s <= a | 5 | 1 | 17 | 1 | 0 | |
P6 / wait on r | 5 | 1 | 17 | 1 | 0 | |
P5 / a := s + 1 | 18 | 1 | 17 | 1 | 0 | |
P5 / r <= a | 18 | 1 | 17 | 1 | 0 | 18 |
P5 / a := r + 1 | 1 | 1 | 17 | 1 | 0 | 18 |
P5 / wait on s | 1 | 1 | 17 | 1 | 0 | 18 |
Dopo l'aggiornamento del segnale | 1 | 1 | 1 | 18 |
Come possiamo vedere, dopo l'esecuzione dei nostri due processi, il risultato è lo stesso indipendentemente dall'ordine di esecuzione.
Questa semantica di assegnazione del segnale contro-intuitiva è la ragione di un secondo tipo di problemi che i principianti del VHDL spesso incontrano: l'incarico che apparentemente non funziona perché è ritardato di un ciclo delta. P5
passo per passo il processo P5
in un debugger, dopo che a r
è stato assegnato 18 e a
è stato assegnato r + 1
, ci si potrebbe aspettare che il valore di a
sia 19 ma il debugger dice ostinatamente che r=0
e a=1
...
Nota: lo stesso segnale può essere assegnato più volte durante la stessa fase di esecuzione. In questo caso, è l'ultimo compito che decide il valore successivo del segnale. Gli altri compiti non hanno alcun effetto, proprio come se non fossero mai stati eseguiti.
È tempo di verificare la nostra comprensione: per favore, torna al nostro primo esempio di scambio e cerca di capire perché:
process
begin
---
s <= r;
r <= s;
---
end process;
scambia effettivamente i segnali r
e s
senza la necessità di un terzo segnale temporaneo e perché:
process
begin
---
r <= s;
s <= r;
---
end process;
sarebbe strettamente equivalente. Cerca di capire anche perché, se s
è un segnale intero e il suo valore attuale è 15, e eseguiamo:
process
begin
---
s <= 2 * s;
s <= s - 5;
s <= s / 5;
print(s);
wait on s;
print(s);
---
end process;
le prime due assegnazioni di segnali s
non hanno alcun effetto, perché s
viene infine assegnato 3 e perché i due valori stampati sono 15 e 3.
Tempo fisico
Per modellare l'hardware è molto utile poter modellare il tempo fisico impiegato da qualche operazione. Ecco un esempio di come questo può essere fatto in VHDL. L'esempio modella un contatore sincrono ed è un codice VHDL completo, autonomo che potrebbe essere compilato e simulato:
-- File counter.vhd
entity counter is
end entity counter;
architecture arc of counter is
signal clk: bit; -- Type bit has two values: '0' and '1'
signal c, nc: natural; -- Natural (non-negative) integers
begin
P1: process
begin
clk <= '0';
wait for 10 ns; -- Ten nano-seconds delay
clk <= '1';
wait for 10 ns; -- Ten nano-seconds delay
end process P1;
P2: process
begin
if clk = '1' and clk'event then
c <= nc;
end if;
wait on clk;
end process P2;
P3: process
begin
nc <= c + 1 after 5 ns; -- Five nano-seconds delay
wait on c;
end process P3;
end architecture arc;
Nel processo P1
l' wait
non viene utilizzata per attendere che il valore di un segnale cambi, come abbiamo visto fino ad ora, ma per attendere una determinata durata. Questo processo modella un generatore di clock. Signal clk
è l'orologio del nostro sistema, è periodico con periodo 20 ns (50 MHz) e ha duty cycle.
Process P2
modella un registro che, se si verifica un fronte di salita di clk
, assegna il valore del suo input nc
al suo output c
e quindi attende il successivo cambio di valore di clk
.
Process P3
modella un incrementatore che assegna il valore del suo input c
, incrementato di uno, al suo output nc
... con un ritardo fisico di 5 ns. Quindi attende finché il valore del suo input c
cambia. Anche questo è nuovo. Fino ad ora abbiamo sempre assegnato segnali con:
s <= value;
che, per le ragioni spiegate nelle sezioni precedenti, possiamo implicitamente tradurre in:
s <= value; -- after delta
Questo piccolo sistema hardware digitale potrebbe essere rappresentato dalla seguente figura:
Con l'introduzione del tempo fisico e sapendo che abbiamo anche un tempo simbolico misurato in delta , ora abbiamo un tempo bidimensionale che denoteremo T+D
dove T
è un tempo fisico misurato in nano-secondi e D
un numero di delta (senza durata fisica).
Il quadro completo
C'è un aspetto importante della simulazione VHDL che non abbiamo ancora discusso: dopo una fase di esecuzione tutti i processi sono in suspended
. Abbiamo dichiarato informalmente che lo scheduler aggiorna i valori dei segnali che sono stati assegnati. Ma, nel nostro esempio di contatore sincrono, aggiornerà i segnali clk
, c
e nc
allo stesso tempo? E i ritardi fisici? E cosa succede dopo con tutti i processi in suspended
e nessuno in stato di run-able
?
L'algoritmo di simulazione completo (ma semplificato) è il seguente:
- Inizializzazione
- Imposta l'ora corrente
Tc
su 0 + 0 (0 ns, 0 delta-cycle) - Inizializza tutti i segnali.
- Esegui ogni processo finché non si sospende su un'istruzione
wait
.- Registrare i valori e i ritardi delle assegnazioni del segnale.
- Registrare le condizioni per riprendere il processo (ritardo o cambio di segnale).
- Calcola la prossima volta
Tn
il più presto possibile:- Il tempo di ripresa dei processi sospeso da un
wait for <delay>
. - La prossima volta in cui un valore del segnale cambierà.
- Il tempo di ripresa dei processi sospeso da un
- Imposta l'ora corrente
- Ciclo di simulazione
-
Tc=Tn
. - Aggiorna i segnali che devono essere.
- Metti in stato di
run-able
tutti i processi che erano in attesa di una variazione di valore di uno dei segnali che è stato aggiornato. - Metti in stato di
run-able
tutti i processi sospesi inwait for <delay>
e per i quali il tempo di ripresa èTc
. - Esegui tutti i processi eseguibili fino alla loro sospensione.
- Registrare i valori e i ritardi delle assegnazioni del segnale.
- Registrare le condizioni per riprendere il processo (ritardo o cambio di segnale).
- Calcola la prossima volta
Tn
il più presto possibile:- Il tempo di ripresa dei processi sospeso da un
wait for <delay>
. - La prossima volta in cui un valore del segnale cambierà.
- Il tempo di ripresa dei processi sospeso da un
- Se
Tn
è infinito, ferma la simulazione. Altrimenti, avvia un nuovo ciclo di simulazione.
-
Simulazione manuale
Per concludere, facciamo ora esercitare manualmente l'algoritmo di simulazione semplificato sul contatore sincrono presentato sopra. Decidiamo arbitrariamente che, quando diversi processi sono eseguibili, l'ordine sarà P3
> P2
> P1
. Le seguenti tabelle rappresentano l'evoluzione dello stato del sistema durante l'inizializzazione e i primi cicli di simulazione. Ogni segnale ha la propria colonna in cui è indicato il valore corrente. Quando viene eseguita un'assegnazione del segnale, il valore programmato viene aggiunto al valore corrente, ad esempio a/b@T+D
se il valore corrente è a
e il valore successivo sarà b
al tempo T+D
(tempo fisico più cicli delta) . Le ultime 3 colonne indicano la condizione per riprendere i processi sospesi (nome dei segnali che devono cambiare o tempo in cui il processo deve riprendere).
Fase di inizializzazione:
operazioni | Tc | Tn | clk | c | nc | P1 | P2 | P3 |
---|---|---|---|---|---|---|---|---|
Imposta l'ora corrente | 0 + 0 | |||||||
Inizializza tutti i segnali | 0 + 0 | '0' | 0 | 0 | ||||
P3/nc<=c+1 after 5 ns | 0 + 0 | '0' | 0 | 0/1 @ 5 + 0 | ||||
P3/wait on c | 0 + 0 | '0' | 0 | 0/1 @ 5 + 0 | c | |||
P2/if clk='1'... | 0 + 0 | '0' | 0 | 0/1 @ 5 + 0 | c | |||
P2/end if | 0 + 0 | '0' | 0 | 0/1 @ 5 + 0 | c | |||
P2/wait on clk | 0 + 0 | '0' | 0 | 0/1 @ 5 + 0 | clk | c | ||
P1/clk<='0' | 0 + 0 | '0' / '0' @ 0 + 1 | 0 | 0/1 @ 5 + 0 | clk | c | ||
P1/wait for 10 ns | 0 + 0 | '0' / '0' @ 0 + 1 | 0 | 0/1 @ 5 + 0 | 10 + 0 | clk | c | |
Calcola la prossima volta | 0 + 0 | 0 + 1 | '0' / '0' @ 0 + 1 | 0 | 0/1 @ 5 + 0 | 10 + 0 | clk | c |
Ciclo di simulazione n. 1
operazioni | Tc | Tn | clk | c | nc | P1 | P2 | P3 |
---|---|---|---|---|---|---|---|---|
Imposta l'ora corrente | 0 + 1 | '0' / '0' @ 0 + 1 | 0 | 0/1 @ 5 + 0 | 10 + 0 | clk | c | |
Aggiorna i segnali | 0 + 1 | '0' | 0 | 0/1 @ 5 + 0 | 10 + 0 | clk | c | |
Calcola la prossima volta | 0 + 1 | 5 + 0 | '0' | 0 | 0/1 @ 5 + 0 | 10 + 0 | clk | c |
Nota: durante il primo ciclo di simulazione non c'è fase di esecuzione perché nessuno dei nostri 3 processi ha soddisfatto la sua condizione di ripresa.
P2
è in attesa di una modifica del valore diclk
e c'è stata una transazione suclk
, ma poiché i valori vecchi e nuovi sono uguali, non si tratta di un cambiamento di valore.
Ciclo di simulazione n. 2
operazioni | Tc | Tn | clk | c | nc | P1 | P2 | P3 |
---|---|---|---|---|---|---|---|---|
Imposta l'ora corrente | 5 + 0 | '0' | 0 | 0/1 @ 5 + 0 | 10 + 0 | clk | c | |
Aggiorna i segnali | 5 + 0 | '0' | 0 | 1 | 10 + 0 | clk | c | |
Calcola la prossima volta | 5 + 0 | 10 + 0 | '0' | 0 | 1 | 10 + 0 | clk | c |
Nota: di nuovo, non esiste una fase di esecuzione.
nc
cambiato ma nessun processo è in attesa sunc
.
Ciclo di simulazione n. 3
operazioni | Tc | Tn | clk | c | nc | P1 | P2 | P3 |
---|---|---|---|---|---|---|---|---|
Imposta l'ora corrente | 10 + 0 | '0' | 0 | 1 | 10 + 0 | clk | c | |
Aggiorna i segnali | 10 + 0 | '0' | 0 | 1 | 10 + 0 | clk | c | |
P1/clk<='1' | 10 + 0 | '0' / '1' @ 10 + 1 | 0 | 1 | clk | c | ||
P1/wait for 10 ns | 10 + 0 | '0' / '1' @ 10 + 1 | 0 | 1 | 20 + 0 | clk | c | |
Calcola la prossima volta | 10 + 0 | 10 + 1 | '0' / '1' @ 10 + 1 | 0 | 1 | 20 + 0 | clk | c |
Ciclo di simulazione n. 4
operazioni | Tc | Tn | clk | c | nc | P1 | P2 | P3 |
---|---|---|---|---|---|---|---|---|
Imposta l'ora corrente | 10 + 1 | '0' / '1' @ 10 + 1 | 0 | 1 | 20 + 0 | clk | c | |
Aggiorna i segnali | 10 + 1 | '1' | 0 | 1 | 20 + 0 | clk | c | |
P2/if clk='1'... | 10 + 1 | '1' | 0 | 1 | 20 + 0 | c | ||
P2/c<=nc | 10 + 1 | '1' | 0/1 + 2 @ 10 | 1 | 20 + 0 | c | ||
P2/end if | 10 + 1 | '1' | 0/1 + 2 @ 10 | 1 | 20 + 0 | c | ||
P2/wait on clk | 10 + 1 | '1' | 0/1 + 2 @ 10 | 1 | 20 + 0 | clk | c | |
Calcola la prossima volta | 10 + 1 | 10 + 2 | '1' | 0/1 + 2 @ 10 | 1 | 20 + 0 | clk | c |
Ciclo di simulazione n. 5
operazioni | Tc | Tn | clk | c | nc | P1 | P2 | P3 |
---|---|---|---|---|---|---|---|---|
Imposta l'ora corrente | 10 + 2 | '1' | 0/1 + 2 @ 10 | 1 | 20 + 0 | clk | c | |
Aggiorna i segnali | 10 + 2 | '1' | 1 | 1 | 20 + 0 | clk | c | |
P3/nc<=c+1 after 5 ns | 10 + 2 | '1' | 1 | 1/2 @ 15 + 0 | 20 + 0 | clk | ||
P3/wait on c | 10 + 2 | '1' | 1 | 1/2 @ 15 + 0 | 20 + 0 | clk | c | |
Calcola la prossima volta | 10 + 2 | 15 + 0 | '1' | 1 | 1/2 @ 15 + 0 | 20 + 0 | clk | c |
Nota: si potrebbe pensare che l'aggiornamento
nc
sia programmato a15+2
, mentre è programmato su15+0
. Quando si aggiunge un ritardo fisico diverso da zero (qui5 ns
) a un'ora corrente (10+2
), i cicli delta scompaiono. Infatti, i cicli delta sono utili solo per distinguere i diversi tempi di simulazioneT+0
,T+1
... con lo stesso tempo fisicoT
Non appena l'ora fisica cambia, i cicli delta possono essere ripristinati.
Ciclo di simulazione n. 6
operazioni | Tc | Tn | clk | c | nc | P1 | P2 | P3 |
---|---|---|---|---|---|---|---|---|
Imposta l'ora corrente | 15 + 0 | '1' | 1 | 1/2 @ 15 + 0 | 20 + 0 | clk | c | |
Aggiorna i segnali | 15 + 0 | '1' | 1 | 2 | 20 + 0 | clk | c | |
Calcola la prossima volta | 15 + 0 | 20 + 0 | '1' | 1 | 2 | 20 + 0 | clk | c |
Nota: di nuovo, non esiste una fase di esecuzione.
nc
cambiato ma nessun processo è in attesa sunc
.