Very High Speed Integrated Circuit Hardware Description Language/Travail pratique/TP 4

Début de la boite de navigation du travail pratique


L'objectif de ce troisième TP est enfin de lire les scan-codes d'un clavier. Nous allons construire petit à petit un ensemble de composants capable de cet exploit en partant du travail déjà réalisé dans le TP précédent. Nos techniques de conceptions seront identiques, c'est-à-dire facile à comprendre mais pas très professionnelles. Pour rendre ce travail un peu plus conformes aux règles de construction des horloges, nous avons ajouté en fin de ce chapitre une section spécifique (Mise en conformité des horloges). Cette section aura toute son importance pour des élèves ingénieurs mais pourra être laissée de côté par les étudiants L1 et L2.

TP 4
Image logo représentative de la faculté
T.P. no 4
Leçon : Very High Speed Integrated Circuit Hardware Description Language

TP de niveau 15.

Précédent :TP 3
Suivant :TP 5
En raison de limitations techniques, la typographie souhaitable du titre, « Travail pratique : TP 4
Very High Speed Integrated Circuit Hardware Description Language/Travail pratique/TP 4
 », n'a pu être restituée correctement ci-dessus.

Introduction modifier

On a retenu du TP précédent que l'appui sur une touche était compliqué car on recevait plus de fronts d'horloge (qui étaient comptés dans le TP précédent en question ) que prévu. On en attendait 11 et finalement on en recevait 33.

Techniquement cela peut s'expliquer par le fonctionnement du clavier. Lorsqu'on appui sur une touche, on finit par la relâcher et c’est ce relâchement qui envoie lui aussi des codes. On résume ci-dessous le fonctionnement des claviers à travers l'histoire (tiré de Wikipédia anglais) adaptée au clavier AZERTY :

IBM PS/2 scancodes (Clavier AZERTY)
Touche set 1 (IBM PC XT) set 2 (IBM PC AT) set 3 (IBM 3270 PC)
Appui relâchement Appui relâchement Appui relâchement
Q (lettre normale) 1E 9E 1C F0 1C 1C F0 1C
Retour Chariot / Entrée (clavier principal) 1C 9C 5A F0 5A 5A F0 5A
Retour Chariot / Entrée (clavier numérique) E0 1C E0 9C E0 5A E0 F0 5A 79 F0 79
Touche Windows gauche E0 5B E0 DB E0 1F E0 F0 1F 8B F0 8B
Touche Windows droite E0 5C E0 DC E0 27 E0 F0 27 8C F0 8C
  On s'intéressera par la suite au clavier "set2 (BM PC AT)" du tableau ci-dessus.


Lecture partielle d'un clavier PS/2 modifier

L'objectif de ce TP est de réaliser la lecture des codes (scancodes) d'un clavier sur un port PS/2. À la différence du TP précédent, on doit retrouver maintenant le scancode de la touche appuyée et non plus le nombre de fronts d'horloge que cet appui a généré.

Nous allons compléter le TP précédent, c'est-à-dire utiliser l'horloge échantillonnée (notée "Sortie" dans le schéma ci-dessous) comme horloge du compteur et du registre à décalage. Cette façon de faire n’est pas bonne en tous points, mais elle est simple et conduit à un séquenceur à trois états (schématisé par un GRAFCET dans la figure ci-dessous).

Travail à réaliser et sa correction modifier

Comme dans les travaux précédents le travail à réaliser est donné sous le forme d'un schéma.

Schéma à réaliser modifier

On donne le schéma global de ce que vous allez réaliser ci-dessous. Comme d'habitude le travail à réaliser est présenté dans un rectangle bleu clair. Ce rectangle permet de déterminer en un seul coup d'œil les entrées et les sorties. Le rectangle gris représente quant à lui le travail déjà réalisé dans le TP1.

 
Lecture simplifiée des scancodes d'un clavier PS2

Par rapport au TP précédent on garde le compteur sur 8 bits mais il est actif sur front descendant maintenant, et toujours commandé par « sortie » qui commande aussi un registre à décalage de 11 bits. Après un transfert correct on trouvera dans ce registre les 8 bits de données, le bit de start, le bit de stop et le bit de parité. Seuls les 8 bits intéressants (Q1 ... Q8) seront transférés dans le registre d'affichage. On a ajouté d’autre part un circuit combinatoire détectant l'état cmpt=11. Sa sortie est utilisée par un séquenceur décrit par un grafcet. Son objectif est qu'une fois les 11 fronts d'horloge détectés, le transfert est lancé pour les 8 bits intéressants dans le registre d'affichage, puis il remet le compteur à 0 afin d'attendre le début d'un autre transfert.


Il est clair que la synthèse demandée est complexe. Comme toute synthèse complexe elle peut être décomposée en deux parties distinctes (voir ici pour des compléments) :

  • un séquenceur
  • une partie opérative ou chemin de données

On remarquera que puisque ces deux parties utilisent une horloge commune, le séquenceur fonctionne sur front montant tandis que la partie opérative fonctionne sur front descendant.

Quelques scancodes :

scancode hexadécimal pour clavier AZERTY
Touche Scancode Touche Scancode Touche Scancode
b 0x32 d 0x23 f 0x2B
g 0x34 h 0x33 j 0x3B
q 0x1C s 0x1B t 0x2C

Éléments de VHDL donnés pour commencer votre travail modifier

L'entité globale est :

entity tp3 is
  port(
    PS2_Clk_M16, PS2_Data_M15,Clk_T9, reset_L14 : in std_logic;
    sorties_aff : out std_logic_vector(6 downto 0);
    aff : out std_logic_vector(3 downto 0));
end tp3;

L'ensemble des composants à réaliser est maintenant :

  component Counter8 -- compteur {{Unité|8|bits}}
	port(horloge,reset : in std_logic;
	 q : out std_logic_vector(7 downto 0));
  end component;
  component ShiftReg -- registre a decalage {{Unité|8|bits}}
    port(clk,entree : in std_logic;
         q : out std_logic_vector(7 downto 0));
  end component;
  component ShiftReg11 -- registre a decalage {{Unité|11|bits}} mais seuls {{Unité|8|bits}} sont sortis !
    port(clk,entree : in std_logic;
         q8 : out std_logic_vector(7 downto 0));
  end component;
  component comb -- filtrage des aleats
    port(
      entrees : in std_logic_vector(7 downto 0);
      eQ : in std_logic;
      F : out std_logic);
  end component;
  component dflipflop --bascule D
    port(
      d,clk : in std_logic;
      q : out std_logic);
  end component;
  component tp1 -- realise en TP 1
    port (
       clk : in std_logic;
    entrees : in std_logic_vector(7 downto 0);
    s7segs : out std_logic_vector(6 downto 0);
    aff : out std_logic_vector(3 downto 0));
  end component;
  component seq -- séquenceur ({{Abréviation|GRAFCET|graphe fonctionnel de commande par étapes et transitions}})
  port(
    clk, cmpt_is_11,init : in std_logic;
    reset,transfert : out std_logic
  );
  end component;
  component compare -- comparateur
  port (
    entrees : in std_logic_vector(7 downto 0);
    s_cmpt_is_11 : out std_logic
  );
  end component;

On utilisera complètement le travail fait en TP 1 et partiellement celui de TP 2.

Questions modifier

Relever certains scancodes en plus de ceux fournis en exemple pour les tests, et en particulier celui des flèches directionnelles.

Travail à réaliser sans sa correction modifier

La correction donnée ci-dessus ne correspond pas tout à fait au schéma.

Ajout du registre d'affichage modifier

La correction donnée fonctionne correctement mais on s'est passé du registre d'affichage.

Exercice modifier

On vous demande de créer et d'ajouter ce registre d'affichage.


Gestion des scancodes 16 bits modifier

On vous demande de modifier le séquenceur (GRAFCET) pour gérer l'envoi des scancodes sur deux octets ainsi que leurs affichage. Le principe est le suivant :

  • ajouter un deuxième registre d'affichage
  • modifier le GRAFCET de séquencement

Le principe du GRAFCET est le suivant :

  • retirer l'action de reset de la troisième étape
  • à partir de la troisième étape, on attend environ 100 ms et on va dans une quatrième étape qui réalise le reset et boucle au début du GRAFCET
  • l'autre branche de la troisième étape sera conditionnée par compteur=12 puis on attend compteur=22 pour faire un transfert dans le deuxième registre d'affichage, puis le reset.

Un schéma sera plus parlant :

 
Lecture de scancodes sur 16 bits

Dans cette figure on a pris soin de montrer en rouge les changements matériels auxquels il faut ajouter naturellement le GRAFCET. Dans le GRAFCET il faut utiliser un timer. Le timer est laissé sous la forme d'une variable "t" dans le GRAFCET. On a déjà dit qu’il est bon de choisir une valeur proche de 100 ms. Cela sous-entend donc que si un deuxième octet est envoyé par le clavier ce sera fait en moins de 100 ms.

Nous montrons dans la solution partielle ci-dessous comment il est possible de réaliser cela.

Estimez la valeur "t" du timer qui a été choisie dans la solution ci-dessus.

Mise en conformité des horloges modifier

 

Cette section est difficile à comprendre. Même si elle ne fait intervenir que des notions du niveau indiqué, il est conseillé d'avoir du recul sur les notions présentées pour bien assimiler ce qui suit. Cependant, ce contenu n'est pas fondamental et peut être sauté en première lecture.

Nous avons traité dans le chapitre VHDL et machines à états algorithmiques le problème des horloges. Dans ce chapitre (et le précédent) nous avons utilisé sans discernement des horloges sur front montants et des horloges sur fronts descendants parce qu’il est souvent plus facile d’en comprendre le fonctionnement. Disons que cette facilité de compréhension est effective si vous séparez correctement le séquenceur et la partie opérative (ou chemin de données) ce qui n'a pas toujours été fait. Qu'en est-il du filtrage des fronts de l'horloge PS2 par exemple ?

Si l’on veut faire ce travail dans les règles de l'art avec des exigences professionnelles il serait certainement plus judicieux de n'utiliser que des horloges sur front montant.

Début d’un principe
Fin du principe

Graphe d'états modifier

 
Graphe d'état de la lecture PS2

Rappelons que le meilleur moyen de spécifier un montage qui respecte les horloges sont les (en) FSMD (Finite State Machine with Data path) utilisés par les anglo-saxons. Nous, Français, sommes plus habitués au GRAFCET et à sa version simplifiée : le graphe d'états (lire ou relire le chapitre description par graphe d'état du cours de logique séquentielle). La particularité de ce graphe d'états est que les actions seront elles aussi séquentielles et c’est ce fait que leur programmation en VHDL sera parfois délicate.

Nous allons commencer par présenter notre problème sous la forme d'un graphe d'états.

Cette figure est facile à lire et nous l'implanterons telle quelle. Il faut noter cependant qu’il est possible de retirer tous les états qui précèdent les transitions "=1", c'est-à-dire qui se font systématiquement. Comme nous allons utiliser une horloge à 50 MHz nous n'avons pas besoin de faire cette optimisation : cela a l'avantage de simplifier l'écriture VHDL (mais l'inconvénient de consommer un peu plus de puissance car on commute plus de bascules !).

Ce qui peut choquer le lecteur qui a suivi ce que nous avons déjà fait est le n initialisé à 9 alors que nous avions un compteur qui comptait jusqu'à 11 ! Voici les explications :

  • quand vous passez de "e0" vers "e1" vous avez eu un front d'horloge ps2 et vous initialisez seulement votre compteur à 9 (cela fait donc 10 fronts)
  • la sortie de l’ensemble des états "e5" "e2" "e3" se fait quand n=0 mais en passant par la transition e2 → e3 qui détecte un front supplémentaire... et voila les 11 fronts descendants de l'horloge PS2.

La construction de ce genre de graphe d'états demande donc une certaine expérience : l'initialisation du compteur à 11 serait catastrophique !

Le graphe d'état passe sous silence la détection du front (variable "fall_edge") et nous allons commencer par sa construction.

Réalisation VHDL modifier

Nous allons présenter étape par étape la réalisation du graphe d'états de la section précédente. Nous allons commencer par la détection des fronts descendants de l'horloge PS2, puis ajouter le séquenceur pour enfin ajouter à tout cela le chemin de données. Notre entité est :

-- notre entite inspiree de P. P. Chu
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity ps2_rx is
   port (
      clk, reset: in  std_logic;
      ps2d, ps2c: in  std_logic;  -- key data, key clock
      rx_en: in std_logic;
      rx_done_tick: out  std_logic;
      dout: out std_logic_vector(7 downto 0)
   );
end ps2_rx;

Réalisation de la détection de front modifier

Si l’on veut que la détection de front ne dure qu'une période d'horloge, on peut faire comme ci-dessous :

-- encore une fois inspiré de P. P. Chu (voir programme complet plus loin)
   process (clk, reset)
   begin
      if reset='1' then
         filter_reg <= (others=>'0');
         f_ps2c_reg <= '0';
      elsif (clk'event and clk='1') then
         filter_reg <= filter_next;
         f_ps2c_reg <= f_ps2c_next;
      end if;
   end process;
-- on remplit sans arret filter_reg
   filter_next <= ps2c & filter_reg(7 downto 1);
-- on detecte si remplit de 1 ou de 0
   f_ps2c_next <= '1' when filter_reg="11111111" else
                  '0' when filter_reg="00000000" else
                  f_ps2c_reg;
-- pret pour la detection du front descendant
   fall_edge <= f_ps2c_reg and (not f_ps2c_next);

Cette partie est séquentielle et ne fonctionne que sur des fronts d'horloge montants.

Réalisation du séquenceur modifier

Le séquenceur va être réalisé avec deux process. Cette façon de faire a été peu décrite dans ce document mais elle est tellement commune que nous laissons le soin au lecteur de trouver des documents expliquant ce style de programmation.

   type statetype is (e0, e1, e2, e3, e4, e5);
   signal state_reg, state_next: statetype;

   process (clk, reset)
   begin
      if reset='1' then
         state_reg <= e0;
      elsif (clk'event and clk='1') then
         state_reg <= state_next;
         n_reg <= n_next;
         b_reg <= b_next;
      end if;
   end process;
   -- next-state logic
   process(state_reg,n_reg,b_reg,fall_edge,rx_en,ps2d)
   begin
      -- pour eviter certains else 
      state_next <= state_reg;
      
      case state_reg is
         when e0 =>
            if fall_edge='1' and rx_en='1' then
               state_next <= e1;
            end if;
         when e1 =>  
              state_next <=e2;
         when e2 =>
            if fall_edge='1' then
              state_next <= e3;
            end if;
         when e3 =>
            if n="0000" then
              state_next <= e4;
            else
              state_next <= e5;
            end if;
          when e4 =>
            state_next <=e0;
          when e5 =>  
              state_next <=e2;
      end case;
   end process;

Puisque nous ne nous intéressons qu'au séquenceur, nous retirons tout ce qui concerne les actions. Ceci sera ajouté en section suivante.

Réalisation de l’ensemble modifier

Dans cette section, nous allons assembler le code des deux sections précédentes à la description du graphe d'états pour en faire un ensemble complet avec six états. Nous présenterons ensuite la version en trois états qui a fortement inspiré ce code VHDL.

-- notre entite inspiree de P. P. Chu
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity ps2_rx is
   port (
      clk, reset: in  std_logic;
      ps2d, ps2c: in  std_logic;  -- key data, key clock
      rx_en: in std_logic;
      rx_done_tick: out  std_logic;
      dout: out std_logic_vector(7 downto 0)
   );
end ps2_rx;
architecture arch of ps2_rx is
   type statetype is (e0, e1, e2, e3, e4, e5);
   signal state_reg, state_next: statetype;
   signal filter_reg, filter_next:
          std_logic_vector(7 downto 0);
   signal f_ps2c_reg,f_ps2c_next: std_logic;
   signal b_reg, b_next: std_logic_vector(10 downto 0);
   signal n_reg,n_next: unsigned(3 downto 0);
   signal fall_edge: std_logic;
begin
   --=================================================
   -- filter and falling edge tick generation for ps2c
   --=================================================
   process (clk, reset)
   begin
      if reset='1' then
         filter_reg <= (others=>'0');
         f_ps2c_reg <= '0';
      elsif (clk'event and clk='1') then
         filter_reg <= filter_next;
         f_ps2c_reg <= f_ps2c_next;
      end if;
   end process;

   filter_next <= ps2c & filter_reg(7 downto 1);
   f_ps2c_next <= '1' when filter_reg="11111111" else
                  '0' when filter_reg="00000000" else
                  f_ps2c_reg;
   fall_edge <= f_ps2c_reg and (not f_ps2c_next);

   --=================================================
   -- fsmd to extract the 8-bit data
   --=================================================
   -- registers
   process (clk, reset)
   begin
      if reset='1' then
		 state_reg <= e0;
         n_reg  <= (others=>'0');
         b_reg <= (others=>'0');
      elsif (clk'event and clk='1') then
         state_reg <= state_next;
         n_reg <= n_next;
         b_reg <= b_next;
      end if;
   end process;
   -- next-state logic
   process(state_reg,n_reg,b_reg,fall_edge,rx_en,ps2d)
   begin
      -- pour eviter certains else 
      rx_done_tick <='0';
      state_next <= state_reg;
      n_next <= n_reg;
      b_next <= b_reg;
      case state_reg is
         when e0 =>
            if fall_edge='1' and rx_en='1' then
               state_next <= e1;
            end if;
         when e1 =>  
              state_next <=e2;
              b_next <= ps2d & b_reg(10 downto 1);
              n_next <= "1001";
         when e2 =>
            if fall_edge='1' then
              state_next <= e3;
            end if;
         when e3 =>
            if n_reg = "0000" then
              state_next <= e4;
            else
              state_next <= e5;
            end if;
            b_next <= ps2d & b_reg(10 downto 1);
	 when e4 =>
	 state_next <=e0;
	 rx_done_tick <='1';
          when e5 =>  
              state_next <=e2;
              n_next <= n_reg - 1;
      end case;
   end process;
   -- output
   dout <= b_reg(8 downto 1); -- data bits
end arch;

Bien sûr ce code nécessite au minimum l'ajout d'un registre d'affichage et l’affichage sur au moins deux afficheurs sept segments comme cela a été fait au TP1.

Version à trois États de Pong P. Chu modifier

Pong P. Chu dans son livre "FPGA prototyping by VHDL examples" propose une version utilisant seulement trois états que nous donnons maintenant pour comparer avec ce que nous avons fait (d'ailleurs très inspiré par son livre). L’idée est en fait assez simple si l’on était capable de noter des actions qui se déroulent seulement quand on quitte les états. Ceux qui nous ont suivi jusque là se disent mais pourquoi donc, n'était-ce pas le cas comme expliqué dans la section glissement sémantique du chapitre VHDL et machines à états algorithmiques. Et bien non ! Pas si l’on reste coincé dans l'étape ! C'est vrai que l'action se réalise au sortir de l'étape mais seulement si elle est suivie par une transition toujours réceptive ("=1"). Autrement elle est aussi réalisée pendant qu'on reste dans l'étape mais avec un front d'horloge de retard. Tout cela est bien compliqué et probablement pas à la portée d'étudiants de niveau 14.

-- Listing 8.1 (Pong. P. Chu "FPGA prototyping by VHDL examples")
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity ps2_rx is
   port (
      clk, reset: in  std_logic;
      ps2d, ps2c: in  std_logic;  -- key data, key clock
      rx_en: in std_logic;
      rx_done_tick: out  std_logic;
      dout: out std_logic_vector(7 downto 0)
   );
end ps2_rx;

architecture arch of ps2_rx is
   type statetype is (idle, dps, load);
   signal state_reg, state_next: statetype;
   signal filter_reg, filter_next:
          std_logic_vector(7 downto 0);
   signal f_ps2c_reg,f_ps2c_next: std_logic;
   signal b_reg, b_next: std_logic_vector(10 downto 0);
   signal n_reg,n_next: unsigned(3 downto 0);
   signal fall_edge: std_logic;
begin
   --=================================================
   -- filter and falling edge tick generation for ps2c
   --=================================================
   process (clk, reset)
   begin
      if reset='1' then
         filter_reg <= (others=>'0');
         f_ps2c_reg <= '0';
      elsif (clk'event and clk='1') then
         filter_reg <= filter_next;
         f_ps2c_reg <= f_ps2c_next;
      end if;
   end process;

   filter_next <= ps2c & filter_reg(7 downto 1);
   f_ps2c_next <= '1' when filter_reg="11111111" else
                  '0' when filter_reg="00000000" else
                  f_ps2c_reg;
   fall_edge <= f_ps2c_reg and (not f_ps2c_next);

   --=================================================
   -- fsmd to extract the 8-bit data
   --=================================================
   -- registers
   process (clk, reset)
   begin
      if reset='1' then
         state_reg <= idle;
         n_reg  <= (others=>'0');
         b_reg <= (others=>'0');
      elsif (clk'event and clk='1') then
         state_reg <= state_next;
         n_reg <= n_next;
         b_reg <= b_next;
      end if;
   end process;
   -- next-state logic
   process(state_reg,n_reg,b_reg,fall_edge,rx_en,ps2d)
   begin
      rx_done_tick <='0';
      state_next <= state_reg;
      n_next <= n_reg;
      b_next <= b_reg;
      case state_reg is
         when idle =>
            if fall_edge='1' and rx_en='1' then
               -- shift in start bit
               b_next <= ps2d & b_reg(10 downto 1);
               n_next <= "1001";
               state_next <= dps;
            end if;
         when dps =>  -- 8 data + 1 pairty + 1 stop
            if fall_edge='1' then
            b_next <= ps2d & b_reg(10 downto 1);
               if n_reg = 0 then
                   state_next <=load;
               else
                   n_next <= n_reg - 1;
               end if;
            end if;
         when load =>
            -- 1 extra clock to complete the last shift
            state_next <= idle;
            rx_done_tick <='1';
      end case;
   end process;
   -- output
   dout <= b_reg(8 downto 1); -- data bits
end arch;

Et pour finir, un peu de travail.

Travail à réaliser modifier

On vous demande d’utiliser une des corrections que l’on vient de donner (à six états ou à trois états) et de l'insérer dans une application comme indiqué dans la figure ci-dessous pour en tester le bon fonctionnement.

 
Schéma complet à tester

L'appui sur la touche 'a' donnera 15 en hexadécimal comme résultat.