Lab 3: SystemVerilog Verification and FIFO Design
- Made by Hans :)
Overview
Due date: 11:59 PM 10/05/26 via design notebook.
In Lab 2, you learned how to write combinational, sequential, and FSM-based RTL.
In this lab, the focus shifts toward verification: checking whether your RTL behaves according to its specification.
You will work with:
- module instantiation and hierarchy
- self-checking testbenches
- directed testing
- randomized stimulus
- reference models
- FIFO design and verification
By the end of this lab, you should be able to write a basic self-checking testbench and verify a small RTL design.
1. Set Up Lab 3
Update your onboarding repository:
git switch main
git pull
Create a branch and directories:
git switch -c lab_3
mkdir -p lab_3/rtl lab_3/tb
Your Lab 3 directory will contain:
lab_3/
├── rtl/
│ ├── accumulator.sv
│ └── fifo.sv
└── tb/
├── accumulator_tb.sv
└── fifo_tb.sv
2. Module Instantiation and Hierarchy
Large digital systems are built by connecting smaller modules together. This organization is called module hierarchy.
For example, suppose accumulator.sv contains:
module accumulator #(
parameter WIDTH = 8
) (
input logic clk,
input logic reset,
input logic enable,
input logic [WIDTH-1:0] data_in,
output logic [WIDTH-1:0] sum
);
// Accumulator RTL
endmodule
Another SystemVerilog file can create an instance of that module:
accumulator #(
.WIDTH(8)
) accum_inst (
.clk(clk),
.reset(reset),
.enable(enable),
.data_in(data_in),
.sum(sum)
);
Here:
accumulator → module being instantiated
.WIDTH(8) → set its WIDTH parameter to 8
accum_inst → name of this particular instance
.clk(clk) → connect module port clk to local signal clk
The general idea is:
module_type instance_name (
.port(local_signal)
);
If the module has parameters, you can override them with #(...).
Modules can instantiate other RTL modules to build larger hardware systems:
top
├── cpu
│ ├── alu
│ └── register_file
├── uart
└── timer
A testbench uses the same syntax to instantiate the module being tested, usually called the DUT or Device Under Test.
3. Basic Testbench Structure
A testbench is SystemVerilog code used to simulate and verify hardware.
A typical testbench contains:
signal declarations
DUT instantiation
clock generation
input stimulus
output checks
For example:
`timescale 1ns/1ps
module example_tb;
logic clk;
logic reset;
example dut (
.clk(clk),
.reset(reset)
);
initial begin
clk = 0;
forever #5 clk = ~clk;
end
initial begin
// Tests here
end
endmodule
Common Testbench Constructs
`timescale 1ns/1ps
sets the simulation time unit and precision:
1ns → time unit
1ps → time precision
So:
#5;
waits 5 ns.
An:
initial begin
...
end
block begins once at simulation time 0.
A:
forever
loop repeats until the simulation ends.
Therefore:
initial begin
clk = 0;
forever #5 clk = ~clk;
end
toggles clk every 5 ns, giving a 10 ns clock period.
To wait for a clock edge:
@(posedge clk);
waits for clk to go from 0 to 1.
@(negedge clk);
waits for clk to go from 1 to 0.
A common pattern is:
@(negedge clk);
data_in = 8'd5;
@(posedge clk);
#1;
This changes an input before the active rising edge, then waits briefly before checking the updated output.
Testbench-Only Constructs
The following constructs used in this lab are intended for simulation/testbenches and are generally not synthesizable hardware:
initial
# delays
forever
@(posedge ...)
@(negedge ...)
$display
$fatal
$urandom_range
queues
That is fine because testbenches are not meant to become physical hardware.
4. Self-Checking Verification
A useful testbench checks results automatically.
For example:
if (result !== expected)
$fatal(1, "Expected %0d, got %0d", expected, result);
$fatal stops the simulation if the test fails.
%0d prints a value in decimal.
!== is case inequality, which also catches unknown (X) and high-impedance (Z) values.
A success message can be printed using:
$display("All tests passed!");
The basic verification process is:
apply inputs
↓
run DUT
↓
observe outputs
↓
compare against expected behavior
5. Directed Testing
A directed test applies specific inputs chosen by the designer.
Good directed tests should cover both normal and edge cases, such as:
reset
minimum/maximum values
disabled behavior
overflow
full/empty conditions
simultaneous operations
Passing one normal case does not mean a design is correct.
6. Exercise 1: Verify an Accumulator
An accumulator stores a running sum.
When enable = 1, it adds data_in to its current value on the next rising clock edge.
Create:
lab_3/rtl/accumulator.sv
with:
module accumulator #(
parameter WIDTH = 8
) (
input logic clk,
input logic reset,
input logic enable,
input logic [WIDTH-1:0] data_in,
output logic [WIDTH-1:0] sum
);
always_ff @(posedge clk) begin
if (reset)
sum <= '0;
else if (enable)
sum <= sum + data_in;
end
endmodule
The RTL is provided. Your task is to verify it.
Create:
lab_3/tb/accumulator_tb.sv
Start with:
`timescale 1ns/1ps
module accumulator_tb;
logic clk;
logic reset;
logic enable;
logic [7:0] data_in;
logic [7:0] sum;
accumulator #(
.WIDTH(8)
) dut (
.clk(clk),
.reset(reset),
.enable(enable),
.data_in(data_in),
.sum(sum)
);
initial begin
clk = 0;
forever #5 clk = ~clk;
end
// Write your tests here.
endmodule
At minimum, verify:
- reset clears
sum - one addition works
- several additions accumulate correctly
enable = 0holds the value- counting resumes when re-enabled
- 8-bit overflow wraps correctly
For example:
250 + 10 = 260
but an 8-bit result wraps to:
4
Use $fatal(1, ...) for failures. Only after all checks pass, print a success message and end the simulation:
$display("All accumulator tests passed!");
$finish;
Also add a separate watchdog so a bug cannot leave the simulation running forever:
initial begin
#100us;
$fatal(1, "Simulation timed out");
end
If your testbench becomes longer, increase the timeout as needed.
Testbench Tasks
A task packages repeated testbench behavior.
Example:
task check_sum(input logic [7:0] expected);
begin
if (sum !== expected)
$fatal(
1,
"Expected sum=%0d, got sum=%0d",
expected,
sum
);
end
endtask
Then:
check_sum(8'd10);
can replace a repeated checking block.
Compile and run:
verilator --binary --timing \
lab_3/rtl/accumulator.sv \
lab_3/tb/accumulator_tb.sv \
--top-module accumulator_tb
./obj_dir/Vaccumulator_tb
Do not continue until your directed tests pass.
7. Randomized Testing
Directed tests target cases you deliberately choose.
Randomized tests automatically explore many additional input combinations.
SystemVerilog provides:
$urandom_range(max, min)
For example:
data_in = $urandom_range(255, 0);
enable = $urandom_range(1, 0);
To repeat a test:
repeat (100) begin
// Randomized test cycle
end
repeat (100) executes its body 100 times.
Random testing supplements directed testing; it does not replace important edge cases.
8. Reference Models
A reference model keeps track of what the correct behavior should be.
For the accumulator:
logic [7:0] expected_sum;
and:
if (enable)
expected_sum = expected_sum + data_in;
The testbench can then compare:
if (sum !== expected_sum)
$fatal(1, "Expected %0d, got %0d", expected_sum, sum);
The reference model does not need to resemble the RTL implementation.
RTL must describe hardware:
registers
clock edges
control logic
data paths
Reference-model code can instead describe the desired behavior directly in a simple, software-like way.
For example:
expected_sum = expected_sum + data_in;
is easy to reason about: it simply calculates what the correct result should be.
Keeping the reference model simple and different from the RTL also reduces the chance of copying the same design mistake into both.
Add Random Testing
Extend your accumulator testbench to:
- reset the DUT
- initialize
expected_sum - run at least 100 randomized cycles
- randomly choose
enableanddata_in - update the reference model
- compare
sumafter every cycle
9. Exercise 2: FIFO
You will now design and verify a FIFO:
First In, First Out
Example:
Write: 10, 20, 30
Read: 10, 20, 30
FIFOs are commonly used to buffer data between hardware modules.
For this lab, you will build a synchronous FIFO, meaning reads and writes use the same clock.
10. FIFO Interface
Create:
lab_3/rtl/fifo.sv
with:
module fifo #(
parameter WIDTH = 8,
parameter DEPTH = 4
) (
input logic clk,
input logic reset,
input logic wr_en,
input logic [WIDTH-1:0] wr_data,
input logic rd_en,
output logic [WIDTH-1:0] rd_data,
output logic full,
output logic empty
);
// Your logic here
endmodule
For this lab, assume:
WIDTH > 0DEPTHis a power of two greater than or equal to 2
These assumptions keep the pointer-width and wraparound logic simple.
11. FIFO Storage and Pointers
Use a memory array:
logic [WIDTH-1:0] mem [0:DEPTH-1];
For WIDTH = 8 and DEPTH = 4, this creates four 8-bit entries.
The FIFO also needs:
wr_ptr → next location to write
rd_ptr → next location to read
After a successful operation, the corresponding pointer advances and wraps around at the end of the memory.
The SystemVerilog function:
$clog2(...)
stands for ceiling log base 2.
Examples:
$clog2(4) = 2
$clog2(8) = 3
$clog2(5) = 3
It is useful for determining how many bits are required for a pointer:
localparam PTR_WIDTH = $clog2(DEPTH);
A localparam is a constant internal to the module that cannot be overridden like a normal parameter.
12. Tracking Full and Empty
Use an occupancy counter:
count = number of stored entries
Then:
count == 0 → empty
count == DEPTH → full
Since count represents 0 through DEPTH:
logic [$clog2(DEPTH+1)-1:0] count;
13. FIFO Behavior
A write succeeds when:
wr_en && !full
A read succeeds when:
rd_en && !empty
On a successful write:
mem[wr_ptr] receives wr_data
wr_ptr advances
count increases
On a successful read:
rd_data receives mem[rd_ptr]
rd_ptr advances
count decreases
If both succeed on the same cycle:
both pointers advance
count does not change
For this lab, determine whether operations are accepted using full and empty before the clock edge.
Therefore:
- if the FIFO begins full, a write is rejected even if a read also occurs
- if the FIFO begins empty, a read is rejected even if a write also occurs
This keeps the control logic simpler.
Other FIFO designs may allow additional simultaneous behavior, but you do not need to implement that here.
On reset:
wr_ptr → 0
rd_ptr → 0
count → 0
rd_data → 0
You do not need to clear every memory entry.
14. Your FIFO Task
Implement fifo.sv.
Requirements:
- parameterized
WIDTHandDEPTH - synchronous reset
- correct
fullandempty - reject writes while full
- reject reads while empty
- preserve FIFO ordering
- wrap pointers correctly
- support simultaneous accepted read/write
- track occupancy correctly
For count:
| Write | Read | count |
|---|---|---|
| 0 | 0 | no change |
| 1 | 0 | +1 |
| 0 | 1 | -1 |
| 1 | 1 | no change |
15. FIFO Verification
Create:
lab_3/tb/fifo_tb.sv
At minimum, directed tests should verify:
- reset produces
empty = 1 - one write makes the FIFO non-empty
- write then read returns the correct data
- multiple values preserve order
- filling the FIFO asserts
full - a write while full is rejected
- draining the FIFO asserts
empty - a read while empty is rejected
- pointer wraparound
- simultaneous read/write
Do not move to randomized testing until these pass.
16. Queue Reference Model
SystemVerilog testbenches support dynamically sized queues:
logic [7:0] expected_queue[$];
The [$] means the queue can grow and shrink during simulation.
Add to the back:
expected_queue.push_back(value);
Remove the oldest entry:
expected = expected_queue.pop_front();
Get the number of entries:
expected_queue.size()
This provides a simple software-style FIFO model:
DUT FIFO Reference Queue
-------- ---------------
write 10 push_back(10)
write 20 push_back(20)
read pop_front() → 10
The queue is testbench code, not synthesizable FIFO hardware.
17. Randomized FIFO Testing
After directed testing, run at least 200 randomized cycles.
For each cycle:
wr_en = $urandom_range(1, 0);
rd_en = $urandom_range(1, 0);
wr_data = $urandom_range(255, 0);
Before the rising edge:
do_write = wr_en && !full;
do_read = rd_en && !empty;
Update the reference queue according to the operations that should be accepted.
After the edge:
- compare
rd_dataif a read occurred - verify
empty - verify
full
For example:
if (empty !== (expected_queue.size() == 0))
$fatal(1, "Incorrect empty flag");
if (full !== (expected_queue.size() == DEPTH))
$fatal(1, "Incorrect full flag");
Use tasks where helpful to keep your testbench organized.
Only after all directed and randomized checks pass, print a success message and end the simulation.
$display("All FIFO tests passed!");
$finish;
Also include a watchdog in the FIFO testbench (increase the time if necessary):
initial begin
#100us;
$fatal(1, "Simulation timed out");
end
18. Compile and Run
verilator --binary --timing \
lab_3/rtl/fifo.sv \
lab_3/tb/fifo_tb.sv \
--top-module fifo_tb
./obj_dir/Vfifo_tb
The success message should only be reached if all tests passed.
19. Key Ideas From This Lab
Module hierarchy:
build larger designs from smaller modules
Verification:
drive inputs and compare actual vs. expected outputs
Directed testing:
deliberately test important cases
Random testing:
explore many additional combinations
Reference model:
simple behavioral model of the correct result
FIFO:
memory + pointers + occupancy tracking
20. Submit Lab 3
git status
git add lab_3
git commit -m "Complete Lab 3"
git push -u origin lab_3
Open a pull request:
lab_3 → main
Review the changes, then Squash and merge.
After merging:
git switch main
git pull
git branch -d lab_3
21. Design Notebook
Before starting your design notebook work, update the documentation repository’s main.
If using the fork workflow:
git switch main
git pull upstream main
git push origin main
Then create your new design notebook branch.
Briefly discuss:
- the tests you wrote
- bugs or edge cases you encountered
- how your FIFO works
- how randomized testing or the reference model helped
Include a link to your onboarding repository.
Use the PR title:
docs(dn): First Last mm/dd/yy
Completion Checklist
- Accumulator directed testbench completed
- Accumulator randomized/reference-model testing completed
- Parameterized FIFO implemented
- FIFO directed tests completed
- FIFO randomized testing completed
- FIFO reference queue used successfully
- Lab 3 added to your repo
- Design notebook entry submitted
After completing this lab, you should be able to write and verify a small stateful RTL design using directed tests, randomized stimulus, and a reference model.