Chapter 06 · Week 6

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:

ProtocolShapeWhere it lives
APBSimple, non-pipelined, one transfer at a timeControl registers: timers, GPIO, UART, block configuration
AHBPipelined, shared-bus heritageOlder or modest peripherals; still common inside subsystems
AXIHigh-performance, memory-mapped, burst-oriented, many outstanding transactionsThe main data path — CPU, GPU, NPU, display, camera, storage
ACE / CHICoherent extensions and a scalable coherent protocolCache-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:

  1. volatile is 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.
  2. volatile is not a concurrency primitive. No atomicity, no inter-processor ordering. Between CPUs use std::atomic with real acquire/release semantics. Using volatile for 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.

InterfaceCarriesWhy it looks the way it does
MIPI DSISoC → display panelFew high-speed differential lanes; short reach; low power per pixel
MIPI CSI-2Camera sensor → SoCSame physical family, opposite direction; sensor-paced, no back-pressure
UFSSoC ↔ NAND storageSerial, full-duplex, command queuing; successor to half-duplex eMMC
I²C / SPI / I3CSoC ↔ sensors and small partsCheap pin counts, low rates, simple controllers
USB (Type-C)SoC ↔ the outside worldOne connector for data, power delivery and display alternate modes
PCIeSoC ↔ a discrete high-speed deviceWhere 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.

  1. 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.
  2. 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.
  3. 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:

  1. 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.
  2. 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.
  3. 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.

Interactive Lab — Week 6
The Interconnect Arbiter

§6.2 and §6.9 stop being tables here and start being consequences you caused. Panel 1 hands you one fixed path to DRAM and the seven bus masters from bus-traffic.csv; you choose the policy, the order and the floor, and the widget reports who missed, who starved, and what the person holding the phone would actually see. Panel 2 moves the same bytes on a radio three ways and prices the tail. Predict before you touch a control — that is Appendix C's rule and it is the point of the lab.

Panel 1 · One fixed budget, seven masters, and a policy that chooses a loser

Work it in the chapter's order. One: write down who you expect to lose under strict priority and who under equal shares. Two: reproduce both, then try to find an allocation where nobody misses. Three: when you have failed at two, choose your victim on purpose and defend it in one sentence. Somewhere in there, try biggest consumers first — the policy that feels like good engineering.

24,000 MB/s

Strict priority

0% reserved · no floor

Service order — Raise moves a master up one place. Strict priority walks this list top to bottom.

  1. 1 Display controller hard-real-time
  2. 2 Camera receiver hard-real-time
  3. 3 GPU soft-real-time
  4. 4 Modem DMA soft-real-time
  5. 5 NPU soft-real-time
  6. 6 CPU cluster best-effort
  7. 7 Storage (UFS) best-effort
MasterLatency classSustained need PeakGrantedBurst vs deadlineVerdict

Every row above is read straight from bus-traffic.csv, whose own first line says it is synthetic teaching data modeled on published behaviour, not a measurement of any product. The allocation arithmetic is the same arithmetic qos_arbiter.py runs. Replace these rows with figures you can cite before you use any of it to make a claim about a real device.

Panel 2 · The radio is billed by the second, not by the byte

The same payload, moved three ways. Watch the bytes moved column stay still while the joules move by an order of magnitude. Then try to beat batching with the compression slider. You cannot, and the failure is the lesson.

60 transfers

4 KiB each

every 10 s

20 Mbit/s

8.0 s of tail per wake

50% of the bytes removed

Your schedule

One wake per transfer

PolicyBytes movedActive s Tail sJoulesAgainst unbatched

Modeled, not measured. The power states are radio_energy.py's illustrative defaults — 1.20 W while transferring, 0.50 W during the tail, 0.02 W in the low-power state — and the tail is a slider because real tail timers are operator-configured and vary by network, band and vendor. The 3GPP standards give you the shape (connected, inactive and idle states, with discontinuous reception between listening windows); they do not give you a number you may quote. Replace every parameter here with figures you can cite before this output appears in a write-up.

Check Your Reps

Week 6 Knowledge Check

Question 1 of 5
The weighted-fair run of 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
Why: At 120 Hz a frame is 8.3 ms wide and the panel sets that schedule, not the software. Missing by 0.3 ms is not 4% worse — it is a torn or dropped frame, and there is no retry. The sustained-average answer is the seductive one: an average keeps you alive over seconds, but the deadline is per burst, which is exactly why the arbiter reports the burst time and the deadline in separate columns. Fairness is a bug when one of your clients has a deadline.
Question 2 of 5
A discrete Wi-Fi part runs vendor firmware nobody on your team audited, and it moves packet buffers into DRAM by DMA. Your platform puts an SMMU in front of it. What has that changed?
Why: A DMA-capable device holding a raw physical address can write anywhere in memory, which is a spectacular privilege to hand to firmware you did not write. The SMMU inserts a translation stage per device, which is the difference between "a compromised Wi-Fi firmware can read your DRAM" and "it cannot." The coherency answer is the most tempting distractor because both hazards live in the same section, but they are separate mechanisms: coherency is answered by an IO-coherent interface (or by hand-written clean and invalidate), containment is answered by the SMMU. And the cost framing is only half true — yes it costs a walk, but calling that the whole story misses the security boundary it exists to create.
Question 3 of 5
The model in 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?
Why: Run it: ./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.
Question 4 of 5
This is the same 240 KiB moved three ways over a 598-second window. A teammate proposes a sprint to halve the telemetry payload. Using this model, roughly what will that buy, and why?
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
Why: A radio is not billed per byte; it is billed per second spent awake. Sixty transfers each drag a full 8-second tail behind them, so the model accumulates 480 seconds of tail against 0.098 seconds of transmission — halving the bytes optimizes the 0.098 and leaves the 480 untouched, for 0.0% saved. Batching attacks the tail instead and saves 93.4%. Worse, an expensive compression algorithm can burn more CPU energy than it saves radio energy, so the responsible-feeling optimization can be a net loss. Count wake-ups before you count bytes.
Question 5 of 5
An idle-power regression lands on a build: standby battery life falls badly. One sensor turns out to be reporting at 200 Hz with its hardware FIFO batching disabled. CPU profiling shows almost no application-processor activity. What is going on, and what is the fix?
Why: This is the sensor hub's whole reason for existing, failing. The point of a hardware FIFO is that the hub is notified when a buffer fills, not per sample — one wake every few seconds instead of one every few milliseconds — and disabling it turns a quiet island into a metronome that never lets the platform reach its deep idle state. The profiler distractor is the one that wastes a week: from the CPU's point of view almost nothing happened, so no CPU profiler will ever show you this. Moving the work up to the application processor is exactly backwards; the cheapest work is work that does not require the expensive thing to be awake at all. Idle power is an integration property — audit what is allowed to be awake, then what wakes what.
YOU FINISHED. NICE WORK.