URL: https://linuxfr.org/news/taptempo-en-verilog Title: TapTempo en Verilog Authors: martoni Yves Bourguignon, Ysabeau đŸ§¶, Davy Defaud, BAud, Xavier Teyssier, BenoĂźt Sibaud, FrĂ©dĂ©ric Massot, palm123, claudex, bobble bubble et mzf Date: 2020ćčŽ09月13æ—„T14:25:36+02:00 License: CC By-SA Tags: open_hardware, taptempo, matĂ©riel, hdl et verilog Score: 79 Le projet [TapTempo](https://linuxfr.org/tags/taptempo/public) semble faiblir depuis quelques mois maintenant. En panne de langage informatique pour en faire une dĂ©pĂȘche ? N. D. M. — TapTempo est un _dĂ©tecteur de tempo_ oĂč _l’utilisateur frappe une touche en cadence rĂ©guliĂšre et le programme en dĂ©duit le tempo correspondant._ Il a Ă©tĂ© dĂ©clinĂ© en de multiples langages de programmation. 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](https://fr.wikipedia.org/wiki/Langage_de_description_de_mat%C3%A9riel "hardware description language")) : le [Verilog](https://en.wikipedia.org/wiki/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](https://github.com/q3k/chubby75/blob/master/5a-75b/hardware_V7.0.md). ![Vue d’ensemble du montage TapTempo](http://fabienm.eu/partage/taptempocolorlight.JPG) ---- [Le projet TapTempoASIC dĂ©crit en Verilog](https://github.com/Martoni/TapTempoASIC) [La vidĂ©o de TapTempo en action sur une carte Colorlight (ECP5)](https://www.youtube.com/watch?v=03z5ehVXYvc) [ColorLight 5A-75B](https://github.com/q3k/chubby75/blob/master/5a-75b/hardware_V7.0.md) ---- 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](https://github.com/Martoni/TapTempoASIC/blob/master/hdl/taptempo.v) : ```Verilog 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 : ```Verilog /* 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’à 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 : ```Verilog /* 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 pris en charge 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](http://fabienm.eu/partage/schema_taptempo_crayon.jpg) 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 une pulsation 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Ă©](https://en.wikipedia.org/wiki/Metastability_(electronics)). ```Verilog /* 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 « antirebond » `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 processeur — 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](http://www.fabienm.eu/flf/et-pourquoi-pas-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](http://fabienm.eu/partage/taptempo_entree_morse_modif.jpg) 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](http://fabienm.eu/partage/voltmetre_taptempo.jpg) 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 modulation de largeur d’impulsion, en anglais _Pulse Width Modulation_ — PWM. 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 (5 V) Ă  250. ![PWM](http://fabienm.eu/partage/pwm_taptempo.jpg) La pĂ©riode de la modulation de largeur d’impulsion sera configurĂ©e suffisamment rapide pour que l’aiguille n’oscille pas. Pulsation de temporisation ([timepulse](https://github.com/Martoni/TapTempoASIC/blob/master/hdl/timepulse.v)) -------------------------------------- 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 5 120 ns : ```Verilog 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 5 120 ns on dĂ©finit un registre de comptage : ```Verilog `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 : ```Verilog 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 : ```Verilog assign tp_o = (counter == 0); ``` Gestion des rebonds ([debounce](https://github.com/Martoni/TapTempoASIC/blob/master/hdl/debounce.v)) ------------------------------ 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` : ```Verilog 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 : ```Verilog `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 Ă  quatre Ă©tats : ```Verilog /* 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 ». ```Verilog 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 : ```Verilog 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, et 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 : ```Verilog assign btn_o = (state_reg == s_cnt_high) || (state_reg == s_wait_high); ``` Mesure de la pĂ©riode de tempo ([percount](https://github.com/Martoni/TapTempoASIC/blob/master/hdl/percount.v)) ---------------------------------------- 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. ```Verilog `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 ! ```Verilog 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 : ```Verilog 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 : ```Verilog 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](https://github.com/Martoni/TapTempoASIC/blob/master/hdl/per2bpm.v)) --------------------------------------------------- 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 : ```Verilog /* inputs */ input [(`BTN_PER_SIZE-1):0] btn_per_i, input btn_per_valid, ``` Et on doit en ressortir une frĂ©quence (BPM) : ```Verilog /* outputs */ output [`BPM_SIZE - 1:0] bpm_o, output bpm_valid ``` Suivant la formule : $$ bpm = \frac{\frac{MIN\_NS}{TP\_NS}}{btn\_per\_i}$$ Il faut donc diviser la constante $\frac{MIN\_NS}{TP\_NS}$ 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$ ```Verilog 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Ă©e Ă  la constante Ă  diviser : ```Verilog `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` : ```Verilog 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 : ```Verilog 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 : ```Verilog quotient <= {quotient[(`DIVIDENTWITH-2):0], 1'b0}; ``` - dans tous les cas, on dĂ©cale le diviseur Ă  droite : ```Verilog divisor <= {1'b0, divisor[(`REGWIDTH-1):1]}; ``` La division est orchestrĂ©e par une machine Ă  trois Ă©tats : ```Verilog 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` : ```Verilog assign bpm_o = quotient[(`BPM_SIZE-1):0]; assign bpm_valid = (state_reg == s_result); ``` GĂ©nĂ©ration de la tension de sortie ([pwmgen](https://github.com/Martoni/TapTempoASIC/blob/master/hdl/pwmgen.v)) ------------------------------------------- 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) : ```Verilog /* 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 : ```Verilog 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` : ```Verilog 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. Le second `pwmthreshold` est rafraĂźchi en fin de cycle d’une pĂ©riode de la modulation de largeur d’impulsions. 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 il 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](https://accellera.org/downloads/standards/systemchttps://accellera.org/downloads/standards/systemc) sont utilisĂ©s depuis longtemps pour cela. S’ils sont utilisĂ©s en conjonction avec [Verilator](https://linuxfr.org/news/verilator-4-002), ils permettent d’atteindre des puissance et 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](https://linuxfr.org/news/cocotb-1-4-0-la-maturite). 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 Ă©galement une librairie C++). Le test 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 Python 3) et [Icarus](https://github.com/steveicarus/iverilog) pour la partie simulateur (on laissera l’apprĂ©ciation de l’installation au lecteur en fonction de ses affinitĂ©s linuxdistributive). [La simulation](https://github.com/Martoni/TapTempoASIC/blob/master/cocotb/test_taptempo/test_taptempo.py) consiste Ă  tester trois appuis sur le bouton Ă  des intervalles diffĂ©rents : ```python @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 Gio (qui se compresse Ă  70 Mio avec xz !) permettant de visionner les signaux au cours du temps grĂące Ă  [GTKWave](https://linuxfr.org/news/simplifier-la-visualisation-de-chronogrammes) : ```Shell $ gtkwave -g taptempo.vcd ``` Et donne la trace suivante : ![simulation_taptempo_full](http://fabienm.eu/partage/full_taptempo_simu.png) 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](https://fr.aliexpress.com/item/32281130824.html) 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](https://github.com/q3k/chubby75/tree/master/5a-75b) s’est rendu compte qu’elle Ă©tait munie d’un FPGA de chez Lattice : l’[ECP5](https://www.latticesemi.com/en/Products/FPGAandCPLD/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 _open source_ ! Jusqu’à la Colorlight, les kits de dĂ©veloppement ECP5 n’étaient pas donnĂ©s puisque [les premiĂšres cartes dĂ©butaient Ă  100 US$ minimum](https://www.latticesemi.com/en/Solutions/Solutions/SolutionsDetails01/CommunitySourced). Mais avec la Colorlight, on tombe Ă  15 US,ăƒ‰ăƒ« ce qui en fait un kit de dĂ©veloppement ultra bon marchĂ© pour se faire la main avec des FPGA. Et comme tout est _open source_, il est aisĂ© d’aller installer les logiciels permettant de synthĂ©tiser TapTempo sur sa distribution GNU/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_ n^(o) 35](https://www.hackable.fr/)), mais une fois les outils installĂ©s, il suffit de se rendre dans le rĂ©pertoire `synthesis/colorlight` du projet et de faire `make` : ```Shell $ 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 : ```Shell $ 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](http://fabienm.eu/partage/taptempo_routing.png) Pour tĂ©lĂ©charger le _bitstream_ dans le FPGA, on pourra utiliser [openFPGALoader](https://github.com/trabucayre/openFPGALoader) en donnant simplement le nom du _bitstream_ : ```Shell $ 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](https://www.crowdsupply.com/quicklogic/quickfeather), [FireAnt](http://www.fabienm.eu/flf/fireant-un-petit-nouveau-dans-le-monde-du-fpga-a-bas-cout/), [Tang Nano](https://tangnano.sipeed.com/en/)... 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](https://linuxfr.org/users/zerodeux/journaux/taptempo-en-une-ligne), quand [on embarque ça dans un microcontrĂŽleur](https://linuxfr.org/users/belegar--2/journaux/taptempo-sur-stm32f469i-discovery), 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](https://github.com/Martoni/TapTempoASIC/blob/master/hdl/debounce.v)), 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](https://linuxfr.org/users/martoni/journaux/integration-de-taptempo-chisel-sur-apf27) pour taptempo), Migen/Litex, MyHDL, Clash (en plus, ça permettrait de dĂ©bloquer [la dĂ©pĂȘche _LinuxFr.org_](https://linuxfr.org/redaction/news/sortie-de-c-ash-1-0) 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](https://fr.wikipedia.org/wiki/M%C3%A9thode_formelle_(informatique)#Preuves_formelles) grĂące au logiciel libre [Yosys-SMTBMC](http://www.clifford.at/papers/2017/smtbmc-sby/slides.pdf).

AltStyle ă«ă‚ˆăŁăŠć€‰æ›ă•ă‚ŒăŸăƒšăƒŒă‚ž (->ă‚ȘăƒȘă‚žăƒŠăƒ«) /