Part 5 · Sequences · Intermediate

lock / grab / unlock / ungrab: Atomic Register Programming

Exclusive sequencer access, lock vs grab semantics, and atomic multi-beat register block programming.

When FIFO is not enough

FIFO arbitration interleaves items from concurrent sequences. That is correct for independent traffic streams but wrong when the DUT requires atomic multi-beat programming — for example, writing an entire register block before background traffic resumes, or completing a mode-switch sequence without a stress write in the middle.

UVM provides lock / grab on the sequencer for exclusive access : while held, no other sequence's start_item is granted until unlock/ungrab.

diagram
[SEQ] FIFO interleaving vs lock — register programming

  WITHOUT lock (broken):
    setup writes CTRL=0x01    bg writes random addr    setup writes MODE=0x02
    DUT sees partial config with bg traffic in the middle  

  WITH lock (correct):
    setup.lock(this)
    setup writes CTRL=0x01    setup writes MODE=0x02    setup writes START=0x01
    setup.unlock(this)
    bg traffic resumes  

lock / unlock — fair exclusive access

Call p_sequencer.lock(this) at the start of an atomic region. lock waits until no other sequence holds the lock and the current in-flight item completes — fair handoff. Pair every lock with unlock(this).

systemverilog
class prog_regs_seq extends uvm_sequence #(apb_item);
  `uvm_declare_p_sequencer(apb_sequencer)
  `uvm_object_utils(prog_regs_seq)

  bit [31:0] reg_vals[$];  // values to program in order

  task body();
    apb_item req = apb_item::type_id::create("req");

    // [SEQ] Acquire exclusive access — no other sequence interleaves
    p_sequencer.lock(this);

    foreach (reg_vals[i]) begin
      start_item(req);
      assert(req.randomize() with {
        addr  == REG_BASE + i * 4;
        write == 1;
        data  == reg_vals[i];
      });
      finish_item(req);
    end

    // [SEQ] Release — background sequences resume arbitration
    p_sequencer.unlock(this);
  endtask
endclass

grab / ungrab — immediate seizure

Call grab(this) when a sequence must seize the sequencer immediately if free, with higher precedence than pending lock requests. ungrab(this) releases. Use grab sparingly — urgent reset or error injection that cannot wait behind a fair lock queue.

diagram
[UVM] lock vs grab semantics

  lock(this):
    • Waits for current driver item to finish
    • Waits for any other lock holder to unlock
    • Fair — queued lock requests served in order
    • Use for: register programming, mode switches, calibration

  grab(this):
    • Seizes immediately if sequencer not locked
    • Preempts pending lock requests
    • Use for: urgent reset, fatal error injection, watchdog recovery
    • Danger: starving lock holders if grab never released

lock vs grab decision table

  • Register block programming → lock (fair, waits for current item).

  • Multi-beat config without interleaving → lock.

  • Emergency bus reset during stress → grab (preempt).

  • Never grab in background sequences — starves everyone else.


Atomic register programming — full example

A DMA engine requires four registers programmed in order before START is written. Background APB traffic runs concurrently. lock ensures the four writes execute back-to-back on the bus.

systemverilog
class dma_config_seq extends uvm_sequence #(apb_item);
  `uvm_declare_p_sequencer(apb_sequencer)
  rand bit [31:0] src, dst;
  rand int unsigned len;

  task body();
    apb_item req = apb_item::type_id::create("req");

    p_sequencer.lock(this);

    // Atomic programming sequence — no bg interleave
    write_reg(req, REG_SRC,  src);
    write_reg(req, REG_DST,  dst);
    write_reg(req, REG_LEN,  len);
    write_reg(req, REG_CTRL, CTRL_ENABLE);

    p_sequencer.unlock(this);
  endtask

  task write_reg(apb_item req, bit [31:0] addr, bit [31:0] data);
    start_item(req);
    assert(req.randomize() with { this.addr == addr; this.data == data; write == 1; });
    finish_item(req);
  endtask
endclass
diagram
[STIM] bus timeline — with lock

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

  bg_seq:    [item]──BLOCKED──────────────────────────[item]──[item]──...
  dma_cfg:         lock──SRC──DST──LEN──CTRL──unlock
  DRIVER:    bg0         dma0  dma1  dma2  dma3  ctrl        bg1  ...

  Between lock and unlock: ONLY dma_config_seq items on the bus

Key takeaways

  • lock/unlock — fair exclusive access for atomic multi-beat operations.

  • grab/ungrab — immediate seizure; use only for urgent preemption.

  • Always pair lock with unlock — forgotten unlock starves all other sequences.

Common pitfalls

  • Forgotten unlock after lock — every other sequence hangs at start_item forever.

  • lock inside a fork branch without unlock on all paths — use try/finally pattern.

  • Using lock for every sequence — serializes everything; defeats concurrent stimulus.

  • grab in a loop — permanent starvation of lower-priority sequences.