The driver is the only stimulus-side class that touches pins. It consumes one transaction at a time, expands it into legal cycles, holds a driven idle between transfers, and reacts to reset at any moment.
Watch valid against ready across the six slots. Payload holds through the stall and the gap stays idle.
Slot 0: valid 1 and ready 0, so addr a0 waits.
Slot 1: the stall persists and addr still reads a0.
Slot 2: valid and ready are both 1, so the transfer accepts.
Slot 3: valid is 0 and addr is 0, the gap idle cycle.
Slot 4: valid 1 with addr b1 accepts at once.
Slot 5: valid is 0 and the bus rests idle.
The next listing is a fragment and is not run in this course's checks: bus_if and bus_txn are defined elsewhere, and it has no top module.
// not-run: fragment: bus_if and bus_txn are defined elsewhere, no top module
class driver;
mailbox #(bus_txn) in;
virtual bus_if vif;
int unsigned driven_n;
function new(mailbox #(bus_txn) in);
this.in = in;
endfunction
task run();
drive_idle();
forever begin
bus_txn t;
in.get(t);
repeat (t.gap) @(vif.drv_cb);
fork
begin // isolating fork: disable fork stops only these two
fork
drive_one(t);
watch_reset();
join_any
disable fork;
end
join
if (vif.rst_n !== 1) handle_reset();
end
endtask
task drive_one(bus_txn t);
@(vif.drv_cb);
vif.drv_cb.valid <= 1;
vif.drv_cb.write <= (t.kind == WRITE);
vif.drv_cb.addr <= t.addr;
vif.drv_cb.wdata <= t.wdata;
t.t_driven = $time;
do @(vif.drv_cb); while (vif.drv_cb.ready !== 1);
drive_idle();
driven_n++;
endtask
task drive_idle();
vif.drv_cb.valid <= 0;
vif.drv_cb.write <= 0;
vif.drv_cb.addr <= '0;
vif.drv_cb.wdata <= '0;
endtask
task watch_reset();
@(negedge vif.rst_n);
endtask
task handle_reset();
bus_txn dropped;
drive_idle();
while (in.try_get(dropped))
$display("DRV: flushed txn%0d on reset", dropped.id);
@(posedge vif.rst_n);
repeat (2) @(vif.drv_cb);
endtask
endclassAll pin writes use the clocking block with nonblocking assigns, so output skew removes drive races. The ready wait compares with !== 1, so an X stalls instead of passing as truthy. The drive-or-reset race sits inside its own fork ... join, so disable fork kills only this driver's two threads. A named disable would kill that block in every driver instance.
Payload stays stable while valid is high and ready is low, per handshake rules.
Reset handling idles pins first, flushes pre-reset stimulus, then re-synchronizes.
The driver honors gap spacing on top of one idle cycle it always inserts after a handshake, so gap 0 still gives one idle cycle. True back-to-back needs the next item driven at the handshake edge when gap is 0.
The next listing is a fragment and is not run in this course's checks: axi_if and bus_txn are defined elsewhere, and it has no top module.
// not-run: fragment: axi_if and bus_txn are defined elsewhere, no top module
class pipelined_driver;
mailbox #(bus_txn) in;
virtual axi_if vif;
bus_txn pending [bit [3:0]];
semaphore id_pool; // bounds how many are outstanding
bit [3:0] free_ids[$]; // which IDs are not in flight
function new(mailbox #(bus_txn) in, int max_out = 8);
if (max_out > 16) $fatal(1, "max_out %0d exceeds the 16 IDs", max_out);
this.in = in;
id_pool = new(max_out);
for (int i = 0; i < 16; i++) free_ids.push_back(i);
endfunction
task run();
vif.drv_cb.rready <= 1; // always accept responses
fork
issue_loop();
response_loop();
join_none
endtask
task issue_loop();
forever begin
bus_txn t;
in.get(t);
id_pool.get(1);
t.id_tag = free_ids.pop_front(); // never an ID still in flight
pending[t.id_tag] = t;
@(vif.drv_cb);
vif.drv_cb.arvalid <= 1;
vif.drv_cb.arid <= t.id_tag;
vif.drv_cb.araddr <= t.addr;
do @(vif.drv_cb); while (vif.drv_cb.arready !== 1);
vif.drv_cb.arvalid <= 0;
end
endtask
task response_loop();
forever begin
bit [3:0] rid;
@(vif.drv_cb);
if (vif.drv_cb.rvalid === 1) begin // rready is held at 1 by run()
rid = vif.drv_cb.rid;
if (!pending.exists(rid))
$display("DRV: response for unknown id %0d", rid);
else begin
pending[rid].rdata = vif.drv_cb.rdata;
pending.delete(rid);
free_ids.push_back(rid); // the ID may be reused now
id_pool.put(1);
end
end
end
endtask
endclassIssue and response run as separate processes joined by an id-keyed pending array, so out-of-order responses match by lookup. The semaphore throttles outstanding count with one get per issue and one put per response. A free-ID queue picks the tag: a plain counter would wrap onto an ID that is still pending when one response is slow, and overwrite it. A one-key semaphore around the pin sequence serializes two masters onto a shared bus.
The bench deadlocks at exactly max_outstanding transactions. Every key stays checked out, so issue blocks forever.
A one-key semaphore around the full pin sequence. The holder owns the bus for one complete transaction.
A sink that stalls its first cycles and checks the handshake rules at the pins, plus a testbench that drives two transfers through it like the driver above: hold through the stall, one idle gap, then the next transfer.
module stall_sink (
input wire clk,
input wire valid,
input wire [7:0] addr,
output reg ready,
output reg [7:0] got0,
output reg [7:0] got1,
output reg [1:0] n_acc,
output reg moved
);
timeunit 1ns; timeprecision 1ps;
reg [2:0] c = 0;
reg [7:0] last = 0;
reg stalled = 0;
initial begin ready = 0; n_acc = 0; moved = 0; got0 = 0; got1 = 0; end
always @(posedge clk) begin
c <= c + 3'd1;
ready <= (c >= 3'd2); // stalls the first cycles
if (valid && ready) begin // accept
if (n_acc == 0) got0 <= addr; else got1 <= addr;
n_acc <= n_acc + 2'd1;
end
if (stalled && (!valid || addr !== last)) moved <= 1; // retracted or changed
stalled <= valid && !ready;
last <= addr;
end
endmodule