After reset the CPU stalls at its vector with ARVALID high and no RVALID, yet resetting just the CPU later boots fine. The release put rst_cpu_n up in the same step as rst_sys_n; the CPU-side 2-flop sync at 192 MHz is faster than the fabric's 18 cycles at 96 MHz, so the first fetch entered the async bridge while the crossbar was still in reset and was lost. Slaves and fabric first, masters last is not a slogan; it is this bug.
Goal: the reset tree plus a release order the CPU cannot outrun.
Inputs: four sources, three nets, the R0 to R6 step table.
Steps: draw the tree; compute each release time from the plan; model the buggy same-step release and confirm the order assertion fires and the first fetch is lost; file the fixed order.
Four sources feed the controller in clk_per: por_n resets everything including RST_CAUSE, pad_rst_n debounces 16 clk_per cycles (666.7 ns) and spares RST_CAUSE and SRAM0 contents, sw_rst needs the 0xA5 key and holds rst_sys_n plus rst_cpu_n for 16 clk_sys cycles, pwr_rst follows SW_ON rising. Three nets leave, all asynchronously asserted and synchronously released through 2-flop syncs in their own domains: rst_per_n, rst_sys_n, rst_cpu_n. Cold release runs R0 por_n at zero, R1 oscillator ready over 1024 clk_per (42.667 us), R2 rst_per_n after 2 clk_per, R3 PLL lock over 260 clk_per (10.833 us), R4 gates open after 2 clk_per, R5 rst_sys_n after an 18-clk_sys hold and sync (187.5 ns), R6 rst_cpu_n after an 8-clk_sys gap plus 2 clk_cpu (93.75 ns), totalling 53.948 us. Minimum assertion is 16 domain cycles.
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 ----
# each net: (what it waits for, how long after that), from the R1 to R6 table
PLAN = {"rst_per_n": ("por", 1026 * T_PER), # 1024 oscillator cycles plus a 2-flop sync
"rst_sys_n": ("gates", 18 * T_SYS), # 16-cycle hold plus a 2-flop sync in clk_sys
"rst_cpu_n": ("rst_sys_n", 8 * T_SYS + 2 * T_CPU)} # 8-cycle gap plus a 2-flop sync in clk_cpu
def release_times(plan):
t = {"por": 0.0}
t["rst_per_n"] = plan["rst_per_n"][1]
t["gates"] = t["rst_per_n"] + 262 * T_PER # PLL start and lock (260) and gates open (2)
for net in ("rst_sys_n", "rst_cpu_n"):
after, wait = plan[net]
t[net] = t[after] + wait
return t
def fetch_lost(t):
# the first fetch reaches the crossbar 1 clk_cpu + 3 clk_sys after the CPU leaves reset
return t["rst_cpu_n"] + T_CPU + 3 * T_SYS < t["rst_sys_n"]
def order_ok(t):
return t["rst_per_n"] <= t["rst_sys_n"] and t["rst_cpu_n"] >= t["rst_sys_n"] + 8 * T_SYS
good = release_times(PLAN)
assert abs(good["rst_cpu_n"] / 1000 - 53.948) < 0.01
assert abs(good["rst_sys_n"] - good["gates"] - 187.5) < 0.1 and abs(good["rst_cpu_n"] - good["rst_sys_n"] - 93.75) < 0.1
assert order_ok(good) and not fetch_lost(good)
bug = release_times(dict(PLAN, rst_cpu_n=("gates", 2 * T_CPU))) # bug 2: released in the same step as rst_sys_n
assert not order_ok(bug) and fetch_lost(bug)
assert abs(bug["rst_sys_n"] - bug["rst_cpu_n"] - 177.1) < 0.1
print("PASS: CPU released at 53.948 us, 93.75 ns after the fabric; same-step release breaks the order and loses the fetch")One PASS line with the CPU release time. The bug plan is the bug-2 demonstration: both order_ok and fetch_lost react, because they compare real release times, and the CPU leaves reset 177 ns before the fabric does.
A total failure means a step counted the wrong domain's cycles: the sys and CPU steps use their own periods, the earlier ones the 24 MHz period. An order failure on the good plan means the cpu net is anchored to the gates instead of to rst_sys_n. A pass on the bug plan means the predicates compare names instead of times, which is exactly the bug being hidden.
Hint 1: convert each step to nanoseconds in its own domain before summing. Hint 2: the CPU-side 2-flop sync takes 2 clk_cpu, 10.4 ns; the fabric takes 18 clk_sys, 187.5 ns. Released together, the CPU runs 177 ns before the fabric is out of reset. Hint 3: the lost fetch is already inside the bridge when the fabric wakes, so no retry can save it. Answer: release the CPU 8 clk_sys plus 2 clk_cpu after rst_sys_n, and add a reset-order assertion that rst_cpu_n never rises before rst_sys_n.
Each side's flops leave reset on its own clock; asserting both nets together keeps write and read pointers in the same reset epoch so the gray comparison starts valid.