# CPU Power and Thermal Management MontaukOS manages CPU frequency, idle states, and package temperature on Intel hardware. Without this, a modern many-core laptop part (the reference target is a TUXEDO Gemini Gen2 with an i9-13900HX, 24 cores / 32 threads) runs at whatever P-state the firmware left it, never drops below C1 when idle, and heats until the hardware throttles at TjMax - or until the embedded controller intervenes (on Clevo boards: forced full-volume beeping, then a hard power-off). Everything below is feature-gated by CPUID, so virtual machines (KVM masks the power bits in CPUID.06H) and non-Intel CPUs skip it safely. ## Components ### HWP frequency scaling (`Hal/CpuPower.cpp`) `Hal::CpuPower::InitializeBsp()` enables Hardware P-states (IA32_PM_ENABLE), reads the performance range from IA32_HWP_CAPABILITIES, and programs IA32_HWP_REQUEST with: - min = lowest performance level, max = highest - desired = 0 (fully autonomous hardware frequency selection) - EPP = 0x80 (balanced; same as Linux "balance_performance") This is the same mechanism intel_pstate uses on Linux and is the single biggest thermal win: the CPU idles near its minimum frequency and ramps only while there is work. APs apply the same request in `ApEntry` via `InitializeAp()` (HWP request MSRs are per-logical-CPU). CPUs with EPB but no HWP get IA32_ENERGY_PERF_BIAS = 6 (balanced) instead. ### C1E promotion MSR_POWER_CTL bit 1 lets HLT-idle drop core voltage/frequency to minimum (C1E) instead of parking at the current ratio. Set per-core on the BSP and every AP. Gated on the DTS CPUID bit as a real-hardware heuristic, because MSR_POWER_CTL is not architectural and hypervisors may not emulate it. ### Thermal governor (`Hal/CpuPower.cpp`) A closed-loop passive-cooling controller on the BSP, stepped from `Sched::RunBspMaintenance()` (reached from both the idle loop and the timer tick, so it keeps running under full load). Every 500 ms it reads the package digital thermal sensor (IA32_PACKAGE_THERM_STATUS readout relative to TjMax from MSR_TEMPERATURE_TARGET) and adjusts the HWP performance ceiling: - **>= 85 C (passive):** step the ceiling down 2 ratio units (~200 MHz) - **>= 94 C (hot):** step down 8 units per interval - **<= 78 C (resume):** step back up 1 unit per interval - between resume and passive: hold (hysteresis) The ceiling propagates through a policy epoch counter: each CPU compares the epoch in `Sched::Tick()` (and the AP idle loop) against the last one it applied and rewrites its own IA32_HWP_REQUEST when it changed, since MSR writes only affect the executing core. APs tick every 10 ms, so a throttle decision reaches all cores within one tick. Throttle engage/release transitions are logged (WARNING/OK, "CpuPower"). ### Deep AP idle (`ACPI/CpuIdle.cpp`) APs use deep MWAIT C-states while idle; C6 power-gates the core entirely, which is where most of the idle-heat win lives on many-core parts. The hint comes from ACPI `_CST` FFH entries (the GAS address low byte is the MWAIT hint, same convention as Linux), validated against the CPUID.05H sub-state enumeration and capped at the C6 family (0x2F). With no usable `_CST`, the deepest CPUID-enumerated hint (up to C6) is used. **The BSP stays in C1 (HLT).** On some laptops deep MWAIT states freeze the LAPIC timer even though ARAT is advertised, and the BSP's periodic 1 ms tick is the system timekeeper (see the comment in `Timekeeping::IdleOnce`). APs have no timekeeping duty: an idle AP is woken by the reschedule IPI, and its coarse tick resumes on wake. ### S3 wake IA32_PM_ENABLE, HWP requests, and MSR_POWER_CTL do not survive suspend. `Hal::CpuPower::ReapplyAfterWake()` runs in the S3 resume path, redoes the BSP setup, and bumps the policy epoch so every AP re-applies its HWP request (and re-checks C1E) on its next tick. ### Battery (`ACPI/Battery.cpp`, `ACPI/EmbeddedController.cpp`) Battery state comes from the ACPI Control Method Battery (`_HID PNP0C0A`) and, for the AC adapter, `ACPI0003`: - `_STA` bit 4 says whether a pack is installed. A laptop with the pack removed still has the device, which is why "has a battery device" and "has a battery" are separate answers. - `_BIX` (or `_BIF` on older firmware) gives the power unit (mWh vs mAh), the design capacity, and the last full charge capacity. - `_BST` gives the charge/discharge state, the present rate, the remaining capacity and the present voltage. - `_PSR` on the adapter says whether it is plugged in. Percentage is remaining / last-full, clamped to 100 and falling back to the design capacity when firmware reports last-full as zero. Results are cached for two seconds so that a panel repaint does not turn into EC traffic. Almost all of this reaches the CPU through the **embedded controller**. `Hal::EmbeddedController` finds the EC's command/data port pair from the ECDT table when one exists, and otherwise from `_CRS` on the `PNP0C09` device, then drives the polled read/write protocol (`0x80`/`0x81` with OBF/IBF handshaking). It is wired into the AML interpreter as the `EmbeddedControl` region space, so any `OperationRegion` in the DSDT that targets the EC now works -- battery methods being the first user. Supporting `_BST` also needed real package element storage in the AML interpreter. Firmware overwhelmingly builds its result package with `Store(x, Index(BST0, n))`, which requires `Index()` to name a writable element rather than yield a value. `AML::PackageStore` provides that: a persistent region for packages declared with `Name()`, and a transient ring rewound at each top-level evaluation for packages built inside a method, so polling `_BST` cannot exhaust it. ## Diagnostics `SYS_POWERINFO` (149) fills a `PowerInfo` struct: package temperature, TjMax, HWP state and performance range, the governor's current ceiling, CPUID base/turbo frequency, the measured average active frequency (APERF/MPERF over the 500 ms governor window), and the AP idle MWAIT hint. Returns -1 when the CPU exposes no power features (e.g. under QEMU). `SYS_BATTERYINFO` (188) fills a `BatteryInfo` struct: percentage, charge state, AC presence, capacities, present rate, voltage and an estimated time remaining. It returns -1 on a machine with no battery device at all, which is how the desktop panel decides not to draw an indicator. Like `SYS_POWERINFO` it is ungated read-only telemetry: the panel indicator has to work in an unprivileged session, which holds no device capability to spend on it. The `power` program (`programs/src/power`) prints both: ``` power one-shot snapshot power watch 1 Hz samples for 60 s (temp, avg freq, ceiling) power watch 300 same, for 300 s ``` Boot log lines to look for (Kernel Log app): ``` [CpuPower] HWP enabled: perf range 4-54, EPP=0x80 [CpuPower] Thermal governor armed: TjMax=100C, passive=85C, hot=94C, ... [CpuIdle] AP deep idle: MWAIT hint 0x20 [EC] Embedded Controller at cmd=66 data=62 [Battery] ACPI battery at \_SB.PCI0.LPCB.EC0.BAT0 (AC adapter present) ``` The desktop panel shows a battery icon left of the Wi-Fi and network icons, using the icon pack's `battery-level-N[-charging]-symbolic` glyphs (one per 10% step, reloaded only when the step changes). Clicking it opens a popout with the percentage in numbers, the charge state, time remaining and raw capacity. The icon is absent entirely when `SYS_BATTERYINFO` fails, so a desktop machine shows nothing there. ## Tuning The governor constants live at the top of `Hal/CpuPower.cpp` (`PassiveTempC`, `HotTempC`, `ResumeTempC`, `GovernorIntervalMs`, step sizes) and were chosen to keep the package well below both TjMax (100 C on the i9-13900HX) and the Clevo EC's emergency thresholds. If the fans still spin up aggressively under sustained load, lower `PassiveTempC` first; if builds feel throttled while cool, raise the recovery rate instead of the trip points.