01signal.com

Asynchrone Resets auf FPGAs: Nicht so einfach, wie viele glauben

Diese Seite ist die erste von drei Seiten in einer Serie über Resets in FPGAs. Da viele asynchrone Resets (asynchronous resets) verwenden, ohne zu wissen, dass sie nicht wirklich so funktionieren, wie sie erwarten, erklärt diese erste Seite, warum das ganze Thema gar nicht so einfach ist, wie es scheinen mag.

Funktioniert Ihr Reset wirklich?

Betrachten Sie das folgende Beispiel zum Zurücksetzen einer Zustandsmaschine (state machine) – es ist falsch:

   always @(posedge clk or negedge resetn)
     if (!resetn)
       state <= ST_START;
     else
       case (state)
	 ST_START:
	   begin
	      state <= ST_NEXT;
	      [  ... do some stuff maybe? ... ]
	   end

	 ST_NEXT:
	   begin
	      [ ... do something ... ]
	   end
       endcase

Sie fragen sich vielleicht, was an dem Reset hier falsch sein soll? Das ist doch genau so, wie es in den Lehrbüchern steht! Ein asynchroner Reset, der bei Low aktiv ist und das Register, das @state implementiert, in seinen Ausgangszustand versetzt. Was könnte da schon schiefgehen?

Nehmen wir für die Diskussion an, dass das @resetn-Signal für sich genommen in Ordnung ist. Mit anderen Worten: Es ist nicht direkt mit einem Taster oder Ähnlichem verbunden. Vielleicht wurde ein Chip verwendet, der ein Reset-Signal erzeugt, oder das Reset wird intern vom FPGA erzeugt. Wie auch immer – wir nehmen an, dass das Reset stabil aktiv wird, lange genug aktiv bleibt und anschließend wieder inaktiv wird. Trotzdem bleibt es falsch.

Und wenn ich sage, dass es falsch ist, meine ich die Art von Fehler, die dazu führt, dass sich das FPGA ab und zu ohne erkennbaren Grund seltsam verhält.

Wo liegt also das Problem? Nun, da das Reset lange genug aktiv ist, bringt es @state sicher in seinen Ausgangszustand. Aber was passiert, wenn es inaktiv wird (also im obigen Beispiel zurück auf '1' wechselt)? Genau dann sollten die betreffenden Flip-Flops auf steigende Flanken von @clk reagieren.

Es dauert jedoch eine gewisse Zeit, bis sich das Flip-Flop vom Reset-Signal erholt hat und damit beginnt, den Dateneingang bei steigenden Taktflanken abzutasten (sampling). Und da der Reset per Definition asynchron ist, kann er zu jedem beliebigen Zeitpunkt relativ zu @clk inaktiv werden.

Wenn die erste steigende Flanke von @clk zu früh nach der Deaktivierung des Resets kommt, ignoriert das Flip-Flop diese Flanke. Das ist völlig in Ordnung, wenn alle Flip-Flops, die mit @clk verbunden sind, das tun. Aber nicht alle Flip-Flops sind exakt gleich: Manche bekommen die Taktflanke etwas früher als andere, und manche bekommen die Deaktivierung des Resets später.

Um ehrlich zu sein, ist diese Erklärung etwas vereinfacht. Eine genauere Sicht auf das Problem finden Sie im Abschnitt über Recovery und Removal auf der Seite, die die Grundlagen des Timings erklärt.

Darauf läuft es also hinaus: Bei etwas Pech wird das Reset so nahe an der steigenden Taktflanke inaktiv, dass einige Flip-Flops auf die erste steigende Flanke reagieren und andere sie ignorieren. Manche Flip-Flops brauchen sogar etwas länger, um zu entscheiden, was sie tun sollen. Ob man nun die Unterschiede zwischen den Flip-Flops auf demselben Chip verantwortlich macht oder den Taktversatz (clock skew) und den Reset-Versatz: Im Endeffekt liegen einige Flip-Flops einen Taktzyklus vor den anderen.

Um zu verstehen, wie schlimm das ist, betrachten wir das obige Beispiel. Wenn der Synthesizer erkennt, dass es sich um eine Zustandsmaschine handelt, wird er die Zustandsvariable mit hoher Wahrscheinlichkeit in One-Hot-Kodierung (one-hot encoding) implementieren. Mit anderen Worten: Er weist jedem Zustand ein Ein-Bit-Register zu. Jedes dieser Register ist aktiv, wenn sich die Zustandsmaschine in dem zugehörigen Zustand befindet. Angenommen, der Synthesizer hat dem Zustand ST_START ein Register namens hot_state_0 zugewiesen und dem Zustand ST_NEXT ein Register namens hot_state_1. Der Reset macht dann offensichtlich hot_state_0 aktiv und hot_state_1 inaktiv.

Beachten Sie, dass die Zustandsmaschine bedingungslos von ST_START nach ST_NEXT übergeht. Dementsprechend wird hot_state_0 beim ersten Takt nach der Deaktivierung des Resets inaktiv, und hot_state_1 wird aktiv.

Was aber, wenn das Reset zu einem unglücklichen Zeitpunkt inaktiv wird und daher einige Flip-Flops die erste Taktflanke verpassen und andere nicht? Eine Möglichkeit ist, dass hot_state_0 den ersten Takt verpasst, hot_state_1 aber darauf reagiert. Dadurch werden beide aktiv, was bei der One-Hot-Kodierung ein unzulässiger Zustand ist. Im umgekehrten Fall werden beide Register inaktiv; dann sind also alle One-Hot-Register der Zustandsmaschine inaktiv. In beiden Fällen kann die Zustandsmaschine unter Umständen nie wieder in einen zulässigen Zustand zurückfinden.

Wie sieht das in der Praxis aus? Das hängt natürlich von der Anwendung ab, aber die Chancen stehen gut, dass etwas nicht richtig funktioniert, bis erneut ein Reset auf dem FPGA ausgelöst wird. Den Grund für dieses Verhalten zu finden, kann extrem schwierig sein, denn das Problem tritt zufällig auf – und nicht unbedingt häufig. Außerdem verhält es sich wahrscheinlich von einem Durchlauf des FPGA-Designs zum nächsten anders und vielleicht auch von einem Board zum anderen. Kurz gesagt ist das die Art von Fehler, die einen in den Wahnsinn treiben kann. In dieser Diskussion klingt das vielleicht gar nicht so schlimm, weil die Ursache des Problems das Thema der Diskussion ist. Wenn eine solche Instabilität aber im echten Leben auftritt, kann sie praktisch alles sein, und oft fühlt es sich an, als wäre das FPGA verhext.

Aber ich mache das ständig, und es funktioniert!

In der Tat. In den allermeisten Fällen spielt es keine große Rolle, wenn einige Flip-Flops den ersten Takt nach dem Reset verpassen.

Der Hauptgrund, warum das obige Beispiel der Zustandsmaschine fehlschlagen kann, ist, dass sie den Ausgangszustand bereits im ersten Taktzyklus verlässt. Die meisten Zustandsmaschinen in realen Entwürfen haben eine Regel für das Verlassen des Ausgangszustands, sodass sie während der ersten Taktzyklen in diesem Zustand bleiben. Mit diesem Fehler kommt man also davon.

Hier ist ein weiteres Beispiel. Ein einfacher Zähler:

   reg [15:0] counter;

   always @(posedge clk or negedge resetn)
     if (!resetn)
       counter <= 0;
     else
       counter <= counter + 1;

In diesem Fall besteht @counter aus 16 Flip-Flops. Jedes dieser Flip-Flops erhält an seinem Dateneingang den Wert, den der Zähler im nächsten Taktzyklus annehmen soll, und zusätzlich @resetn an seinem asynchronen Reset-Eingang.

Wenn @resetn aktiv ist, erhält @counter den Wert 0; der Wert für den nächsten Taktzyklus ist dann 1. Daher bleiben alle Flip-Flops außer counter[0] auf Null, ganz gleich, ob sie die erste Taktflanke verpassen oder nicht. In beiden Fällen beginnt der Zähler also korrekt zu zählen. In den allermeisten Fällen, in denen solcher Code geschrieben wird, spielt es keine Rolle, ob der Zähler den ersten Taktzyklus verpasst hat.

Das ist allerdings eine andere Geschichte:

   reg [15:0] counter;

   always @(posedge clk or negedge resetn)
     if (!resetn)
       counter <= 0;
     else
       counter <= counter - 1;

Ein kleiner Unterschied, aber ein bedeutender: Wenn der Zähler bei Null startet und herunterzählt, ist der Wert des Zählers im nächsten Taktzyklus 0xffff. Mit anderen Worten: Alle Flip-Flops müssen ihren Wert im ersten Takt nach dem Reset ändern. Wenn also einige auf die erste Taktflanke nach dem Reset reagieren und andere nicht, kann der Zähler praktisch mit jedem Zufallswert starten.

Aber wer setzt einen Zähler schon auf Null zurück und zählt dann rückwärts?

Hier ist also ein realistischeres Beispiel: Ein Taktfreigabesignal (clock enable), das die Logik so arbeiten lässt, als wäre die Taktfrequenz halbiert (und das auf diese Weise, falls erforderlich, einen Multi-Cycle-Pfad (multi-cycle path) ermöglicht):

   reg en;

   always @(posedge clk or negedge resetn)
     if (!resetn)
       en <= 0;
     else
       en <= !en;

   always @(posedge clk)
     if (en)
       [ ... do something ... ]

Man könnte meinen, dass hier nichts schiefgehen kann: Die Taktfreigabe @en ist ein einzelnes Register; es spielt also keine große Rolle, wann sie zu toggeln beginnt – oder doch? Die Sache ist die, dass ein Taktfreigabesignal tendenziell einen hohen Fan-out hat, sodass der Synthesizer es duplizieren könnte, um das Fan-out-Limit nicht zu überschreiten.

Mein anekdotischer Versuch mit einem Vivado-Synthesizer zeigte, dass jedes der duplizierten Register, das @en implementierte, von seinem eigenen Ausgangssignal abhing. Anders ausgedrückt: Es gab kein einziges Signal, anhand dessen alle Flip-Flops ihren nächsten Ausgangswert bestimmten. Stattdessen gab es viele unabhängige Flip-Flops, die ihren Wert bei jeder steigenden Taktflanke änderten. Wenn diese Flip-Flops also nicht im selben Taktzyklus mit dem Toggeln beginnen, bleiben ihre Ausgänge auf unbestimmte Zeit unterschiedlich.

Wenn ein solcher Zwischenfall passiert, wird die Logik mit hoher Wahrscheinlichkeit vollständig falsch arbeiten. Wenn Sie also auf einem asynchronen Reset bestehen, sorgen Sie zumindest dafür, dass sich alle Taktfreigaben auf eine einzige Quelle stützen, beispielsweise so:

   reg pre_en; // Apply some don't-touch synthesis directive on this
   reg en;

   always @(posedge clk or negedge resetn)
     if (!resetn)
       pre_en <= 0;
     else
       pre_en <= !pre_en;

   always @(posedge clk or negedge resetn)
     if (!resetn)
       en <= 0;
     else
       en <= pre_en;

   always @(posedge clk)
     if (en)
       [ ... do something ... ]

Der Trick ist, dass @pre_en das Register ist, das über den nächsten Wert entscheidet. Dieses Flip-Flop hat einen niedrigen Fan-out und möglicherweise ein Attribut, das dem Synthesizer sagt, er solle es nicht verändern. Dadurch ist es mit Sicherheit ein einzelnes Register. Alle Flip-Flops, die @en implementieren, stützen sich auf @pre_en, sodass sie sich über den Wert von @en im nächsten Takt einig sind. Was den ersten Takt nach dem Reset angeht, spielt es keine Rolle, wenn einige Flip-Flops ihn verpassen, denn der Wert von @en ist im ersten Taktzyklus ohnehin Null.

Unterm Strich führt die falsche Verwendung eines asynchronen Resets normalerweise trotzdem zu einem korrekten Verhalten, vor allem weil die Logik für gewöhnlich tolerant gegenüber der Unsicherheit ist, wann das Reset relativ zum Takt inaktiv wird. Dennoch kann ein sorgloser Einsatz asynchroner Resets zu gelegentlichem Fehlverhalten führen, das sich nur äußerst schwer beheben lässt.

Timing-Constraints zwischen Reset und Takt verwenden

Der scheinbar naheliegende Weg, die unsichere zeitliche Beziehung zwischen der Deaktivierung des Resets und der steigenden Taktflanke zu vermeiden, besteht darin, Timing-Constraints (timing constraints) auf das Reset-Signal anzuwenden. Dadurch wird das Reset-Signal allerdings synchron.

Aber zu welchem Takt ist das Reset-Signal dann synchron? Es ist oft praktisch, einen einzigen globalen asynchronen Reset für den gesamten Logikentwurf zu haben. Dieser Reset wird mit Logik erzeugt, die zu einem bestimmten Takt synchron ist. Wenn dieser Reset zusammen mit Logik verwendet wird, die zu einem anderen Takt synchron ist, haben wir einen Taktdomänenwechsel (clock domain crossing). Das ist ein Thema für sich, aber der wichtigste Punkt ist die Möglichkeit, dass die Tools das Timing auf den betreffenden Pfaden (paths) ignorieren.

Der Ausgangspunkt für Timing-Constraints bei asynchronen Resets ist also: Es muss für jeden Takt einen separaten asynchronen Reset geben. Oder, wenn Sie darauf bestehen, einen separaten asynchronen Reset für jede Gruppe zusammenhängender Takte (related clocks). Andernfalls ergibt eine Timing-Constraint keinen Sinn. Wenn das seltsam klingt, dann deshalb, weil der asynchrone Reset dann nicht mehr asynchron ist.

Dass der Verilog-Code ein Muster für einen asynchronen Reset verwendet, spielt keine Rolle. Ebenso wenig spielt es eine Rolle, ob der asynchrone Reset-Eingang des Flip-Flops benutzt wird oder ob das Flip-Flop so konfiguriert ist, dass es seinen Reset-Eingang als asynchron ansieht: Wenn das Reset synchron zu einem Takt ist und eine Timing-Constraint verwendet wird, ist das Reset praktisch synchron. In diesem Fall sollten Sie erwägen, das Verilog-Muster dafür direkt zu verwenden:

   always @(posedge clk)
     if (!resetn)
       state <= ST_START;
[ ... ]

Allerdings wirbt Intels YouTube-Video über Timing Closure (timing closure) dafür, asynchrone Resets zu verwenden, die synchron zu einem Takt sind, und dabei ebenfalls Timing-Constraints einzusetzen. Die Entscheidung für einen asynchronen Reset dient dazu, dedizierte Ressourcen für die globale Verdrahtung im FPGA zu nutzen. Ich finde das ziemlich eigenartig, denn selbst eine globale Verdrahtung kann eine große Verzögerung haben, insbesondere bei großen FPGAs. Es gibt aber sicherlich Szenarien, in denen das Sinn ergibt.

Es gibt noch einen weiteren Grund, beim asynchronen Reset zu bleiben, selbst wenn er praktisch synchron ist; das wird auf der nächsten Seite erläutert: Er ermöglicht es, die Aktivierung des Reset-Signals asynchron zu propagieren, was in Simulationen ebenso nützlich ist wie bei ASIC-Tests.

Wenn diese Methode mit einem synchronisierten asynchronen Reset angewendet wird, ist es wichtig sicherzustellen, dass die Timing-Constraint für alle Signalpfade (signal paths) gilt, die zu den asynchronen Reset-Eingängen der Flip-Flops führen. Die Tatsache, dass das Reset von einem Flip-Flop erzeugt wird, das mit demselben Takt synchron ist wie das Flip-Flop am Ziel, garantiert für sich genommen nicht, dass der Pfad (path) zwischen ihnen zeitlich analysiert wird. Standardmäßig ignorieren einige Timing-Tools jeden Pfad, der an asynchronen Eingängen endet; daher muss diese Analyse unter Umständen explizit aktiviert werden. Prüfen Sie in den Timing-Reports unbedingt, ob diese Pfade tatsächlich von den Timing-Constraints abgedeckt werden. In diese Falle tappt man leicht.

Damit endet die erste Seite dieser Serie über Resets. Die nächste Seite behandelt die verschiedenen Möglichkeiten für Resets und die Initialisierung des FPGA.

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