AMBA APB · Module 8
Wait-State Verification Challenges
Why wait states are the most under-verified area of APB — the default zero-wait responder hides the held-bus paths — and how to close the gap with a wait-count coverage model, the assertions that pin the held-bus contract, and a randomised wait-injecting responder.
You have spent this module learning everything a wait state is: why it exists, how PREADY extends an access, how the bus is held, the glitch that breaks completion, the unbounded-wait hang, and the catalogue of design bugs. This closing chapter is about the uncomfortable truth that ties them together: wait states are the single most under-verified region of APB, and they are under-verified for a structural reason — the easy testbench responder is zero-wait, so it completes every access in its first cycle and never creates a held-bus cycle at all. Every bug the rest of this module described lives on the held-bus path, and the default regression never walks it. The discipline of this chapter is to stop trusting "the tests pass," build a wait-count coverage model that exposes the hole, write the assertions that pin the wait contract, and drive the corners with a responder that deliberately injects 0..N waits.
1. Problem statement
The problem is proving that wait-state behaviour is correct when the cheapest, default way to build a testbench guarantees you never exercise it.
A wait state only happens when a subordinate holds PREADY low for one or more access cycles, forcing the manager to keep PSEL, PENABLE, the address, and (on a write) the data stable while it waits. That held-bus interval is where all the interesting behaviour lives: the bus must stay byte-for-byte stable, PENABLE must stay high, PRDATA/PSLVERR must arrive valid exactly on the completing edge, and the access must terminate in bounded time. None of that is reachable if the responder always returns PREADY = 1 on the first cycle. A zero-wait responder makes every access single-cycle; the held interval has length zero; the held-bus logic in both the design and the testbench executes only in its "no wait" direction.
So the job is not "write some wait-state tests." It is threefold:
- Make the hole visible. Build a coverage model whose bins are wait counts (
0, 1, 2, N, max/back-to-back) crossed with read/write and error/no-error, so a green regression that only fills the0bin is exposed as 80% empty. - Pin the contract with assertions. Encode the wait-state rules — bus held stable,
PENABLEhigh, bounded completion,PREADYneverX, ready-implies-data-valid — as properties that fire the instant the held-bus path misbehaves. - Drive the corners. Replace the zero-wait responder with a randomised one that returns
0..Nwaits per access, and give the scoreboard a latency-tolerant timing model so variable wait counts do not look like errors.
The throughline: wait-state verification is the deliberate act of injecting the very behaviour the default testbench omits, and then covering and asserting against it.
2. Why previous knowledge is insufficient
Across this module you learned what wait states do; none of those chapters told you how to prove a real design handles them, and the gap between the two is exactly where silicon escapes happen.
- Chapter 8.3 — PREADY timing showed that
PREADYcorrectness is a timing property and that a combinationalPREADYcan glitch into the sample window. You now know the bug exists. This chapter asks the next question: which assertion and which simulation level catches it, and what coverage point proves you exercised the path it lives on? Knowing a glitch is possible is not the same as having a glitch guard-band check bound and a first-cycle-completion bin covered. - Chapter 8.4 — transfer-extension mechanics taught that
PREADY = 0extends the access cycle by cycle, holding the whole bus stable. That stability is a contract the design can violate — and a zero-wait responder never tests it, because there is no held cycle to be unstable across. You need an assertion that the bus is byte-stable across the wait and a covergroup that actually produces multi-cycle holds. - Chapter 8.5 — multiple wait cycles covered long and unbounded waits and the timeout. Knowing the hang exists does not sign it off: you still need a watchdog assertion with a bound, a directed unbounded-wait test, and a coverage point for the
maxbin. "We discussed timeouts" is not "the timeout path is covered and asserted." - Chapter 8.6 — PREADY common design bugs catalogued the bugs — held-data drift, asserted-too-early, missing default-ready. This chapter does not re-teach those bugs. It teaches the coverage point and assertion that would have caught each one, and why a passing functional regression let them through anyway. The bug catalogue is the input; the closure plan is the output.
So the model to add is the verification one: a coverage model that makes the hole measurable, an assertion set that encodes the contract, and a stimulus generator that produces the held cycles in the first place. Without all three, "the wait-state tests pass" means almost nothing.
3. Mental model
The model: the default testbench is a flashlight that only points at one cell of a grid, and wait-state verification is the work of lighting up the rest of the grid on purpose.
Picture the full space of wait-state behaviour as a matrix. Down the side are wait-count bins — 0 waits (single-cycle), 1, 2, an arbitrary N, and the extreme max / back-to-back held accesses. Across the top are the orthogonal axes that change what the wait means: read versus write (held read data versus held write data are different paths) and clean versus PSLVERR (an error arriving on the completing edge of a held access exercises a different corner than a clean completion). The default zero-wait responder illuminates exactly one row of that matrix — the 0-wait row — and reports green. Every cell below the top row is dark, and every bug in this module lives in a dark cell.
Three refinements make it precise:
- A green regression can be a nearly empty grid. Functional pass and code coverage measure that statements ran, not that they ran in the held direction. A held-data register can hit 100% line coverage while only ever being written in the no-wait path. Coverage of behaviour (wait-count bins) is a different axis from coverage of code, and only the former exposes the hole.
- Lighting a cell requires injecting the wait, not hoping for it. You cannot cover the
2-wait read-with-error cell by running more random transfers against a zero-wait responder — the responder structurally cannot produce it. You must drive it: a responder that randomly returns0..Nwaits is the only thing that populates the grid. - The completing edge is where the contract is checked. During the wait, the assertions watch for stability (nothing moves). On the single completing edge (
PREADYrising whilePSEL && PENABLE), the assertions check validity (data andPSLVERRare correct, completion is bounded). Wait verification is "hold-stable for N cycles, then valid-on-completion" — and you must reach a range ofNto trust it.
4. Real SoC implementation
In a real APB verification environment the wait-state contract is enforced by a small set of bound SVA properties and exercised by a randomised wait-injecting responder, with a covergroup measuring which wait bins were hit. Here is the core of all three — the properties that pin the contract, the responder that creates the held cycles, and the covergroup that proves you reached them.
// ============================================================
// WAIT-STATE CONTRACT — bound assertions (run in RTL sim)
// Convention: sampled @(posedge pclk), disabled in reset.
// Held-bus interval = PSEL && PENABLE && !PREADY (a wait cycle).
// ============================================================
// 1) BUS HELD STABLE ACROSS A WAIT.
// While we are waiting (access phase, PREADY low), the manager must NOT
// move address/control/wdata. Catches held-data drift (8.6) — the bug a
// zero-wait responder can never expose because it never produces a wait.
property p_bus_stable_in_wait;
@(posedge pclk) disable iff (!presetn)
(psel && penable && !pready) |=>
$stable(paddr) && $stable(pwrite) && $stable(pprot) &&
$stable(pstrb) && (pwrite ? $stable(pwdata) : 1'b1);
endproperty
a_bus_stable_in_wait: assert property (p_bus_stable_in_wait);
// 2) PENABLE STAYS HIGH THROUGH THE WAIT.
// Once in access, PENABLE must remain high until the transfer completes
// (PREADY high). It may only fall on the completing edge.
property p_penable_held;
@(posedge pclk) disable iff (!presetn)
(psel && penable && !pready) |=> penable;
endproperty
a_penable_held: assert property (p_penable_held);
// 3) BOUNDED COMPLETION (watchdog).
// From the first access cycle, PREADY must go high within MAX_WAIT cycles.
// This is the timeout sign-off for the unbounded-wait path (8.5): an
// open-ended hang fails here instead of running the sim forever.
property p_bounded_completion;
@(posedge pclk) disable iff (!presetn)
($rose(penable)) |-> ##[1:MAX_WAIT] pready;
endproperty
a_bounded_completion: assert property (p_bounded_completion);
// 4) PREADY NEVER X WHILE SELECTED+ENABLED.
// Catches an undriven / floating PREADY path (the missing default-ready
// bug, 8.6) — an X here propagates into the manager's completion decision.
property p_pready_known;
@(posedge pclk) disable iff (!presetn)
(psel && penable) |-> !$isunknown(pready);
endproperty
a_pready_known: assert property (p_pready_known);
// 5) READY IMPLIES DATA/RESPONSE VALID ON THE COMPLETING EDGE.
// When PREADY rises to complete the access, PRDATA (on a read) and PSLVERR
// must be known/valid THAT cycle — not asserted a cycle early (8.3/8.6).
property p_ready_implies_valid;
@(posedge pclk) disable iff (!presetn)
(psel && penable && pready) |->
(!$isunknown(pslverr)) && (pwrite || !$isunknown(prdata));
endproperty
a_ready_implies_valid: assert property (p_ready_implies_valid);// ============================================================
// WAIT-INJECTING RESPONDER — drives the held cycles on purpose.
// Replaces the zero-wait default so the wait bins become reachable.
// Per access it picks a random wait count 0..MAX_WAIT (weighted so the
// corners — 0, 1, and max — are not starved), holds PREADY low for that
// many cycles, then completes with valid data/response.
// ============================================================
class apb_wait_responder;
rand int unsigned wait_n; // waits to inject this access
int unsigned MAX_WAIT = 8;
// Weighted so single-cycle (0), one-wait (1) and the max corner all
// appear often — pure uniform under-samples the interesting extremes.
constraint c_wait {
wait_n inside {[0:MAX_WAIT]};
wait_n dist { 0 := 30, 1 := 25, [2:MAX_WAIT-1] := 30, MAX_WAIT := 15 };
}
// Called once the access phase is entered (PSEL && PENABLE).
task drive(virtual apb_if vif);
assert(this.randomize()); // fresh wait_n per access
repeat (wait_n) begin // hold the bus: PREADY low
vif.pready <= 1'b0;
@(posedge vif.pclk);
end
// Completing edge: present valid data + (optionally) PSLVERR.
vif.prdata <= (vif.pwrite) ? '0 : model_read(vif.paddr);
vif.pslverr <= decode_error(vif.paddr, vif.pwrite); // wait+error corner
vif.pready <= 1'b1; // ready-implies-valid holds here
@(posedge vif.pclk);
vif.pready <= 1'b0;
endtask
endclass// ============================================================
// WAIT-COUNT COVERGROUP — proves the grid is lit, not just the top row.
// wait_n is the per-access wait count the monitor reconstructed.
// ============================================================
covergroup cg_wait_state @(posedge sample_ev);
// The wait-count bins — the rows of the coverage matrix.
cp_waits : coverpoint wait_n {
bins zero = {0}; // single-cycle (the only bin the
// default responder ever fills)
bins one = {1};
bins two = {2};
bins many = {[3:MAX_WAIT-1]}; // the "N" bin
bins maxwait = {MAX_WAIT}; // back-to-back / max corner
}
cp_dir : coverpoint pwrite { bins rd = {0}; bins wr = {1}; }
cp_err : coverpoint pslverr { bins ok = {0}; bins err = {1}; }
// The crosses — wait on read vs write, and wait combined with error.
x_wait_dir : cross cp_waits, cp_dir; // held read vs held write
x_wait_err : cross cp_waits, cp_err; // PSLVERR during/after a wait
endgroupThree facts make this real rather than ceremonial. First, the responder is the keystone — without it, every property above is technically true but vacuously so on the held-bus path, and every covergroup bin except zero reads 0%. The randomised wait count is what populates the matrix. Second, the dist weighting is deliberate: a uniform 0..MAX_WAIT under-samples the 0-wait and max corners (each is one value out of nine), so you weight the extremes up — single-cycle and back-to-back are exactly where the timing bugs cluster. Third, the crosses are the point, not the singletons: wait × error and wait × write are where 8.6's bugs live (held-data drift on a multi-cycle write, PSLVERR arriving on a delayed completing edge), and an empty cross is a real, named hole even when the marginal bins all read 100%.
5. Engineering tradeoffs
The wait-state coverage model is itself the deliverable here — each coverage point exists to catch a specific class of bug, and choosing the bins is choosing what you can prove. This table is the model.
| Coverage point | Bins | Why it matters / what bug it catches |
|---|---|---|
| Wait count | 0, 1, 2, N (3..max−1), max/back-to-back | The core axis. 0 is all the default responder fills; 1 exercises the shortest held interval; 2/N exercise sustained holds (held-data drift, 8.6); max is the watchdog/timeout corner (8.5). An empty non-zero bin means the held-bus path never ran. |
| Direction × wait | read × {0,1,N}, write × {0,1,N} | Held read data and held write data are different paths. A held write must keep PWDATA/PSTRB stable (8.4); a held read must keep PRDATA driven only on the completing edge (8.3). Covering only one direction hides half the bugs. |
| Error × wait | clean × {0,N}, PSLVERR × {0,N}, PSLVERR on completing edge | PSLVERR arriving after a wait, on the delayed completing edge, is a distinct corner from a zero-wait error. Catches "error asserted a cycle early" and "error dropped during the hold." |
| Completion timing | combinational-ready (complete cycle 1), registered-ready (complete cycle 2+) | Distinguishes the two PREADY implementations (8.3). The first-cycle completion path is exactly where the combinational glitch lives; a suite that never hits it has a hole at the glitch. |
| Unbounded / timeout | wait ≥ MAX_WAIT (watchdog fires), directed never-ready test | Signs off the hang path (8.5). The watchdog assertion turns an infinite wait into a bounded failure; the bin proves you reached the bound at least once. |
| Back-to-back held accesses | held access immediately followed by another held access | Exercises the responder/scoreboard re-arm between waits — a common place for an off-by-one in the wait counter or a stale PREADY. |
The throughline: every row is a bug class from earlier in the module turned into a measurable target. The trap is treating wait-state coverage as one bin ("did a wait happen?"); the discipline is the cross — wait count against direction against error against completion timing — because the bugs live in the cells, not the margins.
6. Common RTL mistakes
7. Debugging scenario
A held-data-drift bug (the kind catalogued in 8.6) escaped to first silicon and surfaced only when a real slow peripheral exercised the held-bus path the regression never walked. Here is the full trace from symptom to the coverage hole that let it through.
- Observed symptom: the chip passed its entire RTL regression green, but in the lab a specific read from a slow peripheral (an external flash controller that inserts two wait cycles) intermittently returned corrupted data — the low byte was a stale value from the previous access. Faster, zero-wait peripherals on the same bus were fine.
- Waveform clue (Figure 2): on a lab capture, the access shows
PSEL/PENABLEhigh withPREADYlow for two cycles (a real two-wait hold), and during the hold the subordinate's internal read-data register is re-driven mid-wait, so the value the manager finally latches on the completing edge differs from what the peripheral actually fetched. On the bench's zero-wait responder,PREADYwas always high in cycle 1 — there was no hold for the data to drift across, so the waveform looked perfect. - Root cause: the subordinate registered its read data on every
PCLKedge while selected, not just on the completing edge. With zero waits the first edge is the completing edge, so the bug is invisible. With two waits, two extra edges occur during the hold, and a combinational decode update on one of them overwrites the held data before the manager latches it — held-data drift, exactly the 8.6 bug. It is in the RTL; the regression simply never produced a held cycle for it to manifest in. - Verification assertion (what would have caught it): the bus-held-stable property, extended to the subordinate's read-data:
assert property (@(posedge pclk) disable iff(!presetn) (psel && penable && !pready) |=> $stable(prdata_internal));— read data presented to the manager must not change during a wait. Combined with theready-implies-validproperty on the completing edge, this fires the cycle the data drifts. But the assertion only fires if a wait actually occurs — which is why the coverage hole, not the missing assertion, is the real root cause of the escape. - Correct RTL: register the read data only on the completing edge (or hold it explicitly across the wait):
if (sel && penable && data_ready) prdata_q <= read_mux;— capture once, when the access is about to complete, and leaveprdata_quntouched through the hold so it cannot drift. The held value is then stable for the manager to latch. - Debug habit: when a design passes RTL regression but a slow peripheral misbehaves in the lab, do not re-read the data path — check the coverage report for the wait-count bins first. If the
1/2/Nbins read0%, the held-bus path was never simulated and any held-bus bug escaped by construction. Then bring up the wait-injecting responder, re-run, and watch the stability assertions fire. The escape is almost always a coverage hole, not a missing check.
8. Verification perspective
Closing the wait-state hole is a deliberate, multi-layer effort — stimulus, scoreboard, coverage, and the level question — and it is the discipline that catches everything Module 8 described. The plan has five parts.
- Stand up a wait-injecting VIP, and treat its knobs as first-class. Replace the zero-wait responder with one that returns a randomised
0..MAX_WAITwaits per access. The randomisation knobs are part of the verification plan, not implementation detail: the distribution (weighted to hit0,1, andmaxoften, since uniform starves the corners), the per-slaveMAX_WAIT(a slow flash model needs a higher ceiling than a register block), and error injection during/after the wait (so thewait × PSLVERRcross is reachable). A constrained-random responder with these knobs is what populates the coverage matrix; everything else measures what it produced. - Make the scoreboard latency-tolerant. With variable wait counts, completion lands on a variable cycle, so the scoreboard cannot key off a fixed latency or it flags every multi-wait transfer as a spurious mismatch. Model the transaction as issued on the address phase, completed on the edge where
PREADYis high, and compare data/response at completion regardless of how many wait cycles elapsed. The wait count itself becomes a field on the transaction the monitor reconstructs (PREADY-low cycles between issue and completion) — which is also what feeds the covergroup. - Drive coverage closure on the wait bins, not just functional pass. Sign-off is the coverage matrix filled, not "regression green." Track the wait-count bins and their crosses (direction, error, completion timing) as a closure target; an empty
wait × errorcross or an unhitmaxbin is an open item even if every directed test passes. Theback-to-backbin specifically guards the responder/scoreboard re-arm and the design's wait-counter reset between accesses. - Use the right simulation level per bug class. The functional held-bus bugs — held-data drift, bus instability across the wait, asserted-too-early, missing default-ready — are caught in RTL by the stability,
PREADY-known, and ready-implies-valid assertions, once the responder produces the holds. The timing glitch on the completing edge (8.3) is invisible in zero-delay RTL and requires gate-level / SDF-annotated simulation with a glitch guard-band check around the sampling edge. The verification plan must name the level for each: RTL for behaviour, gate-level for the glitch. - Explicitly sign off the unbounded-wait / timeout path. A subordinate that never asserts
PREADYhangs the bus (8.5). Bind a watchdog property —$rose(penable) |-> ##[1:MAX_WAIT] pready— so an open-ended wait fails as a bounded assertion instead of a timed-out sim, run a directed never-ready test to prove the watchdog fires, and cover thewait ≥ MAX_WAITbin so you can show the bound was reached at least once. Sign-off on the timeout is "the watchdog fired in a directed test and the bound bin is hit," not "we discussed timeouts."
The point: wait-state verification is not a test case, it is a closure discipline — inject the waits, tolerate the latency, fill the matrix, and pick the right level — and it is what makes the rest of this module's bugs catchable before tape-out rather than in the lab.
9. Interview discussion
"Your wait-state tests all pass — does that mean wait states are verified?" is a deceptively sharp DV question, because the honest answer is "almost certainly not, and here is why," and that answer separates engineers who run regressions from engineers who close coverage.
Open with the structural insight: the default testbench responder is zero-wait, so a passing functional regression typically fills only the 0-wait bin and never produces a single held-bus cycle — every interesting wait-state bug lives on the held path that was never walked. Then make it concrete with the coverage axis: you need a wait-count covergroup (0/1/2/N/max) crossed with read/write, with/without PSLVERR, and the two completion-timing styles, and you populate it with a randomised wait-injecting responder (weighted so the 0 and max corners are not starved). Deliver the depth points: code coverage is not behaviour coverage — 100% line coverage can coexist with an empty 1-wait bin; the scoreboard must be latency-tolerant — key off the PREADY-high completing edge, not a cycle count, or every multi-wait transfer is a false mismatch; and the level matters — the functional held-bus bugs (drift, stability, early-ready) are RTL assertions, but the completing-edge glitch needs gate-level/SDF. Close with the sign-off line: "I sign off wait states on the coverage matrix being filled and the watchdog firing in a directed never-ready test — not on the regression being green." That sentence signals you have actually closed wait-state coverage on a real chip.
10. Practice
- Build the matrix. Write out the wait-state coverage model as a covergroup: the wait-count coverpoint (
0/1/2/N/max), the direction and error coverpoints, and the two crosses (wait × dir,wait × err). State which single cell a zero-wait responder fills. - Write the contract. From memory, write the four RTL-level wait-state assertions: bus-held-stable across a wait,
PENABLEheld high through the wait,PREADYneverXwhile selected+enabled, and ready-implies-data/response-valid on the completing edge. - Inject the waits. Sketch the wait-injecting responder: a randomised
0..Nwait count with a distribution weighted toward the0andmaxcorners, holdingPREADYlow for that many cycles then completing with valid data/PSLVERR. Explain why uniform randomisation is the wrong choice. - Tolerate the latency. Explain why a fixed-latency scoreboard produces spurious mismatches under a wait-injecting responder, and rewrite its completion rule to key off the
PREADY-high edge. - Pick the level. For each of held-data drift, bus instability across the wait, the completing-edge glitch, and the unbounded-wait hang, state whether RTL or gate-level/SDF simulation catches it and the assertion or coverage point that signs it off.
11. Q&A
12. Key takeaways
- Wait states are the single most under-verified region of APB, for a structural reason: the default testbench responder is zero-wait, so it never produces a held-bus cycle, and every wait-state bug lives on that held path. "Tests pass" says almost nothing about wait behaviour until you inject waits.
- Code coverage is not behaviour coverage. 100% line/branch coverage can coexist with empty
1/2/N-wait bins. You need a wait-count covergroup (0/1/2/N/max) crossed with read/write, with/withoutPSLVERR, and the two completion-timing styles — and the matrix being filled is the real sign-off, not a green regression. - A randomised wait-injecting responder is the keystone. It returns
0..Nwaits per access (weighted so the0andmaxcorners are not starved), holdsPREADYlow, then completes with valid data/response — and it is the only thing that populates the coverage matrix the default responder leaves empty. - The contract is a small set of assertions: bus-held-stable across the wait,
PENABLEheld high,PREADYneverXwhile selected+enabled, ready-implies-data/response-valid on the completing edge, and a bounded-completion watchdog for the timeout path. - Pick the level per bug class: the functional held-bus bugs (drift, instability, early-ready, missing default-ready) are caught in RTL by the assertions once the holds exist; the completing-edge glitch (8.3) needs gate-level/SDF; the unbounded-wait hang (8.5) is signed off by the watchdog firing in a directed never-ready test plus the
maxbin being hit. - The scoreboard must tolerate variable latency — key completion off the
PREADY-high edge, not a fixed cycle count — or every multi-wait transfer reads as a spurious mismatch. Wait-state verification is a closure discipline, and it is what catches everything the rest of Module 8 described before tape-out instead of in the lab.