Menu

Máquinas de estados finitos en Verilog: el patrón FSM estándar

Cómo escribir una FSM de Verilog como hacen los profesionales - un registro de estado sincronizado por reloj, un bloque combinacional de siguiente estado y una separación limpia que es fácil de leer y sintetizar.

Esta página incluye editores ejecutables: edita, ejecuta y ve el resultado al instante.

Qué es una FSM

Una máquina de estados finitos es un controlador que:

  • Mantiene uno de un conjunto fijo de estados con nombre en cualquier momento dado.
  • Transita entre estados basándose en entradas (y posiblemente en el estado actual).
  • Produce salidas que dependen del estado actual (y quizá de las entradas actuales).

Eso cubre una cantidad sorprendente de diseño digital: controladores de semáforos, transmisores UART, controladores de memoria, manejadores de protocolos de red, decodificadores de instrucciones, cualquier cosa que tenga modos de operación discretos.

Lo que hace que las FSMs se sientan distintas de la lógica de datapath es que son estados, no valores. Un contador avanza por 1, 2, 3, 4. Una FSM avanza por IDLE, FETCHING, BUSY, DONE - misma forma, pero los estados tienen nombre y las transiciones tienen significado.

El patrón de dos procesos

La FSM estándar de Verilog usa dos bloques always:

  1. Un bloque sincronizado por reloj que captura el registro de estado en cada flanco de reloj. Usa asignación no bloqueante. Pequeño - normalmente tres líneas.
  2. Un bloque combinacional que usa un case sobre el estado actual para calcular el siguiente estado y las salidas. Usa asignación bloqueante. El código "interesante" vive aquí.

Esta separación tiene tres beneficios: te fuerza a pensar en el estado explícitamente, produce hardware que mapea limpiamente a "un registro más un bloque combinacional" y es el estándar - cualquiera que lea tu Verilog lo reconocerá al instante.

Un ejemplo trabajado: detector de secuencia

Una FSM clásica para enseñar: detecta la secuencia de bits 1011 en una entrada serie. Saca un pulso de un ciclo cuando la secuencia se completa.

La estructura de dos bloques es el punto de bala de arriba. Los detalles de limpieza valen la pena señalarlos:

  • Valores por defecto al inicio del bloque combinacional. next_state = state (quédate donde estás) y detected = 1'b0 (sin pulso) son las asignaciones "no hagas nada". Cada rama del case solo establece lo que difiere. Esto hace imposible inferir un latch.
  • localparam para los nombres de estado. Quien lea el módulo piensa en S0, S1, S2, S3, no en 3'd0, 3'd1. El sintetizador hace la sustitución.
  • Sin salidas desde el bloque sincronizado. Toda la lógica de "qué hace este estado" vive en el bloque combinacional. El bloque sincronizado no es responsable de nada excepto mantener el estado actual.

Moore vs Mealy

Moore: la salida depende solo del estado actual. Mealy: la salida depende del estado actual y de las entradas actuales.

En el ejemplo de arriba, detected se establece dentro de la rama S3 solo cuando in coincide con uno de los patrones esperados que completan la secuencia. Esa es una salida Mealy - depende de in además de state. Una versión Moore tendría un estado separado para "recién detectado" y pondría detected = 1 cuando ese estado estuviese activo; la salida pulsaría un ciclo más tarde pero nunca sería sensible a un glitch en in.

Los dos estilos son válidos. Moore es el por defecto en los libros de texto porque las salidas están garantizadas a no tener glitches cuando las entradas cambian a mitad de ciclo. Mealy es más rápida (sin latencia de registro para salidas dirigidas por entrada) y produce hardware más pequeño en muchos casos. Elige según el protocolo que estés implementando.

Codificación de estados: binaria, one-hot, Gray

El patrón de bits que asignas a cada estado importa para el área y la velocidad:

  • Binaria (S0 = 3'd0, S1 = 3'd1, ...): registro de estado más pequeño - log2N\lceil \log_2 N \rceil bits para NN estados. Lógica de decodificación máxima.
  • One-hot (S0 = 4'b0001, S1 = 4'b0010, ...): N bits para N estados. La lógica de decodificación es trivial (cada estado es un wire); las transiciones son rápidas. Las FPGAs a menudo lo eligen por defecto.
  • Código Gray: estados consecutivos difieren en un bit. Útil cuando los bits de estado cruzan dominios de reloj.

La mayoría de las herramientas de síntesis modernas eligen la codificación por ti (Vivado, Quartus, Design Compiler todos tienen un modo automático que prueba cada una y elige la mejor). Rara vez necesitas especificar. Especifica cuando lo hagas: la mayoría de las herramientas aceptan una anotación attribute o un pragma (* fsm_encoding = "one_hot" *).

Una variante de tres bloques

Ocasionalmente verás la FSM dividida en tres: un bloque sincronizado para el estado, un bloque combinacional para el siguiente estado, un bloque combinacional para las salidas. Eso es el patrón de dos bloques con el cálculo de salidas sacado a su propio bloque:

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

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

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

El estilo de salidas separadas es útil cuando las salidas son grandes y la lógica de siguiente estado quedaría desordenada si estuvieran en el mismo bloque. Para FSMs pequeñas es excesivo.

Qué hace default en una FSM

La sentencia case de cada FSM debería tener una rama default. Dos razones:

  1. Seguridad: si el registro de estado de alguna manera toma un valor inválido (corrupción, bug, reset parcial), default lo devuelve a un estado conocido.
  2. Pista de síntesis: cuando los cases explícitos son exhaustivos (un estado de 2 bits con los 4 valores manejados, por ejemplo), default: next_state = 'x; le dice al sintetizador "prometo que el default es inalcanzable, optimiza libremente". Si el camino inalcanzable se alcanza en simulación, el x resultante se propaga y surge el bug inmediatamente.
default: begin
    next_state = S0;   // safe recovery
    // or
    next_state = 'x;   // unreachable, optimize freely
end

Elige según si has probado que el default es realmente inalcanzable.

Cosas que vigilar

Olvidar los valores por defecto al inicio del bloque combinacional. Sin next_state = state y los valores por defecto de las salidas, una rama que no asigna todo deja escapar un latch.

Poner salidas en el bloque sincronizado. Si detected <= 1 vive en el bloque always @(posedge clk), la salida está registrada - aparece un ciclo tarde. Eso puede ser intencional (una salida "Mealy registrada"), pero es un error de diseño accidental común cuando la especificación pide un pulso instantáneo.

Mezclar blocking y non-blocking. Bloque sincronizado: <=. Bloque combinacional: =. Mezclar los dos dentro de un bloque es una condición de carrera.

Un bloque always combinacional que referencia next_state y asigna state. Eso construye un bucle de realimentación que el simulador no puede resolver. El bloque sincronizado posee state; el combinacional posee next_state; nunca dejes que ninguno toque la variable del otro.

Qué viene a continuación

Ya puedes construir cualquier controlador que puedas describir. El siguiente capítulo da un paso atrás del diseño sintetizable y cubre los testbenches que lo ejercitan - cómo excitar estímulos, observar salidas, volcar formas de onda y validar que tus módulos realmente hacen lo que crees.

Preguntas frecuentes

¿Qué es una máquina de estados finitos en Verilog?

Una FSM es un controlador que mantiene uno de un pequeño conjunto de estados con nombre y transita entre ellos según las entradas. En Verilog, la implementación estándar tiene dos bloques: un always sincronizado por reloj que actualiza el registro de estado en cada flanco de reloj, y un always combinacional que calcula el siguiente estado y las salidas basándose en el estado actual y las entradas.

¿Cuál es el patrón FSM estándar en Verilog?

FSM de dos procesos: un bloque sincronizado always @(posedge clk) mantiene el registro de estado y usa asignación no bloqueante, y un bloque combinacional always @(*) usa un case sobre el estado actual para calcular el siguiente estado y las salidas. Esta separación hace el código fácil de leer, hacer lint y sintetizar limpiamente.

¿Cuál es la diferencia entre una FSM Mealy y Moore?

En una FSM Moore, las salidas dependen solo del estado actual. En una FSM Mealy, las salidas dependen tanto del estado actual como de las entradas actuales. Las máquinas Mealy reaccionan un ciclo más rápido (sin latencia de registro para salidas dependientes de entrada) pero pueden producir glitches si las entradas cambian a mitad de ciclo. Las máquinas Moore son más lentas en un ciclo pero más predecibles - son la elección por defecto a menos que necesites la velocidad.

¿Cómo se codifican estados en Verilog?

Usa constantes localparam dentro del módulo: localparam IDLE = 3'd0; etc. Tres codificaciones comunes: binaria (estados 0, 1, 2, ... - registro de estado más pequeño), one-hot (un bit por estado, menos niveles lógicos por transición), y código Gray (estados consecutivos difieren en un bit - minimiza glitches). Las herramientas de síntesis normalmente eligen la codificación por ti; fijarla rara vez es necesario.

Coddy programming languages illustration

Aprende a programar con Coddy

COMENZAR