01signal.com

Utiliser les commandes Tcl pour sélectionner des éléments logiques

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, abordé la contrainte de période d’horloge, présenté les principes de la convergence temporelle (timing closure) et commencé à explorer l’environnement Tcl. Cette page explique les commandes qui permettent de rendre les contraintes temporelles précises.

Vue d’ensemble

Rappelons que le but des contraintes temporelles (timing constraints) est de garantir que les exigences temporelles de tous les chemins (paths) du design sont satisfaites. Différents chemins peuvent avoir des exigences différentes ; chaque contrainte temporelle doit donc viser le groupe de chemins concerné, sans inclure autre chose.

La seule manière de définir des chemins est de faire référence à des éléments logiques liés à ces chemins. Il est donc important de savoir écrire des expressions précises qui sélectionnent les bons groupes d’éléments logiques. Autrement dit, il faut savoir utiliser correctement des commandes telles que get_clocks, get_ports et get_cells, afin que leurs résultats correspondent exactement à ce que l’on cherche.

Cette page passe en revue les techniques fondamentales pour décrire des groupes d’éléments logiques. Presque tout ce qui suit est propre à la syntaxe SDC. Je supposerai aussi qu’une interface en ligne de commande Tcl est disponible. C’est le cas pour Vivado et Quartus, ainsi que pour la plupart des outils FPGA récents.

Certains fabricants de FPGA déclarent ouvertement que le logiciel de Synopsys est intégré dans leurs outils ; il est donc naturel que ces outils utilisent les mêmes commandes Tcl. Vivado, quant à lui, n’est pas considéré comme dérivé de Synopsys. Il existe pourtant quelques ressemblances frappantes entre les deux, notamment en ce qui concerne l’interface Tcl. Quartus semble avoir été développé indépendamment, et son interface Tcl est donc un peu différente.

À première vue, cette page pourrait sembler trop plongée dans les détails des scripts Tcl. La vérité est tout le contraire : la précision est extrêmement importante, et elle ne peut être obtenue qu’en comprenant exactement comment les commandes sont interprétées. D’ailleurs, cette page n’explique que les principes de base. Rien ne remplace la lecture de la documentation.

Se faire guider par les outils FPGA

Il est souvent difficile de se lancer dans l’écriture de contraintes temporelles, car il y a énormément de détails à surveiller. Les outils FPGA peuvent aider.

La commande « help » peut être utilisée dans la console Tcl pour consulter la documentation d’une commande Tcl. C’est souvent exactement la même information que dans les documents officiels (par exemple au format pdf).

La plupart des outils FPGA offrent une interface graphique pour créer des contraintes temporelles automatiquement. Cette méthode sert d’habitude à choisir le type de contrainte temporelle nécessaire. L’étape suivante consiste à sélectionner dans une liste les éléments logiques concernés. Le résultat est une ou plusieurs contraintes temporelles ajoutées au fichier SDC.

Utiliser un assistant de ce genre est utile pour se faire une idée de la syntaxe Tcl. Les contraintes générées automatiquement peuvent parfois être utilisées telles quelles. Cela dit, dans la plupart des cas, il vaut mieux résister à la tentation d’utiliser directement la sortie de l’assistant et réfléchir plutôt soigneusement à la meilleure façon d’atteindre l’objectif de la contrainte. Il est aussi important de réfléchir à l’évolution prévisible du projet et de s’assurer que les contraintes resteront correctes avec le temps. Une session rapide avec un assistant graphique ne permet généralement pas d’y parvenir.

Certains outils FPGA ont aussi une interface graphique pour trouver des éléments logiques dans le design. Lorsqu’on utilise cette interface, la commande Tcl qui correspond à la recherche demandée est souvent affichée. C’est un moyen pratique d’obtenir des expressions Tcl pour trouver des éléments logiques précis. Là encore, ces expressions devraient servir de point de départ pour un travail plus approfondi.

Une troisième fonctionnalité de la plupart des outils FPGA est une console en ligne de commande Tcl qui permet de taper des commandes (ou de faire un copier-coller) et d’en voir le résultat à l’écran. On peut ainsi tester les commandes et leurs motifs de recherche, et voir quels objets sont trouvés. Cela aide à vérifier que le motif de recherche est correct.

En conclusion, les outils FPGA peuvent aider à créer la commande Tcl. Mais, comme déjà dit, les commandes générées par les outils ne devraient être qu’une base pour écrire des motifs de recherche précis dont on est certain qu’ils fonctionneront correctement dans le temps.

Maintenant que nous connaissons le moyen paresseux de créer des contraintes temporelles, il est temps d’apprendre à le faire correctement.

La netlist : un bref rappel

Avant d’entrer dans le détail de l’environnement Tcl, je voudrais faire un bref rappel sur les netlists.

Le format de fichier le plus courant pour les netlists est l’EDIF, mais beaucoup d’outils FPGA ont aussi leur propre format. En général, c’est le synthétiseur (synthesizer) qui crée la netlist, même si les outils peuvent encore la modifier au cours des étapes suivantes. Ce fichier décrit la conception logique en termes de composants de base et de connexions entre ces composants. C’est un peu comme un schéma de câblage, mais sous forme textuelle plutôt que graphique.

Les composants d’une netlist s’appellent des « cellules » (cells). La grande majorité des cellules sont des sortes de LUT, un autre petit élément de logique combinatoire (combinatorial logic), ou une bascule. De même, les instanciations (instantiation) de boîtes noires dans le code Verilog (par exemple des cœurs IP — IP cores) sont représentées par une cellule dans la netlist. D’autres cellules sont des PLL, des block RAM et de grands éléments logiques (« hard IP ») : blocs PCIe, émetteurs-récepteurs MGT, processeurs, etc.

Chaque cellule possède un certain nombre de broches (pins) : ces broches sont analogues aux points de connexion externes d’un composant électronique physique. Mais il ne faut pas confondre cela avec les entrées-sorties externes du FPGA : les cellules comme les broches existent à l’intérieur du FPGA.

L’interconnexion dans la netlist est constituée de fils (nets), qui sont comme des fils physiques. Par exemple, lorsqu’un signal est défini avec « wire » en Verilog, cela donne un fil. Un fil relie deux broches ou plus, garantissant ainsi que ces broches ont toujours le même niveau logique.

La représentation des éléments logiques comme objets

Lorsque les outils FPGA exécutent une implémentation de projet, ce qui se passe réellement est que des scripts Tcl sont exécutés. C’est vrai pour Vivado, pour Quartus et pour plusieurs autres outils FPGA. Même avec un logiciel qui fonctionne différemment, cette hypothèse reste valable : l’illusion que tout est un grand script Tcl est créée par l’API destinée aux contraintes et aux autres fichiers de scripts.

Dans l’environnement de ce script Tcl (imaginaire ou non), tous les éléments logiques sont représentés comme des objets créés à partir de différentes classes. Les contraintes temporelles d’un fichier SDC (ou XDC pour Xilinx) peuvent accéder à ces objets. De même, les commandes Tcl dans la console et les scripts Tcl ont accès à ces objets.

Voici cinq commandes Tcl prises en charge par tous les outils FPGA qui travaillent avec la syntaxe SDC. Ces commandes servent à trouver des objets de différents types (c’est-à-dire de différentes classes). Je les ai déjà utilisées dans les exemples précédents de contraintes temporelles. En fait, il est tout simplement impossible d’écrire des contraintes temporelles utiles sans ces commandes.

Sans argument, ces commandes trouvent tous les objets du type concerné. Nous verrons plus loin comment affiner la recherche.

En dehors de ces cinq commandes, chaque outil FPGA possède ses propres commandes et ses propres objets supplémentaires. Par exemple, Vivado a des commandes supplémentaires telles que all_ffs, all_registers, all_inputs, all_outputs, all_rams et quelques autres du même genre. Quartus en prend en charge certaines et dispose aussi de get_registers, get_keepers, get_nodes, get_fanins, get_fanouts, etc.

Ces objets sont importants parce qu’ils servent dans les contraintes temporelles à désigner des éléments logiques du design FPGA. Ils peuvent aussi servir dans des scripts Tcl pour obtenir des informations, par exemple la fréquence d’une horloge. Chaque outil FPGA a sa propre API pour accéder à ces objets dans les scripts Tcl. Par exemple, cette commande Tcl peut être utilisée dans Vivado pour obtenir la période d’une horloge :

> get_property PERIOD [get_clocks clk]
4.000

Et l’équivalent dans Quartus :

> get_clock_info -period [get_clocks clk]
4.000

Malgré ces différences, la plupart des outils FPGA s’accordent sur la syntaxe et le sens des contraintes temporelles au format SDC. Les informations sur l’API supportée par chaque outil se trouvent généralement dans des guides d’utilisation dont les titres concernent les scripts Tcl ou la convergence temporelle.

Sauf indication contraire, les exemples de cette page sont basés sur Vivado.

Quelques remarques sur Tcl

Tcl est un langage ancien, mais comme il est solidement implanté dans le domaine de la conception logique, ce langage n’est pas près de disparaître. Heureusement, on peut faire des choses utiles en Tcl sans le connaître parfaitement.

D’abord, une chose que j’ai déjà mentionnée brièvement : les crochets (« [ » et « ] »). En Tcl, cela signifie qu’il faut exécuter la commande contenue entre les crochets et placer le résultat à cet endroit. Pour ceux qui connaissent les scripts shell ou Perl, c’est comme les guillemets inversés (backticks). Par exemple, dans cette commande, get_ports est remplacé par l’objet de port nommé « clk » :

create_clock -period 4.000 -name clk [get_ports clk]

On aurait d’ailleurs pu écrire ceci :

set the_clk_port [get_ports clk]
create_clock -period 4.000 -name clk $the_clk_port

Comme le montre ce second exemple, les variables se définissent et se voient affecter une valeur avec la commande « set ». Pour accéder à la valeur d’une variable, on utilise le signe dollar ($), comme en script shell et en Perl.

Quant aux accolades (« { » et « } »), c’est une autre histoire : comme dans plusieurs autres langages, leur signification dépend beaucoup du contexte. En Tcl, l’un des sens les moins attendus des accolades est que la chaîne qu’elles entourent doit rester telle quelle. Autrement dit, aucune substitution ne doit avoir lieu, et les espaces doivent être considérés comme des caractères ordinaires. Par exemple, la même contrainte temporelle aurait pu être écrite ainsi :

create_clock -period {4.000} -name {clk} [get_ports {clk}]

Dans cet exemple, les accolades sont parfaitement inutiles, et cette commande signifie exactement la même chose qu’avant. Les accolades inutiles sont malheureusement courantes, et souvent elles ne veulent rien dire, comme dans cet exemple.

Une astuce pour la console Tcl

Il arrive souvent que le nombre d’objets trouvés soit important, ce qui rend la lecture de la sortie difficile. On peut résoudre cela avec de simples commandes Tcl. La manière exacte de procéder dépend de l’outil utilisé. Avec Vivado, cette commande affiche toutes les cellules du design. Chaque cellule est affichée sur une ligne séparée, ce qui rend la sortie lisible même s’il y a beaucoup de cellules :

> join [get_cells -hierarchical] \n
GND
VCC
bar__0_i_1
bar_reg_OBUF_inst
bar_reg__0
[ ... ]

La commande « join » insère un saut de ligne entre chaque élément du tableau produit par get_cells.

Avec Quartus, on obtient le même résultat avec :

join [query_collection -all [get_cells -hierarchical] ] \n

Ou encore, en utilisant query_collection plus intelligemment :

query_collection -all -report_format [get_cells -hierarchical]

Rechercher des éléments précis

Après une longue introduction, il est enfin temps de parler de ce qui est vraiment intéressant. Pour les exemples ci-dessous, référez-vous au code Verilog suivant, qui est aussi utilisé plus loin :

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

Comme mentionné en haut de cette page, la précision des contraintes temporelles dépend de la précision avec laquelle on sélectionne les groupes d’éléments logiques. Il est donc possible et nécessaire de restreindre les résultats de recherche des cinq commandes get_* mentionnées plus haut. Il y a plusieurs façons de faire, mais la plus courante repose sur le nom de l’objet. Le motif le plus simple consiste à trouver un objet unique dont le nom correspond exactement à ce que l’on cherche. Par exemple, dans la console Tcl de Vivado :

> get_ports clk
clk

De même, il est possible de trouver tous les objets dont le nom correspond à un motif précis :

> get_pins pll_i/*
pll_i/clk_in1 pll_i/clk_out1 pll_i/clk_out2

Notez que, dans les deux exemples, la sortie est constituée d’objets. Dans la console Tcl de Vivado, les noms de ces objets sont affichés par commodité.

Plus important encore, notez que chaque outil FPGA se comporte légèrement différemment avec ces motifs de recherche. Vivado est utilisé dans les exemples ici. Les mêmes principes s’appliquent aux autres outils.

L’astérisque (« * ») est un caractère générique qui remplace n’importe quelle suite de caractères. Un point d’interrogation (« ? ») remplace un caractère unique. Cela fonctionne comme les caractères génériques pour les noms de fichiers.

Notez que, comme pour les noms de fichiers, les caractères génériques ne correspondent pas au séparateur de hiérarchie (par exemple « / » avec Vivado et « | » avec Quartus).

Il y a aussi une ressemblance entre le chemin hiérarchique et les répertoires de fichiers : la recherche se fait par rapport au niveau supérieur de la hiérarchie, un peu comme à partir du répertoire racine d’un système de fichiers. Ainsi, par exemple :

> get_pins */clk_*
pll_i/clk_in1 pll_i/clk_out1 pll_i/clk_out2
> get_pins pll_i/clk_out?
pll_i/clk_out1 pll_i/clk_out2

La nécessité de préciser la position exacte de chaque élément logique dans la hiérarchie est souvent un inconvénient important : les éléments que l’on cherche se trouvent fréquemment à des positions différentes. Cela se résout avec l’option « -hierarchical » : lorsque cet indicateur est présent, la recherche du motif est faite à toutes les positions de la hiérarchie.

D’une manière générale, l’usage de caractères génériques pour trouver des éléments logiques n’est pas recommandé. Les seules exceptions sont lorsque le motif de recherche est très simple ou lorsqu’on n’a pas le choix. Il existe une page dédiée qui explique comment utiliser les caractères génériques et « -hierarchical », et qui montre aussi les limites de cette méthode.

Utiliser -filter

Les motifs de recherche fondés sur des caractères génériques présentent plusieurs inconvénients. Le plus important est que la hiérarchie doit être définie très précisément, ou pas définie du tout. Un problème est que l’on souhaite souvent limiter la recherche aux objets appartenant à une sous-hiérarchie. Or cela n’est pas possible avec les caractères génériques, et l’option « -hierarchical » ne résout pas ce problème.

Pour cette raison et pour d’autres, la manière préférée de définir des motifs de recherche est l’option -filter. Cette option s’accompagne d’une expression booléenne. Lorsqu’elle est utilisée, seuls les résultats de recherche pour lesquels cette expression est vraie sont conservés.

Par exemple,

> get_pins */clk_*
pll_i/clk_in1 pll_i/clk_out1 pll_i/clk_out2
> get_pins */clk_* -filter {name =~ *2*}
pll_i/clk_out2

Dans cet exemple, la propriété nommée « name » a été examinée sur chacun des objets trouvés grâce au caractère générique. L’objet n’est conservé dans le résultat que si cette propriété correspond au motif « *2* ». Autrement dit, seulement si le nom de l’objet contient « 2 ».

Cet exemple n’a pas grand intérêt pratique. Il devient bien plus intéressant lorsqu’on utilise « -hierarchical » : sans motif de recherche, tous les objets sont trouvés. Autrement dit, cette commande trouve toutes les broches de toutes les hiérarchies :

get_pins -hierarchical

À partir de là, on peut restreindre les résultats avec -filter :

> get_pins -hierarchical -filter {name =~ pll_i/*/*out1 }
pll_i/inst/clk_out1
> get_pins -hierarchical -filter {name =~ pll_i/*out1 }
pll_i/clk_out1 pll_i/inst/clk_out1
> get_pins -hierarchical -filter {name =~ *out1 }
pll_i/clk_out1 pll_i/inst/clk_out1

Notez que les accolades (« { » et « } ») signifient simplement que la partie qu’elles contiennent ne doit pas être modifiée par l’interpréteur Tcl.

Remarquez aussi que le motif de recherche appartient à l’option -filter et qu’il se comporte différemment : il traite le « nom » comme une propriété d’un objet. Tous les caractères sont donc traités de la même manière : « / » n’a aucune signification particulière. Peu importe que « / » soit le séparateur de hiérarchie. Tous les caractères, y compris « / », peuvent correspondre au caractère générique (« * »). De même, tous les caractères, y compris « / », peuvent être utilisés dans le motif de recherche. Ce n’est évidemment pas le cas sans -filter.

Le fait que n’importe quel caractère puisse correspondre à « * » fait de -filter un outil plus puissant, mais cet avantage est aussi une source d’erreurs : il est facile d’oublier qu’un « * » innocent peut accidentellement correspondre à la fois à « pll_i/clk_ » et à « pll_i/inst/clk_ », comme le montre l’exemple ci-dessus.

Un usage correct de cette fonctionnalité consiste à trouver un élément logique dont on connaît le nom quelque part dans la hiérarchie :

> get_pins -hierarchical -filter {name =~ */clkout2_buf/O }
pll_i/inst/clkout2_buf/O

C’est correct si l’on est certain qu’il n’existe qu’une seule cellule nommée « clkout2_buf » dans le design. Avec cette commande, la broche de sortie de cette cellule sera toujours trouvée, même si le module qui la contient est déplacé dans la hiérarchie du projet. En situation réelle, mieux vaut choisir un nom plus caractéristique que « clkout2_buf ».

La même méthode fonctionne avec get_cells et get_nets, par exemple :

> get_cells -hierarchical -filter {name =~ */clk*_buf}
pll_i/inst/clkf_buf pll_i/inst/clkout1_buf pll_i/inst/clkout2_buf

Mais -filter agit sur toutes les propriétés, pas seulement sur le nom. Cette commande trouve ainsi tous les registres qui contiennent « bar » dans leur nom :

get_cells -hier -filter {primitive_type =~ register.*.* && name =~ *bar*}

Notez l’opérateur « && », qui signifie ET logique (comme en Verilog et en C).

Dans cette commande, « primitive_type » et « name » ne sont que des noms de propriétés. L’opérateur « =~ » effectue une comparaison et autorise les caractères génériques.

Rappelons que les propriétés d’un objet peuvent être listées avec la commande « report_property » (dans Vivado).

-filter est donc souple à utiliser. Malheureusement, il n’est pas disponible avec tous les outils FPGA.

Notez que Vivado possède une commande nommée filter, qui fait la même chose que l’option -filter. Les deux commandes suivantes sont donc équivalentes :

set result [get_cells -hierarchical -filter {name =~ *_reg}]
set result [filter [get_cells -hierarchical] {name =~ *_reg}]

Le second format est utile lorsqu’une liste d’objets est stockée dans une variable. Les deux commandes ci-dessus sont donc aussi équivalentes à ceci :

set all_cells [get_cells -hierarchical]
set result [filter $all_cells {name =~ *_reg}]

Utiliser -regex

Ceux qui connaissent les expressions régulières seront tentés d’utiliser cette option. Ce n’est généralement pas une bonne idée, principalement parce que cela rend les contraintes temporelles plus difficiles à comprendre pour les autres. Les outils FPGA qui acceptent les expressions régulières acceptent en général aussi -filter ; il est donc presque toujours préférable d’utiliser -filter.

Rappelons que le principal problème des motifs de recherche habituels est que le séparateur de hiérarchie ne correspond pas aux caractères génériques.

-regexp peut résoudre cela, comme le montrent ces deux exemples :

> get_pins -hierarchical -regexp {.+/clk_out[123]}
pll_i/clk_out1 pll_i/clk_out2 pll_i/inst/clk_out1 pll_i/inst/clk_out2
> get_pins {pll_i/[^/]+/clk[^/]+} -hierarchical -regexp
pll_i/inst/clk_in1 pll_i/inst/clk_out1 pll_i/inst/clk_out2

La première commande montre que « . » correspond à n’importe quel caractère, y compris « / », le séparateur de hiérarchie.

La deuxième commande montre comment « [^/]+ » est utilisé pour correspondre à tout ce qui n’est pas un séparateur de hiérarchie. Cela permet donc de contrôler précisément la profondeur hiérarchique des résultats. Cette commande montre aussi que le motif de recherche n’est pas un argument de -regexp, mais que -regexp modifie la syntaxe du motif de recherche.

Notez que l’expression régulière doit correspondre à la totalité du nom de l’objet. Autrement dit, les outils ajoutent implicitement un « ^ » au début de l’expression régulière et un « $ » à la fin.

Mais n’utilisez pas -regex s’il existe un autre moyen d’obtenir le même résultat. La plupart des autres concepteurs FPGA ne pourront pas comprendre le motif de recherche.

Utiliser -of_objects

Au lieu de trouver des objets d’après leur nom, -of_objects permet de trouver des objets d’après leur relation avec d’autres objets. Dans la plupart des cas, « -of_objects » signifie à peu près « est connecté à ».

Par exemple, pour trouver toutes les broches connectées à un fil :

> get_pins -of_objects [get_nets bar]
bar_reg_reg/D baz_metaguard_reg/D bar_reg__0/Q

Ou toutes les broches d’une cellule :

> get_pins -of_objects [get_cells bar_reg_reg]
bar_reg_reg/Q bar_reg_reg/C bar_reg_reg/CE bar_reg_reg/D bar_reg_reg/R

Ou encore la broche qui est à l’origine d’une horloge :

> get_pins -of_objects [get_clocks clk_out1_clk_wiz_1]
pll_i/inst/mmcme3_adv_inst/CLKOUT0

La même méthode fonctionne pour trouver des fils. Par exemple, quels fils sont connectés à une broche précise ?

> get_nets -of_objects [get_pins bar_reg_reg/C]
pll_clk_6

Ou quels fils sont connectés à la cellule associée ?

> get_nets -of_objects [get_cells bar_reg_reg]
bar_reg_OBUF pll_clk_6 <const1> bar <const0>

De la même manière, on peut rechercher des cellules. Par exemple, quelles cellules sont connectées à @pll_clk_6 ?

> get_cells -of_objects [get_nets pll_clk_6]
bar_reg__0 bar_reg_reg pll_i

Notez que « pll_i » est le nom de l’instanciation (instantiation) de l’IP du Clocking Wizard. C’est probablement un résultat non souhaité. Ne pourrait-on pas restreindre le résultat aux seules bascules ?

> get_cells -of_objects [get_nets pll_clk_6] -filter {primitive_type =~ register.*.*}
bar_reg__0 bar_reg_reg

Jusqu’ici, les exemples avec -of_objects ont montré comment broches, fils et cellules peuvent se référer les uns aux autres. Mais il est aussi possible de trouver des objets d’horloge avec cette option :

> get_clocks -of_objects [get_nets pll_clk_6]
clk_out2_clk_wiz_1
> get_clocks -of_objects [get_cells bar_reg_reg]
clk_out2_clk_wiz_1
> get_clocks -of_objects [get_pins bar_reg_reg/C]
clk_out2_clk_wiz_1

Ces trois commandes montrent comment l’horloge est trouvée à partir de l’élément logique qui lui est connecté. Notez que lorsque cet élément logique est une cellule, le résultat peut compter plus d’une horloge. Par exemple :

> get_clocks -of_objects [get_cells pll_i]
clk clk_out1_clk_wiz_1 clk_out2_clk_wiz_1

Rappelons que « pll_i » est une IP ; ses broches sont donc les ports de ce module :

> get_pins -of_objects [get_cells pll_i]
pll_i/clk_in1 pll_i/clk_out1 pll_i/clk_out2

Ainsi, pour trouver une horloge précise, get_pins est plus sûr :

> get_clocks -of_objects [get_pins pll_i/clk_out2]
clk_out2_clk_wiz_1

Mais bien sûr, l’objet d’horloge sur un port d’entrée-sortie externe se trouve le mieux avec :

> get_clocks -of_objects [get_ports clk]
clk

Ou, avec la commande plus courte, qui est équivalente :

> get_clocks [get_ports clk]
clk

En parlant de get_ports, cette commande fonctionne aussi avec -of_objects. Reportez-vous à la documentation pour connaître les possibilités propres à get_ports. Les possibilités analogues à celles des autres commandes sont assez peu utiles, par exemple :

> get_ports -of_objects [get_nets clk]
clk

En résumé, -of_objects est une excellente méthode pour sélectionner des éléments logiques précis. C’est particulièrement vrai à cause des difficultés liées à la recherche par nom, qui est le sujet suivant.

Malheureusement, beaucoup d’outils FPGA ne prennent pas en charge -of_objects, ce qui ne laisse d’autre choix que de s’appuyer sur des méthodes moins fiables.

Le problème de la recherche par nom

Comme nous l’avons déjà dit plus haut, la précision des contraintes temporelles dépend de la précision avec laquelle on trouve les éléments logiques. Une partie au moins de ces recherches repose sur le nom d’un objet. Voyons comment cela peut causer des ennuis.

Par exemple, « get_ports clk » sert à établir le lien entre un objet d’horloge et le signal présent sur une broche d’entrée-sortie précise. Cette broche est représentée par un objet de port nommé « clk » :

create_clock -period 4.000 -name clk [get_ports clk]

Mais pourquoi cet objet de port s’appelle-t-il « clk » ? La réponse est liée à la manière dont le synthétiseur (synthesizer) a créé la netlist : le nom du port de plus haut niveau dans le code Verilog a été repris comme nom du port de plus haut niveau dans la netlist. C’est un choix évident, et tout bon synthétiseur fait de même.

Mais qu’en est-il des noms des cellules ? Prenons le registre nommé « foo_reg » dans le code Verilog ci-dessus. Quel est le nom de l’objet cellule qui représente la bascule de ce registre ? Le synthétiseur de Vivado a choisi « foo_reg_reg » pour cet objet. Il est donc clair que ce synthétiseur tend à ajouter un suffixe « _reg » au nom du code Verilog. Cela ressemble à une règle fiable. Mais un autre synthétiseur ferait probablement autrement.

Et le registre nommé « bar » ? Le nom de la cellule correspondante aurait dû être « bar_reg », mais le synthétiseur a fait un autre choix : « bar_reg__0 ». La raison est qu’il existe un registre appelé « bar_reg » dans le code Verilog. Pour éviter une collision de noms, le synthétiseur a choisi un nom légèrement différent : il a ajouté « _reg__0 » au lieu de « _reg ». Ce simple exemple montre bien le problème posé par la dépendance aux noms des objets.

Pour compliquer encore les choses, supposons que la contrainte temporelle ait été écrite avant que le registre nommé « bar_reg » ne soit ajouté au code Verilog. Dans ce cas, la cellule liée à @bar s’appelle « bar_reg », comme d’habitude. La contrainte utilise donc ce nom. Plus tard, le registre « bar_reg » est ajouté au design. Le nom de la cellule recherchée change alors de « bar_reg » à « bar_reg__0 ». La contrainte, qui repose sur le nom « bar_reg », devient soudainement incorrecte. Avec un peu de chance, les outils émettront un avertissement à ce sujet.

Il y a d’autres raisons possibles pour lesquelles l’utilisation des noms d’objets peut poser problème. Par exemple, les outils peuvent dupliquer automatiquement un registre si son fan-out dépasse une certaine limite. Lorsque cela arrive, le registre supplémentaire peut ne pas être pris en compte dans la contrainte temporelle, parce que son nom ne correspond pas au motif de recherche.

Un problème plus grave est celui de l’inclusion accidentelle d’éléments logiques dans une contrainte temporelle. Cela peut arriver avec des éléments appartenant à des blocs IP (IP cores). Comme nous n’avons aucun contrôle sur les noms de ces éléments, il est possible que ces noms correspondent accidentellement au motif d’une contrainte temporelle.

L’inclusion accidentelle peut aussi résulter de la paresse : les contraintes temporelles sont généralement écrites en même temps que l’ajout de nouvelles fonctionnalités au design. Si le motif de recherche est écrit par tâtonnements, les noms des futurs éléments logiques ne sont pas pris en compte. Lorsqu’on ajoute ensuite de la logique, certains noms peuvent correspondre involontairement aux contraintes existantes.

Comment éviter les erreurs avec les contraintes temporelles

La première règle, et la plus importante, est qu’il ne suffit pas de tester quels éléments logiques correspondent au motif de recherche d’une contrainte. Même si vous dressez la liste complète de ces éléments et que vous l’examinez soigneusement, rien ne garantit ce qui se passera avec la logique qui sera ajoutée plus tard. Une telle vérification ne garantit pas non plus que les contraintes continueront de fonctionner si les noms des objets sont modifiés par le synthétiseur ou pendant une autre étape de l’implémentation.

Il est donc important de traiter les motifs de recherche comme des expressions mathématiques : il ne suffit pas qu’ils fonctionnent comme prévu à l’instant présent. S’ils ne fonctionnent pas, il ne suffit pas non plus de faire une petite retouche pour les faire marcher. Il doit plutôt exister une explication logique pour laquelle le motif est correct, et pour laquelle il a de bonnes chances de le rester.

Il est aussi important que les motifs s’appuient sur des choses qui risquent peu de changer. Par exemple, les outils ne modifient pas les noms des instanciations (instantiations) écrites dans le code Verilog. Cela vaut pour les instanciations de modules, d’IP et de primitives. Il est donc prudent de s’appuyer sur le chemin hiérarchique lorsque celui-ci n’est constitué que de noms d’instanciations écrits dans le code Verilog. Il est moins prudent d’utiliser des noms créés à l’intérieur d’un bloc IP. Pour expliquer cela, regardons cette commande :

get_clocks -of_objects [get_pins pll_i/inst/clkout1_buf/O]

C’est une méthode pour obtenir un objet d’horloge en désignant la broche de sortie du buffer d’horloge global qui distribue cette horloge. La question est de savoir si l’on peut être certain que cela fonctionnera à long terme.

Le problème de cette méthode est que « inst » et « clkout1_buf » sont des noms d’instanciations créées à l’intérieur de l’IP Clocking Wizard. Même si ces noms ont peu de chances de changer, rien ne garantit qu’ils ne changeront pas.

Une solution possible est de trouver le code Verilog qui implémente l’IP et d’inclure ce Verilog directement dans le projet. Cela garantit que rien ne changera à l’avenir.

Une alternative consiste à regarder l’instanciation de l’IP en Verilog. Rappelons qu’elle était :

 clk_wiz_1 pll_i
   (.clk_in1(clk),
    .clk_out1(pll_clk_8),
    .clk_out2(pll_clk_6));

Comme il s’agit de l’instanciation d’une boîte noire (black box), chacun de ses ports possède une broche dans la netlist. Les noms ne peuvent pas changer, car c’est ainsi que les ports de l’IP sont identifiés. Il est donc garanti que la broche nommée « pll_i/clk_out1 » assure la connexion vers @pll_clk_8. C’est donc un moyen sûr d’obtenir l’objet d’horloge :

get_clocks -of_object [get_pins pll_i/clk_out1]

Notez que cela ne fonctionnera probablement pas si clk_wiz_1 n’est qu’un simple module Verilog du projet : dans ce cas, aucune broche n’est créée pour l’instanciation de ce module (car le synthétiseur implémente généralement les connexions de ports en fusionnant les fils). Une solution possible serait d’utiliser les noms qui apparaissent aux niveaux inférieurs de la hiérarchie.

Il existe donc quelques types de noms sur lesquels on peut compter :

Mieux vaut échouer en grand

La pire situation est celle d’une contrainte temporelle presque correcte : lorsqu’il ne manque que quelques chemins. Ou lorsqu’il n’y a que quelques chemins inclus par erreur dans la contrainte. Les erreurs de ce genre sont les plus difficiles à trouver.

C’est la principale raison pour laquelle les contraintes temporelles courtes, concises et de style mathématique sont préférables. S’il existe une contrainte pour chaque petit groupe d’éléments logiques, il est facile pour une erreur de se glisser dans cette longue liste de règles.

Pour illustrer cette idée, revenons à l’exemple montré plus haut pour trouver un objet d’horloge :

get_clocks -of_objects [get_pins pll_i/inst/clkout1_buf/O]

Comme mentionné plus haut, le problème de cette expression est que si l’instanciation de pll_i se déplace à une autre position dans la hiérarchie, aucun objet d’horloge ne sera trouvé. Quelle en sera la gravité ?

Supposons que cet objet d’horloge soit stocké dans une variable Tcl comme ceci :

set my_clock [get_clocks -of_objects [get_pins pll_i/inst/clkout1_buf/O]]

Et que $my_clock soit ensuite utilisé dans de nombreuses contraintes temporelles, de sorte que beaucoup de chemins soient concernés. Dans ce cas, ce n’est pas très grave si $my_clock ne contient soudainement plus d’objet à cause d’un bug : il y a de bonnes chances que l’erreur soit vite remarquée, car beaucoup de choses se passeront mal.

Mais si $my_clock n’est utilisé que dans une seule contrainte, destinée à résoudre un petit problème qui n’a que rarement un effet, la situation est mauvaise. Il y a fort à parier que l’erreur passera inaperçue.

En conclusion : une contrainte temporelle bien écrite doit soit fonctionner parfaitement, soit ne pas fonctionner du tout.


Cette page a montré comment sélectionner précisément des éléments logiques. Sur la page suivante, ces connaissances serviront à définir des contraintes temporelles sélectives.

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)