< Very High Speed Integrated Circuit Hardware Description Language < 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.

Introduction

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)
AppuirelâchementAppuirelâchementAppuirelâchement
Q (lettre normale)1E9E1CF0 1C1CF0 1C
Retour Chariot / Entrée (clavier principal)1C9C5AF0 5A5AF0 5A
Retour Chariot / Entrée (clavier numérique)E0 1CE0 9CE0 5AE0 F0 5A79F0 79
Touche Windows gaucheE0 5BE0 DBE0 1FE0 F0 1F8BF0 8B
Touche Windows droiteE0 5CE0 DCE0 27E0 F0 278CF0 8C
Panneau d’avertissement On s'intéressera par la suite au clavier "set2 (BM PC AT)" du tableau ci-dessus.
Remarque

Un article en anglais détaille les scancodes du clavier QWERTY. Notez que les scancodes codent la position des touches et par conséquent passer du clavier QWERTY au clavier AZERTY ne présente aucune difficulté.

Lecture partielle d'un clavier PS/2

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

Pourquoi cette lecture est-elle partielle ?

On parle de lecture partielle car notre montage final sera incapable :

  • de savoir si ce qu’il affiche est un scancode d'appui ou de relâchement
  • de faire la différence entre une touche normale et une touche étendue (qui envoie 0xE0 en plus)

Travail à réaliser et sa correction

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

Schéma à réaliser

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.

Remarque

La transformation d'un GRAFCET en programme VHDL est examinée dans un autre projet qu’il vous est fortement conseillé de parcourir.

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.

Remarque

Aucun test de parité n'est réalisé dans cette version. Les évolutions possibles sont donc ce test de parité et un séquencement un peu plus proche de l'état du bus.

Quelques scancodes :

scancode hexadécimal pour clavier AZERTY
ToucheScancodeToucheScancodeToucheScancode
b0x32d0x23f0x2B
g0x34h0x33j0x3B
q0x1Cs0x1Bt0x2C

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

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

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

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

Ajout du registre d'affichage

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

Exercice

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

Remarque

Ce registre d'affichage n’est pas nécessaire sauf si on a l'intention d'étendre les fonctionnalités de notre lecture des codes. Ceci sera demandé dans la prochaine section.

Gestion des scancodes 16 bits

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

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.

Pour faire les choses sérieusement

Un essai rapide m'a permis de constater que changer tous les fronts d'horloge descendants en fonts montants ne perturbait pas le fonctionnement. Autrement dit le travail précédent de ce chapitre fonctionne encore si toutes les sensibilités d'horloges sont changées pour des fronts montants. Pourtant, pour ne prendre aucun risque, nous allons construire un nouveau recepteur PS/2 de A à Z en essayant à toute étape de respecter les règles d'horloges.

Fin du principe

Graphe d'états

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

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

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

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

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

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

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.

Cet article est issu de Wikiversity. Le texte est sous licence Creative Commons - Attribution - Partage dans les Mêmes. Des conditions supplémentaires peuvent s'appliquer aux fichiers multimédias.