SystemVerilog en 13 minutes

Introduction

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 SystemVerilog est proche de celle de C, cependant ne pas oublier les quelques nuances importantes :

Une description SystemVerilog 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.
SystemVerilog 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.

Chapitres:

 

I. 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 ainsi :

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

Exemple 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   // Un bus entrant 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_signal ;               // Un signal de largeur 1 bit  appelé "mon_signal"
      logic [3:0] mon_autre_signal ;   // Un bus de largeur 4 bits appelé "mon_autre_signal"
      ...    

Quelques règles générales de syntaxe

Les commentaires sont comme en C : // ou /* .... */

Le ";" a le même rôle de séparateur qu'en C.

Il ne faut pas mettre de séparateur ";" après les mots-clefs en end... comme endmodule et endcase.

II. 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 faut interpréter cette syntaxe comme étant: Le signal nom_du_signal est la sortie de la fonctionfonction_combinatoire. A chaque fois qu'une entrée de cette fonction est modifiée, le signal nom_du_signalest remis à jour.

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 porte logique AND 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 (c'est un alias)


   logic x;                 // Le signal x est défini par une
                            // fonction complexe de Z, b c et d...
   always@(*)
    case (Z)
     2'b00   : x <= b ;     // cas où Z vaut 0
     2'b01   : x <= c & d ; // cas où Z vaut 1
     default : x <= d ;     // Tous les autres cas
   endcase

Comme vous le remarquez dans cet exemple, 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.

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é, soit explicitement, soit implicitement via l'expression default.

Les valeurs indiquées dans le case sont des 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 sur 8 bits valant 25 (en base 10)
   3'b101 : ...   // constante sur 3 bits valant 101 en base 2 (donc 5 en base 10)
   4'ha : ...     // constante sur 4 bits valant A en hexadécimal (donc 10 en base 10)

 

Manipulation des vecteurs (bus)

Les entrées/sorties ainsi que les signaux internes peuvent être des vecteurs. Il est possible de sélectionner un élément ou une partie d'un vecteur. Les exemples suivant vous montrent quelques cas d'utilisation :

     logic [7:0] A;           // Déclaration d'un vecteur de dimension 8
     logic [3:0] B, C, D;     // Déclaration de 3 vecteurs de dimension 4
     logic Z;                 // Élement sur 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 de 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 standards :+ et -

Sytem Verilog gère automatiquement les opérations d'extension de taille et de troncature 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
                                     // codé sur 4 bits (a vaut 4'b0011)
    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          
                                     // (bit de poids faible de la constante)
    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 (1)
                                     // t prends les 3 bits de poids fort du résultat : t = 1  
    always@(*) z <= {4{c}} ^ a ;     // Le bus z est identique au bus a si c est égal a 0,      (2)
                                     // sinon le bus z est le complémentaire du bus a.

 

  1. Remarquez l'usage des accolades pour concaténer deux bus
  2. Remarquez l'usage des accolades pour générer un bus 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 >>
Fonction Symbole
Et &&
Ou ||
Égalité ==
Négation logique !
Non égalité !=
Inferieur
Supé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é 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.Contenu des modules : Description d'un comportement séquentiel synchrone

Vous avez utilisé la syntaxe always@(*) pour décrire des fonctions combinatoires dans le langage SystemVerilog. Dans le jargon des HDL, nous appelons cela un processus.

Une description HDL d'un matériel est donc un ensemble de processus s'exécutant parallèlement. Pour décrire des bascules D et des registres (ou plus généralement tout élément synchrone sur une horloge) nous devons disposer d'une syntaxe permettant de décrire le fait de déclencher l'évaluation d'un processus sur des événements de type transition d'un signal.

Pour cela, en SystemVerilog, nous pouvons utiliser le processus always@( posedge clk).

La syntaxe générale est :

   always@( évènement_déclenchant )
     action_à_réaliser ;

 

Une bascule D

 

Le code suivant décrit une bascule D sensible au front montant d'une horloge CLK:

     logic D,Q ;
     ...
     always@( posedge clk )
       Q <= D ;

Vous pouvez combiner la bascule D avec l'équation combinatoire de son entrée, le code suivant décrit une bascule D dont l'entrée est le calcul du AND entre deux signaux A et B.

    logic Q, A, B;
   
    always@( posedge clk )
      Q <= A & B;

 

Vous pouvez décrire un groupe de bascules D (un registre) en utilisant la notation vectorielle déjà vue précédemment. Le code suivant décrit un registre 8 bits.

    logic [7:0] Q, D;
   
    always@( posedge clk )
      Q <= D;

Pour déclarer une bascule fonctionnant sur front descendant il suffit de remplacer le mot-clef posedge par negedge.

 

Une bascule D avec initialisation asynchrone

Le processus always@(posedge clk) doit pouvoir être déclenché par le signal d'initialisation. De plus ce signal doit être prioritaire sur l'horloge.

La syntaxe suivante permet de coder une bascule D avec une initialisation asynchrone active à l'état bas d'un signal init, et dont l'action est de mettre à 0 la bascule.

   logic  Q, D, init;
   
   always@( posedge clk or negedge init)
     if (~init )
       Q <= 0;
     else
       Q <= D

 

Vous pouvez interpréter cette syntaxe de la façon suivante:

  1. S'il y a un événement (front montant) sur l'horloge , ou un événement sur init (front descendant) alors il faut réévaluer le contenu de la bascule D.
  2. S'il faut réévaluer et que init = 0 alors c'est init qui a déclenché le processus (il es prioritaire sur l'horloge) et on passe donc la sortie de la bascule à 0.
  3. S'il faut réévaluer et que init = 1 alors c'est le front montant d'horloge qui a déclenché le processus et donc Q reçoit D.

Vous êtes évidemment libres de choisir:

  • l'état actif de l'initialisation :
    • posedge pour un signal actif à l'état haut (1) , associé au test if(init)
    • negedge pour un signal actif à l'état bas (0) , associé au test if(~init)
  • la valeur initiale : 0 ou 1

 

Généralisation des processus always

 

Jusqu'à maintenant, vous n'avez décrit que des processus (always) n'ayant qu'une seule action à réaliser. Il est possible de regrouper plusieurs actions dans le corps d'un processus en utilisant la syntaxe suivante :

    begin
      première_action ;
      deuxième_action ;
      ...
    end

Remarque : begin et end remplacent les accolades du C..;

Exemple:

   logic [7:0] Q;
   logic [4:0] D;
   ...
   always@( posedge clk )
   begin
     Q[7:4] <= Q[3:0];
     Q[3:0] <= D;
   end

 

IV. 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 B 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èse, 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)
  ...

 

 

V. Codage des états des automates finis

Définition de types énumérés pour décrire des états

SystemVerilog supporte la définition de signaux de type énumérations de la façon suivante:

   // sig est un signal pouvant prendre 7 valeurs "SWAIT", "S1", "S2", "S3", "S4", "S5" ou "S6".
   enum logic[2:0] {SWAIT, S1, S2, S3, S4, S5, S6} sig;
      // On peut alors écrire les choses suivantes :
      always@(*)
         sig <= S1;
 
   enum logic {V1, V2} sig1, sig2; // sig1 et sig2 sont deux signaux
                                          //pouvant prendre 2 valeurs "V1" ou "V2".
   // On peut alors écrire :
      always@(*)
         if (sig1 == sig2)
            sig1 <= V2;
         else
            ...

Par défaut, les valeurs énumérées correspondents aux codes 0, 1, 2, 3... en partant de la première valeur.

La taille nécessaire au codage des différentes valeurs est définie dans la définition du type.

Codage de l'évolution d'un automate fini.

L'usage de la syntaxe case permet de traduire facilement la table d'évolution des états. Pour cela vous pouvez regrouper dans un même vecteur l'ensemble des signaux d'entrée et l'état courant de manière à définir la table sous la forme :

   always@(*)
     case ({etat_courant, entrée_1, entrée_2})
       {WAIT,1'b0, 1'b1} : next_state <= S1;
       {S1,  1'b1, 1'b1} : next_state <= S2;
       ...
       default : next_state <= current_state;
     endcase

Extension de l'usage du case: le casez

Il peut parfois être long et fastidieux de coder explicitement tous les cas. Lorsque certains signaux ne sont pas utiles dans les équations de transition, il est possible de les représenter par un "?". Dans ce cas la syntaxe case doit être casez.

   always@(*)
     casez ({etat_courant, entrée_1, entrée_2})
       {WAIT,1'b0, 1'b1} : next_state <= S1;
       {S1,  1'b1, 1'b?} : next_state <= S2;
       ...
       default : next_state <= current_state;
     endcase

Dans l'exemple précédent, on passe de l'état S1 à l'état S2 indépendamment de la valeur de entrée_2.