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.
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.
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
| Mode | Selection algorithm | Priority field used? | Typical use |
|---|---|---|---|
SEQ_ARB_FIFO | Round-robin — each ready sequence takes one turn in order | No — all sequences equal | Default. General-purpose. Fair access. |
SEQ_ARB_WEIGHTED | Random, weighted by priority value — higher priority = more likely to be selected | Yes — weight | Probabilistic traffic shaping |
SEQ_ARB_RANDOM | Uniform random — any ready sequence equally likely | No — all equal | Stress testing — maximum interleaving |
SEQ_ARB_STRICT_FIFO | Highest priority selected first; ties broken by FIFO order | Yes — strict | ISR modelling, priority traffic |
SEQ_ARB_STRICT_RAND | Highest priority selected first; ties broken randomly | Yes — strict | Priority with randomness at same level |
SEQ_ARB_USER | Calls user’s user_priority_arbitration() override | User-defined | Custom scheduling policies |
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
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:
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
| Method | Timing | Release with | Use when |
|---|---|---|---|
grab(this) | After current item_done — pre-empts queue | ungrab(this) | ISR that must respond ASAP |
lock(this) | After all active sequences’ current items complete | unlock(this) | Atomic burst that must not be interleaved |
is_grabbed() | — | — | Check lock state from another sequence or component |
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
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
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
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
| Pitfall | Symptom | Fix |
|---|---|---|
| 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 response | get_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 repeatedly | All 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_item | Sequencer 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 tests | Non-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. |
| Item | Key fact |
|---|---|
| Default arbitration | SEQ_ARB_FIFO — round-robin, fair, priority-agnostic |
| Set arbitration | m_sequencer.set_arbitration(SEQ_ARB_*) — from sequence body() or test |
| Start with priority | seq.start(seqr, parent, priority_int) — higher = preferred |
| Priority -1 | Default — maps to sequencer’s internal default priority |
| SEQ_ARB_STRICT_FIFO | Highest priority first; ties by FIFO — use for ISR modelling |
| SEQ_ARB_WEIGHTED | Random 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 pattern | Monitor thread waits for interrupt → grab() → start ISR seq → ungrab() |
| Response routing requirement | rsp.set_id_info(req) before put(rsp) — mandatory for correct routing |
| Clone before put | Always $cast(rsp, req.clone()) — prevents multiple FIFO entries pointing to same object |
| Slave sequence | Reactive — responds to DUT-initiated transactions. Attached to slave-side driver on monitored interface. |
| grab() inside handshake | Never call grab() between start_item and finish_item — deadlock guaranteed |
| wait fork vs disable fork | Prefer wait fork — lets forked sequences complete naturally before continuing |