Systèmes électroniques
Fermer ×

Les bases de VHDL

Introduction

Chaîne de conception

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 :

  • L'approche fonctionnelle qui permet de réaliser la simulation fonctionnelle du modèle à l'aide du fichier de test (testbench).
  • La partie conception du circuit numérique associé à la simulation du fonctionnement du circuit en prenant en compte l'implémentation des différentes fonctions, c'est la simulation après routage qui utilise le même fichier de test (testbench).

Au niveau de la conception, il y a deux finalités distinctes qui dépendent si le circuit est un ASIC ou un PLD :

  • Dans le cas d'un ASIC, l'étape finale est le masque de fabrication du circuit.
  • Dans le cas d'un PLD, l'étape finale est le fichier de programmation du circuit qui sera chargée en SRAM ou servira au programmateur dans le cas d'une technologie antifusible.

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

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.

La structure du langage

Entité et architecture

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.

  • S = a ⊕ b
  • Rs = a.b

Composant

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 :

S = S 1 = S 0 R e = a b R e
R s = R 0 + R 1 = a b + R e S 0 = a b + R e ( a b ) = a b + R e ( a + b )
Voir la démonstration
R s = a b + R e ( a b ) = a b + a b R e + R e ( a b ) = a b + R e ( a b + a b ) = a b + R e ( a ¯ b + a b ¯ + a b ) = a b + R e ( a ¯ b + a ) = a b + R e ( b + a )

Les types de données

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é.

Les commentaires

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

Les opérateurs de calcul et comparaisons

Affectation

L'affectation du résultat d'une expression à un signal utilise les symboles : <=.

Opérateurs logiques

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.

Opérateurs arithmétiques

+, - 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.

Les opérateurs de comparaison

= 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.

Expressionstd_logic_vectorunsignedsigned
"001" = "0001" fauxvraivrai
"001" > "0001" vraifauxfaux
"010" < "1000" vraivraifaux
"100" < "00100" fauxfauxvrai
"100" < "01000" fauxvraivrai
  • 1ère ligne : pour des vecteurs on a une différence à la troisième position
  • 2ème ligne : la différence de la troisième position rend l'expression vraie, pour des entiers l'expression est fausse
  • 3ème ligne : pour les vecteurs l'expression est vrai dès la première position, pour un entier signé, la deuxième valeur est négative
  • 4ème ligne : pour les vecteurs l'expression est fausse dès la première position, fausse également pour des entiers non signés
  • 5ème ligne : l'expression est fausse pour les vecteurs, mais elle est vraie pour des entiers signés et non signés

L'opérateur de concaténation de vecteurs &

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’;

Les librairies et paquetages

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 :

Entité

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)

  • nom : nom du signal avec les caractères AZaz09_
  • sens : in pour un signal d'entrée, out pour un signal de sortie, inout pour une entrée-sortie bidirectionnelle (entrée ou sortie mais pas les deux simultanément), buffer pour une sortie dont la valeur peut être utilisée comme entrée
  • type : type de signal comme std_logic, std_logic_vector

Architecture

architecture NOM_ARCH of NOM_ENTITE is
−− declaration des composants
−− declaration des types
−− declaration des signaux internes
begin
−− description du fonctionnement
end NOM_ARCH;

Exemple

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

Voir le test-bench du demi-additionneur

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.

Voir le résultat de la simulation du demi additionneur

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.

Voir le testbench de l'additionneur complet

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é.

Voir le résultat de la simulation de l'additionneur complet

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.

Le mode concurrent

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;

Les structures de contrôle

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.

La structure when ... else ...

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.

Voir le testbench et résultat de la simulation du codeur

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.

Voir le testbench et le résultat de la simulation du décodeur

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.

La structure with ... select ... when

Une adresse permet de sélectionner la donnée à transmettre parmi n données.

withaselect
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;.

Voir le testbench et le résultat de la simulation du multiplexeur

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.

  • a=0 : s=e0=0 puis 1
  • a=1 : s=e1=0
  • a=2 : s=e2=1
  • a=3 : s=e3=0
  • a=4 : s=e4=0
  • a=5 : s=e5=1
  • a=6 : s=e6=0
  • a=7 : s=e7=1

Le processus

Structure

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.

Les contrôles utilisés dans le processus

L'alternative if ... then ... else ... end if

Cette structure de contrôle effectue un traitement conditionné par une ou plusieurs expressions booléennes.

ifexpression booléenne then
-- traitement si expression booléenne vraie
else
-- traitement si expression booléenne fausse
endif;
ifexpression 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
endif;

Le traitement sélectif case ... is when ... end case;

Cette structure effectue un traitement en fonction de la valeur d'un signal

casesignal is
when valeur 1 =>
-- traitement si signal = valeur 1
when valeur 2 =>
-- traitement si signal = valeur 2
-- ...
when others =>
-- traitement par défaut
endcase;

Utilisation en combinatoire

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.

Voir le testbench et le résultat de la simulation du multiplexeur utilisant un processus

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.

Utilisation en séquentiel

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.

Le compteur

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.

Voir le testbench et le résultat de la simulation du compteur

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.

Le registre à décalage

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.

Voir le testbench et le résultat de la simulation du registre à décalage

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.

La machine à états

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;
				
Voir le testbench et le résultat de la simulation de la machine à états

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;