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.
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.
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).
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);
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.
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...
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.
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 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.
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;
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é.
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 :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 :
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.
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 :
Environnement de test de l'additionneur.
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.
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.
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...
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.
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 :
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).
Déclaration du module
On commence par déclarer ce qu'on va décrire, ici un "module" (un bloc, un système) :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 :
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 :
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 :
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.