Software on NanoSoC knows three words, RUN, IDLE and SLEEP, and each word is a promise about latency, power and survival. IDLE must wake on any enabled interrupt with the PLL still locked; SLEEP may take 14.073 µs to wake but must burn barely a milliwatt. If the hardware's state table and the driver's idea of it disagree, the chip either never sleeps or never wakes. This lesson writes the table once, with the NanoSoC numbers attached.
RUN is everything on and clocked. IDLE keeps both domains powered with the PLL on, but the CPU clock is gated after a wait-for-interrupt and the DMA and stream-bridge clocks are gated by their enable bits, so any enabled interrupt resumes in a few cycles. SLEEP powers PD_SW off entirely, stops the PLL, keeps only the peripheral clock running, and holds SRAM0 in retention at 60 percent of its normal leakage. Entry to SLEEP is a software write to PWR_CTRL.SW_OFF_REQ followed by the hardware sequence; exit is a wake interrupt into nano_rstpwr, which sets RST_CAUSE bit 3 and resumes from the retained SRAM0 image instead of reloading.
State | PD_AON | PD_SW | PLL | Clocks | Entry | Exit |
|---|---|---|---|---|---|---|
RUN | On | On | On | All on | Boot or wake | CPU wait-for-interrupt, or software writes SW_OFF_REQ |
IDLE | On | On, CPU clock gated | On | clk_cpu gated, rest on | CPU wait-for-interrupt | Any enabled interrupt |
SLEEP | On, SRAM0 in retention | Off, outputs isolated | Off | Only clk_per on | PWR_CTRL.SW_OFF_REQ | Enabled GPIO, UART or power interrupt wakes nano_rstpwr |
The power roll-up below recomputes the NanoSoC totals from the per-block table: 82.74 mW in RUN, 16.89 mW in IDLE and 1.40 mW in SLEEP. Two ratios are worth memorising. IDLE costs about a fifth of RUN: 5.64 mW is leakage, 6.00 mW is the PLL, and the remaining 5.25 mW is the still-clocked PD_AON peripherals plus the gated CPU and fabric residue; gating the CPU still saves its 38.00 mW dynamic. SLEEP costs about one sixtieth of RUN: PD_SW leakage collapses to 0.06 mW through the switch, and PD_AON is dominated by the retained SRAM0 at 0.72 mW.
# NanoSoC state power roll-up, NanoSoC teaching values (NanoSoC power table).
# Dynamic power in mW per block: (RUN, IDLE, SLEEP); leakage handled below.
DYN = {
"cpu": (38.00, 1.90, 0.00), "icache": (4.00, 0.20, 0.00),
"axi_cdc": (3.00, 0.30, 0.00), "xbar": (5.00, 0.50, 0.00),
"dma": (3.00, 0.00, 0.00), "sbr": (2.00, 0.00, 0.00),
"axi2apb": (1.00, 0.10, 0.00), "apb_cdc": (0.50, 0.05, 0.00),
"sram1": (6.00, 0.30, 0.00), "switches": (0.00, 0.00, 0.00),
"sram0": (6.00, 0.30, 0.00), "rom": (1.00, 0.00, 0.00),
"uart": (0.30, 0.30, 0.03), "spi": (0.40, 0.40, 0.04),
"gpio": (0.20, 0.20, 0.02), "intc": (0.20, 0.20, 0.02),
"rstpwr": (0.50, 0.50, 0.05), "pll": (6.00, 6.00, 0.00),
}
LEAK = {"RUN": 5.64, "IDLE": 5.64, "SLEEP": 1.24}
dyn_run = sum(v[0] for v in DYN.values())
dyn_idle = sum(v[1] for v in DYN.values())
dyn_sleep = sum(v[2] for v in DYN.values())
assert abs(dyn_run - 77.10) < 0.005, dyn_run
assert abs(dyn_idle - 11.25) < 0.005, dyn_idle
assert abs(dyn_sleep - 0.16) < 0.005, dyn_sleep
tot = {s: round(d + LEAK[s], 2) for s, d in
[("RUN", dyn_run), ("IDLE", dyn_idle), ("SLEEP", dyn_sleep)]}
assert tot == {"RUN": 82.74, "IDLE": 16.89, "SLEEP": 1.40}, tot
assert tot["IDLE"] / tot["RUN"] < 0.25
assert tot["RUN"] / tot["SLEEP"] > 50.0
print("PASS: RUN 82.74 mW, IDLE 16.89 mW, SLEEP 1.40 mW; ratios hold")The state table names the entry trigger, the exit trigger and the wake latency class for every state.
The milliwatt totals reconcile with the per-block budget to the second decimal; IDLE and SLEEP carry the duty assumptions they were computed with.
Software's power driver handles exactly these three states, no hidden fourth mode.
Measure each state on the bench or in power-aware simulation with a fixed workload: RUN streaming one DMA job, IDLE in wait-for-interrupt with the DMA idle, SLEEP after SW_OFF_REQ with a GPIO wake armed. If IDLE reads near RUN, a clock enable is stuck on, usually the DMA or bridge bit in CLK_EN; if SLEEP reads high, the switch never opened or the PLL never stopped. The script above is the oracle for all three readings.
Either the sequence never ran because the request was never accepted (a secure-only RSTPWR write from the wrong security state), or the switch opened but the PLL kept running. Check PWR_STATUS.SW_ON first: if it never cleared, the request path is the suspect; if it cleared but power is high, the PLL or a CLK_EN bit is the leak.