Skip to content

DWC2 Driver: Unable to power cycle vbus regulator enabled USB port more than once. #7349

@gizahNL

Description

@gizahNL

Describe the bug

I have a CM5 where I need to switch off USB 5v VBUS to the USB2 (OTG) USB Port, this is implemented on our PCB.

While implementing my voltage regulator driver I noticed that I can only power cycle the USB port once, further power cycle requests (i.e. via uhubctl) fail to disable the regulator. And the regulator usage count stays >0.

I've asked claude for possible reasons, this is what it responded with:


● Found the definitive mechanism. dwc2_core_host_init only re-asserts HPRT0_PWR if it was cleared — and it's called from dwc2_hcd_reinit during _dwc2_hcd_resume. Now let me confirm the call
  chain and the final write at line 4548:

  The root cause is now fully identified:

  _dwc2_hcd_resume is registered as the bus_resume hook (line 5052-5053). When the USB bus resumes from a suspended state (lx_state == DWC2_L2), it does three things that fight
  ClearPortFeature(PORT_POWER):

  1. Line 4508: dwc2_hcd_reinit → dwc2_core_host_init (line 2262-2270): if op_state == OTG_STATE_A_HOST (which is always set for dr_mode = "host") and HPRT0_PWR is currently 0 — it writes it
   back to 1.
  2. Line 4538: dwc2_vbus_supply_init — unconditional regulator_enable, incrementing use_count.
  3. Line 4548: explicitly writes HPRT0_PWR | HPRT0_CONNDET | HPRT0_ENACHG to HPRT0 — forcing HPRT0_PWR = 1 regardless.

  The trigger sequence:
  1. ClearPortFeature(PORT_POWER) clears HPRT0_PWR = 0, calls dwc2_vbus_supply_exit
  2. If the bus was in lx_state = DWC2_L2 (auto-suspended), OR the device disconnect event causes the USB core to call bus_resume to process the port status change...
  3. _dwc2_hcd_resume runs: lx_state == L2 check passes, CONNSTS == 0 check passes (device just disconnected), falls through to the full resume path
  4. HPRT0_PWR is re-written to 1, use_count is incremented again

  Why the bus is in L2: USB autosuspend. When the connected device goes idle (default: 2 seconds), the USB stack auto-suspends it, then auto-suspends the root hub, calling bus_suspend →
  _dwc2_hcd_suspend → sets lx_state = L2.

  ---
  Fix: disable root hub autosuspend

  echo -1 > /sys/bus/usb/devices/usb1/power/autosuspend_delay

  With autosuspend disabled, _dwc2_hcd_suspend (bus_suspend) is never called, lx_state stays at DWC2_L0, and _dwc2_hcd_resume exits immediately at line 4455:
  if (hsotg->lx_state != DWC2_L2)
      goto unlock;  // ← exits here, no HPRT0_PWR re-assertion

  To make this permanent, add a udev rule:
  # /etc/udev/rules.d/99-usb-no-autosuspend.rules
  ACTION=="add", SUBSYSTEM=="usb", DRIVER=="usb", ATTR{idVendor}=="0000", \
      ATTR{power/autosuspend_delay}="-1"

  Or target the root hub specifically:
  ACTION=="add", SUBSYSTEM=="usb", KERNELS=="usb1", ATTR{power/autosuspend_delay}="-1"

  The deeper issue: DWC2's bus_resume is designed to unconditionally restore port power (it has no awareness that ClearPortFeature(PORT_POWER) was intentional). ClearPortFeature(PORT_POWER)
  and bus autosuspend/resume are fighting each other. Disabling autosuspend removes bus_suspend/resume from the equation entirely, leaving use_count managed only by _dwc2_hcd_start (initial
  enable) and ClearPortFeature/SetPortFeature (user's uhubctl calls).

Disabling autosuspend does indeed work around the issue.

Steps to reproduce the behaviour

Couple a regulator to the USB port via a dtoverlay (untested example!):

/dts-v1/;
/plugin/;
/{
    compatible = "brcm,bcm2708";
    fragment@0 {
    target-path = "/";
        __overlay__ {
            usb_power_regulator: fixedregulator@0 {
                compatible = "regulator-fixed";
                regulator-name = "fixed-supply";
                regulator-min-microvolt = <5000000>;
                regulator-max-microvolt = <5000000>;
                gpio = <&gpios 14 0>;
                enable-active-high;
            };
        };
    };
    fragment@1 {
        target = <&usb>;
        dwc2_usb: __overlay__ {
                power-domains = <0 0>;
                compatible = "brcm,bcm2835-usb";
                dr_mode = "host";
                g-np-tx-fifo-size = <32>;
                g-rx-fifo-size = <558>;
                g-tx-fifo-size = <512 512 512 512 512 256 256>;
                status = "okay";
                vbus-supply = <&usb_power_regulator>;
        };
    };
};

And use uhubctrl to disable the USB port (CM5):
uhubctl -l 5 -p 1 -a off
verify the regulator switched to disabled via sysfs then re-enable it:
uhubctl -l 5 -p 1 -a on

Observe the regulator is back on, then repeat and observe the regulator doesn't get disabled anymore

Device (s)

Raspberry Pi CM5

System

CM5, custom receiver PCB, Custom Yocto based OS, RPI-Linux 6.12.75

Logs

No response

Additional context

If this is by design, feel free to close, the work-around also works for me.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions