Every AXI transaction on NanoSoC passes through one combinational decision: which of the four slaves, or the default, owns this address. Get one bit wrong and a peripheral write lands in SRAM, or SRAM1 opens to non-secure software. The equations are short enough to hold in your head, which is exactly why they get hand-edited, and hand edits to decode are how two slaves end up answering at once. Write them once, check them with a table, and never touch them by hand again.
Each window is one equality compare, taken straight from the map alignment of the previous topic. The ROM window is 16 KB at zero, so bits 31 down to 14 are all zero. SRAM0 and SRAM1 are 64 KB windows, so bits 31 down to 16 name the bank: 0x2000 or 0x2001. The APB bridge owns the 64 KB window at 0x4000_0000, so bits 31 down to 16 equal 0x4000. Anything else falls through to the default slave, which answers holes with DECERR. A secure-filter violation gets its own select and its own responder, never the default: the fault responder answers SLVERR with no data. The distinction is the lesson to keep, and it is semantic before it is about security: DECERR means no slave decoded the address, SLVERR means a slave (or its filter) refused it. SRAM1 exists and refused, so it answers SLVERR; answering DECERR would misreport an existing region as absent and send anyone debugging the access to the wrong level of the decode. As a defence-in-depth remark, not the reason: designs that care can additionally hide existence from a prober, but that is a separate policy choice, not what the response codes are for. Exactly one of the six outputs fires for every possible address.
// NanoSoC teaching values: first-level interconnect decode plus the SRAM1 secure filter.
// Holes select sel_default (answered DECERR); a secure violation selects sel_fault
// (answered SLVERR by the fault responder, never the default slave).
// The RSTPWR per-slot secure check lives in the APB slot decoder (next lesson).
module nano_addr_dec (
input logic [31:0] addr,
input logic prot_ns, // AxPROT[1]: 1 = non-secure
output logic sel_rom,
output logic sel_sram0,
output logic sel_sram1,
output logic sel_apb,
output logic sel_default,
output logic sel_fault
);
logic in_rom, in_sram0, in_sram1, in_apb;
logic sram1_ns;
assign in_rom = (addr[31:14] == 18'h0);
assign in_sram0 = (addr[31:16] == 16'h2000);
assign in_sram1 = (addr[31:16] == 16'h2001);
assign in_apb = (addr[31:16] == 16'h4000);
assign sram1_ns = in_sram1 & prot_ns;
assign sel_rom = in_rom;
assign sel_sram0 = in_sram0;
assign sel_sram1 = in_sram1 & ~sram1_ns;
assign sel_apb = in_apb;
assign sel_default = ~(in_rom | in_sram0 | in_sram1 | in_apb);
assign sel_fault = sram1_ns;
endmoduleThe module above compiles cleanly, but compilation only proves the syntax. The table below proves the behaviour: it decodes the same addresses the same way, including the two cases newcomers get wrong, that hole H3 still selects APB at this level and that a non-secure SRAM1 access selects the fault responder, never the default and never the memory.
def decode(addr, prot_ns):
in_rom = (addr >> 14) == 0
in_sram0 = ((addr >> 16) & 0xFFFF) == 0x2000
in_sram1 = ((addr >> 16) & 0xFFFF) == 0x2001
in_apb = ((addr >> 16) & 0xFFFF) == 0x4000
fault = bool(in_sram1 and prot_ns)
if in_rom:
sel = "ROM"
elif in_sram0:
sel = "SRAM0"
elif in_sram1 and not fault:
sel = "SRAM1"
elif fault:
sel = "FAULT"
elif in_apb:
sel = "APB"
else:
sel = "DEFAULT"
return sel, fault
cases = [
(0x0000_0000, 0, ("ROM", False)),
(0x0000_3FFF, 0, ("ROM", False)),
(0x0000_4000, 0, ("DEFAULT", False)),
(0x2000_0000, 0, ("SRAM0", False)),
(0x2000_FFFF, 0, ("SRAM0", False)),
(0x2001_0000, 0, ("SRAM1", False)),
(0x2001_0000, 1, ("FAULT", True)),
(0x4000_1000, 0, ("APB", False)),
(0x4000_7000, 0, ("APB", False)),
(0x4001_0000, 0, ("DEFAULT", False)),
(0xFFFF_FFFF, 0, ("DEFAULT", False)),
]
for addr, ns, want in cases:
assert decode(addr, ns) == want, hex(addr)
print("PASS: 11 decode cases incl. H3-as-APB and secure SRAM1 to FAULT")The four equations match the map table bit for bit: window size to compare width, base to compare value.
Holes select only the default (DECERR) and a secure violation selects only the fault responder (SLVERR), so the two error codes can never be confused.
Secure filtering is visible beside the selects with its own output, not buried inside one equation.
Exhaust the boundaries, not the middles: each window contributes its base, its last byte and one address either side, plus both security states for SRAM1. The eleven cases above cover the main boundaries; the full rule, about twenty addresses including 0x1FFF_FFFF, 0x2001_FFFF, 0x2002_0000 and 0x4000_FFFF, is generated from the map table so a new window adds its own boundary rows automatically.
The default slave answers DECERR, which means the address does not exist. SRAM1 exists and refused, so it must answer SLVERR with no data; sharing the default would give faulted accesses the wrong code and send the debugger looking for a missing region that is really there. (Hiding existence from a prober is a separate policy choice.)