Le langage VHDL (VHSIC* Hardware Description Language) est un langage de description de matériel inspiré du langage de programmation ADA.
D'abord conçu pour la simulation, le langage VHDL a, ensuite, été étendu à la synthèse des circuits numériques.
*VHSIC : Very High Speed Integrated Circuit
Dans le procédé de fabrication à partir du modèle VHDL, on trouve donc deux grandes étapes :
Au niveau de la conception, il y a deux finalités distinctes qui dépendent si le circuit est un ASIC ou un PLD :
La synthèse de haut niveau consiste à établir un schéma du circuit sous la forme de machines à état.
La synthèse de bas niveau consiste à établir un schéma adapté à la structure interne de l'ASIC.
Le placement routage permet de choisir la place des cellules sur le circuit ainsi que les trajets des connexions entre les cellules. Cette phase est composée de plusieurs cycles afin d'optimiser le placement et les connexions.
Le fichier obtenu est ensuite transmis à l'outil de programmation ou de fabrication.
Les outils dépendent des technologies des circuits mis en oeuvre et dépendent des fabricants de circuits programmables, ils sont en général gratuit pour quelques circuits. On trouve le logiciel vivado de chez Xilinx, quartus de chez intel et LSE de chez Lattice. Ces logiciels réalisent l'ensemble de la chaîne de conception décrite auparavant.
Il existe un grand nombre de simulateurs, dans ce chapitre et le suivant nous utiliserons le simulateur fonctionnel ghdl qui génère un fichier pour gtkwave pour afficher le résultat de la simulation. Ce logiciel open-source est disponible sur le site officiel de GHDL.
L'entité est la vision externe du circuit avec les signaux. Dans l'exemple, on a deux signaux d'entrées a et b ainsi que deux signaux de sorties S et Rs
Cet exemple représente le demi-additionneur (half-adder) qui est un additionneur sans retenue d'entrée.
L'architecture représente la structure interne et complète du composant. Dans cet exemple le OU exclusif pour calculer la somme et la fonction ET pour calculer la retenue.
Le VHDL est un langage, qui utilise un modèle de hierarchie, permet de créer un circuit à partir de composants qui sont également des circuits. Cela permet de décomposer un circuits en composants élémentaires.
L'exemple ci-contre présente un additionneur complet à partir des composants demi-additionneurs. En prenant les équations du demi-addtioneur, on peut déduire les équations du montage :
Ce langage est destiné à réaliser ou simuler un circuit numérique, c'est pourquoi les données sont des signaux binaires, dont les principaux types sont :
Les valeurs de ces types de signaux peuvent être exprimées en binaire, hexadécimal ou plus rarement octal et sont exprimés entre guillemets pour les bus et entre apostrophes pour les bits :
Contrairement à ce qui est attendu, un bit peut prendre 9 valeurs différentes :
Cela permet d'exprimer, par exemple, le fonctionnement d'un drain ou collecteur ouvert qui utilise les états 0 ou H.
Les agrégats sont des expressions ou symboles qui permettent de simplifier l'écriture. Si le vecteur v est de type std_logic_vector(3 downto 0), on peut écrire :
Les attributs permettent d'accéder à une propriété de la donnée comme le changement d'état, le nombre de bits d'un vecteur, l'indice haut et bas, .... avec le bit b ou le vecteur v :
La déclaration d'un signal se fait avec le mot clé signal suivi de nom du signal et du type de signal comme par exemple :
signal vecteur : std_logic_vector(7 downto 0) := "00010001";
les := et la valeur qui suit ne sont pas obligatoires et servent uniquement si le vecteur doit initialisé.
On peut largement utiliser les commentaires pour expliquer la structure du code. Ces commentaires s'écrivent ligne par ligne en les précédent de deux symboles --
-- ceci est un commentaire
L'affectation du résultat d'une expression à un signal utilise les symboles : <=.
Ils sont notés : not, or , and, xor, sll pour le décalage à gauche, srl pour le décalage à droite, rol pour la rotation à gauche, ror pour la rotation à droite. Les opérateurs de décalage et rotation ne sont pas synthétisables, c'est à dire qu'ils ne génèrent pas de circuit.
+, - et * pour la multiplication qui se fait sur des entiers signés ou non signés ou des signaux convertis en entiers, / pour la division sur des entiers pas des signaux même convertis en entiers, l'opérateur de division n'est pas synthétisable.
= pour égal à, /= pour différent de, < pour inférieur à , <= pour inférieur ou égal à , > pour supérieur à, >= pour supérieur ou égal à.
Pour les vecteurs la comparaison se fait de gauche à droite par comparaison lexicographique et pas par valeur numérique. Pour réaliser des comparaisons numériques il faut utiliser des types numériques unsigned ou signed ou bien convertir les vecteurs en entiers.
Expression | std_logic_vector | unsigned | signed |
---|---|---|---|
"001" = "0001" | faux | vrai | vrai |
"001" > "0001" | vrai | faux | faux |
"010" < "1000" | vrai | vrai | faux |
"100" < "00100" | faux | faux | vrai |
"100" < "01000" | faux | vrai | vrai |
En prenant les vecteurs suivants :
signal s : std_logic_vector(7 downto 0);
signal e : std_logic_vector(3 downto 0);
On peut écrire :
s <= "0011" & e ;
ou réaliser un décalage à droite de 1 bit :
s <= ’0’ & s(7 downto 1) ;
ou encore réaliser un décalage à gauche de 1 bit :
s <= s(6 downto 0) & ’0’;
Comme tous les langages l'utilisation de certains types de données nécessitent d'utiliser des librairies et des paquetages qui définissent les types, opérateurs, fonctions, ...
Les librairies sont chargées avec le mot clé library et les paquetages de ces librairies avec le mot clé use
Au minimum, il faut :
library ieee;
use ieee.std_logic_1164.all;
il faut ajouter d'autres paquetages :
entity NOM_ENTITE is
port ( signaux entree/sortie );
end NOM_ENTITE;
Les signaux se définissent avec la syntaxe :
nom : sens type; (pas de ; pour le dernier signal avant la parenthèse fermante)
architecture NOM_ARCH of NOM_ENTITE is
−− declaration des composants
−− declaration des types
−− declaration des signaux internes
begin
−− description du fonctionnement
end NOM_ARCH;
On va étudier le code VHDL du demi-additionneur, de l'additionneur complet et du test-bench qui permet de simuler le fonctionnement.
On commence par le demi-additionneur
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
-- pas d'autres paquetages nécessaires
-- demi-additionneur d'entrée a et b
-- et de sortie somme S et retenue R
entity addhalf is
port( a,b: in STD_LOGIC;
S,R:out STD_LOGIC);
end addhalf;
architecture architecture_addhalf of addhalf is
begin
-- on utilise directement les expressions booléennes
S <= a xor b;
R <= a and b;
end architecture_addhalf;
On commence par inclure la librairie IEEE et les paquetages nécessaires. Ici on n'a pas besoin de paquetages supplémentaires.
Ensuite on déclare l'entité avec deux entrées a et b et deux sorties S pour la somme et R pour la retenue. On remarque qu'il n'y a pas de ; après la dernière déclaration du signal avant la parenthèse.
Le code de l'architecture correspond au schéma
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
-- le testbench n'a pas de signaux d'entrées ni de sorties
entity addhalf_tb is
end addhalf_tb;
architecture tb_arch of addhalf_tb is
-- declaration du composant à utiliser
component addhalf
port (a,b: in STD_LOGIC;
S,R : out STD_LOGIC);
end component;
-- signaux internes
signal tba, tbb, tbS, tbR : STD_LOGIC;
begin
-- instanciation du composant addhalf
add: addhalf port map(a=>tba, b=>tbb,S=>tbS,R=>tbR);
-- suites de valeurs pour les signaux d'entrées : stimulis
tba <= '0','1' after 100 ns, '0' after 200 ns,
'1' after 300 ns, '0' after 400 ns;
tbb <= '0', '1' after 200 ns, '0' after 400 ns;
end;
Il n'y a pas de données ou de fonctions qui nécessitent de paquetages complémentaires.
Quelques fois il faut ajouter la librairie work qui représente le répertoire de travail, elle n'est pas toujours nécessaire, cela dépend des logiciels utilisés. L'utilisation de cette librairie pour le composant qui testé n'est pas nécessaire avec GHDL.
L'entité ne possède pas de signaux d'entrées ni de sorties, en effet le testbench est le fichier qui fournit les signaux pour simuler le composant, signaux nommés stimulis.
Cette remarque est valable pour tous les fichiers testbench.
Pour utiliser un composant, il faut le déclarer avec le mot clé composant en respectant l'entité du composant.
Mais cela ne suffit pas à l'utiliser.
IL faut ensuite le connecter aux autres composants et signaux dans l'architecture, on parle d'instance du composant.
Cela se fait avec la ligne port map où le mot clé avant les : est le nom donné à l'instance, nom que l'on retrouvera dans le simulateur.
Il faut également déclaré tous les signaux qui seront connectés au composant, on peut utiliser des identifiants identiques à ceux utilisés dans le composant. Pour ma part je préfère utiliser des noms différents qui sont les noms des signaux du composant préfixés de tb comme testbench.
Il faut maintenant générer les signaux d'entrées du composant qui sont tba et tbb. La solution la plus simple est de donner une suite de valeurs associées à des temps.
Les valeurs des temps sont absolues par rapport au début de la simulation.
Il existe d'autres méthodes pour générer ces signaux, elles seront présentées dans les paragraphes qui suivent.
Les valeurs des temps sont arbitraires, mais l'unité de la nano seconde correspond en général aux tests réalisés sur les circuits FPGA.
On retrouve bien la réponse du OU exclusif pour S et la réponse du ET pour la retenue R. La fonction réalisée est bien un additionneur 1 bit sans retenue d'entrée.
Il n'y a pas de retard entre signaux d'entrées et sorties car il s'agit d'une simulation fonctionnelle réalisée avec GHDL.
Cette simulation fonctionnelle ne garantit pas que le code fonctionne, pour cela il faudra effectuer un placement sur un circuit et effectuer une simulation après routage.
Maintenant, on code l'additionneur complet
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
-- pas d'autres paquetages nécessaires
-- additionneur complet d'entrée a,b et Re
-- et de sortie somme S et retenue R
entity addfull is
port( a,b,Re: in STD_LOGIC;
S,Rs:out STD_LOGIC);
end addfull;
architecture architecture_addfull of addfull is
-- declaration du composant à utiliser
component addhalf
port ( a,b: in STD_LOGIC;
S,R : out STD_LOGIC);
end component;
-- signaux internes
signal S0, R0, R1 : STD_LOGIC;
begin
-- on utilise les composants et expressions booléennes
add0: addhalf port map(a=>a, b=>b,S=>S0,R=>R0);
add1: addhalf port map(a=>S0, b=>Re,S=>S,R=>R1);
Rs <= R1 or R0 ;
end architecture_addfull;
On remarque que les signaux internes correspondent aux signaux utilisés dans le schéma du paragraphe précédent. Ce qui confirme qu'en programmation VHDL, il faut penser schéma électronique numérique.
Le code de l'architecture correspond à la hiérarchie du schéma avec deux instances du composant addhalf et la porte OU qui calcule la retenue de sortie à partir des retenues des demi-additionneurs.
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
-- le testbench n'a pas de signaux d'entrées ni de sorties
entity addfull_tb is
end addfull_tb;
architecture tb_arch of addfull_tb is
-- declaration du composant à utiliser
component addfull
port ( a,b,Re: in STD_LOGIC;
S,Rs : out STD_LOGIC);
end component;
-- signaux internes
signal tba, tbb, tbRe, tbS, tbRs : STD_LOGIC;
begin
-- instanciation du composant addfull
add: addfull port map(a=>tba, b=>tbb,Re => tbRe, S=>tbS,Rs=>tbRs);
-- suites de valeurs pour les signaux d'entrées : stimulis
tba <= '0','1' after 100 ns, '0' after 200 ns,
'1' after 300 ns, '0' after 400 ns,
'1' after 500 ns, '0' after 600 ns,
'1' after 700 ns, '0' after 800 ns;
tbb <= '0', '1' after 200 ns, '0' after 400 ns,
'1' after 600 ns, '0' after 800 ns;
tbRe <= '0' , '1' after 400 ns, '0' after 800 ns;
end;
Ce nouveau testbench comprend 3 signaux à générer a,b et Re
Les temps sont calculés pour établir toutes les combinaisons des signaux d'entrées et ainsi vérifier la table de vérité de l'additionneur complet.
Comme pour le testbench précédent la valeur est arbitraire, il faut juste générer toutes les combinaisons de la table de vérité.
On vérifie que toutes les combinaisons des entrées a,b et Re sont présentes. L'ordre importe peu car nous avons, ici, un système combinatoire.
Comme pour la simulation précédente, la simulation est fonctionnelle et ne présente pas les temps de retard occasionnés par les portes logiques et les connexions sur un circuit réel.
L’ordre d’écriture des lignes n’a pas d’influence sur le fonctionnement du circuit.
Les codes suivants donnent exactement le même résultat :
S <= a xor b; | ⇔ | R <= a and b; | ||
R <= a and b; | S <= a xor b; |
Elles permettent de réaliser des fonctions combinatoires évoluées sans avoir à utiliser les équations booléennes comme cela a été présenté aux chapitres sur l'algèbre de boole et sur la logique combinatoire.
Affectation d'un valeur à un signal qui dépend d'une ou plusieurs conditions
S | <= | valeur 1 when expressionbooleene 1 VRAIE else |
valeur 2 when expressionbooleene 2 VRAIE else | ||
... | ||
valeur n when expressionbooleene n VRAIE else | ||
valeur par défaut; |
Exemple avec le codeur
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
-- codeur 8 vers 3
-- entrée croissante de 0 à 7
entity codeur is
Port ( e : in STD_LOGIC_VECTOR(0 to 7);
S: out STD_LOGIC_VECTOR(2 downto 0));
end codeur;
architecture architecture_codeur of codeur is
begin
S <= "000" when e = "10000000" else
"001" when e = "01000000" else
"010" when e = "00100000" else
"011" when e = "00010000" else
"100" when e = "00001000" else
"101" when e = "00000100" else
"110" when e = "00000010" else
"111" when e = "00000001" else
"XXX" ;
-- toutes les autres combinaisons donnent
-- une valeur indéterminée
end architecture_codeur;
On choisit de manière arbitraire l'ordre croissant pour l'entrée e.
La structure de contrôle permet de traiter tous les cas donnés par la table de vérité. Il reste le choix du résultat pour les combinaisons qui ne sont pas utilisées dans la liste.
Le choix de "XXX" permet au logiciel de simplifier la solution qui sera implémentée sur le circuit. On fait ce choix si on est absolument certain que les autres cas ne se présenteront pas, sinon il faut faire un choix par défaut ou ajouter une sortie supplémentaire qui indique que le code est valide ou non.
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.NUMERIC_STD.all;
entity codeur_tb is
end codeur_tb;
architecture tb_arch of codeur_tb is
-- declaration composant a simuler : codeur
component codeur
port ( e : in STD_LOGIC_VECTOR(0 to 7);
S: out STD_LOGIC_VECTOR(2 downto 0));
end component;
-- signaux relies au composant codeur
signal tbe : STD_LOGIC_VECTOR(0 to 7); -- entree 8 bits
signal tbS : STD_LOGIC_VECTOR(2 downto 0); -- sortie 3 bits
begin
-- instanciation physique du composant codeur
code83: codeur port map(e => tbe, S => tbS);
-- gestion des stimulis
tbe <= "10000000" ,
"01000000" after 50 ns,
"00100000" after 100 ns,
"00010000" after 150 ns,
"00001000" after 200 ns,
"00000100" after 250 ns,
"00000010" after 300 ns,
"00000001" after 350 ns,
"10000000" after 400 ns;
end;
On retrouve bien en sortie la valeur qui correspond au numéro de l'entrée qui activée.
Exemple avec le décodeur
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
-- decodeur 3 vers 8
-- sortie dans l'ordre croissant
entity decodeur is
port ( e : in STD_LOGIC_VECTOR(2 downto 0);
S: out STD_LOGIC_VECTOR(0 to 7));
end decodeur;
architecture architecture_decodeur of decodeur is
begin
S <= "10000000" when e = "000" else
"01000000" when e = "001" else
"00100000" when e = "010" else
"00010000" when e = "011" else
"00001000" when e = "100" else
"00000100" when e = "101" else
"00000010" when e = "110" else
"00000001" when e = "111" else
"XXXXXXXX" ;
end architecture_decodeur;
On retrouve la valeur indéterminée qui intervient pour les combinaisons non utilisées.
Il ne devrait pas y avoir de valeur indéterminée car toutes les combinaisons binaires des entrées ont bien été utilisées. Mais il faut jamais oublier qu'en VHDL, un signal peut avoir 9 valeurs différentes, et, sur cet ensemble de 9 valeurs toutes les combinaisons n'ont pas été utilisées. Prendre en compte les 9 valeurs des signaux n'est pas toujours obligatoire, mais il ne faut pas oublier que ces 9 valeurs existent.
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.NUMERIC_STD.all;
use WORK.decodeur;
entity decodeur_tb is
end decodeur_tb;
architecture tb_arch of decodeur_tb is
-- signaux relies au composant codeur
component decodeur
port ( e : in STD_LOGIC_VECTOR(2 downto 0); -- entree 3 bits
S: out STD_LOGIC_VECTOR(0 to 7)); -- sortie 8 bits
end component;
-- declaration composant a simuler : decodeur
signal tbe : STD_LOGIC_VECTOR(2 downto 0);
signal tbS : STD_LOGIC_VECTOR(0 to 7);
begin
-- instanciation physique du composant decodeur
decode83: decodeur port map(e=>tbe,S=>tbS);
-- gestion des stimulis
tbe <= o"0" ,
o"1" after 50 ns,
o"2" after 100 ns,
o"3" after 150 ns,
o"4" after 200 ns,
o"5" after 250 ns,
o"6" after 300 ns,
o"7" after 350 ns,
o"0" after 400 ns;
end;
Le numéro de la sortie activée correspond bien à la valeur de l'entrée.
On remarque la notation octale à la place du binaire, cela montre un exemple des notations des valeurs des vecteurs dans des bases différentes de la base 2 habituelle.
Une adresse permet de sélectionner la donnée à transmettre parmi n données.
with | a | select | |
S | <= | valeur 1 when valeur 1 de a, | |
valeur 2 when valeur 2 de a, | |||
... | |||
valeur n when valeur n de a, | |||
valeur par défaut when others; |
Exemple avec le multiplexeur
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.NUMERIC_STD.all;
-- multiplexeur 1 parmi 8
entity multiplexeur is
Port ( a : in STD_LOGIC_VECTOR(2 downto 0);
e : in STD_LOGIC_VECTOR(0 to 7);
S : out STD_LOGIC);
end multiplexeur;
architecture architecture_multiplexeur of multiplexeur is
begin
with a select
S <= e(0) when "000",
e(1) when "001",
e(2) when "010",
e(3) when "011",
e(4) when "100",
e(5) when "101",
e(6) when "110",
e(7) when "111",
'X' when others;
end architecture_multiplexeur;
On transmets la valeur e(a) en sortie avec a adresse comprise entre 0 et 7,
on peut écrire : s = e(a).
ici encore toutes les combinaisons de a sont utilisées, pourtant on utilise la valeur par défaut qui correspond aux autres combinaisons de a, sauf que cette fois c'est obligatoire car cette structure de contrôle exige que toutes les combinaisons de l'adresse a soient utilisées.
Comme on ne traite généralement pas les combinaisons avec les 9 valeurs, cette structure de donnée se termine toujours avec la valeur par défaut : valeur when others;.
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.NUMERIC_STD.all;
entity multiplexeur_tb is
end multiplexeur_tb;
architecture tb_arch of multiplexeur_tb is
-- declaration composant a simuler : multiplexeur
component multiplexeur
Port ( a : in STD_LOGIC_VECTOR(2 downto 0);
e : in STD_LOGIC_VECTOR(0 to 7);
S : out STD_LOGIC);
end component;
-- signaux relies au composant multiplexeur
signal tba : STD_LOGIC_VECTOR(2 downto 0); -- entree 3 bits
signal tbe : STD_LOGIC_VECTOR(0 to 7); -- entree 8 bits
signal tbS : STD_LOGIC; -- sortie
begin
-- instanciation physique du composant multiplexeur
mux38: multiplexeur port map(a => tba, e => tbe, S => tbS);
-- gestion des stimulis
tbe <= x"00",
x"a5" after 50 ns;
tba <= o"0" ,
o"1" after 100 ns,
o"2" after 150 ns,
o"3" after 200 ns,
o"4" after 250 ns,
o"5" after 300 ns,
o"6" after 350 ns,
o"7" after 400 ns,
o"0" after 450 ns;
end;
En partant de l'adresse a=0 jusqu'à 7, on retrouve la valeur x"a5"="10100101" en commençant par e0.
nom: | process ( liste de sensibilité ) |
begin | |
--- traitement combinatoire ou séquentiel | |
end process nom; |
nom est facultatif mais très vivement conseillé pour des questions de visibilité.
La liste de sensibilité peut être laissée vide, mais cela ne fonctionne pas toujours, notamment en simulation fonctionnelle. Cette liste contient la liste exhaustive des signaux, séparés par une virgule, qui affectent le changement d'état des résultats à l'intérieur du processus.
Cette structure de contrôle effectue un traitement conditionné par une ou plusieurs expressions booléennes.
if | expression booléenne then |
-- traitement si expression booléenne vraie | |
else | |
-- traitement si expression booléenne fausse | |
end | if; |
if | expression booléenne then |
-- traitement si expression booléenne vraie | |
elsif expression booléenne 2 | |
-- traitement si expression booléenne 2 vraie | |
-- ... | |
else | |
-- traitement si toutes les expressions booléennes sont fausses | |
end | if; |
Cette structure effectue un traitement en fonction de la valeur d'un signal
case | signal is |
when valeur 1 => | |
-- traitement si signal = valeur 1 | |
when valeur 2 => | |
-- traitement si signal = valeur 2 | |
-- ... | |
when others => | |
-- traitement par défaut | |
end | case; |
On va reprendre le multiplexeur avec une version processus
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.NUMERIC_STD.all;
-- multiplexeur 1 parmi 8
entity multiplexeur is
Port ( a: in STD_LOGIC_VECTOR(2 downto 0);
e : in STD_LOGIC_VECTOR(0 to 7);
S: out STD_LOGIC);
end multiplexeur;
architecture architecture_multiplexeur of multiplexeur is
begin
mux: process (a,e)
begin
case a is
when o"0" =>
S <= e(0);
when o"1" =>
S <= e(1);
when o"2" =>
S <= e(2);
when o"3" =>
S <= e(3);
when o"4" =>
S <= e(4);
when o"5" =>
S <= e(5);
when o"6" =>
S <= e(6);
when o"7" =>
S <= e(7);
when others =>
S <= 'X' ;
end case;
end process mux;
end architecture_multiplexeur;
On utilise la structure case pour traiter chaque combinaison de l'adresse, en ajoutant la valeur par défaut.
Dans la liste de sensibilité, il faut mettre les deux signaux a et e. Si on oublie le signal e, le circuit ne commutera que sur des changements de valeur de a, ou si on oublie le signal a, le circuit ne commutera que sur des changements de valeurs de e.
La liste de sensibilité contient tous les signaux susceptibles de faire commuter le circuit.
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.NUMERIC_STD.all;
entity multiplexeur_tb is
end multiplexeur_tb;
architecture tb_arch of multiplexeur_tb is
-- declaration composant a simuler : multiplexeur
component multiplexeur
Port ( a: in STD_LOGIC_VECTOR(2 downto 0);
e : in STD_LOGIC_VECTOR(0 to 7);
S: out STD_LOGIC);
end component;
-- signaux relies au composant multiplexeur
signal tba : STD_LOGIC_VECTOR(2 downto 0); -- entree 3 bits
signal tbe : STD_LOGIC_VECTOR(0 to 7); -- entree 8 bits
signal tbS : STD_LOGIC; -- sortie
begin
-- instanciation physique du composant multiplexeur
mux38: multiplexeur port map(a => tba, e => tbe, S => tbS);
-- gestion des stimulis
simulation: process
begin
tba <= o"0";
tbe <= x"00";
wait for 50 ns ;
tbe <= x"a5";
wait for 50 ns ;
tba <= o"1";
wait for 50 ns ;
tba <= o"2";
wait for 50 ns ;
tba <= o"3";
wait for 50 ns ;
tba <= o"4";
wait for 50 ns ;
tba <= o"5";
wait for 50 ns ;
tba <= o"6";
wait for 50 ns ;
tba <= o"7";
wait for 50 ns ;
wait; -- fin du process de simulation
end process simulation;
end;
On retrouve les mêmes résultats qui sont toujours corrects.
Pour le testbench, cette fois-ci, les temps sont relatifs à la dernière valeur. Le dernier wait sans valeur de temps est un temps infini qui met fin à la simulation.
On remarque que ce processus n'a pas de liste de sensibilité, car on utilise des instructions wait. En résumé, soit on utilise le processus avec une liste de sensibilité sans wait, ou bien sans liste de sensibilité avec wait.
Le fait que le processus puisse être synchronisé sur des signaux, fait que l'on peut implémenter les fonctions de la logique séquentielle.
On réalise un compteur synchrone avec reset asynchrone, entrée de verrouillage synchrone et sortie de retenue activée lors du passage à 0.
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;
-- compteur synchrone décimal
-- reset asynchrone
-- validation synchrone
-- sortie de retenue
entity compteur is
Port ( clk : in STD_LOGIC;
reset : in STD_LOGIC;
en : in STD_LOGIC;
tc : out STD_LOGIC;
Q : out STD_LOGIC_VECTOR(3 downto 0));
end compteur;
architecture arch_compteur of compteur is
signal cpt : STD_LOGIC_VECTOR (3 downto 0);
constant Nmax : integer := 9 ;
begin
Q <= cpt;
tc <= '1' when cpt = Nmax else '0';
comptage: process(clk,reset)
begin
if reset='0' then
cpt <= (others => '0'); -- mise à 0 asynchrone
elsif rising_edge(clk) and en ='1' then
if cpt < Nmax then
cpt <= cpt + 1;
else
cpt <= (others => '0'); -- retour à 0
end if;
end if;
end process comptage;
end arch_compteur;
Comme dans beaucoup de langages, l'utilisation de constantes est conseillé, pour des raisons de lisibilité et aussi pour n'avoir à changer la valeur seulement à un endroit du code. La valeur maximale du compteur est utilisée plusieurs fois dans le code, c'est pourquoi on utilise une constante Nmax.
Ce processus vérifie ce qui a été dit dans le chapitre logique séquentielle à propos de la mise à 0 asynchrone.
Cela est réalisé par le fait que reset fait partie de la liste de sensibilité et surtout que le test de sa valeur est réalisé en priorité.
Le retour à 0 après la valeur 9 est, quant à lui, synchrone.
La détection du front montant est réalisée par la fonction rising_edge(clk).
Dans de nombreux codes VHDL on trouve l'utilisation de l'attribut event : clk'event and clk='1'. Le résultat est identique si le signal utilise les états 0 et 1. La différence réside dans le fait que l'on a en réalité 9 états. La fonction rising_edge détecte uniquement la transition de l'état 0 à l'état 1, alors que l'attributevent détecte un changement d'état quelque soit ces états. En conséquence la syntaxe clk'event and clk='1' détecte un changement d'état quelconque à l'état 1. Cette syntaxe détecte donc un passage de l'état 'Z' à l'état 1.
Pour détecter un front descendant, il existe la fonction falling_edge.
L'utilisation du signal interne cpt vient du fait que Q est une sortie et ne peut être utilisé comme signal d'entrée, c'est à dire que l'on ne peut pas écrire Q <= Q + 1;.
L'autre solution serait de remplacer out par buffer ce qui rendrait possible d'utiliser Q comme entrée dans l'expression de calcul.
L'inconvénient de cette solution est que si la sortie change de niveau à cause d'un courant trop important, cela perturberait le fonctionnement du compteur.
Le type buffer est, en réalité, utilisé pour la détection du niveau de sortie comme dans le cas du bus I2C.
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
Use ieee.std_logic_unsigned.all;
entity compteur_tb is
end compteur_tb;
architecture tb_arch of compteur_tb is
-- declaration composant a simuler : compteur
component compteur is
Port ( clk : in STD_LOGIC;
reset : in STD_LOGIC;
en : in STD_LOGIC;
tc : out STD_LOGIC;
Q : out STD_LOGIC_VECTOR(3 downto 0));
end component;
-- signaux relies au composant compteur
signal tbQ : STD_LOGIC_VECTOR(3 downto 0);
signal tbclk, tbreset, tben, tbtc : STD_LOGIC;
-- période de l'horloge
constant clk_period : time := 20 ns;
begin
-- instanciation physique du composant compteur
chipcompteur : compteur port map(clk => tbclk, reset => tbreset,
en => tben, tc => tbtc, Q => tbQ);
-- processus horloge permanent de période clk_period
clk_process :process
begin
tbclk <= '0';
wait for clk_period/2;
tbclk <= '1';
wait for clk_period/2;
end process;
-- gestion des stimulis
stim_proc: process
begin
tbreset <= '1';
tben <= '0' ;
wait for clk_period*2;
tbreset <= '0';
wait for clk_period*2;
tben <= '1';
wait;
end process;
end;
La période de l'horloge est souvent utilisée dans le code du testbench, d'où l'utilisation de la constante. De plus le langage VHDL permet d'utiliser des unités, comme ici, avec l'unité de temps. La valeur de 20ns correspond à celle que l'on trouve sur les cartes FPGA basiques.
Dans ce testbench, il faut générer un signal d'horloge en parallèle avec les autres signaux. On va donc utiliser la propriété de concurrence du VHDL en utilisant deux processus : un pour l'horloge, un pour les autres signaux. Cette est méthode est très utilisée dans le test des systèmes séquentiels.
Ces deux processus utilisent la fonction wait donc il n'y a pas de liste de sensibilité.
La sortie tc est une sortie de retenue qui permet de valider le passage à 0 du compteur. Elle permet, par exemple, d'activer un deuxième compteur en la connectant à l'entrée en de ce dernier. Elle doit être active sur la valeur 9, car c'est toujours l'état précédent le front de l'horloge qui est pris en compte.
On réalise un registre à décalage sur 4 bits.
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
-- registre à décalage 4 bits
entity registre is
Port( clk,d,rst : in STD_LOGIC;
Q : out STD_LOGIC_VECTOR(0 to 3));
end registre;
architecture arch_registre of registre is
signal qreg : STD_LOGIC_VECTOR (0 to 3);
begin
registre_right: process(clk,rst)
begin
if rst = '1' then
qreg <= (others => '0');
elsif rising_edge(clk) then
qreg <= d & qreg(0 to 2);
end if;
end process registre_right;
Q <= qreg;
end arch_registre;
Comme pour le compteur, on a un registre à décalage synchrone avec une mise à 0 asynchrone.
Pour le décalage, on utilise la concaténation qreg <= d & qreg(0 to 2), ce qui a pour effet de concaténer la valeur de l'entrée avec les bits de 0 à n-2, n étant le nombre de bit, pour fabriquer un nouveau vecteur.
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
entity registre_tb is
end registre_tb;
architecture tb_arch of registre_tb is
-- declaration composant a simuler : registre
component registre
Port( clk,d,rst: in STD_LOGIC;
Q : out STD_LOGIC_VECTOR(0 to 3));
end component;
-- signaux relies au composant registre
signal tbclk, tbd, tbrst : STD_LOGIC;
signal tbQ : STD_LOGIC_VECTOR(0 to 3);
signal Q3,Q2,Q1,Q0 : STD_LOGIC;
-- période de l'horloge
constant clk_period : time := 20 ns;
begin
Q3 <= tbQ(3);
Q2 <= tbQ(2);
Q1 <= tbQ(1);
Q0 <= tbQ(0);
-- instanciation physique du composant registre
registre8: registre port map(clk=>tbclk, rst => tbrst,d=>tbd,Q=>tbQ);
-- processus horloge permanent de période clk_period
clk_process :process
begin
tbclk <= '0';
wait for clk_period/2;
tbclk <= '1';
wait for clk_period/2;
end process;
-- gestion des stimulis
stim_proc: process
begin
tbrst <= '1';
tbd <= '0';
wait for 50 ns;
tbrst <= '0';
tbd <= '0';
wait for 65 ns;
tbd <= '1';
wait for 100 ns;
tbd <= '0';
wait for 50 ns;
tbd <= '1';
wait for 50 ns;
tbd <= '0';
wait;
end process;
end;
Les signaux internes Q0 à Q3 ont été créés pour permettre d'afficher les signaux su vecteur Q séparément pour la simulation.
On code la machine à états décrite dans le chapitre logique séquentielle en VHDL
Dans on trouve le mot clé type qui permet de déclarer un nouveau type de donnée.
La syntaxe générale est
type nom is definition du type;
Le nouveau type présenté est une énumération de valeurs qui permet d'améliorer la lisibilité du code. C'est le logiciel qui assigne une valeur aux différents éléments de ce type.
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
-- exemple de machine à état
entity machine_etat is
Port ( clk : in STD_LOGIC;
reset : in STD_LOGIC;
e : in STD_LOGIC_VECTOR(0 to 2);
S : out STD_LOGIC_VECTOR(3 downto 0));
end machine_etat;
architecture architecture_machine_etat of machine_etat is
-- liste des états
type T_etat is (S1,S2,S3);
-- état courant et état suivant
signal etat_suivant, etat_courant : T_etat;
begin
-- registre de mémorisation de l'état suivant
registre_etat_suivant: process(clk,reset)
begin
if reset = '0' then
etat_courant <= S1;
else if rising_edge(clk) then
etat_courant <= etat_suivant;
end if;
end if;
end process registre_etat_suivant;
-- logique de calcul de l'état suivant
bloc_logique_etat_suivant: process(etat_courant,e)
begin
-- traitement par défaut
etat_suivant <= etat_courant;
case etat_courant is
when S1 =>
if e(1) = '1' then
etat_suivant <= S2;
end if;
when S2 =>
if e(2) = '1' then
etat_suivant <= S3;
end if;
when S3 =>
if e(0) = '1' then
etat_suivant <= S1;
end if;
end case;
end process bloc_logique_etat_suivant;
-- bloc logique de sortie
bloc_sortie: process(etat_courant)
begin
case etat_courant is
when S1 =>
S <= "0001" ;
when S2 =>
S <= "1010" ;
when S3 =>
S <= "1100" ;
end case;
end process bloc_sortie;
end architecture_machine_etat;
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
entity machine_etat_tb is
end machine_etat_tb;
architecture tb_arch of machine_etat_tb is
component machine_etat
Port ( clk : in STD_LOGIC;
reset : in STD_LOGIC;
e : in STD_LOGIC_VECTOR(0 to 2);
S : out STD_LOGIC_VECTOR(3 downto 0));
end component;
signal tbclk,tbreset,e0,e1,e2 : STD_LOGIC;
signal tbe : STD_LOGIC_VECTOR(0 to 2);
signal tbS : STD_LOGIC_VECTOR(3 downto 0);
constant clk_period : time := 20 ns;
begin
-- signaux pour la simulation
e0 <= tbe(0);
e1 <= tbe(1);
e2 <= tbe(2);
-- composant machine à état
machineetat: machine_etat port map(clk => tbclk,reset => tbreset,
e => tbe, S => tbS);
-- Clock process definitions
clk_process :process
begin
tbclk <= '0';
wait for clk_period/2;
tbclk <= '1';
wait for clk_period/2;
end process;
-- stimulis
stim_proc: process
begin
tbreset <= '0';
tbe <= "000";
wait for clk_period*2;
tbreset <= '1';
wait for 50 ns;
tbe(1) <= '1';
wait for 30 ns;
tbe(1) <= '0';
wait for 50 ns;
tbe(2) <= '1';
wait for 30 ns;
tbe(2) <= '0';
wait for 50 ns;
tbe(0) <= '1';
wait for 30 ns;
tbe(0) <= '0';
wait;
end process;
end;