Diese Seite gehört zu einer Serie von Seiten über Timing. Die vorherigen Seiten haben einige grundlegende Themen erklärt: die Theorie hinter Timing-Berechnungen und die Taktperioden-Timing-Vorgabe. Außerdem wurden einige Timing-Reports gezeigt und erläutert. Nun ist es Zeit, über einen der wichtigsten Zwecke dieses Wissens zu sprechen: das Lösen von Timing-Problemen.
Einführung
Die größte Herausforderung für die FPGA-Tools besteht darin, die Anforderungen der Timing-Vorgaben (timing constraints) zu erfüllen. Hoffentlich gelingt das – aber manchmal scheitert es. Und wenn es scheitert, liegt es an uns Menschen herauszufinden, warum, und das Problem zu beheben. Diese Aufgabe hat einen Namen: Wir nennen sie das Timing-Closure (englisch: timing closure). Und sie ist nicht einfach.
Warum ist Timing-Closure schwierig? Die Sache ist die: Die Tools verfügen über einen Algorithmus für Platzierung und Verdrahtung (Place and Route), der versucht, die Ressourcen des FPGAs optimal zu nutzen. Normalerweise beginnt dieser Algorithmus damit, die Logikelemente ohne allzu großen Aufwand auf dem FPGA zu platzieren. Dann beginnt ein iterativer Prozess: Die Tools gehen alle Pfade (paths) durch und suchen diejenigen, welche die Timing-Vorgaben nicht erfüllen. Um diese Fehlschläge zu beheben, werden an diesen Pfaden Korrekturmaßnahmen ergriffen. Am wichtigsten ist, dass Logikelemente auf andere Positionen im FPGA verschoben und die Verdrahtung angepasst wird. Darüber hinaus hat jedes FPGA-Tool eigene weiterführende Korrekturmethoden.
Wenn alle Pfade die Timing-Vorgaben erfüllen, gilt die Implementierung als abgeschlossen. Die Implementierung kann aber auch enden, weil die Tools dieses Ziel nicht erreichen konnten und den Versuch daraufhin aufgegeben haben. In dieser Situation bekommen wir das, was die Tools erreicht haben, als die Bemühungen eingestellt wurden. Dieses Ergebnis ist nicht unbedingt optimal: Es könnte Pfade in der Implementierung geben, die man hätte verbessern können, aber die Tools waren damit beschäftigt, etwas anderes zu reparieren. Und als das scheiterte, gaben die Tools auf, ohne andere Dinge zu verbessern. Es ist, als würden die Tools sagen: „Es hat keinen Sinn, Zeit in eine Implementierung zu stecken, die ohnehin scheitert.“
Unsere Aufgabe als FPGA-Entwickler ist es, sich dieses suboptimale Ergebnis anzusehen und den Grund zu finden, warum die Ziele der Timing-Vorgaben nicht erreicht wurden.
Die Algorithmen werden mit der Zeit besser. Wenn es einen häufigen Grund dafür gibt, dass Timing-Vorgaben nicht erreicht werden, enthält die nächste Version der Software eine spezielle Lösung für diese Situation. Wenn die Tools also scheitern, gibt es dafür meist einen guten Grund.
Wir sehen uns also an, was die Tools erreicht haben, und fragen uns: Warum sind die Tools gescheitert? Haben wir etwas Unmögliches verlangt? Noch wichtiger: Haben wir etwas Unnötiges verlangt? Vielleicht ist das Hindernis, an dem die Tools gescheitert sind, etwas, das wir gar nicht brauchen. Oder vielleicht hat der Optimierungsalgorithmus nicht so gut funktioniert? Manchmal ist es einfach Pech: Die anfängliche Platzierung der Logikelemente kann so schlecht sein, dass die anschließenden Verbesserungsversuche zum Scheitern verurteilt sind.
Was auch immer das Problem ist: Das Finden des Grundes für das Scheitern ähnelt der Arbeit eines Detektivs am Tatort: Die Fakten liegen vor uns, aber der Grund ist oft verborgen. Die meisten dieser Fakten stehen in den Timing-Reports, aber die Hinweise verraten sich nicht leicht. Die Frage, die man sich immer stellen muss, ist, was im Timing-Report falsch, ungewöhnlich oder abnormal ist. Wie der Detektiv, der den Täter finden will, ist das Ziel, das Detail zu finden, das zum Problem führt.
Aber um das Abnormale zu finden, muss man wissen, was normal ist: Was ist zum Beispiel eine normale Verzögerung für eine Leitung mit einem bestimmten Fan-out? Wie viele Logikstufen sind normal, um eine bestimmte Logikfunktion zu implementieren? Die Antworten auf solche Fragen sind von FPGA zu FPGA unterschiedlich. Es ist daher notwendig, Erfahrung zu sammeln, indem man die Timing-Reports liest und versteht, auch wenn alles in Ordnung ist. Man muss wissen, wie ein Timing-Report aussieht, wenn alles in Ordnung ist, um die Stellen zu finden, an denen der Report ein Problem anzeigt. Falls Sie sich gefragt haben, warum ich auf den vorherigen Seiten so sehr ins Detail gegangen bin: Das ist einer der Gründe.
Der kritische Pfad
Wenn die Tools die Timing-Vorgaben nicht erfüllen, bedeutet das, dass es mindestens einen Pfad mit negativer Zeitmarge (slack) gibt. Der Pfad mit der negativsten Zeitmarge wird als kritischer Pfad (critical path) bezeichnet. Dieser Name spiegelt die übliche Strategie für das Timing-Closure wider: Sich auf den kritischen Pfad zu konzentrieren, ist oft der Weg, ein Timing-Problem zu lösen. Aber ich werde im Folgenden zeigen, dass diese Strategie auch Zeitverschwendung sein kann.
Wenn die Timing-Vorgaben erreicht werden, ist der kritische Pfad der Pfad mit der minimalen Zeitmarge. Dieser Pfad ist oft nicht interessant, weil die Tools nicht versuchen, Pfade mit positiver Zeitmarge zu verbessern. Wenn also der schlechteste Pfad eine positive Zeitmarge hat, kann es Zufall sein, dass ausgerechnet dieser Pfad der schlechteste ist.
Ist die Zeitmarge jedoch positiv und nahezu null (sagen wir, weniger als 0,2 ns), kann das ein Hinweis darauf sein, dass es schwierig war, diesen Pfad die Timing-Vorgaben erfüllen zu lassen. Solche kritischen Pfade kann man als Warnungen betrachten, dass diese Pfade in Zukunft Probleme bereiten können (insbesondere, wenn das FPGA mit mehr Logik gefüllt wird, sodass die Bemühungen der Tools auf andere Pfade umgeleitet werden).
Der Timing-Report enthält normalerweise eine begrenzte Anzahl kritischer Pfade für jeden Takt. Die Voreinstellung der meisten FPGA-Tools ist es, einige kritische Pfade anzuzeigen, auch wenn ihre Zeitmarge positiv ist (wenn die Timing-Vorgaben also erreicht wurden). Diese Einstellung ist zu empfehlen.
Ein Beispiel für einen kritischen Pfad
Ich beginne mit einem Beispiel für die Analyse eines kritischen Pfads. Für dieses Beispiel lautet der Verilog-Code wie folgt:
reg [24:0] calc, result;
reg [11:0] x, y, z;
always @(posedge clk)
begin
calc <= x * y + z;
result <= calc;
end
In diesem Beispiel beträgt die Frequenz von @clk 250 MHz, und zur Erzeugung dieses Takts wird keine PLL verwendet. Nehmen wir außerdem an, dass @x, @y und @z Register sind, die mit @clk synchron sind. Der Verilog-Code, der diesen Registern Werte zuweist, ist nicht gezeigt, weil er unerheblich ist.
Beim Versuch, diesen Code mit Vivado umzusetzen, wurden die Timing-Vorgaben nicht erreicht. Im Timing-Report war dies der kritische Pfad:
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
Die Zeitmarge dieses Pfads betrug −0,239 ns, das ist also eine knappe Verfehlung der Timing-Vorgaben. Das Erste, was man untersuchen sollte, sind Anfang und Ende dieses Pfads: Wir schauen auf „Source“ und „Destination“ im Kopfbereich des Reports und finden x_reg und calc_reg. Die Problemquelle ist also eindeutig dieser Teil:
calc <= x * y + z;
Das sollte keine Überraschung sein, denn es ist die einzige nennenswerte Operation im Verilog-Code. In einem realen Szenario ist es nicht so offensichtlich, welcher Teil der Logik das Problem verursacht hat.
Aus dem Timing-Report geht außerdem hervor, dass die Anzahl der Logikstufen groß ist: 7. Der kombinatorische Pfad (combinatorial path) ist zu lang. Mit anderen Worten: Zwischen zwei Taktflanken von @clk muss zu viel erledigt werden.
Aber was ist der eigentliche Grund für das Scheitern? Vielleicht die Tatsache, dass das Routing für 61 % der Verzögerung verantwortlich ist? Zur Erinnerung: Es gibt die Faustregel, dass die Routing-Verzögerung normalerweise etwa 40 % der Gesamtverzögerung ausmacht. Vielleicht sollte man also versuchen, die FPGA-Tools zu besserer Arbeit zu bewegen? Das ist aber wahrscheinlich keine erfolgreiche Lösung, denn die Tools arbeiten normalerweise hart, bevor sie die Versuche aufgeben, die Timing-Vorgaben eines Pfads zu erreichen.
Auch der Versuch, die Logikfunktion zwischen @x und @calc zu verändern, ist zwecklos: Die Multiplikation ist wesentlich, es gibt also keinen Weg, etwas Einfacheres einzusetzen.
Auf der nächsten Seite werde ich andere mögliche Ansätze zur Lösung eines solchen Problems vorstellen. Aber eine Liste von Techniken durchzugehen, wird in diesem Fall nicht helfen. Dieses einfache Beispiel zeigt, dass wir manchmal wie Detektive denken müssen.
Kein Ersatz für den eigenen Verstand
Die erste Frage beim Lesen eines Timing-Reports ist, was daran ungewöhnlich ist. In diesem Beispiel lautet die Antwort, dass der kombinatorische Pfad nur aus Slices besteht: Praktisch alle FPGAs besitzen dedizierte Recheneinheiten (DSPs, ALUs – die Namen variieren), die bei einer Multiplikation verwendet werden. Tatsächlich ist Multiplizieren-und-Addieren die häufigste Funktion in solchen dedizierten Logikblöcken. Die einfachste Lösung ist also in den meisten Fällen, die Tools dazu zu bringen, eine dedizierte Recheneinheit zu verwenden. Der Timing-Report für diese Lösung ist unten auf dieser Seite zu sehen.
Aber die eigentliche Frage, die wir uns stellen sollten, ist, warum Slices anstelle einer dedizierten Recheneinheit verwendet wurden. Der häufigste Grund ist, dass alle verfügbaren Recheneinheiten des FPGAs bereits von einem anderen Teil des Designs belegt sind. In diesem Fall könnte die nötige Änderung überhaupt nichts mit dem kritischen Pfad zu tun haben: Vielleicht müssen ein paar Recheneinheiten freigemacht werden, indem man etwas Logik aus dem Design entfernt. Eine andere Möglichkeit wäre, den Tools vorzugeben, diese Recheneinheiten anders auf die verschiedenen Teile des Designs zu verteilen.
In diesem Beispiel wurden Slices statt Recheneinheiten verwendet, weil ich das so herbeigeführt habe: Ich habe die Nutzung von Recheneinheiten absichtlich deaktiviert (mit einem Parameter von Vivados Synthesizer: max_dsp auf null gesetzt). Aber das macht dieses Beispiel nicht künstlich. Manchmal werden falsche Parameter bei den FPGA-Tools verwendet, die genau so eine Situation herbeiführen. Tatsächlich ist es manchmal richtig, Recheneinheiten absichtlich nicht zu verwenden, weil sie an anderer Stelle im Design dringender gebraucht werden.
Die einfache Lösung war also, dedizierte Recheneinheiten zu verwenden. Aber was, wenn wir Slices verwenden müssen? Auch hier ist die Lösung indirekt. Zur Erinnerung: Der problematische Teil war:
calc <= x * y + z;
Aber beachten Sie, dass unmittelbar danach Folgendes kommt:
result <= calc;
Wenn @calc nur in dieser Zeile verwendet wird und sonst nirgends, kann man die Berechnung in zwei Stufen aufteilen. Diese Technik wird oft als Pipelining bezeichnet. Der Verilog-Code ändert sich dann zu Folgendem:
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
In dieser Lösung erhält @calc nur das Ergebnis der Multiplikation. Der Wert von @z wird erst in der nächsten Stufe zu @calc addiert. Genauer gesagt findet die Additionsoperation zwischen @calc und @z_d statt, weil diese Operation einen Taktzyklus später passiert. Der Wert von @result ist also exakt wie zuvor.
Es war leicht, das Problem auf diese Weise zu lösen, weil @result im ursprünglichen Verilog-Code nur eine verzögerte Kopie von @calc war. Im echten Leben haben wir normalerweise nicht so viel Glück.
Beachten Sie, dass der kritische Pfad nur @calc und @x betraf. @z wurde in dem Pfad nicht einmal erwähnt. Der Zweck dieser Umstellung ist es also, die Last der Rechenoperation zu verringern. Genauer gesagt: die Anzahl der Logikstufen zu reduzieren.
Zur Erinnerung: Der kritische Pfad ist der schlechteste Pfad nach der Ausführung eines Optimierungsalgorithmus. Dieser Algorithmus fragt nicht nach dem Grund des Problems. Er versucht vielmehr, die Pfade zu verbessern, die eine negative Zeitmarge haben. Selbst wenn die Lösung des Problems eine Änderung an @z erfordert, war der mit @z zusammenhängende Pfad nicht der kritische Pfad. Das war nur ein Zufall, aber so etwas passiert häufig.
Der Timing-Report mit dem kritischen Pfad dieser Lösung ist ebenfalls unten auf dieser Seite zu sehen. Er zeigt, dass die Anzahl der Logikstufen von 7 auf 6 reduziert wurde. Dadurch verringerte sich die Datenpfadverzögerung um 0,715 ns, was mehr als genug war, um die Timing-Vorgaben zu erfüllen.
Die Lektion aus diesem Beispiel ist, dass der kritische Pfad nicht immer der unmittelbare Grund für das Problem ist. Es ist trotzdem richtig zu fragen, warum dieser Pfad scheiterte, aber die Lösung kann woanders liegen. Denken Sie daran, dass jedes FPGA-Tool eigene Hilfsmittel hat, die Informationen liefern, welche beim Finden der eigentlichen Problemursache helfen können. Es lohnt sich, diese Hilfsmittel zu erkunden und ihre Dokumentation zu lesen.
Das Problem besser früh vermeiden, als es später zu lösen
Ein großer Teil der Arbeit am Timing-Closure lässt sich vermeiden, wenn das Logikdesign von Anfang an richtig gemacht wird. Das erfordert ein ständiges Bewusstsein dafür, dass ein Logikdesign keine Software ist. Der Zweck des Verilog-Codes ist es nicht, während der Simulation das richtige Ergebnis zu erzeugen. Was wirklich zählt, ist die Ausgabe, die der Synthesizer aus dem Verilog-Code erzeugt.
Ein gutes Logikdesign beginnt damit, durchzudenken, wie die Logik ihren Zweck am besten erfüllt. Dazu gehört auch, die potenziellen timingbezogenen Hindernisse zu erkennen.
Weniger erfahrene FPGA-Entwickler entwickeln Verilog-Code oft nach Versuch und Irrtum. Die Simulation wird verwendet, um zu sehen, ob die Logik wie erwartet funktioniert, und es werden schrittweise Korrekturen vorgenommen, bis die Ausgabe der Simulation stimmt. Das Ergebnis kann Logik sein, die auf Hardware nicht verwendbar ist: Der Verilog-Code muss vollständig neu geschrieben werden, um die Timing-Vorgaben zu erreichen.
Es ist wichtig, im Voraus über die kombinatorischen Pfade nachzudenken, die der Verilog-Code erzeugt. Die Idee ist, sich jedes Register anzusehen und dem kombinatorischen Pfad bis zu seinem Ende zu folgen. Zur Erinnerung: Ein kombinatorischer Pfad beginnt immer an einem Register und endet an einem Register.
Sehen wir uns dieses Beispiel an:
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;
Was @a betrifft: Wenn sich dieses Register ändert, erreicht der kombinatorische Pfad zuerst @x. Aber @x ist kein Register. @x wird durch eine kontinuierliche Zuweisung aktualisiert. Der Pfad setzt sich also bis @z fort. Die Logik führt in diesem kombinatorischen Pfad also zwei wesentliche Operationen aus: eine arithmetische Addition und eine arithmetische Multiplikation. Ist das zu viel? Muss man das mit Pipelining in zwei Taktzyklen aufteilen? Das hängt von der Taktfrequenz und vom verwendeten FPGA ab.
Ein weiterer wichtiger Faktor ist, wie schwierig es ist, die kombinatorischen Pfade zu verkürzen. Manchmal ist ein langer kombinatorischer Pfad unvermeidlich. Wenn es aber leicht ist, das Timing eines Pfads zu verbessern, sollte man das tun, selbst wenn es im Design viel schlechtere Pfade gibt: Wenn es in einem Design einige problematische Pfade gibt, können die Tools die Timing-Vorgaben oft erreichen, indem sie ihre Anstrengungen auf diese Pfade konzentrieren. Es hilft sehr, wenn die Timing-Anforderungen der anderen Pfade leicht zu erfüllen sind.
Es gibt also keine Regeln dafür, was erlaubt ist und was nicht, um ein Design zu erhalten, das die Timing-Vorgaben erfüllt. Es erfordert Erfahrung mit FPGA-Design, um auf diesem Gebiet richtige Entscheidungen zu treffen. Auch Erfahrung mit den konkreten FPGA-Tools ist wichtig. Die einzige Regel, die immer richtig ist: Wenn sich das Timing durch eine einfache Änderung im Verilog-Code verbessern lässt, dann tu es. Sei nicht faul und mach diese Änderung von Anfang an. Habe das Timing immer im Kopf.
Schnelle Logik schreiben
Wie bereits erwähnt, ist das Ziel, kurze kombinatorische Pfade zwischen Registern zu erreichen: Die Logikfunktionen, die den nächsten Wert von Registern berechnen, sollten einfach sein. Mit anderen Worten: Es sollten nur wenige Logikstufen nötig sein, um diese Logikfunktionen zu implementieren.
Unsere Aufgabe als FPGA-Entwickler ist es, den Verilog-Code anzusehen und abzuschätzen, wie kompliziert die Logikfunktionen sein werden. Dafür ist Kenntnis darüber nötig, wie der Synthesizer Verilog in Logikelemente übersetzt – in LUTs und andere Logik-Primitive (primitives). Dieses Wissen erwirbt man durch Erfahrung, die zum Teil durch die Analyse der Timing-Reports entsteht. Um die Sache noch schwieriger zu machen: Diese Übersetzung ist von FPGA zu FPGA unterschiedlich. Es ist also keine leichte Aufgabe, Verilog-Code zu schreiben, der schnelle Logik ergibt.
Wenn Sie ein FPGA-Neuling sind, empfiehlt es sich, sich zum Lernen die Ergebnisse der Implementierung anzusehen. Der Timing-Report zeigt Beispiele dafür, wie das Logikdesign in einfache Logikelemente zerlegt wird. Die FPGA-Tools bieten auch andere Hilfsmittel an, um die zugrundeliegenden Logikelemente zu betrachten.
Außerdem gibt es ein paar einfache Regeln, die helfen können:
- Pipelining: Seien Sie großzügig mit Registern. Wenn möglich, teilen Sie die Aufgaben der Logik in kleine Stücke und fügen Sie nach jedem Schritt ein Register ein. FPGAs haben sehr viele Flipflops (oft liegt neben jeder LUT ein Flipflop), daher erhöht das Einfügen von Registern die Auslastung des FPGAs nicht. Der einzige Grund, Pipelining zu vermeiden, ist, wenn es das Design zu stark verkompliziert.
- Wenn if-then-else verwendet wird, sollten Sie vermeiden, viele else-Zweige aneinanderzureihen. Wenn stattdessen eine case-Anweisung verwendet werden kann, ist das meist besser. Ein else-Zweig erfordert oft eine Logikfunktion, die sicherstellt, dass alles vor diesem Zweig nicht zutrifft. Mehrere else-Zweige können deshalb mehrere Logikstufen erfordern.
- Vermeiden Sie unnötige Resets. Insbesondere ein synchroner Reset (synchronous reset) macht die Logikfunktion ein wenig komplexer. Beide Arten von Resets – also auch der asynchrone Reset (asynchronous reset) – erschweren das Routing, weil diese Signale viele Logikelemente erreichen müssen. Zu diesem Thema gibt es eine eigene Seitenreihe.
- Erzeugen Sie keine riesigen Zustandsautomaten (state machines). Es gibt keine feste Grenze, wie viele Zustände in Ordnung sind. Wenn die Anzahl der Zustände jedoch über 20 hinausgeht, sollten Sie eine Umstrukturierung des Designs in Betracht ziehen. Stellen Sie außerdem sicher, dass der Synthesizer für große Zustandsautomaten die One-Hot-Kodierung verwendet (standardmäßig tun die meisten Synthesizer das). Das hilft, schnelle Logik zu erzeugen.
- Ein zusätzliches Register nach einem RAM ist meist besser. Sehen wir uns dieses Beispiel eines implizit erzeugten RAMs an:
reg [7:0] array[0:127]; reg [7:0] val; reg [6:0] addr; always @(posedge clk) val <= array[addr];Dieser Verilog-Code ist korrekt, aber beachten Sie, dass @val der synchrone Ausgang des RAMs ist. Bei einer steigenden Taktflanke beginnt also die Operation des RAMs, und @val wird erst aktualisiert, wenn der Wert aus dem Speicherfeld geholt wurde. @val hat daher eine relativ große Takt-zu-Ausgang-Verzögerung (clock-to-output delay) im Vergleich zu einem Flipflop. Entsprechend haben Pfade, die bei @val beginnen, einen inhärenten Nachteil. Das lässt sich durch ein zusätzliches Register beheben:
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; endBeachten Sie, dass das nicht funktional äquivalent ist: @mem_out ist hier der Ausgang des RAMs. Erst einen Takt später wird dieser Ausgang in @val_d kopiert, es ist also kein exakter Ersatz für @val. Aber @val_d ist ein echtes Register mit einer geringen Takt-zu-Ausgang-Verzögerung. In vielen FPGAs ist dieses zusätzliche Register Teil der Block-RAMs, sodass keine Flipflops verschwendet werden. Habe ich schon erwähnt, dass man sich nie Sorgen um verschwendete Flipflops machen sollte?
Leider verkompliziert das Hinzufügen eines solchen Registers das Design oft erheblich. Wenn das der Fall ist, ist es besser, dieses zusätzliche Register nicht einzufügen, sondern zu versuchen, den kombinatorischen Pfad ab @val kurz zu halten.
Zwei zusätzliche Timing-Reports
Ich habe in dem Abschnitt „Kein Ersatz für den eigenen Verstand“ weiter oben zwei Timing-Reports versprochen. Ich habe sie hierher gestellt und nicht dorthin, wo sie erwähnt werden, weil sie lang und nicht vollständig relevant sind.
Beachten Sie, dass jeder dieser beiden Timing-Reports der kritische Pfad für das jeweilige Szenario ist. Der Pfad beginnt und endet also nicht an denselben Registern wie der oben gezeigte Pfad.
Der erste Timing-Report bezieht sich auf das erste Verilog-Code-Beispiel. Im Gegensatz zum Timing-Report oben durften die Tools dedizierte Recheneinheiten verwenden. Das Ergebnis ist, dass die Timing-Vorgaben problemlos erreicht wurden.
Dieser Timing-Report wurde für ein Kintex-Ultrascale-FPGA erzeugt. In dieser FPGA-Familie heißt eine dedizierte Recheneinheit DSP48E2. Beachten Sie, dass der Pfad auf derselben DSP48E2-Einheit beginnt und endet. Die Logikverzögerung beträgt daher 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
Der zweite Timing-Report bezieht sich auf das zweite Beispiel des Verilog-Codes. In diesem Beispiel wurde die Situation durch Pipelining verbessert:
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
Der Unterschied ist nicht so dramatisch wie bei der Verwendung einer dedizierten Recheneinheit. Aber er ist immer noch gut genug, um die Timing-Vorgabe zu erfüllen.
Damit ist die allgemeine Diskussion zum Timing-Closure abgeschlossen. Die nächste Seite schlägt einige praktische Strategien zu diesem Thema vor.