Skip to content

AMBA APB · Module 8

PREADY Common Design Bugs

The recurring PREADY bugs that ship — stuck-low hangs, asserted-too-early garbage reads, the combinational glitch, ignoring PSEL/PENABLE, the default-ready violation that stalls another slave, and bad reset behaviour — each as a named symptom, root cause, and correct RTL fix.

Almost every APB transfer that hangs or reads garbage is a PREADY bug — and they cluster into a small, recognisable set. You have spent this module learning how PREADY should behave: it extends a transfer, the manager samples it once per access edge, and its timing must be clean. This chapter inverts that knowledge into a debugging instrument. It is a catalog of the classic PREADY design bugs — the recurring mistakes that pass a quick sim and ship — each as a named bug with a symptom, a root cause, the wrong RTL that produces it, and the correct RTL that fixes it. The single idea to carry: when an APB access "never completes" or "captures the wrong data," you should not start from scratch — you should pattern-match against this small set, because the bug is almost certainly one of them.

1. Problem statement

The problem is that PREADY is the single bit on which the entire APB completion contract rests, and a handful of recurring RTL mistakes break that contract in ways that look like different failures but trace back to the same signal.

A subordinate's PREADY carries two distinct promises every access: "the transfer may complete now" and, implicitly, "the data accompanying this completion is valid." Break the first promise and the access hangsPREADY never asserts and the manager waits forever. Break the second and the access completes with garbagePREADY asserts before the data it vouches for is actually present, and the manager captures stale or undefined bits. Every classic PREADY bug is a violation of one of those two promises:

  • Hang bugs leave PREADY low when it should rise: a forgotten done path, an FSM state with no exit, an undriven reset value, an unselected slave pulling the shared PREADY low, or a completion term from an unsynchronised clock domain that never resolves.
  • Garbage bugs raise PREADY when the data is not yet trustworthy: a ready that leads the read-data mux by a cycle, a combinational glitch into the sample window, or a PREADY that asserts in the wrong phase or for the wrong slave because it ignores PSEL/PENABLE.

The engineering problem of this chapter is therefore not "what does PREADY mean" — you know that — but "what are the specific ways real RTL gets PREADY wrong, what does each look like on the bus, and what is the correct fix for each." That is a pattern library, and the fastest debuggers carry it in their heads.

2. Why previous knowledge is insufficient

This module has built the model of PREADY from the handshake outward, and each prior chapter is now a reference this catalog points back to — but none of them is the catalog itself.

  • Chapter 8.3 — PREADY timing taught the combinational-versus-registered choice and the glitch hazard in full. That chapter is the deep treatment of the combinational-glitch bug; here it is one entry in the catalog, summarised and linked, not re-taught. Knowing how a glitch happens does not give you the full taxonomy of every other way PREADY ships broken.
  • Chapter 8.4 — transfer-extension mechanics taught how PREADY=0 holds the bus cycle by cycle. That explains the mechanism a stuck-low PREADY exploits to hang an access — but it assumed PREADY eventually rises. This chapter is about the cases where it never does, or rises wrongly.
  • Chapter 8.5 — multiple wait cycles taught the unbounded-wait hang and the timeout/watchdog mitigation at the manager. That is the defensive side — protecting the bus from a broken slave. This chapter is the offensive side — finding and fixing the broken slave's RTL so the hang never happens. You need both, but they are different jobs.

The gap is this: prior chapters taught the correct behaviour and a few individual hazards. They did not assemble the failure taxonomy — the named set of bugs with wrong-versus-right RTL — that lets you debug a real transfer by recognition instead of derivation. Building that taxonomy is this chapter; carrying it forward into a verification plan that proves a slave is free of all of them is chapter 8.7 — wait-state verification challenges.

3. Mental model

The model: PREADY makes two promises per access, and every classic bug breaks one of them — so sort every symptom into one of two bins first, then pick the bug.

The two bins are the two ways a PREADY contract fails:

  • It hangs (broken promise #1: "you may complete"). PREADY stays low when it should have risen. On the bus you see PSEL and PENABLE high, the access cycle repeating forever, no completion. The cause is always a missing or unreachable path to drive PREADY high — a done signal that is never generated, an FSM state with no exit, a reset value left at 0, an unselected slave pulling the shared line low, or a cross-domain term that never resolves cleanly.
  • It reads garbage (broken promise #2: "the data is valid"). PREADY rises too soon or wrongly, so the manager completes the access and latches data that is not yet — or not actually — correct. The cause is always readiness running ahead of, or independent of, validity — a ready that leads the data mux by a cycle, a glitch into the sample window, or a PREADY that fires in the wrong phase or for the wrong slave.

The discipline this model buys you: bin before you debug. "It hangs" instantly excludes the garbage family and points you at the four hang causes; "every Nth read is wrong" instantly excludes the hang family and points you at the three garbage causes. Three refinements sharpen it:

  • A hang is a liveness failure; garbage is a safety failure. Liveness means "something good never happens" (completion); safety means "something bad does happen" (wrong data committed). They need different assertions — a bounded-completion property for hangs, a ready-implies-data-valid property for garbage.
  • The default-ready bug is a hang that hides on a different slave. An unselected slave pulling PREADY low stalls whoever the manager is actually talking to — so the symptom appears far from the buggy RTL. Bin it as a hang, but suspect every slave, not just the addressed one.
  • The reset and cross-clock bugs are hangs that depend on conditions, not just logic. They pass when reset deasserts cleanly or when the clocks happen to align, and fail otherwise — which is why they are intermittent and why "it worked yesterday" does not clear them.
A two-column taxonomy figure. The left amber column titled SYMPTOM: ACCESS HANGS lists stuck-low PREADY, wrong in/after reset, default-ready violation, and cross-clock/unsynchronised, with a fix family below. The right red column titled SYMPTOM: READS GARBAGE lists asserted too early, combinational glitch, and ignores PSEL/PENABLE, with a shared-root note and a fix family below.
Figure 1 — the PREADY bug families, organised by the symptom they show on the bus. The left (amber) column is the HANGS family: stuck-low PREADY, wrong-in/after-reset, default-ready violation (an unselected slave pulling the shared PREADY low), and an unsynchronised cross-clock done term — all breaking the 'you may complete' promise so the access never finishes. The right (red) column is the READS-GARBAGE family: PREADY asserted too early before the read-data mux settles, the combinational glitch (taught in full by chapter 8.3 and only linked here), and a PREADY that ignores PSEL/PENABLE and fires in the wrong phase or for the wrong slave — all breaking the 'the data is valid' promise so the manager captures stale bits. The figure is the chapter's spine: bin the symptom into hangs or garbage first, then pick the specific bug and apply its known fix.

4. Real SoC implementation

In real RTL the top bugs are a line or two of wrong code, and the fixes are equally short — which is exactly why they ship: each looks reasonable in isolation. Here are the wrong-versus-right pairs for the two highest-frequency bugs, stuck-low and asserted-too-early, plus the !sel → 1 default that every slave must carry.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ============================================================
// BUG 1 — STUCK-LOW PREADY: a done path that can never fire
// ============================================================
// WRONG: PREADY is gated on a 'done' that is only set in a state the FSM
// can't reach for a single-beat access, so PREADY never rises -> hang.
always_ff @(posedge pclk or negedge presetn) begin
  if (!presetn)      state <= IDLE;
  else case (state)
    IDLE:   if (psel)            state <= ACCESS;
    ACCESS: if (op_multi_beat)   state <= DRAIN;   // single-beat op never enters DRAIN...
            // ...and there is no 'else state <= DONE' here -> ACCESS has no exit
    DRAIN:                       state <= DONE;
    DONE:                        state <= IDLE;
  endcase
end
assign pready_bug = (state == DONE);   // never high for a single-beat access -> STUCK LOW
 
// CORRECT: every access path reaches a completion that drives PREADY.
// A clean registered ready that follows a real data-valid term.
always_ff @(posedge pclk or negedge presetn) begin
  if (!presetn)              pready <= 1'b0;          // known, low, out of reset
  else if (psel && penable && data_valid && !pready)
                             pready <= 1'b1;          // assert exactly when data is valid
  else if (psel && penable && pready)
                             pready <= 1'b0;          // one-cycle complete, then drop
  else                       pready <= 1'b0;          // idle / setup phase: not ready
end
// Every access now has a guaranteed path to pready=1 the moment data_valid holds.
 
// ============================================================
// BUG 2 — ASSERTED TOO EARLY: ready leads the read-data mux
// ============================================================
// WRONG: PREADY tracks a 'busy' counter that hits zero ONE cycle before
// the read-data mux output is registered, so the manager completes and
// captures the PREVIOUS register's value -> intermittent garbage reads.
assign pready_bug = (wait_cnt == 0);              // ready when the *counter* is done...
assign prdata     = rdata_mux_q;                  // ...but rdata_mux_q updates a cycle later
 
// CORRECT: derive PREADY from the SAME term that makes PRDATA valid.
// data_valid_q and the registered read data come up together -> no lead.
always_ff @(posedge pclk or negedge presetn) begin
  if (!presetn) begin
    prdata_q     <= '0;
    data_valid_q <= 1'b0;
  end else if (psel && penable) begin
    prdata_q     <= rdata_mux;                    // capture the settled mux output
    data_valid_q <= rdata_mux_settled;            // same cycle the data is real
  end else begin
    data_valid_q <= 1'b0;
  end
end
assign pready = psel && penable && data_valid_q;  // ready FOLLOWS valid data, never leads
assign prdata = prdata_q;
 
// ============================================================
// BUG 5 — DEFAULT-READY VIOLATION: unselected slave pulls PREADY low
// ============================================================
// WRONG: PREADY low whenever this slave isn't busy -> when ANOTHER slave is
// addressed, this one drives the shared/muxed PREADY low and stalls it.
assign pready_bug = busy ? 1'b0 : busy_done;      // 0 when !sel -> hangs the bus
 
// CORRECT: an unselected slave must NOT pull PREADY low.
// Drive 1 when not selected (or stay out of the return mux entirely).
assign pready = (psel && penable) ? data_valid_q  // answer only in our own access
                                  : 1'b1;          // !sel -> default ready, never stall

Two facts drive these fixes. First, a hang fix is structural: guarantee a reachable path to pready=1. The stuck-low bug is not a typo on PREADY — it is a hole in the FSM. The fix is to ensure every access path arrives at a completion term, and to drive PREADY from a known, registered state that is 0 out of reset and rises exactly when a real data_valid holds. Second, a garbage fix is causal: PREADY must be derived from the same term that makes the data valid, not from a proxy that runs ahead of it. A counter, a busy flag, or a decode that "is usually done by now" all lead the data under some conditions; only deriving PREADY from the actual data_valid_q (the same flop that gates PRDATA) guarantees readiness never outruns validity. (The combinational-glitch variant of the garbage family — PREADY pulsing high mid-cycle off a rippling decode — is the subject of chapter 8.3; the fix there is to register PREADY or its inputs, and it is not re-derived here.)

5. Engineering tradeoffs

The catalog itself is the deliverable: one row per bug, the symptom you will actually see on the bus, the root cause in the RTL, and the fix. Memorise the shape of each row — symptom-binned, with a structural or causal fix — and you can debug most APB completion failures by recognition.

Bug nameSymptom on the busRoot causeFix
Stuck-low PREADYPSEL/PENABLE high forever; access never completes (hang)Forgotten done path, FSM state with no exit, or reset state leaves PREADY at 0 with no rise conditionGuarantee every access path reaches a completion term; register PREADY, 0 out of reset, rising on a real data_valid
Asserted too earlyAccess completes, but data is the previous register's value — intermittent "every Nth read is wrong"PREADY derived from a proxy (counter/busy) that leads the read-data mux by a cycleDerive PREADY from the same data_valid_q flop that gates PRDATA; readiness follows validity, never leads
Combinational glitchIntermittent early-complete / garbage; passes RTL, fails gate-level (see 8.3)Combinational PREADY off a rippling PADDR decode pulses high in the sample windowRegister PREADY or its inputs so it is glitch-free at the sampling edge — covered in full in 8.3
Ignores PSEL/PENABLECompletes in the wrong phase, or one slave answers for another's addressPREADY not gated on PSEL && PENABLE; asserts on decode or busy aloneGate PREADY strictly on PSEL && PENABLE so it only answers inside its own access phase
Default-ready violationA different slave's access stalls/hangs whenever this idle slave is on the shared busUnselected slave drives the shared/muxed PREADY low instead of highDrive PREADY = 1 when !sel (or stay out of the return mux); never pull the shared line low when idle
Wrong in / after resetHang or X-propagation right after reset; depends on reset polarityPREADY undriven or X out of reset, or reset asserts/deasserts on the wrong polarityReset PREADY to a known 0 on the correct reset polarity; never leave it undriven on any path
Cross-clock / unsynchronisedIntermittent hang or glitch tied to clock-ratio/phase; non-reproduciblePREADY (or its done term) depends on a signal from another clock domain with no synchroniserSynchronise the cross-domain term (2-flop or handshake) before it gates PREADY; never sample it raw

The throughline: every row sorts into hang or garbage, and every fix is either "guarantee a reachable completion path" or "make readiness follow validity." Two bins, two fix families. The combinational-glitch and reset bugs add the level dimension — some bugs (glitch, reset-X) only surface in gate-level or with real delays, which is why a clean RTL run is not proof. The default-ready and cross-clock bugs add the non-local dimension — the symptom can appear on a different slave or a different clock ratio than the bug, which is why you suspect the whole bus, not just the addressed slave.

6. Common RTL mistakes

7. Debugging scenario

Pick the asserted-too-early bug, because it is the most maddening of the catalog: it passes every directed test, ships, and then surfaces in the field as an intermittent software fault that no one can reproduce on demand.

  • Observed symptom: firmware reading a peripheral's status registers intermittently gets the wrong register's value — roughly "every 100th read is wrong." Single-stepping in the debugger never reproduces it; a tight read loop in production does. The bus shows the access completing normally, with no error, no hang, no protocol violation.
  • Waveform clue: in a delay-annotated capture (Figure 2), PREADY for this peripheral asserts one full cycle before PRDATA presents the addressed register's value. The peripheral's read-data mux is still settling — PRDATA is showing the previous read's value — at the exact edge the manager samples on a completing access. When two consecutive reads target the same register, the stale value happens to equal the correct one and the bug is invisible; when they target different registers, the manager latches the prior register's data.
  • Root cause: PREADY was derived from a wait_cnt == 0 term — a busy counter — that reaches zero one cycle before the registered read-data mux (rdata_mux_q) updates. Readiness was wired to a proxy for "should be done by now," not to the actual data-valid event. Under back-to-back reads to different addresses, the one-cycle lead lets the manager complete while PRDATA still holds the prior beat's data.
  • Correct RTL: derive PREADY from the same registered term that validates PRDATA, so readiness and data come up on the same edge:
    Azvya Education Pvt. Ltd.VLSI Mentor
    Snippet
    always_ff @(posedge pclk or negedge presetn)
      if (!presetn)             data_valid_q <= 1'b0;
      else if (psel && penable) data_valid_q <= rdata_mux_settled;  // same cycle PRDATA is real
      else                      data_valid_q <= 1'b0;
    assign pready = psel && penable && data_valid_q;   // ready FOLLOWS valid data
    assign prdata = prdata_q;                          // captured on the same edge
  • Verification assertion: prove readiness never leads validity — when this slave completes, the data must already be valid:
    Azvya Education Pvt. Ltd.VLSI Mentor
    Snippet
    // ready-implies-data-valid: completion must not precede a settled read mux
    assert property (@(posedge pclk) disable iff (!presetn)
      (psel && penable && pready && !pwrite) |-> data_valid_q
    );
  • Debug habit: when reads are intermittently wrong but the bus protocol looks clean, do not chase the data path first — chase the alignment of PREADY and the read-data valid edge. Capture a back-to-back read of two different registers with annotated delays and check whether PREADY rises before PRDATA settles. The "every Nth read is wrong" signature almost always means readiness is leading validity by a cycle.
Two stacked APB read timing diagrams of the same access. The top correct case shows PREADY rising on the same edge PRDATA becomes valid, and the manager sampling correct data. The bottom buggy case, in red, shows PREADY rising a cycle early while PRDATA still holds the previous register's value, and the manager sampling stale data at the completion edge.
Figure 2 — the asserted-too-early bug versus the correct alignment, on the same read with one wait state. Top (correct, green): PREADY stays low while the read-data mux is still settling and rises on the very edge PRDATA presents this register's valid value, so the manager samples correct data. Bottom (bug, red): PREADY asserts a full cycle early — while PRDATA still holds the previous register's value because the mux has not settled — so on the completion edge the manager captures stale data. The figure marks the early-assert instant and the manager's sampling edge, and notes the intermittent 'every Nth read is wrong' signature: the fault only shows when the prior read targeted a different register, which is why it survives directed tests and surfaces in the field.

8. Verification perspective

Because the catalog splits cleanly into liveness (hangs) and safety (garbage) failures, the verification plan needs one assertion class per bug family plus an awareness of which level each bug actually surfaces at — a stability check in RTL will never see a glitch, and a value check will never see a hang.

  • Bounded-completion catches the hang family. A single liveness property — once PSEL && PENABLE assert, PREADY must rise within N cycles — catches stuck-low, the FSM-with-no-exit, and the reset-stuck case in one stroke: assert property (@(posedge pclk) disable iff(!presetn) (psel && penable) |-> ##[1:N] pready);. This is the offensive counterpart to the manager-side timeout of chapter 8.5: the watchdog survives the hang; this assertion finds it.
  • Ready-implies-data-valid catches the garbage family. Bind a property that a completing read may not precede a settled data-valid term — (psel && penable && pready && !pwrite) |-> data_valid_q — to catch asserted-too-early. Pair it with a never-X check on PREADY while selected ((psel && penable) |-> !$isunknown(pready)) to catch the reset/undriven bugs, and a phase-gating check that PREADY is only meaningful inside PSEL && PENABLE to catch the ignores-PSEL/PENABLE bug.
  • Default-ready needs a multi-slave check; the glitch needs gate-level. Assert that an unselected slave does not pull the shared PREADY low (!sel |-> pready per slave, or a bus-level "no idle slave drives the return low"), which directed single-slave tests cannot see. And know the level split: the never-X and ready-before-complete and bounded-completion checks all run in RTL, but the combinational-glitch bug (and a reset-polarity X) only appears in gate-level or delay-annotated simulation — a clean RTL run is necessary but not sufficient. Carrying every one of these into a coherent wait-state coverage and assertion plan is the whole job of chapter 8.7.

The point: each bug family has a named assertion that catches its whole class, and each has a level at which it actually manifests — so a PREADY verification plan is "one liveness property, one safety property, one X-check, one default-ready check, one phase-gate, and a gate-level pass for the glitch," not a pile of directed reads.

9. Interview discussion

"Walk me through the common PREADY bugs you have seen" is a senior screening question because a weak answer names one ("it can get stuck low") and stops, while a strong answer reveals an organised mental catalog and the fixes — exactly the recognition speed that separates someone who has debugged APB from someone who has only read the spec.

Frame it as two promises, two failure modes: PREADY says "you may complete" and "the data is valid," and every bug breaks one of those — so the bus either hangs or reads garbage. Then enumerate crisply: in the hang family, stuck-low (forgotten done path or FSM state with no exit), reset-stuck (undriven or wrong polarity out of reset), the default-ready violation (an unselected slave pulling the shared line low and stalling a different slave — the non-local one that catches people out), and the cross-clock term that never resolves; in the garbage family, asserted-too-early (readiness leads the data mux — the "every Nth read is wrong" field bug), the combinational glitch (rippling decode into the sample window, invisible in RTL, caught in gate-level), and ignores PSEL/PENABLE (answers in the wrong phase or for the wrong slave). Land the depth points: the fix for a hang is structural (guarantee a reachable path to pready=1), the fix for garbage is causal (derive PREADY from the same data_valid that gates PRDATA, never a proxy that leads it), and the verification is one liveness property plus one ready-implies-valid property, with the glitch needing gate-level. Closing with "and the default-ready bug taught me to suspect every slave on the shared return, not just the one being addressed" signals real silicon debugging, not spec reading.

10. Practice

  1. Bin the symptom. Given three field reports — "the access never completes," "every 100th read returns the wrong register," and "it only hangs when slave B is idle on the bus" — assign each to the hang or garbage family and name the specific bug.
  2. Fix the FSM hole. Given an FSM where ACCESS transitions to DRAIN only on op_multi_beat and PREADY = (state==DONE), explain why a single-beat access hangs and write the corrected exit condition.
  3. Catch the lead. Write the SVA property that proves a completing read never precedes a settled data_valid_q, and state which bug it catches and which it does not.
  4. Reason about the default. Explain why driving PREADY low when !sel is a bug and high is correct, and describe the symptom and which slave shows it.
  5. Pick the level. For each of stuck-low, asserted-too-early, combinational glitch, and reset-X, state whether a zero-delay RTL simulation can catch it or whether it needs gate-level / a never-X check, and the assertion for each.

11. Q&A

12. Key takeaways

  • PREADY makes two promises every access — "you may complete" and "the data is valid" — and every classic bug breaks one of them. Break the first and the access hangs; break the second and it reads garbage. Bin the symptom into hang-or-garbage before debugging.
  • The hang family — stuck-low (forgotten done path / FSM state with no exit), wrong-in/after-reset, default-ready violation, and unsynchronised cross-clock — is fixed structurally: guarantee every access path reaches a reachable, registered, reset-known path to pready=1.
  • The garbage family — asserted-too-early, combinational glitch (see 8.3), and ignores-PSEL/PENABLE — is fixed causally: derive PREADY from the same data_valid term that gates PRDATA, gate it on PSEL && PENABLE, and never let readiness lead validity.
  • The default-ready violation is non-local: an unselected slave pulling the shared PREADY low stalls a different slave's access, so suspect every slave on the shared return, not just the addressed one. Drive PREADY = 1 when !sel.
  • A clean RTL run is not proof. The combinational glitch and the reset-X bug only surface in gate-level or delay-annotated simulation; verify at the right level, with explicit X-checks.
  • The verification reduces to a few named properties: one bounded-completion (liveness) for hangs, one ready-implies-data-valid (safety) for garbage, a never-X check for reset/undriven, a phase-gate, and a default-ready check — carried forward into the full plan of chapter 8.7.