Archives par mot-clé : Chisel

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.

SpinalHDL va-t-il remplacer Chisel ?

SpinalHDL est un langage HDL ressemblant à s’y méprendre à Chisel. Et pour cause, son créateur est un ancien utilisateur intensif de Chisel.

  • Tout comme Chisel, SpinalHDL est basé sur le langage Scala.
  • Tout comme Chisel, les entrées/sorties sont décrites au moyen de Bundles.
  • Tout comme Chisel, Spinal génère un langage HDL pour la synthèse.

Alors pourquoi ne pas utiliser Chisel ?

  • Car SpinalHDL génère du VHDL pour la synthèse, et non du Verilog
  • Car SpinalHDL gère les domaines d’horloges de manière élégante
  • Car la déclaration des entrées sortie est plus «naturelle» pour un habitué de VHDL/Verilog. En effet, pour déclarer les des signaux comme entrée ou sortie d’un module sur Chisel il faut faire :
    
       val io = new Bundle {
         val a = Bool(INPUT)
         val b = Bool(INPUT)
         val c = Bool(OUTPUT)
       }
    

    Alors que sur SpinalHDL on fera:

    
       val io = new Bundle {
         val a = in Bool
         val b = in Bool
         val c = out Bool
       }
    

    Ce qui est plus naturel.

  • La gestion des blackbox est mieux intégrée.

De plus, Charles (dolu1990) m’informe qu’il y a maintenant une implémentation de RISCV avec 5 étages de mul/div/interruptions fonctionnel en SpinalHDL:

Bonjour,

Flash info Spinal XD
Il y a maintenant une implémentation de RISCV, 5 stages mul/div/interrupt
fonctionnel codée en Spinal.
Le cpu égualement débuggable via JTAG, fork openOCD, GDB et eclipse. (c'est
a ma connaissance la seul implémentation RISCV qui cible les FPGA avec
cette fonctionnalité)

Au plaisir de libérer les FPGA de leur asservissement.
Charles

Bref pas mal de choses intéressantes qui j’espère dynamiserons le développement de Chisel également et peut-être fusionnerons à terme ?

Pour la documentation officielle c’est par là.

Synchronisation d’un signal externe avec Chisel

Quand on récupère un signal extérieur au fpga dont on ne maitrise pas le timing d’arrivée des fronts — typiquement un bouton ou une interruption — il est nécessaire de le synchroniser par rapport à l’horloge qui cadence le FPGA.

Il faut donc utiliser le montage classique des deux bascules. En Verilog et en VHDL cela se traduit par la déclaration d’un signal intermédiaire permettant la connection entre les deux bascules:

reg tmp, sig_s;

always @(posedge clk)
 begin
    tmp <= sig;
    sig_s <= tmp;
 end

Avec Chisel l’exemple donné dans le tutorial (page 14) est assez décevant car il impose aussi l’utilisation d’un signal intermédiaire :

s1 :=  signalA
s2 := s1;
signalB := s2

Pourtant il existe une méthode un peu plus élégante ne nécessitant qu’une seule ligne de code si on regarde du coté de la classe ChiselUtil : ShiftRegister()

Cette fonction permet de décaler un signal du nombre de coup d’horloge donné en argument, la synchronisation d’un signal par une double bascule n’étant rien de plus qu’un registre à décalage de deux coups d’horloge il suffit donc de l’utiliser avec un décalage de 2 :

val  sig_s = ShiftRegister(sig, 2)

Si on regarde le Verilog généré par le backend on obtient bien les deux bascules souhaitées:

always @(posedge clk) begin
[...]
sig_s <= R23;
R23 <= sig;
[...]
end

Voila qui nous simplifie grandement les design, et évite l’empilement des copier/coller de signaux temporaire pour synchroniser des grappes de signaux.

Du futur de Chisel

Comme on le sait, les membres du Berkeley Architecture Research group continuent à travailler d’arrache-pied sur Chisel.

On peut trouver un tutorial et les applications futures de Chisel ici (pdf).

Un modulateur PWM en Chisel

Pour changer des LED qui clignotent, voici un modulateur PWM écrit en Chisel avec son testbench.

Le code ci-dessous (PWM.scala) est du Scala standard utilisant la librairie Chisel.

import Chisel._
/* Classe contenant le comportement du module */
class PWM extends Module {
  /* Bundle io obligatoire */
  val io = new Bundle {
    val duty  = UInt(INPUT,  8)
    val out  = Bool(OUTPUT)
  }

  /* Registre 8 bits pour le compteur */
  val counter = Reg(UInt(width=8))
  counter := counter + UInt(1)

  /* Registre de sortie avec le comparateur */
  io.out := Reg(next=(counter<io.duty))  
}

/* Point d'entrée du programme */
object Example {
  def main(args: Array[String]): Unit = {
    chiselMain(args, () => Module(new PWM()))
  }
}

Le code du modulateur ci-dessus est découpé en deux parties.

La première décrit le comportement du module. Elle comprend le bundle (similaire au record en VHDL) appelé io, qui doit être présent dans chacun des modules pour décrire les entrées et les sorties, un compteur 8 bits, et le registre de sortie qui est alimenté par le comparateur de rapport cyclique.

La deuxième partie est nécessaire pour le module qui se trouve au sommet de la hiérarchie afin de décrire le point d’entrée qui servira à la génération du code.

Il est aussi nécessaire de fournir le fichier build.sbt qui sert à indiquer a SBT (l’outil de build Scala) qu’il doit également aller chercher Chisel dans ses paquets avant de lancer la compilation. SBT va d’office compiler tout les fichiers Scala (avec l’extension .scala) qu’il trouve dans le répertoire courant.

libraryDependencies += "edu.berkeley.cs" %% "chisel" % "latest.release"

scalacOptions ++= Seq("-deprecation", "-feature", "-unchecked", "-language:reflectiveCalls")

Il faut ensuite s’assurer que Chisel génère du Verilog en lançant la commande

sbt 'run --v'

Il manque un banc de test pour exercer le design. Chisel propose de compiler également pour nous un modèle C++ du design qu’on peut obtenir
en utilisant la commande suivante :

sbt 'run --test --genHarness --vcd'

On va maintenant créer un testbench C++ qui permettra de générer les VCD. Appelons ce fichier main_pwm.cpp.

#include "PWM.h"
#include <iostream>

int main (int argc, char* argv[]) { 
  // Créer l'objet PWM
  PWM_t* c = new PWM_t();

  // Recupérer le nombre de cycles à exécuter
  int lim = (argc > 1) ? atoi(argv[1]) : -1;
  if(lim == -1) {
     cout << "wrong argument, should be :" << endl;
     cout << "a.out <n>" << endl;
     return 1;
  }
  // Initialiser le modèle
  c->init();

  // Ecrire l'entête du VCD
  c->dump_init(stdout);

  // Ecrire le rapport cyclique
  c->PWM__io_duty = LIT<8>(100);
  for (int t = 0; lim < 0 || t < lim; t++) {
    // Active le reset sur le premier cycle
    dat_t<1> reset = LIT<1>(t == 0);

    // Dump du VCD
    c->dump(stdout,t);

    // Avancer d'une période d'horloge
    c->clock(reset);
    } 
}

Il reste juste à compiler le tout avec g++.

g++ PWM.cpp main_pwm.cpp

À exécuter

./a.out 10000 > out.vcd

Et admirer le résultat dans gtkwave

gtkwave out.vcd

Compiler les exemples Chisel sur Jessie

Chisel est un langage de description hardware développé par l’université de Berkley basé sur le langage Scala.

Les mainteneurs du projet Chisel fournissent un github de code d’exemples avec un tutoriel qui va avec. Le guide d’installation est par contre un peu léger et nécessite quelques adaptations pour tourner correctement sous Debian Jessie.

Dépendances

Les paquets debian à installé sont les suivants:

  • Scala
  • sbt
  • g++4.8
  • openjdk-7-jre

Que l’on peut installer sous Jessie avec les commandes suivantes:

sudo apt-get install scala
sudo apt-get install g++-4.8-multilib
sudo apt-get install openjdk-7-jre

sbt n’étant pas intégré dans Jessie, il est nécessaire de l’installer à la main en décompressant l’archive:

wget https://dl.bintray.com/sbt/native-packages/sbt/0.13.6/sbt-0.13.6.tgz
tar -zxvf sbt-0.13.6.tgz

Puis en l’ajoutant à son .bashrc :

# Scala built tool
export PATH=$PATH:"/opt/sbt/bin/"

Installation des tutoriels

Tout d’abord il nous faut descendre le git des tutoriels :

git clone https://github.com/ucb-bar/chisel-tutorial.git
cd chisel-tutorial/example

Puis tenter de compiler l’exemple Parity avec le makefile :

$ make Parity.out
set -e -o pipefail; sbt -Dsbt.log.noformat=true "run Parity --genHarness --compile --test --backend c " | tee Parity.out
/bin/sh: 0: Illegal option -o pipefail
../suffix.mk:61: recipe for target 'Parity.out' failed
make: *** [Parity.out] Error 2

Et c’est le drame !

Pour que ça fonctionne il faut taper la commande à la main sans le «set -e -o pipefail».

$ sbt -Dsbt.log.noformat=true "run Parity --genHarness --compile --test --backend c " | tee Parity.out

...

STEP 1 -> 10
 PEEK Parity.io_out -> 0x0
EXPECT Parity.io_out <- 0 == 0 PASS
RAN 10 CYCLES PASSED
PASSED
[success] Total time: 31 s, completed 27 oct. 2014 12:28:46
$