Interconnect Fabrics and Arbitration

You now speak AXI — the language of one master talking to one slave. But an SoC has M masters (CPU cores, DMA engines, a GPU, a display controller that must never miss a frame) and S slaves (DRAM, SRAM, a rack of peripherals), and all of them want to talk at once. The machinery in the middle — the interconnect fabric — is a full design discipline of its own: it decides who may speak, routes every transaction to its owner, referees the fights, and quietly determines more of a chip's real-world performance than most of the blocks it connects.

From shared bus to crossbar

The simplest fabric is the shared bus: one set of wires, everyone attached, one conversation at a time. It's small and easy — and it serialises the whole chip. While the DMA engine streams a megabyte to memory, the CPU's urgent little UART poke stands in line behind it. Shared buses ruled the microcontroller era; they suffocate anything bigger.

The opposite extreme is the crossbar: a private potential path from every master to every slave — M \times S crosspoints. Now the CPU can read SRAM while the DMA engine writes DRAM while the display controller fetches a frame from a third port: any set of transfers to distinct slaves proceeds concurrently. The price is area and wiring that grow as the product M \times S — double both sides and the fabric quadruples. So real SoCs build partial and cascaded fabrics: a small, fast crossbar between the bandwidth players (CPU cluster, GPU, DMA, memory controller), with the dozens of slow peripherals hanging off a shared branch behind a single crossbar port. Spend crosspoints where the traffic is.

Inside the fabric, your old friend address decode does the routing: the high bits of each transaction's address select the output port, exactly as in the memory-map lesson — just replicated per master and pipelined.

See the crossbar work

The last step is the important one: a crossbar removes structural conflicts (fights over wires) but cannot remove destination conflicts. Two masters, one slave — someone must choose. That someone is the arbiter.

Arbitration: who goes first?

An arbiter is a small combinational-plus-state machine at each contended port, answering one question per cycle: of the masters requesting this slave, who wins? The classic policies:

Which thumb, on which scale, is quality of service (QoS). The CPU's cache miss is latency-critical — a stalled pipeline bleeds performance every waiting cycle, but it only wants a cache line. The DMA engine is the opposite: bandwidth-hungry but patient — no one cares whether any single beat waits, only that megabytes per second keep flowing. A good fabric lets urgent-and-small overtake bulky-and-patient, and modern interconnects carry explicit QoS priority values on each transaction for exactly this purpose.

Two more realities of grown-up fabrics. Outstanding transactions: with AXI IDs, several masters' requests can be in flight through the fabric at once, and responses must find their way back — the fabric appends routing information to each ID so the R and B channels retrace their steps. And pipelining: a big fabric's paths are long, so designers drop register slices (a rank of flip-flops on each channel) into the middle. Each slice buys back clock frequency — shorter combinational paths, exactly the timing-closure trade you know from the RTL module — at the cost of a cycle of latency. The fabric is a pipeline you route messages through, not a wire.

Race the arbiters

Policies sound abstract until you count grants. Below, four masters share one slave for 300 cycles. Master 0 is greedy (requests 95% of cycles); the others request 40% of cycles. Same traffic, two arbiters — compare who actually gets served:

// Fixed priority vs round-robin, same requests, one contended slave. let seed = 2026; const rand = () => { seed = (seed * 1664525 + 1013904223) % 4294967296; return seed / 4294967296; }; const N = 4; const fixedGrants = [0, 0, 0, 0]; const rrGrants = [0, 0, 0, 0]; let rrLast = N - 1; // round-robin pointer for (let cycle = 0; cycle < 300; cycle++) { const req: boolean[] = []; for (let m = 0; m < N; m++) req.push(rand() < (m === 0 ? 0.95 : 0.4)); // Fixed priority: lowest index wins. for (let m = 0; m < N; m++) { if (req[m]) { fixedGrants[m]++; break; } } // Round-robin: search from just past the last winner. for (let k = 1; k <= N; k++) { const m = (rrLast + k) % N; if (req[m]) { rrGrants[m]++; rrLast = m; break; } } } console.log("master | wants | fixed-priority | round-robin"); console.log("-------+-----------+----------------+------------"); for (let m = 0; m < N; m++) { const wants = m === 0 ? "95%/cycle" : "40%/cycle"; console.log(` ${m} | ${wants} | ${String(fixedGrants[m]).padStart(3)} | ${String(rrGrants[m]).padStart(3)}`); } console.log("\nFixed priority: master 0 devours the slave; master 3 starves."); console.log("Round-robin: everyone eats.");

Under fixed priority, masters 1–3 only ever win in the rare cycles when master 0 is silent — and master 3 needs masters 0, 1 and 2 all silent at once. Round-robin serves the same greedy master less and everyone else steadily. Nudge master 0's 0.95 up to 1.0 and rerun: under fixed priority the other three masters flatline at exactly zero. That's starvation — not slow service, no service.

A surprising QoS folk theorem from real chips: the humble display controller often carries the highest priority in the whole memory system — above the mighty CPU. Why? Because its deadline is physical. The panel's pixel clock drains the display FIFO at a fixed, non-negotiable rate; if a frame fetch arrives even slightly late, the FIFO underruns and the user sees the screen glitch. A CPU that waits an extra 100 ns merely runs imperceptibly slower — a display that waits too long fails visibly, every time. So QoS design starts not from "who is important?" but from "who has a hard real-time deadline, and what is the cost of missing it?" Bandwidth-hungry but deadline-free agents (DMA bulk copies) go last, latency-sensitive agents (CPU misses) go in the middle, and anything wired to physics goes first.

Fixed priority feels harmless in simulation, because testbenches politely take turns. Real traffic doesn't. A GPU or DMA engine in a busy phase can request every single cycle — and behind a fixed-priority arbiter, a lower-priority master then waits literally forever. The system-level symptom is horrible to debug: the chip doesn't crash; one subsystem just silently never makes progress — a watchdog fires seconds later, far from the cause, and nothing in any single block's verification ever looked wrong. Chips have shipped with exactly this bug. The rule of the experienced integrator: default to round-robin (or any policy with a service guarantee), and use bare fixed priority only where you can prove the high-priority master's traffic is bounded. An arbiter is a promise about worst-case behaviour, not average behaviour.