Skip to content

usb: typec: tipd: sn201202x: Sleep while idle - #637

Closed
erikwb wants to merge 8 commits into
aurora-silicon:integration/aurorafrom
erikwb:j713/typec-idle-sleep
Closed

erikwb wants to merge 8 commits into
aurora-silicon:integration/aurorafrom
erikwb:j713/typec-idle-sleep

Conversation

@erikwb

@erikwb erikwb commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Depends on #574 (the first 7 commits here are #574; the new commit is the last one).

The two SN201202x USB-C controllers on the MacBook Air (13-inch, M4) stay awake as long as the system does, even with nothing attached. The SMC reads about 200 mW on the 3.3 V rail that feeds them while they are awake, and about 1 mW once they sleep.

This lets an idle controller sleep, with runtime PM:

  • A register access wakes it, and it sleeps again 1 s after the last one (power/autosuspend_delay_ms).
  • Its event interrupt stays live while it sleeps, so an attach or a power change still reaches the handler, whose first read wakes it.
  • It sleeps through charging and with a USB device attached. A DisplayPort, Thunderbolt or USB4 link keeps it awake, as does DBMa debug mode.
  • After a few failed tries to sleep, it stays awake until the next access.
  • System sleep wakes an idle controller before its own SLEEP handshake, and resume hands it back to runtime PM. If a controller does not wake, it sleeps on through system sleep without the wakeup watch, and resume wakes it.
  • Only the T8132 controllers use idle sleep. Elsewhere the SPMI bus's runtime PM reference is kept and nothing changes.
  • power/control = on turns idle sleep off.

Testing

On a MacBook Air (13-inch, M4):

  • Power. The display is blanked and nothing is plugged in. Four alternating pairs compare power/control on against auto:
    • total system power is 1199 mW → 1113 mW on average, a saving of ~87 mW; each pair saved 64–116 mW;
    • the 3.3 V rail drops from ~209 mW to ~0.
  • Plug tests, both controllers idle-asleep first:
    • Charger plug and unplug: the controller wakes for the PD negotiation, sleeps again 2 s later, and stays asleep for 90 s while the machine charges.
    • USB keyboard: it enumerates and works while the controller is asleep, and the unplug is seen.
    • The port with nothing attached never wakes.
  • Suspend-to-idle with both controllers asleep: plugging in a USB keyboard wakes the system and the keyboard enumerates. Both controllers are asleep again within 5 s.
  • KUnit: the sn201202x-pm suite passes 21/21 on arm64 QEMU. Register accesses go through the driver's own regmap bus to a simulated SPMI controller, and the fixture binds and unbinds the way the SPMI bus does. It covers:
    • idle sleep, woken by a register access;
    • accesses from the system sleep event path, which leave runtime PM alone;
    • retry, give-up, re-arm, and an access restarting the count;
    • an unconfirmed sleep with a failed wake;
    • the display link, an unplug, and DBMa found at probe or entered through debugfs;
    • unbind from idle sleep and rebind;
    • a controller without idle sleep;
    • system sleep from idle sleep, including a failed wake and the wakeup watch.
  • Checks: checkpatch --strict is clean, and so is a W=1 build.

Known limitation

If a wake fails while runtime PM still counts the controller as active, register access fails until the next successful wake. Before this change that state was likewise unrecoverable.

This change was developed and tested by Claude (Anthropic), working with me on the J713.

@erikwb
erikwb force-pushed the j713/typec-idle-sleep branch from 4af54db to aa728be Compare October 30, 2026 14:32
On the J713 the SuperSpeed port refuses system sleep for as long as any
USB device is attached: atcphy_suspend() returns -EBUSY while the PHY
owns its common block, so a laptop with a USB keyboard on that port
never suspends:

  PM: dpm_run_callback(): atcphy_suspend returns -16
  PM: Some devices failed to suspend, or early wake event detected

Suspend-to-idle does not take the link down. While its core runs, DWC3
is a syscore device and keeps the shared power domain on across system
sleep, and xHCI keeps running so that a USB device can wake the system.
The PHY keeps its state with the domain, and resume restores it only if
the domain went off.

Allow suspend-to-idle with an active common epoch while a host runs on
it. Keep refusing one without a host, as in a USB3 device role, where
nothing holds the domain; an incomplete one; and any deeper sleep state,
which would lose the link. Warn on resume if the domain went off under
an active host link after all. A KUnit case covers these outcomes.

Signed-off-by: Erik Bourget <erik@ebourget.net>
On the J713 a SuperSpeed link does not come back from U3 once the core
has suspended the USB3 PHY with it (GUSB3PIPECTL.SUSPHY): resuming the
port drops the link, and the whole tree behind it disconnects. This
happens when a USB3 hub autosuspends, and on every resume from
suspend-to-idle, where the USB core suspends all ports. A mounted disk
behind a USB3 hub is removed under its filesystem.

Clear SUSPHY for the USB3 PHY once the T8132 PIPE switch to USB3 is
done, by setting dis_u3_susphy_quirk, which dwc3_enable_susphy() then
honours when the xHCI host starts. The switch itself still runs with
SUSPHY set, as it requires. Restore the device tree's setting before the
host is stopped, which sets SUSPHY again: the PHY hands the PIPE back in
P3, and fails to lock the PIPE handler otherwise. The core stop does the
same for the error paths.

USB2 PHY suspend is unchanged. The port routes the USB3 PIPE for any
USB-C device in USB mode, so the USB3 PHY stays out of P3 while any USB
device is attached, also a USB 2.0 one. The idle USB-C ports are
powered down, so it costs nothing with nothing attached.

The host test stubs of struct dwc3 and struct dwc3_apple gain
dis_u3_susphy_quirk and dt_dis_u3_susphy, and the teardown test checks
that the device tree's setting is back before the host exits.

Signed-off-by: Erik Bourget <erik@ebourget.net>
The USB 2.0 hub of the VIA Labs VL822 (2109:2822), as in Anker USB-C
hubs, does not pass on a remote wakeup from a device below it while it
is suspended itself, like the VL122. A USB keyboard behind it cannot
wake the system from suspend-to-idle.

Give it USB_QUIRK_HUB_NO_SUSPENDED_WAKEUP, so that it is kept awake in
suspend-to-idle while a device below it may wake the system. Its
autosuspend is unchanged. A KUnit case checks the entry.

Signed-off-by: Erik Bourget <erik@ebourget.net>
…a plug

The resume revalidates the port and reports a cable removed in system
sleep, but reads the power status only while a plug is present. A power
source removed in sleep then leaves the cached power status, and with it
the port's USB power supply, online until the next power event, and
upower reports AC power while on battery.

Read the power status unconditionally. Without a plug it reads as no
connection.

This matters once the events of a sleeping controller are acknowledged
in sleep, before resume could handle them, which the following changes
do.

Signed-off-by: Erik Bourget <erik@ebourget.net>
As on ti,tps6598x, the controller may wake the system from sleep for a
change on its port.

Signed-off-by: Erik Bourget <erik@ebourget.net>
…link

A USB device plugged into a J713 in suspend-to-idle cannot wake it, and
cannot wake it later either: an idle USB-C port is powered down, so the
device is never enumerated. Only the Type-C controller notices it: its
event interrupt fires for the attach even while it sleeps.

As a plain wakeup source that interrupt would also wake the system for
a charger, on every plug, unplug and power negotiation. Filter the
events instead. With wakeup-source set, the event interrupt stays live
in system sleep (IRQF_NO_SUSPEND) and sn201202x_sleep_event() handles
it there: it wakes the controller, acknowledges its events, reads its
data status and status, and wakes the system with pm_wakeup_dev_event()
only for a link that was not there before:

 - a USB2 or USB3 link with the port as the host: a port attaches as a
   USB device, as to a charger, before any data role swap;
 - from a power source, only one capable of USB communication, as its
   first source capability PDO says: a charger may still report a USB
   link as host for a moment after the swap;
 - DisplayPort, Thunderbolt or USB4.

A charger plugged in or unplugged leaves the system asleep. A read that
fails wakes it. The controller goes back to sleep either way, and the
resume revalidates the whole port as before. A controller reset seen in
sleep is set up again on resume, as cd321x_check_setup() latches it.

The interrupt is not armed for wakeup, so IRQF_NO_SUSPEND is used as
Documentation/power/suspend-and-interrupts.rst describes for a handler
that decides itself whether to wake the system, with pm_system_wakeup()
underneath. The watch is set up for suspend-to-idle only.

In suspend-to-idle the tick is frozen, so a sleeping wait does not end
until something else wakes the system: the controller is polled with
busy waits there, within a fixed budget, and WAKEUP is repeated in case
it reached a controller still going to sleep. A resume whose WAKEUP is
not confirmed now falls back to polling as well. Its other interrupts
are suspended, so this needs a controller without select notification,
as on T8132; others keep their behaviour. A transfer the SPMI controller
never answers still waits on its timer, so it ends only once the system
is awake again. The port's power/wakeup setting turns the wakeup off.

KUnit cases cover the link decision with the data status and status as
read on a J713 for a charger as it attaches, swaps and settles, and for
a USB keyboard.

The tipd_init() host test stubs gain sleep_event and IRQF_NO_SUSPEND,
and the SN201202x transport test covers the atomic selection too.

Signed-off-by: Erik Bourget <erik@ebourget.net>
…ttach

Mark both USB-C port controllers as wakeup sources. A USB device, dock
or display plugged in while the J713 is in suspend-to-idle wakes it; a
charger does not.

Signed-off-by: Erik Bourget <erik@ebourget.net>
An SN201202x stays awake for as long as the system is, even with nothing
attached and nothing to do. On a MacBook Air (13-inch, M4) the SMC reads
about 190 mW on the 3.3 V rail that feeds the two controllers while they
are awake, and about 1 mW once they sleep.

Let an idle controller sleep, with runtime PM: a register access wakes
it, and it sleeps again a second after the last one. Its event interrupt
stays live while it sleeps, so an attach or a power change still reaches
the handler, whose first read wakes it. Nothing else reads the
controller on its own, so it sleeps through charging and with a USB
device attached too. A display, Thunderbolt or USB4 link keeps it awake,
as nothing was tested with one while it sleeps, and so does Apple's DBMa
debug mode. After a few failed tries to sleep, it stays awake until the
next access. System sleep wakes an idle controller before its own SLEEP
handshake, and resume hands it back to runtime PM; one that does not
wake sleeps on through system sleep, without the wakeup watch, and
resume wakes it.

The SPMI bus holds a runtime PM reference while the driver is bound.
Probe gives it to idle sleep only on the T8132 controllers, where the
event interrupt is known to stay live while they sleep, as the system
sleep event filter relies on. Elsewhere it is kept and the controller
stays awake as before. power/autosuspend_delay_ms and power/control
change the delay or turn idle sleep off.

The SN201202x transport test now takes __regmap_sn201202x_read(), the
transfer itself, rather than its new wrapper.

Signed-off-by: Erik Bourget <erik@ebourget.net>
@erikwb
erikwb force-pushed the j713/typec-idle-sleep branch from aa728be to 8e872a1 Compare October 30, 2026 20:27
@erikwb

erikwb commented Oct 30, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto the current integration/aurora together with #574. host-tests failed here because the new host harnesses (neo-stability, apple-dcp) compile the dwc3-apple and tipd functions that #574 changes against stubs that lacked its new fields. #574 now updates those stubs in the commits that need them, and host-tests pass on both PRs. The sn201202x transport selftest is updated as well. The idle-sleep change itself is unchanged.

@erikwb

erikwb commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by #833, which carries only the idle-sleep commit, stacked on #832.

@erikwb erikwb closed this Nov 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant