Le jour du 27 mai 2015 sera à marquer d’une pierre blanche, en effet, c’est le jour où un FPGA a été libéré du joug des logiciels privateurs.
C’est le jour où Wolf Clifford a sorti une première version fonctionnelle du projet IceStorm permettant de synthétiser un design écrit en Verilog via Yosys et de faire le placement routage grâce à Arachne-pnr.
La conversion en bitstream et la programmation du chip se fait grâce aux utilitaires icepack et iceprog du projet IceStorm.
Pour l’instant le composant ciblé est le Lattice iCE40 HX1K-TQ144 pouvant être trouvé sur le kit d’évaluation lowcost (~$20) iCEstick.
Voici ci-dessous un petit howto rapide permettant de faire les manipulations se trouvant dans la vidéo de Clifford.
Icestorm howto
Installation de Yosys
$ cd /opt
$ git clone https://github.com/cliffordwolf/yosys.git
$ yosys_deps="build-essential clang bison flex libreadline-dev gawk
tcl-dev libffi-dev git mercurial graphviz xdot pkg-config python"
$ sudo apt-get install $yosys_deps
$ make config-gcc
$ make
$ make test
$ sudo make install
Installation d’IceStorm
$ sudo apt-get install libftdi-dev
$ cd /opt/
$ mkdir icestorm
$ wget http://www.clifford.at/icestorm/icestorm-snapshot-150526.zip
$ unzip icestorm-snapshot-150526.zip
$ make
$ sudo make install
Et enfin arachne-pnr
$ cd /opt/
$ git clone https://github.com/cseed/arachne-pnr.git
$ make && sudo make install
Synthèse, placement-routage, bitstream et programmation
Un exemple de «programme» est donné avec arachne-pnr dans le répertoire
example/rot. Cet exemple est composé du source en vérilog rot.v ainsi que du
placement des pin rot.pcf.
Synthèse (Génération du blif)
$ cd /opt/arachne-pnr/example/rot/
$ yosys -p "synth_ice40 -blif rot.blif" rot.v
Le fichier généré rot.txt est la description finale du placement et du routage
du design dans le fpga. Cette description est parfaitement lisible en ascii
avec n’importe quel éditeur de texte.
Pour le télécharger dans le fpga nous devons le convertir en format binaire au
moyen de la commande «icepack» :
$ icepack rot.txt rot.bin
Nous avons un bitstream parfaitement compatible avec le ice40, il
nous faut maintenant le télécharger dans le fpga.
Et même pour cela, un logiciel libre est fourni : iceprog !
$ sudo iceprog rot.bin
On peut faire le tout en une seule ligne aussi si on veut :
Plusieurs solutions de simulations s’offrent à nous quand on développe un composant en Verilog. On peut écrire le testbench en Verilog, de manière à être compatible avec la plupart des simulateurs du marché. Dans ce cas, le simulateur libre le plus célèbre est Icarus.
Mais la solution du «tout Verilog» est relativement lente en temps de simulation, et l’utilisation du langage Verilog est restrictive, en effet il n’est pas facile de s’approprier toutes les subtilités du langage.
C’est là que la solution de Verilator devient très intéressante. Verilator permet de convertir un modèle de composant écrit en Verilog synthétisable en un modèle C++. De cette manière, écrire le code du testbench revient à instancier notre composant dans un main() en C++ et à décrire nos tests avec toute la liberté qu’offre ce langage.
Mieux encore, il est possible d’écrire notre testbench en SystemC, et de profiter ainsi de cette librairie conçue pour la simulation de circuit numérique.
L’idée va être ici de mesurer le temps effectif de simulation de chacune de ses deux solutions. On se servira pour cela du composant d’antirebond du projet «blinking led project» (blp) disponible sur github.
Le composant synthétisable
Le composant button_deb commute sa sortie button_valid à chaque front montant du signal d’entrée button_in. Pour que le fonctionnement soit un peu plus complexe qu’une simple commutation, et surtout pour bien coller au fonctionnement réel, le composant est muni d’un antirebond de 20 ms (par défaut). Quand le premier front survient, un compteur est déclenché et aucun autre front n’est pris en compte pendant 20 ms.
Tout comme en VHDL, un testbench en Verilog se présente sous la forme d’un module sans entrée/sortie.
La première chose à définir est le temps, avec la directive `timescale:
`timescale 1ns/100ps;
Le premier chiffre indique le pas de simulation, cela correspondra au temps d’attente que l’on retrouvera tout au long du code avec l’«instruction» ‘#’. Le deuxième nombre indique la précision maximum.
Par exemple, pour simuler l’horloge à 95Mhz on écrira :
/* Make a regular pulsing clock. */
always
#5.263158 clk = !clk;
Mais comme la précision indiquée en début de code est de 100ps, seule le 5.2 sera pris en compte pour la simulation, ce qui dans notre cas est tout à fait suffisant.
La simulation de l’appui sur le bouton avec rebond se fait ensuite dans un process que l’on appel souvent «stimulis» par convention :
/* Stimulis */
initial begin
$display("begin stimulis");
$dumpfile("simu/button_deb_tb.vcd");
$dumpvars(1, clk, rst, button_in, button_valid, button);
$monitor("At time %t, value = %h (%0d)",
$time, button_in, button_in);
[...]
end
Le Verilog fourni tout un tas de primitive permettant de simplifier le debuggage. Notamment $display() et $monitor(), pour afficher du texte pendant la simulation. $display() ne fait qu’afficher du texte au moment où la fonction est appelée, alors que monitor va afficher le texte à chaque changement d’état de ses paramètres.
Les functions $dump* permettent de définir le format de fichiers des traces ainsi que les signaux a dumper. Dans ce cas précis on choisira le format vcd qui est un format non compressé, de manière à améliorer la comparaison avec verilator qui lui ne sait faire que du vcd. Mais cela génère vite de très gros fichiers, il sera préférable d’utiliser le format compressé lxt2, fst ou fsdb pour des simulations plus longues.
Pour factoriser un peu de code d’attente on décrit des tâches d’attentes wait_ms et wait_us :
/* some usefull functions */
task wait_us;
input integer another_time;
begin
repeat(another_time) begin
# 1_000;
end
end
endtask
task wait_ms;
[...]
Tâches qui seront appelées lors de la simulation des rebonds :
Icarus va ainsi créer un binaire exécutable nommé simu/button_deb que l’on lancera avec les arguments de dumps:
vvp simu/button_deb -lvcd
Le fichier de traces (VCD) généré fait une taille vénérable de 752Mo et peut être visualisé avec gtkwave en l’indiquant simplement en paramètre de la commande :
gtkwave simu/button_deb_tb.vcd
Sur un Lenovo T430, la simulation prend 1 minute et 17 secondes, ce qui est tout de même relativement lent pour une simulation d’appui sur un boutton 😉
Voyons maintenant ce que nous donne Verilator.
Le testbench C++
Un testbench Verilator se présente sous la forme d’un main() C++. Dans le main() du testbench nous instancierons l’objet correspondant au model verilog transformé par verilator.
Pour cela nous devons donc convertir notre bouton_deb.v en C++ au moyen de la commande Verilator suivante :
Cette commande va nous créer un projet avec le code source du modèle ainsi que le makefile pour compiler. Il suffira donc de se rendre dans le répertoire obj_dir et de faire «make» pour compiler le modèle.
Il ne nous restera plus qu’à instancier notre bouton dans notre testbench test_button_deb.cpp :
Vbutton_deb* top = new Vbutton_deb;
Ainsi que l’objet tfp pour les dump VCD:
VerilatedVcdC* tfp = new VerilatedVcdC;
Pour simuler notre objet «top» il faut assigner des valeurs aux signaux d’entrées :
top->rst = 1;
top->button_in = 0;
top->clk = 0;
Puis évaluer les sorties avec la méthode eval() :
top->eval();
À chaque front d’horloge il faut donc changer la valeur de clk et évaluer :
top->clk = !top->clk;
top->eval();
Dans tout ce que nous venons de voir le temps n’intervient pas. En réalité, le temps n’est tout simplement pas géré dans les modèles Verilator, le modèle ne fait qu’évaluer les sorties en fonction des entrées, c’est à nous de gérer le temps comme nous le souhaitons.
Nous allons donc gérer le temps au moment du dump des signaux en lui indiquant le temps en argument:
tfp->dump(10);
Pour éviter d’avoir à taper tout ça à chaque fois on pourra créer une fonction «time_pass» qui fait passer le temps, avec un compteur global pour incrémenter le temps:
#define BASE_TIME_NS ((1000*1000)/(CLK_FREQ*2))
int base_time = 0;
/* the time is passing */
void time_pass(VerilatedVcdC *tfp, Vbutton_deb *top) {
top->clk = !top->clk;
tfp->dump(base_time*BASE_TIME_NS);
top->eval();
base_time++;
}
À l’image du testbench verilog, on pourra aussi créer des fonctions d’attente wait_ms et wait_us:
/* wait for us */
void wait_us(VerilatedVcdC *tfp, Vbutton_deb *top, int timeus) {
int wait_time = 0;
while((wait_time * BASE_TIME_NS) < (timeus * 1000)) {
wait_time++;
time_pass(tfp, top);
}
}
/* wait for ms */
void wait_ms(VerilatedVcdC *tfp, Vbutton_deb *top, int timems) {
int wait_time = 0;
while((wait_time * BASE_TIME_NS) < (timems * 1000 * 1000)) {
wait_time++;
time_pass(tfp, top);
}
}
Le code simulant les rebonds du bouton ressemble ainsi à s’y méprendre au code Verilog:
Pour lancer la simulation on va d’abord compiler le tout :
make -C obj_dir/ -j -f Vbutton_deb.mk Vbutton_deb
Puis le lancer simplement comme un vulgaire binaire exécutable :
./Vbutton_deb
La simulation crée un fichier VCD de 654Mo que nous pouvons visualiser avec gtkwave.
Sur le même Lenovo T430 la simulation ne dure cette fois que 17 secondes, ce qui est très nettement plus rapide qu’Icarus !
Mieux, si on optimise le code à la compilation avec l’option -O3 :
La rapidité d’exécution est telle que certain réussissent à faire «tourner» un soft-core avec son programme, à des fréquences allant jusqu’à la centaine de kilohertz.
Formidable !
C’est un peu l’expression que l’on a lors des premiers tests de verilator, pouvoir faire de la simulation 20 fois plus rapidement qu’Icarus semble formidable. Surtout quand cela passe par de la simplicité d’écriture du testbench en C++ ou SystemC.
Néanmoins il faut relativiser un peu notre ferveur, Verilator a encore un gros point noir: il ne gère pas le temps. Verilator n’est capable que de gérer des modèle Verilog synthétisable. Mais si ce code synthétisable inclu des primitives du constructeurs : RAM, Multiplieur, … et surtout PLL. Verilator ne sera pas capable de les simuler.
En ce qui concerne les Ram ou les multiplieurs cela ne pose pas trop de problème dans la mesure ou il est assez simple de les inférer. Mais pour les PLL cela devient bien plus compliqué, et je ne parle même pas des composants spécifique au constructeur (Bus serie, transceiver, core, …).
Ce «bug» a été répertorié il y 5 ans déjà mais pour l’instant personne ne semble s’être attelé à la tâche. D’après un des auteur de Verilator ajouter cette fonctionnalité devrait prendre quelques mois de programmation et de test. Mais cela vaudrait quand même le coup !
Un language de description hardware basé sur Haskell permettant de simuler ses composants en Haskell pure. Permet aussi de générer le design synthétisable en VHDL.
Pour changer des LED qui clignotent, voici un modulateur PWM écrit en Chisel avec son testbench.
Le code ci-dessous (PWM.scala) est du Scala standard utilisant la librairie Chisel.
import Chisel._
/* Classe contenant le comportement du module */
class PWM extends Module {
/* Bundle io obligatoire */
val io = new Bundle {
val duty = UInt(INPUT, 8)
val out = Bool(OUTPUT)
}
/* Registre 8 bits pour le compteur */
val counter = Reg(UInt(width=8))
counter := counter + UInt(1)
/* Registre de sortie avec le comparateur */
io.out := Reg(next=(counter<io.duty))
}
/* Point d'entrée du programme */
object Example {
def main(args: Array[String]): Unit = {
chiselMain(args, () => Module(new PWM()))
}
}
Le code du modulateur ci-dessus est découpé en deux parties.
La première décrit le comportement du module. Elle comprend le bundle (similaire au record en VHDL) appelé io, qui doit être présent dans chacun des modules pour décrire les entrées et les sorties, un compteur 8 bits, et le registre de sortie qui est alimenté par le comparateur de rapport cyclique.
La deuxième partie est nécessaire pour le module qui se trouve au sommet de la hiérarchie afin de décrire le point d’entrée qui servira à la génération du code.
Il est aussi nécessaire de fournir le fichier build.sbt qui sert à indiquer a SBT (l’outil de build Scala) qu’il doit également aller chercher Chisel dans ses paquets avant de lancer la compilation. SBT va d’office compiler tout les fichiers Scala (avec l’extension .scala) qu’il trouve dans le répertoire courant.
Il faut ensuite s’assurer que Chisel génère du Verilog en lançant la commande
sbt 'run --v'
Il manque un banc de test pour exercer le design. Chisel propose de compiler également pour nous un modèle C++ du design qu’on peut obtenir
en utilisant la commande suivante :
sbt 'run --test --genHarness --vcd'
On va maintenant créer un testbench C++ qui permettra de générer les VCD. Appelons ce fichier main_pwm.cpp.
#include "PWM.h"
#include <iostream>
int main (int argc, char* argv[]) {
// Créer l'objet PWM
PWM_t* c = new PWM_t();
// Recupérer le nombre de cycles à exécuter
int lim = (argc > 1) ? atoi(argv[1]) : -1;
if(lim == -1) {
cout << "wrong argument, should be :" << endl;
cout << "a.out <n>" << endl;
return 1;
}
// Initialiser le modèle
c->init();
// Ecrire l'entête du VCD
c->dump_init(stdout);
// Ecrire le rapport cyclique
c->PWM__io_duty = LIT<8>(100);
for (int t = 0; lim < 0 || t < lim; t++) {
// Active le reset sur le premier cycle
dat_t<1> reset = LIT<1>(t == 0);
// Dump du VCD
c->dump(stdout,t);
// Avancer d'une période d'horloge
c->clock(reset);
}
}
vhd2vl est un petit utilitaire écris en C (flex/bison) permettant de convertir du VHDL synthétisable en verilog. La page officiel présente la version 2.4, cette version ne compile qu’avec quelques modification sur une distribution récente.
Une version modifiée pour compiler sur debian jessie se trouve sur le github de Martoni. Pour l’utiliser il suffit de descendre le code avec git :
git clone git@github.com:Martoni/vhd2vl.git
Et faire un simple «make» dans le répertoire src/.
Pour convertir un fichier vhdl en verilog rien de plus simple (on pourra utiliser les exemples se trouvant dans les sources):
vhd2vl exemple.vhd > exemple.v
Le programme fonctionne plutôt bien à condition d’adapter son code vhdl de manière à générer un verilog correct.
En le testant sur mon «blinking led project» (blp), j’ai pu néanmoins constater quelques problèmes comme:
Support hasardeux du type CONSTANT: Le type constant est converti en un «reg» ce qui n’est pas reconnu comme une constante par les logiciels de synthèse. On doit pouvoir modifier ça simplement pour qu’il génère un «localparam» par exemple.
Pas de warnings sur les mots clef: Les mots clefs en vhdl ne sont pas les même qu’en verilog, vhd2vl ne râle pas quand il y a une variable en vhdl qui est un mot clef en verilog (par exemple avec le mot clef «edge»).
Pas de support de l’underscore ‘_’ pour les nombres. En verilog/VHDL on peut mettre des séparateur pour les milliers histoire que ça soit plus lisible 1_000_000, vhd2vl ne comprend pas.
Pas de support du type time (unité sec): bon ça c’est un peu tordu, car ça n’est pas synthétisable en l’état de toute manière.
Bref vhd2vl est un petit logiciel comportant peu de fichiers sources : en fait juste deux. S’il ne répond pas tout à fait à nos attentes il est très facile d’aller le modifier pour l’adapter.
Après discussion avec Larry, visiblement la version 2.4 sera la dernière car l’objectif est de l’intégrer au projet icarus verilog. Mais j’ai beau compiler la dernière version du trunk de icarus, je ne parviens pas à faire la même chose avec.
Migen est une tentative open-source de faire du design FPGA en python. La solution initiée par S.Bourdeauducq (principale développeur de milkymist) permet de simuler le design en python pur et de le générer en vérilog pour la synthèse sur le FPGA proprement dite.
Installation sur debian Wheezy
Pour installer Migen, il faut tout d’abord «cloner» le projet git hub avec la commande suivante :
$ git clone https://github.com/m-labs/migen.git
Puis de se rendre dans le répertoire ainsi créé et de taper la commande d’installation suivante:
$ cd migen
$ sudo python3 setup.py install
Migen requires python 3.3 or greater
Premier problème sur debian wheezy : python est trop vieux. Comme le dit le message il nous faut un python 3.3 minimum, et celui de wheezy en est à la version 3.2 🙁
Il va donc nous falloir installer la dernière version 3.4 de python (tant qu’à faire prendre la dernière).
tar -Jxvf Python-3.4.0.tar.xz
cd Python-3.4.0
./configure
make
make test
sudo make install
De là il est facile de s’installer Migen
sudo python3.4 setup.py install
Icarus
Migen génère du code en verilog. Pour pouvoir le simuler il est conseillé d’utiliser le simulateur libre Icarus, logiciel qui se trouve dans le paquet «verilog» de debian :
sudo apt-get install verilog
Dans le répertoire vpi des sources de migen il faut également générer le plugin permettant de se connecter à icarus :
$ cd migen/vpi/
$ make
$ sudo make install
install -m755 -t /usr/lib/ivl migensim.vpi
Premiers pas avec Migen
Le «helloworld» de l’électronicien, c’est la led qui clignote. La conception sur FPGA se rapprochant plus de l’électronique que du logiciel, ça ne loupe pas : l’exemple donné dans la doc est une led qui clignote !
L’exemple pose plusieurs problème :
testmigen.py :
from migen.fhdl.std import *
from mibuild.platforms import m1
plat = m1.Platform()
led = plat.request("user_led")
m = Module()
counter = Signal(26)
m.comb += led.eq(counter[25])
m.sync += counter.eq(counter + 1)
plat.build_cmdline(m)
Si on exécute tel quel ce code on tombe sur une erreur de path «/opt/Xilinx»
$ python3.4 testmigen.py
Traceback (most recent call last):
File "testmigen.py", line 11, in
plat.build_cmdline(m)
File "/usr/local/lib/python3.4/site-packages/migen-unknown-py3.4.egg/mibuild/generic_platform.py", line 272, in build_cmdline
self.build(*args, **kwargs)
File "/usr/local/lib/python3.4/site-packages/migen-unknown-py3.4.egg/mibuild/xilinx_ise.py", line 246, in build
self.map_opt, self.par_opt)
File "/usr/local/lib/python3.4/site-packages/migen-unknown-py3.4.egg/mibuild/xilinx_ise.py", line 133, in _run_ise
vers = [ver for ver in os.listdir(ise_path) if _is_valid_version(ise_path, ver)]
FileNotFoundError: [Errno 2] No such file or directory: '/opt/Xilinx'
Car Migen est capable d’aller jusqu’à la génération du design avec le webpack de xilinx. Mais pour cela il faut lui indiquer le bon chemin d’installation de ISE. Le code python de Migen référençant en dur le path, pour éviter de toucher au code, le plus simple est de faire un liens sur /opt/Xilinx.
De cette manière, nous pouvons maintenant lancer le «helloworld» qui va nous générer le bitstream final directement :
$ python3.4 testmigen.py
...
Sun May 4 16:06:25 2014
Running DRC.
DRC detected 0 errors and 0 warnings.
Creating bit map...
Saving bit stream in "top.bit".
Saving bit stream in "top.bin".
Bitstream generation is complete.
Tous les fichiers générés se trouve dans le répertoire «build» :
Deuxième problème de l’exemple : la plate-forme utilisée est une m1, or nous utilisons une apf27. Il faut que nous importions la bonne plate-forme pour pouvoir faire clignoter notre led. Nous allons donc changer notre script python pour intégrer l’apf27:
#!/usr/local/bin/python3.4# -*- coding: utf-8 -*-from migen.fhdl.stdimport *
from mibuild.generic_platformimport Pins, IOStandard
from mibuild.platformsimport apf27
ios = [("user_led", 0, Pins("J2:22"), IOStandard("LVCMOS33"))]
plat = apf27.Platform()
plat.add_extension(ios)
led = plat.request("user_led", 0)# led pin on apf27dev
m = Module()
counter = Signal(26)
m.comb += led.eq(counter[25])
m.sync += counter.eq(counter + 1)
plat.build_cmdline(m)
Le bitstream généré se nomme top.bit et se trouve dans le répertoire «build» nouvellement créé.
Il suffit ensuite de télécharger top.bit sur le fpga (via uboot ou via Linux au choix)