Préambule

Objectifs

Passons maintenant aux descriptions comportementales. Dans ce type de descriptions, il y a plusieurs niveaux

Nous allons ici étudier les principales descriptions : 


Les objectifs de ce chapitre sont de comprendre :

Plan du chapitre


Back to Top

Affectations continues

Les affectations continues permettent d'affecter des valeurs aux noeuds (wire).
Elles modélisent des circuits combinatoires, sous une forme concise. Cette forme ne présume rien de la façon dont la fonction sera implémentée en pratique. Ce n'est qu'une représentation de la fonction réalisée, qui peut être réaliser en pratique avec d'autres fonctions logiques que celles écrites (du moment que la fonction booléenne reste la même).

Les affectations continues modélisant des circuits combinatoires, ce sont des affectation permanentes. Une fois qu'un noeud subit une affectation, cette affectation ne peut plus être modifiée.

Une affectation continue de déclare ainsi, à l'intérieur d'un module :

assign noeud = .... ;


Une forme concise permet de déclarer un noeud et de lui assigner une valeur en même temps.

Exemple :

wire [7:0] bus = a & b;

La cible d'une affectation continue peut être :


Les affectations continues sont déclarées en dehors de tout processus always ou initial. C'est normal : elles sont continues. Si l'expression à affecter à un signal doit être modifiée au cours du temps, il est alors nécessaire d'utiliser le type reg et les affectations procédurales.

Les affectations continues définissent des processus implicites. Par exemple : assign s = a + b; définit aussi un processus implicite, exécuté à chaque changement de a ou b.


A retenir :
Les affectations continues et le type wire seront utilisées pour modéliser de la logique combinatoire dont l'expression est simple.
Pour la logique séquentielle, ou combinatoire dont l'expression est complexe, on utilisera les reg et les affectations procédurales.

Attention :
Il est possible d'utiliser le type reg dans une affectation continue. Cela a une signification bien particulière, qui dépasse le cadre de ce cours. Mais il ne faut pas compter sur les simulateurs et synthétiseurs pour détecter un emploi erroné du type reg dans une affectation continue.



Back to Top

Processus

Verilog est un langage concurrent (parallèle) contrairement au C qui est par nature un langage séquentiel : les différentes tâches d'un programme Verilog s'exécutent en parallèle les unes des autres. Ces tâches sont appelées processus. Nous avons déjà vu des processus, sans l'avoir dit :

Les processus peuvent aussi être déclarés explicitement dans un module. Ils se composent alors d'une en-tête, et d'une suite d'instructions.

Type des processus

Il existent deux type de processus explicites, ceux qui ne sont exécutés qu'une seule fois en tout et pour tout, et ceux exécutés plusieurs fois (généralement au changement d'état d'une entrée)

initial
les processus déclarés par le mot-clef initial ne sont exécutés qu'une seule fois, au début de la simulation (au temps 0)
L'ordre dans lequel les diférents processus initial sont exécutés n'est pas spécifié, et dépend du simulateur.
Les processus initial sont typiquement utilisés pour initialiser les variables.
always
 
les processus déclarés par le mot-clef always sont exécutés en permanence, en commençant au temps 0. Ils doivent alors comporter au moins un point d'arrêt pour permettre au temps d'avancer (voir le chapitre sur la simulation).
L'ordre dans lequel les diférents processus always sont exécutés n'est pas spécifié, et dépend du simulateur. C'est au concepteur de faire attention à concevoir un code ne dépendant par de l'ordre d'exécution des processus.
Les processus always sont utilisés pour modéliser une activité répétée continuement dans un circuit (un bloc, ...), qui ne s'arrête qu'à la mise hors-tension (off) du circuit.
Exécution des instructions

Les instructions comportementales dans un processus peuvent être exécutées de façon parallèle ou séquentielle.

On notera que les blocs begin...end et fork...join :

Exemple

L'exemple suivant modélise un générateur d'horloge :

module clock_gen (clock);

  output clock;
  reg   clock;

  // initialise la variable clock
  initial
    clock = 0;

  // en permance change l'état de clock tous les 10ns
  always
    begin
      #10;            // met le processus en pause pour 10 unités de temps.
      clock = ~clock; // inverse clock
    end
endmodule

Exercice :

On a inversé l'ordre des instructions du bloc begin...end :

module clock_gen (output clock);

  reg clock;

  // initialise la variable clock
  initial
    clock = 0;
 
  // en permanence change l'état de clock tous les 10ns
  always
    begin
      clock = ~clock;
      #10;
    end
endmodule


Back to Top

Affectations procédurale

Nous avons vu les affectation continues, qui affectent en permanence une valeur à un noeud (wire). Elle sont déclarées en dehors de tout processus (car elles définissent un processus implicite).
Dans un processus, les affectations ne concernent que les variables (reg, integer, ...). On les appelle affectations procédurales.

Comme vu au chapitre sur la simulation, il en existe deux types :

Exercice :

module test;

  reg a, b;

  initial
    begin
      a <= 1;
      $display("a = ", a);
    end

  initial
    begin
      b = 1;
      $display("b = ", b);
    end

endmodule

reg [15:0] word;

always @(posedge clk)
  begin
    // échange les octets de poids fort et faible de word
    word[15:8] = word[7:0];
    word[7:0] = word[15:8];
  end



Back to Top

Affectations procédurale forcées

Nous avons vu les affectation continues et les affectations procédurales. Il existe un troisième type d'affectations, plus rare, qu'on mentionne ici pour être complets. Ce sont les affectations procédurales forcées. Elles permettent de préempter une autre affectation depuis un processus, c'est-à-dire de forcer une variable ou un net à une valeur précise, et ce même si d'autres affectations surviennent : en d'autres mots, elles priment sur tout. C'est pour cela qu'on les utilise parfois pour modéliser des reset dans des testbenchs. Mais attention, elles ne sont souvent pas synthétisables !

assign / deassign
assign permet de forcer une variable (reg) à une certaine valeur (qui peut être une expression) même si d'autres affectation surviennent plus tard.
deassign permet de retirer ce forçage. La variable garde quand même la même valeur jusqu'à ce qu'une affectation (forcée ou non) lui en donne une nouvelle.
force / release
force fait la même chose que assign, mais peut porter sur une reg ou un wire.
release est équivalent à deassign. Dans le cas où force portait sur un wire, le wire retrouve instantanément sa valeur normale.
Exemple:
module top;

  wire [9:0] pixels, black_pixels;

  // Produit un premier flux normal de pixels
  video_output(pixels);

  // Produit un deuxième flux de pixels contenant seulement des synchros (EAV/SAV)
  empty_video(black_pixels);

  // Switch au vol entre les deux flux
  always @(posedge clk)
    begin
      if(line==220)
        force pixels = black_pixels + 16;
 
      if(line==21)
        release pixels;
    end
endmodule



Back to Top

Structures de contrôle

On retouve en Verilog des structures de contrôle similaires à celles du C :

ainsi que des structures de contrôle du temps (synchronisation des processus, mise en veille) :

Instructions de test

Syntaxe :

La condition est évaluée, si elle est vraie l'instruction est exécutée, sinon celle de l'éventuel else l'est.
Attention : une condition est fausse si elle évaluée en 0, z ou x.

Exemple :

if(a==0)
  begin
    // bloc exécuté si a vaut 0
  end
else
  begin
    // bloc exécuté si a ne vaut pas 0
  end


if(a)
  begin
    // bloc exécuté si a est vrai
  end
else
  begin
    // bloc exécuté si a vaut 0, x, ou z
  end

Exemple : les deux blocs suivants ne sont pas équivalents ! Pourquoi ?

if(a == b)
  <instruction1>;
else
  <instruction2>;


if(a != b)
  <instruction2>;
else
  <instruction1>;

 

Une autre forme d'instructions de test : le case , qui diffère de celui du C par le fait que :

Exemples :

reg [15:0] rega;
reg [9:0] result;

case (rega)
  16'd0: result = 10'b0111111111;
  16'd1: result = 10'b1011111111;
  16'd2: result = 10'b1101111111;
  16'd3: result = 10'b1110111111;
  16'd4: result = 10'b1111011111;
  16'd5: result = 10'b1111101111;
  16'd6: result = 10'b1111110111;
  16'd7: result = 10'b1111111011;
  16'd8: result = 10'b1111111101;
  16'd9: result = 10'b1111111110;
  default result = 10'bx;
endcase

/****************************/

reg [7:0] ir;

casez (ir) // les z (ou ?) sont ignorés
  8'b1???????: <instruction>;
  8'b01zzzzzz: <instruction>;
  8'b00010???: <instruction>;
  8'b000001??: <instruction>;
endcase

/****************************/

reg [7:0] r, mask;

// r = 8'b01100110

mask = 8'bx0x0x0x0;
casex (r ^ mask) // les z (ou ?) sont ignorés
  8'b001100xx: <instruction>;
  8'b1100xx00: <instruction>;
  8'b00xx0011: <instruction>;
  8'bxx010100: <instruction>;
endcase


/*****************************************************/
// Cas du selecteur constant
// pour les machines à état one-hot
// ATTENTION, ce style de code est souvent illisible !

reg [2:0] encode;

case (1)
  encode[2] : $display("Select Line 2");
  encode[1] : $display("Select Line 1");
  encode[0] : $display("Select Line 0");
  default $display("Error: One of the bits expected ON");
endcase

Remarque importante pour les gens venant du VHDL

parallel_case
Avec les casez et casex, Verilog donne la possibilité d'avoir des conditions qui se chevauchent (contrairement au VHDL). Un encodeur de priorité est alors construit. Lorsqu'on sait qu'en réalité les conditions ne se chevaucheront jamais, on a la possibilité d'indiquer au synthétiseur que les conditions sont purement parallèle et que ce n'est donc pas la peine de synthétiser un encodeur de priorité. Cela est fait au moyen d'attributs :
(* parallel_case *)
casez(Va)
   3'b1??: a0 = 1;
   3'b?1?: a1 = 1;
   3'b??1: a2 = 1;
endcase

// ou bien

casez(Va) // pragma parallel_case
   3'b1??: a0 = 1;
   3'b?1?: a1 = 1;
   3'b??1: a2 = 1;
endcase
La logique synthétisée est alors plus rapide car plus petite. Mais ça a deux inconvénients majeurs :
  1. les comportements en simulation et post-synthèse sont alors différents ! La preuve d'équivalence est impossible...
  2. beaucoup de gens ajoutent cet attribut automatiquement sans avoir conscience qu'il faut être sûr qu'on n'aura jamais de chevauchement.  En cas de chevauchement, le design a de bonnes chances de partir en tape-out avec des erreurs majeures alors que la simulation se passe correctement !
Recommandation : coder les encodeurs de priorité à base de if / else, et oublier l'attribut parallel_case !
full_case
Si on ne spécifie pas tous les cas possible du case, des latchs sont syntétisés. Un autre attribut existe : (* full_case *) indiquant au synthétiseur que les cas non spécifiés sont don't care, et qu'il peut générer ce qu'il veut dans ces cas là.
Mêmes problèmes que plus haut :
  1. les comportements en simulation et post-synthèse sont alors différents ! La preuve d'équivalence est impossible...
  2. contrairement à une croyance répandue, cela n'empêche pas les latchs !
Recommandations :
  1. oublier cet attribut
  2. toujours prévoir un cas default

Pour plus de précisions, on se rapportera à l'excellent papier de Don Mills et Sutherland sur le sujet !

Les boucles

Attention aux boucles forever, il faut une condition d'arrêt dans l'intérieur de la boucle, sinon le temps n'avance pas !
Les mêmes remarques sur l'évaluation des conditions que pour le if s'appliquent ! (une condition est fause si elle s'évalue en 0, x ou z, elle est vraie sinon)

Exemple :

begin: count1s
  reg [7:0] tempreg;

  tempreg = rega;
  for(count = 0; tempreg; tempreg = tempreg >> 1)
    count = count + tempreg[0];
end

Exercice :




Back to Top

Structures de contrôle du temps, synchronisation des processus

Il en existe trois, qui servent aussi de point d'arrêt :

Contrôle événementiel

Le symbole @ est utilisé pour spécifier un contrôle événementiel. Il bloque un processus (mise en veille), jusqu'à ce qu'un événement (changement d'état) sur le signal spécifié se produise.
On peut lui adjoindre le modificateur posedge ou negedge pour filtrer les transitions montantes ou descendantes.

La table suivante définit ce que sont les transistions montantes et descendantes :

vers 0 vers 1 vers x vers z
  de -   montante   montante montante
de 1   descendante   -   descendante     descendante  
de x descendante montante - -
de z descendante montante - -

Si l'opérande de @ est un vecteur, c'est le bit de poids faible qui donne le type de transition.

On peut grouper plusieurs événements avec le mot-clef or ou avec une virgule (ou une combinaison des deux).

Exemple :

@r rega = regb; // l'affectation ne s'effectue qu'après un changement de n'importe quel bit de r

@(posedge clock) rega = regb; // l'affectation ne s'effectue qu'après un front montant de clock

@(negedge clock) rega = regb; // l'affectation ne s'effectue qu'après un front descendant de clock


@(posedge clock or negedge reset) // l'affectation suivante ne s'effectue qu'après un front
   rega = regb;                   // montant de clock ou un front descendant de reset

@(a, b or c)    // l'affectation suivante ne s'effectue qu'après un événement
   rega = regb; // sur a, b, ou c

 
Contrôle sur état

L'instruction wait prend en argument une condition.
Elle teste la condition et, si elle est fausse, bloque l'exécution du processus jusqu'à ce qu'elle soit vraie.
Si la condition est vraie lors du test, l'exécution n'est pas bloquée.

Exemple :

wait (enable) // l'affectation attend que enable vaille 1 pour s'exécuter
  rega = regb;

// Attention, les deux exemples suivant ne SONT PAS equivalents (pourquoi ???)
begin
  @(posedge clk) q1 <= d;
  wait(clk) q2 <= q1;
end
 
begin
  @(posedge clk) q1 <= d;
  @(posedge clk) q2 <= q1;
end

 
Délais

On peut spécifier explicitement des pauses dans l'exécution d'un processus, en unité de temps, au moyen du symbole #.
Le temps de pause peut être spécifié à l'aide d'une constante ou d'une expression.

Si le temps de pause n'est pas entier, il est arrondi selon la précision temporelle du simulateur.
La précision temporelle est spécifiée au moyen de la directive `timescale (le premier caractère est un guillemet simple inversé, obtenu sur un clavier de PC par les touches AltGr et 7...). Cette directive spécifie l'unité de temps du simulateur et sa précision. L'unité de temps et la précision utilisées par défaut, si aucune directive `timescale n'est utilisée, dépendent du simulateur (généralement 1ns / 1ps).

Exemple :

`timescale 1 ns / 1 ps // le temps sera spécifié en multiples de 1ns, avec une précision de 1ps
...
begin
  a = 1;
  #10; // mise en pause du processus pour 10ns
  a = 0;
end



`timescale 10 ns / 100 ps // le temps sera spécifié en multiples de 10ns, avec une précision de 100ps
real half_period;
reg clock;

initial
  begin
    clock = 0;
    half_period = 5.99999;
  end

always
  begin
    #half_period; // mise en veille du processus pendant 60 ns.
    clock <= ~clock;
  end

Exercice :

Les délais sont généralement utilisés dans les blocs initial pour générer des stimuli de test.

Exemple :

reg [7:0] a, b;
reg cin;

...

initial
  begin
    a = 0; b = 0; cin = 0;
    #10;
    a = 1 ; b = 2; cin = 0;
    #10;
    a = 221; b = 134; cin = 1;
    #10;
    a = 0; b = 255; cin = 1;
  end



Back to Top

Processus et listes de sensibilité

Liste de sensibilité

Un processus peut être synchronisé explicitement, à l'aide d'une des instructions de contrôle temporel ci-dessus, ou de façon implicite à l'aide d'une liste de sensibilité.
Cette liste de sensibilité est spécifiée à l'aide du symbole @ juste après le mot-clef always. Cette liste peut comporter un ou plusieurs signaux, séparés alors par le mot-clef or ou une virgule.
Elle permet de n'exécuter le processus que lors d'un événement sur l'un des signaux dans la liste.

Exemple : une bascule D simple est un dispositif qui échantillonne une entrée sur le front montant d'une horloge. Une description Verilog d'une bascule D serait donc :

module DFF(clock, d, q);
  input clock;
  input d;
  output q;

  reg q;

  always @(posedge clock)
    q <= d;

endmodule

Exercice :

 

 Logique séquentielle et logique combinatoire

Nous avons vu que les affectations continues (assign et wire) permettent de modéliser de la logique combinatoire seulement.
Nous allong voir qu'un reg, en dépit de son nom rappelant les registres, peut servir à modéliser aussi bien de la logique séquentielle que de la logique combinatoire.

Exemple :

reg a;
...
always @(b, c)
  a <= b & c;

L'affectation a <= b & c; est exécutée dès que b ou c change d'état. En d'autres termes, a ne dépend que de l'état actuel de b et c, et pas de leur passé. Ceci est la définition de la logique combinatoire.
Ce processus modélise donc une porte ET : logique combinatoire, même si a est un reg !

Exemple :

reg a;
...
always @(b, c)
  if(b)
    a <= c;
  else
    a <= 0;

La valeur de a dépend de b et c.
Comme b et c figurent tous les deux dans sa liste de sensibilité, le processus est exécuté à chaque changement d'état de b ou de c.
De plus, à chaque exécution du processus, une valeur est affectée à a :

a ne dépend donc que de l'état actuel de b et c, et pas de leur passé. En d'autres termes, la fonction réalisée est encore une fonction combinatoire.
Plus précisément la fonction réalisée est a = b & c (rappel : une porte ET est aussi une mise à 0).

Exemple :

reg a;
...
always @(b, c)
  if(b)
    a <= c;

La valeur de a dépend de b et c.
Comme b et c figurent tous les deux dans sa liste de sensibilité, le processus est exécuté à chaque changement d'état de b ou de c.
Mais dans le cas où b vaut 0, a n'est pas affecté et garde son ancienne valeur. La fonction réalisée est donc une fonction séquentielle.
Plus précisément, la fonction réalisée est un latch.
Dans ce processus-ci, un des chemins de contrôle (le if ) n'affecte pas de valeur à a. C'est de là que vient la séquentialité du processus.

 

Un processus est combinatoire si :

  1. les sorties reçoivent une valeur à chaque à chaque modification des variables dont elle dépendent.
    En d'autre termes, la liste de sensibilité du processus contient toutes les variables dont dépendent les sorties.
  2. les sorties sont affectées quel que soit le chemin de contrôle dans le processus.

Il est séquentiel sinon.

Un des pièges classiques des HDL est l'inférence accidentelle de latch ou bascule D alors qu'on voulait écrire un processus combinatoire.

Exemples :

always @(*) // équivalent à @(a or b or c or d or f)
  y = (a & b) | (c & d) | myfunction(f);

// Le code suivant modélise la tabel d'évolution d'une machine à états,
// souvent utilisé pour spécifier un encodage one-hot,
// MAIS C'EST UN STYLE DE CODAGE DEPLORABLE !!!

always @* // équivalent à @(state or go or ws)
  begin
    next = 4’b0; // next est initialisé à 0 par défaut.
                 // Il reçoit donc une valeur quel que soit le chemin de contrôle choisi (case...)

    case (1’b1)
      state[IDLE]:
        if (go)
          next[READ] = 1’b1;
        else
          next[IDLE] = 1’b1;

      state[READ]: next[DLY ] = 1’b1;

      state[DLY ]:
        if (!ws)
          next[DONE] = 1’b1;
        else
          next[READ] = 1’b1;

      state[DONE]: next[IDLE] = 1’b1;
    endcase
end


always @* // équivalent à @(a or en)
  begin
    y = 8’hff;
    y[a] = !en;
  end

 Exercice

  1. que fait le dernier exemple de code ?



Back to Top

Affectations et délais

Verilog permet de spécifier des délais dans les affectations. Selon le type d'affectation (bloquante, non bloquante) et l'endroit où le délai est spécifié, le comportement modélisé ne sera pas le même. Il est nécessaire de bien comprendre quel délai modélise quoi, c'est l'objectif de cette section.

Il existe trois type d'affectations avec délai :

Délai simple

Cette forme de délai est spécifié à gauche de l'affectation.
Un délai simple retarde le moment d'exécution d'une affectation toute entière.

Affectation bloquante : #10 x = y;
Le processus est mis en veille pendant 10 unités de temps.
Puis x est affecté de façon immédiate avec ce que vaut y au temps 10.
Affectation non bloquante : #10 x <= y;
Le processus est mis en veille pendant 10 unités de temps.
Puis x est affecté de façon différée avec ce que vaut y au temps 10.

En d'autres termes, une affectation avec délai simple : #10 <affectation> est équivalente à ceci :
   #10;
   <affectation>;

Délai intra-affectation

Cette forme de délai est spécifié au mileu de l'affectation (après le signe d'affectation).
Dans ce type de délai, la partie de droite est évaluée instantanément et mise en mémoire. Puis l'affectation est effectuée au bout du délai spécifié (en respectant le fait qu'elle soit bloquant on non bloquante).

Affectation bloquante : x = #10 y;
Au temps 0, la valeur de y est mémorisée.
Puis le processus est mis en veille pendant 10 unités de temps.
Puis x est affecté avec la valeur mémorisée.
Affectation non bloquante : x <= #10 y;
Au temps 0, la valeur de y est mémorisée.
Puis le processus continue de façon normale (sans faire l'affectation).
Au temps 10, l'affectation est effectuée de façon différée (x prend la valeur mémorisée).
Délai nul

Des affectations dans différents processus peuvent être effectuée au même temps (physique). L'ordre dans lequel elles seront exécutées est alors non déterministe.
Pour éviter cette situation, les délais nuls peuvent être utilisés : ils assurent qu'une affectation sera effectuée en dernier, après toutes les autres.

Exemple :

initial
x = 0;

initial
  #0 y = x; // assure que cette affectation sera effectuée APRES celle du haut

Bien sûr, si plusieurs affectations ont un délai nul, l'ordre d'exécution entre elles est non déterminé.

De toutes façons, un code ayant besoin de ce genre d'artifice est souvent un code mal écrit.
Il vaut mieux écrire son code proprement, en évitant par construction le non déterminisme.

Modélisations

Dans le monde réel, il existe deux types de délais :

Délai de transport
C'est le temps de propagation d'un système.
Tous les changements des entrées sont propagés, avec ce délai.
Delai inertiel
Il modélise un temps de transition fini.
Des variations d'une entrée durant moins que ce délai sont ignorées (filtrage des parasites).

Exercice :


always@(in)
  o1 = in;

always@(in)
  o2 <= in;

always@(in)
  #5 o3 = in;

always@(in)
  #5 o4 <= in;

always@(in)
  o5 = #5 in;

always@(in)
  o6 <= #5 in;

chronogramme entree exos

Entrée du buffer (in)

Les délais peuvent aussi être utilisés avec le sign @. L'attente d'un délai fixe est alors remplacée par une attente d'un événement.
Cela permet d'écrire ce genre de choses :

always @(IN)
  OUT <= repeat (8) @(posedge clk) IN;

Exercice :


Délais et Verilog structurel

Les délais peuvent apparaitre dans une description structurelle, lors de l'instanciation d'un module ou lors d'une affectation continue (assign).

Ces délais spécifient un temps de propagation. Ils peuvent être différents selon le type de transition, et avoir une valeur minimum, maximum et typique.
Dans leur forme simple, ils sont déclarés ainsi :

and #10 and1 (out, i1, i2); // une porte and ayant un temps de propagation de 10 unités de temps

assign #20 out = i1 & i2; // une fonction (AND) ayant un temps de propagation de 20 unités de temps

Verilog dispose aussi de moyens de spécifier des délais encore plus évolués (de port à port, distribués, localisés, dépendant des transitions, des conditions de simulation, ...), mais cela sort du cadre de ce cours... 



Back to Top

Paramètres et généricité

Paramètres

Comme nous l'avons vu en introduction, les modules peuvent inclure des paramètres permettant d'écrire des modules génériques.

Les paramètres doivent être définis dans le module paramétré par le mot-clef parameter, et y recevoir une valeur par défaut.

La valeur par défaut d'un paramètre d'un module peut être modifiée :

Exemple :

module addn(A, B, S);
  parameter taille = 8, id = 0;

  input [taille-1:0] A, B;
  output [taille-1:0] S;

  assign S = A + B;
  initial
    $display("Hello,je suis l'additionneur %d, de taille %d", id, taille);
endmodule

// Passage de paramètre au moment de l'instanciation
module sum;
  wire [15:0] A, B, C, D, S;
  wire [15:0] TMP1, TMP2;

  addn add1(A, B, TMP1);
  addn #(16, 1) add2(C, D, TMP2);
  addn #(.id(2), .taille(16)) add3(TMP1, TMP2, S);
endmodule


// Passage explicite de paramètres
module sum2;
  wire [15:0] A, B, C, D, S;
  wire [15:0] TMP1, TMP2;

  defparam add1.taille = 16, add2.taille = 16, add3.taille = 16;
  defparam add2.id = 1, add3.id = 3;

  addn add1(A, B, TMP1);
  addn add2(C, D, TMP2);
  addn add3(TMP1, TMP2, S);
endmodule

Généricité

Les paramètres ne sont parfois pas suffisant pour écrire un code générique : on peut aussi vouloir instancier un nombre variable de portes (en fonction d'un paramètre, par exemple), voire même différentes portes... Ceci est fait au moyen des mots-clef generate...endgenerate, genvar et localparam.

Exemple : on veut décrire un multiplieur générique, tel que

module multiplier (a, b, product);
  parameter a_width = 8, b_width = 8;
  localparam product_width = a_width + b_width;
 
  input [a_width-1:0] a;
  input [b_width-1:0] b;
  output [product_width-1:0] product;

  generate
    if((a_width < 8) || (b_width < 8))
      begin
        CLA_multiplier #(a_width, b_width) u1 (a, b, product);
        initial
          $display("On a instancié un multiplieur CLA");
      end
    else
      begin
        WALLACE_multiplier #(a_width, b_width) u1 (a, b, product);
        initial
          $display("On a instancié un multiplieur de Wallace");
      end
  endgenerate
endmodule // multiplier

Exemple : l'exemple classique de l'additionneur n bits... On utilise ici des boucles for pour instancier à la main les différentes primitives ainsi que les noeuds les connectant entre elles.

module Nbit_adder (co, sum, a, b, ci);
  parameter SIZE = 4;

  output [SIZE-1:0] sum;
  output co;
  input [SIZE-1:0] a, b;
  input ci;

  wire [SIZE:0] c;

  assign c[0] = ci;
  assign co = c[SIZE];

  genvar i;
  generate
    for(i=0; i<SIZE; i=i+1)
      begin:addbit
        wire n1,n2,n3; //internal nets
        xor g1 ( n1, a[i], b[i]);
        xor g2 (sum[i],n1, c[i]);
        and g3 ( n2, a[i], b[i]);
        and g4 ( n3, n1, c[i]);
        or g5 (c[i+1],n2, n3);
      end
  endgenerate
endmodule
Exemple : tiré de la norme Verilog 2001.
parameter SIZE = 2;
genvar i, j, k, m;
generate
  for (i=0; i<SIZE; i=i+1) begin:B1 // scope B1[i]
    M1 N1(); // instantiates B1[i].N1
    for (j=0; j<SIZE; j=j+1) begin:B2 // scope B1[i].B2[j]
      M2 N2(); // instantiates B1[i].B2[j].N2
      for (k=0; k<SIZE; k=k+1) begin:B3 // scope B1[i].B2[j].B3[k]
        M3 N3(); // instantiates B1[i].B2[j].B3[k].N3
      end
    end
 
    if (i>0) begin:B4 // scope B1[i].B4
      for (m=0; m<SIZE; m=m+1) begin:B5 // scope B1[i].B4.B5[m]
        M4 N4(); // instantiates B1[i].B4.B5[m].N4
      end
    end
  end
endgenerate

// Some examples of hierarchical names for the module instances:
// B1[0].N1 B1[1].N1
// B1[0].B2[0].N2 B1[0].B2[1].N2
// B1[0].B2[0].B3[0].N3 B1[0].B2[0].B3[1].N3
// B1[0].B2[1].B3[0].N3
// B1[1].B4.B5[0].N4 B1[1].B4.B5[1].N4
Pour aller plus loin

Maintenant que vous êtes un peu familliers avec la génération de blocs, nous vous conseillons de lire attentivement la norme Verilog 2001 section 12.4 (pages 181 à 191) , qui contient beaucoup plus d'exemples, dont certains bien complexes...



Back to Top

Tâches et fonctions

Pour faciliter l'écriture de codes complexes, Verilog dispose de tâches et de fonctions, qui jouent un peu le rôle des sous-programmes (procedures et fonctions) des langages de programmation habituels.
Les différences entre tâches et fonctions sont les suivantes :

Les fonctions et les tâches :

Les fonctions sont donc utilisées pour les fonctions purement combinatoires (et sans délais).
Les tâches sont utilisées comme des sous-programmes.

Exemples :

task MinMax;
  input [7:0] a, b;
  output [7:0] Min, Max;

  begin
    #3 Min = (a < b) ? a : b;
    #3 Max = (a < b) ? b : a;
  end
endtask

...

MinMax(x, y, z, t);

function [7:0] Min;
  input [7:0] a, b;
    begin
      Min = (a < b) ? a : b;
   end
endfunction

...

z = Min(x, y);

module sequence;

  reg clock;

  initial
    init_sequence; // invoque la tâche init_sequence

  always
    sequence; // invoque la tâche sequence

  task init_sequence; // cette tache opere directement sur le reg clock du module
    begin
      clock = 1'b0;
    end
  endtask

  task sequence; // cette tache opere directement sur le reg clock du module
    begin
      #12 clock <= 1'b0;
      #5 clock <= 1'b1;
      #3 clock <= 1'b0;
      #10 clock <= 1'b1;
    end
  endtask

endmodule



function automatic [63:0] factorial;
  // automatic : chaque appel de la fonction dispose de ses propres variables (n)
  input [31:0] n;

  if (n == 1)
    factorial = 1;
  else
    factorial = n * factorial(n-1);
endfunction
Automatic ou non ???
On peut se demander quel est l'intérêt (hormis pour les fonctions récursives) des fonctions automatiques... Au point de vue "consommation mémoire", qu'en pensez-vous ?

Fonctions constantes

Il est possible d'utiliser certaines fonctions pour le calcul de paramètres. Ces fonctions sont appelées constantes car leurs paramètres doivent être constants. En d'autres termes, ces fonctions doivent pouvoir être exécutées à la compilation (rigoureusement : à l'élaboration). Typiquement, on s'en servirait pour calculer le logarithme de la profondeur d'une RAM pour connaitre la largeur de son bus d'adresse. Des exercices vous seront proposés plus tard dans le cours vous permettant de manipuler ce genre de fonctions.


Back to Top

En résumé

Ce chapitre vous a présenté les façons de décrire la fonctionnalité des processus, c'est-à-dire leur description comportementale.

Les descriptions comportementales ne s'opposent pas aux description structurelles. Elle se complètent : on adopte une description structurelle pour séparer un bloc en sous-systèmes simples, qui eux seront décrits comportementalement.

wire / reg ?


Les processus s'exécutent en parallèle les uns des autres, mais leur déroulement interne est séquentiel. Ils peuvent utiliser la plupart des structures de contrôle du C.

Les processus always s'exécutent en boucle, et nécessitent donc un point d'arrêt pour que le temps puisse s'écouler. Il en existe trois :

L'objectif du prochain chapitre est de :

.

Back to Top