UVM-15: Sequence Control & Arbitration — VLSI Trainers
VLSI Trainers UVM Series · UVM-15
UVM Series · UVM-15

Sequence Control & Arbitration

How the sequencer manages multiple concurrent sequences — the six built-in arbitration modes, priority-based sequencing, grab() and lock() for exclusive access, interrupt handling patterns using sequence priorities, response routing, and slave (reactive) sequences.

📋 How Sequencer Arbitration Works

The sequencer is not a passive pipe — it is an active arbitrator. When multiple sequences are started on the same sequencer simultaneously, each sequence calls start_item() requesting access to the driver. The sequencer maintains an internal list of all sequences waiting for access and uses an arbitration algorithm to decide which sequence’s item goes through next.

The decision cycle happens each time the driver calls get_next_item(). The sequencer scans its ready queue, applies the arbitration algorithm, selects one sequence, grants access (unblocking that sequence’s start_item()), and waits for the selected sequence to call finish_item(). Only then does it run arbitration again for the next item.

Sequencer Arbitration — Multiple Sequences, One Driver Seq A (prio=100) Seq B (prio=200) Seq C (prio=100) start_item Sequencer arbitration algorithm picks highest priority or round-robin if tied item from Seq B (highest priority) Driver get_next_item item_done → re-arbitrateA, C still waiting next round: A or C (equal priority) vlsitrainers.com
Figure 1 — Sequencer arbitration with three concurrent sequences. Seq B has priority 200; Seq A and Seq C both have priority 100. With SEQ_ARB_STRICT_FIFO, Seq B’s item goes through first. After item_done(), the sequencer re-arbitrates — Seq A and Seq C are now equal priority and are selected in FIFO order between themselves.

📋 Six Arbitration Modes

The arbitration mode is set on the sequencer using set_arbitration(). The default mode is SEQ_ARB_FIFO.

// ── Set arbitration mode ──────────────────────────────────
// Can be called from a sequence body() or from a test/env
m_sequencer.set_arbitration(SEQ_ARB_STRICT_FIFO);

// ── Available modes ───────────────────────────────────────
// SEQ_ARB_FIFO        — round-robin (default)
// SEQ_ARB_WEIGHTED    — weighted random by priority
// SEQ_ARB_RANDOM      — uniform random from all ready seqs
// SEQ_ARB_STRICT_FIFO — highest priority first; ties by FIFO
// SEQ_ARB_STRICT_RAND — highest priority first; ties random
// SEQ_ARB_USER        — user-defined arbitration function
ModeSelection algorithmPriority field used?Typical use
SEQ_ARB_FIFORound-robin — each ready sequence takes one turn in orderNo — all sequences equalDefault. General-purpose. Fair access.
SEQ_ARB_WEIGHTEDRandom, weighted by priority value — higher priority = more likely to be selectedYes — weightProbabilistic traffic shaping
SEQ_ARB_RANDOMUniform random — any ready sequence equally likelyNo — all equalStress testing — maximum interleaving
SEQ_ARB_STRICT_FIFOHighest priority selected first; ties broken by FIFO orderYes — strictISR modelling, priority traffic
SEQ_ARB_STRICT_RANDHighest priority selected first; ties broken randomlyYes — strictPriority with randomness at same level
SEQ_ARB_USERCalls user’s user_priority_arbitration() overrideUser-definedCustom scheduling policies

📋 Priority-Based Sequencing

Sequence priority is passed as the third argument to start(). Higher numbers mean higher priority. Priority only affects arbitration when a priority-sensitive mode is active (SEQ_ARB_WEIGHTED, SEQ_ARB_STRICT_FIFO, or SEQ_ARB_STRICT_RAND).

// ── start() with priority ─────────────────────────────────
// task start(sequencer, parent_seq, this_priority, call_pre_post)
//                          ↑ default = -1 (use sequencer default)

// High priority ISR sequence
isr_seq.start(m_sequencer, this, 500);   // priority = 500

// Normal background traffic at lower priority
bg_seq.start(m_sequencer, this, 100);    // priority = 100

// ── Typical priority structure ────────────────────────────
// 1000  — safety-critical / error response
// 500   — interrupt service routines
// 200   — configuration sequences
// 100   — normal data traffic
// 50    — background/idle sequences

// ── Forked concurrent sequences with different priorities
task body();
  m_sequencer.set_arbitration(SEQ_ARB_STRICT_FIFO);
  fork
    config_seq.start(m_sequencer, this, 200);  // config goes first
    data_seq.start(m_sequencer,   this, 100);  // data waits its turn
    idle_seq.start(m_sequencer,   this, 50);   // idle last resort
  join
endtask
Priority -1 means “use the sequencer’s current default priority.” When a sequence is started without specifying priority, it gets -1, which the sequencer maps to its internal default (typically 100). When mixing explicit priorities with -1 sequences, the -1 sequences all share the same effective priority — they round-robin among themselves while explicitly-prioritised sequences are selected first when in a strict mode.

📋 grab() and lock()

Both grab() and lock() give a sequence exclusive access to a sequencer — no other sequence can send items while the lock is held. The difference is timing:

grab() vs lock() — Timing Difference grab() — Pre-empts after current item Seq A item Seq B item grab(ISR) ISR exclusive ISR exclusive ungrab() A/B resume ISR takes over immediately after current item_done lock() — Waits in FIFO, then exclusive Seq A item lock(CFG) Seq B item waits… CFG exclusive CFG exclusive unlock() CFG waits for Seq B to complete naturally, then takes exclusive control vlsitrainers.com
Figure 2 — grab() vs lock() timing. grab() (left) takes exclusive control immediately after the current item_done — the ISR jumps to the front of the queue. lock() (right) waits for all currently active sequences to complete their current item, then takes exclusive control in FIFO order — the CFG sequence waits behind Seq B.
task body();
  // ── grab(): immediate exclusivity after current item ─────
  m_sequencer.grab(this);
  // Only this sequence can send items now
  start_item(req); req.randomize(); finish_item(req);
  m_sequencer.ungrab(this);   // release — MUST be called

  // ── lock(): wait your FIFO turn, then exclusive ───────────
  m_sequencer.lock(this);
  // Gains exclusive control after current items complete
  start_item(req); req.randomize(); finish_item(req);
  m_sequencer.unlock(this);   // release — MUST be called

  // ── is_grabbed(): check if sequencer is locked ───────────
  if (m_sequencer.is_grabbed())
    `uvm_info("SEQ", "Sequencer is currently grabbed", UVM_LOW)
endtask
MethodTimingRelease withUse when
grab(this)After current item_done — pre-empts queueungrab(this)ISR that must respond ASAP
lock(this)After all active sequences’ current items completeunlock(this)Atomic burst that must not be interleaved
is_grabbed()Check lock state from another sequence or component
Always call ungrab() or unlock() — even if body() exits abnormally. A grabbed sequencer that is never ungrabbed permanently blocks all other sequences. If your sequence may hit a fatal error mid-grab, structure body() so ungrab/unlock runs in all exit paths. One approach: use a fork/join_none monitor thread that calls ungrab() when a flag is set.

📋 Interrupt Handling with Sequences

Sequences are an effective way to model interrupt service routines (ISRs). The interrupt monitor thread waits for an interrupt signal, then starts a high-priority ISR sequence using grab() to get immediate exclusive access to the sequencer.

class int_controller_seq extends uvm_sequence #(apb_seq_item);
  `uvm_object_utils(int_controller_seq)

  virtual apb_if vif;       // set by test before start()
  int            num_main = 50;

  function new(string name="int_controller_seq");
    super.new(name);
  endfunction

  task body();
    apb_main_seq main_seq;
    apb_isr_seq  isr_seq;

    main_seq = apb_main_seq::type_id::create("main_seq");
    isr_seq  = apb_isr_seq::type_id::create("isr_seq");

    m_sequencer.set_arbitration(SEQ_ARB_STRICT_FIFO);

    fork
      // Thread 1: main traffic at normal priority
      main_seq.start(m_sequencer, this, 100);

      // Thread 2: interrupt monitor — runs forever alongside main
      begin
        forever begin
          // Wait for interrupt assertion
          @(posedge vif.IRQ);

          // Immediately grab the sequencer
          m_sequencer.grab(this);

          // Run ISR — reads status, clears interrupt
          isr_seq.start(m_sequencer, this);

          // Release sequencer — main traffic resumes
          m_sequencer.ungrab(this);
        end
      end
    join_any
    wait fork;
  endtask
endclass
Preferred ISR pattern: grab + dedicated ISR sequence. The ISR sequence calls grab() at the start of body() and ungrab() at the end. This encapsulates the exclusive access requirement inside the ISR itself — the caller does not need to manage grab/ungrab. The interrupt monitor simply starts the ISR sequence; the sequence handles its own exclusivity.

📋 Response Routing

When multiple sequences send items through a single sequencer, the sequencer tracks which item belongs to which sequence using internal ID fields. When the driver returns a response (via put(rsp)), the sequencer routes it to the correct sequence’s get_response() call. This routing is automatic — but requires the response item to carry the correct ID.

// ── Driver: build response with correct ID ───────────────
task run_phase(uvm_phase phase);
  apb_seq_item req, rsp;
  forever begin
    seq_item_port.get(req);      // unblocks finish_item immediately

    // ... drive DUT signals ...

    // Clone request, copy ID info — required for routing
    $cast(rsp, req.clone());
    rsp.set_id_info(req);        // copies sequence + transaction IDs
    rsp.read_data = vif.PRDATA;
    rsp.error     = vif.PSLVERR;
    seq_item_port.put(rsp);      // sequencer routes to correct sequence
  end
endtask

// ── Sequence: collect response ────────────────────────────
task body();
  apb_seq_item req, rsp;
  req = apb_seq_item::type_id::create("req");
  start_item(req);
  req.randomize();
  finish_item(req);      // returns immediately (get() unblocks it)
  get_response(rsp);     // blocks until driver calls put(rsp)
  `uvm_info("SEQ", rsp.convert2string(), UVM_MEDIUM)
endtask
set_id_info(req) is mandatory when using get() + put(). Without it, the sequencer cannot route the response back to the originating sequence. In a multi-sequence environment, responses will be delivered to the wrong sequence or silently dropped. Always clone the request and call set_id_info before put().

📋 Slave (Reactive) Sequences

A slave or reactive sequence responds to transactions initiated by the DUT rather than generating its own stimulus. This is used to model slave interfaces — a memory model that responds to DUT read requests, or an interrupt controller that responds to interrupt signals. The slave sequence runs on a driver that is attached to the slave side of the interface.

The key mechanism is get_next_item() in the slave driver — which is now responding to DUT-initiated transactions rather than driving them. The slave sequence uses a dedicated “slave sequencer” on the interface being monitored.

// ── Slave driver: reacts to DUT transactions ─────────────
class mem_slave_driver extends uvm_driver #(mem_req_item);
  `uvm_component_utils(mem_slave_driver)
  virtual mem_if vif;

  task run_phase(uvm_phase phase);
    mem_req_item req;
    forever begin
      // Wait for DUT to initiate a read/write
      @(posedge vif.PCLK);
      if (vif.CS && vif.OE) begin
        // Fetch response from the slave sequence
        seq_item_port.get_next_item(req);
        // Respond with data from req (populated by slave sequence)
        vif.RDATA   <= req.rdata;
        vif.WAIT_N  <= req.wait_states > 0;
        @(posedge vif.PCLK);
        seq_item_port.item_done();
      end
    end
  endtask
endclass

// ── Slave sequence: provides response data ────────────────
class mem_slave_seq extends uvm_sequence #(mem_req_item);
  `uvm_object_utils(mem_slave_seq)

  // Memory model — populated with test data before start()
  logic [31:0] mem [0:255];

  task body();
    mem_req_item req;
    forever begin
      req = mem_req_item::type_id::create("req");
      start_item(req);
      // Do NOT randomise — populate with memory response
      req.rdata       = mem[req.addr[9:2]];
      req.wait_states = 0;
      finish_item(req);
    end
  endtask
endclass

📋 Multiple Concurrent Sequences — Patterns

Several useful patterns for running multiple sequences on the same sequencer simultaneously:

// ── Pattern 1: weighted traffic mix ──────────────────────
// 70% write traffic, 30% read traffic — weighted arb
task body();
  m_sequencer.set_arbitration(SEQ_ARB_WEIGHTED);
  fork
    write_seq.start(m_sequencer, this, 700);  // weight=700
    read_seq.start(m_sequencer,  this, 300);  // weight=300
  join
endtask

// ── Pattern 2: config then background ────────────────────
// Config sequence runs at higher priority until done
task body();
  m_sequencer.set_arbitration(SEQ_ARB_STRICT_FIFO);
  fork
    cfg_seq.start(m_sequencer, this, 500);   // high priority
    bg_seq.start(m_sequencer,  this, 50);    // fills gaps
  join
endtask

// ── Pattern 3: randomised interleaving ───────────────────
// Three sequences competing randomly — stress test
task body();
  m_sequencer.set_arbitration(SEQ_ARB_RANDOM);
  fork
    seq_a.start(m_sequencer, this);
    seq_b.start(m_sequencer, this);
    seq_c.start(m_sequencer, this);
  join
endtask

📋 Common Pitfalls

PitfallSymptomFix
Forgetting ungrab() / unlock()Sequencer permanently locked. All other sequences stall forever. Simulation timeout.Always call ungrab()/unlock() in all body() exit paths. Structure with a flag + monitor thread if needed.
Priority mode not set before concurrent start()Priorities ignored — FIFO mode is default so all sequences are treated equally regardless of their priority value.Call set_arbitration(SEQ_ARB_STRICT_FIFO) before starting concurrent sequences.
Missing set_id_info() in responseget_response() never returns, or returns wrong data. Responses routed to wrong sequence.Always rsp.set_id_info(req) before seq_item_port.put(rsp).
Cloning the same response object repeatedlyAll responses in the FIFO point to the same object. Successive get_response() calls yield identical (last-written) data.Always $cast(rsp, req.clone()) to create a new response object for each item.
Calling grab() inside start_item/finish_itemSequencer deadlock — grab() waits for the current item_done, but item_done cannot happen because finish_item is blocked waiting for the grab.Call grab() only from outside the start_item/finish_item block — not from between them.
Using SEQ_ARB_RANDOM for reproducible testsNon-deterministic — same seed may produce different sequence ordering on different tools.Use SEQ_ARB_FIFO or SEQ_ARB_WEIGHTED for deterministic ordering. Use RANDOM only for intentional stress/randomisation.

📋 Quick Reference

ItemKey fact
Default arbitrationSEQ_ARB_FIFO — round-robin, fair, priority-agnostic
Set arbitrationm_sequencer.set_arbitration(SEQ_ARB_*) — from sequence body() or test
Start with priorityseq.start(seqr, parent, priority_int) — higher = preferred
Priority -1Default — maps to sequencer’s internal default priority
SEQ_ARB_STRICT_FIFOHighest priority first; ties by FIFO — use for ISR modelling
SEQ_ARB_WEIGHTEDRandom selection weighted by priority value — probabilistic mix
grab()Immediate exclusive access after current item_done. Release with ungrab().
lock()Exclusive access after FIFO turn. Release with unlock().
is_grabbed()Returns true if any sequence holds a grab or lock on the sequencer
ISR patternMonitor thread waits for interrupt → grab() → start ISR seq → ungrab()
Response routing requirementrsp.set_id_info(req) before put(rsp) — mandatory for correct routing
Clone before putAlways $cast(rsp, req.clone()) — prevents multiple FIFO entries pointing to same object
Slave sequenceReactive — responds to DUT-initiated transactions. Attached to slave-side driver on monitored interface.
grab() inside handshakeNever call grab() between start_item and finish_item — deadlock guaranteed
wait fork vs disable forkPrefer wait fork — lets forked sequences complete naturally before continuing
Scroll to Top