Arbitration and priority
Predict which sequence the sequencer grants next under each of the six arbitration modes, and know where a priority comes from and when it is read.
- Lesson
- U5.1
- Time
- 30 min
- Runs on
- Verilator with UVM
- Check
- 4 questions
A test starts its control sequence with priority 500 so that its register writes go out before the bulk traffic. The log shows the writes interleaved one for one with the data items, as if the priority were not there. It was not read: the sequencer was still in its default mode, FIFO, which never looks at a priority.
| Mode | Three sequences, priorities 100, 200, 300 | What decided |
|---|---|---|
| FIFO (default) | A0 B0 C0 A1 B1 C1 A2 B2 C2 | Order of the requests; priorities ignored |
| STRICT_FIFO | C0 C1 C2 B0 B1 B2 A0 A1 A2 | Highest priority first; C starves the others |
| STRICT_FIFO, A's first item at 500 | A0 C0 C1 C2 B0 B1 B2 A1 A2 | An item priority beats the sequence's own |
Three grant orders measured on one bench. Only the mode changed between the first two rows.
Where the choice is made
In U4.1 a sequence asked for a grant with start_item and got one when the driver asked for an item. With one sequence there was nothing to decide. With several, every start_item puts a request into the sequencer's arbitration queue, and the sequencer chooses among the queued requests each time the driver calls get_next_item.
A request is a candidate only if three things hold. It is a plain item request, so lock requests are handled first and separately. Its sequence is not blocked by another sequence's lock. And the sequence's is_relevant() returns 1, which it does unless you override it. U5.2 covers locks and relevance; this lesson assumes every request is a candidate.
When only one request is a candidate, every mode grants it at once without reading a priority. The mode matters only when two or more sequences are waiting at the same moment. That is why a priority bug hides in tests where sequences rarely overlap, and shows up when traffic gets dense.
The driver drives the choice. Nothing is granted while the driver is busy with an item, so a sequence that calls start_item during another item waits until that item's item_done. In the example, every scenario starts three sequences in the same time step, so they compete from the first grant.
The six modes, measured
The example runs the same three sequences, A, B and C with three items each, under every mode. The driver takes one item per clock and appends the item's sequence name and index to a grant log, which the test prints as one line per scenario.
| Mode | Reads priority | Rule in m_choose_next_request | Measured, priorities 100 200 300 |
|---|---|---|---|
UVM_SEQ_ARB_FIFO | No | The oldest candidate request wins | A0 B0 C0 A1 B1 C1 A2 B2 C2 |
UVM_SEQ_ARB_WEIGHTED | Yes | Random draw; each candidate's chance is its priority over the sum | C0 B0 C1 B1 A0 C2 B2 A1 A2 in one run |
UVM_SEQ_ARB_RANDOM | No | Uniform random draw among the candidates | A0 A1 C0 A2 C1 B0 B1 B2 C2 in one run |
UVM_SEQ_ARB_STRICT_FIFO | Yes | Only the highest-priority candidates count; the oldest of them wins | C0 C1 C2 B0 B1 B2 A0 A1 A2 |
UVM_SEQ_ARB_STRICT_RANDOM | Yes | Only the highest-priority candidates count; a random one of them wins | C0 C1 C2 B0 B1 B2 A0 A1 A2 |
UVM_SEQ_ARB_USER | Your choice | Your user_priority_arbitration picks one candidate | B0 B1 B2 A0 C0 A1 C1 A2 C2 (favours B) |
The modes of uvm_sequencer_arb_mode, set with sqr.set_arbitration(mode). The two random modes change their order from seed to seed.
Read the FIFO line first. The order is a round robin, and here is why. After A0 is done, A asks again, and its new request goes to the back of the queue, behind B0 and C0. Every sequence re-requests after each item, so the three take turns. Giving A, B and C different priorities changes nothing, as the second scenario of the example shows.
Now the STRICT_FIFO line. C has the highest priority, so C0 wins. When C0 is done, C's next request is already in the queue, because the sequencer lets the sequences run for a delta before it arbitrates again. C1 wins, then C2. B gets the driver only when C has nothing left, and A only after B. A sequence that keeps asking starves everyone below it.
STRICT_RANDOM gives the same order here, because at every arbitration only one candidate had the highest priority. It differs from STRICT_FIFO only when two sequences share the top priority: then it picks one of them at random where STRICT_FIFO takes the older request.
WEIGHTED shares the driver in proportion. Each candidate's chance is its priority divided by the sum of the candidates' priorities, so with 100, 200 and 300 the chances are one sixth, one third and one half. Proportion is all it promises. Every draw is fresh, with no memory of who has waited, so a low weight can lose many draws in a row. A priority of 0 is legal but never wins while a positive one competes. In the measured run C0 won the first draw and B0 the second. RANDOM ignores priorities and draws uniformly. Both orders come from the simulator's random seed: repeated runs here agree, because the seed is fixed, but another seed or another simulator gives another order.
Where a priority comes from
A sequence gets its priority from the third argument of start(sequencer, parent, priority). The default, -1, means inherit: 100 for a sequence with no parent, and the parent's priority for a child. Anything below -1 is a fatal error. You can also change it at any time with set_priority(value); the new value is used at the next arbitration.
An item can carry its own priority. start_item(it, priority) and the PRIORITY argument of uvm_do attach it to that one request. When the arbiter compares requests, an item priority other than -1 wins over the sequence's priority, and it must be greater than 0. Higher numbers win.
The fourth scenario shows the two together under STRICT_FIFO. A keeps priority 100, the lowest, but gives its first item priority 500. That item, A0, is granted first. A's later items fall back to 100, so A1 and A2 go last: A0 C0 C1 C2 B0 B1 B2 A1 A2.
Each arbitration takes the highest priority among the waiting requests. A's first request carried 500; its later ones carry the sequence priority.
Priority set, mode left alone
fork
ctrl.start(sqr, this, 500); // sqr is still in FIFO
bulk.start(sqr, this, 100);
join
FIFO never reads the 500. The two sequences alternate item by item.
Priority set, and a mode that reads it
sqr.set_arbitration(UVM_SEQ_ARB_STRICT_FIFO);
fork
ctrl.start(sqr, this, 500);
bulk.start(sqr, this, 100);
join
Every control item waiting at an arbitration goes before any bulk item.
Only three modes read priorities: WEIGHTED, STRICT_FIFO and STRICT_RANDOM. If a test sets priorities, it must also set one of these modes, and the natural place is the sequence that starts the competing sequences, through m_sequencer.set_arbitration(...).
Writing your own rule
The sixth mode hands the decision to you. Extend the sequencer and override user_priority_arbitration(int avail_sequences[$]). The argument lists the positions, in the arbitration queue, of every request that is a candidate right now. Return one of them. The default implementation returns the first, which makes USER behave like FIFO; returning a position that is not in the list is a fatal error.
function int user_priority_arbitration(int avail_sequences[$]);
foreach (avail_sequences[i])
if (arb_sequence_q[avail_sequences[i]].sequence_ptr.get_name() == favourite)
return avail_sequences[i];
return avail_sequences[0];
endfunctionThe queue, arb_sequence_q, is a protected member of uvm_sequencer_base, so a subclass may read it. Each entry has a sequence_ptr to the requesting sequence and an item_priority. With the favourite set to B, the measured order is B0 B1 B2 A0 C0 A1 C1 A2 C2: B takes every slot while it has a request, and the rest is FIFO.
A user rule is the right tool when the bus has a documented arbitration scheme that the test must mimic. A fixed slot order or a credit scheme per requester are examples. For anything simpler, one of the built-in modes is easier to explain in a review.
Choosing a mode
FIFO is fair, repeatable and the default; most agents never need anything else. STRICT_FIFO is the choice when one path must win whenever it asks, such as a control sequence that reconfigures the design between bursts. There the starvation it causes is the point. WEIGHTED produces a traffic mix with a chosen ratio. The price is an order that depends on the seed, and no bound on how long one requester may wait. When a sequence needs bounded service, use FIFO, or a user rule that ages or caps requests.
Two cautions. The mode is a property of the sequencer, so sequences started on it from anywhere see the same rule. Set it in one place, usually the virtual sequence or the test, and log it. And a starved sequence looks exactly like a hung one in the log. When a sequence seems stuck under a strict mode, print the sequencer with sqr.print() to see the queue before suspecting the driver.
Try it
Each line is one scenario: the mode, the three priorities, and the grant order the driver saw. The WEIGHTED and RANDOM lines are drawn from the simulator's random stream and depend on its seed; the other six are fixed by the rules above.
Change favourite to "C" and predict the USER line before you run. Then give B's first item priority 500 as well as A's in the fourth scenario: both have the top priority, so STRICT_FIFO falls back to request order between them.
FIFO pri 100 100 100 -> A0 B0 C0 A1 B1 C1 A2 B2 C2 FIFO pri 100 200 300 -> A0 B0 C0 A1 B1 C1 A2 B2 C2 STRICT_FIFO pri 100 200 300 -> C0 C1 C2 B0 B1 B2 A0 A1 A2 STRICT_FIFO A item 0 at 500 -> A0 C0 C1 C2 B0 B1 B2 A1 A2 STRICT_RANDOM pri 100 200 300 -> C0 C1 C2 B0 B1 B2 A0 A1 A2 USER favours B -> B0 B1 B2 A0 C0 A1 C1 A2 C2 UVM_ERROR : 0Check yourself
Remember
- The sequencer arbitrates only when the driver asks for an item, and only among requests that are candidates at that moment.
- FIFO, the default, and RANDOM never read priorities; WEIGHTED, STRICT_FIFO and STRICT_RANDOM do.
- A sequence priority comes from
start(sqr, parent, priority): -1 inherits, 100 at the root. An item priority fromstart_item(it, priority)overrides it for that request. - Strict modes starve lower priorities while a higher one keeps asking; WEIGHTED shares in proportion, with no bound on waiting.
- UVM_SEQ_ARB_USER calls your
user_priority_arbitration(avail_sequences), which must return one of the listed queue positions.
Practice
- Catch the sequencers that grant out of turn Verify · Medium