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.
// Whether this actually stalls the bus depends on how the interconnect
// combines the subordinates' PREADY outputs. A bridge that MUXES by PSEL is
// immune; one that ANDs them - a common and legal implementation choice -
// hangs on every access to any other subordinate. AMBA specifies PREADY as a
// per-subordinate output and leaves the combining to the bridge, so driving 1
// when unselected is the portable habit rather than a protocol requirement.
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.)

4b. A complete subordinate with configurable wait states

The snippets above fix individual bugs. This is the whole subordinate they belong to — the IDLE / SETUP / ACCESS model with a parameterised wait count, written so that each of those bugs is structurally unreachable.

apb_wait_slave.sv — parameterised APB4 subordinate
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module apb_wait_slave #(
  parameter int ADDR_W = 12,
  parameter int DATA_W = 32,
  // Wait cycles inserted in ACCESS before completing. 0 = always-ready.
  parameter int WAITS  = 3
) (
  input  logic                  pclk,
  input  logic                  presetn,
  // APB4 subordinate interface
  input  logic                  psel,
  input  logic                  penable,
  input  logic                  pwrite,
  input  logic [ADDR_W-1:0]     paddr,
  input  logic [DATA_W-1:0]     pwdata,
  input  logic [DATA_W/8-1:0]   pstrb,
  output logic [DATA_W-1:0]     prdata,
  output logic                  pready,
  output logic                  pslverr
);
  // APB phases, named for what they are. SETUP is PSEL high with PENABLE low
  // and lasts exactly one cycle; ACCESS is both high and lasts until PREADY.
  typedef enum logic [1:0] { P_IDLE, P_SETUP, P_ACCESS } phase_e;
  phase_e phase;
 
  always_ff @(posedge pclk or negedge presetn) begin
    if (!presetn) phase <= P_IDLE;
    else unique case (phase)
      P_IDLE  : phase <= (psel && !penable) ? P_SETUP : P_IDLE;
      P_SETUP : phase <= (psel &&  penable) ? P_ACCESS : P_IDLE;
      // Stay in ACCESS until the transfer completes. PENABLE must drop after,
      // so a back-to-back transfer re-enters through SETUP, never ACCESS.
      P_ACCESS: phase <= (psel && penable && pready)
                         ? ((psel && !penable) ? P_SETUP : P_IDLE)
                         : P_ACCESS;
    endcase
  end
 
  // ── Context captured in SETUP. APB holds address, direction and write data
  //    stable for the whole transfer, so this could be read combinationally -
  //    capturing makes the "context stable" assumption explicit and gives the
  //    assertions something to check against.
  logic [ADDR_W-1:0]   addr_q;
  logic                write_q;
  logic [DATA_W-1:0]   wdata_q;
  logic [DATA_W/8-1:0] strb_q;
 
  always_ff @(posedge pclk or negedge presetn)
    if (!presetn) begin
      addr_q <= '0; write_q <= 1'b0; wdata_q <= '0; strb_q <= '0;
    end else if (psel && !penable) begin          // SETUP
      addr_q <= paddr; write_q <= pwrite; wdata_q <= pwdata; strb_q <= pstrb;
    end
 
  // ── Wait counter.
  //    The off-by-one lives here, and the shape that avoids it is to count
  //    cycles ALREADY SPENT in ACCESS and compare with >=, rather than
  //    counting down and testing ==0. See the Debug Lab below.
  localparam int CW = (WAITS == 0) ? 1 : $clog2(WAITS + 1);
  logic [CW-1:0] wait_cnt;
 
  wire in_access = (phase == P_ACCESS);
  wire waits_met = (wait_cnt >= WAITS[CW-1:0]);
 
  always_ff @(posedge pclk or negedge presetn)
    if (!presetn)          wait_cnt <= '0;
    else if (!in_access)   wait_cnt <= '0;            // re-arm between transfers
    else if (!waits_met)   wait_cnt <= wait_cnt + 1'b1;
 
  // ── The response, and the term that makes it valid.
  //    PREADY is derived from the SAME condition that makes PRDATA valid, so
  //    readiness can never lead the data.
  logic [DATA_W-1:0] mem [16];
  wire [ADDR_W-1:0]  idx = addr_q[5:2];
  wire               unmapped = (addr_q[ADDR_W-1:6] != '0);
 
  wire response_valid = in_access && waits_met;
 
  // PREADY: valid only in ACCESS, and 1 when unselected so a bridge that ANDs
  // the subordinates' PREADY outputs is not stalled by this one.
  assign pready = (psel && penable) ? response_valid : 1'b1;
 
  // PRDATA and PSLVERR are only meaningful on the completing cycle. Driving
  // them from the same response_valid term keeps them phase-aligned.
  assign prdata  = (response_valid && !write_q && !unmapped) ? mem[idx] : '0;
  assign pslverr =  response_valid && unmapped;
 
  // ── The write commits on the completing cycle, exactly once.
  wire commit = psel && penable && pready && write_q && !unmapped;
 
  always_ff @(posedge pclk)
    if (commit)
      for (int b = 0; b < DATA_W/8; b++)
        if (strb_q[b]) mem[idx][b*8 +: 8] <= wdata_q[b*8 +: 8];
endmodule

Checking it

apb_wait_slave_tb.sv — self-checking, counts the wait cycles
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module apb_wait_slave_tb;
  localparam int AW = 12, DW = 32, W = 3;
 
  logic pclk = 0, presetn = 0;
  logic psel = 0, penable = 0, pwrite = 0;
  logic [AW-1:0]   paddr  = '0;
  logic [DW-1:0]   pwdata = '0;
  logic [DW/8-1:0] pstrb  = '1;
  logic [DW-1:0]   prdata;
  logic            pready, pslverr;
 
  apb_wait_slave #(.ADDR_W(AW), .DATA_W(DW), .WAITS(W)) dut (.*);
  always #5 pclk = ~pclk;
  int fails = 0;
 
  // One APB transfer. Returns how many ACCESS cycles were spent waiting, so a
  // test can assert the wait count rather than only the data.
  task automatic xfer (input bit wr, input logic [AW-1:0] a,
                       input logic [DW-1:0] wd,
                       output logic [DW-1:0] rd, output logic err,
                       output int waits_seen);
    waits_seen = 0;
    @(negedge pclk); psel = 1; penable = 0; pwrite = wr;
                     paddr = a; pwdata = wd;              // SETUP
    @(negedge pclk); penable = 1;                          // ACCESS
    forever begin
      @(posedge pclk);
      if (pready) break;
      waits_seen++;
    end
    rd = prdata; err = pslverr;
    @(negedge pclk); psel = 0; penable = 0;                // PENABLE must drop
  endtask
 
  task automatic chk (input string what, input int got, exp);
    if (got !== exp) begin
      $error("%s: got %0d expected %0d", what, got, exp); fails++;
    end else $display("PASS %-38s = %0d", what, got);
  endtask
 
  logic [DW:0] rd; logic er; int w;
 
  initial begin
    repeat (2) @(negedge pclk); presetn = 1;
 
    // The wait count is the check that catches an off-by-one. Asserting only
    // the data would pass a subordinate that completes a cycle early or late.
    xfer(1, 'h010, 32'hA5A5_1234, rd, er, w);
    chk("write inserts exactly WAITS cycles", w, W);
 
    xfer(0, 'h010, '0, rd, er, w);
    chk("read  inserts exactly WAITS cycles", w, W);
    chk("read-back data", rd, 32'hA5A5_1234);
 
    xfer(0, 'h400, '0, rd, er, w);      // beyond the mapped window
    chk("unmapped access asserts PSLVERR", er, 1'b1);
 
    if (fails == 0) $display("All APB subordinate checks passed.");
    else            $fatal(1, "%0d check(s) failed", fails);
    $finish;
  end
endmodule
apb_wait_slave_sva.sv — phase rules and response validity
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module apb_wait_slave_sva #(parameter int MAX_WAIT_POLICY = 32) (
  input logic pclk, presetn,
  input logic psel, penable, pwrite, pready, pslverr,
  input logic [11:0] paddr, input logic [31:0] pwdata
);
  // SETUP lasts exactly one cycle.
  a_setup_one_cycle: assert property (@(posedge pclk) disable iff (!presetn)
    (psel && !penable) |=> (psel && penable));
 
  // ACCESS persists until PREADY - the manager cannot abort a transfer.
  a_access_holds: assert property (@(posedge pclk) disable iff (!presetn)
    (psel && penable && !pready) |=> (psel && penable));
 
  // PENABLE drops after completion, so back-to-back transfers get their own
  // SETUP cycle. Running ACCESS into ACCESS is a protocol violation even
  // though many subordinates tolerate it.
  a_penable_drops: assert property (@(posedge pclk) disable iff (!presetn)
    (psel && penable && pready) |=> !penable);
 
  // Address, direction and write data are stable for the whole transfer -
  // which is what lets the subordinate capture them once in SETUP.
  a_context_stable: assert property (@(posedge pclk) disable iff (!presetn)
    (psel && !(penable && pready)) |=>
      $stable(paddr) && $stable(pwrite) && $stable(pwdata));
 
  // PSLVERR is only meaningful on the completing cycle.
  a_pslverr_qualified: assert property (@(posedge pclk) disable iff (!presetn)
    !(psel && penable && pready) |-> !pslverr);
 
  // ENGINEERING POLICY, not protocol: AMBA places no maximum on wait states.
  // Written unbounded as `##[1:$] pready` this could never fail in simulation -
  // a stuck PREADY would leave one incomplete attempt and zero failures while
  // the run hangs. Bound it from the subordinate's own worst case.
  a_completes_within_policy: assert property (@(posedge pclk) disable iff (!presetn)
    (psel && penable && !pready) |-> ##[1:MAX_WAIT_POLICY] pready)
    else $error("PREADY did not rise within %0d cycles (policy bound)",
                MAX_WAIT_POLICY);
 
  c_waited: cover property (@(posedge pclk) psel && penable && !pready);
  c_error:  cover property (@(posedge pclk) psel && penable && pready && pslverr);
endmodule
1

Every access completed one cycle early and the data was right anyway

WAIT-COUNTER-OFF-BY-ONE
Symptom

A peripheral configured for three wait states completed its accesses in two. The data was correct on every transfer, no assertion fired, and the functional regression was entirely clean — the only visible effect was that a throughput measurement came out about 8% higher than the performance model predicted.

The discrepancy was attributed to model inaccuracy for two months. It surfaced properly when the same subordinate was re-used behind a genuinely slow memory, where completing a cycle early meant returning data the memory had not yet produced — intermittent, address-dependent corruption that reproduced on roughly one access in a thousand.

Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Count down from WAITS and complete when the counter reaches zero.
always_ff @(posedge pclk or negedge presetn)
  if (!presetn)            wait_cnt <= WAITS;
  else if (!in_access)     wait_cnt <= WAITS;
  else if (wait_cnt != 0)  wait_cnt <= wait_cnt - 1'b1;
 
assign pready = (psel && penable) ? (wait_cnt == 0) : 1'b1;
Diagnostic Evidence

Counting the ACCESS cycles rather than checking the data made it immediate:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  WAITS parameter        = 3
  ACCESS cycles observed = 3   (1 completing + 2 waiting)
  ACCESS cycles expected = 4   (1 completing + 3 waiting)
 
  cycle        1     2     3
  phase      ACCESS ACCESS ACCESS
  wait_cnt     3     2     1  -> 0 loaded at the same edge PREADY is sampled
  pready       0     0     1

The counter is loaded with WAITS on the cycle the subordinate enters ACCESS, so its first decrement happens on the first ACCESS cycle rather than after it. Three decrements therefore span two waiting cycles plus the completing one.

The reason no check caught it is that every existing test asserted the returned data, and the data was correct — the memory happened to be fast enough to have it ready. Nothing asserted the timing, so a subordinate that completed early looked identical to one that completed on schedule.

Root Cause

An off-by-one between "cycles of wait inserted" and "decrements performed". The counter is armed on entry to ACCESS and decremented on every ACCESS cycle including the first, so WAITS decrements produce WAITS − 1 waiting cycles before the completing one.

The general trap is that a countdown couples two things that should be independent: the arming edge and the counting edge. Whether the first ACCESS cycle counts depends on exactly when the load happens relative to the phase transition, which is easy to get right by accident and easy to get wrong by accident, and impossible to see by reading either the load or the decrement alone.

What made it survive was the class of check being run. A data check passes whenever the underlying resource happens to be ready, so a subordinate that completes early is correct-by-luck on fast memory and corrupt on slow memory. The bug was latent from the first day and only became a defect when the memory changed.

Fix

Count cycles already spent and compare with >=, so the arming edge and the comparison are independent:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
wire waits_met = (wait_cnt >= WAITS[CW-1:0]);
 
always_ff @(posedge pclk or negedge presetn)
  if (!presetn)        wait_cnt <= '0;
  else if (!in_access) wait_cnt <= '0;            // re-arm between transfers
  else if (!waits_met) wait_cnt <= wait_cnt + 1'b1;
 
assign pready = (psel && penable) ? (in_access && waits_met) : 1'b1;

Counting up from zero makes WAITS = 0 degenerate correctly to an always-ready subordinate with no special case, which the countdown form does not.

The test that fails on the old RTL and passes on the new one is the one the original plan lacked: assert the wait count, not just the data. The waits_seen output in the testbench above exists for exactly this, and the check is chk("inserts exactly WAITS cycles", w, W) — exactly, not "at least", because "at least" passes a subordinate that never completes.

The habit worth generalising: a timing parameter deserves a timing assertion. Any block with a configurable latency should have a test that measures the latency and compares it against the parameter, because a data-only check cannot distinguish a correct latency from a lucky one. The same reasoning applies to AHB wait-state generation, where the HREADYOUT low duration is the equivalent quantity.

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

11b. Where This Is Specified

  • Arm AMBA APB Protocol Specification (ARM IHI 0024). The IDLE → SETUP → ACCESS transfer model; PSEL asserted with PENABLE low for exactly one SETUP cycle; PENABLE asserted throughout ACCESS; the transfer completing only when PSEL && PENABLE && PREADY; address, direction and write data held stable for the duration of a transfer; PRDATA and PSLVERR valid only on the completing cycle; and the absence of any maximum wait-state count.
  • Arm AMBA APB, APB4 additions. PSTRB write strobes and PPROT, and the definition of PREADY as a per-subordinate output whose combination across subordinates is a bridge implementation choice.
  • IEEE 1800-2023 §16 — Assertions. The concurrent properties and cover property used above, and the ##[m:n] bounded range that makes the policy completion bound falsifiable in simulation.
  • IEEE 1800-2023 §11.4.14 and §12.5.3 — the unique case used by the phase FSM and its violation reporting.

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.