Manage high impedance ‘Z’ state with Chisel

In previous (french) article, I described a technic to integrate a Chisel3 component named “TapTempo” in the APF27 board.  This board is made with an i.MX27 CPU and a Spartan3A FPGA.

To communicate with the FPGA, the i.MX27 is plugged on it with a memory bus named «WEIM». But the data signals used in WEIM are also connected on flash nand memory on the board, and this memory contain the Linux kernel and rootfs.

It’s not a problem if the FPGA “release” the data bus in high impedance when not used. Ant it’s the case when we use ctrl signal correctly (with oen signal -> output enable not)

In the first design, as tempo data are only output values read by the CPU, it was declared as “output” in chisel bundle :

class APF27TapTempo extends Module {
val io = IO(new Bundle {
val data = Output(UInt(16.W))
[...]

But with this data description, electrical value on physical data bus is continuously held by the FPGA. And when i.MX want to access nand flash memory it can’t. And we cannot start Linux as it require reading value in flash memory.

The solution is to set data bus as bidirectional signal and use WEIM signal oen to enable output on data bus, and when oen is not active the data bus is left on high impedance state ‘Z’.

In chisel, to manage bidirectional signal, we have to use the type “analog” as it :

class APF27TapTempo extends Module {
  val io = IO(new Bundle {
    val oen = Input(Bool())
    val data = Analog(16.W)
  [...]

And simply deactivate data when oen is ‘1’ ?

when(oen === '1'){
   data := "bZZZZZZZZZZZZZZZZ"
}.otherwize {
   data := taptempovalue
}

Would be so easy … But no, we can’t do that with Chisel3 🙁

In FPGA or ASIC, the high state value doesn’t exist in fact. All signals under FPGA must be input or output and set to ‘0’ or ‘1’. No other values are actually supported.

Other “high impedance” values like ‘Z’, ‘H’ or ‘L’ can’t be synthesized under the fpga. But these values (mostly ‘Z’ state in fact) can be synthesized on the boundary. If a bidirectional signal going out of FPGA, the xilinx synthesizer can instantiate an IOBUF and manage the high state.

Chisel doesn’t manage this high state. It’s a deliberate choice from chisel developers team to simplify Chisel3. But for some design, like on APF27 we need to implement it.

Verilog manage this kind of signal state, then we can write a verilog code to write ‘Z’ state and include it in our chisel design.

To do this, we use a module chisel type named BlackBox. And as we want to add directly verilog source in the chisel code we will add the Trait HasBlackBoxInline like described bellow :

// import verilog code with HasBlackBoxInline
import chisel3.util.{HasBlackBoxInline, HasBlackBoxResource}
...
// Describe blackbox with verilog code
class Apf27Bus extends BlackBox with HasBlackBoxInline {
  val io = IO(new Bundle {
    val dataout = Output(UInt(16.W))
    val dataio = Analog(16.W)
    val datain = Input(UInt(16.W))
    val oe = Input(Bool())
  })

  setInline("Apf27Bus.v",
    s"""
    |module Apf27Bus(
    |     output [15:0] dataout,
    |     inout [15:0] dataio,
    |     input [15:0] datain,
    |     input oen);
    |
    |   assign dataio = (oen == 'b0) ? datain : 'bzzzzzzzzzzzzzzzz;
    |   assign dataout = dataio;
    |endmodule
    """.stripMargin)
}
// Verilog will be written in separate file named Apf27Bus.v

...
// Then connect it in "Top" module
  val iobuf = Module(new Apf27Bus)
  io.data <> iobuf.io.dataio
  iobuf.io.datain := dataout
  iobuf.io.oen <> io.oen

As dataio is Analog type, we have to declare also analog on top bundle module :

class APF27TapTempo extends Module {
  val io = IO(new Bundle {
    val data = Analog(16.W)
    val oen = Input(Bool())
    val button = Input(Bool())
  })

With this method, we keep all our sources codes in the chisel sources and our high ‘Z’ bidirectional port is correctly synthesized with classical commercials software.

All sources of this project described in this article are available on my github project TapTempoChisel.

Intégration de TapTempo-Chisel sur APF27

Dans un premier article je décrivais le «core» de TapTempo en Chisel. Mais si nous souhaitons tester en réel il faut choisir une plate-forme sur laquelle le synthétiser. Ce choix implique nécessairement d’ajouter du code pour «packager» notre composant.

La carte APF27 et son kit de développement conçus par Armadeus Systems sont parfaitement indiqués. En effet la carte possède un FPGA de taille plutôt raisonnable de chez Xilinx : le spartan3A. Ce FPGA est couplé à un microprocesseur i.MX27 permettant de communiquer directement via un OS «évolué» (ici U-Boot). Et … comble du perfectionnement, le kit de développement est muni d’un bouton poussoir, qui nous servira de «touche tempo» !

L’idée est donc d’utiliser le bouton du kit pour la tempo et de venir lire le résultat mesuré par TapTempoChisel au moyen d’une lecture sur le bus de communication du processeur qui est connecté au FPGA.

Architecture du «packaging» de TapTempo
Architecture du «packaging» de TapTempo

On trouvera le code du packaging sur le github du projet. L’interface du Top est donc relativement simple, et se résume à deux signaux :

  • Le signal d’entrée (bouton)
  • Le signal de sortie (data)

Coté processeur,  il suffira de faire une lecture sur le bus pour pouvoir avoir la valeur en temps réel:

BIOS> md.w C8000000

Nous verrons plus tard que le design présenté ici est beaucoup trop simpliste et bloque le bus de l’apf27 ce qui entraîne une impossibilité de lancer Linux sur la carte.

Les différents éléments de notre architecture

Tout d’abord, pour éviter au maximum la métastabilité, il est nécessaire de synchroniser le signal d’entrée avec l’horloge du système. Pour cela nous devons faire passer le signal bouton par deux bascules D.

synchronization d'un signal externe par deux bascules
synchronisation d’un signal externe par deux bascules

Pour réaliser cela, dans un premier temps nous aurions tendance à déclarer deux signaux :

  • Un signal temporaire tmp
  • le signal synchronisé button_s

En chisel cela donnerait un truc dans le genre:

val tmp = RegNext(io.button)
val button_s = RegNext(tmp)

On déclare le registre en même temps que l’on connecte sa valeur d’entrée.

Pourtant à y regarder de plus près, ce montage de la double bascules n’est qu’un registre à décalage de 2 ! Et il existe une fonction pour ça dans la librairie «util» de chisel : ShiftRegister(sig, n)

Du coup nous pouvons réduire notre synchronisation en une simple ligne :

val button_s = ShiftRegister(io.button, 2)

Notre signal est maintenant synchronisé, mais nous n’avons pas filtré les rebonds. Or avec le genre de boutons que nous trouvons sur ces kits de développement c’est indispensable. Le FPGA étant cadencé à une fréquence élevé de 100Mhz nous allons «voir» tous les rebonds, et fausser par la même occasion notre mesure du tempo.

La plupart des «montages FPGA» permettant de faire de l’anti-rebond se basent sur des compteurs. Le tout étant de bien les dimensionner.

  val clk_freq_khz = 100000
  val debounce_per_ms = 20
  val MAX_COUNT = (clk_freq_khz * debounce_per_ms) + 1
  val debcounter = RegInit(MAX_COUNT.U)

La remise à zéro du compteur sera déclenchée par un front (montant ou descendant) du signal d’entrée. Nous déclarerons pour cela deux fonctions très commodes:

def risingedge(x: Bool) = x && !RegNext(x)
def fallingedge(x: Bool) = !x && RegNext(x)

Permettant de détecter respectivement le front montant et le front descendant du signal d’entrée.

Tant que le compteur debcounter n’a pas atteint sa valeur maximal, on ne fait que compter. Si le compteur est à sa valeur max et que l’on a un front sur le signal d’entrée, alors on remet le compteur à zero et on recopie la valeur du signal d’entrée.

  when(debcounter =/= MAX_COUNT.U) {
    debcounter := debcounter + 1.U
  }.otherwise {
    when(risingedge(button_s) || fallingedge(button_s)){
      debcounter := 0.U
      button_deb := button_s
    }
}

De cette manière on répercute rapidement un changement du signal d’entrée sans s’encombrer des multiples changement de valeurs rapide inhérentes aux rebonds.

Synthèse

Chisel est «vendu» à la base comme un langage HDL synthétisable, du coup nous allons le synthétiser, et avec un logiciel du marché s’il vous plaît : ISE.

Avant la synthèse nous avons besoin du code verilog généré. Pour le générer nous appellerons le ‘Driver’ déclaré dans le top:

object APF27TapTempoDriver extends App {
  chisel3.Driver.execute(args, () => new APF27TapTempo)
}

Au moyen de la commande sbt :

sbt 'runMain taptempo.APF27TapTempoDriver'

Le code verilog ainsi généré se retrouve dans le répertoire courant avec le nom APF27TapTempo.v

Notre projet comportant deux modules verilog (APF27TapTempo et TapTempo) leurs déclaration dans le fichier source se fait en partant de la fin -> le «top» est à la fin du fichier et le «core» au début:

...
module APF27TapTempo( // @[:@2517.2]
  input         clock, // @[:@2518.4]
  input         reset, // @[:@2519.4]
  output [15:0] io_data, // @[:@2520.4]
  input         io_button // @[:@2520.4]
);
...

Il ne nous reste plus qu’à intégrer ce source à un projet ISE en y ajoutant la description des signaux d’entrées sorties et leurs placement sur les pins du FPGA. Ce qui peut-être fait en intégrant le fichier de description APF27TapTempoChisel.ucf

# clock
NET "clock" LOC="N9" | IOSTANDARD=LVCMOS18;# CLK0
NET "clock" TNM_NET = "clock";
TIMESPEC "TS_clock" = PERIOD "clock" 10.4167 ns HIGH 50 %;
# data bus
NET "io_data<0>"  LOC="T5" | DRIVE=8 | IOSTANDARD=LVCMOS18; # DATA0
NET "io_data<1>"  LOC="T6" | DRIVE=8 | IOSTANDARD=LVCMOS18; # DATA1
NET "io_data<2>"  LOC="P7" | DRIVE=8 | IOSTANDARD=LVCMOS18; # DATA2
NET "io_data<3>"  LOC="N8" | DRIVE=8 | IOSTANDARD=LVCMOS18; # DATA3
NET "io_data<4>"  LOC="P12"| DRIVE=8 | IOSTANDARD=LVCMOS18; # DATA4
NET "io_data<5>"  LOC="T13"| DRIVE=8 | IOSTANDARD=LVCMOS18; # DATA5
NET "io_data<6>"  LOC="R13"| DRIVE=8 | IOSTANDARD=LVCMOS18; # DATA6
NET "io_data<7>"  LOC="T14"| DRIVE=8 | IOSTANDARD=LVCMOS18; # DATA7
NET "io_data<8>"  LOC="P5" | DRIVE=8 | IOSTANDARD=LVCMOS18; # DATA8
NET "io_data<9>"  LOC="N6" | DRIVE=8 | IOSTANDARD=LVCMOS18; # DATA9
NET "io_data<10>" LOC="T3" | DRIVE=8 | IOSTANDARD=LVCMOS18; # DATA10
NET "io_data<11>" LOC="T11"| DRIVE=8 | IOSTANDARD=LVCMOS18; # DATA11
NET "io_data<12>" LOC="T4" | DRIVE=8 | IOSTANDARD=LVCMOS18; # DATA12
NET "io_data<13>" LOC="R5" | DRIVE=8 | IOSTANDARD=LVCMOS18; # DATA13
NET "io_data<14>" LOC="M10"| DRIVE=8 | IOSTANDARD=LVCMOS18; # DATA14
NET "io_data<15>" LOC="T10"| DRIVE=8 | IOSTANDARD=LVCMOS18; # DATA15
# Button
NET "io_button" LOC="C15" | DRIVE=12 | IOSTANDARD=LVCMOS33; # IO_L24N_1

Et nous pouvons lancer la synthèse/placement&routage/bitstream d’ISE. Une fois le bitstream généré il faut le transférer dans la mémoire de l’apf27 avec U-Boot :

BIOS> tftpboot ${loadaddr} APF27TapTempo.bit

Puis configurer le FPGA.

BIOS> fpga load 0 ${loadaddr}

Nous pouvons enfin lire la valeur du tempo avec la commande de lecture dans l’espace mémoire du bus fpga (WEIM) :

BIOS> md.w C8000000
c8000000: 010e 010e 010e 010e 010e 010e 010e 010e ................
c8000010: 010e 010e 010e 010e 010e 010e 010e 010e ................
c8000020: 010e 010e 010e 010e 010e 010e 010e 010e ................

La valeur est lue en hexadécimal. Et comme l’adresse n’est pas gérée, tant que ça reste dans la zone du bus FPGA, la même valeur se répète.

Ici nous avons donc un tempo de 0x10e soit 270bpm. Pour le calibrer, j’ai pris le chronomètre et tenté d’appuyer sur le bouton toutes les secondes, ce qui doit logiquement donner 60bpm -> 0x3c.

Nous n’en somme pas trop loin :
Test de la calibration de TapTempoChisel sur APF27

Il est désormais possible de l’utiliser dans le cas concret de la mesure du tempo du très mauvais «nuit de folie» du groupe «début de soirée» .

Mesure du tempo du très mauvais «nuit de folie» :

On obtient une valeur de 0x7B soit 123 coups par minute (bpm).

Ps: si vous voulez laver votre cerveau de cette horrible chanson pourquoi pas une petite guérilla ? À moins que vous soyez adepte du crou. Ne me remerciez pas, moi aussi j’ai beaucoup souffert à mesurer le tempo de cette horreur 😉

Un composant électronique TapTempo avec Chisel3

Le «défi TapTempo» a été lancé sur LinuxFr il y a quelques semaines. L’objectif est de réaliser la mesure du tempo de l’appui sur une touche et de l’afficher simplement dans la console le résultat. La mesure du tempo s’effectue par défaut sur 5 appuis consécutif et affiche une moyenne en bpm (Beats Per Minute). L’idée est de réaliser la fonction dans divers langages informatiques pour que chacun puisse promouvoir son langage favoris. Beaucoup de langages ont été représenté jusqu’à présent, mais aucun langages de description matériel n’avait encore été proposé.

Pour palier ce gros manquement dans les langages représenté je vous propose ici de réaliser TapTempo en Chisel (version 3).

Architecture générale

L’idée ici n’est donc plus d’écrire un programme pour calculer le tempo mais de décrire l’architecture d’un composant matériel permettant de réaliser la fonction. Le matériel visé sera un FPGA, nous laissons de coté le développement sur ASIC. Même si une fois terminé il ne devrait pas y avoir de problème pour être porté sur un ASIC, si quelqu’un a suffisamment d’argent pour le claquer dans ce genre d’ânerie 😉

Le bloc fonctionnel de notre composant sera donc constitué d’une entrée button recevant le signal de l’appui sur un bouton permettant de faire le tempo. Dans un premier temps nous laisserons de coté les problèmes de métastabilité ainsi que de rebond. L’implémentation réel dans un FPGA nécessitera obligatoirement l’ajout d’un étage de synchronisation du signal d’entrée avec l’horloge ainsi que d’un bloc «anti-rebond», aucun bouton réel n’étant capable de faire un signal vraiment propre.

La sortie du bloc sera constitué d’un entier non signé bpm dont nous allons discuter la taille ci-dessous.

Et comme nous somme dans un FPGA il est indispensable de concevoir notre fonction synchrone d’une horloge, et souhaitable d’avoir un reset général.

Structure interne

La structure interne de TapTempo est donnée ci-dessous:

L’idée est de compter des ticks générés par timepulse au moyen du compteur count. Quand un appui sur le bouton est détecté, le compteur se remet à zéro et la valeur est enregistrée dans le tableau countx. À chaque coup d’horloge l’addition des 4 valeurs count est réalisée puis on divise par 4. La division par 4 est réalisable dans un FPGA au moyen d’un simple décalage à droite de 2. Vient la partie la plus compliqué : se servir de cette période moyenne pour diviser TMINUTE et obtenir la valeur du tempo en bmp.

Un peu de dimensionnement

On ne fonctionne pas dans un FPGA comme on fonctionne avec un pc, quand on fait des opérations sur des nombres il faut les dimensionner. Et il est fortement recommander d’utiliser des entiers, car le calcul flottant nécessite tout de suite une quantité de ressources phénoménale.

Notre objectif est de mesurer une cadence musicale en bpm que l’on puisse «taper à la main», si l’on regarde l’article wikipedia consacré au Tempo, on se rend vite compte qu’attendre les 200bpm est déjà pas mal. Disons que pour prendre une très large marge nous mettons la marge supérieur à 270bpm. Nous aurons donc en sortie une variable entière sur 9bits ( int(ln2(270))+1).

À 270bpm, le temps entre deux tempos est de ~222ms ce qui nous donnerais une fréquence de pulse de 4.5Hz. Cependant, si nous voulons une précision de 1bpm il va falloir augmenter cette fréquence , pour avoir un chiffre rond nous prendrons un temps de 1ms, soit une fréquence de 1kHz. Ce qui est un peu juste pour 270bpm, mais conviendra à la démonstration.

Décomposons le code

Le code se trouve sur le dépôt github suivant.  Nous allons décrire la description du module à proprement parlé qui se trouve ici.

Dans l’entête du module nous allons retrouver le port d’entrée button ainsi que le port de sortie bpm. Point d’horloge ni de reset ici puisqu’en Chisel ces signaux sont implicite.

// default clock 100Mhz -> T = 10ns
class TapTempo(tclk_ns: Int, bpm_max: Int = 270) extends Module {
val io = IO(new Bundle {
// val bpm = Output(UInt(8.W))
val bpm = Output(UInt(9.W))
val button = Input(Bool())
})

Quelques constantes qui nous servirons ensuite :

  /* Constant parameters */
  val MINUTE_NS = 60*1000*1000*1000L
  val PULSE_NS = 1000*1000
  val TCLK_NS = tclk_ns
  val BPM_MAX = bpm_max

  /* usefull function */
  def risingedge(x: Bool) = x && !RegNext(x)

Pour notre générateur de pulses il suffit d’utiliser une classe présente dans la bibliothèque «util» de chisel : Counter. Qui comme son nom l’indique … compte !

import chisel3.util.Counter
[...]
  val (pulsecount, timepulse) = Counter(true.B, PULSE_NS/tclk_ns)
[...]

Ce compteur prend en paramètre un signal de comptage (ici true.B -> compte tout le temps) ainsi que la valeur max à atteindre.
L’instanciation de l’objet retourne un compteur ainsi qu’un signal qui passe à ‘1’ quand le compteur se remet à zero (quand il dépasse la valeur max).

Ce signal timepulse sera ensuite utilisé par un deuxième compteur 16 bits tp_count que nous allons écrire «à la main» cette fois.
On défini d’abord le registre de 16bits que l’ont initialise à 0 (0.asUInt(16.W))

  val tp_count = RegInit(0.asUInt(16.W))

Puis on écrit le code décrivant le «comptage»:

  when(timepulse) {
    tp_count := tp_count + 1.U
  }
  when(risingedge(io.button)){
    // enregistrement de la valeur du compteur.
    countx(count_mux) := tp_count
    count_mux := Mux(count_mux === 3.U, 0.U, count_mux + 1.U)
    // remise à zéro du compteur
    tp_count := 0.U
  }

Ce deuxième compteur compte les pulse «timepulse» et se remet à 0 lorsqu’un front montant est détecté sur le bouton (quand on appuis sur le bouton).

Pour stocker les 4 valeurs permettant de réaliser une valeurs nous déclarons un vecteur de registres de 19 bits (pour gérer les retenues quand nous ferons l’addition):

  val countx = RegInit(Vec(Seq.fill(4)(0.asUInt(19.W))))

Ainsi que le «pointeur» :

  val count_mux = RegInit(0.asUInt(2.W))

La gestion de l’incrémentation du pointeur ainsi que de l’enregistrement du compteur se trouve dans le code du «compteur de pulse» que nous avons vu plus haut:

    // enregistrement de la valeur du compteur.
    countx(count_mux) := tp_count
    count_mux := Mux(count_mux === 3.U, 0.U, count_mux + 1.U)

Pour faire la somme rien de plus simple, il suffit de faire ‘+’ :

  val sum = Wire(UInt(19.W))
[...]
  sum := countx(0) + countx(1) + countx(2) + countx(3)

Et la division par 4 se résume à un décalage:

  val sum_by_4 = sum(18, 2)

Et nous arrivons à la partie la plus compliquée du design : diviser. Diviser est quelque chose de très compliqué dans un FPGA, on peut réaliser un design permettant d’effectuer une divisions en plusieurs cycles d’horloge, mais c’est tout de suite une très grosse usine à gaz.

Pour la division permettant de faire la moyenne des échantillons nous nous en étions sortie en faisant une division par une puissance de 2, qui se résume de fait à un simple décalage. Mais cette fois on ne pourra pas s’en sortir avec ce genre de pirouette. Car notre division à réaliser est la suivante :

   Nombre de pulse dans une minute
-------------------------------------  = valeur en bpm
moyenne des pulses mesuré (sum_by_4)

Pour nous en sortir la première idée serait de faire une table de valeurs pré-calculée pour chaque valeurs de  sum_by_4. Ce qui se fait très simplement en scala avec une séquence :

val x = Seq.tabulate(pow(2,16).toInt-1)(n => ((MINUTE_NS/PULSE_NS)/(n+1)).U)

Sauf que le nombre de valeurs est particulièrement grand (2^16) et qu’on pourrait certainement factoriser un peut tout ça.

Pour réduire la taille ne notre tableau il faut prendre le problème à l’envers: combien de valeurs possible puis-je avoir en sortie ?

La réponse est simplement 269 puisque je peux aller de 1 à 270bpm. Nous allons donc réaliser un vecteur de 270 valeurs contenant la valeurs minimum du compteurs permettant d’atteindre notre résultat. Et nous mettrons ces valeurs dans 270 registres.

  val bpm_calc = RegInit(Vec(x(0) +: Seq.tabulate(bpm_max)(n => x(n))))

Pour obtenir la valeur finale il faut ensuite générer 270 inéquations ayant pour résultat un vecteur de 270 bits :

  for(i <- 0 to (bpm_max-1)) {
    bpm_ineq(i) := Mux(sum_by_4 < bpm_calc(i), 1.U, 0.U)
  }

Le résultat correspondra ensuite à l’index du dernier bit à 1 dans ce vecteur.

Pour récupérer cet index, une fonction très utile est fournie dans la bibliothèque «util» de chisel : le PriorityEncoder. Qui permet d’obtenir l’index du plus petit bit à 1 dans un vecteur… sauf que nous on veut le plus grand !

Mais ce n’est pas très grave, il suffit de retourner le vecteur avec Reverse puis de faire une soustraction pour avoir le résultat :

  io.bpm := bpm_max.U - PriorityEncoder(Reverse(bpm_ineq.asUInt()))

Simulation

Le test permettant de simuler le design se trouve dans le fichier TapTempoUnitTest.scala

Il peut être lancé avec la commande sbt suivante :

sbt 'test:runMain taptempo.TapTempoMain --backend-name verilator'

Et les traces VCD générées sont disponible dans le répertoire ./test_run_dir/taptempo.TapTempoMain962904038/TapTempo.vcd

Visualisable simplement avec gtkwave.

gtkwave ./test_run_dir/taptempo.TapTempoMain962904038/TapTempo.vcd
Trace vcd de la simulation TapTempo

À suivre

Pour être vraiment complet il faudrait ajouter la gestion des rebonds ainsi que la synchronisation du signal d’entrée puis tester la synthèse. Mais ce sont de nouvelles aventure qui continuerons peut-être dans un prochaine épisode 🙂 .

SymbiFlow, vers la synthèse libre pour la Série7 de Xilinx

Le projet IceStorm permettant générer des bitstreams à partir du verilog vers les FPGA ICE40 de Lattice étant maintenant très avancé, W.Clifford se lance avec d’autres dans le reverse-ingineering des FPGA de la Série 7 de Xilinx.

Pour cela, un nouveau projet nommé SymbiFlow est créé pour fédérer les différents outils permettant de développer autour des FPGA de Xilinx. L’objectif à terme étant d’intégrer également les ICE40 à SymbiFlow.

Le projet inclut un sous projet nommé sobrement «Project X-Ray» permettant de documenter les différents éléments du FPGA Artix7 sous forme de carte en ASCII et HTML. Se sous-projet vise à documenter le FPGA mais également à fournir des outils permettant de piloter Vivado avec des design simplistes permettant de générer des statistiques sur les bitstreams générés et approfondir la documentation.

Un des gros changement de SymbiFlow par rapport à Icestorm est la volontés de migrer le placement-routage de arachne-pnr vers VPR. Un sous-projet de VTR développé depuis bien plus longtemps que Arachne-pnr.

Vu le succès remporté par le projet IceStorm, avec la quasi totalité des FPGA ICE40 documenté ainsi que leurs timings, on peut espérer voir arriver rapidement une chaîne de développement libre pour les FPGA de la Série 7 de xilinx. Et voir ainsi le développement open-source sur FPGA devenir une réalité.

 

Prise en main du kit de dev HiFive1 (Freedom E310)

Les chercheurs de Berkley qui ont fondé le set d’instruction (ISA) Risc-V (prononcez Risque failleve) ont également monté une société nommée SiFive.

Cette société conçoit des cœurs de processeurs nommés Freedom Everywhere et propose à ses clients de l’inclure dans des ASIC personnalisé. Les processeurs créés restent évidemment open-source, et l’intégralité du code (Chisel) est disponible sur le site de SiFive.

Pour promouvoir leur processeur, SiFive a fabriqué un microcontrôleur 32bits nommé Freedom E310. SiFive a également réalisé une carte «compatible arduino» qu’il est possible de commander sur Digikey pour prendre en main ce processeur.

C’est ce que nous allons tester ici. Le kit est livré «sec», pour l’alimenter il faut donc soit trouver un câble d’alimentation, soit décider de l’alimenter via l’USB comme expliqué dans le manuel de démarrage.

Kit HiFive1 (compatible arduino)

Le plus gros composant que l’on voit sur la carte est le convertisseur USB-Série et non le microcontrôleur. Le microcontrôleur se trouve à droite avec le «symbole superman».

Branchement

Le branchement du kit sur l’USB fait apparaître deux convertisseurs FTDI :

[16353.800810] usb 3-1: new high-speed USB device number 2 using xhci_hcd
[16353.941120] usb 3-1: New USB device found, idVendor=0403, idProduct=6010
[16353.941124] usb 3-1: New USB device strings: Mfr=1, Product=2, SerialNumber=0
[16353.941126] usb 3-1: Product: Dual RS232-HS
[16353.941139] usb 3-1: Manufacturer: FTDI
[16354.969029] usbcore: registered new interface driver usbserial
[16354.969056] usbcore: registered new interface driver usbserial_generic
[16354.969076] usbserial: USB Serial support registered for generic
[16354.986140] usbcore: registered new interface driver ftdi_sio
[16354.986162] usbserial: USB Serial support registered for FTDI USB Serial Device
[16354.986298] ftdi_sio 3-1:1.0: FTDI USB Serial Device converter detected
[16354.986354] usb 3-1: Detected FT2232H
[16354.986609] usb 3-1: FTDI USB Serial Device converter now attached to ttyUSB0
[16354.986634] ftdi_sio 3-1:1.1: FTDI USB Serial Device converter detected
[16354.986673] usb 3-1: Detected FT2232H
[16354.986906] usb 3-1: FTDI USB Serial Device converter now attached to ttyUSB1

La carte est livrée avec un bootloader faisant clignoter la led 4-couleurs D6. Il est possible de communiquer avec ce programme via le second port série :

$ sudo screen /dev/ttyUSB1 115200

On obtient le message de superman après avoir appuyé sur reset:

                SIFIVE, INC.

         5555555555555555555555555
        5555                   5555
       5555                     5555
      5555                       5555
     5555       5555555555555555555555
    5555       555555555555555555555555
   5555                             5555
  5555                               5555
 5555                                 5555
5555555555555555555555555555          55555
 55555           555555555           55555
   55555           55555           55555
     55555           5           55555
       55555                   55555
         55555               55555
           55555           55555
             55555       55555
               55555   55555
                 555555555
                   55555
                     5

               'led_fade' Demo 



55555555555555555555555555555555555555555555555
5555555 Are the LEDs Changing? [y/n]  555555555
55555555555555555555555555555555555555555555555
y
PASS

Compilons un programme pour l’E310

La mise en route du kit est relativement simple dans la mesure ou tout est décrit dans le document «getting started».

Évidemment, le cœur du proc est libre, du coup la chaîne de compilation l’est aussi pardi. Il suffit de la télécharger sur sa machine :

$ git clone --recursive https://github.com/sifive/freedom-e-sdk.git

Puis

cd freedom-e-sdk
make tools

Et attendre que ça compile en prenant son café.

Dans mon cas (Debian jessie) il fallait également installer les packets suivant pour que ça compile:

sudo apt-get install libmpc-dev

Un fois installé, on peut compiler la démo de gpio comme ça:

$ make software PROGRAM=demo_gpio BOARD=freedom-e300-hifive1

Puis la télécharger ainsi :

$ make upload PROGRAM=demo_gpio BOARD=freedom-e300-hifive1
work/build/openocd/prefix/bin/openocd -f bsp/env/freedom-e300-hifive1/openocd.cfg & \
/opt/freedom-e-sdk/work/build/riscv-gnu-toolchain/riscv64-unknown-elf/prefix/bin/riscv64-unknown-elf-gdb software/demo_gpio/demo_gpio --batch -ex "set remotetimeout 240" -ex "target extended-remote localhost:3333" -ex "monitor reset halt" -ex "monitor flash protect 0 64 last off" -ex "load" -ex "monitor resume" -ex "monitor shutdown" -ex "quit" && \
echo "Successfully uploaded 'demo_gpio' to freedom-e300-hifive1."
Open On-Chip Debugger 0.10.0+dev (2017-11-18-18:04)
Licensed under GNU GPL v2
For bug reports, read
	http://openocd.org/doc/doxygen/bugs.html
adapter speed: 10000 kHz
Info : auto-selecting first available session transport "jtag". To override use 'transport select '.
Info : ftdi: if you experience problems at higher adapter clocks, try the command "ftdi_tdo_sample_edge falling"
Info : clock speed 10000 kHz
Info : JTAG tap: riscv.cpu tap/device found: 0x10e31913 (mfg: 0x489 (SiFive, Inc.), part: 0x0e31, ver: 0x1)
Info : dtmcontrol_idle=5, dbus_busy_delay=1, interrupt_high_delay=0
Info : dtmcontrol_idle=5, dbus_busy_delay=1, interrupt_high_delay=1
Info : Examined RISCV core; XLEN=32, misa=0x40001105
Info : Listening on port 3333 for gdb connections
Info : dtmcontrol_idle=5, dbus_busy_delay=1, interrupt_high_delay=2
Info : dtmcontrol_idle=5, dbus_busy_delay=1, interrupt_high_delay=3
Info : dtmcontrol_idle=5, dbus_busy_delay=1, interrupt_high_delay=4
Info : dtmcontrol_idle=5, dbus_busy_delay=1, interrupt_high_delay=5
Info : dtmcontrol_idle=5, dbus_busy_delay=1, interrupt_high_delay=6
Info : dtmcontrol_idle=5, dbus_busy_delay=1, interrupt_high_delay=7
Info : dtmcontrol_idle=5, dbus_busy_delay=2, interrupt_high_delay=7
Info : dtmcontrol_idle=5, dbus_busy_delay=2, interrupt_high_delay=8
Info : dtmcontrol_idle=5, dbus_busy_delay=2, interrupt_high_delay=9
Info : dtmcontrol_idle=5, dbus_busy_delay=2, interrupt_high_delay=10
Info : dtmcontrol_idle=5, dbus_busy_delay=2, interrupt_high_delay=12
Info : dtmcontrol_idle=5, dbus_busy_delay=2, interrupt_high_delay=14
Info : dtmcontrol_idle=5, dbus_busy_delay=2, interrupt_high_delay=16
Info : dtmcontrol_idle=5, dbus_busy_delay=2, interrupt_high_delay=18
Info : dtmcontrol_idle=5, dbus_busy_delay=2, interrupt_high_delay=20
Info : dtmcontrol_idle=5, dbus_busy_delay=2, interrupt_high_delay=23
Info : dtmcontrol_idle=5, dbus_busy_delay=2, interrupt_high_delay=26
Info : dtmcontrol_idle=5, dbus_busy_delay=2, interrupt_high_delay=29
Info : dtmcontrol_idle=5, dbus_busy_delay=2, interrupt_high_delay=32
Info : dtmcontrol_idle=5, dbus_busy_delay=2, interrupt_high_delay=36
Info : [0] Found 2 triggers
halted at 0x204000fe due to debug interrupt
Info : Listening on port 6666 for tcl connections
Info : Listening on port 4444 for telnet connections
Info : accepting 'gdb' connection on tcp/3333
Info : Found flash device 'issi is25lp128' (ID 0x0018609d)
trap_entry () at /opt/freedom-e-sdk/bsp/env/entry.S:41
41	  STORE x27, 27*REGBYTES(sp)
Info : JTAG tap: riscv.cpu tap/device found: 0x10e31913 (mfg: 0x489 (SiFive, Inc.), part: 0x0e31, ver: 0x1)
JTAG tap: riscv.cpu tap/device found: 0x10e31913 (mfg: 0x489 (SiFive, Inc.), part: 0x0e31, ver: 0x1)
halted at 0x204000fe due to debug interrupt
halted at 0x204000fe due to debug interrupt
cleared protection for sectors 64 through 255 on flash bank 0
cleared protection for sectors 64 through 255 on flash bank 0
Info : JTAG tap: riscv.cpu tap/device found: 0x10e31913 (mfg: 0x489 (SiFive, Inc.), part: 0x0e31, ver: 0x1)
halted at 0x204000fe due to debug interrupt
Loading section .init, size 0x6c lma 0x20400000
Loading section .text, size 0xbbe6 lma 0x2040006c
Loading section .rodata, size 0x1144 lma 0x2040bc58
Loading section .eh_frame, size 0x68 lma 0x2040cd9c
Loading section .data, size 0x9d0 lma 0x2040ce04
Info : Padding image section 0 with 6 bytes
Info : dtmcontrol_idle=5, dbus_busy_delay=2, interrupt_high_delay=40
Info : Retrying memory read starting from 0x80000000 with more delays
Info : dtmcontrol_idle=5, dbus_busy_delay=2, interrupt_high_delay=45
Info : Retrying memory read starting from 0x80000000 with more delays
halted at 0x80000004 due to software breakpoint
halted at 0x80000004 due to software breakpoint
halted at 0x80000004 due to software breakpoint
halted at 0x80000004 due to software breakpoint
halted at 0x80000004 due to software breakpoint
halted at 0x80000004 due to software breakpoint
halted at 0x80000004 due to software breakpoint
halted at 0x80000004 due to software breakpoint
halted at 0x80000004 due to software breakpoint
halted at 0x80000004 due to software breakpoint
Info : JTAG tap: riscv.cpu tap/device found: 0x10e31913 (mfg: 0x489 (SiFive, Inc.), part: 0x0e31, ver: 0x1)
halted at 0x80000004 due to software breakpoint
Start address 0x20400000, load size 55246
Transfer rate: 52 KB/sec, 6905 bytes/write.
halted at 0x20400004 due to step
halted at 0x20400004 due to step
shutdown command invoked
shutdown command invoked
A debugging session is active.

	Inferior 1 [Remote target] will be detached.

Quit anyway? (y or n) [answered Y; input not from terminal]
Remote communication error.  Target disconnected.: Connection reset by peer.
Successfully uploaded 'demo_gpio' to freedom-e300-hifive1.

Malgrés l’erreur, visiblement le programme a bien été téléchargé dans le micro puisque les leds s’allument bien alternativement.
Et surtout, le message de démarrage s’affiche bien sur le /dev/ttyUSB1 :

core freq at 266646323 Hz
 
                SIFIVE, INC.

         5555555555555555555555555
        5555                   5555
       5555                     5555
      5555                       5555
     5555       5555555555555555555555
    5555       555555555555555555555555
   5555                             5555
  5555                               5555
 5555                                 5555
5555555555555555555555555555          55555
 55555           555555555           55555
   55555           55555           55555
     55555           5           55555
       55555                   55555
         55555               55555
           55555           55555
             55555       55555
               55555   55555
                 555555555
                   55555
                     5

SiFive E-Series Software Development Kit 'demo_gpio' program.
Every 2 second, the Timer Interrupt will invert the LEDs.
(Arty Dev Kit Only): Press Buttons 0, 1, 2 to Set the LEDs.
Pin 19 (HiFive1) or A5 (Arty Dev Kit) is being bit-banged
for GPIO speed demonstration.

.
Avec arduino

Ça n’est pas pour rien que le kit ressemble à s’y méprendre à un arduino : il est possible d’utiliser l’ide d’arduino pour se connecter à la carte.

L’ide arduino se trouvant dans ma debian est trop vieux pour pouvoir ajouter la toolchaine  sifive. J’ai donc du télécharger la 1.8 puis l’installer. Heureusement ça n’est pas très compliqué :

$ tar -Jxvf arduino-1.8.5-linux64.tar.xz
$ cd arduino-1.8.5/
$ ./install.sh 
$ ./arduino

Une fois lancé, il faut ajouter la configuration du package sifive en allant dans les préférences pour ajouter l’url suivante :

http://static.dev.sifive.com/bsp/arduino/package_sifive_index.json

Puis installer la plate-forme «sifive» via le board manager. Il faut également sélectionner «SiFive open ocd» comme programmeur.

Ne pas oublier de relancer l’ide et roulez jeunesse ! On peut facilement compiler/télécharger l’exemple de clignotement de led.

 

Gateware

Mais que veut bien vouloir dire Gateware ?

C’est un mot qu’on commence à voir apparaître un peu partout quand on parle de FPGA. On pourrait penser à un nouveau logiciel/projet mais non. C’est juste un nouveau mot valise inventé pour désigner le code ou les projets FPGA !

On avait le software pour le logiciel, le firmware pour le logiciel enfoui et le hardware pour désigner simplement le matériel. Et bien nous avons maintenant le gateware pour designer le logiciel (bitstream) du FPGA.

  • Gate : porte
  • Ware:  qui concerne

Visiblement les origines du mot proviennent de M-LAB qui a nomme sa spécialisation dans le FPGA comme ça.

Ne dites plus : «je fait du FPGA», mais : «je fait du gateware».

Visiblement, wikipédia n’a pas envie de définir gateware, la page a été supprimée une dizaine de fois !

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.

NVC, l’autre simulateur VHDL libre

Dans le domaine de la simulation libre du VHDL, on connait bien le simulateur GHDL qui est basé sur GCC, on connaît un petit peu le simulateur FreeHDL inclus dans le logiciel graphique de simulation électronique Qucs, mais on connaît moins le simulateur NVC qui pourtant est en train de faire son petit bonhomme de chemin.

NVC est écrit en langage C pur et compilé par défaut avec le compilateur LLVM concurrent de GCC. L’avantage de NVC par rapport à GHDL: c’est un programme indépendant de son compilateur là où ghdl n’est qu’une couche de GCC. Ce qui simplifie grandement la compilation de l’outils.

NVC n’a pas encore atteint sa première version stable, cependant le rythme des commits laisse penser que cela va venir. Et surtout il est déjà utilisable en l’état dans sa version git «master» .

L’outil a été intégré dans la partie VHDL du blp, il s’utilise de la même manière que ghdl avec une phase d’élaboration puis d’analyse avant d’être lancé en simulation.

Pour simuler le testbench permettant de tester le module anti-rebond du blp nous ferons les commandes suivantes dans le répertoire vhdl :

  • analyse
nvc -a ../test/test_button_deb.vhd ../src/button_deb.vhd
  • élaboration
nvc -e button_deb_tb
  • simulation (run)
nvc -r button_deb_tb -w

L’option de simulation -w permet de générer un fichier de sortie (*.fst) pour être lu par gtkwave.