Introduction to FPGA Part 7 - Verilog Testbenches and Simulation
2021-12-20 | By ShawnHymel
License: Attribution
A field-programmable gate array (FPGA) is a reconfigurable integrated circuit (IC) that lets you implement a wide range of custom digital circuits. Throughout the series, we will examine how an FPGA works as well as demonstrate the basic building blocks of implementing digital circuits using the Verilog hardware description language (HDL).
FPGA engineers often simulate designs before uploading them to a hardware device. This allows them to check the functionality of their Verilog code to ensure everything works properly. While writing a testbench can take some time, it’s often faster than uploading the original design to the FPGA and probing lines with a logic analyzer over and over.
In the previous tutorial, we introduced the concept of modular designs in Verilog and demonstrated how to pass parameters from the instantiating module to the instantiated module. In this guide, we show how to write Verilog testbenches and simulate designs using Icarus Verilog (iverilog).
Video
If you have not done so, please watch the following video, which explains the concepts required to complete the challenge. It also demonstrates a working version of the challenge:
Required Hardware
For this challenge, you will need the following hardware:
- FPGA development board based on the Lattice iCE40. I recommend the iCEstick for this series. However, any of the development boards listed as “supported” by the apio project should work.
- Breadboard
- Pushbuttons
- Jumper wires
- (Optional) USB extension cable
Hardware Connections
The PMOD connector at the end of the iCEstick has the following pinout:
A full pinout of the iCEstick can be found here.
Connect 4 pushbuttons to the iCEstick as follows. Note that you do not need pull-up resistors on the buttons. We will use the internal pull-up resistors available in the FPGA.
Resources
The following datasheets and guides might be helpful as you tackle the challenges:
- GitHub repository - contains all examples and solutions for this series
- Verilog documentation
- Apio tool usage
- iCE40 LP/HX Datasheet
- iCEstick Evaluation Kit User’s Guide
Challenge
Your challenge is to write a testbench and simulate your button debouncing design from the episode 5 challenge. Your testbench should toggle the increment button line (from high to low to simulate an actual button press). Each time there is a full button press (e.g. the simulated user holds the button down for longer than some predetermined amount of time), the counter increments by one.
Note that you should simulate some form of button bounce. If I zoom in on one of the transitions in my solution, you can see how the inc_btn line toggles rapidly a few times before settling on a value.
Here are a few hints:
- You might need to use the Verilog system functions $random or $urandom to generate random numbers for your bounce simulation
- You can use for loops in testbench Verilog code (and sometimes synthesizable code, too)
- You might need to adjust your simulated clock speed or timer delay. Trying to simulate 40 ms with a 12 MHz will take a long time and produce a very large .vcd file!
Solution
Spoilers below! I highly encourage you to try the challenge on your own before comparing your answer to mine. Note that my solution may not be the only way to solve the challenge.
I made a slight adjustment to the original button debouncing code: I turned the MAX_CLK_COUNT value into a parameter instead of a local parameter. This allows me to instantiate the debounce module with a different wait period so I can more easily simulate the design.
button-debouncing.v
// Use a state machine to debounce the button, which increments a counter
module debounced_counter #(
// Parameters
parameter MAX_CLK_COUNT = 20'd480000 - 1
) (
// Inputs
input clk,
input rst_btn,
input inc_btn,
// Outputs
output reg [3:0] led
);
// States
localparam STATE_HIGH = 2'd0;
localparam STATE_LOW = 2'd1;
localparam STATE_WAIT = 2'd2;
localparam STATE_PRESSED = 2'd3;
// Internal signals
wire rst;
wire inc;
// Internal storage elements
reg [1:0] state;
reg [19:0] clk_count;
// Invert active-low buttons
assign rst = ~rst_btn;
assign inc = ~inc_btn;
// State transition logic
always @ (posedge clk or posedge rst) begin
// On reset, return to idle state and restart counters
if (rst == 1'b1) begin
state <= STATE_HIGH;
led <= 4'd0;
// Define the state transitions
end else begin
case (state)
// Wait for increment signal to go from high to low
STATE_HIGH: begin
if (inc == 1'b0) begin
state <= STATE_LOW;
end
end
// Wait for increment signal to go from low to high
STATE_LOW: begin
if (inc == 1'b1) begin
state <= STATE_WAIT;
end
end
// Wait for count to be done and sample button again
STATE_WAIT: begin
if (clk_count == MAX_CLK_COUNT) begin
if (inc == 1'b1) begin
state <= STATE_PRESSED;
end else begin
state <= STATE_HIGH;
end
end
end
// If button is still pressed, increment LED counter
STATE_PRESSED: begin
led <= led + 1;
state <= STATE_HIGH;
end
// Default case: return to idle state
default: state <= STATE_HIGH;
endcase
end
end
// Run counter if in wait state
always @ (posedge clk or posedge rst) begin
if (rst == 1'b1) begin
clk_count <= 0;
end else begin
if (state == STATE_WAIT) begin
clk_count <= clk_count + 1;
end else begin
clk_count <= 0;
end
end
end
endmodule
Here is the testbench Verilog code (note the “_tb.v” suffix on the filename!):
button-debouncing_tb.v
// Define timescale
`timescale 1 us / 10 ps
// Define our testbench
module button_debouncing_tb();
// Internal signals
wire [3:0] out;
// Storage elements (buttons are active low!)
reg clk = 0;
reg rst_btn = 1;
reg inc_btn = 1;
integer i; // Used in for loop
integer j; // Used in for loop
integer prev_inc; // Previous increment button state
integer nbounces; // Holds random number
integer rdelay; // Holds random number
// Simulation time: 10000 * 1 us = 10 ms
localparam DURATION = 10000;
// Generate clock signal (about 12 MHz)
always begin
#0.04167
clk = ~clk;
end
// Instantiate debounce/counter module (use about 400 us wait time)
debounced_counter #(.MAX_CLK_COUNT(4800 - 1)) uut (
.clk(clk),
.rst_btn(rst_btn),
.inc_btn(inc_btn),
.led(out)
);
// Test control: pulse reset and create some (bouncing) button presses
initial begin
// Pulse reset low to reset state machine
#10
rst_btn = 0;
#1
rst_btn = 1;
// We can use for loops in simulation!
for (i = 0; i < 32; i = i + 1) begin
// Wait some time before pressing button
#1000
// Simulate a bouncy/noisy button press
// $urandom generates a 32-bit unsigned (pseudo) random number
// "% 10" is "modulo 10"
prev_inc = inc_btn;
nbounces = $urandom % 20;
for (j = 0; j < nbounces; j = j + 1) begin
#($urandom % 10)
inc_btn = ~inc_btn;
end
// Make sure button ends up in the opposite state
inc_btn = ~prev_inc;
end
end
// Run simulation (output to .vcd file)
initial begin
// Create simulation output file
$dumpfile("button-debouncing_tb.vcd");
$dumpvars(0, button_debouncing_tb);
// Wait for given amount of time for simulation to complete
#(DURATION)
// Notify and end simulation
$display("Finished!");
$finish;
end
endmodule
Let’s review some of the important parts of this testbench. First, we generate a constant clock signal at 12 MHz:
always begin
#0.04167
clk = ~clk;
end
Next, we instantiate our debouncing code:
debounced_counter #(.MAX_CLK_COUNT(4800 - 1)) uut (
.clk(clk),
.rst_btn(rst_btn),
.inc_btn(inc_btn),
.led(out)
);
Here, you can see that we set the MAX_CLK_COUNT parameter to 4800. This should give us a delay period of about 400 us (instead of the usual 40 ms) between a detected falling edge and sampling the line again. Without it, you’ll likely find that the simulation will run for a very long time (possibly hours) and your .vcd file will be huge (likely gigabytes).
In our initial begin control block, we briefly pulse the reset line to reset the debouncer’s state machine:
#10
rst_btn = 0;
#1
rst_btn = 1;
Next, we use an outer for loop to toggle the inc_btn line with 1000 cycle delay in between each toggle. We also use an inner for loop to generate up to 20 random button bounce toggles rapidly on the line (with an up to 10 cycle random delay between each simulated bounce). Finally, we ensure that the button ends up in the opposite state after a random number of toggles.
for (i = 0; i < 32; i = i + 1) begin
// Wait some time before pressing button
#1000
// Simulate a bouncy/noisy button press
// $urandom generates a 32-bit unsigned (pseudo) random number
// "% 10" is "modulo 10"
prev_inc = inc_btn;
nbounces = $urandom % 20;
for (j = 0; j < nbounces; j = j + 1) begin
#($urandom % 10)
inc_btn = ~inc_btn;
end
// Make sure button ends up in the opposite state
inc_btn = ~prev_inc;
end
Note that we are using the blocking assignment operator (‘=’) in this block! This means each assignment happens sequentially before the next one executes (similar to how a sequential programming language would operate).
When you are just starting out, a good rule to follow is that you should use non-blocking assignments (‘<=’) in always blocks with a clocked sensitivity list (e.g. always @ (posedge clk)). You should use blocking assignments (‘=’) in always blocks with combinational logic and non-clocked sources (e.g. always @ ( * )). Try not to mix them (at least until you get more comfortable using them).
In testbenches, you can create non-synthesizable code with for loops and initial blocks that only run once. As a result, Verilog starts to look more like a programming or scripting language (for testbenches).
We finally end our testbench with the usual initial block that tells the simulation to run and store the value changes in a particular .vcd file.
initial begin
// Create simulation output file
$dumpfile("button-debouncing_tb.vcd");
$dumpvars(0, button_debouncing_tb);
// Wait for given amount of time for simulation to complete
#(DURATION)
// Notify and end simulation
$display("Finished!");
$finish;
end
Save these files on your computer. Apio will force you to initialize a board (even though you won’t upload your design to a board). From there, verify your code and run the simulation.
apio init -b icestick
apio verify
apio sim
This should cause GTKWave to automatically open, where you can view your waveforms!
Recommended Reading
The following content might be helpful if you would like to dig deeper:
- Verilog Guide’s testbench documentation
- Icarus Verilog documentation
- FPGA Tutorial’s How to Write a Basic Verilog Testbench
Introduction to FPGA Part 1 - What is an FPGA?
Introduction to FPGA Part 2 - Toolchain Setup
Introduction to FPGA Part 3 - Getting Started with Verilog
Introduction to FPGA Part 4 - Clocks and Procedural Assignments
Introduction to FPGA Part 5 - Finite State Machine (FSM)
Introduction to FPGA Part 6 - Verilog Modules and Parameters
Introduction to FPGA Part 8 - Memory and Block RAM
Introduction to FPGA Part 9 - Phase-Locked Loop (PLL) and Glitches
Introduction to FPGA Part 10 - Metastability and FIFO
Introduction to FPGA Part 11 - RISC-V Softcore Processor
Introduction to FPGA Part 12 - RISC-V Custom Peripheral
Have questions or comments? Continue the conversation on TechForum, DigiKey's online community and technical resource.
Visit TechForum