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.

ClaSH un HDL basé sur Haskell

La version 1.0 de CλaSH est sortie en septembre 2019. Profitons de cette sortie pour présenter ce langage de description matériel basé sur Haskell.

Logo de CλaSH

Mais qu’est-ce que CλaSH ? On pourrait commencer par le définir en décrivant ce qu’il n’est pas :

  • CλaSH (of cλaN ?) n’est pas un jeu vidéo se jouant en caressant un téléphone.
  • CλaSH n’est pas un groupe de musique se demandant s’il doit rester ou partir.
  • CλaSH n’est pas un HLS (High Level Synthesis). La fonction d’un logiciel HLS consiste à convertir un algorithme écrit dans un langage de programmation classique (très souvent du C) dans une architecture numérique en Verilog/VHDL.

Non.

CλaSH est un langage de description matériel basé sur le langage fonctionnel Haskell.

Le langage Haskell est né dans les années 90, fondé par une société secrète de mathématiciens qui voulait reprendre le contrôle des ordinateurs. En effet, vexés de voir tous ces geeks maîtriser cette machine mieux qu’eux ils décidèrent de se réunir de manière anonyme dans une cave (où il fallait entrer avec un mot de passe à base de développement limités sur un Algèbre commutatif isomorphe) pour fonder un nouveau langage compris par eux seuls. Le Haskell était né ! Enfin je crois 😉

Haskell est un langage fonctionnel, qui nous impose une autre façons de penser un programme informatique. Du dire des auteurs de CλaSH, le paradigme fonctionnel est plus à même de décrire du matériel que le paradigme impératif.

Un composant décrit en CλaSH est directement convertible en Verilog ou en VHDL (les deux sont gérés dans la version 1.0). Ce qui fait qu’un composant décrit en CλaSH est synthétisable avec n’importe quel logiciel de synthèse FPGA.

Dans le cas de ce type de langage on parle de générateur de code, un peu comme Chisel (basé sur Scala), Migen/Litex (basé sur Python) ou SpinalHDL (basé également sur Scala). Ces langages sont en fait des librairies ou des modules du langage auquel ils sont accolés.
Cette modularité permet de profiter d’un langage souvent bien établi avec énormément de fonctions optimisées (Scala, Python, Haskell…).

Le paradigme fonctionnel n’est pas du tout quelque chose qui coule de source pour un simple électronicien comme votre serviteur. Cette dépêche a donc été un peu différée le temps d’avaler les 100 premières pages de tutoriel permettant de faire un simple « hello world » en Haskell. Puis d’avaler le tutoriel CλaSH pour enfin faire clignoter cette fameuse LED sur un kit FPGA low cost de base.

Nous allons donc tenter un petit CλaSHtest permettant de faire clignoter une led.

Le type de base manipulé en CλaSH est le Signal. CλaSH ne permettant que des descriptions synchrones, un Signal représente une liste infinie de valeurs synchronisée sur une horloge et un reset pour la valeur initiale. L’horloge et le reset du signal sont décrit par le Domaine dom du signal. La forme d’un Signal est donc la suivante :

 Signal (dom :: Domain) a

En logique synchrone, le composant de base est le registre, nommé register:

register
     ( HiddenClockResetEnable dom dom
     , NFDataX a )
  => a
  -> Signal dom a
  -> Signal dom a

Son fonctionnement est assez basique : La valeur initiale (de reset) est donnée en premier argument, le second argument est le signal d’entrée qui est recopié sur le signal de sortie (valeur de retour de la fonction). On peut utiliser CλaSH en ligne de commande avec la commande clash.clashi.

$ clash.clashi
Clashi, version 1.0.1 (using clash-lib, version 1.0.1):
http://www.clash-lang.org/  :? for help
Clash.Prelude> 

Mais on préférera rapidement enregistrer notre module dans un fichiers source au format *.hs.

Pour faire clignoter une LED dans un FPGA, la première chose à mettre en place est un compteur :

Clash.Prelude> counter = register 0 counter + 1

Cette fonction counter que nous venons de créer retourne un registre initialisé à 0 et qui ajoute 1 à sa sortie à chaque coup d’horloge. La définition est récursive, c’est à dire qu’elle ajoute 1 à elle même. Et comme on commence à 0 et qu’on ajoute 1, la valeur initiale sera 1.
Cette fonction étant infinie, si nous voulons connaitre des valeurs il faut échantillonner :

Clash.Prelude> sampleN @System 10 counter
[1,1,2,3,4,5,6,7,8,9]

On constate que la première valeur est en double car c’est la valeur initiale, avant le premier coup d’horloge. @System désigne le domaine d’horloge/reset/enable utilisé. @System est le domaine général, nous pourrions appliquer un domaine (dom) spécifique à une architecture comme @XilinxSystem ou @IntelSystem.

Le problème avec ce compteur c’est qu’il incrémente à l’infini, nous on voudrait un compteur avec une limite et qui se remet à 0 à une certaine valeur comme ça :

Clash.Prelude> counter value = if(value < 100) then value + 1 else 0
Clash.Prelude> counter 10
11
Clash.Prelude> counter 0
1
Clash.Prelude> counter 101
0

[notes]
Je ne sais pas si je vais réussir à comprendre l’exemple de led qui clignote donné sur le blog de l’auteur de clash en fait.

Je suis parti un peu la fleur au fusil pour faire cette dépêche, mais je me rend compte que l’apprentissage de Clash (et surtout Haskell) est un loooong chemin.

Bref, je crois que je ne vais pas réussir à finir cette dépêche. Peut-être un jour aurais-je suffisamment de bouteille en Haskell pour comprendre Clash, mais là …

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 😉

Traitement numérique du signal, prise de notes

On dit souvent que pour bien apprendre un sujet en informatique il faut écrire une doc. Pour des besoins pro j’ai du me re-mettre au traitement numérique du signal. Je commence en général par un bouquin et un projet. Pour le projet comme c’est du pro je c’est à ma discrétion, par contre pour le bouquin je me suis plongé dans le livre de Richard G.Lyons «Understanding digital signal processing» qui a le mérite d’être richement illustré de graphes et d’équations avec beaucoup d’explications visuelles et «avec les mains».

Du bon soleil pour apprendre

L’idée de cette note est donc de faire des exercices en rapport avec ce qui est dans ce livre mais pas que, le tout de manière pratique en python et de voir les implications que ça peut avoir avec les FPGA.

Un signal discret

Dans un premier temps nous aurons besoin de numpy et pylab en python3

import numpy as np
import pylab as plt

Le signal de base est une sinusoïde. Pour représenter un signal de 1 Hertz en python on va d’abord créer un tableau d’un certain nombre de valeur de 0 à 1 secondes :

# Freq
f0 = 1
# 40 points de 0 à 39
t = np.linspace(0, 1, 40)

Puis calculer le sinus

y = np.sin(2*math.pi*f0*t)

Signal qu’il est facile de «plotter» ensuite :

plt.plot(t, y)
plt.show()

Ce qui nous donne cette belle courbe de sinus :

D’après Richard il ne faut pas relier les points

Mais pour bien se représenter un signal numérique il ne faut pas relier les points. Il vaut mieux mettre des points avec des lignes verticales comme ceci :

fix, ax = plt.subplots()
ax.stem(t, y, 'b', markerfmt="b.")
plt.show()

Ce qui nous donne la figure suivante :

Les points non reliés entre eux donnent une meilleur vision d’un signal numérique (discret)

Cette dernière figure illustre bien la notion d’échantillonnage avec une fréquence d’échantillonnage fs de 40Hertz (temps en secondes et 40 points) soit :

# Freq
f0 = 1
# Temps total
T = 1
# Nombre de points:
N = 40
# Fréquence d’échantillonnage :
print(f"fs = {N/T} Hertz")
# fs = 40.0 Hertz

Ici, la fréquence d’échantillonnage (40Hertz) est largement supérieur à la fréquence du signal enregistré (1 Hertz). On peut s’amuser maintenant à monter la fréquence du signal à la fréquence de Nyquist :

f0 = 5
f0 = 10
f0 = 20

Ce que nous dit Nyquist, c’est qu’avec tous les signaux ci-dessus, il est possible de retrouver la sinusoïde du début. Mais si on augmente encore la fréquence on obtient un repliement du spectre.

f0 = 30
f0 = 40

On peut ajouter le l’analyse de spectre en augmentant également le nombre de points mesuré :

# Freq
f0 = 1
# Temps total
T = 1
# Nombre de points:
N = 100
# 100 points de 0 à 99
t = np.linspace(0, T, N)
y = np.sin(2*np.pi*f0*t)
fix, ax = plt.subplots(1,2)
ax[0].stem(t, y, 'b', markerfmt="b.")
ax[1].magnitude_spectrum(y, Fs=N/T, ds="steps-mid")
plt.show()
(f0=1) À gauche le signal sur 100 points, à droite l’analyse des fréquences

Ondelettes

Pour faire une ondelette (wavelet) on multiplie un cosinus (périodique) avec une gaussienne (exp(-t²/2)) :

# Décalage en seconde:
retard = 5
y = np.cos(2*np.pi*f0*t)*np.exp(-np.power(t-retard,2)/2)
La fréquence du signal est toujours de 1Hz mais le signal n’est plus périodique à l’infini

La vidéo suivante explique tout ce que vous avez toujours voulu savoir sur les ondelettes.

Si on change la fréquence du signal, en passant à 2Hz par exemple. On se rend compte que l’échantillonnage tronque les maximum locaux :

La courbe n’est plus symétrique

Ce qui casse la symétrie de la courbe.

Hilbert avec scipy

La transformée de hilbert permet de calculer la partie imaginaire du signal réel. Le package python nommé scipy inclue la fonction qui la calcule.

[...]
from scipy import signal
[...]
# morlet wavelet
y = np.cos(2*np.pi*f0*t)*np.exp(-np.power(t-retard,2)/2)

himg = signal.hilbert(y).imag
hreal = signal.hilbert(y).real
[...]
On distingue bien la partie réel (le signal d’origine) et la partie imaginaire en orange

Comme nous avons la partie réelle et la partie imaginaire de notre signal, il est possible désormais de calculer son module pour en tirer l’enveloppe:

# morlet wavelet
y = np.cos(2*np.pi*f0*t)*np.exp(-np.power(t-retard,2)/2)

himg = signal.hilbert(y).imag
hreal = signal.hilbert(y).real

habs = np.sqrt(np.power(himg, 2) + np.power(hreal, 2))
L’enveloppe du signal en vert (habs)

Si l’on diminue la fréquence d’échantillonnage (division par 10) on remarque que l’enveloppe ne passe plus par les maximums. La transformée de Hilbert semble les avoirs tout de même déduit :

Avec une fréquence de pulsation de 2 Hertz et une division de la fréquence échantillonnage par 10 on constate que l’enveloppe demeure alors que les maximums réel et imaginaire sont tronqués.

Peut-on calculer l’enveloppe sans racine carré et carré ?

habs = np.abs(himg) + np.abs(hreal)
Hmmm ça marche moins bien sans carré (courbe rouge)

Et juste avec des carrés ?

hsquare= np.power(himg, 2) + np.power(hreal, 2)
Hmm c’est pas magique non plus

Transformée de Fourrier

Mais que faites vous encore sur ce blog ! Vite allez visionner l’excellente vidéo de 3Blue1Brown qui parle de la transformée de Fourrier avec force de graphes et de dessins. Vous ne verrez plus la transformée de fourrier comme avant 😉

Sinon y a aussi cette formule trouvée sur twitter qui est vraiment très parlante :

Trouvée sur twitter

Jusqu’à présent, pour calculer et afficher la transformée de fourrier de notre signal, nous nous sommes servi exclusivement de la fonction magnitude_spectrum() inclue dans pylab. C’est intéressant pour avoir un aperçu du spectre, mais ça ne permet pas de dire que l’on maîtrise ça.

Nombre complexes en python

Python permet visiblement d’utiliser nativement des nombres complexes avec ‘j’ à condition d’y mettre un nombre devant :

In [2]: j                                                                                                                                                                                                          
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-2-3eedd8854d1e> in <module>
----> 1 j

NameError: name 'j' is not defined

In [3]: 1j                                                                                                                                                                                                         
Out[3]: 1j

In [4]: 0.02j                                                                                                                                                                                                      
Out[4]: 0.02j

In [5]: -3j                                                                                                                                                                                                        
Out[5]: (-0-3j)

In [7]: np.exp(1j)                                                                                                                                                                                                 
Out[7]: (0.5403023058681398+0.8414709848078965j)

Tentons donc de calculer la transformée de fourrier en mode «brute de force» pour voir:


#temps: 0 points de 0 à N-1
t = np.linspace(0, T, N)
# morlet wavelet
y = np.cos(2*np.pi*f0*t)*np.exp(-np.power(t-retard,2)/2)

# transformée de fourrier
freqs = np.array([])
for k in range(N):
    listexp = [y[n]*np.exp(1j*2*np.pi*k*n/N) for n in range(N)]
    xk = (1/N)*np.array(listexp).sum()
    freqs = np.append(freqs, xk)

#fréquence: 0 points de 0 à N-1
k = np.linspace(0, T, N)
Visiblement nous avons un problème «complexe»

Là ce que nous venons de calculer est la version complexe de la transformée de fourrier dont pylab nous «plot» la partie réelle. Voyons voir le module :

fourrier_module = np.sqrt(np.power(freqs.imag, 2) + np.power(freqs.real, 2))
Mais pourquoi ce deuxième pic ?

Nous avons donc toujours deux pics, sachant que le second pic est au delà de la fréquence de Nyquist (Fs=10Hz) et semble «normal».

Par contre nous avons un facteur 2 entre le calcul de magnitude de python et celui que l’on vient de calculer.

Peut-être parce que la formule de l’image est celle de la transformée inverse ? La transformée discrète donnée dans le livre est plutôt celle là :

Page 60: On retrouve encore moins le facteur 2 sur cette formule.

Voyons voir avec cette nouvelle formule :

# transformée de fourrier
freqs = np.array([])
for k in range(N):
    listexp = [y[n]*np.exp(-1j*2*np.pi*k*n/N) for n in range(N)]
    xk = np.array(listexp).sum()
    freqs = np.append(freqs, xk)

fourrier_module = np.sqrt(np.power(freqs.imag, 2) + np.power(freqs.real, 2))
Bon c’est pas un facteur 2 cette fois!

Si on veut «matcher» la courbe de magnitude il faut ajouter un facteur 2/N au calcul du module :

fourrier_module = (2/N)*np.sqrt(np.power(freqs.imag, 2) + np.power(freqs.real, 2))
Avec le facteur (2/N) on obtient une superposition des courbes.

cos – sin

Pour faire entrer le calcul de la transformée dans un FPGA, l’exponentielle d’un complexe n’est pas super pratique. Décomposons donc en différence cos-sin avec la formule d’Euler, on devrait obtenir le même résultat:

# transformée de fourrier
freqs = np.array([])
for k in range(N):
    listexp = []
    for n in range(N):
        angle = 2*np.pi*k*n/N
        listexp.append(y[n]*(np.cos(angle) - 1j*np.sin(angle)))
    xk = np.array(listexp).sum()
    freqs = np.append(freqs, xk)

Et nous obtenons exactement le même graphe qu’avant.

C’est sans surprise qu’on obtient la même chose en sommant indépendamment partie réelle et partie imaginaire :

# transformée de fourrier
freqs_real = np.array([])
freqs_img = np.array([])
for k in range(N):
    listreal = []
    listimg = []
    for n in range(N):
        angle = 2*np.pi*k*n/N
        listreal.append(y[n]*(np.cos(angle)))
        listimg.append(y[n]*(-np.sin(angle)))
    xkreal = np.array(listreal).sum()
    xkimg = np.array(listimg).sum()
    freqs_real = np.append(freqs_real, xkreal)
    freqs_img = np.append(freqs_img, xkimg)

fourrier_module = (2/N)*np.sqrt(np.power(freqs_img, 2) + np.power(freqs_real, 2))

Nous permettant au passage de dégager le ‘j’ des nombres complexes qui ne passe pas très bien dans un FPGA.

Des entiers ou des virgules fixes

Pour le moment c’était facile: on avait les flottant de python. Seulement voilà, dans un FPGA, les flottants ne sont pas simple. Nous avons besoin de fixer la taille (en bits) des variables/registres utilisés. Il faut également fixer la position de la virgule si l’on souhaite simplifier le calcul.

Le second problème nous vient des fonctions sin() et cos() qui ne sont pas calculables simplement. L’astuce consiste à pré-calculer les valeurs et les stocker dans une table qui ira remplir une ROM du FPGA.

Pour gérer des entiers en virgule fixe et de taille hétérogène on installera le module fxpmath :

$ git clone https://github.com/francof2a/fxpmath.git
$ cd fxpmath/
$ python -m pip install -e .

Pour commencer on va passer le signal ‘y’ en entier signé sur 16bits avec

# morlet wavelet
y = np.cos(2*np.pi*f0*t)*np.exp(-np.power(t-retard,2)/2)

YTYPE="S1.15"
ysint = Fxp(y, dtype=YTYPE)

Le signal se trouvant entre -1 et 1 nous choisirons un format signé sur 16 bits avec tous les chiffres derrière la virgule ‘S1.15’.

Pour les calculs intermédiaires on va rester sur du signé 16 bits mais avec la virgule au milieu cette fois, soit ‘S8.8’:

# transformée de fourrier
freqs_real = np.array([])
freqs_img = np.array([])
for k in range(N):
    listreal = []
    listimg = []
    for n in range(N):
        angle = Fxp(2*fixpi*Fxp(k, dtype=DTYPE)*Fxp(n, dtype=DTYPE)/N,
                dtype=DTYPE)
        listreal.append(Fxp(y[n]*( np.cos(angle)), dtype=DTYPE))
        listimg.append (Fxp(y[n]*(-np.sin(angle)), dtype=DTYPE))
    xkreal = Fxp(np.array(listreal).sum(), dtype=DTYPE)
    xkimg  = Fxp(np.array(listimg ).sum(), dtype=DTYPE)
    print(f"Freq {k} -> {xkreal}({xkreal.dtype}) + {xkimg}j ({xkimg.dtype})")
    freqs_real = np.append(freqs_real, xkreal)
    freqs_img = np.append(freqs_img, xkimg)

#fourrier_module = (2/N)*np.sqrt(np.power(freqs_img, 2) + np.power(freqs_real, 2))
fourrier_power = Fxp(Fxp(freqs_img*freqs_img, dtype=DTYPE) + Fxp(freqs_real*freqs_real, dtype=DTYPE), dtype=DTYPE)
fourrier_module = Fxp((2/N)*Fxp(np.sqrt(fourrier_power), dtype=DTYPE), dtype=DTYPE)

La première surprise de cette méthode est le temps de calcul: on passe d’un calcul de la transformée quasi instantanée à un calcul qui prend presque une minute.

La seconde surprise vient avec le «bruit haute fréquence» qui apparaît dans le résultat et le second pic qui disparaît.

Avec les calculs intermédiaire en S8.8 on parvient à la même courbe, modulo le bruit en haute fréquences.

Le problème de ce bruit vient de l’arrondi calculé sur Pi, si on ajuste la virgule de Pi comme ceci :

# Frequence du signal
Sf0 = 2

f0 = (Sf0 * Fs)/T

# Décalage en seconde:
retard = 5

#temps: 0 points de 0 à N-1
t = np.linspace(0, T, N)
# morlet wavelet
y = np.cos(2*np.pi*f0*t)*np.exp(-np.power(t-retard,2)/2)

YTYPE="S1.15"
ysint = Fxp(y, dtype=YTYPE)

DTYPE="S8.8"
D2TYPE="S16.16"

fixpi = Fxp(np.pi, dtype="U3.13")

# transformée de fourrier
freqs_real = np.array([])
freqs_img = np.array([])
for k in range(N):
    listreal = []
    listimg = []
    for n in range(N):
        angle = Fxp(2*fixpi*Fxp(k*n, dtype="U16.0")/N, dtype=D2TYPE)
        listreal.append(Fxp(y[n], dtype=YTYPE)*Fxp( np.cos(angle), dtype=YTYPE))
        listimg.append (Fxp(y[n], dtype=YTYPE)*Fxp(-np.sin(angle), dtype=YTYPE))
    xkreal = Fxp(np.array(listreal).sum(), dtype=DTYPE)
    xkimg  = Fxp(np.array(listimg ).sum(), dtype=DTYPE)
    print(f"Freq {k}/{Fs} ({k*T/N}) -> {np.sqrt(xkreal*xkreal + xkimg*xkimg)})")
    freqs_real = np.append(freqs_real, xkreal)
    freqs_img = np.append(freqs_img, xkimg)

fourrier_power = Fxp(Fxp(freqs_img*freqs_img, dtype=DTYPE) + Fxp(freqs_real*freqs_real, dtype=DTYPE), dtype=DTYPE)
fourrier_module = Fxp((2/N)*Fxp(np.sqrt(fourrier_power), dtype=DTYPE), dtype=DTYPE)

Avec 128 échantillons (2^7)

Par contre on a un décalage de fréquence avec la fonction magnitude_spectrum() de pylab :

Mais d’où vient ce décalage ?

Ce décalage provient de l’axe des x qui n’est pas le même pour le calcul de python et le calcul maison. En effet, notre calcul «à la main» s’étend sur tout l’espace «de nyquist» (0 à N-1) alors que la fonction magnitude_spectrum() n’affiche le spectre que sur la moitée.

Pour recentrer tout ça on peut simplement récupérer la table des fréquences fournie par magnitude_spectrum() et l’utiliser comme axe des x dans l’affichage de notre spectre :

#...
magnitude, freqs, _ = ax[1].magnitude_spectrum(y, Fs=N/T, ds="steps-mid", label=f"fft (Fs={Fs} Hz)")
#...
ax[1].plot(freqs, fourrier_module[:len(freqs)], label = "freqs (calculée)")
La fréquence est maintenant correcte, reste un problème de magnitude. Problème d’arrondi ?

Et nous obtenons la bonne fréquence pour les deux modes de calculs. Reste maintenant un problème de magnitude maximum, est-ce un problème d’arrondi de la virgule fixe ? Possible.

Ressources

Le code de cet article se trouve sur le dépôt github suivant.

RapidSilicon va-t-elle renverser la table ?

Le FLF a été fondé a une époque obscure où il n’existait (quasiment) pas de solution open-source pour travailler sur des FPGA. Seul un petit village d’irréductibles simulateurs comme icarus, ghdl ou verilator maintenaient un semblant de liberté dans cet océan de sombre verrouillage. Il y avait bien VTR et Alliance, mais ils restaient très universitaire et un peu trop centré sur les ASIC.

Puis est arrivé Yosys le logiciel libre de synthèse Verilog accompagné du reverse engineering de l’ICE40 avec Icestorm. Ont suivis Apicula pour les Gowin, X-Ray pour les Xilinx série 7 ou Trellis pour les ECP5 (Voir le tableau des projets de reverse sur le FLF).

Et pour boucler la boucle de développement, le logiciel libre de placement routage nextpnr a émergé ainsi que le logiciel de configuration universelle openFPGALoader.

Toutes ces initiatives ont énormément gagnées en qualité et en crédibilité depuis 3 ou 4 ans et sont en passe de devenir les références dans le monde du FPGA.

À ces nouvelles très réjouissantes se sont ajouté les annonces de plusieurs mise en production de FPGA avec les outils open-source supportés officiellement par les constructeurs. On pense notamment à :

  • Quicklogic et son microcontrôleur EOS S3 basé sur un cœur Cortex-M4 couplé à une «zone» de FPGA utilisable intégralement avec des logiciels open source.
  • CLEAR: un projet plus anecdotique consistant à générer un FPGA à partir de l’outil open source openFpga et à l’intégrer à la Caravel de la société eFabless pour la graver en 130nm. Pour le coup les outils pour développer le FPGA sont opensource ainsi que les outils pour développer sur le fpga.
  • CologneChip et son GateMate: un FPGA à l’architecture assez original notamment pour la partie calcul/dsp mais qui se défend assez bien niveau taille.

Avec toutes ces nouvelles on aurait pensé que cela allait se calmer et que l’on aurait le temps de digérer et faire clignoter quelques LED avant de passer à une vitesse encore supérieure.

ERREUR !

C’était sans compter sur cette société créé en 2020 (et oui toute neuve) et son annonce de sortie du projet Gemini (un poil prétentieux tout de même pour le nom 😉 de faire un SoC haute performances en se basant sur des outils open sources, et oui même pour la fabrication du FPGA. En parallèle du développement silicium, RapidSilicon développe un IDE open source nommé Raptor se basant sur tout un tas de logiciels libres cité au début de cet article :

Un premier échantillonnage de composants en 16nm est déjà sorti des fonderies de TSMC et nous rassure sur la réalité du produit. Nous ne sommes pas dans le cas d’une société qui fait des annonces «vaporware» juste pour faire des levées de fond.

Hâte de voir la datasheet

L’architecture du prototype est donnée dans la figure suivante:

On est donc dans le cas d’un gros SoC-FPGA avec les caractéristiques suivantes :

  • Arm A53 Dual-Core
  • contrôleur de DDR4
  • «Petit» core Risc-V 32bit pour le temps réel et la supervision
  • Les ports habituels d’un SoC : UART, USB, SPI, I²C, …
  • Du PCIe Gen4, XGMII (pour le réseau)
  • Des liens serdes (très) rapide jusqu’à 16Gbits/s
  • La zone FPGA n’est pas énorme, mais se défend face aux petit Zynq par exemple :
    • 50-75K LUT (6 entrées)
    • Blocks DSP (combien ?)
    • Block de Ram double port 18kb (Combien ?)

Toutes ces parties sont liée ensemble au moyen d’un bloc d’interconnexion nommé FlexNOC.

Alors ?

Xilinx doit-elle trembler avec son Zynq ? et Microsemi avec son PolarFire SoC ?

L’avenir nous le dira, hâte de voir la suite 🙂

Découverte du FPGA européen, le GateMate de CologneChip

Nous l’attendions depuis au moins deux ans, le FPGA européen GateMate des allemands de CologneChip est désormais disponible dans votre crémerie habituelle.

Ça y est il est arrivé, le kit de développement GateMate !

La dimension européenne de ce FPGA n’est pas la seule nouveauté, c’est également un des premier (mais pas le premier) à privilégier les outils open source pour son utilisation. Que cela soit pour la simulation ou pour la synthèse tous les exemples donnés dans la documentation utilisent des logiciels libres (Icarus, GHDL et surtout Yosys). Même pour la visualisation des chronogrammes, gtkwave est utilisé par défaut en exemple.

[À noter que le kit de développement m’a été offert gracieusement par CologneChip]

Caractéristiques du GateMate

Les caractéristiques du GateMate le positionne au niveau d’un petit Spartan7 de Xilinx ou d’un Trion T20 de Efinix.

Le composant est gravé par GlobalFounderies en 28nm.

Architecture générale (DS1001)

La cellule de base est nommée CPE pour «Central Programming Element».

Structure du CPE (DS1001)

On notera l’absence remarqué de blocs multiplieurs. Cette absence est compensée par le «fast signal path routing» qui permet de chaîner les CPE afin de construire un multiplieur de dimension voulue.

Caractéristiques du kit

Pour le moment, seuls les trois petit FPGA de la gamme GateMate semblent être en production. CologneChip propose un kit de développement muni du plus petit GateMate : le CCGM1A1

Le schéma blocs de la carte de développement (source pdf officiel)

La carte de développement possède deux entrées USB:

  • une pour l’alimentation
  • et une pour la programmation et la communication avec le FPGA (FTDI 2232), mais qui peut également servir d’alimentation.

La magie du logiciel libre openFPGALoader permet de détecter le FPGA directement au branchement:

$ openFPGALoader --detect
Jtag frequency : requested 6.00MHz   -> real 6.00MHz  
index 0:
	idcode 0x20000001
	manufacturer colognechip
	family GateMate Series
	model  GM1Ax
	irlength 6

En effet, avant même la sortie du gatemate, CologneChip avait déjà proposé le support du composant sur le dépot openFPGALoader.

Toolchain

CologneChip fourni un guide d’installation de la chaîne de développement sur son site internet.

Hormis le logiciel de placement routage (GateMate), tous les outils sont des logiciels libre bien connus du monde du FPGA (source UG1002).

Tous les outils sont connus du monde du FPGA opensource et leurs installations sont intensivement décrites dans les différents dépôts des projets.

Si l’on souhaite éviter la case compilation, l’entreprise fournie même des versions binaires. Ces binaires ne sont téléchargeable que via un compte enregistré sur leur site pour le moment. Deux paquets de logiciels sont nécessaires :

  • Yosys compilé pour le gatemate: pour la synthèse Verilog
  • p_r: le logiciel de placement routage.

Pour le moment, la version binaire proposée en téléchargement sur le site n’est disponible que pour windows. Ces «.exe» s’exécutent cependant parfaitement sous Linux au moyen de l’émulateur wine bien connu des Linuxiens.

L’archive téléchargée se décompresse simplement avec unzip :

$ unzip cc-toolchain-win.zip 
Archive:  cc-toolchain-win.zip
 extracting: cc-toolchain-win/openFPGALoader-mingw64-v.0.8.0+80eeaef.zip  
 extracting: cc-toolchain-win/p_r-2022.04-001.zip  
  inflating: cc-toolchain-win/ug1002-toolchain-install-2022-04.pdf  
 extracting: cc-toolchain-win/yosys-win32-mxebin-0.15+57.zip  

La version windows de openFPGALoader ne fonctionne pas bien en émulation wine, il est préférable d’en compiler une version à jour à partir des sources officiels.

Pour le reste, on peut s’affranchir de compiler yosys en utilisant celle fournie. Et pour p_r, les sources n’étant pas fournies pour le moment, cette version est la seule que nous pourrons utiliser.

Pour les installer, il suffit de les décompresser :

$ cd cc-toolchain-win/
$ unzip p_r-2022.04-001.zip
$ unzip yosys-win32-mxebin-0.15+57.zip

La notice d’utilisation des commandes est données dans le pdf de l’archive nommé ug1002-toolchain-install-2022-04.pdf.

C’est l’installation la plus simple que j’ai pu avoir à faire pour des outils de développement FPGA. La place occupée sur le disque dur de son ordinateur est plusieurs milliers de fois plus petites que les logiciels habituels :

$ cd p_r-2022.04-001/
$ du -sh .
24M	.
$ cd yosys-win32-mxebin-0.15+57/
$ du -sh .
32M	.

Évidemment, c’est pour seulement un modèle de FPGA, mais cela reste beaucoup plus petit.

Clignotons

Il est temps de rentrer dans le vif du sujet et de faire clignoter les LED. L’exemple donné dans le document ug1002 est trop rapide pour voir les LED clignoter. Nous allons donc faire un clignoteur plus traditionnel comme visible ci-dessous en Verilog :

`timescale 1ns / 1ps

module blink(
		input wire clk,
		input wire rst,
		output reg led
	);

	localparam MAX_COUNT = 10_000_000;
	localparam CNT_TOP = $clog2(MAX_COUNT);

	wire i_clk;
	reg [CNT_TOP-1:0] counter;

	assign i_clk = clk;

	always @(posedge i_clk)
	begin
		if (!rst) begin
			led <= 0;
			counter <= 0;
		end else begin
			if(counter < MAX_COUNT/2)
				led <= 1;
			else
				led <= 0;

			if (counter >= MAX_COUNT)
				counter <= 0;
			else
				counter <= counter + 1'b1;
		end
	end

endmodule

Contrairement à beaucoup de FPGA, le GateMate ne définit pas d’états initial à 0 de ses registres. Une entrée reset est donc nécessaire.

Le pinout est décrit au moyen d’un fichier «ccf» :

## blink.ccf

Pin_in   "clk"  Loc = "IO_SB_A8" | SCHMITT_TRIGGER=true;
Pin_in   "rst"  Loc = "IO_EB_B0"; # SW3
Pin_out  "led"  Loc = "IO_EB_B1"; # D1

Une fois que ces deux fichiers sont prêt il suffit de lancer yosys pour la synthèse :

$ wine ../../cc-toolchain-win/yosys-win32-mxebin-0.15+57/yosys.exe -l yosys.log -p 'read_verilog blink.v; synth_gatemate -top blink -vlog blink_synth.v'

Wine génère tout un tas d’erreurs mais fini par nous lancer la synthèse tout de même

$ wine ../../cc-toolchain-win/yosys-win32-mxebin-0.15+57/yosys.exe -l yosys.log -p 'read_verilog blink.v; synth_gatemate -top blink -vlog blink_synth.v'
wine: created the configuration directory '/home/oem/.wine'
0012:err:ole:marshal_object couldn't get IPSFactory buffer for interface {00000131-0000-0000-c000-000000000046}
0012:err:ole:marshal_object couldn't get IPSFactory buffer for interface {6d5140c1-7436-11ce-8034-00aa006009fa}
0012:err:ole:StdMarshalImpl_MarshalInterface Failed to create ifstub, hres=0x80004002
0012:err:ole:CoMarshalInterface Failed to marshal the interface {6d5140c1-7436-11ce-8034-00aa006009fa}, 80004002
0012:err:ole:get_local_server_stream Failed: 80004002
0014:err:ole:marshal_object couldn't get IPSFactory buffer for interface {00000131-0000-0000-c000-000000000046}
0014:err:ole:marshal_object couldn't get IPSFactory buffer for interface {6d5140c1-7436-11ce-8034-00aa006009fa}
0014:err:ole:StdMarshalImpl_MarshalInterface Failed to create ifstub, hres=0x80004002
0014:err:ole:CoMarshalInterface Failed to marshal the interface {6d5140c1-7436-11ce-8034-00aa006009fa}, 80004002
0014:err:ole:get_local_server_stream Failed: 80004002
Could not find Wine Gecko. HTML rendering will be disabled.
Could not find Wine Gecko. HTML rendering will be disabled.
wine: configuration in L"/home/oem/.wine" has been updated.

On ne va pas recopier toute la trace de synthèse ici mais juste la partie ressources utilisées donnée en fin de synthèse :

2.49. Printing statistics.

=== blink ===

   Number of wires:                 49
   Number of wire bits:            294
   Number of public wires:           5
   Number of public wire bits:      28
   Number of memories:               0
   Number of memory bits:            0
   Number of processes:              0
   Number of cells:                159
     CC_ADDF                        65
     CC_BUFG                         1
     CC_DFF                         25
     CC_IBUF                         2
     CC_LUT1                        24
     CC_LUT2                         5
     CC_LUT4                        36
     CC_OBUF                         1

2.50. Executing CHECK pass (checking for obvious problems).
Checking module blink...
Found and reported 0 problems.

2.51. Executing OPT_CLEAN pass (remove unused cells and wires).
Finding unused cells or wires in module \blink..

2.52. Executing Verilog backend.

2.52.1. Executing BMUXMAP pass.

2.52.2. Executing DEMUXMAP pass.
Dumping module `\blink'.

End of script. Logfile hash: dcf7a4084e
Yosys 0.15+57 (git sha1 207417617, i686-w64-mingw32.static-g++ 11.2.0 -Os)
Time spent: 1% 19x opt_clean (0 sec), 1% 18x opt_expr (0 sec), ...

Le fichier de sortie blink_synth.v est au format … Verilog également! C’était bien la peine de lancer Yosys tient !

Mais non, Verilog est un excellent format pour décrire une netlist autant que du RTL. Et de fait, le code Verilog généré n’est pas hyper lisible. Il est constitué d’une série d’instanciations et de connections des primitives du FPGA :

//...
 
 CC_LUT2 #(
    .INIT(4'h8)
  ) _051_ (
    .I0(_041_[0]),
    .I1(_041_[1]),
    .O(_043_[2])
  );
  CC_LUT4 #(
    .INIT(16'h0001)
  ) _052_ (
    .I0(counter[15]),
    .I1(counter[20]),
    .I2(counter[23]),
    .I3(counter[13]),
    .O(_041_[0])
  );

//...

Code qui reste parfaitement simulable avec Icarus pour faire de la simulation post-synthèse comme expliqué dans la documentation officielle.

Maintenant que nous avons notre netlist passons aux choses sérieuses avec le placement routage :

$ wine ../../cc-toolchain-win/p_r-2022.04-001/p_r.exe -i blink_synth.v -o blink -lib ccag
GateMate (c) Place and Route
Version 4.0 (4 April 2022)
All Rights Reserved (c) Cologne Chip AG

...

Comme pour la synthèse, nous n’allons pas mettre tous les messages ici. Une des information qui nous intéresse en priorité pour le placement routage est la performance en vitesse.

...
Static Timing Analysis
Longest Path from Q of Component 25_1 to D-Input of Component 33/1 Delay: 18215 ps
Maximum Clock Frequency on CLK 230/3:   54.90 MHz
...

La vitesse d’horloge maximale est donc de 54.90Mhz. Cela peut sembler ridicule mais il faut prendre en compte que :

  • L’architecture du «clignoteur» avec un énorme compteur pour diviser l’horloge n’est absolument pas optimisée. Pour faire bien il faudrait pipeliner le compteur mais ça n’est pas le sujet ici. Ces mauvais résultats sont cohérent avec ce qu’on pourrait obtenir avec un autre FPGA «mainstream».
  • Ces performances sont «conservatrice» c’est le pire cas quand le FPGA est très chaud.

Bref, pour faire clignoter une LED, on peut raisonnablement doubler cette fréquence d’horloge si on veut 🙂 Mais comme nous n’utilisons pas de PLL ici, la fréquence d’entrée de 10Mhz rentre dans la specification.

Les statistiques d’utilisation du FPGA sont données à la fin de la synthèse :

CPE_USAGE_INPUT - CPE_COMBSEQ         1/8 :     0 /     21   (  0.0%)
CPE_USAGE_INPUT - CPE_COMBSEQ         2/8 :    11 /     21   ( 52.4%)
CPE_USAGE_INPUT - CPE_COMBSEQ         3/8 :     0 /     21   (  0.0%)
CPE_USAGE_INPUT - CPE_COMBSEQ         4/8 :     0 /     21   (  0.0%)
CPE_USAGE_INPUT - CPE_COMBSEQ         5/8 :     0 /     21   (  0.0%)
CPE_USAGE_INPUT - CPE_COMBSEQ         6/8 :    10 /     21   ( 47.6%)
CPE_USAGE_INPUT - CPE_COMBSEQ         7/8 :     0 /     21   (  0.0%)
CPE_USAGE_INPUT - CPE_COMBSEQ         8/8 :     0 /     21   (  0.0%)

CPE_USAGE_INPUT - CPE_COMB            1/8 :     3 /      3   ( 100.0%)
CPE_USAGE_INPUT - CPE_COMB            2/8 :     0 /      3   (  0.0%)
CPE_USAGE_INPUT - CPE_COMB            3/8 :     0 /      3   (  0.0%)
CPE_USAGE_INPUT - CPE_COMB            4/8 :     0 /      3   (  0.0%)
CPE_USAGE_INPUT - CPE_COMB            5/8 :     0 /      3   (  0.0%)
CPE_USAGE_INPUT - CPE_COMB            6/8 :     0 /      3   (  0.0%)
CPE_USAGE_INPUT - CPE_COMB            7/8 :     0 /      3   (  0.0%)
CPE_USAGE_INPUT - CPE_COMB            8/8 :     0 /      3   (  0.0%)

CPE_USAGE_INPUT - CPE COMBSEQ+COMB    1/8 :     3 /     24   ( 12.5%)
CPE_USAGE_INPUT - CPE COMBSEQ+COMB    2/8 :    11 /     24   ( 45.8%)
CPE_USAGE_INPUT - CPE COMBSEQ+COMB    3/8 :     0 /     24   (  0.0%)
CPE_USAGE_INPUT - CPE COMBSEQ+COMB    4/8 :     0 /     24   (  0.0%)
CPE_USAGE_INPUT - CPE COMBSEQ+COMB    5/8 :     0 /     24   (  0.0%)
CPE_USAGE_INPUT - CPE COMBSEQ+COMB    6/8 :    10 /     24   ( 41.7%)
CPE_USAGE_INPUT - CPE COMBSEQ+COMB    7/8 :     0 /     24   (  0.0%)
CPE_USAGE_INPUT - CPE COMBSEQ+COMB    8/8 :     0 /     24   (  0.0%)

IO_USAGE                         :      3 /   144    (  2.1%) of all IOs
IO_USAGE_TYPE - IBF              :      2 /   144    (  1.4%) of all IOs    ( 66.7%) of used IOs
IO_USAGE_TYPE - OBF              :      1 /   144    (  0.7%) of all IOs    ( 33.3%) of used IOs
IO_USAGE_TYPE - TOBF             :      0 /   144    (  0.0%) of all IOs    (  0.0%) of used IOs
IO_USAGE_TYPE - IOBF             :      0 /   144    (  0.0%) of all IOs    (  0.0%) of used IOs

CPE_USAGE_PHYS - CPE_COMB_ONLY   :     73 / 20480    (  0.4%) of all CPEs   ( 60.3%) of occupied CPEs
CPE_USAGE_PHYS - CPE_SEQ_ONLY    :      5 / 20480    (  0.0%) of all CPEs   (  4.1%) of occupied CPEs
CPE_USAGE_PHYS - CPE_BRIDGE_ONLY :      0 / 20480    (  0.0%) of all CPEs   (  0.0%) of occupied CPEs
CPE_USAGE_PHYS - CPE_CARRY_ONLY  :      0 / 20480    (  0.0%) of all CPEs   (  0.0%) of occupied CPEs
CPE_USAGE_PHYS - CPE_COMB+SEQ    :     10 / 20480    (  0.0%) of all CPEs   (  8.3%) of occupied CPEs
CPE_USAGE_PHYS - CPE_COMB+BRIDGE :      0 / 20480    (  0.0%) of all CPEs   (  0.0%) of occupied CPEs
CPE_USAGE_PHYS - CPE_COMBSEQ     :     21 / 20480    (  0.1%) of all CPEs   ( 17.4%) of occupied CPEs

CPE_USAGE_LOGIC - CPE_COMB       :      8 / 20480    (  0.0%) of all CPEs   (  6.6%) of occupied CPEs
CPE_USAGE_LOGIC - CPE_SEQ        :     15 / 20480    (  0.1%) of all CPEs   ( 12.4%) of occupied CPEs
CPE_USAGE_LOGIC - CPE_COMBSEQ    :     21 / 20480    (  0.1%) of all CPEs   ( 17.4%) of occupied CPEs
CPE_USAGE_LOGIC - CPE_BRIDGE     :      0 / 20480    (  0.0%) of all CPEs   (  0.0%) of occupied CPEs

CPE_USAGE_OVERALL                :    121 / 20480    (  0.6%) of all CPEs occupied
CPE_USAGE_LOGIC                  :     94 / 20480    (  0.5%) of all CPEs used for customer logic

Component Statistics:
         AND        32        19%
        ADDF         2         1%
       ADDF2        60        37%
        C_OR        48        29%
      Route1         4         2%
    CP_route        15         9%
              --------
Sum of COMB:       161

           D        25        92%
       C_0_1         2         7%
              --------
Sum of SEQ:         27

Sum of all:        188

Et le bitstream généré est au format *.cfg (ascii) ou *.cfg.bit (binaire).

$ ls -lha blink_00.*
-rw-rw-r-- 1 oem oem 121K May  1 08:38 blink_00.cdf
-rw-rw-r-- 1 oem oem 1.4M May  1 08:38 blink_00.cfg
-rw-rw-r-- 1 oem oem  48K May  1 08:38 blink_00.cfg.bit
-rw-rw-r-- 1 oem oem  465 May  1 08:38 blink_00.pin
-rw-rw-r-- 1 oem oem 6.0K May  1 08:38 blink_00.place
-rw-rw-r-- 1 oem oem  65K May  1 08:38 blink_00.SDF
-rw-rw-r-- 1 oem oem  42K May  1 08:38 blink_00.used
-rw-rw-r-- 1 oem oem  79K May  1 08:38 blink_00.V

Pour configurer le FPGA nous utiliserons openFPGALoader en «natif» :

$ openFPGALoader -b gatemate_evb_jtag blink_00.cfg.bit
Jtag frequency : requested 6.00MHz   -> real 6.00MHz  
Load SRAM via JTAG: [==================================================] 100.00%
Done
Wait for CFG_DONE DONE

Et la LED clignote :

Ressources

sv2chisel, le convertisseur (System)Verilog vers Chisel

Le monde du FPGA (et de l’ASIC) regorge aujourd’hui de langages de description matériel. Au Verilog et VHDL s’ajoute maintenant tout un tas de langages comme Migen, Clash, BlueSpec, Amaranth, Chisel, SpinalHDL, Silice, … et j’en oublie plein. Tous ces langages permettent de générer du Verilog. Les possibilité de conversion de VHDL vers Verilog ne sont maintenant plus une utopie grâce aux évolutions de GHDL et de Yosys.

Bref, il existe désormais toujours une possibilité de convertir l’intégralité du projet en Verilog de manière à le simuler et synthétiser avec les outils conçus pour lui.

C’est quelque chose de très appréciable pour faire de la réutilisation de code. Il n’est plus nécessaire de re-concevoir un composant en VHDL tout ça parce que le contrôleur open source qui nous est nécessaire est codé en Verilog.

L’homogénéité de langage des sources d’un projet peut cependant être appréciable dans certain cas. Notamment quand le langage de description possède ses propres librairies de simulation comme c’est le cas en Chisel.

On peut certes instancier les «sous»-modules Verilog au moyen de BlackBox, mais ils ne seront pas simulable avec ChiselTest par exemple car treadle se cassera les dents dessus.

C’est là qu’intervient le nouveau projet nommé sv2chisel pour convertir du (system)Verilog en chisel.

Je vous propose ici de tester ensemble l’utilitaire dans un cas pratique. Je souhaite convertir le module Verilog fake_differential du projet d’exemple de l’ulx3s qui permet de générer un signal HDMI différentiel pour l’intégrer dans le projet HdmiCore écrit en Chisel. L’objectif étant de porter le projet HdmiCore sur la plate-forme ulx3s en restant dans du pure Chisel.

Installation de l’outil

Toutes les caractéristiques et limitations du convertisseur sont données sur le wiki. Pour l’installer nous allons cloner le projet github. Le projet n’en est pas à sa version 1.0, il est sans doute préférable de «travailler» sur le main du git plutôt que sur une release.

$ git clone https://github.com/ovh/sv2chisel.git
$ cd sv2chisel

Les étapes d’installations depuis les sources sont données dans le readme. Il faut publier localement sv2chisel ainsi que les «helpers» :

$ sbt publishLocal

Cette commande fonctionne pas chez moi, je pensais naïvement que c’était la même commande que celle consistant à lancer sbt puis taper «publishLocal» mais non 😉

Donc pour publier localement, on fera plutôt comme recommandé dans le readme :

$ sbt
sbt:sv2chisel> publishLocal
sbt:sv2chisel> helpers/publishLocal
sbt:sv2chisel> assembly

La commande «assembly» génère le fichier jar exécutable.

Le binaire de l’utilitaire est généré dans le répertoire utils/bin/ et se nomme tout simplement sv2chisel

$ ./sv2chisel -help
sv2chisel [Options] sv_files... or sv2chisel [Options] -c config_file

Commons Options:
    -l, --log-level <error|warn|struct|info|debug|trace>
                                     Logger verbosity level
    -L, --class-log-level CLASS_NAME:<error|warn|struct|info|debug|trace>
                                     Logger verbosity level within given CLASS_NAME (useful for transforms)
    -o, --emission-path PATH         Base emission path

Config File (prio over manually specified files):
    -c, --config-file FILE           Yaml Configuration File

Manual command-line configuration
    -i, --base-path PATH             Base path for files
    -n, --name NAME                  Project name
    -h, --help                       Show this message

Si l’on regarde dans le fichier on trouve un simple lien vers l’archive «jar» se trouvant dans le même répertoire:

$ ls 
sv2chisel  sv2chisel.jar

$ cat sv2chisel
#!/bin/bash

path=`dirname "$0"`
cmd="java -cp ${path}/sv2chisel.jar sv2chisel.Main ${@:1}"

Conversion

Pour convertir en Chisel, on peut simplement donner les noms des fichiers sources en arguments:

$ ./sv2chisel fake_differential.v
[log] ---- Processing project ----
[log] ############# Parsing fake_differential.v #############
[log] ######### Elapsed time for fake_differential.v #########
[log] # Lexing+Parsing Time : 335.206505 ms
[log] # Mapping to IR Time : 126.985877 ms
[log] ######### Executing 18 transforms #########
[log] ####### sv2chisel.transforms.CheckUseBeforeDecl #######
[log] # Elapsed time : 26.214516 ms
[log] ####### sv2chisel.transforms.CheckScopes #######
[log] # Elapsed time : 7.222317 ms
[log] ####### sv2chisel.transforms.CheckBlockingAssignments #######
[log] # Elapsed time : 1.448437 ms
[log] ####### sv2chisel.transforms.InferDefLogicClocks #######
[info] Registering a new clock clk_shift for module fake_differential (non blocking assignment) at fake_differential.v:3:0>>54:0
[warn] Unable to find module module ODDRX1F instanciated as ddr_p_instance in current module fake_differential for clock inference processing. at fake_differential.v:33:10>>41:11
[warn] Unable to find module module ODDRX1F instanciated as ddr_n_instance in current module fake_differential for clock inference processing. at fake_differential.v:42:10>>50:11
[log] # Elapsed time : 41.125895 ms
[log] ####### sv2chisel.transforms.PropagateClocks #######
[warn] Module ODDRX1F referenced by instance ddr_p_instance cannot be found in current design. Clock & Reset management might be inaccurate. at fake_differential.v:33:10>>41:11
[warn] Module ODDRX1F referenced by instance ddr_n_instance cannot be found in current design. Clock & Reset management might be inaccurate. at fake_differential.v:42:10>>50:11
[log] # Elapsed time : 1.878423 ms
[log] ####### sv2chisel.transforms.FlowReferences #######
[info] Declaring actual port directions for module fake_differential at fake_differential.v:5:2->8
[info] Running FlowReference Transform another time on module fake_differential at fake_differential.v:3:0>>54:0
[log] # Elapsed time : 28.311422 ms
[log] ####### sv2chisel.transforms.InferUInts #######
[info] Converting in_clock to UInt based on its usage in the module at fake_differential.v:7:9->11
[info] Converting tmds[_] to UInt based on its usage in the module at fake_differential.v:11:10->12
[log] # Elapsed time : 30.181078 ms
[log] ####### sv2chisel.transforms.InferParamTypes #######
[log] # Elapsed time : 5.803376 ms
[log] ####### sv2chisel.transforms.TypeReferences #######
[critical] Unsupported Type 'Bool' for subindex expression 'out_n[i]' at fake_differential.v:47:15->22
[log] # Elapsed time : 18.082934 ms
[log] ####### sv2chisel.transforms.LegalizeExpressions #######
[warn] Unknown remote type for port #2 (Q) of instance ddr_p_instance of module ODDRX1F: casting by reference by default at fake_differential.v:38:12->23
[critical] Unsupported Type 'Bool' for subindex expression 'out_n[i]' at fake_differential.v:47:15->22
[warn] Unknown remote type for port #2 (Q) of instance ddr_n_instance of module ODDRX1F: casting by reference by default at fake_differential.v:47:12->23
[log] # Elapsed time : 29.292823 ms
[log] ####### sv2chisel.transforms.FixFunctionImplicitReturns #######
[log] # Elapsed time : 1.314473 ms
[log] ####### sv2chisel.transforms.NameInstancePorts #######
[log] # Elapsed time : 2.628666 ms
[log] ####### sv2chisel.transforms.RemovePatterns #######
[log] # Elapsed time : 4.862502 ms
[log] ####### sv2chisel.transforms.RemoveConcats #######
[log] # Elapsed time : 3.143862 ms
[log] ####### sv2chisel.transforms.AddDontCare #######
[log] # Elapsed time : 1.28653 ms
[log] ####### sv2chisel.transforms.LegalizeParamDefaults #######
[warn] Cannot find module ODDRX1F in current project at fake_differential.v:33:10>>41:11
[warn] Cannot find module ODDRX1F in current project at fake_differential.v:42:10>>50:11
[log] # Elapsed time : 4.072062 ms
[log] ####### sv2chisel.transforms.FixReservedNames #######
[log] # Elapsed time : 4.126536 ms
[log] ####### sv2chisel.transforms.ToCamelCase #######
[log] # Elapsed time : 0.830815 ms
[log] # Total Elapsed time running transforms : 216.675871 ms
[log] ######### EMISSION #########
[log] ######### CHISELIZING fake_differential.v #########
[info] At fake_differential.v:11: Emitting unpacked for node tmds
[info] At fake_differential.v:18: Emitting unpacked for node R_tmds_p
[info] At fake_differential.v:18: Emitting unpacked for node R_tmds_n
[log] # Elapsed time : 21.267262 ms
[log] ######### EMITTING to /fake_differential.scala #########
Exception in thread "main" java.io.FileNotFoundException: /fake_differential.scala (Permission denied)
at java.base/java.io.FileOutputStream.open0(Native Method)
at java.base/java.io.FileOutputStream.open(FileOutputStream.java:291)
at java.base/java.io.FileOutputStream.(FileOutputStream.java:234)
at java.base/java.io.FileOutputStream.(FileOutputStream.java:123)
at java.base/java.io.FileWriter.(FileWriter.java:66)
at sv2chisel.Emitter$.$anonfun$emitChisel$9(Emitter.scala:174)
at sv2chisel.Utils$.time(Utils.scala:185)
at sv2chisel.Emitter$.$anonfun$emitChisel$1(Emitter.scala:163)
at scala.collection.TraversableLike.$anonfun$map$1(TraversableLike.scala:286)
at scala.collection.mutable.ResizableArray.foreach(ResizableArray.scala:62)
at scala.collection.mutable.ResizableArray.foreach$(ResizableArray.scala:55)
at scala.collection.mutable.ArrayBuffer.foreach(ArrayBuffer.scala:49)
at scala.collection.TraversableLike.map(TraversableLike.scala:286)
at scala.collection.TraversableLike.map$(TraversableLike.scala:279)
at scala.collection.AbstractTraversable.map(Traversable.scala:108)
at sv2chisel.Emitter$.emitChisel(Emitter.scala:134)
at sv2chisel.Driver$.emitChisel(Driver.scala:66)
at sv2chisel.Main$.$anonfun$new$10(Main.scala:159)
at scala.collection.immutable.List.foreach(List.scala:431)
at sv2chisel.Main$.delayedEndpoint$sv2chisel$Main$1(Main.scala:157)
at sv2chisel.Main$delayedInit$body.apply(Main.scala:55)
at scala.Function0.apply$mcV$sp(Function0.scala:39)
at scala.Function0.apply$mcV$sp$(Function0.scala:39)
at scala.runtime.AbstractFunction0.apply$mcV$sp(AbstractFunction0.scala:17)
at scala.App.$anonfun$main$1$adapted(App.scala:80)
at scala.collection.immutable.List.foreach(List.scala:431)
at scala.App.main(App.scala:80)
at scala.App.main$(App.scala:78)
at sv2chisel.Main$.main(Main.scala:55)
at sv2chisel.Main.main(Main.scala)

Après de multiple messages plus ou moins critiques, la commande se termine sur une étonnante erreur java de fichier non trouvé. Visiblement il faut lui fournir le path complet en argument (sans doute un bug) :

 ./sv2chisel /opt/chiselmod/sv2chisel/utils/bin/fake_differential.v 
[log]  ---- Processing project  ---- 
[log] ############# Parsing /opt/chiselmod/sv2chisel/utils/bin/fake_differential.v #############
[log] ######### Elapsed time for /opt/chiselmod/sv2chisel/utils/bin/fake_differential.v #########
[log] # Lexing+Parsing Time : 337.962186 ms
[log] # Mapping to IR Time  : 123.022396 ms
[log] ######### Executing 18 transforms #########
[log]    ####### sv2chisel.transforms.CheckUseBeforeDecl #######
[log]    # Elapsed time : 24.704003 ms
[log]    ####### sv2chisel.transforms.CheckScopes #######
[log]    # Elapsed time : 6.588446 ms
[log]    ####### sv2chisel.transforms.CheckBlockingAssignments #######
[log]    # Elapsed time : 1.838544 ms
[log]    ####### sv2chisel.transforms.InferDefLogicClocks #######
[info] Registering a new clock `clk_shift` for module fake_differential (non blocking assignment) at /opt/chiselmod/sv2chisel/utils/bin/fake_differential.v:3:0>>54:0
[warn] Unable to find module module ODDRX1F instanciated as ddr_p_instance in current module fake_differential for clock inference processing. at /opt/chiselmod/sv2chisel/utils/bin/fake_differential.v:33:10>>41:11
[warn] Unable to find module module ODDRX1F instanciated as ddr_n_instance in current module fake_differential for clock inference processing. at /opt/chiselmod/sv2chisel/utils/bin/fake_differential.v:42:10>>50:11
[log]    # Elapsed time : 37.088031 ms
[log]    ####### sv2chisel.transforms.PropagateClocks #######
[warn] Module ODDRX1F referenced by instance ddr_p_instance cannot be found in current design. Clock & Reset management might be inaccurate. at /opt/chiselmod/sv2chisel/utils/bin/fake_differential.v:33:10>>41:11
[warn] Module ODDRX1F referenced by instance ddr_n_instance cannot be found in current design. Clock & Reset management might be inaccurate. at /opt/chiselmod/sv2chisel/utils/bin/fake_differential.v:42:10>>50:11
[log]    # Elapsed time : 1.73635 ms
[log]    ####### sv2chisel.transforms.FlowReferences #######
[info] Declaring actual port directions for module fake_differential at /opt/chiselmod/sv2chisel/utils/bin/fake_differential.v:5:2->8
[info] Running FlowReference Transform another time on module fake_differential at /opt/chiselmod/sv2chisel/utils/bin/fake_differential.v:3:0>>54:0
[log]    # Elapsed time : 25.221955 ms
[log]    ####### sv2chisel.transforms.InferUInts #######
[info] Converting in_clock to UInt based on its usage in the module at /opt/chiselmod/sv2chisel/utils/bin/fake_differential.v:7:9->11
[info] Converting tmds[_] to UInt based on its usage in the module at /opt/chiselmod/sv2chisel/utils/bin/fake_differential.v:11:10->12
[log]    # Elapsed time : 29.58874 ms
[log]    ####### sv2chisel.transforms.InferParamTypes #######
[log]    # Elapsed time : 5.646461 ms
[log]    ####### sv2chisel.transforms.TypeReferences #######
[critical] Unsupported Type 'Bool' for subindex expression 'out_n[i]' at /opt/chiselmod/sv2chisel/utils/bin/fake_differential.v:47:15->22
[log]    # Elapsed time : 18.257447 ms
[log]    ####### sv2chisel.transforms.LegalizeExpressions #######
[warn] Unknown remote type for port #2 (Q) of instance ddr_p_instance of module ODDRX1F: casting by reference by default at /opt/chiselmod/sv2chisel/utils/bin/fake_differential.v:38:12->23
[critical] Unsupported Type 'Bool' for subindex expression 'out_n[i]' at /opt/chiselmod/sv2chisel/utils/bin/fake_differential.v:47:15->22
[warn] Unknown remote type for port #2 (Q) of instance ddr_n_instance of module ODDRX1F: casting by reference by default at /opt/chiselmod/sv2chisel/utils/bin/fake_differential.v:47:12->23
[log]    # Elapsed time : 21.168114 ms
[log]    ####### sv2chisel.transforms.FixFunctionImplicitReturns #######
[log]    # Elapsed time : 0.631086 ms
[log]    ####### sv2chisel.transforms.NameInstancePorts #######
[log]    # Elapsed time : 1.467731 ms
[log]    ####### sv2chisel.transforms.RemovePatterns #######
[log]    # Elapsed time : 5.09245 ms
[log]    ####### sv2chisel.transforms.RemoveConcats #######
[log]    # Elapsed time : 2.749951 ms
[log]    ####### sv2chisel.transforms.AddDontCare #######
[log]    # Elapsed time : 1.251281 ms
[log]    ####### sv2chisel.transforms.LegalizeParamDefaults #######
[warn] Cannot find module ODDRX1F in current project at /opt/chiselmod/sv2chisel/utils/bin/fake_differential.v:33:10>>41:11
[warn] Cannot find module ODDRX1F in current project at /opt/chiselmod/sv2chisel/utils/bin/fake_differential.v:42:10>>50:11
[log]    # Elapsed time : 3.092851 ms
[log]    ####### sv2chisel.transforms.FixReservedNames #######
[log]    # Elapsed time : 4.132691 ms
[log]    ####### sv2chisel.transforms.ToCamelCase #######
[log]    # Elapsed time : 0.890969 ms
[log] # Total Elapsed time running transforms : 195.82181 ms
[log] ######### EMISSION #########
[log] ######### CHISELIZING /opt/chiselmod/sv2chisel/utils/bin/fake_differential.v #########
[info] At /opt/chiselmod/sv2chisel/utils/bin/fake_differential.v:11: Emitting unpacked for node tmds
[info] At /opt/chiselmod/sv2chisel/utils/bin/fake_differential.v:18: Emitting unpacked for node R_tmds_p
[info] At /opt/chiselmod/sv2chisel/utils/bin/fake_differential.v:18: Emitting unpacked for node R_tmds_n
[log] # Elapsed time : 25.374274 ms
[log] ######### EMITTING to //opt/chiselmod/sv2chisel/utils/bin/fake_differential.scala #########
[log] # Elapsed time : 17.429649 ms

Il y a sans doute beaucoup de chose a parametrer aux petits oignons pour bien utiliser l’outils, mais le résultat «en l’état» est déjà assez intéressant (commentaires FLF ajoutés):

package // FLF: à rajouter à la main ?
// FLF: on comprend pourquoi il voulait un path complet,
//      il le prend pour le nom du package
package .opt.chiselmod.sv2chisel.utils.bin

import chisel3._
// FLF: commentaires gardés, bien.
// DDR mode uses Lattice ECP5 vendor-specific module ODDRX1F

class fake_differential() extends Module { // used only in DDR mode
  // [1:0]:DDR [0]:SDR TMDS
  val in_clock = IO(Input(UInt(2.W)))
  val in_red = IO(Input(Bool()))
  val in_green = IO(Input(Bool()))
  val in_blue = IO(Input(Bool()))
  // [3]:clock [2]:red [1]:green [0]:blue 
  val out_p = IO(Output(Vec(4, Bool())))
  val out_n = IO(Output(Bool()))
// FLF: hmm, j'aurais pas traduit un tableau «wire» par une Mem() 
//      chisel, pas sûr que ça marche bien cette affaire.
  val tmds = Mem(4,UInt(2.W)) 
  tmds(3) := in_clock
  tmds(2) := in_red
  tmds(1) := in_green
  tmds(0) := in_blue

  // register stage to improve timing of the fake differential
  val R_tmds_p = Mem(4,Vec(2, Bool())) 
  val R_tmds_n = Mem(4,Vec(2, Bool())) 
  // genvar i;
  for(i <- 0 until 4){
    R_tmds_p(i) := tmds(i).asBools
    R_tmds_n(i) := ( ~tmds(i)).asBools
  }

  // output SDR/DDR to fake differential

  // FLF: les generate sont détecté et traduit également.
  // genvar i;
  for(i <- 0 until 4){
    // FLF: connexion des primitives sans broncher
    //      Il faudra tout de même inclure la définition
    //      de la blackbox à la main par la suite (import)
    val ddr_p_instance = Module(new ODDRX1F)
    ddr_p_instance.D0 := R_tmds_p(i)(0)
    ddr_p_instance.D1 := R_tmds_p(i)(1)
    out_p(i) := ddr_p_instance.Q.asTypeOf(out_p(i))
    ddr_p_instance.SCLK := clock
    ddr_p_instance.RST := 0.U
    val ddr_n_instance = Module(new ODDRX1F)
    ddr_n_instance.D0 := R_tmds_n(i)(0)
    ddr_n_instance.D1 := R_tmds_n(i)(1)
    out_n(i) := ddr_n_instance.Q.asTypeOf(out_n(i))
    ddr_n_instance.SCLK := clock
    ddr_n_instance.RST := 0.U
  }

}

Conclusion

Visiblement, le code généré doit être passé en revue par un ou une humaine histoire de corriger quelques imprécisions.

Mais cette relecture est facile, le code est très lisible et bien indenté. On retrouve les noms des signaux, variables, registres, modules Verilog. On est loin des bouillies de conversion où le code généré ressemble plus à un binaire compilé qu’à un code source «versionnable». Et cette saine relecture est de toute manière indispensable si l’on souhaite se reposer sur ce nouveau code dans la suite de son projet.

C’est un outil qui va vite devenir indispensable lorsque le besoin de convertion de code open-source se fera sentir. Et c’est une belle passerelle pour tous les habitués du Verilog qui souhaiteraient se lancer dans ce langage de haut niveau qu’est Chisel.

Des dire de l’équipe, l’outil a été testé avec succès sur le code du processeur RISC-V PicoRV32 développé par Claire Clifford (autrice de Yosys) que l’on retrouve un peu partout dans les projets open-source hardware.

C’est également une surprise de voir que ce projet est né au sein du laboratoire OVHCloud. Où l’on découvre que le fleurons du Claude français (cocorico) finance la recherche sur le matériel libre. Ceux qui ont besoin d’un article plus académique pour découvrir l’outil iront lire le papier de l’équipe ici.

ULX3S

Ho mais je me rend compte que je n’avais encore rien écrit sur la carte ULX3S commandée l’été dernier. Il faut dire que les délais d’approvisionnement étant ce qu’ils sont aujourd’hui la carte a tout de même mis presque six mois à arriver. J’ai donc eu tout le temps de passer à autre chose.

Donc oui, après m’être posé la question, j’ai opté pour l’ULX3S au détriment de l’orangecrab. La carte, conçue par le Hackerspace Radiona de Zagreb (croatie) arrive dans un petit carton muni de quelques accessoire «pmod» pour ajouter un second port HDMI, des USB et autres header he10.

L’ULX3S et ses adaptateurs «pmod»

La carte a tout ce qu’il faut pour faire une console de jeux 🙂 Mais pour le moment, on va surtout s’intéresser à la sortie HDMI, après avoir déballé la bête.

Mise en route

Un guide de mise en route est donné sur le github officiel. Pour démarrer la carte il suffit de brancher l’ordinateur sur l’usb de gauche.

Branchement de l’usb pour programmer la carte (src : quickstartguide)

Ce qui a pour effet d’allumer quelques leds de toutes les couleurs

\o/ plein de LEDs multicolors

et de monter un driver tty sur le pc host :

$ dmesg
[1956889.190788] usb 1-1.1.2: new full-speed USB device number 16 using xhci_hcd
[1956889.300502] usb 1-1.1.2: New USB device found, idVendor=0403, idProduct=6015, bcdDevice=10.00
[1956889.300504] usb 1-1.1.2: New USB device strings: Mfr=1, Product=2, SerialNumber=3
[1956889.300506] usb 1-1.1.2: Product: ULX3S FPGA 85K v3.0.8
[1956889.300507] usb 1-1.1.2: Manufacturer: FER-RADIONA-EMARD
[1956889.300508] usb 1-1.1.2: SerialNumber: E20000
[1956889.308375] ftdi_sio 1-1.1.2:1.0: FTDI USB Serial Device converter detected
[1956889.308401] usb 1-1.1.2: Detected FT-X
[1956889.308597] usb 1-1.1.2: FTDI USB Serial Device converter now attached to ttyUSB0

N’oublions pas que le FPGA est un ECP5, il y a donc tous les outils opensource disponibles pour développer avec. De plus, radiona fournie la toolchain précompilé pour la carte, il n’y a plus qu’à la télécharger et l’installer comme expliqué dans le manuel (424Mo pour un pc x64).

$ cd /opt/
$ mkdir ulx3s
$ cd ulx3s/
$ wget https://github.com/YosysHQ/oss-cad-suite-build/releases/download/2022-03-17/oss-cad-suite-linux-x64-20220317.tgz
$ tar xf oss-cad-suite-linux-x64-20220317.tgz

Si on veut faire clignoter vite vite vite, dans relancer de synthèse on peut tout simplement cloner le projet «blink» et télécharger le bitstream. À titre personnel je préfère utiliser openFPGALoader (avant même d’en tester un autre ;).

$ cd /opt/ulx3s
$ git clone https://github.com/ulx3s/blink.git
$ cd blink/
$ tree
.
├── blink_12f.bit
├── blink_45f.bit
├── blink_85f.bit
├── blinky.v
├── blinky.ys
├── LICENSE
├── Makefile
├── README.md
└── ulx3s_v20.lpf

0 directories, 9 files
$ openFPGALoader -bulx3s  blink_85f.bit
Jtag probe limited to 3MHz
Jtag frequency : requested 6000000Hz -> real 3000000Hz
ret 0
Open file: DONE
Parse file: DONE
Enable configuration: DONE
SRAM erase: DONE
Loading: [==================================================] 100.00%
Done
Disable configuration: DONE

Le résultat n’est pas une LED qui clignote, mais 6 LEDs multicolors qui clignotent (qui compte en binaire).

Le projet blink fourni un makefile pour reconstruire le bitstream si l’on veut tester la toolchain :

$ export PATH=/opt/ulx3s/oss-cad-suite/:$PATH
$ make ulx3s.bit
$ openFPGALoader -bulx3s ulx3s.bit

Et voila \o/, c’est tout pour la prise en main !

La suite

La suite va consister à adapter le projet HdmiCore pour la sortie HDMI de la carte.

Ressources