Cette page est la troisième d'une série de cinq pages consacrée au métier de concepteur FPGA. Sur cette page, je vais tenter de décrire les compétences pratiques les plus importantes pour être concepteur FPGA. Je vais aussi essayer de clarifier pourquoi chaque compétence est importante. Ne sautez pas la partie sur le Verilog, même si elle semble évidente — j'ai peut-être quelques choses peu évidentes à en dire.
Et avant de commencer, je vais expliquer pourquoi je ne mentionne que le Verilog et pas le VHDL : simplement parce que le Verilog est recommandé, car il semble qu'il va devenir le langage HDL dominant dans le futur. C'est entre autres parce que le VHDL est rarement utilisé en Chine. Mais tout ce qui est dit ci-dessous est vrai pour le VHDL aussi.
Les deux visages du Verilog
La raison pour laquelle connaître le Verilog est requis n'a guère besoin d'explication. En fait, beaucoup de gens pensent que savoir le Verilog équivaut à être concepteur FPGA. Un ou deux chefs de projet ont envoyé leurs programmeurs juniors à un cours de Verilog, et ont été déçus quand ils ne sont pas revenus en champions du FPGA.
Donc tout d'abord, connaître la syntaxe du Verilog est loin de savoir utiliser ce langage. C'est vrai pour n'importe quel langage, mais avec le Verilog l'écart est bien plus grand. Deuxièmement, il est important de comprendre que le code Verilog n'est pas un programme informatique. Il décrit du matériel. Par conséquent, en principe, tout se passe en parallèle. C'est le basculement mental avec lequel beaucoup de développeurs logiciels ont du mal, en particulier ceux qui ne sont pas habitués à la programmation multi-thread.
La question oui-ou-non du langage de programmation est en fait un peu plus compliquée, parce que le Verilog est utilisé à deux fins différentes : la simulation du matériel et la création de logique pour un FPGA ou un ASIC (synthèse).
Le but d'une simulation est souvent de vérifier le code Verilog avant de le synthétiser en logique dans le FPGA. Pour ce faire, on écrit un banc de test (testbench), qui est essentiellement un programme informatique qui crée artificiellement des signaux de stimulus, et fait quelque chose avec les signaux de sortie provenant du morceau de logique que l'on teste : généralement écrire les valeurs des signaux dans un fichier, ou peut-être vérifier qu'elles sont conformes aux attentes.
Quand le Verilog est utilisé pour la simulation, c'est réellement un langage de programmation, mais rien à voir avec le C, Python ou JavaScript. Pourtant, l'ordinateur fait exactement ce qu'on lui a demandé : quand la simulation s'exécute, le banc de test (testbench) ainsi que le code qu'on destine à la synthèse se comportent exactement comme défini par la syntaxe.
Et voici le piège énorme : quand le même code Verilog qu'on a testé est utilisé pour la synthèse, le Verilog n'est plus un langage de programmation. Il décrit de la logique, de la même manière que le HTML décrit ce qui doit s'afficher sur une page web. Pire encore, et contrairement au HTML, l'interprétation du code Verilog peut être assez floue.
Il est important de comprendre que synthétiser du code Verilog est une sorte de magie. Nous, humains, décrivons le comportement voulu, et le synthétiseur (synthesizer) l'implémente tant bien que mal à l'aide de ressources logiques. Par conséquent, nous devons définir ce comportement avec des schémas de codage spécifiques que le synthétiseur (synthesizer) reconnaît et pour lesquels il génère la logique voulue. Contrairement à la programmation classique, on ne peut pas écrire simplement ce qu'on veut. Nous devons réfléchir aux éléments logiques que le synthétiseur (synthesizer) générera en réponse à nos demandes. Le code Verilog n'est qu'une indication pour le synthétiseur (synthesizer) sur la logique à générer.
À l'ère de l'IA, je dois aussi ajouter : le Verilog, ce n'est pas du « vibe coding ». C'est un langage formel, et le synthétiseur (synthesizer) n'est pas un LLM qu'on amadoue pour qu'il fasse ce qu'on veut. Les synthétiseurs (synthesizers) existaient bien avant que les LLM n'existent, et ils interprètent notre code selon des règles strictes, et non selon un réseau neuronal entraîné.
Et surtout : si on écrit du code Verilog médiocre, la logique ne se comportera pas comme la simulation. Le simulateur fera exactement ce qu'on veut, parce que le simulateur fait ce que le code dit. Le synthétiseur (synthesizer), lui, peut produire silencieusement une logique qui fait quelque chose de différent, si on ne prend pas soin d'écrire correctement le code Verilog.
Ce point est crucial et mérite d'être répété : quand le Verilog est utilisé pour la simulation, c'est un langage de programmation, même s'il est plutôt médiocre. Tout ce qu'on a à faire est d'avoir une syntaxe correcte, et le simulateur fait exactement ce qu'on veut. Le synthétiseur (synthesizer), en revanche, peut générer une logique complètement différente de la simulation, même si la syntaxe Verilog est parfaitement correcte. Avec un peu de chance, le synthétiseur (synthesizer) émettra un avertissement signalant qu'on risque de ne pas obtenir ce qu'on attendait. Espérons qu'on verra cet avertissement, malgré le fait qu'il soit enterré parmi des centaines d'autres. Avec encore plus de chance, le synthétiseur (synthesizer) s'arrêtera avec une erreur. Mais trop souvent, on obtient un bug silencieux. Pour l'éviter, il faut savoir quels schémas de codage sont acceptables pour la synthèse. Et ne pas essayer autre chose.
Et maintenant vient la question : comment apprendre le Verilog correctement ?
Compétences Verilog de base
La première étape est d'apprendre la syntaxe, bien sûr. Il y a beaucoup de livres et de tutoriels sur le Verilog. Malheureusement, un grand nombre d'entre eux passent en revue absolument tout, ce qui est non seulement inutile, mais peut aussi vous inciter à utiliser des schémas de codage que les synthétiseurs (synthesizers) gâcheront.
Je suggère l'ensemble minimal de sujets suivant. Apprenez-les à partir de n'importe quelle source que vous trouverez, et vous êtes bon en ce qui concerne la syntaxe. Si vous tombez sur quelque chose que vous ne connaissez pas dans du code Verilog que vous croisez, demandez à votre IA préférée ce que cela signifie. Ce n'est pas plus compliqué que ça. Voici donc ma liste de courses :
- Modules et instances, ports (input, output, output reg, inout).
- Fils (wires) et registres. Les registres qui créent des bascules (flip-flops) et ceux qui n'en créent pas (avec always @(*) et similaires).
- Affectations avec « assign » vs. avec always @(posedge clk) vs. always @(*). '=' vs '<=' à l'intérieur des blocs always.
- Instructions if
- Instructions case et leur utilisation pour les machines d'état (state machines), les multiplexeurs (muxes) et les ROM.
- Constantes en Verilog, par exemple 16'h1234 et 4'b1001. Comprendre aussi x et z en tant que valeurs.
- Opérations arithmétiques et logiques en Verilog. Comprendre la différence entre & et &&, par exemple. Opérateurs de réduction unaires, par exemple &myreg. Vrai/faux en tant que valeur logique, par exemple myreg <= (this == 1).
- Manipulations de bits : sélection de bits (par exemple myreg[3:2]) ainsi que concaténation (par exemple { myreg1, myreg2 }) et duplication (par exemple { 8{myreg} } ).
- Paramètres de module (parameter et localparam).
- Utiliser de l'IP (IP) et l'instancier (instantiation) dans votre module Verilog. Ce n'est pas une compétence purement Verilog, car cela implique d'utiliser l'outil de développement pour créer et configurer un bloc IP (IP block) (une FIFO, ou une PLL, par exemple). Et pourtant, l'essentiel du travail consiste à instancier (instantiation) et connecter les ports dans votre conception, et c'est une compétence Verilog.
- Le bloc « initial » pour initialiser les registres à la mise sous tension ainsi que pour définir des variables en simulation.
- Commandes utilisées en simulation uniquement : $stop, $finish, $display, $readmemh, et l'usage intensif de « initial ».
- Délais temporels avec « # », et leur utilisation dans les simulations.
- La directive `timescale (et son manque d'importance dans bien des cas).
- Instructions generate — moins importantes pour débuter, mais largement utilisées.
- Attributs de synthèse. Ce sont des instructions directes au synthétiseur (synthesizer), et leur syntaxe varie d'un fournisseur à l'autre. Par exemple, DONT_TOUCH = "TRUE" est souvent utilisé pour dire au synthétiseur (synthesizer) de ne pas éliminer un registre par optimisation, même si cela économiserait de la logique. Assurez-vous donc de parcourir la liste des attributs de ce type que votre synthétiseur (synthesizer) prend en charge, car ils peuvent être très utiles.
Jusqu'ici, la partie facile. Le vrai problème est d'amener le synthétiseur (synthesizer) à générer la logique qu'on veut réellement, en écrivant correctement le code Verilog. En fait, d'amener n'importe quel synthétiseur (synthesizer) au monde à générer exactement la même logique.
Je m'en tiens à un principe simple, qui est la Règle n° 4 de ma propre liste de Règles d'Or : assurez-vous d'utiliser des schémas de codage très courants. L'idée est que si le synthétiseur (synthesizer) interprète mal mon code, il fera la même erreur avec le code de beaucoup d'autres personnes. Et cela n'arrivera pas, parce que toutes ces personnes choisiraient vite un autre synthétiseur (synthesizer). Je regarde donc le code que j'ai écrit, et je demande : « Combien d'autres personnes seraient affectées si le synthétiseur (synthesizer) ratait ceci ? » Si la réponse est « beaucoup », je suis probablement du bon côté. Insistance sur « probablement ».
Mais comment toutes ces autres personnes écrivent-elles du Verilog, alors ? C'est en fait un casse-tête, car la grande majorité du Verilog est écrit dans des entreprises et n'est jamais publié. Et différentes personnes écrivent avec différents styles de codage. Pour empirer les choses, les exemples sur Internet sont souvent écrits par des personnes inexpérimentées. Même si leur code fonctionne, ce n'est pas nécessairement quelque chose à imiter : un autre synthétiseur (synthesizer) pourrait gâcher le même code, peut-être parce que les astuces de synthèse sont poussées à leurs limites. Le code Verilog venant d'OpenCores varie en qualité de l'excellent au code qui ne fonctionne qu'en simulation.
Alors comment savoir ? La meilleure suggestion que je puisse formuler est de lire le code Verilog généré par les outils IP d'AMD. Certains de ces IP (IPs), par exemple les contrôleurs de mémoire DDR, génèrent du Verilog synthétisable, et il est écrit par des gens qui savent ce qu'ils font. Notez cependant que le code Verilog généré automatiquement contient souvent beaucoup de code inutile et non utilisé. C'est typique du code généré par des scripts. Il y a souvent des modules qui se contentent d'instancier (instantiation) d'autres modules, qui à leur tour en instancient d'autres, et ainsi de suite. Envelopper les modules indéfiniment comme ça n'est pas quelque chose à imiter ; c'est juste une façon de garder la hiérarchie structurée comme nécessaire pour permettre différentes configurations du même code. Le code Verilog généré automatiquement a aussi tendance à avoir beaucoup de paramètres, ce qui ne devrait pas nécessairement être imité non plus. Rappelez-vous que votre objectif est d'imiter leur façon d'exprimer la fonctionnalité, pas la hiérarchie en désordre.
Un mot sur SystemVerilog : c'est assez populaire, mais je n'ai personnellement jamais écrit de code dans ce dialecte du langage. C'est principalement parce que je veux que mon Verilog soit aussi simple que possible. Moins j'ai besoin de faire confiance au synthétiseur (synthesizer), mieux c'est. Quand j'ai besoin de structures compliquées, j'écris un script en Perl qui crée du Verilog simple. Nourrissez le synthétiseur (synthesizer) à la petite cuillère, ou il vous vomira dessus.
Outils d'implémentation
Maîtriser cette compétence signifie travailler efficacement avec les outils, bien les contrôler, comprendre leurs messages d'avertissement, permettre une génération reproductible du flux de configuration (bitstream), et garder le projet gérable à mesure qu'il grandit. Bref, contrôler son travail sur le projet.
Vivado est l'outil avec lequel il est préférable de travailler. Non seulement AMD domine le marché, mais les autres fournisseurs, en particulier les nouveaux fournisseurs chinois de FPGA, ont tendance à concevoir leurs outils de manière compatible. Outre savoir utiliser ces outils comme un IDE, le processus de transformation du code Verilog et de l'IP en flux de configuration (bitstream) doit être compris. Il commence par la synthèse, et est suivi de plusieurs autres étapes qui dépendent du fournisseur, mais qui font toutes la même chose en principe : transformer progressivement la sortie du synthétiseur (synthesizer) en un flux de configuration (bitstream) qu'on peut charger dans le FPGA. Cette séquence d'étapes d'exécution n'est pas une boîte noire, et elle ne doit pas être traitée comme telle.
La principale raison de comprendre comment les outils fonctionnent est de répondre correctement aux erreurs et aux avertissements. Pendant l'implémentation d'un projet, beaucoup d'avertissements sont produits, et il est important de distinguer ceux qui sont importants de ceux qu'on peut ignorer. Et si une erreur se produit, le processus échoue, et un problème doit être résolu. La compétence pour résoudre ces problèmes vient de la compréhension de la théorie qui sous-tend les actions des outils, ainsi que de l'expérience accumulée.
Essayer de demander à l'IA comment résoudre un problème fonctionne parfois, mais souvent l'IA vous entraîne dans un long voyage de débogage futile. Et si vous écoutez l'IA sans jugement propre, vous pourriez finir par faire quelque chose qui semble résoudre le problème, alors que vous avez en réalité juste fait disparaître un message d'erreur et créé un vrai problème à la place. Bref : il n'y a pas de substitut à votre propre cerveau, et il n'y en aura jamais.
Un autre point important est que chaque outil a ses bizarreries, par exemple des messages d'erreur trompeurs. Ou pire encore : les outils ignorent silencieusement du code, des réglages ou des contraintes. Apprendre ces choses n'est qu'une question d'expérience, et une partie de l'acquisition de cette expérience consiste à lire réellement les rapports et à comprendre ce que ces messages signifient, même s'ils n'ont aucune pertinence concrète.
Voici une liste de concepts et de routines que je suggère comme liste de contrôle. Ceux-ci ne concernent que la synthèse et l'obtention d'un flux de configuration (bitstream). Je laisse la simulation, la vérification et le débogage pour plus tard.
- Synthèse et génération de listes de connexions (netlists) (edif en particulier)
- Mappage technologique (technology mapping) (même si c'est fait par le synthétiseur (synthesizer) dans Vivado).
- Inclusion des IP (IPs) dans la conception.
- Placement et routage (place and route)
- Optimisations de timing après placement et routage (post-route)
- Génération du flux de configuration (bitstream)
- Utilisation du JTAG pour charger le flux de configuration (bitstream) ou programmer la mémoire flash
- Affichage et analyse de la conception synthétisée et/ou placée et routée avec les outils de Vivado.
Si vous essayez un projet d'exemple, vous passerez très probablement par tout ce qui est mentionné dans cette liste, sauf le dernier point. Utiliser les outils quand quelqu'un a déjà préparé un projet d'exemple pour vous est facile. Dans la vraie vie, les projets sont rarement organisés aussi proprement, et vous serez responsable de faire fonctionner correctement les outils et d'obtenir les meilleurs résultats possibles. Si vous ne comprenez pas comment la machine fonctionne, vous aurez du mal à la réparer quand elle se bloque ou ne fait pas ce que vous voulez. Rappelez-vous, des choses bizarres arrivent tout le temps, même si vous faites tout correctement.
Un océan de fichiers
Les outils de développement créent des fichiers, et beaucoup. Chaque étape que les outils exécutent, de la synthèse au flux de configuration (bitstream) finalisé, est comme un programme informatique qui lit certains fichiers et produit ses résultats sous forme d'autres fichiers.
Pour rendre les choses encore plus difficiles, les outils créent souvent des copies des fichiers sources et se basent sur ces copies plutôt que sur les originaux. Les outils génèrent aussi des fichiers intermédiaires, et se basent sur eux plutôt que sur quoi que ce soit qui ressemble à un fichier source. Gardez cela à l'esprit, en particulier quand vous faites des changements dans les sources et que rien ne change dans les résultats.
Je ne ferais pas de la compréhension de ce que fait chaque fichier et à quoi il sert une priorité, comme première étape d'apprentissage. Il est inutile de tenter de tous les maîtriser, mais il est important de savoir quels fichiers doivent être considérés comme « sources » et lesquels sont « générés ». Mieux vous nagez dans cet océan de fichiers, meilleures sont vos chances de garder la tête hors de l'eau. Vous comprendrez ce que je veux dire la première fois que vous tenterez de créer une copie indépendante de tout un projet, dans le but de le développer dans une direction séparée. Ou de le déplacer vers un autre ordinateur.
La meilleure méthode est de maintenir un ensemble minimal de fichiers qui définissent le projet FPGA dans un dépôt Git. Supprimez tous les autres fichiers de temps en temps, et reconstruisez le projet à partir de cet ensemble minimal. Pour un exemple de la façon dont un projet peut être amorcé à partir d'un ensemble minimal de fichiers, téléchargez et essayez d'implémenter l'un des demo bundles de Xillybus (disponibles pour Vivado et Quartus). Notez que vous n'importez pas les sources normalement dans Vivado pour commencer, mais que vous exécutez plutôt un script Tcl. Cela peut sembler une solution effrayante, mais ce script est facile à modifier même si vous ne connaissez pas le Tcl.
Une chose qui vaut la peine d'être connue à propos de Vivado est que le DCP est un fichier compressé, contenant des listes de connexions (netlists) en edif, des contraintes, et d'autres informations. Par exemple, quand le DCP est le résultat du placement et routage (place and route) ou d'étapes ultérieures, il contient les placements exacts des éléments logiques. C'est un instantané de la conception après l'achèvement d'une étape spécifique du processus. Je suggère de décompresser un DCP et d'y jeter un œil, juste pour le plaisir.
Vérification et simulation
Dans un monde parfait, la conception logique fonctionne du premier coup, et il n'y a pas de bugs à corriger. La réalité est bien sûr presque toujours différente.
Dans tout cours pour débutant sur le Verilog, on vous dira ceci : d'abord, écrivez le code Verilog que vous voulez comme logique dans le FPGA. On appelle cela le « code pour la synthèse » ou le « code synthétisable ». Ensuite, écrivez un banc de test (testbench) en Verilog, et utilisez-le dans une simulation pour vérifier que le code pour la synthèse fonctionne correctement. Ou plutôt, voyez comment il ne fonctionne pas correctement et corrigez les bugs. Le banc de test (testbench) est du code Verilog écrit uniquement dans le but de la simulation, et qui ne s'approche jamais d'un synthétiseur (synthesizer).
En effet, la méthode de travail habituelle consiste à écrire un module Verilog, ou quelques modules Verilog, puis à écrire un banc de test (testbench) pour vérifier qu'ils fonctionnent correctement. Cela se répète à mesure que le projet grandit, et à la fin, il est possible que tout le projet soit simulé. Pour une simulation aussi vaste, il y a souvent plusieurs bancs de test (testbenches) différents, chacun destiné à vérifier des fonctionnalités différentes.
Mais toutes les simulations ne se font pas de la même manière. Il y a trois approches principales de la simulation.
La première approche consiste à simuler dans le but d'obtenir des formes d'onde. Dans cette approche, le banc de test (testbench) ne crée que les signaux qui alimentent les entrées du module qu'on veut simuler. Cela inclut les horloges et les réinitialisations (resets), ainsi que d'autres signaux qui imitent le comportement de vrais signaux physiques ou de signaux générés par d'autres modules. Une fois la simulation terminée, une interface graphique est utilisée pour visualiser les formes d'onde créées par la logique sous test, afin de voir si elle fonctionne correctement, ou pourquoi elle ne le fait pas. Cette méthode convient à de la logique plus simple. Par exemple, la machine d'état (state machine) qui implémente un motif de sortie vidéo peut être simulée de cette façon, car le motif de sortie répété correct est facilement vérifié en ne regardant que les formes d'onde.
La deuxième approche consiste à lire l'entrée depuis des fichiers et à écrire la sortie dans des fichiers. Ici, le banc de test (testbench) crée quelques signaux simples, mais les signaux importants sont lus depuis un fichier à la place. Les valeurs aux sorties, ou à quelques sorties sélectionnées, du module testé sont écrites dans un autre fichier. Il est assez courant d'écrire un programme informatique ou un script qui génère le fichier que le banc de test (testbench) lit, ainsi que le fichier contenant la sortie attendue du banc de test (testbench). Après avoir exécuté la simulation, un simple diff textuel peut être fait entre la sortie attendue et ce que le banc de test (testbench) a réellement écrit. Il existe bien sûr des variantes infinies : le banc de test (testbench) peut faire la comparaison avec les sorties attendues, ou peut-être l'inverse : un logiciel informatique lit la sortie du banc de test (testbench) et l'analyse.
Cette méthode de simulation convient particulièrement à ce que j'ai appelé la « logique de traitement », c'est-à-dire la logique qui implémente une sorte de traitement de données. Elle est aussi utile pour les tests de régression, c'est-à-dire les simulations qui vérifient que rien n'a changé fonctionnellement entre une version du code et une autre.
La troisième approche consiste à laisser le banc de test (testbench) vérifier lui-même la correction, et à s'arrêter avec une erreur si quelque chose de mauvais se produit. Cette méthode nécessite d'écrire des motifs de test significatifs en Verilog, ce qui est souvent plus difficile à faire qu'avec un langage de script. En revanche, le projet FPGA est plus facile à maintenir quand il y a un banc de test (testbench) autonome qui donne une indication de réussite ou d'échec. Celui qui exécute la suite de régression sera content de travailler de cette façon.
Voilà les trois approches principales. J'ai donné l'impression que seul le Verilog que nous, humains, écrivons est simulé, mais ce n'est pas vrai :
Simulations post-synthèse
Comme je l'ai mentionné plus tôt, le synthétiseur (synthesizer) peut produire une logique différente du comportement décrit par le code Verilog. Une façon d'aborder ce problème est de simuler la sortie (c'est-à-dire la liste de connexions (netlist)) produite par le synthétiseur (synthesizer). C'est ce qu'on appelle la simulation post-synthèse.
Pour lancer une simulation post-synthèse, on demande aux outils de développement de créer un modèle Verilog du code synthétisé. C'est un énorme module Verilog qui a les mêmes ports que celui qu'on a écrit pour la synthèse. Mais à l'intérieur, il se compose de petits modules (appelés primitives de simulation (simulation primitives)) qui représentent les éléments logiques réels du FPGA ciblé. C'est donc un fichier Verilog vraiment désordonné, mais on peut y faire référence dans le banc de test (testbench) au lieu du module Verilog original qu'on a écrit.
La méthode classique consiste à exécuter une simulation sur le code Verilog écrit par un humain, puis sur le modèle post-synthèse, et à comparer les sorties. La deuxième méthode mentionnée ci-dessus (avec des fichiers en entrée et en sortie) est la meilleure, car on s'attend à ce que la conception logique se comporte exactement de la même façon après la synthèse. Si ce n'est pas le cas, considérez cela comme un coup dur porté à votre style de codage Verilog. En fait, si vous écrivez le Verilog correctement, vous ne devriez pas avoir besoin d'une simulation post-synthèse. Ce type de simulation est plus courant dans l'industrie des ASIC, où ils sont paranoïaques à l'idée de se retrouver avec un bug dans leur puce finale. À noter aussi : même si votre simulation post-synthèse donne exactement la même sortie que l'original, cela ne garantit toujours pas que le synthétiseur (synthesizer) a fait ce que vous attendiez.
Il y a aussi la simulation post-placement et routage (post-place-and-route). Comme son nom l'indique, elle simule les éléments logiques tels qu'ils sont placés et connectés à l'intérieur du FPGA. Elle prend en compte les délais de propagation (propagation delays) à l'intérieur du FPGA, mais de manière très limitée. Il y a encore une énorme différence entre la simulation et ce qui se passe dans la réalité. C'est parce que les délais réels à l'intérieur du FPGA dépendent de nombreux facteurs physiques, par exemple la température et les tensions d'alimentation. Ces délais sont aussi quelque peu aléatoires, à cause des contaminations dans le silicium de la puce, qui sont dispersées un peu partout. Ces contaminations modifient les propriétés physiques, de sorte que les délais électriques sont légèrement décalés. Chaque FPGA physique a donc des délais différents, bien que dans les spécifications. Quand la simulation s'exécute, un seul délai spécifique est appliqué à chaque chemin (path), qui est généralement le plus grand délai autorisé. Si votre conception fonctionne parfaitement dans une simulation post-placement et routage (post-place-and-route), cela ne signifie toujours pas qu'elle fonctionnera sur un vrai FPGA physique.
Donc dans l'ensemble, les simulations ont des limitations assez sérieuses. D'une part, la simulation obéit à vos souhaits là où le synthétiseur (synthesizer) ne le fera pas. Et même les simulations post-synthèse ne peuvent pas couvrir de nombreux effets réels à l'intérieur du FPGA : les parasites impulsionnels (glitches), les tolérances des éléments logiques physiques dues à la température, à la tension d'alimentation ou aux tolérances de fabrication. La simulation ne peut pas non plus reproduire le comportement de la logique en cas de violations de timing, par exemple lors d'une traversée de domaines d'horloge (clock domain crossing) non sûre.
Si la conception est correctement écrite et contrainte, la simulation et la réalité se comportent de la même façon. Sinon, la simulation ne vaut rien. Il est important de garder cela à l'esprit.
Une autre limitation est que la simulation ne peut couvrir qu'un très court segment dans le temps. Si la simulation tourne pendant un million de cycles d'horloge, ce qui peut prendre beaucoup de temps, cela ne couvre que 10 ms de temps réel quand l'horloge tourne à 100 MHz. Les problèmes rares et les cas limites sont facilement manqués, simplement parce qu'ils ne sont pas tombés par hasard dans la fenêtre simulée.
Ma suggestion sur les simulations
Que recommander à un débutant ? Savoir simuler est indispensable, et c'est quelque chose à apprendre en même temps que les autres compétences liées au Verilog. En particulier, assurez-vous d'être à l'aise avec la deuxième méthode de simulation, utilisant des fichiers en entrée et en sortie. C'est la méthode considérée comme sérieuse, entre autres parce qu'elle est courante dans les tests de régression. Essayez-la aussi avec des simulations post-synthèse et post-placement et routage (post-place-and-route), au moins pour l'entretien d'embauche.
Mais : ne devenez pas accro aux simulations. Donnez-vous une tape sur la main chaque fois que vous trouvez un bug grâce à une simulation. Si vous trouvez un bug avec la première méthode (en regardant les formes d'onde d'une simulation), donnez-vous une tape encore plus fort. L'objectif ultime est d'écrire du code qui fonctionne du premier coup. Les simulations attrapent les bugs simples, pas ceux qui causent quelque chose de bizarre tous les milliards de cycles d'horloge. Ce sont ceux-là qu'on ne veut pas poursuivre éternellement.
Enfin, un petit secret sur moi-même : je lance rarement une simulation. Je fais attention à écrire du code Verilog avec soin et réflexion, pour qu'il fonctionne correctement dès le départ, ou du moins avec assez peu de bugs pour les corriger directement sur le FPGA. Mais je ne connais personne d'autre qui travaille de cette façon, et je ne suis pas sûr de la recommander comme approche générale.
En fait, j'aimerais ajouter un conseil bonus avancé, à garder en tête plus tard : quand les gens lancent des simulations, ils ajoutent souvent des réinitialisations (resets) à leur logique, parce que dans une simulation tous les registres commencent à l'état inconnu, marqué « X ». Par conséquent, rien de précieux n'en sort, car tout le système reste à l'état « X ». L'erreur courante est de mettre rapidement des réinitialisations asynchrones (asynchronous resets) partout pour se débarrasser de ces X. Et puis d'oublier que cela peut être une très mauvaise idée, comme expliqué sur une page séparée. Donc oui, résolvez absolument le problème des X, mais faites-le sagement : peut-être avec une réinitialisation synchrone (synchronous reset), ou peut-être avec un bloc « initial » dans le code synthétisable ? La plupart des synthétiseurs (synthesizers) implémentent ce bloc, même s'il est à l'origine destiné uniquement à la simulation. Ne laissez pas la simulation dicter la façon dont vous écrivez votre code.
Tests et débogage
Après tout cela, vous finirez par tester votre conception et par trouver les raisons pour lesquelles elle ne fonctionne pas comme prévu. Il y a un dicton qui dit que le débogage est comme un roman policier, où la même personne est la victime, le détective et le coupable.
Et la vérité est que dans ce roman policier, il n'y a pas de meilleure façon unique de résoudre le mystère. Il s'agit d'être créatif pour trouver chaque fois la bonne approche, les bons outils et les bonnes méthodes, afin de se rapprocher de la source du bug. Certaines personnes s'en tiennent aux mêmes méthodes de débogage à chaque fois. Elles trouvent parfois le problème assez rapidement, et parfois cela leur prend une éternité.
Il est assez courant de développer un ensemble d'outils et de méthodes de débogage spécialisés pour un projet spécifique. Cela arrive aussi dans les grands projets logiciels, mais avec la conception FPGA, c'est souvent inévitable. C'est comme développer un banc de test (testbench) pour tester, mais en matériel.
Mais même s'il n'existe pas de méthode optimale unique pour trouver un bug, il y a tout de même certains outils couramment utilisés. Je vais en mentionner quelques-uns.
L'outil avec lequel la plupart des gens préfèrent commencer est un analyseur logique embarqué (on-chip logic analyzer). Selon la suite de développement que vous avez, cet outil s'appelle ILA, ChipScope, SignalTap, ou quelque chose de similaire, et ils font tous la même chose : ils transforment votre ordinateur en analyseur logique. Avec cet outil, vous pouvez visualiser des formes d'onde capturées à l'intérieur du FPGA de la même manière que vous regardez des formes d'onde issues de simulations. Vous choisissez les signaux à observer et la condition pour déclencher une capture de ces signaux.
La communication entre l'ordinateur et le FPGA se fait sur l'interface JTAG, la même que celle utilisée pour envoyer le fichier de flux de configuration (bitstream) au FPGA. Il n'y a donc aucun matériel supplémentaire requis, et beaucoup de concepteurs FPGA qui n'aiment pas l'électronique apprécient ce fait. Le FPGA et son lien JTAG déjà existant sont aussi l'outil de débogage.
Cette méthode est si pratique que beaucoup de concepteurs FPGA y deviennent accros, et oublient d'autres méthodes qui peuvent être plus adaptées dans certaines situations.
Le principal inconvénient de cette méthode est que le JTAG a une bande passante de données relativement faible, donc ILA et les outils similaires ne conviennent pas quand de grandes quantités de données sont nécessaires pour l'investigation. Il faut aussi un peu de temps pour que les données arrivent à travers le lien JTAG. Cela peut être problématique quand on veut une réponse immédiate aux événements, afin de pouvoir les corréler avec d'autres choses qui se produisent.
Quand les limitations du lien JTAG deviennent un problème, des interfaces plus rapides sont nécessaires. Si la carte FPGA possède une interface PCIe, elle peut être utilisée pour transporter des données à haut débit depuis et vers un ordinateur. Implémenter une interface PCIe et le pilote informatique peut être un projet en soi, mais avec Xillybus il est rapide et facile de mettre en place un tel lien. Une solution similaire avec Xillybus est aussi possible avec les cartes Zynq-7000 fonctionnant avec Xillinux.
Une connexion à haut débit est souvent nécessaire quand il y a un problème ponctuel caché parmi beaucoup de données. Par exemple, dans le traitement d'images, auquel cas l'image est transmise à l'ordinateur via Xillybus et visualisée sur l'écran de l'ordinateur à l'aide d'un programme informatique dédié.
Que vous choisissiez un analyseur logique embarqué (on-chip logic analyzer) ou Xillybus, ce sont des outils de débogage stériles. On ne touche à aucun matériel. Mais parfois, nous avons besoin de cette réponse simple et immédiate du matériel pour comprendre ce qui se passe.
Alors d'abord, permettez-moi de suggérer l'outil de débogage le plus simple de tous : la LED. Étonnamment souvent, la méthode la plus rapide pour trouver un bug est de définir différentes conditions logiques, et de faire en sorte qu'une LED clignote quand elles sont vraies. En particulier, faire clignoter des LED quand quelque chose qui ne devrait jamais arriver arrive effectivement. Plus il y a de LED, mieux c'est. C'est un peu comme insérer un « printf » dans du code C pour déboguer. De la même façon, ajoutez un peu de code Verilog pour le débogage. Chargez le flux de configuration (bitstream) dans le FPGA, essayez quelques choses farfelues au hasard, et peut-être ces LED révéleront à quoi le bug est lié.
Il y a une chose importante à retenir quand vous utilisez cette méthode : si la condition est remplie, assurez-vous que la LED reste allumée au moins 20 ms en réponse. Sinon, l'œil humain ne le verra pas.
Une version plus sophistiquée de la méthode des LED est l'utilisation d'un oscilloscope. C'est pourquoi j'ai suggéré d'acheter et de se familiariser avec un oscilloscope dans la première page de cette série. L'idée est de connecter plusieurs signaux de l'intérieur du FPGA à ses broches de sortie accessibles avec une sonde d'oscilloscope. Dans les vrais projets, il n'est pas rare d'avoir un connecteur dédié sur un PCB, où le concepteur de la carte a rassemblé beaucoup de broches inutilisées du FPGA, afin qu'elles puissent servir au débogage.
Comparé à l'analyseur logique embarqué (on-chip logic analyzer), l'oscilloscope peut sembler médiocre : il peut surveiller deux signaux à la fois, peut-être un peu plus avec des oscilloscopes plus chers. Sa bande passante est limitée, donc des signaux vraiment rapides peuvent être manqués. Et pourtant, parfois c'est l'outil le plus puissant à disposition. En particulier quand il y a des motifs de signaux répétés, les observer à l'oscilloscope peut aider à attraper ce qui ne va pas.
Et parfois l'oscilloscope est nécessaire pour comprendre ce qui se passe au simple niveau matériel. Les tensions sont-elles correctes ? Les signaux numériques ont-ils l'air comme ils devraient ? Y a-t-il un bruit bizarre sur le signal de tension ?
Et cela m'amène au type de débogage le plus terre-à-terre. Plus souvent qu'on ne veut le croire, le problème est un problème matériel vraiment idiot, en particulier avec des PCB développés pour un projet spécifique. L'une des tensions d'alimentation peut être instable, peut-être avec des pics ou des creux brefs occasionnels. L'oscillateur d'horloge peut ne pas se comporter comme prévu, générant un signal d'horloge inadéquat. C'est toujours une bonne idée de regarder cela avant de développer des théories sophistiquées.
Donc si vous voulez devenir bon en conception FPGA, il vous faut un peu des deux : des méthodes stériles pour récupérer des données de l'intérieur du FPGA, et aussi mettre les mains dans le matériel.
Et n'oubliez jamais : il n'y a qu'un seul outil de débogage vraiment efficace, si et quand il est utilisé correctement : votre propre cerveau.
Langages de programmation
Même si la programmation n'est pas directement liée à la conception FPGA, un ensemble minimal de compétences en programmation est nécessaire pour faire le travail. Par exemple, j'ai mentionné plus haut que les données de test pour la simulation sont souvent créées avec un programme ou un script spécialement écrit.
Je vais mentionner quelques langages de programmation qui pourraient valoir la peine d'être maîtrisés. Parmi toutes les choses que je suggère d'apprendre, c'est en fait la partie la plus facile, et il n'est certainement pas nécessaire de savoir tout cela dès le premier jour. Ni jamais, d'ailleurs.
- Tcl : ce langage est couramment utilisé pour écrire des scripts exécutés par Vivado et d'autres outils de développement FPGA. Les one-liners sont souvent utiles aussi. Apprendre la syntaxe complète de ce langage et toutes ses fonctionnalités est cependant inutile. Vous ne ferez probablement jamais de vraie programmation dans ce langage, mais maîtriser les bases de sa syntaxe est recommandé. En particulier, apprenez les différents types de guillemets et de crochets, et ce qu'ils signifient.
- C : ce langage est si répandu que je considérerais comme un certain handicap de ne pas le connaître. Pour le développement FPGA, c'est souvent un bon candidat pour créer de grandes quantités de données de test.
- Python : c'est un langage de script populaire, qui peut être utile pour générer des données de test, mais plus important encore : pour générer du Verilog qui implique du code répétitif qui ne s'exprime pas proprement avec les capacités syntaxiques du Verilog lui-même. Il est souvent plus facile d'écrire un script qui génère du code Verilog à partir de modèles de code que de le faire directement avec Verilog.
- Perl : ce langage peut être utilisé aux mêmes fins que celles que j'ai mentionnées pour Python ci-dessus. À mon avis, Perl est nettement meilleur pour faire à peu près tout, mais Python est plus populaire parce qu'il a été adopté par les universités : la syntaxe de Python est ce que les universitaires en langages de programmation aiment, et Perl a une syntaxe très nette et pragmatique, mais elle enfreint les règles académiques sur ce à quoi un langage de programmation devrait ressembler. Donc Perl est excellent pour faire le travail, mais il est considéré comme un langage dépassé, parce que la plupart des gens considèrent Python comme le langage de script principal. Si vous n'êtes pas déjà adepte de Python, envisagez plutôt Perl. Vous ne le regretterez pas.
- Makefile et scripts shell Bash : si vous êtes déjà familier avec ceux-ci, gardez-les en tête pour construire certains projets. Sinon, je ne les mettrais pas en haute priorité. Il est très difficile d'écrire un Makefile correct pour un projet FPGA, parce que les dépendances ne sont pas toujours aussi simples que dans une compilation logicielle ordinaire. Et pourtant, j'utilise des Makefiles quand le processus de compilation est complexe et que je veux ne compiler que les parties qui nécessitent une recompilation.
Il y a plusieurs autres langages, par exemple MATLAB, qui peuvent être utiles aussi, en particulier pour créer des données de test pour les simulations.
Un autre aspect de la connaissance des langages de programmation est qu'ils créent souvent un langage commun avec l'équipe logicielle. Cela est pertinent pour les projets FPGA qui impliquent une sorte de processeur embarqué ou même un ordinateur. Donc être un peu programmeur soi-même aide à communiquer avec les gars du logiciel. De plus, il est souvent plus facile d'écrire le programme de bas niveau qui communique avec votre conception FPGA que de faire faire cela par quelqu'un d'autre à partir d'une spécification.
Et comme recommandation générale, apprenez à utiliser Git et utilisez-le, même si vous travaillez seul sur un projet. Le contrôle de version n'est pas seulement pour contenter les managers, mais c'est un outil précieux qui vous permet d'essayer quelque chose, puis de revenir en arrière, puis de regretter d'être revenu en arrière. Et une fois que vous avez fini de faire le fou, rien de ce désordre n'est visible.
J'utilise gitk pour une représentation graphique de l'arbre des versions.
Ceci conclut la troisième page de cette série. La page suivante traite aussi des compétences pratiques, mais de celles qui sont moins importantes pour commencer. L'essentiel est d'expliquer comment et quand elles peuvent être importantes.