The CPU wants the fastest clock the timing will close; the UART wants the slowest clock that still oversamples its baud rate; the fabric wants whatever moves bursts without becoming the bottleneck. One clock for all three wastes power in the peripherals or starves the CPU. Three clocks is the smallest set that gives each job what it needs, and integer ratios keep every crossing cheap to reason about. This lesson makes that trade explicit. Its one job is the choice; how pairs sign off is next.
The fast domain, clk_cpu at 192 MHz, holds the core, its instruction cache and the CPU side of the asynchronous AXI bridge: exactly the logic whose performance scales with frequency. The system domain, clk_sys at 96 MHz, holds the fabric, the memories, the DMA, the bridge and the fast APB segment: the logic that must keep up with bursts but gains nothing from core speed. The slow domain, clk_per at 24 MHz, holds the UART, SPI, GPIO, interrupt controller and reset controller: peripherals whose rates are 115.4 kbaud (UART) and 6 Mb/s (SPI) against a 24 MHz clock, plus the always-on logic that must run while the PLL sleeps. Ratios 8:4:1 mean one slow period holds exactly 4 system and 8 CPU periods, which bounds synchroniser latency to small whole numbers and keeps the crossing table readable.
# Domain choice arithmetic: every rate the slow domain must still meet.
UART_BAUD = 24e6 / (16 * 13) # NanoSoC teaching values: clk_per, 16x, BAUDDIV 13
assert abs(UART_BAUD - 115384.6) < 0.1, UART_BAUD
SPI_CLK = 24e6 / (2 * (1 + 1)) # CLKDIV reset 1
assert SPI_CLK == 6e6, SPI_CLK
# The slow domain still oversamples comfortably: 24 MHz / 115 kbaud ~= 208x.
assert 24e6 / UART_BAUD > 200
# Fabric peak is clock-limited: 4 B x 96 MHz = 384 MB/s; the burst-16
# sustained rate is 384 x 16/17 = 361.4 MB/s (NanoSoC specification, bandwidth table).
peak_mb = 4 * 96e6 / 1e6
assert peak_mb == 384.0
assert round(peak_mb * 16 / 17, 1) == 361.4
# The slow domain would NOT serve a clock-halving: 12 MHz gives 104x
# oversampling of the fixed 115.4 kbaud only if the divisor halves too.
assert not (12e6 / UART_BAUD > 200)
print("PASS: 24 MHz still serves UART/SPI; 96 MHz fabric peaks at 384, sustains 361.4 MB/s")Each domain has a stated reason tied to a rate: CPU speed, burst throughput, always-on survival.
All ratios are small integers, and any proposed new domain states its ratios before its name.
No block sits in a faster domain than its rate needs; the slow peripherals are the test of this.
Check the slowest job per domain against its clock: the UART baud divider, the SPI divider and the interrupt synchroniser hold times all derive from 24 MHz, and halving it must break them loudly in simulation. If a peripheral works at any clock, its rate logic is over-margined or secretly asynchronous; either deserves a question.
They bound latency to small whole destination cycles, align edges for testable behaviour, and let one timing analysis cover the pair. Non-integer ratios still work through the same synchronisers but make every budget a worst-case sweep.