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
PREADYlow 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
PREADYthe answer is critical: an unselected slave must drivePREADYhigh (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-timingderived the protocol view:PREADYis sampled on the rising clock edge in the ACCESS phase (PSEL & PENABLE), the combinational-vs-registered choice, the sample window, and the glitch hazard ifPREADYmoves near the edge. That chapter tells you whenPREADYis 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-cyclesshowed what a multi-wait transfer looks like on the bus —PREADYlow for several cycles, then high. It did not show how the slave decides the wait length. That decision — count to N, or follow adone, 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
PREADYandPENABLEare both high; the read path must present valid data on that same cycle. Those chapters assumedPREADYexisted and asked "what fires when it asserts?" This chapter builds the thing that asserts it — and forward,pslverr-generationwill 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
PREADYhigh; 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 — holdPREADYlow and assert it when a slow source raises adone/ready/ackstrobe (a memory'smem_rdy, a synchronised CDC acknowledge); for variable latency. Small FSM —IDLE → 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,
PREADYmust 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 ondone, a lost or never-arrivingdonedeadlocks the bus. The fix is a second exit: a timeout watchdog that forcesREADY(often withPSLVERR) 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
PSELis low this slave is idle, but itsPREADYoutput still exists. The rule:!PSEL ⇒ PREADY = 1. On a per-slavePREADY(muxed by the interconnect) this just keeps your default clean; on any topology wherePREADYis 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.
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.
// 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;
endmoduleThree 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 style | Latency | RTL complexity | Deadlock risk | When to use it |
|---|---|---|---|---|
| Always-ready (tie high) | 0 wait states | Trivial — assign pready = 1'b1 (gated by !psel) | None — bounded by construction | Plain register read/write; any peripheral that truly finishes the same cycle |
| Fixed-N wait counter | N (known, constant) | Small — load + count down | None — terminal count always fires | Peripheral with a known constant pipeline latency (e.g. a 3-cycle ROM) |
| Event-driven done | Variable, source-dependent | Moderate — follow a strobe + watchdog | High if unbounded — must add a timeout | Slow/async source: external memory, CDC ack, slow I/O; latency not known at design time |
| Small FSM (IDLE→ACCESS→READY) | Variable, multi-step | Larger — explicit states + watchdog | High if any state lacks an exit | Multi-step response: arbitration, a sequence of internal ops, or read-modify-write |
| Registered vs combinational PREADY | +1 cycle if registered | Registered is cleaner-edged | None directly | Register 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,
PSELandPENABLEare both high and stay high indefinitely whilePREADYsits low and never rises. The slave's internalstateis stuck inWAIT, its watchdog counter (if any) is not incrementing or has no terminal action, and the source'ssrc_donestrobe 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 → DONEguarded only bysrc_done. The slow source can, in a rare path (a CDC glitch, an aborted memory op, a clock-domain reset), fail to ever raisedone. With no other exit, the FSM is trapped in WAIT,PREADYstays 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 drivesPREADYhigh (and raisestimed_out→PSLVERR, 11.6, so the master learns the access failed rather than seeing a fake success):Azvya Education Pvt. Ltd.VLSI MentorSnippet// 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,
PREADYmust assert withinWATCHDOG_Ncycles, no matter what the source does:Azvya Education Pvt. Ltd.VLSI MentorSnippet// 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
PREADYlow 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'sdonegoes missing.
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)PREADYasserts withinWATCHDOG_Ncycles, independent of the source. This is the property that catches the deadlock: drive the testbench to withholdsrc_doneentirely and confirm the watchdog still completes the access. A generator that passes only whendonecooperates has not been verified — the corner wheredonenever arrives is the whole point. - Unselected-ready check. Assert
!PSEL |-> PREADY == 1for this slave, and in an interconnect-level test, verify that an idle slave never lowers the shared/effectivePREADYseen 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 /
donesame 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 — especiallyWAIT → DONE via timeout(not just viadone). The single most important coverage point is "WAIT exited because the watchdog expired whilesrc_donewas 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 thePREADYcycle, 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
- 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.
- 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 whatPREADY(andPSLVERR) do on timeout. - Apply the unselected rule. Write the one-line
PREADYassignment that enforces!PSEL ⇒ PREADY = 1ahead of the FSM, and explain what breaks on a sharedPREADYwithout it. - Spot the deadlock. Given a three-state FSM, identify which state can trap
PREADYlow forever and add the minimal bounded exit. - Gate correctly. State the exact cycle the write commits and the read data must be valid relative to the
PREADYedge, and explain the failure if the generator assertsPREADYone 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 sharedPREADYlow stalls the slave the master is actually talking to. GatePREADYon!PSELahead 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.