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?
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:
// "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.
// ─────────────────────────────────────────────────────────────────────────
// 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;
endmoduleSixteen 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.
=== 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 target | ERR from the default | |
|---|---|---|
| select vector | one bit set | empty |
| what it means | the map reserves this; nothing was built | the map reserves nothing here |
| likely cause | a pointer inside a valid region, or an over-sized window | a bad address, or a missing map entry |
| where to look | the target's implemented range | the map |
| when it is expected | probing a peripheral's feature set | almost 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 address | target? | offset? | termination? |
|---|---|---|---|---|
| 1 | 0x0000_0008 | |||
| 2 | 0x0000_0404 | |||
| 3 | 0x0000_2000 | |||
| 4 | 0x4000_0004 | |||
| 5 | 0x4000_1008 | |||
| 6 | 0x4000_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 identical — ERR, 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
Related tutorials
- Related topic
Data Flow
One Wishbone access, followed through every block in both directions: what the master drives, where the address changes form, which signals are broadcast and which are decoded, and how read data and termination find their way back to exactly one requester.
- Related topic
DAT_O
Both masters and slaves have a DAT_O, and they mean opposite things. What each must drive, when it must be valid, and why a master may legally leave stale write data on the bus during a read.
- Related topic
DAT_I
A master's DAT_I is returned read data; a slave's is incoming write data. Neither may be believed without the condition that qualifies it — and sampling one cycle late returns the previous transaction's value.
- Related topic
WE_O
One bit decides which data path carries the meaningful value. It is a direction selector and never a write trigger, and a slave that ignores it writes a register on every read with the previous write's data.
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.
