Skip to content

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 drive PREADY low 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 PREADY waveform — the signature — and shows that the signature, not the cause, is what you actually see in a trace.
  • The handshake view treats PREADY as 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; PREADY is the light. Internally the peripheral runs IDLE → BUSY → DONE. PREADY is simply (state != DONE) inverted — low through every BUSY cycle, high on the DONE/valid cycle. The number of BUSY cycles 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's BUSY duration 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.
An APB waveform in two halves: the top shows a peripheral FSM going IDLE then three cycles of BUSY then DONE with a data_valid and done pulse, and the bottom shows PSEL and PENABLE high with PREADY held low across the three BUSY cycles in an amber band then rising on the done edge to complete.
Figure 1 — a slow peripheral's internal busy→done FSM translated into the PREADY signature on the bus. The top half is inside the peripheral: it leaves IDLE on select, spends three cycles in BUSY fetching from a slow source, and raises an internal data_valid / done pulse the cycle the result is genuinely available. The bottom half is what the manager sees: PENABLE held high across the whole access, PREADY driven low for every BUSY cycle (the amber wait band) and rising exactly on the done edge to complete the transfer. The vertical alignment is the lesson — each internal BUSY cycle becomes one PREADY-low wait cycle and the done edge becomes the PREADY rising edge, so the bus signature is a direct image of internal latency. A fixed-latency peripheral makes the amber band a constant width; a variable-latency one makes it stretch and shrink per access.

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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ============================================================================
//  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 rises

Two 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 typeLatency profileTypical wait cyclesWhat sets the countThroughput / risk implication
UART / SPI register blockFixed0–1Shallow register read; sometimes always-readyCheap, predictable; safe to model as constant
SRAM-backed bufferFixed (structural)1–3SRAM read access time / pipeline depthConstant N; throughput = 1/(N+1) of bus rate, easy to model
CDC FIFO status readVariable2–5+Synchroniser depth + source-clock phaseWorst-case must be characterised; never assume the best case
FIFO-data read (occupancy-dependent)Variable1–NFIFO occupancy / empty-vs-filledData-dependent; corner is the empty/just-filled boundary
Register behind a clock dividerVariable0 to (divider−1)Phase between fast bus edge and slow update edgeWorst case = full divider ratio; bus-rate phase dependent
Shared resource behind arbitrationVariable0 to (N−1)×grantContention from other mastersUnbounded-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 done term — which drives PREADY high — is asserted one cycle before data_valid, so PREADY rises while PRDATA is still the previous read's value; the fast manager samples PREADY high on that edge and latches the stale PRDATA. In the passing directed test the extra synchroniser cycle pushed done and data_valid into the same cycle, hiding the lead.
  • Root cause: the variable-latency FIFO-status path was modelled as if it had a predictable completion, and done was derived from a speculative "synchroniser should be flushed by now" count rather than from the actual data_valid strobe. 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 PREADY from the registered, real data-valid term, never from a predicted done — assign pready = access_active && data_valid && !busy; where data_valid is the genuine synchronised strobe — so PREADY can only rise on the cycle PRDATA is 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 Mentor
    Snippet
    // 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 PREADY rising edge against the data_valid / PRDATA-valid edge in the trace and check they coincide on every access, especially the fast/bursty ones. A PREADY that 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?"
Two stacked APB read waveforms: the top correct case shows PREADY rising on the same edge that PRDATA becomes valid, drawn in green; the bottom buggy case shows PREADY rising one cycle before PRDATA updates, drawn in red, with a dashed marker showing the manager completing against stale data.
Figure 2 — a variable-latency peripheral, correct versus bug, on the same read. Top (correct, green): the peripheral keeps PREADY low while its internal data path is still settling and raises PREADY only on the same edge PRDATA becomes valid, so the manager captures correct data on the completing edge. Bottom (bug, red): the done term that drives PREADY is asserted one cycle early, before PRDATA has updated; PREADY leads PRDATA by a cycle, the fast manager samples PREADY high and completes the access, latching the previous (stale) PRDATA value. The waveform clue is the alignment of the PREADY rising edge with the PRDATA-valid edge — coincident when correct, a one-cycle lead when buggy — and the bug only bites when the manager is fast enough to sample the early-completion edge before the data catches up.

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_valid property (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 — where done and data_valid only 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

  1. Read the profile, not the trace. Given a single APB read where PREADY is 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.
  2. 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.
  3. 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.
  4. Build the FSM. From memory, write the variable-latency busy/data_valid FSM and the PREADY assignment, and explain why PREADY is gated on data_valid rather than on entering the done state.
  5. 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 PREADY rose.

11. Q&A

12. Key takeaways

  • Every slow peripheral looks the same on the busPREADY held 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 PREADY signature is a direct image of its internal latency. Each internal BUSY cycle becomes one PREADY-low wait cycle, and the done/valid edge becomes the PREADY rising 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.
  • PREADY must rise with genuinely valid data, never a cycle early. Asserting done before PRDATA settles 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.