Files
openipc-builder/devices/gk7205v200_otg_generic/general/overlay/usr/bin/usb-mode
T
OpenIPC AI teamandGitHub f86ea4f4a6 gk7205v200_otg: settle the USB role before xHCI probes (#147)
The OTG profile could not see USB devices in host mode. On a board with a
UVC webcam on the USB tail, `lsusb` showed only the two root hubs, while the
same board running a plain lite image enumerated the webcam normally.

The controller was in the host role and the host controller was running --
mode file "host", xhci-hcd bound, USBCMD 0x5, PORTSC1 port-power set -- and
the port still never reported a connect (PORTSC1 0x00000280, CCS clear).

dr_mode = "otg" makes dwc3_core_init() program GCTL.PRTCAPDIR = OTG(3), and
dwc3_core_init_mode() registers xhci-hcd on top of that. This 4.9 dwc3 has no
working OTG role-detection block, so xHCI probes, resets the controller and
brings up its root hubs while the core is in a role it cannot drive. The port
is then dead for the life of that probe.

S72usbmode's `usb-mode host` did not repair it and could not: writing the
debugfs mode file reaches dwc3_set_mode(), which flips GCTL.PRTCAPDIR and
nothing else, leaving the already-probed xHCI with the port state machine it
built in OTG. It also skipped the bind, because xhci-hcd was already bound.

Fix it where the ordering is: patch dwc3_core_init() to program
PRTCAPDIR = HOST for the OTG case, so xHCI probes into a role the core can
drive. dwc3_gadget_init() only registers a UDC and does not care, and
`usb-mode device` re-aims the controller itself when it takes the port.

usb-mode's host path also learns to re-probe an xHCI that was bound in some
other role, which is what makes a newer rootfs recover on an older kernel.
2026-09-06 09:57:08 +03:00

192 lines
6.6 KiB
Bash
Executable File

#!/bin/sh
#
# Switch the single DWC3 controller between USB host and USB device roles.
#
# host - xHCI drives the port. This is what the usbcam second-camera path
# needs (majestic consuming a USB webcam).
# device - the UVC gadget drives the port, so a PC sees this camera as a
# webcam.
#
# The two are mutually exclusive: xHCI HCSPARAMS1 reports MaxPorts = 1, so
# there is one port and one role at a time. Switching does not need a reboot.
#
# Why this is more than one echo: dwc3_set_mode() (drivers/usb/dwc3/core.c)
# only flips GCTL.PRTCAPDIR. It does not stop the host stack or start the
# gadget stack. Writing the debugfs `mode` file on its own leaves xHCI still
# bound and its DMA engine still live, pointed at a controller that has been
# re-aimed underneath it. Unbind first, then flip, then bring the other side
# up.
set -e
STATE=/etc/usbmode
GADGET=/sys/kernel/config/usb_gadget/uvc0
die() { echo "usb-mode: $*" >&2; exit 1; }
dwc3_mode_file() {
for f in /sys/kernel/debug/*.dwc3/mode; do
[ -e "$f" ] && { echo "$f"; return 0; }
done
return 1
}
xhci_device() {
for d in /sys/bus/platform/drivers/xhci-hcd/*.auto; do
[ -e "$d" ] && { basename "$d"; return 0; }
done
# Not currently bound; fall back to the device that exists on the bus.
for d in /sys/bus/platform/devices/xhci-hcd.*.auto; do
[ -e "$d" ] && { basename "$d"; return 0; }
done
return 1
}
udc_name() {
for u in /sys/class/udc/*; do
[ -e "$u" ] && { basename "$u"; return 0; }
done
return 1
}
need_debugfs() {
mountpoint -q /sys/kernel/debug 2>/dev/null ||
mount -t debugfs none /sys/kernel/debug
MODE_FILE=$(dwc3_mode_file) ||
die "no dwc3 mode file; is CONFIG_USB_DWC3_DUAL_ROLE enabled and debugfs mounted?"
}
teardown_gadget() {
[ -d "$GADGET" ] || return 0
# Unbind from the UDC first: this drops the D+ pullup so the host sees a
# disconnect rather than a device that stops answering.
if [ -e "$GADGET/UDC" ] && [ -s "$GADGET/UDC" ]; then
echo "" > "$GADGET/UDC" 2>/dev/null || true
fi
# configfs rmdir is strict about ordering: links before the things they
# point at, leaves before their parents. ENOTEMPTY here means something
# below was missed.
rm -f "$GADGET/configs/c.1/uvc.0"
F="$GADGET/functions/uvc.0"
rm -f "$F/control/class/fs/h" "$F/control/class/ss/h"
rm -f "$F/streaming/class/fs/h" "$F/streaming/class/hs/h" "$F/streaming/class/ss/h"
# uvc-gadget-setup composes whichever single format majestic is set to, so
# tear down both possibilities: only one of them exists on any given boot.
rm -f "$F/streaming/header/h/f"
rmdir "$F/streaming/header/h" 2>/dev/null || true
rmdir "$F/streaming/mjpeg/m/f" 2>/dev/null || true
rmdir "$F/streaming/mjpeg/m" 2>/dev/null || true
rmdir "$F/streaming/framebased/h/f" 2>/dev/null || true
rmdir "$F/streaming/framebased/h" 2>/dev/null || true
rmdir "$F/control/header/h" 2>/dev/null || true
# EBUSY here means a userspace process still holds the gadget's
# /dev/videoN open -- almost always majestic. Stop it first.
if ! rmdir "$F" 2>/dev/null; then
die "cannot remove uvc.0 (EBUSY?) -- is majestic still holding /dev/video*? stop it first"
fi
rmdir "$GADGET/configs/c.1/strings/0x409" 2>/dev/null || true
rmdir "$GADGET/configs/c.1" 2>/dev/null || true
rmdir "$GADGET/strings/0x409" 2>/dev/null || true
rmdir "$GADGET" 2>/dev/null || true
}
to_device() {
need_debugfs
if X=$(xhci_device) && [ -e "/sys/bus/platform/drivers/xhci-hcd/$X" ]; then
# Unbind only from the host role. Asking xhci_halt() to halt a
# controller whose GCTL.PRTCAPDIR is not HOST means halting a host
# that was never really running, and it times out:
# xhci-hcd.0.auto: Host not halted after 16000 microseconds.
# xhci-hcd.0.auto: Host controller not halted, aborting reset.
# Leaving it un-halted is precisely the live-DMA-engine state this
# unbind exists to avoid. Since all-patches 0018 the core is already
# in the host role whenever xHCI is bound, so this flip is a no-op; it
# stays as the cheap guarantee that the unbind below halts cleanly.
[ "$(cat "$MODE_FILE")" = host ] || echo host > "$MODE_FILE"
echo "usb-mode: unbinding host controller $X"
echo "$X" > /sys/bus/platform/drivers/xhci-hcd/unbind
fi
echo device > "$MODE_FILE"
modprobe configfs 2>/dev/null || true
modprobe libcomposite 2>/dev/null || true
modprobe usb_f_uvc 2>/dev/null || true
mountpoint -q /sys/kernel/config 2>/dev/null ||
mount -t configfs none /sys/kernel/config
/usr/bin/uvc-gadget-setup
UDC=$(udc_name) ||
die "no UDC registered -- is dr_mode=\"otg\" in the device tree?"
echo "$UDC" > "$GADGET/UDC"
echo device > "$STATE"
echo "usb-mode: device (UVC gadget bound to $UDC)"
}
to_host() {
need_debugfs
teardown_gadget
# Remember which role we are leaving: it decides whether an already-bound
# host stack has to be re-probed. See below.
was=$(cat "$MODE_FILE")
echo host > "$MODE_FILE"
if X=$(xhci_device); then
if [ ! -e "/sys/bus/platform/drivers/xhci-hcd/$X" ]; then
echo "usb-mode: rebinding host controller $X"
echo "$X" > /sys/bus/platform/drivers/xhci-hcd/bind
elif [ "$was" != host ]; then
# Bound, but bound while the core was in another role: it
# probed, reset the controller and brought up its root hubs
# against a port it was not driving, and it will never report a
# connect -- the port powers up and stays at CCS = 0, so lsusb
# shows nothing but the root hubs however many devices are
# plugged in. Writing MODE_FILE only flips GCTL.PRTCAPDIR; it
# does not re-initialise a live xHCI. Making it probe again is
# what actually brings the port up.
#
# The kernel now settles the role before registering the host
# (all-patches 0018), so this cannot trigger on a matching
# kernel. It is here for a rootfs running on an older one.
echo "usb-mode: re-probing host controller $X (was $was)"
echo "$X" > /sys/bus/platform/drivers/xhci-hcd/unbind
echo "$X" > /sys/bus/platform/drivers/xhci-hcd/bind
fi
fi
echo host > "$STATE"
echo "usb-mode: host"
}
status() {
need_debugfs
echo "dwc3 role : $(cat "$MODE_FILE")"
echo "persisted : $([ -r "$STATE" ] && cat "$STATE" || echo '(unset, defaults to host)')"
echo "udc : $(udc_name || echo '(none)')"
echo "xhci bound: $(xhci_device >/dev/null 2>&1 && \
([ -e "/sys/bus/platform/drivers/xhci-hcd/$(xhci_device)" ] && echo yes || echo no) || echo '(no device)')"
echo "gadget : $([ -d "$GADGET" ] && echo "composed, UDC='$(cat "$GADGET/UDC" 2>/dev/null)'" || echo 'not composed')"
}
case "$1" in
device) to_device ;;
host) to_host ;;
status) status ;;
*)
echo "Usage: $0 {host|device|status}"
exit 1
;;
esac