How a core learns to sleep, wake on cue, and delegate.
Nine terms — WFI, WFE, interrupts, ISR, callback handlers, mailboxes, SCMI, SCMI mailbox, and DMA — are usually taught as a disconnected glossary. They are not. They are one continuous mechanism, told at rising altitudes, that answers a single question:
A core is fast and expensive. Most of the time it has nothing to do. How does it stop burning energy, get woken at exactly the right instant, respond in microseconds, coordinate with other processors, speak a shared language, and offload the grunt work — all without spinning a single wasted cycle?
◇ STORY boxes give each concept a structurally-accurate analogy; ⊕ PRECISION boxes hold the exact spec-level detail (and flag the two corrections from the accuracy pass). The animations are interactive — step, auto-run, hover, and race them.
SoC Coordination Fabric · ambient viewidle
Layer 0 · Physics
The energy floor that forces everything above it
Every primitive on this page exists to dodge one physical bill: dynamic switching energy. If you understand why a core wants to stop, every instruction and protocol above becomes inevitable rather than arbitrary.
◇The idling engine
A 3 GHz core spinning on a status flag is a car idling at a red light: the engine burns fuel at full rate to move you exactly nowhere. Clock-gating is the engine's start-stop system — cut fuel to the cylinders the instant you're stationary. Power-gating is switching the car fully off.
But a car that's off needs a working battery and starter to come back. That single question — "how do I restart, and who turns the key?" — is the seed of every wake mechanism on the rest of this page. WFI, WFE, interrupts, the GIC, mailboxes: all of them are starters for a core that chose to go quiet.
A CMOS gate dissipates dynamic power roughly as P = α · C · V² · f — activity factor α, switched capacitance C, supply voltage squared, and clock frequency. Two terms in that equation are levers the system can pull in microseconds: if the core has nothing useful to do, you can drive α toward zero by clock-gating (freeze f locally so flip-flops stop toggling), or you can attack the whole expression by power-gating (collapse V to a retention rail or to zero).
But here is the constraint that propagates all the way up the ladder: a gated core is a deaf core. A clock-stopped pipeline cannot poll a flag. A power-collapsed core has lost its register state. So the moment you decide to save energy by stopping, you create a new problem — how does the silicon know when to start again, and who tells it? That single question is the seed of WFI, WFE, interrupts, the GIC, mailboxes, and SCMI. They are all answers to "how do I wake a core that chose to go quiet."
The win
A busy-wait loop checking a flag can burn the full active power of a 3+ GHz core. Idling instead can drop a cluster's power by one to two orders of magnitude — the difference between minutes and hours of battery, or a server rack staying inside its thermal envelope.
The cost
Entry/exit isn't free. Clock-gating wakes in nanoseconds; deeper states that flush caches and collapse rails cost microseconds and re-warming penalties. Choosing a state too deep for a short idle loses energy — the "race-to-idle vs. stay-shallow" dilemma.
⊕ Precision
P = α·C·V²·f counts only dynamic (switching) power. Modern process nodes also burn static leakage — subthreshold and gate current that flows whenever the rail is up, doing no work. Clock-gating drives α toward zero but does nothing to leakage; only power-gating (collapsing V) removes it.
That asymmetry is the deeper reason for race-to-idle: a merely clock-gated core still leaks, so for a long enough idle, the energy-optimal move is to finish quickly and fully power off — accepting the re-warming cost — rather than sit shallow and leak.
Layer 1 · The ISA
The two instructions that say "stop me"WFI · WFE
WFI (Wait For Interrupt) and WFE (Wait For Event) are the architectural hooks that let software request the low-power state Layer 0 made desirable. They differ in exactly one thing: what is allowed to wake you.
◇Two kinds of nap
WFI is "wake me when the fire alarm goes off." You don't care which alarm or why — any interrupt pulls you straight up. It's the right nap when you have genuinely nothing to do and just want the next event, whatever it is.
WFE is "wake me when my pager buzzes" — a specific, software-coordinated signal. And the Event Register is a sticky note on your forehead: if someone buzzes the pager while you're still settling into the chair, the note stays stuck, so when you finally close your eyes you immediately notice it and don't sleep through the buzz. SEVL is writing that note to yourself before you sit down — guaranteeing your very first check happens.
Both are hints, not commands. The Arm architecture permits a PE (processing element) to treat them as a NOP, or to enter a low-power standby state where the clock — and on some implementations more — is gated. The instruction is the software's permission slip; the power-management hardware decides how deep to actually go.
WFI — wake on interrupt
Execute WFI and the core enters standby until a physical interrupt becomes pending (IRQ, FIQ, or SError), or a debug/reset event arrives. The subtle and important detail: a pending interrupt wakes the core even if that interrupt is masked in PSTATE. Masking only governs whether the interrupt is taken; the wake-up is a separate signal. This is the classic idle path: the OS idle loop, with nothing runnable, executes WFI and parks the core until the next device or timer interrupt.
WFE — wake on event
WFE wakes on a strictly larger set of conditions, gated through a one-bit, sticky, per-PE Event Register. WFE first checks that bit: if it's already set, WFE clears it and falls straight through (no sleep). If it's clear, the core sleeps until any of these set it:
another PE executes SEV (Send Event) — sets the Event Register on every PE in the system;
the local PE executes SEVL (Send Event Local) — sets only its own, used to prime the first iteration of a wait loop and dodge a lost-wake race;
the exclusive monitor is cleared — when some other agent's store lands on an address you were tracking with a load-exclusive. This is the mechanism that makes WFE-based spinlocks work without anyone explicitly sending an event;
an interrupt becomes pending (same as WFI), or the architected event stream fires (the generic timer can be configured to emit periodic events so a WFE can never sleep longer than, say, 100 µs).
Why this distinction matters in real firmware: WFI is for "I have genuinely nothing to do, wake me when the world changes." WFE is for "I'm waiting for one specific, software-coordinated thing — a lock, a flag, a peer — wake me cheaply when a peer signals." A naive lock spins and burns power; the architected idiom below sleeps between contention events.
Core power state · WFI vs WFERUNNING
running (full power)standby (clock-gated)wake signal
A running core burns full dynamic power. Put it to sleep, then choose how to wake it.
; Arm64 ticket-style spin: sleep between contention events, never busy-burn
spin: ldaxr w1, [x0] ; load-exclusive: arm the monitor on the lockcbnz w1, wait ; locked? go wait for an eventstxr w2, w3, [x0] ; try to take itcbnz w2, spin ; lost the race, retryret; got the lock
wait: wfe; SLEEP — woken when the owner's release storeb spin ; clears OUR exclusive monitor: that IS an event.; unlock side: stlr wzr,[x0] <- the store alone wakes WFE waiters;; no explicit SEV is needed for an exclusive-based lock.
WFE buys
A contended lock costs almost nothing while waiting — the waiter is clock-gated, not spinning. Power and the memory-system traffic of repeated polling both collapse.
WFE risks
Lost-wake races: if you check a flag, then someone sets it, then you WFE, you sleep forever. The Event Register's sticky bit plus SEVL priming exist precisely to close that window — get the ordering wrong and you deadlock.
◆ Forward look
On modern Arm cores (Armv8.7+) the WFET/WFIT variants add a timeout operand, letting firmware bound a sleep without arming a separate timer interrupt — tightening the latency/energy trade-off on the hot idle path that power-management firmware lives in.
⊕ Precision · the SEV myth
A common over-statement is that releasing a lock requires an explicit SEV to wake WFE waiters. For an exclusive-based lock it does not. The waiter armed the global exclusive monitor with LDAXR; the owner's release store to that address clears the monitor, and a monitor-clear is itself a WFE wake event. The store alone is sufficient.
Explicit SEV/SEVL is for the cases the monitor can't cover: waiters spinning without exclusives, or waking a waiter that armed its monitor on a different address than the one you wrote. And SEVL specifically closes the first-iteration lost-wake window — demonstrated below.
The lost-wake race · why the Event Register is stickypick a mode
Two timelines run in parallel. The signaler fires between the waiter's check and its WFE — the classic race. Compare a naive transient signal against Arm's sticky Event Register.
Layer 2 · Asynchrony
Interrupts — injecting "now" into a sequential machine
A CPU executes one instruction stream, in order. The world is asynchronous and arrives whenever it likes. An interrupt is the hardware that splices an external "something happened" into that orderly stream — and it's the very signal that wakes a WFI-parked core.
◇The executive's receptionist
Polling is a busy executive walking to the lobby every thirty seconds to ask "anyone here for me?" — exhausting, and a visitor who arrives just after a check waits a full cycle. The GIC is the receptionist who fixes this. Visitors sign in (interrupt becomes pending), she ranks them by importance (priority), and taps the executive only for the one that matters — even pulling them out of a meeting if someone more important arrives (preemption). When the executive is done, they tell her "show them out, next" (EOI).
The three kinds of caller map exactly: SPIs are walk-in visitors routed to whichever core is free; PPIs are the executive's own recurring calendar (the per-core timer); SGIs are hand-carried notes from one executive to another — inter-processor interrupts.
Without interrupts your only option is polling: loop, read a status register, repeat — which is exactly the energy-wasting busy-wait Layer 0 told us to avoid, and it adds latency proportional to your poll interval. An interrupt inverts the control flow: the device raises a line, and the CPU — wherever it was — saves its place and diverts to a handler. Latency becomes the hardware's signal-propagation time, not your software's loop period.
On Arm, the traffic controller is the GIC
You can't let every device yank the core's exception input directly — you'd have no prioritization, no routing across many cores, no way to mask selectively. The Generic Interrupt Controller (GICv3/v4) is the arbiter. Three pieces:
Distributor (GICD) — the global router. Owns SPIs (Shared Peripheral Interrupts, IDs 32–1019): which core gets each one, at what priority, with what enable.
Redistributor (GICR) — one per core. Owns the per-core PPIs (Private Peripheral Interrupts, 16–31, e.g. the per-core timer) and SGIs (Software Generated Interrupts, 0–15, used for core-to-core IPIs).
CPU interface (ICC_* system registers) — the per-core gate that actually presents an interrupt to the PE, enforces the priority mask, and handles acknowledge and end-of-interrupt.
An interrupt walks a small state machine: Inactive → Pending (asserted) → Active (the core acknowledged it by reading ICC_IAR1_EL1, which returns the interrupt ID) → Inactive again only after the core writes ICC_EOIR1_EL1 (End Of Interrupt). Priority and the running-priority mechanism let a higher-priority interrupt preempt a lower one mid-handler.
Press “Device fires” to watch an SPI travel from a peripheral, through the distributor and CPU interface, into an exception on the core.
Interrupts buy
Bounded, low latency and zero idle cost — the core sleeps (WFI) and is woken only when there is real work, instead of paying to ask "anything yet?" forever.
Interrupts cost
Determinism erosion and overhead. Every interrupt is a context switch with cache/TLB disturbance; a flood (an "interrupt storm") can livelock a core. Real-time systems cap rates and use threaded handlers to keep worst-case latency bounded.
⊕ Precision · the fourth state & split EOI
The lifecycle has a fourth state the simple diagram omits: Active+Pending. If a level-sensitive interrupt is still asserted when the core acknowledges it, or an edge re-triggers while the handler runs, it sits Active+Pending and re-fires the moment you write EOI — which is why a driver must quiesce the device source before ending the interrupt, or loop forever.
End-of-interrupt can also be split. With ICC_CTLR_EL1.EOImode = 1, writing ICC_EOIR1_EL1 only drops the running priority (re-enabling lower-priority preemption), while a separate ICC_DIR_EL1 deactivates the interrupt. Threaded-IRQ designs use this to re-open the priority gate early, before the slow part of servicing is done.
Layer 3 · The response
The ISR — what actually runs when the core divertsISR
The Interrupt Service Routine is the code the hardware jumps to. Its design is dominated by one rule: you are running with the world stalled behind you, so do the minimum and get out.
◇The doorbell while you're cooking
You're mid-recipe with the stove on and guests waiting (a running task) when the doorbell rings. You bookmark the exact recipe line (ELR) and your state of mind — what's simmering, what's next (SPSR) — then walk to the door (the vector). At the door you do the absolute minimum: sign for the package. You do not unbox it and assemble the furniture inside on the doorstep while everything burns.
"Unbox and assemble" goes on your to-do list for later (the bottom half). Then you return to the precise recipe line you bookmarked (ERET), and your guests never knew you left. The whole reason to be fast: the longer you stand at the door, the longer every other doorbell in the house goes unanswered.
When an interrupt is taken, the PE does a fixed sequence in hardware: it banks the return address into ELR_ELx, the processor state into SPSR_ELx, sets the new PSTATE (masking further interrupts), and jumps to an address computed from the Vector Base Address Register, VBAR_ELx. The Arm64 vector table is 16 entries of 0x80 bytes, indexed by exception type (Sync / IRQ / FIQ / SError) crossed with where it came from (same EL with SP0, same EL with SPx, lower EL in AArch64, lower EL in AArch32).
The entry stub then saves context (the general-purpose registers the handler will clobber), runs the handler body, restores context, and executes ERET to atomically restore PC and PSTATE. Because all of this happens with interrupts masked and a task frozen underneath, the discipline is universal:
Top half (the ISR proper): acknowledge the device, clear the interrupt source, grab the minimum data, schedule the real work. Microseconds.
Bottom half / deferred work: the heavy lifting — softirqs, tasklets, threaded IRQs (Linux), or DPCs (Windows) — runs later with interrupts re-enabled, so it can't extend worst-case latency for everyone else.
An interrupt is pending. Step through exactly what the hardware and the entry stub do, in order.
◆ Forward look
This is why "keep the ISR short" is not folklore but a latency-budget law: time spent in any handler with interrupts masked is time stolen from every other interrupt's worst case. On the power-management firmware you work with, an over-long ISR doesn't just hurt throughput — it can violate the response deadline the OS assumes when it parked a core in WFI.
⊕ Precision · what the 16 slots encode
The vector offset is not just "which exception" — it encodes a 4 × 4 grid: four classes (Synchronous, IRQ, FIQ, SError) crossed with four sources (current EL with SP0, current EL with SPx, lower EL in AArch64, lower EL in AArch32). That's the 16 entries, each 0x80 bytes.
The 0x80-byte budget is deliberately too small to hold a handler — it forces a branch to the real code, which keeps the table compact and cache-friendly. And entry auto-sets the target PSTATE.{I,F,A} masks: the handler runs with asynchronous exceptions masked by construction, which is precisely why "the world is stalled behind you."
Layer 4 · Software glue
Callback handlers — turning one ISR into many behaviours
A callback handler is a function you hand to a lower layer ahead of time, to be invoked later when a condition occurs. It's the software hinge that decouples "an interrupt happened" from "here is the specific thing to do about it."
◇Leave your number at the bakery
Standing at the bakery counter watching the oven is polling. The civilized alternative: leave your phone number and go live your life. The bakery owns the timing — it knows when the bread is ready. You own the reaction — what happens when the call comes. That inversion is the whole idea: the lower layer controls when, your callback controls what.
The registration table is simply the bakery's list of who-to-call, indexed by order. One bakery (one ISR, one mailbox driver, one timer wheel) serves hundreds of customers without any of them knowing about each other.
The vector table gives you a handful of fixed entry points, but a real system has hundreds of interrupt sources. The bridge is a registration table: a software array mapping interrupt ID → function pointer (plus an opaque void *context). The generic top-half ISR reads the acknowledged interrupt ID from the GIC, indexes the table, and calls back into whichever driver registered for that ID. The driver never touches the vector table; it just says "call me when IRQ 47 fires."
/* Registration: a driver hands the framework a callback ahead of time */typedef void (*irq_cb_t)(unsigned id, void *ctx);
struct { irq_cb_t fn; void *ctx; } table[1020];
voidregister_handler(unsigned id, irq_cb_t fn, void *ctx) {
table[id].fn = fn; table[id].ctx = ctx; /* store the hook */
}
/* The single generic ISR — dispatches to whoever registered */voidgic_top_half(void) {
unsigned id = read_iar(); /* ack: who fired? */if (table[id].fn) table[id].fn(id, table[id].ctx); /* CALLBACK */write_eoir(id); /* end of interrupt */
}
The same pattern recurs everywhere above the metal — a timer subsystem invokes your callback when a timeout expires; a mailbox driver (next layer) invokes your callback when a message lands; an SCMI stack invokes your callback when an asynchronous notification arrives. Callbacks are how each layer says "I'll handle the plumbing and the timing; you give me the policy." Inversion of control is the load-bearing idea: the lower layer owns when, you own what.
Callbacks buy
Decoupling and reuse. One ISR, one mailbox driver, one timer wheel serve arbitrarily many clients. Drivers compose without knowing about each other or about the vector table.
Callbacks cost
Indirection that's hard to reason about: callbacks may run in interrupt context (constraints on what they may call), the indirect call dodges the branch predictor, and lifetime bugs — a callback firing after its context is freed — are a classic source of crashes.
⊕ Precision · the context a callback inherits
A callback invoked from the top half runs in interrupt context, and that context is unforgiving: it must not sleep (there's no thread to schedule away), and it must not take a lock that might be held by the very code it preempted (instant deadlock). This isn't a style guideline — it's the structural reason the top/bottom-half split exists.
The top-half callback is the bakery's quick "your order's up" text. The real errand — picking up, paying, carrying home — is deferred to a schedulable context (softirq, threaded IRQ, workqueue) where sleeping and locking are safe again.
Layer 5 · Inter-processor comms
Mailbox & doorbell — how two processors talk
A modern SoC is not one CPU; it's a federation — application cores plus a small System Control Processor and other microcontrollers. A mailbox is the hardware that lets one ring a bell on another. The payload rides in shared memory; the mailbox just delivers the "go look" signal — which arrives as an interrupt.
◇The shared mailbox in the fence
Two neighbors share a mailbox built into the fence between their yards. You drop a letter in (write the shared-memory buffer) — but your neighbor has no reason to wander out and check an empty-looking box. So you also press the doorbell on their porch. The bell carries no words; it means exactly one thing: "go look in the box."
All the meaning is in the letter; all the timing is in the bell. That separation is the trick — because the bell is contentless, the same fence-and-bell can carry a grocery list, a birthday card, or (next layer) a formal request to the building's facilities manager.
Decompose the problem. Two processors share DRAM/SRAM, so passing data is easy: writer stores it in an agreed buffer. The hard part is notification with synchronization — the reader must learn the data is ready, and the two sides must not stomp each other. Polling shared memory works but reintroduces the Layer-0 energy waste. So hardware gives you a doorbell: a register the sender writes; the write sets a bit that asserts an interrupt on the receiver's core. The receiver's ISR (Layer 3) dispatches a callback (Layer 4) that reads the shared buffer.
Arm's reference IP is the MHU (Message Handling Unit): a set of channels, each with a register whose write raises a receiver interrupt and which the receiver clears to re-arm. The canonical handshake:
Step the handshake: write payload to shared memory, ring the doorbell, the receiver is interrupted, it reads the buffer and rings back.
Notice what just composed: the mailbox is built out of shared memory + an interrupt + an ISR + a callback. Every lower rung of the ladder is load-bearing here. What's still missing is meaning — the bytes in that shared buffer are just bytes until both sides agree on a format. That agreement is the next layer.
⊕ Precision · the doorbell is just a bit
The MHU doorbell is genuinely contentless — typically a single register bit whose write asserts the receiver's interrupt line; the receiver clears the bit to re-arm. Multiple bits in the register are simply multiple independent doorbells on one channel.
Because the bell and the data are fully decoupled, the MHU is protocol-agnostic: it has no idea whether the shared buffer holds SCMI, RPMsg, or a vendor scheme. SCMI is just one tenant renting the same fence-and-bell.
Layer 6 · The protocol
SCMI — the shared language for power & system control
The System Control and Management Interface (Arm DEN 0056) is the standardized message format and command set spoken over that mailbox. It lets a non-trusted agent (an OS, a hypervisor, another core) ask a trusted platform (the SCP firmware) to change clocks, voltages, power domains, and performance levels — without the agent ever touching the underlying PMIC or clock-tree registers directly.
◇The facilities request desk
You're a tenant who wants the office colder. You cannot walk into the basement and flip the building's breakers yourself — you'd fight other tenants, exceed the main's limit, and maybe trip the whole floor. Instead you fill out a standardized form at the facilities desk (the SCP): which department it's for (protocol_id), the specific request (message_id), and your ticket number so you can match the reply (token).
Facilities reconciles everyone's forms against the building's real limits, then actually moves the hardware. SCMI is that form; the SCP is the only one allowed near the breakers. This is the same separation of policy from mechanism as a callback — raised to a whole-SoC safety contract.
Why centralize this at all? Power and clock resources are shared across many requesters and are safety- and stability-critical. If every OS, secure-world service, and coprocessor wrote voltage registers directly, they'd race and brick the chip. SCMI makes the SCP the single arbiter: agents express intent ("I want performance level 5 on this domain"), and the platform firmware reconciles all requests, enforces limits, and actually programs the hardware. This is the same separation-of-policy-and-mechanism idea from Layer 4, raised to a whole-SoC contract.
The protocol family
SCMI is split into independent protocols, each with its own ID, so a platform implements only what it supports:
Base (0x10) — discovery: vendor, version, which protocols exist.
Power domain (0x11) & System power (0x12) — turn domains on/off, suspend/shutdown.
Performance (0x13) — DVFS. The OS's cpufreq governor lives here, requesting performance levels that map to operating points (frequency + voltage).
Clock (0x14), Sensor (0x15), Reset (0x16), Voltage (0x17) — plus newer additions (pin control, power-capping).
Anatomy of a message header
Every SCMI message begins with a 32-bit header. Hover the fields:
message_id selects the command; message_type marks command / delayed-response / notification; protocol_id routes to the right handler; token matches a reply to its request.
The four message types map onto the call patterns you'd expect: a command (request), an immediate response, a delayed response (the platform says "working on it" and notifies completion later — itself delivered through the doorbell/ISR/callback chain), and unsolicited notifications (e.g. "performance was limited because the chip got hot").
⊕ Precision · the message_type field
The original explainer fudged the 2-bit encoding. Corrected:0 = Command, 2 = Delayed response, 3 = Notification, and 1 = reserved. An immediate response does not get its own type — it reuses the command's header (type 0) and is correlated to the request by the channel it arrives on.
A delayed response (type 2) is the SCMI realization of "I'll call you back": the platform first returns a quick acknowledgment, frees the channel, and later sends a separate message carrying the result — which travels the full doorbell → IRQ → ISR → callback path on the agent side. Notifications (type 3) carry no token and answer no request; they're the platform volunteering information.
Layer 7 · The binding
SCMI mailbox — protocol meets transport
SCMI is transport-agnostic; the SCMI mailbox is its most common transport binding — the concrete realization of Layer 6's messages over Layer 5's hardware. A region of shared memory carries the header + payload, and an MHU doorbell signals each direction.
◇The form, dropped in the shared mailbox
This layer is just the Layer 6 form delivered through the Layer 5 fence-and-bell — nothing new there. The one genuinely new piece is a single-occupancy sign on the channel: a restroom-style "occupied / vacant" flag (the channel_status free bit).
You check the sign reads vacant, step in (write your message into shared memory), flip it to occupied, and ring the bell. The platform does its work, writes the reply into the same space, flips the sign back to vacant, and rings back. That one bit is the entire concurrency-control story — it's what stops agent and platform writing the buffer at the same time.
The shared-memory channel has a defined layout, and the part that makes it safe is a single channel-status word with a free/busy bit. The protocol is a strict ownership ping-pong:
Agent waits until the channel reads free, then writes the header + payload into shared memory and marks the channel busy.
Agent rings the A2P doorbell (agent-to-platform). This is an interrupt on the SCP, handled by an ISR, dispatched to the SCMI callback.
SCP parses the header, routes by protocol_id to the right protocol handler (here: Performance), executes (reconciles requests, programs the clock/voltage), writes the response back into the same shared buffer, and marks the channel free.
SCP rings the P2A doorbell (or the agent polls the status bit). The agent's ISR fires, its callback reads the response, and matches it to the request by token.
Full SCMI transaction · OS sets a DVFS level via the SCPidle
A concrete request: the OS governor wants the big cluster faster. Walk the entire round-trip — and watch every lower layer of the ladder do its job.
⊕ Precision · the channel layout & the free bit
The shared-memory channel isn't a bare buffer — it has a defined header: a channel_status word (bit 0 = free, bit 1 = error), reserved fields, channel flags, a length, then the 32-bit SCMI message_header, then payload. The ownership protocol is precisely: agent confirms free == 1, writes its message, clears free to 0; platform processes, writes the response into the same region, sets free back to 1.
FastChannels strip both the header machinery and the doorbell: a bare, lockless slot the agent simply overwrites and the platform samples. There's no acknowledgment and no round-trip — you give up "did it happen?" certainty to buy sub-microsecond DVFS retargeting on the governor's hot path.
◆ Forward look · FastChannels
A full doorbell round-trip costs microseconds — fine for power-domain changes, but a DVFS governor may want to retarget frequency every millisecond. SCMI's FastChannel binding sidesteps the handshake: a small lockless shared-memory slot the agent writes directly, polled or sampled by the platform with no doorbell. It trades the protocol's safety/acknowledge guarantees for latency — exactly the kind of quantitative tension this whole stack is built around. Watching where each transport sits on the latency/overhead curve is the heart of tuning a real power-management firmware.
Layer 8 · Offload
DMA — getting the core out of the data path entirelyDMA
Even a sleeping-and-waking core wastes itself if it copies bytes by hand. Direct Memory Access is a dedicated engine that moves data between memory and peripherals (or memory and memory) on its own, freeing the core to sleep (WFI) until the move is done — completion announced, of course, by an interrupt.
◇Hire the movers
Programmed I/O is carrying every box up the stairs yourself, one at a time, pausing at each slow door. DMA is handing a moving company an address list — from here, to there, this much (the descriptor) — and then going to take a nap (WFI). The movers haul everything on the freight elevator (the interconnect) without bothering you, and ring the buzzer when the last box lands (the completion interrupt).
The catch is the punchline of the whole chapter: if you'd memorized the contents of some boxes (cached those lines) and the movers swapped them out, your memory is now stale. Either you re-open and check (cache invalidate), or you hire movers who text you on every change (an I/O-coherent interconnect that snoops your caches).
Consider the alternative — programmed I/O: the core executes a load/store per word. Moving a 1 MB buffer is hundreds of thousands of instructions during which the core does nothing but shuttle data, blocking on every slow peripheral access. A DMA controller flips this. The core acts as a manager: it builds a descriptor — source address, destination address, length, and control flags — programs the DMA channel, and issues "go." The DMA engine then arbitrates for the interconnect (AXI/CHI), bursts the data across while the core does other work or sleeps, and raises a completion interrupt when finished.
DMA transfer · core programs, sleeps, is woken on completioncore: idle
coreDMA enginedata burst / IRQ
Step it: program the descriptor, kick off DMA, the core WFIs, data streams, and a completion interrupt wakes the core.
DMA buys
The core is removed from the byte-by-byte path: it issues one descriptor and is free for thousands of instructions or a deep sleep. Throughput is set by the interconnect, not by instruction issue rate.
DMA costs
Coherency. The DMA engine writes memory behind the core's back — if the core has stale cached copies, you get corruption. You pay with cache-maintenance ops (clean/invalidate) around every transfer, or with an I/O-coherent interconnect (CCI/CMN) that snoops the caches — silicon area and power for the convenience.
⊕ Precision · three answers to coherency
Descriptors can chain (scatter-gather): one "go" moves a list of fragmented buffers, so a network packet scattered across pages becomes a single transfer. The coherency hazard then has three solutions, in rising silicon cost:
1 · Software maintenance — clean the cache before a memory→device write (so the device sees fresh data) and invalidate after a device→memory write (so the core doesn't read stale lines). Cheap hardware, error-prone software. 2 · A coherent interconnect (CCI / CMN) snoops the caches automatically — the movers text you on every change. 3 · An SMMU/IOMMU additionally translates and bounds the device's view of memory, so an untrusted DMA master can't scribble outside its lane — the same policy-vs-mechanism arbitration SCMI applied to power, now applied to memory access.
Synthesis · ∑
One loop, eight layers
Put the whole page in one breath. An OS governor decides the big cluster needs to run faster, then idles a spare core. Watch every term you asked about light up in sequence — none of them is separable from the others:
governor wants DVFS→writes SCMI msg to shared mem→rings mailbox doorbell→doorbell = interrupt on SCP→SCP ISR runs→dispatches callback→SCP programs voltage/clock (maybe via DMA)→rings reply doorbell→agent ISR + callback complete it
meanwhile the spare core executes WFI→clock-gated, near-zero power→next timer/device interrupt wakes it→and the contended lock it wanted uses WFE
The unifying truth, stated once: a core is a precious, power-hungry resource that should be doing useful work or nothing at all. WFI/WFE let it choose nothing. Interrupts, the GIC, ISRs, and callbacks let "nothing" end at exactly the right instant with minimal disturbance. Mailboxes, SCMI, and the SCMI mailbox transport let it coordinate that with the other processors it shares silicon with. And DMA lets it delegate the dumb, bulk work so it can sleep through it. Every constraint at the bottom — switching energy, the sequential pipeline, masked-handler latency, shared power rails, interconnect bandwidth — propagates upward to force the shape of the layer above it. That propagation is the whole game.