State machine example#
This tutorial presents how we can describe a basic behavioral state machine in Systemverilog.
Assume that we have the following specification:
The signals are written in italics. We have \(rst\) and \(clk\) as global signals that effect the circuit in every state and as a transition signal we have \(change\_state\). The states have upper and lower parts. The upper part describes the name of the state which is usually a short but meaningful word for describing what the state does and the lower part depicts what the state outputs.
Interface#
We typically begin with the interface of our circuit. We have three inputs \(rst\), \(clk\), \(change\_state\) and a three bit wide output which shows which state is currently active. Using this info we can form our module interface:
input clk, rst, change_state,
output logic [STATE_COUNT-1:0] state_active
We want to output in which state we are by using a signal which has as many bits as the number of states. Instead of writing 3-1
, we introduce the constant STATE_COUNT
(pay attention to the backtick), because in programming it is a good practice to name every constant as a reading aid. Constants in Systemverilog can be defined using localparam
keyword. The following shows the whole port list:
package state_machine_pkg;
localparam STATE_COUNT=3;
endpackage;
module state_machine
import state_machine_pkg::*;
#(
CLK_DIV_WIDTH=25, // For creating a slow clock from 100 MHz
localparam STATE_COUNT=3
)(
input clk, rst, change_state,
output logic [STATE_COUNT-1:0] state_active
);
For the STATE_COUNT
we use a constant instead of a parameter (param
), because our description does not support modifying the state count as we will see later.
State machine#
We use enum
to describe the states:
enum {FIRST, SECOND, THIRD} state, next_state;
We do not specify any code for the states and leave the state encoding to the synthesizer. state
is our state register and holds the current state. next_state
is the output of the combinational transition logic.
State register#
Let us describe an asynchronous reset for our state register:
// State machine
always_ff @(posedge clk_slow, posedge rst)
state <= rst ? state.first() : next_state;
An asynchronous reset is not dependent on any clock for the register, so our always
block should not only be sensitive to our slow clock but also to the reset signal.
The ternary operator ?:
will set the state to FIRST
if reset signal is asserted, otherwise to the next_state
.
To describe sequential logic we should use always_ff
. This gives the synthesizer the hint that this block should only describe sequential logic. If otherwise, the synthesizer will warn us in the logs.
Transition logic#
For describing transition logic we use the case
statement:
always_comb begin
next_state = state;
unique case (state)
FIRST:
begin
state_active = 1<<0;
if (change_state)
next_state = state.next();
end
SECOND:
begin
state_active = 1<<1;
if (change_state)
next_state = state.next();
end
THIRD: begin
state_active = 1<<2;
next_state = state.first();
end
endcase
end
logic clk_slow;
logic [CLK_DIV_WIDTH-1:0] clk_div_cntr;
There are multiple versions of the case
statement in Systemverilog. A typical case
statement used in programming checks for a sequence of conditions like in a if-else if-...-else
chain. If we want to describe a multiplexer which can test all the case
conditions in parallel, then we should use unique case
. Refer there for more details about case
.
always_comb
is a hint for the synthesizer that we are describing only combinational logic. If the block does not fully describe combinational logic, then the synthesizer will warn us. Another advantage is that we do not have to provide the sensitivity list or @*
.
Note next_state = state;
in the beginning of the always
block. We want next_state
to be a combinational signal, so we have to ensure that next_state
is defined for every condition in the always
block. If would have not provided this line, then the always
block would create a latch for next_state
, because next_state
would not be defined if a state is holding, e.g., state == SECOND && change_state = 1
.
Note
enum
is a class in Systemverilog and supports helpful methods like first()
and next()
. The benefits of using these methods are:
we can describe the states semantically instead of by their name: the first state or the next defined state.
if we have to modify the states, then we have less lines to modify. These are defined in 6.19.5 Enumerated type methods of the standard.
We can demonstrate the advantage of always_comb
by removing next_state = state;
. In Vivado, you should see a warning during synthesis/elaboration step that the block not only describes combinational logic but sequential logic (next_state_reg
).
Did the synthesizer create the circuit that we wanted? The schematic created at the synthesis step in Vivado is useful to check that:
In the schematic we see that rst
is connected to the CLR
input of a component called RTL_REG_ASYNC
. This is probably a register with an asynchronous reset.
Using logic
instead of reg
and wire
#
We used logic
keyword instead of reg
even they have the same functionality, because reg
resembles the word register but we can both describe combinational and sequential logic using logic
and always
. Refer to this warning note for more details.
We also do not use wire
and use logic
throughout the design, because in FPGA design typically we do not have multiple drivers on a single signal.
Clock divider#
We want to slow down our circuit to a speed which can be recognized by a human, so we introduce a clock divider using a counter.
// Clock divider signals
always_ff @(posedge clk)
clk_div_cntr += 1;
assign clk_slow = clk_div_cntr[CLK_DIV_WIDTH-1];
CLK_DIV_WIDTH
parameter defines the number of bits in the clock divider counter.
Note
FPGAs typically have special hardware blocks to generate additional clock signals with different properties (e.g., frequency, skew etc) using a clock source signal. For example Xilinx 7-series FPGAs have the following primitives:
We should prefer these primitives where possible to save resources and avoid design errors. Here we want to generate a very slow clock and keep the design simple so we opt for a counter-based approach.
Testing the circuit#
We should test our circuit before synthesizing, because
typical synthesis tools (for example Vivado) need couple of minutes to synthesize and program the FPGA
debugging the circuit in a simulator gives us insight to the internal signals.
The project uses Verilator and Gtkwave instead of Vivado for testing the circuit. Verilator typically uses C++ to test the circuit, but in this project the verification logic is mostly done in Systemverilog.
import state_machine_pkg::*;
module tb;
state_machine
#(.CLK_DIV_WIDTH(1)) // Set the minimum value for simulation purposes.
dut(clk, rst, change_state, state_active);
logic rst, clk, change_state;
logic [STATE_COUNT-1:0] state_active;
integer cycle = 0;
initial begin
$dumpfile("signals.fst");
$dumpvars();
end
assign #1 clk = ~clk;
always @(posedge dut.clk_slow) begin
cycle += 1;
end
always_comb begin
case (cycle)
1:
begin
rst = 1;
assert (state_active == 3'b001);
end
2:
begin
rst = 0;
change_state = 1;
assert (state_active == 3'b001);
end
3:
assert (state_active == 3'b010);
4:
begin
assert (state_active == 3'b100);
change_state = 0;
end
5:
assert (state_active == 3'b001);
6:
begin
rst = 1;
end
8:
$finish;
endcase
end
endmodule
Note the structure revolving around the cycle numbers. Assertions are used to automatize some parts of testing. The resulting waveform is:
Testing on hardware#
The provided makefile vsyn.mk
automatically programs the FPGA.
We should see that:
we can advance to the second and third states using the button and the third state automatically jumps to the first state
at the second state we are able to reset our circuit using the reset button.
Project files#
This project could be useful as a starter template for your project. You find
state machine
testbench and simulation Makefile for Verilator and Gtkwave
constraints file and synthesis Makefile for the Boolean board
in the following tabs:
package state_machine_pkg;
localparam STATE_COUNT=3;
endpackage;
module state_machine
import state_machine_pkg::*;
#(
CLK_DIV_WIDTH=25, // For creating a slow clock from 100 MHz
localparam STATE_COUNT=3
)(
input clk, rst, change_state,
output logic [STATE_COUNT-1:0] state_active
);
enum {FIRST, SECOND, THIRD} state, next_state;
// State machine
always_ff @(posedge clk_slow, posedge rst)
state <= rst ? state.first() : next_state;
always_comb begin
next_state = state;
unique case (state)
FIRST:
begin
state_active = 1<<0;
if (change_state)
next_state = state.next();
end
SECOND:
begin
state_active = 1<<1;
if (change_state)
next_state = state.next();
end
THIRD: begin
state_active = 1<<2;
next_state = state.first();
end
endcase
end
logic clk_slow;
logic [CLK_DIV_WIDTH-1:0] clk_div_cntr;
// Clock divider signals
always_ff @(posedge clk)
clk_div_cntr += 1;
assign clk_slow = clk_div_cntr[CLK_DIV_WIDTH-1];
endmodule
import state_machine_pkg::*;
module tb;
state_machine
#(.CLK_DIV_WIDTH(1)) // Set the minimum value for simulation purposes.
dut(clk, rst, change_state, state_active);
logic rst, clk, change_state;
logic [STATE_COUNT-1:0] state_active;
integer cycle = 0;
initial begin
$dumpfile("signals.fst");
$dumpvars();
end
assign #1 clk = ~clk;
always @(posedge dut.clk_slow) begin
cycle += 1;
end
always_comb begin
case (cycle)
1:
begin
rst = 1;
assert (state_active == 3'b001);
end
2:
begin
rst = 0;
change_state = 1;
assert (state_active == 3'b001);
end
3:
assert (state_active == 3'b010);
4:
begin
assert (state_active == 3'b100);
change_state = 0;
end
5:
assert (state_active == 3'b001);
6:
begin
rst = 1;
end
8:
$finish;
endcase
end
endmodule
# Set Bank 0 voltage
set_property CFGBVS VCCO [current_design]
# Configuration bank voltage select
set_property CONFIG_VOLTAGE 3.3 [current_design]
# These attributes help Vivado to spot for errors
# More info: https://support.xilinx.com/s/article/55660
set_property -dict {PACKAGE_PIN F14 IOSTANDARD LVCMOS33} [get_ports {clk}]
# On-board LEDs
set_property -dict {PACKAGE_PIN G1 IOSTANDARD LVCMOS33} [get_ports {state_active[0]}]
set_property -dict {PACKAGE_PIN G2 IOSTANDARD LVCMOS33} [get_ports {state_active[1]}]
set_property -dict {PACKAGE_PIN F1 IOSTANDARD LVCMOS33} [get_ports {state_active[2]}]
# On-board Buttons
set_property -dict {PACKAGE_PIN J2 IOSTANDARD LVCMOS33} [get_ports {rst}]
set_property -dict {PACKAGE_PIN J5 IOSTANDARD LVCMOS33} [get_ports {change_state}]
TOP ?= $(basename $(firstword $(wildcard *.v)))
# If only TOP.v and TOP_tb.v exist, firstword returns TOP_tb.v, lastword TOP.v
SRC ?= $(TOP:_tb=).v
# User-defined src files
SRC := $(TOP).v $(SRC) tb.cpp
# Prepend testbench model and append simulation driver
default: signals.fst
SIM_MODEL = obj_dir/V$(TOP)
# Creates the model for simulation
$(SIM_MODEL): $(SRC)
verilator \
--assert \
--cc \
--exe \
--build \
--trace-fst \
-j \
-CFLAGS -DTOP=$(TOP) \
$^
# Executes the model
signals.fst: $(SIM_MODEL)
obj_dir/V$(TOP) +trace +verilator+error+limit+10
.PRECIOUS: signals.fst
# Visualizes the signals
vis: signals.fst
gtkwave -A $<
# If signals.gtkw is provided as a Gtkwave save file, it will be read.
# Simulation driver
define TB_CPP
#define vIncFile3(x) #x
#define vIncFile2(x) vIncFile3(V##x.h)
#define vIncFile(x) vIncFile2(x)
#include vIncFile(TOP)
#define VTOP3(x) V##x
#define VTOP2(x) VTOP3(x)
#define VTOP VTOP2(TOP)
#include "verilated.h"
int main(int argc, char** argv) {
VerilatedContext context;
context.traceEverOn(true); // Enable signal dump generation
context.commandArgs(argc, argv); // Forward the arguments to the Verilated model
VTOP top(&context); // Instantiate the model
top.clk = 0;
while (!context.gotFinish()) {
context.timeInc(1); // Increment the time
top.clk = !top.clk;
top.eval(); // Evaluate the model
}
}
endef
export TB_CPP
tb.cpp:
echo "$$TB_CPP" >> $@
clean:
rm -rf obj_dir
rm -f signals.fst
SYN_TOP ?= $(basename $(lastword $(wildcard *.v)))
SRC += $(SYN_TOP).v
# If only TOP.v and TOP_tb.v exist, firstword returns TOP_tb.v, lastword TOP.v
DESIGN_FILES = \
$(SRC) \
$(DESIGN_CONSTRAINTS_FILE)
PART := xc7s50csga324-1
DESIGN_CONSTRAINTS_FILE ?= boolean.xdc
DESIGN_CONSTRAINTS_URL := \
https://www.realdigital.org/downloads/8d5c167add28c014173edcf51db78bb9.txt
# Configuration end ###########################################################
BITSTREAM := $(SYN_TOP).bit
# Default goal
.PHONY: syn
syn: program
.PHONY: program
program: program.tcl $(BITSTREAM)
vivado $(VIVADO_OPT) -source $<
.PHONY: bitstream
bitstream: $(BITSTREAM)
SYN_PROJ := syn-proj/syn.xpr
.PHONY: syn-proj
syn-proj: $(SYN_PROJ)
VIVADO_OPT := \
-tempDir /tmp \
-nojournal \
-applog \
-mode batch
$(SYN_PROJ): syn-proj.tcl
vivado $(VIVADO_OPT) -source $<
syn-proj.tcl:
@echo create_project -part $(PART) syn.xpr syn-proj > $@
@echo add_files {$(SRC)} >> $@
@echo set_property file_type SystemVerilog [get_files {$(SRC)}] >> $@
@echo add_files $(DESIGN_CONSTRAINTS_FILE) >> $@
@echo set_property top $(SYN_TOP) [current_fileset] >> $@
@echo exit >> $@
$(DESIGN_CONSTRAINTS_FILE):
curl $(DESIGN_CONSTRAINTS_URL) > $@
$(BITSTREAM): \
syn.tcl \
$(DESIGN_FILES) \
| $(SYN_PROJ)
vivado $(VIVADO_OPT) -source $<
# Run synthesis
## based on
## https://docs.xilinx.com/r/en-US/ug892-vivado-design-flows-overview/Using-Non-Project-Mode-Tcl-Commands
## TODO create a non-project flow
syn.tcl:
@echo open_project $(SYN_PROJ) > $@
@echo synth_design >> $@
@echo opt_design >> $@
@echo place_design >> $@
@echo phys_opt_design >> $@
@echo route_design >> $@
@echo report_timing_summary >> $@
@echo report_utilization >> $@
@echo report_power >> $@
@echo write_bitstream -force $(BITSTREAM) >> $@
program.tcl:
@echo open_hw_manager > $@
@echo connect_hw_server >> $@
@echo open_hw_target >> $@
@echo current_hw_device >> $@
@echo puts \"Selected device: [current_hw_device]\" >> $@
@echo set_property PROGRAM.FILE {$(BITSTREAM)} [current_hw_device] >> $@
@echo program_hw_device >> $@
@echo close_hw_manager >> $@
syn-clean:
# Does not clean .tcl files. They could have been modified by the user.
$(RM) vivado*.log
$(RM) -r syn-proj
$(RM) $(BITSTREAM)
clean: syn-clean
syn-clean-all: syn-clean
# Additionally removes files potentially user-modified files
$(RM) syn-proj.tcl syn.tcl program.tcl
$(RM) $(DESIGN_CONSTRAINTS_FILE)
clean-all: syn-clean-all