In a senior interview you will be asked to sketch an agent for a protocol, and the interviewer is not checking whether you know that a driver calls get_next_item. They want to hear the two or three things that make this protocol's agent hard: the access phase APB drivers forget, the outstanding IDs AXI drivers must track, the stability rule every stream driver must keep under backpressure. Every protocol agent has the same skeleton. What changes between protocols, and where do the protocol's own rules get checked?
Every protocol agent has an item, a driver loop, a monitor that reconstructs items from the pins, and a sequencer. The protocol decides three things about that skeleton: when a transfer completes (the monitor's publish condition), what the driver must hold still or wait for (its timing rules), and how many transfers can be in flight (whether the driver is a simple loop or a set of threads). Think of a skeleton with the same bones in every animal; what differs is how the joints move.
APB | AXI (full) | AXI-Stream | |
|---|---|---|---|
Channels | One, with setup and access phases | Five: AW, W, B, AR, R, each valid/ready | One, valid/ready |
Transfer completes when | PSEL, PENABLE and PREADY are all 1 at a clock edge | Per channel: valid and ready at an edge; a transaction when its last data or response arrives | TVALID and TREADY at an edge |
Driver timing rule | Setup one clock (PENABLE=0), then hold access until PREADY | Hold each channel's payload stable until its handshake; never wait for ready before asserting valid | TVALID must not wait for TREADY; hold TDATA, TLAST, TKEEP stable while TVALID is high and TREADY low |
In flight at once | One | Many, tracked by ID; same-ID in order | One beat; packets are a higher layer |
Driver shape | A loop | One thread per channel and an outstanding table | A loop |
Slave side | PREADY wait states, PSLVERR | Reactive slave with a memory, reordering by ID | Random TREADY (a reactive slave) |
The bench's apb_driver is the reference: setup on one clock, PENABLE raised on the next, then do @(posedge clk); while (!pready);, which always spends at least one edge with PENABLE high before it looks at PREADY. A driver that tests PREADY in the same timestep it raised PENABLE ends the transfer before the access phase ever reaches the bus when the slave has no wait states. The edge-by-edge walk-through is in The driver. Two senior details on top: a production APB driver bounds the PREADY wait with a timeout error instead of hanging, and the monitor publishes on the completing edge only, with PRDATA and PSLVERR sampled there.
The stream driver in the bench is short, and two of its lines are the protocol:
// snippet: from the course DMA bench
class axis_driver extends uvm_driver #(axis_item);
`uvm_component_utils(axis_driver)
virtual axis_if vif;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (!uvm_config_db#(virtual axis_if)::get(this, "", "vif", vif))
`uvm_fatal("NOVIF", "axis_if not set for this driver")
endfunction
task run_phase(uvm_phase phase);
vif.tvalid <= 1'b0;
@(posedge vif.rst_n);
forever begin
axis_item beat;
seq_item_port.get_next_item(req);
beat = pre_drive(req);
if (beat != null) begin
repeat (beat.idle) @(posedge vif.clk);
@(posedge vif.clk);
vif.tdata <= beat.data;
vif.tlast <= beat.last;
vif.tvalid <= 1'b1;
do @(posedge vif.clk); while (!vif.tready); // the beat moves on TVALID && TREADY
vif.tvalid <= 1'b0;
end
seq_item_port.item_done();
end
endtask
// Hook for derived drivers: return the beat to drive (the sequence's item, or a
// modified copy), or null to drop it. The base driver drives every item as is.
virtual function axis_item pre_drive(axis_item it);
return it;
endfunction
endclassIt raises TVALID without looking at TREADY first. AXI-Stream forbids a source from waiting for TREADY before asserting TVALID, because a sink may legally wait for TVALID before asserting TREADY, and two sides waiting for each other deadlock.
Once TVALID is high, nothing changes until do @(posedge clk); while (!vif.tready); sees the handshake. TDATA and TLAST stay stable for as many stall cycles as the sink wants.
Then decide the item granularity. The bench uses one item per beat, which suits the DMA scoreboard because every word is predicted on its own. A packet-level item (a queue of beats plus TKEEP and TUSER) suits protocols where meaning lives at packet level, and makes packet-length coverage a coverpoint instead of a counter. Many VIPs publish both: beats on one analysis port, assembled packets on another, so each subscriber takes the level it needs.
A full AXI master cannot be one loop, because the protocol's whole point is overlap: addresses issued before earlier data returns, responses out of order across IDs. The driver becomes one thread per channel sharing an outstanding table that holds a queue per ID. Same-ID transactions complete in order, different IDs may not, write data is never interleaved between transactions in AXI4, and read data beats of different IDs may interleave. That driver is built, run and explained in Pipelined and out-of-order drivers. The monitor mirrors it: per-channel samplers feed a correlator that joins address, data and response into one transaction by ID, and the scoreboard keys a queue per ID as well.
A protocol rule such as "TDATA is stable while stalled" can be checked by the monitor in code, but the natural home is a concurrent assertion in the interface. It works in active and passive mode alike, it fires at the exact cycle of the violation, and formal tools can prove it on the RTL with no testbench at all:
// snippet: AXI-Stream source rules as assertions inside axis_if
property p_stable_while_stalled;
@(posedge clk) disable iff (!rst_n)
(tvalid && !tready) |=> (tvalid && $stable(tdata) && $stable(tlast));
endproperty
a_stable: assert property (p_stable_while_stalled)
else $error("AXIS: TVALID dropped or payload changed while stalled");
a_no_x_valid: assert property (@(posedge clk) disable iff (!rst_n) !$isunknown(tvalid))
else $error("AXIS: TVALID is X out of reset");Give each assertion a switch (an interface bit the agent sets from its config) so an error-injection test can turn off exactly the rule it breaks on purpose, and nothing else.
dma_smoke_test with +UVM_VERBOSITY=UVM_HIGH shows what each monitor considers one transfer. An excerpt:
UVM_INFO dma_tb.sv(151) @ 75000: uvm_test_top.env.apb.mon [APB_MON] WR addr=0c data=a5a50f0f
UVM_INFO dma_tb.sv(151) @ 115000: uvm_test_top.env.apb.mon [APB_MON] WR addr=08 data=00000008
UVM_INFO dma_tb.sv(151) @ 155000: uvm_test_top.env.apb.mon [APB_MON] WR addr=00 data=00000003
UVM_INFO dma_tb.sv(299) @ 205000: uvm_test_top.env.in_agt.mon [AXIS_MON] data=1364cd0b
UVM_INFO dma_tb.sv(299) @ 215000: uvm_test_top.env.out_agt.mon [AXIS_MON] data=b6c1c204
UVM_INFO dma_tb.sv(299) @ 435000: uvm_test_top.env.in_agt.mon [AXIS_MON] data=aee114a1 LAST
UVM_INFO dma_tb.sv(151) @ 475000: uvm_test_top.env.apb.mon [APB_MON] RD addr=04 rdata=00000002APB writes complete 40 ns apart: setup, access, one wait state from the DMA, and the driver's idle clock. One transfer at a time, as the protocol says.
Each stream word appears at the output one clock after the input (205 ns in, 215 ns out), XORed with KEY: the DMA's single output register. Data values are random per seed.
The STATUS read at 475 ns returns DONE (0x2), published on the edge where PREADY was high, with PRDATA from that edge.
A team's stream VIP passed every block test for a year. At subsystem level it was connected to a third-party sink IP and the first packet never moved. The waveform showed TVALID low and TREADY low forever. The VIP's driver did wait (vif.tready); vif.tvalid <= 1;, waiting for the sink to be ready before offering data. Every sink it had met so far raised TREADY while idle, so it had never mattered. The new sink, legally, waited for TVALID before raising TREADY.
Both sides waited for each other: a protocol deadlock written into the testbench. The fix was the bench's shape (assert TVALID, then wait for the handshake), and the lesson was the assertion above plus a slave-side test: run the VIP's own regression against a sink that never asserts TREADY before TVALID, which is exactly what the bench's reactive slave does.
Two items per access: one on the wait-state cycle, where PREADY is 0 and PRDATA is not yet valid, and one on the completing cycle. The transfer completes only on the edge where PSEL, PENABLE and PREADY are all 1, which must be the publish condition.
The source. A sink may wait for TVALID before asserting TREADY; a source must not wait for TREADY before asserting TVALID. Together they deadlock, and the source is the side at fault.
So an error-injection test can switch off exactly the rule it violates on purpose, keeping every other check active. Without enables, the only options are to tolerate the assertion failure (which breaks the pass criterion) or to disable assertions wholesale (which hides real violations).
Next: a VIP that is right today still has to change tomorrow without breaking its users. Versioning and delivery covers how.