Almost every APB subsystem that returns garbage data or hangs on a legal address has a PSEL fanout bug — and they all trace back to one broken invariant: the decoder must produce exactly one PSEL per access. You have learned that PSEL is the per-subordinate select a decoder derives from PADDR, and that the addressed subordinate's PREADY and PRDATA come back through a shared return path. This chapter inverts that knowledge into a debugging instrument. It is a catalog of the classic PSEL distribution bugs — the recurring mistakes in the decoder and in slave PSEL handling that pass a quick directed sim and ship — each as a named bug with a symptom on the bus, the decoder/RTL root cause, the wrong RTL that produces it, and the correct one-hot RTL that fixes it. The single idea to carry: when an APB access "reads contention garbage," "hangs on an address that should work," or "the wrong slave answers," you should not start from scratch — you should pattern-match against the small set of ways a one-hot PSEL vector gets broken, because the bug is almost certainly one of them.
1. Problem statement
The problem is that PSEL is a one-hot select vector that the decoder must keep exactly one-hot for every access, and a handful of recurring decoder and slave mistakes break that one-hot invariant in ways that look like different failures but trace back to the same vector.
A correct subsystem holds one and only one PSEL high during an active access: zero selects when the bus is idle, exactly one ($onehot) when an access is in flight (AMBA APB IHI 0024C §2.1, PSEL and address decode). That single bit carries two promises to the shared return path: "this subordinate, and only this one, owns this access" and, implicitly, "there is a subordinate that owns it at all." Break the first — assert two PSELs at once — and two subordinates drive the shared PREADY/PRDATA simultaneously: contention, and the manager latches corrupted or X read data. Break the second — assert no PSEL for a legal address — and nothing drives the return path: the access hangs, PREADY never rises. Every classic PSEL bug is a violation of the one-hot invariant or of who is allowed to honour it:
- Multi-select bugs raise more than one
PSELat once: overlapping decode ranges, comparators that are not mutually exclusive, or a non-onehot encoding. Two slaves answer; the shared return path is in contention. - No-select bugs raise zero
PSELfor a legal address: a decode hole with noelse/defaultbranch covering the gap, so a legal access selects nothing and hangs with no responder. - Phase and glitch bugs raise the right
PSELat the wrong time:PSELasserted in the setup-vs-access window incorrectly, or a combinational decode glitching whilePADDRripples, briefly selecting a neighbour. - Slave-side bugs honour a
PSELthat is not theirs: a subordinate that ignores its ownpsel_sNand responds to a neighbour's access, or one that mis-orders thePSEL/PENABLEdeassert at the end of an access.
The engineering problem of this chapter is therefore not "what does PSEL mean" — you know that from the PSEL select-signal chapter — but "what are the specific ways a real decoder and real slaves get the PSEL fanout wrong, what does each look like on the bus, and what is the correct one-hot fix for each."
2. Why previous knowledge is insufficient
This module has built the model of PSEL and the decode path from the address outward, and each prior chapter is now a reference this catalog points back to — but none of them is the catalog itself.
- The PSEL select signal taught what
PSELis — the per-subordinate qualifier the manager phase relies on. That chapter is the definition; herePSELis the thing that breaks, and knowing its definition does not give you the taxonomy of every way the decoder mis-drives it. - The address decoder and address allocation strategy taught how
PADDRmaps to a select and how to lay out non-overlapping, gap-free regions. That is the correct construction — but it assumed the construction is right. This chapter is about what happens when ranges do overlap, when a gap is left, and when the map and the RTL disagree. - The read-data mux taught how the addressed subordinate's
PRDATAis selected back to the manager. That explains the mechanism a multi-select bug corrupts — two drivers into the same mux — but it assumed a one-hot select feeding it. This chapter is about the select itself going non-onehot.
The gap is this: prior chapters taught the correct behaviour and the correct construction. They did not assemble the failure taxonomy — the named set of PSEL fanout bugs with wrong-versus-right RTL — that lets you debug a real access by recognition instead of derivation. Building that taxonomy is this chapter; the protocol rules those bugs violate live in the legal/illegal transition rules and are checked continuously by APB assertions.
3. Mental model
The model: PSEL is a one-hot vector across an access, and every classic bug breaks the one-hot count or the ownership it implies — so count the selects first, then pick the bug.
The bins are the ways a one-hot PSEL contract fails:
- Too many selects (count > 1). Two or more
PSELs high at once. On the bus you see contention — two subordinates driving the sharedPREADY/PRDATA, so the read data is corrupted or X and the completion is ambiguous. The cause is always the decode ranges are not mutually exclusive: overlapping address windows, comparators that both fire, or a glitch that briefly co-asserts a neighbour duringPADDRripple. - Too few selects (count = 0 for a legal address). No
PSELhigh when one should be. On the bus you see a hang —PSEL/PENABLEwould-be high but nothing answers,PREADYnever rises. The cause is always a hole in the decode: a legal address that no branch covers, noelse/defaultto catch it, so the access selects nothing. - Right select, wrong time or wrong owner. The count is one-hot but the timing or honouring is wrong:
PSELasserted in the wrong phase, a decode glitch selecting the wrong slave mid-ripple, or a subordinate that ignores its own select and answers a neighbour's access. The bus symptom is the wrong slave's data, or a completion in the wrong phase.
The discipline this model buys you: count before you debug. "Read data is X / contention" instantly points at too-many-selects; "it hangs on one specific address range" points at too-few-selects; "the wrong register answers" points at the timing/ownership bin. Three refinements sharpen it:
- A multi-select is a safety failure; a no-select is a liveness failure. Multi-select commits bad data (something wrong happens); no-select never completes (something good never happens). They need different assertions — a one-hot property for the first, a bounded-completion-or-error property for the second.
- A decode glitch is a multi-select that exists only in time. While
PADDRripples through a combinational decoder, the select can momentarily land on a neighbour before settling. It is one-hot at every settled edge yet non-onehot between edges — invisible in zero-delay RTL, real in gate-level. Bin it with multi-select, but suspect it only when the steady-state decode is provably clean. - A slave that answers a neighbour's access is a one-hot decode betrayed downstream. The vector can be perfectly one-hot and the bug still happens — because a subordinate qualified its response on
PENABLEalone, or on a stale latched select, instead of on its own livepsel_sN. Bin it as ownership, and check the slave, not the decoder.
4. Real SoC implementation
In real RTL the top PSEL bugs are a few lines of plausible-looking decode, and the fixes are equally short — which is exactly why they ship. Here are the wrong-versus-right pairs for the three highest-frequency bugs — overlapping ranges (multi-select), decode hole (no-select), and a slave that honours the wrong select — using the real APB signal names.
// ============================================================
// BUG 1 — MULTIPLE-SELECT: overlapping decode ranges, two PSEL high
// ============================================================
// WRONG: the two range checks are NOT mutually exclusive. paddr in the
// overlap (0x2800..0x2FFF) makes both psel_s0 and psel_s1 assert -> two
// subordinates drive the shared PREADY/PRDATA -> contention / X reads.
assign psel_s0 = sel && (paddr >= 32'h0000_2000) && (paddr <= 32'h0000_2FFF);
assign psel_s1 = sel && (paddr >= 32'h0000_2800) && (paddr <= 32'h0000_3FFF);
// ^^^^ ranges overlap at 0x2800..0x2FFF
// CORRECT: one-hot decode from a single, mutually-exclusive case on the
// region field of paddr. Exactly one psel_sN can be high; an unmapped
// address selects NOTHING here and is steered to a default responder (below).
always_comb begin
psel_s0 = 1'b0; psel_s1 = 1'b0; psel_s2 = 1'b0;
if (sel) begin
unique case (paddr[15:12]) // region field -> one branch only
4'h2: psel_s0 = 1'b1; // 0x2000..0x2FFF -> S0, exclusively
4'h3: psel_s1 = 1'b1; // 0x3000..0x3FFF -> S1
4'h4: psel_s2 = 1'b1; // 0x4000..0x4FFF -> S2
default: /* no slave: handled by default responder, see BUG 2 */ ;
endcase
end
end
// 'unique case' makes overlap a tool error, not a silent runtime contention.
// ============================================================
// BUG 2 — NO-SELECT (decode hole): legal address selects nothing -> hang
// ============================================================
// WRONG: only the mapped ranges are decoded; a legal-but-unmapped paddr
// (e.g. 0x1800) drives NO psel_sN, nothing answers, pready never rises.
assign psel_s0 = sel && (paddr[15:12] == 4'h2);
assign psel_s1 = sel && (paddr[15:12] == 4'h3);
// ...gap at 4'h0,4'h1,4'h5.. -> no responder -> bus HANGS forever
// CORRECT: cover the WHOLE legal space. Unmapped addresses select a
// default error responder that completes the access with pslverr, so a
// hole returns an ERROR instead of hanging the bus.
always_comb begin
psel_s0 = 1'b0; psel_s1 = 1'b0; psel_s2 = 1'b0; psel_err = 1'b0;
if (sel) begin
unique case (paddr[15:12])
4'h2: psel_s0 = 1'b1;
4'h3: psel_s1 = 1'b1;
4'h4: psel_s2 = 1'b1;
default: psel_err = 1'b1; // unmapped -> default responder, NOT a hole
endcase
end
end
// psel_err's subordinate drives pready=1, pslverr=1, prdata='0 -> clean error,
// no hang. (Illegal-address handling is detailed in /protocols/apb/illegal-address-access.)
// ============================================================
// BUG 3 — SLAVE HONOURS THE WRONG SELECT: answers a neighbour's access
// ============================================================
// WRONG: this subordinate qualifies its response on PENABLE alone (or a
// stale latched select), so it answers ANY access in its access phase --
// including a neighbour's -> the wrong slave's data on the shared bus.
always_comb begin
pready_s2 = penable; // <- ignores its own psel_s2!
prdata_s2 = reg_file[paddr[5:2]]; // drives the bus even when not selected
end
// CORRECT: a subordinate answers ONLY inside its own selected access, and
// must drive the return path quiet (0 / high-Z per integration) when !psel_s2.
always_comb begin
if (psel_s2 && penable) begin
pready_s2 = data_valid_s2; // ready only in OUR access phase
prdata_s2 = reg_file[paddr[5:2]];
pslverr_s2 = 1'b0;
end else begin
pready_s2 = 1'b0; // not selected -> stay out of the
prdata_s2 = 32'h0; // shared return path entirely
pslverr_s2 = 1'b0;
end
end
// Gate every response term on the slave's OWN psel_sN && penable, never on
// penable alone -- so one-hot ownership is honoured downstream too.Three facts drive these fixes. First, a multi-select fix is structural one-hot construction: derive every PSEL from a single, mutually-exclusive unique case on the region field, so overlap becomes a tool error rather than a silent runtime contention. The overlap bug is not a typo — it is two independent range comparators that the author never proved disjoint. Second, a no-select fix is total coverage: the decode must cover the entire legal address space, routing any unmapped-but-legal address to a default error responder that completes the access with pslverr instead of leaving a hole that hangs the bus (the error path is covered in illegal-address access). Third, a slave-side fix is ownership: every response term — pready_sN, prdata_sN, pslverr_sN — must be gated on the slave's own psel_sN && penable, never on penable alone, so a perfectly one-hot decode is also honoured by the subordinate that receives it.
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 decoder or slave, and the fix. Memorise the shape of each row — count-binned, with a one-hot construction fix — and you can debug most APB select failures by recognition.
| Bug name | Symptom on the bus | Root cause | Fix |
|---|---|---|---|
| Multiple-slave-selected | Read data is X / corrupted; two subordinates contend on shared PREADY/PRDATA; ambiguous completion | Overlapping decode ranges or non-mutually-exclusive comparators raise two PSELs at once | Build the one-hot from a single unique case on the region field; make overlap a tool error, not runtime contention |
| No-slave-selected (decode hole) | Access hangs on one specific legal address range; PREADY never rises, no responder | A legal-but-unmapped address falls in a gap with no else/default branch | Cover the whole legal space; route unmapped addresses to a default error responder that completes with pslverr |
| PSEL in the wrong phase | Completes in a setup-only phase, or a PSEL pulse with no valid PENABLE access | PSEL asserted outside the proper select-then-enable sequence; not aligned to the access phase | Assert PSEL in setup, hold through access; gate slave response on PSEL && PENABLE, never PSEL alone mid-sequence |
| Decode glitch (PADDR ripple) | Intermittent wrong-slave selection / brief contention; passes RTL, fails gate-level | Combinational decode off a rippling PADDR momentarily co-asserts a neighbour before settling | Register PSEL (or qualify it with a stable PENABLE); decode off a settled address, never raw rippling PADDR |
| Slave ignores its PSEL | The wrong slave's data appears; a neighbour answers an access that was not its own | Subordinate qualifies response on PENABLE alone or a stale latched select, not its live psel_sN | Gate every response term on the slave's own psel_sN && PENABLE; drive the return path quiet when !psel_sN |
| PSEL/PENABLE deassert ordering | Spurious extra select pulse or a slave that re-triggers at end of access | At access end PENABLE and PSEL deassert in the wrong order, leaving a one-cycle PSEL-only window misread as a new setup | Deassert cleanly to idle: drop PENABLE and PSEL together (or PSEL last) so no stray setup-looking pulse remains |
The throughline: every row sorts into too-many-selects, too-few-selects, or wrong-time/wrong-owner, and every fix is either "construct the one-hot correctly," "cover the whole space," or "honour the select downstream." The decode-glitch and deassert-ordering rows add the level dimension — they only surface with real delays or at gate-level, which is why a clean RTL run is not proof. The multi-select and slave-ignores rows add the non-local dimension — the symptom (corruption, a wrong responder) can appear on a different slave than the one the manager addressed, which is why you suspect the whole fanout, not just the addressed subordinate.
6. Common RTL mistakes
7. Debugging scenario
Pick the multiple-slave-selected bug, because it is the most corrosive of the catalog: it passes every directed test that happens to address the non-overlapping part of each range, ships, and then surfaces as intermittent read corruption that no one can pin to a single slave.
- Observed symptom: firmware reading a peripheral's data register intermittently gets a value that is neither slave A's nor slave B's register — it looks like a bitwise mix of two registers, or an X that the bus pulls to a random pattern. It happens only for addresses near a region boundary; reads in the middle of each region are always clean. The bus shows a completion with no protocol error and no hang — just wrong data.
- Waveform clue: at the offending
PADDR(Figure 2, middle panel), bothpsel_s0andpsel_s1are high in the same access. Two subordinates assertPREADYand both drivePRDATAonto the shared return path; the capturedPRDATAis the bitwise contention of S0's and S1's register values (or X if the integration resolves contention to X). The$onehot0({psel_s0,psel_s1,psel_s2})check, if present, fires on exactly this access. - Root cause: the two region comparators were written as independent
>=/<=range checks and were never proven disjoint. S0 was specced as0x2000–0x2FFFand S1 as0x2800–0x3FFF; the windows overlap at0x2800–0x2FFF. Any address in that overlap asserts bothPSELs, so both subordinates own the access and both drive the shared return — contention. Directed tests that read0x2000and0x3800(the clean halves) never expose it; only the boundary addresses do. - Correct RTL: derive the whole one-hot vector from a single mutually-exclusive
unique caseon the region field, so the overlap cannot exist and the tool flags any author who reintroduces it:Azvya Education Pvt. Ltd.VLSI MentorSnippetalways_comb begin psel_s0 = 1'b0; psel_s1 = 1'b0; psel_s2 = 1'b0; if (sel) unique case (paddr[15:12]) // one branch only, ever 4'h2: psel_s0 = 1'b1; 4'h3: psel_s1 = 1'b1; 4'h4: psel_s2 = 1'b1; default: ; // -> default responder, never two endcase end - Verification assertion: prove the select vector is one-hot (or all-zero) on every cycle — the single property that catches the entire multi-select family:
Azvya Education Pvt. Ltd.VLSI MentorSnippet
// one-hot PSEL: never two subordinates selected at once assert property (@(posedge pclk) disable iff (!presetn) $onehot0({psel_s0, psel_s1, psel_s2}) ); - Debug habit: when reads are wrong only near region boundaries and the bus protocol looks clean, do not chase the data path or a single slave first — chase the select vector. Capture the
PSELbus at the offendingPADDRand check$onehot0. A read that is a mix of two registers is the fingerprint of twoPSELs high at once, and the fix is always in the decoder ranges, not in either slave. The address allocation strategy is where you confirm the intended ranges are disjoint and gap-free.
8. Verification perspective
Because the catalog splits cleanly into safety (too-many-selects: contention) and liveness (too-few-selects: hang) failures, plus a timing/ownership class, the verification plan needs one assertion class per family plus an awareness of which level each bug surfaces at — a one-hot check in RTL will never see a glitch, and a value check will never see a hang.
- One-hot catches the multi-select family. A single safety property — the select vector is never more than one-hot — catches overlapping ranges, non-exclusive comparators, and the settled-edge case of the glitch in one stroke:
Azvya Education Pvt. Ltd.VLSI MentorSnippet
// never two (or more) subordinates selected simultaneously a_psel_onehot: assert property (@(posedge pclk) disable iff (!presetn) $onehot0({psel_s0, psel_s1, psel_s2})); - No-decode-hole + bounded-completion catches the no-select family. Pair a property that some responder is selected for any legal in-flight access with a bounded-completion watchdog, so a decode hole becomes a located assertion failure instead of a silent hang:
Azvya Education Pvt. Ltd.VLSI MentorSnippet
// a legal access must select a responder (one slave OR the default error one) a_no_decode_hole: assert property (@(posedge pclk) disable iff (!presetn) (sel && legal_addr) |-> $onehot({psel_s0,psel_s1,psel_s2,psel_err})); // and it must complete within N cycles, never hang a_bounded_complete: assert property (@(posedge pclk) disable iff (!presetn) (psel_any && penable) |-> ##[1:N] pready); - Ownership and timing need slave-level and gate-level checks. Assert that an unselected subordinate stays out of the return path (
!psel_s2 |-> (prdata_s2 == '0) && !pready_s2) to catch the slave-ignores-its-PSELbug, which directed single-slave tests cannot see. And know the level split: the one-hot, no-hole, bounded-completion, and ownership checks all run in RTL, but the decode-glitch bug (and a marginal deassert-ordering race) only appear in gate-level or delay-annotated simulation asPADDRripples — a clean RTL run is necessary but not sufficient. Carrying every one of these into a coherent decode-and-select assertion plan is the job of the APB assertions and APB monitors work, against the legal/illegal transition rules.
The point: each bug family has a named assertion that catches its whole class — $onehot0 for contention, no-hole + bounded-completion for hangs, an unselected-quiet check for ownership — and each has a level at which it actually manifests, so a PSEL verification plan is "one one-hot property, one no-hole property, one bounded-completion, one unselected-quiet check, and a gate-level pass for the glitch," not a pile of directed reads.
9. Interview discussion
"Walk me through the PSEL decode bugs you have seen in a multi-slave APB subsystem" is a senior screening question because a weak answer names one ("two slaves can get selected") and stops, while a strong answer reveals an organised mental catalog keyed on the one-hot invariant — exactly the recognition speed that separates someone who has debugged an APB fabric from someone who has only read the spec.
Frame it as one invariant, three failure modes: the decoder must keep PSEL exactly one-hot per access, and every bug breaks the count (too many, too few) or the ownership (right count, wrong honourer). Then enumerate crisply: in the too-many family, multiple-slave-selected (overlapping ranges or non-exclusive comparators → two subordinates contend on the shared PREADY/PRDATA → corrupted/X reads — the boundary-address bug that survives directed tests) and the decode glitch (rippling PADDR into a combinational decode briefly co-asserts a neighbour, invisible in RTL, caught at gate-level); in the too-few family, no-slave-selected (a legal address in a decode gap with no default → nothing answers → hang, the non-local deadlock); in the ownership family, the slave that ignores its own PSEL (gates response on PENABLE alone and answers a neighbour) and the PSEL/PENABLE deassert-ordering race at end of access. Land the depth points: the fix for too-many is structural one-hot construction (one unique case on the region field, so overlap is a tool error not runtime contention); the fix for too-few is total coverage (a default error responder for unmapped-but-legal addresses, so a hole returns pslverr instead of hanging); the fix for ownership is downstream gating (every slave response term on its own psel_sN && PENABLE); and the verification is one $onehot0 property plus one no-decode-hole + bounded-completion property, with the glitch needing gate-level. Closing with "and the no-slave-selected bug taught me to give every legal address a responder — a default error slave — so a decode hole fails as a clean pslverr instead of a silent bus hang" signals real fabric integration, not spec reading.
10. Practice
- Count the selects. Given three field reports — "reads near the 0x2800 boundary return a mix of two registers," "the bus hangs only when firmware touches 0x1800," and "slave S2 answers when firmware reads S1" — assign each to the too-many, too-few, or ownership family and name the specific bug.
- Fix the overlap. Given
psel_s0 = sel && paddr inside [0x2000:0x2FFF]andpsel_s1 = sel && paddr inside [0x2800:0x3FFF], explain why an address in the overlap corrupts a read and rewrite the decode as a single mutually-exclusiveunique caseon the region field. - Close the hole. Explain why a legal-but-unmapped address with no
defaultbranch hangs the bus, and write the decode change (apsel_errdefault responder) that converts the hang into a cleanpslverrcompletion. - Honour the select. Show why a subordinate gating
pready_s2/prdata_s2onpenablealone answers a neighbour's access, and rewrite it to gate on its ownpsel_s2 && penableand stay quiet when!psel_s2. - Pick the level. For each of multiple-slave-selected, no-slave-selected, decode glitch, and slave-ignores-
PSEL, state whether a zero-delay RTL simulation can catch it or whether it needs gate-level / a delay-annotated run, and the assertion for each.
11. Q&A
12. Key takeaways
PSELis a one-hot select vector the decoder must keep exactly one-hot per access — and every classic bug breaks the count or the ownership it implies. Too many selects → contention and garbage reads; too few → a hang; right count, wrong honourer → the wrong slave answers. Count the selects before debugging.- The too-many family — multiple-slave-selected (overlapping ranges) and the decode glitch — is fixed by structural one-hot construction: derive every
PSELfrom a single mutually-exclusiveunique caseon the region field, so overlap is a tool error, not silent runtime contention. - The too-few family — no-slave-selected (a decode hole) — is fixed by total coverage: route every unmapped-but-legal address to a default error responder that completes with
pslverr, so a hole returns a clean error instead of a silent bus hang. - The ownership family — a slave that ignores its own
PSEL, and thePSEL/PENABLEdeassert-ordering race — is fixed downstream: gate every slave response term on its ownpsel_sN && PENABLE, keep unselected slaves quiet on the shared return, and deassert cleanly to idle. A one-hot decode is necessary but not sufficient. - A clean RTL run is not proof. The decode glitch and the deassert-ordering race only surface in gate-level or delay-annotated simulation as
PADDRripples; verify at the right level. - The verification reduces to a few named properties: one
$onehot0(safety) for contention, one no-decode-hole plus bounded-completion (liveness) for hangs, and one unselected-quiet check for ownership — carried forward into the full plan of the APB assertions and monitor work against the legal/illegal transition rules.