Tous les articles par Fabien Marteau

Getting started with FPGAS

Russell Merrick est un ingénieur en électronique qui travail sur des FPGA depuis plus de 15 ans. C’est l’auteur du site internet Nandland qui propose toute une série de tutoriels pour débuter et s’amuser avec des FPGA.

Russell vient de sortir un livre chez No Starch Press pour débuter avec un FPGA.

En seulement 280 pages, on peut dire que l’auteur couvre bien le sujet. Les deux langages HDL du «marché» sont décrits et tous les exemples sont donnés en Verilog ainsi qu’en VHDL.

C’est la première fois que, dans un livre, je vois un vrai comparatif des deux HDL. En effet, l’un est souvent balayé au profit de l’autre avec un «si vous connaissez l’un vous saurez vous servir de l’autre». Même si l’accent est mis sur le ICE40 de Lattice (Célèbre FPGA lowcost reversé dans le projet icestorm), on sent bien qu’il existe d’autres constructeurs et que l’auteur a travaillé avec.

Le livre n’est pas si gros et pourtant il traite vraiment de tout ce qu’il faut savoir pour bien commencer (et avancer) dans le FPGA.

Un chapitre entier est consacré aux bascule D (FlipFlop) et à la problématique de conception synchrone. La notion de domaines d’horloge et son franchissement, les machines d’états, les macro classique (RAM, PLL, DSP) ne sont pas en reste.

Et avant d’aborder les entrées sorties (I/O, LVDS, SerDes) un chapitre particulièrement intéressant sur l’arithmétique est abordé. Tout est dit pour additionner, soustraire, multiplier et diviser (enfin surtout les méthodes de contournement de la division) des entiers mais également des nombres en virgules fixe (Qn.m) dans un FPGA.

C’est un livre que j’aurais adoré avoir pour débuter en FPGA, mais qui fera tout de même un très bon livre de référence au besoin.

Cartes Gatemate

Nous avons déjà parlé du Gatemate dans les colonnes du FLF. Mais jusqu’à présent, seul le kit de développement officiel de CologneChip était disponible. Le tarif du kit officiel étant assez élevé on attendait avec impatience d’avoir des cartes développées par des tiers pour pouvoir investir.

Ce qui est chose faite avec les kits suivant. La note reste éditable au grès des sorties de kit muni du FPGA.

Evaluation board officielle par CologneChip

À tout seigneur tout honneur, citons d’abord du kit officiel qui est proposé à un prix de 226€ sur digikey.

Le kit officiel du GateMateA1 proposé par CologneChip dans sa belle boite en carton

Olimex GateMate-A1

Annoncé en décembre 2023 par Olimex, le kit GateMateA1 devrait être proposé au tarif de 50€.

La carte GateMate A1 annoncée par Olimex

TrenzMicro TEG2000-01-P001

Le SoM de TrenzMicro TEG2000-01-P001 est dans les même ordre de grandeur au niveau tarif (69€) mais nécessitera une carte d’accueil pour s’en servir.

Le SoM (System on Module) de Trenz micro avec le GatemateA1

Identification des bitstreams de la série 7 avec usr_access2

Le processus de synthèse/placement/routage/bitstream prenant beaucoup de temps, on est amené à faire d’autres activité pendant le traitement. Ce «switch» de tâche nous amène à faire des erreurs fréquentes de version de bitstream au moment de la configuration du FPGA.

Il est fréquent de passer des heures voir des jours sur un bug qui n’en était finalement pas un puisque nous n’avions pas mis à jour la version du bitstream.

Pour éviter ce problème il faut pouvoir lire la version du bitstream généré de manière à s’assurer qu’on travail bien avec la bonne.

C’est exactement l’objet de la macro «usr_access» des FPGA de la série 7 de Xilinx.

Cette macro est appelée de la manière suivante en VHDL :

library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.numeric_std.all;

Entity usr_accesse2 is
port
(
  CFGCLK : out std_logic;
  DATA : out std_logic_vector(31 downto 0);
  DATAVALID : out std_logic
);
end entity;

Architecture usr_accesse2_1 of usr_accesse2 is

begin

  CFGCLK <= '1';
  DATA <= x"00000E0F";
  DATAVALID <= '1';

end architecture usr_accesse2_1;

Et la valeurs de la sortie «DATA» est ré-inscriptible jusqu’à la génération du bitstream avec l’option -g USR_ACCESS.

Pour y mettre la date et l’heure on utilisera l’option timestamp dans le menu tools->edit device property.

Cette action à pour effet d’ajouter la commande suivante dans le xdc :

set_property BITSTREAM.CONFIG.USR_ACCESS TIMESTAMP [current_design]

Mais elle ne met pas la date chez moi pour le moment 🙁

Un timeout dans cocotb

Avec Cocotb nous avons parfois des coroutines qui sont susceptible de rester «coincées» dans une boucle d’attente infinie. Si l’on y prête pas garde, on a vite fait de remplir son disque dur de traces totalement inutile.

# Une coroutine qui attend bien trop longtemps
async def too_long_coroutine(self):
    await Timer(1, units="sec")

Pour éviter ce problème, l’idéal serait de pouvoir ajouter un «timeout» à l’appel de la coroutine susceptible de bloquer.

Ça tombe bien, cocotb a prévu un trigger pour ça : with_timeout()

from cocotb.triggers import with_timeout
await with_timeout(testcls.too_long_coroutine(), 100, "ns")

Sauf que python n’a pas trop l’air d’accord pour exécuter notre coroutine comme un trigger.

TypeError: All triggers must be instances of Trigger! Got: coroutine

C’est dommage, on perd beaucoup de l’intérêt de ce trigger !

La solution donnée par marlonjames est d’«empaquetter» la coroutine dans la fonction start_soon() comme ceci :

await with_timeout(
     cocotb.start_soon(testcls.too_long_coroutine()),
     100, "ns")

De cette manière, le test s’interromps sur une levé d’interruption SimTimeoutError et le test est marqué FAIL sans ruiner notre disque dur.

    raise cocotb.result.SimTimeoutError
cocotb.result.SimTimeoutError

Installation des outils libres de simulations pour windows

C’est une réalité aujourd’hui, il y a encore beaucoup d’entreprises qui tournent exclusivement avec le système d’exploitation de Microsoft.

L’environnement Microsoft n’est pas idéal pour faire fonctionner les outils de simulations libre, mais c’est tout de même possible.

Voyons comment faire pour installer Yosys, GHDL, smtbmc, gtkwave et autres outils classique dans le monde de la simulation libre sur Windows 10 Famille.

MSYS2

MSYS2 est un environnement/shell de développement open-source pour windows. Pour l’installer il suffit de se rendre sur la page officiel du projet et de télécharger l’installeur (83.5Mo).

Le répertoire par défaut «c:/msys64» convient très bien et installera un raccourci dans le menu de windows. N’oubliez pas de désactiver votre antivirus avant de lancer l’installation, sinon il aura peur de gpg et vous empêchera de l’installer correctement !

Console MSYS2 permettant d’installer les différents package avec pacman

Une fois installé, la console se lance. Nous allons commencer par mettre à jours MSYS avec pacman :

$ pacman -Syu

La commande aura pour effet de fermer la fenêtre de console, qu’il faudra réouvrir pour lancer les commandes suivantes pour installer les dépendances:

$ pacman -Su
$ pacman -S --needed base-devel mingw-w64-x86_64-toolchain
(defaut=all)
$ pacman -S git

Cet environnement de travail est nécessaire pour installer ensuite nos outils de simulations.

GHDL

GHDL est le simulateur libre de base pour le VHDL. Nous l’installerons avec la commande suivante tirée du tutoriel de darkmagicdesign.

$ pacman -S mingw-w64-x86_64-ghdl-llvm

Yosys

Yosys est inclue comme package pacman, ce qui nous simplifie grandement la vie :

$ pacman -S mingw-w64-x86_64-yosys

GTKwave

$ pacman -S mingw-w64-x86_64-gtkwave

ghdl-yosys-plugin

Le package «eda» fait doublon avec certain paquet installé avant, mais en lançant la commande suivante :

$ pacman -S mingw-w64-x86_64-eda

On est sûr d’avoir Yosys compilé en statique avec l’extension GHDL.

python3

$ pacman -S mingw-w64-x86_64-python-scipy mingw-w64-x86_64-python-matplotlib mingw-w64-x86_64-python-numpy
$ pacman -S --needed make mingw-w64-x86_64-gcc mingw-w64-x86_64-python3-pip mingw-w64-x86_64-python3-setuptools mingw-w64-x86_64-python3-wheel

À ce point de l’installation on peut fermer la fenêtre de console pour aller ouvrir la console nommée «MSYS2 MINGW64» dans laquelle on exécutera les programmes fraîchement installés.

Le répertoire «home/user» de cette console se retrouve ensuite dans le répertoire C:\msys2\home de votre ordinateur.

Cocotb

Cocotb utilise l’installateur de python nommé «pip»:

$ pip install --no-build-isolation cocotb
$ pip install --no-build-isolation pytest
$ pip install --no-build-isolation cocotbext-axi

Pour finir

En ouvrant la console nommée «MSYS2 MINGW64» on est sûr d’avoir désormais un environnement de simulation correct pour développer des IP sous Windows.

L’exécution de ses différents makefile habituels est presque transparente. Il faudra quand même adapter certaine ligne de commande. Par exemple avec GHDL il est obligatoire d’élaborer le design avec «-e» avant de le lancer avec «-r». Alors que sous Linux on peut passer directement de l’analyse à l’exécution, GHDL devine ce qu’il faut élaborer.

Simulons la FFT de Xilinx

Après avoir simulé des FFT avec Python et pylab, voyons comment les intégrer dans un FPGA réel de Xilinx.

Faire une FFT dans un FPGA est quelque chose qui n’est pas trivial. L’avantage, suivant la taille du FPGA, est de pouvoir en faire tourner plusieurs en parallèle pour accélérer le traitement. L’inconvénient étant le temps de développement qui est décuplé par rapport à une solution embarquée sur les habituels DSP ou microcontrôleurs.

Pour accélérer le développement, l’utilisation de modules fournis par les constructeurs est très tentante. Bien sûr, si on utilise la FFT d’un constructeur X, elle ne sera pas utilisable sur le FPGA du constructeur Y… Mais c’est de bonne guerre.

Plus gênant est la difficulté de simuler le module sur son PC pour valider l’algorithme que l’on souhaite mettre en œuvre.

C’est pour cela que Xilinx fournit un modèle C de sa FFT. Modèle que l’on peut utiliser gratuitement avec GCC.

Voyons voir comment mettre tout ça en œuvre.

Installation du modèle

Pour compiler le modèle il faut d’abord le générer à partir d’un projet Vivado. On crée donc un projet Vivado avec un FPGA cible et on instancie le bloc «Fast Fourrier Transform» dans l’«IP designer». Pour pouvoir générer le modèle il faut que les entrées/sorties soient connectées à quelques chose, dans notre cas nous nous contenterons d’exporter les ports.

Le bloc FFT de Vivado

L’archive au format zip est générée dans le répertoire suivant :

test_fft/test_fft.gen/sources_1/bd/fft_test_design/ip/fft_test_design_xfft_0_0/cmodel/xfft_v9_1_bitacc_cmodel_lin64.zip

Archive que l’on dézippera dans le répertoire de son choix :

$ unzip xfft_v9_1_bitacc_cmodel_lin64.zip
$ ls -l
gmp.h
libgmp.so.11
libIp_xfft_v9_1_bitacc_cmodel.so
make_xfft_v9_1_mex.m
run_bitacc_cmodel.c
run_xfft_v9_1_mex.m
xfft_v9_1_bitacc_cmodel.h
xfft_v9_1_bitacc_mex.cpp

et à laquelle nous ajouterons un fichier main() et un Makefile. Par contre ne rêvez pas, il n’y a pas les sources du modèle 😉 le modèle se trouve dans le fichier binaire de librairie libIp_xfft_v9_1_bitacc_cmodel.so

L’explication pour la compilation est donnée sur le site officiel. Avec g++ ça donne :

$ g++ -std=c++11 -I. -L. -lgmp -Wl,-rpath,. run_bitacc_cmodel.c -o run_fft -lIp_xfft_v9_1_bitacc_cmodel

La compilation génère un binaire nommé run_fft qu’il faut lancer en intégrant les librairies du répertoire courant pour le lien dynamique :

$  LD_LIBRARY_PATH=$$LD_LIBRARY_PATH:. ./run_fft
Running the C model...
Simulation completed successfully
Outputs from simulation are correct
$ 

Le résultat est relativement frustrant: certes il n’y a pas d’erreur, mais enfin bon … on n’est pas super avancé. On aimerait bien avoir de belles courbes et pouvoir admirer le résultat spectral de cette FFT !

Pour cela il va falloir se plonger dans le code «main()» et injecter son propre signal.

Plongée dans le code

Pour avoir la documentation du modèle on pourra bien sûr se référer à la documentation officiel, mais on peut également se plonger dans le header xfft_v9_1_bitacc_cmodel.h qui est bien commenté.

Le calcul est lancé avec la fonction xilinx_ip_xfft_v9_1_bitacc_simulate déclarée ainsi :


/**
 * Simulate this bit-accurate C-Model.
 *
 * @param     state      Internal state of this C-Model. State
 *                       may span multiple simulations.
 * @param     inputs     Inputs to this C-Model.
 * @param     outputs    Outputs from this C-Model.
 *
 * @returns   Exit code   Zero for SUCCESS, Non-zero otherwise.
 */
Ip_xilinx_ip_xfft_v9_1_DLL
int xilinx_ip_xfft_v9_1_bitacc_simulate
(
 struct xilinx_ip_xfft_v9_1_state*   state,
 struct xilinx_ip_xfft_v9_1_inputs   inputs,
 struct xilinx_ip_xfft_v9_1_outputs* outputs
 );

L’état est créé avec la fonction xilinx_ip_xfft_v9_1_create_state() et la structure d’entrée (inputs) possède un tableau de double pour la partie imaginaire et un tableau de double pour la partie réelle. La taille de la FFT étant donnée en 2^n par l’attribut nfft.

struct xilinx_ip_xfft_v9_1_inputs
{
  int      nfft;              //@- log2(point size)

  double*  xn_re;             //@- Input data (real)
  int      xn_re_size;

  double*  xn_im;             //@- Input data (imaginary)
  int      xn_im_size;

  int*     scaling_sch;       //@- Scaling schedule
  int      scaling_sch_size;

  int      direction;         //@- Transform direction
}; // end xilinx_ip_xfft_v9_1_inputs

La structure de sortie est encore plus simple :

struct xilinx_ip_xfft_v9_1_outputs
{
  double*  xk_re;          //@- Output data (real)
  int      xk_re_size;

  double*  xk_im;          //@- Output data (imaginary)
  int      xk_im_size;

  int      blk_exp;        //@- Block exponent

  int      overflow;       //@- Overflow occurred
}; // xilinx_ip_xfft_v9_1_outputs

Dans l’exemple donnée, la partie imaginaire est fixée à 0 sur les 1024 échantillons et la partie réel à 0.5.

    // Create input data frame: constant data
    double constant_input = 0.5;
    int i;
    for (i=0; i<samples; i++) {
      xn_re[i] = constant_input;
      xn_im[i] = 0.0;
    }

Si le signal est constant, en toute logique seule la fréquence continue (0Hz) doit être différente de 0. C’est ce qui est vérifié après avoir effectué le calcul :

    // Check xk_re data: only xk_re[0] should be non-zero
    double expected_xk_re_0;
    if (C_HAS_SCALING == 0) {
      expected_xk_re_0 = constant_input * (1 << C_NFFT_MAX);
    } else {
      expected_xk_re_0 = constant_input;
    }
    if (xk_re[0] != expected_xk_re_0) {
      cerr << "ERROR:" << channel_text << " xk_re[0] is incorrect: expected " << expected_xk_re_0 << ", actual " << xk_re[0] << endl;
      ok = false;
    }
    for (i=1; i<samples; i++) {
      if (xk_re[i] != 0.0) {
        cerr << "ERROR:" << channel_text << " xk_re[" << i << "] is incorrect: expected " << 0.0 << ", actual " << xk_re[i] << endl;
        ok = false;
      }
    }

    // Check xk_im data: all values should be zero
    for (i=1; i<samples; i++) {
      if (xk_im[i] != 0.0) {
        cerr << "ERROR:" << channel_text << " xk_im[" << i << "] is incorrect: expected " << 0.0 << ", actual " << xk_im[i] << endl;
        ok = false;
      }
    }

Transformée de wavelet

Tout ceci n’est pas très parlant pour le moment, testons maintenant le modèle sur la «wavelet» générée à partir d’un script python. Le script permettant de générer le signal et de l’écrire dans un fichier *.txt se trouve dans le répertoire cmodel du dépot github.

Le signal que l’on va injecter dans le modèle FFT de xilinx

Le script génère un fichier ysig.txt avec toutes les valeurs flottantes écrites en ASCII. On va ensuite relire le fichier avec le programme C++ :

    // Read input data from file ysig.txt
    std::ifstream yfile; yfile.open("ysig.txt");
    if(!yfile.is_open()){
        perror("Open error");
        exit(EXIT_FAILURE);
    }
    string line;
    int i=0; 
    while(getline(yfile, line)){
        xn_re[i] = stof(line);
        cout << stof(line) << endl;
        xn_im[i] = 0.0;
        i++; 
    }

Le programme écrira le résultat sous dans le fichier xfft_out.txt une fois le résultat calculé:


    // save outputs in xfft_out.txt
    std::ofstream outfile; outfile.open("xfft_out.txt");
    if(outputs.xk_re_size != outputs.xk_im_size){
        printf("Error imaginary part size is not equal to real part");
    }
    for(int i=0; i < outputs.xk_re_size; i++){
        outfile << outputs.xk_re[i] << ", " << outputs.xk_im[i] << endl;
    }
    

Fichier que l’on relira pour l’afficher au moyen du script python plot_fft.py

Résultat plutôt concluant puisque identique au calcul de la fonction magnitude_spectrum de pylab.

Et nous avons la bonne surprise d’obtenir le même spectre du module qu’avec la fonction de pylab.

On peut maintenant jouer avec les paramètres du module Xilinx et affiner notre modèle de simulation avant de le synthétiser dans un FPGA (de chez Xilinx évidement 😉