Una FSM en Verilog es una máquina de estados escrita como bloques de lógica separados, normalmente memoria de estado, lógica de transición y lógica de salida.
En el artículo anterior vimos la teoría de las Máquinas de Estados Finitos (FSM). Vimos círculos, flechas y debatimos entre Moore y Mealy. Hoy toca bajar al barro: Vamos a escribir el código.
Si buscas por internet, verás que hay mil formas de escribir una FSM en Verilog. Alguna gente lo mete todo en un solo bloque gigante, otros usan dos…
Nosotros vamos a usar la Técnica de los 3 Procesos. Es la metodología más ordenada, legible y segura contra errores (especialmente contra la creación involuntaria de Latches). Consiste en dividir mentalmente y en código la máquina en tres bloques hardware distintos.
Definición de estados
Antes de los procesos, necesitamos definir nuestros estados.
Para que el código sea legible, jamás uses números hardcodeados (case(0)). Usa constantes simbólicas.
En Verilog moderno (localparam), solemos dejar que el sintetizador elija la codificación (Binaria o One-Hot), así que nosotros solo damos nombres.
// Definición de estados con nombres legibles
localparam ESTADO_IDLE = 2'b00;
localparam ESTADO_TRABAJO = 2'b01;
localparam ESTADO_FIN = 2'b10;
// Necesitamos DOS registros de estado
reg [1:0] state; // Estado Actual (Memoria)
reg [1:0] next_state; // Próximo Estado (Calculado)Fíjate en esta distinción
- :state: Es lo que somos AHORA
- next_state: Es lo que seremos en el SIGUIENTE ciclo de reloj.
Proceso 1: memoria de estado
Este es el único bloque que tiene reloj (posedge clk) y que usa asignación no bloqueante (<=).
Su única misión es actualizar el estado actual con el valor calculado del siguiente estado o resetear la máquina.
// --- PROCESO 1: Actualización de Estado ---
always @(posedge clk) begin
if (reset) begin
state <= ESTADO_IDLE; // Reset síncrono
end else begin
state <= next_state; // Avanzamos al futuro
end
endProceso 2: lógica de próximo estado
Aquí reside la inteligencia de las transiciones (las flechas del diagrama). Es un bloque puramente combinacional (always @*).
Su misión es mirar el state actual y las entradas (inputs), y decidir cuál debe ser el next_state.
¡Peligro de Latch!
Para evitar crear memorias indeseadas, SIEMPRE debemos asignar un valor por defecto a next_state al principio del bloque. Generalmente, el valor por defecto es “quedarse igual”.
// --- PROCESO 2: Cálculo del Siguiente Estado ---
always @(*) begin
// 1. Valor por defecto (Evita Latches)
next_state = state;
// 2. Lógica de transiciones
case (state)
ESTADO_IDLE: begin
if (boton_start == 1'b1)
next_state = ESTADO_TRABAJO;
end
ESTADO_TRABAJO: begin
if (tarea_terminada == 1'b1)
next_state = ESTADO_FIN;
end
ESTADO_FIN: begin
// Volvemos al inicio incondicionalmente
next_state = ESTADO_IDLE;
end
default: next_state = ESTADO_IDLE; // Seguridad
endcase
endProceso 3: lógica de salida
Finalmente, decidimos qué hacen los pines de salida de la FPGA en función del estado.
Si estamos diseñando una máquina de Moore, las salidas solo dependen de state.
// --- PROCESO 3: Decodificación de Salidas ---
// Ejemplo: Controlar un LED y un Motor
always @(*) begin
// 1. Valores por defecto (Apagado)
led_status = 1'b0;
motor_run = 1'b0;
// 2. Activación según estado
case (state)
ESTADO_IDLE: begin
led_status = 1'b1; // LED encendido esperando
end
ESTADO_TRABAJO: begin
motor_run = 1'b1; // Motor en marcha
end
// En ESTADO_FIN usamos los valores por defecto (todo a 0)
endcase
endA veces, si la lógica de salida es muy simple, puedes sustituir este bloque always por un simple assign.
assign motor_run = (state == ESTADO_TRABAJO);
Ejemplo completo: un semáforo simple
Vamos a juntar todo en un ejemplo real. Un semáforo que cambia de Verde a Amarillo y a Rojo cuando pasa el tiempo.
Asumiremos que tenemos una entrada timer_done que nos avisa cuándo cambiar.
module semaforo (
input wire clk,
input wire rst,
input wire timer_done, // Señal externa de tiempo
output reg [2:0] luces // R, A, V
);
// 1. Definición de Estados (codificación binaria simple)
localparam VERDE = 2'b00;
localparam AMARILLO = 2'b01;
localparam ROJO = 2'b10;
reg [1:0] state, next_state;
// --- PROCESO 1: Memoria ---
always @(posedge clk) begin
if (rst) state <= VERDE;
else state <= next_state;
end
// --- PROCESO 2: Transiciones ---
always @(*) begin
next_state = state; // Por defecto: Quedarse quieto
case (state)
VERDE: begin
if (timer_done) next_state = AMARILLO;
end
AMARILLO: begin
if (timer_done) next_state = ROJO;
end
ROJO: begin
if (timer_done) next_state = VERDE;
end
default: next_state = VERDE;
endcase
end
// --- PROCESO 3: Salidas (Moore) ---
always @(*) begin
luces = 3'b000; // Por defecto todo apagado
case (state)
VERDE: luces = 3'b001; // Bit 0 ON
AMARILLO: luces = 3'b010; // Bit 1 ON
ROJO: luces = 3'b100; // Bit 2 ON
endcase
end
endmodule¿Por qué hacerlo así?
Podrías pensar: “¡Cuánto código para algo tan simple! Podría haber usado ifs anidados”.
La ventaja de esta plantilla es la Escalabilidad y Depuración.
- Si el semáforo no cambia de color, miras el Proceso 2.
- Si el semáforo está en rojo pero la luz no se enciende, miras el Proceso 3.
- Si la máquina se resetea sola, miras el Proceso 1.
Separar las preocupaciones nos permite diseñar máquinas de estados con decenas o cientos de estados sin volvernos locos.