Coordinate two sequencers

Drive two interfaces from one virtual sequence, wait for the work it starts, decide what other traffic may do on each lane, and give the sequence its sequencer handles in a way that survives reuse.

Lesson
U10.1
Time
25 min
Runs on
Verilator with UVM
Check
6 questions

A new test for a two-lane design passes on its first run with zero errors, in zero nanoseconds. Its virtual sequence started both lanes with fork ... join_none and returned at once. The test dropped its objection, the run phase ended at time 0, and neither driver ever finished an item.

TimeVirtual sequenceTestLanes
0 nsForks lane a and lane b, join_none returns at onceWaiting in vseq.startBoth sequences call start_item
0 nsbody() endsvseq.start returns; drops the objectionWaiting for the drivers
0 ns-Run phase endsKilled before any item is granted

A sequence ends when its body() and completion callbacks return, whatever its children are doing. Nothing waited for the lanes.

One plan above two agents

Two lanes meet at one joinThe plan starts a and b together. The next sequence, c, needs both leaf sequences to return.
Two lanes meet at one joinThe plan starts a and b together. The next sequence, c, needs both leaf sequences to return. plan Virtual sequence Start a and b inparallel stimulus Lane a sequence 16 then 32; returnsat 45 ns stimulus Lane b sequence 100, 101, 102;returns at 75 ns wait fork ... join Both child calls mustreturn next step Then start c Lane a accepts 200at 85 ns start astart ba endsb endsstart c
  1. Start together0 ns (illustrative)

    Two start calls run in separate branches. The virtual sequence waits at join.

    More context

    Each start call blocks its own branch until that sequence's body returns. The first values, 16 on lane a and 100 on lane b, are both accepted at 15 ns.

    Follow the calls, values and waits
    1. call start(a): Virtual sequence → Lane a sequence
    2. call start(b): Virtual sequence → Lane b sequence
    3. Waiting: fork ... join — Waiting for both lanes
  2. One lane returns45 ns (illustrative)

    Lane a has had both requests accepted. Lane b still owes its third request, so c cannot start yet.

    More context

    The log shows lane a's 32 and lane b's 101 accepted on the same 45 ns edge. With join_any, the join would release here and c would start at 45 ns.

    Follow the calls, values and waits
    1. return a returned: Lane a sequence → fork ... join
    2. Waiting: fork ... join — Waiting for lane b
  3. The join releases75 ns (illustrative)

    Lane b returns after 102 is accepted. Both branches are finished, so the next start call can run.

    More context

    The log prints "75 ns vseq both lanes done" when the join returns, on the edge that accepted 102. Lane a has been idle since 45 ns.

    Follow the calls, values and waits
    1. return b returned: Lane b sequence → fork ... join
    2. call start(c): fork ... join → Then start c
    3. Released: fork ... join
  4. Stimulus is complete85 ns (illustrative)

    The driver completes c when 200 is accepted. Its result is still inside the DUT; checking completion is a separate wait.

Read the diagram as text
  • Virtual sequence: Start a and b in parallel
  • Lane a sequence: 16 then 32; returns at 45 ns
  • Lane b sequence: 100, 101, 102; returns at 75 ns
  • fork ... join: Both child calls must return
  • Then start c: Lane a accepts 200 at 85 ns
  • Virtual sequence → Lane a sequence: start a
  • Virtual sequence → Lane b sequence: start b
  • Lane a sequence → fork ... join: a ends
  • Lane b sequence → fork ... join: b ends
  • fork ... join → Then start c: start c

Arrows show sequence calls and branch completion, not DUT wires. This driver calls item_done at acceptance; a returned sequence does not prove its results were checked.

Each lane has its own sequencer and driver. A leaf sequence supplies items to one lane; a virtual sequence is an ordinary sequence used as a coordinator that starts leaves on several lanes. The example reuses U7's incrementer and the U4.3 driver, with two copies of the design.

The virtual sequence starts a and b together, waits for both, then starts c on lane a. The join is what makes "then" true.

Own every forked child

The hook: returns immediately

fork
  a.start(a_sqr, this);
  b.start(b_sqr, this);
join_none

body() returns at 0 ns while both leaves still wait for their drivers; the test ends with no traffic.

Waits for both

fork
  a.start(a_sqr, this);
  b.start(b_sqr, this);
join

Both start calls return before the next statement runs: 75 ns in the example.

join_any returns after the first branch and join_none immediately; neither waits for the rest. Use wait fork later when the remaining branches must finish before the coordinator returns. wait fork waits for every child of the calling process, including earlier background children, so keep timed or background work in a helper process of its own.

Two different parent relationships are at work. fork makes SystemVerilog child processes; the second argument of start makes the leaf a UVM child sequence of the coordinator. A forked a.start(a_sqr) without this has no UVM parent, which matters as soon as the coordinator grabs or locks a lane.

An empty scoreboard alone can look successful when no traffic ran. Require the scenario's intended progress as well; [U7.3](U7-3-uvm-scoreboard-drain.html) counts it.

Wait for the right kind of completion

Accepted input and observed result are different events
012345time (ns)4565758595105lane a input32-200-lane a result sampled-33-201lane b input101-102-virtual sequencewait for bstart creturns-stimulus ends before last result

Selected rising edges. Result timing follows the design pipeline: 201 would be driven just after 95 ns and sampled at 105 ns. This coordination example has no checker and ends at 85 ns.

finish_item returns when this driver calls item_done, at acceptance. A returned leaf therefore proves its requests were accepted, not that their effects were checked. [U6.1](U6-1-uvm-pipelined-driver.html)'s pipelined handoff returns even earlier, when a driver worker takes the item.

A configuration dependency needs a committed write: use a leaf that waits for the bus response, or wait for a monitor-observed completion, before starting dependent data. A delay chosen from one run gives no such guarantee.

Choose what other traffic on a lane may do

Three ways to treat other traffic on a leafTwo copies of lane a run side by side, one left running and one grabbed; each already runs a background sequence when the coordinator needs three setup items. Arrows are requests, grants and the coordinator's calls; the driver takes 10 ns per item. Steps 1-2 replay the left-running copy and steps 3-5 the grabbed copy, each from 15 ns.
Three ways to treat other traffic on a leafTwo copies of lane a run side by side, one left running and one grabbed; each already runs a background sequence when the coordinator needs three setup items. Arrows are requests, grants and the coordinator's calls; the driver takes 10 ns per item. Steps 1-2 replay the left-running copy and steps 3-5 the grabbed copy, each from 15 ns. stimulus Backgroundsequence b items from 0 ns plan Coordinator Started withstart(null) stimulus Setup child Three s items,start(a_sqr, this) sequencer Lane a sequencer FIFO arbitration driver Lane a driver One item per 10 ns b requestss requestsstart childgrab, ungrab callsgrant one item
  1. Left running: the setup child queues15 ns (illustrative)

    On the copy without a grab, the coordinator starts the setup child and its first request queues. The driver is still busy with b2 until 20 ns; the background sequence waits in finish_item(b2).

    More context

    The coordinator was started with start(null) and sends no items itself. Only its child's requests reach the lane a sequencer, where FIFO arbitration orders them by age.

    Follow the calls, values and waits
    1. call start child: Coordinator → Setup child
    2. call s item: Setup child → Lane a sequencer
    3. Waiting: Lane a driver — driving b2 until 20 ns
    4. Waiting: Background sequence — in finish_item(b2)
  2. Left running: the oldest request wins20 ns (illustrative)

    The s request from 15 ns is older than the third b request, which queues at 20 ns, so s1 is granted first. From then on the two alternate: b b s b s b s b b.

    More context

    Right for contention and stress tests, wrong for a setup step that must be indivisible.

    Follow the calls, values and waits
    1. call b item: Background sequence → Lane a sequencer
    2. data s item: Lane a sequencer → Lane a driver
    3. Waiting: Background sequence — b3 queued behind s1
    4. Released: Lane a driver
    5. Released: Background sequence
  3. Grabbed copy: grab, then start the child15 ns (illustrative)

    On the other copy the coordinator calls grab(a_sqr), which is granted at once without preempting b2, then starts the child with start(a_sqr, this).

    More context

    This is the second copy, replayed from 15 ns. The grab is placed at the front of the queue and granted at once, because no other lock is held; it does not take b2 away from the driver.

    Follow the calls, values and waits
    1. call grab: Coordinator → Lane a sequencer
    2. call start child: Coordinator → Setup child
    3. call s item: Setup child → Lane a sequencer
    4. Waiting: Lane a driver — driving b2 until 20 ns
    5. Waiting: Background sequence — in finish_item(b2)
    6. Released: Background sequence
  4. Grabbed copy: only the grabber's family is granted20 ns (illustrative)

    The third b request queues at 20 ns but is blocked; the coordinator and its descendants pass is_blocked, so s1, s2 and s3 go out back to back.

    More context

    A child started without this as its parent is not a descendant and would block behind the grab.

    Follow the calls, values and waits
    1. call b item: Background sequence → Lane a sequencer
    2. data s item: Lane a sequencer → Lane a driver
    3. Waiting: Background sequence — b3 blocked by the grab
    4. Released: Lane a driver
    5. Released: Background sequence
  5. Grabbed copy: release50 ns (illustrative)

    After s3 the coordinator calls ungrab(a_sqr). The b request queued since 20 ns is granted next: b b s s s b b b b.

    More context

    The $display prints "left running: bbsbsbsbb; grabbed: bbsssbbbb". The grab held the lane from 15 to 50 ns; the setup items used 20 to 50 ns of it.

    Follow the calls, values and waits
    1. call ungrab: Coordinator → Lane a sequencer
    2. data b item: Lane a sequencer → Lane a driver
    3. Released: Background sequence
  6. Or never start it

    If the background traffic is a phase default sequence, set that default_sequence to null from the test, so it outranks the environment's setting, before the phase starts. The lane then carries only the coordinator's traffic; a sequence that a test starts itself is unaffected.

    More context

    A third alternative, not in the runnable example. Nothing competes, so each s request is granted as soon as the driver asks; no grab is needed.

    Follow the calls, values and waits
    1. call start child: Coordinator → Setup child
    2. call s item: Setup child → Lane a sequencer
    3. data s item: Lane a sequencer → Lane a driver
Read the diagram as text
  • Background sequence: b items from 0 ns
  • Coordinator: Started with start(null)
  • Setup child: Three s items, start(a_sqr, this)
  • Lane a sequencer: FIFO arbitration
  • Lane a driver: One item per 10 ns
  • Background sequence → Lane a sequencer: b requests
  • Setup child → Lane a sequencer: s requests
  • Coordinator → Setup child: start child
  • Coordinator → Lane a sequencer: grab, ungrab calls
  • Lane a sequencer → Lane a driver: grant one item

Leave running, grab, or disable startup. The two copies run separately; the grab predict item prints both driven orders.

Starting a virtual sequence does not silence other sequences on a leaf sequencer. Decide per scenario. Leave background traffic running for contention, disable a phase default sequence before the phase starts, or grab the leaf for an indivisible step. [U5.2](U5-2-uvm-lock-grab-relevance.html) explains grab and lock.

A grab admits only the grabbing sequence and its descendants, so start the setup child with start(a_sqr, this). The grab does not interrupt the item the driver already holds. Reserve several leaves in one consistent order and release every reservation; two coordinators that each hold one lane and wait for the other never finish.

Killing a coordinator removes its queued requests and its grabs from every sequencer it used; a lane it had grabbed carries on at its next grant. Killing does not undo work already driven, and it does not drop an objection raised by hand: do that in do_kill ([U5.3](U5-3-uvm-sequence-method-set.html)). To stop live traffic rather than hold it off, stop_sequences() flushes the sequencer but leaves the driver's pins as they are; [U10.2](U10-2-uvm-reset-accounting.html) coordinates the two.

Give the coordinator its sequencer handles

Handles in the sequence (the example)

class lanes_vseq_base extends uvm_sequence;
  uvm_sequencer #(incr_item) a_sqr, b_sqr;
endclass

// base test helper
function void init_vseq(lanes_vseq_base v);
  v.a_sqr = sqr_a;
  v.b_sqr = sqr_b;
endfunction

init_vseq(vseq);
vseq.start(null);

The base test, which knows the hierarchy, assigns the handles once; every derived test and sequence reuses that helper.

Handles in a virtual sequencer

class lanes_vsqr extends uvm_sequencer;
  uvm_sequencer #(incr_item) a_sqr, b_sqr;
endclass

// in the sequence
`uvm_declare_p_sequencer(lanes_vsqr)
a.start(p_sequencer.a_sqr, this);

vseq.start(vsqr);

The sequence requires a compatible lanes_vsqr whose leaf handles are assigned. Reuse must preserve or adapt that component and its wiring.

The UVM Cookbook calls the virtual sequencer the legacy approach. Its reason is coupling. A sequence that reads its handles through p_sequencer depends on the virtual-sequencer class and the handle members it expects. Every environment that runs it must build that component type and wire its handles. That gets in the way of vertical reuse, where a block environment is placed inside a larger one. The recommended pattern is the left one: a base virtual sequence holding the handles, a base-test method that assigns them, and start(null).

The virtual-sequencer form also has a trap. uvm_declare_p_sequencer(lanes_vsqr) declares a typed p_sequencer and casts the sequence's sequencer into it; a wrong non-null type gives fatal DCLPSQ. Started with start(null), the cast of null succeeds silently, p_sequencer stays null, and the first p_sequencer.a_sqr dereferences null. The handle-based form has no such state to get wrong.

Neither form discovers or connects sequencers by itself. Check that every required handle is set before starting traffic.

Transfer: configuration finishes after handoff

Change the scenario: the configuration driver accepts a write at 40 ns, but the design commits it at 70 ns. The leaf returns at acceptance. Decide what event must release the data sequence, and what happens if that event never arrives.

Reason through the dependency

Returning at 40 ns is too early. Wait for the write response or an observed commit at 70 ns, with a deadline. Keep unrelated background writes from changing the same configuration while dependent data runs, for example by grabbing the configuration lane.

A join orders the calls you gave it; it cannot strengthen what those calls mean. Changing the driver's handoff point therefore requires reviewing every sequence dependency.

Try it

Each leaf sequence logs the moment its finish_item returns, which is the edge where its design accepts the value. Lane a finishes at 45 ns, but the virtual sequence waits for lane b at 75 ns before sending 200.

Change join to join_any and rerun: c starts at 45 ns, offers 200 at 50 ns, and 200 is accepted at 75 ns, on the same edge as lane b's final 102. Then delete the c.start line, try join_none, and watch the test end at time 0. With c.start left in, body() would still wait for lane a to accept 200.

RUNVerilator + UVM 2020.3.1 · simulator service
Edit the code, then run it
Expected among the UVM messages: 15 ns lane a x=16 accepted 15 ns lane b x=100 accepted 45 ns lane a x=32 accepted 45 ns lane b x=101 accepted 75 ns lane b x=102 accepted 75 ns vseq both lanes done 85 ns lane a x=200 accepted 85 ns test virtual sequence done UVM_ERROR : 0

Check yourself

Predict the outputEach driver takes 10 ns per item. Sequence la sends three items on lane a; lb and lc send one each on lane b. What line does the $display print?
`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)
  int idle = 0, per_item = 10, count;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  task run_phase(uvm_phase phase);
    #(idle);
    forever begin
      seq_item_port.get_next_item(req);
      #(per_item);
      count++;
      seq_item_port.item_done();
    end
  endtask
endclass

class leaf_seq extends uvm_sequence #(item_c);
  `uvm_object_utils(leaf_seq)
  int n = 1, done;
  function new(string name = "leaf_seq"); super.new(name); endfunction
  task body();
    repeat (n) begin
      item_c it = item_c::type_id::create("it");
      start_item(it);
      finish_item(it);
      done++;
    end
  endtask
endclass

class vseq_c extends uvm_sequence;
  `uvm_object_utils(vseq_c)
  uvm_sequencer #(item_c) a, b;              // set by the test; the vseq runs on null
  function new(string name = "vseq_c"); super.new(name); endfunction
  task body();
    leaf_seq la = leaf_seq::type_id::create("la");
    leaf_seq lb = leaf_seq::type_id::create("lb");
    leaf_seq lc = leaf_seq::type_id::create("lc");
    int t_c;
    la.n = 3;
    fork
      la.start(a);
      lb.start(b);
    join_any
    lc.start(b);
    t_c = $time;
    wait fork;
    $display("c done at %0d, all done at %0d", t_c, $time);
  endtask
endclass

class my_test extends uvm_test;
  `uvm_component_utils(my_test)
  uvm_sequencer #(item_c) sa, sb;
  drv_c da, db;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    sa = uvm_sequencer #(item_c)::type_id::create("sa", this);
    sb = uvm_sequencer #(item_c)::type_id::create("sb", this);
    da = drv_c::type_id::create("da", this);
    db = drv_c::type_id::create("db", this);
  endfunction
  function void connect_phase(uvm_phase phase);
    da.seq_item_port.connect(sa.seq_item_export);
    db.seq_item_port.connect(sb.seq_item_export);
  endfunction
  task run_phase(uvm_phase phase);
    vseq_c v = vseq_c::type_id::create("v");
    v.a = sa;
    v.b = sb;
    phase.raise_objection(this);
    v.start(null);
    phase.drop_objection(this);
  endtask
endclass

module tb;
  initial run_test("my_test");
endmodule
Spot the bugLane a must send 2 items and lane b 3. Each option is the end of the virtual sequence's body(), after la and lb are created; the test assigned a_sqr and b_sqr and called start(null). Which one lets the test end before the lanes finish?
Predict the outputTwo copies of lane a each run a background sequence of six b items from 0 ns; the driver takes 10 ns per item and records the tags it drives. At 15 ns a coordinator starts a child of three s items on each copy, one copy with grab and ungrab around it. What line does the $display print?
`timescale 1ns/1ns
`include "uvm_macros.svh"
import uvm_pkg::*;

class tag_item extends uvm_sequence_item;
  `uvm_object_utils(tag_item)
  string tag;
  function new(string name = "tag_item"); super.new(name); endfunction
endclass

// Takes 10 ns per item and records the order of the tags it drove.
class tag_driver extends uvm_driver #(tag_item);
  `uvm_component_utils(tag_driver)
  string order;
  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);
      #10;
      order = {order, req.tag};
      seq_item_port.item_done();
    end
  endtask
endclass

class tag_seq extends uvm_sequence #(tag_item);
  `uvm_object_utils(tag_seq)
  string tag = "b";
  int    n = 6;
  function new(string name = "tag_seq"); super.new(name); endfunction
  task body();
    repeat (n) begin
      tag_item it = tag_item::type_id::create("it");
      start_item(it);
      it.tag = tag;
      finish_item(it);
    end
  endtask
endclass

// The coordinator: three setup items on lane a, starting at 15 ns.
class coord_vseq extends uvm_sequence;
  `uvm_object_utils(coord_vseq)
  uvm_sequencer #(tag_item) a_sqr;
  bit use_grab;
  function new(string name = "coord_vseq"); super.new(name); endfunction
  task body();
    tag_seq setup = tag_seq::type_id::create("setup");
    setup.tag = "s";
    setup.n   = 3;
    #15;
    if (use_grab) grab(a_sqr);
    setup.start(a_sqr, this);              // a child of the coordinator
    if (use_grab) ungrab(a_sqr);
  endtask
endclass

class my_test extends uvm_test;
  `uvm_component_utils(my_test)
  uvm_sequencer #(tag_item) sqr_free, sqr_grab;
  tag_driver drv_free, drv_grab;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    sqr_free = uvm_sequencer #(tag_item)::type_id::create("sqr_free", this);
    sqr_grab = uvm_sequencer #(tag_item)::type_id::create("sqr_grab", this);
    drv_free = tag_driver::type_id::create("drv_free", this);
    drv_grab = tag_driver::type_id::create("drv_grab", this);
  endfunction
  function void connect_phase(uvm_phase phase);
    drv_free.seq_item_port.connect(sqr_free.seq_item_export);
    drv_grab.seq_item_port.connect(sqr_grab.seq_item_export);
  endfunction
  task run_phase(uvm_phase phase);
    tag_seq    bg_free = tag_seq::type_id::create("bg_free");
    tag_seq    bg_grab = tag_seq::type_id::create("bg_grab");
    coord_vseq c_free  = coord_vseq::type_id::create("c_free");
    coord_vseq c_grab  = coord_vseq::type_id::create("c_grab");
    c_free.a_sqr = sqr_free;
    c_grab.a_sqr = sqr_grab;
    c_grab.use_grab = 1;
    phase.raise_objection(this);
    fork
      bg_free.start(sqr_free);             // background traffic on each lane
      bg_grab.start(sqr_grab);
      c_free.start(null);
      c_grab.start(null);
    join
    $display("left running: %s; grabbed: %s", drv_free.order, drv_grab.order);
    phase.drop_objection(this);
  endtask
endclass

module tb;
  initial run_test("my_test");
endmodule
ConceptA virtual sequence uses uvm_declare_p_sequencer(lanes_vsqr) and is started with vseq.start(null). What happens in uvm-core 2020.3.1?
ConceptIn the virtual-sequencer form, the coordinator runs on the driverless virtual sequencer vsqr. Why does it start leaf sequences instead of sending incr_items itself?
ConceptLane a's configuration sequence waits for the write response before returning. Lane b needs that completed write. Which dependency keeps working when the design gets slower?

Remember

  • A virtual sequence coordinates leaf sequences on several sequencers; it sends items only through those leaves.
  • Use fork ... join, or join_any plus wait fork, so body() returns only when the lanes are done; pass this as the leaves' parent.
  • Assign the leaf handles in a base-test helper and start the coordinator with start(null); a virtual sequencer couples the sequence to one component class.
  • Decide what other traffic on a lane may do: leave it running, disable its default sequence before the phase starts, or grab the lane for an indivisible step.
  • A returned leaf proves handoff or acceptance only; wait for the event a dependent interface actually needs.