Back to Technology

USB Part 10: Debugging USB — Capture, Decode, and Diagnose

March 31, 2026 Wasil Zafar 28 min read

USB bugs are silent — the host gives up without telling you why. Master the full debugging toolkit: software captures with Wireshark, hardware protocol analyzers, TinyUSB UART logs, enumeration decision trees, descriptor validation, bus utilization math, and the 10 most common USB problems with their fixes.

Table of Contents

  1. USB Debugging Tools Overview
  2. Capturing USB Traffic with Wireshark
  3. Reading USB Packet Captures
  4. Hardware Protocol Analyzers
  5. STM32 USB Debug via UART
  6. Diagnosing Enumeration Failures
  7. Descriptor Validation
  8. Bus Utilization Analysis
  9. The 10 Most Common USB Problems
  10. Practical Exercises
  11. USB Debug Plan Generator
  12. Conclusion & Next Steps
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.

USB Development Mastery

Your 17-step learning path • Currently on Step 10

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.

USB Debug Plan Generator

Document your USB debugging session. Download as Word, Excel, PDF, or PPTX.

Draft auto-saved

All data stays in your browser. Nothing is sent to or stored on any server.

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.

Technology