Testbench Best Practices
SkillDev toolsTestbench 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"
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Testbench Best Practices skill
What this skill tells your AI
The instructions your AI receives, as published by codejunkie99/gateflow-plugin in skills/tb-best-practices/SKILL.md and read by ahel’s review.
Professional verification patterns for SystemVerilog testbenches.
Layered Testbench Architecture
┌─────────────────────────────────────────┐
│ Test Layer │ ← Test scenarios
├─────────────────────────────────────────┤
│ Environment Layer │ ← Agent coordination
├──────────────┬──────────────────────────┤
│ Agent │ Agent │ ← Protocol-specific
│ ┌────────┐ │ ┌────────┐ │
│ │ Driver │ │ │Monitor │ │
│ └────────┘ │ └────────┘ │
│ ┌────────┐ │ ┌────────┐ │
│ │Sequencer│ │ │Scoreboard│ │
│ └────────┘ │ └────────┘ │
├──────────────┴──────────────────────────┤
│ Interface Layer │ ← Signal abstraction
├─────────────────────────────────────────┤
│ DUT │
└─────────────────────────────────────────┘
1. Testbench Structure
Basic Self-Checking TB
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
2. Transaction Class
Best Practice: Separate data from timing
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
3. Driver
Best Practice: Drive interface, not signals
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
4. Monitor
Best Practice: Passive observation only
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
5. Scoreboard
Best Practice: Compare expected vs actual
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
6. Randomization Best Practices
Constraint Layering
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
Weighted Distribution
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
Solve Before
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
7. Coverage Best Practices
Functional Coverage
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 Goals
| Coverage Type | Target |
|---|---|
| Line | >95% |
| Branch | >90% |
| FSM State | 100% |
| FSM Transition | >95% |
| Functional | >98% |
8. Threading Patterns
Fork/Join
// 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
Timeout Pattern
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
9. Interface Best Practices
Parameterized Interface
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
Virtual Interface in Class
class Agent;
virtual axi_if vif;
function new(virtual axi_if vif);
this.vif = vif;
endfunction
endclass
10. Assertions in Testbench
Protocol Checks
// 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");
11. Test Organization
Test Base Class
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
12. Checklist
Before Starting
- Define verification plan
- Identify coverage goals
- List test scenarios
- Design TB architecture
During Development
- Use transactions, not raw signals
- Separate driver/monitor/scoreboard
- Use virtual interfaces
- Add functional coverage
- Include protocol assertions
Before Signoff
- All tests pass
- Coverage goals met
- No X/Z in simulation
- Edge cases tested
- Error injection tested
Reference
[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)
UVM-Lite for Verilator v5
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.
Cocotb Equivalents
| 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 |
Coverage Closure Checklist
- Extract coverage report after full regression
- Categorize: unreachable code (exclude with justification), untriggered FSM transitions (write directed test), empty cross bins (constraint override), low-hit coverpoints (increase iterations or add weighted distribution)
- Write directed tests for each remaining hole
- Re-run with merged coverage, verify all targets met
- Document all exclusions with justification comments
10 TB Anti-Patterns
- Magic numbers -> Use localparam with meaningful names
- No timeout -> fork/join_any with $fatal timeout guard
- Happy path only -> Test errors, resets mid-operation, edge cases
- #delay for sync -> Use @(posedge clk) for synchronization
- Ignoring X/Z -> Use $isunknown() and
===operator - Monolithic test -> Split into reusable tasks
- Print-and-pray -> Use assert with $error messages
- Race conditions -> Non-blocking
<=at clock edges - Global state -> Encapsulate in classes, pass via constructor
- Not draining pipeline -> Wait PIPELINE_DEPTH + margin cycles before checking
Performance Optimization
| 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).
Signals
- GitHub stars
- 112
- Forks
- 14
- Last commit
- May 2026
Advanced
- Catalog kind
- skill
- Gateway key
tb-best-practices- Source
- github.com/codejunkie99/gateflow-plugin