Wishbone · Module 2
Address Space
An address is a number until a decoder turns it into a target selection and a local offset. Range comparison and mask comparison are different engineering choices with different costs; exhaustive, mutually exclusive decode is a property to be proved rather than assumed; and a flat decoder's critical path is what eventually forces a hierarchy.
Chapter 2.1 gave gpio_target a sel input and said only the decoder chose you. That is the gap this chapter closes.
Chapter 1.2 established the software-facing half of the address story: peripheral registers occupy processor addresses, an address carries a region part and an offset part, and the map is a contract. This chapter is the hardware half — what a decoder actually computes, what makes it correct, and what it costs in gates and in nanoseconds.
The question: given a 32-bit address and several targets, what selects one — and what makes that selection correct rather than merely working?
1. What the Decoder Owes the Rest of the System
The decoder sits between the initiator and every target, and it produces exactly two things.
The local offset is the half people forget. A target designed around offsets 0x00, 0x04, 0x08 cannot be handed 0x4000_1004; it must receive 0x004. That truncation is what makes the target reusable, and Chapter 2.1 §9 worked through what is lost if a target decodes the system address itself.
The map this module uses, carried forward from Module 1:
| Target | Base | Size | Offset bits | Region tag bits |
|---|---|---|---|---|
| SRAM | 0x2000_0000 | 64 KiB | addr[15:0] | addr[31:16] |
| GPIO | 0x4000_0000 | 4 KiB | addr[11:0] | addr[31:12] |
| UART | 0x4000_1000 | 4 KiB | addr[11:0] | addr[31:12] |
| Timer | 0x4000_2000 | 4 KiB | addr[11:0] | addr[31:12] |
| Default | everything else | — | — | — |
Note the offset widths differ, which is the first real complication: there is no single answer to how many bits are the offset, only an answer per region.
2. Two Ways to Express a Region
A region is a set of addresses. Hardware can test membership in two ways, and the choice is a genuine engineering trade rather than a style preference.
Range comparison asks whether the address falls between two bounds:
hit = (addr >= BASE) && (addr < BASE + SIZE)Mask comparison asks whether the address's upper bits equal a constant:
hit = ((addr & MASK) == BASE)They are equivalent only when the region is a power-of-two size and is naturally aligned — aligned to its own size. Outside that condition, only range comparison can express the region at all.
| Range compare | Mask compare | |
|---|---|---|
| Any region size | yes | no — power-of-two only |
| Any alignment | yes | no — natural alignment only |
| Hardware | two magnitude comparators | one equality comparator |
| Structure | carry chains, width-dependent delay | wide XNOR then AND-reduce |
| FPGA depth | grows with address width | ~1–2 LUT levels, flat |
| Overlap detectable at elaboration | harder | easy — compare constants |
The delay difference is the point. A magnitude comparator is a subtract: its carry must propagate across the compared width before the result is known, so its delay grows with address width. An equality comparison is a bitwise XNOR followed by an AND-reduction — a tree, not a chain — which an FPGA folds into a couple of LUT levels regardless of width.
And there are two comparators per region for range, one for mask. With five targets that is ten carry chains against five XNOR trees, all on the critical path of every access.
3. RTL 1 — A Parameterised Decoder
Both forms, in one module, with the region table as a parameter rather than as hand-written comparisons.
// ─────────────────────────────────────────────────────────────────────────
// bus_decoder — address → one-hot target select + local offset.
//
// PURPOSE. Turn one address into exactly one target selection, for EVERY
// address in the space, and hand the chosen target an offset local to its
// own region. This is the block Chapter 2.1's `sel` input came from.
//
// THE REGION TABLE IS A FLAT PACKED VECTOR, not an unpacked array. Array
// parameters are legal SystemVerilog and are unevenly supported across
// tools, so production IP that must build everywhere commonly carries the
// table flat and slices it with a part-select. The helper functions below
// restore readability without giving up portability.
//
// `USE_MASK` selects the comparison style of Section 2: mask comparison when
// every region is power-of-two sized and naturally aligned, range comparison
// otherwise.
//
// Generic educational interface — not any bus's signal names.
// ─────────────────────────────────────────────────────────────────────────
module bus_decoder #(
parameter int unsigned AW = 32,
parameter int unsigned NTARGET = 4,
// Region bases, index 0 first — so the concatenation reads bottom-up:
// index 0 = SRAM, 1 = GPIO, 2 = UART, 3 = Timer.
parameter logic [NTARGET*32-1:0] BASE_FLAT =
{ 32'h4000_2000, 32'h4000_1000, 32'h4000_0000, 32'h2000_0000 },
// Region sizes in bytes, same index order.
parameter logic [NTARGET*32-1:0] SIZE_FLAT =
{ 32'h0000_1000, 32'h0000_1000, 32'h0000_1000, 32'h0001_0000 },
parameter bit USE_MASK = 1'b1
) (
input logic [AW-1:0] addr,
output logic [NTARGET:0] target_sel, // bit NTARGET = default target
output logic [AW-1:0] offset, // local offset for the hit region
output logic unmapped
);
function automatic logic [31:0] base_of(input int unsigned i);
return BASE_FLAT[i*32 +: 32];
endfunction
function automatic logic [31:0] size_of(input int unsigned i);
return SIZE_FLAT[i*32 +: 32];
endfunction
// ── Elaboration-time checks. These convert map requirements from comments
// into build failures — the move Chapter 1.4 argued for. A map error
// caught here costs a compile; caught in the lab it costs a week.
initial begin
for (int unsigned i = 0; i < NTARGET; i++) begin
if (USE_MASK && (size_of(i) != (32'd1 << $clog2(size_of(i)))))
$fatal(1, "bus_decoder: region %0d size %0h is not a power of two",
i, size_of(i));
if (USE_MASK && ((base_of(i) & (size_of(i) - 32'd1)) != 32'd0))
$fatal(1, "bus_decoder: region %0d base %0h is not naturally aligned",
i, base_of(i));
for (int unsigned j = i + 1; j < NTARGET; j++)
// Two half-open ranges overlap unless one ends at or before the
// other begins. Checking it here is the only way the property is
// guaranteed rather than tested.
if ((base_of(i) < base_of(j) + size_of(j)) &&
(base_of(j) < base_of(i) + size_of(i)))
$fatal(1, "bus_decoder: regions %0d and %0d overlap", i, j);
end
end
logic [NTARGET-1:0] hit;
// ── Combinational: membership, one comparison per region, all parallel.
always_comb begin
for (int unsigned i = 0; i < NTARGET; i++) begin
if (USE_MASK) begin
// Equality on the bits above the region: a flat XNOR/AND tree.
// ~(SIZE-1) is the region mask, e.g. 4 KiB → 32'hFFFF_F000.
hit[i] = ((addr & ~(size_of(i) - 32'd1)) == base_of(i));
end else begin
// Two magnitude comparisons: carry chains, and twice as many of them.
hit[i] = (addr >= base_of(i)) && (addr < base_of(i) + size_of(i));
end
end
end
// Unmapped is the ABSENCE of every hit. Defining it this way is what makes
// the select vector total: no address produces an all-zero vector.
assign unmapped = ~(|hit);
always_comb begin
target_sel = '0;
target_sel[NTARGET-1:0] = hit;
target_sel[NTARGET] = unmapped;
end
// ── The local offset. Keep only the bits below the hit region's size.
// The loop looks like a priority encoder and is not: at most one hit[i]
// can be set, a fact the elaboration overlap check guarantees rather
// than this loop enforcing. Stating that dependence here is deliberate.
always_comb begin
offset = addr; // default: unmapped passes through
for (int unsigned i = 0; i < NTARGET; i++)
if (hit[i]) offset = addr & (size_of(i) - 32'd1);
end
endmoduleReading this module
Purpose. One address in; one target selected and one local offset out, for every address.
Interface contract. target_sel is one-hot and NTARGET+1 bits wide — the extra bit is the default target, and its presence is what upgrades the correctness property from at most one to exactly one. offset is meaningful for whichever target is selected.
Combinational behaviour. Every region is compared in parallel; there is no priority and none is wanted. unmapped is the NOR of the hit vector. The offset loop looks like a priority encoder and is not, because at most one hit[i] can be set — a fact the elaboration check guarantees rather than the loop enforcing.
No sequential behaviour at all. The decoder is pure combinational logic. Registering the select would add a cycle to every access and split the decode from the multiplexer it feeds; at this scale that buys nothing. Chapter 2.7 revisits the trade when the path gets long.
Timing. The whole module is one combinational cone from addr. Its delay is the comparison depth plus the offset multiplexer, and both are on the critical path of every transfer.
Deliberate simplifications. Every region uses the same comparison style; real maps sometimes mix. There is no protection checking, no support for a region whose size differs per instantiation beyond the SIZE array, and no registered pipeline stage.
How it could fail. A wrong BASE constant routes accesses to the wrong target and the elaboration check will not notice, because the map is self-consistent — it is simply not the map the software header describes. A USE_MASK map with a misaligned base now fails at elaboration instead of silently aliasing. And if a future edit made NTARGET larger than the BASE/SIZE arrays, elaboration fails on the array bound, which is the right outcome.
Scaling. Five regions is five parallel comparisons and a five-input offset multiplexer — cheap. Thirty-two regions is a 32-input multiplexer on the offset and a 32-wide OR for unmapped, and the comparison count is now a real contributor to the path. Section 6 is what to do about it.
4. Verification — Proving It for All Addresses
The decoder's two properties are claims about every address, which makes them exactly the shape a formal tool is good at and a directed test is bad at.
// ─────────────────────────────────────────────────────────────────────────
// Decoder properties. Bind to bus_decoder with an UNCONSTRAINED address; a
// formal tool then proves these over all 2^AW addresses. A directed
// simulation can only check the addresses someone thought to write, and an
// aliasing bug lives precisely in the ones nobody thought to write.
// ─────────────────────────────────────────────────────────────────────────
module bus_decoder_checker #(
parameter int unsigned AW = 32,
parameter int unsigned NTARGET = 4
) (
input logic clk,
input logic rst_n,
input logic [AW-1:0] addr,
input logic [NTARGET:0] target_sel,
input logic [AW-1:0] offset,
input logic unmapped
);
default disable iff (!rst_n);
// P1 — EXACTLY one target, always. $onehot, not $onehot0: the all-zero
// vector means nobody answers, which is a hang rather than a benign
// miss. Only the default target makes the stronger property provable.
property p_exactly_one;
@(posedge clk) $onehot(target_sel);
endproperty
a_exactly_one : assert property (p_exactly_one)
else $error("target_sel=%b — zero targets hangs, two corrupts", target_sel);
// P2 — the two encodings of "nobody owns this" never disagree.
property p_unmapped_agrees;
@(posedge clk) unmapped == target_sel[NTARGET];
endproperty
a_unmapped_agrees : assert property (p_unmapped_agrees)
else $error("unmapped=%b but default select=%b", unmapped, target_sel[NTARGET]);
// P3 — the offset is a SUFFIX of the address. Whatever region was chosen,
// the offset must be the low bits of addr unchanged; this catches an
// offset computed from the wrong region's size.
property p_offset_is_suffix;
@(posedge clk) (offset & ~offset) == '0 and (offset == (addr & offset_mask(offset)));
endproperty
// Helper: the smallest all-ones mask covering `v`.
function automatic logic [AW-1:0] offset_mask(input logic [AW-1:0] v);
logic [AW-1:0] m;
m = '0;
for (int unsigned i = 0; i < AW; i++) if (v[i]) m = (AW'(1) << (i + 1)) - AW'(1);
return m;
endfunction
// P4 — decode is a pure function of the address. Trivially true of a
// combinational block, and it stops being trivial the moment someone
// registers an intermediate signal to close timing.
property p_pure;
@(posedge clk) $stable(addr) |-> $stable(target_sel) && $stable(offset);
endproperty
a_pure : assert property (p_pure)
else $error("decode changed while addr was stable");
endmoduleWhy P1 is $onehot and not $onehot0. $onehot0 permits at most one, which catches overlapping regions and allows the all-zero vector. The all-zero vector is the worse bug: no target responds, the initiator waits for a completion that never arrives, and a stray pointer hangs the system. $onehot catches both — and is only achievable because a default target exists. If your fabric has no default target, $onehot0 is the strongest property available, and that is a fact about the fabric rather than about the assertion.
Why the elaboration check and P1 are both needed. They prove the same property by different means and at different times. The elaboration check is exhaustive over the map and runs at build time, with a message naming the two offending regions. P1 is exhaustive over the address and catches a decoder whose comparison logic does not implement the map it was given. A wrong mask expression passes the elaboration check and fails P1.
5. RTL 2 — What the Wrong Decode Looks Like
The most common decoder bug is not an overlap in the map. It is a comparison that examines too few bits.
// WRONG — compares only the bits that distinguish UART from its immediate
// neighbours, ignoring the twenty bits above.
assign hit_uart = (addr[15:12] == 4'h1);The UART now answers at 0x4000_1000, as intended — and at 0x0000_1000, 0x2000_1000, 0xDEAD_1000, and roughly sixteen million other addresses.
What breaks, and when. Nothing, at first: every test uses the documented address. Then one of three things happens.
- Another target is placed at an aliased address. Two selects assert; P1 fires in simulation, and in silicon the shared return path merges two targets' data into a value belonging to neither.
- A stray pointer lands on an alias and writes a UART register instead of faulting. The symptom shows up in the UART, arbitrarily later, with no connection to the write.
- Someone writes a driver against an alias because it happened to work, and an unintended address becomes load-bearing.
Why it survives so long: the defect is in the addresses nobody tests, which is all of them. This is the case that most justifies formal verification for a decoder — P1 with an unconstrained address refutes it in seconds and produces the counterexample address.
6. Where a Flat Decoder Stops Working
Two pressures grow with target count, and they are different from each other.
The comparisons stay cheap. They are parallel, one per region, and adding a region adds one more. Even at thirty-two targets the comparison layer is a couple of LUT levels wide.
The fan-in does not. unmapped is an OR of every hit. The offset multiplexer selects among every region's masked address. And — Chapter 2.3's subject — the read-data multiplexer selects among every target's data, on every bit. Those all grow with target count, and on an FPGA they grow in steps as the device's LUT input count is exceeded.
The answer is hierarchy, and it is why address maps look the way they do. Decode the high bits to a group first, then decode within the group. Two narrow levels replace one wide one, and the delay grows logarithmically rather than linearly.
That structure only works if regions belonging to one group are contiguous in the address space — which is exactly why real maps cluster all peripherals into a band like 0x4000_0000 rather than scattering them. The address map's shape is a consequence of the decoder's structure, not an aesthetic choice.
One more consequence worth stating: a hierarchical fabric adds a level of routing to every access, so it trades latency for scalability. At four targets it is a pessimisation. At thirty-two it is the only option. Knowing roughly where the crossover sits for your device is a real skill, and Chapter 2.7 returns to it with the complete fabric in view.
7. Failure Modes and Discriminating Evidence
Symptom: writes to one peripheral also change another.
Candidates. Overlapping regions in the map; a comparison examining too few bits; a target ignoring sel.
Discriminating evidence. Probe target_sel on the failing access. Two bits high is decode. One bit high, with the other target still changing state, is that target ignoring sel — a Chapter 2.1 P1 violation.
Likely RTL location. The region constants, or the comparison expression.
Property that catches it. P1 in formal; the elaboration overlap check at build time if the map itself is wrong.
Symptom: the system hangs on one specific address.
Candidates. Unmapped address with no default target.
Discriminating evidence. target_sel is all zero. That is conclusive.
Likely RTL location. The absence of a default target, or unmapped not wired into the select vector.
Property that catches it. P1 — this is precisely the case $onehot0 would have permitted.
Symptom: a peripheral's registers appear shifted by a constant.
Candidates. The target is receiving the full address rather than the local offset, or the offset is masked with the wrong region's size.
Discriminating evidence. Compare offset against addr on a known access. If offset == addr, the truncation is missing entirely. If offset is right for one target and wrong for another, the size array and the hit index have diverged.
Likely RTL location. The offset multiplexer.
Property that catches it. P3.
Symptom: it worked until a peripheral was added, with no other change.
Candidates. The new region overlaps an existing alias created by an under-specified comparison, exposing a latent bug.
Discriminating evidence. Remove the new target. If the old one recovers, the two are colliding and the old comparison is the suspect, not the new block.
8. Common Mistakes
"A one-hot select is what the decoder produces, so it must be one-hot."
Wrong mental model: the encoding guarantees the property.
Concrete failure: two hits assert, the AND-OR return path ORs two targets' data, and the initiator receives a number belonging to neither.
Observable evidence: read values that share bits with two different registers — recognisable by eye once seen.
Correct model: one-hot is a property to be proved, by an elaboration check over the map and an assertion over the address. The encoding is a hope until then.
"Unmapped addresses are not our problem."
Wrong mental model: the map describes the regions that exist, and the rest is undefined in a harmless way.
Concrete failure: a hang. No target answers, the initiator waits forever, and one stray pointer takes the system down.
Observable evidence: a reproducible lock-up on one address, with target_sel all zero.
Correct model: the default target is a design element you build. The map is incomplete until unmapped behaviour is specified.
"The target can work out its own offset from the full address."
Wrong mental model: offsetting is bookkeeping the target can do.
Concrete failure: the target now contains its base address, so it cannot be instantiated twice or moved without modification.
Observable evidence: an attempt to add a second UART that requires editing the UART.
Correct model: the decoder owns the region and the target owns the offset. That split is what makes the block portable, and Chapter 1.2 is the argument.
"Mask decode is just a faster range decode."
Wrong mental model: the two are interchangeable and one is better.
Concrete failure: a region that is not power-of-two sized or not naturally aligned is expressed as a mask anyway, and it silently covers a larger span than intended.
Observable evidence: a target answering at addresses outside its documented region.
Correct model: they are equivalent only under alignment and power-of-two size. Outside that, mask comparison cannot express the region at all — which is what the elaboration check in Section 3 enforces.
9. Interview Reasoning
Because two selects and zero selects are both failures, and they are different failures.
Two selects means two targets answer one access. On a shared return path the read data is merged — with an OR reduction, the bitwise OR of two registers, which is a value belonging to neither and looks like corruption rather than a routing fault. Two write targets both accept the data.
Zero selects means nobody answers. The initiator waits for a completion that never arrives and the system hangs. A stray pointer becomes a lock-up.
$onehot0 catches only the first; $onehot catches both. The stronger property is achievable only if a default target claims every address no real target owns — so the assertion you can write is a diagnostic about the fabric's design.
The detail that shows depth: this is a property over all 2³² addresses, which makes it a formal problem rather than a simulation problem. A directed test checks the addresses someone wrote down; an aliasing bug lives in the ones nobody did.
10. Understanding Check
11. What's Next
The decoder now produces a one-hot selection and a local offset, both provable rather than hoped for, and the cost of the two comparison styles is explicit.
Selection is only half of routing, though. The decoder says which target; it says nothing about how the values actually move.
Once a target is selected, how does write data reach it, how does read data come back, and what happens when the bus is wider than the register at the other end?
Chapter 2.3 — Data Transfer takes the return path seriously: the read-data multiplexer that Section 6 named as the largest fan-in structure, byte lanes and what they are for, and the width mismatches that produce wrong values rather than errors. The full path is on the Wishbone curriculum index.
Continue learning
Related tutorials
- Related topic
FPGA Design Challenges
Six peripherals and two masters on one FPGA is where on-chip integration stops being theory. The decode that must be exhaustive and one-hot, the read multiplexer that grows with every target and carries the critical path, the latency and reset conventions that refuse to agree, and the point at which the fabric rather than the peripherals starts failing timing.
- Related topic
The Interconnect
Two conforming Wishbone interfaces still cannot talk without something between them. The INTERCON holds the address map, distributes exactly one strobe, merges read data and terminations, and answers for addresses nobody owns — and the specification defines it by its job rather than by a signal set.
- Related topic
Control Signals
Address and data are payload; they say what values are involved and nothing about what should happen to them. Control information is what makes a bus interpretable: a qualifier that says a request is real, a direction, lane enables, a completion and an error — each derived from a failure that occurs without it.
- Related topic
Bus Transactions
A transaction is the unit of bus work: one beginning, one ending, and an interval in between during which the request must not move. Making waiting expressible is what lets a slow target share a bus with a fast one, and it is what turns an initiator from a wire into a state machine with real failure modes.
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.
