[PATCH v3 5/7] usb: dwc3: qcom: Support broken suspend for SA8255P

From: Faisal Hassan

Date: Sat Oct 10 2026 - 01:36:17 EST


During APPS Suspend, the clock controller unit disables all clocks
to the CPU and subsystems, causing the controller to enter the
Power-On Reset (POR) state. This results in the loss of
Qscratch programming.

Upon APPS Resume, the USB controller does not have the proper
PIPE_UTMI_CLK_SEL or VBUS_VALID. Hence, if only the High-Speed
PHY is used for a controller capable of SuperSpeed operations,
the controller will assume it should use the USB3 PHY and not
the HS PHY as configured before APPS Suspend. This behavior
disrupts the normal operation of the USB controller. Similarly,
if the controller was operating in device mode prior to
APPS suspend, and VBUS_VALID signal is not set after APPS resume,
this will cause the PHYs to transition into the suspend state.
This behavior disrupts the normal operation of the USB controller.

Address the issues by ensuring that the PIPE_UTMI_CLK_SEL and
VBUS_VALID are handled properly after the system resumes from
APPS Suspend. This is crucial for maintaining the correct
operational state of the USB controller and preventing
unintended suspensions of the PHYs.

Also, as the controller is starting from POR state, make
sure the controller Reset is performed.

Because the controller is reset on every system resume, the
Qscratch restore is done whether or not wakeup was enabled, and
dwc3 core is told via needs_full_reinit to fully re-initialize the
core on system resume even in host mode with wakeup enabled. Wakeup
on this platform comes from the PHY interrupts routed through PDC,
not from the controller staying powered. VBUS_VALID is only forced
outside host mode, matching what probe does.

The XHCI controller is reset by the same event, so also set the
xhci_reset_on_resume dwc3 property. dwc3 passes it on to the xhci-plat
device as xhci-reset-on-resume-quirk, which makes xhci-plat set
XHCI_RESET_ON_RESUME.

Also make the .prepare callback return 0 on these platforms. A
runtime-suspended device in device mode would otherwise be left in
place across system sleep (direct_complete), skipping the system
resume callback and with it the reset and Qscratch restore.

Co-developed-by: Sriram Dash <sriram.dash@xxxxxxxxxxxxxxxx>
Signed-off-by: Sriram Dash <sriram.dash@xxxxxxxxxxxxxxxx>
Signed-off-by: Faisal Hassan <faisal.hassan@xxxxxxxxxxxxxxxx>
---
drivers/usb/dwc3/core.c | 3 +
drivers/usb/dwc3/core.h | 3 +
drivers/usb/dwc3/dwc3-qcom.c | 107 ++++++++++++++++++++++++++++-------
drivers/usb/dwc3/glue.h | 2 +
drivers/usb/dwc3/host.c | 5 +-
5 files changed, 98 insertions(+), 22 deletions(-)

diff --git a/drivers/usb/dwc3/core.c b/drivers/usb/dwc3/core.c
index d9f4e4109abb..d782d5ef9c57 100644
--- a/drivers/usb/dwc3/core.c
+++ b/drivers/usb/dwc3/core.c
@@ -1697,6 +1697,9 @@ static void dwc3_get_software_properties(struct dwc3 *dwc,
if (properties->skip_phy_init)
dwc->skip_phy_init = true;

+ if (properties->xhci_reset_on_resume)
+ dwc->xhci_reset_on_resume = true;
+
dwc->gsbuscfg0_reqinfo = DWC3_GSBUSCFG0_REQINFO_UNSPECIFIED;

if (properties->gsbuscfg0_reqinfo !=
diff --git a/drivers/usb/dwc3/core.h b/drivers/usb/dwc3/core.h
index 1c8d441ea9f8..3cbe4eeb0a5a 100644
--- a/drivers/usb/dwc3/core.h
+++ b/drivers/usb/dwc3/core.h
@@ -1177,6 +1177,7 @@ struct dwc3_glue_ops {
* 3 - Reserved
* @dis_metastability_quirk: set to disable metastability quirk.
* @dis_split_quirk: set to disable split boundary.
+ * @xhci_reset_on_resume: set to reset the xHCI host controller on system resume.
* @enable_usb2_transceiver_delay: Set to insert a delay before the
* assertion of the TxValid signal during a HS Chirp.
* @sys_wakeup: set if the device may do system wakeup.
@@ -1433,6 +1434,8 @@ struct dwc3 {
unsigned dis_metastability_quirk:1;

unsigned dis_split_quirk:1;
+ unsigned xhci_reset_on_resume:1;
+
unsigned enable_usb2_transceiver_delay:1;
unsigned async_callbacks:1;
unsigned sys_wakeup:1;
diff --git a/drivers/usb/dwc3/dwc3-qcom.c b/drivers/usb/dwc3/dwc3-qcom.c
index 508b1ee7cd0f..3860ab83c28c 100644
--- a/drivers/usb/dwc3/dwc3-qcom.c
+++ b/drivers/usb/dwc3/dwc3-qcom.c
@@ -68,6 +68,10 @@ struct dwc3_qcom_port {
enum usb_device_speed usb2_speed;
};

+struct dwc3_qcom_priv_data {
+ bool broken_suspend;
+};
+
struct dwc3_qcom {
struct device *dev;
void __iomem *qscratch_base;
@@ -86,6 +90,13 @@ struct dwc3_qcom {
struct icc_path *icc_path_apps;

enum usb_role current_role;
+
+ bool broken_suspend;
+ bool ignore_pipe_clk;
+};
+
+static const struct dwc3_qcom_priv_data sa8255p_dwc3_qcom_priv_data = {
+ .broken_suspend = true,
};

#define to_dwc3_qcom(d) container_of((d), struct dwc3_qcom, dwc)
@@ -346,6 +357,23 @@ static void dwc3_qcom_enable_interrupts(struct dwc3_qcom *qcom)
qcom->wakeup_irqs_armed = true;
}

+static void dwc3_qcom_select_utmi_clk(struct dwc3_qcom *qcom)
+{
+ /* Configure dwc3 to use UTMI clock as PIPE clock not present */
+ dwc3_qcom_setbits(qcom->qscratch_base, QSCRATCH_GENERAL_CFG,
+ PIPE_UTMI_CLK_DIS);
+
+ usleep_range(100, 1000);
+
+ dwc3_qcom_setbits(qcom->qscratch_base, QSCRATCH_GENERAL_CFG,
+ PIPE_UTMI_CLK_SEL | PIPE3_PHYSTATUS_SW);
+
+ usleep_range(100, 1000);
+
+ dwc3_qcom_clrbits(qcom->qscratch_base, QSCRATCH_GENERAL_CFG,
+ PIPE_UTMI_CLK_DIS);
+}
+
static int dwc3_qcom_suspend(struct dwc3_qcom *qcom, pm_message_t msg)
{
u32 val;
@@ -398,6 +426,28 @@ static int dwc3_qcom_resume(struct dwc3_qcom *qcom, pm_message_t msg)
if (!qcom->is_suspended)
return 0;

+ if (qcom->broken_suspend && !PMSG_IS_AUTO(msg)) {
+ /*
+ * Only system suspend fully powers off the controller and
+ * puts it into POR state. Runtime suspend does not, so skip
+ * the reset on runtime resume to avoid needlessly clobbering
+ * state that was never lost.
+ */
+ ret = reset_control_assert(qcom->resets);
+ if (ret) {
+ dev_err(qcom->dev, "failed to assert resets, err=%d\n", ret);
+ return ret;
+ }
+
+ usleep_range(10, 1000);
+
+ ret = reset_control_deassert(qcom->resets);
+ if (ret) {
+ dev_err(qcom->dev, "failed to deassert resets, err=%d\n", ret);
+ return ret;
+ }
+ }
+
/*
* Wakeup interrupts may have been armed by an earlier runtime suspend
* even if wakeup is now disabled, so always disarm them. On failure
@@ -421,6 +471,18 @@ static int dwc3_qcom_resume(struct dwc3_qcom *qcom, pm_message_t msg)
PWR_EVNT_LPM_IN_L2_MASK | PWR_EVNT_LPM_OUT_L2_MASK);
}

+ /*
+ * The reset above returned Qscratch to its POR defaults regardless of
+ * wakeup, so restore the configuration done at probe time.
+ */
+ if (qcom->broken_suspend && !PMSG_IS_AUTO(msg)) {
+ if (qcom->ignore_pipe_clk)
+ dwc3_qcom_select_utmi_clk(qcom);
+ /* Make sure vbus valid is set for PHYs after PM resume */
+ if (!dwc3_qcom_is_host(qcom))
+ dwc3_qcom_vbus_override_enable(qcom, true);
+ }
+
qcom->is_suspended = false;

return 0;
@@ -445,23 +507,6 @@ static irqreturn_t qcom_dwc3_resume_irq(int irq, void *data)
return IRQ_HANDLED;
}

-static void dwc3_qcom_select_utmi_clk(struct dwc3_qcom *qcom)
-{
- /* Configure dwc3 to use UTMI clock as PIPE clock not present */
- dwc3_qcom_setbits(qcom->qscratch_base, QSCRATCH_GENERAL_CFG,
- PIPE_UTMI_CLK_DIS);
-
- usleep_range(100, 1000);
-
- dwc3_qcom_setbits(qcom->qscratch_base, QSCRATCH_GENERAL_CFG,
- PIPE_UTMI_CLK_SEL | PIPE3_PHYSTATUS_SW);
-
- usleep_range(100, 1000);
-
- dwc3_qcom_clrbits(qcom->qscratch_base, QSCRATCH_GENERAL_CFG,
- PIPE_UTMI_CLK_DIS);
-}
-
static int dwc3_qcom_request_irq(struct dwc3_qcom *qcom, int irq,
const char *name)
{
@@ -640,8 +685,8 @@ static int dwc3_qcom_probe(struct platform_device *pdev)
struct dwc3_qcom *qcom;
struct resource res;
struct resource *r;
+ const struct dwc3_qcom_priv_data *priv_data;
int ret;
- bool ignore_pipe_clk;
bool wakeup_source;

qcom = devm_kzalloc(&pdev->dev, sizeof(*qcom), GFP_KERNEL);
@@ -650,6 +695,10 @@ static int dwc3_qcom_probe(struct platform_device *pdev)

qcom->dev = &pdev->dev;

+ priv_data = of_device_get_match_data(dev);
+ if (priv_data)
+ qcom->broken_suspend = priv_data->broken_suspend;
+
qcom->resets = devm_reset_control_array_get_optional_exclusive(dev);
if (IS_ERR(qcom->resets)) {
return dev_err_probe(&pdev->dev, PTR_ERR(qcom->resets),
@@ -704,9 +753,9 @@ static int dwc3_qcom_probe(struct platform_device *pdev)
* Disable pipe_clk requirement if specified. Used when dwc3
* operates without SSPHY and only HS/FS/LS modes are supported.
*/
- ignore_pipe_clk = device_property_read_bool(dev,
- "qcom,select-utmi-as-pipe-clk");
- if (ignore_pipe_clk)
+ qcom->ignore_pipe_clk = device_property_read_bool(dev,
+ "qcom,select-utmi-as-pipe-clk");
+ if (qcom->ignore_pipe_clk)
dwc3_qcom_select_utmi_clk(qcom);

qcom->mode = usb_get_dr_mode(dev);
@@ -732,6 +781,9 @@ static int dwc3_qcom_probe(struct platform_device *pdev)
probe_data.ignore_clocks_and_resets = true;
probe_data.properties = DWC3_DEFAULT_PROPERTIES;
probe_data.properties.skip_phy_init = true;
+ /* Controller is reset on system resume, even with wakeup enabled */
+ probe_data.properties.needs_full_reinit = qcom->broken_suspend;
+ probe_data.properties.xhci_reset_on_resume = qcom->broken_suspend;
ret = dwc3_core_probe(&probe_data);
if (ret) {
ret = dev_err_probe(dev, ret, "failed to register DWC3 Core\n");
@@ -820,6 +872,15 @@ static void dwc3_qcom_complete(struct device *dev)
static int dwc3_qcom_prepare(struct device *dev)
{
struct dwc3 *dwc = dev_get_drvdata(dev);
+ struct dwc3_qcom *qcom = to_dwc3_qcom(dwc);
+
+ /*
+ * The controller loses power across system suspend, so the system
+ * resume callback must always run to reset and reprogram it. Don't
+ * let the PM core leave a runtime-suspended device as-is.
+ */
+ if (qcom->broken_suspend)
+ return 0;

return dwc3_pm_prepare(dwc);
}
@@ -864,6 +925,10 @@ static const struct dev_pm_ops dwc3_qcom_dev_pm_ops = {
};

static const struct of_device_id dwc3_qcom_of_match[] = {
+ {
+ .compatible = "qcom,sa8255p-dwc3",
+ .data = &sa8255p_dwc3_qcom_priv_data,
+ },
{ .compatible = "qcom,snps-dwc3" },
{ }
};
diff --git a/drivers/usb/dwc3/glue.h b/drivers/usb/dwc3/glue.h
index 72c5593e657a..028a4f8a4fac 100644
--- a/drivers/usb/dwc3/glue.h
+++ b/drivers/usb/dwc3/glue.h
@@ -15,11 +15,13 @@
* @needs_full_reinit: indicate the controller may not remain power during system
* pm and need full initialization
* @skip_phy_init: skip xHCI PHY initialization on host init
+ * @xhci_reset_on_resume: reset the xHCI host controller on system resume
*/
struct dwc3_properties {
u32 gsbuscfg0_reqinfo;
unsigned needs_full_reinit:1;
unsigned skip_phy_init:1;
+ unsigned xhci_reset_on_resume:1;
};

#define DWC3_DEFAULT_PROPERTIES ((struct dwc3_properties){ \
diff --git a/drivers/usb/dwc3/host.c b/drivers/usb/dwc3/host.c
index 3fb8be8be590..a9c81def13c2 100644
--- a/drivers/usb/dwc3/host.c
+++ b/drivers/usb/dwc3/host.c
@@ -130,7 +130,7 @@ static int dwc3_host_get_irq(struct dwc3 *dwc)

int dwc3_host_init(struct dwc3 *dwc)
{
- struct property_entry props[7];
+ struct property_entry props[9];
struct platform_device *xhci;
int ret, irq;
int prop_idx = 0;
@@ -177,6 +177,9 @@ int dwc3_host_init(struct dwc3 *dwc)
if (dwc->skip_phy_init)
props[prop_idx++] = PROPERTY_ENTRY_BOOL("xhci-skip-phy-init-quirk");

+ if (dwc->xhci_reset_on_resume)
+ props[prop_idx++] = PROPERTY_ENTRY_BOOL("xhci-reset-on-resume-quirk");
+
/**
* WORKAROUND: dwc3 revisions <=3.00a have a limitation
* where Port Disable command doesn't work.
--
2.34.1