01signal.com

Franchissement de domaine d’horloge avec des données

Cette page est la dernière d’une série de trois pages consacrée aux domaines d’horloge.

Quand un bit ne suffit pas

Il arrive très souvent que le signal à faire passer entre domaines d’horloge soit un mot de données, et non un simple bit. La solution évidente dans ce cas est la FIFO à double horloge (dual-clock FIFO) fournie par le fabricant du FPGA, comme cela a déjà été suggéré. Mais parfois, cette option n’est pas possible. Et d’ailleurs, il a bien fallu que quelqu’un implémente cette FIFO un jour.

Le but est donc de faire apparaître correctement un signal vectoriel dans un autre domaine d’horloge. Commençons par un exemple naïf et incorrect de franchissement de domaine d’horloge (clock domain crossing), afin d’expliquer pourquoi ce n’est pas si simple :

reg [7:0] foo, bar, bar_metaguard;

always @(posedge clk1)
  foo <= foo + 1;

always @(posedge clk2)
  begin
    bar_metaguard <= foo; // This will fail sometimes!
    bar <= bar_metaguard;
  end

C’est exactement comme l’exemple de protection contre la métastabilité (metastability guard) de la page précédente, mais les registres sont des vecteurs de 8 bits, et @foo est un compteur au lieu de changer entre '0' et '1'.

Pourquoi est-ce incorrect ? Le problème vient des différences de délais de routage (routing delays) sur les chemins (paths) entre @foo et @bar_metaguard : chacun des huit bits présente un délai différent. Quand @foo change, certaines des transitions de ces bits peuvent arriver avec un timing valable aux bascules de @bar_metaguard, et d’autres n’arriveront pas à temps.

Ainsi, même s’il n’y a pas de métastabilité dans les 8 bascules qui constituent @bar_metaguard, il peut arriver que @foo change et que seulement certains bits de @bar_metaguard reçoivent la nouvelle valeur, tandis que d’autres conservent l’ancienne. Si @foo passe de 0xff à 0x00, la valeur suivante de @bar_metaguard pourrait être n’importe quoi, car certains bits ont capté la valeur de @foo avant le changement, et d’autres après. Cette valeur incorrecte sera visible sur @bar après un cycle d’horloge de @clk2.

Pour résoudre ce problème, il faut d’abord définir le besoin : @bar doit-il contenir une valeur valide en permanence, ou bien est-il destiné à faire passer de l’information, de temps à autre, d’un domaine d’horloge à l’autre ? Je vais examiner ces deux options séparément.

Option n°1 : échantillonnage continu

Si le mot de destination (@bar dans l’exemple) doit continuellement échantillonner (sampling) le mot de l’autre domaine d’horloge (@foo), et toujours contenir une valeur valide et signifiante, il n’y a qu’un seul moyen d’y parvenir : utiliser la méthode naïve de franchissement de domaine d’horloge montrée ci-dessus, mais en veillant à ce que, sur chaque cycle d’horloge de @clk1, un seul des bits de @foo change (ou aucun). Autrement dit, utilisez une protection contre la métastabilité avec le signal vectoriel, mais évitez le problème lorsque plusieurs bits changent dans le même cycle d’horloge.

Comme un seul bit peut changer à chaque cycle d’horloge, chaque transition est soit échantillonnée, soit manquée par @bar_metaguard ; dans les deux cas, on retrouve dans @bar l’une des valeurs que @foo a effectivement eues.

Ainsi, si @foo dans l’exemple ci-dessus était un compteur utilisant le code de Gray plutôt que du binaire pur, cela fonctionnerait parfaitement : l’essence du code de Gray est qu’un seul bit change à chaque incrément du mot, donc @bar serait garanti de toujours porter une valeur signifiante.

Mais attention, que se passe-t-il si @clk1 a une fréquence plus élevée que @clk2 ? Cela n’a pas d’importance s’il est acceptable que @bar saute certaines valeurs de @foo. Par exemple, si @foo est un compteur en code de Gray, certains nombres peuvent être sautés lorsqu’on regarde @bar. Et pourtant, toutes les valeurs observées sur @bar sont correctes dans le sens où elles ont bien été présentes sur @foo à un moment donné.

L’utilisation la plus courante de cette méthode se trouve à l’intérieur des FIFO à double horloge, où le code de Gray sert à transférer l’adresse dans la RAM de la FIFO entre les deux domaines d’horloge : le côté écriture de la FIFO code en code de Gray l’adresse du dernier mot qu’il a écrit dans la RAM. Le mot codé est transféré vers le côté lecture, qui se trouve dans un autre domaine d’horloge, en utilisant des protections contre la métastabilité sur le mot. La même méthode est utilisée dans l’autre sens.

Comme chaque côté connaît l’adresse mise à jour de l’autre côté (après le délai des protections contre la métastabilité), chaque côté peut aussi calculer combien d’éléments se trouvent dans la FIFO, et donc produire des signaux comme "empty" (vide) et "full" (plein).

Transmettre des événements à une cadence plus élevée

Rappelons, d’après la page précédente, que la protection contre la métastabilité avec un bit unique présente une limitation sur la fréquence de l’horloge du domaine source. Si chaque changement de ce bit est un moyen de signaler un événement à l’autre côté, le côté récepteur risque de manquer des événements si ceux-ci se produisent trop souvent.

La solution consiste à faire passer, entre les domaines d’horloge, un compteur codé en code de Gray. Ainsi, le côté récepteur sait combien d’événements se sont produits, et ne perd donc aucune information. Le nombre de bits de ce compteur est choisi de sorte que, même si les événements se produisent à chaque cycle d’horloge de @clk1, le côté récepteur soit encore capable de déduire combien d’événements ont eu lieu.

En ce sens, il peut être plus facile de travailler avec un vecteur qu’avec un bit unique : si un bit unique change et que ce fait est manqué parce que l’horloge de destination est plus lente, on conclut qu’il ne s’est rien passé du tout. En revanche, avec un mot correctement transféré entre domaines d’horloge (par exemple avec le code de Gray), aucune information n’est perdue.

Et si un seul bit suffit à cet usage, le compteur en code de Gray n’est alors qu’un bit unique qui change de valeur à chaque événement. Autrement dit, avec un bit unique, la solution du compteur en code de Gray est exactement la même que la simple protection contre la métastabilité.

Un commentaire pointilleux sur le timing des chemins

Comme le titre de cette section le laisse entendre, il est peut-être prudent de passer directement à la section suivante.

Le principe de la protection contre la métastabilité pour les signaux vectoriels repose sur une hypothèse sous-jacente : la différence entre les délais de ces chemins ne doit pas dépasser un cycle d’horloge de l’horloge source.

Cette hypothèse est presque certainement satisfaite sans qu’il soit nécessaire de faire quoi que ce soit, et pourtant, examinons cet exemple théorique : disons que @clk1 a une fréquence de 500 MHz, et que l’un des chemins de @foo vers @bar_metaguard a un délai de routage de 1 ns. Disons aussi qu’un autre chemin a un délai de routage de 4 ns – ce qui est évidemment extrêmement improbable, mais voyons ce qui peut se passer :

L’un des bits change de valeur, et cette transition entame un voyage qui dure 4 ns. Au cycle d’horloge suivant, 2 ns plus tard, l’autre bit change et atteint @bar_metaguard au bout de 1 ns, c’est-à-dire 1 ns avant l’arrivée du premier bit. @bar_metaguard peut donc échantillonner le mot entier avec une valeur que @foo n’a jamais eue.

Comme les délais de routage sont généralement beaucoup plus courts que dans cet exemple, cela ne devrait pas se produire en pratique. Néanmoins, n’importe quel délai de routage est théoriquement possible. Pour éliminer complètement cette possibilité, on peut utiliser une contrainte temporelle (timing constraint) du genre de celle-ci (au format Vivado) :

set_max_delay -datapath_only -from [ get_pins -hier -filter {name=~*/C} ] -to [ get_pins -hier -filter {name=~*_metaguard*/D} ] 1.5

Cette contrainte ressemble à celle avec set_max_delay donnée à la page précédente. Remarquez toutefois que, dans la page précédente, la protection contre la métastabilité se trouve dans la partie « -from », alors qu’ici elle se trouve dans la partie « -to ». Ces contraintes ne s’appliquent donc pas aux mêmes chemins. Le but de la contrainte de la page précédente était de laisser à la protection contre la métastabilité le temps de sortir de la métastabilité. En conséquence, cette contrainte s’applique à des chemins situés dans la même horloge. La contrainte ci-dessus, elle, concerne le franchissement de domaine d’horloge lui-même.

Cette contrainte s’écrit donc différemment : comme les chemins concernés relient des domaines d’horloge d’horloges non liées (unrelated clocks), il n’a pas de sens de tenir compte des décalages d’horloge (clock skew) et des gigues (jitter) de ces horloges. C’est ce que signifie la partie -datapath_only : ne tenez pas compte du temps mis par les horloges pour atteindre les bascules. Mesurez simplement le chemin.

Ce qui rend cette contrainte déroutante, c’est que le chemin commence à la broche d’horloge de la bascule source et se termine à la broche de données (D) de la destination. Le chronomètre démarre donc lorsque la bascule source reçoit son horloge et s’arrête lorsque le signal mis à jour arrive à destination, en veillant à respecter son temps de préparation (setup time). Ce chemin inclut donc les exigences temporelles des deux côtés.

En limitant tous ces chemins à 1,5 ns, comme le fait cette contrainte temporelle, aucun chemin ne peut dépasser cette limite, et l’écart entre les délais des chemins est donc lui aussi plafonné à cette valeur. Ainsi, même si @clk1 a une période de 2 ns, il est impossible que les chemins arrivent dans le mauvais ordre. Ce qui, encore une fois, est extrêmement improbable de toute façon, mais c’est ainsi qu’on peut s’en assurer.

Notez que si le chemin qui mène à la protection contre la métastabilité est soumis à une contrainte de faux chemin (false path) (par exemple set_false_paths ou set_clock_groups), set_max_delay n’aura probablement aucun effet : la contrainte de faux chemin a toute chance de prendre le dessus. Vérifiez donc toujours les chemins dans le rapport de timing afin de vous assurer que les outils interprètent les contraintes comme vous le souhaitez. Une autre page consacrée au timing aborde ce sujet.

Option n°2 : mise à jour occasionnelle du registre

La restriction selon laquelle un seul bit peut changer à chaque cycle d’horloge est souvent trop contraignante. Lorsque la donnée n’est mise à jour qu’occasionnellement, une autre technique peut être employée. Pour l’exemple suivant, supposons que @do_update ne soit actif (c’est-à-dire à la valeur '1') qu’une fois tous les quelques cycles d’horloge. Supposons aussi que ce registre serve à indiquer que la valeur de @foo doit être mise à jour avec @new_value :

reg [7:0] foo, bar;
reg       toggle, toggle_metaguard, toggle_a, toggle_b;
reg       new_bar;

always @(posedge clk1)
  if (do_update)
    begin
      foo <= new_value;
      toggle <= !toggle;
    end

always @(posedge clk2)
  begin
    toggle_metaguard <= toggle;
    toggle_a <= toggle_metaguard;
    toggle_b <= toggle_a;

    if (toggle_a != toggle_b)
      bar <= foo; // No metastability guard, because foo is stable
    new_bar <= (toggle_a != toggle_b); // Not necessary, just side info
  end

Ignorons @new_bar pour l’instant. J’y reviendrai plus tard.

Voici comment cela fonctionne : @foo n’est mis à jour que lorsque @do_update est actif. À ce moment, @toggle change de valeur le même cycle d’horloge.

Dans le domaine d’horloge de @clk2, @toggle_metaguard reçoit la valeur de @toggle en tant que protection contre la métastabilité. Au cycle d’horloge suivant, cette valeur est copiée dans @toggle_a. Au cycle d’horloge d’après, la valeur de @foo est copiée directement dans @bar, car @toggle_a et @toggle_b ont des valeurs différentes pendant exactement un cycle d’horloge.

Le fait que @bar et @foo appartiennent à des domaines d’horloge différents n’a pas d’importance, car @foo est stable depuis bien plus longtemps qu’il n’en faut pour satisfaire les exigences temporelles.

Pourquoi en suis-je si sûr ? Cette fois, j’ai une bonne raison, et la voici : toute la procédure commence lorsque @toggle_metaguard change de valeur parce que @toggle a changé. Si @bar avait échantillonné @foo au même cycle de @clk2, cela aurait été risqué, mais avec un peu de chance cela aurait pu passer. Mais il s’écoule ensuite un autre cycle de @clk2 avant que la nouvelle valeur de @toggle_metaguard n’atteigne @toggle_a. Et @bar n’est même pas mis à jour à ce moment-là, mais seulement au cycle suivant de @clk2.

Ainsi, depuis le moment où @foo change jusqu’à celui où @foo est échantillonné par @bar, il s’écoule une durée qui correspond au moins à deux cycles de @clk2. Comparée au temps de préparation (setup time) de n’importe quelle bascule, c’est une éternité. Cela dit, il est raisonnable d’appliquer set_max_delay comme indiqué à la page précédente sur @toggle_metaguard. On peut faire de même pour les chemins qui mènent à @bar, même s’il est très peu probable que ce soit nécessaire, à cause de cette éternité dont je viens de parler.

Le talon d’Achille de cette méthode, c’est que @do_update doit être actif assez rarement pour garantir que @foo reste stable lorsqu’il est échantillonné par @bar. Un intervalle minimal raisonnable entre de telles mises à jour correspond à quatre cycles d’horloge de @clk2. Le calcul consiste donc à déterminer à combien de cycles de @clk1 correspondent quatre cycles de @clk2, puis à arrondir à l’entier supérieur. Si @clk1 est quatre fois plus lente que @clk2 (ou plus lente encore), cette restriction n’en est pas une. Sinon, il faut prévoir dans la logique un mécanisme qui empêche @do_update de devenir actif plus souvent que permis.

À vrai dire, dans les conceptions réelles, lorsque la cadence de mise à jour est très lente, il arrive que l’on franchisse les domaines d’horloge sans précaution particulière, sans la protection qu’offre @toggle. Dans ce cas, @foo est copié en continu dans @bar. Quand @foo change une fois de temps en temps, @bar peut contenir une valeur incorrecte pendant un cycle d’horloge, mais qui s’en soucie ? Le plus souvent, cette erreur résulte du fait que l’on néglige toute la question des domaines d’horloge, parce que hé, ça fonctionne. Jusqu’à ce que, de temps en temps, ça ne fonctionne plus.

En parlant de négligence, remarquez que ni @toggle ni aucun de ses registres associés n’est réinitialisé ni n’a de valeur initiale dans l’exemple ci-dessus. C’est généralement sans conséquence, car il y a fort à parier que le synthétiseur (synthesizer) leur attribue à tous une valeur initiale de 0. Et même si ces registres n’ont pas la même valeur au départ, il en résultera simplement un échantillonnage inutile de @foo, et rien de plus. Il peut toutefois être bon de réinitialiser ces registres malgré tout.

Variantes plus avancées

Jusqu’ici, j’ai présenté trois exemples simples :

Ces exemples simples servent de base à plusieurs autres mécanismes.

Tout d’abord, j’ai promis de dire un mot sur @new_bar dans l’exemple ci-dessus. Il s’agit simplement d’un registre qui est à l’état haut pendant un cycle d’horloge, lorsque @bar a une nouvelle valeur. Rien de particulier là-dedans, mais remarquez que @bar et @new_bar reflètent @foo et @do_update dans l’autre domaine d’horloge. C’est donc une façon de faire passer des commandes et des messages d’état entre domaines d’horloge (ai-je mentionné qu’il vaut mieux utiliser une FIFO lorsque c’est possible ?).

Une autre extension intéressante du dernier exemple consiste à remplacer la paire de registres @foo et @bar par une RAM à double port. C’est une méthode pour faire passer des tampons de données (buffers) entre domaines d’horloge : supposons que la logique du domaine de @clk1 écrive des données dans la RAM et remplisse une moitié de cette RAM au bout d’un certain temps. Pendant que la logique continue de remplir la seconde moitié, elle change la valeur de @toggle. Ce registre est copié dans le domaine de @clk2, exactement comme ci-dessus. Mais au lieu de mettre @bar à jour comme dans l’exemple, la logique consomme les données de la première moitié de la RAM.

C’est ainsi qu’un simple registre peut synchroniser un mécanisme de double tampon (double buffer), où un côté écrit les données dans la RAM et l’autre côté les lit. En fait, le rôle de @toggle ne se limite pas à changer de valeur : il indique aussi à l’autre côté quelle moitié de la RAM est en cours d’écriture.

Et pourtant, il vaut mieux utiliser une FIFO quand c’est possible. Même si ce mécanisme de double tampon peut sembler tentant, il ne devrait être utilisé qu’en l’absence de meilleure alternative. Par exemple, lorsque les données de la RAM sont lues dans un ordre différent de celui dans lequel elles y ont été écrites.

Résumé

En fin de compte, tout se résume à ceci : dans la transition entre domaines d’horloge d’horloges non liées (unrelated clocks), il y a toujours une logique de resynchronisation. Le mot de données qui traverse cette resynchronisation est limité, de sorte qu’un seul bit peut changer de valeur à chaque cycle d’horloge de l’horloge source (@clk1 dans les exemples). Sinon, des données illégales peuvent arriver à destination.

Dans certaines applications, cela suffit amplement, mais lorsque cette limitation est trop contraignante, on peut faire passer les données entre domaines d’horloge par l’intermédiaire d’un registre vectoriel ou d’une RAM, sans qu’aucune logique de resynchronisation ne soit appliquée aux données elles-mêmes. Cela fonctionne grâce à une logique qui maintient un intervalle de temps minimal entre l’écriture et la lecture des données. Cet intervalle garantit que le mot de données est stable lorsqu’il est échantillonné à destination. Cette logique repose néanmoins sur la même technique de franchissement de domaine d’horloge. Par conséquent, cette solution fait appel à une logique de resynchronisation qui se limite à changer un bit à la fois, éventuellement en utilisant le code de Gray.

Ainsi, la logique de resynchronisation et cette règle du bit unique sont toujours présentes dès lors que des horloges non liées (unrelated clocks) sont impliquées. C’est simplement une question de savoir comment les appliquer.

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)