Skip to content

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 PADDR actually select a register. APB is a word-oriented bus: registers sit on 4-byte boundaries, so PADDR[1:0] are byte-lane bits and play no part in choosing a register. The decoder must compare the word-aligned offsetPADDR[upper:2] — against the register map, and deliberately ignore the low bits. Comparing the full PADDR is 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_iflag and merged wr_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 those wr_en_* lines from PADDR. 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 PSEL to 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/PADDR describe 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 compares PADDR[upper:2] (equivalently, the offset shifted right by 2). Whether you implement it as an explicit comparator on the upper bits or a case on 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 point decode_err is 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. PADDR is 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 moment PADDR settles; the action it gates (a write commit, a read return) is qualified separately by PWRITE, PENABLE, and PREADY. Decode now, act at commit — keeping decode combinational off PADDR and the commit timing in the write/read logic is what keeps each piece simple.
A block diagram of an APB slave's address decoder: PADDR is split into ignored byte-lane bits and a word-aligned offset that feeds a bank of equality comparators against the register map, producing a one-hot per-register select (sel_ctrl, sel_status, sel_iflag) for mapped offsets and a decode-error flag for an in-window-but-unmapped offset, with the selects feeding the write logic and read mux and the error flag feeding PSLVERR generation.
Figure 1 — the slave's internal address decoder. PADDR arrives and is split: the low two bits PADDR[1:0] are byte-lane bits, ignored by the decoder (dashed), while the word-aligned offset PADDR[upper:2] feeds a bank of equality comparators. Each comparator tests the offset against one register-map entry (CTRL at 0x0, STATUS at 0x4, IFLAG at 0x8) and drives one line of a one-hot select bus. An offset that lands inside the slave's window but matches no mapped register (e.g. 0xC) leaves every select line low and asserts the amber decode_err flag instead. On the right, the select lines fan out: the write logic consumes them as per-register write-enables, the read mux uses them to choose the read source, and decode_err feeds PSLVERR generation. The decoder is purely combinational off the stable PADDR; the action it gates happens at the access-phase commit.

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."

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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
endmodule

Three 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 styleHow it worksArea / timingScalabilityDecode-error & one-hot support
One-hot comparator bankOne == per register on PADDR[upper:2], OR-reduce for "any hit"Small for a few regs; grows linearly with register countPoor for large/sparse maps (one comparator each)One-hot by construction; decode_err = ~(|sel) is trivial
unique case on word offsetA single case on the word-aligned offset; default = errorSynthesises to compact priority/one-hot logic; tool warns on overlapGood for a handful of named registers (most slaves)One-hot by unique; default arm gives decode_err cleanly
Range-table / base+limitCompare offset against [base, base+size) rangesLarger compares (two bounds each); risk of overlap if hand-codedGood 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 arraySmallest for a dense, contiguous RW blockExcellent 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, PADDR is 0x6 (or 0x5, 0x7) — a byte within the STATUS word at 0x4 — and every sel_* line is low while decode_err is high, even though PSEL to this slave is asserted and the address is plainly inside the window. On the aliasing variant, the wrong sel_* goes high for an address whose low bits happen to match another register's full value.
  • Root cause: the comparator tested paddr == OFF_STATUS against the full PADDR instead of the word-aligned offset. Because OFF_STATUS = 0x4 includes zero low bits, only PADDR == 0x4 exactly matches; 0x5/0x6/0x7 — the same register, different byte lane — fail every comparator and fall through to the decode_err default. 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 the STATUS word decodes to sel_status, and only a genuinely unmapped word reaches the decode_err default.
  • 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 PADDR the compare uses. If PADDR[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 same sel_* asserts for both; if it doesn't, the decoder is comparing the byte bits.
Two stacked panels: the top green panel shows a correct decoder comparing only the word-aligned offset and asserting exactly one select line for a STATUS access; the bottom red panel shows a full-PADDR compare that misses a legal byte-2 access and an overlapping-range compare that drives two select lines hot at once, with a shared $onehot0 assertion that detects both failures.
Figure 2 — correct word-aligned one-hot decode versus the two classic decode bugs. Top (correct, green): the decoder compares only the word-aligned offset PADDR[upper:2], so accessing STATUS at byte offset 0x4 asserts exactly one line, sel_status, regardless of the byte-lane bits, and $onehot0 holds. Bottom (bugs, red): on the left, the decoder compared the full PADDR including PADDR[1:0], so a legal byte-2 access at 0x6 matches no comparator and the access vanishes (or aliases); on the right, two comparators used overlapping ranges, so one offset matched two entries and sel_ctrl and sel_iflag both went hot, writing two registers on one transfer. The shared $onehot0 assertion catches both — zero hot on a legal access, or two hot at once.

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 (proves decode_err fires 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_err asserts with no sel_* hot; drive sub-word/byte-offset accesses to mapped words and check the correct sel_* asserts with decode_err low. A directed test sweep over the whole window — every word offset from 0 to 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

  1. Slice the address. For a 4 KB slave (ADDR_W = 12) with registers at 0x0, 0x4, 0x8, 0xC, state which bits of PADDR the decoder compares and which it ignores, and why.
  2. Write the decoder. From memory, write the always_comb unique case that produces sel_ctrl/sel_status/sel_iflag and decode_err, with the default arm as the decode error. Explain why the default must not select register 0.
  3. Find the alias. Given a decoder that compares paddr == 12'h008 and a CPU access at PADDR = 0x00A, state what happens to the select and decode_err, identify the bug, and give the one-line fix.
  4. 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.
  5. 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 PADDR that 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, and PSLVERR all plug into.
  • Compare the word-aligned offset PADDR[upper:2], never the full PADDR. 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_err and select nothing — never fall through to register 0. This chapter produces the flag; 11.6 turns it into PSLVERR.
  • 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 $onehot0 and "select xor error."
  • Decode is combinational and unregistered because PADDR is stable across SETUP and ACCESS — decode now, act at the commit, which is timed downstream by PWRITE/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.