This lesson has one job: turn the path of the previous lesson into a budget the integrator can sign against a deadline. A latency number without a deadline is trivia; a deadline without a latency budget is hope. NanoSoC has a genuine deadline to check: the UART holding register is one byte deep, so a received byte must be collected before the next one finishes arriving, or it is lost. The lesson prices the worst case end to end and asks whether the UART survives it.
Total latency has four terms: synchroniser delay into the controller, controller decision time, CPU entry time, and the software prologue before the handler touches the peripheral. The first three are the routing topic's 182.3 ns worst case, which is the bound for a clk_sys source and a safe upper bound for the clk_per sources too. CPU entry is a handful of cycles to flush and fetch the vector; the prologue is the handler's own saving of state before it reads the peripheral. On top of the sum sits two kinds of waiting: the longest non-interruptible section the CPU may be inside when the irq arrives, and any higher-priority handler already running, because fixed priority means the UART handler waits behind power and DMA service. The budget is only closed when the worst pile-up still beats the deadline.
The UART runs at 115,384.6 baud with 10 bits per byte, so a byte arrives every 86.67 us and the one-byte holding register must be drained inside that window. The script below builds the full worst case: routing, CPU entry, prologue, the handler's own read of the UART DATA register (281.25 ns, a clk_per register read from the CPU), one higher-priority DMA handler ahead of it, and a short non-interruptible section, then checks the margin. All clock figures are NanoSoC teaching values; the entry, prologue and critical-section figures are labelled illustrative and kept consistent everywhere they appear.
T_CPU = 1 / 192e6
routing_worst = 182.3e-9 # worst case for a clk_sys source; used as a bound here
cpu_entry = 10 * T_CPU # illustrative: flush + vector fetch
prologue = 40 * T_CPU # illustrative: save state, read TOP
data_read = 281.25e-9 # handler's own DATA read, clk_per register
dma_handler = 200 * T_CPU # illustrative: higher-priority DMA service
crit_section = 50 * T_CPU # illustrative: longest non-interruptible window
total = (routing_worst + cpu_entry + prologue + data_read
+ dma_handler + crit_section)
baud = 24e6 / (16 * 13) # UART BAUDDIV reset value 13
byte_time = 10 / baud # 10 bits per byte on the wire
ratio = byte_time / total
print(f"worst-case UART latency: {total * 1e6:.3f} us")
print(f"byte arrival period: {byte_time * 1e6:.3f} us")
print(f"deadline-to-cost ratio: {ratio:.1f}x")
assert total < byte_time
assert 42 < ratio < 44
print("PASS: worst-case latency 2.03 us against an 86.7 us deadline")Every latency term names its source: NanoSoC reference numbers for routing and register reads, illustrative numbers labelled as such for software terms.
The worst case includes preemption by every higher-priority input, not just the lonely-interrupt case.
The tightest peripheral deadline in the design is named, and the margin is written down, not waved at.
Stress the pile-up in simulation: assert the UART receive line while the DMA handler runs long, and confirm the byte is collected in time. If the margin ever inverts, the fixes in order of cheapness are: shorten the higher-priority handlers, raise the threatened input's priority in the next revision, deepen the peripheral holding, or quicken the peripheral clock. Never fix a missed deadline by trimming the synchroniser: those two flops are load-bearing.
It waits behind every higher-priority handler that is pending plus the routing, entry and prologue terms, unbounded if a chatty high-priority source never stops. There is no wire-speed deadline because GPIO levels persist until the pin condition changes; software polls or debounces at its own pace.