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.
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
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.
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.
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
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.