A signed-off driver stays untouched while tests add behavior. The driver publishes hook points, and each test installs callback objects that run there.
Subclassing forks verified code per test, and a factory override swaps the whole class. Callbacks add small behaviors onto one driver instead, and several stack on the same run.
Install: the test pushes callbacks onto the driver queue before the run starts.
Pre hooks: each callback sees the live transaction before the pins move.
Drive: the frozen driver core moves the possibly mutated transaction to the pins.
Post hooks: each callback samples exactly what was driven.
Question | Factory override | Callback |
|---|---|---|
What changes | The created type | Behavior at a hook point |
Installed | Before build | Before run |
Stacks with others | No, one type wins | Yes, in queue order |
One base class declares empty virtual hooks. The driver holds a queue of that base type and iterates it at each hook with foreach.
Empty beats pure virtual. Each callback overrides only the hooks it uses instead of stubbing every one.
Hooks receive the driver handle and the live transaction. A callback can read component state and mutate the in-flight object, and hooks are tasks so delays stay legal.
Coverage sampling is the classic post-drive callback: its hook calls a covergroup's sample(). Covergroups are taught in the Coverage part; the bench below records the same sampling intent with plain counters.
post_drive while errors inject in pre_drive. Can sampling see clean data the pins never got?No. Every pre_drive runs before the pins move and every post_drive runs after, so the sampler always sees exactly what was driven.
The one installed first. A shared hook runs in queue order, so install injection before any same-hook monitor.
virtual instead of pure virtual?A callback then overrides only the hooks it uses. pure virtual would force every callback to implement every hook.
Counter-based callbacks with no covergroup and no interface timing, proving hook order on eight transactions.
typedef class driver;
class bus_txn;
bit [31:0] data;
function string convert2string();
return $sformatf("data=%08h", data);
endfunction
endclass
class driver_cb;
virtual task pre_drive(driver drv, bus_txn t);
endtask
virtual task post_drive(driver drv, bus_txn t);
endtask
endclass
class driver;
driver_cb cbs[$];
int unsigned n_driven;
bit [31:0] last_data;
function void add_cb(driver_cb cb);
cbs.push_back(cb);
endfunction
task run(int unsigned n, bit [31:0] start_data);
bus_txn t;
bit [31:0] d;
d = start_data;
repeat (n) begin
t = new();
t.data = d;
d = d + 1;
foreach (cbs[i]) cbs[i].pre_drive(this, t);
n_driven++;
last_data = t.data;
foreach (cbs[i]) cbs[i].post_drive(this, t);
end
endtask
endclass
class err_inject_cb extends driver_cb;
int unsigned corrupted;
int unsigned seen;
virtual task pre_drive(driver drv, bus_txn t);
seen++;
if ((seen % 4) == 0) begin
t.data = t.data ^ 32'h80000000;
corrupted++;
end
endtask
endclass
class count_cb extends driver_cb;
int unsigned n_post;
int unsigned n_hi;
virtual task post_drive(driver drv, bus_txn t);
n_post++;
if (t.data[31] != 0) n_hi++;
endtask
endclass
PASS: pre hooks corrupt before pins, post hooks sample afterOne way to write it; the driver logs the bytes instead of driving pins, so the hook order is easy to see:
typedef bit [7:0] bytes_t[$];
// Hooks a test can attach to the driver without editing it.
class pkt_driver_cb;
virtual function void pre_send(int n, ref bytes_t b); // empty: override what you need
endfunction
endclass
// Error injection: corrupt the check byte of every second packet.
class bad_check_cb extends pkt_driver_cb;
virtual function void pre_send(int n, ref bytes_t b);
if (n % 2 == 1) b[b.size() - 1] = ~b[b.size() - 1];
endfunction
endclass
class pkt_driver;
pkt_driver_cb cbs[$];
bytes_t wire_log[$]; // what went out, one entry per packet
int n_sent;
function void add_cb(pkt_driver_cb cb);
cbs.push_back(cb);
endfunction
function void send(bytes_t b);
foreach (cbs[i]) cbs[i].pre_send(n_sent, b); // hooks run in the order added
wire_log.push_back(b); // stands in for driving the pins
n_sent++;
endfunction
endclass
packet 0: check byte bb ok
packet 1: check byte 44 bad
packet 2: check byte bb ok
packet 3: check byte 44 bad
PASS: the callback corrupted packets 1 and 3 without a change to pkt_driverRun with Verilator 5.048 (--binary --timing) on 2026-10-18; every bench printed the PASS line shown, with no warnings. These class benches were run on Verilator only.
Covergroups are not run in this course's checks: Verilator 5.048 compiles them but ignores the bins (warning COVERIGN), and Icarus 13 does not support them.