TD5 VHDL et CAO
Nous allons dans ce chapitre explorer les liens entre VHDL et la Conception assistée par ordinateur (C.A.O. ou plus simplement CAO) puis passer en revue des exemples utilisant des séquenceurs.
= Génération automatique de Netlist =
La majorité des logiciels de schéma modernes (frontal des logiciels de CAO) ont pour objectif de réaliser une liste des connexions électriques que l'on appellera netlist dans la suite de ce document. Ces netlists ont en général un format propriétaire mais certains outils sont capables générer ces netlists en VHDL. Il est ainsi possible à partir d'un schéma, de générer un circuit imprimé ou de choisir la logique programmable en utilisant des FPGAs ou ASICs.
La base du travail de l'électronicien reste donc toujours de rassembler des composants électroniques ayant une certaine fonctionnalité pour réaliser une nouvelle fonctionnalité. Nous allons présenter dans ce chapitre un certain nombre d'exemples.
= Décomposer une synthèse complexe =
Lorsque l'on doit réaliser un circuit complexe, il convient en général de le décomposer le problème en deux parties : la partie de commande (ou séquenceur) et la partie opérative ou chemin de données. Si cette décomposition est classique en automatisme, elle demande une certaine expérience ici car à la différence de l'automatisme la partie opérative est elle-même composée d'un ou plusieurs circuits électroniques. 
Disons pour simplifier que le niveau simple correspond à ce que l'on fait en schéma logique traditionnel : utiliser des composants simples existants et les assembler puis ajouter un séquenceur. C'est ce niveau qui va nous intéresser maintenant dans le cas très particulier où le chemin de données est réalisé par deux compteurs.
Utilisations de compteurs.
Le problème que l'on présente comme trame de travail dans cette section sera appelé compteur de passages dans toute la suite de ce chapitre. Il est décrit un tout petit peu plus loin.
Le chemin de données est constitué par un ou plusieurs compteurs. On lui ajoute une partie de commande spécifiée par un ou plusieurs diagrammes d'évolutions. Une exemple de réalisation pratique est détaillé ci-contre sous forme schématique.
Il s'agit d'un compteur de passages : deux capteurs séparés appelés Droite et Gauche font incrémenter les compteurs si des personnes (des voitures, des canettes de bière ou de tout autres objets fruit de votre imagination débordante) rentrent dans une pièce en passant d'abord par Droite puis par Gauche. Si elles sortent elles passent d'abord devant Gauche puis devant Droite et l'on décrémente les compteurs. C'est un cahier des charges pas très compliqué mais qui va nous occuper un certain temps.
Ici le chemin de données est réalisée en circuit TTL classiques et la partie commande est une GAL 20V8. Si vous disposez de FPGAs, il est possible de mettre à la fois la partie commande et le chemin de données dans ce circuit.
Remarque :
Si vous réalisez cet ensemble dans un FPGA, le compilateur doit vous donner au moins un avertissement : l'horloge est réalisée par une partie combinatoire ce qui est une pratique déconseillée. 
Il est possible de contourner le problème mentionné en remarque, en s'arrangeant pour que la partie commande fonctionne sur front montant d'une horloge et la partie opérative sur front descendant de cette même horloge. Le compteur possédera alors une entrée de validation pour compter, que la partie commande mettra à un ou pas, pour autoriser un comptage/décomptage. Cette technique pourra être utilisée dès que la partie commande et la partie opératives sont toutes deux séquentielles. Cette façon de faire n'est pas considérée par tout le monde comme une bonne méthode ; un méthode consistant à utiliser des fronts d'horloge identiques pour les deux parties est explorée dans un , et particulièrement au chapitre 4, tandis qu'une méthode utilisant la même horloge mais avec une sensibilité aux fronts différente est explorée dans la section suivante.
Nous allons maintenant plutôt nous concentrer sur les cas où la décomposition en deux parties est immédiate et donc va de soi.
=Conceptions autour de séquenceurs=
La série d'exemples donnée ci-dessous a comme objectif de préparer le lecteur à des assemblages de composants autour de divers séquenceurs. Les schémas sont donnés pour expliquer ce que l'on veut faire, mais il vous faudra faire un effort de concentration pour bien les comprendre. Parfois on donnera quelques indications VHDL mais il nous semble bien plus important à ce niveau de raisonner sur les schémas et de bien comprendre le rôle du séquenceur.
Le compteur de passages revu et corrigé.
Le compteur de passages déjà présenté, est un dispositif destiné à compter des personnes qui entrent dans un pièce, sachant qu'elles ont la possibilité de sortir. Il est naturellement possible de compter des voitures qui entrent et sortent d'un parking avec le même principe.
Un ensemble de deux capteurs infrarouge perçoit le passage d'une personne et en fonction de son sens de passage incrémente ou décrémente un compteur qui décodé affiche le nombre de personnes sur deux afficheurs 7 segments. C'est ce que l'on a déjà nommé compteur de passage et que l'on continuera à nommer ainsi.
Nous avons eu l'occasion d'exprimer des doutes sur la création d'une horloge par le séquenceur. Si l'on veut éviter cela il nous faut une architecture du type :
Remarque : on notera sur cette figure que l'horloge du séquenceur et l'horloge du chemin de données (compteur/décompteur) est la même, mais que ces deux parties sont sensibles à des fronts d'horloge différents (à cause de l'inverseur en bas). Cet inverseur peut être retiré, ce qui est d'ailleurs considéré comme une bonne pratique dans les FPGA, mais une mauvaise pratique si on le garde.
La partie notée compteur/décompteur est en fait un peu plus complexe qu'un simple compteur/décompteur puisqu'elle sort directement sur deux afficheurs sept segments. Cela signifie qu'elle comporte un décodeur, mais ces détails ne nous intéressent pas pour l'instant. Par contre il est important de savoir que :
Il nous faudra comme composants :
Seule la logique de contrôle est spécifiée à l'aide d'un graphe d'évolution.
Éléments d'implantation en VHDL.
Nous allons donner un certain nombre d'indications pour une implantation complète dans un FPGA dans cette section. Les compteurs ne seront donc plus externes et devront être décrit et câblés en VHDL.
Commençons donc par le compteur BCD cascadable 
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 compteurbcd is port (
 clk :in std_logic;
 en : in std_logic;
 init : in std_logic;
 enout : out std_logic; -- appelée rco en TD3
 s: out std_logic_vector(3 downto 0);
 du: in std_logic
end entity;
architecture behavior of compteurbcd is
 signal n : std_logic_vector(3 downto 0);
begin
 increment : process(clk) begin
 if clk'event and clk='1' then
 if init ='1' then
 n <= (others => '0');
 elsif en='1' then
 if du='1' then -- up : on compte
 if n < 9 then
 else
 end if;
 else -- down : on décompte 
 if n > 0 then
 n <= n - 1 ;
 else
 n <= "1001";
 end if;
 end if;
 end if;
 end if;
 end process;
 enableout: process(n,en,du)
 begin
 if du='1' then
 if en='1' and n=9 then
 else
 end if;
 else
 if en='1' and n=0 then
 else
 end if;
 end if;
 end process;
 s <= n;
end behavior;
On a laissé un certain nombre de points d'interrogations à remplir par le lecteur.
Nous allons maintenant nous intéresser au séquenceur.
library ieee;
use ieee.std_logic_1164.all;
use IEEE.STD_LOGIC_ARITH.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;
entity machine_a_etat is port (
 clk: in std_logic;
 init: in std_logic;
 g: in std_logic;
 d: in std_logic;
 en: out std_logic;
 du: out std_logic
end machine_a_etat;
architecture archi_machine of machine_a_etat is
type state is (S1,S2,S3,S4,S5,S6,S7);
signal next_etat,reg_etat:state;
begin
 -- premier process : met à jour le présent avec le futur
 valide_etat:process(clk)
 begin
 if rising_edge(clk) then
 if init='1' then
 reg_etat<=S1;
 else
 reg_etat<=next_etat;
 end if;
 end if;
 end process valide_etat;
 -- deuxième process : calcule l'état futur
 etat_suivant: process (reg_etat,d,g)
 begin
 case reg_etat is
 when S1=>
 if d='1' and g='0' then
 next_etat<=S2;
 elsif g='1' and d='0' then
 next_etat<=S3;
 else
 next_etat<=S1;
 end if;
 when S2=>
 when S3=>
 when S4=>next_etat<=S6;
 when S5=>n-- ?????????
 when S6=>
 when S7=>
 end case;
 end process etat_suivant;
-- partie combinatoire
 en<='0' when (reg_etat=S1 or reg_etat=S3 or reg_etat=S2 or reg_etat=S6 or reg_etat=S7) else '1';
 du<='0' when (reg_etat=S1 or reg_etat=S3 or reg_etat=S5) else '1';
end archi_machine;
où encore une fois on a laissé un peu de travail au lecteur.
Le transcodeur peut être pour un fonctionnement led allumée si entrée correspondante à 0.
En aura-t-on fini un jour avec ce compteur de passages ?
Nous allons ajouter une liaison RS232 à ce compteur de passages. Il nous faudra donc un hyperterminal pour voir s'afficher les nombres à l'intérieur. L'intérêt de cet exemple est de présenter un autre séquenceur (que celui du compteur de passage). Nous allons commencer par présenter l'ensemble complet.
Insérer un composant pour gérer la RS232.
La RS232 est décrite mais nous tenons à dire avant de continuer qu'il est inutile de comprendre la liaison en question dans tous ses détails pour comprendre la problématique évoquée ici.
Comme tout dessin nous avons suivi un certain nombre de conventions pour le réaliser.
D'autres parties ont été ajoutée par rapport à l'exercice précédent : un diviseur de fréquence par exemple. Il a une double fonction :
Construire ce composant.
Le composant présenté par le rectangle gris est maintenant bâti petit à petit.
Générateur de bauds.
La liaison série est une liaison sans horloge. Elle est cependant caractérisée par une vitesse de transmission. 
Nous désirons transmettre à 19200 bauds. Pour cela il nous faut réaliser un signal 16 fois plus rapide que la fréquence correspondante soit 307200 Hz.
Si l'on dispose d'une horloge à 50 MHz jusqu'à combien devra compter le compteur pour réaliser cette fréquence de 307200 Hz ? Combien de bit aura ce compteur ?
Réponse : M=163 et N= 8 bits.
Construction de la liaison série.
Lire pour une réalisation très orientée Xilinx. Un schéma nous rappelle comment tout cela fonctionne :
Lorsque l'entrée "write_buffer" passe à un, on écrit les données dans "data-in" et la partie matérielle se débrouille alors pour les envoyer sur la RS232.
A ce point nous allons réaliser le schéma ci-dessous :
Rappelons-nous que ce composant est inséré dans un autre non représenté... et donc que les entrées rouges sont des signaux. La donnée qui est envoyée sur la RS232 est un nombre fixe correspondant au caractère 'A'.
Comment régler votre hyperterminal ?
Réponse : gtkterm (sous LINUX) est configuré comme ci-dessous (à part peut-être le PORT) : 
PORT :/dev/ttyS1, vitesse : 19200, Parité : odd, Bits 8, Bit de stop : 1, contrôle de flux : none
Expliquer pourquoi plusieurs 'A' sont reçus dans l'hyperterminal
Réponse : parce que le signal "en" est un signal lent et qu'ainsi il resta à un pendant plusieurs périodes de l'horloge du logicore de transmission (qui elle est à 50 MHz).
Connexion au compteur de passages... et hop encore un séquenceur.
On va donc ajouter un séquenceur qui va agir sur une partie combinatoire que l'on commence à présenter.
Partie combinatoire.
Le but de la partie combinatoire est de calculer la parité et de gérer ce que l'on envoie suivant deux entrées appelées mux1 et mux2.
Le caractère '0' a comme code ASCII "0110000" sur 7 bits soit "00110000" sur 8 bit. On peut ainsi construire simplement les codes ASCII des chiffres de '0' à '9' : "0011" suivi du chiffre sur 4 bits.
Puisque l'on a choisi une parité impaire (odd en anglais) elle est calculée en inversant un ou exclusif sur tous les 8 bits. Nous pouvons remarquer que le ou exclusif de "0011" (partie constante de l'octet) est 0 et qu'ainsi seuls les 4 derniers bits sont nécessaires pour le calcul de cette parité.
Voici schématisé le travail réalisé par ce composant combinatoire dans la figure ci-contre (partie droite).
Il vous montre comment sont construits les neuf bits à envoyer, comment est calculée la parité et comment le dernier multiplexeur permet de choisir entre une valeur fixe (ici un retour chariot qui nécessitera de choisir dans configuration, CR/LF auto pour le GTKTerm) et une autre valeur.
Séquenceur.
Le séquenceur met en œuvre le composant combinatoire en positionnant correctement mux1 et mux2. Comme d'habitude, nous présentons le graphe d'évolution de ce séquenceur (figure ci-contre).
Comme nous l'avons vu, "en" est un signal très lent et le séquenceur attendra que ce signal repasse à zéro pour revenir dans son état initial S0.
A ce point ce que nous avons réalisé peut se schématiser ainsi :
... et seul un décalage de '1' entre la valeur affichée sur les deux digits et la valeur reçue dans l'hyperterminal nous indique une petite erreur.
deuxième séquenceur.
Le défaut constaté dans la section précédente est dû au fait que "en" arrive trop tôt. En effet, quand il est positionné à 1 le compteur sera incrémenté seulement sur le front suivant (de l'horloge très lente), c'est à dire quand "en" retombe à 0.
Le nouveau séquenceur partira encore avec "en", mais attendra qu'il repasse à 0 avant de commencer à envoyer dans la RS232.
Indication VHDL.
Le programme VHDL correspondant au schéma ci-dessus aura bien évidemment la structure suivante :
entity Travail_a_realiser is
 port (clk,reset : in std_logic;
 data : in std_logic_vector(7 downto 0);
 en : in std_logic; --relié au fil en
 serial_out : out std_logic --sortie série
end Travail_a_realiser;
architecture Travail_a_realiser of Travail_a_realiser is 
component uart9_tx is
 Port ( data_in : in std_logic_vector(8 downto 0);
 write_buffer : in std_logic;
 reset_buffer : in std_logic;
 en_16_x_baud : in std_logic;
 serial_out : out std_logic;
 buffer_full : out std_logic;
 buffer_half_full : out std_logic;
 clk : in std_logic);
end component;
component mod_m_counter is 
 generic(
 N : integer := 4; -- number of bits
 M : integer := 10 -- mod-M
 port (
 clk, reset : in std_logic;
 max_tick : out std_logic;
 q : out std_logic_vector(N-1 downto 0)
end component;
component combinatoire is
 port (data_in : in std_logic_vector(7 downto 0);
 mux1, mux2 : in std_logic;
 data_out : out std_logic_vector(8 downto 0)
end component;
component sequenceur is
 port(
 clk,init,en : in std_logic; 
 mux1,mux2,write_buffer : out std_logic -- sorties
end component;
signal en_tick : std_logic;
signal q : std_logic_vector(8 downto 0);
signal s_data : std_logic_vector(8 downto 0);
signal s_write_buffer, s_mux1, s_mux2 : std_logic;
begin
 -- Set baud rate to 19200 for the UART communications
 -- Requires en_16_x_baud to be 307200Hz which is a single cycle pulse every 163 cycles at 50MHz 
 baud : mod_m_counter 
 generic map(N=>8,M=>163)
 port map(clk => clk,reset =>reset,q=>q,max_tick=>en_tick);
 --sequencer : des que "en" je prépare les trois octets à envoyer et remplit le buffer au fur et à mesure
 sequencer : sequenceur port map( 
 clk => clk,
 init => reset,
 en => en,
 mux1 => s_mux1,
 mux2 => s_mux2,
 write_buffer => s_write_buffer 
 comb: combinatoire port map(
 data_in => data,
 data_out => s_data,
 mux1 => s_mux1,
 mux2 => s_mux2); 
 uart: uart9_tx port map(data_in => s_data,
 write_buffer => s_write_buffer,
 reset_buffer => reset,
 en_16_x_baud => en_tick,
 serial_out => serial_out,
 buffer_full => open,
 buffer_half_full => open,
 clk => clk); 
end Travail_a_realiser;
La réalisation de cet ensemble se fait de manière systématique. Ce qui peut vous surprendre à ce stade, c'est le "compteur modulo" que nous n'avons pas détaillé outre mesure.
Et un petit dernier séquenceur pour la route....
Nous allons utiliser un séquenceur dans un tout autre contexte pour terminer cet ensemble d'exemples.
L'envoi de données par la RS232 va maintenant servir à remplir une mémoire RAM avec des valeurs prédéterminés. Ces valeurs seront ensuite envoyées sur des LEDs pour réaliser divers chenillards à l'aide d'un compteur.
Travail préliminaire.
On désire réaliser un ensemble comprenant un diviseur de fréquence (déjà rencontré), un compteur 4 bits binaire possédant une entrée "en" de validation" et une entrée "init" (devinez pourquoi faire ?) et une mémoire.
Préparation.
Pour éviter le changement des options de compilation (décrit dans une autre section) pour qu'ISE infère correctement une mémoire BRAM (Block RAM) nous allons utiliser un composant faisant partie des librairies Xilinx. Ce composant s'appelle RAM16X8S. Le meilleur moyen d'en trouver la documentation est le suivant :
Vous avez en bas de la page de la documentation un exemple d'instanciation de ce composant en VHDL (puis en verilog, ce qui ne nous intéresse pas). Ce que l'on vous demande de faire à ce stade est de comprendre comment cette RAM est initialisée.
Calculer les valeurs prédéfinies à mettre dans la RAM pour réaliser un chenillard aller et retour d'une LED.
Travail à réaliser.
Malheureusement le travail décrit ci-dessous est très lié au fabricant Xilinx. Si quelqu'un peu faire un travail similaire chez Altera ce sera sans refus.
On vous demande d'importer le diviseur de fréquence de ce qui a été fait précédemment, de réaliser un compteur sur 4 bits avec entrées "init" et "en".
Ajouter la mémoire RAM16X8S (dépendant de Xilinx) correctement initialisée, sans oublier les lignes :
Library UNISIM;
use UNISIM.vcomponents.all;
avant votre entité "Travail_A_Realiser".
Le schéma de principe est présenté ci-dessous :
Cet ensemble est donc destiné à réaliser un chenillard visuel. C'est pour cela qu'on utilise un diviseur de fréquence : notre œil ne perçoit rien au-delà de 20Hz). Le compteur est destiné à générer les adresses (de la mémoire RAM) qui sort une suite de valeurs destinées à l'effet visuel.
Modification du contenu de la RAM.
On désire maintenant s'intéresser à la possibilité de modifier le contenu de cette mémoire à l'aide d'une liaison RS232. Par rapport au TP sur le compteur de passage, la liaison devra être dans l'autre sens : c'est le PC qui maintenant va fournir des valeurs au FPGA qui finiront dans une RAM (le problème des RAM dans les FPGAs est évoqué dans une autre section du chapitre 2). La réalisation de cette liaison se fait pratiquement comme dans la section précédente ("_rx" au lieu de "_tx") :
Un séquenceur sera responsable dès qu'il y a une donnée reçue ("buffer_full") de lire "data_out" et de l'écrire dans la mémoire puis d'incrémenter un compteur (d'adresses). Nous aurons en final deux compteurs : un pour lire le contenu de la mémoire et un pour écrire. Ils devront donc se partager le bus d'adresse, cela pourra se faire avec un multiplexeur dont l'entrée sera gérée par le séquenceur. Quand le séquenceur sera en train d'écrire dans la mémoire, il devra bloquer le compteur de lecture.
Travail à réaliser.
Comme habituellement le travail à réaliser vous est donné sous forme schématique. Vous devez l'implanter en VHDL avec des PORT MAP.
Il y a beaucoup de travail, mais si vous respectez les noms des signaux (en rouge) et les noms de chacune des entrées et sorties de chaque bloc, le travail se fait rapidement. Les deux compteurs du haut sont identiques et sont simplement les compteurs de l'exercice précédent de ce document.
Le multiplexeur est classique et déjà présenté, mais vous avez une chance sur deux de le faire fonctionner comme il faut : comme le séquenceur ci-dessous vous l'indique, quand sel = 1 vous lisez e0 pour l'amener sur la sortie du multiplexeur !
Le plus long à concevoir est indiscutablement le séquenceur. Nous avons un petit peu hésité à le donner, il aurait été intéressant de vous le faire chercher. Son principe est de lancer le remplissage de la mémoire dès que le buffer est plein. Cette technique fonctionne correctement parce que nous avons justement 16 valeurs à envoyer à la RAM. Si vous avez moins de 16 valeurs à envoyer, n'attendez pas que le buffer soit plein car il ne le sera jamais. Vous pouvez commencer à lire dès qu'il y a une valeur dans le buffer, technique qui doit aussi être utilisée dans le cas où il y a plus de 16 valeurs. Dans ces cas, un des problème est de savoir quand la transmission est finie.
Une fois que toute la partie matérielle est en place, il vous faut réaliser un fichier binaire de 16 valeurs pour remplir la RAM. On vous propose le fichier C++ suivant :
/*** tpmemo.cpp ***/ 
char valhex(char aconv){ 
 static char hex[17]="0123456789ABCDEF"; 
 char i; 
 i=0; 
 while(aconv!=hex[i]) i++; 
 return i; 
main() { 
 FILE *sortie; 
 static char hexa[17]="0123456789ABCDEF"; 
 char trame[128],nomfichier[34]; 
 unsigned char nval,i,tab[255]; 
 printf(" ****************************************************"); 
 printf("\n ***** Conversion valeur -> fichier Intel HEX *****"); 
 printf("\n ******** TP programmation de memoires ************"); 
 printf("\n ******** Version 0.1 (S. Moutou & Cie) ***********"); 
 printf("\n ****************************************************"); 
 printf("\n\nCombien de valeurs voulez-vous entrer ? "); 
 scanf("%d",&nval); 
 for (i=0;i<nval;i++) { 
 printf("%d° valeur (en hexadecimal) : ",i+1); 
 scanf("%x",&tab[i]); 
 printf ("\nQuel est votre nom de fichier de sauvegarde ? (extension .bin) "); 
 scanf("%s",nomfichier); 
 sortie=fopen(nomfichier,"w"); 
 fwrite(tab,1,16,sortie); 
 fclose(sortie);
que l'on compile avec g++ tpmemo.cpp -o tpmemo
On le lance alors avec ./tpmemo :
Tester avec gtkterm configuré comme ci-dessous : 
PORT :/dev/ttyS0, vitesse : 19200, Parité : odd, Bits 8, Bit de stop : 1, contrôle de flux : none
Fichier -> envoi de fichier brut en cherchant votre fichier binaire créé.
Si tout fonctionne, vous obtiendrez un nouveau chenillard.