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.

A one-clock driver against a design that stalls
012345edge (ns)152535455565validreadyx163248accepted16-48-x=32 offered while ready is 0: lost

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

The example driver's loop: one decision at every falling edge.
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();
end

The 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.

The example: three items through a design that stalls
0123456edge (ns)15253545556575validreadyx163248sequencedone 1wait 2done 2wait 3done 3accepts 16accepts 32accepts 48

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.

One request, three milestones in U6.1
012time (ns)0515driver / pinsget(req)acceptedcompletefinish_itemreturnsdoneget_responsewaitingreturnshandoffacceptancecompletion

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.

CallWaits forCompletes the handshake?Use it when
get_next_item(req)An item to be availableNo: call item_done laterThe sequence must wait until the bus transfer is over
try_next_item(req)Nothing; req is null if no item is waitingNo: call item_done later, but only if req is not nullThe driver chooses between an item and an idle cycle at every edge
get(req)An item to be availableYes, at handoffThe next request may be issued while this one is still in flight
peek(req)An item to be availableNo; the item stays queuedThe driver must look at an item before taking it
item_done()Nothing: it is a functionYesAfter 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.

RUNVerilator + UVM 2020.3.1 · simulator service
Edit the code, then run it
Expected among the UVM messages: 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 : 0

Check yourself

Predict the outputThe design is busy until 32 ns. What line does the $display print? UVM's own messages are not part of the answer.
`timescale 1ns/1ns
`include "uvm_macros.svh"
import uvm_pkg::*;
interface bus_if (input logic clk);
  logic       valid = 0, ready = 0;
  logic [7:0] x = 0;
endinterface
class item_c extends uvm_sequence_item;
  `uvm_object_utils(item_c)
  bit [7:0] x;
  function new(string name = "item_c"); super.new(name); endfunction
endclass
class drv_c extends uvm_driver #(item_c);
  `uvm_component_utils(drv_c)
  virtual bus_if vif;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  task run_phase(uvm_phase phase);
    forever begin
      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();
    end
  endtask
endclass
class seq_c extends uvm_sequence #(item_c);
  `uvm_object_utils(seq_c)
  function new(string name = "seq_c"); super.new(name); endfunction
  task body();
    int done[2];
    for (int i = 0; i < 2; i++) begin
      item_c it = item_c::type_id::create("it");
      start_item(it);
      it.x = 8'(i + 1);
      finish_item(it);
      done[i] = $time;
    end
    $display("finish_item returned at %0d and %0d", done[0], done[1]);
  endtask
endclass
class my_test extends uvm_test;
  `uvm_component_utils(my_test)
  drv_c drv;
  uvm_sequencer #(item_c) sqr;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    virtual bus_if vif;
    drv = drv_c::type_id::create("drv", this);
    sqr = uvm_sequencer #(item_c)::type_id::create("sqr", this);
    if (!uvm_config_db #(virtual bus_if)::get(this, "", "vif", vif))
      `uvm_fatal("NOVIF", "no virtual interface")
    drv.vif = vif;
  endfunction
  function void connect_phase(uvm_phase phase);
    drv.seq_item_port.connect(sqr.seq_item_export);
  endfunction
  task run_phase(uvm_phase phase);
    seq_c seq = seq_c::type_id::create("seq");
    phase.raise_objection(this);
    seq.start(sqr);
    phase.drop_objection(this);
  endtask
endclass
module tb;
  bit clk;
  always #5 clk = ~clk;              // rising edges at 5, 15, 25, ...
  bus_if bus (clk);
  initial #32 bus.ready = 1;         // the design is busy until 32 ns
  initial begin
    uvm_config_db #(virtual bus_if)::set(null, "uvm_test_top", "vif", bus);
    run_test("my_test");
  end
endmodule
Spot the bugThe design is busy for two cycles after each transfer, and the test checks that it accepted exactly 16, 32 and 48. Each option is the body of the driver's forever loop. Which one fails?
ConceptA driver uses seq_item_port.get(req) and then spends three cycles driving the item. When does the sequence's finish_item return?
ConceptAt a falling edge, try_next_item(req) returns with req set to null. What should the driver do?
Read the waveformThe driver calls item_done at the rising edge that accepts each value. At which edge does finish_item return for x = 7?
Rising edges on a bus with backpressure
012345edge (ns)51525354555validreadyx0678

One column per rising edge.

Remember

  • The driver decides what item_done means; tie it to the design accepting the item.
  • Wait for an edge with valid and ready both high before calling item_done.
  • A blocked driver keeps driving its last values, so drop valid before waiting for an item that may not come.
  • try_next_item lets the driver choose between an item and an idle cycle at every edge; a null result needs no item_done.
  • get completes the request handshake at handoff; a separate response can report the result later.

Practice