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.
192 lines
6.6 KiB
Bash
Executable File
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
|