UVM sequence lock() and grab() give one sequence exclusive access to a sequencer, so that no other sequence can send items to the driver until it releases the sequencer with unlock() or ungrab(). A lock waits for its normal turn in arbitration, while a grab jumps to the front of the queue. A related pair of calls, kill() and stop_sequences(), end sequences early.

Exclusive access matters whenever several sequences share one driver. A register write burst that must not be interleaved, or an interrupt service routine that must run immediately, cannot simply be given a higher priority and hoped for the best.

8 min read | Intermediate

What You'll Learn

  • Use lock() and grab() to keep the driver for one sequence
  • Choose between a lock and a grab based on how urgently access is needed
  • Release exclusive access correctly and check who holds it
  • Stop a running sequence safely instead of killing it mid-item

Syntax

The calls exist on both the sequence and the sequencer. From inside a sequence, the short form uses the sequence's own sequencer when no argument is given:


// uvm_sequence_base (call from inside a sequence)
task          lock   (uvm_sequencer_base sequencer = null);   // blocks until granted
task          grab   (uvm_sequencer_base sequencer = null);   // blocks until granted
function void unlock (uvm_sequencer_base sequencer = null);
function void ungrab (uvm_sequencer_base sequencer = null);
function bit  is_blocked();
function bit  has_lock();
function void kill();

// uvm_sequencer_base (call on a sequencer, passing the sequence)
virtual task              lock   (uvm_sequence_base sequence_ptr);
virtual task              grab   (uvm_sequence_base sequence_ptr);
virtual function void     unlock (uvm_sequence_base sequence_ptr);
virtual function void     ungrab (uvm_sequence_base sequence_ptr);
virtual function bit      is_grabbed();
virtual function uvm_sequence_base current_grabber();
virtual function void     stop_sequences();

So inside a sequence, lock() and m_sequencer.lock(this) do the same thing. The argument form is what a virtual sequence uses to lock a sequencer that is not its own.

Lock vs Grab

Both calls are tasks that block until exclusive access is granted, and both are released the same way. The difference is when they are granted:

  • A lock request goes into the arbitration queue like any item request. It is granted after every request queued ahead of it has been served, and only when no other lock or grab is in the way.
  • A grab request is placed at the front of the queue and is arbitrated before anything else, regardless of priority. Only an existing lock or grab can delay it.

In practice, use a lock when you need an uninterrupted block of items but can wait your turn, such as a multi-beat configuration sequence. Use a grab when the sequence must take over immediately, such as responding to an interrupt.

unlock() and ungrab() are identical: each removes any locks and grabs the sequence holds on that sequencer. Pick the one that matches the call you made, for readability.

Example: An Atomic Register Burst

The testbench has a background traffic_seq that sends random items to one driver, plus a cfg_burst_seq that writes four consecutive registers. Those four writes must reach the driver back to back, so the burst sequence locks the sequencer around them. bus_item is a sequence item with random addr, data and write fields.


class traffic_seq extends uvm_sequence #(bus_item);
  `uvm_object_utils(traffic_seq)
  // Rest of the code (constructor)

  virtual task body();
    repeat (20) begin
      req = bus_item::type_id::create("req");
      start_item(req);
      if (!req.randomize()) `uvm_error("TRAFFIC", "Randomization failed")
      finish_item(req);
    end
  endtask
endclass

class cfg_burst_seq extends uvm_sequence #(bus_item);
  `uvm_object_utils(cfg_burst_seq)
  // Rest of the code (constructor)

  virtual task body();
    lock();                                   // wait for our turn, then keep the driver
    for (int i = 0; i < 4; i++) begin
      req = bus_item::type_id::create("req");
      start_item(req);
      if (!req.randomize() with { addr == 8'h10 + i; write == 1; })
        `uvm_error("CFG", "Randomization failed")
      finish_item(req);
    end
    unlock();                                 // let traffic_seq continue
  endtask
endclass

The test starts both on the same sequencer:


virtual task run_phase(uvm_phase phase);
  traffic_seq   traffic = traffic_seq::type_id::create("traffic");
  cfg_burst_seq cfg     = cfg_burst_seq::type_id::create("cfg");
  phase.raise_objection(this);
  fork
    traffic.start(m_env.m_agent.m_sequencer);
    begin
      #100ns;
      cfg.start(m_env.m_agent.m_sequencer);
    end
  join
  phase.drop_objection(this);
endtask

traffic_seq starts sending items immediately. At 100 ns cfg calls lock(). Any traffic item already waiting in the queue is served first. Then the lock is granted, the four register writes go out with nothing in between, and once unlock() runs, traffic_seq picks up where it left off. Without the lock, the default FIFO arbitration would alternate between the two sequences and split the burst.

Example: Taking Over for an Interrupt

An interrupt handler cannot wait behind queued traffic, so it grabs instead. Here irq_seq reads a status register and clears it, with nothing allowed in between:


class irq_seq extends uvm_sequence #(bus_item);
  `uvm_object_utils(irq_seq)
  // Rest of the code (constructor)

  virtual task body();
    grab();                                   // next slot is ours, whatever is queued
    req = bus_item::type_id::create("req");
    start_item(req);
    if (!req.randomize() with { addr == 8'hF0; write == 0; }) `uvm_error("IRQ", "Randomization failed")
    finish_item(req);                         // read the status register

    req = bus_item::type_id::create("req");
    start_item(req);
    if (!req.randomize() with { addr == 8'hF0; write == 1; data == 0; }) `uvm_error("IRQ", "Randomization failed")
    finish_item(req);                         // clear it
    ungrab();
  endtask
endclass

A grab cannot interrupt an item the driver is already working on. It takes the next arbitration slot. If another sequence already holds a lock, the grab waits until that lock is released, so a lock and a grab never overlap.

A sequence that holds a lock can still start child sequences on the same sequencer, and those children are allowed through. If a child takes its own lock, the parent itself is blocked until the child unlocks. Keep locks in the leaf sequences that actually send items, rather than in a high-level sequence that starts many others.

Quick Check

Q: cfg_burst_seq holds a lock and is halfway through its four writes when irq_seq calls grab(). What happens?

Show Answer

The grab waits. A grab only jumps ahead of ordinary requests; an existing lock blocks it like everything else. The two remaining writes finish, unlock() runs, and then the grab is granted before any queued traffic item.

Checking Who Has the Sequencer

Four query functions help when debugging contention or writing sequences that adapt to it:

  • is_blocked() on a sequence returns 1 if another sequence's lock or grab currently prevents it from running. A 0 is not a promise: another sequence can still lock the sequencer before this one gets its next item through.
  • has_lock() on a sequence returns 1 if it currently holds a lock.
  • is_grabbed() on the sequencer returns 1 if any sequence holds a lock or grab.
  • current_grabber() on the sequencer returns the sequence that holds it, which is handy in a message: current_grabber().get_full_name().

Stopping a Sequence

Sometimes a test wants to end a sequence before its body() finishes, for example to stop background traffic when the interesting part of the test is over. UVM offers two forceful ways to do it:

  • seq.kill() ends one sequence. Its state becomes UVM_STOPPED, its pending requests and locks on its default sequencer are removed, and its post_body() and post_start() are not called.
  • sequencer.stop_sequences() kills every sequence running on that sequencer and clears all queued requests, locks and responses, returning it to idle.

Both call the sequence's do_kill() hook, which you can override to clean up state such as a flag or an objection.

The danger is timing. Neither call checks whether the driver is in the middle of an item. If the driver later calls item_done() for an item whose sequence has been killed, the result can be a lost response or a fatal error from the sequencer. For that reason, killing should be a last resort.

A cleaner way: ask the sequence to stop

The safer pattern is to let the sequence finish its current item and exit by itself. Give it a stop request flag and check it between items:


class traffic_seq extends uvm_sequence #(bus_item);
  `uvm_object_utils(traffic_seq)
  bit stop_req;                               // set by the test to end the loop
  // Rest of the code (constructor)

  virtual task body();
    while (!stop_req) begin
      req = bus_item::type_id::create("req");
      start_item(req);
      if (!req.randomize()) `uvm_error("TRAFFIC", "Randomization failed")
      finish_item(req);
    end
  endtask
endclass

// In the test
fork
  traffic.start(m_env.m_agent.m_sequencer);
join_none
// ... the main scenario runs here ...
traffic.stop_req = 1;
traffic.wait_for_sequence_state(UVM_FINISHED);   // returns once post_start() is done

Because the flag is only checked between items, every item that was started is also finished, and the driver is never left holding an orphaned item. wait_for_sequence_state() lets the test continue only after the sequence has completely ended.

Common Mistakes

Mistake #1: Forgetting to unlock

A sequence that ends, or returns early through an error path, without calling unlock() or ungrab() can leave the sequencer locked, and every other sequence on it stalls. Make the release the last statement of body() and check every early return.

Mistake #2: Using priority when exclusive access is needed

A high priority only wins individual arbitration rounds, and only with a priority-aware arbitration mode. Another sequence can still win a round in between your items. If items must be back to back, lock the sequencer.

Mistake #3: Locking in a high-level sequence

A lock taken by a parent covers its children, and a lock taken by a child blocks its parent. Placing locks high in the hierarchy makes these interactions hard to follow. Lock only around the items that must not be interleaved.

Mistake #4: Killing a sequence while the driver is busy

kill() and stop_sequences() do not wait for the driver. Prefer a stop flag checked between items, and use the forceful calls only when you know the driver is idle.

Summary

In this article, you learned:

  • lock() waits its turn in arbitration, then gives one sequence exclusive use of the sequencer
  • grab() goes to the front of the queue and wins the next slot, unless another lock or grab is already held
  • unlock() and ungrab() release either kind, and must always be called
  • is_blocked(), has_lock(), is_grabbed() and current_grabber() report who holds the sequencer
  • kill() and stop_sequences() end sequences forcibly; a stop flag is the safer way to end background traffic
Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5