2026-09-21T20:40:00

TUXEDO laptop CPU stuck at 200 MHz after suspend on battery

After resuming my TUXEDO laptop on battery, the CPU remained at 200 MHz under full load for 30 to 40 seconds. An external PROCHOT signal caused the throttling. A systemd sleep hook now starts a worker that disables the CPU's response to the signal temporarily and re-enables it after 45 seconds.

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, the TUXEDO power profile limits the CPU to a range of 900 MHz to 2.5 GHz. The configured 2.5 GHz maximum was therefore 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 occurred only after suspending and resuming on battery. Resuming with the power supply connected 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

CPU temperatures remained between 35 °C and 50 °C, which ruled out throttling caused by the CPU reaching its thermal limit.

turbostat confirmed that the cpufreq value reflected the effective frequency. With the logical CPUs almost fully busy, Bzy_MHz remained at 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 released the limit immediately. The failure was therefore specific to the post-resume battery power state.

Finding the actual cause

I then inspected these Intel model-specific registers (MSRs):

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

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

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

Bit 2 was set only during the slow period. According to the Intel Software Developer's Manual, this bit reports a PROCHOT# or FORCEPR# signal asserted by another platform agent. IA32_CORE_PERF_LIMIT_REASONS changed at the same time:

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

The CPU remained cool and did not report an internal thermal event. Instead, the external signal forced it to its lowest P-state.

These measurements do not identify which component asserted the signal. Plausible sources include the embedded controller, voltage regulator, and battery power-path logic.

Reloading uniwill_wmi and restarting tccd did not clear the condition:

modprobe -r uniwill_wmi
modprobe uniwill_wmi
systemctl restart tccd

Disabling bidirectional PROCHOT in MSR_POWER_CTL, however, released the CPU immediately:

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

The response to bit 0 of MSR_POWER_CTL isolated the active external PROCHOT signal as the cause. The intel_pstate limits and governor did not cause the 200 MHz cap.

Workaround

Bidirectional PROCHOT allows other platform components to throttle the CPU for protection, so I did not want to disable it permanently. The workaround clears only bit 0 of MSR_POWER_CTL for 45 seconds after a battery resume, then sets the bit again.

Warning: This workaround temporarily disables a hardware protection input. Use it only after confirming the same MSR state and behavior on the affected machine. The register and bit definitions are processor-specific. If the worker is killed before its cleanup handler runs, verify the bit manually or reboot.

The script rewrites the register throughout those 45 seconds because firmware or System Management Mode (SMM) may set the bit again after resume. Before each write, it reads the current value and changes 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

A systemd sleep hook starts the script asynchronously, so the 45-second interval does not block resume. 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 confirms that bidirectional PROCHOT is enabled again on all eight logical CPUs:

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

After a battery resume, an external platform component asserted PROCHOT for 30 to 40 seconds and forced the i7-11370H to 200 MHz. The normal temperatures, MSR state, and immediate response to both AC power and the bidirectional PROCHOT enable bit distinguish this condition from an intel_pstate or governor problem.

The measurements point to a firmware, embedded-controller, voltage-regulator, or battery power-path defect, but do not identify the responsible component. Until that behavior is fixed, masking bidirectional PROCHOT for 45 seconds avoids the slowdown during the affected interval. The script then re-enables the protection.