Einführung in FPGA-Module und Zustandsmaschinen

Bisher wurden einfache Register und darauf basierende Zähler synthetisiert.

In diesem Projekt kommen State Machines,  im deutschen Sprachgebrauch auch Zustandsmaschinen oder endliche Automaten dazu.

State Machines werden für komplexeren Vorgänge benötigt und dienen bei entsprechender Programmierung auch der Les- und Wartbarkeit des VHDL-Codes.

Ich habe Grok3 und DeepSeek die gleiche Frage gestellt um mir ein Grundgerüst für eine State-Machine zu erstellen. Es funktioniert übrigens sehr gut zusammen mit einer KI zu programmieren, wenn man selber programmieren kann:
"Ich erstelle einen VHDL-Kurs für FPGAs und möchte jetzt State-machines erklären.
Erstelle mir dazu bitte einen Prozess mit den zugehörigen Konstanten und Signalen.
Die States sind Reset, Init, Boot, Run und Error (Du kannst gerne die Namen ändern, wenn es didaktisch von Vorteil ist).
Reset geht sofort über in Init, das wartet auf InitCompleteR geht dann nach Boot. Dort das Gleiche mit BootCompleteR und Run. Von jedem State nach Reset wird Error angefahren wenn ErrorR gesetzt ist.
Der Prozess hat einen Ausgang LedC, der ist '0' bei Boot, '1' bei Init, BlinkSlowR bei Run und BlinkFastR bei Error."

Beide haben mir gleich ein VHDL-Modul gemacht, die Ergebnisse sind beide sehr ähnlich. Das Ergebnis von DeepSeek hat mir etwas besser gefallen, weswegen ich es zur Grundlage genommen habe. Ich habe erst mal nur das Interface weggenommen und es so abgespeckt, dass das die generiertn Prozesse innerhalb eines VHDL-Moduls verwendet werden können.

Diesen Code gehen wir jetzt abschnittsweise durch. Vorweg gesagt:  Diese Art für State-Machines ist am verbreitesten auch wenn ich es etwas anders mache.

   -- State type definition (didaktisch gut: explizite Typdefinition)
    type State_Type is (
        ST_RESET,  -- Reset state
        ST_INIT,   -- Initialization state
        ST_BOOT,   -- Boot state
        ST_RUN,    -- Normal operation
        ST_ERROR   -- Error state
    );
    -- State signal
    signal CurrentState : State_Type := ST_RESET;
    signal NextState    : State_Type;
    
    -- Constants for better readability (optional)
    constant C_LED_ON  : STD_LOGIC := '1';
    constant C_LED_OFF : STD_LOGIC := '0';
    
    -- inputs and outputs of state machine
    signal ResetR        : STD_LOGIC; -- Active high reset
    signal InitCompleteR : STD_LOGIC; -- Init complete flag
    signal BootCompleteR : STD_LOGIC; -- Boot complete flag
    signal ErrorR        : STD_LOGIC; -- Error flag
    signal BlinkSlowR    : STD_LOGIC; -- Slow blink signal
    signal BlinkFastR    : STD_LOGIC; -- Fast blink signal
    signal LedC          : STD_LOGIC  -- LED control

Neu ist hier die erste Hälfte.

Es wird ein Typ State_Type definiert, der die einzelnen States auflistet. Dieser wird von den Signalen CurrenState (registered) und NextState (kombinatorisch) verwendet.

Der Rest ist nur zur Vollständigkeit, auch wenn der Code nur zur Anschauung dient und auf nichts getestet wurde.

    -- State register process (synchron)
    StateRegister: process(Clk)
    begin
        if rising_edge(Clk) then
            if ResetR = '1' then
                CurrentState <= ST_RESET;
            else
                CurrentState <= NextState;
            end if;
        end if;
    end process;

Die Behandlung der State-Variable ist trivial. Man fängt mit dem Startwert nach Reset an und übernimmt dann immer den State der aus dem Kontext hervorgeht.

    -- Next state logic process (combinatorial)
    StateTransition: process(CurrentState, InitCompleteR, BootCompleteR, ErrorR)
    begin
        -- Default: remain in current state
        NextState <= CurrentState;
        
        case CurrentState is
            when ST_RESET =>
                -- Immediately transition to INIT after reset
                NextState <= ST_INIT;
                
            when ST_INIT =>
                if ErrorR = '1' then
                    NextState <= ST_ERROR;
                elsif InitCompleteR = '1' then
                    NextState <= ST_BOOT;
                end if;
                
            when ST_BOOT =>
                if ErrorR = '1' then
                    NextState <= ST_ERROR;
                elsif BootCompleteR = '1' then
                    NextState <= ST_RUN;
                end if;
                
            when ST_RUN =>
                if ErrorR = '1' then
                    NextState <= ST_ERROR;
                end if;
                
            when ST_ERROR =>
                -- Stay in error state until reset
                if ResetR = '1' then
                    NextState <= ST_RESET;
                end if;
        end case;
    end process;

Eine Ablaufsteuerung braucht natürlich einen Ablauf. Der ergibt sich durch die Übergänge von einem State zum nächsten.

Genau dass sieht man in diesem Prozess:

  • Falls nichts besonderes passiert bleibt man im alten State
  • Je nach State gibt es Übergangsbedingungen, diese sind immer priorisiert (z. B. ist ErrorR Trumpf und schlägt die anderen Bedingungen)

Somit ist das Ablaufprogramm geschrieben.

    -- Output logic (combinatorial)
    OutputLogic: process(CurrentState, BlinkSlowR, BlinkFastR)
    begin
        case CurrentState is
            when ST_RESET =>
                LedC <= C_LED_OFF;  -- LED off during reset
            
            when ST_INIT =>
                LedC <= C_LED_ON;   -- LED steady on during init
            
            when ST_BOOT =>
                LedC <= C_LED_OFF; -- LED off during boot
            
            when ST_RUN =>
                LedC <= BlinkSlowR; -- Slow blinking during run
            
            when ST_ERROR =>
                LedC <= BlinkFastR; -- Fast blinking during error
        end case;
    end process;

Wenn die Ablaufsteuerung sich nicht autistisch verhalten soll. Muss sie natürlich auch irgend etwas von sich geben. Das passiert im Ausgabeprozess.

Hier wird nur ein einziges Ausgabesignal behandelt, aber normalerweise sind es mehrere.

Was mache ich anders?

Ich unterscheide stark zwischen kombinatorischen und getakteten Signalen. Kombinatorische Signale enden mit einem C, getaktete mit einem R. Signale die weder das eine noch das andere haben, sind entweder Konstanten oder Moduleingänge. Da ich Moduleingänge nur mit R-Signalen verbinde, können solche Signale wie R-Signale behandelt werden.

Des weiteren verwende ich kaum kombinatorische Prozesse, schon gar nicht, wenn diese viele Signale verwenden. Schnell ist mal ein Signal in der Sensitivitätsliste vergessen und man hat gated Clocks generiert.

Im folgenden nun die Code-Snippets, die bei mir anders ausschauen.
    type TApplicState is (
        ST_RESET,  -- Reset state
        ST_INIT,   -- Initialization state
        ST_BOOT,   -- Boot state
        ST_RUN,    -- Normal operation
        ST_ERROR   -- Error state
    );

	-- State signal
    signal ApplicStateR : TApplicState; -- actual state of application
    signal ApplicStateR : TApplicState; -- next state of application
    

Da in Modulen viele State-Machines nebeneinander residieren verwende ich prägnantere Namen für die State-Machines und eben die C- und R-Nomenklatur

    -- State register process (synchron)
    StateRegister: process(Clk)
    begin
        if rising_edge(Clk) then
            if ResetR = '1' then
                ApplicStateR <= ST_RESET;
            else
                ApplicStateR <= ApplicStateC;
				-- registered outputs as example
		        case ApplicStateR is
  	              	when ST_BOOT =>
                    	BootR <= '1'; -- set in BOOT-State only
                	when ST_RUN =>
                    	BootR <= '0'; -- reset RUN-State only
                	when others =>
                    	null; -- BootR does not change
            	end case;
            end if;
        end if;
    end process;

Oft werden in State-Machines auch getaktete Signale erzeugt. Das mache ich durch die case Anweisung im synchronen Prozess.

-- Next state logic as when-else statement
ApplicStateC <= 
    -- Reset state has highest priority
    ST_INIT when ApplicStateR = ST_RESET else
    
    -- Error condition has priority in all operational states
    ST_ERROR when (ApplicStateR = ST_INIT  and ErrorR = '1') else
    ST_ERROR when (ApplicStateR = ST_BOOT  and ErrorR = '1') else
    ST_ERROR when (ApplicStateR = ST_RUN   and ErrorR = '1') else
    
    -- Normal state transitions
    ST_BOOT  when (ApplicStateR = ST_INIT  and InitCompleteR = '1') else
    ST_RUN   when (ApplicStateR = ST_BOOT  and BootCompleteR = '1') else
    ST_RESET when (ApplicStateR = ST_ERROR and ResetR = '1') else
    
    -- Default case: keep current state
    ApplicStateR;

Den nächsten State leite ich rein kombinatorisch aus dem aktuellen State und den Übergangsbedingungen ab.

-- Output logic as when-else statement
LedC <= C_LED_OFF  when (CurrentState = ST_RESET) else
        C_LED_ON   when (CurrentState = ST_INIT)  else
        C_LED_OFF  when (CurrentState = ST_BOOT)  else
        BlinkSlowR when (CurrentState = ST_RUN)   else
        BlinkFastR; -- Default case (ST_ERROR)

Beide Lösungsansätze sind Äquivalent. Die erzeugte Logik ist identisch. Das Zeitverhalten ist identisch. Es ist also reiner Programmierstil.

Ich verwende meinen Ansatz weil er meines Erachtens lesbarer und gleichzeitig kompakter ist.

Was ist eine One-Hot Kodierung

Kleine State-Machines wie diese hier werden normalerweise sequentiell, also binär kodiert. Also

ST_RESET = "000",
ST_INIT = "001",
ST_BOOT = "010",
ST_RUN = "011",
ST_ERROR = "100"

Jetzt braucht man ja viele Vergleiche, durch die ganzen case-statements. Man muss ja immer wieder testen, in welchem state man gerade ist. Verwendet man ein FPGA mit 4-Input-Lookup-Table braucht man jetzt schon drei Inputs für den State. Bei größeren State-Machines entsprechend mehr.

Die Alternative Kodierung ist "one hot". Das wäre jetzt  


ST_RESET = "00001",
ST_INIT = "00010",
ST_BOOT = "00100",
ST_RUN = "01000",
ST_ERROR = "10000"

Für jeden Zustand wäre also immer genau ein Bit von 5 gesetzt. Die Synthese weiß das natürlich. Wenn jetzt auf die Bedingung ST_RUN abgefragt wird, muss nur noch das ApplicStateR_var(3) auf '1' getestet werden. Gerade wenn ein Zustand etwas triggern soll z. B. Run ist das Clock-Enable für einen bestimmten Prozess, dann könnte man guter Dinge
EnableProcR <= '1' when ApplicStateR = ST_RUN else '0';
programmieren, denn EnableProcR ist in Wirklichkeit ApplicStateR_var(3), also ein Registerausgang.

Fazit one-hot ist bei State-Machines oft viel effizienter.

Weiter zu Schritt 1.