usb: dwc3: Keep the host bus across suspend-to-idle
Subsystems (3)
cover usb: dwc3: Keep the host bus across suspend-to-idle
In host mode, dwc3_suspend_common() tears the core down for every system suspend unless the controller is a wakeup source (dwc3_core_exit(): PHYs off, core reset). That has two consequences for xhci-plat on top of it:
1. xhci_resume() is not told that the controller was reset. It writes the saved operational registers back and asks for a state restore (USBCMD.CRS), which fails on a freshly reset core, and only then falls back to a reinit:
xhci-hcd xhci-hcd.1.auto: xHC error in resume, USBSTS 0x401, Reinit
xhci-plat already has xhci_plat_priv.power_lost for this (cdns3 sets it). Patch 1 sets it from dwc3 whenever the core was re-initialised and clears it when the core was kept for wakeup.
2. Every device behind the root ports was put into U3 by the bus suspend and expects to keep its power session; resetting the core ends all of them. On resume the USB core has to reset-resume every device, which only works for devices that come back within the 2 s usb_port_resume() waits. A SanDisk Extreme SSD and a card reader behind a hub do not (2.4-3 s, measured): they are disconnected and enumerated again after each resume, which kills a mounted filesystem. Patch 2 keeps the core, its PHYs and the suspended bus across suspend-to-idle, as the wakeup path already does and as PCI xHCI controllers do, and marks the device as being on the awake path so a power domain with GENPD_FLAG_ACTIVE_WAKEUP stays on. Suspend-to-RAM, hibernation and needs_full_reinit controllers are unchanged.
Patch 2 depends on patch 1 (it clears power_lost on the kept-core path). Both are changes to the dwc3 core; no glue driver is involved (the RK3588 nodes bind to the "snps,dwc3" core driver directly).
Tested on an Orange Pi 5 (RK3588S), suspend-to-idle through rtcwake:
- Patch 1: 55 cycles on a kernel with patch 1 (and an ohci-platform change sent separately); the "xHC error in resume, USBSTS 0x401" message is gone, the controller re-initialises without the failed restore, no other change in behaviour.
- Patch 2: 47 cycles on a kernel with both patches: every USB device kept its device number, all 7 SuperSpeed links were back after each resume, no "root hub lost power or was reset" and no re-enumeration in dmesg, USB drives readable afterwards. Without patch 2 the SSD and the card reader were disconnected and re-enumerated after every resume and their mounted filesystems went offline.
- Not tested: suspend-to-RAM and hibernation (unchanged by this series; the test platform has no working deep suspend), other dwc3 platforms.
This series was developed with Claude (Anthropic) as a coding assistant:
it wrote the code and the commit messages, and I reviewed and tested every change as described above.
---
Arun Iyengar (2):
usb: dwc3: Tell xhci-plat when the core lost its state on resume
usb: dwc3: Keep the host bus suspended, not torn down, across suspend-to-idle
drivers/usb/dwc3/core.c | 43 +++++++++++++++++++++++++++++++++++++++---- drivers/usb/dwc3/core.h | 5 +++++ drivers/usb/dwc3/host.c | 29 ++++++++++++++++++++++++++--- 3 files changed, 70 insertions(+), 7 deletions(-)
---
base-commit: 6628e7d4c6b25c8d18f1345e3a20d21829aae7e4
change-id: 20261110-dwc3-s2idle-6e332174c57b
Best regards,
Signature · 1 line
Arun Iyengar <[email protected]>
PATCH 1/2 usb: dwc3: Tell xhci-plat when the core lost its state on resume
In host mode without wakeup, dwc3_suspend_common() tears the core down with dwc3_core_exit() and dwc3_resume_common() initialises it again, so the xHCI registers are back at their reset values when xhci-plat resumes. xhci_resume() is not told about this: it writes the saved operational registers back and asks the controller to restore its internal state (USBCMD.CRS), which cannot work on a freshly reset core. It fails with USBSTS.SRE, and xhci_resume() only then falls back to a full reset:
xhci-hcd xhci-hcd.1.auto: xHC error in resume, USBSTS 0x401, Reinit
xhci-plat already has a way for the glue to report this:
xhci_plat_priv.power_lost, which cdns3 sets the same way. Set it from dwc3_resume_common() whenever the core was re-initialised (and clear it when it was kept running for wakeup), so xhci_resume() goes straight to the reset and re-init path.
--- drivers/usb/dwc3/core.c | 3 +++ drivers/usb/dwc3/core.h | 3 +++ drivers/usb/dwc3/host.c | 29 ++++++++++++++++++++++++++--- 3 files changed, 32 insertions(+), 3 deletions(-)
drivers/usb/dwc3/core.c +3 -0
@@ -2639,8 +2639,10 @@ static int dwc3_resume_common(struct dwc3 *dwc, pm_message_t msg)if (ret)return ret;dwc3_set_prtcap (dwc,DWC3_GCTL_PRTCAP_HOST , true); +dwc3_host_resume (dwc, true);break ; } +dwc3_host_resume (dwc, false);/* Restore GUSB2PHYCFG bits that were modified in suspend */ for (i =0 ; i < dwc->num_usb2_ports; i++) { reg =dwc3_readl (dwc,DWC3_GUSB2PHYCFG (i));
@@ -2672,6 +2674,7 @@ static int dwc3_resume_common(struct dwc3 *dwc, pm_message_t msg)dwc3_otg_init (dwc);if (dwc->current_otg_role ==DWC3_OTG_ROLE_HOST ) {dwc3_otg_host_init (dwc); +dwc3_host_resume (dwc, true); }else if (dwc->current_otg_role ==DWC3_OTG_ROLE_DEVICE ) {dwc3_gadget_resume (dwc); }
drivers/usb/dwc3/core.h +3 -0
@@ -1677,11 +1677,14 @@ static inline void dwc3_pre_run_stop(struct dwc3 *dwc, bool is_on)#if IS_ENABLED (CONFIG_USB_DWC3_HOST ) ||IS_ENABLED (CONFIG_USB_DWC3_DUAL_ROLE )int dwc3_host_init (struct dwc3 *dwc);void dwc3_host_exit (struct dwc3 *dwc); +void dwc3_host_resume (struct dwc3 *dwc,bool power_lost);#else static inline int dwc3_host_init (struct dwc3 *dwc) {return 0 ; }static inline void dwc3_host_exit (struct dwc3 *dwc) { } +static inline void dwc3_host_resume (struct dwc3 *dwc,bool power_lost) +{ }#endif #if IS_ENABLED (CONFIG_USB_DWC3_GADGET ) ||IS_ENABLED (CONFIG_USB_DWC3_DUAL_ROLE )
drivers/usb/dwc3/host.c +26 -3
@@ -14,9 +14,7 @@#include <linux/usb/hcd.h> #include <linux/bitfield.h> -#include "../host/xhci-port.h" -#include "../host/xhci-ext-caps.h" -#include "../host/xhci-caps.h" +#include "../host/xhci.h" #include "../host/xhci-plat.h" #include "core.h"
@@ -223,6 +221,31 @@ int dwc3_host_init(struct dwc3 *dwc) }EXPORT_SYMBOL_GPL (dwc3_host_init); +/** +* dwc3_host_resume - tell xhci-plat whether the controller lost its state +* @dwc: pointer to our controller context structure +* @power_lost: true if the core was re-initialised during system resume +* +* When the core is torn down in system suspend (dwc3_core_exit()) and +* initialised again on resume, the xHCI registers are back at their reset +* values. xhci_resume() must then reset and re-initialise the host instead +* of trying to restore its saved register state with CRS, which fails +* (USBSTS.SRE) on a freshly reset controller. +*/ +void dwc3_host_resume (struct dwc3 *dwc,bool power_lost) +{ +struct usb_hcd *hcd; + +if (!dwc->xhci) +return ; + + hcd =platform_get_drvdata (dwc->xhci); +if (!hcd) +return ; + +hcd_to_xhci_priv (hcd)->power_lost = power_lost; +} +void dwc3_host_exit (struct dwc3 *dwc) {if (dwc->sys_wakeup)
Signature · 1 line
2.55.0
[email protected]
AI reviewRe: [PATCH 1/2] usb: dwc3: Tell xhci-plat when the core lost its state on resume
Sashiko has reviewed this patch and found no issues.
Quoted 12 lines
From: Arun Iyengar <[email protected]>
In host mode without wakeup,
dwc3_suspend_common()tears the core down withdwc3_core_exit()anddwc3_resume_common()initialises it again, so the xHCI registers are back at their reset values when xhci-plat resumes.xhci_resume()is not told about this: it writes the saved operational registers back and asks the controller to restore its internal state (USBCMD.CRS), which cannot work on a freshly reset core. It fails with USBSTS.SRE, andxhci_resume()only then falls back to a full reset:xhci-hcd xhci-hcd.1.auto: xHC error in resume, USBSTS
0x401, Reinit
Quoted 7 lines
xhci-plat already has a way for the glue to report this:
xhci_plat_priv.power_lost, which cdns3 sets the same way. Set it fromdwc3_resume_common()whenever the core was re-initialised (and clear it when it was kept running for wakeup), soxhci_resume()goes straight to the reset and re-init path.Signed-off-by Arun Iyengar <[email protected]>
Sashiko has reviewed this patch and found no issues.
Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=1
PATCH 2/2 usb: dwc3: Keep the host bus suspended, not torn down, across suspend-to-idle
In reply to Arun IyengarIn host mode, dwc3_suspend_common() tears the core down for system suspend (dwc3_core_exit(): PHYs off, core reset) unless the controller is a wakeup source. By then xhci-plat has already suspended the bus:
the root ports and every device behind them are in U3/suspend and expect to keep their power session, as the USB specification requires of a suspended bus (Documentation/driver-api/usb/persist.rst). Resetting the core ends all of them. On resume xhci-plat finds a reset controller ("root hub lost power or was reset") and the USB core has to reset-resume every device, which only works for devices that come back within the 2 s that usb_port_resume() waits for a connection.
Devices that are cut off in U3 do not necessarily come back that fast. On an Orange Pi 5 (RK3588S) the root port of the Type-A USB3 host is back in Polling 110 ms after resume, but a SanDisk Extreme drive that was suspended on it does not answer: the link goes to SS.Inactive after ~1.1 s, the drive drops its terminations at ~2.4 s and only connects again after ~3 s. A card reader behind a USB3 hub on the other controller loses VBUS when the hub is reset as part of its reset-resume and needs ~2.35 s. Both are disconnected and enumerated again on every resume, which kills a filesystem mounted on them. With the bus kept suspended they resume from U3 in milliseconds.
Suspend-to-idle does not power anything down behind the drivers' back, so there is no reason to end the power sessions there. Keep the core, its PHYs and the suspended ports, as is already done for wakeup, and mark the device as being on the awake path so that a power domain with GENPD_FLAG_ACTIVE_WAKEUP stays on as well. This matches what PCI xHCI controllers do in any system sleep state that keeps auxiliary power. Suspend-to-RAM, hibernation and controllers that need a full reinit (needs_full_reinit) still tear the core down.
--- drivers/usb/dwc3/core.c | 40 ++++++++++++++++++++++++++++++++++++---- drivers/usb/dwc3/core.h | 2 ++ 2 files changed, 38 insertions(+), 4 deletions(-)
drivers/usb/dwc3/core.c +36 -4
@@ -16,6 +16,7 @@#include <linux/spinlock.h> #include <linux/platform_device.h> #include <linux/pm_runtime.h> +#include <linux/suspend.h> #include <linux/interrupt.h> #include <linux/ioport.h> #include <linux/io.h>
@@ -2539,6 +2540,29 @@ static int dwc3_core_init_for_resume(struct dwc3 *dwc)return ret; } +/* +* In host mode, system suspend suspends the bus: xhci-plat puts the root ports +* and with them every attached device into U3/suspend, and the USB core +* expects them to keep that power session (see +* Documentation/driver-api/usb/persist.rst). Tearing the core down here ends +* all power sessions: the devices then have to go through a reset-resume, and +* a device that takes longer than the USB core allows to come back (it has to +* recover its link from scratch, a hub behind the port is reset and switches +* its own ports off) is disconnected, e.g. with a mounted filesystem. +* +* Only do that when the core will lose its state anyway or the system goes +* into a state that powers it down: keep it for wakeup, and for +* suspend-to-idle, where nothing is powered off behind the drivers' back. +*/ +static bool dwc3_host_core_exit_needed (struct dwc3 *dwc) +{ +if (dwc->needs_full_reinit) +return true; +if (device_may_wakeup (dwc->dev)) +return false; +return pm_suspend_target_state !=PM_SUSPEND_TO_IDLE ; +} +static int dwc3_suspend_common (struct dwc3 *dwc,pm_message_t msg) {u32 reg;
@@ -2569,12 +2593,20 @@ static int dwc3_suspend_common(struct dwc3 *dwc, pm_message_t msg)dwc3_core_exit (dwc);break ;case DWC3_GCTL_PRTCAP_HOST : -if (!PMSG_IS_AUTO (msg) && - (!device_may_wakeup (dwc->dev) || dwc->needs_full_reinit)) { + dwc->host_core_exited = false; +if (!PMSG_IS_AUTO (msg) &&dwc3_host_core_exit_needed (dwc)) {dwc3_core_exit (dwc); + dwc->host_core_exited = true;break ; } +/* +* The core and its PHYs stay powered to keep the suspended bus: +* keep the power domain on as well. +*/ +if (!PMSG_IS_AUTO (msg)) +device_set_awake_path (dwc->dev); +/* Let controller to suspend HSPHY before PHY driver suspends */ if (dwc->dis_u2_susphy_quirk || dwc->dis_enblslpm_quirk) {
@@ -2633,8 +2665,8 @@ static int dwc3_resume_common(struct dwc3 *dwc, pm_message_t msg)dwc3_gadget_resume (dwc);break ;case DWC3_GCTL_PRTCAP_HOST : -if (!PMSG_IS_AUTO (msg) && - (!device_may_wakeup (dwc->dev) || dwc->needs_full_reinit)) { +if (!PMSG_IS_AUTO (msg) && dwc->host_core_exited) { + dwc->host_core_exited = false; ret =dwc3_core_init_for_resume (dwc);if (ret)return ret;
drivers/usb/dwc3/core.h +2 -0
@@ -1133,6 +1133,7 @@ struct dwc3_glue_ops {* @usb3_lpm_capable: set if hadrware supports Link Power Management * @usb2_lpm_disable: set to disable usb2 lpm for host * @usb2_gadget_lpm_disable: set to disable usb2 lpm for gadget +* @host_core_exited: the host-mode core was torn down in system suspend * @needs_full_reinit: set to indicate the core may lose power and need full * initialization during system pm * @disable_scramble_quirk: set if we enable the disable scramble quirk
@@ -1403,6 +1404,7 @@ struct dwc3 { unsigned usb2_lpm_disable:1; unsigned usb2_gadget_lpm_disable:1; unsigned needs_full_reinit:1; + unsigned host_core_exited:1; unsigned disable_scramble_quirk:1; unsigned u2exit_lfps_quirk:1;
Signature · 1 line
2.55.0
[email protected]
AI reviewRe: [PATCH 2/2] usb: dwc3: Keep the host bus suspended, not torn down, across suspend-to-idle
Thank you for your contribution! Sashiko AI review found 3 potential issues to consider: - [High] usb: dwc3: dwc3_google_suspend() resets core in s2idle when wakeup is off - [High] usb: dwc3…
Thank you for your contribution! Sashiko AI review found 3 potential issues to consider:
- [High] usb: dwc3: dwc3_google_suspend() resets core in s2idle when wakeup is off
- [High] usb: dwc3: dwc3_host_core_exit_needed() breaks parent glue resets in s2idle
- [High] usb: dwc3: device_set_awake_path() fails to keep non-wakeup genpd on in s2idle
--
commit 2b3424195227cb5706b93edc71af29a9569a4ddf
Author: Arun Iyengar <[email protected]>
usb: dwc3: Keep the host bus suspended, not torn down, across suspend-to-idle
This commit skips tearing down the DWC3 core in host mode during suspend-to-idle when full reinitialization is not required, and marks the device as being on the awake path to keep its power domain active.
Quoted 4 lines
drivers/usb/dwc3/core.c +0
-0
[ ... ]
Quoted 16 lines
drivers/usb/dwc3/core.c +12
-0@@ -2539,6 +2540,29 @@ static int dwc3_core_init_for_resume(struct dwc3 *dwc)return ret; } +/* +* In host mode, system suspend suspends the bus: xhci-plat puts the root ports +* and with them every attached device into U3/suspend, and the USB core +* expects them to keep that power session (see +* Documentation/driver-api/usb/persist.rst). Tearing the core down here ends +* all power sessions: the devices then have to go through a reset-resume, and +* a device that takes longer than the USB core allows to come back (it has to +* recover its link from scratch, a hub behind the port is reset and switches +* its own ports off) is disconnected, e.g. with a mounted filesystem. +* +* Only do that when the core will lose its state anyway or the system goes +* into a state that powers it down: keep it for wakeup, and for
drivers/usb/dwc3/core.c static int dwc3_core_init_for_resume(struct dwc3 *dwc) · line 2564
Quoted 10 lines
drivers/usb/dwc3/core.c +10
-0+* suspend-to-idle, where nothing is powered off behind the drivers' back. +*/ +static bool dwc3_host_core_exit_needed (struct dwc3 *dwc) +{ +if (dwc->needs_full_reinit) +return true; +if (device_may_wakeup (dwc->dev)) +return false; +return pm_suspend_target_state !=PM_SUSPEND_TO_IDLE ; +}
[Severity: High]
Does skipping dwc3_core_exit() in PM_SUSPEND_TO_IDLE break parent glue drivers that reset or power down the hardware in their own .suspend() callbacks?
Parent glue drivers that populate a child snps,dwc3 device via of_platform_populate() bind it to dwc3_probe(), where needs_full_reinit is false. During PM_SUSPEND_TO_IDLE, device .suspend() callbacks still run:
for example, dwc3_of_simple_suspend() asserts simple->resets when simple->need_reset is set (such as on rockchip,rk3399-dwc3), dwc3_meson_g12a_suspend() powers off and exits the PHYs before pulsing reset on resume, and dwc3_exynos_suspend() disables the vdd33/vdd10 regulators.
Because dwc3_suspend_common() now leaves host_core_exited as false on those platforms, dwc3_resume_common() skips dwc3_core_init_for_resume() and dwc3_set_prtcap() and passes power_lost = false to xhci-plat after the parent glue driver has reset or power-cycled the controller, leaving the host controller uninitialized.
Quoted 6 lines
drivers/usb/dwc3/core.c +1
-0+static int dwc3_suspend_common (struct dwc3 *dwc,pm_message_t msg) {u32 reg;@@ -2569,12 +2593,20 @@ static int dwc3_suspend_common(struct dwc3 *dwc, pm_message_t msg)dwc3_core_exit (dwc);
drivers/usb/dwc3/core.c static int dwc3_suspend_common(struct dwc3 *dwc, pm_message_t msg) · line 2601
Quoted 10 lines
drivers/usb/dwc3/core.c +3
-2break ;case DWC3_GCTL_PRTCAP_HOST : -if (!PMSG_IS_AUTO (msg) && - (!device_may_wakeup (dwc->dev) || dwc->needs_full_reinit)) { + dwc->host_core_exited = false; +if (!PMSG_IS_AUTO (msg) &&dwc3_host_core_exit_needed (dwc)) {dwc3_core_exit (dwc); + dwc->host_core_exited = true;break ; }
[Severity: High]
Does dwc3_google_suspend() also need to check !google->dwc.host_core_exited instead of device_may_wakeup()?
drivers/usb/dwc3/dwc3-google.c:dwc3_google_suspend() {
if (google->dwc.current_dr_role == DWC3_GCTL_PRTCAP_HOST) {
/*
* Follow dwc3_suspend_common() guidelines for deciding between
* a full teardown and hibernation.
*/
if (PMSG_IS_AUTO(msg) || device_may_wakeup(google->dev)) {
...
return 0;
}
}
reset_control_bulk_assert(google->num_rsts, google->rsts);
clk_bulk_disable_unprepare(google->num_clks, google->clks);
}When wakeup is disabled during PM_SUSPEND_TO_IDLE, dwc3_suspend_common() now keeps the core active and sets host_core_exited to false, but dwc3_google_suspend() still falls through and asserts google->rsts. On resume, dwc3_google_resume() deasserts the resets and dwc3_resume_common() sees host_core_exited == false, skipping dwc3_core_init_for_resume() and dwc3_set_prtcap() on a reset controller.
drivers/usb/dwc3/core.c static int dwc3_suspend_common(struct dwc3 *dwc, pm_message_t msg) · line 2608
Quoted 7 lines
drivers/usb/dwc3/core.c +6
-0+/* +* The core and its PHYs stay powered to keep the suspended bus: +* keep the power domain on as well. +*/ +if (!PMSG_IS_AUTO (msg)) +device_set_awake_path (dwc->dev);
[Severity: High]
What happens on SoCs where the DWC3 power domain does not set GENPD_FLAG_ACTIVE_WAKEUP?
drivers/pmdomain/core.c:genpd_finish_suspend() {
if (device_awake_path(dev) && genpd_is_active_wakeup(genpd) &&
!device_out_band_wakeup(dev))
return 0;
...
genpd_sync_power_off(genpd, true, 0);
}genpd_finish_suspend() still runs during PM_SUSPEND_TO_IDLE and only honors device_awake_path() when the domain sets GENPD_FLAG_ACTIVE_WAKEUP. On platforms whose DWC3 power domain does not set that flag (for example, RK3568_PD_PIPE on RK3566/RK3568, where usb_host0_xhci and usb_host1_xhci bind to dwc3_probe() with needs_full_reinit == false), genpd still stops clocks and powers off the domain during suspend-to-idle, and dwc3_resume_common() then skips dwc3_core_init_for_resume() because host_core_exited is false.
Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=2
Re: [PATCH 2/2] usb: dwc3: Keep the host bus suspended, not torn down, across suspend-to-idle
In reply to Arun IyengarFrom: Vladyslav Tymoshchuk <[email protected]>
Quoted 5 lines · Arun Iyengar
Suspend-to-idle does not power anything down behind the drivers' back, so there is no reason to end the power sessions there. Keep the core, its PHYs and the suspended ports, as is already done for wakeup, and mark the device as being on the awake path so that a power domain with
GENPD_FLAG_ACTIVE_WAKEUPstays on as well.
A data point from a Qualcomm board where the first sentence does not hold. I have not run this series; what follows is the existing "kept for wakeup" path, which this patch extends to every suspend-to-idle.
On SM8750 (OnePlus Pad 3, v7.3-rc5 plus board patches) the controller node is a wakeup source, so dwc3 keeps the core in host mode. The USB GDSCs are PWRSTS_OFF_ON in gcc-sm8750 and the qcom GDSC driver does not set GENPD_FLAG_ACTIVE_WAKEUP, so genpd powers them off in s2idle all the same. With a USB stick on the port the xHCI then comes back like this, in 3 of 3 cycles of 150 s:
arm-smmu 15000000.iommu: Unhandled context fault: fsr=0x402, iova=0x669fd3200, fsynr=0x370011, cbfrsynra=0x40, cb=5 xhci-hcd xhci-hcd.3.auto: WARNING: Host System Error
xhci-hcd xhci-hcd.3.auto: xHCI host not responding to stop endpoint command
xhci-hcd xhci-hcd.3.auto: xHCI host controller not responding, assume dead
xhci-hcd xhci-hcd.3.auto: HC died; cleaning up
With the GDSCs in retention [1], as SM8650 and X1E80100 have them, the same 150 s cycle passed 5 of 5 times, and two shorter cycles of 25 s passed as well.
This is the third point of the Sashiko review seen on hardware:device_set_awake_path() does nothing for a domain without the flag, and the core is treated as kept although its power went away. I would expect 2/2 to bring this failure to SM8750 controllers with wakeup disabled too, unless [1] is applied.