gk7205v200: add an OTG profile that makes the camera a USB webcam (#113)

Adds gk7205v200_otg_generic, a firmware profile that puts the single DWC3
controller into dual-role so the board can either consume a USB webcam
(existing usbcam path, host role) or present itself to a PC as one (UVC
gadget, device role). The roles are mutually exclusive -- xHCI HCSPARAMS1
reports MaxPorts = 1 -- but usb-mode switches between them at runtime, so
one image covers both and no reboot is needed.

Kernel side, three patches:

0015 sets dr_mode = "otg" in the DTS so dwc3 registers both an xHCI host
and a UDC.

0016 makes device mode actually work on a part that has only ever run as
a host. Three fixes: re-run dwc3_event_buffers_setup() on gadget start
(an xHCI reset wipes GEVNTADR), soft-reset the core before taking the
lock so the PHY is re-initialised (without it high-speed chirp fails),
and swallow the run_stop timeout on pullup(0), which otherwise
permanently poisons cdev->deactivations.

0017 disables the vendor's UVC_SG_REQ path. It hands a whole frame to the
UDC as one scatter-gather request; dwc3 chains those sg entries into a
single isochronous service interval, which on a full-speed link is one
1023-byte packet, so every frame larger than that reached the host
truncated and H.264 decoders reported "bytestream -5" on roughly half the
frames. Goke never shipped this path enabled -- CONFIG_USB_CONFIGFS_F_UVC
is unset in every vendor defconfig, including gk7605v100's, and the SDK
carries no userspace UVC sample -- so it was never exercised. The
mainline path (32 plain one-packet requests) is used instead.

Userspace side: usb-mode switches roles with the unbind/flip/bind
sequencing dwc3_set_mode() does not do on its own, uvc-gadget-setup
composes the configfs gadget, and S72usbmode forces a concrete role at
boot -- not optional, since dr_mode = "otg" leaves GCTL.PRTCAPDIR in a
state this 4.9 dwc3 never moves off by itself.

The descriptors advertise exactly the one format, frame size and interval
majestic is configured to serve, read from /etc/majestic.yaml. majestic
answers every PROBE/COMMIT with its own bFormatIndex/bFrameIndex because
it has one stream and cannot follow the host, so anything extra is an
invitation to select something that will not arrive: asking for MJPEG got
raw H.264 (an SPS where a JPEG SOI belongs, garbage to every consumer),
and asking for 1080p got 720p.

Profile drops the air-unit packages (wifibroadcast, rtl8812au, mavfwd,
mavlink-router, vtund, linux-firmware, wireless-tools) and the whole
wireless kernel stack, which pays for usb_f_uvc.ko and for keeping the
full sensor list -- the fpv trim to IMX307/IMX335 is keyed on
OPENIPC_VARIANT="fpv", and a general-purpose USB webcam should not be
sensor-locked.

Measured on a gk7205v200 lab board over a full-speed link: H.264
1280x720 at 30 fps with zero decoder errors and no dropped buffers, and
MJPEG delivering valid JPEGs at the jpeg channel's own rate.

Requires a majestic built with ENABLE_UVC_GADGET to feed the gadget;
that is a separate change.
This commit is contained in:
Dmitry Ilyin
2026-07-28 23:21:12 +03:00
committed by GitHub
parent 6c0a45e66d
commit 96633d5577
9 changed files with 3529 additions and 0 deletions
+3
View File
@@ -176,6 +176,9 @@ jobs:
- hi3516cv300_mini
- hi3518ev200_mini
# OTG (camera presents itself to a PC as a USB webcam)
- gk7205v200_otg_generic
steps:
- name: Checkout source
uses: actions/checkout@v4
@@ -0,0 +1,55 @@
# Architecture
BR2_arm=y
BR2_cortex_a7=y
BR2_ARM_EABI=y
BR2_ARM_FPU_NEON_VFPV4=y
BR2_ARM_INSTRUCTIONS_THUMB2=y
# Toolchain
BR2_TOOLCHAIN_EXTERNAL=y
BR2_TOOLCHAIN_EXTERNAL_DOWNLOAD=y
BR2_TOOLCHAIN_EXTERNAL_URL="https://github.com/openipc/firmware/releases/download/$(OPENIPC_TOOLCHAIN).tgz"
BR2_TOOLCHAIN_EXTERNAL_CUSTOM_PREFIX="arm-openipc-linux-musleabi"
BR2_TOOLCHAIN_EXTERNAL_HEADERS_4_9=y
BR2_TOOLCHAIN_EXTERNAL_CUSTOM_MUSL=y
BR2_TOOLCHAIN_EXTERNAL_CXX=y
# Kernel
BR2_LINUX_KERNEL=y
BR2_LINUX_KERNEL_CUSTOM_TARBALL=y
BR2_LINUX_KERNEL_CUSTOM_TARBALL_LOCATION="https://github.com/openipc/linux/archive/$(OPENIPC_KERNEL).tar.gz"
BR2_LINUX_KERNEL_USE_CUSTOM_CONFIG=y
BR2_LINUX_KERNEL_CUSTOM_CONFIG_FILE="$(EXTERNAL_VENDOR)/board/$(OPENIPC_SOC_FAMILY)/gk7205v200.generic-otg.config"
BR2_LINUX_KERNEL_UIMAGE=y
BR2_LINUX_KERNEL_XZ=y
# Filesystem
BR2_PACKAGE_BUSYBOX_CONFIG="$(BR2_EXTERNAL)/package/busybox/busybox.config"
BR2_PACKAGE_UBOOT_TOOLS=y
BR2_PACKAGE_LIBZIP=y
BR2_PACKAGE_JSON_C=y
# BR2_PACKAGE_IFUPDOWN_SCRIPTS is not set
BR2_TARGET_ROOTFS_CPIO=y
BR2_TARGET_ROOTFS_SQUASHFS=y
BR2_TARGET_ROOTFS_SQUASHFS4_XZ=y
# OpenIPC
BR2_OPENIPC_SOC_VENDOR="goke"
BR2_OPENIPC_SOC_MODEL="gk7205v200"
BR2_OPENIPC_SOC_FAMILY="gk7205v200"
BR2_OPENIPC_VARIANT="otg"
BR2_OPENIPC_FLASH_SIZE="8"
# Packages
BR2_PACKAGE_DROPBEAR_OPENIPC=y
BR2_PACKAGE_GOKE_OSDRV_GK7205V200=y
BR2_PACKAGE_HISILICON_OPENSDK=y
BR2_PACKAGE_IPCTOOL=y
BR2_PACKAGE_LIBCURL_OPENIPC=y
BR2_PACKAGE_LIBCURL_OPENIPC_CURL=y
# BR2_PACKAGE_LIBCURL_OPENIPC_PROXY_SUPPORT is not set
# BR2_PACKAGE_LIBCURL_OPENIPC_COOKIES_SUPPORT is not set
# BR2_PACKAGE_LIBCURL_OPENIPC_EXTRA_PROTOCOLS_FEATURES is not set
BR2_PACKAGE_MAJESTIC=y
BR2_PACKAGE_MBEDTLS_OPENIPC=y
BR2_PACKAGE_YAML_CLI=y
@@ -0,0 +1,33 @@
#!/bin/sh
#
# Force a concrete USB role at boot.
#
# This is NOT optional on this board. The device tree sets dr_mode = "otg" so
# that dwc3 registers both an xHCI host and a UDC, which is what makes runtime
# role switching possible. But dwc3_core_init() then leaves
# GCTL.PRTCAPDIR = OTG(3), and this 4.9 dwc3 has no working OTG role-detection
# block -- nothing ever moves it off OTG on its own. Left alone the controller
# sits in a role it cannot drive and USB works in neither direction.
#
# Runs after S70vendor so the vendor modules are up first.
case "$1" in
start)
mode=host
[ -r /etc/usbmode ] && read -r mode < /etc/usbmode
case "$mode" in
host|device) ;;
*) mode=host ;;
esac
echo "Setting USB role to $mode..."
/usr/bin/usb-mode "$mode" || echo "usb-mode $mode failed"
;;
stop)
;;
*)
echo "Usage: $0 {start|stop}"
exit 1
;;
esac
@@ -0,0 +1,159 @@
#!/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
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
echo host > "$MODE_FILE"
if X=$(xhci_device) && [ ! -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
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
@@ -0,0 +1,165 @@
#!/bin/sh
#
# Compose the UVC gadget in configfs, advertising exactly the one format and
# frame size majestic is configured to serve.
#
# Advertising a menu is worse than useless here. majestic answers every
# PROBE/COMMIT with its own bFormatIndex/bFrameIndex, because it has exactly one
# stream to give -- it cannot switch encoder geometry or codec to follow the
# host. So anything extra in the descriptors is a resolution or a codec the host
# is invited to select and will then not receive: ask for MJPEG and H.264
# arrives (an SPS where a JPEG SOI belongs, which is garbage to every consumer),
# ask for 1080p and 720p arrives (decoders recover from the SPS, anything
# trusting the negotiated V4L2 format does not).
#
# Re-run this after changing uvcgadget.format/size (usb-mode host && usb-mode
# device, or a reboot) or the descriptors will describe the previous config.
#
# Run after the gadget modules are loaded and configfs is mounted, and BEFORE
# anything writes the UDC name. `usb-mode device` does both for you.
#
# Notes that cost real debugging time, do not "fix" these:
#
# * The streaming group is "framebased", NOT "frame_based". The latter is the
# upstream >=5.15 spelling; this 4.9 vendor tree uses framebased. Using the
# wrong one fails silently -- mkdir just creates a junk directory that the
# gadget ignores, and the host sees no H.264 format.
# * control/class has only fs and ss. streaming/class has fs, hs and ss.
# Creating control/class/hs fails.
# * Attribute file names are camelCase (wWidth, dwFrameInterval, ...), even
# though the kernel's C identifiers are snake_case.
# * Do NOT try to set streaming_maxpacket / streaming_interval /
# streaming_maxburst. uvc_configfs.c's UVCG_OPTS_ATTR macro takes no `aname`
# parameter but still calls UVC_ATTR(..., aname), so all three land in
# configfs as files literally named "aname" and are unreachable. This is
# harmless: uvc_alloc_inst() already defaults them to 1 / 3072 / 15, and
# 3072 is the USB 2.0 high-speed maximum (3 transactions x 1024 B per
# microframe = ~24.5 MB/s isoc ceiling). There is nothing better to set.
# * The framebased format's GUID is hardcoded to H.264 by the kernel, so
# there is no guidFormat attribute to write for it.
set -e
G=/sys/kernel/config/usb_gadget/uvc0
YAML=/etc/majestic.yaml
# Read majestic's config, falling back to the same defaults uvcgadget_start()
# uses so a missing key or a missing `cli` still composes a working gadget.
cfg() {
_v=$(cli -g ".$1" 2>/dev/null) || _v=""
case "$_v" in ""|null|"~") echo "$2" ;; *) echo "$_v" ;; esac
}
FORMAT=$(cfg uvcgadget.format h264)
SIZE=$(cfg uvcgadget.size 1280x720)
WIDTH=${SIZE%%x*}
HEIGHT=${SIZE##*x}
case "$WIDTH$HEIGHT" in
*[!0-9]*|"") WIDTH=1280; HEIGHT=720 ;;
esac
# MJPEG frames come from the always-on JPEG channel, which runs at jpeg.fps and
# not at uvcgadget.fps. Advertise whichever actually feeds this format.
if [ "$FORMAT" = "mjpeg" ]; then
FPS=$(cfg jpeg.fps 5)
else
FPS=$(cfg uvcgadget.fps 30)
fi
[ "$FPS" -gt 0 ] 2>/dev/null || FPS=30
# Frame intervals are in 100 ns units: 10000000 / fps.
INTERVAL=$((10000000 / FPS))
if [ -d "$G" ]; then
echo "uvc-gadget-setup: $G already exists, nothing to do"
exit 0
fi
mkdir -p "$G"
# 0x1d6b/0x0102 is the Linux Foundation vendor ID with the "UVC gadget"
# product ID used by the in-tree g_webcam example. Fine for a device that is
# never going to ship a USB-IF certified VID/PID.
echo 0x1d6b > "$G/idVendor"
echo 0x0102 > "$G/idProduct"
mkdir -p "$G/strings/0x409"
echo "OpenIPC" > "$G/strings/0x409/manufacturer"
echo "gk7205v200 UVC" > "$G/strings/0x409/product"
[ -r /sys/class/net/eth0/address ] &&
tr -d ':' < /sys/class/net/eth0/address > "$G/strings/0x409/serialnumber" ||
echo "0000" > "$G/strings/0x409/serialnumber"
mkdir -p "$G/configs/c.1/strings/0x409"
echo "UVC" > "$G/configs/c.1/strings/0x409/configuration"
# Self-powered: the camera runs off its own PSU, it draws nothing from the host.
echo 0xc0 > "$G/configs/c.1/bmAttributes"
echo 0 > "$G/configs/c.1/MaxPower"
mkdir -p "$G/functions/uvc.0"
F="$G/functions/uvc.0"
# One interval, the one we can sustain. Hosts pick the fastest entry on offer,
# so a rate we cannot feed is not a harmless extra: the host commits to it,
# sizes its timing from it, and then reports the shortfall as missing frames.
#
# This MUST reach the kernel as a single write(): configfs invokes the
# attribute's store() once per write() syscall, and each store() replaces the
# whole list rather than appending. busybox `printf '%s\n%s\n' a b` emits one
# write() per directive, so it stores only the last value and then fails with
# EINVAL on the second write. An `echo` of one string is a single write().
# $1 format dir, $2 min bitrate, $3 max bitrate
add_frame() {
_d="$1/f"
mkdir -p "$_d"
echo "$WIDTH" > "$_d/wWidth"
echo "$HEIGHT" > "$_d/wHeight"
echo "$INTERVAL" > "$_d/dwDefaultFrameInterval"
echo "$INTERVAL" > "$_d/dwFrameInterval"
echo "$2" > "$_d/dwMinBitRate"
echo "$3" > "$_d/dwMaxBitRate"
}
# Bit rates are informational; scale them off the frame area so the numbers stay
# sane whatever size was configured.
PIXELS=$((WIDTH * HEIGHT))
if [ "$FORMAT" = "mjpeg" ]; then
mkdir -p "$F/streaming/mjpeg/m"
add_frame "$F/streaming/mjpeg/m" $((PIXELS * 13)) $((PIXELS * 33))
# dwMaxVideoFrameBufferSize is an upper bound the host sizes its buffer
# from; w*h*2 is a generous ceiling for JPEG. Undersizing truncates frames.
echo $((PIXELS * 2)) > "$F/streaming/mjpeg/m/f/dwMaxVideoFrameBufferSize"
STREAM_FORMAT="$F/streaming/mjpeg/m"
else
mkdir -p "$F/streaming/framebased/h"
add_frame "$F/streaming/framebased/h" $((PIXELS * 2)) $((PIXELS * 9))
# dwBytesPerLine is meaningless for a compressed format; 0 says "variable".
echo 0 > "$F/streaming/framebased/h/f/dwBytesPerLine"
STREAM_FORMAT="$F/streaming/framebased/h"
fi
# --- headers and class links -------------------------------------------------
# Formats link INTO the streaming header; the header links INTO each speed
# class.
#
# Targets must be ABSOLUTE. configfs_symlink() resolves the target with
# kern_path(), which resolves a relative path against the calling process's
# CWD -- not against the directory the link lives in. The upstream
# gadget-testing.txt recipe gets away with relative targets only because it
# cd's into each directory first. Absolute paths do not care where we are.
mkdir -p "$F/streaming/header/h"
ln -s "$STREAM_FORMAT" "$F/streaming/header/h/f"
ln -s "$F/streaming/header/h" "$F/streaming/class/fs/h"
ln -s "$F/streaming/header/h" "$F/streaming/class/hs/h"
ln -s "$F/streaming/header/h" "$F/streaming/class/ss/h"
mkdir -p "$F/control/header/h"
ln -s "$F/control/header/h" "$F/control/class/fs/h"
ln -s "$F/control/header/h" "$F/control/class/ss/h"
ln -s "$F" "$G/configs/c.1/uvc.0"
echo "uvc-gadget-setup: composed $G ($FORMAT ${WIDTH}x${HEIGHT}@${FPS})"
@@ -0,0 +1,40 @@
From: OpenIPC
Subject: [PATCH] gk7205v200: run the dwc3 controller in dual-role (otg) mode
The gk7205v200 has a single DWC3 USB controller wired as a host port. This
image needs it to work both ways: as a host (the usbcam second-camera path,
consuming a USB webcam) and as a device (the UVC gadget path, presenting the
camera to a PC as a webcam). They are never active at the same time -- xHCI
HCSPARAMS1 reports MaxPorts = 1 -- but switching should not require a reboot.
With dr_mode = "host", dwc3_core_init_mode() only calls dwc3_host_init(), so
no UDC is ever registered and the gadget side is unreachable. With
dr_mode = "otg" it calls both dwc3_host_init() and dwc3_gadget_init(), so an
xHCI controller and a UDC coexist and userspace can pick a role at runtime via
/sys/kernel/debug/<dwc3>/mode.
The controller already carries Goke's peripheral-side endpoint remap in this
same node (eps_directions, eps_map, snps,eps_new_init), which the vendor would
only have added if device mode was exercised; and CONFIG_USB_DWC3_DUAL_ROLE is
already enabled, so dwc3's gadget.o/ep0.o are already linked in.
IMPORTANT: dr_mode = "otg" also makes dwc3_core_init() leave
GCTL.PRTCAPDIR = OTG(3), and this 4.9 dwc3 has no working OTG role-detection
block. A concrete role must be forced early in userspace -- see
/etc/init.d/S72usbmode, which writes "host" at boot. Without it the controller
sits in a role it cannot drive and USB does not work in either direction.
This patch ships only in the gk7205v200_otg_generic device profile, so other
gk7205v200 variants keep dr_mode = "host".
--- a/arch/arm/boot/dts/gk7205v200.dtsi
+++ b/arch/arm/boot/dts/gk7205v200.dtsi
@@ -395,7 +395,7 @@
phys = <&usb2_phy0>;
phy-names = "usb2-phy";
maximum-speed = "high-speed";
- dr_mode = "host";
+ dr_mode = "otg";
eps_directions = <0x6a>;
snps,eps_new_init;
eps_map=<0x0 0x1 0x2 0x3 0x4 0x5 0x7>;
@@ -0,0 +1,210 @@
From: OpenIPC
Subject: [PATCH] usb: dwc3: make device mode work on a host-first dual-role part
On gk7205v200 the DTS boots in host mode and the role is switched at
runtime, so the gadget always starts from a controller that xHCI has
already owned and reset. Three things were missing for that to work; all
three are needed, and all three are things upstream's later
__dwc3_set_mode() rework does for the DWC3_GCTL_PRTCAP_DEVICE case.
1. The event buffer was never re-armed. xHCI's controller reset clears
GEVNTADRLO/HI and nothing programmed them again, so the core DMA'd its
event descriptors to physical address 0 -- not RAM on this SoC, DRAM
starts at 0x40000000. No event ever reached the driver: the link
chirped and counted SOFs in hardware and the host saw a device, but the
gadget never observed the USB reset.
2. The device side was never reset before being started, so the USB2 PHY
was left in whatever state xHCI put it in. The high-speed chirp then
failed, DSTS.CONNECTSPD came back full-speed, CONNECT_DONE was never
raised and the host timed out with -71.
3. Re-attach never worked, for two separate reasons -- see the comments in
dwc3_gadget_pullup().
With all three, enumeration is reliable across repeated attach/detach
cycles.
--- a/drivers/usb/dwc3/core.c
+++ b/drivers/usb/dwc3/core.c
@@ -144,7 +144,7 @@
* dwc3_core_soft_reset - Issues core soft reset and PHY reset
* @dwc: pointer to our context structure
*/
-static int dwc3_core_soft_reset(struct dwc3 *dwc)
+int dwc3_core_soft_reset(struct dwc3 *dwc)
{
u32 reg;
int retries = 1000;
@@ -316,7 +316,7 @@
*
* Returns 0 on success otherwise negative errno.
*/
-static int dwc3_event_buffers_setup(struct dwc3 *dwc)
+int dwc3_event_buffers_setup(struct dwc3 *dwc)
{
struct dwc3_event_buffer *evt;
--- a/drivers/usb/dwc3/core.h
+++ b/drivers/usb/dwc3/core.h
@@ -1155,6 +1155,8 @@
/* prototypes */
void dwc3_set_mode(struct dwc3 *dwc, u32 mode);
+int dwc3_event_buffers_setup(struct dwc3 *dwc);
+int dwc3_core_soft_reset(struct dwc3 *dwc);
u32 dwc3_core_fifo_space(struct dwc3_ep *dep, u8 type);
/* check whether we are on the DWC_usb31 core */
--- a/drivers/usb/dwc3/gadget.c
+++ b/drivers/usb/dwc3/gadget.c
@@ -1563,6 +1563,9 @@
return 0;
}
+static int __dwc3_gadget_start(struct dwc3 *dwc);
+static void __dwc3_gadget_stop(struct dwc3 *dwc);
+
static int dwc3_gadget_pullup(struct usb_gadget *g, int is_on)
{
struct dwc3 *dwc = gadget_to_dwc(g);
@@ -1571,8 +1574,80 @@
is_on = !!is_on;
+ if (!is_on) {
+ spin_lock_irqsave(&dwc->lock, flags);
+ ret = dwc3_gadget_run_stop(dwc, false, false);
+ spin_unlock_irqrestore(&dwc->lock, flags);
+ /*
+ * Never report a failure on the way down.
+ *
+ * dwc3_gadget_run_stop(0) polls DSTS.DEVCTRLHLT and returns
+ * -ETIMEDOUT if the core does not halt in time, which this one
+ * regularly does not. That error is not harmless: it propagates
+ * out through usb_gadget_disconnect() into
+ * usb_gadget_deactivate(), which then skips "gadget->deactivated
+ * = true", and into usb_function_deactivate(), which then skips
+ * "cdev->deactivations++". The refcount is left permanently
+ * unbalanced, so every later usb_function_activate() trips
+ * WARN_ON(cdev->deactivations == 0) and never calls
+ * usb_gadget_connect() -- the gadget can be disconnected once
+ * and then never comes back.
+ *
+ * RUN_STOP has been cleared regardless, so the pullup really is
+ * down; only the halt handshake was slow. Whatever state that
+ * leaves the core in does not matter, because the is_on path
+ * below soft-resets and re-initialises it from scratch.
+ */
+ if (ret) {
+ dev_dbg(dwc->dev,
+ "pullup(0): run_stop returned %d, ignoring so the deactivation refcount stays balanced\n",
+ ret);
+ ret = 0;
+ }
+ return ret;
+ }
+
+ /*
+ * Bringing the pullup back up after a disconnect needs a full device-side
+ * re-init, not just RUN_STOP.
+ *
+ * A gadget that has been disconnected once (f_uvc does this every time
+ * userspace closes /dev/videoN, via usb_function_deactivate()) leaves the
+ * core half torn down: the USB2 PHY keeps whatever state the disconnect
+ * left it in and the event buffer registers are stale. Re-asserting
+ * RUN_STOP on its own gets the host as far as SET_ADDRESS -- the device
+ * reaches "addressed" -- and then control transfers fail, so enumeration
+ * never completes. Only the first connect after dwc3_gadget_start() ever
+ * worked, because that path does the reset for us.
+ *
+ * So do here what dwc3_gadget_start() does: quiesce, soft reset (which
+ * re-runs usb_phy_init()/phy_init()), then start the device side again.
+ * dwc3_event_buffers_setup() is called from __dwc3_gadget_start().
+ *
+ * Callable only from process context: dwc3_core_soft_reset() sleeps, so it
+ * must run outside dwc->lock. That holds for every caller we have --
+ * usb_gadget_connect() reaches us from usb_gadget_activate() (userspace
+ * opening the video node) and from udc_bind_to_driver(). A platform that
+ * called usb_udc_vbus_handler() from hard IRQ would need reworking here.
+ */
spin_lock_irqsave(&dwc->lock, flags);
- ret = dwc3_gadget_run_stop(dwc, is_on, false);
+ __dwc3_gadget_stop(dwc);
+ spin_unlock_irqrestore(&dwc->lock, flags);
+
+ ret = dwc3_core_soft_reset(dwc);
+ if (ret) {
+ dev_err(dwc->dev, "pullup: soft reset failed: %d\n", ret);
+ return ret;
+ }
+
+ spin_lock_irqsave(&dwc->lock, flags);
+ ret = __dwc3_gadget_start(dwc);
+ if (ret) {
+ spin_unlock_irqrestore(&dwc->lock, flags);
+ dev_err(dwc->dev, "pullup: gadget start failed: %d\n", ret);
+ return ret;
+ }
+ ret = dwc3_gadget_run_stop(dwc, true, false);
spin_unlock_irqrestore(&dwc->lock, flags);
return ret;
@@ -1652,6 +1727,27 @@
int ret = 0;
u32 reg;
+ /*
+ * Re-program the event buffer address before starting the device side.
+ *
+ * dwc3_core_init() sets GEVNTADRLO/HI up once at probe, but on a
+ * dual-role part the host is brought up straight afterwards and xHCI's
+ * controller reset clears them again. Nothing else re-arms them, so on
+ * any host->device transition (and on this SoC, that includes the very
+ * first one, because the DTS boots in host mode) the core is left with
+ * an event buffer address of 0. It then DMAs its event descriptors to
+ * physical address 0 -- which is not RAM here, DRAM starts at
+ * 0x40000000 -- so no event ever reaches the driver: the link chirps and
+ * counts SOFs in hardware, the host sees a high-speed device, but the
+ * gadget never observes the USB reset and enumeration dies with -71.
+ *
+ * Upstream fixed this by calling dwc3_event_buffers_setup() from
+ * __dwc3_set_mode() when switching to DWC3_GCTL_PRTCAP_DEVICE; this
+ * tree predates that rework, so re-arm here instead. This covers both
+ * the gadget-bind and the resume path, and is idempotent.
+ */
+ dwc3_event_buffers_setup(dwc);
+
reg = dwc3_readl(dwc->regs, DWC3_DCFG);
reg &= ~(DWC3_DCFG_SPEED_MASK);
@@ -1751,6 +1847,29 @@
int ret = 0;
int irq;
+ /*
+ * Re-initialise the controller and its PHYs before handing the port to
+ * the device side.
+ *
+ * On a dual-role part the host is started first, so by the time a gadget
+ * driver binds, the USB2 PHY is still in the state xHCI left it in and
+ * the device side has never been reset. Symptom: the link comes up but
+ * loses the high-speed chirp handshake, so DSTS.CONNECTSPD reads
+ * full-speed, DWC3_DEVICE_EVENT_CONNECT_DONE is never raised, and the
+ * host -- which is driving high-speed -- times out with -71.
+ *
+ * dwc3_core_soft_reset() re-runs usb_phy_init()/phy_init() and resets
+ * the device side, which is exactly what upstream's __dwc3_set_mode()
+ * does for the DWC3_GCTL_PRTCAP_DEVICE case. This tree predates that
+ * rework. Must be called outside dwc->lock: it sleeps (udelay loops and
+ * PHY init).
+ */
+ ret = dwc3_core_soft_reset(dwc);
+ if (ret) {
+ dev_err(dwc->dev, "soft reset before gadget start failed: %d\n", ret);
+ return ret;
+ }
+
irq = dwc->irq_gadget;
ret = request_threaded_irq(irq, dwc3_interrupt, dwc3_thread_interrupt,
IRQF_SHARED, "dwc3", dwc->ev_buf);
@@ -0,0 +1,53 @@
uvc gadget: drop the vendor scatter-gather request path
Goke's f_uvc carries a private UVC_SG_REQ mode that replaces the mainline
request ring with a *single* in-flight usb_request (UVC_NUM_REQUESTS 1)
whose scatterlist is sized to hold an entire video frame:
num_sgs = (video->imagesize / (req_size - 2)) + 1;
uvc_video_encode_isoc() then fills every sg entry of that one request and
hands the whole chain to the UDC in one go.
dwc3 turns each mapped sg entry into a TRB chained to the next, so the
chain describes one isochronous service interval. On a full-speed link an
isoc interval carries exactly one 1023-byte packet, so only the first sg
entry ever reaches the wire; the rest of the frame is dropped. Because the
encoder had already consumed the buffer into the sg list, the buffer is
marked done and the FID toggles, so the host sees a *complete* frame that
happens to be 1021 bytes long (1023 minus the 2-byte UVC payload header).
The visible symptom is that every frame larger than one packet arrives
truncated. H.264 decoders report a stream of
error while decoding MB x y, bytestream -5
and roughly half the frames fail to decode, while frames that happen to
fit in a single packet come through intact.
Fall back to the mainline path: 32 plain one-packet requests kept in
flight by uvcg_video_pump(). Measured on gk7205v200 at full speed, this
takes a 1280x720 H.264 stream from ~50% corrupt frames to 120/120 frames
delivered with zero decoder errors and no gaps in the buffer sequence.
The SG code is left in place behind the now-undefined macro rather than
deleted, so it stays available for anyone who wants to revisit it on a
high-speed link where an interval can hold mult*maxpacket bytes.
diff --git a/drivers/usb/gadget/function/uvc.h b/drivers/usb/gadget/function/uvc.h
--- a/drivers/usb/gadget/function/uvc.h
+++ b/drivers/usb/gadget/function/uvc.h
@@ -17,7 +17,12 @@
#include <linux/types.h>
#include <linux/usb/ch9.h>
-#define UVC_SG_REQ
+/* UVC_SG_REQ: the vendor scatter-gather path uses a single in-flight request
+ * carrying a whole frame as a chained TRB list. On this full-speed link the
+ * host only ever receives the first 1023-byte packet of each frame, so every
+ * frame larger than one packet arrives truncated (H.264 "bytestream -5").
+ * Fall back to the mainline path: 32 plain one-packet requests. */
+#undef UVC_SG_REQ
#define UVC_EVENT_FIRST (V4L2_EVENT_PRIVATE_START + 0)
#define UVC_EVENT_CONNECT (V4L2_EVENT_PRIVATE_START + 0)
#define UVC_EVENT_DISCONNECT (V4L2_EVENT_PRIVATE_START + 1)