This lab assembles the topic into one bench: a generator fills a bounded two-slot mailbox, a driver moves each item across a two-clock transfer, and a completion counter ends the test. No DUT is needed.
The class version: a generator puts randomized items into a two-slot mailbox, and a driver takes each one and spends two clocks (20 ns) on it. randomize() picks values for the rand fields that obey the constraint; the Randomization part teaches both.
timeunit 1ns; // one time unit for the classes and the module
timeprecision 1ps;
class txn_c;
rand bit [15:0] addr;
rand bit [31:0] data;
constraint c_addr { addr inside {[16'h0000:16'h00FF]}; }
endclass
class generator_c2;
mailbox #(txn_c) mb;
function new(mailbox #(txn_c) m);
mb = m;
endfunction
task run(int n);
repeat (n) begin
txn_c t = new();
void'(t.randomize());
mb.put(t);
end
endtask
endclass
class driver_c2;
mailbox #(txn_c) mb;
int done_count;
function new(mailbox #(txn_c) m);
mb = m;
endfunction
task run();
txn_c t;
forever begin
mb.get(t);
#20; // the two-clock transfer, pins omitted
done_count++;
end
endtask
endclass
module top_lab;
mailbox #(txn_c) mb;
generator_c2 gen;
driver_c2 drv;
initial begin
mb = new(2);
gen = new(mb);
drv = new(mb);
fork
gen.run(8);
drv.run(); // daemon: never returns
join_none
wait (drv != null && drv.done_count == 8); // null guard: Verilator evaluates this early
$display("PASS: 8 of 8 randomized items driven");
$finish;
end
endmodulePASS: 8 of 8 randomized items drivenEight items, deterministic values, blocking handoff both ways, nonblocking drives at clock edges, and a level-sensitive finish condition, all in one module.
module tb_gen_drv_handshake;
timeunit 1ns;
timeprecision 1ps;
bit clk = 0;
always #5 clk = ~clk;
int q_addr[2];
int q_data[2];
int head = 0;
int tail = 0;
int count = 0;
int errors = 0;
int generated = 0;
int done_count = 0;
logic bus_valid = 0;
logic [15:0] bus_addr = 0;
logic [31:0] bus_data = 0;
int beats = 0;
always @(posedge clk) // monitor: checks every beat on the bus itself
if (bus_valid) begin
if (bus_addr !== beats[15:0]) errors++;
if (bus_data !== (32'hA000 + beats)) errors++;
beats++;
end
initial begin : producer
for (int i = 0; i < 8; i++) begin
wait (count < 2);
q_addr[tail] = i;
q_data[tail] = 32'hA000 + i;
tail = (tail + 1) % 2;
count++;
generated++;
end
end
initial begin : consumer
int want = 0;
int got_addr = 0;
int got_data = 0;
repeat (8) begin
wait (count > 0);
got_addr = q_addr[head];
got_data = q_data[head];
head = (head + 1) % 2;
count--;
@(posedge clk);
bus_valid <= 1;
bus_addr <= got_addr[15:0];
bus_data <= got_data;
@(posedge clk);
bus_valid <= 0;
if (got_addr !== want) errors++;
if (got_data !== (32'hA000 + want)) errors++;
want++;
done_count++;
end
end
initial begin
wait (done_count == 8);
#1; // let the monitor sample the last beat at this edge
if (generated !== 8) errors++;
if (count !== 0) errors++;
if (beats !== 8) errors++;
if (errors == 0) $display("PASS: 8 of 8 items driven with 2-clock transfers");
else $fatal(1, "FAIL: done=%0d generated=%0d", done_count, generated);
$finish;
end
endmodulePASS: 8 of 8 items driven with 2-clock transfersThe mailbox, or the store in the module version, holds two items, small enough that the instant producer visibly stalls behind the two-clock driver. That stall is the backpressure lesson made concrete.
Completion uses a counter with a level wait, immune to trigger order. An edge count on a done event would undercount whenever a trigger precedes its waiter, so the canonical form here is the counter.
Drives use nonblocking assignment at clock edges. A blocking drive in the same timestep as a sampler would leave the sampled value to scheduler order.
Puts never block, so the queue grows without limit while the driver lags. The run still completes but memory creeps, hiding the slow consumer.
mb = new(2) to mb = new(8). Does the generator still wait?No. All 8 items fit at once, so every put returns immediately; the run takes as long as before, because the driver is the slow part.
@(done_ev) in a loop. Can the count come up short?Yes. If a trigger fires while the test is not waiting at @(done_ev), that occurrence is lost; the counter with a level wait cannot miss one.
Try both, and wrap the finish wait in a watchdog with a generous limit so a lost item fails instead of hanging.
The structure maps onto UVM: generator to sequence, handoff to the sequencer-driver port, completion to item done. The UVM driver port is a different mechanism, so carry the pattern, not the mailbox.
One way to write it:
typedef enum bit [1:0] {DATA = 2'd0, CTRL = 2'd1, ERR = 2'd2} kind_e;
typedef bit [7:0] bytes_t[$];
interface pkt_if (input logic clk, input logic rst_n);
logic valid, ready, last;
logic [7:0] data;
endinterface
class pkt_txn;
kind_e kind;
bit [1:0] dest;
bit [7:0] payload[$];
function bit [7:0] header();
return {kind, dest, 4'(payload.size())};
endfunction
function bytes_t to_bytes(); // header, payload, check byte
bytes_t b = {header()};
bit [7:0] c = header();
foreach (payload[i]) begin
b.push_back(payload[i]);
c ^= payload[i];
end
b.push_back(c);
return b;
endfunction
endclass
class pkt_gen; // makes packets, one fresh object each
mailbox #(pkt_txn) mb;
function new(mailbox #(pkt_txn) mb);
this.mb = mb;
endfunction
task run(int n);
for (int i = 0; i < n; i++) begin
pkt_txn p = new();
p.kind = (i % 2 == 1) ? CTRL : DATA;
p.dest = 2'(i);
p.payload = {8'hA5, 8'(i)};
mb.put(p); // blocks while the mailbox is full
end
endtask
endclass
class pkt_driver; // takes packets and drives their bytes
mailbox #(pkt_txn) mb;
virtual pkt_if vif;
int sent;
function new(mailbox #(pkt_txn) mb, virtual pkt_if vif);
this.mb = mb;
this.vif = vif;
endfunction
task run();
pkt_txn p;
bytes_t b;
forever begin
mb.get(p);
b = p.to_bytes();
foreach (b[i]) begin
@(posedge vif.clk);
vif.valid <= 1'b1;
vif.data <= b[i];
vif.last <= (i == b.size() - 1);
do @(posedge vif.clk); while (!vif.ready);
vif.valid <= 1'b0;
end
sent++;
end
endtask
endclass
// DEPTH is a power of two of at least 2, so PTR_W is at least 1.
module fifo #(parameter int DEPTH = 16, parameter int DATA_W = 32) (
input logic clk, rst_n, wr_en, rd_en,
input logic [DATA_W-1:0] wdata,
output logic [DATA_W-1:0] rdata,
output logic full, empty
);
localparam int PTR_W = $clog2(DEPTH);
logic [DATA_W-1:0] mem [DEPTH];
logic [PTR_W:0] wr_ptr, rd_ptr;
assign empty = (wr_ptr == rd_ptr);
assign full = (wr_ptr[PTR_W] != rd_ptr[PTR_W]) &&
(wr_ptr[PTR_W-1:0] == rd_ptr[PTR_W-1:0]);
assign rdata = mem[rd_ptr[PTR_W-1:0]];
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
wr_ptr <= '0;
rd_ptr <= '0;
end else begin
if (wr_en && !full) begin
mem[wr_ptr[PTR_W-1:0]] <= wdata;
wr_ptr <= wr_ptr + 1;
end
if (rd_en && !empty) rd_ptr <= rd_ptr + 1;
end
end
endmodule
// PacketFIFO v0 with pkt_if on both sides.
module pkt_fifo (pkt_if in, pkt_if out);
logic full, empty;
logic [8:0] rdata;
fifo #(.DEPTH(4), .DATA_W(9)) u_f (
.clk(in.clk), .rst_n(in.rst_n), .wr_en(in.valid), .rd_en(out.ready),
.wdata({in.last, in.data}), .rdata(rdata), .full(full), .empty(empty));
assign in.ready = !full;
assign out.valid = !empty;
assign {out.last, out.data} = rdata;
endmodule
out: '{'h2, 'ha5, 'h0, 'ha7, 'h52, 'ha5, 'h1, 'hf6, 'h22, 'ha5, 'h2, 'h85}
PASS: 3 packets went generator -> mailbox -> pkt_driver -> PacketFIFO and came out byte for byteRun 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.
The finish wait in the PacketFIFO bench polls a class field once per clock: Verilator 5.048 evaluated a wait() on that field before the class handle was set and stopped with a null-pointer error.