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.
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.
|
| |
|---|---|---|
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 |
|
|
Who may still send while it is held | The holder and its children (started with it as parent) | The same |
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.
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.
// 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
endclassgrab() 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:
// 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
endtaskScene 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.)
[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.
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.
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:
// 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 reachedUVM_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 issueNo 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().
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:
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 doneThe 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.
Situation | Use | Why |
|---|---|---|
Interrupt or error handler that must run next |
| Goes ahead of any lock queued before it |
Read-modify-write, multi-register programming |
| 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 | Children pass; strangers deadlock |
Stop a lock holder early |
| Releases its locks and queued requests |
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().
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.
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.