Wiring the System Together
What joins many parts into one working body?
Chapter 6 — Wiring the System Together
“Every wire on a chip is a decision about who waits.” — an industry adage
“joined and held together by every joint with which it is equipped, when each part is working properly” — Ephesians 4:16 (ESV)
Why This Matters
For five weeks you have studied blocks: the CPU clusters in Chapter 2, the cache hierarchy in Chapter 3, the accelerator fleet in Chapter 4. Then Chapter 5 took all of it away and handed it back priced in joules.
This week the blocks stop being the subject and the wires between them become the subject. A system-on-chip is not a collection of processors that happen to share a package. It is a negotiation, arbitrated in hardware, over one badly oversubscribed path to DRAM, conducted every nanosecond, between clients whose needs are not merely different but incompatible. The display controller must be served on a fixed schedule or the user sees a torn frame. The camera must be drained or the sensor overruns. The GPU and the NPU both want everything there is, at the same moment, because the user asked for a photo and the thing that describes it. The CPU has no deadline at all and will still be blamed when it is starved, because a starved CPU is a stuttering interface. Someone has to decide who waits. That decision is the interconnect, and it is the most under-taught subject in computer architecture.
Hold The Four Questions against this material and watch them land differently. Performance stops being a property of a block and becomes a property of a path — the fastest NPU in the world is slow if it cannot get weights. Energy becomes dominated by things that are not computing at all: a radio’s tail, a core woken four hundred times a second for nothing, a frame composited on the GPU when a fixed-function overlay would have done it for a fraction of the joules. Thermals acquire a new actor, because a sustained cellular transfer is often the largest single power draw on the device and it heats the chassis right where the user’s hand is. And placement grows an axis: not just which processor, but which processor is even awake, because the whole point of a sensor hub is that the application cores stay dark while the device keeps listening.
Both of this book’s AI registers show up here. AI as the workload: an on-device model is a bus master, its weights megabytes crossing the same wires the camera is using — which is why Chapter 4 told you inference is usually memory-bound, and why “run the model while recording video” is an integration question before it is a compute question. AI as an untrustworthy research assistant: this chapter is dense with interface names and bandwidth figures, precisely the material a language model invents most fluently. Ask one for the peak bandwidth of a named SoC’s fabric and you get two significant figures and no source. So, stated once and enforced by the rubric: use a model to explain a mechanism; never to source a number.
The Christian question this week is about joints, not members. Paul describes a body “joined and held together by every joint with which it is equipped, when each part is working properly” (Ephesians 4:16, ESV). Last week’s thesis was endurance; this week’s is connection — and the unglamorous truth that most system failures are failures at the seam between two components, each of which passed its own tests.
6.1 — The Interconnect Is the System Architecture
Strip the block diagram down to its truth. Every block is either an initiator that issues read and write transactions or a target that answers them. CPU cluster, GPU, NPU, display controller, camera receiver, modem DMA engines, storage controller: initiators. DRAM, the system-level cache, every peripheral’s register block: targets. Between them sits a fabric that routes transactions, buffers them, reorders them, and — the part that matters — decides the order in which competing requests are served.
On Arm-based silicon that fabric speaks the AMBA family. Know four by name and by job:
| Protocol | Shape | Where it lives |
|---|---|---|
| APB | Simple, non-pipelined, one transfer at a time | Control registers: timers, GPIO, UART, block configuration |
| AHB | Pipelined, shared-bus heritage | Older or modest peripherals; still common inside subsystems |
| AXI | High-performance, memory-mapped, burst-oriented, many outstanding transactions | The main data path — CPU, GPU, NPU, display, camera, storage |
| ACE / CHI | Coherent extensions and a scalable coherent protocol | Cache-coherent traffic between clusters and IO-coherent devices |
The AXI detail worth carrying is that it is five independent channels, not one bus: write address, write data, write response, read address, read data. Address and data are decoupled, so an initiator issues a burst address and streams data behind it. Each transaction carries an ID, and transactions with different IDs may complete out of order. That one decision is what makes the whole thing tolerable: a master with many outstanding requests keeps the memory system busy and converts DRAM’s ugly latency into usable bandwidth — Chapter 3’s memory-level-parallelism argument, expressed in wiring.
Above the protocol sits topology. A handful of masters can be joined by a crossbar; once you have dozens, its area and wire length become intolerable and you build a network-on-chip: routers, links and packets arranged as a ring, a mesh, or something irregular that follows the floorplan. Topology is not abstract. A wire crossing the die costs area, energy per bit, and latency, so on a chip large enough to need a network-on-chip, where a block sits is part of its performance specification.
Coach’s Note — When you read a block diagram, stop looking at the boxes and start tracing the lines. Ask three things of every line: what is its width, who else shares it, and what happens to me when they are busy? A student who can name every block scores a C. A student who can say “the NPU and the display controller hang off the same port into the system-level cache, so a sustained inference run will show up as jank” is doing the job.
Map that onto The Four Questions. The interconnect answers performance by giving each master enough concurrency to hide DRAM latency, and energy by keeping data on chip — every transaction the system-level cache absorbs is a DRAM access that never happens, which Chapter 3 priced at orders of magnitude. It touches thermals, because sustained bandwidth is sustained power in the memory controller and its physical interface. And it constrains placement by making some placements physically cheaper than others.
6.2 — Arbitration and Quality of Service: Deciding Who Waits
Supply is finite. Demand is not.
Start with the client that cannot be argued with. A display controller reads the composed frame out of memory and pushes it at the panel on a schedule the panel sets, not the software. The arithmetic follows from stated assumptions — it is arithmetic, not a claim about any product:
1080 x 2400 pixels x 4 bytes/pixel = 10,368,000 bytes per frame
x 120 frames/second = 1,244,160,000 bytes/second
≈ 1.24 GB/s, continuously, forever
That is the floor, for one already-composed layer under those assumptions. If the display controller composites several layers itself it reads each of them; if the GPU composites instead, the GPU reads them, writes a result, and the display controller reads that — the same pixels crossing the bus three times instead of once. Change the resolution or the refresh rate and rerun it. The point is not the number; the point is that the number never pauses.
And the deadline is absolute. At 120 Hz a frame is 8.3 ms wide. If the fetch for the next scanline is not serviced in time, the panel is fed stale or missing data and the user sees tearing or a dropped frame. There is no retry. That is what hard real-time means, and the display controller is the reason quality-of-service arbitration exists in mobile silicon at all.
The camera is the same argument running the other way. A sensor streams over MIPI CSI-2 at a rate the sensor chooses; if the receiver cannot drain into memory fast enough, lines are lost and the frame is corrupt:
12,000,000 pixels x 30 frames/second x 10 bits/pixel ÷ 8 = 450,000,000 bytes/s
≈ 450 MB/s of raw sensor data
Every other master is negotiable and knows it. A late GPU frame costs smoothness, not correctness; the NPU can take longer to produce a token; storage can wait. The CPU has no hardware deadline at all, which is exactly why it is the one sacrificed, and why users experience that sacrifice as “the phone is slow.”
Hardware expresses this with mechanisms you should be able to name. AXI carries a small per-transaction quality-of-service field, so a master can mark a request urgent. Interconnects add bandwidth regulators that meter a greedy master to a configured rate, and latency targets that raise a master’s effective priority as its deadline approaches. Some designs give the display controller a private port or a reserved system-level-cache allocation so it never competes at all. All the same idea in different clothes: the fabric must know which client has a deadline, because the clients cannot tell each other.
Make it concrete
code/bus-traffic.csv is a synthetic traffic table for seven masters — its first line says so, and it is not a measurement of any product. Each row carries a peak demand, the average it must sustain, a latency class and a deadline. code/qos_arbiter.py hands out one fixed total under two policies and tells you who suffers.
python3 code/qos_arbiter.py --total 24000
masters: 7 sum of peaks: 36,600 MB/s sum of averages: 16,620 MB/s supply: 24,000 MB/s
oversubscription at peak: 1.52x
=== fixed-priority arbitration @ 24,000 MB/s total ===
master class need peak granted burst ms deadline verdict
camera hard-real-time 1600 2400 2400 16.7 16.7 ok
display hard-real-time 2400 3600 3600 8.3 8.3 ok
gpu soft-real-time 5200 12000 12000 16.7 16.7 ok
modem soft-real-time 220 600 600 20.0 20.0 ok
npu soft-real-time 4200 9000 5400 55.5 33.3 DEADLINE MISS
cpu best-effort 2200 6000 0 - - STARVED
storage best-effort 800 3000 0 - - STARVED
in trouble: npu, cpu, storage
=== weighted-fair arbitration @ 24,000 MB/s total ===
master class need peak granted burst ms deadline verdict
camera hard-real-time 1600 2400 2310 17.3 16.7 DEADLINE MISS
display hard-real-time 2400 3600 3466 8.6 8.3 DEADLINE MISS
gpu soft-real-time 5200 12000 7509 26.7 16.7 DEADLINE MISS
modem soft-real-time 220 600 318 37.8 20.0 DEADLINE MISS
npu soft-real-time 4200 9000 6065 49.4 33.3 DEADLINE MISS
cpu best-effort 2200 6000 3177 - - ok
storage best-effort 800 3000 1155 - - ok
in trouble: display, camera, gpu, npu, modem
Read those two tables against each other until the lesson is physical. Weighted fairness — dividing bandwidth in proportion to what each master needs — made every client with a deadline miss it. The display missed by 0.3 ms, a visible glitch. The camera missed by 0.6 ms, a corrupted frame. Nobody was treated unfairly. Everybody failed.
Strict priority produced a worse-looking table and a better-behaving device: display and camera perfect, the price paid by masters with no deadline. But look at what it did to the CPU — zero. Not reduced. Zero. A CPU held at zero bandwidth is a frozen interface, which is not shippable either, and it is why real interconnects reserve a floor. Run --policy fixed --floor 20 and the CPU comes back to 3520 MB/s and storage to 1280, both above their sustained need — while the NPU collapses to 600 MB/s, below even its average, taking 499.5 ms for a burst it wanted to finish in 33.3.
Somebody always pays. An architect’s job is not to make the pain vanish — it cannot, the supply is fixed — but to choose the victim deliberately and say in one sentence why that victim was right. Here it probably is: yes, the on-device model gets slower while the camera runs, because a dropped video frame is unrecoverable and a late caption is not.
Coach’s Note — Fairness is a bug when one of your clients has a deadline. This is the most transferable idea in the chapter and it is not about chips. Any scheduler — bandwidth, CPU, network, storage queue, human attention — that divides a scarce resource evenly among clients with unequal deadlines makes the client with the tightest deadline fail first. Equal shares are only just when the consequences of delay are equal. They almost never are.
6.3 — Memory-Mapped I/O, volatile, and Barriers
You have been treating memory as the only thing behind a load or a store. On essentially every modern SoC, device control registers are memory-mapped: a peripheral’s registers occupy a range of the physical address space, and a load or store to that range is not a memory access at all — it is a bus transaction that reaches a device, and the device does something as a side effect. Reading a status register can clear it. Writing a command register can start a DMA. (Contrast x86’s separate port-I/O space — recognize it, but you will not meet it in Arm mobile.)
This breaks two assumptions the compiler and the core are otherwise entitled to make.
The compiler assumes a load with no observable use can be deleted, and that two loads from one address with no intervening store return the same value. Both are false for a device register. volatile tells the compiler one thing: every access in the source must appear in the object code, exactly as often, in this order relative to other volatile accesses.
// WRONG — the compiler may hoist the load out of the loop and spin forever on a
// value it read once, because nothing in this function writes to it.
uint32_t* status = reinterpret_cast<uint32_t*>(0x1234'0000);
while ((*status & DONE_BIT) == 0) { /* wait */ }
// RIGHT — volatile forces a real load on every iteration.
auto* status = reinterpret_cast<volatile uint32_t*>(0x1234'0000);
while ((*status & DONE_BIT) == 0) { /* wait */ }
The core assumes it may reorder and buffer accesses no single-threaded program can tell apart. Also false at a device boundary: write a descriptor into a buffer, then write the “go” bit into a device register, and if those two stores reach the bus in the other order the device starts on garbage. That is what memory barriers are for. On AArch64 you have DMB (orders memory accesses around it), DSB (additionally waits for completion), and ISB (flushes the pipeline so later-fetched instructions see new system state). The memory type matters too: regions mapped as Device memory carry attributes restricting gathering, reordering and early write acknowledgement, precisely so a driver’s accesses arrive at the peripheral in the shape the driver wrote them.
Two corrections that separate the graduate from the hobbyist:
volatileis not a memory barrier. It constrains the compiler, not the hardware. Ordering across a device boundary needs an explicit barrier as well — in practice, the kernel or framework accessor functions that bundle the volatile access and the barrier together.volatileis not a concurrency primitive. No atomicity, no inter-processor ordering. Between CPUs usestd::atomicwith real acquire/release semantics. Usingvolatilefor a shared flag is a bug that happens to work on some machines, which is the worst kind.
Coach’s Note — Almost every “impossible” driver bug I have seen is one of three things: a missing
volatile, a missing barrier, or a cache-maintenance operation on the wrong side of a DMA. All three have the same signature — the code is obviously correct when you read it and the hardware obviously disagrees. When your reasoning and the device disagree, the device is not wrong. Go find the ordering assumption you did not know you were making.
6.4 — Interrupts, the GIC, and the Energy Case for Sleeping
A device needs to tell the CPU something happened, and it has two ways: the CPU asks (polling), or the device raises a signal that diverts the CPU (an interrupt). On Arm systems that signal is routed by the GIC, the Generic Interrupt Controller, which prioritizes interrupts, decides which core receives each one, and handles masking and acknowledgement. The GIC classifies interrupts by origin — software-generated (one core poking another), private-per-core (a core’s own timer), and shared peripheral interrupts for everything else; recent versions add message-signalled interrupts so a system can carry far more sources than there are physical wires.
The mobile default is interrupts, for a one-sentence reason: sleeping is the whole point. A polled core is an awake core, and Chapter 5 taught you an awake core burns both dynamic and leakage power the entire time. Interrupt-driven design is what lets a phone do nothing, which is statistically what a phone is doing almost always.
But an interrupt is not free, and its costs are what the crossover is made of. There is entry and exit overhead — take the exception, save state, dispatch, acknowledge the controller, restore, return. There is a wake-up: if the core was power-gated, the interrupt must restore it, costing exit latency and energy, which is Chapter 5’s break-even-residency problem in a new costume. There is cache and pipeline damage, because the handler evicts the interrupted workload’s working set and the core resumes cold. And there is scheduling disturbance, because real systems keep the handler short (the “top half”) and defer the work to a thread (the “bottom half,” or a threaded IRQ) — another wake and another context switch.
Run those costs against a rising event rate. At one event per second the interrupt path is free and polling is absurd. At a million events per second the overhead alone consumes the core, and the system can enter interrupt livelock, spending all its time taking interrupts and never running the code that drains the device. Linux’s NAPI exists because network drivers hit exactly this wall, and it responds by switching the driver from interrupts to polling under load, then back again.
code/irq_vs_poll.cpp makes the crossover arithmetic rather than folklore. Every constant in it is an illustrative parameter, not a measurement; the shape is what you are learning.
g++ -std=c++17 -O2 -o irq_vs_poll code/irq_vs_poll.cpp && ./irq_vs_poll
model: irq overhead 3.00 us | service 1.00 us | wake 2.00 uJ / 40 us
core active 1.50 W | core idle 0.05 W | poll iteration 0.20 us
events/s | irq cpu irq mW irq lat | poll cpu poll mW poll lat | cheaper
-------------+------------------------------+------------------------------+--------
100 | 0.0% 50.8 43.0u | 100.0% 1500.0 0.10u | interrupt
1000 | 0.4% 57.8 43.0u | 100.0% 1500.0 0.10u | interrupt
10000 | 4.0% 128.0 43.0u | 100.0% 1500.0 0.10u | interrupt
50000 | 20.0% 440.0 43.0u | 100.0% 1500.0 0.10u | interrupt
100000 | 40.0% 830.0 43.0u | 100.0% 1500.0 0.10u | interrupt
150000 | 60.0% 1220.0 43.0u | 100.0% 1500.0 0.10u | interrupt
200000 | 80.0% 1610.0 43.0u | 100.0% 1500.0 0.10u | polling
500000 | 100.0% 2500.0 43.0u | 100.0% 1500.0 0.10u | polling
1000000 | 100.0% 3500.0 43.0u | 100.0% 1500.0 0.10u | polling
5000000 | 100.0% 11500.0 43.0u | 100.0% 1500.0 0.10u | polling
crossover at ~185897 events/s (interrupt path is then 74% CPU-busy).
Three things. The crossover is a number, not a matter of taste — you can compute it for your device from four parameters. Latency and energy point in opposite directions: the polled column is two orders of magnitude better on latency at every rate and two orders worse on energy at low rates. That is the entire tradeoff, in two columns. And the crossover moves when the handler gets fatter — double the interrupt overhead and it roughly halves, so a sloppy driver drags a device into the polling regime early and makes the platform pay a core’s worth of power for it.
Against The Four Questions: interrupts answer energy and cost performance at the margin; polling answers performance under a hard latency requirement and is an energy catastrophe if you are wrong about the rate. It is a placement decision too, because the right answer for a high-rate device is usually neither — it is a DMA engine and one completion interrupt per batch.
6.5 — DMA and the SMMU: Bulk Movement and Containment
If a core executes a load and a store for every word a device produces, the core is a very expensive, very hot memory-copy engine. Direct memory access takes it out of the path: the device, or a dedicated DMA engine, moves the bytes itself and interrupts the CPU once at the end. The core sleeps through the transfer.
The mechanics generalize across nearly every peripheral. Software builds descriptors in memory — source, destination, length, flags — and links them into a ring. Scatter-gather lets one logical transfer span many physically discontiguous pages, the only way this works under virtual memory. The device walks the ring and raises one interrupt per completed descriptor or, better, per batch, which is how you climb back down §6.4’s curve.
Two hazards live here.
Coherency. If a device writes DRAM while a core holds stale copies of those lines, the core reads garbage. An IO-coherent device participates in the coherence protocol — on Arm, over an ACE-Lite-class interface — and the interconnect snoops the CPU caches for it. A non-coherent device does not, and the driver must do cache maintenance by hand: clean (write back) the buffer before a device reads it, invalidate it after a device writes it. Get the direction backwards and you get intermittent corruption that survives every code review, because the code looks symmetric.
Containment. A DMA-capable device with a raw physical address can write anywhere in memory — a spectacular privilege to hand to a peripheral running vendor firmware you did not audit. The SMMU, Arm’s system MMU (an IOMMU in general terms), puts a translation stage in front of every device transaction so each device sees only its own address space, mapped to exactly the pages it may touch. The performance framing is that this costs a translation and a walk. The architecture framing is that the SMMU is the difference between “a compromised Wi-Fi firmware can read your DRAM” and “it cannot.”
Coach’s Note — Notice what just happened. A mechanism introduced for convenience — let the device move its own bytes — created a security boundary that did not previously exist, and a second mechanism had to be invented to hold it. That is the normal shape of systems engineering, not an anomaly. Every capability you grant a component is a capability an attacker inherits if that component falls. Chapter 7 spends a week on it.
6.6 — The Edges: Display, Camera, Storage, and the Ports
Inside the die it is AMBA. At the edge of the die it is a zoo, and each animal exists because a different physical problem had to be solved.
| Interface | Carries | Why it looks the way it does |
|---|---|---|
| MIPI DSI | SoC → display panel | Few high-speed differential lanes; short reach; low power per pixel |
| MIPI CSI-2 | Camera sensor → SoC | Same physical family, opposite direction; sensor-paced, no back-pressure |
| UFS | SoC ↔ NAND storage | Serial, full-duplex, command queuing; successor to half-duplex eMMC |
| I²C / SPI / I3C | SoC ↔ sensors and small parts | Cheap pin counts, low rates, simple controllers |
| USB (Type-C) | SoC ↔ the outside world | One connector for data, power delivery and display alternate modes |
| PCIe | SoC ↔ a discrete high-speed device | Where a discrete modem, Wi-Fi part or laptop-class storage attaches |
Four notes that matter more than the table.
The display path is a power decision, not just a pixel decision. A frame reaches the panel because something composited the visible layers into one image. That can be the GPU — which reads every layer, writes a result, and hands it to the display controller — or fixed-function overlay planes in the display controller itself, which read the layers directly during scanout and never write an intermediate buffer at all. On Android the hardware composer decides this per frame. When overlays work, the GPU stays asleep and a whole round trip through DRAM never happens. When they do not — too many layers, or a transform the hardware cannot do — the GPU wakes and the frame costs several times more energy for a pixel-identical result. That is Chapter 4’s tile-memory argument one level up the stack: the traffic you avoid is worth more than the arithmetic you optimize.
Variable refresh is also a power feature. A panel that drops its refresh rate on static content (LTPO-class panels are the usual example) reduces both panel power and the display controller’s DRAM traffic, because the frame is fetched fewer times per second. Panel specifics vary by vendor and generation; the architectural point does not.
The camera path is a pipeline, not a device. The sensor streams raw data over CSI-2 with no back-pressure whatsoever; a receiver writes it to memory; the ISP does demosaicing, noise reduction, lens correction, tone mapping and multi-frame fusion; and increasingly the NPU is invited in for segmentation or scene understanding before a fixed-function video block encodes the result. Every one of those stages is a bus master. “Record 4K video” is not one client on the interconnect. It is four or five, arriving simultaneously, with a deadline set by a sensor that will not slow down for you.
PCIe lives at the seams. Inside the SoC, coherent AMBA traffic dominates; PCIe appears where something is not on the die — a discrete cellular modem, a discrete Wi-Fi part, laptop-class storage in a tablet. That is an integration choice with consequences: a discrete part is thermally separable and independently sourceable, and it costs link power, latency and board area an on-die block does not.
6.7 — Sensors, Fusion, and the Always-On Island
A phone that is asleep is not asleep. It is counting steps, waiting for a wake word, watching for a raise-to-wake gesture, keeping a barometer’s history. If any of that woke the application processor, the battery would be gone before lunch. So the architecture answers with a very old idea in a very small package: do the small, endless work on a small, endless processor.
The parts inventory first: a MEMS accelerometer, gyroscope and magnetometer, a barometer, an ambient light sensor, a proximity sensor, and a GNSS receiver. Note that terminology, because people get it wrong: GNSS is the general term for satellite positioning; GPS, Galileo, GLONASS and BeiDou are particular constellations, and a modern receiver uses several at once. These parts are individually cheap and individually useless — an accelerometer alone cannot tell gravity from acceleration, a magnetometer alone is confused by the metal in the device, a gyroscope alone drifts. Sensor fusion, the filter that combines them into an orientation estimate, is where the usable signal comes from, and it is continuous low-rate work that must never stop.
That work runs on the sensor hub, also called the always-on processor: a small, very-low-power core (often a DSP, sometimes a Cortex-M-class core) with its own memory, its own firmware, and access to the sensor buses. Around it sit the mechanisms that make the always-on story work:
- Hardware FIFOs and batching. A sensor writes samples into a buffer and the hub is notified when the buffer fills, not per sample — one wake every few seconds instead of one every few milliseconds. §6.4’s crossover argument, applied to a device that produces small events forever.
- Wake-word detection. A tiny always-listening model runs on the hub. It does not understand you; it decides whether to wake something that does.
- Thresholded and gestural wakes. Raise-to-wake, tap-to-wake and step detection are evaluated on the hub, and the application processor learns a result, not a stream.
This is the chapter’s clearest placement answer, and it differs from the earlier ones. In Chapter 2 placement meant choosing among CPU cores; in Chapter 4, among processors. Here it means choosing among power domains — the winning move is not a cheaper core but the one island allowed to stay powered while everything else is gated off. The cheapest work is work that does not require the expensive thing to be awake at all.
Coach’s Note — When you audit a device’s idle power, do not start with the application cores. Start by asking what is allowed to be awake, then ask what wakes what. One misconfigured sensor reporting at 200 Hz with batching disabled will hold an entire SoC out of its deep idle state and destroy a standby-battery target — and no CPU profiler will ever show it to you, because from the CPU’s point of view almost nothing happened. Idle power is an integration property.
6.8 — The Radios: A Combo Chip and a Computer Next to Yours
There are two radio stories on a modern device, and they are architecturally different.
Wi-Fi and Bluetooth are usually one combo subsystem: a shared radio front end, its own processor, its own firmware, attached over a high-speed interface. It is substantial software running next to yours, managing association, encryption, power-save states and coexistence between two radios sharing overlapping spectrum. Generations matter mostly for behaviour under contention rather than headline rates: Wi-Fi 6 (802.11ax) is largely a scheduling generation, Wi-Fi 6E adds the 6 GHz band, Wi-Fi 7 (802.11be) continues both. Bluetooth’s split into Classic and Low Energy is a power-architecture split first and a protocol split second — LE exists so a peripheral can hold a connection on a coin cell.
The cellular modem is a different order of thing, and this is the paragraph to remember. A modem is not a peripheral. It is a large subsystem containing its own DSPs, its own control processors, and a real-time software stack implementing a standard measured in tens of thousands of pages. It runs its own operating system and has hard real-time deadlines imposed by a network it does not control. It may be integrated on the SoC die or attached as a discrete part over a high-speed link. Either way the honest description should make you sit up: it is a computer running its own operating system next to yours, sharing your memory system, built by a different team on a different schedule against a different standard.
That sentence has three consequences.
- It is a bus master with real-time needs. Uplink and downlink buffers cross to DRAM continuously during a transfer, and its deadlines come from the air interface.
- It is a major thermal actor. During sustained transfer the modem and its power amplifiers are frequently the largest single power draw on the device, dissipating heat near the antennas rather than into the main spreader. Re-read Chapter 5’s skin-temperature argument with that in mind: a device can be thermally limited by a subsystem that is not the SoC at all.
- It is an attack surface with memory access. Firmware you did not write, processing data from a network you do not control, on a processor next to yours. That is why §6.5’s SMMU matters, and it is the direct handoff to Chapter 7, which builds a threat model on exactly this fact.
Know the 5G shape without pretending to know its numbers. 5G NR operates in FR1 (sub-6 GHz) and FR2 (millimetre wave); deployments are non-standalone (a 5G radio on a 4G core network) or standalone. FR2 offers very wide channels over short distances, penetrates obstacles poorly, and requires beamforming with dense antenna modules — costing power and physical volume, two things Week 1 said you do not have. What you must be able to say is architectural, not numeric: more bandwidth needs more radio-frequency chain, more antennas and more digital signal processing, and every one is a claim on the same battery and the same skin-temperature budget the compute blocks are spending.
6.9 — Radio Energy: Why Batching Beats Compressing
Here is the most useful thing an application-side engineer can take from this chapter, and almost nobody knows it.
A radio is not billed per byte. It is billed per second spent awake.
Bringing a radio up costs energy and time. Once up, the standards keep the connection in a high-power state for a period after the last byte — a tail — because tearing it down and rebuilding it for the next transfer would cost more. The 3GPP standards structure this as connection states (connected, idle, and in 5G NR an intermediate inactive state), with discontinuous reception letting the device doze between scheduled listening windows. The exact timers are operator-configured and vary by network, band and vendor, so do not quote them. What is universal is the shape: after your transfer finishes, the radio keeps drawing meaningful power for a while, and you are paying for it.
code/radio_energy.py models the same payload moved three ways. Its defaults are illustrative model parameters — replace them with figures you can cite before using it to claim anything about a real network.
python3 code/radio_energy.py
60 transfers x 4.0 KiB every 10.0 s at 20 Mbit/s
modeled tail 8.0 s | active 1.20 W | tail 0.50 W | idle 0.02 W
observation window: 598.0 s
policy bytes active s tail s joules chart
unbatched 240K 0.098 480.0 242.48 ##################################
compressed -50% 120K 0.049 480.0 242.42 ##################################
batched 240K 0.098 8.0 15.92 ##
compressed -50% saves 0.0% of the unbatched energy (1.0x cheaper)
batched saves 93.4% of the unbatched energy (15.2x cheaper)
Look at the active s and tail s columns and the whole argument falls out. The transfers themselves account for 0.098 seconds of radio time across the entire ten-minute window. The tails account for 480 seconds. Compression optimizes the 0.098; batching optimizes the 480. Halving the bytes saved essentially nothing; sending them together saved 93%.
That inverts the instinct nearly every engineer arrives with. Compression feels responsible; on a radio it is a rounding error, and an expensive algorithm can cost more CPU energy than it saves radio energy. The crude, unglamorous fix — hold the requests, send them together, let the radio sleep between — is worth an order of magnitude.
The platform consequences follow directly. Batch and defer: Android’s job-scheduling APIs (and equivalents elsewhere) exist to coalesce deferrable network work into shared wake windows across all apps, and an app that runs its own timer instead externalizes its cost onto everyone else’s battery. Push, don’t poll: a polling app pays a full wake-and-tail cycle every interval, forever, mostly to learn nothing happened. Align to somebody else’s wake: the cheapest transfer happens while the radio is already up for another reason — over Wi-Fi, while charging, or during a wake the user already caused.
Coach’s Note — Every one of those bullets is the same trick you have now seen three times this week: batch the events so the expensive thing wakes once instead of many times. It is the sensor FIFO in §6.7. It is the per-batch completion interrupt in §6.5. It is the crossover in §6.4. When you design an interface between a fast thing and a sleepy thing, the question is never “how do I make each event cheaper?” It is “how few times must the sleepy thing wake up?“
6.10 — The Hardware/Software Boundary: Where Integration Actually Fails
Everything here reaches software eventually, and the shape of that reach is itself an architecture. At the bottom a driver owns a block: it maps the register range, requests the interrupt, programs clocks and regulators, sets up DMA, and exposes a kernel-level interface. Above that, Android’s hardware abstraction layer is a versioned contract between the framework and vendor hardware code, defined in an interface description language (AIDL today, HIDL historically), with Project Treble having separated the vendor implementation from the framework so the two can update on different schedules. Details vary by release; the structure does not. And the existence of non-discoverable hardware is described declaratively — a device tree tells the kernel which blocks exist at which addresses with which interrupts and clocks, because unlike PCIe there is nothing on an AMBA bus to enumerate.
Now the part that earns this section a place in a graduate course. Most system failures are interface failures. Not component failures. Each side passed its own tests, and the seam between them had no test because it had no owner. Memorize the recurring shapes:
- Version skew. The framework expects version N of a HAL interface; the vendor shipped N−1. Both are correct. The combination is not.
- Format and layout mismatch. A buffer produced with one pixel format, stride, alignment or colour space and consumed expecting another. The image is green, or sheared, or fine on one device and wrong on another.
- Ordering and lifecycle bugs. A clock or regulator enabled in the wrong order, a block accessed before it has left reset, a DMA started before the descriptor write drained. Works on the bench, fails on the tenth boot.
- Deadline mismatch. One side assumes best-effort service; the other has a hard deadline, written down nowhere a machine could check. §6.2’s tragedy in software clothing.
- Error semantics. Component A returns an error meaning “retry”; component B treats every error as fatal. The device reboots to recover from a recoverable condition.
The discipline that answers this is unglamorous and it is the actual job: specify the seam. For each interface, write down the data format, the ownership and lifetime of every buffer, the latency and throughput contract, the deadline if there is one, the error semantics, and the versioning rule. Then instrument it — a transaction counter, a latency histogram, a deadline-miss count — because an interface you cannot observe is one you cannot debug. That is what your project asks you to produce, and what Chapter 8 will teach you to measure honestly.
6.11 — Interactive Lab: The Interconnect Arbiter
Below this chapter on the site you will find The Interconnect Arbiter. It is not optional colour; it is where §6.2 and §6.9 stop being tables and start being consequences you caused.
Panel one gives you the seven bus masters from code/bus-traffic.csv and one fixed total bandwidth to allocate by priority and share. The widget reports which deadlines were met, which master was starved, and — the part that matters — what the user would actually experience: a torn frame, a stuttering preview, a caption four seconds late, a scroll that judders. Work it in this order:
- Predict first. Before touching a control, write down which master you expect to lose under strict priority and which under equal shares. That is the predict-before-you-measure rule Appendix C makes non-negotiable in every lab.
- Reproduce the two policies from §6.2, then try to find an allocation where nobody misses a deadline. You will fail, because at this oversubscription it is impossible — and discovering that by trying is worth more than being told.
- Then choose your victim on purpose, and write one sentence defending the choice. That sentence is the skill.
Panel two is the radio: the same bytes moved under different scheduling, with the tail modeled. Toggle batching and watch the joules move by an order of magnitude while the byte count does not move at all. Then try to beat batching with compression. You cannot, and the failure is the lesson.
In one line: an interconnect is a policy, and a policy chooses a loser. You will not have designed one by the end of the lab — but you will know that somebody did, on purpose, and what question to ask them.
6.12 — Joined by Every Joint
Paul writes of a body “joined and held together by every joint with which it is equipped, when each part is working properly” (Ephesians 4:16, ESV). Read that carefully, because it is doing something more precise than the usual “we’re all one team.” It does not say the body is held together by its members. It says the body is held together by every joint — by the connections — and that the whole depends on each part working properly in its place, which is to say working at the seam, in relation to what it is joined to.
Chapter 2 took the other half of this image and made the point about differentiated members: the efficiency core is not the lesser core. This week is the harder half. A body of excellent members with bad joints is a body that does not work — and that is a description of the engineering, not a metaphor imposed on it. A display controller that cannot be late and an NPU that cannot be told, joined by a fabric that had to decide between them. A driver whose correctness depended on a barrier appearing in neither component’s specification. A modem that is a whole computer, joined to yours by a shared memory system and a firmware contract that carries the entire security burden of the arrangement. In every case the components were fine. The engineering lived in the connection.
And here is the uncomfortable professional observation: nobody is celebrated for the joint. The CPU team ships a core with a name and a launch slide. The GPU team ships a number. The integration engineer ships a device that boots, and if she does her job perfectly the visible result is that nothing happened — no jank, no dropped frames, no mysterious drain, no reboot on the tenth cycle. Her excellence is legible only as absence. That is why integration is understaffed on most programs, discovered late, and why the last three months of every hardware schedule look the way they do.
The Lutheran category for this is vocation. Vocation is not about the visibility of the work; it is about the neighbor the work serves. God works through ordinary people in ordinary stations, and the value of the labor is in the service rendered, not the recognition attracted — which is why Luther’s favourite examples were deliberately unglamorous: the milkmaid, the cobbler, the magistrate. The engineer who owns a seam is squarely in that tradition. Her work is real, load-bearing, and nearly invisible when done well. That is not a tragedy to be fixed by better marketing; it is the ordinary shape of faithful work.
There is a warning here for those of us who like components. The parts that “work properly” are working in relation. A subsystem optimized in isolation — the fastest possible NPU, taking every byte of bandwidth it can get — is not thereby a better part; a member that takes what it can from the body is not functioning, it is feeding. Ask of your own block not “how fast can I make this?” but “what does this cost everyone else, and is that trade right for the whole?” That is not a compromise of excellence. It is excellence, correctly aimed.
And joints must be specified, or they are only assumed. Write the contract down. Instrument it. Own it. That no one will thank you for it is beside the point.
6.13 — Common Pitfalls
Pitfall: Reading a block diagram as a parts list and never tracing a path. Example: A review names all twelve blocks on an SoC and predicts a bottleneck in “the CPU,” having never asked which blocks share a port into memory. Fix: Trace the path for every workload: which masters are active at once, what do they share, who has a deadline? The bottleneck is almost always a shared path, not a block.
Pitfall: Believing that fair bandwidth sharing is the safe default. Example: Weighted-fair arbitration in §6.2 made all five deadline-bearing masters miss, including the display. Fix: Identify every hard-real-time client before you design any sharing policy, guarantee it first, then divide what remains. Fairness among unequal deadlines is a defect.
Pitfall: Using volatile as if it were a memory barrier or a concurrency primitive.
Example: A shared flag between two threads is declared volatile; it works on the developer’s machine and fails intermittently on the target under load.
Fix: volatile constrains the compiler only. Use explicit barriers (or the platform’s accessor functions) at device boundaries, and std::atomic with real acquire/release semantics between CPUs.
Pitfall: Treating polling as automatically wasteful and interrupts as automatically efficient.
Example: A driver takes an interrupt per packet at a high event rate, saturates a core with entry and exit overhead, and enters interrupt livelock — while a polled loop would have used less energy and had lower latency.
Fix: Compute the crossover for your parameters with code/irq_vs_poll.cpp. Below it, interrupts win because the core sleeps. Above it, switch to polling — which is what NAPI-style mechanisms do automatically.
Pitfall: Optimizing network payload size while ignoring how often the radio wakes.
Example: A team spends a sprint halving a telemetry payload and measures no battery improvement, because the tail — not the transfer — was the entire bill.
Fix: Count wake-ups before you count bytes. Batch deferrable work into shared windows, prefer push over polling, and run code/radio_energy.py before you commit to a compression project.
Pitfall: Accepting an interconnect or interface figure from a language model. Example: A report cites a peak bandwidth for a named SoC’s fabric; the figure appears in no vendor document, no reference manual and no measurement — the model produced it because the sentence needed a number. Fix: Use AI to explain a mechanism, never to source a figure. Every number in a report carries a primary-source citation — a vendor specification page, an architecture reference manual, or your own measurement logged per Appendix C. A fabricated figure is an integrity failure, not a deduction.
6.14 — Reps
Open the exercises and do all of them. This week’s reps build the reflexes the project grades: reading a traffic table and predicting who loses, running two arbitration policies and defending the victim, locating the interrupt-versus-polling crossover, proving that batching beats compressing on a radio, and budgeting an always-on island.
This week’s AI policy. You may use a model to explain any mechanism here — what an AXI write-response channel is for, or how cache maintenance works around a DMA. You may not use it as a source for any number, interface name, or capability claim that lands in your write-up. Every rep that asks you to look something up also asks you to verify it against a primary source, and Rep 11 makes catching a fabricated figure the exercise itself. Each rep ends with an honest one-line AI-usage note; Appendix D defines the disclosure format.
A preview:
- Rep 2 — run the arbiter under fixed priority and under weighted fairness, and name the master each policy chose to starve.
- Rep 3 — bisect the interconnect budget to find the bandwidth cliff where the display first misses its deadline.
- Rep 5 — locate the crossover event rate where polling stops being wasteful and interrupts start being expensive.
- Rep 8 — compare batching against compressing the same bytes and report which one the radio actually rewards.
- Rep 10 — budget the always-on island: decide what may stay awake inside a stated idle-power target.
A short “Check Your Reps” quiz sits on this page below the lab — the display controller’s priority, what an SMMU contains, when polling beats interrupts, why batching beats compressing, and what the sensor hub is for. It is an ungraded self-check; take it before you move on. Then sit this week’s graded knowledge check in Canvas — worth 1.5%, same material, larger pool, randomized draw, so retaking it is real practice rather than memorization.
6.15 — This Week’s Project
Project 6 — The Integration Map is in Project 6, and it is worth 7% of the course grade, like every weekly lab.
You will produce an integration map for one real device class: every significant bus master, its bandwidth and deadline class, what it shares, and a contention analysis for a named use case. You will run code/qos_arbiter.py and code/radio_energy.py for real and log every run in measurements.xlsx per Appendix C. Then the graded core, which is reasoning rather than output: identify the two most likely integration failures for a scenario such as capturing 4K video while navigating on cellular, say precisely why each would happen, and state what you would measure to confirm it.
Normal tier is completable on Appendix A’s browser workbench, with no device and no admin rights. Medium adds the always-on budget. Hard is the memo: name one interface on your map you would change, and defend the change in bandwidth, energy and risk. Every external figure needs a primary-source citation, and ai-usage.txt goes in with the rest.
6.16 — Coach’s Final Word
You came into this week able to describe seven kinds of processor. You leave it able to describe the argument between them.
That is a bigger promotion than it sounds. You can look at a block diagram and see the paths instead of the boxes — who shares a port, who has a deadline, who gets sacrificed and by which policy. You can explain why volatile is necessary and insufficient, why a DMA engine needed an SMMU invented for it, where the interrupt-versus-polling crossover sits and what moves it, and why the sensor hub exists at all. And you can tell any engineer who will listen the one thing that saves the most battery for the least work: batch the transfers, because the radio is billed by the second, not by the byte.
The Four Questions read differently now. Performance is a property of a path, not a block. Energy is dominated by wake-ups, not bytes. Thermals have an actor you were not counting. And placement now includes which power domain is allowed to be awake at all. Next week those same wires get read a second time, by someone hostile: Chapter 7 takes the modem you just met and builds a threat model on exactly what it is.
The joints, not the members. Nobody will thank you for the seam you specified, the buffer contract you wrote down, the counter you added to a boundary nobody was watching. The device will simply work, and the absence of the failure will be your only monument. That has always been fine. The body is joined and held together by every joint with which it is equipped — and the joints are where the work is.
See you next week.
Up next: Work the exercises end to end — they are the conditioning, not homework. Then open Project 6 and build the integration map. Set up your workbench with Appendix A, read block diagrams with Appendix B, log every run per Appendix C, and check the disclosure format in Appendix D. Anything whose name you cannot place is in Appendix E. After that, Chapter 7 — trust in silicon, from the root of trust upward.
Previously: Chapter 5 — the budget that governs everything: power, battery, and the thermal envelope that makes a peak number a claim about a chassis.
Week 6 Knowledge Check
qos_arbiter.py at 24,000 MB/s produced these two rows. Nobody was treated unfairly — the display was granted 3466 MB/s against a peak of 3600, a shortfall under 4%. What actually follows for the user?master class need peak granted burst ms deadline verdict
display hard-real-time 2400 3600 3466 8.6 8.3 DEADLINE MISS
camera hard-real-time 1600 2400 2310 17.3 16.7 DEADLINE MISS irq_vs_poll.cpp reports a crossover near 186,000 events per second at a 3 microsecond interrupt overhead. A driver rewrite fattens the handler until the overhead doubles. What happens, and why should a platform team care?./irq_vs_poll 6.0 moves the crossover from about 185,897 to about 119,342 events per second, a fall of roughly a third. The crossover is a number, not a matter of taste — you compute it from four parameters — and it is a property of your driver, not of the universe. The latency distractor is the sharp one: reported latency barely moves, because it is dominated by the wake-up out of a deep idle state, not by the handler. So a sloppy driver drags its device into the polling regime early and bills the whole platform for it, and no one profiling that driver alone would see the charge. policy bytes active s tail s joules
unbatched 240K 0.098 480.0 242.48
compressed -50% 120K 0.049 480.0 242.42
batched 240K 0.098 8.0 15.92