Menu

Macchine a stati finiti in Verilog: il pattern FSM standard

Come scrivere una FSM in Verilog come fanno i professionisti: un registro di stato sincronizzato dal clock, un blocco combinatorio per lo stato successivo e una separazione netta, facile da leggere e da sintetizzare.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

Cos'è una FSM

Una macchina a stati finiti è un controller che:

  • In ogni momento si trova in uno di un insieme fisso di stati con un nome.
  • Passa da uno stato all'altro in base agli ingressi (e magari allo stato corrente).
  • Produce uscite che dipendono dallo stato corrente (e forse dagli ingressi correnti).

Questo copre una quantità sorprendente di progettazione digitale: controller di semafori, trasmettitori UART, controller di memoria, gestori di protocolli di rete, decodificatori di istruzioni, tutto ciò che ha modalità di funzionamento discrete.

Ciò che rende le FSM diverse dalla logica di datapath è che lavorano con stati, non con valori. Un contatore scorre 1, 2, 3, 4. Una FSM scorre IDLE, FETCHING, BUSY, DONE: stessa forma, ma gli stati hanno un nome e le transizioni hanno un significato.

Il pattern a due processi

La FSM standard in Verilog usa due blocchi always:

  1. Un blocco sincronizzato dal clock che cattura il registro di stato a ogni fronte. Usa l'assegnazione non-blocking. È minuscolo: di solito tre righe.
  2. Un blocco combinatorio che usa un case sullo stato corrente per calcolare lo stato successivo e le uscite. Usa l'assegnazione blocking. Il codice "interessante" sta qui.

Questa separazione ha tre vantaggi: ti obbliga a ragionare esplicitamente sullo stato, produce hardware che corrisponde in modo pulito a "un registro più un blocco combinatorio" ed è lo standard: chiunque legga il tuo Verilog lo riconoscerà all'istante.

Un esempio completo: rilevatore di sequenza

Una FSM classica nei corsi: rilevare la sequenza di bit 1011 su un ingresso seriale ed emettere un impulso di un solo ciclo quando la sequenza è completa.

La struttura a due blocchi è quella descritta nell'elenco precedente. Vale la pena sottolineare alcuni dettagli di pulizia:

  • Valori predefiniti in cima al blocco combinatorio. next_state = state (resta dove sei) e detected = 1'b0 (nessun impulso) sono le assegnazioni "non fare nulla". Ogni ramo del case imposta poi solo ciò che cambia. Così è impossibile dedurre un latch.
  • localparam per i nomi degli stati. Chi legge il modulo ragiona in termini di S0, S1, S2, S3, non di 3'd0, 3'd1. La sostituzione la fa il sintetizzatore.
  • Nessuna uscita dal blocco sincronizzato. Tutta la logica del tipo "cosa fa questo stato" sta nel blocco combinatorio. Il blocco sincronizzato si occupa solo di mantenere lo stato corrente.

Moore e Mealy

Moore: l'uscita dipende solo dallo stato corrente. Mealy: l'uscita dipende dallo stato corrente e dagli ingressi correnti.

Nell'esempio precedente, detected viene impostato dentro il ramo S3 solo quando in corrisponde a uno dei pattern che completano la sequenza. È un'uscita di Mealy: dipende da in oltre che da state. Una versione di Moore avrebbe uno stato separato per "appena rilevato" e imposterebbe detected = 1 ogni volta che quello è lo stato corrente; l'impulso arriverebbe un ciclo dopo, ma non sarebbe mai sensibile a un glitch su in.

Entrambi gli stili sono validi. Nei libri di testo Moore è la scelta predefinita, perché le uscite non producono glitch quando gli ingressi cambiano a metà ciclo. Mealy è più veloce (nessuna latenza di registro per le uscite guidate dagli ingressi) e in molti casi produce hardware più piccolo. Scegli in base al protocollo che stai implementando.

Codifica degli stati: binaria, one-hot, Gray

Lo schema di bit assegnato a ogni stato conta per area e velocità:

  • Binaria (S0 = 3'd0, S1 = 3'd1, ...): il registro di stato più piccolo, ⌈log⁡2N⌉\lceil \log_2 N \rceil bit per NN stati. La logica di decodifica più complessa.
  • One-hot (S0 = 4'b0001, S1 = 4'b0010, ...): N bit per N stati. La logica di decodifica è banale (ogni stato è un wire) e le transizioni sono veloci. Sulle FPGA è spesso la scelta predefinita.
  • Codice Gray: stati consecutivi differiscono di un solo bit. È utile quando i bit di stato attraversano domini di clock diversi.

La maggior parte degli strumenti di sintesi moderni sceglie la codifica per te (Vivado, Quartus e Design Compiler hanno tutti una modalità automatica che le prova e sceglie la migliore). Raramente devi specificarla. Quando serve, la maggior parte degli strumenti accetta un'annotazione attribute o un pragma (* fsm_encoding = "one_hot" *).

Una variante a tre blocchi

Ogni tanto vedrai la FSM divisa in tre parti: un blocco sincronizzato per lo stato, un blocco combinatorio per lo stato successivo e un blocco combinatorio per le uscite. È semplicemente il pattern a due blocchi con il calcolo delle uscite spostato in un blocco proprio:

// State register
always @(posedge clk) ...

// Next-state logic
always @(*) ...

// Output logic
always @(*) begin
    case (state)
        ...
    endcase
end

Lo stile con le uscite separate è utile quando le uscite sono tante e la logica dello stato successivo diventerebbe confusa se stessero nello stesso blocco. Per FSM piccole è eccessivo.

Cosa fa default in una FSM

L'istruzione case di ogni FSM dovrebbe avere un ramo default, per due motivi:

  1. Sicurezza: se il registro di stato assume in qualche modo un valore non valido (corruzione, bug, reset parziale), default lo riporta in uno stato noto.
  2. Suggerimento per la sintesi: quando i casi espliciti sono esaustivi (per esempio uno stato a 2 bit con tutti e 4 i valori gestiti), default: next_state = 'x; dice al sintetizzatore "ti garantisco che il default è irraggiungibile, ottimizza liberamente". Se in simulazione il percorso irraggiungibile viene raggiunto, la x risultante si propaga e fa emergere subito il bug.
default: begin
    next_state = S0;   // safe recovery
    // or
    next_state = 'x;   // unreachable, optimize freely
end

Scegli in base al fatto di aver dimostrato o meno che il default è davvero irraggiungibile.

A cosa fare attenzione

Dimenticare i valori predefiniti in cima al blocco combinatorio. Senza next_state = state e i valori predefiniti delle uscite, un ramo che non assegna tutto genera un latch.

Mettere le uscite nel blocco sincronizzato. Se detected <= 1 sta nel blocco always @(posedge clk), l'uscita è registrata e compare con un ciclo di ritardo. Può essere voluto (un'uscita "Mealy registrata"), ma è un errore di progetto frequente quando la specifica richiede un impulso immediato.

Mescolare blocking e non-blocking. Blocco sincronizzato: <=. Blocco combinatorio: =. Mescolarli nello stesso blocco crea una race condition.

Un blocco always combinatorio che legge next_state e assegna state. Crea un anello di retroazione che il simulatore non riesce a risolvere. Il blocco sincronizzato possiede state, quello combinatorio possiede next_state: nessuno dei due deve mai toccare la variabile dell'altro.

Cosa viene dopo

Ora puoi costruire qualsiasi controller tu sappia descrivere. Il prossimo capitolo fa un passo indietro rispetto al progetto sintetizzabile e tratta i testbench che lo mettono alla prova: come generare gli stimoli, osservare le uscite, registrare le forme d'onda e verificare che i tuoi moduli facciano davvero ciò che pensi.

Domande frequenti

Cos'è una macchina a stati finiti in Verilog?

Una FSM è un controller che si trova in uno di un piccolo insieme di stati con un nome e passa dall'uno all'altro in base agli ingressi. In Verilog l'implementazione standard ha due blocchi: un always sincronizzato dal clock che aggiorna il registro di stato a ogni fronte, e un always combinatorio che calcola lo stato successivo e le uscite in base allo stato corrente e agli ingressi.

Qual è il pattern FSM standard in Verilog?

La FSM a due processi: un blocco sincronizzato always @(posedge clk) contiene il registro di stato e usa l'assegnazione non-blocking, mentre un blocco combinatorio always @(*) usa un case sullo stato corrente per calcolare lo stato successivo e le uscite. Questa separazione rende il codice facile da leggere, da verificare con i linter e da sintetizzare senza sorprese.

Qual è la differenza tra una FSM di Mealy e una di Moore?

In una FSM di Moore le uscite dipendono solo dallo stato corrente. In una FSM di Mealy dipendono sia dallo stato corrente sia dagli ingressi correnti. Le macchine di Mealy reagiscono un ciclo prima (nessuna latenza di registro per le uscite che dipendono dagli ingressi), ma possono produrre glitch se gli ingressi cambiano a metà ciclo. Le macchine di Moore sono più lente di un ciclo ma più prevedibili: sono la scelta predefinita, a meno che non ti serva velocità.

Come si codificano gli stati in Verilog?

Usa costanti localparam dentro il modulo: localparam IDLE = 3'd0; e così via. Tre codifiche comuni: binaria (stati 0, 1, 2, ...: il registro di stato più piccolo), one-hot (un bit per stato, meno livelli logici per transizione) e codice Gray (stati consecutivi differiscono di un solo bit: riduce al minimo i glitch). Di solito gli strumenti di sintesi scelgono la codifica per te; fissarla è raramente necessario.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA