Archives par mot-clé : hdl

TapTempo en Verilog

Il y a plus de deux ans et demi maintenant, mzf publiait un journal sur le site LinuxFR parlant de son projet «TapTempo». L’objectif de son programme était simplement de mesurer la cadence d’une musique en tapant sur une touche de son clavier, le résultat s’affichant simplement dans la console.

Ce journal fut le point de départ d’une série de «projets TapTempo» proposé par les lecteurs du site dans à peu prêt tous les langages informatique possible… Mais pas le Verilog.

Voici donc la lacune comblée avec TapTempo en Verilog.

[Dépêche parue initialement sur LinuxFR]

Le projet TapTempo semble faiblir depuis quelques mois maintenant. En panne de langage informatique pour en faire une dépêche ?
Laissez‑moi vous présenter un langage assez particulier puisqu’il ne sert pas à faire de la programmation. Ce langage permet de décrire le comportement numérique d’un composant électronique (on parle alors de langage de description de matériel — HDL) : le Verilog.

C’est aussi un langage utilisé pour faire de la synthèse numérique sur les circuits logiques programmables (FPGA). Dans cet exemple, nous utiliserons la carte de développement à bas coût ColorLight 5A‑75B.

Vue d'ensemble du montage TapTempo

Sommaire

Le Verilog

Le Verilog est un langage conçu à l’origine pour rédiger des spécifications de circuits logiques en électronique numérique. Le langage permet de décrire le comportement de sortie par rapport à des entrées logiques.

Un peu comme les logiciels de saisie de schéma électronique, le Verilog est très hiérarchique, on décrit des modules avec leurs entrées-sorties. Que l’on assemble ensuite dans d’autres modules pour finir dans un module « top » qui décrit le composant final.

Dans le cas de TapTempo, le module « top » est déclaré comme ceci :

module taptempo #(
    parameter CLK_PER_NS = 40, // 25Mhz clock (ns)
    parameter TP_CYCLE = 5120, // timepulse cycle period (ns)
    parameter BPM_MAX = 250 // BPM max (bpm)
)(
    input clk_i,
    input btn_i,
    output pwm_o
);
//corps du module
endmodule

Le module possède deux entrées : l’horloge (clk_i) et le bouton (btn_i) ainsi qu’une sortie pwm (pwm_o) pour l’affichage. Les paramètres seront vus comme des constantes au moment de la simulation, ils permettent de configurer les composants en fonction de la cible.

Le changement de valeur des signaux se fait dans des processus qui sont déclenchés sur événement. Ces processus sont décrits au moyen du mot clef always@() en Verilog.
Par exemple, dans le code suivant:

/* Detect rising edge*/
reg btn_old, btn_rise;
always@(posedge clk_i)
begin
    btn_old <= btn_i;
    if(btn_old == 0 && btn_i == 1)
        btn_rise <= 1;
    else
        btn_rise <= 0;
end

L’événement déclencheur du process est le front montant de l’horloge clk_i. À chaque fois qu’un front montant d’horloge se présente, le processus est exécuté de manière séquentielle.

L’opérateur <= est l’opérateur d’affectation dit « non bloquant ». Cela signifie que la valeur ne sera effectivement appliquée qu’a la fin de l’exécution du process. Donc la valeur du signal btn_old ne sera pas nécessairement égale à btn_i à la ligne du if() comme on aurait pu instinctivement le croire.

Le langage Verilog a beaucoup de succès dans le monde du logiciel libre. En effet il est relativement peu verbeux et ressemble au C pour de nombreux aspects.

Il est par exemple possible de décrire des macros de la même manière qu’en C, il suffit de remplacer le symbole # par ` pour créer des constantes qui seront remplacées par le pré-processeur.

/* count tap period */
`define MIN_NS 60_000_000_000
`define BTN_PER_MAX (`MIN_NS/TP_CYCLE)
`define BTN_PER_SIZE ($clog2(1 + `BTN_PER_MAX))

Le Verilog reprend également les opérateurs booléen et binaire &,&&, |,||, etc. du C.

C’est le langage HDL le mieux supporté par les différents logiciels libres. Si l’on souhaite se lancer dans le domaine des FPGA et/ou des ASIC, il est préférable de commencer par lui. C’est également le langage « de sortie » de quasiment tous les générateurs de code HDL.

Architecture de TapTempo

L’outil indispensable pour commencer un projet en Verilog est… le papier et le crayon. Il est en effet indispensable d’avoir une vue d’ensemble assez claire de ce que l’on souhaite réaliser avant de se lancer dans le code.

Voici donc l’architecture générale du composant TapTempo :

Schema TapTempo au Crayon

Même si l’on doit revenir plusieurs fois (ce qui est le cas ici puisque les constantes ne sont pas à jour) sur ce schéma général en cours de développement, cette partie est très importante. Si elle est bien pensée, le reste coule de source.

Le composant va nécessiter quelques compteurs, mais l’horloge utilisée ici étant très rapide nous allons d’abord factoriser le comptage au moyen du module nommé timepulse, ce module va distribuer un pulse qui servira de base aux autres compteurs pour leur fonctionnement interne.

L’entrée utilisateur se compose d’un bouton (touche télégraphique « morse »). Les fronts montant et descendant de cette entrée n’étant pas synchronisés sur l’horloge du système nous allons devoir le faire au moyen de deux bascules en série pour éviter la métastabilité.

/* Synchronize btn_i to avoid metastability*/
reg btn_old, btn_s;
always@(posedge clk_i or posedge rst)
begin
    if(rst) begin
        btn_old <= 1'b0;
        btn_s <= 1'b0;
    end else begin
        btn_old <= btn_i;
        btn_s <= btn_old;
    end
end

Le second problème que pose notre entrée est que l’appui sur le bouton ne génère pas des changements francs de son état. Chaque “appui et relâche” génère une série de rebonds et donc une série de 0 et de 1 avant de se stabiliser. Pour lisser le signal il va donc falloir faire passer le signal dans le bloc « anti-rebond » debounce.

Le bloc percount va ensuite se charger de mesurer le temps entre deux appuis sur le bouton. Cette période va devoir être transformée en fréquence « BPM » (Beat Per Minute) via le module per2bpm puis en une valeur pseudo-analogique (PWM) grâce au module pwmgen.

La carte cible ne possédant pas de bouton « reset », il va falloir le générer grâce au module rstgen de manière à s’assurer de l’état de départ de notre système au démarrage.

Entrée sortie

La plupart des programmes TapTempo proposés jusqu’ici supposaient – en plus d’un CPU – la présence d’un clavier et d’une console texte de sortie (avec toute la pile de pilotes et de système d’exploitation associés). Ici, nous allons devoir tout définir dans le « portegramme » – Dans l’industrie on va parler d’IP pour Intellectual Property, quel horrible nom –.

L’idée est donc de simplifier au maximum l’entrée « clavier » et la sortie histoire de pouvoir les décrire simplement.

Pour l’entrée nous allons nous contenter d’un contact de type bouton, ou d’une touche de type télégraphe « morse ».

TapTempoEntreeMorse

Comme on peut le voir dans le schéma ci-dessus, quand la touche est appuyée, l’entrée « bouton » est mise à la masse et donne un niveau logique à 0 sur notre système. Lorsque l’on relâche le bouton, la résistance de tirage ramène le niveau de tension à Vcc pour avoir un niveau 1 sur l’entrée.

Pour la sortie, l’idée de mettre un écran complexifie énormément le système. En effet, il est nécessaire de faire une machine d’état assez complexe pour initialiser l’écran puis rafraîchir l’affichage. Il est souvent nécessaire d’ajouter un processeur « soft » rien que pour ça d’ailleurs. (Bon il est vrai que le VGA n’est pas si compliqué, mais il reste plus complexe que la solution proposée ici).

Non, l’idée ici est d’utiliser les graduations de l’antique voltmètre à aiguille trouvé dans une cave et qui gradue de 0 à 300 comme on peut le voir sur la photo :

vuemetretaptempo

Et comme un système numérique ne sort que des 0 et des 1 sur ses broches, on va « simuler » une valeur analogique au moyen d’une PWM (Pulse With Modulation). Il suffit de changer le rapport cyclique entre le temps haut et le temps bas de notre signal pour faire varier la tension moyenne qui sera vue par le voltmètre. Si on l’ajuste correctement avec une résistance en série, il est relativement facile de forcer la valeur maximale (5V) à 250.

pwm

La période de la pwm sera configurée suffisamment rapide pour que l’aiguille n’oscille pas.

Pulsation de temporisation (timepulse)

Le module ne prend pas de valeur d’entrée hormis l’horloge et le reset qui sont de rigueur dans tout le projet. Son signal de sortie tp_o est une pulsation de l’horloge émise toutes les 5120 ns :

module timepulse #(
    parameter CLK_PER_NS = 40,
    parameter PULSE_PER_NS = 5120
)(
    /* clock and reset */
    input clk_i,
    input rst_i,
    /* output */
    output tp_o);

Pour pouvoir compter des périodes de 5120ns on définit un registre de comptage :

`define MAX_COUNT (PULSE_PER_NS/CLK_PER_NS)
`define MAX_COUNT_SIZE ($clog2(`MAX_COUNT))

reg [`MAX_COUNT_SIZE-1:0] counter = 0;

Puis on compte de manière synchronisée avec l’horloge :

always@(posedge clk_i or posedge rst_i)
begin
    if(rst_i)
    begin
        counter <= 0;
    end else begin
        if (counter < `MAX_COUNT)
        begin
            counter <= counter + 1'b1;
        end else begin
            counter <= 0;
        end
    end
end

La pulsation est émise lorsque le compteur passe par 0 :

assign tp_o = (counter == 0);

Gestion des rebonds (debounce)

L’entrée de ce module est le signal de bouton préalablement synchronisé avec l’horloge du système btn_s. Le compteur utilisera la pulsation tp_i généré par le module timepulse décrit ci-avant.

La sortie du module est un signal btn_o proprement lissé. La période de temporisation de 20 ms est donné ici en paramètre DEBOUNCE_PER_NS.

module debounce #(
    parameter PULSE_PER_NS = 5120,
    parameter DEBOUNCE_PER_NS = 20_971_520
)(
    /* clock and reset */
    input clk_i,
    input rst_i,
    /* inputs */
    input tp_i,
    input btn_i,
    /* output */
    output btn_o
);

La gestion des rebonds est réalisée au moyen d’un compteur utilisé pour temporiser.

`define MAX_COUNT ((DEBOUNCE_PER_NS/PULSE_PER_NS)-1'b1)
`define MAX_COUNT_SIZE ($clog2(`MAX_COUNT))

/* Counter */
reg [`MAX_COUNT_SIZE-1:0] counter = 0;

Ainsi que d’une machine d’états à 4 états :

/* State machine */
localparam [1:0] s_wait_low  = 2'h0,
                 s_wait_high = 2'h1,
                 s_cnt_high  = 2'h2,
                 s_cnt_low   = 2'h3;

reg [1:0] state_reg, state_next;

Les transitions de la machine d’états sont données dans le code ci-dessous dans un processus dit « combinatoire » (always@*) par opposition à un processus « synchrone ».

always@*
begin
    case(state_reg)
        s_wait_low:
            if(btn_i)
                state_next = s_cnt_high;
            else
                state_next = s_wait_low;
        s_wait_high:
            if(!btn_i)
                state_next = s_cnt_low;
            else
                state_next = s_wait_high;
        s_cnt_high:
            /* verilator lint_off WIDTH */
            if(counter == `MAX_COUNT)
            /* verilator lint_on WIDTH */
                state_next = s_wait_high;
            else
                state_next = s_cnt_high;
        s_cnt_low:
            /* verilator lint_off WIDTH */
            if(counter == `MAX_COUNT)
            /* verilator lint_on WIDTH */
                state_next = s_wait_low;
            else
                state_next = s_cnt_low;
    endcase;
end

L’état de la machine est tout de même synchronisé dans un second processus :

always@(posedge clk_i or posedge rst_i)
    if(rst_i)
        state_reg <= s_wait_low;
    else
        state_reg <= state_next;

Le principe de « lissage » des rebonds est donc le suivant : Dans l’état initial s_wait_low on attend que le bouton passe à la valeur 1. Lorsque le signal passe à 1, on change d’état pour s_cnt_high.

Le passage dans l’état s_cnt_high a pour effet de faire passer le signal de sortie à 1 et déclencher le compteur. Tant que le compteur compte et n’a pas atteint la valeur MAX_COUNT, on reste dans cet état quelles que soient les variations du signal d’entrée.
Lorsque le compteur atteint la valeur maximale, la machine d’état passe dans l’état s_wait_high (en attente de valeurs hautes).

Dans l’état s_wait_high on surveille la valeur du bouton d’entrée, si elle passe à 0 on change d’état pour s_cnt_low.

De manière symétrique à s_cnt_high on déclenche donc le compteur en ignorant la valeur d’entrée. Et, lorsqu’elle atteint son maximum on passe à l’état initial s_wait_low.

La valeur « lissée » du bouton en sortie est donnée par l’état de la machine d’état :

assign btn_o = (state_reg == s_cnt_high) || (state_reg == s_wait_high);

Mesure de la période de tempo (percount)

L’interface du module percount se compose des entrées habituelles d’horloge clk_i, de reset rst_i ainsi que de la pulsation tp_i.

Le signal de mesure en entrée est btn_i et la sortie est un vecteur btn_per_o donnant la valeur mesurée. La valeur est considérée comme valide uniquement lorsque la sortie btn_per_valid est à 1. Cette astuce permet d’économiser un registre si la sauvegarde de la valeur mesurée est inutile comme c’est le cas ici.

`define MIN_NS 60_000_000_000
`define BTN_PER_MAX (`MIN_NS/TP_CYCLE)
`define BTN_PER_SIZE ($clog2(1 + `BTN_PER_MAX))

module percount #(
    parameter CLK_PER_NS = 40,
    parameter TP_CYCLE = 5120,
    parameter PULSE_PER_NS = 5120,
)(
    /* clock and reset */
    input clk_i,
    input rst_i,
    /* time pulse */
    input tp_i,
    /* input button */
    input btn_i,
    /* output period */
    output [(`BTN_PER_SIZE-1):0] btn_per_o,
    output btn_per_valid);

Maintenant que nous avons un signal de bouton btn_b propre et lissé, nous pouvons entamer la mesure de la période entre deux appuis au moyen de… devinez quoi ? D’un compteur pardi !

reg [($clog2(`BTN_PER_MAX+1)-1):0] counter = 0;
reg counter_valid = 0;

assign btn_per_valid = counter_valid;
assign btn_per_o = counter;

Il nous faut tout d’abord détecter le front descendant du bouton  :

reg btn_old;
wire btn_fall = btn_old & (!btn_i);

always@(posedge clk_i or posedge rst_i)
begin
    if(rst_i)
        btn_old <= 1'b0;
    else
        btn_old <= btn_i;     
end

Le signal btn_fall sert de remise à zéro du compteur ainsi que de validation de la valeur de sortie :

always@(posedge clk_i or posedge rst_i)
begin
    if(rst_i)
    begin
        counter <= 0;
    end else begin
        if(btn_fall) begin
            counter_valid <= 1'b1;
        end else if(counter_valid) begin
            counter <= 0;
            counter_valid <= 1'b0;
        end else begin
            /* stop counting if max, count tp_i */
            if(tp_i && counter < `BTN_PER_MAX)
                counter <= counter + 1'b1;
        end
    end
end

Le compteur compte le nombre de pulsations de tp_i jusqu’à atteindre la saturation BTN_PER_MAX. Si un front montant du bouton se présente avec btn_fall, on valide le compteur avec counter_valid. Et si le signal de validation passe à 1 (donc le coup d’horloge suivant) on remet le compteur à zéro et on recommence à compter.

Calcul de la fréquence en Beat Per Minute (per2bpm)

Avec le module per2bpm on arrive dans la partie critique du projet, car il va nous falloir faire une division. On entre une période dans le module :

    /* inputs */
    input [(`BTN_PER_SIZE-1):0] btn_per_i,
    input btn_per_valid,

Et on doit en ressortir une fréquence (BPM) :

    /* outputs */
    output [`BPM_SIZE - 1:0] bpm_o,
    output bpm_valid

Suivant la formule :

Il faut donc diviser la constante

par la variable btn\_per\_i

La division (tout comme la multiplication) est un point sensible en Verilog. En effet, l’opérateur de division existe bien dans le langage et il se peut que cela simule parfaitement.

C’est lorsque arrivera l’étape de la synthèse que l’on risque d’avoir quelques surprises. Il est possible que certains logiciels de synthèse réussiront à faire quelque chose en un coup d’horloge. Mais il est certain que cela se fera au prix de très mauvaises performances en matière de ressources utilisées et de fréquence d’horloge. Il est surtout probable que votre logiciel de synthèse jette l’éponge.

Pour réaliser cette division, nous allons donc en revenir aux fondamentaux appris au primaire et poser la division. Une division, c’est la recherche du Quotient et du Reste de l’équation suivante :

Dividend = Quotient \times Divisor + Remainder
reg [(`REGWIDTH-1):0] divisor;
reg [(`REGWIDTH-1):0] remainder;
reg [(`REGWIDTH-1):0] quotient;

La taille des registres sera celle de la période en entrée BTN_PER_SIZE additionné à la constante à diviser.

`define DIVIDENTWITH ($clog2(1 + `MIN_NS/(TP_CYCLE)))
`define REGWIDTH (`BTN_PER_SIZE + `DIVIDENTWITH)

La division s’effectue avec une série de soustraction du reste (remainder) et de décalage du diviseur.

À l’étape initiale, on place le diviseur à gauche du registre divisor et le dividende dans le reste remainder :

     divisor <= {btn_per_i, (`DIVIDENTWITH)'h0};
     remainder <= `MIN_NS/TP_CYCLE;
     // le résultat est initialisé à 0:
     quotient <= 0;

Puis on effectue une série de comparaison-soustraction-décalage avec l’algorithme comme décrit ci-dessous :

  • si le diviseur (divisor) inférieur ou égal au reste (remainder), on soustrait le reste avec le diviseur et on décale le quotient à gauche en ajoutant 1 :
            if(divisor <= remainder)
            begin
                remainder <= remainder - divisor;
                quotient <= {quotient[(`DIVIDENTWITH-2):0], 1'b1};
  • si le diviseur (divisor) est supérieur au reste, on décale le quotient à gauche en ajoutant 0. On ne touche pas au reste :
                quotient <= {quotient[(`DIVIDENTWITH-2):0], 1'b0};
  • dans tous les cas, on décale le diviseur à droite.
            divisor <= {1'b0, divisor[(`REGWIDTH-1):1]};

La division est orchestrée par une machine à trois états :

localparam [1:0] s_init    = 2'h0,
                 s_compute = 2'h1,
                 s_result  = 2'h2;

reg [1:0] state_reg, state_next;

Et le résultat est disponible en sortie quand state_reg est dans l’état s_result:

assign bpm_o = quotient[(`BPM_SIZE-1):0];
assign bpm_valid = (state_reg == s_result);

Génération de la tension de sortie (pwmgen)

La génération du signal pseudo analogique décrite en introduction est presque la partie la plus simple.

On compte (oui encore) de 0 à 250 (BPM_MAX) :

/* count */
always@(posedge clk_i or posedge rst_i)
begin
    if(rst_i)
        count <= BPM_MAX;
    else begin
        if(tp_i)
        begin
            if (count == 0)
                count <= BPM_MAX;
            else
                count <= count - 1'b1;
        end
    end
end

Et on passe le signal de sortie pwm_o à 1 lorsque le compteur est inférieur à la fréquence demandée :

assign pwm_o = (count <= pwmthreshold);

Il y a juste une subtilité consistant à sauvegarder la valeur de la fréquence donnée en entrée dans deux registres pwmthreshold et bpm_reg :

reg [($clog2(BPM_MAX+1)-1):0] bpm_reg;
reg [($clog2(BPM_MAX+1)-1):0] pwmthreshold;

/* Latching bpm_i on bpm_valid */
always@(posedge clk_i or posedge rst_i)
begin
    if(rst_i)
    begin
        bpm_reg <= 0;
        pwmthreshold <= 0;
    end else begin
        if(bpm_valid)
            bpm_reg <= bpm_i;
        if(count == BPM_MAX)
            pwmthreshold <= bpm_reg;
    end
end

Le premier registre bpm_reg est mis à jour lorsque le signal d’entrée bpm_valid est à 1. Pour mémoriser la valeur d’entrée et pouvoir l’utiliser au moment où l’on en a besoin.
Et le second pwmthreshold est rafraîchi en fin de cycle d’une période de la pwm. Pour éviter d’avoir un changement de valeur en cours de période, et donc un rapport cyclique faux.

Simulation de l’ensemble avec Cocotb

Jusqu’ici nous avons décrit le comportement du composant final en Verilog. Toutes les développeuses ou développeurs HDL le savent très bien, il est impossible de réaliser un projet Verilog (ou autre HDL) sans faire un minimum de simulation.

Pour simuler le composant, il est nécessaire de décrire les stimuli en entrée du composant et de lire/valider les sorties. On va généralement créer un composant hiérarchiquement au-dessus du top de notre composant appelé « testbench » dans lequel nous décrirons les changements de valeurs des entrées au cours du temps. Cette partie peut tout à fait se faire en Verilog.

Cependant, l’idée de mélanger la partie banc de test et composant « synthétisable » n’est pas terrible. En effet on va très vite confondre les deux parties et mélanger les codes. L’exemple de la division est criant : l’opérateur diviser « / » fonctionne très bien dans la partie testbench mais elle pose de gros problèmes dans la partie « synthétisable ».

Pour éviter ce mélange des genres, une solution radicale consiste à utiliser un autre langage pour la partie banc de test. Le C++ et le SystemC sont utilisés depuis longtemps pour cela. S’ils sont utilisés en conjonction avec Verilator ils permettent d’atteindre des puissance/rapidité de simulation inégalées par les simulateurs « propriétaires ».

Une autre méthode consiste à piloter le simulateur Verilog avec un autre programme, on parle alors de cosimulation. C’est le cœur du fonctionnement du module python CocoTB. L’idée ici est d’écrire son banc de test en python, ce qui est nettement plus confortable que du Verilog ou même du C++ (SystemC est une librairie C++ également).

Le testbench pour simuler l’ensemble du projet taptempo se trouve dans le répertoire cocotb/test_taptempo. Pour le simuler il suffit de s’y rendre et d’y exécuter un make.
À condition cependant d’avoir installé cocotb (en python3) et Icarus pour la partie simulateur (On laissera l’appréciation de l’installation au lecteur en fonction de ses affinités linuxdistributive).

La simulation consiste à tester trois appuis sur le bouton à des intervalles différents :

@cocotb.test()
async def debounce_upanddown(dut):
    td = TestTapTempo(dut)
    td.log.info("Running test!")
    await td.reset()
    td.log.info("System reseted!")
    await Timer(1000, units="us")
    td.log.info("up")
    await td.bounce_up(10, bounce_per=(10000, "ns"))
    await Timer(24, units="ms")
    td.log.info("down")
    await td.bounce_down(10, bounce_per=(10000, "ns"))
    await Timer(300, units="ms")
    td.log.info("up")
    await td.bounce_up(10, bounce_per=(10000, "ns"))
    await Timer(30, units="ms")
    td.log.info("down")
    await td.bounce_down(10, bounce_per=(10000, "ns"))
    await Timer(800, units="ms")
    td.log.info("up")
    await td.bounce_up(10, bounce_per=(10000, "ns"))
    await Timer(30, units="ms")

    td.log.info("Wait stable")
    await Timer(1000, units="us")

Cela génère un fichier de « traces » au format VCD particulièrement volumineux de 2,3 Go (qui se compresse à 70 Mo avec xz !) permettant de visionner les signaux au cours du temps grâce à gtkwave:

$ gtkwave -g taptempo.vcd

Et donne la trace suivante :
simulation_taptempo_full

Cette simulation est particulièrement longue (il m’a fallu environ une heure et demie sur mon vieux T430) et génère un fichier de trace monstrueux. En phase de développement on va généralement lancer de petites simulations par modules comme on peut le voir pour le module debounce dans le répertoire cocotb/test_debounce. On changera également certaines constantes de temps pour limiter les «  pas » de simulation consommant inutilement du calcul processeur.

Il est également possible de laisser l’ordinateur écrire les stimuli grâce à la méthode de preuve formelle. C’est la méthode qui a été utilisée ici pour les modules. Les fichiers de configuration se trouvent dans le répertoire formal/*.

Synthèse sur ColorLight

La Colorlight n’est pas initialement une carte de développement pour les FPGA. C’est une carte permettant de piloter des panneaux de LED qui nous agressent un peu partout dans les rues commerçantes. Cependant, un petit malin s’est rendu compte qu’elle était munie d’un FPGA de chez Lattice : l’ECP5.

Ce FPGA possède deux gros avantages :

  • il est relativement gros, suffisamment pour posséder des multiplieurs câblés, des sérialiseurs-désérialiseurs…
  • on peut développer dessus avec une chaîne de développement intégralement opensource !

Jusqu’à la colorlight, les kits de développement ECP5 n’étaient pas donnés puisque les premières cartes débutaient à 100 $ minimum. Mais avec la colorlight, on tombe à 15 $, ce qui en fait un kit de développement ultra bon marché pour se faire la main avec des FPGA.

Et comme tout est opensource, il est aisé d’aller installer les logiciels permettant de synthétiser TapTempo sur sa distribution Linux préférée.
L’explication de l’installation des outils est hors de propos de cet article (un article détaillé sur la colorlight est disponible dans le Hackable 35), mais une fois les outils installés, il suffit de se rendre dans le répertoire synthesis/colorlight du projet et de faire make :

$ make
[...]
Info: Device utilisation:
Info:          TRELLIS_SLICE:   328/12144     2%
Info:             TRELLIS_IO:     3/  197     1%
Info:                   DCCA:     1/   56     1%
Info:                 DP16KD:     0/   56     0%
Info:             MULT18X18D:     0/   28     0%
Info:                 ALU54B:     0/   14     0%
Info:                EHXPLLL:     0/    2     0%
Info:                EXTREFB:     0/    1     0%
Info:                   DCUA:     0/    1     0%
Info:              PCSCLKDIV:     0/    2     0%
Info:                IOLOGIC:     0/  128     0%
Info:               SIOLOGIC:     0/   69     0%
Info:                    GSR:     0/    1     0%
Info:                  JTAGG:     0/    1     0%
Info:                   OSCG:     0/    1     0%
Info:                  SEDGA:     0/    1     0%
Info:                    DTR:     0/    1     0%
Info:                USRMCLK:     0/    1     0%
Info:                CLKDIVF:     0/    4     0%
Info:              ECLKSYNCB:     0/   10     0%
Info:                DLLDELD:     0/    8     0%
Info:                 DDRDLL:     0/    4     0%
Info:                DQSBUFM:     0/    8     0%
Info:        TRELLIS_ECLKBUF:     0/    8     0%
Info:           ECLKBRIDGECS:     0/    2     0%
[...]
ecppack --svf taptempo.svf taptempo_out.config taptempo.bit

On voit ici que les ressources utilisées pour TapTempo sont ridicules par rapport au FPGA utilisé. La curieuse ou le curieux qui voudra « voir » le placement routage dans le FPGA utilisera l’option --gui dans la commande NextPnR pour avoir l’interface graphique :

$ nextpnr-ecp5 --25k --package CABGA256 --speed 6 --json taptempo.json --textcfg taptempo_out.config --lpf taptempo.lpf --freq 25 --gui

Ce qui donne un autre aperçu du remplissage du FPGA.

taptempo routage

Pour télécharger le bitstream dans le FPGA, on pourra utiliser openFPGALoader en donnant simplement le nom du bitstream :

$ openFPGALoader taptempo.bit

Exercices de travaux pratiques

Pour celles et ceux qui ont suivi jusqu’ici et qui voudraient se faire la main avec ce projet, voici quelques propositions de « sujet de TP » :) :

  • utilisation d’un multiplieur câblé de l’ECP5 pour faire la division dans per2bpm ;
  • ajout un module de moyennage sur cinq échantillons pour coller à la spécification initiale de TapTempo ;
  • utilisation d’autres plates‑formes FPGA à bas coût : QuickFeather, FireAnt, Tang Nano

N’hésitez pas à me proposer des demandes d’intégration Git pour améliorer le projet.

Conclusion

On voit que dès que l’on passe dans le domaine de l’embarqué les choses se compliquent et prennent plus de temps. Alors que sur un PC on aurait pu faire ça en une ligne de code, quand on embarque ça dans un microcontrôleur, c’est déjà plus compliqué. Mais si l’on passe dans le monde des FPGA et des ASIC, le projet prend une toute autre dimension. C’est la raison pour laquelle il faut toujours se demander si un FPGA est bien à propos pour notre projet, non seulement cela coûtera plus cher en composant qu’une solution sur étagère, mais en plus le temps de développement (et donc le coût) sera nettement supérieur.

L’idée d’utiliser une touche de télégraphe pour mesurer le tempo n’était peut‑être pas la meilleure, compte tenu des rebonds qui sont relativement violents. Même avec le module lisseur de rebond (debounce), il subsiste quelques rebonds trop longs. Un tempo maximum à 250 n’est pas si rapide et l’on est vite frustré de l’atteindre alors qu’on pourrait mesurer des tempos de musiques plus… rythmées. On peut facilement passer à 300, mais ça reste lent. Si l’on veut un tempo plus rapide, il faut tout d’abord changer la graduation sur le voltmètre, puis modifier le paramètre BPM_MAX dans le code.

On a ici un modèle de projet qui est facile à synthétiser sur n’importe quel petit FPGA. C’est un projet qui peut être intéressant si l’on souhaite se sortir un peu les doigts des LED qui clignotent. La démonstration étant faite du fonctionnement de l’architecture globale, il est aisé de s’en servir pour la réécrire dans d’autres langages de description de matériel comme le VHDL, Chisel (même s’il y en a déjà une pour taptempo), Migen/Litex, MyHDL, Clash (en plus, ça permettrait de débloquer la dépêche LinuxFr.org sur le sujet)…

Pour le curieux, ou la curieuse, qui sera allé voir le code sur le projet GitHub, ce projet a été développé avec une dose de preuves formelles grâce au logiciel libre Yosys-SMTBMC.

Cocotb Tips

Some tips for python HDL test module Cocotb.

Read and Write signal

Write:

clk.value = 1
dut.input_signal <= 12
dut.sub_block.memory.array[4] <= 2

Read:

count = dut.counter.value
print(count.binstr)
print(count.integer)
print(count.n_bits)
print(int(dut.counter))

See it under the official documentation.

Yielding a coroutine in a select list fashion

Question asked on stackoverflow.

Using latest python version with virtualenv

If you compile python yourself, don’t forget to add option --enable-shared at configure time (./configure --enable-shared)

$ virtualenv --python=/usr/local/bin/python3.7 ~/envp37
$ source ~/envp37/bin/activate
$ python -m pip install cocotb

Do not forget to re-source your environnement each time you open a new terminal :

$ source ~/envp37/bin/activate

Logging messages and main test class template

This is a template for declaring a class used for test in function @cocotb.test() :

import logging
from cocotb import SimLog
...

class MyDUTNameTest(object):
    """ Test class for MyDUTName"""
    LOGLEVEL = logging.INFO
    # clock frequency is 50Mhz
    PERIOD = (20, "ns")

    def __init__(self):
        if sys.version_info[0] < 3: # because python 2.7 is obsolete
            raise Exception("Must be using Python 3")
        self._dut = dut
        self.log = SimLog("RmiiDebug.{}".format(self.__class__.__name__))
        self.log.setLevel(self.LOGLEVEL)
        self._dut._log.setLevel(self.LOGLEVEL)
        self.clock = Clock(self._dut.clock, self.PERIOD[0], self.PERIOD[1])
        self._clock_thread = cocotb.fork(self.clock.start())

# ....

@cocotb.test()
def my_test(dut):
    mdutn = MyDUTNameTest()
    mdutn.log.info("Launching my test")

Les traces VCD se compressent bien

La simulation HDL génère assez vite des traces (waves) de plusieurs centaines de méga-octets, voir des dizaines de giga.

Ces traces sont le plus souvent au format VCD, qui n’est qu’un fichier texte répétant les même schéma au court du temps. Ces fichiers ayant énormément de redondance, ils se compressent très bien.

Si je prend par exemple un fichier vcd messignaux.vcd faisant ~500Mo :

$ ls messignaux.vcd
-rw-r--r-- 1 fabien fabien 504M avril 3 10:17 messignaux.vcd

Je compresse avec zip en environ 30 secondes:

$ zip -c messignaux.zip messignaux.vcd
adding: messignaux.vcd (deflated 89%)
$ ls -lha messignaux.zip
-rw-r--r-- 1 fabien fabien 56M avril  5 14:16 messignaux.zip

Et je réduis mon fichier à 56Mo soit un rapport de presque 10.

Avec gzip on gagne la même chose à une queue de vache près :

$ gzip messignaux.vcd
$ ls -lha messignaux.gz
-rw-r--r-- 1 fabien fabien 57M avril  3 10:17 messignaux.vcd.gz

Et avec xz alors là on réduit environs à 20 fois plus petit, … mais il faut plus de 5 minutes à xz pour faire la compression:

$ xz -z messignaux.vcd
$ ls -lha messignaux.vcd.xz
-rw-r--r-- 1 fabien fabien 27M avril  3 10:17 messignaux.vcd.xz

Bref, si vos traces de simulation prennent trop de place sur votre pc, n’hésitez pas à les compresser.