Le projet TapTempo 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) : 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.
Sommaire
- Le Verilog
- Architecture de TapTempo
- Simulation de l’ensemble avec Cocotb
- Synthèse sur ColorLight
- Exercices de travaux pratiques
- Conclusion
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’à 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 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 :
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é.
/* 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 » — 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 » :
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 :
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.
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)
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 :
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 :
`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 à quatre é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, 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 :
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
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 :
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 :
`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. 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 sont utilisés depuis longtemps pour cela. S’ils sont utilisés en conjonction avec Verilator, 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. 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 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 Gio (qui se compresse à 70 Mio avec xz !) permettant de visionner les signaux au cours du temps grâce à GTKWave :
$ gtkwave -g taptempo.vcd
Et donne la trace suivante :
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 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. 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 no 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 :
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.
Aller plus loin
- Le projet TapTempoASIC décrit en Verilog (36 clics)
- La vidéo de TapTempo en action sur une carte Colorlight (ECP5) (205 clics)
- ColorLight 5A-75B (35 clics)
# Chouette article
Posté par quebert (site web personnel) . Évalué à 5.
Ça donne envie de s'y mettre!
estsera disponible: teasing sur le sommaire du prochain numéro à paraître?[^] # Re: Chouette article
Posté par Ysabeau 🧶 (site web personnel, Mastodon) . Évalué à 7.
Ça m’a aussi donné envie de m’y mettre à la première lecture à vrai dire. Après et aux lectures suivantes, je me suis rendu compte que ce n’était pas du tout à ma portée mais ça reste une très chouette dépêche.
« Tak ne veut pas quʼon pense à lui, il veut quʼon pense », Terry Pratchett, Déraillé.
[^] # Re: Chouette article
Posté par Pierre Jarillon (site web personnel) . Évalué à 10.
La programmation bas niveau est très intéressante et cet article fait découvrir les types de difficultés à surmonter pour faire des choses que l'on peut croire très simples.
Lors de ma thèse, je m'étais intéressé aux contacts et à leurs défauts. Il n'y a qu'un seul type de contact qui soit quasiment parfait, ce sont les contacts mouillés au mercure. Tous les autres sont affectés par des rebondissements parfois très importants (ils ont aussi d'autres défauts).
Derrière un clavier, un simple interrupteur ou un bouton poussoir, le traitement des rebondissements est impératif. Cet article montre ce qu'il a fallu faire pour masquer ce problème aux utilisateurs du niveau suivant.
[^] # Re: Chouette article
Posté par martoni (site web personnel, Mastodon) . Évalué à 6.
Tout à fait ;)
J'ai plus qu'une balle
[^] # Re: Chouette article
Posté par tao popus . Évalué à 4.
Super article pour découvrir ce monde, merci. Et effectivement, cette carte à l'air intéressante, elles à plein de connecteurs en plus du FPGA, bonne découverte.
# Debounce ?
Posté par Tonton Th (Mastodon) . Évalué à 3.
Dans le passé, j'ai utilisé un composant électronique spécialise de chez Maxim : le MAX6818.
Le jury auto-proclamé peut-il confirmer que cette solution ne brise pas les règles du taptempisme ?
# Quid TapTempo ?
Posté par Thomas Capricelli (site web personnel) . Évalué à 2.
C'est intéressant. Mais c'est quoi TapTempo ???? Si je cherche dans google je tombe sur cett page. Et ça fait bizarre de devoir chercher sur internet pour comprendre de quoi parle l'article :-/
[^] # Re: Quid TapTempo ?
Posté par Benoît Sibaud (site web personnel) . Évalué à 3.
Corrigé, merci.
[^] # Re: Quid TapTempo ?
Posté par Nicolas Boulay (site web personnel) . Évalué à 6.
C'est un logiciel qui permet de mesurer un tempo en appuyant sur une touche, cela permet de faire une démonstration de langage utilisant ses entrées sorties de façon simple et fonctionnelle.
Il existe plein de port de taptempo sur linuxfr.
"La première sécurité est la liberté"
[^] # Re: Quid TapTempo ?
Posté par BAud (site web personnel) . Évalué à 4.
ouais, parce que LinuxFr sait faire :-)
# IHM
Posté par Anonyme . Évalué à 4. Dernière modification le 21 septembre 2020 à 03:12.
C'est extrêmement compliqué pour afficher les trois chiffres des BPM. On peut plus simplement utiliser des afficheurs 7 segments avec ou sans décodeur BDC intégré. Ça n'est certainement pas aussi élégant de simplicité que la solution du voltmètre mais ça ne requiert qu'un peu plus de blocs logiques avec éventuellement un multiplexage depuis le signal d'horloge si on manque de sorties ou si on utilise un afficheur LCD. Dans le temps ça se faisait avec quelques puces spécialisées donc pas du tout besoin de la complexité d'un softcore ni même de celle d'un signal VGA.
Aussi, ça ne réduit pas la précision si BPM_MAX augmente, contrairement à un mouvement d'Arsonval.
On pourrait se passer entièrement de la fonction de debounce en choisissant un interrupteur qui n'en produit pas du tout, comme par exemple un bouton poussoir momentané utilisant l'effet Hall.
Évidemment avec tout ça le look sera beaucoup moins rétro et ça sera moins portable.
# Debounce materiel
Posté par meumeul . Évalué à 7.
Et si on gagnait en complexité avec un bête debounce matériel ? Un petit condensateur et le tour est joué !
On peut aussi aller chercher une puce trigger de Schmitt…
# je sais pas vous...
Posté par djibb (site web personnel) . Évalué à 10.
mais moi j'aimerais bien voir le tout en fonctionnement via une vidéo…
[^] # Re: je sais pas vous...
Posté par aplc (site web personnel, Mastodon) . Évalué à 2.
mais moi je trouve qu'il y a de grands malades sur ce site :-)
[^] # Re: je sais pas vous...
Posté par Selso (site web personnel) . Évalué à 3.
Bien tu cliques sur le lien en fin d'article…
[^] # Re: je sais pas vous...
Posté par djibb (site web personnel) . Évalué à 2.
MERCI !!!
# bon article
Posté par Selso (site web personnel) . Évalué à 2.
Salut !
Merci du partage, je trouve l'article bien complet, il suscite l'espoir que nous aussi on peut y arriver :) Je l'ai lu plusieurs fois en diagonale mais je vais essayer de m'y plonger sérieusement.
Cependant j'ai eu l'impression que les détails et le contexte sont inversés, par exemple tu expliques comment trouver la carte utilisée après avoir donnés des listing de code (je sais on appelle pas ça comme ça en vérilog mais j'ai oublié).
Et je crois que les explications et exemples de taptempo sont données après ces listings. Perso j'ai pas réfléchi de suite à ce que ça pouvait être et j'ai cherché dans ton article de quoi il en retournait.
Aussi pour le matériel de brocante que tout le monde n'a pas, on peut très bien le remplacer par un petit bouton poussoir et un simple multimètre numérique, voir couplé avec une capacité pour lisser un peu la pwm, n'est-ce pas ?
cdlt,
[^] # Re: bon article
Posté par martoni (site web personnel, Mastodon) . Évalué à 5.
Je parle de la colorlight dans l'introduction. Et l'étape de synthèse vient à la fin dans le développement FPGA. Même si c'est une bonne pratique de tester la synthèse régulièrement sur chaque module, pour être sur que ce que l'on écrit est synthétisable.
C'est une idée le multimètre numérique en effet, il faudra que j'essaie ;)
J'ai plus qu'une balle
[^] # Re: bon article
Posté par Selso (site web personnel) . Évalué à 0.
Salut,
Je comprends ton point de vue, tu complètes le sujet en suivant tes étapes de développement.
Mais pour la synthèse d'article on peut présenter l'environnement de développement à l'intro. C'est aussi plus facile pour les non coutumier comme moi des travaux sur FPGA.
Concrètement je pense qu'à la suite de l'intro :
Tu pouvais mettre :
[^] # Re: bon article
Posté par Anonyme . Évalué à 4.
Les galvanomètres encastrables à aiguille ce n'est pas du "matériel de brocante". C'est encore fabriqué mais pas seulement pour le look aussi car ce n'est pas nécessairement rendu obsolète par les afficheurs numériques. Chaque techno a ses avantages et ses inconvénients selon les usages.
[^] # Re: bon article
Posté par Selso (site web personnel) . Évalué à 1.
Je parlais de l'ensemble, :)
Son galva paraît ancien, même si on continue de fabriquer du matériel analogique (même des touches de télégraphe) le multimètre numérique est plus répandu.
Bon la conversation prend une tangente qui n'apporte rien à l'article.
Mais au fait c'est pas une carte prévue pour piloter des panneaux à LED ? :D
[^] # Re: bon article
Posté par KiKouN . Évalué à 8.
Les supercars ont un problème nécessitant encore des aiguilles. L'indicateur de vitesse par affichage digitale ne permet pas de donner une bonne représentation de l'accélération de la voiture. L'affichage digitale étant fixe un certain moment dans le but de permettre la lecture de la valeur.
Rien qu'en moto, je me fis au compte-tour à aiguille plutôt qu'à l'indicateur de vitesse digital pendant une grosse accélération. Bien que le compte-tour soit géré numériquement, la lecture d'un affichage à aiguille est toujours possible malgrès un rafraîchissement presque permanent, Chose impossible en digitale, et permet une visualisation de son évolution.
[^] # Re: bon article
Posté par Grunt . Évalué à 5.
Sans même avoir de grosses accélérations, je connais deux personnes qui préfèrent conduire avec un affichage à aiguille car ça leur est plus pratique que "un chiffre qui bouge tout le temps".
THIS IS JUST A PLACEHOLDER. YOU SHOULD NEVER SEE THIS STRING.
# Alors bravo, mais j'ai quand même l'impression que
Posté par Goffi (site web personnel, Mastodon) . Évalué à 1.
tu bluffes martoni !
[^] # Re: Alors bravo, mais j'ai quand même l'impression que
Posté par Colin Pitrat (site web personnel) . Évalué à 4.
P't'être qu'il bluffe hein, p't'être qu'il avait qu'un seul FPGA …
[^] # Re: Alors bravo, mais j'ai quand même l'impression que
Posté par BAud (site web personnel) . Évalué à 2.
il dit qu'il n'a plus de genou
aréoport de nice, 2 min d'arrêt !
# j'ai quelques questions msieur !
Posté par Selso (site web personnel) . Évalué à 1.
J'ai lu l'article au complet mais des choses m'échappe, si tu peux m'éclaircir :)
Métastabilité :
J'ai consulté 'rapidement' l'article et j'ai compris le problème de propagation du signal sur des bascules avec plusieurs clocks dans le système.
Mais est-ce c'est notre cas ? Tous les signaux clock et pulse ont la même base non ? Ou considères-tu qu'ils peuvent dériver suffisamment pour créer le phénomène ?
Pour le listing, j'avoue que réaliser la double bascule en passant btn_i dans btn_old, puis dans btn_s, ça me dépasse un peu.
Je pense qu'il y a un fondamentale avec l'exécution et la notion de registre qui me manque.
Je pensais aussi que tout s'exécutait en même temps dans le FPGA et donc que l'on aurait dans la même clock les deux opérations, mais ç'est vrai qu'il ne peut pas réaliser la seconde sans avoir exécuté la première d'où le "forçage" de l'utilisation de bascule à la synthèse.
Calcul bpm : pas suivi le calcul
MIN_NS 6e10 (1 minute en nanoseconde)
TP_NS = ??
Peut-être aussi que btn_per_i est aussi divisé par TP_NS mais cela ne m'a pas paru évident.
Entrée/sortie de module
L'entrée de per_count c'est btn_i (qui existait déjà mais est-ce le même), mais la sortie de debounce c'est btn_o.
J'imagine donc que ces variables sont locales au module.
A quel moment se fait le lien entre les Entrées/Sortie des modules ?
Division :
Est-ce que tu as essayé de voir ce que ça donnait avec le logiciel de synthèse, ou tu es certains que cela n'en vaut pas la peine et qu'il ne faut jamais compter dessus ?
Quelque part tu l'as posé le calcul, le logiciel ne peut pas le reproduire ?
[^] # Re: j'ai quelques questions msieur !
Posté par Nicolas Boulay (site web personnel) . Évalué à 6.
Métastabilité :
C'est un problème lors qu'une bascule "lit" un signal non stable. Selon le design de la bascule (FlipFlop ou 2 latchs), il existe un temps de "sampling" et si cette valeur n'est pas à 0 ou 1, il y a n'importe quoi derrière y compris une oscillation. Cela arrive quand on lit une entrée numérique qui n'est pas synchrone avec l'intérieur de la puce, ce qui est pratiquemnt toujours le cas.
Une façon de lire et de tuer toute meta stabilité (l'oscillation) est de mettre 2 bascules à la suite (voir 3 dans certains designs). C'est plus une histoire de probabilité d'occurence qui baisse.
Division :
La division entière est un truc monstrueux en place, encore pire qu'une multiplication. Par contre, il existe un algo itératif qui permet de permet de faire une division 32 bits en 32 cycles. Il existe aussi des inverses de racine carré bien plus rapide qu'une division classique (instruction PowerPC de mémoire)
En théorie, cela serait possible. En pratique, toute opération se fait en asynchrone et ce sont les bascules décrites qui définissent les barrières temporelles. Il existe des synthétiseurs capable de répartir des opérations sur des suites de bascules reliées entre elle, mais ils ne le font pas tous (option "balanced_pipeline").
"La première sécurité est la liberté"
[^] # Re: j'ai quelques questions msieur !
Posté par martoni (site web personnel, Mastodon) . Évalué à 6.
Le signal provenant du bouton n'est pas synchronisé avec l'horloge du FPGA. Il est donc possible qu'un front (montant ou descendant) survienne en même temps que le front montant de l'horloge.
Or, si cela arrive on viole les temps de setup/hold et le comportement du registre d'entré devient analogique. La valeur de sortie de la bascule va rester dans un état «entre-deux» pendant un temps plus ou moins aléatoire puis elle va basculer vers 0 ou 1. Mais ce temps peut devenir suffisamment long pour que la bascule suivante récupère cette valeurs analogique-instable et la transmette elle même dans tout le design. Cette instabilité sera d'autant plus amplifiée qu'il y aura de la logique entre les deux bascules.
C'est la raison pour laquelle il est recommandé de mettre deux bascules D en série d'un signal d'entrée qui n'est pas synchronisé avec l'horloge du système. Cela diminue la probabilité de métastabilité, mais ça peut tout de même arriver. Par contre si cette double bascule n'est pas mise, il est à peu prêt certain que l'on aura des problèmes de métastabilité, et le comportement du système sera vraiment bizarre.
Ma référence sur la métastabilité et le franchissement de domaine d'horloge.
btn_per_i compte le nombre de période de timepulse TP_NS. Une autre manière d'écrire l'équation serait :
Tout à fait, les liens entre les modules se font par les ports que l'on connecte dans le composant «TOP»
Dans le top qui s'appelle taptempo.
La connexions du module debounce est donné par le code suivant :
J'avoue ne pas avoir essayé, mais je ne suis pas sûr que Yosys aurait suivi. C'est fondamentalement une mauvaise idée de toute manière. Le verilog n'est pas un langage de description «de haut niveau», il faut le voir un peut comme le «langage assembleur» des FPGA. Mais même avec des langages générateurs comme Migen/Chisel/SpinalHDL/Clash/… tu ne fait qu'instancier un «module» de division explicitement. Si l'on veut juste décrire l'algoritme ou le calcul et laisser le logiciel trouver comment l'architecturer il faut passer à la «synthèse de haut niveau».
Ça n'est pas le rôle d'un logiciel de synthèse, c'est par contre le rôle d'un logiciel de synthèse de haut niveau dit «HLS» pour High Level Synthesis. Mais dans ce cas on prend carrément un programme en C, C++, Matlab, Python que le logiciel va tenter de convertir en une architecture logiciel+hardware.
J'ai plus qu'une balle
[^] # Re: j'ai quelques questions msieur !
Posté par Selso (site web personnel) . Évalué à 1. Dernière modification le 25 septembre 2020 à 21:36.
Merci pour les réponses.
pour la métastabilité : j'ai consulté l'image wikipedia et je voyais dans le diagramme qu'il y avait un soucis pour à la sortie de la seconde bascule, car celle-ci comportait une autre clock que la première relié à l'entrée. Mais je veux bien croire que le problème existe (sinon lire les 55 pages de ta référence).
Néanmoins l'implémentation des bascules m'étonne car on passe dans la même itération le signal dans deux valeurs. Il y a une notion basique que j'ignore.
calcul bpm : merci c'est beaucoup plus clair.
calcul division : ok solution haut niveau ou choisir un module de division.
[^] # Re: j'ai quelques questions msieur !
Posté par martoni (site web personnel, Mastodon) . Évalué à 5. Dernière modification le 26 septembre 2020 à 07:48.
Tu veux sans doute parler de ce code :
Toute l'explication se trouve dans l'opérateur d'affectation non bloquante
<=
. Cet opérateur signifie que l'affectation ne sera effective qu'à la sortie du processus (always@).Donc si l'on regarde les deux lignes suivantes :
J'ai plus qu'une balle
[^] # Re: j'ai quelques questions msieur !
Posté par Selso (site web personnel) . Évalué à 2.
Super merci c'était l'explication qui me manquait.
Astucieux l'opérateur je me demandais pourquoi utiliser deux caractères pour une affectation.
Merci pour le lien, il y a pleins d'autres article orientés beginner.
Je sais pas si un jour je ferais réellement quelque chose avec mais c'est devenu très abordable question devkit et de plus en plus d'article apparaissent pour le démocratiser.
Et l'on trouve aussi de plus en plus d'architectures hybrides ou processeur avec des unités FPGA intégrées, les devs classiques devront savoir comment tirer parti de ça.
Suivre le flux des commentaires
Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.