Archives par mot-clé : xilinx

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 🙁

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 😉

Les BlackBox et RawModule de Chisel3

Quelque soit le langage HDL utilisé il est très important de se garder la possibilité d’intégrer des modules provenant d’autre langages et/ou n’ayant pas de descriptions HDL.

C’est par exemple le cas des primitives matériel permettant d’instancier des modules intégrés au FPGA du constructeur au moyen de «template» Verilog : sérialiseur/désérialiseur, PLL, entrées/sorties spécifiques, …

BlackBox

Pour intégrer ce genre de module dans son projet Chisel3 on utilise des «BlackBox».  L’idée est de décrire les entrées/sorties du module ainsi que ses paramètres, et Chisel se chargera de convertir ça en une déclaration Verilog.

Le problème est assez classique sur les kits de développement de Xilinx qui sont cadencé par une horloge différentielle : Obligé d’instancier un buffer différentiel pour pouvoir récupérer l’horloge. Ce qui n’est pas prévu dans la classe Module de base de Chisel3 puisque l’horloge − tout comme le reset − est implicite.

D’après la documentation Xilinx, le buffer différentiel IBUFDS doit être instancié de la manière suivante en Verilog pour que le logiciel de synthèse le repère et l’instancie correctement:

IBUFDS #(
    .DIFF_TERM("TRUE"),
    .IOSTANDARD("DEFAULT")
) ibufds (
    .IB(ibufds_IB),
    .I(ibufds_I),
    .O(ibufds_O));

Cette instanciation est composée de deux paramètres «generic» et de trois entrées sorties.

Une BlackBox() se comporte comme un Module() sans les horloges et reset implicites. De plus le nom des IO est recopié tel quel par Chisel, il n’ajoute pas le préfixe «io_» comme pour un module normale.

Pour déclarer ce buffer différentiel en Chisel il suffira donc d’écrire le code suivant:

import chisel3._
import chisel3.util._
import chisel3.experimental._

class IBUFDS extends BlackBox(
    Map("DIFF_TERM" -> "TRUE",
        "IOSTANDARD" -> "DEFAULT")) {
    val io = IO(new Bundle {
        val O = Output(Clock())
        val I = Input(Clock())
        val IB = Input(Clock())})
}

Le Map en paramètre de la class BlackBox() permet d’ajouter les paramètres «generic» et les entrées sortie sont déclarés par la variable io.

Il suffira alors de l’instancier dans notre module top :

val ibufds = Module(new IBUFDS)
ibufds.io.I := clock_p
ibufds.io.IB:= clock_n

Pour que le code Verilog soit correctement écrit dans le fichier final.

Top RawModule

Maintenant que nous avons notre entrée d’horloge, notre but est d’aller faire clignoter une led (quelle originalité !) en utilisant un compteur. Avec le module suivant:

class Blink extends Module {
    val io = IO(new Bundle {
        val led = Output(Bool())
        })

    val MAX_COUNT = 100000000

    val count = Counter(MAX_COUNT)

    count.inc()

    io.led := 0.U
    when(count.value <= UInt(MAX_COUNT)/2.U){
    io.led := 1.U
    }
}

Ce module étant un module «normal» l’horloge et le reset sont implicite, alors comment allons nous faire pour qu’il soit cadencé par la sortie du buffer IBUFDS ?

On peut simplement les intégrer dans un Module() classique que l’on appellera Top :

class Top extends Module {
  val io = IO(new Bundle {
    val clock_p = Input(Clock())
    val clock_n = Input(Clock())
    val led     = Output(Bool())
  })

  val ibufds = Module(new IBUFDS)
  ibufds.io.I := io.clock_p
  ibufds.io.IB:= io.clock_n

  val blink = Module(new Blink)
  blink.clock := ibufds.io.O
  blink.reset := 1'0
  io.led := blink.io.led }

Notez que pour connecter explicitement l’horloge, la technique est en phase de développement mais il faut désormais utiliser la classe withClockAndReset()  pour faire les choses proprement . Plutôt que :

  val blink = Module(new Blink)
  blink.clock := ibufds.io.O
  blink.reset := false.B
  io.led := blink.io.led

Faire :

withClockAndReset(ibufds.io.O, false.B) {
    val blink = Module(new Blink)
    io.led := blink.io.led
  }

Cette méthode va fonctionner mais elle va nous ajouter les signaux clock et reset implicites. Signaux qui ne serviront pas à grand chose dans notre cas et généreront des warning pénible dans le logiciel de synthèse:

module Top(
  input   clock,
  input   reset,
  input   io_clock_p,
  input   io_clock_n,
  output  io_led
);
  wire  ibufds_IB;
  wire  ibufds_I;
  wire  ibufds_O;
  wire  blink_clock;
  wire  blink_reset;
  wire  blink_io_led;
  IBUFDS #(.DIFF_TERM("TRUE"), .IOSTANDARD("DEFAULT")) ibufds (
    .IB(ibufds_IB),
    .I(ibufds_I),
    .O(ibufds_O)
  );
  Blink blink (
    .clock(blink_clock),
    .reset(blink_reset),
    .io_led(blink_io_led)
  );
  assign io_led = blink_io_led;
  assign ibufds_IB = io_clock_n;
  assign ibufds_I = io_clock_p;
  assign blink_clock = ibufds_O;
  assign blink_reset = 1'h0;
endmodule

C’est pour cela qu’une nouvelle hiérarchie de classe est en développement pour les Module().

Un module Top est un module un peu spécial en conception HDL. En effet, ce type de module se contente simplement de «relier des boites entre elles». Ce n’est que du tire-fils, pas besoin d’horloge, de registres et autre structures complexes ici.

Dans la nouvelle hiérarchie des classes Module nous avons donc une nouvelle classe appelée RawModule qui apparaît.  Ce module n’a plus aucun signaux implicite et se contente de relier les fils. Dans le code Chisel précédent nous pouvons juste renommer Module en RawModule pour voir que les signaux reset et clock disparaissent:

class Top extends RawModule {

Nous obtenons alors une entête Verilog plus propre :

module Top(
  input   io_clock_p,
  input   io_clock_n,
  output  io_led
);

Nous avons tout de même ce préfixe «io_» disgracieux qui peu devenir pénible pour l’intégration, notamment dans certaine plate-forme où le pinout est déjà fourni pour des noms de pin précis.

Il est possible de les éviter avec les RawModule simplement en utilisant plusieurs variable IO() sans Bundle :

class Top extends RawModule {
  val clock_p = IO(Input(Clock()))
  val clock_n = IO(Input(Clock()))
  val led = IO(Output(Bool()))

  val ibufds = Module(new IBUFDS)
  ibufds.io.I := clock_p
  ibufds.io.IB:= clock_n

  withClockAndReset(ibufds.io.O, false.B) {
    val blink = Module(new Blink)
    led := blink.io.led
  }
}

De cette manière c’est le nom exact de la variable qui sera pris en compte pour générer le Verilog:

module Top(
  input   clock_p,
  input   clock_n,
  output  led
);

Et voila comment nous pouvons désormais faire un projet proprement écrit en Chisel de A à Z, ce qui n’était pas le cas avant où nous étions obligé d’encapsuler le projet dans des Top.v écrit à la main, et obligé de les modifier à chaque changement d’interface.

Le code décrit dans cet article se retrouve sur le Blinking Led Projet, dans le répertoire platform. Pour pouvoir le tester correctement, ne pas oublier de télécharger sa propre version de Chisel3 et de merger la branche modhier comme expliqué dans le README.

FPGA Prototyping By Verilog Examples: Xilinx Spartan-3 Version

 

185322_cover.indd

Très bon début pour commencer le Verilog pour le FPGA. Ce livre ne s’attarde pas trop sur les arcanes du langage, mais se contente de décrire les structures essentielles pour être rapidement opérationnel dans la conception de designs FPGA en Verilog.

Les exemples sont données pour le Spartan3 de chez Xilinx. Cependant ils sont de bonnes bases pour développer sur n’importe quel autre FPGA, même d’une autre marque.

Et si vous préférez vous mettre plutôt au VHDL, sachez qu’il existe exactement le même livre en version VHDL.

FPGA Prototyping by VHDL Examples: Xilinx Spartan-3 Version

vhdl_xilinx_book

Très bon début pour commencer le VHDL pour le FPGA. Ce livre ne s’attarde pas trop sur les arcanes du langage, mais se contente de décrire les structures essentielles pour être rapidement opérationnel dans la conception de designs FPGA en VHDL.

Les exemples sont données pour le Spartan3 de chez xilinx. Cependant ils sont de bonnes bases pour développer sur n’importe quel autre FPGA, même d’une autre marque.

Et si vous préférez vous mettre plutôt au Verilog, sachez qu’il existe exactement le même livre en version Verilog.

Compiler Torc sous Debian Jessie

Prérequis :

sudo apt-get install libboost-all-dev

Descendre le svn (avec git c’est mieux) :

git svn clone https://svn.code.sf.net/p/torc-isi/code/trunk torc

Faire un premier «make» dans src/, ce qui va créer un fichier nommé Makefile.local. Ouvrir ce fichier et ajouter les path vers boost :

BOOST_INCLUDE_DIR = /usr/include/boost/
BOOST_LIB_DIR = /usr/lib/

Faire un make (toujours dans src/) :

$ make

La configuration converti tous les warning en erreur (-Werror), et comme il reste des warnings visiblement dans le svn on arrive pas à tout compiler. Pour compiler malgré tout, virer l’option dans le fichier src/torc/Makefile.targets  (ligne 59):

-Werror \

 

Torc est un logiciel libre permettant de générer les bitstreams pour les fpga Xilinx:

http://torc-isi.sourceforge.net/documentation.php