01signal.com

Partielle Rekonfiguration mit Vivado verstehen

Dies ist der erste Beitrag in einer vierteiligen Serie über partielle Rekonfiguration (Partial Reconfiguration) mit Xilinx' Vivado, die dort auch Dynamic Function eXchange (DFX) heißt. In diesem Beitrag sollen die wichtigsten Konzepte dieses Themas erklärt werden. Damit wird die Grundlage für den nächsten Beitrag gelegt, der die praktischen Schritte zum Einrichten eines FPGA-Projekts mit partieller Rekonfiguration beschreibt.

Einführung

Partielle Rekonfiguration ist eine Technik, mit der sich die Logik einzelner Teile des FPGAs austauschen lässt, während andere Teile normal weiterarbeiten. Dazu wird dem FPGA ein Bitstrom (Bitstream) zugeführt, genau wie der initiale Bitstrom, der beim Einschalten die Funktionalität programmiert. Der Bitstrom für die partielle Rekonfiguration bringt das FPGA jedoch nicht zum Anhalten. Er wirkt stattdessen auf bestimmte Logikelemente und aktualisiert die Speicherzellen, die deren Verhalten steuern. Es werden also bestimmte Logikblöcke im laufenden Betrieb ausgetauscht.

Xilinx-FPGAs unterstützen diese Funktion seit Virtex-4 (Intel-FPGAs ab der Serie V).

Dieser Beitrag führt durch die Konzepte hinter der partiellen Rekonfiguration, ohne sich in praktischen technischen Details zu verlieren – als Vorbereitung auf den nächsten Beitrag, der genau das tut. Da in diesem Thema alles mit allem zusammenhängt, ist es wichtig, das Gesamtbild zu verstehen, bevor man es in einzelne Handgriffe zerlegt.

Noch einmal erklärt?

Beginnen wir damit, wie die Funktionalität eines FPGAs ohne partielle Rekonfiguration geändert wird. Nehmen wir an, es gibt irgendwo in der Designhierarchie eine Instanziierung (Instantiation) eines Moduls. Zum Beispiel in Verilog:

   moduleA reconfig_ins
     (
      .clk(clk),
      .this(this_w),
      .that(that_w),
 [ ... ]
      );

oder in VHDL:

  reconfig_ins : moduleA
    port map(
      clk        => clk,
      this       => this_w,
      that       => that_w,
 [ ... ]
      );

Natürlich gibt es irgendwo im Projekt ein Modul mit dem Namen moduleA.v oder moduleA.vhd, oder einen IP-Core namens moduleA, der die Instanziierung zusammen mit seinen Untermodulen ausfüllt. Dann führen wir eine Implementierung des Projekts durch, erhalten eine Bitstrom-Datei und laden sie in das FPGA. So weit das übliche Vorgehen.

Jetzt nehmen wir aber an, dass wir im obigen Code moduleB statt moduleA schreiben und eine Implementierung der Logik durchführen, um eine Bitstrom-Datei zu erhalten. Damit das funktioniert, muss es im Design ein moduleB.v oder moduleB.vhd geben oder einen IP-Core namens moduleB.

Wir haben nun zwei Bitstrom-Dateien, die sich in der Logik innerhalb der Instanz namens reconfig_ins unterscheiden. Um zwischen diesen beiden Bitströmen umzuschalten, müssen wir das gesamte FPGA mit dem gewünschten Bitstrom laden. Das führt zu einer Unterbrechung des FPGA-Betriebs.

Partielle Rekonfiguration ist eine Technik, mit der man ohne diese Unterbrechung von einer Version zur anderen wechseln kann: Das FPGA arbeitet normal weiter, während sich die Logik in reconfig_ins von moduleA zu moduleB ändert – und umgekehrt. Es versteht sich fast von selbst, dass das nicht einfach durch eine getrennte Implementierung der beiden Designs möglich ist.

moduleA und moduleB werden als rekonfigurierbare Module (RM) bezeichnet. Das bedeutet, dass ihre Logik mithilfe der partiellen Rekonfiguration in das FPGA eingebracht werden kann.

Motivation

Es gibt mehrere Gründe für den Einsatz partieller Rekonfiguration, zum Beispiel:

Der partielle Bitstrom

Wenn Sie etwas Erfahrung mit FPGA-Design haben, kennen Sie vermutlich eine einfache Routine: Sie ändern den Quellcode (und die IPs) des Designs, starten die Implementierungswerkzeuge und prüfen, ob alles durchgelaufen ist. Dann laden Sie den Bitstrom über JTAG in das FPGA. Oder Sie laden alternativ einen Flash-Baustein mit einem Abbild des Bitstroms.

Dadurch, dass wir alle daran gewöhnt sind, liegt es nahe, den Bitstrom für eine große Datenmenge zu halten, die das gesamte FPGA mit mystischen Informationen darüber füllt, wie sich jedes Logikelement verhalten soll. In Wirklichkeit besteht ein Bitstrom aus einer Reihe von Befehlen, die vom FPGA beim Laden sequenziell ausgeführt werden. Zwar lädt der übliche Bitstrom das gesamte FPGA mit Informationen, aber das geschieht über mehrere Befehle, die den Ablauf steuern. Wichtiger ist, dass diese Befehle festlegen, welche Logikelemente jeweils mit welchen Daten geladen werden.

Da der Bitstrom selbst angibt, welche Logikelemente betroffen sind, ist es möglich, einen Bitstrom zu erzeugen, der nur bestimmte Logikelemente verändert und andere unangetastet lässt. Das ist der Grundstein der partiellen Rekonfiguration.

Allerdings muss der partielle Bitstrom mit der bereits im FPGA geladenen Logik kompatibel sein. Es geht nicht nur darum, versehentlich die falschen Logikelemente zu überschreiben: Der initiale Bitstrom ist eng mit dem partiellen Bitstrom verzahnt, insbesondere weil der initiale Bitstrom Logik- und Routing-Ressourcen nutzt, die innerhalb des Bereichs liegen, den der partielle Bitstrom verändert. Ist der partielle Bitstrom korrekt auf den initialen Bitstrom abgestimmt, bleibt dieser heikle Tanz unbemerkt. Ist er das nicht, wird das FPGA sehr wahrscheinlich verrücktspielen – einschließlich der Funktionalität, die unberührt bleiben sollte.

Den partiellen Bitstrom laden

Das Übergeben eines Bitstroms für partielle Konfiguration an das FPGA kann über jede Schnittstelle erfolgen, die das Laden von Bitströmen erlaubt, solange der Vorgang bei laufendem FPGA durchgeführt werden kann. Dazu gehört die JTAG-Schnittstelle, sodass eine .bit-Datei für die partielle Konfiguration wie gewohnt mit dem Hardware Manager geladen werden kann. Noch interessanter ist, dass dies aus der FPGA-Logik selbst heraus geschehen kann, und zwar über den dedizierten Internal Configuration Access Port (ICAP). Dieser Port kann nur für partielle Rekonfiguration verwendet werden, da der Teil der Logic Fabric des FPGAs, der den Bitstrom lädt, während des gesamten Vorgangs intakt bleiben muss.

Der ICAP ist nur eine Schnittstelle zum Teilsystem des FPGAs, das für das Laden von Bitströmen zuständig ist, und schreibt nichts über die Quelle des Bitstroms vor. Entsprechend gibt es keine Einschränkung, wie die Bitstrom-Daten ins FPGA gelangen oder wo und wie sie gespeichert werden. Sie müssen lediglich irgendwie für das FPGA-Logikteil verfügbar sein, das den ICAP speist.

Xillybus bietet zum Beispiel ein einfaches Mittel, um eine Bitstrom-Datei von einem Computer über eine PCIe- oder USB-3.x-Schnittstelle an den ICAP zu senden, sofern das Board darüber verfügt.

Statische Logik

Für eine korrekte partielle Rekonfiguration muss man das Gegenstück berücksichtigen: die statische Logik. Das ist ein allgemeiner Begriff für die Teile des FPGA-Designs, die unangetastet bleiben müssen und daher von Anfang an vorhanden sind, seit der initiale Bitstrom geladen wurde.

Diese Logik ist in zweierlei Hinsicht statisch: Zum einen funktional – die Logik besteht aus den Teilen des FPGA-Designs (HDL und IP), die vom initialen Start des FPGAs an ununterbrochen funktionieren. Der zweite, nicht weniger wichtige Aspekt ist, dass die Platzierung dieser Logik auf Plätzen (Sites) in der Logic Fabric beschränkt ist, die als statisch ausgewiesen sind. Auf diesen Plätzen sind spätere Veränderungen nicht erlaubt.

In einem realen Design reicht es nicht, dass die statische Logik unverändert bleibt. Sie muss auch während des Austauschs anderer FPGA-Teile korrekt weiterfunktionieren. Da es mit ziemlicher Sicherheit Netze gibt, die die statische Logik mit der wechselnden Logik verbinden, liegt es in der Verantwortung des FPGA-Designers, dafür zu sorgen, dass alles reibungslos läuft. Der dritte Beitrag dieser Serie behandelt dieses Thema.

Trennung von statischer und rekonfigurierbarer Logik

Damit partielle Rekonfiguration überhaupt möglich ist, müssen statische Logik und rekonfigurierbare Logik streng voneinander getrennt sein. Insbesondere müssen die physischen Logikelemente auf dem FPGA getrennt sein, sodass beim Laden des Bitstroms keiner der Plätze, die statische Logik enthalten, beeinflusst wird.

Um zu verstehen, was das erfordert, schauen wir uns zunächst an, was wir alle gewohnt sind.

Zur Erinnerung: Der übliche FPGA-Implementierungsprozess beginnt mit der Synthese des HDL-Designs. Dabei bedeutet die Instanziierung von Modulen in HDL keine Trennung zwischen ihnen. Das Gegenteil ist der Fall: Der Synthesizer behandelt Instanziierungen als Beschreibung dafür, wie die Logik funktionieren soll. Er kann also das gesamte Design als ein einziges großes, flaches Logikgebilde betrachten. Optimierungen über Modulgrenzen hinweg sind nicht nur erlaubt, sondern erwünscht und kommen häufig vor. Wenn beispielsweise ein Register in Modul X zufällig einem völlig unabhängigen Register in Modul Y entspricht, wird eines der Register entfernt und das verbleibende in beiden Modulen verwendet (sofern dem Synthesizer nicht ausdrücklich untersagt wird, das zu tun).

Sobald die Synthese des HDL abgeschlossen ist, wird die synthetisierte Netzliste mit den Netzlisten der IPs des Designs zusammengeführt (falls vorhanden).

Als Nächstes wird dieser große Haufen Logikelemente über die Logic Fabric des FPGAs verteilt, und die Verbindungen werden so geroutet, dass die Timing-Constraints und andere Ziele erreicht werden. Logik aus verschiedenen Teilen des Designs kann in dieselbe Slice gepackt werden oder in gegenüberliegenden Bereichen des FPGAs landen. Selbst die kleinste Änderung am Design kann eine völlig andere Platzierung bewirken. Das ist chaotisch, aber harmlos, denn jede Implementierung ist unabhängig, und es ist egal, wie die Logik über die Logic Fabric verstreut ist.

Zurück zur partiellen Rekonfiguration: Wie gerade erwähnt, muss es für diese Funktion eine klare Unterscheidung zwischen statischer und rekonfigurierbarer Logik geben. Dazu wird eine Technik namens Hierarchical Design verwendet. Die Idee ist, das gesamte Design als eine Sammlung von Komponenten zu betrachten, ähnlich wie physische Bauteile auf einer Leiterplatte. Das bedeutet zum einen, dass jeder Komponente (also jedem instanziierten Modul) ein bestimmter Bereich in der Logic Fabric zugewiesen wird. Und weil jede Komponente getrennt sein muss, ist es nur folgerichtig, ihre Synthese ebenfalls getrennt durchzuführen – so wie man die Komponente auch separat herstellen würde.

Verbinden wir dieses Konzept nun mit der partiellen Rekonfiguration. Im Wesentlichen ergeben sich zwei Hauptunterschiede in der Arbeit mit dem Design:

Pblocks

Vivado bezeichnet mit Pblock eine Floorplanning-Einheit. Ein Pblock ist letztlich nur ein Informationscontainer in Vivado. Es gibt Tcl-Funktionen, um einen Pblock zu erzeugen, Logikzellen hinzuzufügen und dann Gruppen (Sets) von FPGA-Logik-Sites zuzuweisen. Vivado interpretiert das als Platzierungs-Constraint: Die Logikzellen, die dem Pblock hinzugefügt wurden, dürfen nur auf den Sites platziert werden, die ihm zugewiesen wurden. Letztendlich sind Pblocks also nur weitere Constraints in der XDC-Datei.

Pblocks werden häufig über die Vivado-GUI definiert: Man öffnet das synthetisierte oder implementierte Design und zeichnet rechteckige Bereiche auf der grafischen FPGA-Darstellung. Dadurch entsteht ein Pblock, der alle Logikelemente im gezeichneten Rechteck enthält. Genauer gesagt nicht alle Typen von Logikelementen, sondern nur jene, die für Floorplanning (bei dieser FPGA-Familie) zugelassen sind. Vivado übersetzt das Rechteck also in Bereiche von Logikelementen.

Es ist genauso möglich, diese Bereiche manuell per Bearbeitung der XDC-Datei festzulegen. Auch ist es erlaubt, eine Region aus mehreren Rechtecken zu bilden, sodass die Form komplexer als ein einzelnes Rechteck sein darf. Xilinx' Dokumentation (UG909) empfiehlt jedoch, die Formen möglichst einfach zu halten, um Routing-Probleme zu vermeiden.

Hier ist ein Beispiel für eine XDC-Datei für Kintex-7:

create_pblock pblock_pr_block_ins
add_cells_to_pblock [get_pblocks pblock_pr_block_ins] [get_cells -quiet [list pr_block_ins]]
resize_pblock [get_pblocks pblock_pr_block_ins] -add {SLICE_X118Y0:SLICE_X153Y99 SLICE_X118Y250:SLICE_X145Y349 SLICE_X0Y0:SLICE_X117Y349}
resize_pblock [get_pblocks pblock_pr_block_ins] -add {DSP48_X5Y100:DSP48_X5Y139 DSP48_X5Y0:DSP48_X5Y39 DSP48_X0Y0:DSP48_X4Y139}
resize_pblock [get_pblocks pblock_pr_block_ins] -add {RAMB18_X4Y0:RAMB18_X6Y39 RAMB18_X4Y100:RAMB18_X5Y139 RAMB18_X0Y0:RAMB18_X3Y139}
resize_pblock [get_pblocks pblock_pr_block_ins] -add {RAMB36_X4Y0:RAMB36_X6Y19 RAMB36_X4Y50:RAMB36_X5Y69 RAMB36_X0Y0:RAMB36_X3Y69}

Das Bild unten zeigt, wie das im implementierten Design aussieht. Die rekonfigurierbare Partition (hier pblock_pr_block_ins, die in diesem Beispiel fast keine Logik enthält) ist lila eingezeichnet. Ihre Form ergibt sich als Vereinigung von drei Rechtecken (den drei Bereichen, die in den resize_pblock-Befehlen oben aufgeführt sind).

In dieser Darstellung ist die gesamte platzierte Logik cyanfarben gezeichnet. Die überwiegende Mehrheit dieser Logik gehört zur statischen Region, und man sieht deutlich, wie sie auf eine kleine Fläche beschränkt ist.

Example of device view with Pblock partition

Beachten Sie, dass nur Slices, RAMs und DSP48 durch Constraints eingeschränkt werden. Das sind bei Series-7-FPGAs die einzigen Logiktypen, die durch partielle Rekonfiguration gesteuert werden können. Alles andere – also praktisch alles, was nicht „reine Logik“ ist – muss zum statischen Design gehören.

Bei Ultrascale-FPGAs und neueren kann praktisch jede Logik per partieller Rekonfiguration neu geladen werden.

Es gibt weitere Einschränkungen für Pblocks, aber es hat keinen Sinn, hier Kapitel 6 bis 8 aus UG909 zu wiederholen.

Es ist auf jeden Fall eine gute Idee, ein implementiertes Design zu öffnen, in der FPGA-Ansicht hinein- und herauszuzoomen und zu beobachten, wie die Logikelemente im FPGA angeordnet sind. Dabei fällt insbesondere auf, dass es Spalten gleichartiger Logik gibt. Manchmal befinden sich ein paar Logikelemente in der Mitte dieser Spalten, die die Gleichförmigkeit durchbrechen (vor allem spezielle Logikelemente wie der ICAP-Block, PCIe-Blöcke usw.).

Es ist erwähnenswert, dass das Thema Pblocks nicht nur etwas mit partieller Rekonfiguration zu tun hat. Alles, was in diesem Abschnitt gesagt wurde, gilt beispielsweise auch für die Verwendung von Pblocks beim hierarchischen Design.

Mehr zum Floorplanning

Und hier kommen ein paar kontraintuitive Tatsachen: Auch wenn die grafische Darstellung des Floorplannings aus Formen besteht, die auf der FPGA-Karte gezeichnet werden, gilt sie nur für die Logiktypen, die durch die Platzierungs-Constraints gesteuert werden. Wenn also fast alle Slices des FPGAs für partielle Rekonfiguration freigegeben sind, können Inseln aus anderen Logikelementen, die vollständig von diesen Slices umgeben sind, ohne Weiteres zur statischen Logik gehören. So ist es beispielsweise überhaupt kein Problem, wenn der ICAP-Block selbst in der Mitte eines Rechtecks liegt, das für partielle Rekonfiguration vorgesehen ist.

Das ist gar nicht so ungewöhnlich, wenn man bedenkt, dass der Bitstream für partielle Rekonfiguration auf bestimmte Logikelemente zielt und andere unberührt lässt. Aber wie sieht es mit dem Routing aus? Wenn ein ICAP-Block mitten in rekonfigurierbarer Logik steckt, wie werden die Verbindungen zu Slices der statischen Logik geführt?

Das führt zu der zweiten kontraintuitiven Tatsache: Das Routing für das statische Design nutzt Ressourcen innerhalb der rekonfigurierbaren Region. Dieses Routing bleibt während des gesamten Prozesses der partiellen Rekonfiguration stabil, sonst wäre es nicht statische Logik. Innerhalb der rekonfigurierbaren Region ändert sich also das Routing für die rekonfigurierbare Logik, während das Routing für die statische Logik unverändert bleibt. Wenn es bei diesem Thema etwas gibt, das sich wie Magie anfühlt, dann ist es genau diese Tatsache. Sie ist auch der Grund, warum die Verwendung eines partiellen Bitstroms, der nicht zur statischen Logik kompatibel ist, das FPGA wahrscheinlich komplett durcheinanderbringt.

Umgekehrt gilt das natürlich nicht: Die rekonfigurierbare Logik nutzt keine Ressourcen außer denen, die explizit wie im XDC-Beispiel oben aufgeführt sind. Was Routing-Ressourcen betrifft, ändert sich nichts in der statischen Region, während ein Bitstrom für partielle Rekonfiguration geladen wird. In diesem Sinne beeinflusst die rekonfigurierbare Logik die statische Region nie. Nun ja, fast: Wenn die Form der rekonfigurierbaren Region kein einfaches Rechteck ist, kann Vivado das Routing auch außerhalb der rekonfigurierbaren Region verlaufen lassen. Das passiert nur bei Ultrascale-FPGAs und dient dazu, das Routing zu verbessern.

An diesem Punkt sollte klar sein, dass die Regeln für das Floorplanning nicht einfach sind. Die gute Nachricht ist, dass Vivado bei Verstößen gegen die Floorplanning-Regeln ziemlich aussagekräftige kritische Warnungen (Critical Warnings) ausgibt. Daher ist es durchaus vernünftig, den passenden Floorplan durch Ausprobieren zu finden.

Parent-Implementierungen und Child-Implementierungen

Bei der Implementierung der rekonfigurierbaren Logik ist es wichtig, sich vor Augen zu halten, dass jeder einzelne Pfad (Path) im FPGA die Timing-Constraints erfüllen muss – und das sowohl vor als auch nach dem Laden der rekonfigurierbaren Logik. Deshalb ist eine Implementierung der rekonfigurierbaren Logik getrennt von der statischen Logik nicht möglich. Vielmehr wird die Implementierung für jedes rekonfigurierbare Modul immer über das gesamte FPGA durchgeführt. Die Timing-Constraints (und auch andere Constraints) werden bei jeder Implementierung durchgesetzt.

Um das zu verdeutlichen, kehren wir zu dem obigen Beispiel mit moduleA und moduleB zurück. In diesem Beispiel führt Vivado die Implementierung des vollständigen Designs mit moduleA durch und anschließend dasselbe mit moduleB. Als Nebenprodukt entstehen für beide Varianten reguläre Bitstrom-Dateien.

Das sollte man betonen: Jede Implementierung erzeugt sowohl einen vollständigen initialen Bitstrom als auch einen partiellen Bitstrom. Das gilt für alle Implementierungen, egal ob Parent oder Child. Beim Einschalten des FPGAs kann man es daher mit einem beliebigen dieser initialen Bitströme laden.

Damit der Wechsel von moduleA zu moduleB per partieller Rekonfiguration möglich ist, muss in der statischen Partition alles exakt gleich sein. Dazu gehören die Logik selbst, die Platzierung und das Routing. Um das zu erreichen, führt Vivado die Implementierung für ein Szenario (z.B. mit moduleA) als Parent-Implementierung durch. Für alle anderen Szenarien (z.B. mit moduleB) führt es anschließend Child-Implementierungen aus. Wie das genau funktioniert, wird im letzten Beitrag dieser Serie beschrieben. Um es kurz zu machen:

Vivado beginnt damit, die Parent-Implementierung für moduleA wie beim hierarchischen Design üblich durchzuführen. Die Synthese der statischen und der rekonfigurierbaren Logik erfolgt also getrennt, und die Floorplanning-Constraints erzwingen die Platzierung auf getrennten Sites im FPGA. Abgesehen von diesen beiden Unterschieden wird eine ganz normale Implementierung durchgeführt. Insbesondere werden Platzierung und Verdrahtung für optimale Ergebnisse in genau diesem Szenario durchgeführt (auch wenn die Floorplanning-Constraints und die getrennte Synthese zu suboptimalen Ergebnissen führen können).

Der nächste Schritt ist die Durchführung der Child-Implementierung für moduleB. Eine Synthese des statischen Designs ist nicht nötig, weil sie bereits im Rahmen der Parent-Implementierung erledigt wurde. Es wird also nur die rekonfigurierbare Logik synthetisiert.

Die Implementierung läuft dann genauso ab wie die Parent-Implementierung, mit einem entscheidenden Unterschied: Die Platzierung und Verdrahtung (Place and Route) der gesamten statischen Logik wird zwangsweise identisch zum Ergebnis der Parent-Implementierung gehalten. Unter dieser Einschränkung wird die Platzierung und Verdrahtung der rekonfigurierbaren Logik für optimale Ergebnisse durchgeführt.

Der Schlüssel zum Verhältnis zwischen Parent und Child liegt also darin, dass eine Child-Implementierung dort anknüpft, wo die Parent-Implementierung aufgehört hat, aber die rekonfigurierbare Logik durch ihre eigene Logik ersetzt. Danach läuft die Child-Implementierung wie gewohnt weiter, ohne jedoch irgendetwas im Bereich der statischen Logik anzufassen.

Da sich alle Child-Implementierungen an die Platzierung und das Routing der statischen Logik anpassen müssen, kann es schwieriger sein, die Timing-Constraints zu erfüllen als bei einer regulären Implementierung des Designs. Dabei gibt es zwei Hindernisse:

Das sollte man im Hinterkopf behalten, wenn man entscheidet, welches der rekonfigurierbaren Module für die Parent-Implementierung verwendet wird. Das könnte zum Beispiel das Modul sein, bei dem es am schwierigsten ist, die Timing-Constraints zu erreichen. Oder das Modul, das die anderen Module hinsichtlich der Verbindung zur statischen Logik repräsentiert. Oder auch genau umgekehrt: ein rekonfigurierbares Modul, das praktisch keine Logik enthält (eine „Greybox“), um eine neutrale Implementierung der statischen Logik zu erreichen.

Was die Verwendung der Bitströme betrifft, lautet das Paradigma von Vivado für die Implementierung eines Projekts mit partieller Rekonfiguration: Die Implementierung ist erst dann abgeschlossen, wenn alle Bitströme aktuell und miteinander kompatibel sind. Mit anderen Worten: Jeder der initialen Bitströme aus den Implementierungen kann zum Laden des FPGAs am Anfang verwendet werden. Danach kann jeder partielle Bitstrom aus den Implementierungen geladen werden.

Deshalb startet der erste „Generate Bitstream“ die Parent-Implementierung und alle Child-Implementierungen. Bei späteren Kompilierungen führt Vivado wie üblich nur die Runs aus, die aktualisiert werden müssen.

Der Dynamic Function eXchange Wizard

Dieser Wizard, der über das Menü Tools gestartet werden kann, dient dazu, die Parent-Implementierung und die Child-Implementierungen zu definieren – insbesondere welche Implementierung welches rekonfigurierbare Modul enthält.

Am einfachsten lässt sich der Wizard anhand der Tcl-Befehle erklären, die er beim Hinzufügen einer Child-Implementierung erzeugt:

create_reconfig_module -name bpf -partition_def [get_partition_defs pr ]
add_files -norecurse /path/to/pr_block1.v  -of_objects [get_reconfig_modules bpf]
create_pr_configuration -name config_2 -partitions [list pr_block_ins:bpf ]
create_run child_0_impl_1 -parent_run impl_1 -pr_config config_2 -flow {Vivado Implementation 2020}

Ich gehe diese Tcl-Sequenz rückwärts durch, von der letzten Zeile zur ersten:

In der letzten Zeile wird also ein Run für eine Child-Implementierung erzeugt. Der neue Run erhält den Namen „child_0_impl_1“, und als Parent-Run wird „impl_1“ gewählt. Nicht weniger wichtig: Die Konfiguration dieses neuen Runs wird auf „config_2“ gesetzt.

„config_2“ wird in der dritten Zeile definiert. Dort steht, dass „bpf“ das rekonfigurierbare Modul für die rekonfigurierbare Partition namens „pr_block_ins“ ist. „pr_block_ins“ wurde oben bereits erwähnt – aber was ist „bpf“?

In der ersten Zeile wird ein reconfig_module mit dem Namen „bpf“ erzeugt – das ist einfach ein beliebiger Name, der sich gut eignet, um zu erkennen, was die Logik tut. Die zweite Zeile besagt, dass eine bestimmte Verilog-Datei zu diesem rekonfigurierbaren Modul hinzugefügt wird.

Alles in allem erzeugen diese vier Zeilen also eine neue Child-Implementierung und legen fest, dass die Synthese einer bestimmten Verilog-Datei erforderlich ist, um ein rekonfigurierbares Modul zu erzeugen. Außerdem werden zwei Objekte in der Tcl-Umgebung angelegt: „bpf“ und „config_2“.

Zurück zum Dynamic Function eXchange Wizard: Er ist ein GUI-Werkzeug, das die Beziehungen zwischen Designquellen, rekonfigurierbaren Modulen, Konfigurationen und Implementierungs-Runs darstellt. Es ist einfach eine bequeme Möglichkeit, die Informationen einzugeben, um Tcl-Befehle wie die oben gezeigten zu erzeugen.

Dieses Werkzeug wirkt vielleicht übermäßig kompliziert, aber das liegt daran, dass das Beispiel einfach ist. In einem realistischen Design hat das reconfig_module wahrscheinlich mehrere Quelldateien und möglicherweise zugeordnete IPs. Die GUI macht die Sache dann leichter.

Warum ist die Konfiguration („config_2“) aber nötig? Warum wird die Verbindung zwischen „bpf“ und „pr_block_ins“ nicht direkt im create_run-Befehl hergestellt? Auch das ist eine berechtigte Frage, denn dieser Beitrag beschränkt sich auf eine einzige rekonfigurierbare Partition. Gibt es mehrere solcher Partitionen, legt eine Konfiguration fest, welche Partition welches rekonfigurierbare Modul bekommt. Es ist also sinnvoll, jede solche Kombination zu benennen, z.B. als config_*.

Wenn es mehrere Partitionen gibt: Muss man dann für jede mögliche Kombination rekonfigurierbarer Module eine Implementierung anlegen? Diese Frage ist für diese Beitragsserie nicht relevant, Sie können also gerne zum nächsten Abschnitt springen.

Zur Erinnerung: Vivados Implementierung erfolgt über das gesamte Design – statische und rekonfigurierbare Logik zusammen – und stellt sicher, dass es als Ganzes die Timing-Constraints erfüllt. Gibt es mehrere Partitionen, ist der sichere Weg bei der partiellen Rekonfiguration, alle Partitionen mit ihren partiellen Bitströmen zu laden, sodass alle partiellen Bitströme aus demselben Implementierungs-Run stammen. Anders ausgedrückt: Alle partiellen Bitströme wurden mit derselben Konfiguration (z.B. „config_2“) erzeugt. Die Kombination dieser partiellen Bitströme ist also das Ergebnis einer Implementierung, die von den Tools validiert wurde. Insbesondere ist bekannt, dass diese Implementierung die Timing-Constraints erreicht.

Und doch: Wenn die rekonfigurierbaren Module keinerlei Wechselwirkung haben (d.h. alle Top-Level-Ports der rekonfigurierbaren Module sind mit statischer Logik verbunden und nicht miteinander), kann ich mir nicht vorstellen, was schiefgehen könnte, wenn man jede Partition separat behandelt. Zwar hat Vivado das Timing des FPGAs als Ganzes nicht ausdrücklich freigegeben, wenn partielle Bitströme aus verschiedenen Runs gemischt verwendet werden. Aber da alle Pfade mit der statischen Logik die Timing-Constraints erfüllen und die statische Logik in allen Implementierungs-Runs exakt identisch ist – ist das nicht gut genug? Die offizielle Dokumentation scheint zu diesem Thema keine Angaben zu machen.

Routing und Partition Pins

Es fehlt noch ein Puzzleteil: das Routing zwischen der statischen und der rekonfigurierbaren Logik. Zur Erinnerung: Die Parent-Implementierung führt das Place-and-Route des Designs optimal für die rekonfigurierbare Logik aus, die in der jeweiligen Konfiguration enthalten ist. Das rekonfigurierbare Modul des Child muss aber in dieselbe rekonfigurierbare Partition passen und mit dem statischen Design verbunden werden. Mindestens ein Teil des Routings gehört zur statischen Logik und darf sich daher nicht ändern.

Hier kommen die Partition Pins ins Spiel. Man kann sich die rekonfigurierbare Logik wie eine physische Komponente vorstellen und die Partition Pins wie die Metallpins, die sie mit der Leiterplatte verbinden.

In Wirklichkeit sind Partition Pins aber nur Positionen im Koordinatensystem der Routing-Ressourcen des FPGAs. Sie sind die Stellen, an denen das Routing der statischen Logik endet und das Routing der rekonfigurierbaren Logik weitergeht. Ihre einzige Bedeutung liegt darin, dass Parent-Implementierung und Child-Implementierungen sich darüber einig sind, wo sie liegen.

Für diese Ankerpunkte sind keine physischen Ressourcen wie LUTs oder Flip-Flops erforderlich, und sie tragen keine zusätzliche Routing-Verzögerung bei. Das Routing-Stück zu den Partition Pins und von ihnen weg erzeugt natürlich eine Verzögerung, aber die Partition Pins selbst addieren keine Verzögerung.

Die Positionen der Partition Pins werden während der Parent-Implementierung automatisch von den Tools gewählt; Child-Implementierungen müssen sich dem anpassen. Mit anderen Worten: Das Routing zwischen statischer und rekonfigurierbarer Logik beginnt dort, wo die Parent-Implementierung es vorgegeben hat, und die Child-Implementierung kann innerhalb der rekonfigurierbaren Partition nur ihr Bestes versuchen. Es kann vorkommen, dass einige Partition Pins für die rekonfigurierbare Logik des Child ungünstig platziert sind, was das Erreichen der Timing-Constraints erschweren kann.

Partition Pins werden häufig in der Nähe des Rands der rekonfigurierbaren Partition gruppiert. Vivado scheint darauf ausgelegt zu sein, Stellen zu wählen, die nicht zu sehr auf ein bestimmtes Design spezialisiert sind.

Allerdings können Partition Pins überall innerhalb der rekonfigurierbaren Partition liegen, wenn das während der Parent-Implementierung nötig war, um die Timing-Constraints zu erfüllen. Zur Erinnerung: Das statische Design darf Routing-Ressourcen innerhalb der rekonfigurierbaren Partition nutzen. Es ist also kein Problem, wenn ein Teil des statischen Routings in die rekonfigurierbare Partition hineinreicht.

Um Probleme mit Timing-Constraints und Partition Pins zu vermeiden, ist es vorteilhaft, wenn die Ausgangsports des rekonfigurierbaren Moduls Register sind und auch die Eingänge von Registern abgetastet werden. Ebenso ist es besser, wenn die statische Logik ähnlich vorgeht. Eigentlich ist es immer eine gute Idee, diese Regel zu befolgen, wann immer es möglich ist und das Design nicht verkompliziert.

Die Greybox

Ein weiterer Aspekt des DFX-Wizards ist die Greybox: Im Fenster Edit Configuration kann man der rekonfigurierbaren Partition eine Greybox als rekonfigurierbares Modul zuweisen, statt eines der regulären rekonfigurierbaren Module. Eine Greybox ist ein Platzhalter-Modul, das von Vivado erzeugt wird. Es passt zu den Ports des echten rekonfigurierbaren Moduls, aber anstelle echter Logik gibt es eine LUT für jeden Port-Pin. Die LUTs für Eingänge sind auf der anderen Seite mit nichts verbunden, und die LUTs für Ausgänge erzeugen eine Null. Bei Vektor-Ports wird für jedes Bit des Vektors eine LUT erzeugt.

Es ist vermutlich keine gute Idee, in der Parent-Implementierung eine Greybox zu verwenden, weil sie dem Place-and-Route-Prozess die Sache zu leicht macht. Selbst wenn sich die rekonfigurierbaren Module stark voneinander unterscheiden, ist es wahrscheinlich besser, ein einfaches Modul zu schreiben, das die Tools einigermaßen fordert.

Wenn man jedoch eine initiale Bitstrom-Datei mit minimaler Logik erzeugen möchte, kann eine Child-Implementierung, die nur Greybox-Module enthält, nützlich sein. Zur Erinnerung: Alle Implementierungen erzeugen vollständige Bitströme, die alle als initialer Bitstrom verwendet werden können, da sie alle exakt dieselbe statische Logik enthalten.

Clearing-Bitstreams (nur Ultrascale)

Das betrifft nur Ultrascale-FPGAs (nicht Ultrascale+).

Wie oben erwähnt, erzeugen alle Implementierungen zwei Bitströme: Einen Bitstrom für das gesamte Design, mit dem das FPGA initial geladen werden kann und der das zugehörige rekonfigurierbare Modul enthält. Der zweite Bitstrom ist für die partielle Rekonfiguration mit demselben rekonfigurierbaren Modul bestimmt.

Bei Ultrascale-Bausteinen gibt es einen dritten Bitstrom, den sogenannten Clearing-Bitstream, der bei jeder Implementierung erzeugt wird. Dieser Bitstream muss vor dem partiellen Bitstrom an das FPGA gesendet werden. Beachten Sie, dass der Clearing-Bitstream, der an das FPGA gesendet wird, zu der Logik passen muss, die sich gerade im FPGA befindet – nicht zu dem Bitstream, der als Nächstes geladen werden soll. Man muss also den aktuellen Zustand des FPGAs im Auge behalten, was bei anderen FPGA-Familien nicht nötig ist.

Das Laden des Clearing-Bitstreams schaltet das rekonfigurierbare Modul ab, auch wenn es die Logik nicht wirklich verändert. Die Ausgangsports dieses Moduls können beliebige Werte zeigen, bis ein neuer partieller Bitstrom geladen und gestartet wurde.

Laut UG909 kann das Laden des Clearing-Bitstreams des falschen rekonfigurierbaren Moduls (also nicht des Clearing-Bitstreams, der zu der bereits im FPGA befindlichen Logik passt) auch die statische Logik stören und damit eine Fehlfunktion des Rekonfigurationsmechanismus verursachen.

Xilinx' Dokumentation scheint vage zu sein, was passiert, wenn ein partieller Bitstrom geladen wird, ohne vorher den Clearing-Bitstream zu laden. In Kapitel 9 von UG909 heißt es zunächst: „Vor dem Laden eines partiellen Bitstroms für ein neues rekonfigurierbares Modul muss das vorhandene rekonfigurierbare Modul gelöscht werden.“ Man würde also den Schluss ziehen, dass der Clearing-Bitstream zwingend erforderlich ist.

Aber ein paar Zeilen darunter sagt derselbe Leitfaden: „Wenn keine Clearing-Bitdatei geladen wird, haben die Initialisierungsroutinen (GSR) keine Wirkung.“ Das deutet darauf hin, dass es in Ordnung ist, ganz auf den Clearing-Bitstream zu verzichten, wenn man damit leben kann, dass alle synchronen Elemente (RAMs und Flip-Flops) im Prinzip in einem unbekannten Zustand aufwachen. In meinen eigenen anekdotischen Versuchen, den Clearing-Bitstream wegzulassen, habe ich keine Probleme gesehen – aber das beweist nichts.

Bei Ultrascale-FPGAs muss man also auf jeden Fall nachverfolgen, was im FPGA geladen ist. Das kann man zum Beispiel tun, indem man dem rekonfigurierbaren Modul einen Ausgangsport hinzufügt, der einen konstanten Wert (einen ID-Code) führt, der für jedes rekonfigurierbare Modul unterschiedlich ist. So kann die statische Logik erkennen, welches rekonfigurierbare Modul gerade geladen ist. Ein solcher ID-Code ist unabhängig davon oft eine gute Idee.

Plugin- vs. Remote-Update-Einsatzszenarien

Die Parent-Child-Methodik, die Vivado übernommen hat, ist offenbar für ein bestimmtes Einsatzszenario gedacht, das ich Plugin-Nutzung nennen möchte: Man senkt die FPGA-Kosten, indem man nur das gerade benötigte rekonfigurierbare Modul lädt, statt alle möglichen Funktionen dauerhaft im FPGA zu halten. Wenn das FPGA z.B. mehrere Bildfilter implementiert, erlaubt die partielle Rekonfiguration, jeden Filter als rekonfigurierbares Modul zu implementieren und das FPGA nur mit dem jeweils benötigten Filter neu zu laden.

Xilinx verwendet den Begriff „Dynamic Function eXchange“ (DFX) für partielle Konfiguration. Das scheint den primär beabsichtigten Verwendungszweck dieser Technik widerzuspiegeln.

Die Parent-Child-Methode funktioniert gut, wenn das der Zweck der partiellen Rekonfiguration ist. Es wird ein komplettes Paket an Bitstrom-Dateien erzeugt. Jeder der initialen Bitströme kann zum Initialisieren des FPGAs verwendet werden, und alle rekonfigurierbaren Bitströme lassen sich später für die partielle Rekonfiguration einsetzen. Wenn eine neue Version des Projekts erscheint, wird das gesamte Paket, das aus allen Bitfiles besteht, ersetzt.

Es gibt aber noch ein anderes Nutzungsmuster, das ich als Remote Update bezeichnen möchte. Dabei wird die partielle Rekonfiguration als Mittel für Versions-Upgrades verwendet – möglicherweise in ferner Zukunft. In diesem Szenario wird der initiale Bitstrom zu einem bestimmten Zeitpunkt freigegeben und kann danach nicht mehr geändert werden. Später werden partielle Bitströme freigegeben, die mit dem initialen Bitstrom kompatibel sein müssen. Diese späteren Freigaben können sich über mehrere Jahre erstrecken.

Für Remote Update ist die Parent-Child-Methode unter Umständen nur schwer unverändert nutzbar. Zwar ist es möglich, eine Child-Implementierung auszuführen, um einen neuen partiellen Bitstrom zu erhalten, ohne die Parent-Implementierung zu wiederholen. Aber über lange Zeit kann das schwierig werden. Wenn zum Beispiel versehentlich eine Änderung am Quellcode der statischen Logik gemacht wird, wird das Parent-Design ungültig und eine erneute Parent-Implementierung ist nötig. Die neue statische Logik ist dann mit der bisherigen nicht kompatibel, und die darauf basierenden partiellen Bitströme können nicht mit dem ursprünglichen initialen Bitstrom verwendet werden.

Wenn partielle Rekonfiguration also als Methode gedacht ist, das FPGA-Design im Laufe der Zeit fortlaufend zu aktualisieren, muss man das Implementierungsverfahren etwas anpassen. Dieses Thema wird im letzten Beitrag dieser Serie behandelt.

Bitströme komprimieren

Das hängt nicht direkt mit partieller Rekonfiguration zusammen, aber manchmal möchte man eine möglichst kleine initiale Bitstrom-Datei haben, insbesondere für einen schnellen Start des FPGAs. In diesem Zusammenhang wird partielle Rekonfiguration zu einem Mittel, um den Hochfahrvorgang (Bringup) nach einem schnellen Start abzuschließen. Das kann von derselben Datenquelle (z.B. dem SPI-Flash) oder von einer völlig anderen (z.B. einer PCIe-Schnittstelle) erfolgen.

Das Komprimieren des Bitstroms ist sowohl für den initialen Bitstrom als auch für die partiellen Bitströme zulässig.

Das ist die Zeile, die man in die XDC-Datei einfügt, um einen komprimierten Bitstrom anzufordern:

set_property bitstream.general.compress true [current_design]

Damit ist der theoretische Teil abgeschlossen. Der nächste Beitrag zeigt die praktischen Schritte, um ein Projekt für partielle Rekonfiguration einzurichten.

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