A chip without a throttling policy has two thermal modes: fine and emergency shutdown. The policy adds the middle gears: slow the work before stopping it, and resume cleanly when the heat passes. NanoSoC barely needs those gears, which makes it the ideal place to learn the shape of the policy without the pressure of a chip that cannot survive without it.
The policy is a trip table with hysteresis. An illustrative on-die sensor near the CPU and fabric watches the junction temperature: at the first trip the DMA stream is paused and the CPU is held to a duty-cycled schedule; at the second trip the chip drops to clock-gated IDLE; at the emergency trip, still below the 105 °C junction limit used in the previous lesson, it asserts a thermal reset through the reset controller. NanoSoC as documented has no thermal sensor, so the sensor and this policy are an extension a real chip would add. Each trip releases only after the temperature falls several degrees below the trip point, so the chip cannot chatter between gears. Recovery is ordered like a small wake: clocks reopen, the DMA resumes only from a clean descriptor boundary, and a sticky status bit records that throttling happened so the lab and the driver agree on what the performance dip was.
Level | Illustrative trip | Action | Release |
|---|---|---|---|
Normal | Below 85 °C | Full RUN | n/a |
Throttle 1 | 85 °C | Pause DMA stream, duty-cycle CPU | Fall below 80 °C |
Throttle 2 | 95 °C | Force clock-gated IDLE | Fall below 87 °C |
Emergency | 100 °C | Thermal reset via reset controller | Full re-sequence, sticky bit set |
# Throttling table sanity check, trips illustrative; limit from the previous lesson.
T_J_MAX = 105.0
LEVELS = [("throttle1", 85.0, 80.0), ("throttle2", 95.0, 87.0), ("emergency", 100.0, None)]
trips = [t for _, t, _ in LEVELS]
assert trips == sorted(trips) and trips[-1] < T_J_MAX # every trip below the limit
for name, trip, rel in LEVELS:
assert rel is None or trip - rel >= 5.0, name # hysteresis gap
def level(temp, current):
# Highest level whose trip is reached; hold a level until below its release.
for name, trip, rel in reversed(LEVELS):
if temp >= trip:
return name
for name, trip, rel in reversed(LEVELS):
if name == current and rel is not None and temp >= rel:
return name
return "normal"
assert level(86.0, "normal") == "throttle1"
assert level(82.0, "throttle1") == "throttle1" # held inside the hysteresis band
assert level(79.0, "throttle1") == "normal" # released below 80
# The earlier table's emergency trip at 110 C would sit above the limit:
assert not (110.0 < T_J_MAX)
print("PASS: trips 85/95/100 C all below 105 C; hysteresis holds throttle 1 until 80 C")Sensor placement follows the heat: one sensor near the CPU and fabric hotspot, one near the SRAM banks, voted so a single faulty sensor cannot force IDLE. The sensors themselves sit in PD_AON and run on the crystal clock, because a thermal guard that sleeps with the switchable domain guards nothing. Trip points are illustrative product values; the hysteresis gaps and the ordered recovery are the portable part any chip reuses.
Every trip names its action, its release point and the hysteresis gap; no trip releases at its own set point.
The emergency path is a reset with the cause recorded, never a clock stop the software must discover.
The sensors are placed, powered and clocked in the always-on island on the review diagram.
Test the policy with a forced-temperature model that ramps through every trip and back: each action must engage at its set point, hold through the hysteresis band, and release in order. Force a sensor disagreement and confirm the vote masks it while flagging the fault. If the chip oscillates between gears on the bench, the hysteresis gap is smaller than the sensor noise plus the thermal time constant; widen the gap, not the filter.
Because the guard must work in every state including SLEEP, when the PLL is off. A sensor in PD_SW or on a gated clock stops reporting exactly when the chip is hottest and darkest.