AMBA APB · Module 8
Slow-Peripheral Behaviour
How a real slow peripheral manifests its internal latency on the bus — its busy-to-done state machine driving PREADY, fixed versus variable latency profiles, and how one logical register read becomes a multi-cycle transaction the manager must reason about.
You already know why wait states exist — SRAM access time, clock-domain crossings, gated clocks, arbitration. This chapter is about the bridge: how a peripheral's internal latency actually manifests on the bus as a PREADY waveform. The single idea to carry: from the manager's side, every slow peripheral looks identical — PREADY held low, then high — but what sets the number of low cycles, and whether that number is constant or changes per access, is a property of the peripheral's internal state machine. A fixed-latency block holds PREADY low for a constant count; a variable-latency block stretches and shrinks it with data, FIFO occupancy, or contention. Knowing a peripheral's latency profile — not just "it's slow" — is what lets you reason about throughput, size corner cases, and debug a transfer that completes against stale data.
1. Problem statement
The problem is translating a peripheral's internal "I am not done yet" into the one bus signal the manager understands — PREADY — and knowing how many cycles that takes for a given access.
A peripheral's latency is an internal fact: an SRAM-backed buffer needs a read-cycle, a CDC FIFO status read needs a synchroniser flush, a register behind a clock divider only updates on its slow edge. None of that is visible on the APB bus directly. The only externally-visible consequence is PREADY: the peripheral holds it low until its internal logic says "the data the manager asked for is now valid," then drives it high for exactly the completing cycle. So the engineering job is:
- Map internal state to
PREADY. The peripheral's busy/done state machine must drivePREADYlow for every cycle it is still working and high on the cycle — and only the cycle — its result is genuinely valid. - Know the latency profile. Is the wait count fixed (the block always takes N cycles, set by structure) or variable (the count depends on data value, FIFO depth, contention, or a slow clock edge)? The manager cannot tell from the waveform of a single access; the designer and verifier must know it from the architecture.
The throughline of the whole chapter: a single logical "register read" becomes a multi-cycle bus transaction, and the shape of that transaction is dictated by the peripheral's internals, not by the APB protocol.
2. Why previous knowledge is insufficient
Chapter 8.1 gave you the root causes of latency — SRAM access time, CDC, gated clocks, arbitration. Module 5 gave you the access-phase cadence (the manager samples PREADY every access edge), and Module 3 named PREADY as the completion-and-backpressure control. All of that is necessary but stops short of the thing a working engineer must do:
- Causes are not signatures. Knowing that an SRAM takes a read-cycle (8.1) does not tell you what the bus looks like during that read-cycle. This chapter turns each cause into an observable
PREADYwaveform — the signature — and shows that the signature, not the cause, is what you actually see in a trace. - The handshake view treats
PREADYas a given bit; here the peripheral has to produce it from a state machine. Module 3 said "low waits, high completes." This chapter shows the FSM behind that bit — busy → done — and how its duration is set by structure or by data. - Fixed and variable latency are different engineering objects, and the idealized view erases the difference. A peripheral that always takes 3 cycles and one whose latency depends on FIFO occupancy produce the same-shaped waveform on any single access, but utterly different throughput and corner-case behaviour. You cannot reason about either from the per-cycle handshake alone.
What this chapter does not do — deliberately — is teach how PREADY is electrically driven (combinational vs registered: chapter 8.3) or the cycle-by-cycle hold mechanics of PREADY=0 (chapter 8.4). Here we stay at the level above those: internal latency → bus signature → latency profile.
3. Mental model
The model: a slow peripheral is a vending machine with a "PLEASE WAIT" light. You make one request (the access). Internally the machine does some work — maybe always the same amount, maybe more when it's busy — and keeps the "PLEASE WAIT" light on (PREADY low) the whole time. The instant your item is genuinely ready, it flips the light off (PREADY high) and dispenses. From the outside you only ever see the light go off-then-on; you never see the mechanism. But the time the light stays on is the machine's internal latency made visible.
Three refinements make it precise and useful:
- The FSM is the mechanism;
PREADYis the light. Internally the peripheral runsIDLE → BUSY → DONE.PREADYis simply(state != DONE)inverted — low through everyBUSYcycle, high on theDONE/valid cycle. The number ofBUSYcycles is the wait count. - Fixed latency = a constant; variable latency = a function. A fixed-latency peripheral counts a hard-coded N (
always 3 cycles) — its light stays on the same duration every time. A variable-latency peripheral'sBUSYduration is a function of state: data value, FIFO occupancy, synchroniser depth, or how many slow-clock edges away the answer is. Same light, but the on-time is now data-dependent. - The manager only counts; it never reasons. From the bus side, a fixed-3-cycle SRAM and a "this time it took 3" variable FIFO are indistinguishable in a single trace. The manager just holds the bus and counts wait cycles. Reasoning about why and for how long is the designer's and verifier's job, using the latency profile — which is exactly the knowledge a waveform alone cannot give you.
4. Real SoC implementation
In silicon, "drive PREADY from a busy/done FSM" takes one of two idiomatic shapes, and which one a peripheral uses is its latency profile. A fixed-latency block uses a hard-coded counter; a variable-latency block waits on a done pulse it cannot predict.
// ============================================================================
// Slow-peripheral PREADY: internal busy/done FSM drives the bus signature.
// Two forms shown — the latency PROFILE is which form the peripheral is.
// ============================================================================
// ----------------------------------------------------------------------------
// FORM A — FIXED latency: an SRAM-backed buffer that always takes N cycles.
// The wait count is structural and constant. PREADY is a down-counter.
// ----------------------------------------------------------------------------
localparam int FIXED_LATENCY = 3; // set by the slow source (e.g. SRAM read cycle)
logic [1:0] wait_cnt;
logic access_active;
// access begins on the access-phase edge (PSEL & PENABLE) of a new transfer
assign access_active = psel & penable;
always_ff @(posedge pclk or negedge presetn) begin
if (!presetn) begin
wait_cnt <= '0;
end else if (access_active && wait_cnt == '0 && !pready_fixed) begin
wait_cnt <= FIXED_LATENCY - 1; // load on first access cycle
end else if (wait_cnt != '0) begin
wait_cnt <= wait_cnt - 1'b1; // count the fixed wait cycles down
end
end
// PREADY high only when the counter has expired -> exactly N wait cycles,
// every access, regardless of data. This is the "always 3 cycles" signature.
assign pready_fixed = access_active && (wait_cnt == '0);
// ----------------------------------------------------------------------------
// FORM B — VARIABLE latency: a CDC FIFO-status read whose completion time is
// NOT predictable. PREADY waits on a done pulse the peripheral cannot count.
// WHY this matters: the wait count depends on synchroniser flush / occupancy,
// so two identical reads can take different numbers of cycles.
// ----------------------------------------------------------------------------
logic busy; // set when the internal fetch starts, cleared when it lands
logic data_valid; // 1 ONLY on the cycle the result is genuinely present
always_ff @(posedge pclk or negedge presetn) begin
if (!presetn) busy <= 1'b0;
else if (access_active && !busy && !data_valid) busy <= 1'b1; // launch fetch
else if (data_valid) busy <= 1'b0; // landed -> release
end
// CRITICAL: gate PREADY on REAL data validity, never on a speculative "almost
// done" term. Asserting PREADY one cycle before data_valid completes the
// access against stale PRDATA (see the debugging scenario below).
assign pready_var = access_active && data_valid && !busy;
// Final bus output: select the form this peripheral actually is.
assign pready = FIXED_LATENCY_BLOCK ? pready_fixed : pready_var;
assign prdata = data_reg; // must be valid on the SAME edge PREADY risesTwo facts drive the real implementation. First, the form is the profile: a counter form is a promise of fixed latency (and lets a verifier and a downstream throughput model assume a constant), while a done-pulse form is an admission of variable latency (and forces everything downstream to assume worst-case). Choosing the counter for a genuinely variable source is a lie that bites in silicon; choosing the done-pulse for a fixed source needlessly forfeits predictability. Second, PREADY and PRDATA must agree on the completing edge: the cycle PREADY rises is the cycle the manager latches PRDATA, so the FSM must hold the data path valid on exactly that edge — asserting "done" while the data is still settling is the canonical variable-latency bug.
5. Engineering tradeoffs
Different slow peripherals have different latency profiles, and the profile — not the raw cycle count — is what governs throughput modelling, verification effort, and corner-case risk. Each row is a real block you will meet on a real SoC.
| Peripheral type | Latency profile | Typical wait cycles | What sets the count | Throughput / risk implication |
|---|---|---|---|---|
| UART / SPI register block | Fixed | 0–1 | Shallow register read; sometimes always-ready | Cheap, predictable; safe to model as constant |
| SRAM-backed buffer | Fixed (structural) | 1–3 | SRAM read access time / pipeline depth | Constant N; throughput = 1/(N+1) of bus rate, easy to model |
| CDC FIFO status read | Variable | 2–5+ | Synchroniser depth + source-clock phase | Worst-case must be characterised; never assume the best case |
| FIFO-data read (occupancy-dependent) | Variable | 1–N | FIFO occupancy / empty-vs-filled | Data-dependent; corner is the empty/just-filled boundary |
| Register behind a clock divider | Variable | 0 to (divider−1) | Phase between fast bus edge and slow update edge | Worst case = full divider ratio; bus-rate phase dependent |
| Shared resource behind arbitration | Variable | 0 to (N−1)×grant | Contention from other masters | Unbounded-ish under load; throughput collapses under contention |
The throughline: fixed-latency peripherals you can model with a constant and verify with a single number; variable-latency peripherals force you to characterise and verify the worst case, because the average is a trap. A fast manager that was sized against a FIFO's typical latency will occasionally meet its worst latency in silicon — and that is exactly where throughput targets miss and stale-data bugs hide. When you see a variable profile, your first question is never "how long does it usually take?" but "what is the longest it can take, and have I exercised that?"
6. Common RTL mistakes
7. Debugging scenario
A variable-latency peripheral whose worst case was never characterised is the canonical "passes the directed test, fails on real traffic" bug — because the directed test happened to hit the typical latency, and only a fast manager on bursty traffic exposes the worst case.
- Observed symptom: an SoC reading a CDC FIFO-status register occasionally — not always — returns a stale value: the manager reads "FIFO not empty," pops, and gets garbage, but only under bursty traffic and only at the higher bus frequency used in the silicon part. In directed simulation against the same RTL it always passed.
- Waveform clue: in the failing trace (Figure 2, bottom), the peripheral's internal
doneterm — which drivesPREADYhigh — is asserted one cycle beforedata_valid, soPREADYrises whilePRDATAis still the previous read's value; the fast manager samplesPREADYhigh on that edge and latches the stalePRDATA. In the passing directed test the extra synchroniser cycle pusheddoneanddata_validinto the same cycle, hiding the lead. - Root cause: the variable-latency FIFO-status path was modelled as if it had a predictable completion, and
donewas derived from a speculative "synchroniser should be flushed by now" count rather than from the actualdata_validstrobe. Because the synchroniser's flush time is variable (it depends on the source-clock phase), the count was right on average — and exactly one cycle early on the unlucky phase. - Correct RTL: drive
PREADYfrom the registered, real data-valid term, never from a predicted done —assign pready = access_active && data_valid && !busy;wheredata_validis the genuine synchronised strobe — soPREADYcan only rise on the cyclePRDATAis actually present. If a counter form is truly needed, its terminal count must be the worst-case synchroniser depth, not the typical one. - Verification assertion: assert that the data is genuinely valid on every completing edge, so the access can never complete against stale data:
Azvya Education Pvt. Ltd.VLSI MentorSnippet
// PREADY may only rise when the data the manager latches is genuinely valid. property p_ready_implies_valid; @(posedge pclk) disable iff (!presetn) (psel && penable && pready) |-> data_valid; endproperty a_ready_implies_valid: assert property (p_ready_implies_valid); - Debug habit: when a slow peripheral returns intermittently stale data, do not re-read the data path — line up the
PREADYrising edge against thedata_valid/PRDATA-valid edge in the trace and check they coincide on every access, especially the fast/bursty ones. APREADYthat leads data validity by even one cycle on the unlucky case is the bug. And whenever a peripheral is variable-latency, your first verification question is "what is the worst case, and have I forced it?" — not "does the typical case pass?"
8. Verification perspective
A slow peripheral's latency profile is a first-class verification target — verifying "it eventually completes" is not enough; you must verify how long it can take and that data is valid whenever it completes.
- Profile the latency, don't just observe it. Build a checker that measures the wait-cycle count of every access to each slow peripheral and bins it. For a fixed-latency block, assert the count is always exactly N — any deviation is a bug (and a sign the source isn't really fixed). For a variable block, record the distribution and, crucially, the maximum observed.
- Cover fixed versus variable explicitly, and the variable corners. Functional coverage must distinguish a constant-latency completion from a data-dependent one, and for variable peripherals must hit the corners that set the latency: FIFO empty vs just-filled, the unlucky clock-divider phase, the maximum synchroniser flush, and peak arbitration contention. A suite that only ever hits a variable peripheral's typical latency has a hole exactly where the silicon bug lives — as in the debugging scenario.
- Characterise the worst case, and force it. The average latency is a trap; the design and the throughput model are sized against the worst case, so verification must drive the worst case (back-pressure the FIFO, phase-align the slow clock adversarially, saturate the arbiter), not wait for it to occur randomly. Tie a coverpoint to "worst-case latency reached" and fail the closure if it's never hit.
- Assert ready-implies-valid at every level. The
(pready) |-> data_validproperty (Beat 7) catches the complete-against-stale-data bug in RTL. But a profile bug that depends on real clock-phase relationships across a CDC — wheredoneanddata_validonly diverge under specific timing — may only surface with realistic clock ratios or in gate-level/CDC-aware simulation; RTL with idealized synchronisers can mask it. So: ready-implies-valid and latency-distribution checks in RTL, and worst-case CDC/divider phase forcing where the real timing lives.
The point: for a slow peripheral, correctness includes its latency profile. You verify a fixed peripheral against a constant, a variable one against its characterised worst case — and in both, that PREADY never rises before the data is genuinely there.
9. Interview discussion
"You have two slow peripherals that both hold PREADY low for three cycles on the trace you're looking at — are they the same?" is a sharp question because the lazy answer is "yes" and the right answer is "I can't tell from one access — what's the latency profile?" That single reframing separates people who read waveforms from people who design the blocks behind them.
Frame it as profile, not duration: on the bus every slow peripheral looks the same — PREADY low then high — so the manager only ever counts wait cycles; the engineering content is whether that count is fixed (structural: SRAM read-cycle, pipeline depth — model it as a constant, verify it as a constant) or variable (a function of data value, FIFO occupancy, clock-divider phase, or arbitration contention — model and verify the worst case, because the average is a trap). Then deliver the depth points: a single logical register read is a multi-cycle bus transaction whose shape is set by the peripheral's internals, not by APB; the form of the RTL is the profile — a hard-coded counter promises fixed latency, a done-pulse admits variable latency, and using the counter for a genuinely variable source (CDC, arbitration) is a latent stale-data bug; and PREADY must rise with valid data, never a cycle early. Closing with "for any variable-latency block my first question is the worst case, not the typical case — that's where throughput misses and stale-data bugs live" signals real bring-up experience.
10. Practice
- Read the profile, not the trace. Given a single APB read where
PREADYis low for 3 cycles then high, list everything you cannot conclude about the peripheral, and state what extra information tells you whether it is fixed- or variable-latency. - Name the signature. For each of an SRAM-backed buffer, a CDC FIFO-status read, and a register behind a /4 clock divider, state the latency profile, the typical and worst-case wait count, and what sets the count.
- Counter or done-pulse? For a peripheral whose completion depends on FIFO occupancy, say which RTL form (fixed counter or done pulse) is correct and why the other is a latent bug.
- Build the FSM. From memory, write the variable-latency
busy/data_validFSM and thePREADYassignment, and explain whyPREADYis gated ondata_validrather than on entering the done state. - Force the worst case. For a CDC FIFO-status read, describe the stimulus that drives the worst-case latency and the coverpoint and assertion you'd add to prove you hit it and that the data was valid when
PREADYrose.
11. Q&A
12. Key takeaways
- Every slow peripheral looks the same on the bus —
PREADYheld low, then high — but why and for how long is set entirely by the peripheral's internal busy/done state machine, not by the APB protocol. The manager only counts wait cycles; it never reasons about them. - A peripheral's
PREADYsignature is a direct image of its internal latency. Each internal BUSY cycle becomes onePREADY-low wait cycle, and the done/valid edge becomes thePREADYrising edge. A single logical register read becomes a multi-cycle bus transaction. - Fixed-latency and variable-latency peripherals are different engineering objects. Fixed (SRAM read-cycle, pipeline depth) gives a constant count you can model and verify as a number; variable (CDC FIFO, clock-divider phase, arbitration) gives a data-/phase-/contention-dependent count whose worst case — never its average — is what governs throughput and risk.
- The form of the RTL is the latency profile: a hard-coded counter promises fixed latency; a done-pulse admits variable latency. Modelling a genuinely variable source with a fixed counter that matches the typical case is a latent stale-data bug.
PREADYmust rise with genuinely valid data, never a cycle early. Asserting done beforePRDATAsettles completes the access against stale data — the canonical variable-latency bug, invisible until a fast manager samples the early edge.- Verify the profile, not just completion: assert
pready |-> data_valid, profile and bin the wait-cycle count, distinguish fixed from variable in coverage, and force the worst case (FIFO empty/just-filled, unlucky clock phase, peak contention) — for variable peripherals the average is a trap.