AMBA APB · Module 14
Peripheral-Access Cost
The end-to-end, software-visible cost of an APB register access — CPU stall plus bridge overhead plus the 2 + N APB cycles plus propagation — paid synchronously on a read while a posted write may not block. Why peripheral reads are expensive and drivers must minimise them.
You learned that one APB transfer costs 2 + N cycles. But a programmer never issues an APB transfer — they issue a LDR or STR, and the cost they actually pay is the whole round trip from instruction to register: the CPU's issue, the system-bus (AHB/AXI) leg, the bridge's own overhead, the APB 2 + N, and the data's journey back. The single idea to carry: a peripheral read is a synchronous, blocking cost the CPU pays in full — the core stalls for the entire round trip, the load cannot retire until the data returns, and nothing overlaps it — so a single register read can cost tens of CPU cycles, not two. The asymmetry that follows is the punchline: a read stalls the CPU, but a posted write can be buffered at the bridge and let the CPU run on. Cost the whole chain, understand posted-versus-non-posted, and you can explain why drivers are written to read as little as possible.
1. Problem statement
The problem is assigning the true, software-visible cost to one peripheral register access — not the APB-only 2 + N, but the end-to-end cost a CPU instruction pays, from issuing the load to the value landing in a register.
The 2 + N from APB latency anatomy is the cost of the APB transfer in isolation. But that transfer does not float free — it sits at the bottom of a stack of layers, and software pays for all of them:
- The access starts and ends at the CPU, not at the APB. A peripheral read begins as a load instruction the core issues onto the system bus (AHB/AXI), and it is not "done" until the read data is written back into a CPU register so the load can retire. Everything in between — system bus, bridge, APB, return path — is cost the instruction bears.
- Every layer adds its own cycles. The system-bus leg costs cycles, the bridge runs its own SETUP/sequencing before it ever drives the APB, the APB transfer is
2 + Nin the slow PCLK domain, and the data has to propagate back up. The APB2 + Nis one term in a longer sum, and often not the largest. - On a read, the CPU pays it synchronously. A load that targets a peripheral cannot retire until the data returns; the core stalls for the whole round trip with no overlap. So the cost is not "added to the bus" — it is charged to the CPU as dead cycles, which is why a single register read can cost tens of CPU clocks.
So the job is to take a register access and cost the entire chain — CPU issue + system bus + bridge overhead + APB 2 + N + return — and to know that on a read this sum is paid as a CPU stall, while a posted write need not be. That total is the number drivers are written against.
2. Why previous knowledge is insufficient
You already have every piece of this cost; what you have not done is assemble them into the end-to-end number a CPU instruction pays, and reckon with the read/write asymmetry that emerges only at the top of the stack.
- APB latency anatomy gave you
2 + N— the APB leg only. That chapter costed the APB transfer in isolation: one SETUP, one ACCESS,Nwaits, measured inPCLK. It is the foundation, but it is one term. The software-visible cost wraps it in everything above the APB, so2 + Nis the floor of the bottom layer, not the bill. - The bridge was a translation mechanism; here it is a cost contributor. You learned the bridge converts an AHB/AXI transaction into an APB one and runs its own state machine. From a cost seat, that state machine is not free: the bridge spends its own cycles latching the request, sequencing into APB SETUP, and registering the result back — overhead that lands on every access on top of the APB
2 + N. - Wait-state propagation showed how the wait travels up; here it becomes CPU stall cycles. You know the APB slave holding
PREADYlow forces the bridge to extend its upstream response, which back-pressures the system bus. The new step is recognising where that back-pressure lands: on a read, it lands on the CPU as a pipeline stall — the load is stuck until the propagated wait clears. The propagation chain is exactly what turnsNAPB waits into CPU dead cycles. - Single-transfer cost said nothing about reads versus writes — the top of the stack does. Structurally APB reads and writes are both
2 + N. But software-visibly they differ sharply: a read must stall the CPU (the data is needed), whereas a write can be posted — buffered at the bridge and acknowledged early so the CPU runs on. That asymmetry only exists once you look at the whole chain from the CPU's seat, which is precisely this chapter.
This chapter assembles those pieces; the next one, APB system impact, takes the response — how this cost shapes driver structure, DMA offload, and where peripherals belong. Here we establish the cost; there you act on it.
3. Mental model
The model: a peripheral read is a synchronous round trip the CPU makes in person and waits at every door. The load instruction walks down four flights — CPU → system bus → bridge → APB slave — collects the data, and walks all the way back up, and the CPU pipeline is frozen the entire time, because the load cannot retire without the value. The cost is the sum of every leg, paid as a single uninterrupted stall.
Three refinements make it precise:
- The cost is a chain, and you sum the whole chain.
cost = CPU issue + system-bus leg + bridge overhead + APB (2 + N) + return leg. Each term is real cycles. The APB2 + Nis in the slow PCLK domain (often a fraction of the CPU clock), so after clock-ratio conversion it can dominate, and the bridge overhead and propagation are never zero. The naive "a register read is 2 cycles" ignores four of the five terms. - A read stalls; the stall is non-overlappable. Because the CPU needs the data, the load is a blocking operation: it cannot retire, and (on a simple core) the pipeline cannot make forward progress past it, for the whole round trip. There is no prefetch, no pipelining of the peripheral access against later instructions — the cost is charged in full as dead CPU cycles. This is why the same
2 + Nthat looked cheap in PCLK becomes expensive once it is a CPU stall. - A posted write breaks the chain early. A write does not need a value back, so the bridge (or an upstream write buffer) can accept the write, acknowledge it immediately to the CPU, and complete the APB transfer on its own time. The CPU continues after a couple of cycles — it does not wait out the
2 + N. This is a posted (fire-and-forget) write, in contrast to a non-posted write that the CPU waits to fully complete. So reads and writes that are structurally identical on APB have wildly different software-visible costs.
4. Real SoC implementation
In a real driver the cost difference between a read and a posted write is something you feel in cycle counts. The snippet below shows the same peripheral touched two ways — a read that stalls the core for the full round trip, and a posted write that the CPU fires and forgets — with realistic end-to-end cycle costs annotated.
// End-to-end software-visible cost of APB peripheral access.
// Assume: CPU @ 200 MHz, PCLK @ 50 MHz (4:1), N = 4 APB wait states.
// One APB cycle (2+N = 6 PCLK cycles) ~= 24 CPU cycles after the 4:1 ratio.
volatile uint32_t *STATUS = (uint32_t *)0x4000'0000; // APB peripheral regs
volatile uint32_t *CTRL = (uint32_t *)0x4000'0004;
uint32_t poll_status(void) {
// READ: the CPU STALLS for the whole round trip and cannot retire
// the load until PRDATA comes back up through bridge + system bus.
// CPU issue (~2) + sysbus (~2) + bridge (~2) + APB 2+N (~24) + return (~4)
// ~= 34 CPU cycles of dead stall, every time. <-- NOT 2 cycles
return *STATUS; // blocking load: ~34 CPU cycles
}
void kick_engine(void) {
// POSTED WRITE: the store is accepted by the bridge/write-buffer and
// acknowledged early; the CPU continues while the APB 2+N finishes
// off in the background. The store does NOT block on the round trip.
*CTRL = 0x1; // posted store: ~2 CPU cycles, fire-and-forget
// ... CPU keeps executing here while the APB write drains ...
}
// The asymmetry in one routine:
void isr_naive(void) {
uint32_t s = *STATUS; // ~34 cyc (stall)
uint32_t d = *STATUS; // ~34 cyc (stall — second read, no caching!)
*CTRL = process(s, d); // ~2 cyc (posted — does NOT stall)
// total ~70 CPU cycles, of which ~68 are read stall. Reads dominate.
}Two facts make this the right way to see the cost. First, the read cost is a stall, paid in CPU cycles, and dominated by the slow-domain APB term: the 2 + N lives in PCLK, so a 4:1 clock ratio multiplies it into the dominant chunk of a ~34-cycle round trip — the bridge and bus legs are real but small beside it, and none of it is the naive "2." Second, the posted write is cheap precisely because it does not wait: the store is decoupled from the APB completion, so its software-visible cost is just the few cycles to hand it off, not the round trip. The same peripheral, the same 2 + N on the wire, costs ~34 cycles to read and ~2 to (posted-)write — the asymmetry is entirely about whether the CPU has to wait for an answer. (Note: a non-posted write — common for strongly-ordered device memory — does stall like a read, because the CPU waits for the write to fully complete.)
5. Engineering tradeoffs
The end-to-end cost decomposes into stages; here is the bill for a read, and the row that shows why a posted write escapes most of it. (Numbers use CPU @ 200 MHz, PCLK @ 50 MHz, N = 4, illustrative.)
| Stage | Read — CPU cycles | Fixed or variable? | Notes |
|---|---|---|---|
| CPU issue (load onto system bus) | ~2 | Fixed-ish | Core drives address/control to the AHB/AXI fabric |
| System bus leg (AHB/AXI to bridge) | ~2 | Variable (fabric depth) | More on a deep/contended fabric |
| Bridge overhead (own SETUP/sequencing) | ~2 | Fixed-ish | The bridge's own state machine — never zero |
APB transfer (2 + N in PCLK) | ~24 | Variable (N, clock ratio) | (2 + N) PCLK cycles × clock ratio — usually the dominant term |
| Return leg (data back up to CPU register) | ~4 | Variable (fabric depth) | Read data + ready propagate up; load retires |
| Read total (CPU stalls) | ~34 | — | Paid synchronously as dead CPU cycles, no overlap |
| Posted write (CPU does not wait) | ~2 | — | Bridge/write-buffer accepts + acks early; APB 2 + N drains in background |
The throughline: the APB 2 + N is real but it is one stage of five, and on a read the whole sum is charged to the CPU as a stall — tens of cycles, dominated by the slow-domain APB term after clock-ratio conversion. A posted write sidesteps almost the entire bill because the CPU never waits for completion. So the cost is asymmetric by direction: reads are expensive (synchronous), posted writes are cheap (decoupled), and that asymmetry — not the bare 2 + N — is what driver design optimises around.
6. Common RTL mistakes
7. Debugging scenario
The signature bug here is a real-time deadline missed by an ISR that does "just a few register reads" — each of which secretly stalls the CPU for a full round trip, so the routine costs many times its naive budget.
- Observed symptom: an interrupt service routine budgeted at ~50 CPU cycles is measured at ~120+, and the system intermittently misses a hard real-time deadline. Functionally the ISR is correct — it reads the right registers and computes the right result. It is purely too slow, and the slowness scales with how many peripheral reads it does.
- Waveform clue: tracing the CPU pipeline against the bus shows the core stalled for long spans, and each stall aligns exactly with a peripheral load: the load issues, the pipeline freezes, and ~30 cycles later the read data returns and the load retires. Decomposing one stall shows the bulk of it is the APB
2 + Nin the slow PCLK domain (multiplied by the clock ratio) plus bridge overhead — not the 2 cycles the author assumed. The ISR does four such reads (including aSTATUSread repeated, and a needless read-back after a write), so four ~30-cycle stalls serialise. - Root cause: the author costed each peripheral read as a cheap APB
2 + N(or even "2 cycles") and ignored that it is a synchronous CPU stall over the whole round trip. Because reads do not overlap, the four reads add up linearly; the repeatedSTATUSread and the read-back-after-write each pay the full round trip again for data that was already known or did not need confirming. - Correct RTL: there is usually nothing wrong with the hardware — the bridge and slave behave correctly; the waits are legitimate. The fix is in the access pattern: read each register once and cache the value in a local instead of re-reading; delete the needless read-back-after-write (a posted write does not need confirming on the hot path); and where a value is written, issue it as a posted write so the CPU does not stall on it. If reads truly cannot be removed, move them off the ISR's critical path or hand the polling to DMA — the response covered in APB system impact.
- Verification assertion: measure and bound the cost. Add a performance check that the end-to-end access latency for the region never exceeds budget, and a coverpoint that the read-stall path and the posted-write path are both exercised:
cover property (@(posedge cpu_clk) load_to_periph |-> ##[1:$] load_retire);paired withassert property (@(posedge cpu_clk) disable iff(!rstn) (load_to_periph && load_retire_seen) |-> (stall_cycles <= MAX_ACCESS_COST));so a read that blows the access-cost budget trips in regression rather than missing a deadline in the lab. - Debug habit: when an ISR or driver is "correct but too slow," count the peripheral reads and cost each as a full round-trip stall, not as
2 + N. Reads dominate; writes (if posted) are nearly free. The lever is almost always fewer synchronous reads — cache values, drop read-backs, batch, post the writes — never shaving the fixed bridge/bus cycles.
8. Verification perspective
End-to-end access cost is a measured property at the CPU boundary, so verification's job is to measure it correctly, cover both directions of the asymmetry, and regress the worst case before silicon — not just check that the read returned the right value.
- Measure latency at the CPU, not at the APB. The number that matters to software is the stall from load-issue to load-retire (or store-issue to store-accept), so the monitor must straddle the whole chain — CPU issue, system bus, bridge, APB, return — and report the round-trip cost in CPU cycles. A monitor that only times the APB
2 + Nreports the cheapest term and hides the bridge overhead and the clock-ratio multiplier; it will tell you a read costs 6 PCLK cycles while the CPU actually stalls for 34. - Cover the read-stall path and the posted-write path. Functional coverage must include that a peripheral read was seen to stall the CPU until data returned, and that a posted write was seen to let the CPU continue without waiting out the APB completion — plus, if the system supports it, a non-posted write that does stall. Covering only reads, or only writes, leaves the costly half of the asymmetry unmeasured; covering only fast-register accesses hides the worst-case
N. - Regress the worst-case access cost with a budget. For latency-critical regions, assert the end-to-end stall never exceeds the budgeted maximum (
assert (... |-> access_cost <= MAX_ACCESS_COST)) and attribute the cost by stage so a regression points at the culprit — a new bridge stall, a deeper fabric, a higher-Nslave, or a worse clock ratio. Explicitly track the bridge contribution as its own line item, because it is the term most often assumed to be zero. This turns "correct but too slow" into a caught failure and keeps the cost honest as the SoC integrates more peripherals.
The point: time the access from the CPU's seat across the full chain, cover both read-stall and posted-write behaviour (and non-posted writes), and assert a per-region access-cost budget with the bridge contribution called out — so the software-visible cost is regressed, not discovered in the lab.
9. Interview discussion
"How much does it cost the CPU to read a peripheral register over APB?" is a precision filter: the weak answer is "2 + N cycles," the strong answer is the end-to-end round trip charged as a synchronous stall, plus the read/write asymmetry.
State it as a chain: the software-visible cost is CPU issue + system-bus leg + bridge overhead + APB (2 + N, in PCLK) + return leg — tens of CPU cycles for one read, not two. Then deliver the depth: the APB 2 + N lives in the slow PCLK domain, so the clock ratio multiplies it into the dominant term; the bridge runs its own sequencing overhead on top, so it is never free; and on a read the entire sum is paid as a non-overlappable CPU stall — the load cannot retire until the data returns, and the pipeline cannot make forward progress past it. The senior flourish is the asymmetry: a read stalls, but a posted write does not — the bridge (or a write buffer) accepts and acknowledges the write early, so the CPU runs on while the APB 2 + N drains in the background; a non-posted write, by contrast, stalls like a read. Closing with "that is exactly why drivers minimise reads — cache values, drop read-backs, batch, and post writes — because each synchronous read is a tens-of-cycles stall the core pays in full" signals you have actually budgeted real firmware against a real SoC, and sets up the system-impact discussion of DMA offload.
10. Practice
- Cost a read end-to-end. With CPU @ 200 MHz, PCLK @ 50 MHz, and
N = 4, lay out the five stages and give the total CPU-cycle cost of one register read. State which stage dominates and why. - Explain the stall. Describe, cycle by cycle, why the CPU pipeline cannot make progress during a peripheral read, and what "the load cannot retire" means concretely.
- Read vs posted write. For the same peripheral and the same
2 + Non the wire, give the software-visible cost of a read and of a posted write, and explain the difference in one sentence. - Posted vs non-posted. Define posted and non-posted writes, state which one lets the CPU continue, and give one reason a system might require a non-posted write.
- Fix the ISR. An ISR reads
STATUStwice, readsDATAonce, and does a read-back after a control write — and overruns its budget. List the changes that bring it back inside budget and the cycles each saves.
11. Q&A
12. Key takeaways
- The software-visible cost of a register access is the whole round trip:
CPU issue + system-bus leg + bridge overhead + APB (2 + N) + return— tens of CPU cycles for one read, not the bare APB2 + N. - A read is a synchronous, blocking stall. The load cannot retire until the data returns, the pipeline cannot make progress past it, and nothing overlaps it — so the whole chain is charged to the CPU as dead cycles.
- The APB
2 + Nlives in slow PCLK, so the clock ratio multiplies it into the dominant term of the round trip; the bridge overhead and bus legs are real but smaller — and the bridge is never free. - Reads and writes are asymmetric to software. A read stalls; a posted write is accepted and acked early so the CPU runs on while the APB drains in the background; a non-posted write stalls like a read.
- The optimisation lever is fewer synchronous reads — cache values, drop read-backs, batch, post writes, and prefer interrupts/DMA over polling — because each read is a tens-of-cycles stall paid in full.
- Verify access cost at the CPU boundary: measure the full round-trip stall, cover read-stall and posted-write (and non-posted) paths, and assert a per-region access-cost budget with the bridge contribution called out — so "correct but too slow" is caught in regression, not the lab.