Skip to content

The most common APB hang is the simplest to describe and the slowest to localise without a method: PREADY never rises, the manager waits forever, and nothing on the bus looks "wrong" — completion just never arrives. Almost every "the APB bus is hung" report traces to a PREADY that stayed low, and the bug can sit in any of half a dozen places — a forgotten done path, an FSM state with no exit, a flop that resets to 0 with no rise condition, a wait counter that never reaches its terminal, a cross-clock done term that never resolves, a gating condition that cannot be true for this access type, or — the one that fools everyone — an unselected slave on the shared return pulling the line low for the slave you are actually addressing. This chapter is not the catalogue of those causes (that is the common-design-bugs page); it is the localisation procedure — a repeatable bisection that halves the search at each step and points you at exactly which of those causes you have, fast.

1. Problem statement

The problem is that a stuck-low PREADY is a silent absence, not an error — there is no offending edge, no protocol violation, nothing fires — so without a disciplined search the engineer burns hours staring at a waveform where everything that is present looks correct.

A PREADY that never asserts holds the access in its wait state indefinitely: PSEL and PENABLE stay high, PADDR and PWRITE stay stable, the manager samples PREADY low on every rising edge and re-enters the same ACCESS-wait cycle forever (per AMBA APB IHI 0024C §2.1, the manager extends the transfer while PREADY is low). The failure mode is the absence of an event — a liveness failure — which is exactly what makes it hard: every checker that watches for a wrong thing stays quiet, and the simulation either runs to its global timeout with no diagnosis or, in silicon, presents as a dead bus with the manager stuck on one address.

Three properties make stuck-low the worst of the hang family to debug by eye:

  • The symptom is uniform regardless of cause. Forgotten done, no-exit FSM, reset-stuck flop, never-terminating counter, unsynchronised cross-clock term, default-ready violation — all of them look identical on the bus: PSEL/PENABLE high, PREADY flat low, access repeating. The waveform alone does not tell you which cause you have, so staring harder does not help.
  • The bug can be non-local. The slave whose RTL is broken may not be the slave the manager is addressing — an idle slave driving the shared/muxed PREADY low stalls whoever is selected. So "look at the addressed slave's RTL" is not even guaranteed to be looking in the right place.
  • The bug can be conditional. Reset-polarity and cross-clock causes only hang under specific reset or clock-phase conditions, so "it worked yesterday" does not clear them and a single passing run does not prove the hang is gone.

The engineering problem of this chapter is therefore not "what causes a stuck-low PREADY" — the catalogue lists those — but "given a hung bus in front of you, what is the fastest repeatable procedure to localise which cause it is." That procedure is a bisection, and it is the deliverable.

2. Why previous knowledge is insufficient

This module taught you how PREADY should behave and catalogued the ways it ships broken — but neither gives you a search procedure for a hang in front of you right now.

  • Transfer-extension mechanics taught that PREADY=0 holds the bus cycle by cycle — the mechanism a stuck-low PREADY exploits. But it assumed PREADY eventually rises and explained what happens while it is low; it did not tell you how to find why it never rose.
  • Multiple wait cycles taught the unbounded-wait hang and the manager-side watchdog that survives it. That is the defensive move — it converts a silent hang into a flagged failure so you know you hung — but a watchdog tells you that you hung, not where the missing PREADY lives. Detection is not localisation.
  • PREADY generation and PREADY timing taught how to build PREADY correctly — registered versus combinational, glitch-free, from a real data-valid term. They are the reference for what correct looks like, which the bisection leans on at each branch, but they are not organised as a debug walk.
  • The common-design-bugs catalogue is the closest sibling: it names every stuck-low cause with wrong-versus-right RTL. But a catalogue is a lookup by name — it assumes you already know which bug you have. This chapter is the procedure that gets you to the name: given only "it hangs," which branch leads to which catalogue entry.

The gap is a method. Prior chapters give correct behaviour, the hang mechanism, the watchdog that detects it, and the catalogue of causes. None of them is the bisection that, starting from a hung waveform, deterministically narrows six interchangeable-looking causes down to one. Building that bisection is this chapter; the catalogue of fixes it points to is common-design-bugs.

3. Mental model

The model: a stuck-low PREADY is a broken path from "the access started" to "PREADY=1, and you find the break by bisecting that path — testing one junction at a time, each test halving where the fault can be. Do not guess a cause; walk the signal forward and find the first junction where the expected value is missing. That first missing junction is the cause.

The path has four junctions, and the bisection asks one yes/no question at each:

  • Q1 — Is the access phase even reached? At the slave's pins, are PSEL and PENABLE both high? If not, the manager never drove a full access (or decode never selected this slave) and the fault is upstream — it is a PSEL/PENABLE problem, not a PREADY problem. This question alone clears the entire class of "it's not actually the slave's readiness." Only if both are high do you proceed.
  • Q2 — Does the slave's internal done / data_valid term ever rise? Probe the slave's internal completion signal, not PREADY. If data_valid (or done, or the wait counter reaching its terminal) never asserts, the bug is in the completion path: a forgotten done term, an FSM state with no exit, or a wait counter whose terminal is unreachable. The fault is behind PREADY, in the logic that should produce the done.
  • Q3 — Internal done is high but PREADY is still low: registered or combinational? Now the done exists but does not reach PREADY, so the break is in the gating between them. If PREADY is registered, the flop never loads its 1 — reset value 0 with no rise condition, an enable term gated on an impossible case, or the done sampled in a cycle it is not high. If PREADY is combinational, one AND-term in its expression is false for this access type or has the wrong polarity. Bisect the gating expression: trace each term to its driver and find the one stuck at the wrong value.
  • Q4 — Is it even this slave? If the addressed slave looks like it should be ready, probe every slave's PREADY output, not just the shared line. An unselected slave driving 0 onto the shared/muxed return is the default-ready violation — the non-local cause that stalls whoever is selected. This question is last because it is the least common, but it is the one a same-slave search will never find.

Two refinements sharpen the walk:

  • Probe internal signals, not just the bus. The entire bisection past Q1 depends on seeing the slave's data_valid/done/counter and each slave's individual PREADY driver — not just the muxed line the manager sees. If you only have the bus, you can do Q1; from Q2 onward you need to bring out the internals (a debug net, a force/probe, or RTL signals in sim). Localisation speed is observability.
  • A conditional hang lives at Q2 or Q3 but only under conditions. The cross-clock done that is missed when a pulse is too narrow looks like a Q2 failure (data_valid never rises) — but only on some clock ratios. The reset-stuck flop looks like a Q3 registered failure — but only on the reset-deassert that leaves it 0. When the hang is intermittent, run the bisection on a failing capture and suspect a CDC or reset condition at whichever junction breaks.
A top-down decision tree for localising a stuck-low PREADY. Q1 tests whether PSEL and PENABLE are high, branching to an upstream-fault leaf. Q2 tests whether the slave's internal done rises, branching to a broken-completion-path leaf listing forgotten done, no-exit FSM, and never-terminating counter. Q3 splits registered versus combinational PREADY to find a reset-stuck flop or a false gating term. Q4 asks whether the addressed slave or an idle slave on the shared return is pulling the line low — the default-ready violation. Causes are in red, fixes in green.
Figure 1 — the stuck-low PREADY bisection tree. Start at Q1: are PSEL and PENABLE both high at the slave? NO means the fault is upstream in the manager or decode, not PREADY. YES proceeds to Q2: does the addressed slave's internal done / data_valid ever rise? NO localises the bug to the completion path — a forgotten done term, an FSM state with no exit, or a wait counter that never reaches its terminal. YES proceeds to Q3: done is high but PREADY is low, so is PREADY registered (a flop that never loads its 1 — reset-stuck, impossible enable, or done sampled when low) or combinational (an AND-term that cannot be true for this access type)? Q4 then asks whether the addressed slave or an unselected slave on the shared return is pulling PREADY low — the non-local default-ready violation. Each leaf names the cause in red and the fix in green; a side note flags the cross-clock case where a narrow done pulse is missed every cycle.

4. Real SoC implementation

The three highest-frequency stuck-low causes are each one or two lines of wrong RTL, and the bisection above is exactly how you reach each. Here are the wrong-versus-right pairs with real signal names, ordered as the tree visits them: the FSM state with no exit (a Q2 failure — done never rises), the reset-stuck flop (a Q3 registered failure — flop never loads its 1), and the never-terminating wait counter (also a Q2 failure — terminal unreachable).

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ============================================================
// CAUSE A (Q2: done never rises) — FSM STATE WITH NO EXIT
// ============================================================
// WRONG: ACCESS only leaves on op_multi_beat; a single-beat read has no exit
// edge, so the FSM sits in ACCESS forever, DONE is never reached, PREADY=0.
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 never enters DRAIN
            // ...and NO 'else state <= DONE' -> ACCESS has no exit for single-beat
    DRAIN:                          state <= DONE;
    DONE:                           state <= IDLE;
  endcase
end
assign pready = (state == DONE);    // DONE unreachable for single-beat -> STUCK LOW
 
// CORRECT: every access path has a reachable exit that arrives at completion.
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;
            else                    state <= DONE;   // single-beat exit -> reachable
    DRAIN:  if (drain_done)         state <= DONE;
    DONE:                           state <= IDLE;
  endcase
end
assign pready = (state == DONE);    // every access now reaches DONE -> PREADY rises
 
// ============================================================
// CAUSE B (Q3 registered: flop never loads its 1) — RESET-STUCK PREADY
// ============================================================
// WRONG: PREADY resets to 0 and the only assignment that could set it high is
// guarded by a condition that is never true in the access phase, so the flop
// holds 0 forever -> the manager waits forever.
always_ff @(posedge pclk or negedge presetn) begin
  if (!presetn)                         pready <= 1'b0;
  else if (data_valid && !penable)      pready <= 1'b1;  // BUG: needs !penable, but
                                                         // data_valid only holds DURING
                                                         // the access, when penable IS 1
  // no other path drives pready high -> it never leaves 0 -> STUCK LOW
end
 
// CORRECT: drive PREADY high on the real data_valid inside the access phase,
// from a known 0-out-of-reset flop, and drop it after the single completion edge.
always_ff @(posedge pclk or negedge presetn) begin
  if (!presetn)                                  pready <= 1'b0;  // known, low, reset
  else if (psel && penable && data_valid && !pready)
                                                 pready <= 1'b1;  // assert on valid data
  else if (psel && penable && pready)
                                                 pready <= 1'b0;  // one-cycle complete
  else                                           pready <= 1'b0;  // idle / setup
end
 
// ============================================================
// CAUSE C (Q2: terminal unreachable) — NEVER-TERMINATING WAIT COUNTER
// ============================================================
// WRONG: the wait counter is loaded with a value, but the decrement is gated on
// a term that is false this access, so it never reaches 0 -> done never fires.
always_ff @(posedge pclk or negedge presetn) begin
  if (!presetn)                 wait_cnt <= '0;
  else if (psel && !penable)    wait_cnt <= WAIT_INIT;            // load at SETUP
  else if (penable && mem_ack)  wait_cnt <= wait_cnt - 1'b1;     // BUG: mem_ack never
                                                                 // asserts for THIS region
end
assign done   = (wait_cnt == 0) && penable;   // wait_cnt never hits 0 -> done STUCK LOW
assign pready = done;
 
// CORRECT: the counter must reach its terminal on a term that is GUARANTEED to
// advance every access cycle; gate the decrement on the access phase itself.
always_ff @(posedge pclk or negedge presetn) begin
  if (!presetn)                          wait_cnt <= '0;
  else if (psel && !penable)             wait_cnt <= WAIT_INIT;
  else if (psel && penable && |wait_cnt) wait_cnt <= wait_cnt - 1'b1;  // advances every
                                                                       // access cycle
end
assign done   = (psel && penable && wait_cnt == 0);  // guaranteed to reach 0 -> done rises
assign pready = done;

Two facts drive every fix. First, a stuck-low fix is structural: guarantee a reachable path from "access started" to pready=1. The no-exit FSM, the reset-stuck flop, and the dead counter are all the same shape — a junction the access can never get past — and the fix is always to make the junction reachable: an exit edge for every state, a rise condition that can be true inside the access, a decrement that must advance. Second, the bisection maps directly onto the RTL: the junction the tree stops at is the line you fix. Q2-stops are completion-path lines (FSM exits, counter terminals, done terms); Q3-registered stops are the flop's enable expression; Q3-combinational stops are the AND-terms; Q4 stops are the !sel default on a different slave. (The non-local default-ready cause and its !sel -> 1 fix are detailed in the catalogue and the slave RTL templates; they are a Q4 leaf here, not re-derived.)

5. Engineering tradeoffs

The bisection itself is the deliverable: one row per junction, what to probe, what the answer means, and which cause it isolates. Memorise the order — Q1 before Q2 before Q3 before Q4 — because each question is cheaper and more common than the next, so the order minimises expected debug time.

StepWhat you probe"Missing" answer meansCause isolated · fix
Q1 — access reached?PSEL and PENABLE at the slave's pinsOne is low → the access phase was never enteredUpstream: manager/decode never selected or enabled. Fix PSEL / PENABLE, not PREADY
Q2 — done rises?Slave-internal data_valid / done / counter terminalNever asserts → completion path is brokenForgotten done, FSM state with no exit, or counter that never hits terminal. Make the path reachable
Q3a — registered PREADYThe PREADY flop's enable / rise conditionDone is high but flop never loads 1Reset-stuck (0 out of reset, no rise condition) or impossible enable. Drive high on real data_valid
Q3b — combinational PREADYEach AND-term of PREADY's expressionOne term is false / wrong-polarity this accessGating that cannot be true for this access type. Trace each term to its driver and fix the dead one
Q4 — which slave?Every slave's individual PREADY driverAn unselected slave drives the shared line 0Default-ready violation (non-local). Drive PREADY = 1 when !sel or leave the return mux

The throughline: localise by bisection, fix by reaching the unreachable. The tradeoff is observability versus speed: Q1 needs only the bus, but Q2–Q4 need internal nets (data_valid, the counter, each slave's PREADY). If those internals are not brought out, you cannot bisect past Q1 and you are back to guessing — so the cheap up-front investment is wiring debug visibility for the completion path before you need it. A second tradeoff is conditional causes: the cross-clock and reset-polarity hangs only fail under specific phase/reset conditions, so you must bisect a failing capture, not a passing one, and a single green run does not retire the bug. (Defensively, the manager-side watchdog from multiple-wait-cycles converts the silent hang into a flagged one so you capture the failing case at all; wait-state propagation explains how the stall ripples up the bridge.)

6. Common RTL mistakes

7. Debugging scenario

Walk one realistic stuck-low capture end to end with the bisection — a memory-mapped peripheral whose reads hang only for a particular address window, which is exactly the kind of case where guessing wastes a day and the tree finds it in four probes.

  • Observed symptom: firmware reads from one peripheral's high register window and the core locks up; a reset recovers it. Reads to the peripheral's low window work. The bus monitor shows the manager parked on the failing address with PSEL and PENABLE high and PREADY flat low — the access cycle repeating forever (Figure 2). No protocol assertion fired; the only sign of life is the manager-side watchdog from multiple-wait-cycles eventually flagging a max-wait timeout, which is what captured the failing window at all.
  • Q1 — access reached? Probe the slave pins: PSEL high, PENABLE high. The access phase is entered, so this is not a PSEL/PENABLE upstream problem — it is genuinely a PREADY that won't rise. Proceed.
  • Q2 — does the internal done rise? Bring out the slave's data_valid and the wait counter. On the failing capture, data_valid never asserts and wait_cnt is frozen at its loaded value — it never decrements. The completion path is broken: the counter's terminal is unreachable for this access. The fault is behind PREADY, in the done logic — Q3 and Q4 are not even needed.
  • Root cause: the wait counter decrements on psel && penable && mem_ack, where mem_ack is the backing memory's acknowledge — but the high address window is unmapped in the memory, so mem_ack never asserts for it. The counter loads WAIT_INIT at SETUP and then sits there forever; done = (wait_cnt == 0) never fires; PREADY never rises. The low window is mapped, mem_ack comes back, the counter drains, and those reads complete — which is why only the high window hangs.
  • Correct RTL: gate the decrement on a term guaranteed to advance every access cycle, and produce an error completion for an unmapped region rather than hanging — an unmapped read should return a slave error and PREADY=1, never an infinite wait (see decode handling):
    Azvya Education Pvt. Ltd.VLSI Mentor
    Snippet
    always_ff @(posedge pclk or negedge presetn)
      if (!presetn)                          wait_cnt <= '0;
      else if (psel && !penable)             wait_cnt <= WAIT_INIT;
      else if (psel && penable && |wait_cnt) wait_cnt <= wait_cnt - 1'b1; // always advances
    assign done   = (psel && penable && wait_cnt == 0);  // terminal now reachable
    assign pready = done;                                // rises for EVERY access window
  • Verification assertion: prove bounded completion so any future unreachable-done regresses loudly instead of hanging to timeout:
    Azvya Education Pvt. Ltd.VLSI Mentor
    Snippet
    // bounded-completion liveness: once in ACCESS, PREADY must rise within N cycles
    a_bounded_complete: assert property (@(posedge pclk) disable iff (!presetn)
      (psel && penable) |-> ##[1:N] pready);
  • Debug habit: when a bus hangs, do not start reading the PREADY expression — run the bisection. Q1 (access reached?) clears the upstream class in one probe; Q2 (internal done?) tells you in the second probe whether the bug is behind PREADY (completion path) or in front of it (gating) — and that single split is what turned "stare at the slave for a day" into "the counter never decrements because mem_ack never comes for the unmapped window." The address-window clue ("only the high range hangs") plus a frozen internal counter is the fingerprint of a completion term gated on something that cannot occur for that access.
An APB read timing diagram showing a stuck-low PREADY hang. PCLK runs across the top; PSEL and PENABLE both go high and stay high through the ACCESS phase; PREADY is drawn flat low in amber and never rises; PADDR and PWRITE are held stable while the manager waits; PRDATA is never presented. Dashed markers show the manager sampling PREADY low on every rising edge, and annotations note the manager waits forever and the failure is the absence of an event.
Figure 2 — the stuck-low capture this scenario localises. PSEL and PENABLE go high and stay high; the access enters its ACCESS-wait phase and never leaves. PREADY (amber) is flat low across every sampling edge — it never asserts — so the manager holds PADDR and PWRITE stable and re-enters the same wait cycle indefinitely; PRDATA is never presented because the read never completes. The figure marks every rising edge where the manager samples PREADY low and notes the defining property of the bug: the failure is the absence of an event — nothing wrong happens on any edge, completion just never arrives — which is why it is invisible to checkers that watch for violations and is caught instead by a bounded-completion watchdog and localised by the bisection of Figure 1.

8. Verification perspective

Because a stuck-low PREADY is a liveness failure — something good (completion) never happens — it needs a bounded-liveness property, not a safety check. A safety assertion fires when something wrong occurs; here nothing wrong occurs, so the assertion must instead require that something right happens within a bound.

  • Bounded-completion is the one property that catches the whole stuck-low family. A single liveness assertion — once PSEL && PENABLE assert, PREADY must rise within N cycles — catches the no-exit FSM, the reset-stuck flop, the never-terminating counter, and the impossible-gate case in one stroke: assert property (@(posedge pclk) disable iff(!presetn) (psel && penable) |-> ##[1:N] pready);. Choose N as the slave's worst-case legal wait bound plus margin; a failure means a real completion path is unreachable, not merely slow. This is the offensive counterpart to the manager-side timeout of multiple-wait-cycles: the watchdog survives the hang in the field; this assertion finds the unreachable path in sim and pins the cycle it should have completed.
  • Make the liveness check non-vacuous and cover each cause. A bounded-completion property passes vacuously if psel && penable never holds, so pair it with a cover that the antecedent was actually reached, and add per-cause covers so a green run proves each stuck-low path was exercised: cover property ((psel && penable) ##[1:N] pready); for normal completion, and a directed cover for the boundary case (single-beat access, the unmapped/extreme address window) that the no-exit and dead-counter bugs hide in. A liveness pass with no cover is silent about whether you ever drove the access type that hangs.
  • The default-ready (Q4) cause needs a multi-slave check the single-slave bench cannot see. Assert per slave that an unselected slave does not pull the shared PREADY low — !sel |-> pready per driver, or a bus-level "no idle slave drives the return low." A single-slave testbench will never reach this; it is a property of the shared return, so it must be bound at the bus level (see apb-assertions for binding patterns and apb-monitors for reconstructing which slave drove the line).
  • The conditional causes need their condition in the stimulus. The cross-clock narrow-pulse miss only hangs on certain clock ratios, and the reset-polarity stuck flop only on the reset-deassert that leaves it 0. Bounded-completion will catch them if the regression drives those conditions — so sweep clock ratios for the CDC done term and include a reset-during/after-access scenario; a clean run at one ratio is necessary, not sufficient.

The point: a stuck-low PREADY reduces to one named liveness property (bounded completion) plus a non-vacuity cover, one multi-slave default-ready check for the non-local Q4 cause, and conditions in the stimulus for the intermittent causes — not a pile of directed reads. The bisection localises a hang you already have; these properties stop it shipping in the first place.

9. Interview discussion

"The APB bus is hung — PREADY never rises — how do you debug it?" is a sharp senior screening question because a weak answer lists causes ("maybe the FSM, maybe reset...") while a strong answer presents a procedure that reaches the cause deterministically — which is exactly what separates someone who has localised a real hang from someone who has only read the bug list.

Lead with the frame: a stuck-low PREADY is a liveness failure — the absence of completion — and every cause looks identical on the bus, so you cannot solve it by staring; you bisect the path from "access started" to "PREADY=1". Then walk the four junctions crisply: Q1, are PSEL and PENABLE both high at the slave — if not, it is an upstream PSEL/PENABLE problem, not PREADY; Q2, does the slave's internal data_valid/done ever rise — if not, the bug is behind PREADY in the completion path (forgotten done, FSM state with no exit, counter that never hits its terminal); Q3, if done is high but PREADY is low, is PREADY registered (a flop that never loads its 1 — reset-stuck or impossible enable) or combinational (a dead AND-term); and Q4, the one that catches people — is it even this slave, or is an unselected slave pulling the shared return low (the non-local default-ready violation). Land the depth points: probe internal signals, because past Q1 the bus alone is blind; the fix is always structural — make the unreachable junction reachable (an exit for every state, a rise condition that can be true in the access, a decrement that must advance); and the verification is one bounded-completion liveness property ((psel && penable) |-> ##[1:N] pready) plus a default-ready check for Q4. Closing with "and I bisect the failing capture, not a passing one, because the cross-clock and reset causes are conditional — and an unmapped address window that hangs instead of erroring is the classic Q2 fingerprint" signals real silicon debugging.

10. Practice

  1. Run the tree. Given a hung capture with PSEL high, PENABLE low, and PREADY low, state which junction the bisection stops at, what it tells you, and where the real fault is.
  2. Behind or in front of PREADY? For each — (a) data_valid never rises; (b) data_valid is high for three cycles but PREADY stays low; (c) reads to address window B hang while window A works — name the junction (Q2/Q3/Q4) and the most likely cause.
  3. Fix the dead counter. Given a wait counter that decrements on psel && penable && mem_ack and an address region where mem_ack never asserts, explain why the access hangs and rewrite the decrement so the terminal is guaranteed reachable.
  4. Write the watchdog. Write the bounded-completion SVA property for stuck-low, state how you choose N, and add the cover that keeps it from passing vacuously.
  5. Find the non-local one. Describe the probe that distinguishes "the addressed slave is holding PREADY low" from "an unselected slave is pulling the shared return low," and the one-line RTL fix for the latter.

11. Q&A

12. Key takeaways

  • A stuck-low PREADY is a liveness failure — the absence of completion — and every cause looks identical on the bus, so you localise it by bisection, not by staring or guessing. Walk the path from "access started" to "PREADY=1" and find the first junction where the expected value is missing; that junction is the cause.
  • The four junctions, in order: Q1 — are PSEL and PENABLE both high (else the fault is upstream, not PREADY); Q2 — does the slave's internal done/data_valid/counter ever rise (else the completion path is broken: forgotten done, FSM state with no exit, never-terminating counter); Q3 — done is high but PREADY is low, so is it a registered flop that never loads its 1 or a combinational AND-term that can't be true; Q4 — is it even this slave, or an unselected one pulling the shared return low.
  • Probe internal signals, not just the bus. Past Q1 the bus is blind — you need data_valid, the wait counter, and each slave's individual PREADY driver. Localisation speed is observability; wire the completion-path visibility before you need it.
  • The fix is always structural: make the unreachable junction reachable — an exit edge for every FSM state, a rise condition that can be true inside the access, a decrement that must advance every access cycle, and a !sel -> 1 default so an idle slave never stalls another. The detailed wrong-versus-right RTL for each lives in the common-design-bugs catalogue.
  • The verification reduces to one bounded-completion liveness property(psel && penable) |-> ##[1:N] pready — plus a non-vacuity cover and a multi-slave default-ready check for the non-local Q4 cause; bisect a failing capture for the conditional cross-clock and reset causes, because a single passing run does not clear them.