Skip to content

J713: wake from suspend-to-idle via USB on both ports, and on a USB-C attach - #574

Closed
erikwb wants to merge 7 commits into
aurora-silicon:integration/aurorafrom
erikwb:j713/usb-wake
Closed

erikwb wants to merge 7 commits into
aurora-silicon:integration/aurorafrom
erikwb:j713/usb-wake

Conversation

@erikwb

@erikwb erikwb commented Oct 29, 2026

Copy link
Copy Markdown

Problem

On the J713 (MacBook Air 13" M4), USB devices did not reliably wake the machine from suspend-to-idle:

  • A device on the SuperSpeed port kept the machine from sleeping at all. With a USB keyboard there, a closed lid looked like sleep, but the suspend had failed (atcphy_suspend returns -16).
  • A USB3 tree did not survive suspend. On resume every SuperSpeed device behind a hub disconnected and re-enumerated. A mounted disk was removed under its filesystem, and ext4 aborted its journal.
  • A keyboard behind an Anker USB-C hub could not wake the machine, with the lid open or closed.
  • A USB device plugged in during sleep could not wake it, then or later.

This builds on Ryan's USB wake work for J700 (#340). Running xHCI in suspend-to-idle, the AIC wake flag and the hub wakeup quirk are what make USB wake possible at all. This series covers the parts that are specific to T8132 and the J713.

Depends on #449 and #450 for J713 suspend-to-idle itself. The series applies to integration/aurora on its own and changes nothing on a J713 that doesn't suspend.

Root causes

  1. atcphy refuses system sleep with an active USB3 link. atcphy_suspend() returns -EBUSY while the PHY owns its common block. In suspend-to-idle that block keeps its state: DWC3 is a syscore device while its core runs, so the ATCn_USB domain stays on, and xHCI keeps running.
  2. The T8132 USB3 PHY does not bring a SuperSpeed link back from U3 once the core has suspended it (GUSB3PIPECTL.SUSPHY, PIPE P3). This shows up for a runtime-autosuspended USB3 hub as well as in system sleep. Before (1) it was hidden: nothing reached U3, because the port didn't sleep and uas disks don't autosuspend.
  3. The VIA VL822 USB 2.0 hub (2109:2822) doesn't pass on a downstream remote wakeup while it is suspended, like the VL122 on J700.
  4. An idle USB-C port is powered down (J713: power the idle USB-C ports down #528), so a device plugged in during sleep is never enumerated. Only the Type-C controller notices the attach. Making its event interrupt a plain wakeup source would also wake the machine for a charger, on every plug, unplug and power negotiation.

Changes (one commit each)

  1. phy: apple: atc: Let an active T8132 USB3 link through suspend-to-idle: allow PM_SUSPEND_TO_IDLE with an active common epoch. It still refuses an incomplete epoch (mode_dirty) and any deeper sleep state, and warns on resume if the domain went off under an active link. KUnit case.

  2. usb: dwc3: apple: Keep the T8132 USB3 PHY out of P3 in host mode: set dis_u3_susphy_quirk once the T8132 host PIPE switch to USB3 is done, so later dwc3_enable_susphy() calls (xHCI start, system suspend) leave USB3 SUSPHY clear. The switch itself still runs with SUSPHY set. It is cleared again before the core stops, because the PHY hands the PIPE back in P3 and otherwise fails to lock the PIPE handler. USB2 PHY suspend is unchanged.

  3. usb: core: quirks: Keep the VIA VL822 USB2 hub awake for wakeups: USB_QUIRK_HUB_NO_SUSPENDED_WAKEUP for 2109:2822, using the mechanism from J700: USB and Bluetooth keyboard wake from s2idle, including clamshell (supersedes #304) #340. KUnit case next to the VL122 one.

  4. usb: typec: tipd: cd321x: Refresh the power status on resume without a plug: the resume revalidation read the power status only with a plug present, so a charger removed in sleep left the port's USB power supply online, and upower reported AC power on battery. It now reads it unconditionally.

  5. dt-bindings: usb: apple,sn201202x: Allow wakeup-source, as ti,tps6598x does.

  6. usb: typec: tipd: sn201202x: Wake from suspend-to-idle for a new USB link: with wakeup-source set, the controller's event interrupt stays live in system sleep (IRQF_NO_SUSPEND). Its handler wakes the controller, acknowledges the events, and wakes the system with pm_wakeup_dev_event() only for a link that wasn't there before:

    • a USB2/USB3 link with the port as host: a port attaches as a USB device, as it does to a charger, before any data role swap;
    • from a power source, only one that is USB communications capable (first source-capability PDO): a charger may still show a USB link as host for a moment after the swap;
    • DisplayPort, Thunderbolt or USB4.

    A failed read wakes the system. The handler busy-waits, because the tick is frozen in suspend-to-idle and a sleeping wait doesn't end until something else wakes the system. It is used only on controllers without select notification, as on T8132; everything else keeps its current behaviour. The per-port power/wakeup switch turns it off. KUnit cases use data and status values read on a J713.

  7. arm64: dts: apple: t8132-j713: Wake from suspend-to-idle on a USB-C attach: wakeup-source on both port controllers.

Checked and left unchanged

  • The SPMI controller. On T8132 it waits for replies on its FIFO notification, which comes through its chained handler and so stays live in sleep. No change was needed there.
  • No IRQCHIP_SKIP_SET_WAKE on the SPMI irqchip. The filter doesn't arm the interrupt as a wake source, so it isn't needed.
  • MagSafe plugged in or out in sleep doesn't wake the machine. This is unchanged: it goes through the SMC.
  • Unplugging a USB device in sleep wakes the machine through xHCI, as before.

Hardware results (J713)

The display and GPU don't come up on integration/aurora alone on a J713 yet, so the device tests ran on our J713 tree (which has #449 and #450) with these seven commits on top, unchanged:

Test Result
USB keyboard on the SuperSpeed port: sleep, key wake wakes, no disconnects
USB-C charger plugged into the other port during sleep, then unplugged stays asleep through both; afterwards both port power supplies are offline and upower reports discharging
Keyboard plugged in during sleep, both ports empty before wakes by itself in 8 s, enumerates
Anker hub with keyboard, USB3 SSD (mounted) and Ethernet: sleep, wake with the keyboard behind the VL822 wakes; no disconnects; a 256 MB file re-read from disk is intact
Runtime U3 cycle of the hub's USB3 tree resumes without a disconnect
Unplug the hub while awake both port domains power off, no PHY errors
Idle sleep with nothing attached only the internal keyboard woke it; no Type-C events

Before that, development testing on the same tree went through more combinations:

Test Result
USB keyboard on the SuperSpeed port: sleep, key wake suspends (no more -EBUSY), wakes
Keyboard behind the Anker hub (VL822): key wake, lid open and lid closed wakes
Runtime U3 cycle of a USB3 hub (VL822 + SSD + Ethernet + card reader) 9/9 without disconnect (before: whole tree dropped)
Sleep with a mounted USB3 SSD at 5 Gb/s behind the hub no disconnects; a 256 MB file re-read from disk is intact
Unplug a USB3 tree while awake port powers down, no Failed to lock pipehandler
Keyboard plugged in during sleep, nothing else attached wakes by itself, enumerates
Same, with a charger on the other port wakes by itself
USB-C charger plugged in / unplugged during sleep stays asleep; afterwards the power supply and upower are correct
Idle with hub (USB3 + DisplayPort) and keyboard attached no Type-C events, no spurious wake
KUnit, arm64 QEMU, lockdep 159/159: ATC PHY, USB quirks, cd321x and sn201202x suites

Static checks: checkpatch --strict is clean on all seven commits. Each commit builds its objects with W=1; the one warning, a -Wvoid-pointer-to-enum-cast in spmi.c's probe, predates this series. dt_binding_check is clean, and so is dtbs_check for all six T8132 boards.

Power: idle ports are powered down, so keeping the USB3 PHY out of P3 costs nothing with nothing attached. It does apply to any device on a USB-C port in USB mode, including a USB 2.0 one, because the Type-C controller reports a USB3 connection for those too. Measured awake with a USB 2.0 keyboard on the port, alternating P3 allowed and not allowed four times, it costs about 25 mW (20 to 35 mW, close to the noise). Keeping P3 for USB 2.0 devices would mean switching SUSPHY after enumeration by link speed, which I left out. Awake, attaching the test hub raised total system power by 4.3 W, of which at most 0.37 W showed on the SoC-side 3.8 V rail; the rest is the hub's own draw through VBUS. The sleep filter costs nothing while no Type-C event happens. A charger plug-in raises about three events of a few milliseconds each.

Not covered

  • A powered dock (charges the Mac and carries USB) plugged in during sleep. The filter treats it as a link from a USB-capable source, and a KUnit case covers that. Any issues there will be fixed in a follow-up PR.
  • Thunderbolt and USB4 docks in sleep.
  • A spurious-interrupt report from the SPMI controller (handle_bad_irq on its parent IRQ) when a Type-C event arrives while that controller's interrupt is disabled during the suspend transition. It's harmless (unhandled: 0) and predates this series; it's a separate follow-up.
  • Plug-in wake on other SoCs. Controllers with select notification (T8140) keep their current behaviour.

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

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>
@erikwb

erikwb commented Nov 2, 2026

Copy link
Copy Markdown
Author

Superseded by #832: the same seven commits, rebased onto integration/aurora.

@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