Skip to content

08 · Power Management & Thermal

A battery-powered or fanless product lives or dies by power management done right. Linux's PM stack spans four largely independent layers — CPU frequency, CPU/SoC idle states, runtime device power, and thermal governance — and a bug in any one of them shows up as the same two symptoms: the board is hotter than it should be, or the battery drains faster than the spec sheet promises.

cpufreq: how fast the CPU runs

$ cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_available_governors
performance powersave ondemand conservative schedutil
$ cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
schedutil
$ cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq
800000
$ echo performance > /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor

schedutil (the modern default) reads scheduler utilization directly and scales frequency proportionally — generally the right choice unless you have a specific reason to pin a governor. performance locks max frequency (useful for repeatable benchmarking, wasteful for a battery product); powersave locks minimum (useful for a thermally-constrained enclosure with no performance requirement).

Per-core governors matter on asymmetric (big.LITTLE-style) SoCs — setting performance on cpu0 and leaving schedutil on cpu4 is a legitimate pattern for pinning one real-time-ish workload to a known-fast core while letting everything else scale normally.

CPU idle states (C-states)

$ cat /sys/devices/system/cpu/cpu0/cpuidle/state*/name
WFI
CPU_OFF
$ cat /sys/devices/system/cpu/cpu0/cpuidle/state1/latency
    500
$ cat /sys/devices/system/cpu/cpu0/cpuidle/state1/usage
    128441

Deeper idle states (CPU_OFF, powering down the core entirely) save more but cost more to exit — latency is in microseconds and is exactly the number a real-time deadline calculation must account for. A cpuidle governor picking too deep a state for a workload with frequent short wakeups (a sensor polled every 2 ms, say) burns more power re-entering and exiting idle repeatedly than it would have spent just staying awake — this is a real, measurable failure mode called "C-state thrashing," and it's invisible unless you're actually watching usage counters and power draw together.

Runtime PM: devices sleep independently of the CPU

A device (I2C controller, USB PHY, camera sensor) can suspend itself while the CPU stays fully awake, if nothing is actively using it:

static int mydev_probe(struct platform_device *pdev)
{
    pm_runtime_enable(&pdev->dev);
    ...
}

static int mydev_open(struct inode *inode, struct file *filp)
{
    int ret = pm_runtime_get_sync(dev);  /* wakes device, blocks until ready */
    if (ret < 0) {
        pm_runtime_put_noidle(dev);
        return ret;
    }
    return 0;
}

static int mydev_release(struct inode *inode, struct file *filp)
{
    pm_runtime_mark_last_busy(dev);
    pm_runtime_put_autosuspend(dev);  /* suspends after idle timeout */
    return 0;
}
$ cat /sys/bus/platform/devices/30a20000.i2c/power/runtime_status
active
$ cat /sys/bus/platform/devices/30a20000.i2c/power/runtime_suspended_time
842103

Trap: every pm_runtime_get_sync (or its modern non-deprecated sibling pm_runtime_resume_and_get) must be matched by exactly one pm_runtime_put* — an unbalanced get leaves the device's usage count above zero forever, and it never autosuspends again for the rest of uptime. This bug is invisible functionally (the device still works, it just never sleeps) and shows up only as "why is idle power 40 mW higher than the reference design," which is exactly the kind of regression that survives code review and ships.

System suspend: suspend/resume

static int mydev_suspend(struct device *dev)
{
    struct mydev_priv *priv = dev_get_drvdata(dev);

    disable_irq(priv->irq);
    writel(0, priv->regs + REG_CTRL);   /* power the block down */
    return 0;
}

static int mydev_resume(struct device *dev)
{
    struct mydev_priv *priv = dev_get_drvdata(dev);

    writel(CTRL_ENABLE, priv->regs + REG_CTRL);  /* restore state */
    enable_irq(priv->irq);
    return 0;
}

static DEFINE_SIMPLE_DEV_PM_OPS(mydev_pm_ops, mydev_suspend, mydev_resume);
$ echo mem > /sys/power/state
[   88.201113] PM: suspend entry (deep)
[   88.220441] Disabling non-boot CPUs ...
...
[   88.998821] PM: suspend exit

Trap: any hardware register state written in probe but not re-written in resume is lost across suspend-to-RAM on most SoCs — the block is genuinely powered off, not just clock-gated. A driver that "forgets" one register write in resume works perfectly through every test that never suspends, then silently misbehaves on every device after its first sleep/wake cycle in the field — this is one of the highest-yield things to specifically test before shipping any PM support at all.

Thermal governance

$ cat /sys/class/thermal/thermal_zone0/temp
68500
$ cat /sys/class/thermal/thermal_zone0/trip_point_0_temp
85000
$ cat /sys/class/thermal/thermal_zone0/trip_point_0_type
passive
$ cat /sys/class/thermal/cooling_device0/type
thermal-cpufreq-0

Passive trips throttle (usually via cpufreq) as temperature approaches the limit; a critical trip triggers an actual shutdown to protect silicon. Device tree wires zones to trips to cooling devices:

thermal-zones {
    cpu-thermal {
        polling-delay-passive = <250>;
        polling-delay = <1000>;
        thermal-sensors = <&tmu 0>;

        trips {
            cpu_alert: cpu-alert {
                temperature = <85000>;
                hysteresis = <2000>;
                type = "passive";
            };
            cpu_crit: cpu-crit {
                temperature = <95000>;
                hysteresis = <2000>;
                type = "critical";
            };
        };

        cooling-maps {
            map0 {
                trip = <&cpu_alert>;
                cooling-device = <&cpu0 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>;
            };
        };
    };
};

Trap: a fanless enclosure design validated only at room temperature often discovers its actual thermal trip behavior for the first time in a 40°C ambient field test — throttling that never triggered on a lab bench can cut sustained CPU frequency by more than half in a hot enclosure, turning a "fast enough" product into a visibly sluggish one purely from enclosure thermal design, with zero software change.

Traps

  • Unbalanced pm_runtime_get/put — see above; the single most common silent idle-power regression.
  • Missing register restore in resume — the single most common "worked in the lab, broke after the first field suspend" bug.
  • Trip points set from a datasheet's absolute maximum instead of the enclosure's actual sustained thermal ceiling — a trip at the silicon's survivable limit still lets sustained throttling degrade UX badly before ever reaching the trip; tune trips from real enclosure thermal testing, not just the SoC datasheet.

Cheat sheet

Interface Purpose
scaling_governor Pick CPU frequency policy (schedutil default)
cpuidle/stateN/{latency,usage} Idle-state cost and how often it's used
pm_runtime_get_sync/put_autosuspend Per-device sleep independent of CPU
SIMPLE_DEV_PM_OPS Wire suspend/resume callbacks into a driver
echo mem > /sys/power/state Trigger suspend-to-RAM
/sys/class/thermal/thermal_zoneN/temp Current temperature reading
trip_point_N_type (passive/critical) Throttle vs. shutdown behavior

On verification

cpufreq/cpuidle/runtime-PM/thermal sysfs interfaces and the DT thermal binding were checked against the documented kernel PM and thermal frameworks; the suspend/resume driver code follows the documented dev_pm_ops contract but was not compiled or exercised through an actual suspend/resume cycle on real hardware from this machine.

How It Actually Works

cpufreq and cpuidle are two independent decision loops with different knobs, one for "how fast" and one for "how off." cpufreq's governor (schedutil on modern kernels) is fed CPU utilization directly from the scheduler's per-run-queue load tracking and, on that signal, requests a new OPM (operating performance point — a paired voltage/frequency step from the SoC's opp-table DT node) from the platform's clk/regulator drivers, which reprogram the PLL and voltage regulator through whatever SoC-specific PMIC I2C/SPI protocol is involved — a real, non-trivial hardware transaction, which is why frequency transitions have measurable latency and governors hysteresis-dampen how often they'll trigger one. cpuidle instead runs when the scheduler finds nothing runnable on a CPU, and picks among idle states (WFI, clock-gated, power-gated) whose exit_latency/target_residency values (again from DT) are used to predict whether the next wake will happen soon enough that a deep, slow-to-exit state isn't worth entering — two separate governors, reacting to load and idleness respectively, that only interact indirectly through the same CPU.

Thermal throttling is a userspace-invisible feedback loop closed entirely in-kernel. A thermal_zone's polling (or interrupt-driven, on SoCs with a real thermal-sensor IRQ) temperature reads are compared against trip_point thresholds from DT; crossing one invokes a bound cooling device — typically the same cpufreq framework, capped via freq_qos_add_request to a ceiling below what the governor would otherwise pick — so the CPU can visibly stay "at" a frequency the governor never asked for. This is why a thermally-throttled board shows utilization pegged at 100% while frequency stays pinned low: the scheduler's utilization signal and the QoS-capped frequency ceiling are computed independently, and the cap simply wins.

Exercise

(1) Add suspend/resume callbacks to Module 2's mydev platform driver that correctly save and restore the one register it writes in probe, and explain what observable symptom you'd expect if resume were accidentally deleted. (2) Given cpuidle usage counters showing state1 entered 50,000 times in one minute with a 500 μs exit latency each, estimate the idle-transition overhead and explain whether this workload is a candidate for C-state thrashing. (3) One paragraph: your fanless product passes every functional test at room temperature but a customer reports it "feels slow" after 20 minutes of continuous use in a warm room. Describe the exact thermal-zone/trip data you'd pull first and what each possible reading would tell you.