A power controller described in words drifts: one reviewer's isolate-then-reset is another's reset-then-isolate, and the chip implements whichever the RTL writer guessed. Written as a state machine with named states, named waits and named timeouts, the NanoSoC sequencer leaves nothing to guess, and this lesson is that machine with the NanoSoC counts on the arcs.
The machine idles in RUN, watching SW_OFF_REQ. The driver guarantees the quiesce contract: it writes the request only after DMA STATUS and SBR STATUS read idle, so the machine never samples live busy levels from the switchable domain. On the request it steps through ISOLATE, asserting iso_sw_en over 2 peripheral clocks; GATE_CLK for 2; ASSERT_RST for 2; SWITCH_OFF with the 64-clock ramp; and SLEEP, where it waits for a wake interrupt. Wake steps through SWITCH_ON with the same 64-clock ramp, WAIT_PGOOD for 8 clocks past power-good, RELOCK for the 256-clock PLL lock, CLK_OPEN for 2 clocks, ISO_RELEASE 4 system clocks later, and RST_RELEASE for the 18-plus-8 reset staging, landing back in RUN with RST_CAUSE bit 3 set. The table after the code gives each state its wait; the code omits the down-counters for space. Every wait state has a timeout arc to a safe state; the timeouts lesson names them.
// NanoSoC teaching values: power sequencer FSM, complete module.
// Waits live in the table in the text; counters are omitted here for space.
module nano_pmu_seq (
input logic clk_per,
input logic rst_per_n,
input logic sw_off_req,
input logic wake_irq,
input logic pgood,
input logic pll_lock,
output logic iso_sw_en,
output logic clk_gate,
output logic rst_sw_n,
output logic sw_open,
output logic seq_done
);
typedef enum logic [3:0] {
S_RUN, S_QUIESCE, S_ISOLATE, S_GATE_CLK, S_ASSERT_RST,
S_SWITCH_OFF, S_SLEEP, S_SWITCH_ON, S_WAIT_PGOOD, S_RELOCK,
S_CLK_OPEN, S_ISO_REL, S_RST_REL
} state_t;
state_t state, nxt;
always_ff @(posedge clk_per or negedge rst_per_n) begin
if (!rst_per_n) state <= S_RUN;
else state <= nxt;
end
always_comb begin
nxt = state;
case (state)
S_RUN: if (sw_off_req) nxt = S_QUIESCE;
S_QUIESCE: nxt = S_ISOLATE;
S_ISOLATE: nxt = S_GATE_CLK;
S_GATE_CLK: nxt = S_ASSERT_RST;
S_ASSERT_RST:nxt = S_SWITCH_OFF;
S_SWITCH_OFF:nxt = S_SLEEP;
S_SLEEP: if (wake_irq) nxt = S_SWITCH_ON;
S_SWITCH_ON: if (pgood) nxt = S_WAIT_PGOOD;
S_WAIT_PGOOD:nxt = S_RELOCK;
S_RELOCK: if (pll_lock) nxt = S_CLK_OPEN;
S_CLK_OPEN: nxt = S_ISO_REL;
S_ISO_REL: nxt = S_RST_REL;
S_RST_REL: nxt = S_RUN;
default: nxt = S_RUN;
endcase
end
assign iso_sw_en = (state == S_ISOLATE) || (state == S_GATE_CLK) ||
(state == S_ASSERT_RST) || (state == S_SWITCH_OFF) ||
(state == S_SLEEP) || (state == S_SWITCH_ON) ||
(state == S_WAIT_PGOOD) || (state == S_RELOCK) ||
(state == S_CLK_OPEN);
// Clocks are gated from GATE_CLK until CLK_OPEN opens them again.
assign clk_gate = (state == S_GATE_CLK) || (state == S_ASSERT_RST) ||
(state == S_SWITCH_OFF) || (state == S_SLEEP) ||
(state == S_SWITCH_ON) || (state == S_WAIT_PGOOD) ||
(state == S_RELOCK);
// rst_sys_n and rst_cpu_n stay asserted (low) from ASSERT_RST until RST_REL.
assign rst_sw_n = !((state == S_ASSERT_RST) || (state == S_SWITCH_OFF) ||
(state == S_SLEEP) || (state == S_SWITCH_ON) ||
(state == S_WAIT_PGOOD) || (state == S_RELOCK) ||
(state == S_CLK_OPEN) || (state == S_ISO_REL));
assign sw_open = (state == S_SWITCH_OFF) || (state == S_SLEEP);
assign seq_done = (state == S_RUN);
endmoduleState | Wait | Drives / does |
|---|---|---|
RUN | Until SW_OFF_REQ | Normal running; seq_done high |
QUIESCE | Request accepted | Driver contract check point |
ISOLATE | 2 clk_per | iso_sw_en asserted; PD_SW outputs clamped to 0 |
GATE_CLK | 2 clk_per | Clocks gated |
ASSERT_RST | 2 clk_per | rst_sys_n and rst_cpu_n asserted |
SWITCH_OFF | 64 clk_per ramp | Switch opened; rush current limited by staged turn-off/on |
SLEEP | Until wake interrupt | PD_SW off; isolation held |
SWITCH_ON | 64 clk_per ramp | Switch closed; rails charge |
WAIT_PGOOD | 8 clk_per past pgood | Settling margin |
RELOCK | 256 clk_per | PLL relock; wait on pll_lock |
CLK_OPEN | 2 clk_per | Clock gates open, resets still asserted |
ISO_RELEASE | 4 clk_sys later | Isolation released while resets are still held |
RST_RELEASE | 18 clk_sys, then 8 clk_sys + 2 clk_cpu | rst_sys_n, then rst_cpu_n; back to RUN |
The waits add up to the NanoSoC totals. Down: 2 + 2 + 2 + 64 = 70 clk_per, 2.917 µs. Up: (64 + 8 + 256 + 2) clk_per at 41.667 ns is 13,750 ns, plus (4 + 18 + 8) clk_sys at 10.417 ns is 312.5 ns, plus 2 clk_cpu at 5.208 ns is 10.42 ns: 14,072.9 ns. The script checks both sums and fails if any state's wait is changed without the total being revisited.
# Wait budget of the sequencer table, NanoSoC teaching values (NanoSoC wake sequence).
T_PER, T_SYS, T_CPU = 1e3 / 24, 1e3 / 96, 1e3 / 192 # ns
DOWN = {"ISOLATE": 2, "GATE_CLK": 2, "ASSERT_RST": 2, "SWITCH_OFF": 64}
UP_PER = {"SWITCH_ON": 64, "WAIT_PGOOD": 8, "RELOCK": 256, "CLK_OPEN": 2}
UP_SYS = {"ISO_RELEASE": 4, "RST_RELEASE_SYS": 18, "RST_RELEASE_CPU": 8}
UP_CPU = {"RST_RELEASE_CPU": 2}
down_clk = sum(DOWN.values())
up_ns = (sum(UP_PER.values()) * T_PER + sum(UP_SYS.values()) * T_SYS
+ sum(UP_CPU.values()) * T_CPU)
assert down_clk == 70, down_clk
assert abs(down_clk * T_PER - 2916.7) < 0.1
assert abs(up_ns - 14072.9) < 0.1, up_ns
# The wake order is isolation release (4 clk_sys) before rst_sys_n (18): the
# resets are still asserted when isolation is released.
assert UP_SYS["ISO_RELEASE"] < UP_SYS["RST_RELEASE_SYS"]
# Injected bug: a RELOCK table entry of 4 + 256 would break the total.
bad_ns = up_ns + 4 * T_PER
assert abs(bad_ns - 14072.9) > 100.0
print("PASS: down 70 clk_per = 2.917 us, up 14,072.9 ns; a stray +4 clk_per is caught")The wait table matches the state list one for one, and the down counts add to 70 clk_per down and 14,072.9 ns up.
Quiesce is a software contract, not a sampled level: SW_OFF_REQ is written only after the driver reads idle in DMA and SBR STATUS; the machine itself trusts the request.
Isolation stays asserted across every state from ISOLATE to CLK_OPEN inclusive; the output equation above is checked, not eyeballed.
Drive the FSM through both paths: a request with the driver-quiesce contract honoured must reach SLEEP, a wake must reach RUN with the cause bit set. Cover every arc, including the wake arriving in the same cycle as SLEEP entry. If the machine sticks in a wait state, the wait input (pgood, pll_lock, or the counter itself) is the suspect, which is the handshake lesson's territory.
The driver: quiesce is a software contract, so the request must follow idle reads in DMA STATUS and SBR STATUS. The machine trusts the request; the guard is the driver plus a power-aware test that requests sleep mid-job and checks nothing powers down.