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.
| Time | Virtual sequence | Test | Lanes |
|---|---|---|---|
| 0 ns | Forks lane a and lane b, join_none returns at once | Waiting in vseq.start | Both sequences call start_item |
| 0 ns | body() ends | vseq.start returns; drops the objection | Waiting for the drivers |
| 0 ns | - | Run phase ends | Killed 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
- 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
- call start(a): Virtual sequence → Lane a sequence
- call start(b): Virtual sequence → Lane b sequence
- Waiting: fork ... join — Waiting for both lanes
- 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
- return a returned: Lane a sequence → fork ... join
- Waiting: fork ... join — Waiting for lane b
- 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
- return b returned: Lane b sequence → fork ... join
- call start(c): fork ... join → Then start c
- Released: fork ... join
- 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
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
- 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
- call start child: Coordinator → Setup child
- call s item: Setup child → Lane a sequencer
- Waiting: Lane a driver — driving b2 until 20 ns
- Waiting: Background sequence — in finish_item(b2)
- 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
- call b item: Background sequence → Lane a sequencer
- data s item: Lane a sequencer → Lane a driver
- Waiting: Background sequence — b3 queued behind s1
- Released: Lane a driver
- Released: Background sequence
- 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 withstart(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
- call grab: Coordinator → Lane a sequencer
- call start child: Coordinator → Setup child
- call s item: Setup child → Lane a sequencer
- Waiting: Lane a driver — driving b2 until 20 ns
- Waiting: Background sequence — in finish_item(b2)
- Released: Background sequence
- 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
thisas its parent is not a descendant and would block behind the grab.Follow the calls, values and waits
- call b item: Background sequence → Lane a sequencer
- data s item: Lane a sequencer → Lane a driver
- Waiting: Background sequence — b3 blocked by the grab
- Released: Lane a driver
- Released: Background sequence
- 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
$displayprints "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
- call ungrab: Coordinator → Lane a sequencer
- data b item: Lane a sequencer → Lane a driver
- Released: Background sequence
- Or never start it
If the background traffic is a phase default sequence, set that
default_sequenceto 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
- call start child: Coordinator → Setup child
- call s item: Setup child → Lane a sequencer
- 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.
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 : 0Check yourself
Remember
- A virtual sequence coordinates leaf sequences on several sequencers; it sends items only through those leaves.
- Use
fork ... join, orjoin_anypluswait fork, sobody()returns only when the lanes are done; passthisas 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.