Verilator, le simulateur Verilog le plus rapide du monde

Magazine
Marque
Hackable
Numéro
43
Mois de parution
juillet 2022
Spécialité(s)


Résumé

Concevoir des composants numériques en Verilog passe nécessairement par la simulation. Pour simuler du Verilog, il existe un logiciel open source nommé Icarus qui remplit bien sa fonction. Il existe également des simulateurs non libres qui sont généralement plus performants. Mais tous ces simulateurs ont le même défaut, ils sont lents. Verilator est un simulateur un peu particulier qui se concentre sur la partie synthétisable du Verilog et génère un objet C++ que l’on va simuler au moyen d’un programme écrit dans ce même langage. Cette approche permet un gain de l’ordre d’une trentaine de fois plus rapide qu’Icarus dans l’exemple que nous allons voir. Il est également nettement plus rapide que tous les simulateurs commerciaux.


Body

La simulation est nécessaire en Verilog. En effet, déverminer un gateware « en réel » nécessite du matériel onéreux et des montages électroniques compliqués. Avec la simulation, il est possible de visualiser n’importe quel signal interne sans avoir à brancher de sonde ou à souder des connecteurs sur le produit fini.

Traditionnellement, on génère les stimulus des signaux d’entrée en les décrivant en Verilog. En effet, le langage inclut toutes les fonctions nécessaires à l’écriture des bancs de test en simulation. Il est donc possible de se contenter du Verilog pour tester l’intégralité de son composant.

Le problème de la simulation, c’est qu’elle mobilise beaucoup de ressources tant en termes de calcul processeur que de mémoire vive ou même de mémoire de masse pour enregistrer les traces. Et surtout, cette simulation prend du temps.

L’approche proposée par Verilator [1] est de transformer la sous-partie « synthétisable » du Verilog en un objet C++ que l’on instancie ensuite dans un banc de test écrit en C++. Bien sûr, cela suppose que les développeurs et développeuses aient des compétences en C++, en plus de leurs connaissances du Verilog. Il est également possible d’utiliser la librairie SystemC du consortium Accelera, spécialisée dans la description matérielle (HDL).

Afin de bien saisir les tenants et les aboutissants de ces deux méthodes, nous allons réaliser un projet Verilog qui affiche une image sur un écran VGA. Nous le simulerons d’abord avec Icarus et un banc de test en Verilog, puis nous passerons à la simulation Verilator avec l’écriture d’un banc de test en C++.

Verilator figure 01-s

Figure 1

1. Le projet vidéo

Le module vidéo est une reprise du projet SimpleVga présenté dans l’article de présentation de la carte Colorlight [2].

Il consiste à afficher les bandes de couleurs différentes entourées d’un rectangle blanc. Le tout avec une résolution de type VGA « classique » de 640x480.

Le module est composé de deux fichiers Verilog : le « top », nommé RgbVideo donné ci-dessous se charge de calculer les couleurs des pixels à envoyer en sortie.

// Fichier: RgbVideo.v
`timescale 1ps/1ps
`define VGA640X480
 
module RgbVideo
(
    input clk_i,
    input rst_i,
    // (0) VGA output
    output red_o,
    output green_o,
    output blue_o,
 
    output vsync_o,
    output hsync_o
);
 
wire display_on;
wire [10:0] hpos;
wire [9:0] vpos;
 
/* (1) hvsync */
HVSync hvs(
    .clk_i(clk_i),
    .rst_i(rst_i),
    .hsync_o(hsync_o),
    .vsync_o(vsync_o),
    .display_on,
    .hpos(hpos),
    .vpos(vpos)
);
 
localparam SQWIDTH = 2;
 
`ifdef VGA640X480
localparam HWIDTH = 640;
localparam VWIDTH = 480;
`endif
 
/* (2) rectangle blanc */
wire square = (hpos <= SQWIDTH)
  || ((hpos <= (HWIDTH - 1)) && (hpos >= (HWIDTH - SQWIDTH - 1)))
  || (vpos <= SQWIDTH)
  || ((vpos <= VWIDTH - 1) && (vpos >= VWIDTH - SQWIDTH - 1));
 
/* (3) raies de couleurs */
assign red_o = display_on & ((hpos[4] == 1'b1) | square);
assign green_o = display_on & ((hpos[5] == 1'b1) | square);
assign blue_o = display_on & ((hpos[6] == 1'b1) | square);
 
endmodule

Chaque couleur (0) est codée sur 1 bit, quand il est à 1 on envoie 0,7 v sur le signal VGA pour afficher la couleur, et quand il est à 0 on laisse la sortie à 0 volt.

Le niveau de tension est forcé à 0,7 v au moyen de résistances en série comme présenté sur la figure 2.

Verilator figure 02-s

Fig. 2 : Branchement du câble VGA sur les sorties FPGA de la carte Colorlight.

Nous avons ainsi 2 couleurs possibles par signal pour afficher les différentes couleurs à l’écran.

Le module HVSync (1) se charge quant à lui de générer les différents signaux de synchronisation vidéo hsync_o, vsync_o et display_on. Ce module génère également les compteurs de position horizontale hpos et verticale vpos.

Cette partie génération des signaux de synchronisation est complexe, car il faut configurer les différents temps ainsi que les « pauses » avant et après l’affichage d’une ligne.

Le motif affiché à l’écran est composé de raies de toutes les couleurs affichables (3) entourées d’un rectangle blanc (2). Le rendu final est visible avec l’image 3.

Tout le code présenté ici est bien sûr disponible sur le projet GitHub [3] de l’auteur avec tous les scripts de configuration et Makefiles nécessaire à la reproduction des manipulations décrites ici.

Dans le cadre de cet article, nous nous contenterons de simuler (faire tourner) le module pour enregistrer une image vidéo de sortie au format PPM [4]. Nous inclurons également les zones non affichables de synchronisation dans l’image pour les visualiser (en noir).

L’avantage de ce projet est qu’il est facile à simuler, puisque les seuls signaux d’entrée nécessaires sont l’horloge clk_i et le reset rst_i, l’image affichée étant toujours la même.

2. Simulation Verilog avec Icarus

Le Verilog est un langage de description matériel qui inclut la partie simulation des composants. L’approche classique pour simuler du Verilog est tout naturellement d’utiliser le même langage pour la partie description des stimulus que celui pour la synthèse. Une fois écrit en Verilog, notre banc de test pourra être exécuté par n’importe quels simulateurs du marché.

Le marché est composé de trois principaux simulateurs commerciaux, qui n’aiment pas trop être comparés mutuellement. Certains ont même mis dans leurs licences une clause interdisant de faire des « benchmarks ». On pourrait disserter sur la légalité de ce genre de clause, mais plutôt que de se frotter aux troupeaux d’avocats, nous nous contenterons de les nommer « les trois gros » [5] comme le fait Wilson Snyder dans ses présentations de Verilator.

Heureusement pour nous, il existe également un simulateur Verilog libre nommé Icarus [6]. Ce simulateur, maintenu par Stephen William, est bien installé dans le paysage open source du monde matériel et supporte bien les différents standards du langage. Malgré ce que pourrait laisser penser son site internet (très web95), le projet est toujours activement maintenu.

Que ce soit avec Icarus ou « les trois gros », la simulation est de type « piloté par événements » (event driven). Le simulateur avance par pas. À chaque pas de temps, si le calcul des signaux déclenche d’autres processus, ils seront activés et calculés avant de faire avancer le pas de temps. Cet ordonnancement du calcul est dynamique, il est recalculé à chaque pas de simulation.

Cette manière de faire permet de simuler les pas temporels ainsi que toute la partie « non synthétisable » du Verilog, ce que ne peut pas faire Verilator.

2.1 Installation d’Icarus

Icarus est un logiciel relativement mature, on peut se rabattre sur le paquet de sa distribution. Avec une distribution dérivée de Debian, on utilisera tout simplement apt :

sudo apt install iverilog

Les sources Verilog du projet disponible sur le dépôt de l’auteur sont stockées dans le répertoire hdl/src :

hdl/
├── icarus
│   └── Makefile
├── src
│   ├── HVSync.v
│   ├── RgbVideo_tb.v
│   └── RgbVideo.v
└── verilator
    ├── Makefile
    └── sim_main.cpp

Le répertoire icarus contient un Makefile qui automatise le lancement des commandes que nous allons décrire dans l’article. En plus de son Makefile, le répertoire verilator contient le fichier source C++ des stimulus du banc de test.

2.2 Banc de test

Pour le moment, nous n’avons écrit que du code Verilog synthétisable pour décrire l’architecture matérielle du composant. Passons maintenant à la simulation du composant via l’écriture d’un banc de test.

L’objectif de la simulation est de générer suffisamment de cycles d’horloge de manière à générer une image VGA complète. Nous allons donc écrire pour cela un « banc de test » (testbench) en Verilog qui va commuter les signaux d’horloge et de reset, et lire les signaux de couleurs en fonction de la synchronisation.

La convention de nommage veut que nous mettions le code du banc dans un fichier du même nom que le composant top suivi de _tb. Nous appellerons donc notre fichier de test RgbVideo_tb.v avec le code ci-dessous que nous allons décrire :

//Fichier RgbVideo_tb.v
/* (0) */
`timescale 1ns/1ns
 
//`define DUMPVARS
 
/* (1) module sans entrée/sortie */
module RgbVideo_tb;
 
/* (2) Signaux de connexion */
reg clk_i = 0;
reg rst_i;
wire red_o;
wire green_o;
wire blue_o;
wire vsync_o;
wire hsync_o;
 
/* (3) Instanciation du top*/
RgbVideo top(
  .clk_i (clk_i ),
  .rst_i (rst_i ),
  .red_o (red_o ),
  .green_o(green_o),
  .blue_o (blue_o ),
  .vsync_o(vsync_o),
  .hsync_o(hsync_o)
);
 
/* (4) Horloge */
always #1 clk_i <= !clk_i;
 
/* (5) banc de test principal*/
initial
begin
`ifdef DUMPVARS
  $dumpfile("RgbVideo_tb.vcd");
  $dumpvars(0, top);
`endif
 
  $display("%0t, Début de test", $time);
  rst_i = 1'b1;
  #5;
  rst_i = 1'b0;
  #2;
  while(!vsync_o) #1;
  #1;
  while(vsync_o) #1;
  #2;
  $display("%0t, vsync_o %1d", $time, vsync_o);
  $display("%0t, Fin de test", $time);
  $finish;
end
 
/* (6) Enregistrement de l'image */
integer fimg;
integer linecount=0;
integer columncount=0;
 
initial
begin
  /* (7) attente de la sortie du reset */
  @(negedge rst_i) #1;
  $display("Ouverture de l'image");
  fimg = $fopen("/tmp/icarus_img.ppm", "w");
 
  /* enregistrement de l'entête de l'image */
  $fwrite(fimg, "P3\n");
  $fwrite(fimg, "# icarus_img.ppm\n");
  $fwrite(fimg, "688 494\n");
  $fwrite(fimg, "1\n");
 
  /* (8) attente de la montée de vsync_o */
  while(vsync_o != 1'b1) #1;
 
  /* (9) écriture d'une image */
  while(vsync_o == 1'b1)
  begin
    @(posedge clk_i)
    begin
      if(hsync_o)
        begin
          if(linecount != 0)
            /* (10) */
            $fwrite(fimg, "%d %d %d ", red_o, green_o, blue_o);
          columncount = columncount + 1;
        end
      else if(columncount != 0)
        begin
          $fwrite(fimg, "\n");
          linecount = linecount + 1;
          $write("line %0d column %0d\15", linecount, columncount);
          $fflush();
          columncount = 0;
        end
      end
    end
    $display("");
    $display("Fermeture de l'image");
    $fclose(fimg);
  end
 
endmodule /* RgbVideo_tb */

La première ligne du fichier source (0) indique le « pas » de simulation. Ce paramètre donne le temps d’un pas simulé. Le second est une subdivision de ce pas. Ici, le temps de cycle nous importe peu, nous mettrons donc 1 nanoseconde comme nous aurions pu mettre 1 milliseconde, cela ne changera pas le temps de simulation.

On reconnaît un module banc de test (1) à son entête sans port d’entrée sortie. Avant d’instancier le module à tester, nous devons déclarer les signaux (2) dont nous aurons besoin pour connecter ledit module (3). Ici, nous déclarerons les entrées horloge et reset en registres (reg), car c’est le testbench qui va les piloter. Puis, nous déclarerons les sorties du module en fils (wire), car nous nous contenterons de les lire.

En plus de l’instanciation du module top en test, trois tâches indépendantes sont nécessaires pour ce banc.

La première tâche (4) sert à commuter l’horloge. À chaque cycle de simulation #1, le signal clk_i est inversé de manière à générer un cycle complet d’horloge.

La tâche principale (5) décrit le déroulement complet du test en commençant par mettre le module dans son état de reset avec le signal rst_i à « 1 ». On se synchronise ensuite sur le front montant du signal vsync_o pour attendre un cycle complet à « 1 » de manière à capturer une image entière. La tache se termine par l’instruction $finish qui indique au simulateur que la simulation est terminée. Sans cette instruction, et sans avoir indiqué de temps maximum, le simulateur continuerait indéfiniment la simulation.

La tâche principale comporte également les fonctions $dumpfile() et $dumpvars() encadrées par une macro ifdef DUMPVARS. Ce code génère une trace des différents signaux simulés dans un fichier au format VCD. Ce fichier de dump est très pratique pour déverminer le projet. On pourra visualiser les chronogrammes avec le programme GTKWave disponible dans toute bonne distribution GNU/Linux :

$ gtkwave RgbVideo_tb.vcd

Mais cette partie enregistrement monopolise (un peu) de temps de calcul. Pour comparer le temps d’exécution du simulateur avec Verilator, on prendra donc bien soin de la désactiver en commentant la déclaration de la macro DUMPVARS :

//`define DUMPVARS

Pour finir (6), la dernière tâche va lire les signaux vidéo en sortie du module RgbVideo pour les enregistrer dans un fichier image au format PPM.

Le format PPM

Le format PPM [4] pour Portable PixMap est un format de fichier image enregistré en ASCII (dans notre cas) très pratique pour être lu/écrit avec un simple éditeur de texte. La première ligne donne le format utilisé avec le nombre magique P3.

Ce nombre magique signifie ici que l’on a affaire à une image en couleurs (rouge, vert, bleu), codé en ASCII. Une ligne de commentaire commence par un # et la résolution est donnée par deux nombres donnant respectivement la largeur et la hauteur :

688 494

Enfin, la dernière ligne de configuration donne la valeur maximale des couleurs. Comme nous l’avons vu en début d’article, notre module RGB ne génère que deux valeurs de couleurs pour chaque signal, soit « 0 » et « 1 ». La valeur maximum sera donc 1.

Le reste du fichier sera ensuite constitué de groupe de trois valeurs entières séparées par des blancs (espace, tabulation ou retour à la ligne).

L’avantage de ce format est qu’il est facile à manipuler, puisque c’est un simple texte. Le second avantage est que c’est un standard lisible par la plupart des logiciels de visualisation et de traitement d’images. Il sera donc facile de visualiser l’image simulée.

On se synchronise (7) d’abord sur la sortie du reset (front descendant de rst_i) pour ensuite ouvrir le fichier image et y écrire l’entête. Le fichier image est écrit dans le répertoire /tmp/ du système, car ce répertoire est généralement monté dans la mémoire vive de l’ordinateur. On optimise ainsi le temps d’écriture dans le fichier qui se fera dans la RAM.

En (8), la tâche reste en attente tant que le signal de synchronisation verticale est à « 0 ». Puis nous entrons dans la boucle d’écriture de l’image (9) au front montant de vsync_o.

Tant que le signal de synchronisation vertical est actif (à « 1 »), à chaque front de l’horloge, nous écrivons la valeur du pixel dans le fichier (10). À condition que le signal de synchronisation horizontal soit actif également.

Notez que ce banc de test, écrit en pur Verilog, peut être exécuté avec n’importe quel simulateur acceptant le standard. Il n’est pas obligatoire de le faire avec Icarus. On peut tout à fait le faire tourner en l’état avec un des trois gros simulateurs commerciaux du marché pour comparer [5].

Le lecteur ou la lectrice aura remarqué que la programmation d’un banc de test en Verilog « pur » se fait en multitâche. La programmation multitâche n’est pas toujours très facile à déverminer, quand c’est possible on préfère généralement éviter.

L’exécution du banc de test avec Icarus se passe en deux phases. Une première phase de compilation/élaboration avec l’utilitaire iverilog du projet Icarus va générer un fichier exécutable avec l’extension vvp.

$ iverilog -o RgbVideo_tb.vvp RgbVideo_tb.v RgbVideo.v HVSync.v

L’exécutable RgbVideo_tb.vvp généré, le code du banc de test est maintenant prêt à être exécuté. Le binaire généré peut être lancé directement comme n’importe quel binaire GNU/Linux :

$ ./RgbVideo_tb.vvp
0, Début de test
Ouverture de l'image
line 498 column 688
Fermeture de l'image
780954000, vsync_o 0
780954000, Fin de test

Il peut également être lancé avec l’utilitaire vvp d’Icarus. Cet utilitaire ajoute quelques options d’affichage ainsi que la possibilité de lancer la simulation en mode interactif.

L’image générée par la simulation est visible en figure 3.

Verilator figure 03-s

Fig. 3 : L'image générée par la simulation. Les parties non affichables sont représentées en noir.

Les performances peuvent être mesurées avec l’utilitaire time inclus dans toute bonne distribution GNU/Linux. Pour les besoins de cet article, les mesures ont été faites sur un petit ordinateur « de voyage » de type netbook (Asus E203M avec un Celeron N4000 cadencé à 1,1 GHz et 4 Go de RAM).

Le temps d’élaboration/compilation est de 51 ms, ce qui est très faible.

$ time iverilog -o RgbVideo_tb.vvp ../src/RgbVideo_tb.v ../src/RgbVideo.v
../src/HVSync.v
 
real    0m0.051s
user    0m0.014s
sys     0m0.011s

Le temps de simulation quant à lui est nettement plus conséquent, puisqu’il faut compter 4,57 secondes pour effectuer la simulation complète de l’image.

$ time ./RgbVideo_tb.vvp
0, Début de test
Ouverture de l'image
line 498 column 688
Fermeture de l'image
780954000, vsync_o 0
780954000, Fin de test
 
real    0m4.568s
user    0m4.544s
sys 0m0.024s

On parle ici de la simulation de l’affichage d’une image VGA. Le taux de rafraîchissement d’un écran VGA standard est de 60 Hz, si nous voulions simuler une seconde d’affichage, il nous faudrait patienter presque 5 minutes. Même si on réduit le taux de rafraîchissement, il est inenvisageable de simuler une image animée en temps réel. Tout au plus, nous pouvons lancer la simulation (la nuit) et enregistrer la suite d’images pour la rejouer une fois calculée.

Voyons maintenant les performances de Verilator.

3. Verilator

L’histoire de Verilator est donnée dans le manuel utilisateur [verilatorman]. Le logiciel est né en 1994 au sein de l’entreprise DEC (Digital Equipement Corporation). Le code a été libéré en 1998 à l’occasion du rachat de cette partie de l’entreprise par Intel. Wilson Snyder a repris le support en 2001 et a rejoint la Chip Alliance en 2019. La Chip alliance est une organisation intégrée à la fondation Linux qui développe et maintient des logiciels open source pour le développement matériel.

Verilator est donc un outil qui existe depuis très longtemps. Il est naturellement intégré comme « package » dans la plupart des distributions GNU/Linux modernes. Mais Verilator est activement maintenu, et les dernières versions intègrent le calcul parallèle sur les processeurs modernes multicœurs pour des performances décuplées. La compilation et l’installation d’une vue à jour de Verilator étant relativement simples et bien expliquées sur le site officiel [7], on préférera cette méthode plutôt que d’installer le paquet de sa distribution.

Pour les irréductibles de Microsoft, il est tout à fait possible d’utiliser Cygwin ou MinGW pour le compiler et le faire tourner sur le système d’exploitation à fenêtres. La méthode d’installation est également donnée sur le site officiel.

3.1 « Verilation »

La première étape de la simulation avec Verilator est appelée la « verilation » (Verilating). Cette étape consiste à transformer le code Verilog en code C++.

La commande pour « veriler » le module présenté en début d’article est la suivante :

$ verilator -Wall -cc ../src/rgbvideo.v ../src/hvsync.v
%Warning-UNUSED: ../src/rgbvideo.v:17:19: Bits of signal are not used: 'hcount'[9:4,2:0]
                                        : ... In instance rgbvideo
   17 | wire [(10 - 1):0] hcount;
      |                   ^~~~~~
                 ... Use "/* verilator lint_off UNUSED */" and lint_on around source to disable this message.
%Warning-UNUSED: ../src/rgbvideo.v:18:18: Signal is not used: 'vcount'
                                        : ... In instance rgbvideo
   18 | wire [(9 - 1):0] vcount;
      |                  ^~~~~~
%Error: Exiting due to 2 warning(s)

Le linter est très tatillon, ici nous avons deux avertissements (warning), car nous n’utilisons pas tous les bits des compteurs horizontaux et verticaux pour générer l’image.

Le plus simple pour éviter cet avertissement est de l’ignorer spécifiquement pour les deux lignes concernées, puis de le réactiver juste après en modifiant le fichier source rgbvideo.v :

/* verilator lint_off UNUSED */
wire [(`H_REG_SIZE - 1):0] hcount;
wire [(`V_REG_SIZE - 1):0] vcount;
/* verilator lint_on UNUSED */

Le message d’erreur est suffisamment bien fait pour que nous connaissions la syntaxe, sans même avoir à nous référer au manuel.

La « verilation » a pour effet de générer une série de fichiers sources C++ dans un répertoire nommé obj_dir/ :

$ ls -l obj_dir/
total 40
-rw-r--r-- 1 flf flf 1522 Jan 9 11:28 Vrgbvideo_classes.mk
-rw-r--r-- 1 flf flf 6978 Jan 9 11:28 Vrgbvideo.cpp
-rw-r--r-- 1 flf flf 3271 Jan 9 11:28 Vrgbvideo.h
-rw-r--r-- 1 flf flf 1448 Jan 9 11:28 Vrgbvideo.mk
-rw-r--r-- 1 flf flf 3580 Jan 9 11:28 Vrgbvideo__Slow.cpp
-rw-r--r-- 1 flf flf 554 Jan 9 11:28 Vrgbvideo__Syms.cpp
-rw-r--r-- 1 flf flf 802 Jan 9 11:28 Vrgbvideo__Syms.h
-rw-r--r-- 1 flf flf 295 Jan 9 11:28 Vrgbvideo__ver.d
-rw-r--r-- 1 flf flf 1285 Jan 9 11:28 Vrgbvideo__verFiles.dat

Le nom du module Top est préfixé avec un V pour Verilated.

Les deux principaux fichiers qui nous intéressent ici sont Vrgbvideo.cpp et Vrgbvideo.h qui décrivent le module Verilog converti en C++.

Si l’on ouvre l’entête Vrgbvideo.h, on retrouve les interfaces du module Verilog :

//...
VL_MODULE(Vrgbvideo) {
  public:
 
    // PORTS
    // The application code writes and reads these signals to
    // propagate new values into/out from the Verilated model.
    VL_IN8(clock,0,0);
    VL_IN8(rst,0,0);
    VL_OUT8(hsync,0,0);
    VL_OUT8(vsync,0,0);
    VL_OUT8(disp_en,0,0);
    VL_OUT8(red,7,0);
    VL_OUT8(green,7,0);
    VL_OUT8(blue,7,0);
//...

La notation VL_IN8 est une macro déclarée dans le fichier d’entête verilated.h qui définit des variables entières non signées de largeur 8 bits maximum. Les variables membres publiques de la classe Vrgbvideo données ci-dessus représentent les entrées (VL_INx) et sorties (VL_OUTx) du module Verilog. Comme nous allons le voir par la suite, elles pourront être lues et écrites au moyen d’assignation C++ (=).

La classe possède également des méthodes publiques pour créer l’objet et évaluer les sorties en fonction des entrées :

  public:
    /// Construct the model; called by application code
    /// The special name may be used to make a wrapper with a
    /// single model invisible with respect to DPI scope names.
    Vrgbvideo(const char* name = "TOP");
    /// Destroy the model; called (often implicitly) by application code
    ~Vrgbvideo();
 
    // API METHODS
    /// Evaluate the model. Application must call when inputs change.
    void eval() { eval_step(); }
    /// Evaluate when calling multiple units/models per time step.
    void eval_step();
    /// Evaluate at end of a timestep for tracing, when using eval_step().
    /// Application must call after all eval() and before time changes.
    void eval_end_step() {}
    /// Simulation complete, run final blocks. Application must call on completion.
    void final();

La méthode importante à retenir ici est la fonction d’évaluation eval() qui lorsqu’elle est appelée calcule les valeurs des sorties en fonction des entrées données.

Tout ce code se présente comme une classe « indépendante » que nous allons devoir instancier dans une fonction main avec notre code de test.

Le manuel de référence propose un exemple minimum de code C++ pour instancier le modèle [8]. Nous allons l’adapter à notre exemple rgbvideo dans un fichier source appelé sim_main.cpp :

#include <assert.h>
#include <iostream>     // Pour std::cout
#include <fstream>      // pour enregistrer le fichier image
#include <verilated.h> // (1) Routines Verilator
#include <verilated_vcd_c.h> // (15) dump VCD
#include "VRgbVideo.h" // (2) Description des interfaces
 
using namespace std;
 
// chemin du fichier image enregistré (/tmp -> dans la RAM)
#define FIMAGENAME "/tmp/verilator_img.ppm"
#define VGA640X480
//(19)
//#define VCDDUMP
 
/* (3) */
class SimMain {
    public:
        VRgbVideo *top;           // (4) Le modèle
        vluint64_t main_time = 0; // (5) Temps de simulation
        int linecount = 0;
        int columncount = 0;
 
        void step(int n=1); // (6) pas
 
        SimMain(VRgbVideo *top, string fimagename);
        ~SimMain();
    private:
#ifdef VCDDUMP
    VerilatedVcdC* tfp; // (16) fichier de dump vcd
#endif
 
        ofstream *fimage;
        void low_cycle(void);
        void high_cycle(void);
        void half_cycle(int clk_value);
        void read_pixel(void);
        void write_img_header(void);
};
 
SimMain::SimMain(VRgbVideo *top, string fimagename)
    : top(top){
 
    // ouverture du fichier image
    // et écriture de l'entête ppm
    fimage = new ofstream(fimagename);
    write_img_header();
 
#ifdef VCDDUMP
    /* (17) ouverture du fichier de dump */
    Verilated::traceEverOn(true);
    tfp = new VerilatedVcdC;
    top->trace(tfp, 99);
    tfp->open("rgbvideo.vcd");
#endif
};
 
SimMain::~SimMain(){
    if(fimage != NULL)
        fimage->close();
 
#ifdef VCDDUMP
    if(tfp != NULL)
        tfp->close();
#endif
}
 
//H_DISPLAY + H_FRONT + H_BACK
#define HMAX 688
//V_DISPLAY + V_TOP + V_BOTTOM
#define VMAX 494
 
void
SimMain::write_img_header(void) {
    *fimage << "P3" << endl;
    *fimage << "# verilator_img.ppm" << endl;
    *fimage << HMAX << " " << VMAX << endl;
    *fimage << "1" << endl;
}
 
/* (9) Enregistrement des pixels de l'image */
void
SimMain::read_pixel(void) {
    if(top->vsync_o == 1 && top->hsync_o == 1) {
    if(linecount != 0) {
        *fimage << " " << (int)top->red_o;
        *fimage << " " << (int)top->green_o;
        *fimage << " " << (int)top->blue_o;
    }
        columncount++;
    } else if(columncount != 0) {
        linecount++;
        columncount = 0;
        *fimage << endl;
    }
}
 
void
SimMain::low_cycle(void){
    half_cycle(0);
}
 
void
SimMain::high_cycle(void){
    half_cycle(1);
}
 
void
SimMain::half_cycle(int clk_value){
    assert(clk_value == 1 || clk_value == 0);
    top->clk_i = clk_value; // (8)
    top->eval(); // fonction d'évaluation
}
 
/* (7) */
void
SimMain::step(int n) {
    int i;
    for(i=0; i < n; i++){
        low_cycle();
        read_pixel();
        high_cycle();
#ifdef VCDDUMP
    tfp->dump(main_time); // (18) dump vcd des signaux
#endif
        main_time++; // Incrémentation du temps
    }
}
 
/* (10) fonction principale */
int main(int argc, char** argv) {
    VRgbVideo top; // Instanciation du module testé
    SimMain sm(&top, FIMAGENAME); // création de l'objet testbench
 
    /* (11) */
    top.rst_i = 1; // écriture de 1 dans le signal reset
    sm.step(5);    // 5 cycles d'horloge
    top.rst_i = 0; // on sort du reset
 
    // (12) attente de la montée de vsync_o
    while(top.vsync_o != 1)
        sm.step();
 
    // (13)
    while(top.vsync_o == 1){
        sm.step();
    }
    // (14)
    cout << "Step " << sm.main_time << " : end of test" << endl;
    cout << "linecount " << sm.linecount << endl;
    top.final(); // Fin de la simulation
}

En plus des entêtes propres à la programmation C++, nous avons besoin d’inclure les routines Verilator avec le fichier verilated.h (1) ainsi que la définition de notre module « verilé » (2).

La classe SimMain est une classe « maison » qui va encapsuler tous les méthodes et attributs propres à la simulation.

On stocke le module VRgbVideo dans le pointeur *top (4) et le temps simulé est compté avec la variable main_time sur 64 bits (5).

La méthode step() (6) va servir quant à elle à faire avancer la simulation du nombre de cycles voulu. C’est cette dernière qui va nous intéresser plus particulièrement (7). Dans cette fonction, qui est le cœur de la simulation, on va principalement s’occuper de l’horloge en deux parties.

Avec la fonction low_cycle(), on passe d’abord la valeur de l’horloge à « 0 » (8), puis on évalue le modèle avec l’appel à la fonction d’évaluation top->eval(). On lit ensuite les valeurs des pixels avec la fonction read_pixel() puis on passe la valeur d’entrée de l’horloge à « 1 » avec la fonction high_cycle().

La fonction read_pixel() est en fait la partie où nous allons enregistrer les pixels de l’image au format PPM (9) de la même manière que nous l’avons fait en Verilog initialement.

Dump VCD

Il est tout à fait possible de générer des traces au format VCD avec Verilator. Traces qui seront visualisable avec GTKWave. Verilator inclut tout ce qu’il faut pour enregistrer un fichier de traces au format désiré à condition d’ajouter l’option --trace à la commande de « verilation ».

$ time verilator --trace -Wall -cc ../src/RgbVideo.v ../src/HVSync.v --exe --build -j sim_main.cpp

On intègre ensuite le fichier d’entête verilated_vcd_c.h (15). Puis on ajoute un objet VerilatedVcdC (16) en attribut à notre classe de test.

On initialise l’objet (17) et on le passe en paramètre de la méthode trace(). Le second paramètre est un entier donnant la profondeur hiérarchique du « dump ».

De la même manière que pour la fonction d’évaluation qui fait avancer la simulation, c’est à nous de « dumper » les valeurs des signaux en appelant la fonction dump() à chaque cycle de simulation (18). L’entier passé en paramètre correspond au numéro de cycle (le temps sans unité) qui sera inscrit dans le VCD.

Le « dump » des signaux dans un fichier VCD est très gourmand en ressources processeur ainsi qu’en mémoire. Les fichiers deviennent très vite volumineux. C’est pourquoi nous prendrons bien soin de garder une option de compilation (19) sans le dump VCD si l’on souhaite améliorer les performances de la simulation.

Le fichier ainsi généré se lit ensuite avec le fameux visualiseur libre GTKWave :

$ gtkwave rgbvideo.vcd

Pour finir, l’écriture du banc de test à proprement parler se trouve dans la fonction principale main() (10).

On instancie tout d’abord le module à tester dans une variable que nous nommerons top. On passe ensuite l’adresse de cette variable au constructeur de notre objet de la classe SimMain() que nous venons de définir.

Et nous commençons par nous assurer que le module commence bien par un état de reset en fixant la valeur de top.rst_i à « 1 » pendant 5 cycles d’horloge.

La suite est similaire au process principal du banc de test en Verilog :

  • (12) on boucle en attendant que le signal vsync_o passe à « 1 » ;
  • (13) on boucle à nouveau sur l’état « 1 » de vsync_o le temps d’enregistrer une image entière ;
  • (14) enfin, on termine le test en affichant le nombre de cycles simulés et le nombre de lignes de l’image enregistrées.

On notera qu’ici tout le code est linéaire et non multitâche, comme on l’a vu dans le banc de test Verilog. On aurait pu se servir des librairies standard du C++ pour écrire un banc de test multitâche. L’intérêt dans cet exemple est relativement minime, d’autant que pour faire avancer le pas de simulation, on doit nécessairement en passer par la fonction d’évaluation. De plus, la règle dans la programmation multitâche, c’est que quand on peut s’en passer, il vaut mieux éviter pour limiter les bugs pénibles.

Verilator permet de « veriler », puis de compiler les sources générées avec les sources du testbench fournies en une seule commande. On y ajoutera la commande time pour mesurer le temps total d’élaboration/compilation.

$ time verilator -Wall -cc ../src/RgbVideo.v ../src/HVSync.v --exe --build -j sim_main.cpp
make: Entering directory '/home/user/art_verilator/hdl/verilator/obj_dir'
[...]
Archive ar -rcs VRgbVideo__ALL.a VRgbVideo__ALL.o
g++    sim_main.o verilated.o VRgbVideo__ALL.a      -o VRgbVideo
rm VRgbVideo__ALL.verilator_deplist.tmp
make: Leaving directory '/home/user/art_verilator/hdl/verilator/obj_dir'
 
real    0m5.321s
user    0m7.127s
sys     0m0.557s

Le temps de compilation de plus de 5 secondes n’est vraiment pas négligeable, mais assez classique pour du C++. Ce temps est une centaine de fois plus long que le temps d’élaboration d’Icarus. C’est pourquoi on réservera toujours la simulation avec Verilator pour les gros projets, pour les tests unitaires, on préférera toujours utiliser Icarus.

Le binaire généré se trouve dans le répertoire obj_dir/ et se lance directement. De la même manière que la compilation, nous le lancerons avec la commande time pour mesurer le temps de simulation :

$ time ./obj_dir/VRgbVideo
[...]
line 498 column 688
Step 390479 : end of test
linecount 498
 
real    0m0.154s
user    0m0.133s
sys     0m0.021s

Le temps de simulation de 154 ms est tout de même une trentaine de fois plus rapide qu’avec Icarus. Sans aucune optimisation de Verilator et en tournant sur un seul thread.

Le manuel donne des options permettant d’optimiser la compilation. Avec ces options, on peut encore gagner une petite dizaine de millisecondes. La programmation multitâche pourrait quant à elle diminuer furieusement le temps d’exécution de la simulation sur des projets complexes, mais elle nécessiterait un article supplémentaire pour la détailler.

4. Pour conclure

Le tableau suivant résume les performances d’Icarus et de Verilator sur un petit ordinateur muni d’un Celeron N4000 cadencé à 1,1 GHz et 4 Go de RAM :

Étape logicielle

Icarus

Verilator

Rapport Icarus/Verilator

Compilation/élaboration

0,051 s

5,321 s

Verilator/Icarus ~ 100

Simulation

4,568 s

0,154 s

Icarus/Verilator ~ 30

Bien sûr, d’un premier coup d’œil en regardant ces chiffres, on se demande l’intérêt de Verilator. Il met tellement de temps à compiler qu’on a plus vite fait de simuler notre Verilog avec Icarus. En plus, le Verilog est portable et on pourra utiliser d’autres simulateurs, si besoin !

Mais la simulation que nous venons d’effectuer est relativement petite. Le taux de rafraîchissement normal du VGA est de 60 Hz, si nous voulions simuler une seconde d’affichage avec Icarus il nous faudrait un peu moins de 5 minutes de simulation, alors qu’avec Verilator, en moins de 10 secondes, c’est plié. Avec Verilator, il est même possible d’exécuter la simulation en temps réel.

En effet, si l’on abaisse le rafraîchissement à 5/6 images secondes, on peut « faire tourner » le modèle en simulation sur son ordinateur et voir les animations directement. Ce qui est inenvisageable avec Icarus, on ne va pas attendre plus de 4 secondes de rafraîchissement entre chaque image.

L’exemple de cet article a été simulé avec un tout petit ordinateur et très peu d’optimisations. Avec un code Verilog plus complexe et un banc de test qui tirerait parti de la puissance de calcul en multicœur, il est possible de décupler la puissance de Verilator ! On parle de temps de simulation jusqu’à 500 fois plus rapide par rapport à Icarus.

Il est également possible de simuler des processeurs écrits en Verilog et de s’y connecter avec un terminal pour le piloter directement. À en croire le site officiel de Verilator, c’est d’ailleurs le simulateur qui est utilisé par tous les grands noms du microprocesseur, de ARM à Intel, en passant par SiFive ou Microchip.

Enfin, Verilator est très utile pour développer des modules de traitement du signal dans un FPGA. Même s’il n’est pas traité en « temps réel » en simulation, il est tout de même envisageable de faire tourner le code avec des échantillons réels de signaux et d’analyser le résultat assez rapidement. Suffisamment en tout cas pour pouvoir enchaîner les cycles de modification-simulation-analyse-déverminage, sans pour autant y passer des mois en laissant les simulations tourner chaque nuit.

Ces performances fulgurantes pourraient donner envie de jeter son simulateur habituel et de tout faire avec Verilator.

Cela serait une erreur. En effet, Verilator vient tout de même avec son lot d’inconvénients. Et le premier d’entre eux : il n’est pas capable de gérer le temps, la directive timescale est complètement ignorée par Verilator, et il ne sait que faire des délais #n donnés dans le banc de test, mais également dans certains modèles de macro interne aux FPGA, par exemple.

Comme on l’a vu, le temps de compilation est particulièrement important. Ce temps peut certes être réduit sur des ordinateurs modernes en jouant sur l’option -jx pour générer des Makefiles qui compilent les fichiers sources en parallèle. Comme on a un minimum de deux fichiers sources (le module « verilé » et le testbench « main »), on y gagne toujours un peu. Mais cela reste long, surtout si l’on passe son temps à recompiler le modèle pour effectuer des tests unitaires ciblés en fonction de paramètres de configuration du module.

Verilator génère des modèles C++ « secs », toute la partie pilotage des signaux et moniteurs est à réécrire à chaque nouveau projet. Il n’existe pas de librairie standard permettant de simuler un bus SPI ou I²C, par exemple, pas plus que des pilotes de bus Wishbone ou AXI-Lite. Le choix de la développeuse ou du développeur (multi ou mono thread, découpage des cycles d’horloge…) de l’appel à la fonction d’évaluation conditionnera toute son « API » utilisée dans les librairies. Bref, à chaque nouveau projet, on en est souvent réduit à réécrire les « librairies ».

La possibilité de générer un modèle en SystemC corrige un peu ce problème, mais il n’existe pas non plus de librairie de test en SystemC.

De plus, il n’est pas évident que les équipes de développement en Verilog aient une maîtrise du langage C++. Les équipes de développement qui font du Verilog sont généralement issues du monde de l’électronique, alors que le C++ est une compétence de développement logicielle. Les deux mondes sont très différents et ne se comprennent pas toujours.

Enfin, le dernier problème de Verilator est qu’il permet de simuler… du Verilog. Or, ça n’est pas le seul langage utilisé pour la conception numérique sur FPGA, il y a également VHDL. Cet inconvénient est cependant en phase de résolution. Il est en effet possible aujourd’hui de convertir du VHDL en Verilog avec le simulateur GHDL [9] soit en utilisant directement l’option de synthèse interne au programme, soit en passant par l’extension yosys-ghdl-plugin [10] de Yosys. La conversion est aujourd’hui assez mature pour récupérer un modèle Verilog qui garde les noms des signaux et que l’on peut ensuite simuler avec Verilator.

Références

[1] Verilator, https://www.veripool.org/verilator/

[2] Une carte pilote de LED RGB hackée en kit de développement FPGA à bas coût, Fabien Marteau, Hackable 35 - https://connect.ed-diamond.com/Hackable/hk-035/une-carte-pilote-de-led-rgb-hackee-en-kit-de-developpement-fpga-a-bas-cout

[3] Les sources de l’article, https://github.com/Martoni/Diamond_HK_GLMF_OS

[4] Format de fichier texte PPM, http://netpbm.sourceforge.net/doc/ppm.html

[5] Modelsim (Siemens EDA ex Mentor Graphics), Incisive (Cadence) et VCS (Synopsys).

[6] Icarus Verilog, http://iverilog.icarus.com/

[verilatorman] Manuel de Verilator, https://verilator.org/guide/latest/

[7] Installation de Verilator, https://verilator.org/guide/latest/install.html

[8] Modèle de fonction main, https://verilator.org/guide/latest/connecting.html#connecting-to-c

[9] GHDL, le simulateur VHDL libre, https://github.com/ghdl/ghdl

[10] Extension de Yosys permettant de « connecter » GHDL, https://github.com/ghdl/ghdl-yosys-plugin



Article rédigé par

Par le(s) même(s) auteur(s)

Transformez votre vieille Game Boy en console de salon HDMI

Magazine
Marque
Hackable
Numéro
44
Mois de parution
septembre 2022
Spécialité(s)
Résumé

On se propose ici d’utiliser un FPGA GW1NSR de la société chinoise Gowin pour transformer sa Game Boy en véritable console de salon, avec le branchement HDMI ainsi que la manette de Super NES. Le (relatif) plug & play du montage transforme ainsi la Game Boy en une Game Boy-Switch rétro à la sauce Formicapunk. On peut y jouer en mode portable comme à l’époque et si on l’insère dans le montage, il est possible d’y jouer sur sa télé HDMI avec une manette de Super NES.

De la preuve formelle en VHDL, librement

Magazine
Marque
Hackable
Numéro
42
Mois de parution
mai 2022
Spécialité(s)
Résumé

Dans cet article, on se propose d’aborder la méthode de vérification formelle pour le VHDL. Cette méthode a récemment été rendue possible avec GHDL et Yosys grâce au projet d’extension ghdl-yosys-plugin qui fait le lien entre les deux logiciels. Nous allons également découvrir le langage PSL (Properties Specification Langage) qui permet de décrire efficacement les propriétés utilisées en preuve formelle. Le support du PSL ayant été ajouté dans GHDL, il sera possible de l’utiliser librement en VHDL.

Le premier FPGA avec sa chaîne de développement open source

Magazine
Marque
Hackable
Numéro
40
Mois de parution
janvier 2022
Spécialité(s)
Résumé

Il existe ! Le premier FPGA avec une chaîne intégralement open source est désormais une réalité. Parler de FPGA est peut-être un peu prétentieux concernant le EOS S3. Disons plutôt que c’est un microcontrôleur à cœur ARM Cortex-M4 qui possède une zone configurable (appelée souvent eFPGA). Mais ce qui est intéressant ici, c’est que les outils de synthèse ainsi que le format du fichier de configuration sont documentés. Ils sont donc utilisables avec des logiciels libres !

Les derniers articles Premiums

Les derniers articles Premium

Bénéficiez de statistiques de fréquentations web légères et respectueuses avec Plausible Analytics

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Pour être visible sur le Web, un site est indispensable, cela va de soi. Mais il est impossible d’en évaluer le succès, ni celui de ses améliorations, sans établir de statistiques de fréquentation : combien de visiteurs ? Combien de pages consultées ? Quel temps passé ? Comment savoir si le nouveau design plaît réellement ? Autant de questions auxquelles Plausible se propose de répondre.

Quarkus : applications Java pour conteneurs

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Initié par Red Hat, il y a quelques années le projet Quarkus a pris son envol et en est désormais à sa troisième version majeure. Il propose un cadre d’exécution pour une application de Java radicalement différente, où son exécution ultra optimisée en fait un parfait candidat pour le déploiement sur des conteneurs tels que ceux de Docker ou Podman. Quarkus va même encore plus loin, en permettant de transformer l’application Java en un exécutable natif ! Voici une rapide introduction, par la pratique, à cet incroyable framework, qui nous offrira l’opportunité d’illustrer également sa facilité de prise en main.

Les listes de lecture

7 article(s) - ajoutée le 01/07/2020
La SDR permet désormais de toucher du doigt un domaine qui était jusqu'alors inaccessible : la réception et l'interprétation de signaux venus de l'espace. Découvrez ici différentes techniques utilisables, de la plus simple à la plus avancée...
8 article(s) - ajoutée le 01/07/2020
Au-delà de l'aspect nostalgique, le rétrocomputing est l'opportunité unique de renouer avec les concepts de base dans leur plus simple expression. Vous trouverez ici quelques-unes des technologies qui ont fait de l'informatique ce qu'elle est aujourd'hui.
9 article(s) - ajoutée le 01/07/2020
S'initier à la SDR est une activité financièrement très accessible, mais devant l'offre matérielle il est parfois difficile de faire ses premiers pas. Découvrez ici les options à votre disposition et les bases pour aborder cette thématique sereinement.
Voir les 31 listes de lecture

Abonnez-vous maintenant

et profitez de tous les contenus en illimité

Je découvre les offres

Déjà abonné ? Connectez-vous