Menu

Finite State Machines em Verilog: O padrão FSM padrão

Como escrever uma FSM Verilog do jeito que profissionais fazem - um state register clocked, um bloco combinacional de next-state e uma separação limpa fácil de ler e sintetizar.

Esta página tem editores executáveis - edite, execute e veja a saída na hora.

O que uma FSM é

Uma máquina de estado finita é um controlador que:

  • Mantém um de um conjunto fixo de estados nomeados em qualquer momento.
  • Transita entre estados com base em entradas (e possivelmente no estado atual).
  • Produz saídas que dependem do estado atual (e talvez das entradas atuais).

Isso cobre uma quantidade impressionante de design digital: controladores de semáforo, transmissores UART, controladores de memória, handlers de protocolo de rede, decodificadores de instrução, qualquer coisa que tem modos discretos de operação.

A coisa que faz FSMs parecerem diferentes de lógica de datapath é que são estados, não valores. Um contador caminha por 1, 2, 3, 4. Uma FSM caminha por IDLE, FETCHING, BUSY, DONE - mesma forma, mas os estados têm nomes e as transições têm significado.

O padrão de dois processos

A FSM Verilog padrão usa dois blocos always:

  1. Um bloco clocked que captura o state register em cada borda de clock. Usa non-blocking assignment. Pequeno - geralmente três linhas.
  2. Um bloco combinacional que usa um case no estado atual para calcular o próximo estado e as saídas. Usa blocking assignment. O código "interessante" vive aqui.

Essa separação tem três benefícios: força você a pensar sobre estado explicitamente, produz hardware que mapeia limpinho para "um register mais um bloco combinacional" e é o padrão - todo mundo que ler seu Verilog vai reconhecer instantaneamente.

Um exemplo trabalhado: Detector de sequência

Uma FSM clássica de ensino: detectar a sequência de bits 1011 em uma entrada serial. Sai um pulso de um único ciclo quando a sequência completa.

A estrutura de dois blocos é o bullet point acima. Os detalhes de limpeza valem a pena apontar:

  • Defaults no topo do bloco combinacional. next_state = state (fique onde está) e detected = 1'b0 (sem pulso) são as atribuições "não fazer nada". Cada ramo do case então só seta as coisas que diferem. Isso torna impossível inferir um latch.
  • localparam para nomes de estado. Quem lê o module pensa em S0, S1, S2, S3, não em 3'd0, 3'd1. O sintetizador faz a substituição.
  • Sem saídas do bloco clocked. Toda a lógica "o que esse estado faz" vive no bloco combinacional. O bloco clocked é responsável por nada exceto manter o estado atual.

Moore vs Mealy

Moore: saída depende apenas do estado atual. Mealy: saída depende do estado atual e das entradas atuais.

No exemplo acima, detected é setado dentro do ramo S3 apenas quando in casa com um dos padrões esperados de finalização de sequência. Isso é uma saída Mealy - depende de in além de state. Uma versão Moore teria um estado separado para "acabou de detectar" e setaria detected = 1 sempre que esse estado estivesse atual; a saída pulsaria um ciclo depois mas nunca seria sensível a um glitch em in.

Ambos os estilos são válidos. Moore é o padrão em livros didáticos porque saídas são garantidas a não ter glitch quando entradas mudam no meio do ciclo. Mealy é mais rápido (sem latência de register para saídas dirigidas por entrada) e produz hardware menor em muitos casos. Escolha com base no protocolo que está implementando.

State Encoding: Binário, One-Hot, Gray

O padrão de bits que você atribui a cada estado importa para área e velocidade:

  • Binário (S0 = 3'd0, S1 = 3'd1, ...): state register menor - log2N\lceil \log_2 N \rceil bits para NN estados. Lógica de decodificação máxima.
  • One-hot (S0 = 4'b0001, S1 = 4'b0010, ...): N bits para N estados. Lógica de decodificação é trivial (cada estado é um wire); transições são rápidas. FPGAs frequentemente padronizam para isso.
  • Gray code: estados consecutivos diferem por um bit. Útil quando bits de estado atravessam domínios de clock.

A maioria das ferramentas modernas de síntese escolhe a codificação por você (Vivado, Quartus, Design Compiler todas têm um modo automático que tenta cada uma e escolhe a melhor). Você raramente precisa especificar. Especifique quando precisar: a maioria das ferramentas aceita uma anotação attribute ou um pragma (* fsm_encoding = "one_hot" *).

Uma variante de três blocos

Você ocasionalmente verá a FSM dividida em três formas: um bloco clocked para estado, um bloco combinacional para next-state, um bloco combinacional para saídas. Esse é apenas o padrão de dois blocos com a computação de saída içada para seu próprio bloco:

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

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

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

O estilo de saída separada é útil quando saídas são grandes e a lógica de next-state ficaria poluída se elas estivessem no mesmo bloco. Para FSMs pequenas é exagero.

O que default faz em uma FSM

Toda declaração case de FSM deve ter um ramo default. Duas razões:

  1. Segurança: se o state register de alguma forma assume um valor inválido (corrupção, bug, reset parcial), default o traz de volta para um estado conhecido.
  2. Dica de síntese: quando os casos explícitos são exaustivos (um estado de 2 bits com todos os 4 valores tratados, por exemplo), default: next_state = 'x; diz ao sintetizador "eu prometo que o default é inalcançável, otimize livremente". Se o caminho inalcançável for atingido em simulação, o x resultante propaga e exibe o bug imediatamente.
default: begin
    next_state = S0;   // recuperacao segura
    // ou
    next_state = 'x;   // inalcancavel, otimize livremente
end

Escolha com base em se você provou que o default é realmente inalcançável.

Coisas para vigiar

Esquecer defaults no topo do bloco combinacional. Sem next_state = state e os defaults de saída, um ramo que não atribui tudo vaza um latch.

Colocar saídas no bloco clocked. Se detected <= 1 vive no bloco always @(posedge clk), a saída é registrada - aparece um ciclo atrasada. Isso pode ser intencional (uma saída "Mealy registrada"), mas é um erro de design acidental comum quando a spec pede um pulso instantâneo.

Misturar blocking e non-blocking. Bloco clocked: <=. Bloco combinacional: =. Misturar os dois dentro de um bloco é uma race condition.

Um bloco always combinacional que referencia next_state e atribui state. Isso constrói um loop de feedback que o simulador não consegue resolver. O bloco clocked dona state; o bloco combinacional dona next_state; nunca deixe nenhum tocar na variável do outro.

O que vem a seguir

Você agora consegue construir qualquer controlador que conseguir descrever. O próximo capítulo dá um passo para trás do design sintetizável e cobre os testbenches que o exercitam - como acionar estímulo, observar saídas, dumpar formas de onda e validar que seus modules de fato fazem o que você acha que fazem.

Perguntas frequentes

O que é uma finite state machine em Verilog?

Uma FSM é um controlador que mantém um de um pequeno conjunto de estados nomeados e transita entre eles com base em entradas. Em Verilog, a implementação padrão tem dois blocos: um always clocked que atualiza o state register em cada borda de clock, e um always combinacional que calcula o próximo estado e as saídas com base no estado atual e nas entradas.

Qual é o padrão FSM padrão em Verilog?

FSM de dois processos: um bloco always @(posedge clk) clocked mantém o state register e usa non-blocking assignment, e um bloco always @(*) combinacional usa um case no estado atual para calcular o próximo estado e as saídas. Essa separação torna o código fácil de ler, fazer lint e sintetizar limpamente.

Qual a diferença entre uma FSM Mealy e Moore?

Em uma FSM Moore, saídas dependem apenas do estado atual. Em uma FSM Mealy, saídas dependem tanto do estado atual quanto das entradas atuais. Máquinas Mealy reagem um ciclo mais rápido (sem latência de register para saídas dependentes de entrada) mas podem produzir glitches se entradas mudarem no meio do ciclo. Máquinas Moore são mais lentas por um ciclo mas mais previsíveis - são a escolha padrão a menos que você precise da velocidade.

Como codifico estados em Verilog?

Use constantes localparam dentro do module: localparam IDLE = 3'd0; etc. Três codificações comuns: binária (estados 0, 1, 2, ... - state register menor), one-hot (um bit por estado, menos níveis lógicos por transição) e gray code (estados consecutivos diferem por um bit - minimiza glitches). Ferramentas de síntese geralmente escolhem a codificação por você; fixar raramente é necessário.

Coddy programming languages illustration

Aprenda a programar com o Coddy

COMEÇAR