An interviewer slides a marker across the table: "Draw what happens between start_item and item_done. Who blocks, and on what?" Most candidates draw a sequence, an arrow, and a driver. The follow-up questions, when is the sequence granted?, when does randomize run?, what if the driver calls item_done twice?, all live in the space that drawing leaves out.
This lesson fills it in. It is the home of the section's key picture: the four calls, the three parties, and what each one waits for.
In a restaurant kitchen, the pass is the counter between the cooks and the waiters. Waiters queue their order slips there; the head chef picks the next one when a cook is free; the cook takes it, cooks, and puts the plate back on the pass, which tells the waiter it is ready. The sequencer is the pass. Sequences queue requests on it, the driver takes one when it is ready, and the driver's "done" is what releases the sequence.
The crucial word is pull. The driver asks for work when it is free. A sequence that calls start_item is only asking for a turn; it is granted when the driver next asks for an item and the sequencer's arbitration picks that sequence. Everything that confuses people about the handshake follows from that one fact.
`start_item` blocks until the grant. If the driver is busy with another transfer, the sequence waits here. With several sequences on one sequencer, this is where they queue (see Inside the sequencer).
Randomize between the two calls. The values are chosen after the grant, at the last moment before the driver sees the item.
`finish_item` blocks until `item_done`. The sequence cannot run ahead of the driver, which is why it can read rdata the moment finish_item returns.
`get_next_item` blocks until an item is sent. A driver with nothing to do simply waits there.
// snippet: from the course DMA bench
class apb_driver extends uvm_driver #(apb_item);
`uvm_component_utils(apb_driver)
virtual apb_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 apb_if)::get(this, "", "vif", vif))
`uvm_fatal("NOVIF", "apb_if not set for this driver")
endfunction
task run_phase(uvm_phase phase);
vif.psel <= 1'b0;
vif.penable <= 1'b0;
@(posedge vif.rst_n);
forever begin
seq_item_port.get_next_item(req);
drive(req);
seq_item_port.item_done(); // rdata travels back inside req itself
end
endtask
// Setup phase: PSEL=1, PENABLE=0 for one clock. Access phase: PENABLE=1, held
// until the slave answers PREADY=1; read data is valid in that cycle.
// Every wait is on the same event, @(posedge vif.clk), and every drive is a
// nonblocking assignment, so a value read right after the edge is the value
// from before it: the one the DUT sampled.
task drive(apb_item it);
@(posedge vif.clk);
vif.paddr <= it.addr;
vif.pwrite <= it.write;
vif.pwdata <= it.data;
vif.psel <= 1'b1;
vif.penable <= 1'b0;
@(posedge vif.clk);
vif.penable <= 1'b1;
do @(posedge vif.clk); while (!vif.pready);
it.rdata = vif.prdata;
it.slverr = vif.pslverr;
vif.psel <= 1'b0;
vif.penable <= 1'b0;
endtask
endclassThe loop is the canonical shape: get_next_item, drive, item_done, forever. Inside drive, the setup clock raises PSEL with PENABLE low; the access phase raises PENABLE and holds it until PREADY. The do @(posedge vif.clk); while (!vif.pready); form matters: it waits at least one edge before looking at PREADY, so with a zero-wait slave the read data is captured in the cycle the slave really completes the access, not one cycle early.
The base test's reg_read helper shows the payoff of the in-place response. It starts one apb_rw_seq, and by the time start returns, finish_item has returned inside it, so s.rdata already holds what the slave sent:
// snippet: from dma_base_test
task reg_read(bit [7:0] addr, output bit [31:0] data);
apb_rw_seq s = apb_rw_seq::type_id::create("s");
s.addr = addr; s.write = 0; s.data = 0;
s.start(env.apb.sqr);
data = s.rdata; // copied from the item after finish_item returned
endtask// 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
endclassTwo details are worth stealing. First, the virtual pre_drive hook lets a derived driver change or drop a beat without copying the loop, which is how dma_truncate_test works. Second, item_done() sits outside the if (beat != null): whether the beat was driven or dropped, the sequencer's item is released exactly once. That exactly-once rule is the whole contract:
Driver mistake | What UVM does |
|---|---|
Never calls | The sequence stays blocked in |
Calls |
|
Calls |
|
`try_next_item(req)` returns without consuming simulation time, with null if no sequence has an item ready. A driver that must drive idle cycles on every clock when there is no work uses it: if req == null, drive one idle cycle and try again.
`get(req)` is get_next_item and item_done in one call: the sequence is released immediately, before anything is driven. Pipelined drivers use it and send responses later; see Responses and pipelined drivers.
`peek(req)` returns the next item without completing it; a later get completes it.
The APB monitor's lines from dma_smoke_test at UVM_HIGH show the handshake's rhythm (Verilator, times in ps):
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(151) @ 475000: uvm_test_top.env.apb.mon [APB_MON] RD addr=04 rdata=00000002One access completes every 40 ns: the driver's wait for the next edge, the setup clock, the access clock and the DMA's one wait state. The next reg_write cannot start sooner, because its sequence is granted only when the driver calls get_next_item again after item_done.
The read returns 00000002, DONE set. The test's check of status[1] passes because the driver wrote rdata into the request before item_done.
While the bench was being built, a random test failed on two jobs out of three. The DMA reported STATUS.ERR because each packet arrived one word short. Yet the sequence had generated all N items, and the stream driver had called item_done N times. By the handshake's own accounting, every beat had been sent.
The cause was in how the drivers waited. The APB driver finished the GO write on a clock edge. In the same timestep, the stream driver received its first item and waited on the stream interface's clocking block event. The two interfaces had separate clocking blocks on the same clock edge, and the order in which their events fire within a timestep is undefined. The stream driver caught the current edge instead of the next one. Its clocking-block drive and sample then disagreed with what the DUT saw: the driver believed TREADY was high on the first beat and called item_done, but the DUT never took that word.
The fix is the timing rule the bench uses everywhere: drivers wait on @(posedge vif.clk), a single shared event, and drive with nonblocking assignments; monitors sample through a clocking block with #1step input skew. The lesson for interviews is that item_done reports what the driver believes happened. It is only as trustworthy as the driver's timing, which is why the scoreboard checks the monitored pins and never the driver's count.
axis_truncating_driver returns null for beats after the cut. Why does the base driver still call item_done for those beats?Each of those beats is an item the sequence is waiting on in finish_item. Skipping item_done would leave axis_pkt_seq blocked forever, and the next get_next_item would report that it was called twice without an item_done. Dropping a beat from the pins does not remove it from the handshake.
start_item at 100 ns, while the APB driver is in the middle of a transfer that ends at 130 ns. When does start_item return, and when are the item's random values chosen?Around 130 ns, when the driver calls item_done for the current transfer and then get_next_item, and the sequencer grants this sequence. The values are chosen after that, because the bench randomizes after start_item returns. That is late randomization: the values reflect the moment the item really goes out.
item_done(req). Which bench sequence is at risk, and why not apb_rw_seq?Each apb_rw_seq sends one item, so its response queue only ever holds one response, and the sequence ends with it unread. apb_poll_seq loops, sending a read every 50 ns, so after 8 polls its queue is full. From the 9th poll on, responses are dropped, with a UVM_ERROR in UVM 1.2. A long job turns a harmless refactor into a red regression.
Next: when the handshake breaks, the symptom is silence. Stalled sequences turns that silence into a short list of places to look.