$ benou docs

Personal documentation and knowledge base

Suspend and resume

#system #suspend #s2idle

Wake sources, spurious wakes out of s2idle, the Thunderbolt dock across sleep, and the open items from the 2026-08-03 slow-resume incident. Idle timeouts and DPMS live in idle-lock.

Machine

ThinkPad T14s Gen 6 AMD — 21M1000PMZ, Ryzen AI 7 PRO 360, BIOS R2NET42W 1.16 (2025-10-10). Only s2idle is available (cat /sys/power/mem_sleep → [s2idle]); there is no S3 to fall back to. Docked to a ThinkPad Thunderbolt 4 Dock driving 2× P24h-10 over DP-tunnel-in-USB4.

The power button cannot wake this machine

cat /proc/acpi/wakeup    # lists SLPB and LID — there is no PWRB entry

No PWRB entry means the ACPI power button has no wake GPE, so it cannot bring the SoC out of s0i3. This is firmware, not configurable from Linux — PNP0C0C:00/power/wakeup reads enabled but that is the driver’s own flag, not an armed wake source.

Wake with the trackpad, the internal keyboard, or the lid. All three are wakeup-enabled:

# what can actually wake the machine
for f in /sys/bus/*/devices/*/power/wakeup /sys/devices/platform/*/power/wakeup; do
  [ "$(cat "$f" 2>/dev/null)" = enabled ] && echo "$f"
done

Note the trackpad is i2c-SYNA8022:00. Devices on the dock cannot wake the machine once the dock has dropped off the bus — which is exactly what happened on 2026-08-03, so the external Keychron was not an option either.

Since 2026-08-03 the displays no longer depend on which device woke the machine — see the after-resume hook in idle-lock.

Spurious wakes from s2idle (2026-08-04) — open, wake source not yet named

Left idle on AC for an afternoon, the machine came back warm with the fans audible and the screen merely locked. It had suspended on schedule every time; it just refused to stay asleep.

17:29:33 suspend entry (s2idle)  ->  17:29:43 suspend exit   asleep 10s
18:59:43 suspend entry           ->  19:00:55 suspend exit   asleep 1m12s
20:30:56 suspend entry           ->  20:31:33 suspend exit   asleep 37s
22:01:33 suspend entry           ->  22:16:05 suspend exit   asleep 14m32s

Resume to next suspend is exactly 1:30:00, three times running: the 5400s rung fires, the machine sleeps properly, something wakes it within seconds, and swayidle re-arms the whole ladder from zero. Asleep 16 minutes out of 4h47. The sleep itself is not the problem — last_hw_sleep was 865.7s against 872s of wall time (99.2%) and every failed_* counter is 0. See idle-lock for the ladder-restart behaviour.

With the lid closed the same wake produces a much tighter loop, driven by logind rather than swayidle:

74 x "PM: suspend entry" in one boot
42 x "amd_pmc AMDI000A:00: Last suspend didn't reach deepest state"
14:43:26 entry -> 14:43:31 exit -> 14:43:57 entry -> 14:44:02 exit -> ...   31s period, ~85 min

HandleLidSwitch=suspend with HoldoffTimeoutUSec=30s, and logind reports Docked=false — a USB-C/Thunderbolt dock is not an ACPI dock station, so HandleLidSwitchDocked=ignore never applies no matter how many external displays are attached. Every spurious wake is therefore followed 30s later by another lid-triggered suspend. After a dozen rounds the platform stopped reaching s0i3 at all, which is where most of the heat came from.

busctl get-property org.freedesktop.login1 /org/freedesktop/login1 \
  org.freedesktop.login1.Manager HandleLidSwitch HandleLidSwitchDocked \
  HoldoffTimeoutUSec Docked LidClosed

To break the lid loop before the wake source is found, the lever is a drop-in under /etc/systemd/logind.conf.d/ — outside this repo, and it trades away lid-close suspend.

Naming the wake source

/sys/power/pm_wakeup_irq reads 9 — the ACPI SCI — so the wake arrives as a GPE, not as a device MSI. That narrows it to something ACPI-signalled but does not identify it; the rest needs root.

echo 1 | sudo tee /sys/power/pm_debug_messages
systemctl suspend
sudo dmesg | grep -iE "Resume caused by|wakeup source"
sudo grep -v "	0	" /sys/kernel/debug/wakeup_sources | sort -t$'\t' -k3 -rn | head

# or diff the GPE counters across one suspend
grep . /sys/firmware/acpi/interrupts/gpe[0-9A-F]* > /tmp/gpe.before
systemctl suspend
grep . /sys/firmware/acpi/interrupts/gpe[0-9A-F]* > /tmp/gpe.after
diff /tmp/gpe.before /tmp/gpe.after

Leading suspect: the WWAN modem. The Quectel RM520N-GL is torn down and re-enumerated once per suspend cycle — mmcli -L reported Modem/74 against 74 suspend entries — and never comes back healthy: mhi mhi1: Wait for device to enter SBL or Mission mode with no success line, then 370 x MBIM error: Transaction timed out in one boot. Its PCIe bridge 0000:00:02.5 shows wakeup_active_count=4153 against 75 for the Wi-Fi bridge.

nmcli radio wwan off     # then one long test suspend
grep . /sys/power/suspend_stats/*    # last_hw_sleep should ~= wall clock asleep
nmcli radio wwan on      # restore

Ruled out already: Wi-Fi (iw phy0 wowlan show → disabled, and NetworkManager disconnects it before sleep), wake-arming systemd timers (none set WakeSystem=true), and the Thunderbolt dock (no thunderbolt or DPIA AUX entries in the affected boot — it was undocked).

Not evidence, however tempting: i2c-SYNA8022:00 at wakeup_active_count=212228, serio0 at 5776, and gpe07 at 153441. Those count every input report and every GPIO interrupt during normal awake use, not wakes. gpe07 is the AMD GPIO controller that carries the touchpad IRQ.

Wake-path checklist

Run this before enabling anything that increases the number of suspends per hour, three times per row. Reproduce the state the machine actually suspends in — outputs already powered down — or the test is easier than reality:

swaylock -f -i ~/.wallpaper/wall_dark.png
swaymsg "output * power off"
systemctl suspend
Wake byMust happen
trackpadscreens light, swaylock takes the password, no second attempt
internal keyboardsame
lid opensame, with no further input — this is what after-resume is for

Note the resume time for each; 2026-08-03 saw a ~17s dock-tree rebuild. If any row is flaky, fix that path — do not compensate with a shorter suspend timeout. The if-unattended.sh rung in idle-lock stays unwired until all three pass.

Open item 1 — check for a BIOS newer than 1.16

Wake-source arming lives in firmware, so a BIOS update is the only thing that could make the power button work. Current BIOS dates from 2025-10-10.

fwupdmgr refresh
fwupdmgr get-upgrades

See fwupd for the Secure Boot signing prerequisites. Verify after:

cat /sys/class/dmi/id/bios_version   # currently R2NET42W
grep -c PWRB /proc/acpi/wakeup       # 0 today; 1 would mean the button became a wake source

Open item 2 — BIOS settings so the dock survives sleep

On 2026-08-03 the dock fell off the bus during a 7-hour s2idle:

thunderbolt 0000:c5:00.5: 0:2: failed to reach state TB_PORT_UP. Ignoring port...
thunderbolt 0000:c5:00.5: 0:2: lost during suspend, disconnecting

First occurrence in 60+ days of journal. Because both monitors are DP-tunnelled through the dock, the whole tree had to be rebuilt on resume (~17 s), and it came back on the other controller — 0-2 on NHI0 became 1-2 on NHI1.

Settings to confirm in BIOS (F1 at boot):

MenuSettingWantWhy
Config → ThunderboltThunderbolt BIOS Assist ModeDisabledCorrect for Linux; the kernel drives the controller itself
Config → PowerAlways On USBEnabledKeeps the dock link powered during s0i3
Config → PowerCharge in Battery ModeEnabledSame, when not on AC

If it recurs after that, capture a proper trace before filing anything — add thunderbolt.dyndbg=+p to the kernel cmdline, reproduce a long sleep, then:

journalctl -k -b -g "TB_PORT_UP|lost during suspend"

Open item 3 — the wedged empty Thunderbolt controller

When the dock migrates to NHI1, the now-empty NHI0 (0000:c5:00.5) is left in a failing runtime-PM loop — roughly every 40 s it runtime-suspends, then tb_domain_runtime_resume → tb_ctl_start → tb_ring_start trips:

thunderbolt 0000:c5:00.5: interrupt for TX ring 0 is already enabled
WARNING: drivers/thunderbolt/nhi.c:147 at ring_interrupt_active+0x255/0x300
thunderbolt 0000:c5:00.5: 0: timeout writing config space 2 to 0x1

Each iteration burns ~18 s in config-space timeouts. This is journal noise and wasted wakeups, not a functional break — the dock works fine on NHI1. It is a known long-standing kernel bug, not an Arch regression.

Two ways to clear it:

# quiet it in place — safe because NHI0 has no devices attached.
# costs a little idle power, does not survive a reboot.
echo on | sudo tee /sys/bus/pci/devices/0000:c5:00.5/power/control

# or reboot: resets both controllers and re-attaches the dock to NHI0

Check whether it is currently looping:

journalctl -k -b -g "already enabled|timeout .* config space" --since "10 min ago"

Confirming a suspend was healthy

The useful signal is hardware-sleep residency — it should be ≈ wall-clock time asleep:

grep . /sys/power/suspend_stats/*

last_hw_sleep is in microseconds. On 2026-08-03 it read 25,521.7 s against 25,526 s of wall time — 99.98% in deep s0i3, all failed_* counters 0. That rules out the sleep itself and points at wake sources or device re-enumeration instead.

Journal timestamps are misleading here: the console is suspended, so the entire suspend+resume sequence is flushed at once and stamped with the resume time. Do not read those bunched timestamps as durations — use suspend_stats and the events after resume.

# what the resume actually did, minus the noise
journalctl -b -k -o short-precise --since "<resume time>" | grep -viE "DPIA AUX failed|mtp-probe"
  • idle-lock — idle timeouts, DPMS, before-sleep / after-resume
  • fwupd — firmware updates, Secure Boot signing
  • display — output layout and stable identifiers
  • journalctl — log querying