Skip to content
VLSI Mentor

Wishbone · Module 12

Sparse Address Spaces

260 implemented words inside 3072 reserved ones, inside a four-billion-word space — and which refusals come from the map rather than the target.

Chapter 12.6 gave every address an owner. Owning an address turns out to be much weaker than having hardware at it.

Three quarters of RAM's window has no storage. 1022 of each peripheral's 1024 words have no register. Every target in this SoC reserves far more than it implements, and that is the ordinary case rather than a flaw in the example.

What does a decoder owe to space that is reserved and empty?

A diagram of three nested scales. The outermost is the addressable space, four billion two hundred and ninety-four million words, decided by the address width. Inside it sit the three windows the map reserves, totalling three thousand and seventy-two words, decided by the system integrator. Inside those sit the two hundred and sixty words that hardware actually implements, decided by each target: two hundred and fifty-six words of RAM storage, two GPIO registers and two timer registers. Each scale is smaller than the one containing it by several orders of magnitude, and the space between them is refused rather than unused.address space4,294,967,296 wordswindows the mapreserves3,072 wordsimplemented260 wordsthe address widthfixed by the busthe integratorfixed by the mapeach targetfixed by its RTLdefault responderanswers everythingoutsidethe target itselfanswers its own holes12

Read the middle row left to right as three decisions made by three different parties, and the top row as who answers for the space each one leaves behind. ERR arrives from a different place depending on which boundary the address fell outside — which is Section 5.

1. Sparse Is the Default, Not a Special Case

A 32-bit address space holds four billion words. No system implements four billion words. Every real map is overwhelmingly holes, and the interesting question is never how to avoid them — it is what answers for them.

Holes come in two kinds, and the difference runs through the whole chapter:

Between windows. 0x0000_1000 through 0x3FFF_FFFF belongs to no target. The decoder reports unmapped and the default responder answers. The interconnect knows about these.

Inside a window. 0x0000_0400 is inside RAM's window and past RAM's storage. The decoder reports RAM — correctly — and the RAM refuses it. The interconnect cannot know about these, because it does not know any target's depth.

Both return ERR to the master. They are not the same event and they do not lead to the same fix, which is why Section 4's sweep prints the responder alongside the class.

2. Why Maps Are Sparse on Purpose

Alignment forces it. A mask decode needs every window to be a power of two on a natural boundary (Chapter 12.3). A peripheral with two registers still gets a 4 KiB window, because the alternative is a map the cheap decoder cannot handle.

Growth wants it. GPIO's 1022 spare words are the difference between adding a register and relocating a peripheral — and relocating one invalidates every binary that ever used it.

Grouping asks for it. Peripherals at 0x4000_xxxx and memory at 0x0000_xxxx lets a reader classify an address at a glance. Keeping that structure means leaving the space between groups empty.

And the space is free. In 32 bits, reserving a gigabyte costs nothing. In a 16-bit space none of the above applies — which is why this is an argument about a particular address width and not a universal principle.

3. The Over-Decoding Shortcut

The dangerous simplification is short enough to look obviously correct:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  // "anything in 0x4000_xxxx is a peripheral"
  sel_periph = (adr[29:20] == 10'h100);

It is cheaper than comparing three windows and it is wrong in a specific way: it legalises the gaps. The reserved region at 0x4000_2000, which Chapter 12.2's map set aside for a future peripheral, now decodes as a peripheral. So does everything up to 0x4000_FFFF.

What happens next depends on what it was routed to. If it reaches a target, that target sees an offset it does not implement and refuses — wasteful but honest. If the coarse decode is paired with a coarse offset, it reaches a register, and the address that was supposed to be empty now aliases onto a real one.

This is Chapter 12.5's alias, moved up a level. There, an over-wide window and a truncated index folded 768 addresses onto real storage. Here, an over-wide decode folds reserved regions onto real peripherals. The same mistake — treating reserved space as implemented space — at two different granularities.

4. Simulation — SIM J: The Whole Map, Swept

The system under test, assembled from the six chapters' parts. This is what every simulation in Module 12 has been driving.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ─────────────────────────────────────────────────────────────────────────
// wb_decode_soc — the running system, assembled.
//
//   master  ->  wb_range_decode  ->  wb_soc_router  ->  { RAM, GPIO, TIMER }
//                                                    \-> wb_default_slave
//
// THE CANONICAL ADDRESS MAP (byte units, as the document writes it):
//
//   window (what the map reserves)          implemented (what exists)
//   RAM     0x0000_0000 .. 0x0000_0FFF  4 KiB   256 words   = 1 KiB
//   GPIO    0x4000_0000 .. 0x4000_0FFF  4 KiB   2 registers
//   TIMER   0x4000_1000 .. 0x4000_1FFF  4 KiB   2 registers
//   everything else                             unmapped -> default -> ERR
//
// The same windows in WORD units, which is what ADR actually carries:
//
//   RAM     0x0000_0000 .. 0x0000_03FF   (words 0..255 implemented)
//   GPIO    0x1000_0000 .. 0x1000_03FF   (words 0..1 implemented)
//   TIMER   0x1000_0400 .. 0x1000_07FF   (words 0..1 implemented)
//
// THE TWO COLUMNS ARE DIFFERENT NUMBERS FOR EVERY TARGET, deliberately.
// A window is space the integrator has reserved; implemented storage is what
// the target actually built. Nothing requires them to match, most real maps
// reserve more than is built, and assuming they match is how an alias gets
// created (Chapter 12.5) and how a hole gets legalised (Chapter 12.7).
//
// Decoding is therefore TWO decisions, not one: the interconnect decides
// which window an address falls in, and the target decides whether anything
// is implemented at that offset. The second decision cannot be delegated
// upward — only the target knows what it built.
//
// The gap between RAM and GPIO is enormous, and 0x4000_2000 upward is left
// free on purpose (Chapter 4.3 placed a third peripheral there). Sparse is
// the normal shape of an address map, not a defect in it.
//
// GEN_DFLT / GEN_ALIAS select the architecture under test, so that every
// comparison in Module 12 is between two systems that differ in exactly one
// documented way.
// ─────────────────────────────────────────────────────────────────────────
module wb_decode_soc #(
  parameter int unsigned BYTE_AW   = 32,
  parameter int unsigned DW        = 32,
  parameter int unsigned RAM_DEPTH = 256,
  parameter bit          GEN_DFLT  = 1'b1,   // 0 -> no default responder
  parameter bit          GEN_ALIAS = 1'b0,   // 1 -> truncating RAM
  parameter bit          GEN_PARK  = 1'b0,   // 1 -> TIMER parks its read data
  localparam int unsigned WAW = BYTE_AW - $clog2(DW / 8)
) (
  input  logic           clk_i,
  input  logic           rst_i,
  input  logic           cyc_i,
  input  logic           stb_i,
  input  logic           we_i,
  input  logic [WAW-1:0] adr_i,
  input  logic [DW-1:0]  dat_i,
  output logic [DW-1:0]  dat_o,
  output logic           ack_o,
  output logic           err_o,
  // observation only — not part of any Wishbone interface
  output logic [2:0]     sel_o,
  output logic           unmapped_o,
  output logic [WAW-1:0] offset_o,
  output logic [7:0]     ram_index_o,
  output logic           ram_in_range_o,
  output logic [DW-1:0]  gpio_out_o,
  output logic [DW-1:0]  timer_load_o
);
  localparam logic [95:0] MAP_BASE =
      { 32'h4000_1000, 32'h4000_0000, 32'h0000_0000 };
  localparam logic [95:0] MAP_SIZE =
      { 32'h0000_1000, 32'h0000_1000, 32'h0000_1000 };

  logic [2:0]     sel;
  logic           unmapped;
  logic [WAW-1:0] offset;

  wb_range_decode #(.BYTE_AW(BYTE_AW), .DW(DW), .NS(3),
                    .BASE_FLAT(MAP_BASE), .SIZE_FLAT(MAP_SIZE)) u_dec (
    .adr_i(adr_i), .sel_o(sel), .offset_o(offset), .unmapped_o(unmapped));

  logic rc, rs, gc, gs, tc, ts, dc, ds, twe;
  logic [WAW-1:0] tadr;
  logic [DW-1:0]  tdat;
  logic [DW-1:0]  rdat, gdat, tdat_r, ddat;
  logic rack, rerr, gack, gerr, tack, terr, dack, derr;

  wb_soc_router #(.BYTE_AW(BYTE_AW), .DW(DW), .HAS_DFLT(GEN_DFLT)) u_rtr (
    .cyc_i(cyc_i), .stb_i(stb_i), .we_i(we_i), .adr_i(adr_i), .dat_i(dat_i),
    .dat_o(dat_o), .ack_o(ack_o), .err_o(err_o),
    .sel_i(sel), .unmapped_i(unmapped), .offset_i(offset),
    .ram_cyc_o(rc),   .ram_stb_o(rs),   .ram_dat_i(rdat),
    .ram_ack_i(rack), .ram_err_i(rerr),
    .gpio_cyc_o(gc),  .gpio_stb_o(gs),  .gpio_dat_i(gdat),
    .gpio_ack_i(gack), .gpio_err_i(gerr),
    .timer_cyc_o(tc), .timer_stb_o(ts), .timer_dat_i(tdat_r),
    .timer_ack_i(tack), .timer_err_i(terr),
    .dflt_cyc_o(dc),  .dflt_stb_o(ds),  .dflt_dat_i(ddat),
    .dflt_ack_i(dack), .dflt_err_i(derr),
    .tgt_we_o(twe), .tgt_adr_o(tadr), .tgt_dat_o(tdat));

  // RAM's local offset is 10 bits wide here only because that is the widest
  // window in the map; the RAM itself uses 8 of them and range-checks the rest.
  logic [7:0] ram_idx;
  logic       ram_inr;
  generate
    if (GEN_ALIAS) begin : g_alias_ram
      wb_ram_alias_slave #(.OFF_AW(10), .DW(DW), .DEPTH(RAM_DEPTH)) u_ram (
        .clk_i(clk_i), .rst_i(rst_i), .cyc_i(rc), .stb_i(rs), .we_i(twe),
        .adr_i(tadr[9:0]), .dat_i(tdat), .dat_o(rdat), .ack_o(rack),
        .err_o(rerr), .index_o(ram_idx), .in_range_o(ram_inr));
    end else begin : g_good_ram
      wb_ram_slave #(.OFF_AW(10), .DW(DW), .DEPTH(RAM_DEPTH)) u_ram (
        .clk_i(clk_i), .rst_i(rst_i), .cyc_i(rc), .stb_i(rs), .we_i(twe),
        .adr_i(tadr[9:0]), .dat_i(tdat), .dat_o(rdat), .ack_o(rack),
        .err_o(rerr), .index_o(ram_idx), .in_range_o(ram_inr));
    end
  endgenerate

  wb_gpio_slave #(.OFF_AW(10), .DW(DW)) u_gpio (
    .clk_i(clk_i), .rst_i(rst_i), .cyc_i(gc), .stb_i(gs), .we_i(twe),
    .adr_i(tadr[9:0]), .dat_i(tdat), .dat_o(gdat), .ack_o(gack),
    .err_o(gerr), .out_q_o(gpio_out_o));

  generate
    if (GEN_PARK) begin : g_park_timer
      wb_timer_parking_slave #(.OFF_AW(10), .DW(DW)) u_timer (
        .clk_i(clk_i), .rst_i(rst_i), .cyc_i(tc), .stb_i(ts), .we_i(twe),
        .adr_i(tadr[9:0]), .dat_i(tdat), .dat_o(tdat_r), .ack_o(tack),
        .err_o(terr), .load_q_o(timer_load_o));
    end else begin : g_gated_timer
      wb_timer_slave #(.OFF_AW(10), .DW(DW)) u_timer (
        .clk_i(clk_i), .rst_i(rst_i), .cyc_i(tc), .stb_i(ts), .we_i(twe),
        .adr_i(tadr[9:0]), .dat_i(tdat), .dat_o(tdat_r), .ack_o(tack),
        .err_o(terr), .load_q_o(timer_load_o));
    end
  endgenerate

  wb_default_slave #(.DW(DW)) u_dflt (
    .cyc_i(dc), .stb_i(ds), .dat_o(ddat), .ack_o(dack), .err_o(derr));

  assign sel_o          = sel;
  assign unmapped_o     = unmapped;
  assign offset_o       = offset;
  assign ram_index_o    = ram_idx;
  assign ram_in_range_o = ram_inr;
endmodule

Sixteen probes across every kind of address the map contains — implemented words, holes inside windows, boundaries, the gaps between windows, the reserved region, and the top of the space. Every classification is checked against the map, not merely printed.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  === SIM J - a sweep across the whole map ===
    three windows of 4 KiB each, in a 4 GiB space. Every window
    contains more reserved space than implemented hardware.

    byte adr    target  offset class answered by
    0x00000000  RAM     0x000  ACK  RAM      RAM word 0
    0x000003fc  RAM     0x0ff  ACK  RAM      RAM word 255, last
    0x00000400  RAM     0x100  ERR  RAM      hole in RAM window
    0x00000ffc  RAM     0x3ff  ERR  RAM      hole, window's last
    0x00001000  -         -    ERR  default  past the RAM window
    0x10000000  -         -    ERR  default  the great gap
    0x3ffffffc  -         -    ERR  default  one word below GPIO
    0x40000000  GPIO    0x000  ACK  GPIO     GPIO DIR
    0x40000004  GPIO    0x001  ACK  GPIO     GPIO OUT
    0x40000008  GPIO    0x002  ERR  GPIO     hole in GPIO window
    0x40000ffc  GPIO    0x3ff  ERR  GPIO     hole, window's last
    0x40001000  TIMER   0x000  ACK  TIMER    TIMER CTRL
    0x40001004  TIMER   0x001  ACK  TIMER    TIMER LOAD
    0x40001ffc  TIMER   0x3ff  ERR  TIMER    hole in TIMER window
    0x40002000  -         -    ERR  default  reserved for growth
    0xfffffffc  -         -    ERR  default  top of the space

    addresses probed          16
    acknowledged              6
    ERR from the target       5   (inside a window, no hardware)
    ERR from the default      5   (outside every window)
    unterminated              0

    Both ERR columns look identical to the master and mean
    different things. 'The map reserves this but nothing is
    built here' is a different fact from 'the map reserves
    nothing here', and only the sel vector separates them.

    words inside the three windows   3072
    words backed by real hardware    260
    reserved but not implemented     2812

    260 implemented words inside 3072 reserved ones, inside a
    4294967296-word space. A window is a reservation, not an
    inventory - and a decoder that treats the reservation as
    the inventory is the over-decoding this chapter is about.

Reading it

Read the class and answered by columns together; neither is sufficient alone.

Six acknowledgements, and they are the entire implemented surface of the SoC — two RAM words probed out of 256, two GPIO registers, two TIMER registers.

Ten errors, split five and five, and the split is the chapter.

Five came from a target. 0x0000_0400 and 0x0000_0FFC are inside RAM's window with no storage behind them; 0x4000_0008 and 0x4000_0FFC are inside GPIO's window with no register; 0x4000_1FFC likewise for TIMER. The select vector names a target in every one of those rows. The map reserved the space and the hardware was never built.

Five came from the default responder. 0x0000_1000 past RAM's window, 0x1000_0000 in the great gap, 0x3FFF_FFFC one word below GPIO, 0x4000_2000 in the reserved region, and the top of the space. The select vector is empty in all five. The map reserves nothing there at all.

Zero unterminated. Every one of the sixteen was answered, which is the Chapter 12.6 guarantee holding across the entire variety of address the map contains.

Then the two rows that close the module.

3072 words inside the three windows; 260 backed by hardware. The map reserves eleven times what the SoC implements — and the SoC is three small targets. 2812 reserved-but-empty words is not a defect in this map; it is what alignment and growth cost, and every one of them is refused by the target that owns it.

260 words inside a 4,294,967,296-word space. The implemented fraction of this address space is about one sixteen-millionth. A decoder is not mostly a device for finding targets; it is mostly a device for establishing that there is no target.

5. The Two Refusals

They are identical at the master and they lead to different places.

ERR from a targetERR from the default
select vectorone bit setempty
what it meansthe map reserves this; nothing was builtthe map reserves nothing here
likely causea pointer inside a valid region, or an over-sized windowa bad address, or a missing map entry
where to lookthe target's implemented rangethe map
when it is expectedprobing a peripheral's feature setalmost never

This extends Chapter 10.5's procedure rather than replacing it. That chapter recovered error provenance from evidence the bus does not carry; the select vector is one more such piece of evidence, and it is free because the decoder already computed it.

Without the select vector these are indistinguishable, and a debug trace that records only the termination class throws away the distinction at the moment it is cheapest to keep.

The second row's "missing map entry" is worth its own note. A newly added peripheral whose window was never added to the map produces ERR from the default responder on every access. The peripheral is fine; the map is incomplete — and the select vector says so immediately, where a bare ERR would send someone into the peripheral's RTL.

6. The Final Integration Lab

Work these out before reading the answers. For each byte address: which target, what local offset, is anything implemented there, what termination, and does any alias exist?

#byte addresstarget?offset?termination?
10x0000_0008
20x0000_0404
30x0000_2000
40x4000_0004
50x4000_1008
60x4000_2004

7. Failure Modes and Discriminating Evidence

Symptom: a reserved address behaves like a real register.

Candidate causes. A coarse decode that selects a block for an entire high-bit region, with no fine decode refusing the gaps.

Discriminating evidence. Write a distinctive value to the reserved address and read every implemented register in that block. If it appears, the reserved region aliases. The distance between them names the bits the fine decode is ignoring.

Likely RTL location: the target's offset comparison, or its absence.

Symptom: accesses to a newly added peripheral all return ERR.

Candidate causes. Its window was never added to the map.

Discriminating evidence. The select vector. Empty means the interconnect does not know the peripheral exists — the map is incomplete and the peripheral is probably fine. One bit set with ERR means the peripheral was reached and refused, which is a different problem in a different file.

Symptom: two peripherals respond to the same address after a map change.

Candidate causes. A window widened into a reserved region that a later peripheral was placed in.

Discriminating evidence. $countones(sel). Two is conclusive, and the elaboration check from Chapter 12.1 would have refused to build. A reserved region is the most likely place for this — it looks free, and a window grown into it collides with whatever claims it next.

Symptom: a driver's register sweep reports far more registers than the block has.

Candidate causes. The block acknowledges every offset in its window rather than only implemented ones.

Discriminating evidence. Compare the count of acknowledged offsets against the block's documented register count. If acknowledgements equal the window size, the fine decode is missing. In this SoC the correct answer is 2 of 1024, and a sweep returning 1024 is reporting the window, not the hardware.

8. Common Mistakes

"A reserved address is unused, so it is safe to alias it to a real register."

Wrong mental model: reserved means nobody cares.

What is true: reserved means claimed for later. Aliasing it creates an address that works today and breaks when the region is allocated — and software that discovered the alias will depend on it.

"Anything in the peripheral region should go to a peripheral."

Wrong mental model: the region is the map.

What is true: the region contains three windows and a lot of nothing. Section 3's shortcut legalises the reserved block and everything up to 0x4000_FFFF. Coarse decode narrows the search; it does not answer the question.

"If the decoder selects the target, the address is valid."

Wrong mental model: selection implies implementation.

What is true: selection means the map reserved it. Five of SIM J's sixteen probes were selected and refused. Only the target knows what it built, which is why the second decode stage cannot be skipped.

"A large window means a large peripheral."

Wrong mental model: the window measures the hardware.

What is true: 3072 reserved words back 260 implemented ones here. The window is sized for alignment and growth, and reading it as a register count overstates this SoC's hardware by a factor of eleven.

"Sparse maps waste address space."

Wrong mental model: unused space is a cost.

What is true: in 32 bits the space is free and the alternatives are not. Packing tightly forfeits natural alignment, forfeits growth room, and — as Chapter 12.2 measured at the GPIO/TIMER seam — turns boundary errors into silent misroutes instead of loud unmapped accesses.

9. Interview Reasoning

One where the implemented locations are a small and non-contiguous fraction of the addressable range — which is every real system.

The numbers make it concrete. This SoC implements 260 words. Its map reserves 3072. Its address space holds four billion. Each is orders of magnitude apart, and none of the gaps is a defect.

Three reasons to design it that way. Alignment: a mask decode needs power-of-two windows on natural boundaries, so a two-register block still gets 4 KiB. Growth: spare offsets mean adding a register instead of relocating a peripheral and invalidating every binary. Grouping: memory low, peripherals high, so an address can be classified by eye.

And the cost is nothing in 32 bits. The argument reverses in a 16-bit space, which is why this is a decision about a particular address width.

The part worth volunteering: sparseness means most of the decoder's work is establishing that no target owns an address. That makes the default responder a central component rather than an edge case.

10. Understanding Check

It does not, and the engineer debugging it does.

At the master they are identicalERR, one clock, no data. Software handles both the same way: the access failed.

The difference is the diagnosis. An ERR from a target means the map reserved that space and nothing was built there. The address is inside a valid region, so the likely cause is a pointer that drifted within a region, or a window sized larger than its hardware.

An ERR from the default means the map reserves nothing there at all. The address is outside every window, so the likely cause is a bad address — or a peripheral whose window was never added to the map.

One observation separates them: the select vector. One bit set, or none. It costs a few flops to expose and it turns a bare "the access failed" into a pointer at the right file.

11. What Module 12 Established

One address, one owner, and the owner is not always a target.

12.1 — an address answers two questions. Which target, and where inside it. The decoder computes both, and ADR is a word address because the specification's own example for a 32-bit byte-granular port is ADR_O(n..2).

12.2 — the map is a contract with five signatories and only one is compiler-checked. Boundaries are where decode bugs live: four probes per window, twelve for this SoC, all measured.

12.3 — a decoder is a predicate, and the implementation is an optimisation of it. Range and mask agreed on 108 addresses and diverged on 256 the moment a window stopped being a power of two.

12.4 — selection has two halves. Qualify the request downstream; select the response upstream. A conformant target that parked its register output turned 0x1111_0001 into 0x3333_0001 through a router whose decode was right.

12.5 — a window is not an inventory. 768 of RAM's 1024 window words have no storage; truncating the index folded all of them onto real data and lost a write.

12.6 — the specification leaves one gap and the integrator closes it. The same unmapped address: 40 clocks and still open, or terminated in the clock it was presented.

12.7 — reserved is not implemented, at every granularity. 260 words inside 3072 inside four billion, with every hole refused by whoever owns it.

The thread through all seven is a single question asked at three scales: does this address have an owner, does that owner have hardware here, and does anything else think it does too. Every defect measured in this module is one of those three answered by assumption instead of by logic.

12. What's Next

Addressing is complete: which target, which offset, which storage word, and what answers when none of those exist.

Every transfer so far has moved a whole word. SEL_O has been tied to all-ones since Chapter 4.7 and never explained.

How does a master write one byte of a 32-bit word without disturbing the other three?

Module 13 — Byte Selects takes up the signal that has been carrying byte granularity all along, and which is the reason ADR does not carry bits 1 and 0. The full path is on the Wishbone curriculum index.

Continue learning

Standards & specifications

Governing standard
Wishbone SoC Interconnection Architecture (OpenCores)(opens OpenCores in a new tab)

Defines the Wishbone signal set, the bus cycles built from it and the interface rules a portable IP core must follow. It deliberately leaves interconnect topology, address map and arbitration policy to the integrator, so those are system decisions rather than requirements of the specification.

This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.

Where this fits

Part of the Wishbone curriculum.