AMBA AXI · Module 12
Arbiter & Arbitration
How an AXI interconnect resolves contention at a shared subordinate — round-robin, fixed-priority, and QoS-weighted arbitration schemes, the fairness-vs-priority trade-off, the arbiter state machine, and starvation avoidance.
When multiple managers target the same subordinate (the per-subordinate contention of Chapter 12.2), the interconnect must decide who goes first — that's the arbiter's job. The arbitration scheme determines the system's fairness (does any manager starve?), throughput (is the port kept busy?), and priority (do latency-critical masters get precedence?). This chapter covers the main schemes — round-robin, fixed-priority, and QoS-weighted — the fairness-vs-priority trade-off, the arbiter as a state machine, and how to avoid starvation.
1. The Arbiter's Job
At each shared subordinate port, an arbiter sees the set of managers currently requesting access and grants one per cycle (or per transaction). The granted manager's transaction proceeds; the others wait and are reconsidered next round. Arbitration happens on the address channels (AW and AR contention are arbitrated, each independently — read and write are separate), and similarly on response paths where they contend.
A good arbiter has three properties: no starvation (every requester eventually wins), high throughput (the port isn't idle while requests are pending — grant someone every cycle there's a request), and policy (it can favor higher-priority/QoS masters when desired). The scheme chosen trades these off.
2. The Arbitration Schemes
The common schemes:
- Round-robin (RR): rotate priority among requesters so each gets a fair turn — after granting a manager, it goes to the back of the line. No starvation, fair bandwidth distribution. The common default for general traffic.
- Fixed-priority: managers have static priorities; the highest-priority requesting manager always wins. Simple and low-latency for the top priority, but lower-priority masters can starve if a high-priority one is always requesting.
- QoS-weighted: use
AxQOS(Chapter 6.5) to weight arbitration — higher-QoS transactions get more grants/precedence. Programmable priority that lets software/system tune which masters get bandwidth under contention. - Weighted round-robin / least-recently-granted: hybrids that give some masters proportionally more turns while still bounding everyone's wait — balancing fairness and priority.
The choice depends on the traffic: RR for fairness among equals, fixed-priority where one master is genuinely most critical (and starvation of others is acceptable or impossible), QoS-weighted for tunable, mixed-criticality systems.
3. The Arbiter State Machine
Conceptually, an arbiter cycles through request → grant → advance:
The rotating pointer is what gives round-robin its fairness: after a manager wins, the pointer moves past it, so it won't win again until everyone ahead of it in the rotation has had a turn — bounding each manager's worst-case wait. Fixed-priority lacks this (the pointer never moves off the top priority), which is why it can starve. The arbiter also typically grants per address transaction (once granted, the manager's burst proceeds on the granted path), then re-arbitrates for the next.
4. Fairness vs Priority — The Core Trade
Arbitration is fundamentally a fairness-vs-priority trade-off:
- Fairness (round-robin) treats requesters equally — no one starves, bandwidth is shared evenly. Best when masters are peers and predictable latency for all matters.
- Priority (fixed-priority) favors the most critical master — lowest latency for it — at the cost of potentially starving others. Best when one master genuinely must come first (e.g., a real-time display controller that must never underflow) and starving others is acceptable.
- QoS-weighted is the tunable middle: it lets the system grant proportionally more bandwidth to higher-QoS masters without fully starving lower ones — combining priority's responsiveness with fairness's safety, programmable per workload.
Real interconnects often use QoS-weighted or weighted round-robin precisely because pure fairness ignores criticality and pure priority risks starvation. The arbitration policy is a key performance knob: it determines, under contention, which master gets the bandwidth and which waits — directly shaping system latency and throughput.
4b. The Arbiter, Written Out
Figure 3 is drawn as a three-state machine because that is how the policy reads. The implementation is not a state machine at all: the grant is combinational from the current requests and a rotation pointer, and the only sequential element is the pointer itself. Writing it out makes the fairness argument concrete, and it makes the starvation bug in the Debug Lab below visible.
module rr_arbiter #(
parameter int N = 4
) (
input logic clk,
input logic rst_n,
input logic [N-1:0] req,
// The granted requester must still be accepted downstream. In an AXI
// interconnect this is the AW/AR channel handshake: the arbiter has issued
// a grant, but nothing has happened until READY is also high.
input logic ready,
output logic [N-1:0] grant
);
// `mask` holds the requesters that come AFTER the last winner in rotation
// order. Priority within a mask is simply lowest-index-first.
logic [N-1:0] mask;
// Isolate the lowest set bit: v & (-v). This is the whole priority encoder.
function automatic logic [N-1:0] lowest_set (input logic [N-1:0] v);
return v & (~v + 1'b1);
endfunction
wire [N-1:0] masked_req = req & mask;
// Try the requesters after the last winner first; if none of them are
// asking, wrap around and serve the lowest-index requester overall. That
// wrap is the "round" in round-robin.
assign grant = (|masked_req) ? lowest_set(masked_req)
: lowest_set(req);
wire granted_now = (|grant) && ready;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
mask <= '1; // first round: everyone eligible
end else if (granted_now) begin
// Set every bit strictly above the winner. `grant | (grant-1)` fills
// bits 0..winner, so the inverse is bits winner+1..N-1.
mask <= ~(grant | (grant - 1'b1));
// When the winner was the top index this evaluates to all-zero, so the
// next cycle finds masked_req empty and wraps to lowest_set(req).
// The wrap is not a special case in the code because it does not need
// to be one.
end
// NOTE the absence of an else. The pointer does NOT move when a grant was
// issued but not accepted. That omission is the entire subject of the
// Debug Lab below.
end
endmoduleFor contrast, fixed priority is the same module with the pointer deleted:
module fp_arbiter #(parameter int N = 4) (
input logic [N-1:0] req,
output logic [N-1:0] grant
);
// No state, no rotation: requester 0 wins whenever it asks.
assign grant = req & (~req + 1'b1);
endmodulePutting them side by side makes the trade-off from Section 4 mechanical rather
than rhetorical. Round-robin costs N flops and one extra mux layer, and in
exchange the pointer guarantees that a requester cannot be passed over twice in
a row while others are served. Fixed priority costs nothing and offers no such
guarantee — requester 0's behaviour alone determines whether requester 3 ever
runs.
Proving the fairness claim
"No starvation" is the property worth checking, and it is the one a directed test cannot establish. These bind onto the arbiter:
module rr_arbiter_sva #(parameter int N = 4, parameter int MAX_WAIT = 64) (
input logic clk, rst_n,
input logic [N-1:0] req, grant,
input logic ready
);
// Safety: at most one grant, and never to a requester that is not asking.
a_onehot: assert property (@(posedge clk) disable iff (!rst_n)
$onehot0(grant));
a_grant_implies_req: assert property (@(posedge clk) disable iff (!rst_n)
(grant & ~req) == '0);
// Progress: if anyone is asking and the downstream is ready, somebody must
// be granted. An arbiter that stalls with requests pending is broken even
// if it never grants incorrectly.
a_no_idle_stall: assert property (@(posedge clk) disable iff (!rst_n)
(|req && ready) |-> |grant);
// Liveness, made checkable by bounding it. True liveness ("eventually
// granted") is unfalsifiable in a finite simulation, so we assert the
// engineering requirement instead: a continuously-asserted request is
// served within MAX_WAIT cycles. Size MAX_WAIT from the worst legal case -
// N-1 other requesters times the longest transfer each can occupy.
for (genvar i = 0; i < N; i++) begin : g_starve
int unsigned waiting;
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) waiting <= 0;
else if (grant[i] && ready) waiting <= 0;
else if (req[i]) waiting <= waiting + 1;
else waiting <= 0;
a_bounded_wait: assert property (@(posedge clk) disable iff (!rst_n)
waiting < MAX_WAIT)
else $error("requester %0d waited %0d cycles - starvation", i, waiting);
end
// Coverage that makes a pass mean something: every requester actually won
// at least once, and the all-requesting case was actually exercised.
for (genvar i = 0; i < N; i++) begin : g_cov
c_won: cover property (@(posedge clk) disable iff (!rst_n) grant[i] && ready);
end
c_full_contention: cover property (@(posedge clk) disable iff (!rst_n) &req);
endmoduleThe counter-based bounded-wait property is the one that earns its place. The
$onehot0 and grant-implies-request checks catch a broken encoder, but a
perfectly well-formed arbiter can still starve a requester forever, and only the
counter notices. Note also c_full_contention: without it, a clean run cannot
distinguish "fair under load" from "never had two simultaneous requesters".
A round-robin arbiter starved a master, and the fairness logic was the cause
ROTATE-ON-GRANT-NOT-ACCEPTA four-master AXI interconnect showed one master achieving roughly a third of the bandwidth the other three got, despite a round-robin arbiter that had been reviewed and was believed fair. The disparity only appeared when the shared slave applied back-pressure — under light load all four masters were served evenly, and the performance model that had signed off the design assumed exactly that light-load behaviour. On a benchmark that saturated the slave, the affected master's worst-case latency was unbounded in the sense that mattered: it kept growing with run length rather than settling.
// The pointer advanced whenever a grant was ISSUED, regardless of whether
// the downstream accepted it.
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) mask <= '1;
else if (|grant) mask <= ~(grant | (grant - 1'b1)); // <-- no `ready`
endTwo measurements localised it. A histogram of accepted grants per master showed
the expected 25% each under no back-pressure and a clear skew as soon as ready
began deasserting — so the bug was conditional on the handshake, not on the
request pattern. Then a waveform of a stalled window showed the decisive detail:
master 2 was granted for three consecutive cycles while ready was low, and on
the fourth cycle, still without any transfer having occurred, the grant moved to
master 3.
That is the whole bug in one screenful. A grant that is never accepted is not a turn. The arbiter had counted it as one.
The rotation pointer advanced on |grant rather than on |grant && ready. In
AXI terms, the arbiter treated issuing AWVALID as completing the transaction,
when the transaction only occurs on the cycle where AWVALID and AWREADY are
both high.
The consequence is not merely a lost turn, it is a systematically unfair one. When
the slave stalls, the arbiter rotates through all four masters while nothing is
transferring. The master that happens to hold the grant on the cycle ready
finally rises is the one that transfers — and which master that is depends on the
phase relationship between the rotation and the stall pattern. A master whose
grant window repeatedly coincided with the low part of a periodic ready was
passed over every round. The fairness mechanism, running open-loop against a
handshake it was ignoring, became the mechanism of unfairness.
This is why the property in the previous section counts cycles a request has been waiting rather than checking grant distribution. A distribution check on issued grants looks perfectly fair here — all four masters get grants at exactly the same rate. Only accepted grants tell the truth.
// Advance the pointer only when a transfer actually occurred.
wire granted_now = (|grant) && ready;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) mask <= '1;
else if (granted_now) mask <= ~(grant | (grant - 1'b1));
endVerifying the fix requires back-pressure in the stimulus, which is the part the
original verification plan was missing. The test that fails on the old RTL and
passes on the new one drives all four masters continuously and toggles the slave's
ready with a duty cycle deliberately chosen to beat against the rotation
period — a pattern random ready stalling will find eventually, and a fixed
"stall every other cycle" pattern may never find at all.
The bounded-wait assertion above is the durable form of the check, because it
holds under any ready pattern rather than the one the test happened to pick. Bind
it and the failure is caught by any test that applies sustained contention, not
only by the test written to look for it.
The generalisation is worth carrying to other arbiters: any arbiter feeding a handshake must advance its state on the handshake, not on its own output. The same defect appears in credit-return logic, in skid-buffer pointer updates, and in scheduler round-robins — anywhere the thing being counted is an intention rather than an event. See VALID/READY handshake for the rule this rests on and handshake deadlock rules for the related failure where the dependency runs the other way.
5. Common Misconceptions
6. Debugging Insight
7. Verification Insight
8. Interview Questions
8b. Where This Is Specified
- Arm AMBA AXI Protocol Specification (ARM IHI 0022). The
VALID/READYhandshake rule that a transfer occurs only when both are asserted, which is the rule the arbiter's pointer update depends on, and the independence of theAW,AR,W,RandBchannels that makes per-channel arbitration necessary. - Arm AMBA AXI,
AxQOSsignalling. The four-bit quality-of-service identifier an interconnect may use to weight arbitration. The specification defines the signal and its transport; the arbitration policy that consumes it is implementation-defined. - IEEE 1800-2023 §16 — Assertions. The concurrent assertion and
cover propertyconstructs used for the safety and bounded-wait checks, and$onehot0. - IEEE 1800-2023 §27 — Generate constructs. The
genvarloop that replicates the per-requester starvation counters.
Round-robin masking, weighted round-robin and least-recently-granted are standard design practice rather than standardised behaviour — AMBA constrains the handshake an arbiter must honour, not the policy it implements.
9. Summary
The arbiter resolves contention at a shared subordinate port (per-subordinate, per-channel), granting one of the competing managers per round. The main schemes: round-robin (rotate turns — fair, no starvation, the default for peers), fixed-priority (top requester always wins — simple and low-latency for it, but can starve others), QoS-weighted (use AxQOS to weight grants — programmable, proportional priority without full starvation), and weighted-RR/LRG hybrids. The rotating pointer is what makes round-robin starvation-free; fixed-priority's static pointer is why it can starve. A good arbiter is starvation-free, work-conserving (no idle cycles with pending requests), and policy-honoring.
The core trade-off is fairness vs priority: round-robin maximizes fairness, fixed-priority maximizes responsiveness for one master (risking starvation), and QoS-weighting is the tunable middle for mixed-criticality systems — which is why real interconnects favor QoS-weighted/weighted-RR. Arbitration is a key performance knob, and its bugs (starvation, unfairness, ignored QoS, priority inversion, non-work-conserving) are performance/fairness failures that functional tests miss — so verify the fairness/weighting behavior (bounded waits, grant distribution, utilization) under sustained contention, not just transaction correctness. Next: mux/demux and ID-based routing — how responses find their way back through the interconnect.
10. What Comes Next
You've got contention resolution; next, routing responses back:
- 12.5 — Mux/Demux & ID-Based Routing (coming next) — how the interconnect muxes/demuxes channels and routes responses home by ID, including ID-width remapping.
Previous: 12.3 — Decoder & Address Map. Related: 6.5 — AxPROT, AxQOS & AxREGION for the QoS signal, and 12.2 — Crossbar Architecture for the per-subordinate contention this resolves. For the broader protocol catalog, see the AMBA family overview doc.