Skip to content

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 allocationwhere 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 as base + offset and lets the decoder ignore paddr[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, … and offset[1:0] is always 00. Those two free bits are why base + index*4 works 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 bits paddr[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 at 0x080, 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.
A peripheral address window of size 0x1000 with word-aligned registers grouped into CONTROL, DATA, and INFO regions on round boundaries, amber reserved-for-growth gaps between them, an 8-byte-aligned 64-bit DATA_LO/DATA_HI pair, and a side panel showing the window-hit test as a single masked compare and the register select as a middle-bit slice.
Figure 1 — an address-allocation layout inside a power-of-two window. The window is 0x1000 (4 KB) with a base aligned to that size, so the low 12 bits are the offset and the upper bits select the slave. Registers are allocated on a 4-byte word stride at aligned offsets, grouped into regions: a CONTROL region (CTRL/STATUS/IFLAG/CMD at 0x000–0x00C), a reserved-for-growth gap (0x010–0x07C, amber, read-as-zero), a DATA region starting on a round 0x080 boundary holding an 8-byte-aligned 64-bit pair DATA_LO/DATA_HI, more reserved slack, and an INFO region with a VERSION/ID register near the top of the window. The right panel shows the decode benefit: because the window is a power of two with an aligned base, the hit test is a single masked compare of the upper bits and the register select is a slice of the middle bits — a mask-and-slice, not a comparator tree. The figure frames allocation as a planning decision that trades density for forward-compatibility and cheap decode.

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.

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

Two 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 strategyDensity (space used)Growth headroomDecode costForward-compatWhen to choose
Tight-packed (contiguous offsets, no gaps)Highest — every word usedNone — adding a register forces a moveCheap if window is still power-of-twoPoor — any addition breaks shipped driversTruly frozen, never-revised IP; or extreme address-space pressure
Aligned-with-headroom (word-aligned, reserved gaps between/after)Moderate — gaps are vacantGood — new registers drop into reserved slotsCheap (mask + slice)Strong — additive growth, nothing movesDefault for any IP expected to evolve
Region-grouped (clusters on round boundaries, headroom per region)Lower — round-boundary rounding wastes someExcellent — each region grows independentlyCheap, and sub-decode is clean per regionStrong — and groups stay coherent across revisionsLarger peripherals with distinct functional blocks (control / data / debug)
Power-of-two-blocks (each region a power-of-two sub-window)Lowest — heavy roundingExcellent — block-level growthCheapest — hierarchical mask decodeStrong — block boundaries never collideBig 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 on DATA: 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 is DATA — lands on the address that v2 now assigns to CFG2. The transaction completes cleanly (PREADY/PSLVERR look normal), but it is hitting the wrong register: a write meant for DATA is reprogramming CFG2, and a read meant for DATA returns CFG2'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 insert CFG2, the only aligned slot was 0x0C, so DATA was shoved to 0x10. Moving a shipped offset is a breaking change to the published contract; the v1 drivers still address 0x0C and now hit CFG2. 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 DATA never moves. The headroom layout makes the v2 change additive:
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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:
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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.
Two-case comparison: on the left, a tight-packed v1 map forces DATA to move from 0x0C to 0x10 when CFG2 is inserted, breaking drivers that hard-coded 0x0C, shown in red; on the right, a headroom map with a reserved gap lets CFG2 drop into 0x0C while DATA stays at 0x20, keeping every driver working, shown in green.
Figure 2 — a forced register move from tight-packing, versus a headroom map that grows in place. Left (bug, red): the v1 map packed CTRL/STATUS/CFG/DATA contiguously at 0x00–0x0C with no gap, so inserting CFG2 in v2 shoved DATA from 0x0C to 0x10; every shipped driver still addresses 0x0C and now hits CFG2 instead of DATA — a silent, fleet-wide break caused by an allocation decision in v1, not an RTL bug. Right (correct, green): the v1 map left a reserved gap (0x0C–0x1C, amber, read-as-zero) after the control cluster and placed DATA on a round 0x20 boundary; in v2, CFG2 drops into the reserved 0x0C slot, DATA stays at 0x20, and every existing driver keeps working. The figure shows the fix is to reserve aligned address space up front so the contract evolves additively.

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 …_LO is 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

  1. 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.
  2. 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.
  3. Derive the decode. For your window base and size, write the single masked-compare win_hit expression and the register-select slice. Show why a non-power-of-two size would not allow this.
  4. 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.
  5. 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, enabling base + index*4 addressing 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.