UVM
Sequencer Interaction
How the sequencer brokers items between sequences and the driver — the seq_item pull connection, arbitration among competing sequences, and lock/grab for exclusive, non-interleaved access.
Sequences · Module 9 · Page 9.2
The Engineering Problem
The previous chapter ran a sequence on a sequencer with start() (Module 9.1), treating the sequencer as a place the sequence runs. But the sequencer is more than a launch point — it's the broker that sits between many sequences (producers) and one driver (consumer), and what it does in that role determines how stimulus actually reaches the pins. Two questions only the sequencer answers: how does the driver get items (the connection), and when several sequences want the driver at once, who goes next (arbitration). Get the connection wrong and the driver hangs with no stimulus; get arbitration wrong and a supposedly atomic operation gets another sequence's transaction spliced into the middle.
The sequencer brokers via a pull connection: the driver's seq_item_port connects to the sequencer's seq_item_export, and the driver pulls items (get_next_item) when it's ready. When multiple sequences run concurrently on one sequencer, the sequencer arbitrates — at each item's grant, it picks which competing sequence's item goes next, by a configurable policy (FIFO, priority, random). And when a sequence needs its items to run without interruption — a register read-modify-write, a locked burst — it can lock or grab the sequencer for exclusive access, so no other sequence's items interleave. This chapter is the sequencer's brokering role: the pull connection to the driver, arbitration among sequences, and lock/grab for atomicity — the layer that turns "a sequence runs on a sequencer" into "stimulus reaches the driver in a controlled order."
How does the sequencer broker items between sequences and the driver — the pull connection the driver gets items through, how it arbitrates among competing sequences, and how lock/grab give a sequence exclusive, non-interleaved access?
Motivation — why a broker is needed between sequences and the driver
The sequencer exists because the producer/consumer relationship between sequences and a driver needs mediation, and each of its jobs addresses one need:
- The driver consumes at its own pace, so items must be pulled, not pushed. The driver is ready for a new item only when it has finished the last (Module 8.3's back-pressure). A pull connection lets the driver ask for an item exactly when ready (
get_next_item), rather than sequences pushing items it isn't ready for. The sequencer is the pull source. - Multiple sequences may target one driver, so someone must arbitrate. A realistic test runs more than one sequence on an agent — background traffic plus an interrupt injector, say. They share one driver, so their items must be serialized, and a policy must decide whose item goes next. The sequencer is that arbiter.
- Arbitration needs a policy, because "next" isn't obvious. With several sequences waiting, fairness, priority, or randomness might all be desired. So the sequencer's arbitration is configurable — FIFO by default, but priority-weighted or random when the test wants it — rather than a fixed rule.
- Some operations must be atomic, so a sequence needs exclusivity. A read-modify-write or a locked burst must not have another sequence's transaction spliced into the middle.
lock/grablet a sequence claim the sequencer exclusively for the duration, so arbitration doesn't interleave others. - The connection is structural, so it's made once. The driver-to-sequencer pull link is wired in the agent's
connect_phaseand is fixed for the simulation — the permanent channel through which all stimulus flows.
The motivation, in one line: a single driver consumed at its own pace by potentially many sequences needs a broker that lets the driver pull, arbitrates among competing sequences by a chosen policy, and grants exclusivity when atomicity demands it — which is exactly the sequencer's role.
Mental Model
Hold the sequencer as a dispatcher at a single service window:
The sequencer is a dispatcher at one service window: many customers (sequences) want service, one server (the driver) handles them one at a time, and the dispatcher decides whose turn it is. The server works at its own pace and calls the next customer when it's free (the driver pulls with
get_next_item) — customers don't shove their requests at a busy server. When several customers are waiting, the dispatcher arbitrates — by a chosen rule: first-come-first-served (FIFO), by priority (VIPs first), or by lottery (random) — and grants one customer the window. That customer is served, then the dispatcher picks again. And when a customer has a multi-step transaction that mustn't be interrupted — a sequence of operations that has to complete as a unit — they can reserve the window exclusively (lock, waiting politely for their turn to reserve; orgrab, reserving it right now), so the dispatcher serves only them until they release it. Forget to even open the window (connect the driver to the sequencer) and customers wait forever with no server; forget to reserve it for a multi-step transaction and another customer's request slips in between your steps.
So the sequencer is the dispatcher that makes one server fairly (or deliberately unfairly) serve many customers, with a reservation mechanism for transactions that must run as an uninterrupted unit. The driver pulls; the sequencer decides who; lock/grab reserve.
Visual Explanation — the sequencer brokers many sequences to one driver
The defining picture is funnel and pull: many sequences feed into one sequencer, which the single driver pulls from — the sequencer arbitrating the merge.
The figure shows the funnel the sequencer manages. On the left, multiple sequences — say a background traffic_seq and an interrupt_seq — run concurrently as producers. They all feed into the sequencer, which is the broker: there is exactly one of it per driver, and it is where the many producers meet the one consumer. On the right, the single driver is the consumer, and the connection between them is a pull link (seq_item_port→seq_item_export): the driver calls get_next_item when it's ready, and the sequencer hands over one item. The crucial work happens in the middle: when more than one sequence has an item waiting, the sequencer arbitrates — picks which sequence's item is granted next — so the two producers' items are serialized into a single ordered stream through the one driver. So the picture is a many-to-one funnel with an arbiter at the neck: many sequences, one arbitrated order, one pulling driver. Everything else in this chapter is about that neck — how the arbiter chooses (arbitration policy) and how a sequence can temporarily own the neck (lock/grab).
RTL / Simulation Perspective — connecting and arbitrating
The sequencer's brokering shows up in two pieces of code: the connection (wired once in connect_phase) and the arbitration setup plus concurrent sequences (in the test).
// ── CONNECTION: wire the driver's pull port to the sequencer (agent connect_phase) ──
class my_agent extends uvm_agent;
function void connect_phase(uvm_phase phase);
driver.seq_item_port.connect(sequencer.seq_item_export); // the pull channel
endfunction
endclass
// ── ARBITRATION: two sequences share one sequencer; set the policy + priorities ──
task my_test::run_phase(uvm_phase phase);
traffic_seq tr = traffic_seq::type_id::create("tr");
interrupt_seq irq = interrupt_seq::type_id::create("irq");
phase.raise_objection(this);
env.agent.sequencer.set_arbitration(UVM_SEQ_ARB_WEIGHTED); // policy: weighted by priority
fork
tr.start (env.agent.sequencer, /*parent*/ null, /*priority*/ 100); // background traffic
irq.start(env.agent.sequencer, /*parent*/ null, /*priority*/ 500); // higher priority
join
phase.drop_objection(this);
endtask
// ── LOCK/GRAB inside a sequence body: claim exclusive access for an atomic operation ──
task rmw_seq::body();
grab(m_sequencer); // take EXCLUSIVE access immediately (no interleaving)
`uvm_do(read_item) // read ─┐ these run as an atomic unit:
`uvm_do(modify_item) // modify │ no other sequence's item can slip between
`uvm_do(write_item) // write ─┘
ungrab(m_sequencer); // release exclusivity
endtaskThe code makes the three roles concrete. The connection is one line in the agent's connect_phase: driver.seq_item_port.connect(sequencer.seq_item_export) wires the driver's pull port to the sequencer's export, establishing the channel the driver pulls items through (get_next_item). This is structural and made once. The arbitration is configured on the sequencer (set_arbitration(UVM_SEQ_ARB_WEIGHTED)) and exercised by running two sequences concurrently (fork/join) on it with different priorities — here, the interrupt_seq at priority 500 outweighs background traffic_seq at 100, so under the weighted policy the sequencer favors the interrupt's items when both compete. (Other policies: UVM_SEQ_ARB_FIFO — the default, first-come order; UVM_SEQ_ARB_RANDOM; strict variants.) The lock/grab appears inside a sequence's body(): grab(m_sequencer) claims exclusive access so the read-modify-write trio runs as an atomic unit with no other sequence's item interleaving, and ungrab releases it. (lock is the polite variant — it waits its turn in arbitration before gaining exclusivity, while grab jumps the queue immediately.) Connection, arbitration, exclusivity — three pieces of the sequencer's brokering, each a small amount of code.
Verification Perspective — how arbitration picks the next item
When multiple sequences have items waiting, the sequencer runs an arbitration decision at each grant. Understanding that decision — and that it happens per item, at start_item — is what lets you reason about which sequence's traffic wins.
Arbitration is a per-item decision, and seeing its inputs clarifies how traffic gets ordered. The decision is triggered when the driver pulls (get_next_item) and a grant is needed. The sequencer collects every sequence that currently has an item waiting — each such sequence is blocked in start_item, having requested the grant (Module 8.3). If only one is waiting, it's granted directly. If several are waiting, the sequencer applies the configured policy: UVM_SEQ_ARB_FIFO grants the one that requested first; UVM_SEQ_ARB_WEIGHTED/priority favors higher-priority sequences; UVM_SEQ_ARB_RANDOM picks randomly; strict variants enforce exact priority order. The chosen sequence's start_item unblocks and its item is delivered to the driver; the losers keep waiting and re-contend at the next grant. The two things to internalize: arbitration happens per item (not per sequence — a sequence doesn't "win" the sequencer for its whole duration, only for one item at a time), and it only considers sequences currently waiting (a sequence between items isn't in the running). This is why, even with a high priority, a sequence's items still interleave with others' between its own items — and why, when interleaving is unacceptable, you need exclusivity (lock/grab), not just priority. Priority biases which item wins each grant; it does not make a sequence's items atomic.
Runtime / Execution Flow — lock and grab for exclusivity
When a sequence needs several items to run as an uninterrupted unit, priority isn't enough — it needs exclusivity. lock and grab give it, by suspending arbitration for other sequences until released.
Exclusivity is the tool for atomicity across multiple items. By default the sequencer arbitrates every item among all waiting sequences, so even a high-priority sequence's items can have another sequence's item land between them — fine for independent traffic, fatal for an operation whose items must stay together. lock/grab solve this by claiming exclusive access: while a sequence holds the lock, the sequencer grants only that sequence, suspending arbitration for everyone else, so its items run as an atomic unit. The difference between them is when exclusivity is gained: grab takes it immediately (jumping ahead of waiting sequences), while lock waits its turn in arbitration before gaining it (more polite, no starvation of others' in-flight grants). While held, a read-modify-write's three items run back-to-back with nothing interleaved; then unlock/ungrab releases exclusivity and normal arbitration resumes. The discipline is to always release — a lock/grab that's never released starves every other sequence forever (the sequencer grants only the holder), a deadlock-by-omission. So the rule of thumb: use priority to bias which sequence's items win each grant, and use lock/grab (briefly, always released) when a group of items must run as an uninterrupted unit — priority orders, exclusivity atomizes.
Waveform Perspective — interleaving vs locked
The difference arbitration and lock/grab make is visible on a timeline: without exclusivity, two sequences' items interleave on the driver; with a lock, one sequence's items run as an uninterrupted block.
Two sequences on one driver: arbitrated (interleaved) vs locked (atomic)
12 cyclesThe waveform contrasts the two regimes directly. In the ARBITRATED region (left), two sequences A and B share the driver, and because the sequencer arbitrates each grant among both, their items interleave: A0, B0, A1, B1 — each sequence gets a turn, item by item. This is the normal, desirable behavior for independent traffic. But it would be wrong for an atomic operation: if A's read-modify-write were subject to arbitration, B's item could land between A's read and write, corrupting the operation. The LOCKED region (right) shows the fix: A grabs the sequencer (locked high), so the sequencer grants only A, and its A-R, A-M, A-W trio runs back-to-back with no B item between — an atomic unit. When A ungrabs, locked falls and arbitration resumes (B2, A2 interleaving again). The picture to carry is the block versus the interleave: arbitration produces an interleaved stream (fair sharing), and lock/grab carves out an uninterrupted block (atomicity) within it. The same two sequences produce completely different driver streams depending on whether exclusivity is held — which is exactly why lock/grab exists, and why forgetting it on an atomic operation is a real bug.
DebugLab — the read-modify-write that got interleaved
An atomic operation corrupted because another sequence's item interleaved it
A test ran a register read-modify-write sequence (read a register, modify a bit, write it back) concurrently with a background traffic sequence on the same sequencer. Intermittently, the written value was wrong — as if the modify had operated on stale data — and the failure was non-deterministic, appearing only on some runs/seeds. Each sequence's items were individually correct; the corruption only happened when both ran together, and only sometimes.
The read-modify-write sequence did not lock/grab the sequencer, so the background sequence's write interleaved between its read and its write — making the read-modify-write operate on data the interleaved write had changed:
both run on one sequencer, NO lock/grab:
rmw_seq: read(reg) ─┐ the sequencer arbitrates EACH item among both sequences,
bg_seq: write(reg, X) │ so bg's write can be granted BETWEEN rmw's read and write
rmw_seq: modify + write(reg) ─┘
timeline (interleaved): rmw.read(reg)=old → bg.write(reg)=X → rmw.write(reg)=modify(old)
result: rmw wrote modify(old), silently overwriting bg's X — and used stale 'old'
→ non-deterministic corruption, only when both sequences are active and arbitration interleaves
fix — make the RMW atomic with exclusive sequencer access:
task rmw_seq::body();
grab(m_sequencer); // ✓ exclusive — no other sequence's item can interleave
`uvm_do(read_item)
`uvm_do(write_item) // read and write now run back-to-back, atomically
ungrab(m_sequencer); // ✓ release
endtaskThis is an arbitration interleaving bug. The sequencer arbitrates every item among all waiting sequences (Figure 2), and arbitration is per item, not per sequence — so a sequence does not own the sequencer for its whole duration, only for one granted item at a time. The read-modify-write sequence assumed its read and write would run together, but between them the sequencer was free to grant the background sequence's write, splicing it into the middle of the atomic operation. The result: the RMW read stale data and/or clobbered the background write, and because arbitration timing depends on the run, the corruption was non-deterministic — present only when both sequences were active and the arbiter happened to interleave them. Crucially, priority alone would not fix this: even at higher priority, the RMW's items are still arbitrated individually, so another item can still land between them. The fix requires exclusivity: grab (or lock) the sequencer for the duration of the read-modify-write, so the sequencer grants only that sequence and its items run as an uninterrupted unit, then ungrab/unlock to release. The rule: operations whose items must run as a unit need lock/grab, because arbitration interleaves by default and priority only biases which item wins each grant — it never makes a group of items atomic.
The tell is non-deterministic corruption that only appears when sequences run concurrently. Diagnose interleaving bugs:
- Check whether the operation is multi-item and assumed atomic. A read-modify-write, a locked burst, any group of items that must run together is a candidate — confirm it relies on the items being uninterrupted.
- Check for lock/grab around it. If the atomic operation has no
lock/grab, the sequencer is free to interleave another sequence's item between its items — the signature. - Correlate with concurrent sequences. The bug appears only when another sequence runs on the same sequencer; running the operation alone "works," which falsely suggests the operation is fine.
- Confirm it's non-deterministic. Arbitration interleaving depends on timing/seed, so the failure is intermittent — a clue distinguishing it from a deterministic logic bug.
Make multi-item atomic operations exclusive:
- Lock or grab around any group of items that must run as a unit. Read-modify-write, locked bursts, multi-beat atomic protocols — claim the sequencer for the whole group, release after.
- Don't rely on priority for atomicity. Priority biases which item wins a grant; it does not prevent interleaving. Only lock/grab makes a group atomic.
- Always release — release on every path. A
lock/grabnever released starves all other sequences forever; pair every claim with a release, including on error/early-exit paths. - Prefer
lockunless you need to jump the queue.lockwaits its arbitration turn (fairer);grabtakes exclusivity immediately (use when the operation truly can't wait). Either way, keep the held duration short.
The one-sentence lesson: the sequencer arbitrates every item individually, so a sequence does not own it across multiple items — a multi-item atomic operation (read-modify-write) run without lock/grab can have another sequence's item interleaved between its items, causing non-deterministic corruption; claim exclusivity with lock/grab for the whole group (and always release), because priority biases grants but never makes items atomic.
Common Mistakes
- Forgetting to connect the driver to the sequencer. Without
driver.seq_item_port.connect(sequencer.seq_item_export)inconnect_phase, the driver'sget_next_itemhas no source and hangs — no stimulus. - Assuming a sequence owns the sequencer for its duration. Arbitration is per item; another sequence's item can land between yours. Use lock/grab when items must stay together.
- Using priority to enforce atomicity. Priority only biases which item wins a grant; it does not prevent interleaving. Atomic groups need lock/grab.
- A lock/grab that's never released. It starves every other sequence forever (the sequencer grants only the holder); always pair claim with release, on all paths.
- Holding a lock/grab too long. Exclusivity blocks all other traffic; keep the held region as short as the atomicity requires.
- Expecting a particular order without setting the policy. The default is FIFO; if you want priority or random arbitration, set it with
set_arbitrationand assign priorities.
Senior Design Review Notes
Interview Insights
The sequencer is the broker that mediates between many sequences (the producers of stimulus) and one driver (the consumer). It does three things. First, it provides the connection through which the driver gets items: the driver's seq_item_port connects to the sequencer's seq_item_export, a pull connection, so the driver calls get_next_item when it's ready and the sequencer hands over an item — the driver pulls at its own pace rather than having items pushed at it. Second, it arbitrates: when more than one sequence is running concurrently and each has an item waiting, the sequencer decides whose item is granted next, according to a configurable policy, so the multiple producers are serialized into one ordered stream through the single driver. Third, it supports exclusivity: a sequence can lock or grab the sequencer to gain exclusive access for an atomic operation, so no other sequence's item interleaves. The reason a broker is needed is that the driver consumes at its own pace and potentially many sequences target it, so someone has to let the driver pull, decide whose item goes next when they compete, and grant exclusivity when atomicity demands it. The sequencer is exactly that mediator. The mental model is a dispatcher at a single service window: the server (driver) calls the next customer when free, the dispatcher (sequencer) decides whose turn it is, and a customer can reserve the window for a multi-step transaction.
It runs an arbitration decision at each item's grant, choosing among the sequences that currently have an item waiting, by a configurable policy. When the driver pulls — calls get_next_item — the sequencer needs to grant one item, so it collects all the sequences that are blocked in start_item with an item ready and applies the policy. With UVM_SEQ_ARB_FIFO, the default, it grants the sequence that requested first; with the weighted or priority policy it favors higher-priority sequences; with the random policy it picks randomly; and there are strict variants that enforce exact priority order. The chosen sequence's start_item unblocks and its item is delivered to the driver, while the others keep waiting and re-contend at the next grant. The crucial properties are that arbitration is per item, not per sequence — a sequence doesn't win the sequencer for its whole duration, only for one granted item at a time — and that it only considers sequences currently waiting, so a sequence between items isn't in the running. You set the policy with set_arbitration on the sequencer, and you assign per-sequence priority when you start a sequence or via set_priority. The practical consequence is that priority biases which sequence's item wins each grant but does not make a sequence's items atomic: even a high-priority sequence's items can have another sequence's item interleaved between them, which is why atomicity requires lock or grab rather than priority.
Exercises
- Wire the connection. Write the agent
connect_phaseline that connects the driver to the sequencer, and state what symptom its absence produces. - Make it atomic. A read-modify-write sequence is interleaved by background traffic. Rewrite its
body()to run atomically, and explain why priority wouldn't fix it. - Choose the policy. A test runs background traffic plus a high-priority interrupt sequence and wants the interrupt favored. Name the arbitration setup (policy + priority) that achieves it.
- Explain the interleave. Explain why, without lock/grab, a high-priority sequence's items can still have another sequence's item between them, referencing how arbitration is scoped.
Summary
- The sequencer brokers between many sequences (producers) and one driver (consumer): the driver pulls items through the
seq_item_port→seq_item_exportconnection (wired once inconnect_phase), and the sequencer serializes the producers onto the one driver. - When sequences compete, the sequencer arbitrates — per item, at each grant — by a configurable policy (
UVM_SEQ_ARB_FIFOdefault, weighted/priority, or random), so concurrent sequences share the driver in a controlled order. - Arbitration is per item, not per sequence: a sequence doesn't own the sequencer across its items, so priority only biases which item wins each grant — it does not make a group of items atomic.
lock/grabgive exclusivity: while held, the sequencer grants only that sequence, so a multi-item operation (read-modify-write, locked burst) runs as an uninterrupted unit —grabtakes it immediately,lockwaits its turn — and must always be released or it starves all other sequences.- The durable rule of thumb: connect the driver to the sequencer (or it hangs), set the arbitration policy and priorities deliberately, and wrap any multi-item atomic operation in
lock/grab(released on every path) — because the sequencer arbitrates per item, so without exclusivity another sequence's item can interleave and corrupt an operation that was supposed to run as a unit.
Next — start_item(): arbitration is decided at the grant, and the grant is what start_item waits for. The next chapter drills into start_item from the sequence's side — exactly what it does, what it blocks on, and how it requests the arbitration this chapter described.