Author SHA1 Message Date
NoahandClaude Opus 5 8066ef7d79 Stop the OTG state machines running blind in either role
Widens the #160 fix, which was scoped too narrowly to host role, and fixes
flare-edge#168.

Device role was deliberately left alone on the reasoning that the state
machine is only meaningless when the port is a host. That was wrong. On a
board with no extcon phandle the machine has no external role signal in
EITHER direction, and with bvalid pinned high by the VBUS bypass its charger
detection cannot be meaningful at all. In device role its verdict suspends
the phy underneath a bound gadget, so the USB recovery link dies.

Reproduced deterministically on warden-c8a3 with a host attached:

  device role, host attached   ccf=1  phy_sus=0x0    udc=configured  works
  -> host role                 ccf=1  phy_sus=0x0    udc=(none)
  -> back to gadget            ccf=0  phy_sus=0x1d1  udc=configured  SUSPENDED
  -> +15s / +30s               ccf=0  phy_sus=0x1d1  udc=configured  never recovers

and the 2207:0019 device disappears from the host for good.

Nothing notices, because every obvious signal lies: udc state reads
"configured", usb0 carrier reads 1, and DCTL RUN_STOP reads 1, so the gadget
really is asserting its pullup. It asserts into a phy whose analog front end
and 480M clock are off, so the SoC sees SE0 and the host sees no device at
all, not even an enumeration error.

It cannot self-heal either. bvalid is pinned high, so a host attaching later
produces no VBUS edge for the machine to trigger on: it ran once, powered the
phy off, and nothing re-evaluates. That is why a reboot appears to fix it and
why this read as a cable fault for weeks. It is not, and the cables were fine.

Gated on the extcon being self-allocated rather than on the board compatible,
so it describes the actual precondition: no external role signal exists.

Renamed from 29-usb2phy-no-otg-sm-in-host.patch, whose name now misstated the
scope.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MVGTC78dgCGfRANNKjoPea
2026-09-23 10:41:29 -06:00
NoahandClaude Opus 5 f6add60a9c 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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MVGTC78dgCGfRANNKjoPea
2026-09-23 00:25:15 -06:00
NoahandClaude Opus 5 4a5481b6a5 Instrument the dwc3 OTG path to test the guard
The guard alone did not fix flare-edge#160: with dr_mode="otg" and the live
re-init skipped, the controller still died 14s after carrier-on under load
(carrier 54.1s, first Tx timeout 63.4s, HC died 68.4s).

That means either the OTG event path is not the trigger, or the harm comes from
somewhere else in it. dr_mode="host" removes far more than this one call: it
also skips dwc3_otgregs_init() and hands port-power control to xHCI instead of
the OTG block.

IRQ 47 is shared between dwc3-otg and xhci-hcd, so /proc/interrupts cannot say
whether OTG events fire at all. CONFIG_DYNAMIC_DEBUG is off, so dev_dbg is
compiled out and invisible.

So this build logs, ratelimited and always compiled:
  - every OTG hardware event with its OEVT value, role and restart flag
  - each time the guard skips a re-init on a live host

Run under load, that answers whether the OTG path is even active during the
failure window, instead of guessing a third patch.

Also worth measuring: load accelerates this dramatically -- 452-587s idle
versus 9-14s under line-rate traffic -- so idle soaks are not comparable to
load tests and earlier comparisons need re-reading with that in mind.

Refs flare-edge#160

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MVGTC78dgCGfRANNKjoPea
2026-09-22 17:34:51 -06:00
NoahandClaude Opus 5 9ff60d60db Do not re-init OTG host registers on a live controller
dwc3_otg_host_init() carries the comment "should be called before Host
controller driver is started". It rewrites OCTL/OCFG, touches GUSB2PHYCFG and
re-asserts port power (OCTL.PrtPwrCtl).

dwc3_otg_irq() sets otg_restart_host ONLY when current_otg_role is already
DWC3_OTG_ROLE_HOST. The threaded handler then calls dwc3_otg_host_init()
unconditionally, so that path always ran against a live, running host -- the
exact condition the comment forbids. Re-asserting port power under a running
xHCI corrupts an in-flight command.

Observed on the RV1106 86-panel as the controller failing to answer a Stop
Endpoint command, after which xhci-hcd declares it dead and tears the bus down.
An r8152 USB NIC stalls a transfer and the controller died 8 times out of 8
between 452s and 587s. Compiling the OTG path out entirely (dr_mode=host)
survived 1810s with zero deaths, which localised it here.

On this board the event is always spurious: the dwc3 node deliberately carries
no extcon, because wiring it pins the role to peripheral on D1-modded panels,
and CONIDSTS reads 0 -- so the role cannot legitimately change while host is
current. Skip the re-init in that case and log it at debug level.

This does not remove the restart capability. dwc3_set_mode() still runs
immediately below, and if the role has genuinely changed dwc3_otg_update()
performs a proper dwc3_host_exit() before re-initialising. The only behaviour
removed is poking a running controller, which was never legitimate.

Refs flare-edge#160

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MVGTC78dgCGfRANNKjoPea
2026-09-22 16:54:54 -06:00
BFE EngineeringandClaude Opus 5 e12bb2c554 Implement RV1106 USB2 PHY signal tuning
Cherry-picked onto the pinned SDK so the ONLY delta versus the pin is this
patch -- the xHCI controller-death investigation needs a controlled build.

The forward-port omitted the vendor driver's rv1106_usb2phy_tuning with the
note "the phy is functional without it". It is functional, but not reliable:
with these registers at reset defaults the HS link is electrically marginal
under sustained traffic, a bulk transfer eventually stalls, and the r8152
Tx-timeout recovery issues an xHCI Stop Endpoint command the controller never
answers. Measured carrier-on to death over four runs: 14s, 519s, 521s, 584s.

Refs flare-edge#160

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MVGTC78dgCGfRANNKjoPea
2026-09-22 12:11:14 -06:00
4 changed files with 270 additions and 2 deletions
@@ -1,12 +1,14 @@
* RV1106 USB2 PHY: single OTG port at 0xff3e0000 (ported from the vendor driver; * RV1106 USB2 PHY: single OTG port at 0xff3e0000 (ported from the vendor driver;
* fields map 1:1 to 6.18 except utmi_iddig -> utmi_id, and the 5.10-only * fields map 1:1 to 6.18 except utmi_iddig -> utmi_id, and the 5.10-only
* iddig_output/iddig_en/bvalid_grf_sel are dropped). Signal-quality phy_tuning is * iddig_output/iddig_en/bvalid_grf_sel are dropped). Signal-quality phy_tuning
* left off for now (guarded, so NULL is safe): the phy is functional without it. * was originally left off here; it is now implemented (patches/27-usb-phy-tuning.patch)
* because "functional without it" turned out not to mean reliable -- see #160.
*/ */
static const struct rockchip_usb2phy_cfg rv1106_phy_cfgs[] = { static const struct rockchip_usb2phy_cfg rv1106_phy_cfgs[] = {
{ {
.reg = 0xff3e0000, .reg = 0xff3e0000,
.num_ports = 1, .num_ports = 1,
.phy_tuning = rv1106_usb2phy_tuning,
.clkout_ctl = { 0x0058, 4, 4, 1, 0 }, .clkout_ctl = { 0x0058, 4, 4, 1, 0 },
.port_cfgs = { .port_cfgs = {
[USB2PHY_PORT_OTG] = { [USB2PHY_PORT_OTG] = {
+128
View File
@@ -0,0 +1,128 @@
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
@@ -247,6 +247,7 @@
struct device *dev;
struct regmap *grf;
struct regmap *usbgrf;
+ void __iomem *phy_base;
struct clk_bulk_data *clks;
struct clk *clk480m;
struct clk_hw clk480m_hw;
@@ -1417,6 +1418,19 @@
return -EINVAL;
}
+ /*
+ * RV1106 tunes signal quality through the PHY's own register block, which
+ * is this node's "reg" resource and is distinct from the GRF regmap above.
+ * Map it only where a tuning callback needs it; every other SoC here tunes
+ * through the GRF and must keep working with phy_base left NULL.
+ */
+ if (of_device_is_compatible(np, "rockchip,rv1106-usb2phy")) {
+ rphy->phy_base = devm_platform_ioremap_resource(pdev, 0);
+ if (IS_ERR(rphy->phy_base))
+ return dev_err_probe(dev, PTR_ERR(rphy->phy_base),
+ "failed to map phy registers\n");
+ }
+
rphy->phy_reset = devm_reset_control_get_optional(dev, "phy");
if (IS_ERR(rphy->phy_reset))
return PTR_ERR(rphy->phy_reset);
@@ -1506,6 +1520,30 @@
return ret;
}
+/*
+ * Direct PHY-register accessors. Most SoCs here tune through the GRF regmap,
+ * but the RV1106's signal-quality registers live in the PHY's own block, which
+ * is mapped separately into rphy->phy_base.
+ */
+static inline void phy_update_bits(void __iomem *reg, u32 mask, u32 val)
+{
+ u32 tmp = readl(reg);
+
+ tmp &= ~mask;
+ tmp |= val & mask;
+ writel(tmp, reg);
+}
+
+static inline void phy_set_bits(void __iomem *reg, u32 bits)
+{
+ writel(readl(reg) | bits, reg);
+}
+
+static inline void phy_clear_bits(void __iomem *reg, u32 bits)
+{
+ writel(readl(reg) & ~bits, reg);
+}
+
static int rk3128_usb2phy_tuning(struct rockchip_usb2phy *rphy)
{
/* Turn off differential receiver in suspend mode */
@@ -2291,10 +2329,65 @@
* iddig_output/iddig_en/bvalid_grf_sel are dropped). Signal-quality phy_tuning is
* left off for now (guarded, so NULL is safe) — the phy is functional without it.
*/
+/*
+ * RV1106 USB2 PHY signal-quality tuning, ported from the vendor 5.10 driver
+ * (drivers/phy/rockchip/phy-rockchip-inno-usb2.c, rv1106_usb2phy_tuning).
+ *
+ * The 6.18 forward-port originally omitted this ("the phy is functional without
+ * it"). It is functional, but not reliable: with these registers left at reset
+ * defaults the HS link is electrically marginal under sustained traffic. On a
+ * bench panel a USB-Ethernet dongle would stall a bulk transfer after a few
+ * minutes; the r8152 Tx-timeout recovery then issues an xHCI Stop Endpoint
+ * command which the controller never answers, and xhci-hcd declares the
+ * controller dead and tears the bus down (flare-edge issue #160). Measured
+ * carrier-on to death across four runs: 14s, 519s, 521s, 584s -- a stochastic
+ * spread, not a timer, which is what marginal signalling looks like.
+ *
+ * Pre-emphasis strength is silicon-revision dependent. The vendor driver picks
+ * it from rockchip_get_cpu_version(), which does not exist in mainline; it
+ * derives from RV1106_OS_REG1 (0xff020204) & 0x7. Our panels read 1, i.e. the
+ * non-zero branch, so 3'b001 is pinned here. If a board ever reports version 0
+ * it wants 3'b011 instead -- see the vendor driver.
+ */
+static int rv1106_usb2phy_tuning(struct rockchip_usb2phy *rphy)
+{
+ /* Mapped only for this compatible; refuse to poke a NULL base. */
+ if (!rphy->phy_base)
+ return -ENODEV;
+
+ /* Always enable pre-emphasis in SOF & EOP & chirp & non-chirp state */
+ phy_update_bits(rphy->phy_base + 0x30, GENMASK(2, 0), 0x07);
+
+ /* Set Tx HS pre_emphasize strength to 3'b001 (cpu_version != 0) */
+ phy_update_bits(rphy->phy_base + 0x40, GENMASK(5, 3), (0x01 << 3));
+
+ /* Set RX Squelch trigger point configure to 4'b0000(112.5 mV) */
+ phy_update_bits(rphy->phy_base + 0x64, GENMASK(6, 3), (0x00 << 3));
+
+ /* Turn off differential receiver by default to save power */
+ phy_clear_bits(rphy->phy_base + 0x100, BIT(6));
+
+ /* Set 45ohm HS ODT value to 5'b10111 to increase driver strength */
+ phy_update_bits(rphy->phy_base + 0x11c, GENMASK(4, 0), 0x17);
+
+ /* Set Tx HS eye height tuning to 3'b011(462 mV) */
+ phy_update_bits(rphy->phy_base + 0x124, GENMASK(4, 2), (0x03 << 2));
+
+ /* Bypass Squelch detector calibration */
+ phy_update_bits(rphy->phy_base + 0x1a4, GENMASK(7, 4), (0x01 << 4));
+ phy_update_bits(rphy->phy_base + 0x1b4, GENMASK(7, 4), (0x01 << 4));
+
+ /* Set HS disconnect detect mode to single ended detect mode */
+ phy_set_bits(rphy->phy_base + 0x70, BIT(2));
+
+ return 0;
+}
+
static const struct rockchip_usb2phy_cfg rv1106_phy_cfgs[] = {
{
.reg = 0xff3e0000,
.num_ports = 1,
+ .phy_tuning = rv1106_usb2phy_tuning,
.clkout_ctl = { 0x0058, 4, 4, 1, 0 },
.port_cfgs = {
[USB2PHY_PORT_OTG] = {
+136
View File
@@ -0,0 +1,136 @@
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;
};
/**
@@ -239,6 +241,8 @@
* @dcd_retries: The retry count used to track Data contact
* detection process.
* @edev: extcon device for notification registration
+ * @edev_self_allocated: no extcon phandle in DT, so the OTG state machine
+ * has no external role signal at all
* @irq: muxed interrupt for single irq configuration
* @phy_cfg: phy register configuration, assigned by driver data.
* @ports: phy port instance.
@@ -257,6 +261,7 @@
enum power_supply_type chg_type;
u8 dcd_retries;
struct extcon_dev *edev;
+ bool edev_self_allocated;
int irq;
const struct rockchip_usb2phy_cfg *phy_cfg;
struct rockchip_usb2phy_port ports[USB2PHY_NUM_PORTS];
@@ -459,6 +464,14 @@
if (ret)
return dev_err_probe(rphy->dev, ret,
"failed to register extcon device\n");
+
+ /*
+ * Nothing external drives the role on this board. The extcon
+ * below is ours, and no code ever sets a cable state on it, so
+ * the OTG state machine's view of the world is permanently
+ * blank rather than merely stale.
+ */
+ rphy->edev_self_allocated = true;
}
rphy->edev = edev;
@@ -649,11 +662,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 +721,15 @@
unsigned long delay;
bool vbus_attach, sch_work, notify_charger;
+ /*
+ * This machine can only get the answer wrong here. In host role its
+ * verdict suspends a phy that is enumerating a device (#160); with a
+ * self-allocated extcon it has no role signal in EITHER direction, and
+ * in device role it strands the gadget the same way (#168).
+ */
+ if (rport->host_mode || rphy->edev_self_allocated)
+ return;
+
vbus_attach = property_enabled(rphy->grf,
&rport->port_cfg->utmi_bvalid);
@@ -825,6 +888,15 @@
bool is_dcd, tmout, vout, vbus_attach;
unsigned long delay;
+ /*
+ * Never probe the front end of a port that is hosting a device, and
+ * never at all on a board whose bvalid is pinned high by the VBUS
+ * bypass: charger detection cannot be meaningful there, and its DCP
+ * verdict powers the phy off underneath a working link (#160, #168).
+ */
+ if (rport->host_mode || rphy->edev_self_allocated)
+ return;
+
vbus_attach = property_enabled(rphy->grf, &rport->port_cfg->utmi_bvalid);
dev_dbg(&rport->phy->dev, "chg detection work state = %d\n",
+2
View File
@@ -18,6 +18,8 @@ with `../build/warden_defconfig` -> `zImage` + `rv1106-warden.dtb`).
| `15-pinctrl.patch` | `pinctrl-rockchip` RV1106 iomux | | `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` | | `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 | | `25-usb-phy.patch` | inno-usb2 phy |
| `27-usb-phy-tuning.patch` | inno-usb2 phy: RV1106 signal-quality register tuning |
| `29-usb2phy-no-blind-otg-sm.patch` | inno-usb2 phy: stop the OTG/charger state machines running blind |
| `30-drm-vop-panel-rgb.patch` | VOP (`rgb_dclk_pol`=0 + WIN1 scanout), panel-simple bus-format, rockchip_rgb | | `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) | | `35-mailbox-hpmcu.patch` | rockchip-mailbox (rv1106 num_chans=1) |
| `40-pvtm-soc.patch` | PVTM (core+pmu) + `include/soc/rockchip/pvtm.h` | | `40-pvtm-soc.patch` | PVTM (core+pmu) + `include/soc/rockchip/pvtm.h` |