Bandwidth decides whether the chip keeps up; latency decides whether software meets its deadlines. The UART holds one byte, and at 115,384.6 baud a new byte arrives every 86.7 us (10 bits at 115,384.6 baud), so a handler that starts 182.3 ns after the interrupt rises has about 475 times that margin to spare: hardware interrupt latency is not what loses a byte, software latency is. By contrast, a line fill is 24 clk_cpu cycles for four 4-byte instructions, so straight-line code pays 6 clk_cpu cycles per instruction on average before anything is wrong. Its one job is the latency method: the blocking measurement discipline, the stage table it checks, and the interrupt path with its best and worst cases.
Latency is measured blocking — one access in flight, nothing queued — because that is what the latency table quotes and what the boot flow assumes. The CPU single-word read from SRAM costs 18 CPU cycles, 93.75 ns: issue, three system clocks of crossing, fabric address, two memory cycles, fabric data, three CPU cycles back. The 4-beat line fill adds three beats at one per system clock, 24 CPU cycles or 125 ns. A fast-APB register read costs 24 CPU cycles or 125 ns; a read of a clk_per peripheral register costs 9 system clocks plus 4 peripheral clocks plus 4 CPU cycles, 281.25 ns. The script below recomputes each from its stage list so the measurement has an expected value before the run.
The interrupt path gets the same treatment with a range, not a number: 135.4 ns best case, 182.3 worst, from the two-flop synchroniser into the slow domain, the controller register, and the two-flop synchroniser into the CPU, plus alignment on each end. The test forces irq_dma and timestamps irq_dma rising and irq_cpu arriving at the core input across many trials, with the phase between the two clocks swept in fixed steps; the spread must sit inside the 26-to-35 CPU-cycle window. The handler entry is later still, because it adds the core's own pipeline and vector time, so it is a software-latency number and not part of this table; the DMA STATUS write is the clear, not the start. A pulse shorter than two peripheral clocks, 83.3 ns, may be missed entirely — that is not a miss, that is the synchroniser contract, and the DMA level interrupt holds until cleared precisely so it never depends on a pulse.
# Latency budget check, NanoSoC teaching values: sum the stage delays in a common
# unit, bound the interrupt spread, and show that a stage fault is caught.
T_CPU, T_SYS, T_PER = 1e9 / 192e6, 1e9 / 96e6, 1e9 / 24e6
sram_read = 7 * T_SYS + 4 * T_CPU # 7 system clocks plus issue and response on the CPU clock
assert abs(sram_read - 93.75) < 0.01, sram_read
line_fill = sram_read + 3 * T_SYS # three more beats, one system clock each
assert abs(line_fill - 125.0) < 0.01, line_fill
per_read = 9 * T_SYS + 4 * T_PER + 4 * T_CPU # clk_per APB segment register
assert abs(per_read - 281.25) < 0.01, per_read
best = 2 * T_PER + 1 * T_PER + 2 * T_CPU # sync, controller, sync out
worst = best + T_PER + T_CPU # one cycle of alignment per crossing
assert abs(best - 135.4) < 0.1 and abs(worst - 182.3) < 0.1
# a fill with one extra system clock per later beat measures 30 CPU cycles, not 24
assert round((line_fill + 3 * T_SYS) / T_CPU) == 30 and round(line_fill / T_CPU) == 24
# the measured spread over a phase sweep must sit in the window; an outlier is a failure
def in_window(ns):
return best - 0.1 <= ns <= worst + 0.1
sweep = [best + k * (worst - best) / 4 for k in range(5)]
assert all(in_window(x) for x in sweep) and not in_window(190.0) and not in_window(120.0)
print(f"sram {sram_read:.2f} ns, fill {line_fill:.1f} ns, per {per_read:.2f} ns")
print(f"irq {best:.1f}..{worst:.1f} ns = {best / T_CPU:.0f}..{worst / T_CPU:.0f} cpu")
print("PASS: blocking latencies and the 26..35-cycle irq window hold")Every latency test states blocking or queued before quoting a number; queued results never compare against this table.
The interrupt test timestamps irq_dma and irq_cpu, not the handler entry, and reports the spread over phase sweeps, not a single timestamp.
The pulse-width contract — levels held two slow clocks minimum — is asserted wherever a source could be short.
A latency overrun is localised by stage: timestamp the crossing entry and exit, the fabric address and data, and the response. An extra slow clock at the crossing means the FIFO was full — the test was not blocking and the result belongs to the queueing analysis, not this table. An interrupt that never arrives after a force is wiring; one that arrives outside the window is a synchroniser or phase assumption to revisit.
The beats after the first: 30 minus 24 is 6 CPU cycles or 3 system clocks, exactly one extra system-clock bubble on each of the three later beats. Check arbitration contention or a bridge inserting a wait — the measurement stopped being blocking.