AMBA APB · Module 12
Address-Allocation Strategy
Laying out a register map's address space — word-aligned offsets, power-of-two decode windows, region grouping, and reserved growth headroom so the map can add registers in later revisions without moving existing ones and breaking shipped drivers.
The previous chapter established the register map as a designed hardware/software contract, and noted in passing that registers are word-aligned. This chapter goes deep on the part it deferred: address allocation — where in the peripheral's window each register actually sits. The single idea to carry: allocation is a planning decision that trades density against forward-compatibility and decode simplicity, and the right plan leaves aligned, reserved headroom so the map can grow without moving anything. Pack offsets tightly and you save a few bytes of address space; you also guarantee that the day you add a register, you must move an existing one — and moving a register breaks every shipped driver. Allocation is how you avoid that, decided up front.
1. Problem statement
The problem is assigning every register a fixed offset inside the peripheral's address window — choosing the stride, the window size, the grouping of related registers, and crucially the gaps between them — such that software can address each register simply, the slave decodes cheaply, and the map can be extended in future silicon without disturbing what already shipped.
A peripheral occupies a contiguous block of the SoC address space — its window — and every register lives at some byte offset within it. That sounds like bookkeeping, but each placement choice is a contract decision with downstream consequences:
- Stride and alignment. On a 32-bit bus the natural access is a 4-byte word, so registers are allocated on a 4-byte stride at word-aligned offsets (
0x00, 0x04, 0x08, …), the low two address bits always zero. This is what lets software address a register asbase + offsetand lets the decoder ignorepaddr[1:0]entirely. - Window size and decode. The window is sized as a power of two (1 KB, 4 KB, …) and its base aligned to that size, so the slave's "is this address mine?" test collapses to a single masked compare of the upper address bits — no range comparator, no adder.
- Grouping and headroom. Related registers are clustered into regions, regions sit on round boundaries, and deliberate gaps are left between and after them — reserved address space that holds nothing today but lets tomorrow's registers slot in without renumbering. This is the forward-compat reason to not pack offsets tightly.
So the job is not "number the registers 0, 1, 2" — it is to author an address layout that is aligned, power-of-two clean, region-grouped, and grown-with-headroom, so it is cheap to decode now and safe to extend later.
2. Why previous knowledge is insufficient
Chapter 12.1 — CSR Design Fundamentals framed the map as a contract and told you registers are word-aligned, but it explicitly deferred how the address space is laid out to this chapter. And Chapter 11.2 — the address decoder built the RTL that reads an address and selects a register — it assumed a layout existed. Neither chapter answers the planning question: given a set of registers, what offsets do you give them, and what do you leave empty?
- 12.1 said "word-aligned"; this chapter says why, and what else. Alignment is necessary but nowhere near sufficient. A perfectly word-aligned map can still be packed so tightly that it cannot grow, or sized to a non-power-of-two window that makes decode expensive or aliases a neighbour. Allocation is alignment plus window sizing plus grouping plus headroom.
- 11.2 implemented a decoder for a given map; this chapter shapes the map so the decoder is cheap. The decoder chapter took the address layout as input. Here you learn that a power-of-two window with an aligned base and a word stride turns the decode into a mask-and-slice rather than a comparator tree — the layout is upstream of, and determines the cost of, the decode. We reference how a clean allocation eases decode; we do not re-teach the decoder RTL.
- The layout outlives the silicon — so headroom is a forward-compat decision. Offsets, like the contract they belong to, are frozen once shipped: drivers hard-code them. The chapter that follows — 12.3 — Reserved Fields — reserves bits within a register for growth; this chapter reserves address space between registers for growth. Both serve the same goal (additive evolution), at different granularities. The map-wide discipline that ties it together is covered in 12.8 — Register-Map Best Practices.
So the model to add is the address-space layout: stride, window size, regions, and reserved gaps — the spatial plan the decoder reads and the contract grows into.
3. Mental model
The model: a peripheral window is a building, and registers are rooms on numbered, evenly-spaced doors — with empty rooms left between departments on purpose. The door numbers (offsets) advance by a fixed stride so anyone can find a room by arithmetic. Related rooms are grouped into a department (a region). And the architect leaves vacant, reserved rooms between departments before the building opens, because adding a room later to a fully-packed corridor would mean renumbering every door — and everyone's printed directions (the drivers) would now point at the wrong room.
Three refinements make it precise:
- Stride = word, alignment = free low bits. On a 32-bit bus the stride is 4 bytes, so offsets are
0x00, 0x04, 0x08, …andoffset[1:0]is always00. Those two free bits are whybase + index*4works and why the decoder can ignore them. A 64-bit register pair (DATA_LO/DATA_HI) is placed on an 8-byte boundary so a single wider access spans it cleanly. - Window = a power-of-two block on an aligned base. Size the window to a power of two and align its base to that size. Then the slave-hit test is
(paddr & ~(SIZE-1)) == BASE— one mask and one compare of the upper bits — and the register select is just the middle bitspaddr[hi:2]. A non-power-of-two window forces a range compare and risks the slack between blocks aliasing or being claimed by a neighbour. - Regions + headroom = a plan for growth. Cluster related registers into regions on round boundaries (control at
0x000, data at0x080, info/version near the top), and leave reserved, read-as-zero gaps between and after them. Density is deliberately traded for the ability to drop a new register into reserved space later without moving a single shipped offset.
4. Real SoC implementation
In practice the layout is captured as a set of parameterised offsets — ideally generated from the same machine-readable map (IP-XACT / SystemRDL) that produces the RTL, the C header, and the docs. The key moves are: a power-of-two window, word-aligned offsets, regions on round boundaries, explicit reserved gaps, and a decode that exploits the alignment.
// ---------------------------------------------------------------------------
// Peripheral address map — allocation plan (one source of truth).
// Window is a POWER OF TWO so decode is a mask, and BASE is aligned to it.
// Offsets are WORD-ALIGNED (4-byte stride, 32-bit bus): offset[1:0] == 2'b00.
// Regions sit on ROUND boundaries with RESERVED headroom between them, so a
// future register drops into reserved space without moving anything shipped.
// ---------------------------------------------------------------------------
localparam int unsigned WIN_LOG2 = 12; // 4 KB window
localparam int unsigned WIN_SIZE = 1 << WIN_LOG2; // 0x1000 (power of two)
localparam logic [31:0] BASE = 32'h4001_0000; // aligned to WIN_SIZE
// --- CONTROL region: 0x000 .. 0x07F (control cluster + growth) -------------
localparam logic [11:0] CTRL = 12'h000; // RW
localparam logic [11:0] STATUS = 12'h004; // RO
localparam logic [11:0] IFLAG = 12'h008; // W1C
localparam logic [11:0] CMD = 12'h00C; // WO
// 0x010 .. 0x07C RESERVED -> read-as-zero, write-ignored (headroom)
// --- DATA region: starts on a round 0x080 boundary -------------------------
localparam logic [11:0] DATA_LO = 12'h080; // [31:0] 8-byte aligned
localparam logic [11:0] DATA_HI = 12'h084; // [63:32] pair spans 64b
// 0x088 .. 0xFF8 RESERVED -> sparse on purpose (density traded)
// --- INFO region: top of window -------------------------------------------
localparam logic [11:0] VERSION = 12'hFFC; // RO capability/discovery
// --- Decode BENEFITS from the allocation -----------------------------------
// Window hit: one masked compare of the upper bits (power-of-two + aligned).
wire win_hit = (paddr & ~(WIN_SIZE - 1)) == BASE; // no range comparator
// Register select: word stride means we drop paddr[1:0] and slice the middle.
wire [11:0] off = {paddr[11:2], 2'b00}; // re-align defensively
always_comb begin
prdata = 32'h0; // default: read-as-zero
if (win_hit) unique case (off)
CTRL : prdata = ctrl;
STATUS : prdata = status;
IFLAG : prdata = iflag;
CMD : prdata = 32'h0; // WO: defined read
DATA_LO : prdata = data[31:0];
DATA_HI : prdata = data[63:32];
VERSION : prdata = HW_VERSION_ID; // constant, RO
default : prdata = 32'h0; // RESERVED reads as 0
endcase
endTwo facts make this allocation and not just "naming offsets." First, the gaps are load-bearing. 0x010 .. 0x07C and 0x088 .. 0xFF8 hold nothing today, but they are the reason a v2 of this peripheral can add a control register at 0x010 or a second data word at 0x088 without touching DATA_LO, DATA_HI, or VERSION — the offsets every shipped driver hard-coded. Second, the power-of-two window and word stride are what make the decode a mask and a slice (win_hit is a single masked compare; off is the middle bits) instead of an arithmetic range check — the allocation chooses the decode cost. The default: read-as-zero over the whole reserved space is part of the contract too: it gives the headroom a defined behaviour so software that strays into it sees zeros, not garbage.
5. Engineering tradeoffs
Allocation strategy is one axis with several positions, each trading density against growth headroom, decode cost, and forward-compatibility.
| Allocation strategy | Density (space used) | Growth headroom | Decode cost | Forward-compat | When to choose |
|---|---|---|---|---|---|
| Tight-packed (contiguous offsets, no gaps) | Highest — every word used | None — adding a register forces a move | Cheap if window is still power-of-two | Poor — any addition breaks shipped drivers | Truly frozen, never-revised IP; or extreme address-space pressure |
| Aligned-with-headroom (word-aligned, reserved gaps between/after) | Moderate — gaps are vacant | Good — new registers drop into reserved slots | Cheap (mask + slice) | Strong — additive growth, nothing moves | Default for any IP expected to evolve |
| Region-grouped (clusters on round boundaries, headroom per region) | Lower — round-boundary rounding wastes some | Excellent — each region grows independently | Cheap, and sub-decode is clean per region | Strong — and groups stay coherent across revisions | Larger peripherals with distinct functional blocks (control / data / debug) |
| Power-of-two-blocks (each region a power-of-two sub-window) | Lowest — heavy rounding | Excellent — block-level growth | Cheapest — hierarchical mask decode | Strong — block boundaries never collide | Big IP / sub-system where regions may become their own decode targets |
The throughline: density is the thing you can almost always afford to spend. Address space inside a peripheral window is cheap; a broken shipped driver is not. Word-align by default for clean addressing and decode, size the window to a power of two so the decode is a mask, group related registers and put them on round boundaries, and leave reserved headroom proportional to how much the block is likely to grow. Only a genuinely frozen IP or real address-space starvation justifies tight-packing — and even then you size the window to a power of two.
6. Common RTL mistakes
7. Debugging scenario
The signature allocation bug is a forced register move on a chip revision: the original map packed offsets with no headroom, so adding one register meant relocating another — silently breaking every driver that hard-coded the moved offset. The defect is in the v1 allocation plan, not in any RTL.
- Observed symptom: after a "minor" silicon revision that added one new configuration register (
CFG2), every previously-shipped driver suddenly misbehaves onDATA: reads return wrong values, writes appear to corrupt configuration. The new firmware (built against v2) works; only field units running v1 drivers fail — a fleet-wide regression from an apparently additive change. - Waveform clue: on the bus, a v1 driver's access to
*(base + 0x0C)— which it believes isDATA— lands on the address that v2 now assigns toCFG2. The transaction completes cleanly (PREADY/PSLVERRlook normal), but it is hitting the wrong register: a write meant forDATAis reprogrammingCFG2, and a read meant forDATAreturnsCFG2's contents. The decoder is doing exactly what the v2 map told it to. - Root cause: the v1 map allocated registers at contiguous offsets (
CTRL 0x00, STATUS 0x04, CFG 0x08, DATA 0x0C) with zero reserved headroom. To insertCFG2, the only aligned slot was0x0C, soDATAwas shoved to0x10. Moving a shipped offset is a breaking change to the published contract; the v1 drivers still address0x0Cand now hitCFG2. The bug was designed in at v1 by packing tightly — an allocation decision, not a logic error. - Correct RTL: the fix belongs in the allocation plan, made in v1: reserve aligned address space after the control cluster so a new register drops into reserved space and
DATAnever moves. The headroom layout makes the v2 change additive:
// v1 plan with headroom — DATA placed past a reserved gap on a round boundary.
localparam logic [11:0] CTRL = 12'h000; // RW
localparam logic [11:0] STATUS = 12'h004; // RO
localparam logic [11:0] CFG = 12'h008; // RW
// 0x00C .. 0x01C RESERVED (read-as-zero) -> growth headroom
localparam logic [11:0] DATA = 12'h020; // on a round boundary, UNMOVED
// v2 simply consumes one reserved slot — every v1 driver still works:
localparam logic [11:0] CFG2 = 12'h00C; // dropped into reserved space
// DATA stays at 0x020; 0x010..0x01C still reserved for the next add.- Verification assertion: prove that every accepted access stays inside the window and is word-aligned, and lock the offsets of shipped registers as constants so a future edit that moves one fails a check rather than a customer's chip:
// Accesses must stay inside the power-of-two window and be word-aligned.
ap_in_window: assert property (@(posedge pclk) disable iff (!presetn)
(psel && penable) |-> ((paddr & ~(WIN_SIZE-1)) == BASE) && (paddr[1:0] == 2'b00));
// Contract guard: a shipped offset must never silently change across revisions.
ap_data_offset_frozen: assert property (@(posedge pclk) (DATA == 12'h020));- Debug habit: when an additive revision breaks shipped software, do not start in the RTL or the decoder — diff the address map across versions and look for a moved offset. A register that changed offset between revisions is the smoking gun, and the real defect is the v1 allocation that left no headroom to add in place. The cure is always upstream: reserve aligned address space in the original plan.
8. Verification perspective
An address layout is verified for the properties that make it a layout — alignment, window containment, no aliasing, defined reserved behaviour, and frozen-offset stability across revisions — not just "every register is reachable."
- Window-boundary coverage. Cover accesses at the first offset (
BASE+0x000), the last legal offset (BASE+SIZE-4), one word past the end (BASE+SIZE), and one word before the base. Prove that in-window aligned accesses are accepted and out-of-window accesses are not claimed by this slave — the power-of-two window's edges are exactly where decode bugs hide. - Alignment checks. Assert that every accepted transaction has
paddr[1:0] == 2'b00, and for any 64-bit pair, that…_LOis 8-byte aligned. Exercise a deliberately misaligned address and confirm it is rejected or correctly handled per spec, never silently routed to a half-register. - No-alias assertion. Prove that no two registers share a decoded address and that the reserved gaps decode to nothing (read-as-zero, write-ignored) — never to a real register. This is the check that would have caught a non-power-of-two window aliasing the slack between blocks, and it is the structural guarantee that the headroom is truly free.
- Growth-region reserved checks. Functionally cover every reserved offset being read (must return zero) and written (must be ignored, leaving all real registers
$stable). This proves the headroom has defined behaviour today, which is what makes it a clean drop-in target for a future register — and it lets you later replace a reserved-reads-zero check with a real-register check when the slot is consumed. - Frozen-offset guards. Encode each shipped register's offset as a constant and assert it, so a refactor that accidentally moves a register fails in CI rather than in the field. The verification environment, generated from the same map source, is the natural place to make "offsets are part of the frozen contract" a checkable property.
The point: verify the spatial contract — aligned, contained, non-aliasing, defined-in-the-gaps, and stable-across-revisions — so the layout software depends on is provably what the silicon decodes.
9. Interview discussion
"How do you lay out a peripheral's register map in its address space?" is a strong design question because a weak answer says "put them at 0, 4, 8…" while a strong one frames allocation as a forward-compatibility and decode-cost decision.
Lead with the framing: address allocation trades density against forward-compatibility and decode simplicity, and the right plan leaves aligned, reserved headroom so the map can grow without moving what shipped. Then name the four moves: word-aligned stride (4-byte on a 32-bit bus, low two bits zero, enabling base + index*4 addressing and letting the decoder ignore paddr[1:0]); power-of-two window with an aligned base (so the slave-hit test is a single masked compare of the upper bits and the register select is a middle-bit slice — a mask-and-slice, not a comparator tree); region grouping on round boundaries (control / data / info clusters, each with its own growth room); and reserved headroom between and after registers (read-as-zero gaps that let a future register drop in additively). Deliver the depth that signals real experience: the classic failure is a chip revision that adds a register and, because the original map packed tightly, has to move an existing one — breaking every shipped driver; that is why density is the cheap thing to spend and headroom is the discipline. Mention that a 64-bit pair goes on an 8-byte boundary so one wide access spans it, and that reserved address space (this chapter) and reserved fields (the next) are the same growth discipline at different granularities. Closing with "I reserve aligned space in the original plan because the offset is part of the frozen contract" shows you understand the map is a versioned API.
10. Practice
- Allocate with headroom. Given CTRL, STATUS, IFLAG, CMD, a 64-bit DATA pair, and a VERSION register, assign word-aligned offsets inside a 4 KB window, grouping them into regions on round boundaries and reserving explicit growth gaps. State each reserved range.
- Size the window. For a peripheral that today needs 6 registers but may grow to ~30, choose a power-of-two window size and justify it against decode cost and headroom.
- Derive the decode. For your window base and size, write the single masked-compare
win_hitexpression and the register-select slice. Show why a non-power-of-two size would not allow this. - Place the 64-bit pair. Put DATA_LO/DATA_HI so a single 64-bit access spans them, and explain why an odd 4-byte offset would break that.
- Find the move. Given a v1 and v2 map where one register changed offset, identify the allocation decision that forced the move and the v1 headroom plan that would have prevented it.
11. Q&A
12. Key takeaways
- Address allocation is a planning decision that trades density against forward-compatibility and decode simplicity — and density is the cheap thing to spend.
- Word-align on a 4-byte stride (32-bit bus): offsets advance by 4,
offset[1:0]is always zero, enablingbase + index*4addressing and letting the decoder ignore the low two bits. Place 64-bit pairs on 8-byte boundaries. - Size the window to a power of two on an aligned base so the slave-hit test is a single masked compare of the upper bits and the register select is a middle-bit slice — a mask-and-slice, not a comparator tree; non-power-of-two windows force range compares and risk aliasing.
- Group related registers into regions on round boundaries and leave reserved, read-as-zero headroom between and after them, so a future register drops into reserved space without moving any shipped offset.
- Tight-packing guarantees a register move on the next revision, which breaks every shipped driver — reserved address space (this chapter) is the same growth discipline as reserved fields, at the address granularity.
- Verify the spatial contract — alignment, window containment, no aliasing, defined-in-the-gaps behaviour, and frozen offsets asserted as constants — so the layout software depends on is provably what the silicon decodes.