Two NanoSoC bugs are wiring or map errors: the DMA interrupt lands on controller input 6 instead of 1, and the SBR slot overlaps the DMA slot through an 8 KB decode. The first passes every block test; the second is a decoder fault that a block test passes unless it checks the whole slot table. Software discovers the first as a handler that never runs and the second as data with bit 0 flipped. A map and wiring sweep discovers both in one short test by touching every region, every hole and every interrupt. Its one job is that sweep: the expected answer per address and the forced toggle per source.
The map sweep reads one word per region and one per hole. Mapped regions return data: ROM, both SRAMs, all seven peripheral slots; the sweep runs once as a secure master, because SRAM1 and RSTPWR answer a non-secure access with an error. Holes H1, H2 and H4 must answer DECERR within two system clocks; hole H3 inside the APB window must answer PSLVERR, seen as SLVERR on AXI — the different responder is the point, and the sweep asserts the difference. Then the alias pair: read DMA CTRL from 0x4000_0000 and from 0x4000_0100 and require equal values, proving the sixteen images exist and are one register; and write the SBR CTRL START bit and require the DMA KEY unchanged, proving slots 0 and 1 do not overlap, which is planted bug 1 stated as a check.
Interrupt wiring is proved by force, not by traffic: drive each source high in turn and read INTC RAW, requiring exactly the matching bit. Force irq_dma and require RAW bit 1 with bit 6 low — planted bug 3 fails exactly this line. Tie the reserved inputs 6 and 7 to zero and require RAW bits 6 and 7 low in every other step, so a stray connection has nowhere to hide. The default slave below is the responder the sweep assumes: a DECERR answer, never a hang. It is a sketch of the two-cycle response to one beat only; a denied burst needs the interconnect's burst rules (a read burst returns every R beat with RLAST, a write burst consumes every W beat and returns one B), taught in the default-slave lessons of the map section.
// NanoSoC teaching values: sketch of the default slave's single-beat response.
// DECERR after two clk_sys cycles. One beat only: a burst needs one response
// beat per requested beat (RLAST on reads, one B after the last W beat).
module nano_default_slave (
input logic clk,
input logic rst_n,
input logic sel, // this slave selected, address unmapped
input logic write, // 1 for write, 0 for read (unused here: DECERR on both)
output logic ready, // response valid
output logic [1:0] resp // 2'b11 DECERR
);
logic [1:0] count;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
count <= 2'd0;
ready <= 1'b0;
resp <= 2'b00;
end else if (sel && count == 2'd0) begin
count <= 2'd1;
ready <= 1'b0;
resp <= 2'b00;
end else if (sel && count == 2'd1) begin
count <= 2'd2;
ready <= 1'b1;
resp <= 2'b11; // DECERR
end else begin
count <= 2'd0;
ready <= 1'b0;
resp <= 2'b00;
end
end
endmodule# Map and interrupt expectations as executable decoders. The expected decode
# comes from the address map; the sweep runs against a good decoder and
# against the planted-bug decoders and must fail on those. NanoSoC teaching values.
def decode(addr, dma_window=0x1000):
"""Returns (target, response) for a secure read."""
if addr >> 14 == 0: return ("ROM", "OKAY")
if addr >> 16 == 0x2000: return ("SRAM0", "OKAY")
if addr >> 16 == 0x2001: return ("SRAM1", "OKAY")
if addr >> 16 == 0x4000:
slot = (addr >> 12) & 0xF
if dma_window == 0x2000 and addr & 0xFFFF < 0x2000: # bug 1: 8 KB DMA window
return ("DMA", "OKAY")
names = {0: "DMA", 1: "SBR", 2: "UART", 3: "SPI", 4: "GPIO", 5: "INTC", 6: "RSTPWR"}
if slot in names: return (names[slot], "OKAY")
return ("APB-default", "SLVERR") # hole H3: PSLVERR becomes SLVERR
return ("xbar-default", "DECERR") # holes H1, H2, H4
def map_sweep(dec):
bad = []
for addr, want in [(0x0, "ROM"), (0x2000_0000, "SRAM0"), (0x2001_0000, "SRAM1"),
(0x4000_0000, "DMA"), (0x4000_1000, "SBR"), (0x4000_2000, "UART"),
(0x4000_6000, "RSTPWR")]:
if dec(addr)[0] != want: bad.append(hex(addr))
for addr, want in [(0x4000, "DECERR"), (0x2002_0000, "DECERR"),
(0x4000_7000, "SLVERR"), (0x4001_0000, "DECERR")]:
if dec(addr)[1] != want: bad.append(hex(addr))
return bad
assert map_sweep(decode) == []
bug1 = lambda a: decode(a, dma_window=0x2000)
assert map_sweep(bug1) == ["0x40001000"] # SBR base now answered by the DMA
# interrupt wiring: force each source, read the INTC RAW bit
GOOD = {0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5} # source irq index -> INTC input
BUG3 = dict(GOOD); BUG3[1] = 6 # irq_dma lands on spare input 6
def irq_sweep(wiring):
bad = []
for src in range(6):
raw = 1 << wiring[src]
if raw != 1 << src: bad.append(f"irq {src} lit RAW bit {wiring[src]}")
return bad
assert irq_sweep(GOOD) == []
assert irq_sweep(BUG3) == ["irq 1 lit RAW bit 6"]
print("PASS: 11 map probes clean; bug 1 and bug 3 are each caught by name")The sweep table names the expected response per hole — DECERR or SLVERR — with the responder named, not just the code.
The alias pair and the overlap check are explicit lines: equal reads across images, KEY unchanged by SBR START.
Every interrupt force names its RAW bit, and the reserved inputs carry a tied-low assertion in all steps.
A wrong hole response means the decoder changed without the map changing: check which level answered — interconnect default or APB default — and the generation flow that should keep them in step. An alias mismatch means the wrapper drops different bits than the sweep assumes: count the implemented paddr bits at the slave. An interrupt force that lights two RAW bits is a short between sources; one that lights none is an open, or a source pulse shorter than two clk_per cycles (83.3 ns) that the controller missed: sources must be levels held until cleared.
The APB slot decoder: H3 must be claimed by the APB default slave and returned as PSLVERR. DECERR means the interconnect default answered, so the access never reached the APB window — the top-level decode is wrong, not the slot table.