Three questions open every design interview: where does it live, how fast does it move, how long does one access take. The answers are three formulas the candidate must own: power-of-two sizing with alignment, bandwidth as width times clock times efficiency, latency as the sum of stage delays. This lesson drills all three on NanoSoC numbers with checkers that prove each result.
Formula: size = 2^ceil(log2(need)), base a multiple of size, waste = size minus need, select bits = 32 minus log2(size). Worked: the ROM needs 16 KB and takes a 16 KB window with zero waste, select bits [31:14]; each SRAM needs 64 KB and takes 64 KB with zero waste, select [31:16]. The seven peripheral slots need 7 x 4 KB = 28 KB, which would round up to 32 KB (4 KB waste) as one window; NanoSoC instead decodes a 64 KB APB window with select [31:16] and then 4 KB slots by address bits [15:12], so slots 7 to 15 form a 36 KB hole that answers with an error. The checker proves alignment and no overlap, and fails on a misaligned base or an overlap.
import math
def window(need):
return 1 << math.ceil(math.log2(need)) # next power of two
def check(windows):
spans = sorted((b, b + s) for _, b, s in windows)
for name, b, s in windows:
if s & (s - 1) or b % s:
return f"{name}: base {hex(b)} not aligned to size {s}"
for (a0, a1), (b0, b1) in zip(spans, spans[1:]):
if b0 < a1:
return f"overlap at {hex(b0)}"
return "ok"
needs = [("ROM", 0x0000_0000, 16 * 1024), ("SRAM0", 0x2000_0000, 64 * 1024),
("SRAM1", 0x2001_0000, 64 * 1024), ("APB", 0x4000_0000, 7 * 4096)]
windows = []
for name, base, need in needs:
size = window(need)
waste = size - need
sel = 32 - int(math.log2(size))
print(f"{name}: need {need // 1024} KB -> window {size // 1024} KB, waste {waste // 1024} KB, select [31:{sel}]")
windows.append((name, base, size))
assert check(windows) == "ok"
# The peripheral need (28 KB) rounds up to 32 KB, but NanoSoC decodes a 64 KB window then 4 KB slots.
assert window(7 * 4096) == 32 * 1024
windows[3] = ("APB", 0x4000_0000, 64 * 1024)
assert check(windows) == "ok" and 7 * 4096 <= 64 * 1024
# The checker must be able to fail: a misaligned base and an overlap.
assert "not aligned" in check([("X", 0x2000_8000, 64 * 1024)])
assert "overlap" in check([("A", 0x2000_0000, 64 * 1024), ("B", 0x2000_8000, 32 * 1024)])
print("PASS: bases aligned, no overlap, checker rejects a misaligned base and an overlap")Variation: add an eighth 4 KB peripheral slot (slot 7). The last mapped address moves from 0x4000_6FFF to 0x4000_7FFF, still inside the same 64 KB APB window, the slot decoder gains select value 7, and the unmapped hole shrinks from 36 KB to 32 KB. Predict the new end address before running, then add the slot to the checker.
Formula: sustained = width x f x L/(L+1), plus splits at every 4 KB boundary the transfer crosses. Worked: one fabric link at 4 bytes and 96 MHz with 16-beat bursts gives 384 x 16/17 = 361.4 MB/s. The 8 KB boot image at 0x2000_0000 is 4 KB-aligned, spans exactly two pages, crosses one boundary, and needs 128 bursts of 64 bytes. Same-bank sharing halves each stream to 180.7 MB/s, which is why placement is a bandwidth decision.
peak = 4 * 96e6 / 1e6
for length in (1, 4, 8, 16):
sustained = peak * length / (length + 1)
print(f"L={length:2d}: {sustained:6.1f} MB/s")
assert abs(sustained - {1: 192.0, 4: 307.2, 8: 341.3, 16: 361.4}[length]) < 0.15
def segments(start, length, page=4096):
out = []
while length > 0:
n = min(length, page - start % page) # bytes left in this 4 KB page
out.append(n)
start, length = start + n, length - n
return out
def crosses(addr, nbytes, page=4096):
return addr // page != (addr + nbytes - 1) // page
segs = segments(0x2000_0000, 8192)
bursts = sum(-(-seg // 64) for seg in segs)
assert segs == [4096, 4096] and bursts == 128, (segs, bursts)
assert crosses(0x2000_0FF0, 64) and not crosses(0x2000_0FC0, 64), "boundary rule"
assert segments(0x2000_0800, 8192) == [2048, 4096, 2048]
bursts48 = sum(-(-seg // 48) for seg in segments(0x2000_0800, 8192))
print(f"aligned image: {len(segs) - 1} boundary, {bursts} bursts of 64 B; shifted image with 48 B bursts: {bursts48}")
print("PASS: 361.4 MB/s at L=16; 1 split, 128 bursts for the image")Variation: start the same 8 KB image at 0x2000_0800. It now crosses two boundaries (0x2000_1000 and 0x2000_2000) and splits into 2 KB, 4 KB and 2 KB segments; the checker prints the split so you can predict the burst count for 64-byte and for 48-byte bursts before running.
Formula: total = sum of stage cycles per domain converted to ns, plus 2 to 3 destination cycles per crossing, plus slave access. Worked: CPU word read 7 system plus 4 CPU clocks = 93.75 ns; slow-peripheral register read 9 system, 4 peripheral and 4 CPU clocks = 281.25 ns; the request and acknowledge bridge alone (2 + 2 peripheral clocks plus 2 + 2 system clocks) 208.33 ns; a 64-word DMA job of 72 system clocks = 750 ns. The checker re-derives all four from the periods.
t_cpu, t_sys, t_per = 1 / 192e6, 1 / 96e6, 1 / 24e6
word = 7 * t_sys + 4 * t_cpu
slow = 9 * t_sys + 4 * t_per + 4 * t_cpu
bridge = 2 * t_per + 2 * t_per + 2 * t_sys + 2 * t_sys # request sync, access, response sync, respond
dma64 = (64 + 8) * t_sys # N words plus 8 clocks of pipeline fill and drain
for name, got, want in [("word", word, 93.75), ("slow-periph", slow, 281.25),
("bridge-alone", bridge, 208.33), ("dma64", dma64, 750.0)]:
ns = got * 1e9
assert abs(ns - want) < 0.02, (name, ns)
print(f"{name:12s} {ns:8.2f} ns")
# Variation: one extra system clock each way inside the bridge.
slow2 = slow + 2 * t_sys
assert abs(slow2 * 1e9 - 302.08) < 0.02
print("PASS: 93.75, 281.25, 208.33 ns and 750 ns re-derived; variation 302.08 ns")Variation: add one extra system clock each way inside the request and acknowledge bridge (2 system clocks, 20.83 ns). Predict the new slow-peripheral total (281.25 + 20.83 = 302.08 ns) before running.
Each drill states the formula before the numbers, so a wrong input still shows a right method.
Every derived number carries its assert: alignment, efficiency, boundary count, nanosecond total.
Waste, splits and tails are quoted, not just the headline rate.
Run each checker, then break one input deliberately: a misaligned base, a burst length of 3, a missing synchroniser term. The assert that fires names the rule you violated; if no assert fires, the checker is missing a rule and you just found the gap. Time yourself: all three drills plus variations inside thirty minutes.
Efficiency falls to 8 over 9 and sustained to 341.3; peak 384 does not change, and the 128-burst image count doubles to 256 bursts of 32 bytes only if the burst size changes with it. State both halves.