An integrator once sizes a window at an honest number, say 48 KB for a memory that might grow, and the decode that results needs two range comparisons instead of one equality check. Multiply that by every slave and the address path, which must settle inside one 96 MHz cycle on NanoSoC, starts to miss timing. Power-of-two sizing is not tidiness; it is what keeps the decode to a single comparison of the upper address bits. The NanoSoC map obeys the rule everywhere, and this lesson shows how to check that mechanically.
A window of size 2 to the n whose base is a multiple of its size is selected by comparing only the top 32 minus n address bits. SRAM0 is 64 KB, which is 2 to the 16, sitting at 0x2000_0000, a multiple of 0x1_0000: the select is simply addr bits 31 down to 16 equal to 0x2000, and the low 16 bits are left over as the offset inside the memory. The ROM is 16 KB at zero, so its select is addr bits 31 down to 14 equal to zero. Each APB slot is 4 KB, 2 to the 12, so the slot number is addr bits 15 down to 12 and the offset inside the slot is bits 11 down to 0. One lesson to carry elsewhere: a 768 MiB window, three times 256 MiB, is not a power of two and can never be one equality compare, however convenient the number looks in a spreadsheet.
Rounding up wastes address space, and the waste is easy to price. A peripheral needing 3 KB of registers takes a 4 KB slot and strands 1 KB; NanoSoC strands that inside every slot on purpose. A memory needing 40 KB takes 64 KB and strands 24 KB. The waste buys single-compare decode and room to grow the register block without moving the base, which is cheap next to a respin caused by an overlapping window.
# not-run: NanoSoC window arithmetic, NanoSoC teaching values
SRAM0: size 64 KB = 2^16, base 0x2000_0000, 0x2000_0000 mod 0x1_0000 = 0 -> select is addr[31:16] == 0x2000
ROM: size 16 KB = 2^14, base 0x0000_0000 -> select is addr[31:14] == 0
APB: window 64 KB at 0x4000_0000, slot 4 KB = 2^12 -> slot is addr[15:12], offset is addr[11:0]
SRAM0 end: 0x2000_0000 + 0x1_0000 - 1 = 0x2000_FFFF; next base 0x2001_0000 (SRAM1, no gap, no overlap)Two rules decide whether a map table is even plausible: each size is a power of two with a base aligned to it, and no two regions overlap. The script below checks both for the ten real NanoSoC regions, then proves it is awake by catching an injected duplicate of the DMA window and two regions buried inside a larger one. Comparing each region with the furthest-reaching earlier region, not just its immediate neighbour, is what makes the overlap test sound. Run it whenever the map table changes; it finishes in milliseconds and has caught more integration bugs than any review meeting.
MAP = [
("BOOTROM", 0x0000_0000, 0x4000),
("SRAM0", 0x2000_0000, 0x1_0000),
("SRAM1", 0x2001_0000, 0x1_0000),
("DMA", 0x4000_0000, 0x1000),
("SBR", 0x4000_1000, 0x1000),
("UART", 0x4000_2000, 0x1000),
("SPI", 0x4000_3000, 0x1000),
("GPIO", 0x4000_4000, 0x1000),
("INTC", 0x4000_5000, 0x1000),
("RSTPWR", 0x4000_6000, 0x1000),
]
def is_pow2(n):
return n > 0 and (n & (n - 1)) == 0
def check(regions):
problems = []
for name, base, size in regions:
if not is_pow2(size):
problems.append(f"{name}: size {size:#x} not a power of two")
if base % size != 0:
problems.append(f"{name}: base {base:#x} misaligned to {size:#x}")
ordered = sorted(regions, key=lambda r: r[1])
run_end, run_name = -1, ""
for name, base, size in ordered:
if base < run_end: # overlaps the furthest-reaching earlier region
problems.append(f"overlap: {run_name} contains {base:#x} ({name})")
run_end = max(run_end, base + size)
if base + size == run_end:
run_name = name
return problems
assert check(MAP) == []
bad = MAP + [("DMA_COPY", 0x4000_0000, 0x1000)]
hits = check(bad)
assert len(hits) == 1 and hits[0].startswith("overlap:")
buried = [("BIG", 0x2000_0000, 0x4_0000), ("IN1", 0x2001_0000, 0x1000),
("IN2", 0x2002_0000, 0x1000)] # two regions inside a large one
assert len(check(buried)) == 2
print("PASS: NanoSoC regions disjoint; duplicate and buried overlaps caught")The checker output for the current map version is attached to the integration review, not a hand-drawn diagram.
Every base is shown with its alignment arithmetic, as in the worked block above, so a reviewer can verify one row in seconds.
Stranded space inside each window is labelled reserved, not left blank for a neighbour to claim later.
If the checker reports an overlap, sort the table by base and walk the boundaries: the usual cause is a size typed in decimal kilobytes instead of hex, or an end address mistaken for a size. If it reports misalignment, the window was probably grown without moving the base. Both are map-table edits, never decoder edits; the decoder is generated from the table, which is the next topic's lesson.
The size is 2 to the 16, so the low 16 bits are the offset and the select is addr[31:16] equal to 0x2000. Bit 16 is the first bit that distinguishes SRAM0 from SRAM1.