TP: Introduction à SystemVerilog et aux circuits logiques programmables

Avertissement: Ce TP ne peux être réalisé sans avoir étudié les leçon L1 et L2. Les salles dédiées à ce TP sont les salles A405 et A406.

L'objectif de ce TP est d'une part de vous familiariser avec le langage SystemVerilog et d'autre part de découvrir l'outil Quartus qui sera utilisé tout au long du module.  Vous allez donc devoir développer du code SystemVerilog puis mettre en oeuvre votre code sur les maquettes disponibles dans les salles de TP.

Les transparents (pour les profs)...

La maquette de TP que vous allez utiliser, est basée sur l'utilisation d'un circuit appelé FPGA (Field Programmable Gate Array). Il s'agit d'un circuit composé d'un grand nombre de blocs de traitement logiques (combinatoires ou séquentiels) , dont on peut reprogrammer la fonctionnalité à volonté. Ces blocs de traitement reliés entre eux par des connections dont la topologie est elle même reprogrammable.

La structure interne d'un FPGA

Le circuit FPGA se situe dans la zone centrale de votre maquette

Le FPGA dans la carte de TP DE1-Soc

Sur le coté gauche vous distinguez la prise d'alimentation de la maquette (vérifiez si elle est bien branchée à l'alimentation, ou si l'alimentation est  elle même bien branchée). L e bouton marche/arrêt (rouge) se situe juste à coté.

L'alimentation et l'interrupteur marche/arrêt

 

Dans la zone en bas à gauche, vous disposez de 10 interrupteurs. Ces interrupteurs permettent de forcer les entrées du FPGA nommée sw[9], sw[8]...sw[0] à 0 (position basse) ou à 1 (position haute).

Les interrupteurs

A droite des interrupteurs, vous trouverez 4 boutons poussoirs, connectés aux entrées key[3],  key[2],  key[1] et key[0] du FPGA. Attention, l'entrée key[i] vaut 1 lorsque le bouton correspondant est relaché, et 0 lorsrqu'il est appuyé.

Les boutons-poussoir

Au dessus des interrupteurs vos disposez de 10 LEDS connectées au sorties ledr[9], ledr[8], ...,ledr[0] du FPGA. Les LEDS sont allumées si la sortie ledr[i] correspondantes vaut 1.

Les LEDS rouges

Vous disposez (en bas à gauche) de 6 afficheurs "7-segment" connectés aux signaux hex5[6:0], hex4[6:0],...,hex0[6:0] du FPGA.

Attention, pour allumer le segment i de l'afficheur j, il faut que le signal hexj[i] soit à 0, pour l'éteindre il faut que ce signal soit à 1.

Les afficheurs 7 segments

Les autres connecteurs sont réservées à des interfaces spécialisées (audio, vidéo, ethernet,...) et ne seront pas utilisées dans ce TP.

Les autres interfaces

Enfin, sur la gauche, se trouve le connecteur USB esclave permettant de programmer le contenu du FPGA depuis votre PC. Vérifiez que ce cable est bien connecté (à la maquette et au PC)

Le connecteur USB pour le chargement du code du FPGA

II A vous de jouer : Un squelette de design pour découvrir les outils Quartus

  • Téléchargez l'archive de projet Quartus disponible ici.
  • Ouvrez une fenêtre de terminal et décomprimez l'archive dans le répertoire ou vous l'avez placée: tar xjvf bac_a_sable.tbz
  • Toujours dans ce terminal placez vous dans le répertoire "bac_a_sable/quartus"
  • Toujours dans ce terminal, lancez l'outil Quartus: quartus &

L'outil prend un peu de temps à ce charger. Cet outil va vous permettre de décrire des structures logiques en langage SystemVerilog, et de les transformer en un fichier de programmation pour le FPGA.

  • Utilisez le menu déroulant  "File->Open Project". Faites bien attention a ouvrir un "projet" et pas un simple fichier.
  • Et ouvrez le fichier fpga.qpf.

Ce fichier contient toutes les informations nécessaires à l'outil pour compiler le source SystemVerilog: Le type de FPGA, où sont ses entrées/sorties, quels sont les fichiers source nécessaires....

Vous disposez maintenant d'une fenêtre "Project Navigator":

  • Dans le menu déroulant sur sa droite, choisissez "Files" à la place de "Hierarchy"

  • Ouvrez le fichier fpga.sv (double-click...)

Sur la gauche apparaît un fichier source en langage SystemVerilog. Pour le moment, nous ne nous attarderons pas sur la syntaxe du langage. Sachez simplement, que ce fichier contient la définition d'un module entre les mots clefs module et endmodule ainsi que la définition des entrées sorties du module

  • Prenez la peine d'établir les correspondances entre les différents signaux déclarés est ceux décrits dans le chapitre précédent.

Nous allons simplement rajouter du code pour connecter l'ensemble des entrées d'interrupteurs sw aux leds rouges ledr.

  • Ajoutez la ligne suivante après le commentaire "// ajouter votre code..." et avant "endmodule"
always @(*) ledr <= sw;

 

Nous allons maintenant compiler le fichier SystemVerilog. Pour cela:

  • Appuyez le ​ ​bouton   situé dans la barre supérieur de l'outil

Même pour un design simple comme celui-ci, la compilation peu prendre une minute ou deux. S'il n'y a pas de message d'erreur (voir la fenêtre Messages), l'outil a créé un fichier de programmation pour fpga.sof le FPGA. Nous allons programmer le FPGA, pour cela vérifiez que la carte  DES1-SoC est bien alimentée, et que le cable de transfert USB est bien connecté.

  • Appuyez le bouton  situé dans la barre supérieure de l'outil

La fenêtre suivante devrait s'ouvrir. 

  • Vérifiez que le fichier fpga.sof est bien sélectionner et que la case Program/Configure est bien cochée.
  • Appuyez sur le bouton Start

Le FPGA est maintenant programmé, si tel n'est pas le cas (messages d'erreur du programmeur, demandez à un encadrant de vous aider).

  • Vérifiez que les leds rouges s'éteignent ou s'allument lorsque vous modifiez la position des interrupteurs.

Votre environnement de TP est maintenant en place. Vous pouvez sauver votre projet en l'état.

Avant de continuer dans les exercices, prenez la peine de lire l'introduction au langage SystemVerilog qui suit.

III Introduction au langage SystemVerilog

SystemVerilog est un langage de description de matériel. Il permet de décrire des circuits (réels ou virtuels) de deux façons principales :

  • Description d'une structure: on décrit de quoi est composé un circuit en terme de blocs, et comment ces blocs sont reliés entre eux. Les blocs peuvent aussi à leur tour être décrits de cette façon, en instanciant des blocs plus petits, etc. jusqu'aux portes élémentaires.
  • Description d'un comportement: plutôt que de décrire un circuit en terme de sous-blocs plus petits, on peut décrire son comportement (ce qu'il fait, sa fonctionnalité).

La syntaxe de System Verilog est proche de celle de C, cependant ne pas oublier les quelques nuances importantes :

  • Une description System Verilog n'est pas un programme : elle décrit le comportement d'un circuit composés de plusieurs entités fonctionnant en parallèle, alors qu'un programme C est une succession d'ordres qu'un processeur exécutera successivement les uns après les autres.
  • System Verilog manipule des signaux plus que des variables (au sens C...)

Nous n'indiquons ici que les éléments de syntaxe de base du langage nécessaires aux séances pratiques.

III.1 Les modules

En SystemVerilog, les blocs sont appelés modules. Avant d'utiliser un module, il faut le déclarer.

On déclare un module de la façon suivante :

module nom_du_module ( déclaration_des_entrées_sorties );
  contenu_du_module
endmodule

Les signaux d'entrée/sortie sont déclarés dans l'entête de déclaration du module. Il s'agit d'une liste de déclarations individuelles séparées par des virgules. Chaque déclaration individuelle doit comprendre :

  • La déclaration du type d'entrée-sortie : input pour une entrée , output pour une sortie.
  • Le type de signal : nous utiliseront exclusivement le type logic.
  • La largeur éventuelle du signal : la notation [j:i] indique un vecteur de (j-i+1) bits indexés de i à j. Attention : i est le bit de poids faible. On a donc généralement j > i...

Exemples de déclaration d'entrées/sorties:

input logic clk,      // Un signal entrant de largeur 1 bit appelé "clk"
output logic aaa,      // Un signal sortant de largeur 1 bit appelé "aaa"
input logic  [7:0] d   // Une donnée (mot) entrante de largeur 8 bits appelé "d"

Les modules contiennent des signaux internes. Ces signaux internes peuvent être déclarés de façon similaire aux entrées/sorties.

...
logic mon_sig ;             // Un signal de largeur 1 bit  appelé "mon_sig"
logic [3:0] mon_autre_sig ; // Un bus de largeur 8 bits appelé "mon_autre_sig"
...

Quelques règles générales de syntaxe

 

  • Les commentaires sont comme en Java : // ou /* .... */
  • Le ";" a le même rôle de séparateur qu'en Java.
  • Il ne faut pas mettre de séparateur ";" après les mots-clefs en end... comme endmodule et endcase.

III.2 Contenu des modules : Description d'un comportement combinatoire

Logique combinatoire

On décrit généralement un comportement en spécifiant les valeurs affectées aux sorties d'un module ou à des signaux à l'intérieur de celui-ci (en fonction des entrées, bien sûr). Pour l'instant nous nous limitons à la description de comportements de type calcul combinatoire. La syntaxe générale utilisée est la suivante:

always @(*)
  nom_du_signal <= fonction_combinatoire ;

Il est important de bien comprendre la signification de cette syntaxe:

  • Le signal nom_du_signal est la sortie d'une fonction combinatoire fonction_combinatoire.
  • A chaque fois (always) qu'un entrée quelconque (*) de cette fonction est modifiée (@), le signal nom_du_signal est remis à jour.

Nous vous proposons deux modes de calcul des fonctions combinatoires :

  • une expression algébrique directe,
  • une expression tabulée grâce à l'usage de la syntaxe case.

Exemples:

logic a, b, c ;             // Déclaration de 3 signaux internes sur 1 bit
logic [1:0] Z ;             // Déclaration d'un signal interne sur 2 bits

always @(*) a <= b & c ;     // Il y a une fontion logique ET dont les entrées
                             // sont b et c et dont la sortie est a.
always@ (*) a <= c ;         // Le signal a est identique au signal c.
                             // Le signal a est défini par une
always@ (*)                  // fonction complexe de Z, b c et d...
 case (Z)
    2'b00   : a <= b ;     // cas où Z vaut O
    2'b01   : a <= c & d ; // cas où Z vaut 1
    default : a <= d ;     // Tous les autres cas
 endcase

Les expressions algébriques utilisent les primitives suivantes :

Fonction Symbole
Négation ~
Et &
Ou |
Xor ^

La syntaxe que nous utiliserons pour le case est :

case (signal_de_selection)
  valeur_1 : mon_signal <= expression_algébrique_1 ;
  valeur_2 : mon_signal <= expression_algébrique_2 ;
  ...
  default :  mon_signal <= expression_algébrique_default ;
endcase

Attention : Lorsque l'on utilise l'expression case il faut veiller à définir complètement la table de vérité de la fonction booléenne correspondante, soit explicitement, soit implicitement via l'expression default.

Les valeurs indiquées dans le case sont des constantes.

 

Valeurs constantes

La base 10 est utilisée par défaut, vous pouvez choisir la base (décimal:d, hexadécimal:h ou binaire:b) ainsi que la taille de la constante de la façon suivante:

25             // constante (par défaut sur 32 bits) valant 25 (en base 10)
8'd25          // constante codée sur 8 bits valant 25 (en base 10)
3'b101 : ...   // constante codée sur 3 bits valant 101 en base 2 (donc 5 en base 10)
4'ha : ...     // constante codée sur 4 bits valant A en hexadécimal (donc 10 en base 10)
1'b1 : ...     // constante codée sur 1 bit et valant 1...

Manipulation des vecteurs (mots ou bus...)

Les entrées/sorties ainsi que les signaux internes peuvent être des vecteurs de bits:

  • Ces vecteurs peuvent être de taille arbitraire.
  • Il est possible d'accéder à n'importe quel élément ou partie d'un vecteur.

Les exemples suivant vous montrent quelques cas d'utilisation :

 

logic [7:0] A;            // Déclaration d'un vecteur A de 8 bits.
logic [3:0] B, C, D;      // Déclaration de 3 vecteurs B, C et D de 4 bits
logic Z;                  // Z est un élément de 1 bit

always @(*) B <= A[7:4]; // Le quartet B est identique à la moitié de
                        // poids fort de l'octet A

always @(*) Z <= A[3];   // Z est identique au bit n°3 de A

always @(*) A <= {C,D};  // A est la concaténation des mots C et D
                        // équivalent à A[7:4] <= C et
                        //              A[3:0] <= D

Arithmétique entière et expressions

En SystemVerilog, les vecteurs de bits de type logic [i:0] sont implicitement interprétés comme des entiers non signés de (i+1) bits. Vous pouvez utiliser les opérateurs arithmétiques standards : +' et -.

SytemVerilog gère automatiquement les opérations d'extension de taille et de modulo lorsque les opérandes sont de tailles différentes:

logic [3:0] a ;
logic [4:0] b ;
logic c ;
logic [5:0] y ;
logic [3:0] z ;
logic [2:0] t ;

always @(*) b <= 5'b01110 ;   // b est un entier de 5 bits valant 14.
always @(*) a <= 2'b11 ;      // la valeur constante 3 est affectée à l'entier a
always @(*) c <= 3 ;          // c est un entier de 1 bit, on lui affecte la valeur 3 (2'b11),
                              // après troncature c vaut la valeur 1          
always @(*) y <= a + b + c ;  // y est codé sur un nombre de bits suffisant
                              // quelles que soient les valeurs de a, b et c          
                              // (ici y = 18)  010010 = 000011 + 001110 + 000011
always @(*) z <= a + b + c ;  // z prends les 4 bits de poids faible du résultat
                              //  (z = 2) 0010 = 0011 + 1110 + 0011  

always @(*) {t,z} <= a + b + c ; // z prends les 4 bits de poids faible du résultat : z = 2 (remarque 1)
always @(*) z <= {4{c}} ^ a ;     // Le mot z est identique au mot a si c est égal a 0
                                  // sinon le mot z est le complémentaire du mot a (remarque 2)

(1) Remarquez l'usage des accolades pour concaténer deux mots

(2) Remarquez l'usage des accolades pour générer un mot de 4 bits identiques.

Expressions booléennes

  • En SystemVerilog, un signal de type logic est équivalent à un booléen :
    • vrai est représenté par un 1
    • faux est représenté par un 0
  • Les opérateurs utilisés dans les expressions booléennes sont semblables à ceux du langage C
  • L'expression ternaire ? : est identique à celle du langage C (dans les cas que vous rencontrerez cette année...)
&& et
|| ou
== égalité
! négation logique
!= non égalité>
inférieur
... ...
logic a, c ,d ;
logic [7:0] b ;

always @(*) a <= (b == 8) ;         // a prend la valeur 1'b1 lorque b est égal à 8

always @(*) a <= (!(b == 8)) && c ; // a prend la valeur 1'b1 lorsque b
                                   // est différent de 8 et que c est égal à 1'b1
always @(*) a <= (b == 8) ? c : d ; // a prend la valeur c si b est égal à 8 ,
                                      // sinon il prends la valeur de d

Nombres signés

Par défaut, les vecteurs de bits déclarés comme logic[i:0] sont considérés comme des nombres non signés. Les opérations arithmétiques ainsi que les opérations de comparaison arithmétique les interpréteront comme des nombres non signés.

Pour pouvoir faire de l'arithmétique sur des nombres signés il faut utiliser le mot clé signed au moment de la déclaration.

logic signed [3:0] A,B;
logic  c;
always @(*) A <= 4'b1111;
always @(*) B <= 4'b0000;
always @(*) c <= (A>B);  // c vaut 0 car A est interprété comme un nombre signé
                        // (A vaut -1 et B vaut 0)

III.3 Descriptions de structures : l'instanciation de modules.

La figure suivante présente l'exemple d'un module M_a instanciant deux occurrences Inst1 et Inst2 d'un même module M_b.

Remarquez le signal nommé S1 qui connecte la sortie Y d'un sous-module à l'entrée A d'un autre sous-module.

Description hiérarchique en SystemVerilog

Le code correspondant à cette description est le suivant :

    module M_a( input logic X,
                input logic Y,
                output logic Z );
   
      logic S1; // Déclaration du signal S1 interne à M_a
   
      M_b Inst1(.A(X),.B(Y),.Y(S1)) ; // Instance Inst1 de  M_b
      M_b Inst2(.A(S1),.B(Y),.Y(Z)) ; // Instance Inst2 de  M_b
    endmodule

En résumé,

  • La déclaration d'un signal interne à un module ce fait en précisant:
    • Le type de signal pour nous exclusivement logic
    • La largeur éventuelle du signal : la notation [i:j] indique un vecteur de i-j+1 bits indexés de j à i
    • Le nom du signal
  • L' instanciation d'un sous-module ce fait en précisant:
    • Le nom du sous_module
    • Le nom de l'instance particulière du sous-module
    • Entre parenthèses, une liste de connexions des entrées/sorties. La syntaxe pour chaque Entrée/Sortie est:
      • .nom_du_signal_du_sous_module( nom_du_signal_du_module )

Exemples de connexions :

   logic [1:0]C ;
   logic [1:0]D ;
 
  xxx inst_xxx(.E(C),...     // Le bus C est relié à l'entrée E de xxx.
                             // Ils ont le même nombre de bits.
  xxx inst_xxx(.F(C[0]),...  // Le bit 0 de C est connecté à l'E/S F de xxx.
                             // (F doit donc normalement avoir une largeur de 1 bit)
  xxx inst_xxx(.G({C,D}),... // Les signaux C et D sont regroupés en un vecteur
                             // de largeur 4 bits, connecté à l'E/S G de xxx.
                             // (G doit donc ici normalement avoir une largeur de 4 bits)
  ...

 

IV Exercices à réaliser.

Ces exercices on pour but de vous familiariser pas à pas aux méthodes de codage en SystemVerilog. Vous ne pourrez peut-re pas réaliser tous les exercices, ce n'est pas grave, mais tentez quand même d'en réaliser le maximum...

Exercice 1:

Il s'agit de visualiser un nombre de 4 bits  au format hexadécimal à l'aide des afficheurs de la maquette.

  1. Modifiez le code du module FPGA de façon à afficher la valeur représentée par les interrupteurs sw[3:0] sur l'afficheur hex0.
  2. Compilez et testez votre code sur la maquette.

Exercice 2:

Nous désirons afficher plusieurs valeurs sur les différents afficheurs (hex0, hex1,...). Nous ne voulons pas dupliquer le code (bonne pratique identique à celle du développement logiciel).

  1. Dans le répertoire src, qui contient déjà le fichier fpga.sv,  créez un nouveau fichier nommé decodeur7seg.sv qui contiendra la définition d'un module decodeur7seg  dont les entrées/sorties sont:
    • entrée : din, mot de 4bits représentant le code à afficher.
    • sortie : dout, mot de 7 bits représentant les bits de l'afficheur.
  2. Modifiez le code de fpga  de façon à instancier le module decodeur7seg en lieu et place du code précédent.
  3. Compilez et testez votre code sur la maquette.

Exercice 3:

Faisons un peu d'arithmétique, nous désirons additionner 2 mots de 4 bits (non signés) , afficher chacune des valeurs ainsi que le résultat.

  1. Nous supposons que sw[3:0] représente un nombre non signé de 4 bits.
  2. Nous supposons que sw[7:4] représente un nombre non signé de 4 bits.
  3. Modifiez fpga pour:
    • Afficher sw[3:0] sur hex0
    • Afficher sw[7:4] sur hex1
    • afficher la somme de sw[3:0] et de sw[7:4] sur hex3 et hex4. (Expliquez pourquoi nous devons utiliser 2 afficheurs)
  4. Compilez et testez sur votre maquette.

Exercice 4:

On suppose maintenant que les nombres sont signés, codés sur 4 bits en complément à 2.

  1. Transformez votre additionneur en conséquence
  2. Compilez et testez sur votre maquette.

Exercice 5:

  1. Transformez votre additionneur en additionneur/soustracteur. sw[9]  sera utilisé pour choisir addition ou soustraction.
  2. Compilez et testez sur votre maquette.

Exercice 6:

Lire en hexadécimal est un peu fatiguant, nous désirons afficher les nombres en décimal:

  • Chaque nombre servant à l'addition devra utiliser 2 afficheurs:
    • le premier afficheur affichera l'amplitude du nombre (de 0 à 8)
    • le second afficheur affichera le signe du nombre s'il est négatif.
  • Le resultat utilisera 2 afficheurs
    • Le couple d'afficheur devra afficher des nombres codés de (-16 a 16)
    • Le premier afficheur affichera les unités (0..9)
    • Le deuxième afficheur affichera les dizaines : -1, 0 ou 1
  1. Transformez votre additionneur/soustracteur en conséquence
  2. Compilez et testez sur votre maquette.

 

Fichier attachéTaille
Binary Data bac_a_sable.tbz2.9 Ko