Cette page fait partie d’une série de pages consacrée au timing. Les pages précédentes ont présenté quelques notions de base : la théorie qui se cache derrière les calculs de timing et la contrainte de période d’horloge. Plusieurs rapports de timing ont aussi été montrés et expliqués. Il est maintenant temps d’aborder l’un des usages importants de ces connaissances : résoudre les problèmes de timing.
Introduction
La plus grande difficulté des outils FPGA est de respecter les exigences des contraintes temporelles (timing constraints). Ils y parviennent en général, mais échouent parfois. Et lorsqu’ils échouent, c’est à nous, les humains, qu’il revient de comprendre pourquoi et de corriger le tir. Cette tâche porte un nom : c’est ce qu’on appelle la convergence temporelle (timing closure). Et ce n’est pas une tâche facile.
Pourquoi la convergence temporelle est-elle difficile ? En réalité, les outils disposent d’un algorithme de placement et de routage (place and route) qui tente d’utiliser les ressources du FPGA de manière optimale. En général, l’algorithme commence par placer les éléments logiques sans trop d’efforts. Puis un processus itératif s’engage : l’outil parcourt tous les chemins (paths) et identifie ceux qui ne respectent pas les contraintes temporelles. Pour corriger ces manquements, des mesures correctives sont appliquées sur ces chemins. Le plus souvent, les éléments logiques sont déplacés à d’autres endroits du FPGA et le routage est ajusté. Quant aux mesures correctives plus avancées, chaque outil FPGA a ses propres méthodes.
Lorsque tous les chemins respectent les contraintes temporelles, l’implémentation est considérée comme terminée. Mais l’implémentation peut aussi se terminer parce que les outils n’ont pas réussi à atteindre cet objectif et ont donc abandonné la tentative. Dans cette situation, on garde ce que les outils ont obtenu au moment où ils ont cessé leurs efforts. Ce résultat n’est pas nécessairement optimal : il peut exister des chemins qui auraient pu être améliorés, mais les outils étaient occupés à corriger autre chose. Et lorsque cette correction a échoué, les outils ont abandonné sans essayer d’arranger le reste. C’est un peu comme si les outils disaient : « À quoi bon perdre du temps à améliorer une implémentation si elle doit de toute façon échouer ? »
Notre rôle de concepteurs FPGA est d’examiner ce résultat sous-optimal et de trouver la raison pour laquelle les objectifs des contraintes temporelles n’ont pas été atteints.
Les algorithmes s’améliorent avec le temps. Lorsqu’une cause d’échec devient courante, la version suivante du logiciel apporte une solution spécifique à cette situation. Ainsi, quand les outils échouent, il y a en général une bonne raison.
Nous regardons donc ce que les outils ont accompli et nous nous demandons : pourquoi les outils ont-ils échoué ? Avons-nous demandé quelque chose d’impossible ? Plus important encore, avons-nous demandé quelque chose d’inutile ? L’obstacle qui a fait échouer les outils est peut-être quelque chose dont nous n’avons même pas besoin. Ou alors, l’algorithme d’optimisation n’a-t-il pas très bien fonctionné ? Parfois, c’est simplement une question de malchance : le placement initial des éléments logiques peut être si mauvais que les tentatives suivantes pour améliorer les performances sont vouées à l’échec.
Quel que soit le problème, trouver la cause de l’échec, c’est un peu comme un détective qui examine une scène de crime : les faits sont devant nous, mais la raison est souvent cachée. La plupart de ces faits se trouvent dans les rapports de timing, mais les indices ne se livrent pas facilement. La question qu’il faut toujours se poser est : qu’est-ce qui est faux, inhabituel ou anormal dans le rapport de timing ? Comme le détective qui cherche le coupable, l’objectif est de trouver le détail qui mène au problème.
Mais pour repérer ce qui est anormal, il faut savoir ce qui est normal : par exemple, quel est le délai normal d’un fil (net) avec un certain fan-out ? Combien de niveaux logiques sont normaux pour implémenter une certaine fonction logique ? Les réponses à ces questions changent d’un FPGA à l’autre. Il est donc nécessaire d’acquérir de l’expérience en lisant et en comprenant les rapports de timing, même lorsque tout va bien. Il faut savoir à quoi ressemble un rapport de timing lorsque tout est en ordre pour repérer les endroits où le rapport signale un problème. Si vous vous demandez pourquoi je suis entré dans le détail dans les pages précédentes, c’est en partie pour cela.
Le chemin critique
Lorsque les outils n’arrivent pas à respecter les contraintes temporelles, cela signifie qu’il existe au moins un chemin dont la marge (slack) est négative. Le chemin dont la marge est la plus négative s’appelle le chemin critique (critical path). Ce nom reflète la stratégie habituelle de convergence temporelle : se concentrer sur le chemin critique est souvent le moyen de résoudre un problème de timing. Mais je montrerai plus bas que cette stratégie peut aussi être une perte de temps.
Lorsque les contraintes temporelles sont respectées, le chemin critique est le chemin dont la marge est minimale. Ce chemin n’est souvent pas intéressant, car les outils ne cherchent pas à améliorer les chemins dont la marge est positive. Si le pire chemin a une marge positive, le fait qu’il soit le pire peut n’être qu’une coïncidence.
En revanche, si la marge est positive mais presque nulle (par exemple moins de 0,2 ns), cela peut indiquer qu’il a été difficile de faire respecter les contraintes à ce chemin. De tels chemins critiques peuvent être considérés comme des avertissements : ils risquent de causer des problèmes plus tard (en particulier lorsque le FPGA se remplit d’autre logique et que les efforts des outils sont détournés vers d’autres chemins).
Le rapport de timing contient en général un nombre limité de chemins critiques pour chaque horloge. Par défaut, la plupart des outils FPGA affichent quelques chemins critiques même lorsque leur marge est positive (c’est-à-dire lorsque les contraintes sont respectées). C’est le réglage recommandé.
Exemple de chemin critique
Je commence par un exemple d’analyse d’un chemin critique. Pour cet exemple, le code Verilog est le suivant :
reg [24:0] calc, result;
reg [11:0] x, y, z;
always @(posedge clk)
begin
calc <= x * y + z;
result <= calc;
end
Dans cet exemple, la fréquence de @clk est de 250 MHz et aucune PLL n’est utilisée pour générer cette horloge. Supposons aussi que @x, @y et @z sont des registres synchrones avec @clk. Le code Verilog qui affecte ces registres n’est pas montré, car il n’a pas d’importance.
Les contraintes temporelles n’ont pas été respectées lorsqu’on a tenté ce code avec Vivado. Dans le rapport de timing, voici le chemin critique :
Slack (VIOLATED) : -0.239ns (required time - arrival time) Source: x_reg[1]__0_replica_2/C (rising edge-triggered cell FDRE clocked by clk {rise@0.000ns fall@2.000ns period=4.000ns}) Destination: calc_reg[23]/D (rising edge-triggered cell FDRE clocked by clk {rise@0.000ns fall@2.000ns period=4.000ns}) Path Group: clk Path Type: Setup (Max at Slow Process Corner) Requirement: 4.000ns (clk rise@4.000ns - clk rise@0.000ns) Data Path Delay: 4.180ns (logic 1.642ns (39.282%) route 2.538ns (60.718%)) Logic Levels: 7 (CARRY8=4 LUT3=1 LUT4=1 LUT6=1) Clock Path Skew: -0.087ns (DCD - SCD + CPR) Destination Clock Delay (DCD): 3.176ns = ( 7.176 - 4.000 ) Source Clock Delay (SCD): 3.864ns Clock Pessimism Removal (CPR): 0.601ns Clock Uncertainty: 0.035ns ((TSJ^2 + TIJ^2)^1/2 + DJ) / 2 + PE Total System Jitter (TSJ): 0.071ns Total Input Jitter (TIJ): 0.000ns Discrete Jitter (DJ): 0.000ns Phase Error (PE): 0.000ns Clock Net Delay (Source): 2.032ns (routing 0.396ns, distribution 1.636ns) Clock Net Delay (Destination): 1.748ns (routing 0.365ns, distribution 1.383ns) Location Delay type Incr(ns) Path(ns) Netlist Resource(s) ------------------------------------------------------------------- ------------------- (clock clk rise edge) 0.000 0.000 r AG12 0.000 0.000 r clk (IN) net (fo=0) 0.000 0.000 clk_IBUF_inst/I AG12 INBUF (Prop_INBUF_HRIO_PAD_O) 0.738 0.738 r clk_IBUF_inst/INBUF_INST/O net (fo=1, routed) 0.105 0.843 clk_IBUF_inst/OUT AG12 IBUFCTRL (Prop_IBUFCTRL_HRIO_I_O) 0.049 0.892 r clk_IBUF_inst/IBUFCTRL_INST/O net (fo=1, routed) 0.839 1.731 clk_IBUF BUFGCE_X1Y0 BUFGCE (Prop_BUFCE_BUFGCE_I_O) 0.101 1.832 r clk_IBUF_BUFG_inst/O X2Y0 (CLOCK_ROOT) net (fo=106, routed) 2.032 3.864 clk_IBUF_BUFG SLICE_X54Y54 FDRE r x_reg[1]__0_replica_2/C ------------------------------------------------------------------- ------------------- SLICE_X54Y54 FDRE (Prop_HFF2_SLICEL_C_Q) 0.137 4.001 r x_reg[1]__0_replica_2/Q net (fo=21, routed) 0.371 4.372 x[1]_repN_2 SLICE_X56Y53 LUT6 (Prop_E6LUT_SLICEL_I1_O) 0.219 4.591 r calc[23]_i_101/O net (fo=2, routed) 0.550 5.141 calc[23]_i_101_n_0 SLICE_X54Y57 CARRY8 (Prop_CARRY8_SLICEL_DI[5]_CO[7]) 0.228 5.369 r calc_reg[23]_i_30/CO[7] net (fo=1, routed) 0.030 5.399 calc_reg[23]_i_30_n_0 SLICE_X54Y58 CARRY8 (Prop_CARRY8_SLICEL_CI_O[1]) 0.163 5.562 r calc_reg[23]_i_22/O[1] net (fo=3, routed) 0.351 5.913 calc_reg[23]_i_22_n_14 SLICE_X56Y57 LUT3 (Prop_C6LUT_SLICEL_I1_O) 0.146 6.059 r calc[23]_i_26/O net (fo=3, routed) 0.240 6.299 calc[23]_i_26_n_0 SLICE_X55Y58 LUT4 (Prop_A6LUT_SLICEM_I0_O) 0.089 6.388 r calc[23]_i_7/O net (fo=1, routed) 0.407 6.795 calc[23]_i_7_n_0 SLICE_X53Y57 CARRY8 (Prop_CARRY8_SLICEM_DI[2]_O[4]) 0.308 7.103 r calc_reg[23]_i_2/O[4] net (fo=1, routed) 0.538 7.641 P[20] SLICE_X54Y56 CARRY8 (Prop_CARRY8_SLICEL_S[4]_O[7]) 0.352 7.993 r calc_reg[23]_i_1/O[7] net (fo=1, routed) 0.051 8.044 P0_out[23] SLICE_X54Y56 FDRE r calc_reg[23]/D ------------------------------------------------------------------- ------------------- (clock clk rise edge) 4.000 4.000 r AG12 0.000 4.000 r clk (IN) net (fo=0) 0.000 4.000 clk_IBUF_inst/I AG12 INBUF (Prop_INBUF_HRIO_PAD_O) 0.515 4.515 r clk_IBUF_inst/INBUF_INST/O net (fo=1, routed) 0.066 4.581 clk_IBUF_inst/OUT AG12 IBUFCTRL (Prop_IBUFCTRL_HRIO_I_O) 0.034 4.615 r clk_IBUF_inst/IBUFCTRL_INST/O net (fo=1, routed) 0.722 5.337 clk_IBUF BUFGCE_X1Y0 BUFGCE (Prop_BUFCE_BUFGCE_I_O) 0.091 5.428 r clk_IBUF_BUFG_inst/O X2Y0 (CLOCK_ROOT) net (fo=106, routed) 1.748 7.176 clk_IBUF_BUFG SLICE_X54Y56 FDRE r calc_reg[23]/C clock pessimism 0.601 7.777 clock uncertainty -0.035 7.741 SLICE_X54Y56 FDRE (Setup_HFF_SLICEL_C_D) 0.063 7.804 calc_reg[23] ------------------------------------------------------------------- required time 7.804 arrival time -8.044 ------------------------------------------------------------------- slack -0.239
La marge de ce chemin était de –0,239 ns : c’est donc un léger échec à respecter les contraintes temporelles. La première chose à examiner, ce sont le début et la fin du chemin : on regarde les lignes « Source » et « Destination » de l’en-tête et on y trouve x_reg et calc_reg. La source du problème est donc clairement cette partie :
calc <= x * y + z;
Cela ne devrait pas surprendre, car c’est la seule opération digne de ce nom dans le code Verilog. Dans un scénario réel, il n’est pas aussi évident de savoir quelle partie de la logique a causé le problème.
Le rapport de timing montre aussi clairement un grand nombre de niveaux logiques : 7. Le chemin combinatoire est trop long. Autrement dit, il y a trop de choses à faire entre deux fronts d’horloge de @clk.
Mais quelle est la véritable raison de l’échec ? Peut-être le fait que le routage représente 61 % du délai ? Rappelons qu’une règle empirique veut que le délai de routage représente généralement environ 40 % du délai total. Alors, faut-il essayer d’amener les outils FPGA à mieux faire ? C’est pourtant peu susceptible de réussir, car les outils travaillent durement avant d’abandonner la tentative de respecter les contraintes d’un chemin.
Essayer de modifier la fonction logique entre @x et @calc est tout aussi vain : la multiplication est essentielle, on ne peut donc pas la remplacer par quelque chose de plus simple.
Je présenterai d’autres approches possibles pour résoudre un problème de ce genre sur la page suivante. Mais égrener une liste de techniques ne suffira pas dans ce cas. Ce simple exemple montre que, parfois, il faut penser comme un détective.
Votre cerveau n’a pas d’équivalent
La première question à se poser à la lecture d’un rapport de timing est : qu’est-ce qui est anormal ? Dans cet exemple, la réponse est que le chemin combinatoire n’est constitué que de slices (slice). Or la quasi-totalité des FPGA possèdent des unités arithmétiques dédiées (DSP, ALU, les noms varient), qui sont utilisées lorsqu’on demande une multiplication. En fait, « multiplier et additionner » est la fonction la plus courante dans ce type de logique dédiée. La solution la plus simple, dans la plupart des cas, est donc de faire en sorte que les outils utilisent une unité arithmétique dédiée. Le rapport de timing correspondant à cette solution est montré en bas de cette page.
Mais la vraie question à se poser est de savoir pourquoi des slices ont été utilisées au lieu d’une unité arithmétique dédiée. La raison la plus courante est que toutes les unités arithmétiques disponibles dans le FPGA sont déjà utilisées ailleurs dans la conception. Dans ce cas, la modification nécessaire n’a peut-être rien à voir avec le chemin critique : il faut peut-être libérer quelques unités arithmétiques en retirant une partie de la logique du design. Une autre possibilité serait d’indiquer aux outils comment répartir ces unités arithmétiques entre les différentes parties de la conception.
Dans cet exemple, des slices ont été utilisées au lieu d’unités arithmétiques parce que je l’ai voulu ainsi : j’ai délibérément désactivé l’utilisation des unités arithmétiques (avec l’un des paramètres du synthétiseur (synthesizer) de Vivado : j’ai mis max_dsp à zéro). Mais cela ne rend pas l’exemple artificiel pour autant. Il arrive que des paramètres incorrects soient utilisés avec les outils FPGA, menant exactement à ce type de situation. En fait, il est parfois correct de ne pas utiliser volontairement les unités arithmétiques, parce qu’on en a davantage besoin ailleurs dans le design.
La solution facile était donc d’utiliser les unités arithmétiques dédiées. Mais que faire s’il faut impérativement utiliser des slices ? Là encore, la solution est indirecte. Rappelons que la partie problématique était :
calc <= x * y + z;
Mais remarquons que ceci vient juste après :
result <= calc;
Si @calc n’est utilisé que dans cette ligne et nulle part ailleurs, il est possible de découper le calcul en deux étages. Cette technique est souvent appelée pipelining (mise en pipeline). Le code Verilog devient alors :
reg [24:0] calc, result;
reg [11:0] x, y, z, z_d;
always @(posedge clk)
begin
z_d <= z;
calc <= x * y;
result <= calc + z_d;
end
Dans cette solution, @calc ne reçoit que le résultat de la multiplication. La valeur de @z n’est ajoutée à @calc qu’à l’étage suivant. Plus précisément, l’addition se fait entre @calc et @z_d, car elle a lieu un cycle d’horloge plus tard. La valeur de @result est donc exactement la même qu’avant.
Il a été facile de résoudre le problème de cette façon parce que @result n’était qu’une copie retardée de @calc dans le code Verilog d’origine. Dans la vraie vie, on n’a généralement pas cette chance.
Notons que le chemin critique ne faisait intervenir que @calc et @x. @z n’était même pas mentionné dans le chemin. Le but de cette manipulation est donc de réduire la charge de l’opération arithmétique. Ou, plus précisément, de réduire le nombre de niveaux logiques.
Rappelons que le chemin critique est le pire chemin après l’exécution d’un algorithme d’optimisation. Cet algorithme ne se demande pas quelle est la cause du problème. Il essaie plutôt d’améliorer les chemins dont la marge est négative. Ainsi, même si la solution du problème passe par une manipulation de @z, le chemin lié à @z n’était pas le chemin critique. C’est une coïncidence, mais ce genre de coïncidence arrive souvent.
Le rapport de timing avec le chemin critique de cette solution est aussi montré en bas de cette page. On y voit que le nombre de niveaux logiques est passé de 7 à 6. Le délai du chemin de données a ainsi été réduit de 0,715 ns, ce qui a largement suffi pour respecter les contraintes.
La leçon à tirer de cet exemple est que le chemin critique n’est pas toujours la cause directe du problème. Il reste pertinent de se demander pourquoi ce chemin a échoué, mais la solution peut se trouver ailleurs. Gardez à l’esprit que chaque outil FPGA dispose de ses propres utilitaires, qui fournissent des informations susceptibles d’aider à trouver la cause profonde du problème. Il vaut la peine de prendre le temps d’explorer ces utilitaires et de lire leur documentation.
Éviter le problème en amont plutôt que le résoudre en aval
Une grande partie du travail de convergence temporelle peut être évitée si la conception logique est bien faite dès le départ. Cela demande d’avoir constamment à l’esprit qu’une conception logique n’est pas du logiciel. Le but du code Verilog n’est pas de produire le bon résultat en simulation. Ce qui compte vraiment, c’est la sortie générée par le synthétiseur (synthesizer) à partir du code Verilog.
Une bonne conception logique commence par une réflexion sur la meilleure façon pour la logique de remplir son rôle. Cela suppose d’identifier les obstacles potentiels liés au timing.
Les concepteurs FPGA moins expérimentés développent souvent du code Verilog par tâtonnements. La simulation sert à vérifier si la logique se comporte comme prévu, et des corrections sont apportées progressivement jusqu’à ce que la sortie de la simulation soit bonne. Le résultat peut être une logique inutilisable sur le matériel : le code Verilog doit alors être entièrement réécrit pour respecter les contraintes temporelles.
Il est important de réfléchir à l’avance aux chemins combinatoires que le code Verilog va générer. L’idée est d’examiner chaque registre et de suivre le chemin combinatoire jusqu’à son extrémité. Rappelons qu’un chemin combinatoire commence toujours à un registre et se termine à un registre.
Regardons cet exemple :
reg [15:0] a, b;
wire [16:0] x, y;
reg [33:0] z;
assign x = a + 2;
assign y = b + 3;
always @(posedge clk)
z <= x * y;
En ce qui concerne @a : lorsque ce registre change, le chemin combinatoire atteint d’abord @x, qui constitue un premier étage. Mais @x n’est pas un registre ; @x est mis à jour par une affectation continue. Le chemin continue donc vers @z. La logique effectue ainsi deux opérations significatives dans ce même chemin combinatoire : une addition arithmétique et une multiplication arithmétique. Est-ce trop ? Faut-il découper cela en deux cycles d’horloge grâce à une mise en pipeline ? Cela dépend de la fréquence d’horloge et du FPGA utilisé.
Un autre facteur important est la difficulté plus ou moins grande à raccourcir les chemins combinatoires. Un long chemin combinatoire est parfois inévitable. Mais lorsqu’il est facile d’améliorer le timing d’un chemin, faites-le même s’il existe des chemins bien pires dans le design : s’il n’y a que quelques chemins problématiques, les outils parviennent souvent à respecter les contraintes en concentrant leurs efforts sur ces chemins. Cela aide beaucoup lorsque les autres chemins sont faciles à satisfaire.
Il n’existe donc pas de règles définissant ce qui est permis ou interdit pour obtenir un design qui respecte les contraintes temporelles. Cela demande de l’expérience en conception FPGA pour prendre les bonnes décisions dans ce domaine. L’expérience des outils FPGA eux-mêmes est aussi importante. La seule règle toujours valable est la suivante : lorsqu’il est possible d’améliorer le timing par une modification simple du code Verilog, faites-le. Ne soyez pas paresseux, et faites cette modification dès le départ. Ayez toujours le timing à l’esprit.
Écrire une logique rapide
Comme déjà mentionné, l’objectif est d’obtenir des chemins combinatoires courts entre les registres : les fonctions logiques qui calculent la prochaine valeur des registres doivent être simples. Autrement dit, leur implémentation doit nécessiter un petit nombre de niveaux logiques.
Notre rôle de concepteurs FPGA est d’examiner le code Verilog et d’évaluer la complexité des fonctions logiques qui en résulteront. Cela suppose de savoir comment le synthétiseur traduit le Verilog en éléments logiques (LUT et autres primitives logiques (logic primitives)). Ce savoir s’acquiert avec l’expérience, notamment en analysant les rapports de timing. Pour compliquer encore les choses, cette traduction varie d’un FPGA à l’autre. Écrire du code Verilog qui donne une logique rapide n’est donc pas une tâche facile.
Si vous êtes débutant en FPGA, il est recommandé de prendre le temps d’examiner les résultats de l’implémentation dans un but d’apprentissage. Le rapport de timing montre comment la conception logique se décompose en éléments logiques simples. Les outils FPGA offrent aussi d’autres utilitaires pour visualiser les éléments logiques de bas niveau.
Quelques règles simples peuvent en outre vous aider :
- Le pipeline (pipelining) : soyez généreux avec les registres. Lorsque c’est possible, découpez le travail de la logique en petites étapes et insérez un registre après chacune d’elles. Les FPGA regorgent de bascules (il y a souvent une bascule à côté de chaque LUT), cette insertion de registres n’augmentera donc pas le taux d’occupation du FPGA. La seule raison d’éviter le pipeline est lorsqu’il complique trop la conception.
- Avec if-then-else, évitez d’enchaîner de nombreux « else ». Lorsqu’une instruction « case » peut être utilisée à la place, c’est en général préférable. Une clause « else » exige souvent une fonction logique qui assure que tout ce qui précède cette clause est faux. Plusieurs « else » successifs peuvent donc demander plusieurs niveaux logiques.
- Évitez les remises à zéro inutiles. En particulier, une remise à zéro synchrone (synchronous reset) ajoute un peu de complexité à la fonction logique. Les deux types de remise à zéro, synchrone et asynchrone (asynchronous reset), compliquent aussi le routage, car ce sont des signaux qui doivent atteindre de nombreux éléments logiques. Il existe une série de pages dédiée à ce sujet.
- Ne créez pas d’énormes machines d’état (state machines). Il n’y a pas de limite stricte sur le nombre d’états acceptable. Mais si ce nombre dépasse 20, envisagez de restructurer la conception. Assurez-vous aussi que le synthétiseur utilise un encodage one-hot pour les grandes machines d’état (par défaut, la plupart des synthétiseurs le font). Cela favorise la génération d’une logique rapide.
- Un registre supplémentaire après une RAM est en général préférable. Regardons cet exemple de RAM créée implicitement :
reg [7:0] array[0:127]; reg [7:0] val; reg [6:0] addr; always @(posedge clk) val <= array[addr];Ce code Verilog est correct, mais notons que @val est la sortie synchrone de la RAM. Ainsi, lorsqu’un front d’horloge se produit, l’opération de la RAM démarre, et @val n’est mise à jour qu’une fois la valeur du tableau obtenue. @val présente donc un délai horloge-vers-sortie relativement important (par rapport à une bascule). Par conséquent, les chemins qui commencent à @val sont intrinsèquement défavorisés. On peut corriger cela en ajoutant un registre supplémentaire :
reg [7:0] array[0:127]; reg [7:0] val_d, mem_out; reg [6:0] addr; always @(posedge clk) begin mem_out <= array[addr]; val_d <= mem_out; endNotons que ce n’est pas fonctionnellement équivalent : ici, @mem_out est la sortie de la RAM. Ce n’est qu’un cycle d’horloge plus tard que cette sortie est copiée dans @val_d. Ce n’est donc pas un remplacement exact de @val. Mais @val_d est un véritable registre, avec un faible délai horloge-vers-sortie. Dans de nombreux FPGA, ce registre supplémentaire fait partie des block RAM, donc aucune bascule n’est gaspillée. Ai-je déjà mentionné de ne jamais s’inquiéter de gaspiller des bascules ?
Malheureusement, ajouter un tel registre complique souvent considérablement la conception. Lorsque c’est le cas, il vaut mieux ne pas ajouter ce registre, mais plutôt essayer de garder un chemin combinatoire court à partir de @val.
Deux rapports de timing supplémentaires
J’ai promis deux rapports de timing dans la section intitulée « Votre cerveau n’a pas d’équivalent » plus haut. Je les place ici, et non à l’endroit où ils sont mentionnés, parce qu’ils sont longs et seulement partiellement pertinents.
Notons que chacun de ces deux rapports concerne le chemin critique du scénario correspondant. Le chemin n’y commence donc pas et ne s’y termine pas aux mêmes registres que le chemin montré plus haut.
Le premier rapport concerne le premier exemple de code Verilog. Contrairement au rapport ci-dessus, les outils ont été autorisés à utiliser les unités arithmétiques dédiées. Le résultat est que les contraintes temporelles sont largement respectées.
Ce rapport a été généré pour un FPGA Kintex Ultrascale. Dans cette famille de FPGA, une unité arithmétique dédiée s’appelle un DSP48E2. Notons que le chemin commence et se termine dans le même DSP48E2. La part de la logique dans le délai est donc de 100 %.
Slack (MET) : 1.406ns (required time - arrival time) Source: calc_reg/DSP_A_B_DATA_INST/CLK (rising edge-triggered cell DSP_A_B_DATA clocked by clk {rise@0.000ns fall@2.000ns period=4.000ns}) Destination: calc_reg/DSP_OUTPUT_INST/ALU_OUT[10] (rising edge-triggered cell DSP_OUTPUT clocked by clk {rise@0.000ns fall@2.000ns period=4.000ns}) Path Group: clk Path Type: Setup (Max at Slow Process Corner) Requirement: 4.000ns (clk rise@4.000ns - clk rise@0.000ns) Data Path Delay: 2.445ns (logic 2.445ns (100.000%) route 0.000ns (0.000%)) Logic Levels: 4 (DSP_ALU=1 DSP_M_DATA=1 DSP_MULTIPLIER=1 DSP_PREADD_DATA=1) Clock Path Skew: -0.010ns (DCD - SCD + CPR) Destination Clock Delay (DCD): 3.392ns = ( 7.392 - 4.000 ) Source Clock Delay (SCD): 4.096ns Clock Pessimism Removal (CPR): 0.694ns Clock Uncertainty: 0.035ns ((TSJ^2 + TIJ^2)^1/2 + DJ) / 2 + PE Total System Jitter (TSJ): 0.071ns Total Input Jitter (TIJ): 0.000ns Discrete Jitter (DJ): 0.000ns Phase Error (PE): 0.000ns Clock Net Delay (Source): 2.264ns (routing 0.756ns, distribution 1.508ns) Clock Net Delay (Destination): 1.964ns (routing 0.696ns, distribution 1.268ns) Location Delay type Incr(ns) Path(ns) Netlist Resource(s) ------------------------------------------------------------------- ------------------- (clock clk rise edge) 0.000 0.000 r AG12 0.000 0.000 r clk (IN) net (fo=0) 0.000 0.000 clk_IBUF_inst/I AG12 INBUF (Prop_INBUF_HRIO_PAD_O) 0.738 0.738 r clk_IBUF_inst/INBUF_INST/O net (fo=1, routed) 0.105 0.843 clk_IBUF_inst/OUT AG12 IBUFCTRL (Prop_IBUFCTRL_HRIO_I_O) 0.049 0.892 r clk_IBUF_inst/IBUFCTRL_INST/O net (fo=1, routed) 0.839 1.731 clk_IBUF BUFGCE_X1Y0 BUFGCE (Prop_BUFCE_BUFGCE_I_O) 0.101 1.832 r clk_IBUF_BUFG_inst/O X2Y1 (CLOCK_ROOT) net (fo=80, routed) 2.264 4.096 calc_reg/CLK DSP48E2_X11Y34 DSP_A_B_DATA r calc_reg/DSP_A_B_DATA_INST/CLK ------------------------------------------------------------------- ------------------- DSP48E2_X11Y34 DSP_A_B_DATA (Prop_DSP_A_B_DATA_DSP48E2_CLK_A2_DATA[9]) 0.302 4.398 r calc_reg/DSP_A_B_DATA_INST/A2_DATA[9] net (fo=1, routed) 0.000 4.398 calc_reg/DSP_A_B_DATA.A2_DATA<9> DSP48E2_X11Y34 DSP_PREADD_DATA (Prop_DSP_PREADD_DATA_DSP48E2_A2_DATA[9]_A2A1[9]) 0.182 4.580 r calc_reg/DSP_PREADD_DATA_INST/A2A1[9] net (fo=1, routed) 0.000 4.580 calc_reg/DSP_PREADD_DATA.A2A1<9> DSP48E2_X11Y34 DSP_MULTIPLIER (Prop_DSP_MULTIPLIER_DSP48E2_A2A1[9]_U[10]) 0.994 5.574 f calc_reg/DSP_MULTIPLIER_INST/U[10] net (fo=1, routed) 0.000 5.574 calc_reg/DSP_MULTIPLIER.U<10> DSP48E2_X11Y34 DSP_M_DATA (Prop_DSP_M_DATA_DSP48E2_U[10]_U_DATA[10]) 0.164 5.738 r calc_reg/DSP_M_DATA_INST/U_DATA[10] net (fo=1, routed) 0.000 5.738 calc_reg/DSP_M_DATA.U_DATA<10> DSP48E2_X11Y34 DSP_ALU (Prop_DSP_ALU_DSP48E2_U_DATA[10]_ALU_OUT[10]) 0.803 6.541 r calc_reg/DSP_ALU_INST/ALU_OUT[10] net (fo=1, routed) 0.000 6.541 calc_reg/DSP_ALU.ALU_OUT<10> DSP48E2_X11Y34 DSP_OUTPUT r calc_reg/DSP_OUTPUT_INST/ALU_OUT[10] ------------------------------------------------------------------- ------------------- (clock clk rise edge) 4.000 4.000 r AG12 0.000 4.000 r clk (IN) net (fo=0) 0.000 4.000 clk_IBUF_inst/I AG12 INBUF (Prop_INBUF_HRIO_PAD_O) 0.515 4.515 r clk_IBUF_inst/INBUF_INST/O net (fo=1, routed) 0.066 4.581 clk_IBUF_inst/OUT AG12 IBUFCTRL (Prop_IBUFCTRL_HRIO_I_O) 0.034 4.615 r clk_IBUF_inst/IBUFCTRL_INST/O net (fo=1, routed) 0.722 5.337 clk_IBUF BUFGCE_X1Y0 BUFGCE (Prop_BUFCE_BUFGCE_I_O) 0.091 5.428 r clk_IBUF_BUFG_inst/O X2Y1 (CLOCK_ROOT) net (fo=80, routed) 1.964 7.392 calc_reg/CLK DSP48E2_X11Y34 DSP_OUTPUT r calc_reg/DSP_OUTPUT_INST/CLK clock pessimism 0.694 8.086 clock uncertainty -0.035 8.050 DSP48E2_X11Y34 DSP_OUTPUT (Setup_DSP_OUTPUT_DSP48E2_CLK_ALU_OUT[10]) -0.104 7.946 calc_reg/DSP_OUTPUT_INST ------------------------------------------------------------------- required time 7.946 arrival time -6.541 ------------------------------------------------------------------- slack 1.406
Le second rapport concerne le deuxième exemple de code Verilog. Dans cet exemple, la situation a été améliorée grâce au pipelining :
Slack (MET) : 0.433ns (required time - arrival time) Source: y_reg[1]__0/C (rising edge-triggered cell FDRE clocked by clk {rise@0.000ns fall@2.000ns period=4.000ns}) Destination: calc_reg[23]/D (rising edge-triggered cell FDRE clocked by clk {rise@0.000ns fall@2.000ns period=4.000ns}) Path Group: clk Path Type: Setup (Max at Slow Process Corner) Requirement: 4.000ns (clk rise@4.000ns - clk rise@0.000ns) Data Path Delay: 3.465ns (logic 1.653ns (47.706%) route 1.812ns (52.294%)) Logic Levels: 6 (CARRY8=4 LUT4=2) Clock Path Skew: -0.129ns (DCD - SCD + CPR) Destination Clock Delay (DCD): 3.373ns = ( 7.373 - 4.000 ) Source Clock Delay (SCD): 4.040ns Clock Pessimism Removal (CPR): 0.538ns Clock Uncertainty: 0.035ns ((TSJ^2 + TIJ^2)^1/2 + DJ) / 2 + PE Total System Jitter (TSJ): 0.071ns Total Input Jitter (TIJ): 0.000ns Discrete Jitter (DJ): 0.000ns Phase Error (PE): 0.000ns Clock Net Delay (Source): 2.208ns (routing 0.756ns, distribution 1.452ns) Clock Net Delay (Destination): 1.945ns (routing 0.696ns, distribution 1.249ns) Location Delay type Incr(ns) Path(ns) Netlist Resource(s) ------------------------------------------------------------------- ------------------- (clock clk rise edge) 0.000 0.000 r AG12 0.000 0.000 r clk (IN) net (fo=0) 0.000 0.000 clk_IBUF_inst/I AG12 INBUF (Prop_INBUF_HRIO_PAD_O) 0.738 0.738 r clk_IBUF_inst/INBUF_INST/O net (fo=1, routed) 0.105 0.843 clk_IBUF_inst/OUT AG12 IBUFCTRL (Prop_IBUFCTRL_HRIO_I_O) 0.049 0.892 r clk_IBUF_inst/IBUFCTRL_INST/O net (fo=1, routed) 0.839 1.731 clk_IBUF BUFGCE_X1Y0 BUFGCE (Prop_BUFCE_BUFGCE_I_O) 0.101 1.832 r clk_IBUF_BUFG_inst/O X2Y1 (CLOCK_ROOT) net (fo=121, routed) 2.208 4.040 clk_IBUF_BUFG SLICE_X54Y88 FDRE r y_reg[1]__0/C ------------------------------------------------------------------- ------------------- SLICE_X54Y88 FDRE (Prop_EFF2_SLICEL_C_Q) 0.138 4.178 r y_reg[1]__0/Q net (fo=25, routed) 0.505 4.683 y[1] SLICE_X53Y91 LUT4 (Prop_B6LUT_SLICEM_I0_O) 0.150 4.833 r calc[7]_i_28/O net (fo=1, routed) 0.344 5.177 calc[7]_i_28_n_0 SLICE_X53Y89 CARRY8 (Prop_CARRY8_SLICEM_DI[2]_CO[7]) 0.424 5.601 r calc_reg[7]_i_9/CO[7] net (fo=1, routed) 0.043 5.644 calc_reg[7]_i_9_n_0 SLICE_X53Y90 CARRY8 (Prop_CARRY8_SLICEM_CI_O[0]) 0.122 5.766 r calc_reg[23]_i_30/O[0] net (fo=3, routed) 0.402 6.168 calc_reg[23]_i_30_n_15 SLICE_X51Y88 LUT4 (Prop_C5LUT_SLICEL_I0_O) 0.169 6.337 r calc[15]_i_8/O net (fo=1, routed) 0.437 6.774 calc[15]_i_8_n_0 SLICE_X51Y92 CARRY8 (Prop_CARRY8_SLICEL_DI[1]_CO[7]) 0.422 7.196 r calc_reg[15]_i_1/CO[7] net (fo=1, routed) 0.030 7.226 calc_reg[15]_i_1_n_0 SLICE_X51Y93 CARRY8 (Prop_CARRY8_SLICEL_CI_O[7]) 0.228 7.454 r calc_reg[23]_i_1/O[7] net (fo=1, routed) 0.051 7.505 calc_reg[23]_i_1_n_8 SLICE_X51Y93 FDRE r calc_reg[23]/D ------------------------------------------------------------------- ------------------- (clock clk rise edge) 4.000 4.000 r AG12 0.000 4.000 r clk (IN) net (fo=0) 0.000 4.000 clk_IBUF_inst/I AG12 INBUF (Prop_INBUF_HRIO_PAD_O) 0.515 4.515 r clk_IBUF_inst/INBUF_INST/O net (fo=1, routed) 0.066 4.581 clk_IBUF_inst/OUT AG12 IBUFCTRL (Prop_IBUFCTRL_HRIO_I_O) 0.034 4.615 r clk_IBUF_inst/IBUFCTRL_INST/O net (fo=1, routed) 0.722 5.337 clk_IBUF BUFGCE_X1Y0 BUFGCE (Prop_BUFCE_BUFGCE_I_O) 0.091 5.428 r clk_IBUF_BUFG_inst/O X2Y1 (CLOCK_ROOT) net (fo=121, routed) 1.945 7.373 clk_IBUF_BUFG SLICE_X51Y93 FDRE r calc_reg[23]/C clock pessimism 0.538 7.910 clock uncertainty -0.035 7.875 SLICE_X51Y93 FDRE (Setup_HFF_SLICEL_C_D) 0.063 7.938 calc_reg[23] ------------------------------------------------------------------- required time 7.938 arrival time -7.505 ------------------------------------------------------------------- slack 0.433
La différence n’est pas aussi spectaculaire qu’avec une unité arithmétique dédiée. Elle reste néanmoins suffisante pour respecter la contrainte temporelle.
Ceci conclut la discussion générale sur la convergence temporelle. La page suivante suggère plusieurs stratégies pratiques sur ce sujet.