USB Descriptor Debugging: What lsusb -v Doesn't Tell You
Wireshark's USB capture and usbmon reveal descriptor negotiation failures that lsusb hides. A systematic approach to USB enumeration failures on Linux.

lsusb -v shows the descriptor after successful enumeration. When enumeration fails — device resets at SET_ADDRESS, configuration never completes, composite interface conflicts — you need usbmon + Wireshark to see the negotiation Wire by Wire.
Systematic approach from debugging a custom STM32F042 USB CDC device that worked on Windows and failed on Linux 6.1 until we found a max packet size mismatch in the HS vs FS descriptor.
Setup
- Device: STM32F042, USB full-speed, CDC ACM + custom HID interface (composite)
- Host: Ubuntu 22.04, kernel 6.1.0-21,
usbcoredefault - Tools:
usbmonmodule, Wireshark 4.2,lsusb,dmesg,uhubctlfor power cycle
Enable capture:
sudo modprobe usbmon
# Wireshark: capture on usbmon0 (root hub) or usbmon1 (bus 1)
Filter: usb.device_address == N after first plug to reduce noise.
What lsusb hides
Failed configurations never appear
Device stuck in Default state after partial descriptor read — lsusb lists device as ID 0483:5740 (STMicro) with no interfaces or missing strings. -v dumps what kernel cached from incomplete read — misleading.
usbmon showed: host sent GET_DESCRIPTOR(Configuration), device NAK'd repeatedly, then reset. Root cause: configuration descriptor wTotalLength field wrong (claimed 64 bytes, actual 59). Windows tolerant; Linux strict.
Timing / ordering
lsusb -v is snapshot. Enumeration is sequence:
- GET_DESCRIPTOR device
- SET_ADDRESS
- GET_DESCRIPTOR configuration (sometimes twice)
- SET_CONFIGURATION
- Class-specific SET_LINE_CODING (CDC)
Wireshark timeline revealed we sent class request before SET_CONFIGURATION in firmware bug path — race on fast hosts only.
String descriptor language ID
Index 0 string descriptor must return LANGID 0x0409 (or list). We returned wrong bLength on string0 — macOS OK, Linux dmesg:
usb 1-1: string descriptor 0 read error: -71
-71 is -EPROTO. lsusb showed serial (error) but not which transaction failed.
Debugging workflow (ordered)
1. dmesg on plug
dmesg -w
Look for -71 (protocol), -32 (EPIPE), -110 (timeout). Note which step last succeeded.
2. usbmon capture single plug
Unplug, start capture, plug once, stop. Count resets — >3 resets before SET_CONFIGURATION usually descriptor or power.
3. Compare against USB Descriptor Tool output
STM32CubeUSB generates descriptors; hand-edits break wTotalLength and bDescriptorType chains often. Verify:
- Interface association descriptor order for composite
- Endpoint
wMaxPacketSizematches FS (64) not HS (512) on FS device bNumConfigurationsmatches reality
4. Wireshark decode limits
Wireshark decodes known classes well. Vendor-specific interfaces show as UNKNOWN — normal. Export hex, compare to spec manually.
For CDC: verify SET_LINE_CODING (bmRequestType 0x21) happens after configuration set.
5. lsusb -v after fix
Sanity check only after kernel logs clean attach.
Failure modes catalog (this project + others)
| Symptom | usbmon clue | Fix |
|---|---|---|
| Endless reset loops | SET_ADDRESS then immediate reset | Power, pull-up, descriptor length |
| Config fails Linux only | NAK on config descriptor | wTotalLength, padding |
| Works USB2 port not USB3 | Same | Often signal integrity — separate from descriptors |
| Composite conflicts | SET_INTERFACE fails | IAD ordering, interface numbers |
| Intermittent enumerate | Random NAK | ISR latency — USB IRQ priority on STM32 |
ISR issue showed in usbmon as late IN token response — not visible in lsusb at all. Cross-ref logic analyzer I2C glitches — same "analog/time domain vs static snapshot" lesson.
USB-C red herring
Dev board used USB-C receptacle with USB2.0 pins only. Confused teammates checking USB-C PD notes. Descriptor debug unchanged — PD negotiation is separate stack on different devices.
Host-side tools complement
# Device node after success
ls -la /dev/ttyACM*
# Permissions
sudo usermod -aG dialout $USER
# Unbind/rebind driver test
echo '1-1' | sudo tee /sys/bus/usb/drivers/usb/unbind
usbip for remote bench — capture still needs physical usbmon on host with device.
Windows vs Linux strictness (quick reference)
| Check | Windows 11 | Linux 6.1 |
|---|---|---|
| wTotalLength mismatch | Often tolerates | Strict fail |
| Extra descriptor padding | Ignored | Can fail |
| SET_CONFIGURATION before class | Flexible | Strict ordering |
| String index 0 LANGID | Lenient | -EPROTO on error |
Firmware tested on Windows only is not tested for half our CI targets.
CI descriptor validation
Planned hook using Linux usbutils/gadgetfs or umtprd — parse generated descriptor C array, compute wTotalLength independently, fail build on mismatch. Caught three CubeMX regen errors in POC — worth automating before next spin.
Common STM32 descriptor footguns
bcdUSB0x0200 vs 0x0110 — FS device claiming 2.0 is OK; claiming HS capabilities without HS PHY fails- Endpoint
bIntervalfor interrupt — 1 ms vs 10 ms affects polling driver load - Interface count mismatch in IAD —
bInterfaceCountmust match following interfaces
Keep a golden usbmon capture from known-good firmware in repo; diff against failing build visually in Wireshark.
What I'd do next
- CI hook: virtual usb gadget (
dummy_hcd+g_ether) parsing our descriptor blob — catch wTotalLength before hardware - Document Windows vs Linux strictness table for firmware team
- Keep Wireshark filter cheat sheet in repo next to OpenOCD scripts
lsusb -v is the autopsy report. usbmon is the security camera. Start with dmesg, capture once, fix descriptors before touching hardware again.
Manish Bookreader
Electronics enthusiast, Embedded Systems Expert, Linux/Networking programmer, and Software Engineer passionate about AI, electronics, books, and cooking.

