Un simulateur est un logiciel capable d’exécuter une description VHDL pour reproduire sur ordinateur le comportement du circuit en développement. Un simulateur permet de visualiser sous forme de chronogrammes les signaux internes de la description, qui sont souvent inaccessibles dans le circuit final. Cela permet de détecter et de corriger des dysfonctionnements avant de lancer la synthèse, voire la fabrication, du circuit.
Pour tester le fonctionnement d’une entité, une pratique fréquente consiste à écrire une autre entité banc de test (ou en anglais test bench) qui modélisera l’environnement. Généralement, un banc de test n’a pas de ports d’entrée/sortie.
Dans l’architecture de test, on trouvera typiquement :
Comme le banc de test ne représentent pas un circuit que l’on cherche à réaliser, son architecture pourra utiliser des types de données et des instructions non synthétisables, dont le rôle est de décrire des scénarios de fonctionnement et de fournir des indications utiles au développeur.
Voici un exemple de banc de test pour l’entité CounterModN
:
entity CounterTestbench is
-- Pas de port.
end CounterTestbench;
library ieee;
use ieee.std_logic_1164.all;
architecture Simulation of CounterTestbench is
-- Déclaration des signaux de test.
signal clk : std_logic := '0';
signal reset : std_logic := '1';
signal inc : std_logic := '0';
signal value : integer range 0 to 9;
signal cycle : std_logic;
begin
-- Une instance de l'entité à tester.
counter_inst : entity work.CounterModN(Behavioral)
generic map(
N => 10
)
port map(
clk_i => clk,
reset_i => reset,
inc_i => inc,
value_o => value,
cycle_o => cycle
);
-- Génération d'une horloge de période 10 ns.
clk <= not clk after 5 ns;
-- Désactiver le signal reset au bout de 10 ns.
reset <= '0' after 10 ns;
-- Commencer à incrémenter le compteur à t=30 ns,
-- terminer à t=230 ns.
inc <= '1' after 30 ns, '0' after 230 ns;
-- Vérifier les valeurs de value et cycle.
process
variable value_prev : integer range 0 to 9;
begin
-- Vérifier la valeur du compteur après la désactivation du reset.
wait until falling_edge(reset);
assert value = 0
report "value devrait être à 0"
severity ERROR;
loop
-- Mémoriser la valeur courante du compteur et attendre
-- un front montant de l'horloge.
value_prev := value;
wait until rising_edge(clk);
-- value doit se mettre à jour de façon synchrone.
-- Attendre un peu après le front pour que les valeurs
-- des signaux soient stabilisées.
wait for 1 ns;
-- Vérifier la nouvelle valeur du compteur.
if inc = '0' then
assert value = value_prev
report "value devrait être stable"
severity ERROR;
elsif value_prev = 9 then
assert value = 0
report "value aurait dû revenir à 0"
severity ERROR;
else
assert value = value_prev + 1
report "value aurait dû s'incrémenter"
severity ERROR;
end if;
-- Vérifier la valeur de la sortie cycle.
assert (value = 9 and cycle = '1') or (value /= 9 and cycle = '0')
report "cycle n'a pas l'état souhaité"
severity ERROR;
end loop;
end process;
end Simulation;
time
et la fonction now
VHDL permet la déclaration de types physiques, c’est-à-dire des types de données
capables de représenter des grandeurs physique sous la forme d’un nombre et d’une
unité de mesure.
En pratique, on utilise le plus souvent le type prédéfini time
dont voici la
déclaration :
type time is range -9223372036854775808 fs to 9223372036854775807 fs
units
fs;
ps = 1000 fs;
ns = 1000 ps;
us = 1000 ns;
ms = 1000 us;
sec = 1000 ms;
min = 60 sec;
hr = 60 min;
end units;
Ce type permet de représenter des valeurs de temps comprises entre -9223 et 9223 secondes environ, avec une précision d’une femtoseconde (10-15 s). Voici quelques exemples de déclarations de constantes utilisant ce type :
constant CLK_PERIOD : time := 10 ns;
constant LATENCY : time := CLK_PERIOD * 16;
constant RETRY_DELAY : time := 3 sec;
constant TOTAL_DELAY : time := RETRY_DELAY + 200 ms;
Comme le montrent les déclarations des constantes LATENCY
et TOTAL_DELAY
,
on peut appliquer des opérations arithmétiques courante sur les types physiques
à condition de respecter certaines règles : on ne peut les multiplier ou les diviser
que par des nombres sans unité ; on ne peut les additionner ou les soustraire
que si elles sont de même type.
VHDL définit également la fonction now
, qui retourne le temps écoulé depuis le
début de la simulation.
On peut l’utiliser dans un processus pour calculer des durées entre des
événements.
Le type time
et la fonction now
sont déclarées dans le paquetage standard
de VHDL.
Vous n’avez pas besoin de les déclarer vous-mêmes ni d’importer ce paquetage
pour les utiliser.
Dans une valeur littérale avec unité, il faut toujours séparer le nombre et son unité par un espace :
constant CLK_PERIOD : time := 10 ns; -- Correct
constant CLK_PERIOD : time := 10ns; -- Incorrect
Voici trois exemples utilisés dans le banc de test du compteur :
architecture Simulation of CounterTestbench is
signal clk : std_logic := '0';
signal reset : std_logic := '1';
signal inc : std_logic := '0';
...
begin
...
clk <= not clk after 5 ns;
reset <= '0' after 10 ns;
inc <= '1' after 30 ns, '0' after 230 ns;
...
end Simulation;
À la date t=0, les signaux clk
, reset
et inc
ont la valeur initiale
indiquée dans leurs déclarations.
Dans les instructions d’affectation concurrentes, La clause after
permet
d’affecter une valeur à un signal au bout d’un certain temps après l’exécution
de l’instruction :
reset
et inc
sont exécutées à t=0.
Chaque clause after
planifie l’affectation d’une nouvelle valeur au bout
d’une certaine durée après cette date (donc à t=10 ns, t=30 ns, t=230 ns).clk
est exécutée à t=0 et à chaque fois que clk
change.
À chaque exécution, la clause after
planifie une nouvelle mise à jour de
clk
au bout de 5 ns.wait
L’instruction wait
peut être utilisée dans un processus pour mettre en pause
l’exécution pendant une certains durée ou jusqu’à la détection d’un événement.
Elle se présente typiquement sous trois formes :
time
:wait for durée;
wait on nom de signal, nom de signal, ...;
wait until condition;
wait;
L’instruction wait until
attend un événement qui rend la condition vraie.
Si la condition est déjà vraie au moment d’exécuter cette instruction,
le processus sera mis en pause jusqu’à ce qu’elle devienne fausse puis vraie
à nouveau.
Nous connaissons déjà la boucle for
, qui est
synthétisable lorsque le compteur de boucle parcourt un intervalle connu.
Pour décrire des scénarios de tests répétitifs, on peut également utiliser les
boucles infinies et les boucles tant que avec les syntaxes suivantes :
-- Exécuter le corps de boucle indéfiniment.
loop
instruction séquentielle
...
instruction séquentielle
end loop;
-- Exécuter le corps de boucle tant que la condition est vraie.
while condition loop
instruction séquentielle
...
instruction séquentielle
end loop;
À l’intérieur des boucles, il est possible d’utiliser les
instruction next
et exit
.
Comme la boucle for
, les instructions loop
, while
, next
et exit
ne sont autorisées que dans les processus.
Il est inutile d’écrire un processus qui ne contiendrait qu’une boucle infinie. En effet, le corps d’un processus est exécuté à chaque fois qu’il détecte un événement auquel il est sensible, ce qui s’apparente déjà à une boucle infinie. Les trois écritures suivantes produisent donc le même comportement :
process(clk_i)
begin
if rising_edge(clk_i) then
...
end if;
end process;
process
begin
wait until rising_edge(clk_i);
...
end process;
process
begin
loop
wait until rising_edge(clk_i);
...
end loop;
end process;
report
et assert
L’instruction report
permet d’afficher un message dans la console du simulateur.
Elle peut se présenter sous deux formes :
report chaîne de caractères;
report chaîne de caractères severity niveau;
Le mot-clé severity
permet d’indiquer le niveau de gravité du message.
Ce champ peut prendre les valeurs symboliques suivantes :
Niveau | Signification |
---|---|
NOTE |
Information |
WARNING |
Avertissement |
ERROR |
Erreur |
FAILURE |
Échec |
L’interprétation de ces niveaux dépend du logiciel de simulation et de la façon
dont il est configuré.
Par exemple, en présence d’un message de type FAILURE
on peut souhaiter que la
simulation s’arrête immédiatement, alors qu’elle peut continuer dans les autres
cas.
L’instruction assert
vérifie si une condition est vraie.
Lorsque la condition est fausse, elle affiche un message à la manière de l’instruction report
.
Les quatre formes de l’instruction assert
sont :
assert condition;
assert condition severity niveau;
assert condition report chaîne de caractères;
assert condition report chaîne de caractères severity niveau;
Dans l’instruction report
, en l’absence d’une clause severity
, c’est le niveau
NOTE
qui est utilisé.
Dans l’instruction assert
, le niveau par défaut est ERROR
.
En l’absence d’une clause report
, le message affiché sera "Assertion violation"
.
En VHDL, les chaînes de caractères sont de type string
et s’écrivent
entre guillemets :
report "Exemple de message";
Pour construire des chaînes de caractères qui contiennent des données de simulation, on peut s’appuyer sur deux opérations :
&
:chaîne de caractères & chaîne de caractères & ...
image
:nom de type'image(expression)
Par exemple, si value
et value_prev
sont de type integer
, on peut écrire :
assert value = value_prev
report "value vaut " & integer'image(value)
& " mais devrait valoir " & integer'image(value_prev)
severity ERROR;
À compléter.