SystemVerilog ofrece ventajas significativas frente a Verilog tradicional a la hora de modelar Máquinas de Estado Finito (FSM). No entraremos en detalles sobre la teoría del diseño de máquinas de estados finitos -suponemos que el lector ya está familiarizado con el diseño de FSM's- en caso contrario te recomiendo echar un vistazo a este tutorial, si no que nos centraremos en presentar las mejoras que ofrece SystemVerilog.
Algunas de estas ventajas son: uso de Tipos enumerados (enum) para los estados, separación de lógica secuencial y combinacional, nuevas declaraciones unique case y priority case, además SystemVerilog permite detectar bugs que en Verilog pasan desapercibidos, tales como estados no inicializados, condiciones solapadas, transiciones olvidadas, asignaciones ilegales a estados, etc. Veamos en detalle alguna de estas mejoras.
En el Tema 3, se vio el tipo de datos enumarados user-defined que SystemVerilog añade a Verilog tradicional. Los tipos enumerados permiten definir un conjunto restringido de valores válidos para variable . Usar typedef enum en SystemVerilog para definir los estados de una FSM es una mejora enorme frente al estilo tradicional de Verilog (parameter o localparam) permitiendo un mayor nivel de abstracción al mismo tiempo que se describe un comportamiento de hardware preciso y sintetizable. Otras ventajas son:
Nombres simbólicos: Los tipos enumerados permiten definir estados con nombres descriptivos (ej. IDLE, READ, LOAD) en lugar de números binarios crudos. El código es más limpio y fácil de entender ya que los valores se representan mediante etiquetas en lugar de valores lógicos digitales. El ejemplo que se muestra a continuación, describe los tres estados posibles de la FSM de un ascensor, empleando variables de tipo enumerado para el estado actual y el estado siguiente.
typedef enum logic [1:0] {STOP, SUBIR, BAJAR} state_t;
state_t current_state, next_state;
Frente a Verilog:
parameter STOP = 2'b00, SUBIR = 2'b01, BAJAR = 2'b10;
reg [1:0] current_state, next_state;
Tipado fuerte (Seguridad de Tipos): Al usar enum, las herramientas de simulación y síntesis pueden detectar si se asigna un valor ilegal a la máquina de estados, evitando errores de diseño comunes que en Verilog tradicional serían difíciles de encontrar; los únicos valores posibles de las variables current_state y next_state son los que figuran en la lista, lo que da lugar a menos errores.
state_t current_state, next_state;
En Verilog reg [1:0] puede tomar cualquier valor (00, 01, 10, 11) → más riesgo de errores.
reg [1:0] current_state;
Codificación automática (opcional): Podemos dejar que el compilador elija la codificación (binario, one-hot, gray...)
typedef enum {STOP, SUBIR, BAJAR} state_t;
Codificación definida por el usuario: En SystemVerilog el usuario también puede definir el tipo base de una variable enumerada. Esto permite utilizar un tipo base de cuatro estados, como por ejemplo logic, para representar con mayor precisión el comportamiento del hardware. Se puede especificar además el valor lógico de las etiquetas, representando explícitamente las codificaciones de los estados; one-hot, Gray, binario o cualquier otro tipo de codificación deseada.
typedef enum logic [1:0] {
STOP = 2'b00,
SUBIR = 2'b01,
BAJAR = 2'b10
} state_t;
state_t current_state, next_state;
Al modelar una FSM es importante que el código sea legible y fiel al resultado que buscamos. Para lograr este objetivo lo más recomendable es describir la FSM empleando el estilo de tres bloques always en el que cada uno de los tres bloques modela un proceso independiente:
Un bloque always para el circuito secuencial que almacena el estado actual. Al ser un circuito secuencial utilizamos always_ff y asignaciones non-blocking <=.
Un segundo bloque always para describir el circuito combinacional que genera el estado siguiente. Con los circuitos combinacionales usamos always_comb y asignaciones blocking =. Las transiciones se implementan con declaraciones if-else, debe haber tantas condiciones if-else como transiciones en el diagrama de estados.
El tercer bloque always se corresponde con el circuito combinacional que genera las salidas en función del estado actual. De igual manera que en el punto 2, empleamos always_comb y asignaciones blocking =. También se pueden emplear asignaciones continuas para describir la lógica de salida, si ésta es relativamente sencilla.
SystemVerilog ofrece variantes especiales de la declaración case como unique y priority . Las palabras clave unique case y priority case se usan para modificar el comportamiento y las comprobaciones de estructuras como case o if, añadiendo intención de diseño + comprobaciones automáticas.
3.1 unique case indica que sólo una condición debe ser verdadera; si dos casos coinciden a la vez → error o si ningún caso coincide (y no hay default) → error.
always_comb begin
unique case (state)
IDLE: y = 0;
LOAD: y = 1;
STORE: y = 2;
endcase
end
Usar unique case si:
Solo una condición puede ser verdadera, es decir, los casos son mutuamente excluyentes y se cubren todos los posibles valores.
Decodificadores bien definidos.
Estás describiendo estados de una FSM.
Para detectar bugs de diseño.
3.2 priority case se usa cuando puede haber varias condiciones verdaderas, pero se ejecuta la primera, es decir, hay prioridad, como en if-else. Utilizamos priority case cuando buscamos prioridad explícita (ej: arbitraje) o un comportamiento tipo if → else if anidado.
En simulación:
Si ninguna condición es verdadera → warning (si no hay default).
NO da error por solapamiento (porque la prioridad lo permite).
En síntesis:
Genera lógica con prioridad (tipo encoder prioritario).
Por ejemplo:
priority case (opcode)
4'b1???: result = A;
4'b10??: result = B;
4'b1001: result = C;
default: result = D;
endcase
Se evalúa de arriba hacia abajo, si opcode = 4'b1001 coincide con los tres primeros, pero solamente se ejecuta el primero (result = A). Si ningún caso coincide → warning de simulación.
Usar priority case si:
El orden importa y representa lógica real.
Hay excepciones.
Es lógico que varias condiciones coincidan.
Como ejemplo de aplicación vamos a describir una FSM que controla un ascensor de cuatro plantas.
Las entradas de la FSM son: por un lado la variable [2:0]comparador que es la salida de un circuito combinacional que compara la posición actual del ascensor con el piso de llamada y devuelve los siguientes valores:
3'b001 si el piso de llamada esta por encima del piso actual --> SUBIR
3'b010 si el piso de llamada está por debajo del piso actual --> BAJAR
3'b100 sin son iguales --> STOP.
Otras entradas son los switches que detectan si la puerta está abierta (door_open) o cerrada (door_closed).
Las salidas de la FSM son las señales de control [2:0]L298 y [2:0]L298P que aplicadas a un driver L298 servirán para controlar el funcionamiento de dos motres de CC que accionan la cabina del ascensor y la apertura y cierre de la puerta.
Los ledes conectados en [2:0]cs_led (current_state) servirán para tareas de monitoreo y depuración, así como para verificar que la FSM transiciona correctamente entre los estados.
En el código que se muestra a continuación declaramos las entradas y salidas, además de los estados de la FSM empleando tipos enumerados, etiquetas y un asignamos valor binario a cada estado.
// FSM Ascensor + Puerta
module elevator (
input logic clk,
input logic rst,
input logic door_open,
input logic door_closed,
input logic [2:0] comparador,
output logic [2:0] L298,
output logic [2:0] L298P,
output logic [2:0] cs_led
);
assign cs_led = current_state;
// Definición de estados
typedef enum logic [2:0] {
STOP = 3'b000,
SUBIR = 3'b001,
BAJAR = 3'b011,
ABRIENDO_PUERTA = 3'b010,
PUERTA_ABIERTA = 3'b110,
CERRANDO_PUERTA = 3'b111,
PUERTA_CERRADA = 3'b101
} state_t;
state_t current_state, next_state;
En la siguiente imagen podemos ver el diagrama de estados de la FSM tipo Moore, las condiciones para cada transición y el valor de las salidas en cada estado.
Para describir la FSM empleamos el estilo de tres bloques always.
Bloque always nº1 para el circuito secuencial que almacena el estado actual. Al ser un circuito secuencial utilizamos always_ff y asignaciones non-blocking <=.
// 1 ️Registro de estado
always_ff @(posedge clk or posedge rst) begin
if (rst)
current_state <= STOP;
else
current_state <= next_state;
end
Bloque always nº2 para describir el circuito combinacional que genera el estado siguiente.
// 2 Lógica de siguiente estado
always_comb begin
// Por defecto quedarse en el mismo estado
next_state = current_state;
case (current_state)
STOP: next_state = ABRIENDO_PUERTA;
ABRIENDO_PUERTA: if (door_open == 1) next_state = PUERTA_ABIERTA;
else next_state = ABRIENDO_PUERTA;
PUERTA_ABIERTA: if (comparador == 3'b001 | comparador == 3'b010 ) next_state = CERRANDO_PUERTA;
else next_state = PUERTA_ABIERTA;
CERRANDO_PUERTA: if (door_closed == 1) next_state = PUERTA_CERRADA;
else next_state = CERRANDO_PUERTA;
PUERTA_CERRADA: if (comparador == 3'b001) next_state = SUBIR; // 3'b001 = SUBIR;
else next_state = BAJAR; // 3'b010 = BAJAR;
SUBIR: if (comparador == 3'b100) next_state = STOP;
else next_state = SUBIR;
BAJAR: if (comparador == 3'b100) next_state = STOP;
else next_state = BAJAR;
default: next_state = STOP; // seguridad
endcase
end
El bloque always nº3 se corresponde con el circuito combinacional que genera las salidas en función del estado actual.
// 3 Lógica de salida
always_comb begin
case (current_state)
STOP: begin // 0
L298 = 3'b000;
L298P = 3'b000;
end
SUBIR: begin // 1
L298 = 3'b110;
L298P = 3'b000;
end
BAJAR: begin // 2
L298 = 3'b101;
L298P = 3'b000;
end
ABRIENDO_PUERTA: begin // 3
L298 = 3'b000;
L298P = 3'b110;
end
PUERTA_ABIERTA: begin // 4
L298 = 3'b000;
L298P = 3'b000;
end
CERRANDO_PUERTA: begin
L298 = 3'b000;
L298P = 3'b101;
end
PUERTA_CERRADA: begin
L298 = 3'b000;
L298P = 3'b000;
end
default: begin
L298 = 3'b000;
L298P = 3'b000;
end
endcase
end
endmodule
Simplificando un poco, el circuito queda como sigue: el encoder ENC_FFCC memoriza el piso actual mediante cuatro finales de carrera, uno por cada piso. Otro encoder ENC_PPBB memoriza el pulsador de llamada. La salida de los encoder se aplica a un comparador que devuelve un valor u otro en función de si el piso de destino es igual, superior o inferior al origen. Con este valor la FSM habilita el motor del ascensor para subir, bajar o detenerse cuando el piso actual coincide con el destino solicitado.
El circuito es en realidad algo más complejo de lo que muestra la imagen, ya que he añadido también un debouncer para cada pulsador y final de carrera. El código se puede descarar al final de la página (también el del encoder y compadaror) no lo pego aquí para que no quede muy extenso.
Esquema VIVADO
Las salidas proporcionadas por la BASYS 3 en los conectores PMOD se aplican a un servo L298 para gobernar los dos motores de CC de cabina y de puerta.
Los swiches door_open y door_closed se simulan con los switches de la BASYS 3, así como los FFCC de cada piso. Los pulsadores de llamada se implementan también con los propios pulsadores de la tarjeta BASYS 3.
Esquema BASYS 3 - L298
Montaje BASYS 3 - L298 - Motores
· Indicar explícitamente el tamaño del vector necesario para definir el valor de las variables typedef enum.
· Reset asíncrono y activo a nivel bajo (preferiblemente). Usar un reset asíncrono permite llevar la FSM a un estado inicial seguro.
· No poner lógica de decisión (if/else complejos) dentro del always_ff. Mantener este bloque lo más minimalista posible: solo la transferencia state_reg <= state_next.
· Usar el estilo de tres bloques always para mayor legibilidad y mejor mantenimiento del código.
· Dentro del bloque always_comb las declaraciones case deben ser completas, usar siempre un default en la sentencia case. Esto previene la inferencia de latches no deseados y asegura que la FSM siempre tenga un destino conocido si ocurre un error.
Eso es todo por el momento, espero que os haya gustado.
¡Hasta pronto!
Puedes descargar el proyecto en VIVADO en el siguiente enlace.