01signal.com

Tcl-Befehle zum Auswählen von Logikelementen

Diese Seite gehört zu einer Serie von Seiten über Timing. Die vorherigen Seiten haben die Theorie hinter Timing-Berechnungen erklärt, die Taktperioden-Vorgabe besprochen, die Prinzipien des Timing-Closure gezeigt und einen Blick auf die Tcl-Umgebung geworfen. Diese Seite erklärt die Befehle, mit denen Timing-Vorgaben (timing constraints) gezielt formuliert werden können.

Überblick

Zur Erinnerung: Timing-Vorgaben haben den Zweck sicherzustellen, dass die Timing-Anforderungen aller Pfade (paths) im Design erfüllt werden. Unterschiedliche Pfade können unterschiedliche Anforderungen haben. Daher muss jede Timing-Vorgabe an die betreffende Gruppe von Pfaden gerichtet sein und nichts anderes mit einschließen.

Pfade lassen sich nur dadurch definieren, dass man sich auf Logikelemente bezieht, die mit diesen Pfaden zusammenhängen. Es ist daher wichtig, präzise Ausdrücke schreiben zu können, die die richtigen Gruppen von Logikelementen auswählen. Mit anderen Worten: Man muss Befehle wie get_clocks, get_ports und get_cells so verwenden, dass ihre Ergebnisse exakt das liefern, was benötigt wird.

Diese Seite geht die grundlegenden Techniken für die Beschreibung von Gruppen von Logikelementen durch. Fast alles hier ist spezifisch für die SDC-Syntax. Ich setze außerdem voraus, dass eine Tcl-Kommandozeilen-Schnittstelle verfügbar ist. Das gilt für Vivado und Quartus sowie für die meisten aktuellen FPGA-Tools.

Einige FPGA-Hersteller erklären offen, dass Software von Synopsys in ihre Tools integriert ist. Natürlich verwenden diese Tools dann dieselben Tcl-Befehle. Vivado hingegen gilt nicht als von Synopsys abgeleitet. Dennoch gibt es einige auffällige Ähnlichkeiten zwischen den beiden, insbesondere bei der Tcl-Schnittstelle. Quartus scheint unabhängig entwickelt worden zu sein, und seine Tcl-Schnittstelle ist daher etwas anders.

Auf den ersten Blick mag es so aussehen, als ginge diese Seite zu sehr ins Detail der Tcl-Skripttechnik (scripting). Das Gegenteil ist der Fall: Genauigkeit ist äußerst wichtig, und die erreicht man nur, wenn man genau versteht, wie die Befehle interpretiert werden. Tatsächlich erklärt diese Seite nur die grundlegenden Prinzipien. Es gibt keinen Ersatz für das Lesen der Dokumentation.

Hilfe von den FPGA-Tools bekommen

Es ist oft schwierig, mit dem Schreiben von Timing-Vorgaben zu beginnen, weil so viele Details zu beachten sind. Die FPGA-Tools können dabei helfen.

Der Befehl „help“ kann in der Tcl-Konsole verwendet werden, um die Dokumentation zu einem Tcl-Befehl anzusehen. Das ist oft genau dieselbe Information wie in den offiziellen Dokumenten (z.B. als PDF).

Die meisten FPGA-Tools haben eine grafische Oberfläche (GUI) zum automatischen Erzeugen von Timing-Vorgaben. Diese Methode wird üblicherweise verwendet, um die Art der benötigten Timing-Vorgabe auszuwählen. Der nächste Schritt ist, die relevanten Logikelemente aus einer Liste auszuwählen. Das Ergebnis sind eine oder mehrere Timing-Vorgaben, die zur SDC-Datei hinzugefügt werden.

Die Verwendung eines solchen Assistenten ist hilfreich, um Hinweise auf die Tcl-Syntax zu bekommen. Gelegentlich können die automatisch erzeugten Vorgaben auch unverändert verwendet werden. In den meisten Fällen lohnt es sich jedoch, der Versuchung zu widerstehen, die Ausgabe des Assistenten einfach zu übernehmen, und stattdessen sorgfältig durchzudenken, wie der Zweck der Timing-Vorgabe am besten erreicht wird. Wichtig ist auch zu überlegen, wie sich das Projekt voraussichtlich entwickeln wird, und sicherzustellen, dass die Timing-Vorgaben im Laufe der Zeit korrekt bleiben. Eine schnelle Sitzung mit einem GUI-Assistenten wird das kaum leisten.

Einige FPGA-Tools verfügen auch über eine grafische Oberfläche zum Auffinden von Logikelementen im Design. Wenn diese Oberfläche verwendet wird, wird oft der Tcl-Befehl angezeigt, der der gewünschten Suche entspricht. Das ist eine bequeme Methode, um Tcl-Ausdrücke für das Auffinden bestimmter Logikelemente zu erhalten. Auch diese Ausdrücke sollten jedoch nur als Ausgangspunkt für weitere Arbeit betrachtet werden.

Ein drittes Merkmal der meisten FPGA-Tools ist eine Tcl-Kommandozeilen-Konsole, in der man Befehle eintippen (oder per Copy-Paste einfügen) und die Ergebnisse auf dem Bildschirm sehen kann. Damit lassen sich die Befehle und ihre Suchmuster testen und die gefundenen Objekte anzeigen. Das hilft bei der Überprüfung, ob das Suchmuster korrekt ist.

Zusammenfassend können die FPGA-Tools beim Erstellen des Tcl-Befehls helfen. Aber wie bereits gesagt, sollten die von den Tools erzeugten Tcl-Befehle nicht mehr als eine Grundlage sein, um präzise Suchmuster zu schreiben, von denen sichergestellt ist, dass sie über die Zeit hinweg korrekt funktionieren.

Nachdem wir nun den bequemen Weg zur Erstellung von Timing-Vorgaben kennen, ist es an der Zeit zu lernen, wie man es richtig macht.

Die Netzliste: Eine kurze Erinnerung

Bevor ich näher auf die Tcl-Umgebung eingehe, möchte ich kurz an Netzlisten erinnern.

Das gebräuchlichste Dateiformat für Netzlisten ist EDIF, aber viele FPGA-Tools haben auch ein eigenes, spezifisches Format. Normalerweise erzeugt der Synthesizer die Netzliste, auch wenn die Tools sie während späterer Stufen noch verändern können. Diese Datei beschreibt das Logikdesign anhand seiner Grundkomponenten und der Verbindungen zwischen diesen Komponenten. Sie ist wie ein Schaltplan, aber in Textform statt als grafisches Bild.

Die Komponenten in einer Netzliste heißen „Cells“ (Zellen). Die überwiegende Mehrheit der Cells besteht aus so etwas wie einer LUT, einem weiteren kleinen Element kombinatorischer Logik (combinatorial logic) oder einem Flipflop. Auch Instanziierungen (instantiations) von Black Boxes im Verilog-Code (z.B. IP-Cores) werden als Cell in der Netzliste dargestellt. Weitere Cells sind PLLs, Block-RAMs und große Logikelemente („Hard-IPs“): PCIe-Blöcke, MGT-Transceiver, Prozessoren usw.

Jede Cell hat eine Reihe von Pins: Diese Pins sind wie die äußeren Anschlusspunkte eines physischen elektronischen Bauteils. Man darf sie aber nicht mit den externen I/Os des FPGAs verwechseln: Sowohl die Cells als auch die Pins existieren innerhalb des FPGAs.

Die Verbindungen in der Netzliste bestehen aus Netzen. Das ist wie bei physischen Leitungen. Wenn in Verilog ein Signal zum Beispiel mit „wire“ definiert wird, entsteht daraus ein Netz. Ein Netz verbindet zwei oder mehr Pins miteinander und stellt dabei sicher, dass diese Pins jederzeit denselben Logikpegel haben.

Die Darstellung von Logikelementen als Objekte

Wenn die FPGA-Tools eine Projektimplementierung durchführen, werden tatsächlich Tcl-Skripte ausgeführt. Das gilt für Vivado und Quartus sowie für mehrere andere FPGA-Tools. Selbst bei Software, die anders funktioniert, ist diese Annahme trotzdem richtig: Die Illusion, dass alles ein großes Tcl-Skript ist, entsteht durch die API für Vorgaben und andere Skriptdateien.

In der Umgebung dieses Tcl-Skripts (ob wirklich vorhanden oder nicht) werden alle Logikelemente als Objekte dargestellt, die aus verschiedenen Klassen erzeugt werden. Timing-Vorgaben in einer SDC-Datei (oder Xilinx‘ XDC-Datei) können auf diese Objekte zugreifen. Ebenso haben Tcl-Befehle in der Tcl-Kommandozeilen-Konsole und Tcl-Skripte Zugriff auf diese Objekte.

Es gibt fünf Tcl-Befehle, die von allen FPGA-Tools unterstützt werden, die mit der SDC-Syntax arbeiten. Diese Befehle dienen dazu, Objekte verschiedener Typen (also Klassen) zu finden. Ich habe sie in den früheren Beispielen von Timing-Vorgaben bereits verwendet. Tatsächlich ist es praktisch unmöglich, ohne diese Befehle sinnvolle Timing-Vorgaben zu schreiben.

Ohne Argumente finden diese Befehle alle Objekte des betreffenden Typs. Später werden wir sehen, wie man die Suche verfeinert.

Abgesehen von diesen fünf Befehlen hat jedes FPGA-Tool seine eigenen zusätzlichen Befehle und Objekte. Vivado hat zum Beispiel zusätzliche Befehle wie all_ffs, all_registers, all_inputs, all_outputs, all_rams und einige weitere dieser Art. Quartus unterstützt einige davon und hat außerdem get_registers, get_keepers, get_nodes, get_fanins und get_fanouts usw.

Diese Objekte sind deshalb wichtig, weil sie in Timing-Vorgaben verwendet werden, um sich auf Logikelemente im FPGA-Design zu beziehen. Sie können auch in Tcl-Skripten verwendet werden, um Informationen zu erhalten, zum Beispiel eine Taktfrequenz. Jedes FPGA-Tool hat seine eigene API für den Zugriff auf diese Objekte in Tcl-Skripten. In Vivado kann zum Beispiel dieser Tcl-Befehl verwendet werden, um eine Taktperiode zu erhalten:

> get_property PERIOD [get_clocks clk]
4.000

Und dasselbe in Quartus:

> get_clock_info -period [get_clocks clk]
4.000

Trotz dieser Unterschiede stimmen die meisten FPGA-Tools in Syntax und Bedeutung von Timing-Vorgaben im SDC-Format weitgehend überein. Die Informationen über die API, die jedes Tool unterstützt, finden sich normalerweise in Benutzerhandbüchern, deren Titel Bezug auf Tcl-Skripte oder Timing-Closure nehmen.

Die Beispiele auf dieser Seite basieren, sofern nicht anders angegeben, auf Vivado.

Ein paar Anmerkungen zu Tcl

Tcl ist eine archaische Sprache, aber weil sie im Logikdesign fest etabliert ist, wird sie so schnell nicht verschwinden. Glücklicherweise kann man mit Tcl auch dann Nützliches tun, wenn man die Sprache nicht besonders gut kennt.

Das erste, was ich bereits kurz erwähnt habe, sind eckige Klammern („[“ und „]“). In Tcl bedeutet das, den Befehl in den eckigen Klammern auszuführen und das Ergebnis an ihre Stelle zu setzen. Für diejenigen, die mit Shell-Skripten oder Perl vertraut sind: Das ist wie Backticks. In diesem Befehl wird zum Beispiel get_port durch das Portobjekt mit dem Namen „clk“ ersetzt:

create_clock -period 4.000 -name clk [get_ports clk]

Ebenso hätte man das so schreiben können:

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

Wie dieses zweite Beispiel zeigt, werden Variablen mit dem Befehl „set“ definiert und mit einem Wert belegt. Um auf den Wert der Variablen zuzugreifen, verwendet man das Dollarzeichen ($). Auch das ähnelt Shell-Skripten und Perl.

Was geschweifte Klammern („{“ und „}“) betrifft, ist die Lage anders: Wie in einigen anderen Sprachen hängt ihre Bedeutung stark vom Kontext ab. In Tcl ist eine der weniger erwarteten Bedeutungen geschweifter Klammern, dass der eingeschlossene String unverändert bleiben soll. Es sollen also keine Substitutionen stattfinden, und Leerzeichen sollen wie normale Zeichen behandelt werden. Dieselbe Timing-Vorgabe hätte man zum Beispiel so schreiben können:

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

In diesem Beispiel sind die geschweiften Klammern völlig unnötig, und der Befehl bedeutet exakt dasselbe wie zuvor. Unnötige geschweifte Klammern sind leider häufig, und oft bedeuten sie nichts – genau wie in diesem Beispiel.

Ein Tipp zur Nutzung der Tcl-Konsole

Es kommt oft vor, dass die Anzahl der gefundenen Objekte groß ist, was die Ausgabe des Suchbefehls schwer lesbar macht. Das lässt sich mit einfachen Tcl-Befehlen lösen. Wie genau, hängt vom verwendeten Tool ab. Mit Vivado gibt der folgende Befehl alle Cells im Design aus. Jede Cell wird in einer eigenen Zeile ausgegeben, sodass die Ausgabe auch bei vielen Cells lesbar bleibt:

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

Der Befehl „join“ fügt zwischen jedem Element des Arrays, das get_cells erzeugt, einen Zeilenumbruch ein.

Mit Quartus erreicht man dasselbe Ergebnis mit:

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

Oder, indem man query_collection geschickter verwendet:

query_collection -all -report_format [get_cells -hierarchical]

Bestimmte Elemente finden

Nach einer langen Einleitung ist es endlich an der Zeit, über das eigentlich Interessante zu sprechen. Für die folgenden Beispiele beziehen wir uns auf diesen Verilog-Code, der später ebenfalls verwendet wird:

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

Wie am Anfang dieser Seite erwähnt, hängt die Genauigkeit von Timing-Vorgaben davon ab, die richtigen Gruppen von Logikelementen auszuwählen. Es ist daher möglich und notwendig, die Suchergebnisse der fünf oben genannten get_*-Befehle einzugrenzen. Dafür gibt es mehrere Wege. Der gebräuchlichste Weg stützt sich auf den Namen des Objekts. Das einfachste Muster findet ein einzelnes Objekt mit dem exakt gesuchten Namen. Zum Beispiel in Vivados Tcl-Konsole:

> get_ports clk
clk

Ebenso ist es möglich, alle Objekte zu finden, deren Namen zu einem bestimmten Muster passen:

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

Beachten Sie, dass die Ausgabe in beiden Beispielen aus Objekten besteht. In Vivados Tcl-Konsole werden die Namen dieser Objekte der Einfachheit halber ausgedruckt.

Noch wichtiger ist, dass jedes FPGA-Tool bei diesen Suchmustern sich etwas anders verhält. In den Beispielen hier wird Vivado verwendet. Für andere Tools gelten dieselben Prinzipien.

Der Stern („*“) ist ein Platzhalterzeichen, das eine beliebige Anzahl von Zeichen ersetzt. Ein Fragezeichen („?“) ersetzt genau ein Zeichen. Das funktioniert genauso wie Platzhalter bei Dateinamen.

Beachten Sie, dass Platzzhalter wie bei Dateinamen nicht mit dem Hierarchietrenner übereinstimmen (z.B. „/“ bei Vivado und „|“ bei Quartus).

Es gibt auch eine Ähnlichkeit zwischen dem hierarchischen Pfad und Dateiverzeichnissen: Die Suche erfolgt relativ zur Top-Level-Hierarchie, ähnlich dem Wurzelverzeichnis eines Dateisystems. Zum Beispiel:

> 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

Die Notwendigkeit, die genaue Position jedes Logikelements in der Hierarchie anzugeben, ist oft ein erheblicher Nachteil: Die Logikelemente, die wir finden wollen, liegen häufig an unterschiedlichen Positionen. Das wird mit „-hierarchical“ gelöst: Wenn dieses Flag gesetzt ist, wird das Muster an allen Positionen der Hierarchie gesucht.

Grundsätzlich ist die Verwendung von Platzhaltern zum Finden von Logikelementen nicht zu empfehlen. Die einzigen Ausnahmen sind sehr einfache Suchmuster oder wenn es keine andere Möglichkeit gibt. Es gibt eine eigene Seite, die erklärt, wie man Platzhalter und „-hierarchical“ verwendet, und die auch die Grenzen dieser Methode aufzeigt.

Verwendung von -filter

Die Verwendung von Suchmustern, die auf Platzhaltern basieren, hat mehrere Nachteile. Der bedeutendste Nachteil ist, dass die Hierarchie entweder präzise angegeben werden muss oder gar nicht. Ein Problem ist, dass die Suche oft auf Objekte beschränkt werden soll, die zu einer Teilhierarchie gehören. Das ist mit Platzhaltern nicht möglich, und die Option „-hierarchical“ löst dieses Problem nicht.

Aus diesem und anderen Gründen ist die bevorzugte Methode zur Definition von Suchmustern die Option -filter. Zu dieser Option gehört ein boolescher Ausdruck. Wenn diese Option verwendet wird, bleiben nur diejenigen Suchergebnisse übrig, für die dieser Ausdruck wahr ist.

Zum Beispiel:

> 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

In diesem Beispiel wird die Eigenschaft namens „name“ an jedem der Objekte geprüft, die durch den Platzhalter gefunden wurden. Das Objekt blieb nur dann im Suchergebnis, wenn diese Eigenschaft zu „*2*“ passte. Mit anderen Worten: nur wenn der Name des Objekts die Zeichenfolge „2“ enthielt.

Dieses Beispiel ist praktisch nicht besonders interessant. Viel interessanter ist es, wenn „-hierarchical“ verwendet wird: Ohne Suchmuster werden alle Objekte gefunden. Der folgende Befehl findet also alle Pins in allen Hierarchien:

get_pins -hierarchical

Von hier aus lässt sich das Suchergebnis mit -filter eingrenzen:

> 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

Beachten Sie, dass die geschweiften Klammern („{“ und „}“) hier nur bedeuten, dass der Teil innerhalb der Klammern vom Tcl-Interpreter nicht verändert werden soll.

Beachten Sie außerdem: Das Suchmuster gehört zu der Option -filter und verhält sich anders: Es behandelt „name“ als eine Eigenschaft eines Objekts. Daher werden alle Zeichen gleich behandelt: „/“ hat keine besondere Bedeutung. Es spielt keine Rolle, dass „/“ der Hierarchietrenner ist. Alle Zeichen, einschließlich „/“, können mit dem Platzhalter („*“) übereinstimmen. Ebenso können alle Zeichen, einschließlich „/“, im Suchmuster verwendet werden. Ohne -filter gilt das natürlich nicht.

Die Tatsache, dass jedes Zeichen mit „*“ übereinstimmen kann, macht -filter zu einem stärkeren Werkzeug, aber dieser Vorteil ist auch eine Fehlerquelle: Man vergisst leicht, dass ein harmloser „*“ versehentlich sowohl „pll_i/clk_“ als auch „pll_i/inst/clk_“ treffen kann, wie im obigen Beispiel gezeigt.

Eine korrekte Verwendung dieser Eigenschaft ist das Auffinden eines Logikelements mit bekanntem Namen irgendwo in der Hierarchie:

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

Das ist richtig, wenn wir sicher sind, dass es im Design nur eine Cell mit dem Namen „clkout2_buf“ gibt. Mit diesem Befehl wird der Ausgangspin dieser Cell immer gefunden, selbst wenn das Modul, das dieses Logikelement enthält, in der Projekthierarchie verschoben wird. In einem realen Szenario wählt man besser einen eindeutigeren Namen als „clkout2_buf“.

Dieselbe Methode funktioniert mit get_cells und get_nets, zum Beispiel:

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

Aber -filter arbeitet mit allen Eigenschaften, nicht nur mit dem Namen. Der folgende Befehl findet also alle Register, in deren Namen „bar“ vorkommt:

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

Beachten Sie den Operator „&&“, der logisches UND bedeutet (genau wie in Verilog und C).

In diesem Befehl sind „primitive_type“ und „name“ nur Namen von Eigenschaften. Der Operator „=~“ führt einen Vergleich durch und erlaubt Platzhalter.

Zur Erinnerung: Die Eigenschaften eines Objekts lassen sich mit dem Befehl „report_property“ (in Vivado) auflisten.

Mit -filter lässt sich also flexibel arbeiten. Leider ist diese Option nicht bei allen FPGA-Tools verfügbar.

Beachten Sie, dass Vivado einen Befehl namens filter hat, der dieselbe Operation wie -filter durchführt. Die folgenden beiden Befehle sind also äquivalent:

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

Das zweite Format ist nützlich, wenn eine Liste von Objekten in einer Variablen gespeichert ist. Die beiden obigen Befehle sind daher auch zu Folgendem äquivalent:

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

Verwendung von -regex

Wer mit regulären Ausdrücken vertraut ist, möchte diese Option vielleicht verwenden. Das ist meist keine gute Idee, vor allem weil es die Timing-Vorgaben für andere Personen schwerer verständlich macht. FPGA-Tools, die reguläre Ausdrücke unterstützen, unterstützen meist auch die Option -filter, daher ist es fast immer besser, -filter zu verwenden.

Zur Erinnerung: Das Hauptproblem beim üblichen Suchmuster ist, dass der Hierarchietrenner nicht mit dem Platzhalter übereinstimmt.

-regexp kann das lösen, wie diese beiden Beispiele zeigen:

> 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

Der erste Befehl zeigt, dass „.“ mit jedem beliebigen Zeichen übereinstimmt. Das schließt „/“ (den Hierarchietrenner) ein.

Der zweite Befehl zeigt, wie „[^/]+“ verwendet wird, um mit allem außer einem Hierarchietrenner übereinzustimmen. Damit lässt sich die genaue Tiefe in der Hierarchie der Suchergebnisse steuern. Dieser Befehl zeigt auch, dass das Suchmuster kein Argument von -regexp ist, sondern dass -regexp die Syntax des Suchmusters verändert.

Beachten Sie, dass der reguläre Ausdruck mit dem gesamten Namen des Objekts übereinstimmen muss. Mit anderen Worten: Die Tools fügen am Anfang des regulären Ausdrucks implizit ein „^“ und am Ende ein „$“ hinzu.

Verwenden Sie -regex aber nicht, wenn es einen anderen Weg gibt, dasselbe Ergebnis zu erreichen. Die meisten anderen FPGA-Entwickler werden das Suchmuster nicht verstehen können.

Verwendung von -of_objects

Statt Objekte anhand ihrer Namen zu finden, erlaubt -of_objects, Objekte anhand ihrer Beziehung zu anderen Objekten zu finden. In den meisten Fällen bedeutet „-of_objects“ in etwa „ist verbunden mit“.

Zum Beispiel, um alle Pins zu finden, die mit einem Netz verbunden sind:

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

Oder alle Pins einer Cell:

> 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

Oder den Pin, der der Ursprung eines Takts ist:

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

Dieselbe Methode funktioniert auch zum Finden von Netzen. Zum Beispiel: Welche Netze sind mit einem bestimmten Pin verbunden?

> get_nets -of_objects [get_pins bar_reg_reg/C]
pll_clk_6

Oder: Welche Netze sind mit der betreffenden Cell verbunden?

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

Ebenso kann man auf diese Weise nach Cells suchen. Zum Beispiel: Welche Cells sind mit @pll_clk_6 verbunden?

> get_cells -of_objects [get_nets pll_clk_6]
bar_reg__0 bar_reg_reg pll_i

Beachten Sie, dass „pll_i“ der Instanziierungsname des Clock-Wizard-IPs ist. Das ist vermutlich kein gewünschtes Suchergebnis. Wie wäre es also, das Suchergebnis auf Flipflops einzugrenzen?

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

Bisher haben die obigen Beispiele mit -of_objects gezeigt, wie Pins, Netze und Cells aufeinander verweisen können. Man kann mit dieser Option aber auch Taktobjekte finden:

> 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

Diese drei Befehle zeigen, wie der Takt anhand des Logikelements gefunden wird, das mit ihm verbunden ist. Beachten Sie, dass es bei einer Cell mehr als ein Ergebnis geben kann. Zum Beispiel:

> get_clocks -of_objects [get_cells pll_i]
clk clk_out1_clk_wiz_1 clk_out2_clk_wiz_1

Zur Erinnerung: „pll_i“ ist ein IP, seine Pins sind also die Ports dieses Moduls:

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

Um einen bestimmten Takt zu finden, ist get_pins also sicherer:

> get_clocks -of_objects [get_pins pll_i/clk_out2]
clk_out2_clk_wiz_1

Aber natürlich findet man das Taktobjekt an einem externen I/O-Port am besten mit:

> get_clocks -of_objects [get_ports clk]
clk

Oder mit dem kürzeren, äquivalenten Befehl:

> get_clocks [get_ports clk]
clk

Was get_ports betrifft, so funktioniert es auch mit -of_objects. Zu den Möglichkeiten, die für get_ports spezifisch sind, lesen Sie bitte die Dokumentation. Die Möglichkeiten, die den anderen Befehlen ähneln, sind ziemlich nutzlos, zum Beispiel:

> get_ports -of_objects [get_nets clk]
clk

Zusammenfassend ist -of_objects eine ausgezeichnete Methode, um bestimmte Logikelemente auszuwählen. Das gilt insbesondere wegen der Schwierigkeiten bei der Suche nach dem Namen des Objekts, die im nächsten Thema behandelt werden.

Leider unterstützen viele FPGA-Tools -of_objects nicht, sodass keine andere Wahl bleibt, als sich auf weniger zuverlässige Methoden zu verlassen.

Das Problem mit der Suche nach Namen

Wie oben bereits besprochen, hängt die Genauigkeit von Timing-Vorgaben von der Genauigkeit beim Auffinden von Logikelementen ab. Zumindest einige dieser Suchergebnisse hängen vom Namen eines Objekts ab. Sehen wir uns an, wie das Probleme verursachen kann.

Zum Beispiel wird „get_ports clk“ verwendet, um die Verbindung zwischen einem Taktobjekt und dem Signal an einem bestimmten I/O-Pin herzustellen. Dieser I/O-Pin wird durch ein Portobjekt mit dem Namen „clk“ repräsentiert:

create_clock -period 4.000 -name clk [get_ports clk]

Aber warum hat dieses Portobjekt den Namen „clk“ bekommen? Die Antwort hängt damit zusammen, wie der Synthesizer die Netzliste erzeugt hat: Der Name des Top-Level-Ports im Verilog-Code wurde auch als Name des Top-Level-Ports der Netzliste gewählt. Das ist eine naheliegende Wahl, daher macht jeder ordentliche Synthesizer dasselbe.

Aber wie sieht es mit den Namen von Cells aus? Nehmen wir das Register mit dem Namen „foo_reg“ im obigen Verilog-Code. Wie heißt das Cell-Objekt, das das Flipflop dieses Registers repräsentiert? Vivados Synthesizer hat für dieses Objekt den Namen „foo_reg_reg“ gewählt. Es ist also klar, dass dieser Synthesizer dazu neigt, dem Namen im Verilog-Code ein Suffix „_reg“ hinzuzufügen. Das klingt nach einer Regel, auf die man sich verlassen kann. Ein anderer Synthesizer würde wahrscheinlich etwas anderes tun.

Aber was ist mit dem Register mit dem Namen „bar“? Der Name des betreffenden Cell-Objekts hätte „bar_reg“ lauten sollen, aber der Synthesizer hat eine andere Wahl getroffen: „bar_reg__0“. Das liegt daran, dass es im Verilog-Code ein Register namens „bar_reg“ gibt. Um eine Namenskollision zu vermeiden, hat der Synthesizer einen etwas anderen Namen gewählt: Er hat statt nur „_reg“ das Suffix „_reg__0“ angehängt. Dieses einfache Beispiel zeigt das Problem, sich auf Namen von Objekten zu verlassen.

Um die Sache noch schlimmer zu machen, nehmen wir an, die Timing-Vorgabe wurde geschrieben, bevor das Register mit dem Namen „bar_reg“ zum Verilog-Code hinzugefügt wurde. In diesem Fall erhält das Cell-Objekt für @bar den üblichen Namen „bar_reg“. Die Timing-Vorgabe würde also diesen Namen für das Objekt verwenden. In einer späteren Phase wird das Register mit dem Namen „bar_reg“ zum Design hinzugefügt. Dadurch ändert sich der Name des gewünschten Cell-Objekts von „bar_reg“ zu „bar_reg__0“. Die Timing-Vorgabe, die sich auf den Namen „bar_reg“ stützt, ist plötzlich falsch. Mit etwas Glück gibt das Tool eine Warnung dazu aus.

Es gibt weitere mögliche Gründe, warum die Verwendung von Objektnamen schiefgehen kann. Die Tools können zum Beispiel ein Register automatisch duplizieren, wenn sein Fan-out einen Grenzwert überschreitet. Wenn das passiert, ist das zusätzliche Register möglicherweise nicht in der Timing-Vorgabe enthalten, weil der Name des neuen Registers nicht zum Suchmuster passt.

Ein ernsteres Problem ist, wenn Logikelemente versehentlich in Timing-Vorgaben eingeschlossen werden. Das kann zum Beispiel mit Logikelementen passieren, die zu IP-Cores gehören. Da wir keine Kontrolle über die Namen dieser Logikelemente haben, können diese Namen versehentlich zu einem Muster in einer Timing-Vorgabe passen.

Das versehentliche Einschließen von Logikelementen in Timing-Vorgaben kann auch eine Folge von Bequemlichkeit sein: Timing-Vorgaben werden meist zusammen mit dem Hinzufügen neuer Funktionen zum Logikdesign geschrieben. Wenn das Suchmuster durch Versuch und Irrtum entsteht, werden die Namen künftiger Logikelemente möglicherweise nicht berücksichtigt. Wenn also neue Logik hinzugefügt wird, können einige ihrer Logikelementnamen unbeabsichtigt zu den vorhandenen Timing-Vorgaben passen.

Wie man Fehler bei Timing-Vorgaben vermeidet

Die erste und wichtigste Regel ist: Es reicht nicht, zu testen, welche Logikelemente zu einem Suchmuster einer Timing-Vorgabe passen. Selbst wenn man eine vollständige Liste dieser Logikelemente erstellt und diese Liste sorgfältig durchsieht, garantiert das nichts für Logik, die später hinzugefügt wird. Eine solche Prüfung garantiert auch nicht, dass die Timing-Vorgaben weiterhin wie erwartet funktionieren, wenn die Namen der Objekte vom Synthesizer oder während einer anderen Stufe der Implementierung geändert werden.

Es ist daher wichtig, die Suchmuster wie mathematische Ausdrücke zu behandeln: Es reicht nicht, dass diese Suchmuster jetzt wie erwartet funktionieren. Und wenn sie nicht wie erwartet funktionieren, reicht es nicht, sie mit einer kleinen Korrektur zum Funktionieren zu bringen. Vielmehr muss es eine logische Erklärung dafür geben, warum das Suchmuster korrekt ist und warum es sehr wahrscheinlich auch im Laufe der Zeit korrekt bleibt.

Wichtig ist auch, dass sich die Suchmuster auf Dinge stützen, die sich wahrscheinlich nicht ändern. Die Tools ändern zum Beispiel die Namen von Instanziierungen im Verilog-Code nicht. Das gilt für Instanziierungen von Modulen, IPs und Primitiven. Es ist daher sicher, sich auf den hierarchischen Pfad zu stützen, wenn er nur aus Namen von Instanziierungen besteht, die im Verilog-Code stehen. Nicht so sicher ist es, Namen zu verwenden, die innerhalb eines IP-Cores entstanden sind. Um das zu erklären, schauen wir uns diesen Befehl an:

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

Das ist eine Methode, ein Taktobjekt zu erhalten, indem man sich auf den Ausgangspin des globalen Taktpuffers bezieht, der den Takt verteilt. Die Frage ist, ob wir sicher sein können, dass das auf Dauer funktioniert.

Das Problem bei dieser Methode ist, dass „inst“ und „clkout1_buf“ Namen von Instanziierungen sind, die innerhalb des Clock-Wizard-IPs erzeugt wurden. Auch wenn sich diese Namen wahrscheinlich nicht ändern, gibt es keine Garantie dafür.

Eine mögliche Lösung ist, den Verilog-Code zu finden, der das IP implementiert, und diesen Verilog direkt in das Projekt aufzunehmen. Dadurch wird sichergestellt, dass sich in Zukunft nichts ändert.

Eine Alternative ist, sich die Instanziierung des IPs in Verilog anzusehen. Zur Erinnerung, sie lautete:

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

Da dies eine Instanziierung einer Black Box ist, hat jeder dieser Ports einen Pin in der Netzliste. Die Namen können sich nicht ändern, weil die Ports des IPs auf diese Weise identifiziert werden. Es ist also garantiert, dass der Pin mit dem Namen „pll_i/clk_out1“ die Verbindung zu @pll_clk_8 herstellt. Das ist also ein sicherer Weg, das Taktobjekt zu erhalten:

get_clocks -of_object [get_pins pll_i/clk_out1]

Beachten Sie, dass das wahrscheinlich nicht funktioniert, wenn clk_wiz_1 nur ein weiteres Verilog-Modul im Projekt ist: In diesem Fall werden für die Instanziierung dieses Moduls keine Pins erzeugt (weil der Synthesizer Verbindungen von Ports normalerweise durch das Zusammenführen von Netzen implementiert). Eine mögliche Lösung wäre, Namen aus niedrigeren Hierarchieebenen zu verwenden.

Es gibt also einige Arten von Namen, auf die man sich verlassen kann:

Besser groß scheitern

Die schlimmste Situation ist, wenn eine Timing-Vorgabe fast richtig ist: wenn nur ein paar Pfade fehlen. Oder wenn nur ein paar Pfade versehentlich in die Timing-Vorgabe eingeschlossen sind. Fehler dieser Art sind am schwierigsten zu finden.

Das ist der Hauptgrund, warum Timing-Vorgaben, die kurz, prägnant und von mathematischer Klarheit sind, besser sind. Wenn es für jede kleine Gruppe von Logikelementen eine eigene Timing-Vorgabe gibt, kann sich leicht ein Fehler in diese lange Liste von Regeln einschleichen.

Um diese Idee zu veranschaulichen, kehren wir zu dem oben gezeigten Beispiel für das Finden eines Taktobjekts zurück:

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

Wie oben erwähnt, besteht das Problem bei diesem Ausdruck darin, dass kein Taktobjekt gefunden wird, wenn die Instanziierung von pll_i an eine andere Position in der Hierarchie wandert. Wie schlimm wäre das?

Angenommen, dieses Taktobjekt wird in einer Tcl-Variablen gespeichert:

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

Und dann wird $my_clock in vielen Timing-Vorgaben verwendet, sodass viele Pfade betroffen sind. In diesem Fall ist es eigentlich kein großes Problem, wenn $my_clock wegen eines Fehlers plötzlich kein Taktobjekt enthält: Es besteht eine gute Chance, dass dieser Fehler bald bemerkt wird, weil dann vieles schiefgeht.

Wenn $my_clock aber nur in einer einzigen Timing-Vorgabe verwendet wird, und diese Vorgabe dazu gedacht ist, ein kleines Problem zu lösen, das selten Auswirkungen hat, ist das eine schlechte Situation. Es ist wahrscheinlich, dass dieser Fehler unbemerkt bleibt.

Zusammenfassend: Eine gut geschriebene Timing-Vorgabe sollte entweder perfekt funktionieren oder überhaupt nicht.


Auf dieser Seite wurde gezeigt, wie man Logikelemente präzise auswählt. Auf der nächsten Seite wird dieses Wissen verwendet, um gezielte Timing-Vorgaben zu definieren.

Diese Seite wurde maschinell aus dem Englischen übersetzt. Im Zweifelsfall siehe den Originaltext.
Copyright © 2021-2026. All rights reserved. (dcc38493)