AMBA APB · Module 11
Read-Data Mux
The slave's read path — the mux that selects the addressed register onto PRDATA, the defined default for an unmapped offset (never X), the combinational-versus-registered PRDATA trade, and the read-side-effect hazard where a clear-on-read must fire exactly once at completion, not on the combinational select.
The register bank holds the values; the read path is what presents the right one on PRDATA. That sounds like a one-line case statement — and the happy path is — but the read mux is where three subtle, interview-grade failures live: an unmapped offset returning X instead of a defined value, a comb-versus-registered timing choice that mirrors the PREADY trade, and the nastiest of all, a read side effect — a clear-on-read or FIFO pop — firing on the combinational select instead of the real completion, silently destroying data. The single idea to carry: the read mux is a pure selector, but the design lives in its three margins — the default value, the timing of when PRDATA settles, and the discipline that any read side effect fires exactly once, at completion, never on the live mux select.
1. Problem statement
The problem is driving the addressed register's value onto PRDATA so it is valid at the moment the master samples it — and only then — while every unmapped offset returns a defined value and every read side effect fires exactly once.
A read is, structurally, a selection: the decoder produces a read-select from PADDR, and the slave must route exactly one register's output onto the shared PRDATA bus. But "route the right register" hides three decisions that are the whole of this chapter:
- What the bus reads when nothing is mapped. The
caseis over a finite set of offsets; the address space is larger. Every gap needs a defined default — conventionally0— so an out-of-range or reserved read returns a known value and never propagates anXinto the master. - When
PRDATAis actually required to be valid. APB does not demandPRDATAbe meaningful for the whole transfer — only in the access phase at completion (whenPSEL & PENABLE & !PWRITE & PREADY). Outside that,PRDATAis don't-care. That single fact decides whether you may drive0when unselected, whether you may register the output, and how a shared read bus is built. - When a read changes state. Some reads have side effects: a clear-on-read status, a FIFO pop, an interrupt-acknowledge. These must fire on the real, completed read — exactly once — not on the combinational mux select, which is live for the entire access including any wait state. Get this wrong and a read destroys data without the master ever seeing it.
So the job is not "mux the registers onto PRDATA" — it is to build a selector with a defined default, settled at the right time, whose side effects fire exactly once at completion.
2. Why previous knowledge is insufficient
You have just built the two things the read mux consumes, and you have already learned the timing discipline it mirrors — but none of those chapters built the read path itself.
- Chapter 11.1 gave you the register outputs, not the path to the bus. The register-bank design produced
ctrl_q,status_q,iflag_q— the typed storage outputs. It deliberately stopped at "the read mux selects among these." It never said what happens at an unmapped offset, how the selection is timed, or what a clear-on-read does. That is this chapter. - Chapter 11.2 gave you the read-select, not what to do with it. The address decoder turns
PADDRinto a one-hot or case selector that says which register is addressed. But a select line is combinational and live for the whole access; the decoder does not know about completion, defaults, or side effects. The mux must add all three on top of the raw select. - Module 8 gave you the timing discipline, not the read mux that obeys it.
PREADYtiming taught the combinational-versus-registered trade and that the master samples in the access phase at completion. The read path mirrors that exact trade — a combinational mux gives the value in the access cycle, a registeredPRDATAadds a cycle and must pair with a registeredPREADY. You know the principle; you have not yet applied it toPRDATA.
So the model to add is the read path as its own module: a selector that consumes the bank outputs (11.1) and the read-select (11.2), obeys the access-phase sampling rule (Module 8), and adds the three things none of them covered — the default, the output timing, and side-effect gating. The write logic (11.4) and PREADY generation (11.5) are the symmetric pieces you build next.
3. Mental model
The model: the read mux is a switchboard operator. The decoder hands the operator a slip of paper — the read-select — naming one line. The operator's only job is to patch that one line's current value through to a single outgoing wire, PRDATA. Three rules make a real operator out of a naive one. First, if the slip names a line that doesn't exist, the operator does not leave the wire floating and does not patch in noise — they patch in a standard silence, a defined 0. Second, the value on the outgoing wire only matters at one instant — the moment the caller picks up at the far end (completion); before that, whatever is on the wire is irrelevant. Third, and most important: patching a line through is not the same as acting on it. Some lines have a meaning to "answering" them — picking up drains a message queue. The operator must only drain the queue when the call truly completes, not every time the slip happens to name that line while the caller is still dialing.
Three refinements make it precise:
- The mux is combinational selection; the default is a real input. The selector is a
case(or one-hot AND-OR) over the read-select, choosing among the register outputs. Thedefaultarm is not an afterthought — it is a wired input,32'h0, that handles every unmapped offset deterministically. An incompletecasewith no default is how anX(or an inferred latch) gets ontoPRDATA. - Validity is a phase property, not an always property.
PRDATAis only required valid in the access phase at completion — the cycle wherePSEL & PENABLE & !PWRITE & PREADY. Outside that window it is don't-care, which gives you freedom: you may drive0when your slave is unselected (easing a shared/ORed read bus), or you may register the output (settling it a cycle later, paired with a registeredPREADY). - A side effect is gated on completion, not on select. The combinational mux select is asserted for the entire access, including wait states. A clear-on-read or FIFO pop must fire on the qualified completion pulse —
PSEL & PENABLE & !PWRITE & PREADY— so it happens exactly once, on the cycle the master actually captures the data. Tying it to the bare select pops during the wait, before the master reads, and loses the entry.
4. Real SoC implementation
In RTL, the read path is a combinational mux with an explicit default, feeding PRDATA, plus a separate completion-qualified pulse for any read side effect. Keep the selection and the side effect in different always-blocks: the mux is pure combinational logic; the side effect is a clocked event gated on completion. Here is the idiomatic structure.
// Read path of an APB slave: a combinational mux onto PRDATA with a defined
// default, plus a completion-gated read side effect (clear-on-read FIFO pop).
// Inputs: the bank outputs (chapter 11.1) and the read-select (chapter 11.2).
module apb_read_path (
input pclk, presetn,
// --- APB phase signals (for the completion qualifier) ---
input psel, penable, pwrite, pready,
input [11:0] paddr,
// --- register-bank read outputs (chapter 11.1) ---
input [31:0] ctrl_q, status_q, iflag_q,
input [31:0] fifo_head, // RO data, head of a read FIFO
// --- side-effect handshake to the FIFO ---
output fifo_pop, // 1-cycle pop pulse on real read
// --- read data to the bus ---
output reg [31:0] prdata
);
// Offset map (word-aligned). The decoder (11.2) could hand a one-hot select
// instead; here we case on paddr for clarity.
localparam CTRL = 12'h000,
STATUS = 12'h004,
IFLAG = 12'h008,
FIFO = 12'h00C;
// 1) The read MUX — pure combinational selection with a DEFINED DEFAULT.
// No latch, no X: every unmapped offset returns 0. Note this is the
// COMBINATIONAL variant: PRDATA is valid in the access cycle itself.
always_comb begin
case (paddr)
CTRL: prdata = ctrl_q;
STATUS: prdata = status_q;
IFLAG: prdata = iflag_q;
FIFO: prdata = fifo_head;
default: prdata = 32'h0000_0000; // unmapped offset → defined 0, never X
endcase
end
// (Optionally drive 0 when unselected to ease a shared/ORed read bus:
// if (!psel) prdata = 32'h0; — legal because PRDATA is don't-care then.)
// 2) The READ COMPLETION qualifier — the ONLY pulse that "really" reads.
// True for exactly one cycle: access phase, a read, and PREADY high.
wire read_done = psel & penable & ~pwrite & pready;
// 3) The read SIDE EFFECT — gated on completion, NOT on the mux select.
// The pop fires once, on the cycle the master captures fifo_head.
// Tying this to (paddr==FIFO) alone would pop during a wait state.
assign fifo_pop = read_done & (paddr == FIFO);
endmoduleThree facts make this the right structure. First, the mux has an explicit default so PRDATA is defined for every possible PADDR — there is no case arm that leaves it un-assigned, so no latch and no X can reach the bus. Second, PRDATA is the combinational selection itself — valid in the access cycle, the read-path analogue of a combinational PREADY; a registered variant simply flops the mux output (always_ff @(posedge pclk) prdata <= mux_out;), adding one cycle of latency that you must absorb by also registering PREADY (Module 8), or the master samples stale data. Third — the critical line — the side effect is read_done & (paddr==FIFO), not just (paddr==FIFO): the pop fires on the single completion pulse, so the FIFO advances exactly when the master captures fifo_head, never during the combinational select that is live for the whole access. The byte lanes follow the same rule as the mux: APB returns a full 32-bit word and the master extracts the bytes it wants, so the slave drives all lanes of the selected register and lets the master's PSTRB-less read take the word whole.
5. Engineering tradeoffs
The read path's shape is one main trade — when PRDATA settles — plus the unselected-drive policy that a shared bus forces.
| Property | Combinational PRDATA | Registered PRDATA | Drive 0 when unselected |
|---|---|---|---|
| Read latency | Value ready in the access cycle (zero extra) | One extra cycle; must pair with registered PREADY | Independent of this trade — applies to either |
| Timing closure | Long path: decode → mux → PRDATA in one cycle | Short path: flop output isolates the mux from the bus | No effect on the data path itself |
| PREADY coupling | Works with combinational or registered PREADY | Must pair with registered PREADY (Module 8) or master samples stale | Orthogonal |
| Shared / ORed read bus | Needs each slave to drive 0 (or tristate) when unselected | Same requirement, on the registered output | This is the enabler — ORing read buses need 0-when-idle |
| Read-side-effect safety | Side effect must be gated on completion regardless | Same gating; the flop does not change the rule | Unaffected — the pulse is still completion-qualified |
| When to choose | Default for small/fast register slaves | Large fan-in mux, deep banks, or tight PCLK | Whenever multiple slaves share an ORed PRDATA |
The throughline: the combinational mux is the default and the simplest — PRDATA is valid in the access cycle, like a zero-wait PREADY. You register the output only when the mux's fan-in or the clock period makes the decode-to-PRDATA path the critical path — and the moment you do, you owe a registered PREADY to keep the master sampling the right cycle. The "drive 0 when unselected" choice is orthogonal to the timing trade: it is what an ORed shared read bus needs, and it is legal precisely because PRDATA is don't-care outside your access phase. None of these touch the side-effect rule — that pulse is completion-qualified in every variant.
6. Common RTL mistakes
7. Debugging scenario
The signature read-path bug is a clear-on-read FIFO that silently loses an entry, caused by gating the read side effect on the combinational mux select instead of the real completion — and it only manifests when the read takes a wait state.
- Observed symptom: a status FIFO (or a read-to-clear interrupt log) occasionally skips an entry. Software reads the FIFO head, gets entry
N, but the next read returns entryN+2— entryN+1vanished. It is intermittent and worsens under bus contention, when reads to that slave are more likely to incur a wait state. Back-to-back zero-wait reads look fine; the bug only appears whenPREADYis low for a cycle. - Waveform clue: zoom in on a read that waits.
PSELandPENABLEare high,PADDRselects the FIFO, butPREADYis low for one cycle (the access-phase wait). Thefifo_popstrobe fires during that wait cycle — whilePREADYis still low — and thenPRDATAat the real completion cycle shows the next entry, not the one that was at the head when the access began. One entry was popped without ever being captured. - Root cause: the side effect was gated on the combinational decode/select —
assign fifo_pop = (paddr == FIFO_OFFSET) & psel;— which is asserted for the whole access, including the wait. So the pop fires on the access cycle regardless ofPREADY, advancing the FIFO before the master samples. The mux dutifully presents the new head at completion, the master captures that, and the entry that was popped during the wait is gone. It is a completion-qualification bug in the side effect, not in the mux or the FIFO. - Correct RTL: qualify the side effect with the full completion condition so it fires exactly once, on the cycle the master actually reads —
assign fifo_pop = psel & penable & ~pwrite & pready & (paddr == FIFO_OFFSET);. Now the pop and the master's capture happen on the same cycle: the master gets entryN, the FIFO advances toN+1, and the next read getsN+1. No wait state can pop early, because the pulse is gated onpready. - Verification assertion: assert the pop only ever fires on a completed read, and exactly once per read —
assert property (@(posedge pclk) disable iff(!presetn) fifo_pop |-> (psel & penable & ~pwrite & pready & (paddr == FIFO_OFFSET)));(every pop coincides with a real FIFO read completion) — plus a no-X check on the data bus at completion:assert property (@(posedge pclk) disable iff(!presetn) (psel & penable & ~pwrite & pready) |-> !$isunknown(prdata));(PRDATAis never X when the master samples). - Debug habit: when a read-to-clear or pop register loses data, do not start in the FIFO or the software driver — go to the gating condition of the side effect and check it against the completion qualifier, not the select. The litmus test: does the strobe survive a wait state without firing? If the side effect can fire while
PREADYis low, it is gated on the wrong signal. Any read with a side effect must be conditioned onPSEL & PENABLE & !PWRITE & PREADY, full stop — the combinational select is a trap because it is live for the entire access.
8. Verification perspective
The read path is verified against three properties — every register reads back, no offset ever returns X, and every read side effect fires exactly once at completion — and the comb-versus-registered timing must be covered explicitly.
- Read back every register, against its kind. For each mapped offset, drive a read and check
PRDATAequals the register's expected value: an RW reads what was written, an RO reflects hardware, a W1C shows the current flags. A register-model-driven check (UVM RAL or equivalent) automates the mirror, but only if the model knows the read value of each kind — a WO/strobe must read its defined value (e.g.0), not whatever the storage holds. - Hit the unmapped space — assert a defined value, never X. Read a sweep of reserved and out-of-range offsets and assert
PRDATAis the defined default (0) and is never unknown:!$isunknown(prdata)at completion for all addresses, mapped and unmapped. This is the single highest-value read-path check because an X onPRDATAcorrupts the master and is exactly what a missingdefaultproduces. Cover that the unmapped range was actually exercised. - Side effect fires exactly once, at completion. For every read with a side effect (clear-on-read, FIFO pop, interrupt-ack), assert the strobe coincides only with
PSEL & PENABLE & !PWRITE & PREADYand never during a wait state — and count it: exactly one pop per read. Drive reads with inserted wait states deliberately; a side effect bug is invisible to zero-wait reads. Cover both the wait-state and zero-wait read of every side-effecting register. - Cover the timing variant. If the slave can be built combinational or registered (or has both modes), functional coverage must record reads in each configuration, and a scoreboard must check that registered
PRDATApairs with registeredPREADYso the master samples the correct cycle. A test passing in combinational mode says nothing about the registered path.
The point: the read path's correctness is defined-value-everywhere plus side-effect-once-at-completion. Read back every register, sweep the unmapped space for a defined non-X value, force wait states to expose early-firing side effects, and cover both timing variants — not just that each mapped address returned the right value on a zero-wait read.
9. Interview discussion
"Walk me through an APB slave's read path" separates candidates fast: a weak answer is "a mux on PADDR onto PRDATA"; a strong one names the three margins that actually break.
Lead with the structure: the read mux is a combinational selector — a case (or one-hot) over the decoder's read-select — routing one register's output onto PRDATA, with an explicit default of 0. Then deliver the depth. First, the default is mandatory: every offset, including reserved and out-of-range, must return a defined value, because an incomplete case infers a latch or drives X that propagates into the master. Second, PRDATA is only required valid in the access phase at completion — outside that it is don't-care, which is why you may drive 0 when unselected to feed an ORed shared read bus, and why you may register the output. Third — and this is the line that signals you have debugged real silicon — a read side effect (clear-on-read, FIFO pop) must be gated on the completion qualifier PSEL & PENABLE & !PWRITE & PREADY, not on the combinational select, because the select is live for the whole access including wait states; gating on the select pops during the wait and silently loses data. Close on timing: the comb-versus-registered choice mirrors the PREADY trade — a combinational mux gives the value in the access cycle; a registered PRDATA adds a cycle and must move together with a registered PREADY (Module 8). Mentioning that the lost-FIFO-entry bug lives in the side effect's gating, not the FIFO, shows you have actually fixed one.
10. Practice
- Write the mux. From memory, write the
always_combread mux for a slave with CTRL, STATUS, IFLAG, and a FIFO data register, with a defined default. State why thedefaultarm is not optional. - Place the default. Given a 4 KB address space and four mapped 32-bit registers, describe what
PRDATAreturns for every unmapped offset and the one line of RTL that guarantees it is neverX. - Gate the side effect. Write the assign for a clear-on-read FIFO pop and explain, cycle by cycle, why it must be
psel & penable & ~pwrite & pready & (paddr==FIFO)and not just(paddr==FIFO). - Convert to registered. Take the combinational mux and turn
PRDATAinto a registered output; state exactly what else must change in the slave so the master still samples the right cycle. - Find the lost entry. Given a FIFO that returns entry
NthenN+2, withfifo_popfiring whilePREADYis low on a waited read, locate the bug and give the one-line fix.
11. Q&A
12. Key takeaways
- The read mux is a pure combinational selector — a
caseor one-hot over the decoder's read-select (11.2), routing one register-bank output (11.1) ontoPRDATA. The design lives in its three margins: the default, the output timing, and side-effect gating. - Every offset must return a defined value — conventionally
0— neverX. Thedefaultarm of thecaseis mandatory; omitting it infers a latch or drivesXthat propagates into the master and corrupts reads. PRDATAis required valid only in the access phase at completion (PSEL & PENABLE & !PWRITE & PREADY). Outside that it is don't-care — so you may drive0when unselected to feed an ORed shared read bus, or register the output.- A read side effect (clear-on-read, FIFO pop) fires on the completion qualifier, not the mux select. The combinational select is live for the whole access, including wait states; gating a side effect on it pops/clears during the wait and silently loses data.
- Combinational versus registered
PRDATAmirrors thePREADYtrade. A combinational mux gives the value in the access cycle; a registeredPRDATAadds a cycle and must move together with a registeredPREADY(Module 8), or the master samples stale data. - Verify defined-value-everywhere and side-effect-once-at-completion. Read back every register, sweep the unmapped space for a non-X default, force wait states to expose early-firing side effects, and cover both timing variants — not just zero-wait reads of mapped addresses.