Sequences and the item handshake

Write a sequence that creates, randomizes and sends items, and know exactly where the sequence and the driver wait for each other.

Lesson
U4.1
Time
25 min
Runs on
Verilator with UVM
Check
5 questions

A new driver passes a one-item smoke test, then a ten-item test never finishes. The log shows the first item driven again and again, an error saying get_next_item was called twice, and finally UVM's timeout. The driver loop never called item_done, so the sequence is still waiting inside finish_item for item 1.

TimeSequenceDriver
0 nsCalls start_item, is granted, calls finish_item and waitsCalls get_next_item and receives item 1
10 nsWaiting in finish_itemDrives item 1, then loops back without item_done
20 nsWaiting in finish_itemCalls get_next_item again: an error, and item 1 comes back
Every cycle afterWaiting in finish_itemDrives item 1 again, until the timeout

The driver never completed the handshake for item 1, so neither side could move on.

A sequence is a recipe for items

In the layered bench, a generator made transactions and put them in a mailbox. UVM splits that job in two. A sequence is an object that decides which items to send. A sequencer is a component that passes them to the driver one at a time.

The item is a class derived from uvm_sequence_item. Its rand fields are the values a test may randomize, and its constraints keep them legal. The example item has one field, x, constrained to the range 16 to 31.

A sequence extends uvm_sequence, parameterized with the item type it sends. Its work lives in one task, body(). The test runs it with seq.start(sqr), which calls body() and returns when body() ends. So start blocks, and the test raises its objection around it.

A sequence can also start other sequences inside its own body(), so long tests are built from small, tested pieces. U10 uses this for virtual sequences, which coordinate several sequencers at once.

Four steps send one item

The four steps inside body(), once per item.
incr_item it = incr_item::type_id::create("it");  // 1. create
start_item(it);                                   // 2. wait for a grant
if (!it.randomize())                              // 3. fill in the values
  `uvm_error("RAND", "randomize failed")
finish_item(it);                                  // 4. send it, wait for item_done

Create the item through the factory, as U2 showed, so a test can override its type. Start with start_item(it), which asks the sequencer for permission to send. It returns when the sequencer grants this sequence the driver.

Fill in the item, usually with randomize(). Check the result: a failed randomization returns 0 and leaves the old values in place, and nothing else warns you. Finish with finish_item(it), which hands the item to the driver. It returns only when the driver says it has finished with that item.

Spend no simulation time between start_item and finish_item. The driver is already waiting for this item, so any delay there stalls the bus.

The handshake, call by call

The driver pulls items; the sequence never pushes them. The driver's loop calls seq_item_port.get_next_item(req), which blocks until an item arrives. Later it calls seq_item_port.item_done() to say it has finished. These two calls pair with the sequence's two calls.

StepSequenceSequencerDriver
1start_item(it): waits for a grantQueues the requestBusy, or not asking yet
2Still waitingArbitrates and grants the sequenceCalls get_next_item(req) and waits
3start_item returns; randomize()-Still waiting
4finish_item(it): waits for donePasses the item handle onget_next_item returns req, the same object
5Still waiting-Drives the pins; time passes
6finish_item returnsReleases the sequenceCalls item_done()
7Next start_item-Next get_next_item

One item through the handshake, as a sequence diagram read top to bottom. Simulation time passes only in step 5.

Read the table as two waits on each side. The sequence waits in start_item until the driver asks for an item, and in finish_item until the driver says it is done. The driver waits in get_next_item until a sequence has an item ready. Nobody polls; each side blocks until the other side acts.

Step 4 matters in the next lesson. The driver receives the very object the sequence created, not a copy. Anything the driver writes into req is visible to the sequence once finish_item returns.

Reading the handshake in a log

The example prints one line at each of these calls. Its driver keeps the bus idle for two clock cycles after time 0, then serves three items, two cycles each.

The example's handshake in 10 ns steps
0123456time (ns)0102030405060sequencewait grantwait done 1wait done 2wait done 3driveridlegot item 1drive 1done 1, got 2drive 2done 2, got 3validdriver asks: grantitem_done for item 1

The sequence asks at 0 ns but is granted at 20 ns, when the driver first calls get_next_item.

Two waits on each side of the sequencerDashed pulses are calls; solid tokens are the item and pin values; outlined markers are a call returning. Times are the example's log lines. The clock's falling edges are at 10, 20, 30 and 40 ns.
Two waits on each side of the sequencerDashed pulses are calls; solid tokens are the item and pin values; outlined markers are a call returning. Times are the example's log lines. The clock's falling edges are at 10, 20, 30 and 40 ns. sequence incr_seq.body start_item,randomize, finish_item component Sequencer Grants a waitingsequence to an askingdriver component Driver get_next_item, drive,item_done design Incrementer y = x + 1 start_item /finish_itemget_next_item /item_donevalid, xy
  1. The sequence asks first0 ns (illustrative)

    The sequence calls start_item and waits. The driver is keeping the bus idle for two falling edges and has not asked for anything.

    More context

    Log line 0 ns sequence start_item: waiting for a grant. The request is queued in the sequencer; nothing can be granted yet.

    Follow the calls, values and waits
    1. call start_item(it): incr_seq.body → Sequencer
    2. Waiting: incr_seq.body — in start_item: no driver asking
    3. Waiting: Driver — idle until 20 ns
  2. The driver asks: grant20 ns (illustrative)

    The driver calls get_next_item. Now a driver is asking, so the sequencer grants the waiting sequence and start_item returns.

    More context

    The grant waited 20 ns for the driver, not for arbitration: with one sequence there is no one to choose between.

    Follow the calls, values and waits
    1. call get_next_item(req): Driver → Sequencer
    2. return grant: Sequencer → incr_seq.body
    3. Waiting: Driver — in get_next_item
    4. Released: incr_seq.body
    5. Released: Driver
  3. Randomize late, then hand over20 ns (illustrative)

    The sequence randomizes x=29 and calls finish_item. The sequencer passes that same object to the driver, and get_next_item returns it.

    More context

    Log lines 20 ns sequence granted; x=29; finish_item and 20 ns driver got x=29. One handle travels: req in the driver is it in the sequence.

    Follow the calls, values and waits
    1. call finish_item(it): incr_seq.body → Sequencer
    2. data item 1: x=29: incr_seq.body → Sequencer
    3. data item 1: x=29: Sequencer → Driver
    4. Waiting: incr_seq.body — in finish_item until item_done
    5. Released: Driver
  4. Drive the pins30 ns (illustrative)

    At the next falling edge the driver sets valid and x. Simulation time passes only here, while the sequence waits.

    More context

    The trace shows valid=1 at 30 ns. The driver holds the item until the following falling edge, at 40 ns.

    Follow the calls, values and waits
    1. data valid=1, x=29: Driver → Incrementer
  5. Done: finish_item returns40 ns (illustrative)

    The driver reads y=30, clears valid and calls item_done. finish_item returns in the same time step.

    More context

    Log lines 40 ns driver design answered y=30; item_done and 40 ns sequence finish_item returned. A driver that skipped item_done would leave the sequence here for good.

    Follow the calls, values and waits
    1. data y=30: Incrementer → Driver
    2. call item_done(): Driver → Sequencer
    3. return finish_item returns: Sequencer → incr_seq.body
    4. Released: incr_seq.body
  6. Item 2 in the same time step40 ns (illustrative)

    The driver asks again at once, and the sequence's next start_item is granted at 40 ns. In steady state the handshake costs no simulation time.

    More context

    Item 2 (x=27) is answered at 60 ns and item 3 (x=21) at 80 ns, each through the same handshake. The last finish_item returns at 80 ns.

    Follow the calls, values and waits
    1. call get_next_item(req): Driver → Sequencer
    2. call start_item(it): incr_seq.body → Sequencer
    3. return grant: Sequencer → incr_seq.body
Read the diagram as text
  • incr_seq.body: start_item, randomize, finish_item
  • Sequencer: Grants a waiting sequence to an asking driver
  • Driver: get_next_item, drive, item_done
  • Incrementer: y = x + 1
  • incr_seq.body → Sequencer: start_item / finish_item
  • Driver → Sequencer: get_next_item / item_done
  • Driver → Incrementer: valid, x
  • Incrementer → Driver: y

The driver pulls; the sequence never pushes. Each side blocks until the other side acts.

Two details stand out. First, the sequence called start_item at 0 ns but was granted at 20 ns, the moment the driver first asked. A grant goes only to a driver that is asking.

Second, finish_item for item 1 returned at 40 ns, in the same time step as item_done. The sequence was granted item 2 at once, because the driver asked again immediately. In steady state the handshake costs no simulation time at all.

The same two waits tell you where to look when a test hangs. A sequence stuck in finish_item points at the driver: it took the item but never called item_done. A sequence stuck in start_item means no driver is asking, because the driver is blocked elsewhere, was never connected to this sequencer, or has not started its loop.

Randomize after the grant

You could randomize before start_item, and the item would go out unchanged. Randomizing after the grant is called late randomization, and it has a real advantage. The values are chosen at the moment the driver is ready for them, so constraints can use the current state of the test.

Picture a sequence that writes to a FIFO and reads a fill-level counter kept by the scoreboard. Randomized late, its next item can be constrained to a write only while there is room. Randomized early, the item would reflect a fill level that may be several cycles old.

Inline constraints narrow one call without changing the class, as in it.randomize() with { x < 20; }. The class constraints still apply, so here x falls between 16 and 19. Ask for x < 10 instead and the two constraints conflict: randomize() returns 0 and x keeps its old value, which is why the example checks the result.

Breaking the handshake

Every get_next_item needs exactly one item_done. Miss one, and the hook's hang follows. In uvm-core, a second get_next_item before item_done reports an error and hands back the same item again. The driver then repeats that item while the sequence waits forever.

Call item_done twice and the second call finds no item outstanding, which is a fatal error. A loop with several exits, such as one that skips some items with continue, is where a missing item_done usually hides.

Skips item_done on one path

seq_item_port.get_next_item(req);
if (req.x == 0) continue;   // leaves the item open
drive(req);
seq_item_port.item_done();

The first zero value leaves its item open. The next get_next_item returns the same item, and the sequence hangs.

Completes every item

seq_item_port.get_next_item(req);
if (req.x != 0) drive(req);
seq_item_port.item_done();

Every path reaches item_done, so the sequence always moves on.

Two other driver calls change the pairing. get(req) fetches the item and completes the request handshake; it does not wait for pin activity or a response. try_next_item(req) returns null if no item becomes available during its delta-cycle check. A null result needs no item_done; lesson U4.3 uses this to drive idle cycles.

Try it

The log has one line per handshake call. Match it against the trace above: the sequence waits for its first grant until 20 ns, and each finish_item returns in the same time step as the driver's item_done.

Delete the repeat (2) line in the driver and predict the new time of the first grant. Then move item_done to just after get_next_item and watch finish_item return before the item is driven.

RUNVerilator + UVM 2020.3.1 · simulator service
Edit the code, then run it
Expected among the UVM messages: 0 ns sequence start_item: waiting for a grant 20 ns driver get_next_item: waiting for an item 20 ns sequence granted; x=29; finish_item 20 ns driver got x=29 40 ns driver design answered y=30; item_done 40 ns driver get_next_item: waiting for an item 40 ns sequence finish_item returned 40 ns sequence start_item: waiting for a grant 40 ns sequence granted; x=27; finish_item 40 ns driver got x=27 60 ns driver design answered y=28; item_done 60 ns driver get_next_item: waiting for an item 60 ns sequence finish_item returned 60 ns sequence start_item: waiting for a grant 60 ns sequence granted; x=21; finish_item 60 ns driver got x=21 80 ns driver design answered y=22; item_done 80 ns driver get_next_item: waiting for an item 80 ns sequence finish_item returned UVM_ERROR : 0

Check yourself

Predict the outputWhat 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::*;
class item_c extends uvm_sequence_item;
  `uvm_object_utils(item_c)
  function new(string name = "item_c"); super.new(name); endfunction
endclass
class drv_c extends uvm_driver #(item_c);
  `uvm_component_utils(drv_c)
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  task run_phase(uvm_phase phase);
    #5;
    forever begin
      seq_item_port.get_next_item(req);
      #10;
      seq_item_port.item_done();
      #2;
    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 granted[2], done[2];
    for (int i = 0; i < 2; i++) begin
      item_c it = item_c::type_id::create("it");
      start_item(it);
      granted[i] = $time;
      finish_item(it);
      done[i] = $time;
    end
    $display("granted %0d %0d, done %0d %0d", granted[0], granted[1], 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);
    drv = drv_c::type_id::create("drv", this);
    sqr = uvm_sequencer #(item_c)::type_id::create("sqr", this);
  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;
  initial run_test("my_test");
endmodule
Spot the bugThe sequence sends x = 3, 0 and 5. Each option is the body of the driver's forever loop. Which one never lets the sequence finish?
ConceptTwo sequences are started on the same sequencer at time 0, and both call start_item. When does one of them get its grant?
ConceptWhy does the example randomize the item after start_item rather than before it?
Read the waveformAt which time does the sequence's finish_item for x = 22 return?
Driver calls and the bus, every 10 ns
0123456time (ns)10203040506070driver callget_next_item-item_doneget_next_item-item_donevalidx02122

The driver takes x = 21 at 10 ns and x = 22 at 40 ns.

Remember

  • A sequence decides what to send in body(); the sequencer passes its items to the driver one at a time.
  • Each item takes four steps: create, start_item, randomize, finish_item.
  • start_item waits for a grant, which comes when the driver asks; finish_item waits for the driver's item_done.
  • The driver receives the same object the sequence created, not a copy.
  • Every get_next_item needs exactly one item_done, on every path through the driver loop.