TD3 VHDL Compteurs et registres
La logique abordée dans ce chapitre est parfois appelée logique séquentielle régulière. 
Les compteurs sont des éléments très utiles en VHDL. Il permettent de gérer tout ce qui est temporisation et évidemment le comptage.
Le compteur simple.
Il est possible d'utiliser un style "case when" (présenté en début de ce livre) pour programmer un compteur. Cela devient vite fastidieux cependant, lorsque le nombre de bits du compteur augmente.
Exercice 1.
Combien d'états comporte un compteur de n bits et donc combien de lignes pour chacun des « case » ? Application numérique : prendre n=16.
Eviter une programmation trop fastidieuse.
L'idéal serait donc de pouvoir écrire quelque chose du style
 compteur <= compteur + 1;
Cela peut se faire en respectant les conditions suivantes :
Avec XILINX cela se fait avec les lignes (devant chacune des entités concernées) :
library ieee;
use ieee.std_logic_1164.all;
use ieee.std_logic_arith.all; -- WARP : use work.std_arith.all;
use ieee.std_logic_unsigned.all;
Mais cette façon de faire n'est pas portable comme le montre le commentaire ci-dessus. Elle reste pourtant très simple par rapport à la façon portable que l'on va présenter maintenant. Les différences commencent par l'utilisation d'autres librairies. Il s'agit maintenant d'utiliser :
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all; 
Jusque là les changements sont limités mais tout commence à se dégrader avec la suite : remplacer systématiquement
compteur <= compteur+1;
par
compteur <= std_logic_vector(unsigned(compteur)+1);
Oui, nous savons bien que c'est douloureux, mais comme on dit, on a rien sans rien.
Remarque : La bonne solution est en fait de déclarer le signal de la manière suivante :
signal compteur : unsigned;
et de l'incrémenter avec
compteur <= compteur + 1;
De cette façon votre code sera portable car respectant le standard défini par l'IEEE (malgré leurs noms les bibliothèques ieee.std_logic_arith, ieee.std_logic_unsigned et ieee.std_logic_signed ne font pas partie du standard IEEE mais sont des extensions propriétaires développées par Synopsys et dont l'implémentation peut varier suivant les outils de développement).
Voici schématiquement le calcul de l'état futur en fonction de l'état présent pour un compteur :
Remarquez que la fonction de calcul s'écrit de manière très simple à l'aide de l'opérateur d'addition.
Exercice 2.
Vous disposez d'une horloge rapide et vous voulez en réaliser une plus lente dont la fréquence est divisée par 32768. Proposez un compteur avec comme entrée horloge et comme sortie h_lente (toutes deux sur un bit). Le compteur intermédiaire sera réalisé par un signal.
Possibilité d'utiliser un signal de type integer.
Plutôt que de déclarer des std_logic_vector vous pouvez utiliser le type integer. Dans ce cas le compilateur peut avoir des problèmes pour trouver le nombre de bits nécessaire pour le compteur. Il faudra donc utiliser une comparaison supplémentaire :
architecture a_cmpt of cmpt is
signal Count : integer :=0; 
 process begin
 if (Clk'event and Clk='0') then
 if (Count=7) then Count <=0; -- sur 3 bits
 else Count <=Count+1;
 end if;
 end if;
 end process;
end a_cmpt;
Nous n'utiliserons pas ce type de programmation dans ce document. Il est juste donné par souci d'exhaustivité.
Compteur avec Remise à Zéro (RAZ).
L'entrée RAZ( Remise à Zéro) (Reset en anglais) sur un compteur est une entrée qui permet de mettre la valeur du compteur à 0. Elle peut être synchrone (prise en compte seulement sur front d'horloge) ou asynchrone. Pour la suite de la section nous utiliserons l'entité que l'on donne maintenant :
library ieee;
use ieee.std_logic_1164.all;
use ieee.std_logic_arith.all; 
use ieee.std_logic_unsigned.all;
ENTITY Compteur IS
PORT (
 clk,raz :IN std_logic;
 q : BUFFER std_logic_vector(3 downto 0));
END Compteur;
Cette entité va nous permettre de présenter la technique synchrone et asynchrone (process seulement) :
-- ******* methode synchrone **********
PROCESS(clk) BEGIN
 IF clk'event and clk='1' THEN
 IF raz='1' THEN
 q <= (OTHERS=>'0'); -- ou q <= "0000";
 ELSE
 q <= q + 1;
 END IF;
 END IF;
END PROCESS;
et
-- ******** methode asynchrone *********
PROCESS(clk,raz) BEGIN
 IF raz='1' THEN
 q<=(OTHERS=>'0'); -- ou q <= "0000";
 ELSIF clk'event and clk='1' THEN
 q <= q + 1;
 END IF;
END PROCESS;
On peut remarquer que la liste de sensibilité n'est pas la même dans les deux cas : "clk" pour le synchrone et "clk,raz" pour l'asynchrone.
Remarque sur le type BUFFER :
Xilinx déconseille d'utiliser le type BUFFER dans une entité, particulièrement quand il s'agit d'un signal interne au FPGA (pas une sortie physique). Il conseille plutôt d'utiliser un signal pour compter et une sortie spécifique pour sortir le ou les bits utiles. Pour l'initialisation synchrone, par exemple, le programme complet sera la suivant :
library ieee;
use ieee.std_logic_1164.all;
use ieee.std_logic_arith.all; 
use ieee.std_logic_unsigned.all;
ENTITY Compteur IS
PORT (
 clk,raz :IN std_logic;
 qs : OUT std_logic_vector(3 downto 0)); -- sortie véritable
END Compteur;
ARCHITECTURE aCmpt OF Compteur IS
SIGNAL q : std_logic_vector(3 downto 0); -- signal intermédiaire
BEGIN
-- RAZ synchrone
 PROCESS(clk) BEGIN
 IF clk'event and clk='1' THEN
 IF raz='1' THEN
 q<=(OTHERS=>'0');
 ELSE
 q <= q + 1;
 END IF;
 END IF;
 END PROCESS;
-- toujours faire :
 qs <= q;
END aCmpt;
On s'efforcera de respecter cette mise en garde par la suite.
Exercice 3.
Réaliser un compteur avec SET et RESET synchrones et asynchrones.
Modifier ce compteur pour qu'il compte jusqu'à 24.
Compteur avec chargement parallèle.
Le chargement parallèle est en général asynchrone dans les circuits existants. Nous allons le conserver comme tel :
library ieee;
use ieee.std_logic_1164.all;
use ieee.std_logic_arith.all; 
use ieee.std_logic_unsigned.all;
ENTITY Compteur IS
PORT (
 clk,load :IN std_logic;
 qs : OUT std_logic_vector(3 downto 0);
 qe : IN std_logic_vector(3 downto 0));
END Compteur;
ARCHITECTURE acmpt OF Compteur IS 
SIGNAL q :std_logic_vector(3 downto 0);
BEGIN
 qs <= q;
 PROCESS(clk,load) BEGIN
 IF load='1' THEN
 q<=qe; 
 -- ou q<=31; valeur predefinie
 ELSIF clk'event and clk='1' THEN
 q<=q+1;
 END IF;
 END PROCESS;
END acmpt;
Compteur 4 bits BCD avec validation d'horloge.
Pour terminer ce chapitre on présente un compteur BCD, c'est à dire qui compte de 0 à 9 qui est emprunté au WikiBook anglais VHDL for FPGA Design.
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_ARITH.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;
entity Counter2_VHDL is
 port( Clock_enable: in std_logic;
 Clock: in std_logic;
 Reset: in std_logic;
 Output: out std_logic_vector(3 downto 0));
end Counter2_VHDL;
architecture Behavioral of Counter2_VHDL is
 signal temp: std_logic_vector(3 downto 0);
begin 
 process(Clock,Reset) begin
 if Reset='1' then
 temp <= "0000";
 elsif(Clock'event and Clock='1') then
 if Clock_enable='0' then
 if temp="1001" then
 temp<="0000";
 else
 temp <= temp + 1;
 end if;
 else
 temp <= temp;
 end if;
 end if;
 end process;
 Output <= temp;
end Behavioral;
Résultats de simulation.
Vous pouvez remarquer que dans cet exemple c'est Output(3) qui est le poids faible, ce qui est conforme à sa déclaration "0 to 3". Je préfère quant à moi utiliser un "3 downto 0" ce qui laisse le poids faible avec le numéro 0.
Compteur BCD cascadable.
Il n'est pas difficile de trouver le code correspondant sur Internet. Voici par exemple :
-- Fichier : compteur_bcd10.vhdl
-- Description : Compteur BCD de 0 a 9
library IEEE; -- On inclus la librairie IEEE
use IEEE.std_logic_1164.all;
use IEEE.STD_LOGIC_ARITH.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;
ENTITY compteur_bcd10 IS -- Définition des entrées/sorties
-- !!!!! ATTENTION le poids fort est en 0 !!!!!!!!!!!!
 PORT(clk, en, clr : IN std_logic;
 rco : OUT std_logic;
 q : OUT INTEGER RANGE 0 TO 9);
END compteur_bcd10;
ARCHITECTURE behav OF compteur_bcd10 IS
SIGNAL cnt : INTEGER RANGE 0 TO 9; -- signal interne
BEGIN
 q <= cnt; -- q, la sortie vaut la valeur du compte actuel en tout temps
 PROCESS(clk, clr) -- Process sensible à l’horloge et au “clear”
 BEGIN
 IF (clr='1') THEN -- “clear” asynchrone
 cnt <= 0;
 --rco <= ’0’;
 ELSIF (clk'EVENT AND clk='1') THEN -- au front montant
 IF (en='1') THEN -- si enable est à 1
 IF (cnt = 9) THEN -- Si on atteint 9 on fait un rco
 cnt <= 0; -- et on remet le compteur a 0
 --rco <= ’1’;
 ELSE -- Sinon on compte
 cnt <= cnt + 1;
 --rco <= ’0’;
 END IF;
 END IF;
 END IF;
 END PROCESS;
 PROCESS(cnt,en) BEGIN
 IF cnt=9 and en='1' then 
 rco <='1';
 ELSE
 rco <='0';
 END IF;
 END PROCESS;
END behav;
Compteur décompteur BCD cascadable.
Une entrée supplémentaire doit être ajoutée. On l'appellera "ud" pour "Down/up" qui permet de choisir entre un comptage et un décomptage. Voici par exemple :
-- description du composant compteur/decompteur cascadable
library ieee;
use ieee.std_logic_1164.all;
use IEEE.STD_LOGIC_ARITH.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;
entity compteurbcd is port (
 clk :in std_logic;
 en : in std_logic;
 init : in std_logic;
--ud=1 up, ud=0 down
 ud : in std_logic;
 enout : out std_logic;
 s: out std_logic_vector(3 downto 0)
end entity;
architecture behavior of compteurbcd is
 signal n : std_logic_vector(3 downto 0);
 begin
 changement : process(clk) begin
 if clk'event and clk='1' then
 if init ='1' then
 n <= (others => '0');
 elsif en='1' then
 if ud = '1' then --up
 if n<9 then
 n <= n + 1 ;
 else
 n <= (others => '0');
 end if;
 else -- down
 if n > 0 then
 n <= n - 1 ;
 else
 n <= "1001";
 end if;
 end if;
 end if; 
 end if;
 end process;
-- gestion enout
 enout <= '1' when en='1' and ud='1' and n=9 else
 '1' when en='1' and ud='0' and n=0 else
 '0';
-- toujours
 s <= n;
end behavior;
Comme vous pouvez le voir dans le code, ud=1 permet un comptage tandis que ud=0 permet un décomptage.
Nous avons choisi une autre manière de gérer la sortie "enout" que dans la section précédente : utilisation d'un when else au lieu d'un if. Cela permet d'éviter le process. En effet la gestion avec un if devrait s'écrire :
 enableout: process(n,en,ud)
 begin
 if en='1' then
 if ud='1' and n=9 then
 enout<= '1';
 elsif ud='0' and n=0 then
 enout<='1';
 else 
 enout<='0';
 end if;
 else -- ajouté 4/5/2011 pour eviter comptage intempestif sur dizaine
 enout<='0';
 end if;
 end process;
En voici pour finir une version avec la partie combinatoire réalisée avec un with select when :
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_ARITH.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;
entity CounterBCD is
 port( EN: in std_logic;
 Clock: in std_logic;
 Reset: in std_logic;
 du: in std_logic;
 ENO : out std_logic;
 Output: out std_logic_vector(3 downto 0));
end CounterBCD;
architecture Behavioral of CounterBCD is
 signal cmpt: std_logic_vector(3 downto 0);
 signal s_en_cmpt: std_logic_vector(5 downto 0);
begin process(Clock,Reset)
 begin
 if Reset='1' then
 cmpt <= "0000";
 elsif(rising_edge(Clock)) then
 if EN='1' then
 if du='1' then
 if cmpt="1001" then
 cmpt<="0000";
 else
 cmpt <= cmpt + 1;
 end if;
 else 
 if cmpt="0000" then
 cmpt<="1001";
 else
 cmpt <= cmpt - 1;
 end if; 
 end if;
 end if;
 end if;
 end process;
 Output <= cmpt;
 s_en_cmpt <= en & du & cmpt;
 with s_en_cmpt select
 ENO <= '1' when "111001",
 '1' when "100000",
 '0' when others;
end Behavioral;
Temporisation.
L'application la plus courante des compteurs est la temporisation.
Exercice 4 : réalisation des signaux de synchronisation d'un écran VGA.
On désire réaliser les deux signaux hsynch et vsynch nécessaire au bon fonctionnement d'un écran VGA. Ils sont caractérisés par les durées suivantes :
On peut distinguer sur cette spécification un exemple de signaux rouge, vert et bleu en haut, puis les deux signaux qui nous intéressent vraiment "hsynch" et "vsynch". Techniquement la réalisation de ces deux signaux est faite à l'aide de deux compteurs de la manière suivante :
On vous demande de répondre aux questions suivantes :
1°) Calculer la période de l'horloge à 25MHz (P88).
2°) Le compteur 0 -> XXX commence à compter au début des 25,6 μs. Jusqu'à combien doit-il compter pour réaliser ces 25,6 μs?
3°) Il lui faut réaliser ensuite 0,64 μs, jusqu'à combien doit-il compter ? Il lui faut réaliser ensuite 3,8 ms, jusqu'à combien doit-il compter ? Il lui faut réaliser ensuite la période complète 31,75 ms, jusqu'à combien doit-il compter ? (C'est la valeur de XXX à un près) On arrondit en général XXX à 799.
Déduire de tout cela la valeur de ZZZ et TTT.
4°) Ce sont les hsynch qui incrémentent le compteur 0->YYY. Quelle est la période correspondante (si l'on prend XXX=799) ?
5°) Combien de temps dure la période des 480 lignes avec le résultat de la question 4° (à comparer à 15,24 ms de la spécification VGA).
6°) À l'aide du résultat de 4°) trouver de combien doit compter le compteur pour réaliser le temps de 0,35 ms.
7°) À l'aide du résultat de 4°) trouver de combien doit compter le compteur pour réaliser le temps de 64 ms.
8°) À l'aide du résultat de 4°) trouver de combien doit compter le compteur pour réaliser la période complète de 16,6 ms. Est-il normal d'arrondir à 520 ?
9°) En déduire les valeurs de UUU et VVV ?
Registre à décalage.
L'opérateur de concaténation "&" est utile pour ce genre de registre. Voici un exemple de registre à décalage vers la droite (vers les poids faible) : le bit de poids faible (indice 0) est perdu, le bit d'entrée est ajouté aux bits restants (indices 7 à 1).
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
entity ShiftReg is
 port(clk,entree : in std_logic;
 q : out std_logic_vector(7 downto 0));
end ShiftReg;
architecture aShiftReg of ShiftReg is
 signal dataq : std_logic_vector(7 downto 0);
begin
 process(clk) begin
 if clk'event and clk='0' then
-- c'est ici que l'on concatène le bit d'entrée au registre décalé
 dataq <= entree & dataq(7 downto 1);
 end if;
 end process;
 process(dataq)begin
 q<=dataq;
 end process;
end aShiftReg;
Autre exemple de registre à décalage 4 bits.
Cet exemple est encore tiré du WikiBook VHDL for FPGA Design.
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_ARITH.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;
entity Shift_register_VHDL is
 port( Clock: in std_logic;
 L,w: in std_logic;
 Output: out std_logic_vector(3 downto 0);
 Input: in std_logic_vector( 3 downto 0));
end Shift_register_VHDL;
architecture Behavioral of Shift_register_VHDL is
 signal temp: std_logic_vector(3 downto 0);
begin
 process
 begin
 wait until Clock'event and Clock='1';
 if L='1' then
 temp <= Input;
 else 
 for i in 0 to 2 loop
 temp(i) <= temp(i+1);
 end loop;
 temp(3) <= w;
 end if;
 end process;
 Output <= temp;
end Behavioral;
Résultats de simulation.
Et maintenant quelques définitions avant de passer à l'exercice suivant.
Nous proposons une méthode qui utilise un registre à décalage à travers un exercice.
Exercice 5 (filtrage de rebonds et/ou aléas).
L'architecture peut être décrite comme suit : un registre à décalage 4 bits sensible aux fronts descendants de T9, une bascule D qui mémorise l'état de notre sortie et une partie combinatoire qui génère un 1 à l'entrée de la bascule D dès que le registre est rempli par 4 bits à 1 et que sortie vaut 0. 
Cette figure peut être expliquée de la manière suivante :
Compléter les chronogrammes ci-dessous.
Ceci termine le TD3.