Series Context: This is Part 10 of the 17-part USB Development Mastery series. Parts 1–9 covered fundamentals, electrical layer, protocol and enumeration, device classes, TinyUSB, CDC, HID, mass storage, and composite devices. You should be comfortable writing working USB firmware before reading this part — debugging requires a device that partially works.
1
USB Fundamentals
USB system architecture, transfer types, host/device model, protocol stack
2
Electrical & Hardware
D+/D- signalling, pull-ups, connectors, USB-C, STM32 USB peripherals
3
Protocol & Enumeration
Enumeration sequence, USB packets, descriptors, endpoint concepts
4
Device Classes
HID, CDC, MSC, MIDI, Audio, composite devices, vendor class
5
TinyUSB Deep Dive
Stack architecture, execution model, STM32 integration, descriptor callbacks
6
CDC Virtual COM Port
CDC class, bulk transfers, printf over USB, baud rate handling
7
HID Devices
HID descriptors, report format, keyboard/mouse/gamepad implementation
8
Mass Storage
MSC class, SCSI commands, FATFS integration, RAM disk
9
Composite Devices
Multiple classes, IAD descriptor, CDC+HID, CDC+MSC
10
USB Debugging
Wireshark capture, protocol analyser, enumeration debugging, common failures
You Are Here
11
RTOS + USB
FreeRTOS + TinyUSB, task priorities, thread-safe communication
12
Advanced Topics
Host mode, OTG, isochronous, USB audio, USB video
13
Performance Optimization
DMA, zero-copy buffers, throughput maximisation, latency tuning
14
Custom Class Drivers
Vendor class, writing descriptors, OS driver interaction
15
Bare-Metal USB
Direct register programming, writing USB stack from scratch, PHY timing
16
USB Security
BadUSB attacks, device authentication, secure firmware, USB firewall
17
Hardware Design
PCB layout, differential pairs, impedance matching, EMI, USB-C PD
USB Debugging Tools Overview
USB debugging is hard because the interface you are debugging is the same interface you would normally use to output debug information. A UART printf tells you exactly what is happening — a USB firmware bug may simply cause silent enumeration failure with no indication of what went wrong. The solution is a dedicated debug channel (UART/SWO) combined with a USB traffic capture tool that can see the full bus transaction sequence.
USB debugging tools fall into two broad categories: software capture tools that intercept USB traffic on the host side, and hardware analyzers that sit between the host and device on the physical bus. Each has distinct advantages and use cases.
Software Capture Tools
Windows: USBPcap + Wireshark — USBPcap is a kernel-mode USB capture driver that hooks into the Windows USB stack and exports packet data to Wireshark via a named pipe. It captures at the URB (USB Request Block) level, which means you see the logical transfer operations as the OS sees them, not raw wire packets. Installation is straightforward: install USBPcap from usbpcap.com, restart Windows, then select the USBPcap interface in Wireshark. No special hardware required.
Linux: usbmon + Wireshark — Linux includes the usbmon kernel module which exposes USB traffic via /sys/kernel/debug/usb/usbmon/. Load it with modprobe usbmon, then open Wireshark and select the usbmon0 (all buses) or usbmon1, usbmon2 etc. (bus-specific) interface. No additional driver installation needed. Root permissions or the wireshark group membership is required.
macOS: Xcode USB Prober / PacketLogger — Apple provides USB diagnostic tools bundled with Additional Tools for Xcode. USB Prober shows the device descriptor tree and can trigger re-enumeration. PacketLogger (historically for Bluetooth but USB-capable) captures low-level USB events. The macOS USB stack is notably stricter about descriptor correctness than Windows — a device that enumerates on Windows may fail on macOS due to subtle descriptor issues.
Hardware Protocol Analyzers
Software capture tools have a fundamental limitation: they only see what the OS USB stack sees. If your device fails to enumerate, the OS never loads a capture driver for that device, so you see nothing useful. Hardware analyzers sit on the physical bus between host and device, capturing every packet at the signal level regardless of whether enumeration succeeds.
| Tool |
Type |
Max Speed |
Approx. Cost |
Best For |
| Total Phase Beagle 480 |
Hardware analyzer |
High Speed (480 Mbit/s) |
$1,400–$1,800 |
Professional HS debugging, compliance, triggers |
| OpenVizsla |
Open-source hardware |
Full Speed (12 Mbit/s) |
$80–$150 (DIY) |
Budget FS debugging, open firmware |
| Cynthion (formerly LUNA) |
FPGA-based hardware |
High Speed (480 Mbit/s) |
$150–$250 |
HS capture, also USB host/device emulation |
| Saleae Logic Pro 8 |
Logic analyzer + USB decoder |
Full Speed only |
$500–$600 |
Combined USB + UART/SPI debugging in one tool |
| USBPcap + Wireshark |
Software (Windows) |
SuperSpeed (sees all) |
Free |
Post-enumeration debugging, filter/decode |
| usbmon + Wireshark |
Software (Linux) |
SuperSpeed (sees all) |
Free |
Linux-side capture, automation-friendly |
Rule of Thumb: Start with Wireshark + USBPcap/usbmon — it's free and handles the majority of post-enumeration debugging. When the device fails to enumerate at all, upgrade to a hardware analyzer. When signal integrity is suspect (HS eye diagram failures, intermittent disconnects under EM interference), the Total Phase Beagle or Cynthion is required.
Capturing USB Traffic with Wireshark
Windows: Installing and Configuring USBPcap
Download USBPcap from https://desowin.org/usbpcap/ and run the installer. During installation, USBPcap registers as a kernel filter driver on each USB root hub. After the required restart, open Wireshark. You will see new interfaces labelled USBPcap1, USBPcap2, etc., corresponding to your system's USB host controllers. Select the one your device is plugged into (trial and error if you have multiple controllers), then click the start capture button before plugging in your device.
Linux: Using usbmon
# Load the usbmon kernel module
sudo modprobe usbmon
# Check which bus your device is on
lsusb -t
# Start Wireshark as root (or add yourself to wireshark group)
sudo wireshark
# In Wireshark, select usbmon0 (all buses) or usbmon1, usbmon2...
# usbmon1 = USB bus 1, usbmon2 = USB bus 2, etc.
Essential Wireshark Filter Expressions
Raw USB captures are extremely noisy — every mouse movement, keyboard scan, and background device poll appears. These filters let you focus on what matters:
| Filter Expression |
What It Shows |
usb.addr == "1.3.0" |
All traffic to/from USB bus 1, device 3, endpoint 0 |
usb.idVendor == 0x0483 |
All traffic from ST Microelectronics devices (VID 0x0483) |
usb.transfer_type == 0x02 |
Control transfers only (0x00=isochronous, 0x01=interrupt, 0x02=control, 0x03=bulk) |
usb.transfer_type == 0x03 |
Bulk transfers only (CDC data, MSC SCSI) |
usb.transfer_type == 0x01 |
Interrupt transfers only (HID reports) |
usb.setup_flag == 0 |
SETUP packets only (enumeration control requests) |
usb.urb_status != 0 |
Failed/error URBs only — start here when something is wrong |
Anatomy of a Captured USB Transaction
When you click on a packet in Wireshark, the packet detail pane shows the URB (USB Request Block) structure. Here is an annotated ASCII representation of what a GET_DESCRIPTOR SETUP packet looks like in the capture:
Frame 42: USB URB — GET_DESCRIPTOR Request
─────────────────────────────────────────────────────────
USB URB Header:
URB id: 0x00000000FFFF0001
URB type: URB_SUBMIT (host sending to device)
Transfer type: Control (0x02)
Endpoint: 0x00 (EP0 OUT, host-to-device)
Device address: 3
Bus id: 1
─────────────────────────────────────────────────────────
USB Control Setup:
bmRequestType: 0x80 (device-to-host, standard, device)
bRequest: 0x06 (GET_DESCRIPTOR)
wValue: 0x0100 (Descriptor type=0x01 DEVICE, index=0x00)
wIndex: 0x0000 (Language ID or interface number)
wLength: 0x0012 (18 bytes requested — device descriptor size)
─────────────────────────────────────────────────────────
Frame 43: USB URB — GET_DESCRIPTOR Response
Data (18 bytes):
12 01 10 02 EF 02 01 40 83 04 11 57 00 02 01 02 03 01
││ ││ ╰──────╯ ╰──╯ ╰──╯ ╰──╯ ╰──╯ ╰──────╯ ╰──╯ ││ ╰─ bNumConfigurations=1
││ ││ bcdUSB │ │ │ │ idProduct ││
││ ││ =0x0210 │ │ EP0 │ =0x5711 ││
││ ││ (USB 2.1) bDev │ MaxPkt │ ╰─ iSerialNumber=1
││ ╰─ bDescriptorType=1 │ =64B idVendor=0x0483
╰─ bLength=18 bDeviceProtocol=1
Key Frame: The very first GET_DESCRIPTOR for the device descriptor uses wLength=8 (not 18) — the host asks for just enough to learn bMaxPacketSize0, then does a full GET_DESCRIPTOR for all 18 bytes. If your first response is wrong, enumeration fails here. Always look at the first GET_DESCRIPTOR response in your capture.
Reading USB Packet Captures
Decoding a GetDescriptor Request Frame by Frame
A complete USB enumeration sequence in Wireshark follows a predictable pattern. Here is what you should see and what each packet means:
| # |
bRequest |
wValue |
wLength |
Meaning |
| 1 |
GET_DESCRIPTOR (0x06) |
0x0100 (Device) |
8 |
Host asks for first 8 bytes to learn bMaxPacketSize0 |
| 2 |
SET_ADDRESS (0x05) |
0x0003 (address=3) |
0 |
Host assigns device address 3; device acks then switches |
| 3 |
GET_DESCRIPTOR (0x06) |
0x0100 (Device) |
18 |
Full device descriptor (18 bytes) at new address |
| 4 |
GET_DESCRIPTOR (0x06) |
0x0200 (Configuration) |
9 |
First 9 bytes of config descriptor to learn wTotalLength |
| 5 |
GET_DESCRIPTOR (0x06) |
0x0200 (Configuration) |
wTotalLength |
Full config descriptor tree including all interfaces/endpoints |
| 6 |
GET_DESCRIPTOR (0x06) |
0x0300 (String) |
4 |
Language ID list (usually 0x0409 = English US) |
| 7–N |
GET_DESCRIPTOR (0x06) |
0x03xx (String index) |
varies |
String descriptors for manufacturer, product, serial number |
| N+1 |
SET_CONFIGURATION (0x09) |
0x0001 |
0 |
Host activates configuration 1 — endpoints become active |
Filtering CDC Bulk Transfers
Once your CDC device has enumerated, use this filter to see only the bulk data flowing on the CDC data interface. Replace the address with your device's actual address from the capture:
# Show only bulk transfers to/from device at address 3
usb.transfer_type == 0x03 && usb.device_address == 3
# Show CDC data endpoint traffic (typically EP1 IN and EP1 OUT)
usb.transfer_type == 0x03 && (usb.endpoint_address == 0x81 || usb.endpoint_address == 0x01)
# Show CDC management notifications (interrupt EP, typically EP2 IN)
usb.transfer_type == 0x01 && usb.endpoint_address == 0x82
Filtering HID Reports
# HID reports are interrupt transfers
# Show all HID IN reports (device sending to host)
usb.transfer_type == 0x01 && usb.endpoint_address.direction == 1
# Inspect the raw HID report bytes
# Right-click a HID IN packet → Follow → USB Stream
Exporting to CSV for Batch Analysis
For timing analysis or regression testing, export your capture to CSV: File → Export Packet Dissections → As CSV. The resulting CSV has columns for frame number, time, source, destination, protocol, length, and info. You can then use Python's pandas to analyse transfer timing, packet sizes, and error rates across a large capture.
Hardware Protocol Analyzers
Total Phase Beagle 480
The Total Phase Beagle 480 is the industry-standard hardware USB analyzer for Full Speed and High Speed. It is a pass-through device: plug the USB cable from the host into the Beagle's upstream port, and the downstream port connects to your device. The Beagle captures every packet on the physical bus — SOF tokens, SETUP/DATA/ACK handshakes, NAKs, STALLs — at the wire level.
Key capabilities of the Beagle 480:
- Capture rate: Full Speed (12 Mbit/s) and High Speed (480 Mbit/s) — all speeds simultaneously if the device negotiates HS
- Timestamp resolution: 16.67 nanoseconds — sufficient to measure individual bit times
- Trigger system: Hardware triggers on specific PID (SETUP, DATA0, DATA1, SOF, NAK, STALL), on specific device address, on specific data pattern in the packet payload, or on a specific endpoint address
- Buffer depth: 4 GB on-board capture memory — captures minutes of HS traffic
- Eye diagram: The Beagle 480 with the Signal Integrity option shows HS eye diagrams directly, enabling compliance-level signal integrity analysis
When Hardware Analysis Is Necessary
Software capture tools (USBPcap, usbmon) only see packets that the OS USB stack successfully received and decoded. The following situations require a hardware analyzer:
- Device fails to enumerate: The OS shows nothing in Device Manager. A hardware analyzer will show whether the device is sending any response to the initial GET_DESCRIPTOR, and if so, what is wrong with the response.
- HS device falls back to Full Speed: The HS handshake (Chirp sequence) happens before the OS USB stack is engaged. Only a hardware analyzer can capture the Chirp K/J sequences and determine whether the device's HS PHY is responding correctly.
- Intermittent disconnects under load: The signal integrity may degrade at high data rates due to cable quality or impedance mismatch. The hardware analyzer's eye diagram shows exactly when signal margins collapse.
- Timing compliance testing: USB compliance requires responses within specific time windows (e.g., device must respond to host tokens within 6.5 bit times at HS). Only hardware analysis with nanosecond timestamps can verify this.
Comparing Hardware vs Software Capture
| Capability |
USBPcap / usbmon |
Hardware Analyzer |
| Pre-enumeration capture |
No |
Yes |
| Signal-level timing |
No |
Yes (ns resolution) |
| Eye diagram / signal integrity |
No |
Yes (Beagle 480 + SI option) |
| See NAK/STALL/SOF tokens |
Partial (URB level) |
Yes (wire level) |
| Trigger on PID or data pattern |
No |
Yes |
| Cost |
Free |
$80–$1,800 |
| Required for enumeration failure |
No |
Often yes |
STM32 USB Debug via UART
When you cannot use a hardware analyzer, the next best option is to redirect TinyUSB's internal debug log output to a UART. TinyUSB has a built-in debug logging system controlled by CFG_TUSB_DEBUG in tusb_config.h. At level 2, it prints every USB transaction including descriptor contents, endpoint activity, and class-specific events.
Enabling TinyUSB Debug Logging
// In tusb_config.h — set debug level
// Level 0: No debug output
// Level 1: Errors only
// Level 2: All USB transactions (use during development)
// Level 3: Very verbose including internal state (slow, for TinyUSB development)
#define CFG_TUSB_DEBUG 2
Redirecting tu_printf to HAL_UART_Transmit
TinyUSB's debug system calls tu_printf() which by default calls the standard C printf(). If your target has no semihosting and printf goes nowhere, you need to override this. The cleanest approach is to define tu_printf as a macro that calls your UART transmit function:
// In tusb_config.h — override tu_printf to use UART
// First, declare your UART debug function
// (define this in a .c file and declare extern here)
#ifdef __cplusplus
extern "C" {
#endif
int usb_debug_printf(const char *fmt, ...);
#ifdef __cplusplus
}
#endif
#define tu_printf usb_debug_printf
// In usb_debug.c — the actual UART printf implementation
#include "usbd_conf.h"
#include "stm32f4xx_hal.h"
#include <stdarg.h>
#include <stdio.h>
extern UART_HandleTypeDef huart2; // Your debug UART handle
int usb_debug_printf(const char *fmt, ...)
{
char buf[256];
va_list args;
va_start(args, fmt);
int len = vsnprintf(buf, sizeof(buf), fmt, args);
va_end(args);
if (len > 0) {
// Use DMA or blocking depending on your setup
// Blocking is fine for debug — it won't affect USB if
// called outside of USB interrupt context
HAL_UART_Transmit(&huart2, (uint8_t*)buf, (uint16_t)len, 10);
}
return len;
}
Using ITM/SWO for USB Debug Messages
For Cortex-M3/M4/M7 targets with a SWD debug probe (ST-Link V2, J-Link), the ITM (Instrumentation Trace Macrocell) over the SWO pin is a zero-overhead alternative to UART. Configure ITM stimulus port 0 and redirect tu_printf to use ITM_SendChar():
// ITM-based debug output for Cortex-M targets with SWO
// CoreDebug and ITM registers are part of CMSIS — no extra headers needed
static inline void itm_send_string(const char *str)
{
while (*str) {
// Wait for ITM stimulus port 0 to be ready
while (ITM->PORT[0].u32 == 0) { __NOP(); }
ITM->PORT[0].u8 = (uint8_t)(*str++);
}
}
int usb_debug_printf(const char *fmt, ...)
{
char buf[256];
va_list args;
va_start(args, fmt);
int len = vsnprintf(buf, sizeof(buf), fmt, args);
va_end(args);
itm_send_string(buf);
return len;
}
Warning: Never call tu_printf or any UART transmit function from within a USB ISR context. TinyUSB debug output is generated from the USB task (or main loop), not from interrupt handlers. Blocking UART transmit inside an ISR will cause USB timing violations and make the problem worse, not better.
Semihosting Alternative
If your project uses a J-Link or OpenOCD-compatible debug probe with semihosting enabled, standard printf will route to the debug console automatically. Enable semihosting by linking with the semihosting libraries: in GCC, add --specs=rdimon.specs -lrdimon to your linker flags and call initialise_monitor_handles() at startup. Semihosting significantly slows down execution (each printf call blocks waiting for the debug probe) — only use it for initial bringup, not for timing-sensitive USB debugging.
Diagnosing Enumeration Failures
Enumeration failure is the most common USB problem. The host connects to your device and… nothing. No COM port appears, no device in Device Manager, or a yellow warning icon. The following decision tree guides systematic diagnosis:
ENUMERATION FAILURE DECISION TREE
══════════════════════════════════════════════════════════════════
Device plugged in → Does anything appear in Device Manager / lsusb?
│
├── NO: Device not detected at all
│ ├── Check VBUS: measure voltage on USB +5V pin — should be 4.5–5.5 V
│ ├── Check D+ pull-up: FS device needs 1.5 kΩ to 3.3 V on D+
│ │ (TinyUSB / STM32 OTG_FS does this in hardware automatically)
│ ├── Check USB clock: STM32 OTG_FS needs 48 MHz exactly from PLL
│ │ (measured with oscilloscope or confirm in CubeMX clock tree)
│ ├── Check reset handler: did tud_init() get called before main loop?
│ └── Hardware analyzer: does device respond to GET_DESCRIPTOR at all?
│
├── YES but "Unknown Device" or error code
│ ├── Address 0 → stuck at first GET_DESCRIPTOR
│ │ ├── Check bMaxPacketSize0 in device descriptor (must be 8, 16, 32, or 64)
│ │ ├── Check that EP0 is configured and tud_task() is running
│ │ └── Enable CFG_TUSB_DEBUG 2 and check UART output for errors
│ │
│ ├── "Device not recognized" (Windows code 43 or similar)
│ │ ├── Wrong bMaxPacketSize0 — must match actual EP0 buffer size
│ │ ├── wTotalLength in configuration descriptor is wrong
│ │ ├── bNumEndpoints does not match actual endpoint descriptors
│ │ └── Class-specific descriptor missing (e.g. CDC functional descriptors)
│ │
│ └── Driver error / wrong class loaded
│ ├── bDeviceClass / bInterfaceClass mismatch
│ ├── Missing IAD descriptor for composite device
│ └── bcdUSB version mismatch (HS device claiming USB 1.1)
│
└── YES and partially works → see Section 9 (top 10 problems)
══════════════════════════════════════════════════════════════════
Symptom / Cause / Fix Reference Table
| Symptom |
Root Cause |
Fix |
| Nothing in Device Manager after plug |
No D+ pull-up, or VBUS not detected |
Check pull-up resistor / OTG_FS VBUS sense pin config |
| Device detected then immediately disconnects |
USB clock not at 48 MHz, or firmware crash in IRQ |
Verify 48 MHz USB clock in CubeMX; add IRQ fault handler |
| "Unknown USB Device (Device descriptor request failed)" |
Device not responding to GET_DESCRIPTOR at address 0 |
Check tud_init() called; check EP0 buffer and IRQ priority |
| "Device not recognized" |
bMaxPacketSize0 does not match EP0 actual size |
Set bMaxPacketSize0=64 for FS; confirm EP0 FIFO is ≥64 bytes |
| Enumeration loop (device keeps re-enumerating) |
Firmware crashes or resets during SET_CONFIGURATION |
Add watchdog breakpoints; use hardware analyzer to see exact point of reset |
| Device shows as generic / no driver loads |
Wrong bDeviceClass or bInterfaceClass values |
Use correct class codes from USB spec; check composite IAD |
| CDC interface appears but no COM port |
Missing CDC functional descriptors (header, call mgmt, ACM) |
Add all three CDC functional descriptors to config descriptor |
| HID device recognized but wrong type |
bInterfaceProtocol wrong (1=keyboard, 2=mouse) |
Set correct protocol; for custom HID set protocol=0 (none) |
| Windows shows "Code 10" error |
Class driver loaded but failed to initialize |
Check class-specific setup; reinstall driver; try different Windows version |
| Works on Windows, fails on macOS |
Descriptor error masked by Windows but rejected by macOS |
Use lsusb -v on Linux to get strict descriptor parse; fix all warnings |
| HS device always runs at FS speed |
HS Chirp not generated — ULPI PHY not initialised |
Check ULPI PHY power and reset; verify HS enable in OTG_HS config |
| STALL on EP0 after SET_CONFIGURATION |
Device not handling all standard requests |
Check GET_INTERFACE, SET_INTERFACE handlers in TinyUSB callbacks |
| Data corruption on bulk endpoint |
DMA cache coherency issue on Cortex-M7 |
Use non-cached SRAM or call SCB_CleanDCache_by_Addr() before DMA |
| Intermittent disconnects under load |
VBUS drooping below threshold, or signal integrity |
Add bulk capacitance on VBUS; check cable impedance |
| String descriptor returns garbage |
String not properly UTF-16LE encoded |
Use TinyUSB's TUD_DESC_STR_INIT() macro for correct encoding |
Descriptor Validation
Before spending hours with Wireshark, validate your descriptors with dedicated tools. A single wrong byte in a descriptor causes silent failure that looks like a hardware problem.
Windows: USB Device Tree Viewer
USB Device Tree Viewer (UVCView.exe, available from Microsoft's Windows Driver Kit samples or from uwe-sieber.de) shows the complete parsed descriptor tree for every connected USB device. It decodes every field, flags invalid values, and shows the raw bytes alongside the parsed interpretation. If your device shows up in Device Manager but the driver is wrong, this tool shows exactly which descriptor field caused the mismatch.
Linux: lsusb -v
# Show verbose descriptor dump for device at bus 1, device 5
lsusb -v -s 1:5
# Or by VID:PID (replace with your values)
lsusb -v -d 0483:5740
# The output includes every descriptor field with names and values:
# bLength, bDescriptorType, bcdUSB, bDeviceClass, etc.
# Look for lines starting with ** — these indicate descriptor parse errors
# Example of a CDC device with correct descriptors:
# bDeviceClass 0xef (Miscellaneous Device Class)
# bDeviceSubClass 2 (Common Class)
# bDeviceProtocol 1 (Interface Association Descriptor)
# ... (IAD present for composite device)
Common Descriptor Mistakes
| Field |
Common Mistake |
Effect |
Correct Value |
wTotalLength |
Hardcoded instead of computed; wrong when adding interfaces |
Host reads partial config, fails to load driver |
Sum of ALL descriptor bytes in config tree |
bNumEndpoints |
Set to 2 but only 1 endpoint descriptor follows |
Host parser goes out of bounds, descriptor corrupt |
Must match exactly number of non-EP0 endpoint descriptors |
bMaxPacketSize0 |
Set to 8 for a Full Speed device |
Slow enumeration, some hosts reject small EP0 |
64 for Full Speed, 512 for High Speed |
bcdUSB |
0x0110 (USB 1.1) on a High Speed device |
Host may not attempt HS negotiation |
0x0200 for USB 2.0, 0x0210 for USB 2.1 (with BOS) |
| String index |
iManufacturer=1 but no string descriptor 1 defined |
Host sends GET_DESCRIPTOR for string index 1 → STALL |
All referenced string indices must have a corresponding descriptor |
bConfigurationValue |
Set to 0 (invalid) |
SET_CONFIGURATION with value 0 means unconfigure |
Must be ≥ 1; typically 1 |
Calculating wTotalLength Manually
The wTotalLength field in the configuration descriptor must be the byte count of every descriptor in the configuration, including interfaces, endpoints, and class-specific descriptors. For a CDC-ACM device:
wTotalLength calculation for CDC-ACM device:
Configuration descriptor: 9 bytes
Interface 0 (CDC Control): 9 bytes
CDC Header Functional: 5 bytes
CDC Call Management: 5 bytes
CDC ACM Functional: 4 bytes
CDC Union Functional: 5 bytes
Endpoint 0x82 (INT IN): 7 bytes
Interface 1 (CDC Data): 9 bytes
Endpoint 0x01 (BULK OUT): 7 bytes
Endpoint 0x81 (BULK IN): 7 bytes
──────────
Total wTotalLength: 67 bytes (0x0043)
For composite CDC + HID, add:
IAD descriptor: 8 bytes (prepend before CDC interface 0)
Interface 2 (HID): 9 bytes
HID Class Descriptor: 9 bytes
Endpoint 0x83 (INT IN): 7 bytes
──────────
Total wTotalLength: 100 bytes (0x0064)
Tip: TinyUSB computes wTotalLength automatically when you use the TUD_CONFIG_DESCRIPTOR macro and include the correct descriptor macro sequence. Manually constructing descriptors is error-prone — always prefer the TinyUSB descriptor macros and let the compiler compute sizes with sizeof().
Bus Utilization Analysis
USB bandwidth is shared. Multiple devices on the same host controller compete for bandwidth within each frame. Understanding bus utilization helps explain why your bulk throughput is lower than theoretical maximum, and is essential for designing isochronous audio/video devices.
Full Speed Frame Structure
At Full Speed (12 Mbit/s), USB operates in 1 ms frames. Each frame starts with a SOF (Start-of-Frame) packet (3 bytes + timing = ~21 bit times), leaving roughly 11,979 bit times (or approximately 1,497 bytes) of usable payload per frame. Every packet also has headers and handshakes that consume bandwidth beyond the payload:
Full Speed Frame Budget (1 ms / 12 Mbit/s):
──────────────────────────────────────────────────────
Total bits per frame: 12,000 bits (1 ms × 12 Mbit/s)
SOF packet overhead: 24 bits (~2 bits header + 22 bit stuffing)
Available payload bits: ~11,976 bits = ~1,497 bytes/frame
Per-transaction overhead (SETUP/DATA/ACK sequence):
Token packet: 3 bytes (PID + ADDR + ENDP + CRC5)
DATA packet: 3 bytes overhead + N bytes payload + 2 bytes CRC16
Handshake: 1 byte (ACK/NAK/STALL)
Total fixed: 7 bytes overhead per transaction
Maximum bulk throughput at FS:
64-byte max packet + 7 bytes overhead = 71 bytes per transaction
~1497 / 71 = ~21 transactions/frame × 64 bytes = ~1344 bytes/ms = ~1.3 MB/s
(theoretical max; real-world ~1.0–1.1 MB/s due to host scheduling)
High Speed Microframe Structure
High Speed Microframe Budget (125 µs / 480 Mbit/s):
──────────────────────────────────────────────────────
Total bits per microframe: 60,000 bits (125 µs × 480 Mbit/s)
SOF/SOA overhead: ~400 bits
Available: ~59,600 bits = ~7,450 bytes/microframe
8 microframes per 1 ms frame → 8 × 7,450 = ~59,600 bytes/ms = ~59.6 MB/s total bus capacity
Maximum bulk throughput at HS:
512-byte max packet + overhead = ~519 bytes per transaction
~7450 / 519 ≈ 14 transactions/microframe × 512 bytes = ~7168 bytes/125 µs
= ~57.3 MB/s theoretical; real-world ~40–45 MB/s
Bandwidth Utilization Formula
Bus Utilization % = (packet_size_bytes × packets_per_frame × 8) / frame_bits × 100
Example: HID mouse at Full Speed
packet_size = 4 bytes (mouse report)
packets/frame = 1 (1 ms polling interval)
frame_bits = 12,000
Utilization = (4 × 1 × 8) / 12,000 × 100 = 0.27%
Example: CDC bulk streaming at Full Speed
packet_size = 64 bytes
packets/frame = 21 (fitting as many as possible)
frame_bits = 12,000
Utilization = (64 × 21 × 8) / 12,000 × 100 = 89.6%
Example: Isochronous audio at Full Speed (44.1 kHz, 16-bit stereo)
44100 samples × 2 channels × 2 bytes = 176,400 bytes/sec
= 176.4 bytes/ms = 176.4 bytes/frame (rounded to 177)
packet_size = 177 bytes (actual USB audio uses alternating 176/177)
packets/frame = 1 (isochronous has guaranteed 1/frame)
frame_bits = 12,000
Utilization = (177 × 1 × 8) / 12,000 × 100 = 11.8%
Why Bulk Gets Leftover Bandwidth
The USB host scheduler assigns bandwidth in this priority order: Control → Isochronous → Interrupt → Bulk. Isochronous and interrupt transfers have guaranteed bandwidth allocations reserved at device enumeration time (the host checks the bInterval field and the wMaxPacketSize and rejects the device if there is insufficient bandwidth). Bulk transfers use whatever bus time remains after all guaranteed transfers have been scheduled. This is why CDC bulk throughput varies under load — if you add a high-bandwidth isochronous audio device to the same host controller, your CDC throughput will decrease.
TinyUSB Note: TinyUSB does not perform bus utilization calculations. The USB host is responsible for bandwidth management. Your firmware simply declares endpoint types and maximum packet sizes in the descriptors — the host decides whether the total requested bandwidth fits in the available frame budget. If you exceed the budget for isochronous/interrupt, the host will reject SET_CONFIGURATION with a STALL.
The 10 Most Common USB Problems
| # |
Problem |
Root Cause |
Diagnosis |
Fix |
| 1 |
Enumeration loop — device keeps re-enumerating endlessly |
Firmware crashes (HardFault) or resets in response to SET_CONFIGURATION or during USB ISR |
Hardware analyzer shows normal enumeration then sudden bus reset; UART log stops after SET_CONFIGURATION |
Add HardFault handler that prints fault reason; ensure USB ISR stack is large enough; use watchdog reset detection to identify the exact crash point |
| 2 |
CDC does not appear as a COM port in Device Manager |
Missing or incorrect CDC functional descriptors (Header, Call Management, ACM, Union); Windows requires all four for CDC-ACM |
USB Device Tree Viewer shows descriptor parse errors; lsusb -v shows missing functional descriptors |
Use TinyUSB's TUD_CDC_DESCRIPTOR() macro which includes all required functional descriptors; do not manually write CDC descriptors |
| 3 |
HID report data is wrong — buttons/axes mapped incorrectly |
HID report descriptor fields do not match the actual byte layout of the report being sent |
Use Wireshark to capture HID IN reports; compare raw bytes to report descriptor; use hidapi or HIDAPI Test App to see parsed report |
Align byte/bit fields in report descriptor exactly with your report struct; use __attribute__((packed)) on the struct; validate with HID Descriptor Tool (DT) |
| 4 |
MSC device not mounting as a drive |
SCSI READ CAPACITY returning wrong sector count/size; or firmware returning STALL to SCSI commands |
Enable TinyUSB debug level 2; UART log will show which SCSI command is failing; Wireshark shows MSC BOT (Bulk-Only Transport) errors |
Verify MSC callbacks: tud_msc_capacity_cb() returns correct block_count and block_size (512); ensure tud_msc_read10_cb() and write10_cb() return actual block count read/written |
| 5 |
High Speed device always falls back to Full Speed |
ULPI PHY not initialized; HS Chirp K sequence not generated; OTG_HS configured for internal FS PHY only |
Hardware analyzer captures Chirp sequence — if no Chirp K from device, PHY is not running; measure 60 MHz ULPI clock on PHY CLK pin |
Enable HS mode in STM32 OTG_HS configuration; initialize ULPI PHY with correct reset sequence; check ULPI data bus and DIR/NXT/STP signals |
| 6 |
Data corruption on bulk endpoint — received data does not match sent data |
DMA cache coherency on Cortex-M7 (STM32H7, STM32F7); D-cache returns stale data to CPU after DMA writes to the same buffer |
Compare sent and received data byte by byte; corruption appears in the first 32 bytes of 32-byte-aligned buffers (one cache line) |
Place USB DMA buffers in non-cached SRAM (SRAM4 on STM32H7); or call SCB_InvalidateDCache_by_Addr() after each DMA receive completes |
| 7 |
Device disconnects under load — works fine with small transfers |
VBUS voltage drops below 4.4 V (USB minimum) when device draws peak current; or signal integrity degrades at high data rates |
Measure VBUS with oscilloscope during high-load transfer; check for voltage dip; hardware analyzer eye diagram shows eye closure at high speed |
Add 100 µF bulk capacitor on VBUS near the MCU; reduce USB power consumption; use shorter, higher-quality USB cable; check PCB trace impedance |
| 8 |
STALL on EP0 control endpoint — enumeration fails partway through |
Firmware returning STALL for a standard request it should handle (GET_INTERFACE, SET_INTERFACE, CLEAR_FEATURE, GET_STATUS) |
Wireshark shows STALL response to a specific control request; compare request type/code against USB spec Table 9-3 |
Implement all mandatory standard requests in TinyUSB callbacks; do not return STALL for unknown optional requests — return ZLP (zero-length packet) instead |
| 9 |
Windows "Code 43" — device has been stopped because it reported problems |
Device sent an unexpected error during class-specific initialization; often a CDC or HID class command (SET_REPORT, SET_LINE_CODING) failing |
USB Device Tree Viewer shows Code 43 with the specific failed URB; Wireshark shows which control request received a STALL |
Implement all class-specific requests the OS will send at startup; for CDC: handle SET_LINE_CODING and GET_LINE_CODING even if baud rate is irrelevant; for HID: handle SET_IDLE and GET_REPORT |
| 10 |
Intermittent disconnects — device works for minutes then randomly drops |
Electrical: loose connector, ESD damage to D+ line, or thermal expansion changing impedance; or firmware: USB task starved by higher-priority tasks |
Log disconnect events with timestamp; check if disconnects correlate with temperature, vibration, or high CPU load; hardware analyzer records exact disconnect event |
Check connector solder joints; add 33 Ω series resistors on D+/D- (if not already present); ensure USB task cannot be preempted for more than 1 ms; add pull-down on D+ to detect genuine disconnect |
Practical Exercises
Exercise 1
Beginner
Capture and Annotate a Complete Enumeration
Use Wireshark + USBPcap (Windows) or usbmon (Linux) to capture the complete enumeration of a USB flash drive or STM32 CDC device from plug-in to ready state. In your capture, identify and annotate: (a) the initial GET_DESCRIPTOR for device descriptor with wLength=8, (b) the SET_ADDRESS request and the address assigned, (c) the full GET_DESCRIPTOR for the configuration descriptor and calculate wTotalLength manually from the response bytes, (d) the SET_CONFIGURATION request. For each step, write down the bmRequestType and bRequest hex values and explain what each means. Save the capture file for comparison in later exercises.
Wireshark
Enumeration
Control Transfers
Exercise 2
Intermediate
Deliberately Break Descriptors and Diagnose
Take a working TinyUSB CDC device on an STM32. Make each of these changes one at a time, observe the result in Wireshark and Device Manager, then fix it and move to the next: (a) change bMaxPacketSize0 from 64 to 32 — observe what Windows says; (b) reduce wTotalLength by 4 bytes — observe that the CDC control interface fails to load; (c) set bNumEndpoints to 3 but only provide 2 endpoint descriptors — observe the parse error. For each broken configuration, record the exact Wireshark error frame and Device Manager error code. This exercise builds the mental model that makes real-world debugging fast.
Descriptor Debugging
Wireshark
Error Analysis
Exercise 3
Advanced
Bus Utilization Measurement and Optimization
Build a TinyUSB composite device with CDC + HID on an STM32. Capture 500 ms of Wireshark traffic while: (a) the HID endpoint sends a report at 1 ms intervals, (b) the CDC endpoint streams bulk data at maximum throughput. Export the capture as CSV and use Python (with pandas) to calculate: actual average CDC bulk throughput in bytes/ms, HID interrupt utilization percentage, ratio of NAK packets to ACK packets on the CDC bulk IN endpoint (high NAK ratio means your firmware is not keeping the buffer full), and the overhead cost of SOF packets. Then reduce the HID polling interval from 1 ms to 10 ms and repeat — measure the improvement in CDC bulk throughput.
Bus Utilization
Performance Tuning
Python Analysis
USB Debug Plan Generator
Use this tool to document your USB debugging session — project details, MCU, chosen debug tool, issue description, capture file path, and findings. Download as Word, Excel, PDF, or PPTX for issue tracking or design review.
Conclusion & Next Steps
USB debugging is systematic once you have the right tools and mental model. The key points from this part:
- Start free: Wireshark + USBPcap (Windows) or usbmon (Linux) handles the majority of post-enumeration debugging at zero cost. Install these tools before you write your first USB descriptor.
- Know the limits of software capture: If the device does not enumerate, the OS sees nothing — you need a hardware analyzer. The Cynthion or OpenVizsla covers Full Speed at low cost; the Total Phase Beagle 480 is the professional standard for High Speed.
- Enable TinyUSB debug level 2: Redirect
tu_printf to UART. The debug output tells you exactly which descriptor request is failing, which callback was called, and what error was returned — before you even open Wireshark.
- Validate descriptors first: Most enumeration failures are descriptor errors. Use
lsusb -v on Linux or USB Device Tree Viewer on Windows to catch descriptor mistakes without debugging firmware at all.
- Know the top 10 problems: Enumeration loops, missing CDC functional descriptors, DMA cache coherency on Cortex-M7, and VBUS voltage droop account for the majority of USB issues in the field. The symptom/cause/fix table in Section 6 covers all of them.
- Bus utilization matters for high-throughput designs: Bulk does not have guaranteed bandwidth — it gets what control, isochronous, and interrupt leave behind. Calculate your utilization budget before committing to USB for a high-throughput application.
Next in the Series
In Part 11: RTOS + USB Integration, we tackle the challenge of running TinyUSB alongside FreeRTOS. You will learn how to design the USB task, set correct priorities to prevent priority inversion, make CDC thread-safe with a mutex, build producer/consumer queues between USB and application tasks, handle DMA cache coherency on STM32H7, and avoid the most common pitfalls that cause TinyUSB to busy-wait and starve all other tasks.
Related Articles in This Series
Part 9: Composite USB Devices
Build USB devices that implement multiple classes simultaneously — CDC + HID, CDC + MSC — using Interface Association Descriptors and TinyUSB composite configuration.
Read Article
Part 11: RTOS + USB Integration
Integrate TinyUSB with FreeRTOS — USB task design, correct priorities, thread-safe CDC, producer/consumer queues, and DMA cache coherency on STM32H7.
Read Article
Part 3: Protocol & Enumeration
The complete USB enumeration sequence, SETUP packet format, descriptor hierarchy, and how the host assigns addresses and activates configurations.
Read Article