The boot flow's 12.581 ms total is a sum of sixteen steps, and each step is a latency from this table times a count. But not every step depends on every latency. If the 281.25 ns peripheral access were really 300 ns, only the four register accesses (one in the reset-cause read of step 9, three in the UART initialisation of step 11) would slip, by 75 ns in total: the banner and the SPI load are limited by their line rates. What does depend on the table is the checksum of step 15 (2048 single reads at 93.75 ns) and the DMA self-test of step 13. Link-by-link recomputation keeps the boot sum honest, and it is also the fastest way to find a slow path: convert each stage to nanoseconds in its own domain and the longest bar names itself.
Every access is a sum of stage costs, each in the cycles of its own domain: issue cycles on the CPU side, crossing cycles on each bridge, fabric address and data cycles, slave latency, and response crossing back. One access in flight at a time keeps the arithmetic blocking and exact. The DMA job adds pipeline fill and drain: N beats plus 8 clocks for read address, SRAM latency, the output register and the write path, so a 64-word job takes 72 system clocks, 750 ns. The interrupt path is two synchronisers plus the controller register, 135.4 ns best and 182.3 worst with alignment.
# NanoSoC teaching values: every NanoSoC latency recomputed, then the boot sum.
T_CPU = 1 / 192e6 * 1e9 # 5.2083 ns
T_SYS = 1 / 96e6 * 1e9 # 10.4167 ns
T_PER = 1 / 24e6 * 1e9 # 41.6667 ns
cpu_sram = (3 + 1 + 2 + 1) * T_SYS + (1 + 3) * T_CPU # 93.75
cpu_line = cpu_sram + 3 * 2 * T_CPU # 125.0, 3 more beats at 1 T_SYS each
cpu_fast = 10 * T_SYS + 4 * T_CPU # 125.0
cpu_slow = 9 * T_SYS + 4 * T_PER + 4 * T_CPU # 281.25
cdc_bridge = 2 * T_PER + 2 * T_PER + 2 * T_SYS + 2 * T_SYS # 208.33
dma_64 = (64 + 8) * T_SYS # 750.0 ns
irq_best = 2 * T_PER + 1 * T_PER + 2 * T_CPU # 135.4
irq_worst = irq_best + T_PER + T_CPU # 182.3
for name, got, want in [("sram", cpu_sram, 93.75), ("line", cpu_line, 125.0),
("fast", cpu_fast, 125.0), ("slow", cpu_slow, 281.25),
("cdc", cdc_bridge, 208.33), ("dma64", dma_64, 750.0),
("irq_best", irq_best, 135.4), ("irq_worst", irq_worst, 182.3)]:
assert abs(got - want) < 0.05, (name, got)
def boot_us(per_access_ns=cpu_slow, sram_word_ns=cpu_sram):
# the sixteen boot steps grouped; time in microseconds
hardware = (1024 + 2 + 260 + 2) * T_PER + 18 * T_SYS + (8 * T_SYS + 2 * T_CPU) # steps 1 to 7
steps = [
hardware,
24 * T_CPU, # 8: first fetch, a 4-word line fill
per_access_ns, # 9: read RST_CAUSE, one clk_per access
1024 * T_SYS, # 10: clear 4 KB of SRAM0, posted writes at 1 per clk_sys
3 * per_access_ns, # 11: UART init, three clk_per accesses
160 / (24e6 / (16 * 13)) * 1e9, # 12: banner at the UART line rate
5 * cpu_fast + 7 * cpu_fast + dma_64 + irq_best + 64 * cpu_sram, # 13: DMA self-test
8196 * 8 / 6e6 * 1e9, # 14: SPI load at 6 Mb/s
2048 * sram_word_ns, # 15: checksum, single reads
]
return sum(steps) / 1e3
total = boot_us()
assert abs(total - 12580.917) < 0.01 # 12.581 ms
# a 300 ns peripheral access moves only the four register accesses: 4 x 18.75 ns
assert abs((boot_us(per_access_ns=300.0) - total) * 1e3 - 75.0) < 0.01
# a 100 ns SRAM word moves the checksum (and the self-test read-back is a fixed row here)
assert abs(boot_us(sram_word_ns=100.0) - total - 12.8) < 0.01
print("PASS: 8 latencies recomputed, boot sum 12,580.917 us, a 300 ns register access slips boot by 75 ns, a 100 ns SRAM word by 12.8 us")Each latency row names every stage with its domain and cycle count, so a clock change invalidates exactly the rows it touches.
The DMA job formula states the 8-cycle fill and drain, not just N beats.
Interrupt latency carries best and worst, with the alignment term explicit; boot-time budgets say which steps depend on which latency.
Timestamp one access per row in simulation with a single transaction in flight and compare against the script: the numbers must match to the cycle. If the slow-register read exceeds 281.25, count the cdc handshake cycles first; one possible cause of an extra peripheral cycle is an access that crossed a synchroniser twice. Then rebuild the boot sum from the checked rows: step 14, the SPI load at 10.928 ms, should still dominate at 86.9 percent.
Yes: 256 plus 8 is 264 system clocks, times 10.4167 ns gives 2.75 us exactly. A longer time means backpressure on m_tready or bank contention, not a slower clock.