Sequencer Arbitration & Control - Managing Concurrent Sequences

When multiple sequences compete for a single driver, who wins? Sequencer arbitration answers this question. Understanding arbitration modes, lock/grab mechanisms, and priority scheduling is essential for interrupt handling, resource sharing, and deterministic test scenarios.

The Arbitration Problem

Consider a CPU interface where normal traffic, interrupt service routines, and debug sequences all need access:

flowchart LR
    subgraph "Competing Sequences"
        S1[Normal Traffic
Priority: Low] S2[ISR Sequence
Priority: High] S3[Debug Sequence
Priority: Medium] end S1 --> ARB[Arbitration
Queue] S2 --> ARB S3 --> ARB ARB --> D[Driver]

Without proper arbitration, your ISR might wait behind 100 normal transactions - not realistic behavior.

Arbitration Modes: Strategy Pattern in Action

UVM's arbitration modes are essentially the Strategy Pattern - interchangeable algorithms for the same problem:

ModeAlgorithmRespects Priority?Use Case
UVM_SEQ_ARB_FIFOFirst In, First OutNoDefault, simple ordering
UVM_SEQ_ARB_RANDOMRandom selectionNoStress testing, randomized scheduling
UVM_SEQ_ARB_STRICT_FIFOHighest priority first, FIFO withinYesPriority-based with deterministic tiebreaker
UVM_SEQ_ARB_STRICT_RANDOMHighest priority first, random withinYesPriority-based with randomized tiebreaker
UVM_SEQ_ARB_WEIGHTEDProbabilistic by priority weightSoftWeighted traffic mix
UVM_SEQ_ARB_USERCustom algorithmCustomSpecial scheduling requirements

Setting Arbitration Mode

class my_env extends uvm_env;
  cpu_sequencer cpu_sqr;
  
  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    cpu_sqr = cpu_sequencer::type_id::create("cpu_sqr", this);
  endfunction
  
  function void connect_phase(uvm_phase phase);
    super.connect_phase(phase);
    // Set priority-aware arbitration
    cpu_sqr.set_arbitration(UVM_SEQ_ARB_STRICT_FIFO);
  endfunction
endclass

Setting Sequence Priority

Priority is passed to the start() method (default is 100, higher = more important):

class interrupt_test extends uvm_test;
  task run_phase(uvm_phase phase);
    phase.raise_objection(this);
    
    fork
      // Low priority: Normal traffic (priority 100 - default)
      normal_seq.start(cpu_sqr);
      
      // High priority: ISR (priority 200)
      isr_seq.start(cpu_sqr, null, 200);
      
      // Medium priority: Debug (priority 150)
      debug_seq.start(cpu_sqr, null, 150);
    join
    
    phase.drop_objection(this);
  endtask
endclass

Lock and Grab: Exclusive Access

Sometimes a sequence needs uninterrupted access - like an atomic operation or critical section. UVM provides two mechanisms:

MethodBehaviorQueue PositionUse Case
lock()Request exclusive accessBack of queuePlanned exclusive sections
grab()Force exclusive accessFront of queueUrgent interruption (ISR)

Lock: Polite Exclusive Access

class atomic_write_seq extends uvm_sequence #(bus_txn);
  `uvm_object_utils(atomic_write_seq)
  
  task body();
    // Request exclusive access (waits in queue)
    lock(m_sequencer);
    
    `uvm_info("SEQ", "Lock acquired - starting atomic write", UVM_MEDIUM)
    
    // Critical section: No other sequence can interrupt
    repeat(4) begin
      `uvm_do_with(req, { addr[1:0] == 2'b00; })  // Word-aligned burst
    end
    
    // Release
    unlock(m_sequencer);
    `uvm_info("SEQ", "Lock released", UVM_MEDIUM)
  endtask
endclass

Grab: Urgent Exclusive Access

class isr_sequence extends uvm_sequence #(bus_txn);
  `uvm_object_utils(isr_sequence)
  
  task body();
    // IMMEDIATELY take control (cuts in line)
    grab(m_sequencer);
    
    `uvm_info("ISR", "Interrupt handler active", UVM_LOW)
    
    // Handle interrupt
    read_status_register();
    clear_interrupt();
    service_interrupt();
    
    // Release control
    ungrab(m_sequencer);
    `uvm_info("ISR", "Interrupt handler complete", UVM_LOW)
  endtask
  
  task read_status_register();
    `uvm_do_with(req, { op == READ; addr == ISR_ADDR; })
  endtask
  
  task clear_interrupt();
    `uvm_do_with(req, { op == WRITE; addr == ICR_ADDR; data == 32'hFFFF; })
  endtask
endclass

Interrupt Handling Pattern

A complete interrupt handling implementation combines grab/ungrab with event-driven triggering:

class interrupt_handler_vseq extends uvm_sequence #(uvm_sequence_item);
  `uvm_object_utils(interrupt_handler_vseq)
  `uvm_declare_p_sequencer(soc_virtual_sequencer)
  
  // Configuration
  int num_irq_lines = 4;
  
  // Sequences
  main_traffic_seq  main_seq;
  isr_sequence      isr_seq[4];
  
  task body();
    // Create ISR sequences for each IRQ line
    foreach(isr_seq[i])
      isr_seq[i] = isr_sequence::type_id::create($sformatf("isr_seq_%0d", i));
    
    main_seq = main_traffic_seq::type_id::create("main_seq");
    
    fork
      // Main traffic (runs continuously)
      main_seq.start(p_sequencer.cpu_sqr);
      
      // IRQ monitors (one per line)
      monitor_irq(0);
      monitor_irq(1);
      monitor_irq(2);
      monitor_irq(3);
    join_any
    disable fork;  // Cleanup
  endtask
  
  task monitor_irq(int irq_num);
    forever begin
      // Wait for interrupt assertion
      @(posedge p_sequencer.vif.irq[irq_num]);
      
      `uvm_info("IRQ", $sformatf("IRQ %0d asserted", irq_num), UVM_LOW)
      
      // Service with appropriate ISR
      isr_seq[irq_num].irq_num = irq_num;
      isr_seq[irq_num].start(p_sequencer.cpu_sqr, this, 300);  // High priority
      
      `uvm_info("IRQ", $sformatf("IRQ %0d serviced", irq_num), UVM_LOW)
    end
  endtask
endclass

ISR with Grab for Immediate Response

class isr_sequence extends uvm_sequence #(bus_txn);
  `uvm_object_utils(isr_sequence)
  
  int irq_num;
  
  task body();
    // Grab immediately - interrupt latency matters!
    grab(m_sequencer);
    
    // Standard ISR flow
    read_isr_status();
    
    case(irq_status)
      IRQ_TIMER:  handle_timer();
      IRQ_DMA:    handle_dma_complete();
      IRQ_ERROR:  handle_error();
      default:    handle_unknown();
    endcase
    
    clear_interrupt();
    ungrab(m_sequencer);
  endtask
endclass

Custom Arbitration: User-Defined Strategy

For complex scheduling requirements, implement custom arbitration:

class fair_arbitration_sequencer extends uvm_sequencer #(bus_txn);
  `uvm_component_utils(fair_arbitration_sequencer)
  
  // Track grants per sequence for fairness
  int grant_count[uvm_sequence_base];
  int max_consecutive = 3;  // Max grants before yielding
  
  function new(string name, uvm_component parent);
    super.new(name, parent);
    set_arbitration(UVM_SEQ_ARB_USER);
  endfunction
  
  // Override: Custom arbitration algorithm
  function integer user_priority_arbitration(integer avail_sequences[$]);
    int best_idx = -1;
    int min_grants = 999999;
    
    // Find sequence with fewest recent grants (fairness)
    foreach(avail_sequences[i]) begin
      uvm_sequence_base seq;
      seq = arb_sequence_q[avail_sequences[i]].sequence_ptr;
      
      // Skip if exceeded consecutive limit
      if (grant_count.exists(seq) && grant_count[seq] >= max_consecutive)
        continue;
      
      // Prefer least-served sequence
      if (!grant_count.exists(seq) || grant_count[seq] < min_grants) begin
        min_grants = grant_count.exists(seq) ? grant_count[seq] : 0;
        best_idx = avail_sequences[i];
      end
    end
    
    // Update tracking
    if (best_idx >= 0) begin
      uvm_sequence_base seq = arb_sequence_q[best_idx].sequence_ptr;
      grant_count[seq]++;
    end
    
    return best_idx;
  endfunction
endclass

Real-World Example: CPU Bus Arbiter

class cpu_bus_test extends uvm_test;
  `uvm_component_utils(cpu_bus_test)
  
  cpu_env env;
  
  task run_phase(uvm_phase phase);
    // Use strict priority for realistic behavior
    env.cpu_sqr.set_arbitration(UVM_SEQ_ARB_STRICT_FIFO);
    
    phase.raise_objection(this);
    
    fork
      // Background: Normal memory traffic (lowest priority)
      begin
        memory_traffic_seq mem_seq = memory_traffic_seq::type_id::create("mem_seq");
        mem_seq.num_transactions = 1000;
        mem_seq.start(env.cpu_sqr, null, 50);  // Priority 50
      end
      
      // Periodic: DMA transfers (medium priority)
      begin
        repeat(10) begin
          #($urandom_range(100, 500) * 1ns);
          dma_burst_seq dma_seq = dma_burst_seq::type_id::create("dma_seq");
          dma_seq.start(env.cpu_sqr, null, 100);  // Priority 100
        end
      end
      
      // Sporadic: Debug probe (high priority)
      begin
        repeat(5) begin
          #($urandom_range(200, 800) * 1ns);
          debug_read_seq dbg_seq = debug_read_seq::type_id::create("dbg_seq");
          dbg_seq.start(env.cpu_sqr, null, 150);  // Priority 150
        end
      end
      
      // Rare: Error handler (highest priority + grab)
      begin
        @(env.error_event);
        error_handler_seq err_seq = error_handler_seq::type_id::create("err_seq");
        // Uses grab() internally for immediate response
        err_seq.start(env.cpu_sqr, null, 200);
      end
    join
    
    phase.drop_objection(this);
  endtask
endclass

Best Practices

1. Always Release Locks

task body();
  lock(m_sequencer);
  
  // Use try-finally pattern (SystemVerilog doesn't have try-finally,
  // so structure code carefully)
  begin
    do_critical_work();
  end
  
  // ALWAYS unlock - even if error occurs
  unlock(m_sequencer);
endtask

2. Prefer Priority Over Grab

// PREFER: Let arbitration handle it
isr_seq.start(sqr, null, 500);  // Very high priority

// AVOID: Grab unless truly necessary
// grab() bypasses priority - use only for true emergencies

3. Document Arbitration Assumptions

class my_test extends uvm_test;
  // ARBITRATION REQUIREMENTS:
  // - Must use STRICT_FIFO for deterministic interrupt latency
  // - ISR sequences use priority 200+
  // - Normal traffic uses priority 50-100
  // - Debug uses priority 150
endclass

4. Test Arbitration Behavior

class arbitration_stress_test extends uvm_test;
  task run_phase(uvm_phase phase);
    // Launch many sequences simultaneously
    fork
      repeat(10) low_priority_seq.start(sqr, null, 50);
      repeat(5)  med_priority_seq.start(sqr, null, 100);
      repeat(2)  high_priority_seq.start(sqr, null, 200);
    join
    
    // Verify high priority completed first
    check_completion_order();
  endtask
endclass

Common Pitfalls

PitfallSymptomSolution
Forgetting unlock()Sequencer deadlocksAlways pair lock/unlock
Using priority with FIFO modePriority ignoredUse STRICT_FIFO or STRICT_RANDOM
Grab without ungrabPermanent exclusive accessAlways ungrab after grab
Priority inversionHigh-priority waits for lowUse grab for true urgency
Non-deterministic testsFlaky regressionsUse STRICT_FIFO, not RANDOM

Key Takeaways

  • Arbitration modes = Strategy Pattern: Choose algorithm matching your needs
  • STRICT_FIFO for priority: Default FIFO ignores priorities
  • Lock for planned exclusion: Politely waits in queue
  • Grab for emergencies: Cuts to front - use sparingly
  • Custom arbitration for special cases: Fair scheduling, weighted access
  • Always release locks: Deadlocks are hard to debug

Previous: Part 3 - Advanced Architectures

This completes the UVM Sequences series. Return to the UVM Hub for more topics.

Sources consulted: ChipVerify, VLSI Verify, Art of Verification

Author
Mayur Kubavat
VLSI Design and Verification Engineer sharing knowledge about SystemVerilog, UVM, and hardware verification methodologies.

Comments (0)

Leave a Comment