Part 5 · Sequences · Intermediate

Why Arbitration Exists: Multiple Sequences, One Driver

The one-driver constraint, concurrent sequence threads, and why the sequencer must serialize start_item access.

The fundamental constraint

A uvm_driver drives one physical interface — one set of virtual interface signals sampled and driven on each clock edge. It can execute one transaction at a time . The driver pulls items via get_next_item from its connected sequencer. If two sequences could deliver items simultaneously, the driver would see conflicting requests with no defined merge semantics.

The uvm_sequencer exists partly to solve this: it accepts start_item calls from many sequences but grants driver access to exactly one pending item at a time. That grant decision is arbitration .

diagram
[UVM] why one driver cannot serve two items at once

  WRONG mental model:
    seq_A.start_item(req_a)  ──┐
                               ├──► driver drives BOTH?   undefined
    seq_B.start_item(req_b)  ──┘

  CORRECT model:
    seq_A.start_item(req_a)  ──► sequencer queue slot A
    seq_B.start_item(req_b)  ──► sequencer queue slot B
                                      │
                                      ▼
                               arbiter picks ONE
                                      │
                                      ▼
                               driver executes item
                                      │
                                      ▼
                               item_done  next slot

Concurrent sequences are normal

Real tests rarely run a single sequence in isolation. Typical chip-level run_phase starts several sequences on the same sequencer:

  • [STIM] Background traffic generator — continuous randomized writes for stress.

  • [STIM] Directed setup sequence — programs control registers before the scenario.

  • [STIM] Error injection sequence — inserts protocol violations mid-run.

  • [SEQ] Test or virtual sequence forks all three with fork/join — they run in parallel threads.

systemverilog
task run_phase(uvm_phase phase);
  apb_bg_seq      bg   = apb_bg_seq::type_id::create("bg");
  apb_setup_seq   setup = apb_setup_seq::type_id::create("setup");
  apb_stress_seq  stress = apb_stress_seq::type_id::create("stress");

  phase.raise_objection(this);

  fork
    bg.start(env.apb_agent.sqr);       // long-running background
    setup.start(env.apb_agent.sqr);    // short directed burst
    stress.start(env.apb_agent.sqr);   // main scenario
  join

  phase.drop_objection(this);
endtask

All three sequences share one sequencer and one driver. Without arbitration, there is no defined rule for which sequence's start_item wins when multiple are pending.


What arbitration serializes — and what it does not

Arbitration operates at start_item boundaries — the point where a sequence requests the next driver slot. It does not serialize entire sequence body() execution. Two sequences can both be 'running' — one blocked at start_item while the other holds the driver.

diagram
[SEQ] two sequences, one driver — thread vs item view

  TIME ─────────────────────────────────────────────────────────────►

  seq_A thread:  [body running]──start_item──BLOCKED──start_item──BLOCKED──...
  seq_B thread:  [body running]──start_item──[DRIVING]──finish_item──start_item──...

  DRIVER:                    idle    item_B1    idle    item_A1    item_B2 ...

  Thread parallelism:  BOTH sequences active (fork/join)
  Driver serialization: ONE item at a time (arbitration)

When arbitration surprises you

  • Register programming interleaved with background traffic — partial config visible to DUT.

  • Directed read inserted between two writes of a burst — breaks burst semantics.

  • Reset sequence must preempt stress traffic — needs priority or lock, not hope.

  • Assuming seq.start() completion order matches fork order — items interleave instead.


The sequencer as arbiter — not just a FIFO pipe

Early testbenches sometimes used a single sequence per interface and never hit arbitration. As soon as you add background generators, layered sequences, or virtual sequences that fork sub-sequences on the same agent, arbitration becomes unavoidable. The sequencer is the only place where multi-sequence access is mediated before the driver.

diagram
[UVM] sequencer responsibilities

  ┌──────────────────────────────────────────────────────────┐
  │ uvm_sequencer                                            │
  │                                                          │
  │  1. Accept start_item from N concurrent sequences        │
  │  2. Queue pending requests per arbitration mode          │
  │  3. Grant ONE item to driver via get_next_item           │
  │  4. Block other sequences at start_item until their turn │
  │  5. Honor lock/grab — exclusive access override          │
  │  6. Apply priority weighting when mode requires it       │
  └──────────────────────────────────────────────────────────┘
         ▲                    │
         │ start_item         │ get_next_item / item_done
    [SEQ] sequences          ▼
                        [STIM] driver

Key takeaways

  • One driver = one item at a time; many sequences need an arbiter.

  • Arbitration serializes start_item grants, not entire sequence threads.

  • Background + directed + stress on one sequencer is the normal case — plan for interleaving.

Common pitfalls

  • Creating a second driver to avoid arbitration — breaks agent encapsulation and protocol checking.

  • Thinking start() on different sequences guarantees ordering — only items are ordered by arbitration.

  • Ignoring arbitration until a register programming test fails intermittently.