Time-domain questions separate candidates who memorised diagrams from candidates who can divide by a frequency. Reset release, clock ratios, interrupt latency and pad counts are four divisions the interviewer expects without a calculator. This lesson drills all four, with the pad drill labelled illustrative throughout because NanoSoC defines no package.
Formula: release waits for lock plus margin, each domain adds its synchroniser cycles, assertion lasts at least 16 domain clocks, and the chain runs slaves before masters. Worked: oscillator ready 1024 peripheral clocks (42.667 us), peripheral reset release 2 (83.333 ns), PLL start-up and lock 260 (10.833 us), clock gates 2 (83.333 ns), fabric reset 18 system clocks (187.5 ns), CPU reset 8 system plus 2 CPU clocks (93.75 ns), hardware total 53.948 us. The checker re-derives each step, the order and the total.
t_cpu, t_sys, t_per = 1 / 192e6, 1 / 96e6, 1 / 24e6
def steps(lock_clocks):
return {"xtal-ready": 1024 * t_per, "per-release": 2 * t_per, "lock": lock_clocks * t_per,
"gates": 2 * t_per, "sys-release": 18 * t_sys, "cpu-release": 8 * t_sys + 2 * t_cpu}
chain = steps(260)
total = sum(chain.values())
for name, seconds in chain.items():
print(f"{name:12s} {seconds * 1e6:9.3f} us")
assert abs(chain["xtal-ready"] * 1e6 - 42.667) < 0.002
assert abs(chain["lock"] * 1e6 - 10.833) < 0.002
assert abs(chain["cpu-release"] * 1e9 - 93.75) < 0.01
assert abs(total * 1e6 - 53.948) < 0.001, total
# Order rule: slaves and fabric first, CPU last.
order = list(chain)
assert order.index("sys-release") < order.index("cpu-release") == len(order) - 1
# Assertion floor: 16 cycles of the slow clock is the 666.7 ns debounce and width.
assert abs(16 * t_per * 1e9 - 666.67) < 0.01
# Variation: a 512-clock lock counter adds 256 slow clocks.
longer = sum(steps(260 + 256).values())
assert abs((longer - total) * 1e6 - 256 * t_per * 1e6) < 1e-9 and abs(longer * 1e6 - 64.615) < 0.001
print(f"total {total * 1e6:.3f} us; with a 512-clock lock {longer * 1e6:.3f} us")
print("PASS: release chain 53.948 us; CPU last; 16-cycle floor is 666.7 ns")Variation: the lock counter is extended to 512 slow clocks for margin. Predict the new total (53.948 plus 256 slow clocks of 41.667 ns = 64.615 us) before running; the checker has a total function so you can test your prediction.
Formula: divided clock f/N, aligned edges every least common multiple of the periods, crossing throughput limited by the slower side times width, synchroniser latency in source cycles = stages x f_src/f_dst. Worked: 8:4:1 ratio, edges align every 41.667 ns, the CPU-to-fabric crossing moves at most 384 MB/s peak, and a 2-stage slow-side synchroniser costs 16 fast clocks. The checker proves each.
import math
f_cpu, f_sys, f_per = 192e6, 96e6, 24e6
assert 24e6 * 32 == 768e6 and 768e6 / 4 == f_cpu and 768e6 / 8 == f_sys
periods = [1 / f_cpu, 1 / f_sys, 1 / f_per]
lcm_ns = periods[2] * 1e9 # one slow clock holds 8 fast and 4 fabric clocks
assert abs(lcm_ns - 41.667) < 0.002, lcm_ns
cross_peak = 4 * min(f_cpu, f_sys) / 1e6
assert abs(cross_peak - 384.0) < 0.01, cross_peak
sync_in_fast = 2 * f_cpu / f_per # 2 slow stages measured in fast clocks
assert abs(sync_in_fast - 16) < 0.01, sync_in_fast
print(f"align every {lcm_ns:.3f} ns; crossing peak {cross_peak:.0f} MB/s")
print(f"2 slow sync stages = {sync_in_fast:.0f} fast clocks")
print("PASS: 8:4:1 ratio, 384 MB/s ceiling, 16-clock sync cost")Variation: divide the CPU clock by 2 in a low-power mode (96 MHz). The ratio becomes 4:4:1: recompute the alignment and the sync cost (now 2 x 96/24 = 8 fast clocks) before running. The two clocks are equal in frequency but still treated as asynchronous at signoff, so the bridge stays.
Formula: T = sync in + controller + sync out + handler allowance, worst case adding one clock of alignment per crossing plus the longest higher-priority handler. Worked: DMA interrupt best 135.4 ns (26 CPU clocks), worst 182.3 ns (35 clocks) for the hardware path; against an illustrative 1 us deadline the hardware consumes 18.2 percent and the rest belongs to software. The checker proves the window and the budget share.
t_cpu, t_per = 1 / 192e6, 1 / 24e6
best = 2 * t_per + 1 * t_per + 2 * t_cpu
worst = best + t_per + t_cpu
deadline = 1e-6 # illustrative teaching value: handler deadline
assert abs(best * 1e9 - 135.4) < 0.1 and abs(worst * 1e9 - 182.3) < 0.1
assert abs(best / t_cpu - 26) < 0.1 and abs(worst / t_cpu - 35) < 0.1
share = worst / deadline
assert abs(share - 0.182) < 0.002, share
print(f"hardware {best * 1e9:.1f}..{worst * 1e9:.1f} ns = 26..35 clocks")
print(f"of 1 us deadline: hardware {share * 100:.1f}%, software {100 - share * 100:.1f}%")
print("PASS: 135.4..182.3 ns window; software owns 81.8% of the budget")Variation: a higher-priority handler runs 400 ns with preemption disabled. Add it to the worst case and check the deadline still holds (582.3 ns, 58.2 percent).
Formula: total pads = signals + power/ground + analog; supply pads from total current over current per pad, each with a ground partner; the simultaneously-switching-output limit per supply pair is a technology input; die side = pads per side x pitch. Worked with illustrative teaching values throughout: 120 signal pads, 4 analog, 2 A total at 100 mA per pad giving 20 supply pads and 20 ground pads, and an illustrative limit of 8 switching outputs per pair against 32 simultaneous outputs. The checker sizes the pad ring and says which rule binds, current or switching.
import math
signals, analog = 120, 4 # illustrative teaching values
i_total, i_per_pad = 2.0, 0.10 # A and A per supply pad, illustrative
supply_pads = math.ceil(i_total / i_per_pad) # each needs a ground partner
power_pads = 2 * supply_pads
ssn_limit = 8 # illustrative: max simultaneously switching outputs per supply pair
switching = 32 # illustrative: worst-case simultaneous outputs
ssn_pairs = math.ceil(switching / ssn_limit)
assert supply_pads == 20 and power_pads == 40, (supply_pads, power_pads)
assert ssn_pairs == 4, ssn_pairs
binding = "current" if supply_pads >= ssn_pairs else "SSN"
assert binding == "current"
total = signals + power_pads + analog
per_side, pitch_um = math.ceil(total / 4), 150 # illustrative pitch
side_mm = per_side * pitch_um / 1000
assert total == 164 and per_side == 41, (total, per_side)
print(f"pads {total} (sig {signals}, pwr {power_pads}, ana {analog}); {binding} sizes the ring")
print(f"side {side_mm:.2f} mm at {pitch_um} um pitch; SSN needs {ssn_pairs} pairs")
print("PASS: 164 pads; current sets 20 supply pairs, SSN needs only 4")Variation: the switching load doubles to 64 outputs. SSN now needs 8 pairs: still below the 20 set by current, so current still sizes the ring. Say which rule binds before running.
Every drill converts units explicitly: megahertz to nanoseconds, bytes to megabytes per second, clocks to shares of a deadline.
Illustrative inputs are labelled where NanoSoC defines nothing: deadlines, currents, pitches, SSN limits.
Variations are predicted before running: the prediction is the drill, the run is the marking.
Run all four checkers, then mistype one constant in each (lock 256 to 265, ratio 8 to 7, deadline 1 us to 1 ms, pitch 150 to 120) and confirm the assert that fires. A drill whose asserts stay green under wrong inputs is testing nothing; add the missing assert. Thirty-five minutes for all four with variations.
The side shrinks to half for the same 164 pads, but the pad count, the pair count and the SSN need do not move: pitch sizes the die, current and switching size the ring. Confusing the two is the classic pad-count error.