Ceci est la deuxième page d’une série consacrée aux resets dans les FPGA. Après avoir expliqué pourquoi les resets asynchrones (asynchronous resets) ne sont pas ce que beaucoup de gens croient dans la page précédente, cette page aborde les différentes options pour les resets et l’initialisation du FPGA.
Avant tout : qu’est-ce qu’un reset ?
Bon, voyons, on sait tous ce qu’est un reset ! C’est comme ce bouton sur l’ordinateur qu’on appuie et tout repart à zéro. C’est ce signal qui entre dans une puce et qui garantit que, quoi qu’il se soit passé avant, à partir de maintenant tout ira bien. Certains appellent le reset le signal qui met le système dans un état connu.
Pour les concepteurs de FPGA, le reset est souvent simplement cette entrée supplémentaire à ajouter à chaque module et à utiliser dans un motif de code répété. Une chose que l’on fait sans prêter nécessairement attention à ce que ce signal de reset garantit vraiment, ou s’il garantit quelque chose, ou encore s’il peut être tout simplement omis.
L’erreur la plus répandue consiste à se concentrer sur le fait que le signal de reset met le système dans un état connu. C’est évidemment vrai, mais l’important est ce qui se passe après la désactivation du reset. Il faut garantir que la logique commence à fonctionner de façon prévisible. Ou du moins suffisamment prévisible pour qu’elle fonctionne correctement. Il est inutile de faire un reset si on n’est pas sûr du comportement du système après la désactivation du reset.
Ce qui rend ce sujet particulièrement difficile, c’est qu’il y a une part de chance : d’une manière générale, l’état du système au moment où le reset est activé est inconnu et aléatoire, tout comme le moment de la désactivation du reset. Une mauvaise gestion du signal de reset peut donc produire des dysfonctionnements rares qui apparaissent à des occasions aléatoires, et qui peuvent sembler être un problème d’un tout autre genre. De même, il est possible de négliger ce problème sans conséquence visible, à l’exception de problèmes occasionnels que l’on traite habituellement par des incantations magiques.
Tout comme des contraintes temporelles (timing constraints) correctes et une bonne gestion des horloges et des domaines d’horloge (clock domains), un traitement correct du réveil et du reset du FPGA est indispensable pour garantir un fonctionnement fiable. Tous ces sujets ont un point commun : on peut plus ou moins s’en sortir en les négligeant, et pas mal d’ingénieurs le font, mais malheureusement au prix d’un FPGA qui se comporte comme s’il était hanté de temps à autre.
Simulation vs. matériel
Cette page se concentre sur la façon dont les décisions concernant les resets influencent le design une fois qu’il est chargé dans le FPGA. Mais évidemment, ces décisions ont aussi un impact sur la simulation de la logique.
Il est important de distinguer ces deux situations. Les simulateurs attribuent la valeur initiale X (inconnue) à tous les registres, en particulier en simulation comportementale. Ces valeurs X se propagent ensuite à tout registre qui dépend d’une valeur X dans sa fonction logique. Il arrive ainsi qu’un seul registre avec une valeur X inonde tout le design de X, et la simulation devient inutile.
Une solution courante mais incorrecte à ce problème consiste à mettre un reset asynchrone (asynchronous reset) sur tous les registres du design afin de se débarrasser de tous les X. On fait en général cela en activant brièvement le reset au début de la simulation. Résultat : tous les registres prennent une valeur connue et tout semble parfait. Malheureusement, cette solution incorrecte donne souvent une illusion de départ propre en masquant les problèmes évoqués dans la page précédente.
Même lorsque les resets sont utilisés correctement, réinitialiser tous les registres pour éviter de rechercher l’origine des valeurs X dans la simulation reste une solution paresseuse. Non seulement cela gaspille des ressources et rend peut-être plus difficile l’atteinte des contraintes temporelles (timing constraints), mais des resets inutiles peuvent aussi masquer des bugs, car un flot de valeurs X peut provenir d’une dépendance involontaire d’un registre à un autre. Ce flot de X peut donc être un avertissement que quelque chose cloche dans le design.
Par rapport à la simulation, le matériel est beaucoup plus tolérant vis-à-vis de l’absence de resets. Mais quand les resets sont utilisés incorrectement, ou pas du tout alors qu’ils sont nécessaires, le matériel peut se comporter de façon tout à fait inattendue.
En conclusion, les resets doivent être utilisés en pensant au matériel, et non pour éliminer les X gênants pendant la simulation. Et comme l’accent doit être mis sur le matériel, c’est tout ce que j’ai à dire sur les simulations.
Stratégies de reset
La décision de savoir si et comment appliquer des resets sur les registres oblige à examiner le cas de chaque registre séparément. Cela permet non seulement d’éviter une sortance (fan-out) inutilement élevée des signaux de reset, mais c’est aussi une bonne occasion de réfléchir à la question de savoir si la logique est garantie de démarrer correctement après le reset, quel que soit son état antérieur.
En principe, il y a quatre options :
- Accepter que la valeur initiale du registre soit inconnue.
- Se reposer sur la valeur initiale configurée dans l’élément synchrone du FPGA. Il n’y a donc pas de reset explicite.
- Utiliser un reset asynchrone (asynchronous reset). Par exemple :
always @(posedge clk or negedge resetn) if (!resetn) counter <= 0; else counter <= counter + 1; - Utiliser un reset synchrone (synchronous reset). Par exemple :
always @(posedge clk) if (!resetn) counter <= 0; else counter <= counter + 1;
Mon avis
Chacune de ces options est examinée plus loin. Je vais donc d’abord présenter ce que je considère comme la bonne méthode, puis développer.
- Examiner chaque registre (et chaque autre élément logique doté d’une entrée de reset) du design séparément, et vérifier si les deux premières options ont du sens. Autrement dit, si un reset peut être évité.
- Si ce n’est pas le cas, privilégier un reset synchrone.
- N’utiliser des resets asynchrones qu’avec les éléments logiques qui l’exigent explicitement (en particulier les primitives FPGA complexes et les cœurs IP (IP cores), par exemple les émetteurs-récepteurs et les contrôleurs de bus), et même dans ce cas, essayer d’utiliser un signal de reset synchrone si possible.
- Toujours réinitialiser correctement les machines à états (state machines). Même si leur définition comportementale garantit qu’elles atteindront un état connu tôt ou tard, assurez-vous qu’un reset explicite et sûr les amène dans un état connu. Cela tient principalement au fait que le synthétiseur (synthesizer) peut implémenter la variable d’état d’une manière différente à cause de l’optimisation (notamment un encodage one-hot). Cette représentation de la variable d’état peut donc ne jamais converger vers un état légal à moins que la machine à états ne soit réinitialisée explicitement.
- Réinitialisez toujours les cœurs IP (IP cores), les blocs de design et les primitives qui ont des entrées de reset si la documentation l’exige (et c’est une bonne idée de le faire dans tous les cas). Il se peut très bien qu’ils fonctionnent sans le reset, mais cela relève peut-être de la pure chance. Même si le reset est lié à une fonctionnalité que vous n’utilisez pas, et même s’il semble simplement remettre à zéro des sorties dont vous n’avez pas besoin, la règle est simple : en matière de reset, suivez soigneusement la documentation. Même s’il n’y a aucune raison apparente pour que ce soit nécessaire.
Ces suggestions sont à peu près conformes à ce que les fabricants de FPGA recommandent aujourd’hui.
J’ajouterai aussi une règle très générale : réinitialisez toujours explicitement la logique de contrôle et laissez les chemins de données (data paths) se purger d’eux-mêmes de leurs données initiales parasites.
Et maintenant, la discussion détaillée sur chaque option.
Option n°1 : valeur initiale inconnue
Certains registres n’ont besoin ni de reset ni de valeur initiale. Cela vaut en particulier pour les registres à décalage et les éléments logiques similaires. D’une manière générale, les chemins de données correspondent probablement à cette option.
Prenons cet extrait :
reg [31:0] d0, d1, d2, d3, d4;
always @(posedge clk)
begin
d4 <= d3;
d3 <= d2;
d2 <= d1;
d1 <= d0;
d0 <= orig_data;
end
Il s’agit clairement de cinq registres à retard de 32 bits chacun. Sur certains FPGA (Xilinx en particulier), le synthétiseur détecte cela comme un registre à décalage si seule la dernière valeur (@d4) est utilisée et qu’aucun de ces registres n’a de reset. Cela peut réduire considérablement la consommation de logique.
Bien évidemment, la logique connectée à la sortie de ces registres à retard doit pouvoir tolérer quelques données initiales aléatoires. Un cas typique où cela ne pose pas de problème est celui d’un autre registre ou d’une machine à états, correctement réinitialisé, qui garantit que les valeurs non initialisées sont ignorées à leur arrivée. Par exemple, si ces lignes à retard sont liées à un pipeline, la logique de contrôle du pipeline ignorera naturellement les données invalides.
D’une manière plus générale, la possibilité de ne pas réinitialiser ni initialiser un registre se reconnaît facilement lorsqu’il existe un indicateur ou un état associé qui en signale la validité. Ou lorsqu’il y a une séquence claire qui consiste d’abord à attribuer une valeur au registre, puis à consommer cette valeur. En bref, quand il est facile de savoir que la valeur du registre est ignorée jusqu’à ce qu’elle ait reçu une valeur correcte.
Un autre cas de valeur initiale inconnue est celui où elle peut dépendre du synthétiseur. Par exemple :
reg val;
always @(posedge clk)
val <= 1;
Le synthétiseur peut décider que @val est un fil qui a une valeur constante de 1. Une autre possibilité est d’attribuer un registre avec zéro comme valeur initiale, de sorte qu’il passe à 1 au premier coup d’horloge. Ce qui se passe réellement dépend du synthétiseur. Donc, même si la valeur de @val est toujours connue, sauf pendant le premier cycle d’horloge, la valeur initiale doit être considérée comme inconnue.
Option n°2 : valeurs initiales du FPGA
La valeur initiale des éléments synchrones de base d’un FPGA (généralement des bascules, des registres à décalage et des mémoires) est fournie dans le flux binaire (bitstream) de configuration. L’utilisation bien connue de cette fonctionnalité consiste à créer une ROM en configurant une RAM en blocs avec des valeurs initiales sans jamais y écrire.
Un autre aspect bien connu de cette fonctionnalité est qu’un FPGA sort généralement de la configuration avec apparemment tous ses registres à zéro. Cela vient du fait que le synthétiseur attribue en général à tous les registres une valeur initiale de zéro, mais il peut y avoir des surprises si la valeur initiale n’est pas définie explicitement.
Certains éléments synchrones, en particulier les registres à décalage et les blocs de RAM dédiés, peuvent être créés en décrivant leur comportement en Verilog ou en VHDL (par inférence). Le synthétiseur crée généralement un registre à décalage lorsque le code ressemble à une ligne à retard. De même, un élément logique de type RAM est créé en réponse à un tableau. Toutefois, si un reset est utilisé sur ces registres (reset synchrone ou reset asynchrone), le synthétiseur ne peut plus utiliser les ressources logiques de cette manière. En effet, ni les registres à décalage ni les RAM n’ont d’entrée de reset qui fixe les valeurs de la mémoire interne.
Alors, comment les valeurs initiales sont-elles définies ? Pendant le processus de configuration, tous les éléments synchrones reçoivent leur valeur initiale juste avant que le FPGA ne devienne actif, c’est-à-dire avant que les éléments synchrones ne commencent à réagir à leurs horloges et à leurs entrées de reset asynchrone. Pour les appareils Xilinx, cela est implémenté par l’intermédiaire du signal Global Set Reset (GSR), qui met tous les éléments synchrones dans leur état initial. Ensuite, le signal Global Write Enable (GWE) est activé, ce qui permet aux éléments synchrones de fonctionner normalement.
Comme le processus de configuration s’effectue indépendamment des horloges utilisées par la logique applicative du FPGA, la transition du FPGA vers son état opérationnel est asynchrone par rapport à chacune de ces horloges. Par conséquent, les éléments synchrones se comportent exactement comme si un reset asynchrone avait été désactivé sans tenir compte d’aucune horloge. Autrement dit, il est possible que certains éléments synchrones répondent au premier front d’horloge qui arrive après la transition vers l’état opérationnel, tandis que d’autres éléments synchrones n’y répondent pas à cause d’une violation des contraintes temporelles. Cela peut provoquer des bugs vicieux, comme expliqué dans la première page de cette série.
Il est important de noter que les horloges ne sont pas nécessairement stables au réveil du FPGA. Si elles sont générées par les PLL internes du FPGA, elles peuvent violer largement les contraintes temporelles. Comme indiqué plus haut, ce n’est pas un problème si l’horloge est ignorée jusqu’à ce qu’elle soit stable (par exemple grâce à une activation d’horloge (clock enable)), ou si l’on sait qu’elle est stable au réveil du FPGA. De même, si le design garantit qu’aucun élément synchrone n’a de raison de changer de valeur avant que l’horloge ne soit stable, cela convient. Sinon, une valeur initiale ne garantit pas grand-chose.
Malgré les limites de la définition de la valeur initiale, cela suffit dans bien des scénarios, et un reset explicite n’est pas nécessaire. Et dans certains cas, on n’a tout simplement pas le choix, car aucun signal de reset n’est disponible. Par exemple, pour la logique qui crée les signaux de reset du FPGA immédiatement après son réveil. Un exemple d’une telle logique est présenté sur la troisième page de cette série.
Avec la plupart des synthétiseurs, définir la valeur initiale d’un registre est assez simple : utilisez « initial » de Verilog :
reg [15:0] counter;
initial counter = 1000;
Il paraîtra peut-être surprenant que « initial » puisse être utilisé dans du code Verilog destiné à la synthèse, mais il se trouve que cet usage est largement supporté. Ce mot-clé est donc définitivement la méthode à privilégier si le synthétiseur le supporte (autrement dit, si cet usage de « initial » est explicitement mentionné dans la documentation). En outre, la méthode alternative a tendance à être spécifique au fabricant, et parfois même spécifique à une famille de FPGA. Ainsi, même si « initial » n’est pas toujours portable, c’est probablement quand même le choix le plus portable.
La méthode alternative pour définir la valeur initiale dépend du FPGA utilisé. Cette méthode consiste généralement à faire une instanciation (instantiation) de l’élément synchrone en tant que primitive, et à attribuer la valeur initiale comme paramètre d’instanciation. Par exemple, une bascule Xilinx :
FDCE myflipflop (
.C(clk),
.D(in),
.Q(out),
.CLR(1'b0),
.CE(1'b1)
);
defparam myflipflop.INIT = 1;
Utiliser « initial » est plus élégant, n’est-ce pas ?
Option n°3 : reset asynchrone
Si vous n’avez pas lu la page expliquant pourquoi les resets asynchrones sont souvent utilisés incorrectement, je vous suggère de le faire d’abord. Sauf si vous n’avez de toute façon pas l’intention d’utiliser ce genre de reset.
Pour une raison ou une autre, beaucoup de gens considèrent le reset asynchrone comme la solution adaptée à tout. Peut-être parce qu’il apparaît souvent dans des exemples de code, peut-être à cause de l’illusion d’un moyen simple d’obtenir un reset global qui atteint tous les éléments synchrones. Peut-être parce que, dans l’ancien monde des ASIC, le reset asynchrone était utile pour tester les puces pendant le processus de fabrication, car il permet de réinitialiser toute la puce et de commencer à appliquer des vecteurs de test.
Alors, revenons à la réalité : la manière propre et correcte d’utiliser un reset asynchrone est de le faire horloges coupées. C’est ce que signifie réellement la partie « asynchrone ». Dans un design pratique, cela se décompose comme suit :
- Désactiver toutes les horloges (sauf celle utilisée par la logique qui pilote cette procédure). Cette désactivation se fait souvent en coupant l’entrée d’activation d’horloge (clock enable) des buffers d’horloge globaux (clock gating).
- Activer puis désactiver le signal de reset asynchrone. Veiller à ce que l’impulsion soit suffisamment longue pour réinitialiser tous les éléments synchrones.
- Attendre assez longtemps pour garantir que tous les éléments synchrones sont prêts à recevoir les horloges.
- Réactiver les horloges.
Cette séquence n’est pas difficile à implémenter, mais il peut être plus difficile de garantir que le premier front de chaque horloge soit correctement formé, sans parasite (glitch). Un problème courant avec les buffers d’horloge est qu’il existe une exigence de timing entre l’activation de la sortie du buffer d’horloge et le premier front d’horloge qui doit le traverser. Si cette exigence de timing n’est pas respectée, le buffer d’horloge peut produire un parasite (une impulsion courte qui viole les exigences du FPGA sur l’horloge). Cela peut entraîner un comportement imprévisible de tous les éléments synchrones qui dépendent de cette horloge.
Malheureusement, la documentation fournie par les fabricants de FPGA n’explique pas toujours comment garantir cette exigence de timing. Il peut donc être impossible de garantir que le premier front d’horloge après le reset se comporte correctement. Et sans garantie sur ce premier front d’horloge, le reset ne sert à rien.
Si vous utilisez cette méthode, assurez-vous qu’aucune contrainte temporelle n’est imposée sur les chemins (paths) liés au reset asynchrone. Une telle imposition est inutile dans ce cas, et elle peut être activée par défaut.
Il existe une autre méthode, couramment suggérée, pour appliquer un reset asynchrone de manière fiable. Cette méthode n’implique pas de fenêtrage d’horloge (clock gating) et ne repose donc pas sur les buffers d’horloge : l’idée est d’ajouter quelques bascules qui laissent passer directement l’activation du signal de reset asynchrone, mais qui désactivent le reset de manière synchrone. Autrement dit, un reset asynchrone synchronisé.
Par exemple, si le reset asynchrone d’origine est @external_resetn, cela génère un reset de ce genre :
reg pre_rstn1, pre_rstn2;
reg resetn;
always @(posedge clk or negedge external_resetn)
if (!external_resetn)
begin
resetn <= 0;
pre_rstn2 <= 0;
pre_rstn1 <= 0;
end
else
begin
resetn <= pre_rstn2;
pre_rstn2 <= pre_rstn1;
pre_rstn1 <= 1;
end
Notez que @clk est l’horloge utilisée par les éléments synchrones qui sont réinitialisés par @resetn.
Lorsque @external_resetn est actif (c’est-à-dire à l’état bas), les trois registres deviennent actifs (c’est-à-dire à zéro) de manière asynchrone. En revanche, quand @external_resetn est désactivé, seul @pre_rstn1 devient inactif au front d’horloge suivant, et cela se propage à @pre_rstn2 puis à @resetn aux fronts d’horloge qui suivent.
Le rôle des deux registres supplémentaires est de se protéger contre la métastabilité (metastability), afin que @resetn se désactive de manière sûre. Cela est nécessaire si et quand @external_resetn devient inactif avec un mauvais timing par rapport à @clk, ce qui peut provoquer une condition métastable sur @pre_rstn1 (cette page explique la métastabilité).
L’avantage de ce synchroniseur est que @external_resetn peut être utilisé comme reset asynchrone : il fonctionne même si aucune horloge n’est active. Néanmoins, les éléments synchrones reçoivent un signal de reset qui devient inactif de manière synchrone, ce qui permet de garantir le timing.
Inutile de préciser que chaque horloge a besoin de son propre reset asynchrone synchronisé.
Il est important de noter que la génération de @resetn comme ci-dessus ne suffit pas. Les contraintes temporelles de @clk doivent être appliquées sur les chemins allant de @resetn aux éléments synchrones. Par défaut, certains outils FPGA ignorent le timing des chemins qui se terminent sur l’entrée de reset asynchrone d’un élément synchrone. Il peut être nécessaire de modifier les réglages de l’outil à cet effet.
Ainsi, si @resetn est utilisé comme un reset asynchrone ordinaire, par exemple :
always @(posedge clk or negedge resetn)
if (!resetn) // Are you sure this path is timed?
the_register <= 0;
else
[ ... ]
alors le synchroniseur présenté ci-dessus ne suffit pas à garantir une sortie de reset fiable. Il vous incombe de vérifier que les chemins qui partent de @resetn et aboutissent aux entrées de reset asynchrone des bascules sont bien pris en compte par les contraintes temporelles.
Il est également important de noter que ce synchroniseur ne protège pas contre les parasites sur @external_resetn : si la durée de l’impulsion active de @external_resetn est plus courte que ce que spécifient les bascules du FPGA, tout peut arriver. @external_resetn doit donc être généré par une logique ou une électronique externe garantissant une impulsion longue. Si cela n’est pas possible, la seule solution est de synchroniser complètement le reset, comme dans cet exemple pour @sync_resetn :
reg pre_rstn1, pre_rstn2;
reg sync_resetn;
always @(posedge clk)
if (!external_resetn)
begin
sync_resetn <= 0;
pre_rstn2 <= 0;
pre_rstn1 <= 0;
end
else
begin
sync_resetn <= pre_rstn2;
pre_rstn2 <= pre_rstn1;
pre_rstn1 <= 1;
end
Mais ce synchroniseur ignore @external_resetn si @clk est inactive. C’est problématique si la logique est censée traiter @external_resetn comme un reset asynchrone. Autrement dit, s’il doit fonctionner même lorsque les horloges ne sont pas actives.
Revenons au premier synchroniseur. Et si on utilisait @resetn comme un simple reset synchrone ? Quelque chose comme ceci :
always @(posedge clk) // @resetn not in sensitivity list!
if (!resetn)
the_register <= 0;
else
[ ... ]
C’est à peu près correct, car la désactivation de @resetn est certainement contrainte par les contraintes temporelles. En revanche, l’activation asynchrone de @resetn n’est pas contrainte, donc les éléments synchrones concernés peuvent se comporter aléatoirement juste avant que le reset ne prenne effet. Il est préférable d’utiliser un reset entièrement synchronisé dans ce but, par exemple @sync_resetn tel que défini plus haut.
Je terminerai ce sujet par une remarque générale : j’ai choisi d’utiliser des resets actifs à l’état bas dans les exemples ci-dessus, principalement à cause d’une tradition qui remonte à l’époque où le signal de reset était généré par un condensateur relié à la tension d’alimentation par une résistance. Comme le condensateur n’avait initialement aucune tension, l’entrée de reset était à « 0 ». Ce condensateur accumulait assez vite sa charge, et l’entrée de reset passait donc à « 1 ». Cet ancien type de reset de mise sous tension est la raison pour laquelle beaucoup de resets sont encore actifs à l’état bas aujourd’hui.
En conclusion, il est possible d’utiliser un reset asynchrone de manière fiable, mais y parvenir n’est décidément pas aussi simple que beaucoup le croient. J’ai présenté deux façons de garantir un reset asynchrone fiable : soit en coupant temporairement les horloges pour éviter les problèmes de timing, soit en utilisant un synchroniseur pour garantir le timing. Comme toujours avec les FPGA, le timing est la clé du jeu.
Dans la plupart des designs réels qui reposent sur un reset asynchrone, aucune de ces méthodes n’est utilisée. En conséquence, la fiabilité du design FPGA ne tient qu’à la chance.
Option n°4 : reset synchrone
Le reset synchrone est surtout connu avec le motif de code suivant :
always @(posedge clk)
if (reset)
the_register <= 0;
else
[ ... ]
Je proposerai plus loin ce que je considère comme un meilleur motif de code, mais pour l’instant, contentons-nous de celui-ci. Quoi qu’il en soit, notez que j’ai choisi un reset actif à l’état haut ici, car c’est le choix le plus courant avec les resets synchrones. C’est en tout cas mon impression.
Un reset synchrone est meilleur que le reset asynchrone dans presque tous les domaines, sauf sur ces points :
- Le signal de reset synchrone peut atteindre des sortances (fan-outs) énormes, et des contraintes temporelles sont imposées sur tous ses chemins. Ce genre de reset peut donc rendre difficile l’obtention des contraintes temporelles.
- Le reset synchrone ne peut pas être utilisé lorsque l’horloge n’est pas active.
- Certains FPGA disposent de ressources dédiées au routage global, qui ne peuvent être utilisées qu’avec des resets asynchrones (je ne suis pas sûr que ce soit réellement le cas, mais comme j’ai vu cette affirmation pour les FPGA Intel, j’ajoute ce commentaire).
Comme les FPGA sont rarement utilisés sans horloge active (contrairement aux ASIC, qui en ont traditionnellement besoin pour les tests), et comme on ne sait pas vraiment si le problème des ressources dédiées au routage existe, je vais me concentrer sur le sujet principal : la sortance (fan-out). Heureusement, c’est facile à résoudre.
Il vaut aussi la peine de mentionner que le même problème de sortance affecte tout autant le reset asynchrone, si l’on utilise le synchroniseur suggéré plus haut. Les seuls qui peuvent vraiment dire que la sortance est un inconvénient du reset synchrone sont donc ceux qui utilisent le reset asynchrone avec les horloges coupées (c’est-à-dire avec fenêtrage).
La première idée qui vient à l’esprit pour résoudre un problème de sortance est d’utiliser une contrainte ou un attribut de synthèse pour limiter la sortance. C’est cependant la méthode la moins recommandée, car le synthétiseur se contente de dupliquer la bascule lorsque la limite est atteinte. Il arrive donc souvent que les sorties des bascules dupliquées aillent vers des modules aux fonctions complètement différentes, si bien que les destinations de ces sorties peuvent être dispersées dans tout le FPGA. Cela se traduit par un routage long et un délai de propagation (propagation delay) important.
Une solution simple et efficace consiste à créer un reset local pour chaque partie significative de la logique. Par exemple :
module medium_sized_module (
input clk,
input reset,
input [15:0] in_data,
output [15:0] out_data
);
(* dont_touch = "true" *) reg local_reset;
reg the_register;
always @(posedge clk)
local_reset <= reset;
always @(posedge clk)
if (local_reset)
the_register <= 0;
else
[ ... ]
L’idée est que @local_reset est une copie locale de @reset (retardée d’un coup d’horloge). Utiliser @local_reset à la place de @reset à partir de ce module et vers le bas maintient la sortance à un niveau raisonnable. Comme les consommateurs de ce reset local seront de toute façon interconnectés étroitement, il y a de bonnes chances qu’ils soient placés dans une certaine région du FPGA. Le reset local n’aura donc pas à parcourir de longues distances à travers la structure logique.
Il est important d’empêcher le synthétiseur de supprimer les registres de reset local pour optimiser la logique. Le synthétiseur fait généralement cela lorsqu’il y a des registres au comportement identique, même s’ils appartiennent à des modules différents. Dans l’exemple ci-dessus, l’attribut de synthèse de Vivado est indiqué, à savoir « dont_touch ». Chaque synthétiseur a sa propre façon de faire (pour Quartus, on obtient le même résultat avec « dont_merge » comme attribut de synthèse).
Pour vérifier que le synthétiseur a bien conservé tous les registres, il est utile de donner à tous ces registres le même nom (par exemple local_reset comme suggéré plus haut), puis de rechercher les registres portant ce nom dans le design implémenté.
Il n’est bien sûr pas nécessaire de créer un reset local pour chaque module. À titre indicatif, une sortance de 50 à 100 est raisonnable pour un reset local, en particulier lorsqu’il atteint des éléments logiques situés dans une petite région physique du FPGA.
Le sujet de la réduction de la sortance est également abordé dans le contexte de la fermeture temporelle (timing closure).
Compléments sur les resets synchrones
Un mythe courant concernant les resets synchrones est que si le synthétiseur rencontre un motif de code Verilog qui correspond à un reset synchrone, il va connecter le signal de reset à l’entrée de reset synchrone de la bascule. Cela peut être le cas, mais souvent ce ne l’est pas.
C’est différent d’un reset asynchrone, qui doit être connecté à l’entrée de reset asynchrone de la bascule. Sinon, le reset ne fonctionnera pas sans horloge.
Les synthétiseurs ont tendance à ne donner aucune signification particulière au motif de code, mais plutôt à calculer l’équation logique dérivée du code Verilog. Prenons cet exemple :
always @(posedge clk)
if (reset)
the_register <= 0;
else if (some_condition)
the_register <= !the_register;
else if (some_other_condition)
the_register <= 0;
Une façon de lire ce code est de dire que l’instruction always commence par un motif standard qui demande un reset synchrone, puis vient une définition spécifique du comportement du registre. Par conséquent, on aurait pu s’attendre à ce que @reset soit connecté à l’entrée de reset synchrone de la bascule concernée, et à ce que la sortie d’une fonction logique (implémentée sous forme de LUT) aille vers l’entrée de données de la bascule.
En réalité, les synthétiseurs implémentent généralement la valeur de @the_register au prochain coup d’horloge de la manière la plus concise possible. Par exemple, l’entrée de reset de la bascule peut être connectée à une fonction logique (c’est-à-dire une LUT) qui implémente l’expression (reset || (some_other_condition && !some_condition) ).
Mais il existe une possibilité encore plus intéressante : l’entrée de reset de la bascule pourrait ne pas être utilisée du tout. À la place, seule l’entrée de données est utilisée, et la fonction logique utilise le signal @reset comme l’une de ses entrées. Donc si @reset est à l’état haut, la sortie de la fonction logique est zéro. De cette façon, @reset amène effectivement @the_register à zéro, mais il n’est pas traité différemment de n’importe quel autre signal.
Donc, pour récapituler : même si la bascule possède une entrée de reset synchrone, les synthétiseurs ont tendance à ne pas traiter le motif de code du reset synchrone de façon particulière, pas plus qu’ils ne traitent le signal de reset différemment d’un autre signal. L’entrée de reset de la bascule est utilisée de la meilleure façon pour implémenter le comportement exigé par le code Verilog. Cela peut parfois signifier connecter l’entrée de reset directement au signal de reset, parfois à une fonction logique qui implique le signal de reset, et parfois ne pas utiliser du tout l’entrée de reset. Le synthétiseur fait tout ce qui l’aide à atteindre ses objectifs de performance, et rien d’autre.
Les utilisateurs du Vivado de Xilinx peuvent mieux contrôler ce point grâce à deux attributs de synthèse, à savoir DIRECT_RESET et EXTRACT_RESET.
Je terminerai avec un autre inconvénient des resets asynchrones : dans la plupart des FPGA, une bascule n’a qu’une seule entrée de reset/set. Cette entrée peut se comporter de manière synchrone ou asynchrone. Si le reset est synchrone, le synthétiseur peut trouver des astuces pour utiliser cette entrée afin d’utiliser moins de LUT en s’efforçant de satisfaire le comportement demandé. Une telle astuce est impossible lorsqu’il y a un reset asynchrone. Un reset asynchrone lie donc les mains du synthétiseur et le force à gaspiller davantage de ressources logiques.
Éviter le blocage accidentel des registres
Il y a un piège avec les motifs de code couramment utilisés pour implémenter des resets, comme le montre cet exemple :
always @(posedge clk or negedge resetn)
if (!resetn)
begin
reg1 <= 0;
reg2 <= 0;
// Ayeee! Forgot to reset reg3 !
end
else
begin
reg1 <= [ ... ];
reg2 <= [ ... ];
reg3 <= [ ... ];
end
Comme le suggère le commentaire, @reg3 n’apparaît pas dans la clause begin-end correspondant à @resetn actif. Par conséquent, le code Verilog ci-dessus exige que @reg3 ne change pas de valeur tant que @resetn est actif. C’est comme si @reg3 était défini avec
always @(posedge clk)
if (resetn)
reg3 <= [ ... ];
ou, en d’autres termes, @resetn joue le rôle d’une activation d’horloge (clock enable) pour @reg3 : l’horloge n’est effective que lorsque @resetn est à l’état haut.
C’est exactement la même chose avec les resets synchrones :
always @(posedge clk)
if (reset)
begin
reg1 <= 0;
reg2 <= 0;
// Ayeee! Forgot to reset reg3 !
end
else
begin
reg1 <= [ ... ];
reg2 <= [ ... ];
reg3 <= [ ... ];
end
C’est en fait plus facile à comprendre, car il s’agit simplement d’une paire de clauses begin-end, la deuxième ne prenant effet que lorsque @reset est inactif. La définition de @reg3 pour cet exemple est donc clairement
always @(posedge clk)
if (!reset)
reg3 <= [ ... ];
La conclusion évidente (et pas nécessairement très maligne) est donc de ne pas oublier de registre dans la clause begin-end du reset. En fait, beaucoup de concepteurs de FPGA réinitialisent tous les registres, qu’ils en aient besoin ou non, parce qu’ils croient que c’est la seule façon de faire. Ou alors, ils adoptent un style de code où chaque registre a sa propre instruction « always ».
Mais que faire si l’on veut délibérément réinitialiser certains registres et pas d’autres ?
Avec un reset asynchrone, le seul choix est de mettre ces registres dans une instruction « always » séparée. Mais avec un reset synchrone, il y a une solution simple :
always @(posedge clk)
begin
reg1 <= [ ... ];
reg2 <= [ ... ];
reg3 <= [ ... ];
if (reset)
begin
reg1 <= 0;
reg2 <= 0;
// I don't want to reset reg3, and that's fine!
end
end
Au lieu de mettre « if (reset) » au début et d’avoir la partie intéressante sous le « else », on place le « if (reset) » à la fin, de sorte que les affectations dues au reset écrasent tout ce qui précède.
Notez que cela n’est pas équivalent aux exemples précédents : @reg1 et @reg2 sont réinitialisés quand @reset est actif, mais @reg3 n’est pas du tout influencé par le reset.
Si cette manière alternative d’appliquer un reset synchrone vous met mal à l’aise, je peux le comprendre, et il y a plusieurs raisons à cela. D’abord, il est généralement de bonne pratique de s’en tenir aux motifs de code couramment utilisés dans la conception FPGA. Sinon, le synthétiseur pourrait révéler un bug exotique, ce qui est beaucoup moins probable avec des motifs de code bien établis (voir la règle d’or n°4). Donc, même si la norme Verilog exige explicitement que cette méthode fonctionne, on peut soutenir qu’il n’est pas forcément judicieux de compter sur cette fonctionnalité.
C’est un argument solide, mais pour ce que cela vaut, je suis ici pour vous dire que j’utilise abondamment des motifs de code de ce genre depuis plus d’une décennie, avec un large éventail de synthétiseurs. C’est notamment ainsi que je définis les resets synchrones dans mon propre code. Je n’ai jamais eu le moindre problème avec cela.
Une autre raison possible de ne pas aimer cette méthode est de penser que le synthétiseur pourrait rater l’indication qu’un reset synchrone est souhaité, parce que ce n’est pas le motif habituel. Cependant, comme déjà mentionné plus haut, la plupart des synthétiseurs ne tiennent de toute façon pas compte de l’indication et considèrent le reset synchrone comme une simple définition du comportement exigé de la logique. Cette raison n’a donc pas de fondement.
Donc si vous voulez bien me croire sur parole quand je dis que c’est sans danger, vous vous éviterez quelques maux de tête.
Peut-on faire la même chose avec un reset asynchrone ? Par exemple, que fera ceci ?
always @(posedge clk or negedge resetn)
begin
reg1 <= [ ... ];
reg2 <= [ ... ];
if (!resetn)
reg1 <= 0;
end
C’est bien sûr un écart par rapport au motif de code habituel du reset asynchrone. Un test anecdotique sur le synthétiseur de Vivado a montré qu’il comprenait l’indication et attribuait un reset asynchrone à @reg1.
Mais le comportement exigé pour @reg2 ne peut pas être réalisé sur un FPGA : tel qu’écrit ci-dessus, cela signifie que @clk et @resetn sont tous deux des horloges, et que @reg2 échantillonne une nouvelle valeur sur leurs fronts montants et descendants, respectivement. Comme les bascules à deux entrées d’horloge n’existent dans aucun FPGA que je connaisse, il est impossible de synthétiser la définition de @reg2.
Le synthétiseur de Vivado a réagi à cela en ignorant la partie « negedge resetn » et a créé une bascule qui utilise uniquement @clk comme horloge. Aucune trace de cette anomalie dans le résultat de la synthèse, et le synthétiseur n’a émis ni avertissement ni plainte. Et pourtant, le synthétiseur a créé une logique qui ne se comporte pas comme le définit le code Verilog.
Donc, plus précisément avec le synthétiseur de Vivado, le même motif de code fonctionne en réalité même pour un reset asynchrone, mais il ne faut pas compter là-dessus : le code Verilog doit dire ce que vous voulez que la logique fasse. Sinon, le synthétiseur est parfaitement en droit de l’interpréter de travers comme il l’entend.
Ceci conclut la deuxième page de cette série consacrée aux resets. La page suivante passe en revue différents aspects du démarrage du FPGA après la mise sous tension et le reset externe.