01signal.com

Resets auf FPGAs: Synchron, asynchron oder gar nicht?

Dies ist die zweite Seite einer Serie über Resets in FPGAs. Nachdem auf der vorherigen Seite erklärt wurde, warum asynchrone Resets nicht das sind, was viele glauben, behandelt diese Seite die verschiedenen Optionen für Resets und die Initialisierung des FPGA.

Zuallererst: Was ist ein Reset?

Also, kommen wir zur Sache: Wir alle wissen, was ein Reset ist! Er ist wie dieser Knopf am Computer, den man drückt, und alles startet von Neuem. Es ist das Signal, das in einen Chip geht und dafür sorgt, dass – egal was vorher passiert ist – ab jetzt alles gut ist. Manche Leute bezeichnen den Reset als das Signal, das das System in einen bekannten Zustand bringt.

Für FPGA-Entwickler ist der Reset oft nur ein zusätzlicher Eingang, den man jedem Modul hinzufügt und in einem sich wiederholenden Codemuster verwendet. Etwas, das man einfach tut, ohne unbedingt darauf zu achten, was dieses Reset-Signal tatsächlich sicherstellt – wenn überhaupt – oder ob es ganz weggelassen werden könnte.

Der häufigste Irrtum besteht darin, sich auf die Tatsache zu konzentrieren, dass das Reset-Signal das System in einen bekannten Zustand bringt. Das stimmt natürlich, aber der wichtige Teil ist das, was nach der Deaktivierung des Resets passiert. Es muss sichergestellt sein, dass die Logik in vorhersehbarer Weise zu arbeiten beginnt – oder zumindest vorhersehbar genug für ein korrektes Funktionieren. Es nützt nichts, ein System zurückzusetzen, wenn man sich über sein Verhalten nach der Deaktivierung des Resets nicht sicher ist.

Was dieses Thema besonders schwierig macht, ist, dass auch Glück im Spiel ist: Allgemein gesprochen ist der Zustand des Systems beim Aktivieren des Resets unbekannt und zufällig, ebenso wie der Zeitpunkt der Reset-Deaktivierung. Eine unsachgemäße Behandlung des Reset-Signals kann daher zu seltenen Störungen führen, die zu zufälligen Gelegenheiten auftreten und wie ein ganz anderes Problem wirken. Ebenso ist es möglich, das Problem ohne sichtbare Konsequenzen zu vernachlässigen – abgesehen von gelegentlichen Problemen, die üblicherweise mit Hexerei behandelt werden.

Genau wie die richtigen Timing-Constraints (timing constraints), der ordnungsgemäße Umgang mit Takten und Taktdomänen (clock domains) und ein korrektes Verhalten beim Aufwachen ist auch eine saubere Behandlung von Reset und Initialisierung notwendig, damit das FPGA zuverlässig arbeitet. Allen diesen Themen ist gemeinsam, dass man sie irgendwie vernachlässigen kann – und so mancher Ingenieur tut das auch –, aber leider um den Preis eines FPGA, das sich ab und zu benimmt, als wäre es verhext.

Simulation vs. Hardware

Diese Seite konzentriert sich darauf, wie Entscheidungen über Resets das Design beeinflussen, wenn es in das FPGA geladen wird. Natürlich haben diese Entscheidungen aber auch Auswirkungen auf die Simulation der Logik.

Es ist wichtig, zwischen diesen beiden Situationen zu unterscheiden. Simulatoren weisen insbesondere in der Verhaltenssimulation allen Registern den Anfangswert X (unbekannt) zu. Diese X-Werte pflanzen sich dann auf alle Register fort, die in ihrer Logikfunktion von einem X-Wert abhängen. So kann es passieren, dass schon ein einziges Register mit einem X-Wert das gesamte Design mit X’en überschwemmt und die Simulation unbrauchbar wird.

Eine häufige falsche Lösung für dieses Problem besteht darin, alle Register im Design mit einem asynchronen Reset (asynchronous reset) zu versehen, um alle X’e loszuwerden. Üblicherweise wird der Reset zu Beginn der Simulation kurz aktiviert. Dadurch bekommen alle Register einen bekannten Wert, und alles sieht perfekt aus. Leider erweckt diese falsche Lösung oft die Illusion eines sauberen Neustarts, indem sie die Probleme versteckt, die auf der vorherigen Seite besprochen wurden.

Selbst wenn die Resets korrekt verwendet werden, ist es trotzdem eine bequeme Lösung, alle Register zurückzusetzen, anstatt der Quelle der X-Werte in der Simulation nachzugehen. Das verschwendet nicht nur Ressourcen und kann es erschweren, die Timing-Constraints zu erfüllen: Überflüssiges Zurücksetzen von Registern kann außerdem Fehler verstecken, denn eine Flut von X-Werten kann von einer unbeabsichtigten Abhängigkeit zwischen Registern herrühren. Diese X-Flut kann also eine Warnung sein, dass mit dem Design etwas nicht stimmt.

Im Vergleich zur Simulation ist die Hardware deutlich nachsichtiger, was das Weglassen von Resets angeht. Wenn Resets jedoch falsch verwendet werden – oder wenn sie nötig wären, aber gar nicht verwendet werden –, kann sich die Hardware durchaus unerwartet verhalten.

Zusammenfassend sollten Resets mit Blick auf die Hardware verwendet werden und nicht dazu dienen, lästige X-Werte in der Simulation loszuwerden. Und da der Fokus auf der Hardware liegen sollte, sind diese wenigen Worte alles, was ich zu Simulationen zu sagen habe.

Strategien für das Zurücksetzen

Die Entscheidung, ob und wie Register zurückgesetzt werden sollen, erfordert, den Fall jedes Registers getrennt zu betrachten. Das vermeidet nicht nur einen unnötig hohen Fan-out (fan-out) der Reset-Signale, sondern ist auch eine gute Gelegenheit, darüber nachzudenken, ob die Logik nach dem Reset garantiert korrekt startet, unabhängig von ihrem vorherigen Zustand.

Im Prinzip gibt es vier Möglichkeiten:

Meine Meinung

Jede dieser Optionen wird unten besprochen. Ich stelle daher zunächst dar, was ich für den richtigen Weg halte, und gehe dann ins Detail:

Diese Vorschläge entsprechen im Großen und Ganzen dem, was FPGA-Anbieter heutzutage empfehlen.

Ich möchte noch eine sehr allgemeine Richtlinie hinzufügen: Setzen Sie Steuerlogik immer explizit zurück, und lassen Sie Datenpfade ihre anfänglichen Junk-Daten einfach durchlaufen.

Und nun die ausführliche Diskussion der einzelnen Optionen.

Option #1: Unbekannter Anfangswert

Manche Register brauchen weder einen Reset noch einen Anfangswert. Das gilt insbesondere für Schieberegister und ähnliche Logikelemente. Allgemein gesprochen passen Datenpfade gut zu dieser Option.

Betrachten Sie diesen Codeausschnitt:

reg [31:0] d0, d1, d2, d3, d4;

always @(posedge clk)
  begin
    d4 <= d3;
    d3 <= d2;
    d2 <= d1;
    d1 <= d0;
    d0 <= orig_data;
  end

Das sind eindeutig fünf Verzögerungsregister mit je 32 Bit. Auf manchen FPGAs (insbesondere Xilinx) erkennt der Synthesizer das als Schieberegister, wenn nur der letzte Wert (@d4) verwendet wird und keines dieser Register einen Reset hat. Das kann den Logikverbrauch erheblich reduzieren.

Natürlich muss die Logik, die mit dem Ausgang dieser Verzögerungsregister verbunden ist, in der Lage sein, anfängliche Zufallsdaten zu tolerieren. Ein typischer Fall, in dem das kein Problem ist, ist, wenn es ein anderes Register oder eine Zustandsmaschine gibt, das ordnungsgemäß zurückgesetzt wird und dafür sorgt, dass nicht initialisierte Werte ignoriert werden, wenn sie ankommen. Wenn diese Verzögerungsleitungen beispielsweise zu einer Pipeline gehören, ignoriert die Steuerlogik der Pipeline ungültige Daten natürlich von selbst.

Allgemeiner gesagt: Die Gelegenheit, ein Register weder zurückzusetzen noch zu initialisieren, erkennt man leicht, wenn ein begleitendes Flag oder ein Zustand seine Gültigkeit anzeigt. Oder wenn es eine klare Reihenfolge gibt, bei der das Register zuerst mit einem Wert belegt und dieser Wert dann konsumiert wird. Kurz: wenn man leicht erkennen kann, dass der Wert des Registers ignoriert wird, bis ihm ein vernünftiger Wert zugewiesen wurde.

Eine andere Art des unbekannten Anfangswerts liegt vor, wenn er vom Synthesizer abhängen kann. Zum Beispiel:

reg val;

always @(posedge clk)
  val <= 1;

Der Synthesizer kann entscheiden, dass @val ein Draht mit konstantem Wert 1 ist. Eine andere Möglichkeit besteht darin, ein Register mit Null als Anfangswert zu erzeugen, damit es beim ersten Takt auf 1 wechselt. Was tatsächlich passiert, hängt vom Synthesizer ab. Obwohl der Wert von @val also immer bekannt ist – außer im ersten Taktzyklus –, sollte der Anfangswert als unbekannt betrachtet werden.

Option #2: Anfangswerte im FPGA

Der Anfangswert der grundlegenden synchronen Elemente in einem FPGA (üblicherweise Flip-Flops, Schieberegister und Speicher) steht im Konfigurations-Bitstrom (bitstream). Die bekannte Nutzung dieser Eigenschaft besteht darin, ein ROM zu erzeugen, indem man einem Block-RAM Anfangswerte zuweist und niemals hineinschreibt.

Ein weiterer bekannter Aspekt dieser Eigenschaft ist, dass ein FPGA nach der Konfiguration typischerweise aufwacht und scheinbar alle Register den Wert Null haben. Das liegt daran, dass der Synthesizer normalerweise allen Registern den Anfangswert Null zuweist – aber es kann Überraschungen geben, wenn der Anfangswert nicht explizit gesetzt wird.

Einige synchrone Elemente – insbesondere Schieberegister und dedizierte RAM-Blöcke – können erzeugt werden, indem ihr Verhalten in Verilog oder VHDL beschrieben wird (durch Inferenz): Der Synthesizer erzeugt normalerweise ein Schieberegister, wenn der Code wie eine Verzögerungsleitung aussieht. Ebenso entsteht ein RAM-Logikelement als Reaktion auf ein Array. Wenn jedoch ein Reset (synchroner Reset oder asynchroner Reset) auf diese Register verwendet wird, hindert das den Synthesizer daran, die Logikressourcen auf diese Weise zu nutzen. Der Grund ist, dass weder Schieberegister noch RAMs einen Reset-Eingang haben, der die Werte des internen Speichers setzt.

Wie werden die Anfangswerte also gesetzt? Während des Konfigurationsprozesses erhalten alle synchronen Elemente ihre Anfangswerte – und zwar unmittelbar bevor das FPGA aktiv wird, also bevor die synchronen Elemente beginnen, auf ihre Takte und asynchronen Reset-Eingänge zu reagieren. Bei Xilinx-Bausteinen wird das über das Global Set Reset (GSR)-Signal umgesetzt, das alle synchronen Elemente in ihren Anfangszustand versetzt. Danach wird Global Write Enable (GWE) aktiviert, wodurch die synchronen Elemente normal arbeiten.

Da der Konfigurationsprozess unabhängig von allen Takten abläuft, die von der Anwendungslogik des FPGA verwendet werden, ist der Übergang des FPGA in seinen Betriebszustand asynchron zu jedem dieser Takte. Folglich verhalten sich die synchronen Elemente genauso, als ob ein asynchroner Reset unabhängig von jedem Takt deaktiviert würde. Mit anderen Worten: Es ist möglich, dass einige synchrone Elemente auf die erste Taktflanke reagieren, die nach dem Übergang in den Betriebszustand kommt, während andere synchrone Elemente aufgrund einer Timing-Verletzung nicht auf diese Taktflanke reagieren. Das kann üble Fehler verursachen, wie auf der ersten Seite dieser Serie besprochen.

Wichtig ist auch, dass die Takte nicht unbedingt stabil sind, wenn das FPGA aufwacht. Wenn sie von den PLLs des FPGA selbst erzeugt werden, können sie die Timing-Constraints massiv verletzen. Wie oben erörtert, ist das kein Problem, wenn der Takt ignoriert wird, bis er stabil ist (z. B. durch eine Taktfreigabe (clock enable)), oder wenn bekannt ist, dass er beim Aufwachen des FPGA stabil ist. Es ist auch in Ordnung, wenn das Design sicherstellt, dass kein synchrones Element einen Grund hat, seinen Wert zu ändern, bis der Takt stabil ist. Andernfalls ist das Setzen eines Anfangswerts keine große Hilfe.

Trotz der Einschränkungen beim Setzen des Anfangswerts reicht das in vielen Szenarien aus, und ein expliziter Reset ist nicht erforderlich. In manchen Fällen hat man schlicht keine Wahl, weil kein Reset-Signal verfügbar ist. Das gilt zum Beispiel für die Logik, die die Reset-Signale des FPGA unmittelbar nach dem Aufwachen erzeugt. Ein Beispiel für solche Logik wird auf der dritten Seite dieser Serie gezeigt.

Bei den meisten Synthesizern ist das Setzen des Anfangswerts eines Registers ganz einfach: Verwenden Sie Verilogs „initial“:

reg [15:0] counter;
initial counter = 1000;

Es mag überraschen, dass „initial“ in synthetisierbarem Verilog-Code verwendet werden kann, aber wie sich herausstellt, ist diese Verwendung weithin unterstützt. Dieses Schlüsselwort ist also definitiv die bevorzugte Methode, wenn der Synthesizer es unterstützt (mit anderen Worten, wenn diese Verwendung von „initial“ in der Dokumentation ausdrücklich erwähnt wird). Außerdem ist die alternative Methode oft herstellerspezifisch und manchmal sogar für eine bestimmte FPGA-Familie spezifisch. Selbst wenn „initial“ also nicht immer portabel ist, ist es wahrscheinlich immer noch die portabelste Wahl.

Die alternative Methode zum Setzen des Anfangswerts hängt vom verwendeten FPGA ab. Diese Methode besteht üblicherweise aus einer Instanziierung (instantiation) des synchronen Elements als Grundbaustein (primitive) und der Zuweisung des Anfangswerts als Instanziierungsparameter. Zum Beispiel ein Flip-Flop von Xilinx:

FDCE myflipflop (
  .C(clk),
  .D(in),
  .Q(out),
  .CLR(1'b0),
  .CE(1'b1)
);

defparam myflipflop.INIT = 1;

„initial“ ist doch schöner, nicht wahr?

Option #3: Asynchroner Reset

Wenn Sie die Seite darüber, warum asynchrone Resets oft falsch verwendet werden, noch nicht gelesen haben, empfehle ich, das zuerst zu tun. Es sei denn, Sie haben ohnehin nicht vor, diese Art von Reset zu verwenden.

Aus irgendeinem Grund halten viele Leute den asynchronen Reset (asynchronous reset) für die richtige Lösung für alle Zwecke. Vielleicht weil er oft in Codebeispielen auftaucht, vielleicht wegen der Illusion eines einfachen Weges zu einem globalen Reset, der alle synchronen Elemente erreicht. Vielleicht weil im alten ASIC-Umfeld der asynchrone Reset für den Chiptest während der Fertigung nützlich war, weil man damit den gesamten Chip zurücksetzen und mit der Anwendung von Testvektoren beginnen kann.

Also zur Realität: Der richtige und saubere Weg, einen asynchronen Reset zu verwenden, besteht darin, dies bei abgeschalteten Takten zu tun. Das bedeutet der Teil „asynchron“ wirklich. In einem praktischen Design besteht das aus den folgenden Schritten:

Diese Abfolge ist nicht schwer zu implementieren, aber es kann schwieriger sein, sicherzustellen, dass die erste Flanke jedes Takts sauber geformt ist, sodass keine Glitches entstehen: Ein häufiges Problem mit Taktpuffern ist, dass eine Timing-Anforderung zwischen der Aktivierung der Ausgangsfreigabe des Taktpuffers und der ersten Taktflanke, die durch den Taktpuffer laufen soll, besteht. Wenn diese Timing-Anforderung verletzt wird, kann der Taktpuffer einen Glitch ausgeben (einen kurzen Puls, der die Anforderungen des FPGA an den Takt verletzt). Das kann zu unvorhersehbarem Verhalten aller synchronen Elemente führen, die von diesem Takt abhängen.

Leider bieten die von den FPGA-Anbietern bereitgestellten Dokumentationen nicht immer eine Erklärung, wie man diese Timing-Anforderung sicherstellt. Dadurch ist es möglicherweise nicht möglich zu gewährleisten, dass sich die erste Taktflanke nach dem Reset korrekt verhält. Und ohne Garantie für die erste Taktflanke ist der Reset zwecklos.

Wenn Sie diese Methode verwenden, stellen Sie sicher, dass auf den Pfaden (paths) im Zusammenhang mit dem asynchronen Reset keine Timing-Constraints erzwungen werden. Eine solche Überwachung ist in diesem Fall unnötig und kann möglicherweise standardmäßig aktiviert sein.

Es gibt eine alternative Methode, einen asynchronen Reset zuverlässig anzuwenden, die häufig empfohlen wird. Diese Methode verwendet kein Takt-Gating und verlässt sich daher nicht auf die Taktpuffer: Die Idee ist, ein paar Flip-Flops hinzuzufügen, die die Aktivierung des asynchronen Reset-Signals direkt durchlassen, das Reset aber synchron deaktivieren. Mit anderen Worten: ein synchronisierter asynchroner Reset.

Wenn das ursprüngliche asynchrone Reset zum Beispiel @external_resetn ist, erzeugt das Folgendes einen solchen Reset:

   reg pre_rstn1, pre_rstn2;
   reg resetn;

   always @(posedge clk or negedge external_resetn)
     if (!external_resetn)
       begin
         resetn <= 0;
         pre_rstn2 <= 0;
         pre_rstn1 <= 0;
       end
     else
       begin
         resetn <= pre_rstn2;
         pre_rstn2 <= pre_rstn1;
         pre_rstn1 <= 1;
       end

Beachten Sie, dass @clk der Takt ist, der von den synchronen Elementen verwendet wird, die durch @resetn zurückgesetzt werden.

Wenn @external_resetn aktiv (also low) ist, werden alle drei Register asynchron aktiv (also null). Wenn aber @external_resetn deaktiviert wird, wird nur @pre_rstn1 bei der nächsten Taktflanke inaktiv, und das pflanzt sich bei den folgenden Takten auf @pre_rstn2 und @resetn fort.

Der Zweck der zwei zusätzlichen Register ist der Schutz vor Metastabilität (metastability), damit @resetn auf sichere Weise inaktiv wird. Das ist nötig, falls @external_resetn zu einem ungünstigen Zeitpunkt relativ zu @clk inaktiv wird, was zu einem metastabilen Zustand an @pre_rstn1 führen kann (diese Seite erklärt Metastabilität).

Der Vorteil dieses Synchronisierers ist, dass @external_resetn als asynchroner Reset verwendet werden kann: Er funktioniert auch, wenn kein Takt aktiv ist. Trotzdem erhalten die synchronen Elemente ein Reset-Signal, das synchron inaktiv wird, sodass das Timing sichergestellt werden kann.

Es ist fast überflüssig zu erwähnen, dass jeder Takt seinen eigenen synchronisierten asynchronen Reset braucht.

Wichtig ist, dass die Erzeugung von @resetn wie oben gezeigt nicht ausreicht. Die Timing-Constraints für @clk müssen auch für die Pfade von @resetn zu den synchronen Elementen gelten. Die Voreinstellung einiger FPGA-Tools ist es, das Timing von Pfaden zu ignorieren, die am asynchronen Reset-Eingang eines synchronen Elements enden. Möglicherweise muss dafür eine Einstellung im Tool geändert werden.

Wenn @resetn also als ganz normaler asynchroner Reset verwendet wird, z. B.

  always @(posedge clk or negedge resetn)
    if (!resetn) // Are you sure this path is timed?
      the_register <= 0;
    else
      [ ... ]

dann ist der oben gezeigte Synchronisierer nicht genug, um eine zuverlässige Erholung vom Reset zu gewährleisten. Es liegt in Ihrer Verantwortung zu prüfen, ob die Pfade, die an @resetn beginnen und an den asynchronen Reset-Eingängen der Flip-Flops enden, tatsächlich zeitlich überwacht werden.

Es ist auch wichtig zu wissen, dass dieser Synchronisierer nicht gegen Glitches auf @external_resetn hilft: Wenn der aktive Puls von @external_resetn kürzer ist als die Spezifikation der Flip-Flops im FPGA, kann alles Mögliche passieren. @external_resetn muss also von einer Logik oder externen Elektronik erzeugt werden, die einen langen Puls sicherstellt. Wenn das nicht möglich ist, besteht die einzige Lösung darin, den Reset vollständig zu synchronisieren, wie in diesem Beispiel für @sync_resetn:

  reg pre_rstn1, pre_rstn2;
   reg sync_resetn;

   always @(posedge clk)
     if (!external_resetn)
       begin
         sync_resetn <= 0;
         pre_rstn2 <= 0;
         pre_rstn1 <= 0;
       end
     else
       begin
         sync_resetn <= pre_rstn2;
         pre_rstn2 <= pre_rstn1;
         pre_rstn1 <= 1;
       end

Aber dieser Synchronisierer ignoriert @external_resetn, wenn @clk inaktiv ist. Das ist problematisch, wenn die Logik @external_resetn wie einen asynchronen Reset behandeln soll. Mit anderen Worten: wenn es auch dann funktionieren soll, wenn die Takte nicht aktiv sind.

Gehen wir zurück zum ersten Synchronisierer. Was ist, wenn man @resetn als ganz normalen synchronen Reset verwendet? Etwa so:

  always @(posedge clk) // @resetn not in sensitivity list!
    if (!resetn)
      the_register <= 0;
    else
      [ ... ]

Das ist mehr oder weniger in Ordnung, weil die Deaktivierung von @resetn aufgrund der Timing-Constraints sicher zeitlich überwacht ist. Die asynchrone Aktivierung von @resetn ist dagegen nicht zeitlich überwacht, sodass sich die betroffenen synchronen Elemente kurz vor dem Wirksamwerden des Resets zufällig verhalten können. Für diesen Zweck ist es besser, einen vollständig synchronisierten Reset zu verwenden, z. B. das oben definierte @sync_resetn.

Ich schließe dieses Thema mit einer allgemeinen Anmerkung ab: Ich habe in den obigen Beispielen Resets mit aktivem Low gewählt, vor allem wegen einer Tradition, die aus der Zeit stammt, als das Reset-Signal mit einem Kondensator erzeugt wurde, der über einen Widerstand mit der Versorgungsspannung verbunden war. Da der Kondensator anfangs keine Spannung hatte, war der Reset-Eingang auf „0“. Dieser Kondensator lud sich ziemlich schnell auf, und folglich wechselte der Reset-Eingang auf „1“. Diese uralte Art von Einschaltreset ist der Grund, warum viele Resets bis heute aktiv Low sind.

Zusammenfassend lässt sich sagen, dass es möglich ist, einen asynchronen Reset zuverlässig zu verwenden – aber das zu erreichen, ist definitiv nicht so einfach, wie viele glauben. Ich habe zwei Wege vorgestellt, einen zuverlässigen asynchronen Reset sicherzustellen: Entweder schalten Sie die Takte vorübergehend ab, um Timing-Probleme zu vermeiden, oder Sie verwenden einen Synchronisierer, um das Timing sicherzustellen. Wie so oft bei FPGAs dreht sich alles um das Timing.

In den meisten realen Designs, die sich auf einen asynchronen Reset verlassen, wird keine dieser Methoden verwendet. Dadurch hängt die Zuverlässigkeit des FPGA-Designs vom reinen Glück ab.

Option #4: Synchroner Reset

Der synchrone Reset (synchronous reset) ist vor allem für das folgende Codemuster bekannt:

  always @(posedge clk)
    if (reset)
      the_register <= 0;
    else
      [ ... ]

Ich werde weiter unten ein Codemuster vorschlagen, das ich für besser halte, aber zunächst bleiben wir bei diesem. Beachten Sie jedoch, dass ich hier einen aktiven High-Reset gewählt habe, wie es bei synchronen Resets üblicher ist. Zumindest ist das mein Eindruck.

Ein synchroner Reset ist dem asynchronen Reset in fast allen Aspekten überlegen, außer in diesen:

Da FPGAs selten ohne aktiven Takt betrieben werden (anders als ASICs, für die das traditionell zum Testen nötig ist) und unklar ist, ob das Problem mit den dedizierten Ressourcen überhaupt existiert, konzentriere ich mich auf das Hauptthema: den Fan-out. Glücklicherweise ist das leicht zu lösen.

Es ist auch erwähnenswert, dass dasselbe Fan-out-Problem den asynchronen Reset gleichermaßen betrifft, wenn der oben vorgeschlagene Synchronisierer verwendet wird. Die einzigen, die also wirklich behaupten können, dass der Fan-out ein Nachteil des synchronen Resets ist, sind diejenigen, die den asynchronen Reset bei abgeschalteten (also gegateten) Takten verwenden.

Das Erste, was einem zur Lösung eines Fan-out-Problems in den Sinn kommt, ist eine Synthesizer-Constraint oder ein Attribut zur Begrenzung des Fan-out. Das ist jedoch die weniger bevorzugte Methode, weil der Synthesizer das Flip-Flop dann einfach dupliziert, wenn der Grenzwert erreicht ist. Daher passiert es oft, dass die Ausgänge der duplizierten Flip-Flops zu Modulen mit völlig unterschiedlichen Zwecken gehen, sodass die Ziele dieser Ausgänge über das gesamte FPGA verstreut sein können. Das führt zu langen Verdrahtungswegen und einer erheblichen Ausbreitungsverzögerung (propagation delay).

Eine einfache und effiziente Lösung ist die Erzeugung eines lokalen Resets für jeden größeren Teil der Logik. Etwa so:

module medium_sized_module (
  input clk,
  input reset,
  input [15:0] in_data,
  output [15:0] out_data
);

  (* dont_touch = "true" *) reg local_reset;
  reg the_register;

  always @(posedge clk)
    local_reset <= reset;

  always @(posedge clk)
    if (local_reset)
      the_register <= 0;
    else
      [ ... ]

Die Idee ist, dass @local_reset eine lokale Kopie von @reset ist (um einen Takt verzögert). Wenn man ab diesem Modul abwärts @local_reset statt @reset verwendet, bleibt der Fan-out auf einem vernünftigen Niveau. Da die Konsumenten dieses lokalen Resets ohnehin eng miteinander verbunden sein dürften, werden sie mit hoher Wahrscheinlichkeit in einer bestimmten Region des FPGA platziert. Der lokale Reset muss also keine langen Wege durch die Logikstruktur zurücklegen.

Es ist wichtig, den Synthesizer daran zu hindern, die lokalen Reset-Register zu Optimierungszwecken zu entfernen. Das macht der Synthesizer normalerweise, wenn es Register mit identischem Verhalten gibt, selbst wenn sie zu verschiedenen Modulen gehören. Im obigen Beispiel ist das Synthese-Attribut von Vivado gezeigt, nämlich „dont_touch“. Jeder Synthesizer hat seine eigene Methode dafür (bei Quartus wird das mit dem Synthese-Attribut „dont_merge“ erreicht).

Um zu prüfen, ob der Synthesizer alle Register tatsächlich behalten hat, ist es nützlich, allen diesen Registern denselben Namen zu geben (z. B. local_reset, wie oben vorgeschlagen) und dann im implementierten Design nach Registern mit diesem Namen zu suchen.

Natürlich muss nicht für jedes einzelne Modul ein lokales Reset erzeugt werden. Als grober Richtwert ist ein Fan-out von 50–100 für ein lokales Reset vernünftig, insbesondere wenn es Logikelemente in einem kleinen physischen Bereich auf dem FPGA erreicht.

Das Thema der Fan-out-Reduktion wird auch im Zusammenhang mit Timing-Closure (timing closure) besprochen.

Mehr zu synchronen Resets

Ein verbreiteter Mythos über synchrone Resets ist, dass der Synthesizer, wenn er auf ein Verilog-Codemuster stößt, das zu einem synchronen Reset passt, das Reset-Signal mit dem synchronen Reset-Eingang des Flip-Flops verbindet. Das kann der Fall sein, aber oft ist es nicht so.

Das ist anders als bei einem asynchronen Reset, der mit dem asynchronen Reset-Eingang des Flip-Flops verbunden sein muss. Andernfalls funktioniert der Reset ohne Takt nicht.

Synthesizer neigen dazu, dem Codemuster keine besondere Bedeutung beizumessen, sondern eher die logische Gleichung zu berechnen, die sich aus dem Verilog-Code ergibt. Betrachten Sie dieses Beispiel:

  always @(posedge clk)
    if (reset)
      the_register <= 0;
    else if (some_condition)
      the_register <= !the_register;
    else if (some_other_condition)
      the_register <= 0;

Man könnte den Code so lesen, dass die always-Anweisung mit einem Standard-Codemuster beginnt, das einen synchronen Reset verlangt, und danach eine spezifische Definition für das Verhalten des Registers kommt. Folglich könnte man erwarten, dass @reset mit dem synchronen Reset-Eingang des betreffenden Flip-Flops verbunden würde und dass der Ausgang einer Logikfunktion (die als LUT implementiert ist) zum Dateneingang des Flip-Flops geht.

In Wirklichkeit implementieren Synthesizer den Wert von @the_register beim nächsten Takt normalerweise so kompakt wie möglich. Zum Beispiel kann der Reset-Eingang des Flip-Flops mit einer Logikfunktion (also einer LUT) verbunden sein, die den Ausdruck (reset || (some_other_condition && !some_condition) ) implementiert.

Aber es gibt eine noch interessantere Möglichkeit: Der Reset-Eingang des Flip-Flops wird möglicherweise überhaupt nicht verwendet. Stattdessen wird nur der Dateneingang verwendet, und die Logikfunktion verwendet das @reset-Signal als einen ihrer Eingänge. Wenn @reset also high ist, ist der Ausgang der Logikfunktion null. Auf diese Weise bringt @reset tatsächlich @the_register auf null, aber es wird nicht anders behandelt als jedes andere Signal.

Um es zu wiederholen: Obwohl das Flip-Flop einen synchronen Reset-Eingang hat, neigen Synthesizer dazu, das Codemuster für den synchronen Reset nicht besonders zu behandeln, und sie behandeln das Reset-Signal auch nicht anders als jedes andere Signal. Der Reset-Eingang des Flip-Flops wird so verwendet, wie es am besten ist, um das vom Verilog-Code verlangte Verhalten zu implementieren. Das kann bedeuten, den Reset-Eingang direkt mit dem Reset-Signal zu verbinden, manchmal mit einer Logikfunktion, die das Reset-Signal enthalten kann, und manchmal den Reset-Eingang gar nicht zu verwenden. Der Synthesizer tut, was ihm hilft, seine Leistungsziele besser zu erreichen – sonst nichts.

Anwender von Xilinx’ Vivado können mit zwei Synthese-Attributen mehr Kontrolle über dieses Thema bekommen: DIRECT_RESET und EXTRACT_RESET.

Ich schließe das mit einem weiteren Nachteil asynchroner Resets ab: In den meisten FPGAs hat ein Flip-Flop nur einen Reset/Set-Eingang. Dieser Eingang kann sich synchron oder asynchron verhalten. Wenn der Reset synchron ist, kann der Synthesizer Tricks finden, diesen Eingang zu nutzen, um weniger LUTs zu verbrauchen, während er das geforderte Verhalten umsetzt. Eine solche Abkürzung ist bei einem asynchronen Reset unmöglich. Ein asynchroner Reset fesselt dem Synthesizer also die Hände und zwingt ihn, mehr Logikressourcen zu verschwenden.

Versehentliches Einfrieren von Registern vermeiden

Es gibt eine Falle bei den üblichen Codemustern zur Implementierung von Resets, wie in diesem Codebeispiel gezeigt:

   always @(posedge clk or negedge resetn)
     if (!resetn)
       begin
         reg1 <= 0;
         reg2 <= 0;
         // Ayeee! Forgot to reset reg3 !
       end
     else
       begin
         reg1 <= [ ... ];
         reg2 <= [ ... ];
         reg3 <= [ ... ];
       end

Wie der Kommentar andeutet, taucht @reg3 im begin-end-Block für ein aktives @resetn nicht auf. Folglich verlangt der obige Verilog-Code, dass @reg3 seinen Wert nicht ändert, solange @resetn aktiv ist. Das ist dasselbe, als wäre @reg3 mit der folgenden Definition festgelegt worden:

   always @(posedge clk)
     if (resetn)
       reg3 <= [ ... ];

oder mit anderen Worten: @resetn fungiert für @reg3 als Taktfreigabe (clock enable): Der Takt wirkt nur, wenn @resetn high ist.

Genau dasselbe passiert bei synchronen Resets:

   always @(posedge clk)
     if (reset)
       begin
         reg1 <= 0;
         reg2 <= 0;
         // Ayeee! Forgot to reset reg3 !
       end
     else
       begin
         reg1 <= [ ... ];
         reg2 <= [ ... ];
         reg3 <= [ ... ];
       end

Das ist eigentlich einfacher zu verstehen, weil es nur ein Paar von begin-end-Blöcken ist, wobei der zweite Block nur wirksam wird, wenn @reset inaktiv ist. Die Definition von @reg3 ist in diesem Beispiel also eindeutig:

   always @(posedge clk)
     if (!reset)
       reg3 <= [ ... ];

Die offensichtliche (und nicht unbedingt clevere) Schlussfolgerung lautet also, kein Register im begin-end-Block für den Reset zu vergessen. Tatsächlich setzen viele FPGA-Entwickler alle Register zurück, ob nötig oder nicht, weil sie glauben, das sei der einzige Weg. Oder sie wählen einen Codierstil, bei dem jedes Register seine eigene always-Anweisung hat.

Aber was ist, wenn Sie absichtlich nur einige Register zurücksetzen wollen und andere nicht?

Bei einem asynchronen Reset bleibt nur die Möglichkeit, diese Register in eine separate always-Anweisung zu legen. Bei einem synchronen Reset gibt es aber einen einfachen Weg, das zu bewerkstelligen:

   always @(posedge clk)
     begin
       reg1 <= [ ... ];
       reg2 <= [ ... ];
       reg3 <= [ ... ];

       if (reset)
         begin
           reg1 <= 0;
           reg2 <= 0;
           // I don't want to reset reg3, and that's fine!
         end
     end

Statt einer if (reset)-Anweisung am Anfang und des interessanten Teils unter der else-Anweisung wird das if (reset) ans Ende gesetzt, sodass die Zuweisungen für den Reset alles überschreiben, was vorher kam.

Beachten Sie, dass das nicht äquivalent zu den obigen Beispielen ist: @reg1 und @reg2 werden zurückgesetzt, wenn @reset aktiv ist, jedoch wird @reg3 überhaupt nicht vom Reset beeinflusst.

Wenn Ihnen diese alternative Art, einen synchronen Reset anzuwenden, unangenehm ist, kann ich das verstehen, und es gibt einige Gründe dafür: Erstens ist es allgemein gute Praxis, in der FPGA-Entwicklung gebräuchliche Codemuster zu verwenden. Andernfalls könnte der Synthesizer einen exotischen Fehler offenbaren, was bei etablierten Codemustern viel weniger wahrscheinlich ist (siehe Goldene Regel #4). Selbst wenn der Verilog-Standard also ausdrücklich verlangt, dass diese Methode funktioniert, könnte man argumentieren, dass es nicht unbedingt eine gute Idee ist, sich auf dieses Feature zu verlassen.

Das ist ein starkes Argument, aber ich bin hier, um Ihnen zu sagen, was es wert ist: Ich verwende solche Codemuster seit mehr als einem Jahrzehnt intensiv mit einer breiten Palette von Synthesizern. Insbesondere definiere ich synchrone Resets in meinem eigenen Code auf diese Weise. Ich hatte damit noch nie ein einziges Problem.

Ein weiterer möglicher Grund, diese Methode nicht zu mögen, ist die Sorge, der Synthesizer könnte den Hinweis übersehen, dass ein synchroner Reset gewünscht ist, weil es nicht das übliche Codemuster ist. Wie jedoch bereits oben erwähnt, beachten die meisten Synthesizer diesen Hinweis ohnehin nicht und betrachten den synchronen Reset nur als eine weitere Definition des gewünschten Logikverhaltens. Dieser Grund hat also keine Basis.

Wenn Sie mir also glauben wollen, dass das sicher ist, ersparen Sie sich einigen Kopfschmerz.

Kann man dasselbe mit einem asynchronen Reset machen? Was wird zum Beispiel Folgendes tun?

    always @(posedge clk or negedge resetn)
      begin
        reg1 <= [ ... ];
        reg2 <= [ ... ];

        if (!resetn)
          reg1 <= 0;
      end

Das ist natürlich eine Abweichung vom üblichen Codemuster für asynchrone Resets. Ein anekdotischer Versuch mit Vivados Synthesizer zeigte, dass er den Hinweis verstand und für @reg1 einen asynchronen Reset zuwies.

Aber das Verhalten, das für @reg2 verlangt wird, kann auf einem FPGA nicht erreicht werden: Wie oben geschrieben, bedeutet es, dass sowohl @clk als auch @resetn Takte sind und dass @reg2 bei ihren steigenden bzw. fallenden Flanken einen neuen Wert abtastet. Da Flip-Flops mit zwei Takteingängen in keinem mir bekannten FPGA verfügbar sind, gibt es keine Möglichkeit, die Definition von @reg2 zu synthetisieren.

Vivados Synthesizer reagierte darauf, indem er den Teil „negedge resetn“ ignorierte und ein Flip-Flop erzeugte, das nur @clk als Takt verwendet. Es gab keine Spuren dieser Merkwürdigkeit im Syntheseergebnis, und der Synthesizer gab auch keine Warnung oder Beschwerde von sich. Das, obwohl der Synthesizer Logik erzeugt hat, die sich nicht so verhält, wie es der Verilog-Code definiert.

Also konkret mit Vivados Synthesizer funktioniert dasselbe Codemuster tatsächlich sogar für einen asynchronen Reset, aber man sollte sich nicht darauf verlassen: Der Verilog-Code sollte sagen, was die Logik tun soll. Andernfalls ist der Synthesizer völlig dazu berechtigt, ihn nach Belieben falsch zu interpretieren.

Damit endet die zweite Seite in dieser Serie über Resets. Die nächste Seite geht auf verschiedene Aspekte des Hochfahrens des FPGA nach dem Einschalten und einem externen Reset ein.

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