A software bug writes to address 0x1800, a gap between the UART and the timer where nothing is mapped. On one chip the write is quietly ignored. On another, the processor reports a precise error that points at the bad instruction. On a third, the system hangs. The software bug is identical. The difference is how each chip's decoder treats an address that belongs to nobody.
How does an address find its target, and what should happen when no target owns it?
The obvious decoder compares the address with each target's range and enables the one that matches. For mapped addresses that is all it needs to do. For an unmapped address no target is enabled, so no target responds, and a requester waiting for its one response (lesson 5.2) waits for ever. A decoder that only knows about the targets that exist is incomplete; it must also decide what happens for every address that does not.
An address map assigns ranges of addresses to targets. The decoder turns each request's address into exactly one select signal. To make the map complete, every address that matches no range selects a default target, which accepts the request and answers with a decode error. Now every request, mapped or not, gets exactly one response.
Range | Target | Notes |
|---|---|---|
0x0000–0x0FFF | RAM | 4 KB |
0x1000–0x10FF | UART | 256 B; registers at the start |
0x1100–0x1FFF | (hole) | Default target: decode error |
0x2000–0x20FF | Timer | 256 B |
0x2100–0xFFFF | (hole) | Default target: decode error |
This is DCLink step 4's decoder, cut down to three targets and 16-bit addresses. The requester sends six addresses, two of them in holes. The checker counts selects and responses.
// DCLink step 4: an address decoder with a default target. Three regions are mapped:
// RAM at 0x0000-0x0FFF, a UART at 0x1000-0x10FF and a timer at 0x2000-0x20FF.
// Every other address selects the error responder, so every request gets exactly one
// response: data from a real target, or an error from the default one.
module tb;
logic clk = 0;
always #5 clk = ~clk;
initial begin $dumpfile("wave.vcd"); $dumpvars(0, tb); end
// Requests: one per cycle, each answered on the next edge
logic req = 0;
logic [15:0] addr = 0;
// Decode (combinational): exactly one select is high whenever req is high
wire sel_ram = req && (addr[15:12] == 4'h0);
wire sel_uart = req && (addr[15:8] == 8'h10);
wire sel_timer = req && (addr[15:8] == 8'h20);
wire sel_err = req && !(sel_ram || sel_uart || sel_timer);
// Response, one cycle later
logic rsp = 0, rsp_err = 0;
logic [7:0] rdata = 0;
always @(posedge clk) begin
rsp <= req;
rsp_err <= sel_err;
rdata <= sel_ram ? addr[7:0] : sel_uart ? 8'h55 : sel_timer ? 8'h7A : 8'h00;
end
// Stimulus: RAM, UART, an unmapped hole, timer, another hole, RAM
logic [15:0] seq [0:5];
initial begin
seq[0] = 16'h0042; seq[1] = 16'h1004; seq[2] = 16'h1800;
seq[3] = 16'h2010; seq[4] = 16'hF000; seq[5] = 16'h0FFF;
end
int i = 0;
always @(posedge clk) begin
if (i < 6) begin req <= 1; addr <= seq[i]; i <= i + 1; end
else req <= 0;
end
// Checker: one select per request, one response per request, errors only for holes
int errors = 0, responses = 0, errs = 0;
always @(posedge clk) begin
if (req && (sel_ram + sel_uart + sel_timer + sel_err) != 1) errors++;
if (rsp) begin responses++; if (rsp_err) errs++; end
end
initial begin
#100;
if (errors == 0 && responses == 6 && errs == 2) $display("PASS: 6 requests, 6 responses; the 2 unmapped addresses got error responses");
else $display("FAIL: errors %0d responses %0d error responses %0d", errors, responses, errs);
$finish;
end
endmoduleThe four selects are mutually exclusive by construction: sel_err is high exactly when a request matches none of the others.
How to read it, slot by slot:
Slot 0: address 0x0042 selects the RAM (sel_ram). One slot later (rsp, slot 1) the RAM's data comes back: 0x42, its low address byte.
Slot 1: 0x1004 selects the UART; its answer, 0x55, arrives in slot 2.
Slot 2: 0x1800 is in the hole between the UART and the timer. Only sel_err is high. In slot 3, rsp and rsp_err are both high: an answer, and it is an error.
Slots 3 to 5: the timer (0x2010), another hole (0xF000, error in slot 5), and the last RAM byte (0x0FFF, data 0xFF in slot 6). Six requests, six responses, two of them errors.
A hole is an unmapped range. It should always reach the default target.
An alias appears when a decoder compares fewer address bits than it could. A UART decoded only on bits 15:12 = 0x1 answers at 0x1000, 0x1100, 0x1200 and so on: sixteen copies. Partial decoding saves a few gates and causes bugs in which a stray pointer writes a device register instead of faulting.
Overlaps (two targets claiming one address) must be impossible. Check the map, and assert one-hot selects in simulation.
System | Unmapped access |
|---|---|
AXI interconnects | A default target returns DECERR, the decode-error response; SoC tools provide an error-response target for exactly this. |
DCLink (step 4) | Any address outside memory gets a completion with ERR status and no data. |
The opening story: the chip that ignored the write had a default target that returned success, so the bug hid for months. The chip that hung had no default target at all. The one that reported a precise error had a default target returning a decode error, and a processor that turned it into an exception at the faulting instruction. The last design is the one to copy: a bad address should fail loudly, at once, and say where.
It answers anywhere in 0x1000 to 0x1FFF. If its registers use only the low 8 address bits, the same registers appear every 256 bytes: sixteen copies, at 0x1000, 0x1100, … 0x1F00.
Success hides the bug: software reads a plausible value or believes its write landed. An error turns a bad pointer into a precise fault.
An address map must be complete: every address selects exactly one target.
Unmapped addresses go to a default target that answers with a decode error, so nothing hangs and nothing is hidden.
Decode every address bit you own; partial decoding creates aliases.