169 lines
7.7 KiB
Markdown
169 lines
7.7 KiB
Markdown
# 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.
|