Was ein FSM ist
Ein endlicher Automat ist ein Controller, der:
- Zu jedem Zeitpunkt einen aus einer festen Menge benannter Zustände hält.
- Zwischen Zuständen wechselt, basierend auf Eingaben (und möglicherweise dem aktuellen Zustand).
- Ausgaben erzeugt, die vom aktuellen Zustand (und vielleicht den aktuellen Eingaben) abhängen.
Das deckt erstaunlich viel Digital-Design ab: Ampelsteuerungen, UART-Sender, Speichercontroller, Netzwerkprotokoll-Handler, Instruktions-Decoder, alles mit diskreten Betriebsmodi.
Was FSMs anders anfühlen lässt als Datapath-Logik: Sie sind Zustände, keine Werte. Ein Counter tickt durch 1, 2, 3, 4. Ein FSM tickt durch IDLE, FETCHING, BUSY, DONE - gleiche Form, aber die Zustände haben Namen und die Übergänge haben Bedeutung.
Das Zwei-Prozess-Muster
Der Standard-Verilog-FSM nutzt zwei always-Blöcke:
- Einen getakteten Block, der das Zustandsregister bei jeder Taktflanke erfasst. Nutzt non-blocking-Zuweisung. Winzig - meist drei Zeilen.
- Einen kombinatorischen Block, der ein
caseauf den aktuellen Zustand verwendet, um den nächsten Zustand und die Ausgaben zu berechnen. Nutzt blocking-Zuweisung. Der "interessante" Code lebt hier.
Diese Trennung hat drei Vorteile: Sie zwingt dich, den Zustand explizit zu denken; sie erzeugt Hardware, die sauber auf "ein Register plus ein kombinatorischer Block" abbildet; und sie ist der Standard - jeder, der dein Verilog liest, erkennt sie sofort.
Ein durchgearbeitetes Beispiel: Sequenz-Detektor
Ein klassisches Lehr-FSM: Detektiere die Bitsequenz 1011 auf einem seriellen Eingang. Gib einen Ein-Takt-Puls aus, wenn die Sequenz vollständig ist.
Die Zwei-Block-Struktur ist der Bullet Point oben. Die Sauberkeitsdetails sind erwähnenswert:
- Defaults oben im kombinatorischen Block.
next_state = state(bleib, wo du bist) unddetected = 1'b0(kein Puls) sind die "nichts tun"-Zuweisungen. Jedercase-Zweig setzt dann nur die Dinge, die abweichen. So lässt sich kein Latch ableiten. localparamfür Zustandsnamen. Wer das Modul liest, denkt inS0,S1,S2,S3, nicht in3'd0,3'd1. Der Synthesizer macht die Ersetzung.- Keine Ausgaben aus dem getakteten Block. Die gesamte "was tut dieser Zustand"-Logik lebt im kombinatorischen Block. Der getaktete Block ist für nichts verantwortlich außer dem Halten des aktuellen Zustands.
Moore vs Mealy
Moore: Ausgabe hängt nur vom aktuellen Zustand ab. Mealy: Ausgabe hängt vom aktuellen Zustand und den aktuellen Eingaben ab.
Im obigen Beispiel wird detected innerhalb des S3-Zweigs gesetzt nur wenn in zu einem der erwarteten Sequenz-Endmuster passt. Das ist ein Mealy-Output - er hängt zusätzlich zu state von in ab. Eine Moore-Version hätte einen separaten Zustand für "gerade detektiert" und setzte detected = 1, wann immer dieser Zustand aktuell ist; die Ausgabe pulst einen Takt später, ist aber nie empfindlich gegenüber einem Glitch auf in.
Beide Stile sind gültig. Moore ist in Lehrbüchern der Default, weil Ausgaben garantiert nicht glitchen, wenn sich Eingaben mitten im Zyklus ändern. Mealy ist schneller (keine Register-Latenz für eingangsgetriebene Ausgaben) und erzeugt in vielen Fällen kleinere Hardware. Wähle anhand des Protokolls, das du implementierst.
Zustandskodierung: Binär, One-Hot, Gray
Das Bitmuster, das du jedem Zustand zuweist, ist relevant für Fläche und Geschwindigkeit:
- Binär (
S0 = 3'd0,S1 = 3'd1, ...): kleinstes Zustandsregister - Bits für Zustände. Maximale Decode-Logik. - One-Hot (
S0 = 4'b0001,S1 = 4'b0010, ...): N Bits für N Zustände. Decode-Logik ist trivial (jeder Zustand ist ein Wire); Übergänge sind schnell. FPGAs nutzen das oft als Default. - Gray-Code: Aufeinanderfolgende Zustände unterscheiden sich um ein Bit. Nützlich, wenn Zustandsbits Clock-Domains überqueren.
Die meisten modernen Synthese-Tools wählen die Kodierung für dich (Vivado, Quartus, Design Compiler haben alle einen automatischen Modus, der jede ausprobiert und die beste wählt). Du musst selten festlegen. Wenn doch: Die meisten Tools akzeptieren eine attribute-Annotation oder ein (* fsm_encoding = "one_hot" *)-Pragma.
Eine Drei-Block-Variante
Gelegentlich wirst du den FSM dreigeteilt sehen: ein getakteter Block für den Zustand, ein kombinatorischer für den nächsten Zustand, ein kombinatorischer für Ausgaben. Das ist nur das Zwei-Block-Muster, bei dem die Output-Berechnung in einen eigenen Block herausgezogen ist:
// Zustandsregister
always @(posedge clk) ...
// Next-State-Logik
always @(*) ...
// Output-Logik
always @(*) begin
case (state)
...
endcase
end
Der Split-Output-Stil ist nützlich, wenn Ausgaben umfangreich sind und die Next-State-Logik überladen würde, wenn beides im selben Block wäre. Für kleine FSMs ist es Overkill.
Was default in einem FSM tut
Jede FSM-case-Anweisung sollte einen default-Zweig haben. Zwei Gründe:
- Sicherheit: Falls das Zustandsregister irgendwie einen ungültigen Wert annimmt (Korruption, Bug, unvollständiger Reset), bringt
defaultes in einen bekannten Zustand zurück. - Synthese-Hinweis: Wenn die expliziten Cases vollständig sind (etwa ein 2-Bit-State mit allen 4 behandelten Werten), sagt
default: next_state = 'x;dem Synthesizer: "Ich versichere, der Default ist unerreichbar, optimiere frei." Falls der unerreichbare Pfad in der Simulation doch erreicht wird, propagiert das resultierendexund der Bug taucht sofort auf.
default: begin
next_state = S0; // sichere Wiederherstellung
// oder
next_state = 'x; // unerreichbar, optimiere frei
end
Wähle danach, ob du wirklich bewiesen hast, dass der Default unerreichbar ist.
Worauf zu achten ist
Defaults oben im kombinatorischen Block vergessen. Ohne next_state = state und die Output-Defaults leckt ein Zweig, der nicht alles zuweist, ein Latch.
Outputs in den getakteten Block stecken. Lebt detected <= 1 im always @(posedge clk)-Block, ist der Output registriert - er erscheint einen Takt verspätet. Das kann gewollt sein (ein "registrierter Mealy"-Output), ist aber ein häufiger Designfehler, wenn die Spezifikation einen sofortigen Puls verlangt.
Blocking und non-blocking mischen. Getakteter Block: <=. Kombinatorischer Block: =. Beides innerhalb eines Blocks zu mischen, ist eine Race Condition.
Ein kombinatorischer always, der next_state referenziert und state zuweist. Das baut eine Rückkopplungsschleife, die der Simulator nicht auflösen kann. Der getaktete Block besitzt state; der kombinatorische besitzt next_state; nie soll eines an die Variable des anderen rühren.
Wie es weitergeht
Du kannst jetzt jeden Controller bauen, den du beschreiben kannst. Das nächste Kapitel tritt einen Schritt vom synthetisierbaren Design zurück und behandelt die Testbenches, die es ausüben - wie man Stimulus treibt, Ausgaben beobachtet, Waveforms dumpt und überprüft, dass deine Module wirklich das tun, was du denkst.
Häufig gestellte Fragen
Was ist ein endlicher Automat in Verilog?
Ein FSM ist ein Controller, der einen aus einer kleinen Menge benannter Zustände hält und basierend auf Eingaben zwischen ihnen wechselt. In Verilog hat die Standard-Implementierung zwei Blöcke: ein getaktetes always, das das Zustandsregister bei jeder Taktflanke aktualisiert, und ein kombinatorisches always, das den nächsten Zustand und die Ausgaben aus aktuellem Zustand und Eingaben berechnet.
Was ist das Standard-FSM-Muster in Verilog?
Zwei-Prozess-FSM: ein getakteter always @(posedge clk)-Block hält das Zustandsregister und nutzt non-blocking-Zuweisung, und ein kombinatorischer always @(*)-Block nutzt ein case auf den aktuellen Zustand, um den nächsten Zustand und die Ausgaben zu berechnen. Diese Trennung macht den Code leicht zu lesen, zu linten und sauber zu synthetisieren.
Was ist der Unterschied zwischen einem Mealy- und einem Moore-FSM?
Bei einem Moore-FSM hängen die Ausgaben nur vom aktuellen Zustand ab. Bei einem Mealy-FSM hängen sie sowohl vom aktuellen Zustand als auch von den aktuellen Eingaben ab. Mealy-Automaten reagieren einen Takt schneller (keine Register-Latenz für eingangsabhängige Ausgaben), können aber Glitches erzeugen, wenn sich Eingaben mitten im Zyklus ändern. Moore-Automaten sind um einen Takt langsamer, aber vorhersehbarer - sie sind die Standardwahl, außer du brauchst das Tempo.
Wie kodiert man Zustände in Verilog?
Verwende localparam-Konstanten innerhalb des Moduls: localparam IDLE = 3'd0; usw. Drei übliche Kodierungen: binär (Zustände 0, 1, 2, ... - kleinstes Zustandsregister), one-hot (ein Bit pro Zustand, weniger Logikebenen pro Übergang) und Gray-Code (aufeinanderfolgende Zustände unterscheiden sich um ein Bit - minimiert Glitches). Synthese-Tools wählen die Kodierung üblicherweise für dich; sie festzulegen ist selten nötig.