Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • Start Here4
  • UVM Foundations14
  • UVM Phases & Simulation Lifecycle26
  • Factory & Configuration15
  • Transaction-Level Modeling (TLM)13
  • UVM Sequences & Stimulus21
    • Sequence Items and the Driver Handshake
    • Sequence Mechanics: Lifecycle, Sequencer, Responses and Control
    • Virtual Sequences and Coordination
      • The Coordination Problem
      • Virtual Sequencer Wiring
      • Declaring p_sequencer
      • Scenario Patterns and Placement
      • Sub-sequences and Sequence Libraries
      • Virtual Sequences Without a Virtual Sequencer
      • lock vs grab: Who Gets the Sequencer
      • Protocol Layering with Translator Sequences
      • Reactive (Slave) Sequences
  • UVM Agents & Protocol VIP21
  • Environment & Test Classes11
  • Checking & Coverage in UVM21
  • UVM Register Abstraction Layer (RAL)22
  • Advanced UVM in Practice13
  • Debug & Regression10
  • Coding Practice47
  • Verification in Industry Practice10
  • Senior Prep: Architecture, Leadership and Interviews32
Loading lesson

‹Virtual Sequences and Coordination
  1. Topics
  2. UVM Verification
  3. UVM Sequences & Stimulus
  4. Virtual Sequences and Coordination
  5. lock vs grab: Who Gets the Sequencer

Part 5 · Sequences · Intermediate

lock vs grab: Who Gets the Sequencer

What lock() and grab() do to the sequencer's arbitration queue, shown with a real log: queue position, the item already in the driver, children of the holder, nested locks, the deadlock of a parentless child, killed and forgetful lock holders, and when to use which.

On this page

    The DMA raises its interrupt while apb_poll_seq is reading COUNT every 50 ns. The handler, dma_irq_seq, must read STATUS and then clear it with no poll read slipping in between, and it should not wait behind a queue of polls to start. The bench uses grab(). An interviewer asks why not lock(), and then: "what exactly happens to the requests already waiting?" Most candidates answer with the reference manual's wording. This lesson answers with a log.

    The idea: a reserved lane

    Normally two sequences on one sequencer interleave item by item. Sometimes a sequence needs several items in a row with nothing in between: a read-modify-write, register programming, an interrupt handler. lock() and grab() give it exclusive use of the sequencer until unlock() or ungrab(). Meanwhile every other sequence's request is blocked: it stays queued and arbitration skips it. The holder's own requests, and its children's, go through.

    Think of a single-lane bridge where a convoy can reserve the lane. A lock request is a convoy that joins the back of the line of convoys waiting to reserve it; a grab is an ambulance convoy that goes to the front of that line. Neither one stops the car already on the bridge: the item in the driver always finishes first.

    lock()

    grab()

    Where the request goes

    The back of the arbitration queue

    The front of the arbitration queue

    When it is granted

    As soon as no other lock or grab blocks it

    The same, and it is checked before any lock queued earlier

    The item already in the driver

    Finishes first

    Finishes first

    Released by

    unlock()

    ungrab()

    Who may still send while it is held

    The holder and its children (started with it as parent)

    The same

    The picture: one sequencer, four sequences

    The timeline below is scene 2 of a scratch run on the bench's APB sequencer (the log is further down). L1 holds a lock. While it does, a plain sequence P asks for one item, L2 asks for a lock, and G asks for a grab, in that order.

    Sequence diagram: L1 locks the sequencer; P, L2 and G queue; on unlock G is granted, then L2, then P.
    Scene 2 of the scratch run. When L1 unlocks, the grab that arrived last is served first, then the earlier lock, and the plain item that asked first of all goes last.
    Diagram
    time     arbitration queue (front ... back)     holding
    605 ns   P:item                                  L1
    615 ns   P:item  L2:lock                         L1
    625 ns   G:grab  P:item  L2:lock                 L1
    755 ns   L1 unlocks: first unblocked lock or grab in queue order is G
             P:item  L2:lock  G:item                 G
    795 ns   G ungrabs: L2's lock is granted at once
             P:item  L2:item                         L2
    835 ns   L2 unlocks: P is no longer blocked      -

    Read the last line twice. P asked first, yet went last. The plain item was never "in line" for the reservation: it was blocked, and every reservation that was granted kept it blocked.

    Build it: the handler on the bench

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    // snippet: from the course DMA bench
    // The interrupt handler: grab the APB sequencer so the handler's accesses go
    // ahead of any queued polling, read STATUS, clear DONE/ERR, then let go.
    class dma_irq_seq extends uvm_sequence #(apb_item);
      `uvm_object_utils(dma_irq_seq)
      bit [31:0] status;
    
      function new(string name = "dma_irq_seq");
        super.new(name);
      endfunction
    
      task access(bit [7:0] addr, bit wr, bit [31:0] data, output bit [31:0] rdata);
        apb_item it = apb_item::type_id::create("it");
        start_item(it);
        if (!it.randomize() with { addr == local::addr; write == wr; data == local::data; })
          `uvm_fatal("RAND", "apb_item randomize failed")
        finish_item(it);
        rdata = it.rdata;
      endtask
    
      task body();
        bit [31:0] unused;
        grab();                                  // jump the queue, hold the sequencer
        `uvm_info("IRQ", "handler has the APB sequencer", UVM_MEDIUM)
        access(STATUS, 0, 0, status);
        access(STATUS, 1, 32'h6, unused);        // write 1 to clear DONE and ERR
        ungrab();
      endtask
    endclass

    grab() and ungrab() with no argument act on the sequence's own sequencer. Between them the handler's two accesses go back to back; a poll read already queued waits. If the handler ever started a child sequence, only a child started with the handler as parent could send while it holds the grab.

    To see the queue rules directly, the scratch run uses a small APB sequence whose instances are named by a tag and that can lock, grab or neither. It reads KEY n times and logs each completed item:

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // snippet: the scratch sequence (body only; the class has the usual macro and constructor)
    task body();
      if (mode == 1) begin `uvm_info("ARB", {tag, " calls lock()"}, UVM_LOW) lock(); `uvm_info("ARB", {tag, " has the lock"}, UVM_LOW) end
      if (mode == 2) begin `uvm_info("ARB", {tag, " calls grab()"}, UVM_LOW) grab(); `uvm_info("ARB", {tag, " has the grab"}, UVM_LOW) end
      for (int i = 0; forever_mode || i < n; i++) begin
        send(i);                               // start_item, randomize (addr == KEY, read), finish_item, log
        if (gap != 0) #(gap * 1ns);
      end
      if (child != null) child.start(m_sequencer, this);   // a child with this sequence as parent
      if (mode == 1) begin unlock(); `uvm_info("ARB", {tag, " unlock()"}, UVM_LOW) end
      if (mode == 2) begin ungrab(); `uvm_info("ARB", {tag, " ungrab()"}, UVM_LOW) end
    endtask

    Watch it run

    Scene 1: A and B each send four reads back to back; 50 ns in, L asks for a lock. Scene 2 is the timeline above. Scene 3: L3 locks, sends one item, then starts a child C with itself as parent, while Q waits. Scene 4: K locks and loops forever; the test kills it. (Each APB access takes 40 ns; times in ps.)

    Diagram
    [SCENE] 1: A and B stream back to back; L asks for a lock
    @ 135000: ...@@A  [ARB] A item 0 done
    @ 150000: ...@@L  [ARB] L calls lock()
    @ 150000: ...@@L  [ARB] L has the lock
    @ 175000: ...@@B  [ARB] B item 0 done
    @ 215000: ...@@L  [ARB] L item 0 done
    @ 255000: ...@@L  [ARB] L item 1 done
    @ 255000: ...@@L  [ARB] L unlock()
    @ 295000: ...@@A  [ARB] A item 1 done
    [SCENE] 2: L1 holds a lock; P queues an item, L2 a lock, G a grab
    @ 755000: ...@@L1 [ARB] L1 unlock()
    @ 755000: ...@@G  [ARB] G has the grab
    @ 795000: ...@@G  [ARB] G ungrab()
    @ 795000: ...@@L2 [ARB] L2 has the lock
    @ 835000: ...@@L2 [ARB] L2 unlock()
    @ 875000: ...@@P  [ARB] P item 0 done
    [SCENE] 3: L3 holds a lock and starts child C with itself as parent; Q waits
    @ 1015000: ...@@L3   [ARB] L3 item 0 done
    @ 1055000: ...@@L3.C [ARB] C item 0 done
    @ 1095000: ...@@L3.C [ARB] C item 1 done
    @ 1095000: ...@@L3   [ARB] L3 unlock()
    @ 1135000: ...@@Q    [ARB] Q item 0 done
    [SCENE] 4: K locks and never unlocks; Q2 waits; the test kills K
    @ 1415000: ...@@K  [ARB] K item 1 done
    @ 1535000: uvm_test_top [ARB] test calls K.kill()
    @ 1595000: ...@@Q2 [ARB] Q2 item 0 done

    (... stands for uvm_test_top.env.apb.sqr; the full log has every line.) Four things to take from it:

    • Scene 1, the in-flight item. B's item, already in the driver at 150 ns, finishes at 175 ns. The lock does not cut it short.

    • Scene 1, the queued item. A's request has waited since 135 ns, yet L's lock is granted at 150 ns, the moment it is asked for, and both of L's items go before A's. A lock waits for other locks and grabs, not for plain items.

    • Scene 3, children pass. C, started with L3 as parent, sends through L3's lock while Q waits.

    • Scene 4, kill releases. kill() removes the sequence's requests and locks; Q2 gets the bus at the next item.

    Note

    The reference manual describes a lock as "arbitrated the same as any other request", which suggests it waits behind items queued before it. The library code this run used (grant_queued_locks in the IEEE 1800.2 2020.3.2 implementation) grants a lock as soon as no other lock or grab blocks it. This run says nothing about older libraries such as UVM 1.2: read grant_queued_locks in the version your project uses before relying on either behaviour. Queue position decides the order among lock and grab requests. In an interview, say both: what the manual says and what the code does.

    The rules that catch people

    Nested locks

    Each lock() or grab() adds an entry to the sequencer's list of holders, so a sequence that locks twice must unlock twice; an early return between a pair leaves an entry behind. unlock() from a sequence that holds nothing gives the warning SQRUNL. A child of the holder may lock too and is granted at once, but then the parent is blocked until the child unlocks: the family check lets descendants of a holder through, not ancestors.

    The deadlock of a parentless child

    Start a child without the parent argument while you hold the lock, and the child is a stranger: its first start_item is blocked by its own parent's lock, and the parent waits forever for the child. A scratch test does exactly that, with a 5 us timeout:

    design.sv
    1
    2
    3
    4
    5
    // bad-example: D holds the lock, then starts its child without "this"
    lock();
    send(0);
    child.start(m_sequencer);              // should be child.start(m_sequencer, this)
    unlock();                              // never reached
    Diagram
    UVM_INFO dma_tb_scratch.sv(865) @ 100000: uvm_test_top.env.apb.sqr@@D [ARB] D has the lock
    UVM_INFO dma_tb_scratch.sv(857) @ 135000: uvm_test_top.env.apb.sqr@@D [ARB] D item 0 done
    UVM_FATAL uvm_phase_hopper.svh(598) @ 5000000: reporter [PH_TIMEOUT] Explicit timeout of 5000000 hit, indicating a probable testbench issue

    No error, no warning: the run just stops. The clue in a real hang is a child named without its parent's prefix (...@@C, not ...@@D.C). This is why dma_vseq passes this to every start().

    A holder that is killed, or forgets

    kill() cleans up, as scene 4 shows. A sequence that simply returns while still holding a lock is also cleaned up when it exits, but the library reports it. A scratch sequence F locks, sends one item and returns:

    Diagram
    UVM_INFO dma_tb_scratch.sv(1344) @ 135000: uvm_test_top.env.apb.sqr@@F [ARB] F returns without unlock()
    UVM_ERROR uvm_sequencer_base.svh(1264) @ 135000: uvm_test_top.env.apb.sqr [SEQFINERR] Parent sequence 'uvm_test_top.env.apb.sqr.F' should not finish before locks from itself and descedent sequences are removed.  The lock held by the child sequence 'uvm_test_top.env.apb.sqr.F' is being removed.
    UVM_INFO dma_tb_scratch.sv(857) @ 175000: uvm_test_top.env.apb.sqr@@Q [ARB] Q item 0 done

    The dangerous case has no cleanup at all: a lock holder whose thread is ended by disable fork. Its start() never reaches the exit path, its entry stays in the lock list, and every other sequence on that sequencer waits forever. To stop a lock holder, call kill() on it.

    When to use which

    Situation

    Use

    Why

    Interrupt or error handler that must run next

    grab()

    Goes ahead of any lock queued before it

    Read-modify-write, multi-register programming

    lock()

    Atomic, and polite to other reservations already waiting

    "Make this sequence more likely to win"

    Priority with a weighted or strict mode

    Neither lock nor grab is about likelihood

    Hold the bus across a child sequence

    Either, and start the child with this

    Children pass; strangers deadlock

    Stop a lock holder early

    kill() on it

    Releases its locks and queued requests

    What breaks: the handler without its grab

    Delete the grab() and ungrab() lines from dma_irq_seq and the bench still passes on most seeds, which is what makes the bug dangerous. Whenever the poller's next read is queued while the STATUS read is in the driver, FIFO arbitration grants that older request before the handler's STATUS write, and a COUNT read lands in between. Harmless on this DMA; on a design where a read has side effects (a FIFO pop, a clear-on-read counter) the device changes between the handler's read and write, and only on the seeds where the timing lines up. lock() would also fix it on this bench, since nothing else locks the APB sequencer, but would start waiting the day someone adds a locking register sequence. The handler needs its accesses together and now: that is grab().

    Interview

    "What's the difference between lock and grab?"

    Weak: "lock waits its turn and grab doesn't." Half right, and it falls apart at the follow-up.

    Strong: "Both give a sequence exclusive use of the sequencer until unlock or ungrab, and both let the holder's children through, so children must be started with the holder as parent or they deadlock. lock puts its request at the back of the arbitration queue, grab at the front. The grant happens as soon as no other lock or grab blocks it, after the item already in the driver finishes. So the difference shows when several reservations compete: a grab is served before locks that asked earlier. Plain items queued earlier don't hold a lock back in the library code, even though the manual says a lock is arbitrated like any request. kill() releases a holder's locks; disable fork on a holder leaves the sequencer locked forever."

    Follow-ups: Two sequences grab while a third holds a lock: who goes first after the unlock? (the grab nearest the front, which is the most recent one, because each grab is pushed to the front), and How would you find a sequencer that is stuck locked? (print the sequencer: its lock_queue field lists the holders).

    Check yourself Sequence X holds a lock. Y calls grab(), then Z calls grab(). X unlocks. In what order do Y and Z get the sequencer?

    Z first, then Y. Each grab request is pushed to the front of the queue, so Z's request sits ahead of Y's, and the first unblocked lock or grab request in queue order is granted. The library code even carries a note that grabs are not arbitrated among themselves.

    Check yourself dma_irq_seq is changed to start an apb_rw_seq child for each access, with s.start(m_sequencer). What happens when the interrupt fires?

    The handler grabs the sequencer, then starts a child with no parent. The child is not a descendant of the grab holder, so its request is blocked by its own parent's grab, and the handler waits for the child forever. The fix is s.start(m_sequencer, this).

    Next: everything so far sends items of the agents' own types. Protocol Layering with Translator Sequences adds an upper layer: whole DMA jobs as items, turned into bus traffic by a sequence.

    Related topics

    • Inside the sequencer: arbitration and priority

    • Scenario patterns: kill and disable fork

    • Stalled sequences: hang triage

    Previous

    91 / 280

    Next: Protocol Layering with Translator Sequences

    Up next

    Up nextProtocol Layering with Translator Sequences

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • The Coordination Problem
    • Virtual Sequencer Wiring
    • Declaring p_sequencer