The interrupt-service sequence on the DMA bench is started with priority 500; the polling sequence runs at the default of 100. Yet the log shows the interrupt handler's STATUS read waiting behind two more COUNT polls. Someone files a bug against UVM. It is not a bug: the sequencer was doing exactly what its default arbitration mode says.
This lesson opens the sequencer. You will see what it stores, when it decides, what priority means in each of the six arbitration modes, and how to write your own rule.
A deli counter hands out numbered tickets and serves them in order. That is the sequencer's default, FIFO arbitration: whoever asked first is served first, and the ticket's colour (the priority) is ignored. Other modes change the rule: serve the most important customer first (strict), or draw tickets from a hat with more tickets for important customers (weighted).
Two facts make the analogy exact. First, the counter only calls a number when the server is free: the sequencer arbitrates when the driver asks for an item, never when a sequence asks for a grant. Second, a customer who is not ready can be skipped without losing their place: that is is_relevant.
start_item(item, priority) calls the sequencer's wait_for_grant, which puts a request into the arbitration queue and blocks. When the driver calls get_next_item, the sequencer chooses one request and grants it, and start_item returns. finish_item then calls send_request, which puts the item into the sequencer's request FIFO where get_next_item picks it up, and wait_for_item_done, which blocks until the driver's item_done. A sequence can call those three methods directly, and the uvm_do macros once did, but start_item / finish_item is the readable form.
A request's priority comes from start_item(item, priority), or else from the sequence's own priority set by start(sqr, parent, priority) or set_priority(). The default is 100, and -1 means "inherit". The mode is set on the sequencer with set_arbitration. Call it once the sequencer exists, in connect_phase or later, or from a sequence through m_sequencer.set_arbitration(...). In an env's build_phase the agent has not built its sequencer yet, so the handle is still null.
// snippet: seq_lab_tb.sv, three sequences race for one sequencer
function void end_of_elaboration_phase(uvm_phase phase);
env.sqr.set_arbitration(mode); // UVM_SEQ_ARB_FIFO, _STRICT_FIFO, ...
endfunction
task run_phase(uvm_phase phase);
lab_burst_seq a = lab_burst_seq::type_id::create("A");
lab_burst_seq b = lab_burst_seq::type_id::create("B");
lab_burst_seq c = lab_burst_seq::type_id::create("C");
phase.raise_objection(this);
fork // four items each
a.start(env.sqr, null, 100);
b.start(env.sqr, null, 200);
c.start(env.sqr, null, 300);
join
`uvm_info("ARB", $sformatf("%s grant order: %s", mode.name(), env.drv.grants), UVM_LOW)
phase.drop_objection(this);
endtaskMode | Rule | Priority used? |
|---|---|---|
| First come, first served | No |
| Random, weighted by priority | Yes, as odds: nobody starves |
| Random | No |
| Highest priority first, FIFO among equals | Yes, absolutely: low priority can starve |
| Highest priority first, random among equals | Yes, absolutely: low priority can starve |
| Your | If your function uses it |
UVM_SEQ_ARB_USER calls the virtual function user_priority_arbitration(int avail_sequences[$]) on the sequencer. It receives the indexes of the requests that passed the lock and relevance checks, and must return one of them. The default returns the first, which is FIFO. This one does the opposite of STRICT_FIFO, picking the lowest priority, to show the mechanism:
// snippet: seq_lab_tb.sv, installed with a factory type override of the sequencer
class lab_user_sqr extends lab_sqr; // lab_sqr is uvm_sequencer #(lab_item)
`uvm_component_utils(lab_user_sqr)
function new(string name, uvm_component parent); super.new(name, parent); endfunction
// Lowest priority first (the opposite of STRICT_FIFO), oldest among equals.
function int user_priority_arbitration(int avail_sequences[$]);
int best = avail_sequences[0];
foreach (avail_sequences[i])
if (arb_sequence_q[avail_sequences[i]].item_priority < arb_sequence_q[best].item_priority)
best = avail_sequences[i];
return best;
endfunction
endclassarb_sequence_q and its item_priority field are library internals rather than documented API, though both UVM 1.2 and the IEEE 1800.2 reference library have them. A real rule usually asks the sequence itself, through arb_sequence_q[i].sequence_ptr, for something like a deadline or a credit count.
A sequence can stay in the queue but decline to be chosen by overriding is_relevant(). If every waiting sequence is irrelevant, the sequencer calls their wait_for_relevant() tasks and arbitrates again when one returns, so a sequence that overrides one must override both (otherwise UVM stops with a RELMSM fatal). A configuration sequence that must not write while a job is running might look like this:
// snippet
class dma_cfg_seq extends uvm_sequence #(apb_item);
`uvm_object_utils(dma_cfg_seq)
bit dma_busy; // kept up to date by the virtual sequence from STATUS reads
function new(string name = "dma_cfg_seq"); super.new(name); endfunction
function bit is_relevant(); // skip me while the DMA is busy
return !dma_busy;
endfunction
task wait_for_relevant(); // tell the sequencer when to try again
wait (!dma_busy);
endtask
// body() writes KEY and LEN as usual
endclassLocks and grabs act one step earlier, removing requests from the choice entirely. How lock() and grab() differ, and how children of the lock holder still get through, is the subject of Lock vs grab. For debugging, is_grabbed(), current_grabber(), has_do_available() and get_arbitration() tell you the sequencer's state at any moment.
The lab test above, run once per mode. Each letter is one item granted, in order:
UVM_INFO seq_lab_tb.sv(386) @ 255000: uvm_test_top [ARB] UVM_SEQ_ARB_FIFO grant order: ABCABCABCABC
UVM_INFO seq_lab_tb.sv(386) @ 255000: uvm_test_top [ARB] UVM_SEQ_ARB_STRICT_FIFO grant order: CCCCBBBBAAAA
UVM_INFO seq_lab_tb.sv(386) @ 255000: uvm_test_top [ARB] UVM_SEQ_ARB_WEIGHTED grant order: BCAACBBACCBA
UVM_INFO seq_lab_tb.sv(386) @ 255000: uvm_test_top [ARB] UVM_SEQ_ARB_USER grant order: AAAABBBBCCCCFIFO interleaves perfectly. Priorities 100, 200 and 300 make no difference: after each grant, the sequence that just finished re-queues behind the other two.
STRICT_FIFO serves C to completion, then B, then A. If C never ran out of items, A and B would never be served.
WEIGHTED is a random mix that favours C over the long run; with only twelve grants and this seed it happens to look even. The order changes with the seed.
USER with the lowest-first rule is STRICT_FIFO in reverse, which proves the function is in charge.
A team added a background sequence that injected random APB errors and gave it priority 300 so that its accesses would land promptly. They also switched the sequencer to UVM_SEQ_ARB_STRICT_FIFO, because priority "did nothing" in the default mode. The injector looped with no delay between items. From that moment every lower-priority request, including the test's register writes, waited forever: each time the driver asked, the injector already had a request in the queue, and strict arbitration always picked it. The test hung and ended in PH_TIMEOUT.
Two fixes were possible, and they used both. The injector waited a random number of clocks between items, so other requests had moments when it was absent. And the sequencer went back to WEIGHTED, which favours high priority without ever reducing anyone else's odds to zero. The general rule: strict modes are for short, bounded bursts of urgent traffic, such as an interrupt handler, not for anything that loops.
Whichever requested first. UVM_SEQ_ARB_FIFO ignores priority. To make priority count, switch to WEIGHTED (a strong bias) or STRICT_FIFO (always first), or have the handler take the sequencer with grab(), which is what the bench's dma_irq_seq does.
apb.sqr.set_arbitration(UVM_SEQ_ARB_STRICT_FIFO) at the end of its build_phase, right after creating the agent. What happens?A null-handle error. The env's build_phase creates the agent, but the agent builds its own sequencer in its own build_phase, which runs after the env's returns. Move the call to connect_phase or end_of_elaboration_phase.
is_relevant() but not wait_for_relevant(). When does it fail?Only when the sequencer finds that every waiting sequence is irrelevant and calls wait_for_relevant() to find out when to try again. The default implementation detects that is_relevant was overridden alone and stops with a RELMSM fatal. Until that moment the bug is invisible.
Next: the grant gets the item to the driver. Responses and pipelined drivers is about the way back: how a driver that answers later, or out of order, returns results to the right sequence.