Inside the chip the integrator controls both ends of every path; at the pins the other end belongs to the board — the SPI flash that answers the second-stage load, the UART receiver sampling the boot banner, the GPIO sampling a wake button. I/O delay budgeting splits the interface period between chip and board so each side has a number it can close independently.
Take the interface window and subtract what is not yours. The SPI clock on NanoSoC is generated from the 24 MHz peripheral clock as clk_per / (2 x (CLKDIV + 1)), which is 6 MHz at the reset value, a 166.667 ns period. The SPI is mode 0: each side launches data on the falling SCK edge and the other side samples on the next rising edge, so the window for a transfer in either direction is half a period, 83.333 ns, not a whole bit time. Board flight time, the flash's clock-to-output and setup, and the pad delay of SCK itself are board, device and pad properties; what remains is the chip's budget. The same split applies to the UART at 115,384.6 baud, where each bit lasts 8.667 us and the chip's share is generous, and in reverse to outputs, where the chip's clock-to-pad budget must leave the receiver's setup intact. The integrator's artifact is one row per pin group: period, board share, chip share, and the constraint that carries the chip share.
# not-run: NanoSoC I/O budget sketches, NanoSoC teaching values
SPI at 6 MHz, mode 0: period 166.667 ns, capture window = half = 83.333 ns.
Illustrative values: flash setup 10 ns, flash clock-to-output 20 ns,
board flight 2 ns each way, SCK pad skew to MOSI 5 ns, SCK output pad 6 ns.
MOSI and CS out (chip to flash): chip clock-to-pad budget
= 83.333 - (10 + 2 + 5) = 66.333 ns.
MISO in (flash to chip), if captured synchronously by the SCK domain:
chip pad plus route budget = 83.333 - (6 + 2 + 20 + 2 + 10) = 43.333 ns,
because SCK leaves the pad late and the data must come back before
the next rising edge (the SCK round trip is part of the budget).
NanoSoC instead takes MISO through the X9 two-flop synchroniser: it is an
asynchronous input, so no input-delay constraint applies and the cost is
two clk_per of latency (83.333 ns), not a delay budget.
UART at 115,384.6 baud: bit period 8,666.667 ns; mid-bit sampling leaves
thousands of ns for the chip share: never the binding constraint.
GPIO wake input: synchroniser costs 2 peripheral clocks (83.333 ns); the
button is level-held, so latency is an envelope, not a frequency limit.The script turns the sketch into checks that can fail: it recomputes the half-period windows and the two chip budgets, and shows that the old whole-period reading would have hidden a violation.
period = 1 / 6e6 * 1e9 # ns, SPI clock = 24 MHz / (2 x (1 + 1))
assert abs(period - 166.667) < 0.001
window = period / 2 # mode 0: launch on falling edge, sample on rising edge
assert abs(window - 83.333) < 0.001
# Illustrative device and board values (teaching values, not a datasheet).
flash_setup, flash_tco, flight, pad_skew, sck_pad = 10.0, 20.0, 2.0, 5.0, 6.0
mosi_budget = window - (flash_setup + flight + pad_skew)
miso_budget = window - (sck_pad + flight + flash_tco + flight + flash_setup)
assert abs(mosi_budget - 66.333) < 0.001, mosi_budget
assert abs(miso_budget - 43.333) < 0.001, miso_budget
# A whole-period reading would give a larger, wrong number.
wrong_miso = period - (flash_tco + flight + flash_setup)
assert wrong_miso > miso_budget
# The check can fail: a 50 ns pad plus route delay on MISO violates the real budget
# but would have passed against the wrong whole-period budget.
chip_delay = 50.0
assert chip_delay > miso_budget and chip_delay < wrong_miso
# NanoSoC routes MISO through the X9 two-flop synchroniser: latency, not a delay budget.
sync_latency = 2 * (1 / 24e6) * 1e9
assert abs(sync_latency - 83.333) < 0.001
print(f"PASS: window {window:.3f} ns, MOSI budget {mosi_budget:.3f} ns, MISO budget {miso_budget:.3f} ns, sync latency {sync_latency:.1f} ns")One budgeted row per external interface with period, board share and chip share.
The chip share lands in the constraint set as an input or output delay, owned by the top level.
Slow interfaces carry short justifications, not full tables; effort follows binding constraints.
Audit the constraint set against the budget rows: every external pin group needs its delay constraint, and every delay constraint needs a budget row that justifies it. The classic failure is a pin timed by default — no constraint, passes signoff, fails on the board — caught by listing unconstrained endpoints. On NanoSoC the SPI output pins (SCK, MOSI, CS) are the ones to check first; the MISO, UART RX and GPIO inputs go through synchronisers and carry no delay budget, but still need their asynchronous treatment declared so a later change cannot silently unconstrain them.
Do not forget the inputs that never toggle during normal operation: the GPIO wake button and the reset pin are level-held, so their budget is a synchroniser latency plus a pull-and-drive check, not a frequency race. The failure to budget for is a floating input sampled at release, caught by a pin-state table for reset rather than by timing analysis. NanoSoC has no boot-mode strap (boot is fixed: ROM first, then the SPI load), so its example is the RESET_N pin, which must be low for more than 16 clk_per cycles (666.7 ns) before it counts. Every pin group gets a row even when the row says not binding; the row is what stops a later change from silently unconstraining it.
Because budget effort follows binding constraints. The UART bit period is 8.667 us against nanosecond chip delays — no analysis needed beyond writing the constraint. The SPI at 6 MHz has a real split to compute.