AMBA APB · Module 8
Multiple Wait Cycles
N>1 wait cycles and wait policy — fixed versus variable counts, the critical bounded-versus-unbounded distinction, the unbounded-wait hazard that hangs the whole bus, the bridge-level timeout that aborts a stuck access, and the head-of-line blocking a slow access imposes on an unpipelined APB.
You already know that a subordinate inserts a wait by holding PREADY low for one access cycle, and exactly how that one-cycle hold works mechanically. This chapter is about what happens when one is not enough: an access that needs many wait cycles. The single idea to carry: one wait cycle is benign, but N waits force three design questions that one wait never raised — how many (fixed or variable), is the count bounded (guaranteed to finish, or possibly never), and what happens if PREADY never comes. The last question is the dangerous one: on a bus with no timeout, an unbounded wait does not merely cost throughput — it can hang the entire bus and every CPU behind it, forever.
1. Problem statement
The problem is completing an access whose subordinate cannot be ready for many cycles — without letting "many" become "never," and without one slow access wedging the whole bus.
A single wait cycle is a solved problem: hold PREADY low for one access edge, re-present address and data, complete on the next. But real peripherals routinely need far more than one cycle of latency:
- A subordinate behind a clock-domain crossing must synchronise the request into a slower domain, do the work, and synchronise the response back — often a handful to dozens of
PCLKcycles. - A slow flash or OTP controller may take tens to hundreds of cycles to return read data.
- A contended SRAM or shared resource stalls a variable, traffic-dependent number of cycles until it wins arbitration.
So the access must be extended by N wait cycles, where N may be a fixed constant, a bounded variable, or — in the failure case — unbounded. Extending the access is the easy part; the engineering problem is the policy around N: guaranteeing it terminates, and surviving the case where the subordinate is broken and PREADY never asserts at all.
2. Why previous knowledge is insufficient
You arrive here with the per-cycle mechanics already solid. Module 8.4 — transfer-extension mechanics showed exactly how a single PREADY-low cycle holds the access: PENABLE stays high, PADDR/PWDATA are re-presented, the manager FSM does not advance. Module 8.1 — why wait states exist established why a subordinate ever needs to stall, and Module 8.2 — slow-peripheral behavior looked at the latency sources. That knowledge is correct — and insufficient for three reasons:
- N waits is the hold repeated, not a new mechanism — so the mechanics are not the interesting part anymore. Each of N wait cycles is mechanically identical to the one cycle you already understand: hold
PENABLE, re-present, do not advance. Re-deriving the per-cycle behaviour N times teaches nothing. The new content is everything around the count. - The single-wait view never asks whether the wait terminates. One wait cycle is, by construction, bounded — it ends next cycle. As soon as N can vary, you must ask: is N bounded by a known worst case, or can the subordinate wait forever? That bounded-versus-unbounded distinction simply does not exist for a one-cycle hold, and it is the central idea of this chapter.
- The single-wait view is local; long waits are a system problem. APB has no pipelining — one access occupies the bus until it completes. A one-cycle wait is invisible at the system level; a hundred-cycle wait blocks every other access behind it (head-of-line blocking), and an unbounded wait blocks them forever. The cost of a wait is no longer a per-transfer latency number — it is a shared-bus availability question.
So the model to add is not mechanical. It is the policy and system layer: counting, boundedness, the unbounded-wait hazard, and the timeout that mitigates it. (When a careless implementation of all this produces a hang as an outright bug — the wrong default PREADY, a counter that wraps — that catalogue lives in Module 8.6 — common PREADY design bugs.)
3. Mental model
The model: an APB access is a single-track railway tunnel, and a wait cycle is the train sitting still inside it. The bus is one track — APB has no pipelining, so only one train (access) is in the tunnel at a time, and nothing else moves until it clears. A slow subordinate is a train that idles in the tunnel for N cycles before moving on. That is fine if you know it will move. The danger is a train that has broken down: it will never leave, and because there is only one track, the entire line behind it — every CPU access queued for this bus — is stopped indefinitely.
Three refinements make it precise:
- Bounded versus unbounded is the whole game. A bounded subordinate guarantees
PREADYhigh within some worst-case N — a CDC slave with a known synchroniser depth, a flash with a datasheet maximum read latency. An unbounded subordinate offers no such guarantee: it assertsPREADY"when it can," which on a fault (gated clock, unmapped address, internal deadlock) is never. A bounded train always leaves the tunnel; an unbounded one might break down inside it. - Without a timeout, an unbounded wait is a system hang, not a slow transfer. The manager has no built-in escape: while it is in the access phase waiting for
PREADY, it cannot abandon the access. IfPREADYnever comes, the manager waits forever, the bus stays locked, and the CPU that issued the access stalls hard. There is no graceful degradation — the line is simply stopped. - The mitigation is a watchdog at the bus, not in the protocol. Because APB itself has no transaction timeout, the bridge or bus fabric must add one: a counter that starts when the access begins and, if
PREADYis not seen within a programmed limit, aborts the access — force-completing it and returning an error — so a broken subordinate produces a clean error response instead of a dead system. That watchdog is the signalman who, after a fixed wait, declares the broken-down train abandoned and clears the track.
4. Real SoC implementation
In silicon, this is two pieces of RTL on opposite ends of the access: the subordinate that produces the N-cycle wait, and the bridge watchdog that survives the case where the subordinate never finishes. The subordinate side is a small counter or a CDC-driven done flag; the bridge side is the safety net every robust SoC fabric carries.
// ============================================================
// (A) Subordinate side: a FIXED-N wait — counts N cycles, then ready.
// Used for a peripheral with a known, constant latency (e.g. a CDC
// pipe of known depth, or a fixed-latency math block). N is BOUNDED.
// ============================================================
localparam int WAIT_N = 5; // worst-case is exactly this — bounded
logic [3:0] wcnt;
always_ff @(posedge pclk or negedge presetn) begin
if (!presetn) begin
wcnt <= '0;
pready <= 1'b0;
end else if (psel && penable && !pready) begin
// We are in the access phase and have not yet completed: count down.
if (wcnt == WAIT_N - 1) begin
pready <= 1'b1; // last wait edge -> complete next edge
wcnt <= '0;
end else begin
wcnt <= wcnt + 1'b1; // still waiting; PREADY stays low
end
end else begin
// Idle or just completed: default PREADY low, reset the counter.
// WHY low when idle: this slave only drives PREADY when selected; the
// bus mux must default an UNSELECTED slave's PREADY high elsewhere so
// the bus never hangs on a slave that isn't the target.
pready <= 1'b0;
wcnt <= '0;
end
end
// For a VARIABLE-but-BOUNDED wait (e.g. CDC), replace the counter with a
// synchronised 'done' pulse — PREADY follows it, and the worst-case cycle
// count is still bounded by the synchroniser depth + the far-side latency:
// assign pready = sel_access & done_sync; // done_sync guaranteed to arrive
// ============================================================
// (B) Bridge / bus-fabric side: a TIMEOUT WATCHDOG.
// This is the engineering response to UNBOUNDED waits. APB has no
// transaction timeout of its own, so the bridge adds one: if PREADY is
// not seen within TIMEOUT_LIMIT cycles of the access starting, ABORT.
// Without this, a dead subordinate (gated clock, unmapped addr, deadlock)
// hangs the bus and every master behind it -- forever.
// ============================================================
localparam int TIMEOUT_LIMIT = 1024; // generous: > any legal bounded wait
logic [10:0] tcnt;
logic timeout_fire;
always_ff @(posedge pclk or negedge presetn) begin
if (!presetn) begin
tcnt <= '0;
end else if (penable && !pready_in) begin
tcnt <= tcnt + 1'b1; // count only while genuinely waiting
end else begin
tcnt <= '0; // reset on completion or when idle
end
end
// Fire one cycle when the limit is hit and PREADY still has not arrived.
assign timeout_fire = penable && !pready_in && (tcnt == TIMEOUT_LIMIT - 1);
// The bridge force-completes the access and reports an error to the master,
// so a hung subordinate becomes a clean error response, NOT a system hang.
assign pready_out = pready_in | timeout_fire; // unblock the manager
assign pslverr_out = timeout_fire ? 1'b1 : pslverr_in; // signal the abort as an errorTwo facts drive the real design. First, the subordinate's job is to keep the wait bounded: a fixed-N counter is trivially bounded; a CDC done flag is bounded by the synchroniser depth and the far-side worst case — what is not acceptable is a PREADY that depends on a condition with no guaranteed arrival. Second, the bridge's job is to assume the subordinate might be broken anyway: TIMEOUT_LIMIT is set comfortably larger than any legal bounded wait, so it never fires on a slow-but-correct access, but it always fires on a dead one — converting an unbounded hang into a PSLVERR error response the CPU's bus-fault handler can act on. The interaction with the error path matters: a timeout abort is an error, so it rides the same PSLVERR mechanism a real slave error uses — the manager cannot tell "the slave said error" from "the slave never answered and we gave up," and for system survival it does not need to.
5. Engineering tradeoffs
The real decision is what wait policy a subordinate offers, because that — not the raw cycle count — determines the system risk and the mitigation you are forced to add.
| Wait policy | What it guarantees | System risk | Required mitigation |
|---|---|---|---|
| Zero-wait (always ready) | Completes in the first access cycle, every time | None — no blocking beyond the one access | None |
| Fixed-N (constant count) | Exactly N cycles, always — fully deterministic | Bounded head-of-line blocking of N cycles | None for correctness; budget N in latency analysis |
| Bounded-variable (CDC, datasheet-max flash) | PREADY high within a known worst-case N_max | Bounded but larger blocking; jitter in latency | None for correctness; size buffers/timeout above N_max |
| Unbounded (no completion guarantee) | Nothing — may assert PREADY "eventually," or never | Total bus hang if it never completes; CPU stalls hard | Mandatory bridge/bus timeout that aborts to PSLVERR |
| Faulty (gated clock / unmapped / deadlock) | Never asserts PREADY | Permanent hang of the whole shared APB | The timeout above is the only thing that saves the system |
The throughline: as you move down the table, the guarantee weakens and the required mitigation strengthens. A zero-wait or fixed-N slave needs nothing but a latency budget. A bounded-variable slave needs sizing but is still safe. The moment a slave is unbounded, a bus-level timeout stops being a nice-to-have and becomes the single mechanism between "one peripheral is broken" and "the entire SoC is wedged." And because any slave can become faulty in silicon (a clock-gating bug, an address-map hole), a robust fabric carries the timeout regardless of what its slaves claim — defence in depth.
6. Common RTL mistakes
7. Debugging scenario
This is the classic bring-up bug: the chip boots, runs for a while, then locks up hard the instant firmware touches one peripheral's register — and the lock-up is total, no further instructions retire.
- Observed symptom: during bring-up, the CPU hangs completely on a specific register access — a read or write to one peripheral never returns, the core's bus interface is stuck busy, the watchdog (if any) eventually resets the chip, and the failure is 100% reproducible the moment firmware accesses that address. Every other peripheral works.
- Waveform clue: on the APB trace (Figure 2, lower case), the access starts normally —
PSELandPENABLEassert,PADDRholds — butPREADYnever rises.PENABLEstays high indefinitely; the manager FSM is frozen in the access phase. Crucially, the peripheral's ownPCLKis flatlined — its clock was gated off (a power-management default left it disabled), so itsPREADYlogic never even runs. - Root cause: the peripheral's clock was gated off at reset and firmware accessed its registers before un-gating it. With no clock, the subordinate can never produce
PREADY— it is an unbounded wait that will never complete. And the bus bridge had no timeout, so the manager waited forever, hanging the whole shared APB and the CPU behind it. The bug is not the gated clock alone (a sequencing mistake firmware could fix) — it is that there was no bus-level escape when the slave failed to answer. - Correct RTL: add the bridge timeout watchdog from Beat 4 so a never-asserting
PREADYaborts toPSLVERRafterTIMEOUT_LIMITcycles, converting the hang into a bus-fault exception the CPU can handle; and, as a system fix, ensure clock-gating un-gates a peripheral's clock before its address range is made accessible (or have the access to a clock-gated slave decode as an error region). The timeout is the robustness fix; the clock sequencing is the root-cause fix — a hardened SoC wants both. - Verification assertion: prove every access completes within a bound, so this hang fails in simulation instead of silicon:
// Bounded-completion: once in the access phase, PREADY must assert within
// TIMEOUT_LIMIT cycles. A subordinate that never completes fails HERE,
// in sim, instead of hanging the chip in the lab.
property p_pready_bounded;
@(posedge pclk) disable iff (!presetn)
(psel && penable && !pready)
|-> ##[1:TIMEOUT_LIMIT] pready; // PREADY must arrive within the bound
endproperty
a_pready_bounded: assert property (p_pready_bounded)
else $error("APB access did not complete within %0d cycles -- unbounded/hung slave", TIMEOUT_LIMIT);- Debug habit: when a system locks up hard on a register access and the trace shows
PENABLEhigh withPREADYstuck low forever, do not start by debugging the FSM — first ask "is the target slave even clocked, and does the bus have a timeout?" A flatlined peripheral clock plus a missing bus watchdog is the signature of this hang. The instinct to build: any access to a possibly-unbounded slave must sit behind a timeout, or one broken peripheral takes the whole bus down.
8. Verification perspective
Long and unbounded waits are a liveness problem — the bug is "something that should happen never does" — so verification must go beyond sampling values and prove completion within a bound, then deliberately break that bound to confirm the timeout catches it.
- Assert bounded completion, always. Bind the
p_pready_boundedproperty (Beat 7) to every APB interface so any access that fails to complete within the worst-case bound fails in simulation. This is the single most valuable check here: it converts an in-silicon hang into a sim failure with a timestamp. Pair it with the basic stability checks (PADDR/PWDATAheld across the waits,PENABLEheld high) so a "completion" that arrives via corrupted control is also caught. - Cover the wait-count distribution, including the corners. Functional coverage must bin the number of wait cycles per access, not just "waited / did not wait": hit 0 (zero-wait), 1 (single wait), a typical N, and the worst-case maximum the slave claims — plus a bin that confirms the timeout itself fired. A wait-state suite that only ever injects one wait has a hole exactly where the multi-wait and boundary bugs live (counter wrap at N_max, off-by-one on the last wait edge).
- Inject randomized waits and force the hang. Drive the subordinate model with randomized, bounded wait counts (and back-to-back accesses with differing counts) to stress the manager's hold logic across many N. Then, in a directed test, deliberately model an unbounded / never-ready slave and assert that the bridge timeout fires, the access aborts to
PSLVERR, and the bus recovers (the next access proceeds) — i.e. verify the hang-detection path, not just the happy path. Hang detection that is never tested is hang detection you do not have.
The point: for wait states, "passes" must mean "every access provably completes (or is provably aborted)," which is a liveness-and-recovery claim — so the verification plan specifies bounded-completion assertions, full wait-count coverage including the maximum, and an explicit test that the timeout rescues a deliberately hung bus.
9. Interview discussion
"What happens if a slave never asserts PREADY?" is a senior-screening question because the shallow answer ("the access waits") misses the entire point: on a non-pipelined bus with no timeout, the access waits forever, and so does every master behind it. A strong answer separates the cases and names the mitigation.
Frame it as bounded versus unbounded. A bounded wait — fixed-N or a CDC slave with a known worst case — is just latency: you budget it and move on. An unbounded wait is a different animal: with no completion guarantee, a faulty subordinate (gated clock, unmapped address, deadlock) can hold PREADY low forever, and because APB has no pipelining and no transaction timeout, the manager is stuck in the access phase indefinitely, the shared bus is locked, and the CPU hangs hard. Then deliver the depth: the mitigation lives at the bridge, not in the protocol — a watchdog counter that aborts a stuck access to PSLVERR after a programmed limit, which is why a robust fabric carries that timeout regardless of what its slaves claim (defence in depth). Close with the system insight that separates seniors: on a shared APB, one slow access head-of-line-blocks all others — there is no overtaking lane — so long waits are a bus-availability decision, not a per-peripheral one, and the classic bring-up hang ("system locks up on a register access") is exactly this bug: a never-ready slave and a missing bus timeout.
10. Practice
- Repeat the hold. Draw an access extended by three wait cycles. Show
PENABLE,PADDR/PWDATA, andPREADYacross all four access edges, and state what is identical on each wait edge to convince yourself N waits is the single hold repeated. - Classify the policy. For each of zero-wait, fixed-5, a CDC slave with a 4-stage synchroniser, and a slave whose
PREADYwaits on an external handshake with no timeout — state whether it is bounded or unbounded and what mitigation (if any) the system needs. - Size a timeout. Given a fabric whose slowest legal bounded slave can wait up to 300 cycles, choose a
TIMEOUT_LIMITand justify why it must be comfortably above 300 yet finite — what breaks if it is 250, and what breaks if it is infinite. - Write the watchdog. From memory, write a bridge timeout that aborts a stuck access to
PSLVERRafter a limit, and explain why it counts only while genuinely waiting (PENABLE && !PREADY) and resets on completion. - Reason about head-of-line blocking. On a shared APB with three slaves, slave A is mid-access with a 200-cycle wait. State what happens to a CPU access targeting slave B issued during those 200 cycles, and why APB cannot overlap them.
11. Q&A
12. Key takeaways
- N waits is the single-cycle hold repeated N times — the mechanics are unchanged. Nothing new happens per cycle; the new content is the policy: how many, is it bounded, and what if
PREADYnever comes. - Bounded versus unbounded is the central distinction. A bounded subordinate guarantees
PREADYwithin a known worst-case N (fixed counter, CDC depth); an unbounded one offers no guarantee and, on a fault, may never assertPREADYat all. - An unbounded wait with no timeout is a system hang, not a throughput cost. APB has no pipelining and no transaction timeout, so a never-ready slave freezes the manager in the access phase, locks the shared bus, and hangs every master behind it (head-of-line blocking).
- The mitigation is a bridge/bus-level watchdog. A counter started at access begin that, on hitting its limit, aborts the stuck access — force-completing it and asserting
PSLVERR— turning a dead subordinate into a recoverable bus fault. It is mandatory for unbounded slaves and is carried as defence-in-depth on any robust fabric. - Size the timeout above the slowest legal bounded wait, never inside it, so it never fires on a slow-but-correct access but always rescues a hung one — and verify it: bounded-completion assertions, wait-count coverage including 0/1/N/max, randomized wait injection, and an explicit test that the timeout aborts a deliberately hung bus and the bus recovers.