2026-09-21T20:40:00

TUXEDO laptop CPU stuck at 200 MHz after suspend on battery

After resuming my TUXEDO laptop from suspend while running on battery, the machine became almost unusable for roughly 30 to 40 seconds. The CPU was really running at 200 MHz under full load. The cause turned out to be an external PROCHOT signal asserted after resume, and a small systemd resume workaround made the machine usable immediately.

Hardware and system

The machine is a TUXEDO InfinityBook Pro MK1 Gen7 with an Intel Core i7-11370H. It runs Debian 13 (Trixie), the intel_pstate CPU frequency driver and the TUXEDO/Uniwill drivers.

On battery I intentionally limit the CPU through the TUXEDO power profile to 900 MHz minimum and 2.5 GHz maximum. Therefore the configured 2.5 GHz maximum was expected and unrelated to the problem.

model name  : 11th Gen Intel(R) Core(TM) i7-11370H @ 3.30GHz

scaling_min_freq:900000
scaling_max_freq:2500000
scaling_governor:powersave
scaling_driver:intel_pstate

intel_pstate/status:active
intel_pstate/min_perf_pct:8
intel_pstate/max_perf_pct:100

Symptoms

The problem happened only when suspending and resuming on battery. Resume while connected to the power supply worked normally.

Immediately after a battery resume the laptop was extremely slow. Reading scaling_cur_freq showed about 200 MHz even though the configured minimum frequency was 900 MHz:

scaling_cur_freq:200007
scaling_min_freq:900000
scaling_max_freq:2500000

Temperatures were completely normal, around 35 to 50 degrees Celsius, so this did not look like ordinary thermal throttling.

turbostat confirmed that the 200 MHz value was not just a misleading cpufreq reading. Even with the CPUs almost fully busy, the actual busy frequency was fixed at exactly 200 MHz:

Avg_MHz  Busy%   Bzy_MHz  CoreTmp  CoreThr  PkgWatt
199      99.60   200      36       0        5.41
199      99.58   200      35       0        5.41
199      99.53   200
199      99.40   200      35       0
200      99.77   200

After roughly 30 to 40 seconds the limit disappeared suddenly and the CPU jumped back to about 2.3 to 2.5 GHz:

Avg_MHz  Busy%   Bzy_MHz  PkgWatt
199      99.72   200      5.48
199      99.38   200
198      98.95   200
1011     43.05   2345     10.91
918      39.75   2309     10.86

Connecting the power supply during the slow period also released the CPU immediately. That was an important clue: the problem was tied specifically to the battery resume power state.

Finding the actual cause

The next step was looking at the Intel model-specific registers. The interesting ones were:

0x19c  IA32_THERM_STATUS
0x1b1  IA32_PACKAGE_THERM_STATUS
0x1fc  MSR_POWER_CTL
0x64f  IA32_CORE_PERF_LIMIT_REASONS

Before suspend, during the 200 MHz period and after recovery, the values changed reproducibly. The important change was in IA32_THERM_STATUS:

before:  ...08
during:  ...0c
after:   ...08

The extra bit during the slow period indicates an externally asserted PROCHOT/FORCEPR condition. IA32_CORE_PERF_LIMIT_REASONS changed at the same time:

before:  0x1010000
during:  0x10001
after:   0x1010000

This matched the observed behavior perfectly: the CPU itself was cool and did not report normal core thermal throttling, but another platform component was asserting PROCHOT and forcing the CPU to its lowest P-state.

The most likely source is the embedded controller, VRM or some battery/power-path logic which is temporarily left in the wrong state after a battery resume.

Reloading uniwill_wmi and restarting tccd did not clear the condition:

modprobe -r uniwill_wmi
modprobe uniwill_wmi
systemctl restart tccd

However, disabling bidirectional PROCHOT in MSR_POWER_CTL immediately released the CPU:

wrmsr -a 0x1fc 0x24005a

Re-enabling it while the external signal was still asserted immediately forced the CPU back to 200 MHz:

wrmsr -a 0x1fc 0x24005b

This proved that the slowdown was caused by the still-active external PROCHOT signal, not by intel_pstate, the governor or CPU temperature.

Workaround

I did not want to disable bidirectional PROCHOT permanently because it is a useful hardware protection mechanism. The workaround therefore masks only bit 0 of MSR_POWER_CTL for 45 seconds after a battery resume and explicitly re-enables it afterwards.

The register is rewritten repeatedly during those 45 seconds because firmware or SMM can change it again after resume. The script always reads the current value first and modifies only the bidirectional PROCHOT enable bit.

The worker script is /usr/local/sbin/tuxedo-resume-prochot:

#!/bin/bash
set -eu

MSR=0x1fc
DURATION=45
INTERVAL=1

modprobe msr

restore_prochot()
{
    for cpu in /dev/cpu/[0-9]*; do
        [ -e "$cpu/msr" ] || continue

        n=${cpu##*/cpu/}

        cur=$(rdmsr -p "$n" "$MSR" 2>/dev/null) || continue
        new=$(printf '%x' $((0x$cur | 1)))

        wrmsr -p "$n" "$MSR" "$new" 2>/dev/null || true
    done
}

trap restore_prochot EXIT INT TERM

# Only run on battery.
for f in /sys/class/power_supply/*/online; do
    [ -e "$f" ] || continue

    if [ "$(cat "$f" 2>/dev/null || echo 0)" = "1" ]; then
        exit 0
    fi
done

logger -t tuxedo-prochot \
    "battery resume: forcing BD PROCHOT off for ${DURATION}s"

END=$(( $(date +%s) + DURATION ))

while [ "$(date +%s)" -lt "$END" ]; do
    for cpu in /dev/cpu/[0-9]*; do
        [ -e "$cpu/msr" ] || continue

        n=${cpu##*/cpu/}

        cur=$(rdmsr -p "$n" "$MSR")
        new=$(printf '%x' $((0x$cur & ~1)))

        wrmsr -p "$n" "$MSR" "$new"
    done

    sleep "$INTERVAL"
done

restore_prochot

v=$(rdmsr -p0 "$MSR")

logger -t tuxedo-prochot \
    "45s elapsed; BD PROCHOT re-enabled: MSR=0x$v BD_PROCHOT=$((0x$v & 1))"

exit 0

The script is started asynchronously from a systemd sleep hook so that the 45-second delay does not block the resume itself. The hook is /usr/lib/systemd/system-sleep/tuxedo-prochot:

#!/bin/sh

case "$1" in
    post)
        systemd-run \
            --quiet \
            --collect \
            --unit=tuxedo-resume-prochot \
            /usr/local/sbin/tuxedo-resume-prochot
        ;;
esac

exit 0

Both files need to be executable:

chmod 755 /usr/local/sbin/tuxedo-resume-prochot
chmod 755 /usr/lib/systemd/system-sleep/tuxedo-prochot

The final log now looks like this:

tuxedo-prochot: battery resume: forcing BD PROCHOT off for 45s
tuxedo-prochot: 45s elapsed; BD PROCHOT re-enabled: MSR=0x5 BD_PROCHOT=1

A final check on all logical CPUs confirms that bidirectional PROCHOT is enabled again:

for n in {0..7}; do
    v=$(rdmsr -p "$n" 0x1fc)
    printf 'CPU%d: 0x%s BD_PROCHOT=%d\n' \
        "$n" "$v" $((0x$v & 1))
done

CPU0: 0x5 BD_PROCHOT=1
CPU1: 0x5 BD_PROCHOT=1
CPU2: 0x5 BD_PROCHOT=1
CPU3: 0x5 BD_PROCHOT=1
CPU4: 0x5 BD_PROCHOT=1
CPU5: 0x5 BD_PROCHOT=1
CPU6: 0x5 BD_PROCHOT=1
CPU7: 0x5 BD_PROCHOT=1

Conclusion

The problem looked initially like a CPU governor or intel_pstate bug, but the CPU frequency limits, temperatures and HWP configuration were all normal. turbostat and the Intel MSRs showed what was actually happening: after a battery resume an external platform component asserted PROCHOT for about 30 to 40 seconds and forced the i7-11370H down to 200 MHz.

Connecting AC power cleared the condition immediately, which further points to an EC, VRM or battery power-path resume bug. Until the underlying firmware behavior is fixed, temporarily masking bidirectional PROCHOT for 45 seconds after battery resume provides a practical workaround while restoring the protection mechanism afterwards.