A budget that spends every cycle on day one has nowhere to go when a block returns late. NanoSoC's timing closes with room to spare at these frequencies, which is exactly why it is the right place to learn the discipline cheaply: hold margin at the top, hand each block a budget tighter than the requirement, and spend the margin only through the change process.
Margin is unallocated budget held by the integrator, not slack left inside a block. The method: set a cycle allowance for the path, hold back spare cycles at the top, and split the remainder among the blocks with a little hand-out slack, so that each block is handed a budget tighter than the requirement but not exactly its measured cycles. A block that closes with room keeps its room against its own budget; it does not donate it silently to a neighbour. A block that misses by less than the top margin can be covered by a governed margin spend; a miss beyond the margin reopens the partition or the frequency, never the requirement.
Step | NanoSoC fetch-path example | Value |
|---|---|---|
Allowance (illustrative requirement) | Fetch must complete within | 9 sys + 4 cpu cycles |
Top-held margin | One spare sys cycle, teaching value | 1 sys cycle (10.4167 ns, 11.1% of the sys allowance) |
Handed to blocks | Allowance minus top margin | 8 sys + 4 cpu cycles |
Measured path | Fetch table rows added | 7 sys + 4 cpu |
Hand-out slack | Handed minus measured | 1 sys cycle, held by the memory wrapper |
Handed per block (sys) | Bridge crossing 3, fabric 2, memory 3 | 8 sys cycles in total |
The script prices the margin policy in cycles and checks that the measured fetch fits the handable total with the spare cycle untouched.
allow_sys, allow_cpu = 9, 4 # illustrative requirement, in cycles of the named clocks
spare_sys = 1 # top-held margin, NanoSoC teaching value
hand_sys = allow_sys - spare_sys
handed = {"bridge crossing": 3, "fabric": 2, "memory wrapper": 3} # sys cycles
measured = {"bridge crossing": 3, "fabric": 1 + 1, "memory wrapper": 2} # fetch table rows
assert sum(handed.values()) == hand_sys
assert sum(measured.values()) == 7 and 1 + 3 == allow_cpu # 7 sys + 4 cpu measured
assert all(measured[b] <= handed[b] for b in handed) # every block closes
slack = sum(handed.values()) - sum(measured.values())
assert slack == 1 # hand-out slack: a budget tighter than the requirement, not zero
def covered(miss_sys):
# a miss is covered by hand-out slack first, then by the top margin
return miss_sys <= slack + spare_sys
assert covered(2) and not covered(3) # the policy can fail: a 3-cycle miss reopens the plan
t_sys = 1 / 96e6 * 1e9
assert abs(spare_sys * t_sys - 10.4167) < 0.001 # margin held in cycles, converted once
margin_share = spare_sys / allow_sys
assert abs(margin_share - 1 / 9) < 1e-9
print(f"PASS: measured 7 sys + 4 cpu inside handed 8 sys + 4 cpu; slack {slack}, margin {spare_sys} sys cycle ({margin_share*100:.1f}%)")The margin share is written down once per project and applied to every handed budget.
Handed budgets name cycles and clocks; the margin is held in cycles of the named clock too, and converted to nanoseconds once.
A miss procedure exists before the first miss: cover from margin, restructure, or replan — in that order.
Recompute every handed budget from requirement minus margin whenever the requirement moves: a frequency change that forgets the margin silently spends it. For the link to analysis mechanics — how the handed numbers become constraints and how signoff proves them — see the STA course's constraint and signoff material, which this topic consumes but does not repeat.
Connect the handed budgets to the constraint set explicitly: each block's cycle allowance becomes its boundary constraints, owned by the top level and merged with the block's own mode set. The audit that closes the loop lists every handed budget against its constraint — an allowance with no constraint is unmanaged, and a constraint with no allowance is unbudgeted optimism. That consistency check between the budget table and the constraint merge is the chip-level discipline the timing signoff flow consumes.
The integrator covers it with a governed margin spend: record the draw, shrink the remaining margin, and continue. No replan needed, but the spend is visible in the ledger.