Sleep is a promise that the chip can hear a wake-up while apparently off. That promise holds only if the right logic stays powered on the right clock. Put a wake path in the switchable domain and sleep becomes coma. This lesson places each always-on block once, then shows where a wake timer would go if a derivative added one. Its one job is placement; the watchdog extension is next.
Everything that must work in sleep lives in PD_AON on clk_per: the NanoSoC specification's always-on list is exactly RSTPWR, PLL, INTC, UART, SPI, GPIO, SRAM0 (arrays, in retention) and ROM. The SLEEP state keeps clk_per running while the PLL, clk_cpu and clk_sys are off, so every block that must react in sleep has to be on the crystal clock. Three of the eight can wake the chip: GPIO and UART through their interrupt lines and the controller itself through irq_pwr. SPI stays powered because the boot ROM loads the image through it, but it is not a wake source. The ROM and the SRAM0 arrays are always-on so that the retained image and the reset vector survive.
NanoSoC has no timer, real-time clock or watchdog. If a derivative added a wake timer, it would sit in PD_AON on clk_per for the same two reasons: the supply because sleep must not kill it, the clock because the PLL is off in sleep. A free-running 32-bit counter on 24 MHz wraps in 178.96 s, so timekeeping needs a prescaler: divide by 24,000,000 for a 1 Hz tick (25 bits) or by 24,000 for a 1 kHz wake tick (15 bits), both illustrative teaching values. The prescaler must be an exact integer divide of 24 MHz, or the tick drifts.
CLK_PER = 24e6 # NanoSoC-specification clock
ALWAYS_ON = {"rstpwr", "pll", "intc", "uart", "spi", "gpio", "sram0", "rom"} # power domain list
WAKE_SOURCES = {"gpio", "uart", "rstpwr"} # GPIO, UART, irq_pwr from the controller
def check_placement(block, domain, clock, wakes):
errors = []
if block in ALWAYS_ON and domain != "PD_AON":
errors.append(block + ": must be PD_AON")
if wakes and (domain != "PD_AON" or clock != "clk_per"):
errors.append(block + ": a wake source needs PD_AON and clk_per")
if wakes and block not in WAKE_SOURCES:
errors.append(block + ": not a NanoSoC wake source")
return errors
assert check_placement("gpio", "PD_AON", "clk_per", True) == []
assert check_placement("spi", "PD_AON", "clk_per", False) == []
# The checks can fail: a wake timer placed in PD_SW or on clk_sys is rejected,
# and so is SPI claimed as a wake source.
assert check_placement("timer", "PD_SW", "clk_sys", True) != []
assert check_placement("uart", "PD_AON", "clk_sys", True) != []
assert check_placement("spi", "PD_AON", "clk_per", True) != []
# Extension arithmetic (illustrative): exact prescaler divides of 24 MHz.
for tick_hz, width in [(1, 25), (1000, 15)]:
div = int(CLK_PER // tick_hz)
assert CLK_PER % tick_hz == 0 # exact: no drift
assert (div - 1).bit_length() == width
assert CLK_PER % 7 != 0 # a 7 Hz tick would not be exact
wrap_s = 2 ** 32 / CLK_PER
assert abs(wrap_s - 178.96) < 0.01, wrap_s
print("PASS: placement rules reject bad wake blocks; prescalers exact; raw 32-bit wrap 178.96 s")Every always-on block states its clock and power domain with the reason: survival or rate.
The wake sources are exactly GPIO, UART and irq_pwr; SPI is powered for boot but is not among them.
No wake-path logic sits outside PD_AON; the supply check proves it, and any added timer is checked the same way.
In power-aware simulation, enter sleep and confirm each always-on block still responds while every system-domain block freezes. A wake that never arrives points at the source's power domain or clock before its enable bit. If a timer extension exists, read it across a sleep-wake cycle and confirm monotonic time with no jump: a jump means the prescaler was reset on wake.
A fractional divide accumulates phase error on every tick, so wake intervals drift against wall time. An exact integer divide keeps every tick the same length forever.