Tâches systèmes
Certaines construction ne sont typiquement pas synthétisable :
celles qui s'adressent directement à un simulateur (pour garder une
trace des chronogrammes, lancer ou stoper une simulation, etc...). Ces
constructions sont appelées tâches systèmes. Elle
commencent toutes par le symbole $.
Nous présentons ici les plus communes, mais il y en bien d'autres !...
Tâche d'affichage
- $display,
$write
- Equivalent du printf
de C. $display ajoute
automatiquement un retour à la ligne en fin de
chaîne, alors que $write
non.
Exemple : $display("La valeur
hexadecimale de a est %h, sa valeur binaire est %b", a, a);
- $monitor
- Cette directive prend en argument des variables, des noeuds
ou des chaînes de caractères.
A chaque changement d'état d'un des paramètres, l'ensemble des
arguments est affiché.
Cette directive n'est évaluée qu'à la toute fin de chaque cycle de
simulation, même après les affectations non
bloquantes.
Exemple : $monitor($time, ", a=%b, \t
b=%b, \t c=%h", a, b, c);
Il ne peut y avoir qu'une seule liste de variable
monitorée. $monitor est
donc appelée dans un bloc initial
seulement. Un appel successif à $monitor
remplace le précédent. - $strobe
- Cette directive est similaire à $monitor
: elle affiche le contenu de ses arguments à la toute fin du cycle de
simulation. Mais on doit l'appeler manuellement, elle ne surveille pas
les changements des arguments.
Dans chacune de ces fonctions, le spécificateurs de format sont les
suivants
:
Spécificateur de format |
Description |
%h
ou %H |
Affiche en hexadécimal |
%d
ou %D |
Affiche en
décimal |
%oou
%O |
Affiche en octal |
%b
ou %B |
Affiche en binaire |
%c
ou %C |
Affiche un caractère |
%s
ou %S |
Affiche une chaîne
de caractères |
%l
ou %L |
Affiche la bibliothèque où
est stockée le module courant |
%m
ou %M |
Affiche le nom du module /
processus / bloc nommé courant |
%t
ou %T |
Affiche un temps |
%u
ou %U |
Affiche une valeur
logique bivaluée (0 ou 1) |
%z
ou %Z |
Affiche une valeur logique
multivaluée (0, 1, X, ou Z) |
%e
ou %E |
Affiche un flottant sous
forme exponentielle |
%f
ou %F |
Affiche
un flottant sous forme décimale |
%g
ou %G |
Choisit
automatiquement entre %f
et %e la forme produisant
l'affichage le plus court |
Tâches de contrôle de simulation
- $stop
- Permet d'arrêter
temporairement la simulation. Elle place le simulateur en mode
interactif (si celui-ci le supporte). Le concepteur peut alors faire du
pas-à-pas, examiner les variables, les modifier, ...
- $finish
- Permet d'arrêter définitivement la simulation.
Généralement, $finish
ferme aussi le simulateur...
Tâches de génération de chronogrammes
Ces
fonctions ne sont utiles qu'avec un visionneur de chronogrammes externe
(GtkWave par exemple). Dans notre cas, Modelsim utilise le sien, et
dump automatiquement les variables qu'on lui spécifie !
- $dumpfile
- Spécifie le nom du fichier de chronogrammes à générer. Le
nom du fichier doit généralement se terminer par .vcd
Cette instruction est spécifiée en début de simulation, dans un bloc initial.
Exemple : $dumpfile("test_counter.vcd");
- $dumpvars
-
Permet de spécifier
les variables dont il faut garder une
trace.
Sans argument : toutes les variable du circuit. Sinon, il y a deux
arguments : la profondeur à atteindre et le nom d'un module. Toutes les
variables de ce module, jusqu'à la profondeur spécifiée sont tracées.
Une profondeur de 0 signifie profondeur illimitée.
Exemple : $dumpvars(1,top); // trace
toutes les variable de top, pas celles des modules instanciés dedans.
Entrées-sorties sur fichier
- $fopen, $fclose, $fmonitor,
$fdisplay
- Ouverture / fermeture de fichier, et équivalents de $monitor et $display
mais sur fichier. Les descripteurs de fichiers sont des bits stockés dans un integer. Conséquence, on ne peut avoir que 32 fichiers ouverts en même temps (dont stdout, qui a le descripteur 1). Il est possible de faire des sorties sur plusieurs fichiers en même temps en ORant bit à bit les descrpteurs de fichiers.
- $readmemb, $readmemh
- Permet la lecture d'un fichier ASCII, contenant des mots
binaires ou hexadécimaux, pour initialiser une mémoire.
Exemples
:
module testmemory;
reg [7:0] memory [9:0];
integer index;
initial begin
$readmemb("mem.dat", memory);
for(index = 0; index < 10; index = index +
1)
$display("memory[%d] = %b",
index[4:0], memory[index]);
end
endmodule
initial begin
integer outfile;
outfile = $fopen("res.dat");
$fmonitor(outfile, $time, , S);
end
Le format des fichiers utilisés par
readmemh /
readmemb est le suivant :
// mem.dat
// Les commentaires sont acceptés
AF // Contenu de l'adresse 0
23 // Contenu de l'adresse 1
@ 87 // On saute directement à l'adresse 87
1A // Contenu de l'adresse 87
FE // Contenu de l'adresse 88
- $fgetc(fd), $fgets(str, fd), $ungetc(c, fd)
- Equivalents aux fonctions du même nom de C.
- $fscanf(fd, format, args)
- Equivament du fscanf de C. Retourne le nombre d'items convertis. En cas d'erreur, retourne 0. En cas de fin de fichiers, retourne EOF.
Attention : les arguments de $fscanf se voient affectés une valeur de façon immédiate (par une affectation bloquante).
- $fread(reg, fd)
$fread(mem, fd, start, count) - Permet
de lire le contenu binaire d'un fichier et de le stocker dans un reg ou
une mémoire. Si la mémoire est plus large que 8 bits, alors elle est
remplie en big-endian.
start est optionnel, et indique l'index du premier élément de la mémoire à écrire. S'il n'est pas présent, l'index de départ vaut 0.
count
est optionnel , et indique le nombre d'emplacements de la mémoire à
charger. Par défaut, c'est autant d'emplacements que possible.
Cette fonction renvoie le nombre d'éléments effectivement lus/écrits.
On dispose aussi des fonction $ftell, $fseek, $rewind, $fflush, $ferror, ... Pour plus de détails on se rapportera à la norme Verilog.
Récupération du temps
- $time, $realtime
- Retourne le temps actuel de la simulation, arrondi à l'unité (timescale) du module où on se trouve.
$time renvoie une entier sur 64 bits, $realtime renvoie un flottant.
- $printtimescale
- Affiche la timescale du module courant.
- $timeformat
- Permet de spécifier comment seront affichés les temps dans le cas d'un $display("%t") par exemple. On se rapportera à la norme pour plus de détails.
Sémantique de
synthèse
L'ensemble de Verilog étant utilisable pour la simulation,
nous allons pencher sur ce qui est spécifique à la synthèse.
La synthèse permet d'aboutir à une représentation structurelle
de
bas niveau d'un circuit. C'est un processus automatisé, effectué par
des logiciels appelés synthétiseurs (!).
Les synthétiseurs actuels acceptent comme langage d'entrée VHDL et
Verilog, et SystemC pour certains d'entre eux.
Le problème est qu'ils n'acceptent pas tout Verilog (ni tout VHDL) :
- parfois parce que c'est normal ($fopen
est évidement non synthétisable),
- parfois parce qu'ils sont limités
Le processus de synthèse faisant intervenir des problèmes
algorithmiques les plus complexes, il faut aider le synthétiseur à
effectuer son travail
- par des directives de synthèse, soit sous forme de
commentaires Verilog, soit sous forme d'un fichier de commandes externes
- par un style de codage clair.
Même si un code vous semble synthétisable (transformable en
matériel
par la pensée), le synthétiseur n'en sera peut-être pas capable : des
restrictions fortes existent sur le langage synthétisable, ces
restrictions dépendant souvent du synthétiseur.
Verilog synthétisable
Le Verilog synthétisable est un sous-ensemble de Verilog ne
décrivant que du matériel :
- pas de fichiers
- pas de types irréalistes (réels)
- pas de temps de propagation (#10)
Les ressources matérielles doivent être
- identifiées : en clair, si vous ne
savez pas où
sont les registres dans votre code, il est probable que le synthétiseur
ne le saura pas non plus !
- allouées : à vous de vous assurer que
tout ce que vous écrivez est réalisable (fonctions mathématiques, ...)
- ordonancées
: vous devez avoir une idée claire de l'enchaînement des calculs (quel
signal provoque un changement sur quel autre), et écrire votre code de
façon suffisament non ambigüe pour que le synthétiseur le comprenne
Il faut avant tout penser en terme de matériel.
Sinon, dans le meilleur des cas le synthétiseur refusera de
synthétiser, et dans le pire il synthétisera n'importe quoi sans vous
le dire.
Exemples
: les codes ci-dessous vous semblent-t-ils
synthétisables ? Attention, la réponse n'est pas forcément "non" ! Si
c'est "oui", expliquer ce qui sera synthétisé.
// ressources identifiées...
module top(out, a, b);
input a, b;
output out;
reg out;
always@(b)
out = a;
endmodule
// ressources allouées...
module top(out, in);
input [3:0] in;
output [15:0] out;
assign out = 2**in;
endmodule
// ressources ordonancées...
module top(out, a, b, c, d);
input a, b, c, d;
output out;
reg out;
always@(posedge a)
begin
if(b)
@(posedge c) out = d;
else
out = d;
end
endmodule
// ressources ordonancées...
module top(clk, in, out);
input in, clk;
output out;
reg out, in1;
always@(posedge clk)
in1 = in;
always@(posedge clk)
out = in1;
endmodule
Boucles
On peut utiliser les boucles, mais elles sont déroulées avant
la synthèse :
- les bornes des boucles for
doivent donc être constantes, et définies à la synthèse
- les boucles while
doivent généralement avoir des conditions constantes (ce qui rend leur
utilité un peu faible)
Exemples
:
Exemple |
Commentaire |
parameter max = 8;
for(i=0; i<max; i=i+1)
<blabla i>
|
synthétisable, mais
tranformé lors de la synthèse en :
<blabla 0>
<blabla 1>
<blabla 2>
<blabla 3>
<blabla 4>
<blabla 5>
<blabla 6>
<blabla 7>
|
reg max;
for(i=0; i<max; i=i+1)
<blabla i>
|
non synthétisable
|
while(0)
|
synthétisable |
reg a;
while(a)
|
généralement
non synthétisable |
Synchronisation
Les clauses wait
sont parfois acceptées mais avec de fortes restrictions, comme par
exemple
- un seule clause wait
par processus
- forcément en première instruction
Les synchronisations événementielles sont acceptées. Attention
alors à l'inférence
accidentelle de latchs !
Exemple
:
reg [2:0] curr_state, next_state;
always @(curr_state);
begin
case (curr_state)
0: next_state <= 3'd4;
1: next_state <= 3'd6;
2: next_state <= 3'd1;
4: next_state <= 3'd5;
5: next_state <= 3'd2;
6: next_state <= 3'd0;
endcase
end
Exercice
:
- le code ci-dessus est-il combinatoire ou séquentiel ?
- comment le modifier pour qu'il soit bien combinatoire ?
[Afficher
la réponse]
Réponse :
- Dans le cas ou curr_state
vaut 7, next_state n'est
pas affecté. Il doit donc garder sa valeur : le processus est
séquentiel (inférence d'un latch).
- Pour le transformer en séquentiel, il suffit que next_state soit affecté dans tous
les cas.
- Soit on rajoute une clause default
au case.
- Soit on rajoute une initialisation par defaut de next_state avant le case
Blocs initial
Les blocs initial
sont rarement synthétisés.
Il est préférable d'initialiser les registres à la main (sur un signal
de reset ou de preset par exemple)