A driver on a clocked bus
Make item_done mean that the design accepted the item, keep the pins honest while the driver waits, and pick the right driver call for idle cycles.
- Lesson
- U4.3
- Time
- 25 min
- Runs on
- Verilator with UVM
- Check
- 5 questions
The incrementer gains a ready output: after accepting a value, it needs two more cycles before it can take the next one. The driver from U4.1 holds each value for one clock and then calls item_done regardless. The sequence sees three items finish, but the design accepted only two of them.
Columns are rising edges. The driver offered 32 at one edge only, while the design was busy, and called item_done anyway.
What item_done promises
UVM does not define what "done" means for an item. The driver decides, by choosing when to call item_done. The sequence only learns that finish_item has returned, and it cannot tell a value the design took from a value the design never saw.
On a bus with backpressure, the useful promise is "the design accepted this item". Here a transfer happens at a rising edge where valid and ready are both 1. If the driver calls item_done at that edge and no earlier, a returned finish_item means the value is inside the design.
The hook's driver made a weaker promise: "I showed the value for one clock". That was true and useless, because the design was not listening. The value 32 was on offer at one edge where ready was 0, and then the driver moved on.
Hold the item until the design takes it
forever begin
@(negedge vif.clk);
seq_item_port.try_next_item(req);
if (req == null) begin
vif.valid <= 0; // nothing to send: an idle cycle
continue;
end
vif.valid <= 1;
vif.x <= req.x;
do @(posedge vif.clk); while (!vif.ready); // the edge that accepts it
seq_item_port.item_done();
endThe driver works on falling edges and the design on rising edges, half a cycle apart, as in U1. At each falling edge the driver either offers the next item or drives an idle cycle.
After offering a value, it waits for rising edges until one has ready at 1. The do ... while loop reads ready just after each edge. Because the design updates ready with a nonblocking assignment, that is the value the design saw at the edge. Only then does the driver call item_done.
Follow the second item in the trace below. At 25 and 35 ns the design is still busy with 16, so ready is 0 and the driver keeps 32 on the bus. At 45 ns ready is back, the design takes 32, and the driver calls item_done.
Columns are rising edges. Each finish_item returns at the edge that accepts its value.
Compare the example's log with this trace. The three finish_item calls return at 15, 45 and 75 ns, exactly the edges where the monitor sees a transfer. The sequence now runs at the design's pace without knowing anything about ready.
The sequence stays the same
Look at the example's sequence: it is the same four steps as in U4.1, with no mention of ready, clocks or stalls. All knowledge of the protocol lives in the driver. The sequence only waits a little longer in finish_item when the design is slow.
This split is what makes sequences reusable. The same sequence runs against a design that never stalls, one that stalls every other cycle, or a model with no pins at all. A test that needs a different traffic pattern swaps the sequence, and a new bus protocol swaps the driver.
The split also means the sequence cannot see a stall that never ends. If ready stays at 0, the driver waits forever, the sequence waits in finish_item, and the test hangs until the global timeout. A driver can guard against this with a cycle counter in its wait loop that reports a uvm_error after, say, 1,000 cycles without ready. The error then names the stuck transfer instead of leaving a silent timeout.
Pins keep their value while the driver waits
A driver that blocks keeps driving whatever it drove last. A blocked get_next_item does not clear valid. So a driver that leaves valid at 1 and then waits for its next item tells the design that the old value is still on offer.
If the next item is already waiting, this never shows, because the new value replaces the old one at the next falling edge. It shows when the sequence runs out of items or pauses between them. The design then accepts the last value again, every time ready returns, and a scoreboard counts more transfers than the sequence sent.
Waits with valid still high
seq_item_port.get_next_item(req);
@(negedge vif.clk);
vif.valid <= 1;
vif.x <= req.x;
do @(posedge vif.clk); while (!vif.ready);
seq_item_port.item_done();
After the last item, valid stays 1 while the driver waits, and the design accepts the last value again.
Drops valid before it waits
seq_item_port.get_next_item(req);
@(negedge vif.clk);
vif.valid <= 1;
vif.x <= req.x;
do @(posedge vif.clk); while (!vif.ready);
seq_item_port.item_done();
@(negedge vif.clk) vif.valid <= 0;
Correct, at the cost of an idle cycle between items. The try_next_item loop above avoids even that.
The try_next_item loop has no such gap. It makes a fresh decision at every falling edge: when an item is waiting it offers that item, and when none is waiting it drives valid to 0 for that cycle. Back-to-back items flow without an idle cycle, and the bus goes quiet the moment the sequence stops. The example shows both, with two idle cycles at the end of its log.
Choosing the driver call
Choose what releases the sequence: taking responsibility for the request, or finishing its bus transfer. U6.1's first pipelined item makes the distinction visible.
This is U6.1's first item, with unevenly spaced milestones. Its driver uses get at 0 ns and returns a separate response at 15 ns. The serial driver above waits for acceptance before item_done.
| Call | Waits for | Completes the handshake? | Use it when |
|---|---|---|---|
get_next_item(req) | An item to be available | No: call item_done later | The sequence must wait until the bus transfer is over |
try_next_item(req) | Nothing; req is null if no item is waiting | No: call item_done later, but only if req is not null | The driver chooses between an item and an idle cycle at every edge |
get(req) | An item to be available | Yes, at handoff | The next request may be issued while this one is still in flight |
peek(req) | An item to be available | No; the item stays queued | The driver must look at an item before taking it |
item_done() | Nothing: it is a function | Yes | After every item taken with get_next_item or try_next_item |
Driver-side calls and what each one waits for.
Read the table by what each call waits for. Only get_next_item, get and peek can block the driver, and while they block the pins hold their last values. try_next_item and item_done always return in the same time step. peek suits a driver that must see an item's kind before choosing which bus phase to start; it then takes the item with get once it has committed.
With get, the sequence's finish_item can return at handoff, before the driver moves any pins. The driver may still return a separate result later with put(rsp) and set_id_info(req). U6.1 uses this split to overlap requests: handoff frees the sequence to send, while each response marks completion.
try_next_item waits a few delta cycles for a sequence that is about to offer an item, then gives up. If a sequence was granted but spends simulation time before its finish_item, uvm-core reports a TRY_NEXT_BLOCKED error and returns null. That is one more reason never to delay between start_item and finish_item.
Try it
The monitor prints each transfer it sees, and the sequence prints the time each finish_item returns. The two always agree, because the driver calls item_done only at an accepting edge. After the last item, try_next_item finds nothing and the driver drives idle cycles.
Replace the do ... while line with a single @(posedge vif.clk); and run again. The monitor now reports fewer transfers than the sequence sent, which is the hook's bug.
10 ns driver offering x=16 15 ns monitor design accepted x=16 15 ns sequence finish_item returned for x=16 20 ns driver offering x=32 45 ns monitor design accepted x=32 45 ns sequence finish_item returned for x=32 50 ns driver offering x=48 75 ns monitor design accepted x=48 75 ns sequence finish_item returned for x=48 80 ns driver no item: idle cycle 90 ns driver no item: idle cycle UVM_INFO main.sv(85) @ 95: uvm_test_top.mon [MON] 3 transfers UVM_ERROR : 0Check yourself
Remember
- The driver decides what
item_donemeans; tie it to the design accepting the item. - Wait for an edge with
validandreadyboth high before callingitem_done. - A blocked driver keeps driving its last values, so drop
validbefore waiting for an item that may not come. try_next_itemlets the driver choose between an item and an idle cycle at every edge; anullresult needs noitem_done.getcompletes the request handshake at handoff; a separate response can report the result later.
Practice
- The burst driver that outran its receiver Fix · Easy