Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

  1. reset clears sum
  2. one addition works
  3. several additions accumulate correctly
  4. enable = 0 holds the value
  5. counting resumes when re-enabled
  6. 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:

  1. reset the DUT
  2. initialize expected_sum
  3. run at least 100 randomized cycles
  4. randomly choose enable and data_in
  5. update the reference model
  6. compare sum after 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 > 0
  • DEPTH is 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:

  1. parameterized WIDTH and DEPTH
  2. synchronous reset
  3. correct full and empty
  4. reject writes while full
  5. reject reads while empty
  6. preserve FIFO ordering
  7. wrap pointers correctly
  8. support simultaneous accepted read/write
  9. track occupancy correctly

For count:

WriteReadcount
00no change
10+1
01-1
11no change

15. FIFO Verification

Create:

lab_3/tb/fifo_tb.sv

At minimum, directed tests should verify:

  1. reset produces empty = 1
  2. one write makes the FIFO non-empty
  3. write then read returns the correct data
  4. multiple values preserve order
  5. filling the FIFO asserts full
  6. a write while full is rejected
  7. draining the FIFO asserts empty
  8. a read while empty is rejected
  9. pointer wraparound
  10. 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_data if 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.