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()andgrab()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 becomesUVM_STOPPED, its pending requests and locks on its default sequencer are removed, and itspost_body()andpost_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 sequencergrab()goes to the front of the queue and wins the next slot, unless another lock or grab is already heldunlock()andungrab()release either kind, and must always be calledis_blocked(),has_lock(),is_grabbed()andcurrent_grabber()report who holds the sequencerkill()andstop_sequences()end sequences forcibly; a stop flag is the safer way to end background traffic