An operating point is a promise that a voltage, three frequencies and every crossing between them work together, and switching clocks between points with a plain multiplexer produces runt pulses that crash the chip at random. The OPP table writes the promises down; the glitch-free switch keeps them while moving. This lesson builds both for NanoSoC's gears.
The table below extends NanoSoC's single characterised point with two illustrative lower gears: the voltages, the three domain frequencies and the use case per row. All rows except the first are illustrative; the first row is the NanoSoC point. One more fact bounds the table. NanoSoC has a single 0.9 V supply for PD_AON and PD_SW, and no level shifters, so lowering the voltage lowers it for every block on the chip, including the always-on logic, the ROM and the SRAM0 arrays. On one shared rail every block must be characterised at every voltage in the table, and the lowest operating point is bounded by the memory minimum operating voltage and the retention voltage, so write both into the OPP table. Scaling PD_SW alone would need two rails and level shifters, which NanoSoC does not have. Switching between gears uses a glitch-free clock multiplexer with a handshake from each source domain, so the output never sees a runt: the switch stops the output cleanly, changes source, and restarts, with a specified dead time. The script below checks the table's consistency, every frequency an exact divide of its row's PLL or crystal source, and prices a transition at the illustrative regulator rate.
# not-run: NanoSoC operating-point table; only OPP0 is a NanoSoC teaching value
OPP0 RUN : 0.90 V, cpu 192 MHz, sys 96 MHz, per 24 MHz (NanoSoC point)
OPP1 EFFICIENT (illustrative): 0.75 V, cpu 96 MHz, sys 64 MHz, per 24 MHz
OPP2 LOW-IDLE (illustrative): 0.70 V, cpu off, sys 24 MHz, per 24 MHz
Limits on the shared rail (illustrative): memory minimum operating voltage 0.70 V,
SRAM0 retention voltage 0.60 V. Every OPP voltage must be at or above 0.70 V.
Switching: glitch-free mux with per-source handshake; dead time 4 slow clocks.
Regulator slew (illustrative): 10 mV/us, so 0.90 -> 0.75 V costs 15 us.# OPP consistency: frequencies divide their sources; voltages respect the shared-rail
# memory limit; transition time priced. OPP0 NanoSoC teaching values; OPP1/2 and limits illustrative.
MEM_VMIN, RET_V = 0.70, 0.60 # illustrative memory minimum and retention voltages
OPP = {"OPP0": {"v": 0.90, "cpu": 192.0, "sys": 96.0, "per": 24.0, "src": 768.0},
"OPP1": {"v": 0.75, "cpu": 96.0, "sys": 64.0, "per": 24.0, "src": 768.0},
"OPP2": {"v": 0.70, "cpu": 0.0, "sys": 24.0, "per": 24.0, "src": 24.0}}
def check(table):
problems = []
for name, r in table.items():
if r["cpu"] and abs(r["src"] / r["cpu"] - round(r["src"] / r["cpu"])) > 1e-9:
problems.append(name + ": cpu not an exact divide")
if abs(r["src"] / r["sys"] - round(r["src"] / r["sys"])) > 1e-9:
problems.append(name + ": sys not an exact divide")
if r["v"] < MEM_VMIN: # one shared rail: PD_AON and the memories scale too
problems.append(name + ": below memory minimum voltage")
return problems
assert check(OPP) == []
# Injected bugs: a 0.60 V point sits below the memory minimum; 100 MHz does not divide 768.
bad = dict(OPP, OPP3={"v": 0.60, "cpu": 96.0, "sys": 48.0, "per": 24.0, "src": 768.0})
assert check(bad) == ["OPP3: below memory minimum voltage"]
bad2 = dict(OPP, OPP4={"v": 0.80, "cpu": 100.0, "sys": 50.0, "per": 24.0, "src": 768.0})
assert "OPP4: cpu not an exact divide" in check(bad2)
# Regulator slew 10 mV/us illustrative: OPP0 -> OPP1 voltage step.
SLEW_MV_PER_US = 10.0 # illustrative regulator dependence
step_us = abs(OPP["OPP0"]["v"] - OPP["OPP1"]["v"]) * 1000.0 / SLEW_MV_PER_US
assert abs(step_us - 15.0) < 1e-9, step_us
print("PASS: OPP table consistent; 0.60 V point and 100 MHz point rejected; 150 mV step costs 15.0 us")The OPP table marks which row is characterised fact and which rows are illustrative targets with owners and dates.
The clock switch is a handshake mux with a specified dead time, and a plain-multiplexer switch appears nowhere.
Each OPP row names its rechecked items: STA corners, synchroniser latencies at the new ratios, and the peripheral baud rates that divide from changed clocks.
Switch between every pair of gears in both directions while traffic crosses every domain bridge, and check no transaction corrupts and no interrupt is lost: the dead time must be absorbed by the bridge FIFOs, not by the endpoints. Recompute the UART baud and SPI clocks at each OPP; the crystal-derived peripheral clock is constant across OPP0 and OPP1 by construction, which is why the slow peripherals never reconfigure. If a switch crashes only in one direction, the dead time or the handshake from the stopped source is the suspect.
Because every peripheral baud rate, the 115,384.6 baud UART and the 6 Mb/s SPI, plus all the APB access latencies, stay valid without recomputation. Only the CPU and fabric timing change, which bounds the re-verification to the fast domains. The shared rail still scales the always-on peripherals and memories, so their low-voltage characterisation is not optional.