Skip to content

AMBA APB · Module 11

PREADY Generation

Building the slave's PREADY generator — always-ready, a fixed-N counter, an event-driven done, or a small FSM — with the unselected-ready rule, a bounded-completion guarantee that never deadlocks, and how the generator gates the write commit and read-data validity.

Every APB access ends when the slave says so, and the signal it says it with is PREADY. The previous chapters built the slave's body — the register bank, the read path, the write logic. This chapter builds the slave's clock on the access: the PREADY generator, the piece of RTL that decides, every cycle, "am I done with this transfer yet?" The single idea to carry: PREADY generation is a design choice, not a protocol fact — pick the simplest form that matches the peripheral's real latency, guarantee it always eventually completes, and make sure an unselected slave never pulls the shared PREADY low. Get those three right and the generator is a few lines of obvious RTL; get any wrong and the slave either hangs the whole bus or corrupts another slave's transfer.

1. Problem statement

The problem is producing, on every clock, a single bit that tells the bus whether this slave has finished the current access — and producing it so the bus never stalls forever and never stalls a different slave by mistake.

PREADY is the slave's only lever over transfer timing. The master drives address, control, and write data; it then waits in the ACCESS phase for the slave to raise PREADY. Until that happens the transfer is held — PENABLE stays high, the master is blocked, and on a shared bus every other access behind it is blocked too. So the generator is not cosmetic timing trim; it is the thing that decides how long the master is held and whether the bus moves at all. That forces three design decisions before any RTL is written:

  • What shape does this peripheral's latency actually have? A register read is done the same cycle — zero wait. A peripheral with a fixed pipeline is done after a known number of cycles. A peripheral fronting a slow or asynchronous source (an off-chip memory, a CDC fabric) is done only when that source signals it. Each shape wants a different generator: tie-high, a counter, or an event follower. Choosing the wrong shape either wastes cycles or, worse, fakes completion before the data is real.
  • How do you guarantee the access always completes? A generator that waits on an external event can, in a corner case, wait forever if that event never arrives. A slave that holds PREADY low indefinitely deadlocks the bus. So a variable-latency generator must be bounded — there must be a path that forces completion within a known number of cycles, no matter what the source does.
  • What does this slave drive when it is not selected? On a shared PREADY the answer is critical: an unselected slave must drive PREADY high (or release it so the selected slave wins), never low. Pull it low and you hold up whichever slave the master is actually talking to.

So the job is not "make a ready signal" — it is to choose a generator matched to real latency, prove it always completes, and obey the unselected-ready rule, all while gating the write-commit and read-data exactly on the cycle it asserts.

2. Why previous knowledge is insufficient

You already know the protocol and timing of PREADY cold — but none of that tells you how to build the generator for a real peripheral.

  • Module 8 gave you the timing discipline, not the design. pready-timing derived the protocol view: PREADY is sampled on the rising clock edge in the ACCESS phase (PSEL & PENABLE), the combinational-vs-registered choice, the sample window, and the glitch hazard if PREADY moves near the edge. That chapter tells you when PREADY is observed and what shape it must hold. This chapter is the slave-design view: how to generate a value that obeys that discipline — the FSM, the counter, the event logic inside the slave. We apply Module 8's conclusions (registered-vs-combinational, clean edges, the sample window); we do not re-derive them.
  • Module 8 also covered multi-cycle waits as a protocol behaviour. multiple-wait-cycles showed what a multi-wait transfer looks like on the busPREADY low for several cycles, then high. It did not show how the slave decides the wait length. That decision — count to N, or follow a done, or step through an FSM — is exactly this chapter.
  • 11.3 and 11.4 built the things PREADY gates, but not the gate. The write logic commits the register write on the cycle PREADY and PENABLE are both high; the read path must present valid data on that same cycle. Those chapters assumed PREADY existed and asked "what fires when it asserts?" This chapter builds the thing that asserts it — and forward, pslverr-generation will hang the error flag off the same completion cycle, so the generator you build here is also where errors get reported.

So the model to add is the generator itself: a small, latency-matched, bounded state machine or counter that produces PREADY in obedience to Module 8's timing rules and gates the read/write paths of 11.3/11.4.

3. Mental model

The model: the PREADY generator is the slave's "are we there yet?" answering machine. The master asks the same question every cycle of the ACCESS phase — "done?" — by holding the access open. The generator answers 0 (not yet, keep waiting) until the peripheral's real work is finished, then answers 1 for exactly one cycle to close the transfer. The entire art is matching how the machine knows it's done to how the peripheral actually finishes.

Three refinements make it precise:

  • There are exactly four shapes of generator, in rising complexity. Always-ready — tie PREADY high; the peripheral finishes the same cycle (a plain register read/write). Fixed-N counter — load a counter when selected, assert ready at terminal count; for a peripheral with a known, constant latency. Event-driven done — hold PREADY low and assert it when a slow source raises a done / ready / ack strobe (a memory's mem_rdy, a synchronised CDC acknowledge); for variable latency. Small FSMIDLE → ACCESS/WAIT → READY; for multi-step responses where the slave must do several things in sequence. Always choose the simplest shape that matches the real latency — a counter for a tie-high peripheral is over-engineering; a tie-high for a slow source is a bug.
  • Completion must be bounded. The one invariant across all four shapes: from the moment the access starts, PREADY must assert within a finite, known number of cycles. Always-ready and fixed-N are bounded by construction. Event-driven and FSM are not automatically bounded — if you only leave the WAIT state on done, a lost or never-arriving done deadlocks the bus. The fix is a second exit: a timeout watchdog that forces READY (often with PSLVERR) after N cycles. A generator without a bounded exit is a latent bus hang.
  • An unselected slave is still driving — make it drive ready. When PSEL is low this slave is idle, but its PREADY output still exists. The rule: !PSEL ⇒ PREADY = 1. On a per-slave PREADY (muxed by the interconnect) this just keeps your default clean; on any topology where PREADY is shared or AND-combined, driving it low while unselected pulls down the selected slave's ready and stalls a transfer you have nothing to do with. The default-ready rule is part of the generator, not an afterthought.
A diagram with four stacked PREADY generation styles (always-ready, fixed-N counter, event-driven done, small FSM) on the left and an explicit IDLE→WAIT→READY FSM on the right, annotated with the unselected-ready rule and the bounded-completion guarantee in green.
Figure 1 — the PREADY generator, shown as its four generation styles and the FSM they generalise to. On the left the styles stack in rising complexity: always-ready (tie PREADY high for a single-cycle slave, zero wait), a fixed-N wait counter (count a known latency, assert ready at terminal count), an event-driven done (PREADY follows a slow source's done strobe — a memory ready or a CDC acknowledge), and a small FSM for multi-step responses. On the right that FSM is drawn explicitly: IDLE holds PREADY high because the slave is unselected; entering WAIT/ACCESS on PSEL and PENABLE holds PREADY low while the slave works; the WAIT→READY transition asserts PREADY for exactly one cycle to complete the transfer, then returns to IDLE. Two rules are annotated in green — the unselected-ready rule (!PSEL drives PREADY high, never pulling a shared PREADY low) and the bounded-completion guarantee (every WAIT path must reach READY within N cycles so the slave can never deadlock the bus). The figure shows that PREADY generation is the slave's am-I-done engine, also gating the write-commit and read-data validity.

4. Real SoC implementation

In RTL the generator is a small block parameterised on the peripheral's latency shape. Below is a realistic event-driven generator with a bounded watchdog — the most common non-trivial case, where the slave fronts a slow source (a memory or a synchronised CDC done) and must assert PREADY when that source finishes or time out so the bus never hangs. The always-ready and fixed-N cases fall out as the WATCHDOG_N = 0 / constant-count degenerate forms.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// PREADY generator for a slave fronting a variable-latency source.
// - holds PREADY low while the source works,
// - asserts PREADY for exactly one cycle when the source signals `done`,
// - is BOUNDED: a watchdog forces completion within WATCHDOG_N cycles,
// - obeys the unselected-ready rule: !PSEL drives PREADY high.
module apb_pready_gen #(
  parameter int WATCHDOG_N = 256            // bounded-completion ceiling
) (
  input  logic        pclk, presetn,
  input  logic        psel, penable,        // APB phase signals
  input  logic        src_done,             // slow source's "I'm finished" strobe
  output logic        kick_src,             // start the source on the SETUP→ACCESS edge
  output logic        timed_out,            // feeds PSLVERR generation (11.6)
  output logic        pready                // to the bus
);
  typedef enum logic [1:0] {IDLE, WAIT, DONE} state_e;
  state_e state, nstate;
  logic [$clog2(WATCHDOG_N+1)-1:0] cnt;
 
  // --- next-state: every WAIT path has a guaranteed exit (no deadlock) ---
  always_comb begin
    nstate = state;
    unique case (state)
      IDLE: if (psel & penable) nstate = WAIT;          // access just started
      WAIT: if (src_done || cnt == WATCHDOG_N)
                                 nstate = DONE;           // done OR timeout → exit
      DONE: nstate = IDLE;                                // one ready cycle, then idle
    endcase
  end
 
  always_ff @(posedge pclk or negedge presetn)
    if (!presetn) begin state <= IDLE; cnt <= '0; kick_src <= 1'b0; end
    else begin
      state    <= nstate;
      kick_src <= (state == IDLE) && (nstate == WAIT);   // 1-cycle start pulse
      cnt      <= (state == WAIT) ? cnt + 1'b1 : '0;     // watchdog only counts in WAIT
    end
 
  // --- PREADY: unselected-ready rule first, then the FSM's completion ---
  // !PSEL  -> drive READY high (never pull a shared PREADY low)
  // DONE   -> assert READY for exactly the one ACCESS cycle
  assign pready    = (!psel) ? 1'b1 : (state == DONE);
  assign timed_out = (state == WAIT) && (cnt == WATCHDOG_N) && !src_done;
endmodule

Three facts make this correct. First, the WAIT state cannot trap the bus — its exit is src_done || cnt == WATCHDOG_N, so even if the source's done is lost, the watchdog forces DONE within WATCHDOG_N cycles; the completion is bounded by construction. Second, PREADY is gated on PSEL: !psel → 1 enforces the unselected-ready rule before the FSM is even consulted, so this slave never holds down a shared ready while idle. Third, DONE is a one-cycle pulse that aligns the ready edge with the ACCESS phase — exactly the sample window Module 8 requires — and it is the single cycle on which the write logic commits and the read mux's data must be valid. The timed_out output is the hook the next chapter (PSLVERR generation) uses to turn a watchdog expiry into a clean error response rather than a silent fake-completion.

5. Engineering tradeoffs

Choosing a generation style is a trade between latency, RTL complexity, and — critically — deadlock risk. The rule is to climb this table no further than the peripheral's real latency demands.

Generation styleLatencyRTL complexityDeadlock riskWhen to use it
Always-ready (tie high)0 wait statesTrivial — assign pready = 1'b1 (gated by !psel)None — bounded by constructionPlain register read/write; any peripheral that truly finishes the same cycle
Fixed-N wait counterN (known, constant)Small — load + count downNone — terminal count always firesPeripheral with a known constant pipeline latency (e.g. a 3-cycle ROM)
Event-driven doneVariable, source-dependentModerate — follow a strobe + watchdogHigh if unbounded — must add a timeoutSlow/async source: external memory, CDC ack, slow I/O; latency not known at design time
Small FSM (IDLE→ACCESS→READY)Variable, multi-stepLarger — explicit states + watchdogHigh if any state lacks an exitMulti-step response: arbitration, a sequence of internal ops, or read-modify-write
Registered vs combinational PREADY+1 cycle if registeredRegistered is cleaner-edgedNone directlyRegister for clean edges / timing closure (8.3); accept the extra cycle

The throughline: simplicity that matches real latency is the goal, and bounded completion is non-negotiable. Tie-high costs nothing and can't deadlock — use it whenever it's honest. The moment latency becomes variable (event-driven or FSM) you take on deadlock risk and must buy it back with a watchdog. And the combinational-vs-registered choice is a timing-closure decision (apply Module 8): registering PREADY gives a clean, glitch-free edge but adds a cycle of latency to every access — pay it only when you need the timing margin, not by default.

6. Common RTL mistakes

7. Debugging scenario

The signature PREADY-generation bug is a slave that hangs the entire APB bus, caused by an event-driven generator whose WAIT state has no exit other than a done strobe that, in a corner case, never arrives.

  • Observed symptom: the SoC boots and runs, then under a specific workload the CPU locks up on an APB access — the access to one peripheral never returns, and because the bus is shared, every subsequent APB transaction (to unrelated peripherals) also hangs. A watchdog reset eventually fires. It is intermittent and load-dependent, and points at "the whole APB bus is dead," not one register.

  • Waveform clue: on the hung transfer, PSEL and PENABLE are both high and stay high indefinitely while PREADY sits low and never rises. The slave's internal state is stuck in WAIT, its watchdog counter (if any) is not incrementing or has no terminal action, and the source's src_done strobe is flat — it never pulsed for this access because the slow source was reset/aborted mid-operation in this corner.

  • Root cause: the generator's WAIT state has a single outgoing transition, WAIT → DONE guarded only by src_done. The slow source can, in a rare path (a CDC glitch, an aborted memory op, a clock-domain reset), fail to ever raise done. With no other exit, the FSM is trapped in WAIT, PREADY stays low, and the slave deadlocks the shared bus. The bug is an unbounded generator — completion was assumed, not guaranteed.

  • Correct RTL: add a bounded second exit — a watchdog counter that forces completion regardless of the source. The WAIT exit becomes src_done || cnt == WATCHDOG_N, and the timeout path drives PREADY high (and raises timed_outPSLVERR, 11.6, so the master learns the access failed rather than seeing a fake success):

    Azvya Education Pvt. Ltd.VLSI Mentor
    Snippet
    // BUG:  WAIT: if (src_done)                    nstate = DONE;  // can trap forever
    // FIX:  WAIT: if (src_done || cnt==WATCHDOG_N)  nstate = DONE;  // bounded exit
  • Verification assertion: prove bounded completion directly — once an access starts, PREADY must assert within WATCHDOG_N cycles, no matter what the source does:

    Azvya Education Pvt. Ltd.VLSI Mentor
    Snippet
    // After an access enters ACCESS (PSEL & PENABLE), PREADY must rise within N cycles.
    property p_bounded_pready;
      @(posedge pclk) disable iff (!presetn)
        (psel && penable && !pready) |-> ##[1:WATCHDOG_N] pready;
    endproperty
    a_bounded_pready: assert property (p_bounded_pready);
     
    // Unselected-ready: when not selected, this slave must drive PREADY high.
    a_unsel_ready: assert property (@(posedge pclk) disable iff (!presetn)
                                    (!psel) |-> pready);
  • Debug habit: when "the whole APB bus is hung," don't chase the peripheral the CPU happened to touch — go to its PREADY generator and ask one question: does every state that holds PREADY low have a guaranteed exit? List the WAIT/ACCESS states, find the one whose only transition depends on an external event, and check for a bounded fallback. A huge fraction of "bus hang" bugs are an unbounded ready generator — invisible until the rare cycle the source's done goes missing.

A two-case FSM comparison: the top red case shows a WAIT state whose only exit is `done`, looping forever when `done` never arrives and hanging the bus; the bottom green case shows WAIT with a second timeout exit forcing READY within N cycles, plus a footnote on the unselected-slave PREADY-low hazard.
Figure 2 — the deadlock bug versus the bounded fix. Top (bug, red): the generator FSM enters WAIT on PSEL and PENABLE and its only exit is the source's `done` strobe; in a corner case `done` never arrives, so WAIT loops on itself forever, PREADY stays low, and the slave hangs the shared APB bus indefinitely. Bottom (fix, green): WAIT now has two exits — the normal `done` path and a watchdog `cnt == N` timeout path that forces READY (optionally raising PSLVERR) — so the FSM is guaranteed to reach READY and assert PREADY within a bounded number of cycles even if `done` is lost. A footnote shows the related unselected-slave hazard: a non-selected slave that drives PREADY low instead of high pulls the shared PREADY down and stalls another slave's transfer, which is why the rule is !PSEL ⇒ PREADY = 1.

8. Verification perspective

A PREADY generator is verified against one hard property — it always eventually completes, within a known bound — plus the unselected-ready rule and full coverage of the wait-length and FSM space.

  • Bounded-completion assertion (the headline check). Assert that from any access start (PSEL & PENABLE) PREADY asserts within WATCHDOG_N cycles, independent of the source. This is the property that catches the deadlock: drive the testbench to withhold src_done entirely and confirm the watchdog still completes the access. A generator that passes only when done cooperates has not been verified — the corner where done never arrives is the whole point.
  • Unselected-ready check. Assert !PSEL |-> PREADY == 1 for this slave, and in an interconnect-level test, verify that an idle slave never lowers the shared/effective PREADY seen by the selected slave. The high-value scenario is two slaves: one in an access, the other unselected — confirm the unselected one cannot stall the active transfer.
  • Wait-length coverage. Cover the distribution of wait lengths actually exercised: zero wait (always-ready / done same cycle), the typical source latency, the maximum legal latency, and crucially the watchdog-expiry length (the timeout path taken). A coverage model that only ever sees the common latency misses the bounded-exit logic entirely.
  • FSM state and transition coverage, including the done-never-arrives corner. Cover every state (IDLE, WAIT, DONE) and every transition — especially WAIT → DONE via timeout (not just via done). The single most important coverage point is "WAIT exited because the watchdog expired while src_done was absent," because that is the path the deadlock bug skips. Pair it with the read/write gating: confirm the write commits and the read data is valid on exactly the PREADY cycle, including on the timeout path (where the access should error, not commit garbage).

The point: don't just check that PREADY can assert — prove it must, bound it, exercise the timeout path on purpose, and confirm an idle slave never steals another's ready.

9. Interview discussion

"How does a slave generate PREADY, and how do you make sure it never hangs the bus?" is a sharp design question because a weak answer says "tie it high or wait for the peripheral," while a strong one shows you reason in latency shapes, bounded completion, and the unselected rule.

Lead with the four styles and the matching principle: always-ready (tie high — single-cycle slave), fixed-N counter (known constant latency), event-driven done (variable latency — follow a done/ack/mem_rdy), and a small FSM (multi-step responses) — and stress you pick the simplest style that matches the peripheral's real latency, because a wait state is a cost paid by the master and, on a shared bus, by everything behind it. Then deliver the senior depth: a variable-latency generator must be bounded. Any generator that waits on an external event can deadlock the bus if that event never arrives, so you add a watchdog that forces completion within N cycles — and you turn that timeout into a PSLVERR so the master sees a clean failure, not a fake success. Add the unselected-ready rule: !PSEL ⇒ PREADY = 1, because an idle slave that drives a shared PREADY low stalls the slave the master is actually talking to. Close by connecting it back: PREADY's asserting edge is the gate — it's the cycle the write commits and the read data must be valid — and you keep that edge clean and in the sample window per the timing rules. Mentioning that "the whole APB bus is hung" almost always traces to an unbounded ready generator signals you've debugged one.

10. Practice

  1. Match the style. For each peripheral — a control register, a 4-cycle-pipelined CRC engine, an SRAM controller fronting an external chip, and a block that must arbitrate then read-modify-write — name the right PREADY generation style and justify the latency match.
  2. Bound an event-driven ready. Given an event-driven generator that exits WAIT only on src_done, add the watchdog: write the WAIT next-state condition and the counter, and state what PREADY (and PSLVERR) do on timeout.
  3. Apply the unselected rule. Write the one-line PREADY assignment that enforces !PSEL ⇒ PREADY = 1 ahead of the FSM, and explain what breaks on a shared PREADY without it.
  4. Spot the deadlock. Given a three-state FSM, identify which state can trap PREADY low forever and add the minimal bounded exit.
  5. Gate correctly. State the exact cycle the write commits and the read data must be valid relative to the PREADY edge, and explain the failure if the generator asserts PREADY one cycle early.

11. Q&A

12. Key takeaways

  • PREADY generation is a design choice, not a protocol fact — it is the slave's "am I done with this access?" engine. Pick the simplest of the four styles (always-ready, fixed-N counter, event-driven done, small FSM) that matches the peripheral's real latency.
  • A wait state is a cost paid by the master and, on a shared bus, by every access behind it. Generate the fewest waits the peripheral actually needs; tie-high whenever it's honest.
  • Bounded completion is non-negotiable. Any variable-latency generator (event-driven or FSM) can deadlock the bus if its WAIT state has no exit but an external event. Add a watchdog that forces completion within N cycles, every time — and turn the timeout into a PSLVERR.
  • Obey the unselected-ready rule: !PSEL ⇒ PREADY = 1. An idle slave that drives a shared PREADY low stalls the slave the master is actually talking to. Gate PREADY on !PSEL ahead of the FSM.
  • The PREADY edge is the gate. It is the exact cycle the write commits and the read data must be valid; keep it clean and in the sample window per the timing rules. Apply Module 8's combinational-vs-registered conclusion — register only when you need the timing margin, accepting the extra cycle.
  • Verify that it must complete, not that it can. Assert bounded completion with the source withheld, check the unselected-ready rule, and cover the watchdog-expiry path and the done-never-arrives FSM corner on purpose — that is where the bus-hang bug hides.