Part 6 · Testbench Architecture · Intermediate
Hooking Coverage Into the TB
Coverage class subscribing to the monitor stream, sampling observed transactions, env wiring, and monitor-embedded vs separate coverage.
Coverage rides the monitor stream
Functional coverage answers "what did the DUT actually see and do?" — so it must sample the monitor's observed transactions, never the generator's intent. The DUT may drop, transform, or error stimulus; coverage credited from the driven side claims scenarios that never reached the design. In a hand-built TB the wiring is simply another consumer mailbox on the monitor's broadcast list.
COVERAGE WIRING IN A HAND-BUILT TB
generator ──mbx──► driver ──► DUT ──► output pins
│
sampling here is WRONG ───┘
(intent, not behavior)
input pins / output pins
│
▼
monitor
(observes)
┌─────┴─────┐
mbx_scb mbx_cov
│ │
▼ ▼
scoreboard bus_coverage
(checking) cg.sample(t)
│
▼
coverage databaseA coverage collector class
class bus_coverage;
mailbox #(bus_txn) mbx_cov; // fed by the monitor
bus_txn tr; // covergroup samples through this handle
bit in_reset;
int unsigned sample_count;
covergroup cg;
option.per_instance = 1;
option.name = "bus_cov";
cp_op : coverpoint tr.op {
bins add_op = {OP_ADD};
bins sub_op = {OP_SUB};
bins logical[] = {OP_AND, OP_XOR};
}
cp_len : coverpoint tr.len {
bins single = {1};
bins small = {[2:4]};
bins large = {[5:16]};
}
cp_resp : coverpoint tr.resp {
bins okay = {0};
bins error = {[1:3]};
}
x_op_len : cross cp_op, cp_len;
endgroup
function new(mailbox #(bus_txn) mbx_cov);
this.mbx_cov = mbx_cov;
cg = new(); // forget this → null covergroup crash
endfunction
task run();
forever begin
mbx_cov.get(tr);
if (in_reset) continue; // reset-phase txns pollute closure
cg.sample();
sample_count++;
end
endtask
function void report();
$display("[COV] %s: %0.1f%% (%0d samples)",
cg.option.name, cg.get_inst_coverage(), sample_count);
endfunction
endclassEnv wiring
class env;
bus_monitor mon;
scoreboard scb;
bus_coverage cov;
mailbox #(bus_txn) mbx_scb = new();
mailbox #(bus_txn) mbx_cov = new();
function void build(virtual bus_if.MON vif);
mon = new(vif, mbx_scb, mbx_cov); // monitor broadcasts to both
scb = new(/* exp mbx */ mbx_exp, mbx_scb);
cov = new(mbx_cov);
endfunction
task run();
fork
mon.run();
scb.run();
cov.run();
join_none
endtask
endclassMailbox vs direct call
Mailbox subscription (shown): coverage runs in its own thread, monitor never blocks, and the collector can be dropped from an env without touching monitor code. Preferred default.
Direct function call (mon calls cov.sample_txn(t)): simpler, zero threading — but couples the monitor to the coverage class and every new consumer means editing the monitor.
Either way, sampling stays a function-level concern: get the txn, gate on reset, cg.sample().
Where should coverage live?
Embedding the covergroup inside the monitor works for tiny benches but ages badly: the monitor stops being reusable (every project wants different bins), compile-time grows on the hot path, and disabling coverage for a debug run means editing observation code. Keep the monitor a pure observer and put covergroups in a separate collector class subscribed to its stream — the same separation of concerns the scoreboard already follows.
Separate class (default): per-project bins without touching the monitor; collectors can be added or removed per test.
Inside the monitor (acceptable): protocol-universal coverage that every user of this interface wants — e.g., handshake-stall-length bins that are part of the protocol itself.
Never in the driver or generator: that is intent coverage — the classic wrong-stream mistake.
Interview angle
Expect "Where do you sample functional coverage and why?" — answer: from the monitor's observed stream, once per completed transaction, gated during reset; never from the driver because the DUT may transform or reject stimulus. Bonus points for naming the per-instance option and the zero-sample sanity check (a collector that sampled nothing should fail the test, mirroring the empty-scoreboard trap).
Key takeaways
Coverage samples observed transactions from the monitor — driven intent is the wrong stream.
A separate collector class subscribed via mailbox keeps the monitor reusable and coverage optional.
Gate sampling during reset and count samples — zero samples should fail the test.
Construct the covergroup in the class constructor — cg = new() or it is a null handle at first sample.
Common pitfalls
Sampling in the driver — coverage credits scenarios the DUT may never have seen.
Forgetting cg = new() in the constructor — null covergroup crash at first sample.
Sampling per beat instead of per completed transaction — inflated hit counts, no new scenarios.
Covergroup hard-wired inside the monitor — bins can't change per project without editing observation code.