AMBA APB · Module 11
The Address Decoder
The slave's internal address decoder — turning PADDR into a one-hot per-register select on a word-aligned offset compare, and detecting an in-window-but-unmapped offset as a decode error that feeds PSLVERR. The seam the read mux, write logic, and error generation all plug into.
The register bank you built in the previous chapter takes a decoded write-enable — this chapter builds the thing that decodes it. The address decoder is the slave's "which register?" function: it takes PADDR, compares the word-aligned offset against the slave's register map, and produces a clean one-hot per-register select plus a single decode-error flag for an offset that lands inside the slave's window but maps to no real register. That select is the seam everything else plugs into — the write logic uses it as a per-register write-enable, the read mux uses it to pick the read source, and the decode-error flag is one of the inputs to PSLVERR. Get the decoder right — word-aligned, mutually exclusive, byte-bits ignored — and the rest of the slave is mechanical. Get it wrong and a legal access silently misses its register, or two registers are written by one transfer.
1. Problem statement
The problem is mapping a 32-bit PADDR onto exactly one of the slave's registers — or onto "none of them, and that's an error" — using combinational logic that the write logic and read mux can consume directly.
A slave occupies a window of the system address map (say a 4 KB page), and inside that window it exposes a handful of registers at fixed offsets: CTRL at 0x0, STATUS at 0x4, IFLAG at 0x8, and so on. When a transfer arrives, the slave must answer one question from PADDR alone: which of my registers is this access for? That question forces three decisions before any write or read happens:
- Which bits of
PADDRactually select a register. APB is a word-oriented bus: registers sit on 4-byte boundaries, soPADDR[1:0]are byte-lane bits and play no part in choosing a register. The decoder must compare the word-aligned offset —PADDR[upper:2]— against the register map, and deliberately ignore the low bits. Comparing the fullPADDRis the single most common decoder bug. - What "no register" means. An offset can be inside the slave's window yet match no mapped register (a gap in the map, an offset past the last register). That is not a valid access, but it is also not the system's unmapped-peripheral case — it is an unmapped register inside a selected slave. The decoder must detect it and raise a decode-error flag that later becomes
PSLVERR, rather than silently selecting nothing or aliasing onto a real register. - The shape of the select the rest of the slave consumes. The decoder's output must be a clean, mutually exclusive (one-hot) per-register select — at most one line hot — because the write logic turns each select line into a per-register write-enable and the read mux uses the same lines to pick exactly one read source. Two lines hot at once means one transfer writes two registers; zero lines hot on a legal address means the access vanishes.
So the job is not "compare an address" — it is to define a word-aligned, one-hot, error-detecting decode that is the clean interface between the bus and the bank.
2. Why previous knowledge is insufficient
Two chapters bracket this one, and neither builds the decoder itself:
- Chapter 11.1 — register-bank design — established that the bank receives a decoded write-enable, but stopped there. It deliberately said the bank takes
wr_en_ctrl,wr_en_iflagand mergedwr_data, and that the address decode lives in the "thin glue." This chapter is that glue's first piece: it builds the logic that produces thosewr_en_*lines fromPADDR. So you know what the decoder must output (a per-register select); you have not yet built how it gets there. - Module 9.3 — illegal-address-access — covered the system decoder, not this one. That chapter built the bridge/interconnect decoder that routes a transfer to the right slave and the default-slave behaviour when no peripheral is mapped. That is the wrong level for this chapter. Here
PSELto this slave is already asserted — the system has decided the access belongs to us — and the question is which of our registers it hits. The two decoders are nested: the system decoder picks the peripheral; this decoder picks the register inside it. Re-using the system view here is a level-of-abstraction error. - The protocol layer never told you how to fan out.
PSEL/PENABLE/PADDRdescribe the access, but nothing in the protocol says how a slave converts an address into one-hot register selects, or what an in-window gap means. That conversion — and its one-hot discipline — is exactly what this chapter adds.
What you build here feeds three later chapters directly: the one-hot select becomes the read source selector in 11.3 — read-data mux — and the per-register write-enable in 11.4 — write logic — while the decode-error flag becomes one input to 11.6 — PSLVERR generation. This chapter produces the flag; 11.6 turns it into the bus error.
3. Mental model
The model: the decoder is a row of equality testers reading the word number off PADDR, with a single "none matched" alarm. Think of the slave's registers as numbered post-office boxes. PADDR is a full street address, but only the box number matters — the byte offset within a box (which of the 4 bytes) is for the byte-strobe logic, not for choosing the box. So the decoder shifts away the byte bits, looks at the box number PADDR[upper:2], and lights up exactly one box's lamp. If the box number is in the building's range but no box has that number, a separate red alarm — decode_err — lights instead, and no box lamp comes on.
Three refinements make it precise:
- Compare the word-aligned offset, ignore the byte-lane bits. Registers are 4 bytes and word-aligned, so
PADDR[1:0]never selects a register — they only choose a byte within the selected word (and that's the strobe logic's job). The decoder comparesPADDR[upper:2](equivalently, the offset shifted right by 2). Whether you implement it as an explicit comparator on the upper bits or acaseon the word index, the principle is identical: the select is a function of the word number only. - The select is one-hot, and "none" is a distinct, meaningful state. Exactly one of
{sel_ctrl, sel_status, sel_iflag, …}is hot for a mapped offset, and zero are hot for an unmapped-in-window offset — at which pointdecode_erris asserted. One-hot is not an aesthetic preference: the read mux and write logic both assume it. Two-hot writes two registers; the "none + decode_err" state is what feeds the error path. So the decoder's real output is a one-hot vector plus an error bit. - Decode is combinational and stable; the action is at commit.
PADDRis held constant by the master from the SETUP phase through the entire ACCESS phase, so the decode is pure combinational logic off a stable input — you do not need to register the select. The select is valid the momentPADDRsettles; the action it gates (a write commit, a read return) is qualified separately byPWRITE,PENABLE, andPREADY. Decode now, act at commit — keeping decode combinational offPADDRand the commit timing in the write/read logic is what keeps each piece simple.
4. Real SoC implementation
In RTL the decoder is a small combinational block that slices the word-aligned offset out of PADDR, runs a one-hot case (or a comparator bank) against the register map, and emits both the select vector and a decode_err for any in-window offset that hits no register. The discipline is: compare only PADDR[upper:2], make the select mutually exclusive by construction, and treat the default as the decode error — never as "select register 0."
// Address decoder: the slave's "which register?" function.
// Pure combinational off PADDR (stable across SETUP + ACCESS).
// Produces a ONE-HOT per-register select and an in-window-but-unmapped
// decode-error flag. The write logic turns sel_* into per-register
// write-enables; the read mux uses sel_* to pick the read source;
// decode_err feeds PSLVERR generation (Ch. 11.6).
module apb_addr_decoder #(
parameter int ADDR_W = 12 // 4 KB slave window
) (
input logic [ADDR_W-1:0] paddr, // held stable by the master
output logic sel_ctrl,
output logic sel_status,
output logic sel_iflag,
output logic decode_err // in window, no register here
);
// Register map, by WORD offset. PADDR[1:0] are byte-lane bits and
// are deliberately ignored: registers are word-aligned. We compare
// the word-aligned offset only.
localparam logic [ADDR_W-1:0] OFF_CTRL = 12'h000;
localparam logic [ADDR_W-1:0] OFF_STATUS = 12'h004;
localparam logic [ADDR_W-1:0] OFF_IFLAG = 12'h008;
// Word-aligned offset: drop the two byte bits so any byte within a
// word maps to the same register select (alignment is checked
// elsewhere; the decoder must not alias on PADDR[1:0]).
wire [ADDR_W-1:0] word_off = {paddr[ADDR_W-1:2], 2'b00};
always_comb begin
// Default: nothing selected, and (until proven mapped) it's a
// decode error. The default arm is the error case, NOT register 0.
sel_ctrl = 1'b0;
sel_status = 1'b0;
sel_iflag = 1'b0;
decode_err = 1'b1; // assume unmapped, clear on a hit
unique case (word_off) // 'unique' flags accidental overlap
OFF_CTRL: begin sel_ctrl = 1'b1; decode_err = 1'b0; end
OFF_STATUS: begin sel_status = 1'b1; decode_err = 1'b0; end
OFF_IFLAG: begin sel_iflag = 1'b1; decode_err = 1'b0; end
default: decode_err = 1'b1; // in window but no register here
endcase
end
endmoduleThree facts make this the right structure. First, the compare is on word_off, not raw PADDR — masking PADDR[1:0] to zero means a byte-2 access to STATUS (0x6) decodes to the same word as 0x4, so the decoder never aliases or misses on the byte bits (alignment policy is a separate, later decision). Second, the default arm is the decode error, not a fallthrough to register 0 — an in-window offset with no register raises decode_err and selects nothing, which is exactly the flag 11.6 turns into PSLVERR; making the default select a real register is how unmapped offsets silently corrupt CTRL. Third, unique case plus a one-cold default makes the select one-hot by construction — at most one sel_* is ever high, so the write logic can use each as a clean per-register write-enable and the read mux can use them as mutually-exclusive selects. Note the block is combinational (always_comb): the select is valid as soon as PADDR settles, and the commit (write strobe, read return) is qualified downstream by PWRITE/PENABLE/PREADY.
5. Engineering tradeoffs
How you build the decode is a trade between area, scalability, and how cleanly decode-errors and one-hot fall out.
| Decode style | How it works | Area / timing | Scalability | Decode-error & one-hot support |
|---|---|---|---|---|
| One-hot comparator bank | One == per register on PADDR[upper:2], OR-reduce for "any hit" | Small for a few regs; grows linearly with register count | Poor for large/sparse maps (one comparator each) | One-hot by construction; decode_err = ~(|sel) is trivial |
unique case on word offset | A single case on the word-aligned offset; default = error | Synthesises to compact priority/one-hot logic; tool warns on overlap | Good for a handful of named registers (most slaves) | One-hot by unique; default arm gives decode_err cleanly |
| Range-table / base+limit | Compare offset against [base, base+size) ranges | Larger compares (two bounds each); risk of overlap if hand-coded | Good for windowed sub-blocks (e.g. a RAM aperture + CSRs) | Overlapping ranges break one-hot — must prove disjoint |
| Array index (offset → index) | Use PADDR[upper:2] directly as an index into a reg array | Smallest for a dense, contiguous RW block | Excellent for dense uniform maps; bad for sparse/typed maps | "Index out of range" = decode_err; no per-reg select to keep one-hot |
The throughline: for a small, typed CSR map — the common slave — a unique case on the word-aligned offset wins on clarity and gives one-hot and decode-error almost for free. A dense, uniform RW block is better served by indexing a register array directly (PADDR[upper:2] is the index), where the one-hot select degenerates into an address. Reach for range tables only when the slave really has windowed sub-apertures, and then prove the ranges are disjoint or you reintroduce the double-select bug. Whatever the style, the two invariants must survive: the select is mutually exclusive, and an in-window-unmapped offset raises decode_err rather than aliasing onto a real register.
6. Common RTL mistakes
7. Debugging scenario
The signature decoder bug is a legal access that silently misses its register because the decoder compared the full PADDR — byte-lane bits included — so only the exactly-word-aligned byte hit, and any other byte of the same word matched nothing (and a stray address aliased onto the wrong register).
- Observed symptom: firmware writes a 32-bit value to
STATUS's word, but reads back the old value — the write appears to vanish. It only happens on some accesses: a register-model test that always uses the exact base offset passes, while a test that exercises sub-word or differently-aligned accesses, or a CPU that issues the access at a byte offset, sees the write disappear. Occasionally a different, unrelated register's value changes instead. - Waveform clue: on the failing transfer,
PADDRis0x6(or0x5,0x7) — a byte within theSTATUSword at0x4— and everysel_*line is low whiledecode_erris high, even thoughPSELto this slave is asserted and the address is plainly inside the window. On the aliasing variant, the wrongsel_*goes high for an address whose low bits happen to match another register's full value. - Root cause: the comparator tested
paddr == OFF_STATUSagainst the fullPADDRinstead of the word-aligned offset. BecauseOFF_STATUS = 0x4includes zero low bits, onlyPADDR == 0x4exactly matches;0x5/0x6/0x7— the same register, different byte lane — fail every comparator and fall through to thedecode_errdefault. The byte-lane bits were never meant to participate in register selection, but the compare let them. - Correct RTL: mask the byte bits before comparing — compare the word-aligned offset, not the raw address:
wire [ADDR_W-1:0] word_off = {paddr[ADDR_W-1:2], 2'b00}; ... unique case (word_off) OFF_STATUS: sel_status = 1'b1; ...— so any byte within theSTATUSword decodes tosel_status, and only a genuinely unmapped word reaches thedecode_errdefault. - Verification assertion: assert the select is always one-hot-or-none, and that a known in-window offset never spuriously errors — at most one select hot:
assert property (@(posedge pclk) disable iff(!presetn) psel |-> $onehot0({sel_ctrl, sel_status, sel_iflag}));and "a mapped word never raises decode_err regardless of byte bits":assert property (@(posedge pclk) disable iff(!presetn) (psel && (paddr[ADDR_W-1:2]==OFF_STATUS[ADDR_W-1:2])) |-> (sel_status && !decode_err));. - Debug habit: when a write "vanishes" or lands in the wrong register, don't start in the write logic or the bank — go to the decoder and check which bits of
PADDRthe compare uses. IfPADDR[1:0]are in the compare, you have a byte-lane aliasing bug. The fast triage is to drive two accesses to the same word at different byte offsets and watch whether the samesel_*asserts for both; if it doesn't, the decoder is comparing the byte bits.
8. Verification perspective
A decoder is verified against two invariants — one-hot-or-error select and complete, gap-aware coverage of the map — and the highest-value tests are the alias/boundary addresses and injected decode errors, not the happy-path base offsets.
- Assert the one-hot invariant continuously.
assert property (psel |-> $onehot0({sel_ctrl, sel_status, sel_iflag}))catches the double-select (overlapping ranges) and never-hot cases. Pair it with a mutual-exclusion of select and error:assert property (psel |-> ($onehot({sel_ctrl, sel_status, sel_iflag}) ^ decode_err))— exactly one register selected xor a decode error, never both, never neither. These two properties pin the decoder's entire output contract. - Cover the full map and the gaps. Address-only coverage that just touches each register's base offset is the weakest possible test — it is exactly the test the byte-lane-aliasing bug passes. The coverage model must include: every register hit at every byte offset within its word (alias coverage — proves
PADDR[1:0]is ignored), every gap offset inside the window hit (provesdecode_errfires where it should), and the boundary offsets (the last mapped register, the first unmapped offset, the top of the window). - Inject decode errors deliberately. Drive accesses to in-window-but-unmapped offsets and check
decode_errasserts with nosel_*hot; drive sub-word/byte-offset accesses to mapped words and check the correctsel_*asserts withdecode_errlow. A directed test sweep over the whole window — every word offset from0to window-top, plus a few byte-offset aliases — is cheap and exhaustively pins down both the map and the gaps. Constrained-random with a coverage point on "accessed an unmapped offset" closes what the directed sweep misses.
The point: the decoder's correctness is a coverage-of-the-whole-window property, not a hit-the-named-registers property. Assert one-hot-xor-error, cover every byte alias and every gap, and inject the unmapped offsets — because the bugs hide in the addresses your happy-path test never sends.
9. Interview discussion
"How does an APB slave decode PADDR into a register select?" is a strong RTL-judgment question because a weak answer says "compare the address" while a strong one shows you think in word-aligned offsets, one-hot selects, and decode errors.
Lead with the structure: the decoder is combinational logic off PADDR that produces a one-hot per-register select plus an in-window-but-unmapped decode-error flag. Immediately make the key distinction that separates seniors: you compare the word-aligned offset PADDR[upper:2], not the full address — PADDR[1:0] are byte-lane bits and must not select a register, or a legal access at a non-zero byte offset misses every comparator. Then handle the error case correctly: an offset inside the slave's window that maps to no register raises a decode_err flag — which feeds PSLVERR — and must never fall through to selecting register 0, since that silently corrupts a real register. Stress the one-hot contract: the write logic uses the select as a per-register write-enable and the read mux uses it as a mutually-exclusive selector, so two-hot writes two registers — assert $onehot0. Close with two pieces of depth: this is not the system/bridge decoder (they are nested levels — system picks the slave, this picks the register), and the decode is combinational and unregistered because PADDR is stable across SETUP and ACCESS, with the action timed downstream at commit. Mentioning that the classic "vanishing write" bug is a full-PADDR compare with byte bits included signals you have debugged one.
10. Practice
- Slice the address. For a 4 KB slave (
ADDR_W = 12) with registers at0x0,0x4,0x8,0xC, state which bits ofPADDRthe decoder compares and which it ignores, and why. - Write the decoder. From memory, write the
always_combunique casethat producessel_ctrl/sel_status/sel_iflaganddecode_err, with thedefaultarm as the decode error. Explain why the default must not select register 0. - Find the alias. Given a decoder that compares
paddr == 12'h008and a CPU access atPADDR = 0x00A, state what happens to the select anddecode_err, identify the bug, and give the one-line fix. - Prove one-hot. Write the SVA that asserts at most one select line is hot, and a second property that says "select xor decode-error" — exactly one register or a decode error, never both.
- Cover the gaps. List the coverage points a happy-path "hit each register's base offset" test would miss, and the addresses you'd add to catch the byte-lane-alias and in-window-gap bugs.
11. Q&A
12. Key takeaways
- The address decoder is the slave's "which register?" function — combinational logic off
PADDRthat produces a one-hot per-register select plus an in-window-but-unmapped decode-error flag. It is the seam the read mux, write logic, andPSLVERRall plug into. - Compare the word-aligned offset
PADDR[upper:2], never the fullPADDR.PADDR[1:0]are byte-lane bits and must not select a register, or a legal access at a non-zero byte offset misses every comparator — the classic "vanishing write." - An in-window-but-unmapped offset must raise
decode_errand select nothing — never fall through to register 0. This chapter produces the flag; 11.6 turns it intoPSLVERR. - The select is one-hot by contract, not by convenience. The write logic uses it as a per-register write-enable and the read mux as a mutually-exclusive selector; two hot writes two registers. Assert
$onehot0and "select xor error." - Decode is combinational and unregistered because
PADDRis stable across SETUP and ACCESS — decode now, act at the commit, which is timed downstream byPWRITE/PENABLE/PREADY. - This is the register-level decoder, nested inside the system decoder (9.3): the system decode picks the slave, this picks the register. Verify it by covering every byte alias and every in-window gap, not just the named base offsets.