Cette page fait partie d’une série de pages consacrée au timing. Les pages précédentes ont expliqué la théorie derrière les calculs de timing, montré comment écrire plusieurs contraintes temporelles et abordé les principes de la convergence temporelle (timing closure). Cette page explique comment définir des contraintes temporelles liées aux domaines d’horloge (clock domains).
Introduction
Fort de ce que nous avons appris sur la manière de trouver des éléments logiques précis et de définir des chemins (paths), nous allons maintenant examiner des contraintes temporelles qui utilisent ce savoir.
La première chose que nous allons examiner est la relation entre les contraintes temporelles et les domaines d’horloge. Ces deux sujets sont étroitement liés, et il est impossible d’aborder l’un sans faire intervenir l’autre. Je suggère donc de parcourir cette série de pages sur les domaines d’horloge, si ce sujet vous est nouveau. Cela est nécessaire, entre autres, pour comprendre la terminologie que j’emploierai plus bas.
La logique définit la relation entre les horloges
Regardons cet exemple de code Verilog :
module top(
input clk,
input foo,
output reg bar_reg,
output reg baz
);
reg foo_reg;
reg bar;
reg baz_metaguard;
wire pll_clk_8, pll_clk_6;
clk_wiz_1 pll_i
(.clk_in1(clk),
.clk_out1(pll_clk_8),
.clk_out2(pll_clk_6));
always @(posedge pll_clk_8)
foo_reg <= foo;
always @(posedge pll_clk_6)
begin
bar <= !foo_reg;
bar_reg <= bar;
end
always @(posedge clk)
begin
baz_metaguard <= bar;
baz <= baz_metaguard;
end
Cet exemple est presque identique à l’exemple avec une PLL que nous avons vu plus tôt. La différence est qu’il contient un nouveau changement de domaine d’horloge (clock domain crossing) : la valeur de @bar est recopiée dans @baz à travers une garde de métastabilité (metastability guard).
Il y a donc deux types de changement de domaine d’horloge dans cet exemple :
- De @foo_reg vers @bar, entre deux horloges liées (related clocks) (il n’y a pas de garde de métastabilité).
- De @bar vers @baz, entre deux horloges non liées (unrelated clocks) (@baz_metaguard est une garde de métastabilité).
Je n’ai examiné qu’un seul critère pour déterminer le type de changement de domaine d’horloge : la présence ou l’absence d’une garde de métastabilité. Si la logique s’attend à des horloges non liées, elle met en œuvre les mécanismes de sécurité nécessaires. Rien d’autre n’a donc d’importance : même s’il est possible de garantir les exigences temporelles sur les chemins entre ces horloges, il n’y a aucune raison de le faire.
Et inversement : si la logique s’attend à des horloges liées, il n’existe aucune protection contre les violations de timing. Les exigences temporelles doivent donc être satisfaites sur tous les chemins entre ces horloges.
En conclusion, la logique fait toujours des hypothèses sur les horloges qui sont liées et celles qui ne le sont pas. Ces hypothèses se reflètent dans la présence ou l’absence de mécanismes de protection. Mais ce sont les outils FPGA qui veillent à ce que ces hypothèses deviennent réalité. En particulier, ils garantissent que les exigences temporelles sont satisfaites sur les chemins entre horloges liées.
Il faut donc s’assurer que les outils connaissent les hypothèses de la logique concernant les horloges.
Notez que dans l’exemple ci-dessus, les outils n’ont aucun moyen de connaître les attentes de la logique concernant @pll_clk_6 et @clk : la logique peut supposer qu’il s’agit d’horloges liées, ou supposer le contraire. J’ai d’ailleurs utilisé un exemple semblable plus tôt (voir « Idée n° 9 ») pour illustrer un chemin entre horloges liées. Dans l’exemple ci-dessus, ces deux horloges sont traitées comme des horloges non liées.
Peut-être les outils pourraient-ils faire une supposition intelligente à partir du nom de @baz_metaguard. Peut-être la structure typique d’une garde de métastabilité fournirait-elle un indice. Mais cela ne suffit pas pour trancher une question aussi importante.
Informer les outils au sujet des horloges
Dans les exemples présentés jusqu’ici, il n’y avait qu’une seule contrainte temporelle :
create_clock -period 4.000 -name clk [get_ports clk]
La première question qui vient à l’esprit est de savoir comment les outils traitent par défaut le changement de domaine d’horloge entre @pll_clk_6 et @clk. Si cette contrainte est la seule, les outils appliqueront-ils quoi que ce soit sur le chemin entre @bar et @baz_metaguard ?
La réponse est que cela dépend de l’outil FPGA utilisé. Même si la quasi-totalité des outils FPGA considèrent @pll_clk_6 et @pll_clk_8 comme des horloges liées, ce qui se passe avec d’autres paires d’horloges n’a rien d’évident. Les outils traitent en général toutes les horloges comme liées par défaut, mais il n’est pas prudent de s’y fier.
De nombreux outils FPGA prennent en charge la commande set_clock_groups. C’est la meilleure option pour définir les relations entre horloges. En raison de l’importance de cette commande, il est recommandé de consulter la documentation de l’outil avant de l’utiliser dans un design.
Pour l’exemple ci-dessus, cette commande pourrait être utilisée ainsi sous Vivado (ou de façon équivalente avec d’autres outils) :
set_clock_groups -asynchronous \
-group [list \
[get_clocks -of_objects [get_pins pll_i/clk_out1] ] \
[get_clocks -of_objects [get_pins pll_i/clk_out2] ] ] \
-group [get_clocks -of_objects [ get_pins pll_i/clk_in1] ]
Cette utilisation de get_clocks et get_pins a été expliquée précédemment : chaque commande get_clocks trouve un objet d’horloge d’après le nom du port correspondant de clk_wiz_1 (notons que l’instanciation (instantiation) de clk_wiz_1 s’appelle pll_i). Par exemple, la dernière commande get_clocks obtient l’objet d’horloge pour @clk.
Cet exemple montre comment set_clock_groups définit des groupes d’horloges. Ici, un groupe est constitué de @pll_clk_6 et @pll_clk_8, et le second ne contient que @clk. Cette contrainte indique aux outils que toutes les horloges d’un même groupe sont des horloges liées. De même, si deux horloges appartiennent à des groupes différents, les outils les considèrent comme non liées.
Autrement dit, les outils appliquent les contraintes temporelles sur un chemin si et seulement si les horloges des deux côtés de ce chemin appartiennent au même groupe.
Plusieurs commandes set_clock_groups sont autorisées dans les contraintes temporelles d’un design. Mais il est préférable d’utiliser une seule commande set_clock_groups pour répartir toutes les horloges en groupes. Cela évite les contradictions et aide à prévenir les confusions : le principal avantage de cette commande est qu’elle constitue une description concise des relations entre horloges. Court, concis et mathématique. C’est ainsi que nous le voulons.
À noter que set_clock_groups doit être utilisée après toutes les commandes create_clock, car les objets d’horloge mentionnés doivent déjà exister.
Des relations incohérentes entre horloges
Si des horloges sont considérées comme liées à certains endroits du design et non liées à d’autres, il n’est pas possible d’utiliser set_clock_groups.
Par exemple, @clk et @pll_clk_6 sont traitées comme des horloges non liées dans le code Verilog ci-dessus. Mais en théorie, il pourrait exister de la logique supplémentaire qui les traite comme des horloges liées. Même si ce n’est pas une bonne idée, il est possible d’imposer des contraintes temporelles entre @pll_clk_6 et @clk pour cette logique supplémentaire.
Si set_clock_groups ne peut pas être utilisée pour cette raison, demandez-vous d’abord si vous ne préféreriez pas modifier la logique afin de pouvoir l’utiliser. Ce n’est pas seulement parce que cette commande est excellente : sans une règle simple indiquant quelles horloges sont liées et lesquelles ne le sont pas, il est facile de commettre des erreurs avec les changements de domaine d’horloge. Si vous ne souhaitez toujours pas modifier la logique en ce sens, la solution consiste à définir des chemins faux (false paths).
Quelques mots sur la commande set_false_path
Il existe deux commandes principales pour déclarer des chemins faux : set_clock_groups et set_false_path.
La commande set_clock_groups a été présentée plus haut : tout chemin entre deux horloges appartenant à des groupes différents est considéré comme un chemin faux. Mais set_clock_groups ne peut pas toujours être utilisée pour tous les chemins qui doivent être déclarés comme faux. Dans certains cas, la sélection des chemins doit être plus fine que la simple définition de groupes d’horloges. La commande set_false_path résout ce problème en permettant une sélection spécifique de chemins.
Par exemple, remplaçons la commande set_clock_groups ci-dessus par une commande set_false_path. La seule chose à corriger est le chemin entre @bar et @baz_metaguard. Pour tous les autres chemins, les exigences temporelles sont déjà correctement appliquées. Voici donc une manière d’écrire la commande set_false_path pour ce chemin. Mais ne prenez pas exemple sur cette façon de faire :
set_false_path -from [get_cells bar_reg__0] -to [get_cells baz_metaguard_reg]
Cette commande utilise get_cells pour sélectionner le début et la fin du chemin. Cela exige de connaître les noms des objets cellule de chaque côté. Ici, on a le nom « bar_reg__0 », ce qui illustre le problème posé par la dépendance aux noms : ce nom aurait dû être « bar_reg », mais, comme déjà expliqué, il s’est finalement retrouvé « bar_reg__0 » à cause d’une coïncidence.
Cet exemple montre donc l’un des problèmes de set_false_path : la sélection fine d’éléments logiques dans le design repose souvent sur les noms des objets. Ce problème et ses solutions possibles ont déjà été abordés.
Cette commande peut être simplifiée : puisque @baz_metaguard est une garde de métastabilité, peu importe d’où viennent les chemins qui aboutissent à ce registre. Pourquoi ne pas ignorer tous les chemins qui y mènent ?
set_false_path -to [get_cells baz_metaguard_reg]
L’effet de cette commande est exactement le même.
set_false_path au lieu de set_clock_groups
Une autre possibilité consiste à définir les chemins d’après les horloges. En réalité, la commande set_clock_groups ci-dessus peut être remplacée par ces contraintes :
set_false_path -from [get_clocks -of_objects [get_pins pll_i/clk_out1]] \
-to [get_clocks -of_objects [ get_pins pll_i/clk_in1] ]
set_false_path -from [get_clocks -of_objects [get_pins pll_i/clk_out2] ] \
-to [get_clocks -of_objects [ get_pins pll_i/clk_in1] ]
set_false_path -from [get_clocks -of_objects [ get_pins pll_i/clk_in1] ] \
-to [get_clocks -of_objects [get_pins pll_i/clk_out1] ]
set_false_path -from [get_clocks -of_objects [ get_pins pll_i/clk_in1] ] \
-to [get_clocks -of_objects [get_pins pll_i/clk_out2] ]
C’est la manière longue et détaillée de créer les mêmes chemins faux. Au lieu de répartir les horloges en groupes, on écrit deux commandes set_false_path pour chaque paire d’horloges non liées : une commande dans chaque sens. Il saute aux yeux qu’il est facile de se tromper avec autant de contraintes. Et ce n’est qu’un simple exemple à trois horloges.
Néanmoins, set_false_path est souvent utilisée de cette manière à la place de set_clock_groups. C’est probablement parce que quelqu’un a copié les contraintes d’ailleurs.
Exemple de la manière dont une erreur peut se produire
Prenons un exemple simple d’erreur possible : d’après la discussion ci-dessus, un seul chemin doit être déclaré comme faux : celui qui va de @bar à @baz_metaguard. Parmi les quatre commandes set_false_path du dernier exemple, une seule a donc de l’importance :
set_false_path -from [get_clocks -of_objects [get_pins pll_i/clk_out2] ] \
-to [get_clocks -of_objects [ get_pins pll_i/clk_in1] ]
Les trois autres commandes set_false_path ne couvrent aucun chemin. Mais attendez : ne pouvons-nous pas simplifier encore davantage la contrainte ? Peut-être que tous les chemins qui aboutissent à @clk devraient être des chemins faux ? Que diriez-vous de ceci ?
set_false_path -to [get_clocks -of_objects [ get_pins pll_i/clk_in1] ]
En fait, comme la commande create_clock qui définit « clk » se trouve juste au-dessus de la commande set_false_path, on peut écrire à la place :
set_false_path -to [get_clocks clk]
Cette contrainte est courte et élégante. Malheureusement, elle est horriblement fausse : j’ai suggéré que tous les chemins se terminant à @clk soient des chemins faux. Mais qu’en est-il du chemin qui va de @baz_metaguard à @baz ? C’est un chemin qui va de @clk vers @clk. Ce chemin ne doit évidemment pas être un chemin faux. Or les deux dernières commandes set_false_path incluent tous les chemins qui aboutissent à @clk, même lorsqu’ils partent de @clk.
Il est facile de commettre ce genre d’erreur, en particulier lorsque les contraintes de chemins faux sont écrites sans la précision d’un état d’esprit mathématique. Il peut être utile de créer des rapports de timing particuliers, qui révèlent quels chemins sont faux et lesquels ne le sont pas.
Ceci conclut la discussion pratique sur les contraintes temporelles pour les chemins internes au FPGA. La page suivante explique les chemins multicycles (multi-cycle paths), ce qui appartient aussi à ce sujet. Toutefois, les contraintes de chemins multicycles sont généralement déconseillées. Il est donc tout à fait acceptable de passer directement à l’introduction aux contraintes d’entrées-sorties à la place.