Day 03 / 25
← Day 02 Day 04 →
HomeVerificationDay 3 — SystemVerilog for Verification
Track 1 — Foundations

SystemVerilog for Verification

SystemVerilog adds powerful non-synthesizable constructs specifically for testbenches: interfaces for clean DUT connection, clocking blocks for race-free stimulus, classes for reusable components, and IPC primitives (mailbox, semaphore, event) for multi-threaded stimulus generation.

⏱ 25 min read📖 Day 3 of 25🎯 SystemVerilog · Interface
Contents
  1. Interface — Bundle Signals
  2. Modport — Directional Views
  3. Clocking Block — Race-Free Sampling
  4. Virtual Interface
  5. Program Block
  6. Classes and OOP in SV
  7. Mailbox — Inter-Process Communication
  8. Semaphore and Event
  9. fork…join Variants

1. Interface — Bundle Signals

An interface bundles related signals into a single object. Instead of 20+ ports on your DUT, you pass one interface. The DUT and testbench both connect to the same interface instance:

// Define the AXI-lite interface
interface axi_lite_if #(parameter DW = 32) (input logic clk, rstn);
  // Write Address
  logic [31:0] awaddr;
  logic        awvalid, awready;
  // Write Data
  logic [DW-1:0] wdata;
  logic          wvalid, wready;
  // Write Response
  logic [1:0] bresp;
  logic       bvalid, bready;
  // ... AR, R channels ...
endinterface

// Top-level testbench instantiation
module tb_top;
  logic clk, rstn;
  always #5 clk = ~clk;
  initial {clk, rstn} = 2'b0;
  initial begin #20 rstn = 1; end

  // Instantiate interface
  axi_lite_if #(32) axi (.clk(clk), .rstn(rstn));

  // Pass to DUT
  my_slave dut (.axi(axi));

  // Pass to test via uvm_config_db (Day 13)
endmodule

2. Modport — Directional Views

A modport defines a directional view of the interface. The master modport drives awvalid/awaddr/wdata; the slave modport drives awready/wready. This enforces directionality at compile time:

interface axi_lite_if (input logic clk);
  logic awvalid, awready;
  logic [31:0] awaddr;

  // Master: drives valid/addr, receives ready
  modport master (output awvalid, awaddr, input awready, input clk);

  // Slave: receives valid/addr, drives ready
  modport slave  (input  awvalid, awaddr, output awready, input clk);
endinterface

// DUT uses the slave modport
module my_slave (axi_lite_if.slave axi);
  // awvalid and awaddr are inputs; awready is output
endmodule

3. Clocking Block — Race-Free Sampling

Without a clocking block, there is a race between the testbench driving signals and the DUT sampling them at the same clock edge. The clocking block adds programmable input and output skews to eliminate this:

interface dut_if (input logic clk);
  logic       valid;
  logic [7:0] data;
  logic       ready;

  // Clocking block for the testbench driver
  clocking driver_cb @(posedge clk);
    // input skew: sample 1ns before rising edge
    default input  #1ns;
    // output skew: drive 1ns after rising edge
    default output #1ns;

    output valid, data;   // TB drives these
    input  ready;         // TB samples this
  endclocking

  modport drv (clocking driver_cb);
endinterface

// Using the clocking block in a driver
task drive_packet(input [7:0] d);
  // Drives exactly 1ns after posedge — no race condition
  vif.driver_cb.valid <= 1;
  vif.driver_cb.data  <= d;
  @(vif.driver_cb);           // advance one clock cycle
endtask
Why clocking blocks matter: At time 0, both the DUT and testbench respond to the same posedge. Without skew, a testbench signal change at exactly posedge may or may not be seen by the DUT depending on simulation event ordering — a race condition. The output skew guarantees the testbench drives after the DUT has sampled.

4. Virtual Interface

A virtual interface is a handle to an interface instance. Class-based components (like UVM agents) are dynamic objects — they cannot statically refer to a module-level interface. A virtual interface variable bridges this gap:

// In the driver class:
class my_driver;
  virtual dut_if vif;    // handle — not an instance

  task drive(input [7:0] d);
    vif.driver_cb.valid <= 1;
    vif.driver_cb.data  <= d;
    @(vif.driver_cb);
  endtask
endclass

// In top-level testbench, assign the actual interface:
initial begin
  my_driver drv = new();
  drv.vif = dut_intf;      // assign the module-level interface
  // In UVM: uvm_config_db #(virtual dut_if)::set(null, "*", "vif", dut_intf)
end

5. Program Block

A program block is a special module designed for testbench code. Unlike modules, programs respond to the Reactive region (after Active/NBA) and automatically add a #0 delay to avoid races. Programs call $exit instead of $finish:

program tb_prog (dut_if.drv dut);
  initial begin
    // Program block is scheduled in Reactive region
    // — avoids Active-region races with DUT
    dut.driver_cb.valid <= 0;
    @(dut.driver_cb);
    dut.driver_cb.valid <= 1;
    dut.driver_cb.data  <= 8'hAB;
    @(dut.driver_cb);
    $exit;   // ends the program (not the whole simulation)
  end
endprogram
Modern practice: UVM environments no longer use program blocks — they use classes instead. Program blocks are useful for understanding the Reactive-region concept, but in production UVM testbenches you will not see them.

6. Classes and OOP in SystemVerilog

SystemVerilog classes support full OOP: inheritance, polymorphism, encapsulation. All UVM components and sequences are SV classes. Key concepts:

// Base transaction class
class base_pkt;
  rand logic [7:0] addr;
  rand logic [31:0] data;
  rand logic        read;

  // Constraint: read accesses only in 0–127 range
  constraint c_read_range {
    if (read) addr inside {[8'h00:8'h7F]};
  }

  function void print();
    $display("addr=%0h data=%0h read=%0b", addr, data, read);
  endfunction
endclass

// Derived class — overrides constraint
class burst_pkt extends base_pkt;
  rand logic [3:0] burst_len;

  // Inherited constraint still applies
  constraint c_burst { burst_len inside {[1:16]}; }
endclass

// Dynamic creation and randomization
base_pkt pkt = new();
assert(pkt.randomize()) else $fatal(1, "Randomize failed");
pkt.print();

7. Mailbox — Inter-Process Communication

A mailbox is a thread-safe queue. The generator thread creates packets and puts them; the driver thread gets them. This decouples the two threads — the generator can run ahead, and the driver paces itself:

// Parameterized mailbox (type-safe, SV2005+)
mailbox #(base_pkt) gen2drv = new();  // unbounded
mailbox #(base_pkt) mon2scb = new(16); // bounded to 16 entries

// Generator thread
initial begin
  for (int i = 0; i < 100; i++) begin
    base_pkt p = new();
    assert(p.randomize());
    gen2drv.put(p);    // blocks if mailbox full (bounded)
  end
end

// Driver thread
initial begin
  base_pkt p;
  forever begin
    gen2drv.get(p);    // blocks until packet available
    drive_packet(p);   // apply to DUT
  end
end

8. Semaphore and Event

// Semaphore — mutual exclusion (e.g., shared bus access)
semaphore bus_key = new(1);  // 1 key = mutex

task drive_bus(base_pkt p);
  bus_key.get(1);     // acquire key — blocks if in use
  drive_packet(p);
  bus_key.put(1);     // release key
endtask

// Event — synchronise threads by triggering/waiting
event reset_done;

// Thread 1 — triggers event after reset
initial begin
  rst_n = 0; repeat(4) @(posedge clk);
  rst_n = 1;
  ->reset_done;          // trigger event
end

// Thread 2 — waits for event before starting
initial begin
  @reset_done;           // wait for event trigger
  // now safe to drive stimulus
end

9. fork…join Variants

ConstructWaits forUse case
fork…joinAll threads completeMultiple independent tasks that must all finish
fork…join_anyFirst thread completesTimeout: run DUT + watchdog, stop when either finishes
fork…join_noneDoes not wait — all threads run in backgroundLaunch background monitors, checkers
// fork…join — wait for all 3 threads
fork
  drive_wr_transactions();
  drive_rd_transactions();
  drive_irq_stimulus();
join

// fork…join_any — timeout watchdog pattern
fork
  begin run_test(); $display("DONE"); end
  begin #10ms; $fatal(1, "TIMEOUT"); end
join_any
disable fork;    // kill the other thread

// fork…join_none — background monitor
fork
  forever begin
    @(vif.valid);
    monitor_capture();
  end
join_none         // continues immediately, monitor runs in background

Key Takeaways — Day 3

Next → Day 04
SVA — SystemVerilog Assertions
Immediate vs concurrent assertions, property, sequence, ##, |=>, disable iff — protocol compliance checkers in SV.