Préambule

Objectifs

Plutôt que de long discours, voici quelques exemples de modules écrits en Verilog. Chaque exemple introduit un nouveau type de construction / type de donnée.

Pour chaque exemple, passez la souris sur le code pour avoir des explications sur la façon dont il est construit.

Plan du chapitre

Back to Top

AND3

On va voir

Commençons par le plus simple des modules ou presque : une porte combinatoire AND à trois entrées. En Verilog, comme en SystemC, les blocs sont appelés modules
Voici une description possible d'une porte AND à 3 entrées.

Passez la souris sur les différents éléments du code pour avoir des explications sur la signification de chaque instruction.

code and3
Déclaration du module
On commence par déclarer ce qu'on va décrire, ici un "module" (un bloc, un système) : 
module and3(e1, e2, e3, s); 
Cela est fait par le mot-clef module, suivi d'un nom (ici and3), et de la liste des entrées-sorties du module
.
Ces entrées-sorties sont appelées ports. L'ordre des entrées-sorties n'est pas important ici. Cependant une bonne habitude est de choisir une convention et de s'y tenir !
Comme en C, chaque ligne est terminée par un point-virgule, à l'exception des mots commençant par "end..." qui n'en ont pas besoin.
Déclaration des ports

Puis viennent les déclarations du sens des ports :

  • input pour les entrées du module
  • output pour les sorties
  • inout pour les ports bidirectionnels

Au passage, on doit aussi déclarer la taille des ports. Par défaut, ils sont sur 1 bit. Pour un bus A sur 8 bits, on aurait ce style de déclaration :

input [7:0] A; 
Assignement continu
Les portes AND sont combinatoires. Autrement dit, la sortie n'est fonction que des entrées, pas du temps.

La valeur de la sortie ne dépendant pas du temps, on peut utiliser l'instruction assign, qui assigne une expression à une sortie ou un fil. On ne peut avoir qu'une seule expression assign par sortie ou fil.

Les expressions utilisables sont globalement les mêmes qu'en C :
  • le ET est noté &
  • le OU est noté |
  • le XOR est noté ^
  • le NON est noté ~ (tilde)
Pour une porte NOR3, on aurait eu :
assign s = ~(e1 | e2 | e3);
Fin du module

La fin du module est déclarée par l'instruction endmodule.

Comme cela a été indiqué plus haut, les mots-clefs commençant par "end" n'ont pas besoin de point-virgule.



Back to Top

Additionneur complet 1 bit

On va voir


Voici une description possible d'un additionneur complet 1 bit.

Rappel : un additionneur complet 1 bit a trois entrées (a, b, et cin la retenue entrante), et deux sorties (le bit de somme s et le bit de retenue sortante cout).

code full adder 1bit
Déclaration du module
On déclare ici un module appelé full_add. Il dispose de 3 entrées, et 2 sorties.

L'ordre des ports n'a pas d'importance en soi. On aurait très bien pu déclarer le module ainsi (même si c'est un style extrêmement mauvais) :

module full_add(s, a, cout, b, cin);

Déclaration des ports
Les directions des ports sont indiquées ici sous forme condensée.
Pour deux ports en entrée sur 8 bits, on aurait écrit
:
input [7:0] a, b;

Verilog 2001, la norme actuelle, permet de grouper la déclaration du module et celle des directions des ports. On aurait donc aussi pu écrire ceci :

module full_add (input a, input b, input cin, output s, output cout);
Assignement continu
Verilog permet de grouper des signaux entre eux pour former un bus. Le bit de poids fort est toujours à gauche, celui de poids faible à droite.
Ici, {cout, s} représente un bus sur 2 bits, formé de cout en bit de poids fort, et s en bit de poids faible.

Par définition de l'additionneur, cout et s sont tels que 2*cout+s=a+b+cin. D'où la relation écrite :
assign {cout, s} = a + b + cin;

Cette description ne précise pas comment l'additionneur est implémenté en terme de portes. Elle donne juste la fonctionnalité du module. C'est le rôle des synthétiseurs de transformer un description fonctionnelle en portes.


Back to Top

Additionneur complet 8 bit

On va voir


Verilog, comme tous les HDL, permet de décrire les système sous forme structurelle. C'est-à-dire, au lieu de décrire un gros système dans un seul gros fichiers, de le séparer en sous-systèmes (sous-modules), eux-mêmes subdivisés en sous-modules, jusqu'à obtenir des modules facilement compréhensibles.

Nous allons ici décrire un additionneur 8 bits en assemblant des additionneurs 1 bit. On aurait aussi pu décrire la fonctionnalité de l'additionneur 8 bits (par opposition à sa structure), cela sera vu à la fin de cet exemple-ci.

On suppose ici que le code de l'additionneur 1 bit vu précédement est disponible, soit dans un fichier à part, soit dans le fichier de l'additionneur 8 bits.

code full adder 8bits
Déclaration du module
Le module full_add8 a lui aussi 5 ports :
  • a, b et s sur 8 bits
  • cin et cout sur 1 bit.
On aurait aussi pu utiliser les déclaration simplifiées des ports :

module full_add8 (input [7:0] a,
    input [7:0] b,
    input cin,
    output [7:0] s,
    output cout);

Déclaration des signaux internes

Les additionneurs 1 bits sont reliés entre eux : la retenue sortante de l'un est la retenue entrante du suivant.

Pour relier les additionneurs entre eux, on instancie un signal. Le type des signaux utilisés pour l'interconnexion entre modules est wire. Ils peuvent être sur un ou plusieurs bits.

Histoire de faire concis, au lieu d'instancier 7 wire différents pour relier les additionneurs, on instancie un bus de wire sur 7 bits appelé retenue :

wire [7:0] retenue; 

Par défaut, si on précise rien, les ports d'entrée sortie déclarent un signal de type wire implicite. Tout se passe comme si on avait écrit :

wire [7:0] a, b, s;
wire cin, cout;
wire [6:0] retenue;

Certains préfèrent le spécifier, les déclarations implicites étant parfois dangeureuses...

Instanciation des sous-modules

On instancie un module de cette façon :

module_a_instancier nom_de_l'instance (signaux connectés à l'instance);

Chaque instance doit avoir un nom unique.

Pour relier le ports de l'instance aux signaux du module courant, on utilise ici la connexion par position : le premier signal est relié au premier port, le deuxième au deuxième, etc.

Cette façon de faire oblige à respecter l'ordre dans lequel les ports ont été déclarés.  Il existe une autre façon de relier les signaux, plus propre, elle sera étudiée plus tard.

Exercice : plutôt que de décrire l'additionneur 8 bits sous forme d'assemblage d'additionneurs 1 bits, on aurait pu directement décrire son comportement (sa fonctionalité). En vous inspirant du code de l'additionneur 1 bit, écrivez une description concise de l'additionneur 8 bits.

[Afficher la réponse]



Back to Top

Bascule D

On va voir
Jusqu'à présent, les modules étudiés étaient purement combinatoires.  Nous allons maintenant voir comment décrire un module séquentiel.

code dff
Déclaration du type des ports

Les ports d'un module ont un nom, un sens, et un type. Jusqu'à présent, leur type était implicite : wire.

Le type wire n'est utilisable que dans deux cas :

  • pour connecter des modules entre eux (mais ce n'est pas le seul utilisable)
  • pour modéliser de la logique combinatoire.  

Pour modéliser la logique séquentielle, un autre type existe : reg.
Les reg mémorisent la dernière valeur qui leur a été affectée. Ils sont donc aptes à modéliser les éléments mémorisants, et par extension la logique séquentielle (et, comme on le verra plus tard, ils peuvent aussi modéliser de la logique combinatoire.).

Le type de la sortie q sera donc reg. Contrairement au type wire, le type reg doit être spécifié explicitement.

Codage des processus

En Verilog, l'utilisation des type reg implique forcément l'instanciation de processus. Un processus est un enchaînement d'instructions décrivant son comportement. Il possède une liste de sensibilité spécifiant à quel moment le processus doit s'exécuter.

Un processus commence généralement par le mot clef always, qui spécifie qu'une fois qu'il a été exécuté il doit se mettre en attente pour être ré-exécuté.

Il faut ensuite spécifier à quel moment le processus doit être réveillé. Pour une bascule D, c'est à chaque front montant de l'horloge :

@(posedge clk)

Enfin, le corps du processus contient les instructions à exécuter. Pour une bascule D, ça consiste à recopier la valeur de l'entrée sur la sortie. On utilise ici une affectation non bloquante, exprimée au moyen d'une flèche :

q <= d;



Back to Top

Compteur 8 bits avec reset asynchrone

On va voir
Voici une description possible d'un compteur synchrone sur 8 bits, avec reset asynchrone actif à l'état bas.

code compteur
Déclaration des signaux
Un compteur est un processus séquentiel, dont la valeur doit être stockée (mémorisée) entre chaque coup d'horloge. La valeur du compteur est donc de type reg [7:0]
Processus principal
La valeur du compteur peut changer sur deux événements :
  • front montant de l'horloge
  • front descendant du reset

En Verilog, on peut combiner plusieurs fronts dans une liste de sensibilité par le mot-clef or. D'où la liste de sensibilité du processus de comptage : posedge clk or negedge resetn.

Les processus always peuvent utiliser des instructions de haut niveau similaires à celles du C. On utilise ici un if pour déterminer quel est l'événement qui a déclenché le processus. Selon l'état de resetn, soit le compteur est remis à zéro, soit il est incrémenté.



Back to Top

Registre à décalage

On va voir
On cherche à réaliser un registre à décalage sur 8 bits, possèdant une sortie supplémentaire (match) active à 1 si le contenu du registre à décalage vaut une valeur précise (val).

code shifter
Déclaration des signaux
La variable shift stocke la valeur du registre à décalage Elle est donc de type reg [7:0].
Processus de décalage

Les processus d'un module s'exécutent tous en parallèle les uns des autres, comme cela a été vu dans la leçon sur les HDL. Cependant, un bloc always peut spécifier une suite d'instructions, qui seront alors exécutées de façon séquentielle, les unes après les autres. Cela est fait au moyen des mots clefs begin...end, qui délimitent une section séquentielle.

On commence donc, à chaque front d'horloge, par mettre à jour le bit de poids faible du registre à décalage (shift[0]), puis les autres bits. Ces deux instructions sont exécutées l'une après l'autre. Cependant, comme les affectation sont différées (en Verilog, on les appelle non bloquantes), elles ne prendront effet qu'une fois que tous les processus auront été exécutés, donc après la sortie du bloc always.

Si les affectation avaient été immédiates (shift[0] = in;), elles auraient pris effet immédiatement, et in aurait été recopié non seulement dans shift[0] (première instruction) mais aussi dans shift[1] (deuxième instruction).

Ces deux instructions auraient d'ailleurs pu être remplacées par une seule :
shift[7:0] <= {shift[6:0], in};
Processus combnatoires

La génération de la sortie match est un processus combinatoire. Le type de match est donc wire (implicite).
De même pour out, qui ne fait que valoir shift[7]...

Ce module comporte en tout 3 processus :

  • le bloc always, processus explicite
  • les deux assign, processus implicites.

Tous les processus d'un module s'exécutent les uns en même temps que les autres. Mais l'intérieur du processus always est séquentiel, à cause du bloc begin...end.


Back to Top

Simulation

Premier exemple

Commençons par un premier exemple. Ouvrez un terminal (xterm), récupérez l'archive first_steps.tgz en cliquant sur ce lien, et sauvez-la dans votre répertoire de travail.
Décompressez cette archive (tar zxvf first_steps.tgz), et placez-vous dans le réprtoire du premier exemple (cd ex1). Vous y trouverez deux fichiers :

Le fichier full_add.v ne contient que la description de l'additionneur. Pour vérifier que cette description est correcte, on peut en effectuer une simulation. Pour cela nous allons lui présenter des vecteurs d'entrée soigneusement choisis, et vérifier que les sorties correspondent bien à nos attentes.

Pour cela, nous allons modéliser (en Verilog toujours) un environnement de test. Pour cela, ouvrez le fichier testbench.v.


code testbench
Structure d'un testbench

Comme pour l'additionneur, l'environnement de test est un module. Mais ce module n'a ni entrées ni sorties : il est autonome, produit lui-même les signaux à envoyer à l'additionneur, et vérifie que les sorties de l'additionneur sont bien correctes, qui correspond au schéma suivant :

testbench

Environnement de test de l'additionneur.

Déclaration des signaux
Il nous faut déclarer une équipotentielle pour chaque entrée et sortie de l'additionneur.
Les signaux connectés aux entrées de l'additionneur sont produits par le testbench de façon séquentielle (les uns après les autres). Ils sont donc de type reg.
Les signaux connectés aux sorties de l'additionneur sont forcément de type wire, car gérés par l'additionneur (ils reflètent l'état des ports de sortie).
Génération des vecteurs de test

Nous ne testerons pas toutes les combinaisons d'entrées possibles, mais juste 4 d'entre elles. Entre chaque combinaison, nous ferons une pause (virtuelle) de 5ns, donnée par l'instruction #5;

La génération des vecteurs de test est produite de façon séquentiel, au moyen d'un processus similaire aux blocs always : initial. La seule différence vient du fait que les processus initial sont exécutés une seule fois au début de la simulation, et plus jamais par la suite. On les utilise donc pour produire des séquences qui ne doivent avoir lieu qu'une seule fois lors de la simulation.

Vérification des sortie de l'additionneur

La génération des vecteurs d'entrée étant faite, il faut maintenant vérifier les sorties. Nous ferons ça manuellement, en affichant leur valeur à chaque combinaison. Pour cela, nous utilserons les instructions système $display et $monitor dont la syntaxe est très similaire à celle du printf du C.

L'instruction $display permet d'affichier une chaîne, comme le ferait printf. Nous l'utilisons ici dans sa forme la plus simple.

L'instruction $monitor est similaire à $display, à la différence près qu'elle est executée à chaque changement d'état d'une des variable passées en argument.

Production de chronogrammes

Le debug d'un système complexe peut devenir rapidement fastidieux avec les $monitor et $display. Il vaut mieux utiliser des chronogrammes, ou des techniques plus évoluées. Pour générer des chronogrammes, Verilog propose les instructions $dump...

  • $dumpfile : donne le nom du fichier dans lequel seront sauvés les chronogrammes (extension .vcd).
  • $dumpvars : détermine les variables à sauvegarder.

La simulation

Le module additionneur et son environnement de test étant écrit, reste à lancer effectivement la simulation. Pour cela, nous allons utiliser le simulateur libre Verilog cver.
Placez-vous dans le répertoire contenant les fichiers testbench.v et full_add.v, et lancez le simulateur : cver testbench.v full_add.v

La sortie du simulateur devrait être la suivante :

GPLCVER_2.00d of 09/14/04 (Linux-elf).
Copyright (c) 1991-2004 Pragmatic C Software Corp.
All Rights reserved. Licensed under the GNU General Public License (GPL).
See the 'COPYING' file for details. NO WARRANTY provided.
Today is Sat Oct 2 00:20:28 2004.
Compiling source file "full_add.v"
Compiling source file "testbench.v"
Highest level modules:
test

time, a, b, cin, s, cout
0 0 0 0 0 0
5 1 0 0 1 0
10 1 1 0 0 1
15 1 1 1 1 1
0 simulation events and 27 declarative immediate assigns processed.
19 behavioral statements executed (6 procedural suspends).
Times (in sec.): Translate 0.0, load/optimize 0.1, simulation 0.1.
There were 0 error(s), 0 warning(s), and 2 inform(s).
End of GPLCVER_2.00d at Sat Oct 2 00:20:28 2004 (elapsed 0.0 seconds).

Nous pouvons vérifier manuellement que les sorties de l'additionneur sont bien correctes.

Pour le vérifier à partir des chronogrammes : gtkwave add.vcd, puis cliquez sur "Show All" .

Pour utiliser les autres simulateurs Icarus Verilog et Modelsim, on se reportera à la page sur les outils.

Simulation avancée

Dans le répertoire ex2, vous trouverez un autre exemple de système. Nous reprenons le code de l'additionneurs, mais sous plusieurs formes :

De plus, l'environnement de test, testbench.v, a été enrichi pour permettre une vérification plus fine :

Exercice
  1. Compilez et testez ces trois modules
    • avec cver
    • avec iverilog
    • avec modelsim
    Les résultats sont-ils corrects ?

  2. Recupérez ce testbench-ci (testbench2.v), et testez-le aussi avec les trois simulateurs.
    • Examinez les chronogrammes produits, avec gtkwave.
    • Les simulateurs mentionnent des erreurs, mais jamais au même endroit, et les chronogrammes semblent justes.
    Avec vos connaissances sur les principes de simulation, expliquez pourquoi !

  3. Quelle modification simple pourrait-on apporter à testbench2.v pour avoir un code correct (mais sale) ?

Back to Top

En résumé


Vous avez vu ici quelques exemples de modules en Verilog, comment instancier un module et lui fournir rapidement quelques vecteurs de test.

L'objectif des prochains chapitres est d'étudier plus précisément les différents types de données disponibles, la façon de décrire structurellement un système (module, signaux, ports), puis fonctionnellement (processus, événements).

Back to Top