State machine example#

This tutorial presents how we can describe a basic behavioral state machine in Systemverilog.

Assume that we have the following specification:

Figure made with TikZ

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. The following shows the whole interface:

`define STATE_COUNT 3

module state_machine 
#(CLK_DIV_WIDTH=25)  // For creating a slow clock from 100 MHz
(
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, 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:

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

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
logic clk_slow;
logic [CLK_DIV_WIDTH-1:0] clk_div_cntr;

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.

`define STATE_COUNT 3

module state_machine_tb
	(input clk);

state_machine
	#(.CLK_DIV_WIDTH(1))  // Set the minimum value for simulation purposes.
	dut(clk, rst, change_state, state_active);

logic rst, change_state;
logic [`STATE_COUNT-1:0] state_active;

integer cycle = 0;

initial begin
	$dumpfile("signals.fst");
	$dumpvars();
end

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:

`define STATE_COUNT 3

module state_machine 
#(CLK_DIV_WIDTH=25)  // For creating a slow clock from 100 MHz
(
input clk, rst, change_state,
output logic [`STATE_COUNT-1:0] state_active
);

// State machine
enum {FIRST, SECOND, THIRD} state, next_state;

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

// Clock divider
logic clk_slow;
logic [CLK_DIV_WIDTH-1:0] clk_div_cntr;

always_ff @(posedge clk)
	clk_div_cntr += 1;

assign clk_slow = clk_div_cntr[CLK_DIV_WIDTH-1];
endmodule
`define STATE_COUNT 3

module state_machine_tb
	(input clk);

state_machine
	#(.CLK_DIV_WIDTH(1))  // Set the minimum value for simulation purposes.
	dut(clk, rst, change_state, state_active);

logic rst, change_state;
logic [`STATE_COUNT-1:0] state_active;

integer cycle = 0;

initial begin
	$dumpfile("signals.fst");
	$dumpvars();
end

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