From f6add60a9c02924706a47025eae08fc4d011c62e Mon Sep 17 00:00:00 2001 From: Noah Date: Wed, 23 Sep 2026 00:25:15 -0600 Subject: [PATCH] Keep the usb2 phy OTG state machines out of host role Issue #160: the USB ethernet link on the OTG port stops passing traffic and the host controller is later declared dead ("xHCI host not responding to stop endpoint command", "HC died"). Root cause. The phy's OTG state machine and charger detection only make sense while the port is a peripheral. This board has no ID source, so in host role their inputs are meaningless: rockchip_usb2phy_otg_sm_work() reaches OTG_STATE_B_IDLE with vbus asserted, schedules rockchip_chg_detect_work(), and a DCP verdict calls rockchip_usb2phy_power_off(). That suspends the phy and drops the 480M clkout the controller's core runs on, while a device is enumerated and working. The controller then stops clocking with its register block still readable, so it reports itself healthy: USBCMD keeps RS set, USBSTS reads 0, PORTSC still shows CCS and PED. Only MFINDEX gives it away by no longer advancing. Nothing completes from that point, and the first transmit afterwards times out. The recovery path cannot help either, because xhci_halt() needs the core clock to update USBSTS.HCH, so it times out and xhci_reset() returns early without ever writing USBCMD.HCRST. dwc3 already calls phy_set_mode() on every role change, but this driver implemented no .set_mode, so the phy never learned the role. Implement it, track the role per port, and return early from both work functions in host role. The check is inside the functions because both have direct callers besides the scheduled work. This keeps dr_mode "otg" and runtime role switching. Setting dr_mode to "host" also avoids the fault, by skipping the same state machine, but compiles out the gadget needed for flashing, recovery and pairing. Measured on warden-c8a3, per arm, transmit packets and xhci interrupts delivered after the stated idle: idle 0 s +9, +10, +9 packets +170, +176, +188 interrupts idle 120 s +0, +0, +0 packets +0, +0, +0 interrupts Also drops 28-dwc3-otg-no-live-reinit.patch. It guarded dwc3_otg_host_init() against re-entry on a live controller, but that path never executes here: GSTS reads OTG IP clear and instrumentation logged no entries on either the hardware irq or the guard site. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01MVGTC78dgCGfRANNKjoPea --- patches/28-dwc3-otg-no-live-reinit.patch | 33 -------- patches/29-usb2phy-no-otg-sm-in-host.patch | 94 ++++++++++++++++++++++ patches/README.md | 2 + 3 files changed, 96 insertions(+), 33 deletions(-) delete mode 100644 patches/28-dwc3-otg-no-live-reinit.patch create mode 100644 patches/29-usb2phy-no-otg-sm-in-host.patch diff --git a/patches/28-dwc3-otg-no-live-reinit.patch b/patches/28-dwc3-otg-no-live-reinit.patch deleted file mode 100644 index 386076d..0000000 --- a/patches/28-dwc3-otg-no-live-reinit.patch +++ /dev/null @@ -1,33 +0,0 @@ -diff --git a/drivers/usb/dwc3/drd.c b/drivers/usb/dwc3/drd.c ---- a/drivers/usb/dwc3/drd.c -+++ b/drivers/usb/dwc3/drd.c -@@ -55,7 +55,17 @@ - - spin_lock(&dwc->lock); - if (dwc->otg_restart_host) { -- dwc3_otg_host_init(dwc); -+ /* -+ * flare-edge#160: dwc3_otg_host_init() is documented as "should be -+ * called before Host controller driver is started" but this path -+ * only ever runs while current_otg_role is already HOST. Skip it -+ * on a live host and record that we did. -+ */ -+ if (dwc->current_otg_role == DWC3_OTG_ROLE_HOST) -+ dev_warn_ratelimited(dwc->dev, -+ "OTGDIAG skipped host re-init on live host\n"); -+ else -+ dwc3_otg_host_init(dwc); - dwc->otg_restart_host = false; - } - -@@ -83,6 +93,10 @@ - if (dwc->current_otg_role == DWC3_OTG_ROLE_HOST && - !(reg & DWC3_OEVT_DEVICEMODE)) - dwc->otg_restart_host = true; -+ /* flare-edge#160 diagnostic: is the OTG event path active at all? */ -+ dev_warn_ratelimited(dwc->dev, -+ "OTGDIAG irq OEVT=0x%08x role=%u restart=%d\n", -+ reg, dwc->current_otg_role, dwc->otg_restart_host); - dwc3_writel(dwc, DWC3_OEVT, reg); - ret = IRQ_WAKE_THREAD; - } diff --git a/patches/29-usb2phy-no-otg-sm-in-host.patch b/patches/29-usb2phy-no-otg-sm-in-host.patch new file mode 100644 index 0000000..932a12b --- /dev/null +++ b/patches/29-usb2phy-no-otg-sm-in-host.patch @@ -0,0 +1,94 @@ +diff --git a/drivers/phy/rockchip/phy-rockchip-inno-usb2.c b/drivers/phy/rockchip/phy-rockchip-inno-usb2.c +--- a/drivers/phy/rockchip/phy-rockchip-inno-usb2.c ++++ b/drivers/phy/rockchip/phy-rockchip-inno-usb2.c +@@ -203,6 +203,7 @@ + * @event_nb: hold event notification callback. + * @state: define OTG enumeration states before device reset. + * @mode: the dr_mode of the controller. ++ * @host_mode: the port is currently acting as a USB host. + */ + struct rockchip_usb2phy_port { + struct phy *phy; +@@ -222,6 +223,7 @@ + struct notifier_block event_nb; + enum usb_otg_state state; + enum usb_dr_mode mode; ++ bool host_mode; + }; + + /** +@@ -649,11 +651,52 @@ + return 0; + } + ++/* ++ * The OTG port's state machines only make sense while the port is a ++ * peripheral. In host role their inputs are meaningless on a board with no ++ * ID source: charger detection drives the analog front end of a port that is ++ * already enumerating a device, and its DCP verdict ends in ++ * rockchip_usb2phy_power_off(), which suspends the phy and drops the 480M ++ * clkout that the host controller's core runs on. The controller then stops ++ * clocking with its registers still readable, so it reports itself healthy ++ * while no transfer ever completes. ++ * ++ * dwc3 already calls phy_set_mode() on every role change, but this driver ++ * implemented no .set_mode, so the phy never learned the role. Track it and ++ * keep both state machines out of host role. ++ */ ++static int rockchip_usb2phy_set_mode(struct phy *phy, enum phy_mode mode, ++ int submode) ++{ ++ struct rockchip_usb2phy_port *rport = phy_get_drvdata(phy); ++ ++ if (rport->port_id != USB2PHY_PORT_OTG) ++ return 0; ++ ++ switch (mode) { ++ case PHY_MODE_USB_HOST: ++ if (!rport->host_mode) { ++ rport->host_mode = true; ++ cancel_delayed_work_sync(&rport->otg_sm_work); ++ cancel_delayed_work_sync(&rport->chg_work); ++ } ++ break; ++ case PHY_MODE_USB_DEVICE: ++ rport->host_mode = false; ++ break; ++ default: ++ break; ++ } ++ ++ return 0; ++} ++ + static const struct phy_ops rockchip_usb2phy_ops = { + .init = rockchip_usb2phy_init, + .exit = rockchip_usb2phy_exit, + .power_on = rockchip_usb2phy_power_on, + .power_off = rockchip_usb2phy_power_off, ++ .set_mode = rockchip_usb2phy_set_mode, + .owner = THIS_MODULE, + }; + +@@ -667,6 +710,10 @@ + unsigned long delay; + bool vbus_attach, sch_work, notify_charger; + ++ /* In host role this machine can only get the answer wrong. */ ++ if (rport->host_mode) ++ return; ++ + vbus_attach = property_enabled(rphy->grf, + &rport->port_cfg->utmi_bvalid); + +@@ -825,6 +872,10 @@ + bool is_dcd, tmout, vout, vbus_attach; + unsigned long delay; + ++ /* Never probe the front end of a port that is hosting a device. */ ++ if (rport->host_mode) ++ return; ++ + vbus_attach = property_enabled(rphy->grf, &rport->port_cfg->utmi_bvalid); + + dev_dbg(&rport->phy->dev, "chg detection work state = %d\n", diff --git a/patches/README.md b/patches/README.md index db08dc1..14f1e34 100644 --- a/patches/README.md +++ b/patches/README.md @@ -18,6 +18,8 @@ with `../build/warden_defconfig` -> `zImage` + `rv1106-warden.dtb`). | `15-pinctrl.patch` | `pinctrl-rockchip` RV1106 iomux | | `20-arch-dts-bindings.patch` | `arch/arm` DTs (`rv1106.dtsi`, `rv1106-warden.dts`, pinctrl), mach, configs, `include/dt-bindings` | | `25-usb-phy.patch` | inno-usb2 phy | +| `27-usb-phy-tuning.patch` | inno-usb2 phy: RV1106 signal-quality register tuning | +| `29-usb2phy-no-otg-sm-in-host.patch` | inno-usb2 phy: keep the OTG/charger state machines out of host role | | `30-drm-vop-panel-rgb.patch` | VOP (`rgb_dclk_pol`=0 + WIN1 scanout), panel-simple bus-format, rockchip_rgb | | `35-mailbox-hpmcu.patch` | rockchip-mailbox (rv1106 num_chans=1) | | `40-pvtm-soc.patch` | PVTM (core+pmu) + `include/soc/rockchip/pvtm.h` |