Testbench verification best practices and patterns. This skill should be used when the user needs testbench architecture guidance, verification methodology, or wants to write professional-quality testbenches. Example requests: "testbench best practices", "how to structure TB", "verification patterns"
Professional verification patterns for SystemVerilog testbenches.
┌─────────────────────────────────────────┐
│ Test Layer │ ← Test scenarios
├─────────────────────────────────────────┤
│ Environment Layer │ ← Agent coordination
├──────────────┬──────────────────────────┤
│ Agent │ Agent │ ← Protocol-specific
│ ┌────────┐ │ ┌────────┐ │
│ │ Driver │ │ │Monitor │ │
│ └────────┘ │ └────────┘ │
│ ┌────────┐ │ ┌────────┐ │
│ │Sequencer│ │ │Scoreboard│ │
│ └────────┘ │ └────────┘ │
├──────────────┴──────────────────────────┤
│ Interface Layer │ ← Signal abstraction
├─────────────────────────────────────────┤
│ DUT │
└─────────────────────────────────────────┘
module tb_dut;
// Clock and reset
logic clk = 0;
logic rst_n = 0;
always #5 clk = ~clk;
// DUT signals
logic [7:0] data_in, data_out;
logic valid_in, valid_out;
// DUT instance
dut u_dut (.*);
// Test
initial begin
$dumpfile("dump.vcd");
$dumpvars(0, tb_dut);
// Reset
rst_n = 0;
repeat(5) @(posedge clk);
rst_n = 1;
// Stimulus + Check
for (int i = 0; i < 100; i++) begin
@(posedge clk);
data_in <= $urandom();
valid_in <= 1;
@(posedge clk);
valid_in <= 0;
// Wait for output
wait(valid_out);
check_result(data_in, data_out);
end
$display("TEST PASSED");
$finish;
end
// Checker
function void check_result(input [7:0] expected, input [7:0] actual);
assert(actual == expected)
else $error("Mismatch: expected %0h, got %0h", expected, actual);
endfunction
endmodule
class Transaction;
rand bit [31:0] addr;
rand bit [31:0] data;
rand bit [3:0] burst_len;
rand bit write;
// Constraints
constraint c_aligned { addr[1:0] == 2'b00; }
constraint c_burst { burst_len inside {1, 4, 8, 16}; }
// Copy
function Transaction copy();
Transaction t = new();
t.addr = this.addr;
t.data = this.data;
t.burst_len = this.burst_len;
t.write = this.write;
return t;
endfunction
// Display
function void display(string prefix = "");
$display("%s addr=%08h data=%08h burst=%0d %s",
prefix, addr, data, burst_len, write ? "WR" : "RD");
endfunction
endclass
class Driver;
virtual bus_if.master vif;
mailbox #(Transaction) mbx;
function new(virtual bus_if.master vif, mailbox #(Transaction) mbx);
this.vif = vif;
this.mbx = mbx;
endfunction
task run();
Transaction t;
forever begin
mbx.get(t);
drive(t);
end
endtask
task drive(Transaction t);
@(posedge vif.clk);
vif.addr <= t.addr;
vif.data <= t.data;
vif.valid <= 1;
@(posedge vif.clk);
while (!vif.ready) @(posedge vif.clk);
vif.valid <= 0;
endtask
endclass
class Monitor;
virtual bus_if.monitor vif;
mailbox #(Transaction) mbx; // To scoreboard
function new(virtual bus_if.monitor vif, mailbox #(Transaction) mbx);
this.vif = vif;
this.mbx = mbx;
endfunction
task run();
Transaction t;
forever begin
@(posedge vif.clk);
if (vif.valid && vif.ready) begin
t = new();
t.addr = vif.addr;
t.data = vif.data;
mbx.put(t);
end
end
endtask
endclass
class Scoreboard;
mailbox #(Transaction) expected_mbx;
mailbox #(Transaction) actual_mbx;
int pass_count, fail_count;
function new(mailbox #(Transaction) expected_mbx, mailbox #(Transaction) actual_mbx);
this.expected_mbx = expected_mbx;
this.actual_mbx = actual_mbx;
endfunction
task run();
Transaction expected, actual;
forever begin
expected_mbx.get(expected);
actual_mbx.get(actual);
compare(expected, actual);
end
endtask
function void compare(Transaction expected, Transaction actual);
if (expected.data == actual.data) begin
pass_count++;
end else begin
fail_count++;
$error("MISMATCH: expected=%08h actual=%08h", expected.data, actual.data);
end
endfunction
function void report();
$display("=============================");
$display("Scoreboard: PASS=%0d FAIL=%0d", pass_count, fail_count);
$display("=============================");
endfunction
endclass
class Transaction;
rand bit [31:0] addr;
rand bit [7:0] data;
// Base constraint
constraint c_valid_addr { addr < 32'h1000_0000; }
// Can be overridden in extended class
constraint c_mode { soft data > 0; }
endclass
class ErrorTransaction extends Transaction;
// Override to inject errors
constraint c_mode { data == 0; }
endclass
class Packet;
rand bit [1:0] pkt_type;
constraint c_type_dist {
pkt_type dist {
0 := 60, // 60% normal
1 := 30, // 30% error
2 := 10 // 10% special
};
}
endclass
class Packet;
rand bit [7:0] length;
rand bit [7:0] payload[];
constraint c_size {
payload.size() == length;
length inside {[1:64]};
solve length before payload;
}
endclass
class Coverage;
Transaction t;
covergroup cg_trans @(posedge clk);
// Coverpoints
cp_addr: coverpoint t.addr[31:28] {
bins low = {[0:3]};
bins mid = {[4:11]};
bins high = {[12:15]};
}
cp_write: coverpoint t.write;
cp_burst: coverpoint t.burst_len {
bins single = {1};
bins burst4 = {4};
bins burst8 = {8};
bins burst16 = {16};
illegal_bins bad = default;
}
// Cross coverage
cross_addr_write: cross cp_addr, cp_write;
endgroup
function new();
cg_trans = new();
endfunction
function void sample(Transaction t);
this.t = t;
cg_trans.sample();
endfunction
endclass
| Coverage Type | Target |
|---|---|
| Line | >95% |
| Branch | >90% |
| FSM State | 100% |
| FSM Transition | >95% |
| Functional | >98% |
// Parallel - wait for all
fork
driver.run();
monitor.run();
scoreboard.run();
join
// Parallel - wait for any (with cleanup)
fork
driver.run();
monitor.run();
timeout_check();
join_any
disable fork;
// Parallel - don't wait
fork
background_task();
join_none
task run_with_timeout(int cycles);
fork
begin
run_test();
end
begin
repeat(cycles) @(posedge clk);
$error("TIMEOUT after %0d cycles", cycles);
$finish;
end
join_any
disable fork;
endtask
interface axi_if #(
parameter int ADDR_W = 32,
parameter int DATA_W = 32
) (
input logic clk,
input logic rst_n
);
logic [ADDR_W-1:0] awaddr;
logic [DATA_W-1:0] wdata;
logic awvalid, awready;
logic wvalid, wready;
logic [1:0] bresp;
logic bvalid, bready;
modport master (
output awaddr, awvalid, wdata, wvalid, bready,
input awready, wready, bresp, bvalid
);
modport slave (
input awaddr, awvalid, wdata, wvalid, bready,
output awready, wready, bresp, bvalid
);
modport monitor (
input awaddr, awvalid, awready, wdata, wvalid, wready,
bresp, bvalid, bready
);
// Clocking block for TB
clocking cb @(posedge clk);
default input #1 output #1;
output awaddr, awvalid, wdata, wvalid, bready;
input awready, wready, bresp, bvalid;
endclocking
endinterface
class Agent;
virtual axi_if vif;
function new(virtual axi_if vif);
this.vif = vif;
endfunction
endclass
// In interface or bind module
property p_valid_stable;
@(posedge clk) disable iff (!rst_n)
valid && !ready |=> valid && $stable(data);
endproperty
assert property (p_valid_stable) else $error("Valid dropped before ready");
property p_handshake;
@(posedge clk) disable iff (!rst_n)
valid |-> ##[1:100] ready;
endproperty
assert property (p_handshake) else $error("No ready within 100 cycles");
virtual class BaseTest;
Environment env;
function new(Environment env);
this.env = env;
endfunction
pure virtual task run();
task pre_test();
env.reset();
endtask
task post_test();
env.report();
endtask
endclass
class SmokeTest extends BaseTest;
virtual task run();
Transaction t = new();
repeat(10) begin
assert(t.randomize());
env.driver_mbx.put(t);
end
endtask
endclass
[SystemVerilog for Verification, 3rd ed. — Spear/Tumbush]
|url: https://picture.iczhiku.com/resource/eetop/wYIEDKFRorpoPvvV.pdf
|relevant: Ch 5 (OOP), Ch 6 (Rand), Ch 7 (Threads), Ch 8 (Advanced OOP), Ch 9 (Coverage), Ch 11 (Complete TB)
Verilator v5 supports UVM 2017 subset. Works: uvm_component, uvm_object, uvm_sequence_item, uvm_driver, uvm_monitor, uvm_scoreboard, uvm_env, uvm_test, uvm_tlm ports, phases. Does NOT work: factory overrides, $cast, RAL, uvm_config_db with virtual interfaces, callbacks.
Pass virtual interfaces via constructor/setter, not config_db. Use direct construction, not factory. Compile with -DUVM_NO_DPI --timing.
| SV Pattern | Cocotb Equivalent |
|---|---|
| Transaction class | Python dataclass |
| rand/constraint | random module + manual constraints |
| mailbox | collections.deque or asyncio.Queue |
| virtual interface | Direct dut handle |
| covergroup | cocotb-coverage decorators |
| fork/join | Combine(start_soon(...)) |
| fork/join_any | First(start_soon(...)) |
| assert property | async def checker coroutine |
=== operator<= at clock edges| Technique | Speedup |
|---|---|
--threads N | 2-4x |
--trace-fst (not VCD) | 2-3x smaller files |
| Disable tracing in regression | 2-5x |
| Minimize $display | 1.2-2x |
--x-assign fast (after X-clean) | 1.1x |
Parallel test execution (make -j) | Nx |
Test tiers: SMOKE (seconds, every commit), RANDOM (minutes, PR merge), COVERAGE (hours, nightly), REGRESSION (hours, weekly).