An APB read can be perfectly legal on every protocol line and still hand firmware the wrong number — and almost always the fault is the read-data path, not the handshake. PSEL, PENABLE, PWRITE and PREADY all look textbook in the waveform; the access completes in the right phase with no wait-state anomaly and no protocol assertion firing. Yet the value on PRDATA at the sampling edge is not the value the addressed register holds. This chapter is the field guide to that class of failure: every distinct way PRDATA returns the wrong register or the wrong value. The single idea to carry: when a read is wrong but the protocol is clean, stop staring at the handshake and interrogate the read-data path — the mux that selects the register, the flop that holds the value, the byte lanes that align it, and the bus that returns it — because the bug lives in one of those four, not in PREADY.
1. Problem statement
The problem is diagnosing and fixing the read-data path when PRDATA returns the wrong register or the wrong value, in a transfer whose handshake is fully spec-compliant. Per AMBA APB (IHI 0024C) §2.1, PRDATA is the read-data bus driven by the selected subordinate during a read (PWRITE low) and sampled by the manager on the rising PCLK edge when PREADY is high. The spec says what must be on the bus and when — it does not, and cannot, tell you why a given slave puts the wrong bits there.
A clean handshake gives you no help here, and that is exactly what makes the class hard. Three properties define it:
- The protocol is silent because nothing protocol-level is broken. The access selects, enables, completes, and the manager samples on a valid
PREADY. No protocol rule is violated, so no protocol assertion fires. The failure is purely semantic — the bits are wrong — and semantic correctness is the scoreboard's job, not the protocol checker's. - The cause is in the data path, which the handshake never observes.
PREADYvouches that the access may complete; it says nothing about which register the mux selected or whether the value is aligned and width-correct. A read-data mux indexed by a stale registeredPADDR, aPRDATAnot held stable through ACCESS, a byte-lane swap, an unmapped-offset default branch, a second slave driving the shared return — every one of these produces a wrong value with a flawless handshake. - The symptom is often intermittent and address-dependent. The worst variants only manifest on certain address sequences — a read of register B right after register A, an access to an unmapped hole between two valid offsets, a narrow register read on an unaligned lane. Directed register-by-register tests, done one at a time with idle between, sail straight past them; the bug surfaces only under back-to-back or boundary traffic, which is why it ships.
So the engineering problem is not "is the read legal?" — it is — but "the read is legal and the value is wrong; which part of the read-data path produced the wrong bits, what does that look like in the capture, and what is the correct RTL?" That is a data-path debugging discipline, and it is distinct from everything the handshake chapters taught.
2. Why previous knowledge is insufficient
This module has built the read-data path from the ground up, and each prior chapter is now a reference this debugging guide points back to — but none of them is the failure taxonomy itself.
- The read-data path taught how
PRDATAis driven; read-data mux taught how the register is selected. Those chapters teach the correct structure — a decode selects a register, a mux routes it ontoPRDATA. This chapter is the inventory of every way that correct structure is implemented wrongly: the mux indexed by the wrong select, the decode that leaves a default hole, the stale index. Knowing how the mux should work does not enumerate the ways it ships broken. - PRDATA timing taught when the data must be stable. That is the deep treatment of the stability requirement —
PRDATAvalid and held through the sampling edge. Here, "not held through ACCESS" is one entry in a larger list, summarised and linked, sitting beside wrong-register-selected and byte-misalignment, which timing alone does not cover. - The sibling PREADY-stuck-low chapter and the PREADY common-bugs catalog own the handshake failures. Those are about the access hanging or completing at the wrong moment — the readiness contract. This chapter is the complement: the access completes correctly and on time, and the value is still wrong. The handshake catalog explicitly excludes the case where the protocol is clean but the data is bad; that case is this chapter.
The gap is precise: prior chapters taught the correct read-data path and the timing it must obey, and the handshake chapters taught when an access completes. None assembled the wrong-value taxonomy — the named set of read-data-path bugs, each with wrong-versus-right RTL — that lets you debug a clean-but-wrong read by recognition. Building that taxonomy is this chapter; proving a register bank is free of all of them with assertions and a scoreboard is the work of apb-assertions and apb-scoreboards.
3. Mental model
The model: a wrong PRDATA is one of exactly two questions — did the read reach the wrong register, or the right register with the wrong bits? — and you answer that question first, before touching any RTL. Every cause sorts cleanly under one of those two, and the bin tells you which part of the path to interrogate.
The two bins:
- Wrong register selected (the routing failure). The mux picked the wrong source. The value on
PRDATAis real, valid, well-formed data — it is just from the wrong address. Causes: the mux selected by a stale registeredPADDR(the index lags the live address by a phase, so back-to-back reads return the previous register), a decode that is one offset off, or an unmapped-offset default branch that silently returns a fixed register or0for an address that should have errored. The tell: the value is a recognisable other register's contents, and it correlates with the prior or neighbouring address. - Right register, wrong value (the integrity failure). The mux picked the correct source, but the bits are mangled on the way out. Causes:
PRDATAnot held stable through ACCESS (it transitions in the sample window), a byte-lane misalignment (the right bytes on the wrong lanes), a width/sign-extension error on a narrow register (upper bits garbage, or sign-extended where zero-extended was meant), or a read-of-write-only / clear-on-read register returning surprising-but-not-actually-wrong data. The tell: the value is almost right — correct register, shifted bytes, wrong upper bits, or a documented side-effect.
The discipline this buys you: bin before you debug. "It returns a recognisable other register's value, tracking the previous read" is instantly the routing bin — go look at the mux index and whether PADDR is registered. "It returns the right register but the high bytes are garbage" is instantly the integrity bin — go look at width and lane alignment. Three refinements sharpen it:
- The stale-index bug is a causality error, not a value error — and it is the sibling of asserted-too-early. Just as a
PREADYleading the data mux completes before the value is real, a mux indexed by a registeredPADDRselects the register the bus addressed last cycle. Both are "the read-data path running on the wrong phase." One leads the handshake; one lags the index. The capture signature is the same family: every Nth read is wrong, only when the prior access hit a different register. - The unselected-slave-drives-bus bug is non-local — the wrong value comes from a slave you are not even addressing. On a shared/OR'd read bus, a slave that drives
PRDATAwhen it is not selected collides with the real source. The corruption appears on the addressed slave's read, but the buggy RTL is in a different slave. Bin it as routing, but suspect the whole bus, not just the target. - The unmapped-default bug fails safe-looking and is therefore the most insidious. A
default: prdata = '0(ordefault: prdata = some_reg) on the read mux turns an access to an unmapped hole into a silent0or a silent wrong register instead of an error. It looks deliberate; it reads as "that address just returns zero." It is a routing bug masquerading as a feature, and it hides real address-decode mistakes — see address-decode-bugs.
4. Real SoC implementation
In real RTL the wrong-value bugs are a line or two of plausible code, and the fixes are equally short — which is why they pass a quick register-by-register sim and ship. Here are the wrong-versus-right pairs for the two highest-impact routing bugs: the stale-registered-PADDR mux (returns the previous register on back-to-back reads) and the unmapped-default branch (silently returns 0 for a hole), plus the hold-through-ACCESS integrity fix.
// ============================================================
// BUG 1 — WRONG REGISTER: read-data mux indexed by a STALE
// registered PADDR, so back-to-back reads return the PREVIOUS register.
// ============================================================
// WRONG: PADDR is registered for "timing closure," and the SAME registered
// copy is used to index the read-data mux. The index therefore lags the live
// address by one phase: in ACCESS the mux still points at LAST cycle's offset.
always_ff @(posedge pclk or negedge presetn)
if (!presetn) paddr_q <= '0;
else paddr_q <= paddr; // registered address (lags by a phase)
always_comb begin
case (paddr_q[7:2]) // <-- indexed by the STALE copy
6'h00: prdata = ctrl_reg;
6'h01: prdata = status_reg;
6'h02: prdata = data_reg;
default: prdata = '0;
endcase
end
// Back-to-back read of 0x00 then 0x04: the 0x04 access muxes ctrl_reg
// (last cycle's offset) -> manager samples the PREVIOUS register's value.
// CORRECT: index the read-data mux on the LIVE decoded select that is valid
// in the same ACCESS phase the manager samples. Register the SELECTED DATA if
// you need a pipeline stage, never the index that chooses it.
wire [5:0] sel = paddr[7:2]; // live address, valid this access
always_comb begin
case (sel) // <-- indexed by the LIVE select
6'h00: rdata_mux = ctrl_reg;
6'h01: rdata_mux = status_reg;
6'h02: rdata_mux = data_reg;
default: rdata_mux = '0; // (see BUG 2 — make this an error, not 0)
endcase
end
// Optional pipeline: register the chosen value, not the choosing index.
always_ff @(posedge pclk or negedge presetn)
if (!presetn) prdata_q <= '0;
else if (psel && !pwrite) prdata_q <= rdata_mux; // capture the SETTLED mux output
assign prdata = prdata_q; // held through ACCESS (BUG 3 fix)
// ============================================================
// BUG 2 — UNMAPPED-DEFAULT: a hole silently returns 0 (or a fixed
// register), hiding a real address-decode mistake as a "feature."
// ============================================================
// WRONG: every unmapped offset falls into default and returns 0. A firmware
// read of a typo'd address gets 0, looks valid, and the decode bug is invisible.
always_comb
case (sel)
6'h00: prdata = ctrl_reg;
6'h01: prdata = status_reg;
default: prdata = '0; // <-- 0x02..0x3F ALL read as 0, no error
endcase
// CORRECT: map exactly the implemented offsets; an unmapped offset returns a
// known pattern AND raises PSLVERR so the access is flagged, not silently 0.
always_comb begin
prdata = '0;
pslverr_d = 1'b0;
unique case (sel) // 'unique' flags overlap/missing in sim
6'h00: prdata = ctrl_reg;
6'h01: prdata = status_reg;
6'h02: prdata = data_reg;
default: begin
prdata = 32'hDEAD_DEAD; // recognisable "unmapped" marker, not 0
pslverr_d = (psel && penable); // error the access, don't fake-succeed it
end
endcase
end
// Now an access to a hole is an ERROR the manager sees, not a silent wrong value.Three facts drive these fixes. First, a routing fix is about which signal indexes the mux and when: the index must be the live decoded select valid in the sampling ACCESS phase, never a registered PADDR that lags it. If you need a pipeline stage for timing, register the selected data (prdata_q), not the select that chooses it — registering the index moves the whole mux a phase late and returns the previous register. Second, an unmapped offset must error, not silently return 0. A default: '0 is a routing bug wearing a feature's clothes: it converts a decode mistake into a plausible-looking value and hides it. Map exactly the implemented offsets and drive PSLVERR (IHI 0024C §2.1) on a hole so the manager is told the access failed. Third, PRDATA must be held stable through the entire ACCESS phase — capturing the settled mux output into prdata_q (the integrity fix above) guarantees the value the manager samples is the value that was selected, not a combinational output still rippling as the decode settles (prdata-timing is the full treatment).
5. Engineering tradeoffs
The catalog is the deliverable: one row per bug, the value you actually see, the root cause in the read-data path, and the fix. Memorise the shape — bin (routing vs integrity), bus signature, structural fix — and you can debug most clean-but-wrong reads by recognition.
| Bug name | Value on the bus | Root cause | Fix |
|---|---|---|---|
Stale registered PADDR index | A recognisable other register's value, tracking the previous read — "every Nth read is wrong" on back-to-back to different offsets | Read-data mux indexed by a registered PADDR that lags the live address by a phase | Index the mux on the live decoded select valid in the sampling ACCESS phase; if pipelining, register the selected data, not the index |
| Wrong-index decode (off-by-one) | A neighbouring register's value on every read of a given offset | Decode arithmetic off by one bit/word — paddr[7:2] mis-sliced, or word vs byte offset confusion | Fix the offset slicing to match the register map; verify against register-map-best-practices |
| Unmapped-default = 0 / fixed reg | An access to a hole returns 0 (or a fixed register) and looks valid — no error | default: prdata = '0 on the read mux turns a decode hole into a silent value | Map exactly the implemented offsets; an unmapped offset returns a marker and asserts PSLVERR (address-decode-bugs) |
PRDATA not held through ACCESS | Right register, but the value transitions in the sample window — intermittent, delay-dependent | Combinational mux output rippling as the decode settles; not registered/held to the sampling edge | Capture the settled mux into prdata_q and hold it stable through ACCESS (prdata-timing) |
| Byte-lane misalignment | Right register, right bytes on the wrong lanes — value looks byte-swapped or shifted | Read mux assembles PRDATA from byte lanes with a swapped/offset lane mapping | Map each register byte to its correct PRDATA[8n+7:8n] lane; check against the bus byte ordering |
| Width / sign-extension error | Right register, wrong upper bits — garbage or sign-extended where zero-extension was meant | Narrow (e.g. 8/16-bit) register driven onto a 32-bit PRDATA without defined upper bits | Zero-extend narrow registers to the full PRDATA width (unless a field is genuinely signed); never leave upper bits undriven/X |
| Unselected slave drives shared bus | The addressed slave's read is corrupted by a different slave's data (collision/OR) | A slave drives PRDATA when !PSEL, colliding on the shared/OR'd read return | Drive PRDATA only when psel && !pwrite; else '0 (for OR) or tristate/stay out of the return mux |
| Read of write-only / clear-on-read | A surprising value — 0, stale, or a side-effect that clears state on the read | Reading a write-only register, or a clear-on-read register whose read has a side-effect | Define read-behaviour per read-only-registers / status-registers; document W1C/clear-on-read semantics |
The throughline: every row sorts into routing or integrity, and every fix is either "index the mux on the live select / drive the bus only when selected" or "hold and width-align the value the mux produced." Two bins, two fix families. The stale-index, off-by-one and unmapped-default rows add the which-source dimension; the hold, byte-lane and width rows add the integrity-of-the-source dimension; the unselected-drive row adds the non-local dimension — the corruption is on the addressed slave but the bug is in another. The tradeoff threaded through all of them: registering PADDR closes timing but, if you register the index rather than the data, it costs you a phase and returns the previous register — pipeline the data, never the select.
6. Common RTL mistakes
7. Debugging scenario
Pick the stale-registered-PADDR mux bug, because it is the most maddening of the routing family: it passes every directed register-by-register test, ships, and surfaces in the field as an intermittent software fault that no one can reproduce by reading one register at a time.
-
Observed symptom: firmware sweeping a peripheral's registers in a tight loop intermittently reads, at offset B, the value that belongs to the register at the previously read offset A. Reading B in isolation — or with any idle between reads — always returns B correctly. The bus shows the access completing normally: right phase,
PREADYhigh, noPSLVERR, no protocol violation. -
Waveform clue: in the capture (Figure 2), on the ACCESS cycle of the read to offset B, the slave's read-data mux select still equals offset A's index — because the mux is indexed by the registered
PADDR, which holds last cycle's value.PRDATAtherefore presents register A's contents at the exact edge the manager samples on the completing read to B. When two consecutive reads target the same offset, the stale index happens to equal the live one and the bug is invisible; when they target different offsets, the manager latches the prior register's data. -
Root cause:
PADDRwas registered ("for timing closure") and the same registered copy was wired to index the read-data mux. The index lags the live address by a phase, so in the ACCESS phase the mux still points at the previous access's offset. Under back-to-back reads to different addresses, the one-phase lag returns register A on the read of B. -
Correct RTL: index the mux on the live decoded select valid in the sampling ACCESS phase; if a pipeline stage is genuinely needed for timing, register the selected data, not the select:
Azvya Education Pvt. Ltd.VLSI MentorSnippetwire [5:0] sel = paddr[7:2]; // LIVE address, valid this access always_comb case (sel) // mux indexed by the live select 6'h00: rdata_mux = ctrl_reg; 6'h01: rdata_mux = status_reg; 6'h02: rdata_mux = data_reg; default: rdata_mux = 32'hDEAD_DEAD; // hole -> marker + PSLVERR elsewhere endcase always_ff @(posedge pclk or negedge presetn) if (!presetn) prdata_q <= '0; else if (psel && !pwrite) prdata_q <= rdata_mux; // register the DATA, not the index assign prdata = prdata_q; // held stable through ACCESS -
Verification assertion: prove the read returns the value mapped to the live address — the selected register, not the previous one (see apb-assertions):
Azvya Education Pvt. Ltd.VLSI MentorSnippet// read returns the register mapped to the LIVE PADDR on the sampling edge assert property (@(posedge pclk) disable iff (!presetn) (psel && penable && pready && !pwrite && (paddr[7:2] == 6'h01)) |-> (prdata == status_reg) ); -
Debug habit: when reads are intermittently wrong but the protocol looks clean, do not chase the data path's contents first — chase the mux select's phase. Capture a back-to-back read of two different offsets and check whether the read-data mux select equals the current address or the previous one at the sampling edge. The "every Nth read returns the previous register" signature almost always means the mux is indexed by a registered
PADDRthat lags the live address — the routing-bin sibling of the asserted-too-early handshake bug.
8. Verification perspective
Because the catalog splits into routing (wrong register) and integrity (wrong bits) failures, the verification plan needs a value-correctness check that knows the live address-to-register mapping, plus stability, width and bus-ownership checks — none of which a protocol checker provides, because none of these bugs violate the protocol.
- A read-returns-mapped-value property catches the routing family. The core check is a scoreboard-style assertion: on a completing read,
PRDATAmust equal the register the livePADDRmaps to —(psel && penable && pready && !pwrite) |-> (prdata == expected_reg(paddr)). Bound per-offset (or driven by a scoreboard reference model), this catches the stale-index, off-by-one, and unmapped-default bugs in one stroke: the stale index returns the previous register, the off-by-one returns a neighbour, the default hole returns0where the model expects an error. - A hold-stable-through-ACCESS property plus a never-X check catch the integrity family. Assert that
PRDATAdoes not change between the first ACCESS cycle and the sampling edge on a read ($stable(prdata)across the held ACCESS whilepsel && penable && !pwrite && !preadythen unchanged at completion) to catch not-held-through-ACCESS, and a never-X check on the sampled lanes ((psel && penable && pready && !pwrite) |-> !$isunknown(prdata)) to catch width/undriven-upper-bit bugs. Byte-lane misalignment is caught by the mapped-value property when the reference model is byte-exact. - The unselected-drive bug needs a multi-slave / bus-ownership check the single-slave testbench cannot see. Assert that a slave does not drive
PRDATA(drives'0, or stays out of the return) when it is not selected:(!psel) |-> (prdata_slave == '0)per slave, or a bus-level "exactly one slave contributes to the OR'd return." This is the read-bus analogue of the default-ready violation — the corruption is non-local, so a directed single-slave read never exercises it. Pair it withPSLVERR-on-hole coverage so the unmapped-default bug is exercised, not just silently passed.
The point: each family has a named property — read-returns-mapped-value for routing, hold-stable plus never-X for integrity, single-driver for the shared bus — and the routing checks must be driven by an address-to-register reference model (the scoreboard), because "the value is wrong" is a semantic fault that only a model of the correct mapping can catch. A protocol-only checker passes every one of these bugs.
9. Interview discussion
"You read an APB register and get the wrong value, but every protocol signal looks correct — how do you debug it?" is a senior screening question because a weak answer chases PREADY (which is fine) while a strong answer immediately leaves the handshake and interrogates the read-data path with an organised taxonomy — exactly the recognition speed that separates someone who has debugged a register bank from someone who has only read the spec.
Frame it as wrong register or wrong bits — ask that first. PRDATA is the read-data bus the selected slave drives and the manager samples on a valid PREADY (IHI 0024C §2.1); a clean handshake says nothing about which register the mux chose or whether the bits are aligned. Then bin and enumerate crisply: in the routing family, the stale registered PADDR index (the mux indexed by a registered address that lags the live one by a phase — back-to-back reads return the previous register, the "every Nth read is wrong" field bug and the sibling of asserted-too-early), the off-by-one decode, the unmapped-default = 0 (a decode hole silently returning 0 instead of PSLVERR — the one that masquerades as a feature), and the unselected slave driving the shared read bus (non-local: corruption on the addressed slave, bug in another); in the integrity family, not-held-through-ACCESS, byte-lane misalignment, and width/sign-extension on a narrow register. Land the depth points: the routing fix is structural — index the mux on the live select, and if you pipeline for timing, register the data the mux selects, never the index that selects it; the integrity fix is to hold and width-align the value — capture the settled mux output and zero-extend narrow registers; and the verification is a scoreboard with an address-to-register reference model, because "the value is wrong" is semantic and a protocol-only checker is blind to it. Closing with "and the unselected-drive bug taught me to suspect the whole read return, not just the addressed slave, and to make every unmapped hole a PSLVERR, never a silent 0" signals real silicon debugging, not spec reading.
10. Practice
- Bin the symptom. Given three field reports — "offset B returns offset A's value, but only right after reading A," "an address between two valid registers returns 0 with no error," and "the right register but the top two bytes are garbage" — assign each to the routing or integrity bin and name the specific bug.
- Fix the stale index. Given
paddr_q <= paddrand a read muxcase (paddr_q[7:2]), explain why a back-to-back read of two different offsets returns the previous register, and rewrite it to index the live select while still allowing a registered pipeline stage. - Kill the silent hole. Rewrite a read mux whose
default: prdata = '0so an unmapped offset returns a recognisable marker and assertsPSLVERR, and state which bug class this exposes that the'0default hid. - Catch the wrong register. Write the SVA property that proves a completing read returns the register mapped to the live
PADDR, and state which bugs it catches (stale index, off-by-one, unmapped-default) and which it does not (byte-lane, width). - Find the non-local driver. Explain why an unselected slave driving
PRDATAcorrupts a different slave's read, write the per-slave assertion that catches it, and describe why a single-slave directed test never exercises it.
11. Q&A
12. Key takeaways
- A wrong
PRDATAwith a clean handshake is a read-data-path bug, not a handshake bug —PREADYvouches the access may complete, never which register the mux chose or whether the bits are aligned. Ask wrong register or wrong bits before touching any RTL. - The routing family — stale registered
PADDRindex (returns the previous register), off-by-one decode (returns a neighbour), unmapped-default0(a silent hole), and an unselected slave driving the shared read bus (non-local) — is fixed structurally: index the mux on the live decoded select, pipeline the data not the index, error every hole withPSLVERR, and drivePRDATAonly when selected. - The integrity family — not-held-through-ACCESS, byte-lane misalignment, and width/sign-extension on a narrow register — is fixed by holding and width-aligning the value: capture the settled mux output (prdata-timing), map bytes to their correct lanes, and zero-extend narrow registers to the full bus width.
- The unmapped-default
0is the most insidious bug because it looks like a feature: it converts a decode hole into a plausible value and hides real address-decode mistakes. Make every unmapped offset aPSLVERRwith a recognisable marker, never a silent0(address-decode-bugs). - The unselected-drive bug is non-local: the corruption is on the addressed slave but the bug is in a different slave on the shared return, so suspect the whole read bus and verify single-driver ownership per slave — a single-slave directed test cannot see it.
- The verification reduces to a few named properties driven by an address-to-register reference model: read-returns-mapped-value (routing) from a scoreboard, hold-stable-through-ACCESS plus never-X (integrity), and single-driver bus ownership (shared bus) — because "the value is wrong" is a semantic fault a protocol-only checker is blind to (apb-assertions).