A DMA copy from SRAM1 to SRAM0 ends with SBR STATUS.ERR on the very first beat while CPU reads of the same address work. The SBR master issues non-secure because its CTRL.NS bit resets to 1, and SRAM1 is secure-only, so the interconnect answers SLVERR before any data moves. Reset-safe is not access-safe: the default suits the common non-secure case and breaks the secure one. The intended cure is secure software clearing the bit, but the register that holds the bit sits in a non-secure slot, and that is a hole.
Goal: the boot timeline with each step's owner and duration, plus the per-master security attributes.
Inputs: the sixteen steps, the 115,384.6 baud banner math, the secure-only rows.
Steps: sum the flow in both orders of RST_CAUSE read and clear; verify the 16-character banner; probe SRAM1 through the SBR with NS at its reset value, after secure software clears it, and from non-secure software; file the NS-clear rule and the open weakness.
Boot starts by reading RST_CAUSE: a WAKE cause means SRAM0 was retained and holds the image, so the clear is skipped and the ROM jumps straight to it. A cold boot (POR, PAD or SW cause) sets the stack pointer to 0x2001_0000 and clears 4 KB of SRAM0, then initialises the UART, prints the banner, self-tests the DMA, loads the image over SPI and checksums it. Each master carries a security attribute: the CPU is secure, and the SBR issues AxPROT[1] from its CTRL.NS bit, which resets to 1 (non-secure). Secure-only SRAM1 therefore refuses the SBR until secure software clears NS. Known weakness of NanoSoC v1: the SBR register slot is non-secure, so non-secure software can also clear CTRL.NS and reach SRAM1 through the SBR. The partition is not closed until NS is writable only from the secure side, or the SBR slot becomes secure-only. The checker below shows the bug, the intended fix and the hole, and the readiness review lists the hole as open.
Step group | Duration | Share |
|---|---|---|
Hardware reset R1-R6 and first fetch | 54.073 us | 0.43% |
Read RST_CAUSE, stack, clear (cold boot), UART init | 11.792 us | 0.09% |
16-char banner at 115,384.6 baud | 1,386.667 us | 11.0% |
DMA self-test, 64 words | 8.385 us | 0.07% |
SPI image 8,196 bytes at 6 Mb/s | 10,928.000 us | 86.9% |
Checksum 2,048 word reads | 192.000 us | 1.5% |
MAP = [
("BOOTROM", 0x00000000, 0x4000),
("SRAM0", 0x20000000, 0x10000),
("SRAM1", 0x20010000, 0x10000),
("DMA", 0x40000000, 0x1000),
("SBR", 0x40001000, 0x1000),
("UART", 0x40002000, 0x1000),
("SPI", 0x40003000, 0x1000),
("GPIO", 0x40004000, 0x1000),
("INTC", 0x40005000, 0x1000),
("RSTPWR", 0x40006000, 0x1000),
]
SECURE_ONLY = {"SRAM1", "RSTPWR"}
SLOTS = ["DMA", "SBR", "UART", "SPI", "GPIO", "INTC", "RSTPWR"]
T_CPU, T_SYS, T_PER = 1e9 / 192e6, 1e9 / 96e6, 1e9 / 24e6
def end_of(base, size):
return base + size - 1
def overlaps(entries=MAP):
bad = []
xs = sorted(entries, key=lambda e: e[1])
for a, b in zip(xs, xs[1:]):
if b[1] < a[1] + a[2]:
bad.append((a[0], b[0]))
return bad
def misaligned(entries=MAP):
return [n for n, b, s in entries if b % s != 0]
def decode(addr, slots=SLOTS):
if addr >> 14 == 0:
return "BOOTROM"
if addr >> 16 == 0x2000:
return "SRAM0"
if addr >> 16 == 0x2001:
return "SRAM1"
if addr >> 16 == 0x4000:
slot = (addr >> 12) & 0xF
return slots[slot] if slot < 7 else "H3"
if addr < 0x20000000:
return "H1"
if addr < 0x40000000:
return "H2"
return "H4"
def response(addr, secure, slots=SLOTS):
t = decode(addr, slots)
if t in ("H1", "H2", "H4"):
return "DECERR"
if t == "H3":
return "SLVERR"
if t in SECURE_ONLY and not secure:
return "SLVERR"
return "OK"
def sustained(peak, beats):
return peak * beats / (beats + 1)
# ---- this lesson starts here ----
banner = "NanoSoC boot ok\n"
assert len(banner) == 16
baud = 24e6 / (16 * 13)
assert abs(baud - 115384.6) < 0.1 and abs(160 / baud * 1e6 - 1386.667) < 0.01
def boot_steps(cause, cause_first=True):
hw = [("osc", 1024 * T_PER), ("rst_per", 2 * T_PER), ("pll", 260 * T_PER), ("gates", 2 * T_PER),
("rst_sys", 18 * T_SYS), ("rst_cpu", 8 * T_SYS + 2 * T_CPU), ("fetch", 24 * T_CPU)]
rd, clr = ("read_cause", 54 * T_CPU), ("clear_sram0", 1024 * T_SYS)
if cause == "WAKE":
return hw + ([rd] if cause_first else [clr, rd]) # the clear before the read erases the retained image
rest = [("uart_init", 3 * 54 * T_CPU), ("banner", 160 / baud * 1e9),
("dma_selftest", 5 * 125 + 7 * 125 + 72 * T_SYS + 26 * T_CPU + 6000),
("spi_load", (8192 + 4) * 8 / 6e6 * 1e9), ("checksum", 2048 * 18 * T_CPU)]
return hw + ([rd, clr] if cause_first else [clr, rd]) + rest
def names(steps):
return [n for n, _ in steps]
cold = boot_steps("POR")
assert len(cold) == 14 and abs(sum(ns for _, ns in cold) / 1000 - 12580.917) < 0.01
assert names(cold).index("read_cause") < names(cold).index("clear_sram0")
assert "clear_sram0" not in names(boot_steps("WAKE")) # the retained image survives
assert "clear_sram0" in names(boot_steps("WAKE", cause_first=False)) # the wrong order would erase it
SBR_CTRL_RESET = 0x4 # CTRL.NS (bit 2) resets to 1
def sbr_ctrl_write(ctrl, wdata, writer_secure, ns_secure_only=False):
if ns_secure_only and not writer_secure:
wdata = (wdata & ~0x4) | (ctrl & 0x4) # the proposed fix: NS writable only from the secure side
return wdata & 0x6 # START self-clears
def sbr_reaches_sram1(ctrl):
return response(0x20010000, (ctrl >> 2) & 1 == 0) == "OK" # AxPROT[1] = NS: the access is secure when NS is 0
assert response(0x20010000, True) == "OK" and response(0x20000000, False) == "OK" and response(0x20010000, False) == "SLVERR"
assert not sbr_reaches_sram1(SBR_CTRL_RESET) # bug 7: the SBR is non-secure out of reset
assert sbr_reaches_sram1(sbr_ctrl_write(SBR_CTRL_RESET, 0x0, True)) # secure software clears NS: it works
assert sbr_reaches_sram1(sbr_ctrl_write(SBR_CTRL_RESET, 0x0, False)) # known weakness: non-secure software can too
assert not sbr_reaches_sram1(sbr_ctrl_write(SBR_CTRL_RESET, 0x0, False, ns_secure_only=True)) # closed by the fix
assert sbr_reaches_sram1(sbr_ctrl_write(SBR_CTRL_RESET, 0x0, True, ns_secure_only=True)) # secure copy still works
print("PASS: boot 12.581 ms over 14 steps, banner 16 chars; SBR blocked at reset, opened by NS = 0, hole shown and closed")One PASS line with the boot total, the banner length and the security verdicts. The first sbr_reaches_sram1 assert is the bug-7 demonstration: the model starts from the reset value 0x4, so a reset value of 0 would fail it. The two writes with a non-secure writer show the known weakness open in v1 and closed by the proposed fix.
A banner failure means the newline was forgotten: 15 visible characters plus backslash-n make 16. A baud failure means BAUDDIV 13 was misread: the divisor is 16 times 13. A boot-total failure means a step was dropped or the 4 command bytes of the SPI load were forgotten. A wake failure means the RST_CAUSE read was placed after the clear. A secure failure on the closed case means the fix was applied to the register slot but not to the NS bit.
Hint 1: the CPU read works, so the memory and the map are fine; only the master's attribute differs. Hint 2: read the SBR CTRL reset value bit by bit: 0x4 means NS is 1. Hint 3: the reset default is deliberate, so the cure for the bug is software; but ask who is allowed to write that bit. Answer: secure software clears CTRL.NS before any secure transfer, the integration spec documents the security attribute of every master, and the hole that non-secure software can do the same is listed as open until NS is writable only from the secure side.
SRAM0 is retained through SLEEP in PD_AON, so the image survived; RST_CAUSE bit 3 records the wake so software knows not to reload.