01signal.com

Métastabilité et bases du franchissement de domaine d’horloge

Cette page est la deuxième d’une série de trois consacrée aux domaines d’horloge.

Portée

Comme cela a déjà été mentionné sur la page précédente, si un chemin (path) traverse des domaines d’horloge (clock domains) dont les horloges sont liées (related clocks), aucune logique particulière n’est requise. Il reste néanmoins nécessaire de s’assurer que les outils FPGA appliquent bien les contraintes temporelles (timing constraints) sur ce chemin, afin de garantir les exigences temporelles de l’élément synchrone en destination (temps de préparation et temps de maintien). Ce cas est donc traité comme s’il n’y avait aucun franchissement de domaine d’horloge, dans la mesure où le chemin est vérifié en timing.

Mais si les horloges sont des horloges non liées (unrelated clocks), une logique de resynchronisation est nécessaire : il faut du code Verilog (ou un autre langage) dédié à la résolution de ce problème. Cette page présente les bases de cette démarche.

Utilisez une FIFO si vous le pouvez

J’ai pensé qu’il serait honnête de commencer par la façon d’éviter complètement ce casse-tête, au bénéfice de ceux qui ont la possibilité d’y échapper. La voici :

Les FIFO à double horloge (dual-clock FIFOs), générées par les outils FPGA, constituent assurément le moyen le plus sûr pour franchir les domaines d’horloge. C’est la solution la plus courante, et le fait qu’elle soit aussi largement utilisée est en soi une raison de lui faire confiance.

En particulier, si vous débutez avec les FPGA, il y a de bonnes raisons de préférer une FIFO, même si pour cela il vous faudra adapter quelque peu votre conception. Utiliser une FIFO peut consommer légèrement plus de ressources qu’une logique taillée sur mesure pour votre besoin. Notez toutefois qu’un bloc RAM (block RAM) n’est généralement pas nécessaire si la profondeur de la FIFO est faible. La différence ne vaut donc pas forcément le risque de tout gâcher.

La FIFO est souvent la solution naturelle pour traverser les domaines d’horloge, en particulier lorsqu’un flux de données va d’une unité fonctionnelle à une autre. Mais même lorsque cela n’est pas si naturel, il est souvent possible de réorganiser la logique pour que cela fonctionne. Par exemple, si une logique d’un domaine d’horloge doit notifier un événement à une logique d’un autre domaine d’horloge, on peut coder un mot de message et l’écrire dans une FIFO peu profonde. Une telle solution n’est pas seulement moins sujette aux bugs, elle aboutit aussi probablement à une conception plus organisée et plus lisible.

La façon de connecter et d’utiliser les FIFO est abordée en détail dans cette série de pages.

Logique de resynchronisation pour un bit unique

Si une FIFO ne résout pas votre problème, il est temps de retrousser nos manches et de mettre en œuvre une logique de resynchronisation pour un franchissement de domaine d’horloge (clock domain crossing) sûr. Le reste de cette page se limite au franchissement de domaines d’horloge avec un signal d’un seul bit. C’est la pierre angulaire de toute logique de resynchronisation, et il y a beaucoup à dire sur ce cas apparemment simple.

La page suivante de cette série traite du franchissement de domaines d’horloge avec des signaux plus larges, en s’appuyant sur la technique de protection contre la métastabilité (metastability guard) présentée ci-dessous.

Métastabilité

Avant de passer aux solutions, il est important de comprendre ce qui se passe lorsque les exigences temporelles d’une bascule (flip-flop) sont violées. Autrement dit, lorsque le signal sur son entrée de données n’est pas stable pendant la période qui commence tsu avant le front d’horloge et se termine thold après celui-ci. Le front d’horloge concerné est bien entendu celui qui arrive sur l’entrée d’horloge de la bascule. C’est ce front qui déclenche l’échantillonnage de l’entrée de données.

Bien sûr, si la donnée change pendant cette période, la sortie de la bascule après ce front d’horloge est imprévisible. Mais c’est encore pire : la bascule peut mettre nettement plus de temps que d’habitude à présenter un ‘0’ ou un ‘1’ valide sur sa sortie. Même si la bascule est conçue pour maintenir sa sortie stable sur l’une de ces deux valeurs possibles, une violation des exigences temporelles peut la faire rester dans un état instable pendant un court instant. Ou, pour reprendre le terme officiel, la bascule devient métastable.

Je ne vais pas éviter l’image cliché de la balle en équilibre au sommet d’une colline, souvent utilisée pour illustrer la situation instable dans laquelle une bascule peut se trouver :

Ball on tip of hill, illustrating metastability

La métastabilité (metastability) est un problème bien plus vaste que la simple incertitude sur la valeur que la bascule finira par adopter. C’est particulièrement vrai si la sortie de la bascule est connectée à plus d’une destination : comme la bascule met plus de temps à se fixer sur un ‘0’ ou un ‘1’, le signal risque de se stabiliser trop tard pour les bascules en destination. Par conséquent, certaines bascules à destination peuvent recevoir le signal hésitant comme un ‘0’, tandis que d’autres le reçoivent comme un ‘1’. Cette divergence peut amener la logique dans un état illégal, et la logique peut alors se comporter d’une manière digne de la magie noire. Une bascule exposée au risque de métastabilité ne doit donc jamais être connectée à plus d’un élément logique.

Pour traiter la métastabilité, il serait bon de savoir combien de temps une bascule met pour retomber dans l’un de ses états stables. Malheureusement, il n’y a pas de réponse précise à cette question. C’est exactement comme demander combien de temps la balle restera au sommet de la colline : cela dépend de nombreux facteurs, et selon toute probabilité, des vibrations aléatoires finiront par la faire basculer d’un côté ou de l’autre. Il en va de même pour la métastabilité d’une bascule : elle sortira de cet état grâce au bruit aléatoire des circuits électroniques, ou à autre chose.

En théorie, une bascule peut donc rester indéfiniment en métastabilité, mais en pratique, elle retombe dans l’un de ses états stables au bout d’un court instant. La durée passée dans cet état est une variable aléatoire. De nombreuses expériences et simulations ont été menées pour tenter d’estimer le comportement de cette variable aléatoire. Mais aucune de ces tentatives n’est vraiment pertinente, car ce comportement dépend de la technique de fabrication du silicium, de la température, du niveau de bruit provenant de la diaphonie et que sais-je encore.

Encore une fois, même s’il aurait été commode de disposer d’une durée maximale garantie que la bascule ne dépasserait jamais en métastabilité, une telle limite n’existe pas. Il n’est même pas possible d’obtenir une valeur approximative, car les procédés de fabrication les plus récents produisent des bascules qui tendent à sortir plus rapidement de l’état de métastabilité.

À la place, voici l’idée à laquelle il faut s’habituer : lorsque vous franchissez des domaines d’horloge avec des horloges non liées (unrelated clocks), il y a toujours un risque qu’une bascule reste en métastabilité plus longtemps que la conception ne peut le tolérer, et que quelque chose se passe mal en conséquence. La seule chose que nous puissions faire en tant que concepteurs est de réduire ce risque. Au mieux, nous pouvons atteindre un MTBF (Mean Time Between Failure, soit en français le temps moyen entre défaillances) avec lequel on peut vivre.

Heureusement, il existe une technique bien établie pour y parvenir, ce qui nous amène au sujet suivant.

La protection contre la métastabilité

Pour faire court, revenons au premier exemple de code de la page précédente. Si @clk1 et @clk2 sont des horloges non liées (unrelated clocks), la logique de resynchronisation habituelle pour obtenir un @bar stable à partir de @foo est :

reg foo, bar, bar_metaguard;

always @(posedge clk1)
  foo <= !foo;

always @(posedge clk2)
  begin
    bar_metaguard <= foo;
    bar <= bar_metaguard;
  end

Comme son nom l’indique, @bar_metaguard est une protection contre la métastabilité (metastability guard). Les exigences temporelles de la bascule qui implémente @bar_metaguard sont parfois violées lors de l’échantillonnage (sampling) de @foo. @bar_metaguard peut donc connaître de brefs épisodes de métastabilité. Comme on s’attend à ce que cette bascule en sorte rapidement, elle se stabilisera assez vite pour satisfaire l’exigence de temps de préparation (setup time) de @bar. Par conséquent, @bar peut être utilisé de manière fiable dans le domaine d’horloge de @clk2.

Cette explication peut sembler imprécise, et elle l’est effectivement. Elle contredit d’ailleurs ce que j’ai écrit plus haut au sujet de la métastabilité, car il n’existe pas de solution absolument sûre contre la métastabilité. J’y reviendrai plus bas. Mais pour l’instant, contentons-nous de la pratique courante, qui consiste à ajouter la protection contre la métastabilité comme ci-dessus, et à ne plus s’en préoccuper. À vrai dire, je n’ai jamais entendu quelqu’un se plaindre de problèmes avec cela.

Ceux qui veulent être encore plus prudents ajoutent davantage de registres. Une double protection contre la métastabilité se fait donc à peu près comme ceci :

reg foo, bar, bar_metaguard_a, bar_metaguard_b;

always @(posedge clk1)
  foo <= !foo;

always @(posedge clk2)
  begin
    bar_metaguard_a <= foo;
    bar_metaguard_b <= bar_metaguard_a;
    bar <= bar_metaguard_b;
  end

@bar_metaguard_a est la première protection contre la métastabilité. Si l’on n’a pas de chance, elle reste en état de métastabilité trop longtemps et les exigences temporelles de @bar_metaguard_b sont violées. @bar_metaguard_b connaît donc à son tour une période de métastabilité au cycle d’horloge suivant. Mais cette fois, la période de métastabilité sera brève, espérons-le : la foudre ne tombe jamais deux fois au même endroit, dit-on. Bien sûr, @bar_metaguard_b peut elle aussi rester en métastabilité assez longtemps pour violer le timing de @bar. Mais quelle est la probabilité ? Rappelez-vous que l’objectif est d’obtenir un MTBF raisonnable.

Pour résumer : si vous cherchez la solution « recette de cuisine » pour franchir un domaine d’horloge avec un bit unique, la voici. Une ou deux protections contre la métastabilité feront très bien l’affaire. Si vous cherchez une solution qui ne puisse jamais échouer, c’est malheureusement impossible. Mais si vous voulez réduire la probabilité d’incidents au maximum, lisez ce qui suit.

Analyse temporelle de la métastabilité

Revenons maintenant à l’exemple ci-dessus avec une seule protection contre la métastabilité, et demandons-nous pourquoi j’étais si sûr que @bar_metaguard sort de la métastabilité assez vite. La réponse, comme déjà mentionné, est qu’il n’y a aucune raison d’en être sûr.

Faisons tout de même un peu d’analyse temporelle (timing analysis). @bar_metaguard et @bar sont toutes deux synchronisées avec la même horloge. En supposant qu’aucune contrainte temporelle particulière n’a été assignée spécifiquement à ces registres, le chemin (path) entre elles sera limité par la contrainte temporelle de @clk2 (la période d’horloge). Autrement dit, les outils garantissent qu’en l’absence de métastabilité, le signal d’entrée de @bar est échantillonné dans des conditions de timing valables.

Mais tout l’intérêt de @bar_metaguard est justement qu’elle a droit, de temps à autre, à de la métastabilité. L’analyse temporelle est donc insuffisante : en réalité, le temps que la bascule passe dans l’état métastable s’ajoute à son délai de sortie (clock-to-output). Autrement dit, la métastabilité retarde la stabilisation de la sortie de la bascule par rapport à son délai habituel. Si donc le chemin entre @bar_metaguard et @bar présente une marge de timing (slack) quasi nulle (c’est-à-dire que son délai de propagation (propagation delay) est suffisant pour tenir le timing, mais sans marge), la métastabilité peut faire dépasser le délai total autorisé sur le chemin. Un tel événement provoquerait une violation de timing sur l’entrée de @bar. Cela vaut bien sûr pour le pire cas de température, de tension et de procédé de fabrication. Et pourtant : le fait que les outils appliquent la contrainte temporelle ordinaire sur la sortie de la protection contre la métastabilité signifie qu’ils ne garantissent pas les exigences temporelles nécessaires.

Heureusement, c’est facile à corriger. Ou plutôt à améliorer. La méthode consiste à ajouter une contrainte temporelle plus stricte sur tous les chemins allant des protections contre la métastabilité vers les registres qui doivent être fiables. En d’autres termes, il s’agit d’autoriser moins de délai de propagation (propagation delay) sur ces chemins. En réduisant le temps alloué au délai de propagation, on libère ce temps au profit de la sortie de métastabilité.

Par exemple, si tous les registres de protection contre la métastabilité portent le suffixe *_metaguard, on peut écrire une contrainte temporelle unique pour exiger que tous les chemins qui en partent disposent d’un peu de temps supplémentaire pour se stabiliser. Avec Vivado, cette contrainte s’écrirait ainsi :

set_max_delay -from [ get_cells -hier -filter {name=~*_metaguard*} ] 0.75

(set_max_delay est expliqué sur la page consacrée aux exceptions de timing)

Cette contrainte temporelle signifie que tout chemin partant d’une protection contre la métastabilité dispose de 0,75 ns pour atteindre sa destination (ces chemins sont sélectionnés grâce au suffixe des noms des registres). C’est le délai minimal que j’ai pu obtenir sans échouer la contrainte sur un FPGA particulier sur lequel j’ai essayé (le résultat peut donc différer sur d’autres FPGA). Pour trouver cette valeur, on choisit un nombre plus petit jusqu’à ce que les outils n’arrivent plus à satisfaire la contrainte. Après cet échec, on augmente la valeur pour obtenir une très petite marge (slack), par exemple 0,2 ns. Cela force les outils à faire de leur mieux sur ces chemins.

Cette commande set_max_delay équivaut à demander une période d’horloge de 0,75 ns (1333 MHz). Ainsi, si par exemple la période d’horloge réelle est de 250 MHz (4 ns), satisfaire cette contrainte donne un surplus d’au moins 4 – 0,75 = 3,25 ns. Par conséquent, si les protections contre la métastabilité restent en métastabilité jusqu’à 3,25 ns, aucune violation de timing ne se produira, grâce à cette contrainte set_max_delay.

C’est évidemment mieux que pas d’assurance du tout, mais 3,25 ns suffisent-elles ? Est-ce beaucoup ? Est-ce assez pour garantir un franchissement de domaines d’horloge pratiquement infaillible ?

Comme déjà dit, la réponse dépend de plusieurs facteurs. À en juger par les expériences publiées, mon impression personnelle est qu’il est extrêmement rare de voir une bascule rester en métastabilité pendant 1 ns ou plus, sur n’importe quel FPGA utilisé aujourd’hui en pratique. Mais il est difficile d’affirmer quoi que ce soit avec certitude sur ce point.

Mais en ajoutant cette contrainte temporelle, et donc en poussant les outils à faire de leur mieux, vous placez votre conception FPGA au niveau, voire au-dessus, de ce que font les autres. Aussi, dans l’esprit de la règle d’or n°4 (ne tentez pas le sort), placez-vous dans une situation telle que si cela échoue pour vous, beaucoup d’autres personnes auront aussi des raisons de se plaindre.

Une autre idée qui ressort de cette discussion sur le timing est que si la fréquence d’horloge est élevée pour le FPGA utilisé, une double protection contre la métastabilité peut être une bonne idée. Cela vaut en particulier si la contrainte temporelle supplémentaire suggérée plus haut n’est pas utilisée : comme mentionné plus haut, la métastabilité consomme de la marge (slack) sur le chemin. Plus la période d’horloge diminue (donc plus la fréquence augmente), plus la marge est souvent faible, et donc moins il y a de temps supplémentaire pour la métastabilité.

Pour finir, demandons-nous comment il se fait que presque tout le monde ignore toute cette question de timing, et que pourtant personne ne se plaint de problèmes. Je vais proposer quelques explications possibles, mais ce ne sont que des suppositions.

La première explication est qu’il y a de bonnes chances que les outils placent physiquement la protection contre la métastabilité très près de l’autre bascule dans la structure logique du FPGA. Le délai de propagation (propagation delay) entre ces deux bascules (par exemple de @bar_metaguard à @bar) est donc de toute façon très court pour ce FPGA. Ce placement rapproché n’est cependant pas garanti sans contraintes temporelles explicites, comme nous l’avons vu plus haut.

Dans les FIFO officielles fournies par les fabricants de FPGA, on trouve souvent ce type de paires de bascules sur la même tranche logique (slice). Quoi qu’il en soit, on peut faire confiance aux FIFO officielles pour avoir réglé ce problème.

Autre chose : à mesure que les FPGA deviennent plus rapides et que des fréquences d’horloge plus élevées deviennent possibles, il s’avère que les bascules sortent aussi plus rapidement de la métastabilité. Le temps nécessaire pour sortir de la métastabilité, ajouté au délai de propagation, reste donc nettement inférieur à la période d’horloge.

Il n’est donc pas surprenant que la plupart des gens se contentent d’ajouter la protection contre la métastabilité et ne s’en préoccupent plus. Ce n’est toutefois pas une excuse pour négliger d’ajouter une contrainte.

Limites liées aux fréquences des horloges

Je n’ai pas encore beaucoup parlé des fréquences des horloges dans chacun des domaines d’horloge. Par exemple, que se passe-t-il si l’horloge du domaine source (@clk1) est plus rapide que l’horloge du domaine de destination (@clk2) ?

Comme aucune synchronisation n’est requise entre les horloges, le fait que la fréquence de la source soit plus élevée ou plus basse n’a pas d’importance en soi. Mais si le signal de la source (@foo) change trop vite, il peut changer plusieurs fois d’état avant que la bascule à destination n’ait eu la possibilité d’échantillonner ce changement. Si toutes les transitions doivent donc être visibles à destination, la période d’horloge de la source doit être légèrement plus longue (c’est-à-dire sa fréquence plus basse) que celle de l’horloge de destination.

De combien ? Pas de beaucoup. Ou si vous insistez pour une réponse précise : cela dépend avant tout des exigences temporelles de la bascule à destination. Voici un calcul rapide (sautez ce passage si cela ne vous intéresse pas vraiment) :

Si l’entrée de cette bascule change exactement tsu avant le front d’horloge de la bascule, on est à la limite d’un échantillonnage correct ; il faut donc que ce changement soit échantillonné au cycle d’horloge suivant. Sinon, le changement risque d’être manqué. Pour que ce changement soit échantillonné de manière fiable au cycle suivant, le signal d’entrée doit rester stable jusqu’à thold après le front suivant. La période d’horloge de la source doit donc être plus longue que celle de la destination de tsu + thold + 2tj. tj représente l’incertitude sur la période d’horloge, c’est-à-dire la gigue (jitter). La gigue est comptée deux fois : une fois pour l’horloge de la source et une fois pour celle de la destination.

Ainsi, c’est tsu + thold + 2tj que j’entendais par période d’horloge « légèrement plus longue » pour l’horloge de la source.

Signaler les protections contre la métastabilité aux outils

La documentation de certains outils FPGA recommande d’ajouter un attribut aux bascules utilisées comme protections contre la métastabilité. Cela évite que les outils ne transforment ces bascules. Cela incite aussi les outils à placer la protection sur la même tranche logique (slice) que la bascule suivante, afin que le routage soit aussi court que possible.

Par exemple, Vivado dispose d’un attribut nommé ASYNC_REG. Comme précédemment, si tous les registres de protection contre la métastabilité portent le suffixe *_metaguard, la ligne suivante dans le fichier XDC définit l’attribut comme il se doit :

set_property ASYNC_REG true [get_cells -hier -filter {name=~*_metaguard*}]

Il est également possible de définir cet attribut dans le code Verilog.

Cependant, cet attribut ne changera probablement pas grand-chose, car les outils gèrent généralement correctement les protections contre la métastabilité de toute façon.

Ceci conclut la deuxième page de cette série. La page suivante présente des techniques pour faire passer des données entre domaines d’horloge.

Cette page a été traduite de l’anglais par une machine. En cas de doute, veuillez vous reporter au texte original
Copyright © 2021-2026. All rights reserved. (dcc38493)