Après avoir déballé le kit gowin proposé par trenz micro. Il faut trouver quelques chose à faire de plus avancé qu’un simple clignotement de LED. Pour le clignotement de LED et la prise en main des outils, le lecteur se référera à l’article Hackable 32.
Pourquoi ne pas tenter le Linux des FPGA, j’ai nommé LiteX (Prononcez Lahïtixe ) ? LiteX est un framework HDL basé sur Migen pour construire des systèmes matériel facilement en python.
LiteX inclut un langage de description matériel, mais également tous les outils permettant de faire des simulations, la synthèses et générer les bitstreams pour la plupart des FPGA du marché. Bien évidement, en ce qui concerne la synthèse et les bitstreams, LiteX pilote les outils propriétaires des constructeurs. Ce pilotage ne pose généralement pas trop de problème, car tous les outils constructeurs proposent des interfaces en ligne de commande.
La carte que nous allons tenter de faire fonctionner avec LiteX est donc la TEC0117-1 produite par Trenz electronic et munie d’un FPGA gowin .
D’après le wiki, la carte n’est pas encore officiellement supporté par LiteX puisqu’elle ne se trouve pas dans la liste du projet LiteX-Boards…
Pas dans le tableau de la documentation du moins, car en fouillant dans le code du projet, il semble qu’il y ait déjà un embryon de quelques chose ici. Voila qui est très engageant pour tester la carte.
Voyons donc voir les étapes nous permettant de construire un système avec LiteX.
$ wget https://raw.githubusercontent.com/enjoy-digital/litex/master/litex_setup.py
$ chmod +x litex_setup.py
$ ./litex_setup.py init install --user (--user to install to user directory)
Attention, si comme moi vous avez un pc qui commence à prendre sérieusement de l’âge, sachez que le script litex_setup.py va descendre beaucoup de projets annexes de LiteX. Ça va prendre quelques minutes.
On aura besoin également de gcc compilé pour RISC-V :
$ ./litex_setup.py gcc
Il faudra bien penser à l’exporter à chaque fois qu’on en aura besoin :
$ export PATH=$PATH:$(echo $PWD/riscv64-*/bin/)
Construire le système pour TEC0117
Pour construire un système pour la carte il faut ensuite se rendre dans le répertoire contenant la carte puis lancer le script python correspondant:
$ export PATH=$PATH:$(echo $PWD/riscv64-*/bin/)
$ cd litex-boards/litex_boards/targets
$ python3 trenz_tec0117.py
Pour synthétiser et générer le bitstream il faut d’abord ajouter le lien vers l’ide (1.9.7 minimum) de gowin :
Ou avec la bonne option litex (qui fait appel à openFPGALoader de toute manière):
$ python3 ../litex-boards/litex_boards/targets/trenz_tec0117.py --load
INFO:SoC: __ _ __ _ __
INFO:SoC: / / (_) /____ | |/_/
INFO:SoC: / /__/ / __/ -_)> <
INFO:SoC: /____/_/\__/\__/_/|_|
INFO:SoC: Build your hardware, easily!
INFO:SoC:--------------------------------------------------------------------------------
INFO:SoC:Creating SoC... (2021-07-12 19:05:10)
INFO:SoC:--------------------------------------------------------------------------------
INFO:SoC:FPGA device : GW1NR-LV9QN88C6/I5.
INFO:SoC:System clock: 25.00MHz.
INFO:SoCBusHandler:Creating Bus Handler...
...
INFO:SoC:Initializing ROM rom with contents (Size: 0x51cc).
INFO:SoC:Auto-Resizing ROM rom from 0x6000 to 0x51cc.
Parse /home/user/myapp/litex/myapp/build/trenz_tec0117/gateware/impl/pnr/project.fs:
checksum 0xa3a3
Done
erase SRAM Done
Flash SRAM: [==================================================] 100.000000%
Done
SRAM Flash: Success
La confirmation de la bonne configuration est donnée par le message de la console, mais également par le chenillard de LED rouge.
Console litex>
Le convertisseur USB-uart de la carte possède deux interfaces ttyUSB, la première vient d’être utilisée par openFPGALoader pour charger le bitstream, la seconde permet de se connecter à la console litex :
$ screen /dev/ttyUSB1 115200
litex> reboot
__ _ __ _ __
/ / (_) /____ | |/_/
/ /__/ / __/ -_)> <
/____/_/\__/\__/_/|_|
Build your hardware, easily!
(c) Copyright 2012-2021 Enjoy-Digital
(c) Copyright 2007-2015 M-Labs
BIOS built on Jul 9 2021 13:17:06
BIOS CRC passed (855636f6)
Migen git sha1: 3ffd64c
LiteX git sha1: 2b49430f
--=============== SoC ==================--
CPU: VexRiscv_Lite @ 25MHz
BUS: WISHBONE 32-bit @ 4GiB
CSR: 32-bit data
ROM: 24KiB
SRAM: 4KiB
L2: 0KiB
SDRAM: 8192KiB 16-bit @ 25MT/s (CL-2 CWL-2)
--========== Initialization ============--
Initializing SDRAM @0x40000000...
Switching SDRAM to software control.
Switching SDRAM to hardware control.
Memtest at 0x40000000 (2.0MiB)...
Write: 0x40000000-0x40200000 2.0MiB
Read: 0x40000000-0x40200000 2.0MiB
Memtest OK
Memspeed at 0x40000000 (2.0MiB)...
Write speed: 5.6MiB/s
Read speed: 6.2MiB/s
--============== Boot ==================--
Booting from serial...
Press Q or ESC to abort boot completely.
sL5DdSMmkekro
Timeout
No boot medium found
--============= Console ================--
litex>
La commande help nous donnes les commandes disponibles :
litex> help
LiteX BIOS, available commands:
leds - Set Leds value
flush_l2_cache - Flush L2 cache
flush_cpu_dcache - Flush CPU data cache
crc - Compute CRC32 of a part of the address space
ident - Identifier of the system
help - Print this help
serialboot - Boot from Serial (SFL)
reboot - Reboot
boot - Boot from Memory
mem_speed - Test memory speed
mem_test - Test memory access
mem_copy - Copy address space
mem_write - Write address space
mem_read - Read address space
mem_list - List available memory regions
sdram_test - Test SDRAM
Photo traditionnelle
Avec LiteX, une tradition «twitter» veux que l’on fasse une photo du kit démarrant LiteX avec la console démarrée.
Et voila \o/
L’entrée en matière est incroyablement facile et fait honneur au slogan «Build your hardware, easily!».
Nous regarderons dans de futur articles ce que l’on peut faire avec.
Les constructeurs de FPGA fournissent des modèles VHDL de leurs primitives. Chez Xilinx cela se présente sous la forme d’une librairie nommée UNISIM.
Le logiciel libre de simulation GHDL n’inclut pas les sources VHDL de cette librairie dans son dépôt officiel car ça n’est pas sa propriété. Cependant, GHDL fourni des scripts permettant de les pré-compiler pour son projet.
Nous allons voir ici comment se servir des primitives disponible dans vivado avec une simulation utilisant CocoTB.
Les sources des primitives se trouvent dans le répertoire d’installation de Vivado
data/vhdl/src/unisims/
Pour les retrouver on peut utiliser la commande fd-find comme ceci :
Il peut être intéressant de mettre les librairies compilé à cet endroit pour y faire référence depuis tous ses projets ensuite :
cd data/vhdl
mkdir ghdl
cd ghdl
/usr/local/lib/ghdl/vendors/compile-xilinx-vivado.sh -all --vhdl2008
[...]
Pour compiler la librairie unisim pour vivado on utilisera le script suivant :
/usr/local/lib/ghdl/vendors/compile-xilinx-vivado.sh -a --vhdl2008
Loading environment...
Not all Xilinx primitives are VHDL-2008 compatible! Setting CONTINUE_ON_ERROR to TRUE.
Analyzing library 'unisim'...
Creating VHDL Library 'unisim'...
Analyzing files into library 'unisim'...
WARNING: /opt/Xilinx/Vivado/2020.2/data/vhdl/src/unisims/primitive/SYSMONE4.vhd:1536:44:warning: prefix of array attribute must be an object name [-Wattribute]
SCRIPT ERROR: Unfiltered line
v_str_time_length := time'image(now)'length;
SCRIPT ERROR: Unfiltered line
^
Warnings detected by filtering script.
Analyzing library 'secureip'...
Creating VHDL Library 'secureip'...
Analyzing files into library 'secureip'...
Analyzing library 'unimacro'...
Creating VHDL Library 'unimacro'...
Analyzing files into library 'unimacro'...
Analyzing library 'unifast'...
Creating VHDL Library 'unifast'...
Analyzing files into library 'unifast'...
Analyzing library 'secureip'...
Creating VHDL Library 'secureip'...
Analyzing files into library 'secureip'...
WARNING: /opt/Xilinx/Vivado/2020.2/data/vhdl/src/unifast/secureip/GTHE2_CHANNEL.vhd:29:1:warning: entity "gthe2_channel" was also defined in file "/opt/Xilinx/Vivado/2020.2/data/vhdl/src/unisims/secureip/GTHE2_CHANNEL.vhd" [-Wlibrary]
SCRIPT ERROR: Unfiltered line
library IEEE;
SCRIPT ERROR: Unfiltered line
^
Warnings detected by filtering script.
WARNING: /opt/Xilinx/Vivado/2020.2/data/vhdl/src/unifast/secureip/GTXE2_CHANNEL.vhd:34:1:warning: entity "gtxe2_channel" was also defined in file "/opt/Xilinx/Vivado/2020.2/data/vhdl/src/unisims/secureip/GTXE2_CHANNEL.vhd" [-Wlibrary]
SCRIPT ERROR: Unfiltered line
library IEEE;
SCRIPT ERROR: Unfiltered line
^
Warnings detected by filtering script.
--------------------------------------------------------------------------------
Compiling Xilinx Vivado libraries [SUCCESSFUL]
La compilation prend un temps infini, compter au moins 5 minutes sur un PC muni de 16 cœurs. Il suffira ensuite d’ajouter les lignes suivantes à son makefile CocoTB :
Le jeudi 3 juin 2021 a eu lieu, pour la seconde fois, le Live embedded event. Un ensemble de conférences totalement en ligne sur le sujet des systèmes embarqués et de l’open source.
J’ai eu l’honneur de pouvoir présenter une courte présentation sur les outils open-source pour le FPGA. La vidéo sera peut-être disponible dans quelques temps sur la chaîne youtube de l’événement. L’événement ayant été un succès, la technique a été relativement compliquée, avec peu de retour des «spectateurs» et quelque déboires filmatoires.
Pour ceux qui souhaitent retrouver les liens et les références de la présentation, je met le PDF de la présentation en ligne ici.
Ais-je bien téléchargé la dernière version de mon gateware dans ce montage ?
C’est un problème classique quand on fait de l’embarqué, entre l’éditeur de code, la génération du code bas niveau (Verilog dans le cas de Chisel) le logiciel de synthèse/placement-routage et le téléchargement du bitstream, il y a plein de façon de se tromper de version du logiciel s’exécutant réellement dans le montage. Et l’on peu passer des heures à chercher un bug dans son système en modifiant des lignes de code alors que le problème vient d’une même (vieille) version téléchargée éternellement.
Pour s’assurer que le gateware qui se trouve dans le FPGA soit bien le dernier généré il faut trouver une manière d’y enregistrer une valeur qui sera différente à chaque fois.
On pourrait mettre une version «en dur» dans nos sources, mais l’expérience montre que l’on oublie 4 fois sur 5 de l’incrémenter avant de générer le fichier. Toute bonne développeuse et tout bon développeur versionne son projet avec un gestionnaire de version. On pourrait du coup mettre le numéro de commit. Sauf que l’on ne commit pas toujours la modification avant de la tester.
Une autre astuce consiste à mettre la date EPOCH. De cette manière nous avons la date et l’heure précise de la synthèse du gateware. Cette solution est intéressante mais elle pose rapidement un problème d’occupation du FPGA. En effet, le temps en secondes filant assez rapidement nous avons besoin de registres de 32 bits minimum voir même 64bits. Ce qui est plutôt inutile pour compter les builds …
Compter les builds ?
La voila la solution toute simple, il suffit d’enregistrer un compteur dans un fichier texte. À chaque build, une fonction vient lire la valeur et l’incrémente. Pour rester à jour il nous suffit ensuite de versionner ce fichier avec le reste du code. Même si le nombre de build est grand, on n’aura rarement besoin d’en faire plus de quelques millier, ce qui rentrera très bien dans un registre de 16 bits voir moins.
Voici comment faire en Scala avec Chisel.
Dans un package avec les utilitaires «personnel» :
package myutil
On importe les packages d’entrées sorties pour lire/écrire dans les fichiers textes:
// Pour fromFile()
import scala.io.Source
// Pour PrintWriter()
import java.io._
Puis on crée une fonction de lecture écriture dans un objet personnel MyUtility :
def genCountVers(className: String = null): Option[Int] = {
if(className == null){
return None
}
println("generate counter for class " + className)
val filename = "src/main/scala/util/gen_count_vers.txt"
val versmap = scala.collection.mutable.Map[String,Int]()
/* read file to hashmap*/
val fp = Source.fromFile(filename)
for(v <- fp.getLines.map(_.split(",").map(_.trim))){
versmap(v(0)) = v(1).toInt
}
fp.close()
/* get and increment version */
val version = versmap.get(className)
if(version == None){
versmap(className) = 0
} else {
versmap(className) = version.get.toInt + 1
}
/* write back file*/
val fpw = new PrintWriter(new File(filename))
for ((hname, hvalue) <- versmap) {
fpw.write(hname + "," + hvalue + "\n")
}
fpw.close()
/* return version */
version match {
case Some(s) => Some(s.toInt + 1)
case None => Some(0)
}
}
La fonction genCountVers() va ouvrir le fichier texte gen_count_vers.txt, qui est un «CSV» composé du nom de classes et d’un numéro. Si le nom de la classe passé en paramètre n’existe pas la fonction va l’ajouter avec un compteur à zero.
Nous n’avons plus qu’a appeler la fonction genCounterVers() avec le nom de la classe (ou autre) de notre choix pour la fourrer dans un registre lisible via notre interface (spi, wisbone, i2c, jtag, …) directement sur le FPGA :
val buildnum = MyUtility.genCounterVers("MyTopClasse").get
La présentation de Jonathan Bachrach donne les bases d’utilisation des interfaces Decoupled() en Chisel. Cependant, elle date (2013). Il est donc nécessaire de se mettre un peu à jour.
Tout d’abord, il ne faut pas oublier d’inclure le package :
import chisel3.util._
Ensuite, l’interface possède en elle même l’information de direction. Il ne faut donc pas l’inclure dans un Output()/Input() lorsqu’on le déclare dans un IO(new Bundle {}) :
// Producteur
val data = Decoupled(UInt(10.W))
// Consommateur
val data = Flipped(Decoupled(UInt(10.W)))
Et pour finir, on ne déclare plus de Reg() comme donné à la fin de la présentation. Il faut utiliser RegNext() à la place :
GHDL est un logiciel écrit en ADA permettant de faire l’analyse, la compilation, la simulation ainsi que la synthèse du VHDL. Le VHDL, quant à lui, est un langage de description matériel très utilisé dans le développement sur FPGA ou ASIC. À l’origine, GHDL est un « side-project » de Tristan Gingold lui permettant de se faire la main avec ADA.
GHDL est devenu l’outil indispensable pour faire de la simulation VHDL aujourd’hui. Après presque 20 ans de développement, voici que sort en version 1.0.0 le logiciel de simulation VHDL nommé GHDL. En prime, GHDL s’offre un nouveau logo:
Peu d’informations ont filtré sur cette sortie pour le moment. On peut soupçonner que ce soit une sortie anniversaire pour marquer les 20 ans du logiciel. Il n’en reste pas moins que GHDL est devenu un maillon indispensable dans l’écosystème opensource du monde FPGA et des ASIC.
Cette version 1.0.0 supporte désormais complètement les standards 1987, 1993 et 2002 du langage défini par l’IEEE. Le support de VHDL 2008 est noté comme partiel pour le moment.
Depuis quelques années, le développement du projet s’est accéléré et supporte de mieux en mieux les projets tierces comme Yosys bien sûr mais également CocoTB pour les testbenchs écrit en Python ainsi que les standards de vérification comme UVVM, OSVVM, VUnit issues du standard d’accelera.
Un support partiel d’un langage de PSL (Properties Specification Language) est également inclus. Il permet de décrire les propriétés du système pour faire de la vérification formelle.
Et surtout, il est possible de faire de la Synthèse. Certes, l’extension ghdl-yosys-plugin est encore en développement, mais l’exemple TapTempo l’a montré : Il est tout à fait possible de faire de la synthèse VHDL avec.
Cette version mature est surtout une occasion de mettre en valeur cet outil indispensable dans le monde du développement numérique (gateware).
Ayant préparé tout le matériel pour faire du TapTempo en Verilog, il était trop tentant de réaliser la même chose en VHDL. L’occasion de se plonger dans ce langage de description matériel concurrent du Verilog. L’occasion également de parler des avancées de GHDL, un simulateur libre du VHDL, et désormais également capable de faire la synthèse en conjonction avec Yosys.
Pour comprendre TapTempo dans la culture moulesque de LinuxFr.org, il est conseillé d’aller faire un petit tour sur la page wiki homonyme.
Comme l’indique le titre de cette dépêche, nous allons effectuer un « portage » du projet TapTempo, qui était écrit en Verilog, vers le language VHDL. Nous aurons donc l’avantage de profiter d’une architecture déjà conçue. Et comme une partie des stimulus de simulation (banc de test) ont été écrit en Python avec CocoTB, nous allons pouvoir les reprendre (presque) tels quels pour simuler notre version VHDL.
Le VHDL
Le VHDL – pour VHSICHardware Description Language – est un langage de description matérielle issu d’une commande du ministère de la Défense américaine, c’est donc en toute logique qu’il soit surtout populaire… en Europe !
Le VHDL s’inspire fortement du langage ADA pour décrire le comportement des circuits intégrés numériques. Cela permet de décrire un modèle que l’on peut ensuite simuler au moyen d’un simulateur. Le simulateur VHDL OpenSource le plus connu est GHDL, initialement développé par le français Tristan Gringold comme une surcouche à GCC. Il existe également un simulateur opensource nommé nvc, mais il est moins mature que GHDL. Les autres simulateurs OpenSource, comme FreeHDL ou VerilatorVHDL, sont plus anecdotiques.
À partir d’une source VHDL il est également possible de générer un schéma de portes et de bascules logiques au moyen d’un logiciel de synthèse. Pendant longtemps, les seuls logiciels libres de synthèse HDL ciblaient le Verilog (OdinII et Yosys). Mais depuis cette année une extension GHDL est apparue pour Yosys. Si cette extension n’est pas encore vraiment stabilisée, elle n’en reste pas moins parfaitement utilisable comme nous allons le voir dans cet exemple.
Le VHDL est très hiérarchique, on décrit des modules avec leurs entrées-sorties que l’on assemble ensuite à la manière d’un schéma bloc dans un composant «top».
Dans le cas de TapTempo, l’interface du module «top» est déclarée dans une entité comme ceci :
entity taptempo is
port (
clk_i : in std_logic;
btn_i : in std_logic;
pwm_o : out std_logic
);
end entity taptempo;
VHDL n’est pas sensible à la casse ! C’est quelque chose qui est très perturbant mais dans l’exemple ci-dessus Entity est exactement identique à entity ou ENTITY. En pratique, le simulateur mettra tout en minuscule.
Le module possède deux entrées : l’horloge (clk_i) et le bouton (btn_i) ainsi qu’une sortie pwm (pwm_o) pour l’affichage. Il est possible de définir des paramètres pour la génération du module en utilisant le mot clef generic de la même manière que le port. Pour plus de simplicité nous choisirons plutôt de mettre tous les paramètres dans un package séparé (nommé taptempo_pkg.vhd), à la manière des include du C/C++.
Le type std_logic est utilisé pour représenter les états que peut prendre un signal. En simulation, il peut prendre 9 valeurs différentes : – 0 : 0 logique; – 1 : 1 logique; – Z : Haute impédence; – X : inconnue, plusieurs pilotes en conflit -> erreur; – L : 0 faible, peut-être vu comme une résistance de tirage à la masse (pull-down); – H : 1 faible, peut-être vu comme une résistance de tirage à Vcc (pull-up); – W : signal faible inconnu, quand il y a un conflit entre L et H mais qu’un troisième pilote fort (0 ou 1) pourrait tout de même mettre le signal à une valeur déterminable; – - : pas d’importance; – U : non initialisé.
Dans la pratique on évitera d’utiliser toutes ces valeurs si on veut limiter les problèmes à la synthèse. Par exemple les valeurs ‘L’ et ‘H’ ne peuvent être synthétisées « à l’intérieur » d’un FPGA, les résistances de tirage ne sont disponibles que sur les entrées sorties. Un bus de données bidirectionnel à l’extérieur du FPGA devra être séparé en deux bus dans le FPGA : un pour chaque direction.
En réalité on se limite à ‘0’ et ‘1’. Les valeurs ‘U’ et ‘X’ apparaissent dans les traces de simulation et nous avertissent d’un problème avec notre code (simulation ou synthèse).
Le type std_logic peut être étendu en tableau avec std_logic_vector très pratique pour représenter des bus.
Architecture de TapTempo
L’architecture global de TapTempo qui est exactement la même que celle de la version Verilog est présenté dans le schéma ci-dessous:
Le tempo en entrée est donné par une touche de morse
qui peut aisément être remplacé par un simple bouton poussoir (c’est d’ailleurs monté en parallèle du bouton de la colorlight). Cette entrée est synchronisée avec l’horloge du FPGA au moyen d’une double bascule en série pour éviter la métastabilité :
-- Synchronize btn
btn_sync_p: process (clk_i, rst)
begin
if rst = '1' then
btn_s <= '0';
btn_old <= '0';
elsif rising_edge(clk_i) then
btn_s <= btn_old;
btn_old <= btn_i;
end if;
end process btn_sync_p;
Le module debounce se charge ensuite de filtrer les rebonds et transfert le signal au module de mesure de la période d’appuis.
Cette période divise ensuite une constante de temps représentant le nombre de cycle de timepulse pour donner la fréquence en battement par minute (bpm).
Pour être « affichée », cette valeur est transformée en un signal périodique avec un rapport cyclique proportionnel au battement bpm.
Ce signal cyclique s’affiche ensuite très bien au moyen d’un voltmètre à aiguille comme celui-ci :
La graduation qui nous intéresse ici est celle qui va de 0 à 250.
Pour configurer les différentes constantes et définir des fonctions utiles, on utilise un « package » nommé taptempo_pkg.vhd qui sera inclus dans tous les modules du projet :
use work.taptempo_pkg.all;
La déclaration d’un package en VHDL s’effectue en deux temps :
on déclare d’abord les «objets» que l’on va utiliser dans le package :
-- La déclaration de ce que le package rend visible
package taptempo_pkg is
constant CLK_PER_NS : natural;
constant BPM_MAX : natural;
constant BPM_SIZE : natural;
constant TP_CYCLE : natural;
constant BTN_PER_MAX : natural;
constant BTN_PER_SIZE : natural;
constant BTN_PER_MIN : natural;
constant MIN_US : natural;
constant MAX_COUNT : natural;
constant DEBOUNCE_PER_US: natural;
constant DEB_MAX_COUNT : natural;
constant DEB_MAX_COUNT_SIZE : natural;
constant ZEROS : std_logic_vector(31 downto 0);
-- Usefull function for register size
function log2ceil(m : integer) return integer;
end package taptempo_pkg;
Puis on définit leurs valeurs et comportement dans le corps (body) :
package body taptempo_pkg is
constant CLK_PER_NS : natural := 40;
constant BPM_MAX : natural := 250;
-- period of tp in ns
constant TP_CYCLE : natural := 5120;
-- Debounce period in us
constant DEBOUNCE_PER_US: natural := 50_000;
constant DEB_MAX_COUNT : natural := (1000 * (DEBOUNCE_PER_US / TP_CYCLE));
constant DEB_MAX_COUNT_SIZE : natural := log2ceil(DEB_MAX_COUNT);
-- constant MIN_NS : natural := 60000000000;
constant MIN_US : natural := 60_000_000;
constant BPM_SIZE : natural := log2ceil(BPM_MAX + 1);
constant BTN_PER_MAX : natural := 1000 * (MIN_US / TP_CYCLE);
constant BTN_PER_SIZE : natural := log2ceil(BTN_PER_MAX + 1);
constant BTN_PER_MIN : natural := 1000 * (MIN_US / TP_CYCLE) / BPM_MAX;
constant MAX_COUNT : natural := TP_CYCLE / CLK_PER_NS;
constant ZEROS : std_logic_vector(31 downto 0) := x"00000000";
function log2ceil(m : integer) return integer is
begin
for i in 0 to integer'high loop
if 2 ** i >= m then
return i;
end if;
end loop;
end function log2ceil;
end package body;
Vous noterez la lourdeur d’avoir à déclarer le type de la constante dans le package avant de donner sa valeur dans le body. Les mauvaises langues diront que ça n’est pas beaucoup plus lourd que le C++ et ses doubles fichier (*.h et *.cpp) pour définir une classe.
Un cadenceur (timer) pour tout le système : Timepulse
Nous allons avoir besoin de compter le temps dans plusieurs blocs du projet. L’horloge câblée sur la colorlight est de 25Mhz. Comme nous n’avons pas besoin de précisions à la quarantaine de nanoseconde nous allons utiliser un compteur global pour générer des « pulses » toutes les 5,12µs. Ces pulses seront utilisés en entrée des blocs ayant besoin de compter le temps. Cela réduira la taille des compteurs puisqu’ils n’auront pas à repartir de l’horloge globale.
Le code est suffisamment concis pour que nous puissions le reproduire ici dans son intégralité.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.taptempo_pkg.all;
entity timepulse is
port (
-- clock and reset
clk_i : in std_logic;
rst_i : in std_logic;
-- timepulse output
tp_o : out std_logic
);
end entity timepulse;
architecture RTL of timepulse is
signal counter : natural range 0 to MAX_COUNT + 1;
begin
tp_o <= '1' when (counter = 0) else '0';
counter_p: process (clk_i, rst_i)
begin
if rst_i = '1' then
counter <= 0;
elsif rising_edge(clk_i) then
if counter < MAX_COUNT then
counter <= counter + 1;
else
counter <= 0;
end if;
end if;
end process counter_p;
end architecture RTL;
Tout module VHDL est constitué d’une partie entité entity pour décrire les interfaces d’entrées-sorties. Ici nous avons les deux entrées horloge clk_i et reset rst_i caractéristiques d’un système synchrone. L’unique sortie est le pulse généré toutes les 5,12µs.
La description du fonctionnement du module est donnée ensuite dans le bloc « architecture ». En plus des signaux d’entrées-sorties, il est possible de déclarer des signaux interne au bloc. Nous avons déclaré ici le signal counter qui est un entier naturel borné de 0 à MAX_COUNT + 1. La possibilité de borner les signaux que l’on déclare en integer permet d’aider l’outil de synthèse à générer un bus de taille adaptée quand on ne souhaite pas déclarer un std_logic_vector avec une taille explicite.
Attention, un signal n’a pas la même signification qu’une variable dans un programme procédural. Une variable prend une seule valeur qui changera au cours de l’exécution du programme. Un signal doit être vu comme une liste des différentes valeurs que prendra le signal au cours de la simulation. La première valeur de la liste sera son état initial, et à chaque événement sur le signal on ajoutera une valeur à la liste jusqu’à la fin de la simulation.
Nous aurions également pu déclarer des constantes à cet endroit, mais comme dit dans l’introduction, une constante étant un paramètre modifiable nous avons préféré le mettre dans le package taptempo_pkg.
Le corps de l’architecture de timepulse possède deux « process » concurrents qui s’exécutent en parallèle. Le premier est une assignation continue :
tp_o <= '1' when (counter = 0) else '0';
Il fait passer tp_o à ‘1’ quand le compteur counter atteint la valeur 0. Ce « process » est activé à chaque évenement/changement sur le signal counter.
Le second process est plus étoffé :
counter_p: process (clk_i, rst_i)
begin
--...
end process counter_p;
Ce process est exécuté à chaque événement surgissant sur les signaux déclarés dans la liste de sensibilité (ici clk_i et rst_i). Le contenu du process est quant à lui exécuté de manière séquentielle.
if rst_i = '1' then
--...
elsif rising_edge(clk_i) then
--...
end if;
Cette structure décrit une bascule D qui sera bien reconnue comme telle par le logiciel de synthèse. Un reset asynchrone ré-initialise le compteur à 0:
counter <= 0;
Et sur un front montant de l’horloge
elsif rising_edge(clk_i) then
on incrémente le compteur jusqu’à son maximum.
if counter < MAX_COUNT then
counter <= counter + 1;
else
counter <= 0;
end if;
Notez l’utilisation de l’opérateur d’affectation non bloquant <= qui n’affectera la valeur qu’à la fin de l’exécution du process.
Si par exemple nous avons le code suivant dans un process :
counter <= 10;
counter <= 5;
La valeur effective de counter à la fin de l’exécution de process sera 5. Et à aucun moment la valeur 10 n’apparaîtra dans counter.
Pour visualiser les signaux de sortie du module on pourra se rendre dans le répertoirecocotb/test_timepulse du projet et lancer le test en donnant ghdl comme simulateur:
$ cd test_timepulse
$ SIM=ghdl make
Le même principe pourra être appliqué pour simuler tous les autres modules de TapTempo.
La simulation consiste à laisser passer le temps pendant une miliseconde :
Le manipulateur morse génère des rebonds, le filtrage avait initialement été réglé à 20 ms sur la version Verilog. Pour être vraiment tranquille nous le monterons à 50 ms, mais c’est particulièrement long comme période. Comme pour le reste, cette configuration se trouve donc dans le package taptempo_pkg.vhd.
Le module debounce va se charger de filtrer les rebonds au moyen d’une machine à états et d’un compteur.
entity debounce is
port (
-- clock and reset
clk_i : in std_logic;
rst_i : in std_logic;
-- inputs
tp_i : in std_logic;
btn_i : in std_logic;
-- outputs
btn_o : out std_logic
);
end entity;
Le comptage des pulsations du module timepulse s’effectue dans un process :
counter_p: process (clk_i, rst_i)
begin
if rst_i = '1' then
counter <= 0;
elsif rising_edge(clk_i) then
if tp_i = '1' then
if (state_reg = s_cnt_high) or (state_reg = s_cnt_low) then
counter <= counter + 1;
else
counter <= 0;
end if;
end if;
end if;
end process counter_p;
La machine à états est constituée de 4 états :
type t_state is (s_wait_low, s_wait_high, s_cnt_high, s_cnt_low);
signal state_reg : t_state;
Deux états d’attentes et deux états de comptages. Dans un état d’attente s_wait_ on attend un front du bouton. Quand un front survient on passe dans un état de comptage s_cnt_.
Dans un état de comptage, le compteur s’incrémente et passe à l’état d’attente lorsqu’il arrive à son maximum.
De cette manière, seuls les fronts intervenant dans un état d’attente sont pris en compte. Une fois la machine à états dans un état de comptage, plus aucun front du bouton n’est « écouté ».
Le module percount (period counter) va compter le temps d’une période entre deux appuis sur le bouton.
Si on passe sur les signaux désormais bien connus clk_i, rst_i et tp_i; l’entrée du module est btn_i qui représente la valeur du bouton (appuyé ou non) sans rebond.
Les deux signaux de sortie sont : – Un vecteur btn_per_o représentant la valeur de la période mesurée – Un signal btn_per_valid donnant la validité de la valeur précédente. La valeur de btn_per_o est considérée comme bonne uniquement si btn_per_valid est à ‘1’.
Dans un premier process détecte les fronts descendants du bouton :
btn_fall <= btn_old and (not btn_i);
btn_p: process (clk_i, rst_i)
begin
if rst_i = '1' then
btn_old <= '0';
elsif rising_edge(clk_i) then
btn_old <= btn_i;
end if;
end process btn_p;
Puis dans un second process on compte le temps et on remet à 0 le compteur quand un front descendant du bouton surgit :
--...
elsif rising_edge(clk_i) then
if btn_fall = '1' then
counter_valid <= '1';
elsif counter_valid = '1' then
counter <= 0;
counter_valid <= '0';
elsif (tp_i = '1') and (counter < BTN_PER_MAX) then
counter <= counter + 1;
end if;
end if;
-- ...
C’est la partie « dure » du système, il va falloir faire une division d’une constante par la période mesurée.
Pour éviter d’avoir à utiliser les blocs multiplieurs dédiés de l’ECP5 et rester « portable » nous allons poser cette division comme on le ferait à la main.
Pour ce faire, on va mettre le dividende dans un registre nommé remainder (pour le reste) :
if to_integer(unsigned(btn_per_i)) < BTN_PER_MIN then
divisor <= std_logic_vector(to_unsigned(BTN_PER_MIN, BTN_PER_SIZE)) & ZEROS(DIVIDENTWIDTH-1 downto 0);
else
divisor <= btn_per_i & ZEROS(DIVIDENTWIDTH-1 downto 0);
end if;
Notez la la valeur minimum de btn_per_i qui nous évitera les problèmes de division par 0 ainsi que des valeurs de bpm trop élevées.
La division est cadencée par une machine à états de 3 états :
type t_state is (s_init, s_compute, s_result);
signal state_reg : t_state;
Un état initial pour démarrer, un état de calcul et un état durant lequel la valeur de sortie est considérée comme bonne.
L’algorithme de division consiste en une série de décalage à droite du diviseur :
divisor <= "0" & divisor(REGWIDTH-1 downto 1);
Et de décalages à gauche du quotient :
if (unsigned(divisor) <= unsigned(remainder)) then
remainder <= std_logic_vector(unsigned(remainder) - unsigned(divisor));
quotient <= quotient(REGWIDTH-2 downto 0) & "1";
else
quotient <= quotient(REGWIDTH-2 downto 0) & "0";
end if;
Avec ajout d’un ‘1’ ou d’un ‘0’ en fonction de la valeur du diviseur et du reste. Lorsque le reste est supérieur au diviseur, on le soustrait du diviseur et on ajoute un bit ‘1’ au moment du décalage du quotient. Sinon on ajoute le bit ‘0’ sans toucher au registre de reste (remainder).
Les registres remainder, divisor et quotient sont des std_logic_vector qui ne représente rien d’autre qu’un « paquet de bits » pour le langage. Si l’on souhaite effectuer des opérations de comparaison ou des additions soustraction dessus il faut donc dire au VHDL que ses bits représentent un nombre (ici non signé) en les « castant » avec unsigned(). Et si l’on souhaite assigner le résultat à un std_logic_vector il faut « caster » à nouveau avec std_logic_vector. Ces multiples conversion de type peuvent vite devenir un casse-tête quand on fait du VHDL.
L’esperluette & est ici un opérateur de concaténation de deux vecteurs std_logic_vector.
Le résultat de la division est donné en continu par les bits de poids faible du registre divisor :
La sortie « d’affichage » de la valeur consiste à générer un signal périodique pwm_o dont le rapport cyclique est proportionnel à la valeur de bpm_o.
On va pour cela compter de 0 à BPM_MAX :
-- count
count_p: process (clk_i, rst_i)
begin
if rst_i = '1' then
count <= BPM_MAX;
elsif rising_edge(clk_i) then
if tp_i = '1' then
if count = 0 then
count <= BPM_MAX;
else
count <= count - 1;
end if ;
end if ;
end if;
end process count_p;
Tant que la valeur du compteur se trouvera en dessous d’un seuil pwmthreshold elle sera à ‘1’, une fois le seuil dépassé elle passera à ‘0’:
Ce seuil ne doit pas changer de valeur pendant le comptage, on va donc devoir utiliser un registre intermédiaire bpm_reg pour stocker la valeur de bpm_i d’entrée :
-- Latching bpm_i on bpm_valid
bpm_register_p: process (clk_i, rst_i)
begin
if rst_i = '1' then
bpm_reg <= BPM_RESET;
pwmthreshold <= BPM_RESET;
elsif rising_edge(clk_i) then
if bpm_valid = '1' then
bpm_reg <= to_integer(unsigned(bpm_i));
end if;
if (count = BPM_MAX) then
pwmthreshold <= bpm_reg;
end if;
end if;
end process bpm_register_p;
Notez, à nouveau, la conversion d’un std_logic_vector (bpm_i) vers un entier naturel (bpm_reg) par le double « cast » to_integer(unsigned())
Synthèse, placement-routage et configuration
Nous voici arrivé à la véritable information de cette dépêche : il est désormais possible de faire de la synthèse VHDL avec un logiciel libre. Le VHDL est le langage avec lequel les étudiants français abordent le monde du FPGA en général. Je l’ai personnellement beaucoup pratiqué en utilisant ghdl pour la simulation et gtkwave pour visualiser les chronogrammes. Mais jusqu’à cette année, j’ai toujours dû passer sur les monstres propriétaires fournis gratuitement par les fabricants de FPGA pour la partie synthèse. Monstres de plusieurs dizaines de Giga-octets à télécharger, souvent précompilé pour une architecture 32 ou 64 bits mais pas les deux, incluant des machines virtuelles java et autres librairies graphique bugués quand on change la langue du système (coucou le MIG de Vivado). Quand ils ne sont tout simplement pas compatibles Linux (de plus en plus rare cependant). Bref, la synthèse VHDL n’était pas une sinécure.
La simulation n’était pas en reste non plus, car si GHDL fonctionnait plutôt bien, il n’était pas des plus rapides à l’époque. Et impossible de faire de la simulation mixte en mélangeant du VHDL et du Verilog.
Impossible également d’utiliser la formule 1 de la simulation qu’est Verilator, un simulateur un peu spécial qui converti son design en un objet C++, le testbench étant ensuite écrit comme du C++ «normal». Le rapport de performance est d’au moins 100 fois plus rapide.
Pour être honnête, signalons qu’il existe bien le logiciel français Alliance, mais c’est très orienté ASIC, et je n’ai jamais réussi à le faire fonctionner correctement. Peut-être que si des développeurs d’Alliance traînent sur LinuxFR, ils pourront nous en parler.
Bref, la lumière est arrivée cette année avec le développement du greffon ghdl pour yosys ainsi que les avancées à toute vapeur de la partie «synthèse» de ghdl. Comme nous allons le voir dans la section suivante.
GHDL + Yosys, la lune de miel
Pour pouvoir synthétiser TapTempo il va falloir compiler et installer les trois projet suivant :
GHDL : Le logiciel de simulation ainsi que de synthèse (ne pas oublier d’option de synthèse dans la configuration avant compilation) ;
Yosys : le logiciel de synthèse Verilog, véritable couteau suisse pour le FPGA et plus si affinité ;
ghdl-yosys-plugin : le greffon qui permet de compiler une librairie ghdl.so à copier dans le répertoire YOSYS_PREFIX/share/yosys/plugins/ de yosys.
Il est fortement conseillé de prendre les dernières versions du code des projets ci-dessus et de les compiler « à la main » car ces projets avancent vite et bougent beaucoup, les paquets des différentes distributions linux sont déjà largement obsolètes.
Une fois installé on peut lancer yosys avec le greffon ghdl comme ceci :
Et c’est tout ! Maintenant on peut reprendre les commandes et la procédure que l’on avait utilisée dans le cas de TapTempo en Verilog pour faire la synthèse, le placement routage et le chargement.
Notez que comme les sources ont été chargées et parsé dans yosys, il est parfaitement possible de le convertir en Verilog grâce à la commande write_verilog:
La version Verilog ainsi générée pourra être simulée avec verilator ou icarus par exemple pour faire de la simulation mixte, ou tout simplement pour accélérer la simulation dans le cas de verilator.
Pour synthétiser sur la colorlight qui est munie d’un FPGA ECP5 de chez Lattice, on utilisera la commande synth_ecp5 avec une sortie netlist au format json.
yosys> synth_ecp5 -json taptempo.json
[...]
=== taptempo ===
Number of wires: 540
Number of wire bits: 1515
Number of public wires: 540
Number of public wire bits: 1515
Number of memories: 0
Number of memory bits: 0
Number of processes: 0
Number of cells: 785
CCU2C 109
L6MUX21 9
LUT4 452
PFUMX 25
TRELLIS_FF 190
2.50. Executing CHECK pass (checking for obvious problems).
Checking module taptempo...
Found and reported 0 problems.
2.51. Executing JSON backend.
Arrivé à cette étape il peut être intéressant de comparer avec la version Verilog les ressources utilisées par le même projet :
$ yosys
yosys> read_verilog debounce.v per2bpm.v percount.v pwmgen.v rstgen.v taptempo.v timepulse.v
yosys> synth_ecp5 -json taptempo.json
...
=== taptempo ===
Number of wires: 487
Number of wire bits: 1435
Number of public wires: 487
Number of public wire bits: 1435
Number of memories: 0
Number of memory bits: 0
Number of processes: 0
Number of cells: 751
CCU2C 105
L6MUX21 1
LUT4 441
PFUMX 13
TRELLIS_FF 191
8.50. Executing CHECK pass (checking for obvious problems).
Checking module taptempo...
Found and reported 0 problems.
8.51. Executing JSON backend.
Nous obtenons un design légèrement plus petit avec la version Verilog, mais les ordres de grandeurs sont tout de même respecté.
Placement routage avec NextPnR
NextPnR est un logiciel de placement routage qui prend le schéma (netlist) de cellules «primitives» généré par le logiciel de synthèse et associe chaque cellule à une cellule disponible dans le FPGA. NextPnR effectue également le routage qui consiste à établir les connexions entre les différentes cellules.
C’est également à cette étape que l’on va préciser la configuration du FPGA (taille, IO…). En plus du fichier json de synthèse nous donnerons un second fichier de configuration des IO nommé taptempo.lpf décrivant nos trois signaux d’entrées-sortie :
LOCATE COMP "clk_i" SITE "P6";
IOBUF PORT "clk_i" IO_TYPE=LVCMOS33;
FREQUENCY PORT "clk_i" 25 MHZ;
LOCATE COMP "btn_i" SITE "M13";
IOBUF PORT "btn_i" IO_TYPE=LVCMOS33;
LOCATE COMP "pwm_o" SITE "P4";
IOBUF PORT "pwm_o" IO_TYPE=LVCMOS33;
Toutes les commandes de synthèse données ici sont bien évidemment disponibles dans un Makefile sur le dépot. Pour faire le placement routage nous pourrions taper la commande suivante :
Qui ne donnera pas le fichier de configuration nommé bitstream permettant de configurer le FPGA !
Car les spécifications des FPGA sont gardées jalousement par les constructeurs, et il faut des heures et des heures d’ingénieries inverses pour venir à bout de ces informations. Travail qui a été effectué via le projet Trellis et qui nous permet de convertir la sortie texte précédente taptempo_out.config en un bitstream reconnu par l’EPC5 :
$ ecppack taptempo_out.config taptempo.bit
Et l’on décroche enfin le Saint Grââl permettant de configurer la colorlight : le bitstream taptempo.bit.
En avant la musique avec openFPGALoader
Arrivé à cette étape il serait vraiment dommage d’être contraint de relancer l’ide proprio du constructeur juste pour télécharger le bitstream dans le FPGA via une sonde USB-Jtag !
C’est là que l’on peut dégainer le projet openFPGALoader qui a pour ambition de permettre la configuration de tous les FPGA existant avec toutes les sondes disponibles sur le marché.
Le VHDL est très verbeux, les évolutions du langage ont tenté de corriger un peu le tir mais cela reste tout de même verbeux. Certaine caractéristiques comme l’insensibilité à la casse font un peu penser à un langage d’un autre âge. Cependant, l’héritage du langage Ada fait de VHDL un langage très strict et déterministe de part sa conception contrairement au Verilog.
Le typage fort peut-être considéré à première vu comme un défaut ralentissant l’écriture du code. Mais il n’en est rien, après avoir souffert de « compiler » votre porte-gramme pour la simulation, vous aurez l’agréable surprise de voir votre système fonctionner parfaitement sur le FPGA du (presque) premier coup.
Le vocabulaire VHDL est très vaste et on se contente en général des structures de code connue dont on sait qu’elles « synthétiseront » correctement, ce qui donne une impression de ne jamais pouvoir atteindre la maîtrise du langage.
Il y a quelques années je m’étais posé la question de la popularité du VHDL par rapport au Verilog. En effet, même si le VHDL est presque aussi bien supporté que le Verilog par les outils des constructeurs, ça n’était pas le cas des logiciels libres. C’est encore largement le cas aujourd’hui, même certain logiciels non libre supportent en priorité le Verilog. Le constructeur Gowin par exemple ne permettait que la synthèse Verilog avec son outil maison. Il fallait installer le logiciel tier synplify de synopsis pour pouvoir accéder à la synthèse VHDL.
Cette extension de ghdl pour Yosys change la donne. Car, comme nous l’avons vu, il est possible de l’utiliser pour convertir son projet en Verilog et avoir accès à tous l’écosystème libre Verilog. Il est également possible de faire de la vérification formelle pour le VHDL.
Avoir la compétence VHDL dans son CV est une assez bonne idée car c’est souvent par ce mot que l’on résume le développement ASIC/FPGA/SoC. En Europe, le VHDL est très apprécié de l’industrie et particulièrement de l’industrie de défense.
Mais si c’est juste pour mesurer le tempo, ce n’est peut-être pas la voie la plus rapide et la plus simple 😉
Le standard RISC-V permet l’ajout d’extension «maison» par le constructeur. Cela fait longtemps qu’on ne propose plus l’ajout de puce à coté du processeur pour ajouter des instructions (comme le coprocesseur arithmétique sur les 386), la perte en bande passante est vraiment trop grande.
Le chinois Andes technology et le Français Menta s’associe pour proposer une solution eFPGA permettant de synthétiser «à la demande» ces instructions maison.
La société Menta va-t-elle publier la spécification du «bitstream» permettant de configurer cette partie ? Rien n’est moins sûr, mais on l’encourage à faire comme quicklogic.