Series Overview: This is Part 4 of our 17-part USB Development Mastery series. Parts 1–3 covered USB fundamentals, electrical and hardware layers, and the protocol and enumeration sequence. Here we survey all major device classes so you can make informed architecture decisions before writing descriptors.
1
USB Fundamentals
USB system architecture, transfer types, host/device model, protocol stack
2
Electrical & Hardware Layer
D+/D- signalling, pull-ups, connectors, USB-C, STM32 USB peripherals
3
Protocol & Enumeration
Enumeration sequence, USB packets, descriptors, endpoint concepts
4
Device Classes Overview
CDC, HID, MSC, Audio, DFU, vendor class, IAD, class code tables
You Are Here
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 Keyboard & Mouse
HID descriptors, report format, keyboard/mouse/gamepad implementation
8
USB Mass Storage
MSC class, SCSI commands, FATFS integration, RAM disk
9
Composite Devices
Multiple classes, IAD descriptor, CDC+HID, CDC+MSC
10
Debugging USB
Wireshark capture, protocol analyser, enumeration debugging, common failures
11
RTOS + USB Integration
FreeRTOS + TinyUSB, task priorities, thread-safe communication
12
Advanced USB Topics
Host mode, OTG, isochronous, USB audio, USB video
13
Performance & Optimisation
DMA, zero-copy buffers, throughput maximisation, latency tuning
14
Custom USB Class Drivers
Vendor class, writing descriptors, OS driver interaction
15
Bare-Metal USB
Direct register programming, writing USB stack from scratch, PHY timing
16
Security in USB
BadUSB attacks, device authentication, secure firmware, USB firewall
17
USB Hardware Design
PCB layout, differential pairs, impedance matching, EMI, USB-C PD
What Is a USB Device Class?
When a USB device connects to a host, the host reads descriptors to determine how to communicate with it. Central to that negotiation is the device class — a standardised category that tells the host OS which driver to load, how to interpret endpoints, and what commands to send. Without a correctly declared class, the host either loads the wrong driver or presents the device as an unknown, unrecognised peripheral.
USB class information can appear in two locations within the descriptor hierarchy, and understanding which location to use is one of the first design decisions you must make:
Device-Level vs Interface-Level Class Codes
The Device Descriptor contains three fields that declare class at the device level:
bDeviceClass — the device class code (0x00 means "see each interface")
bDeviceSubClass — the device subclass code
bDeviceProtocol — the device protocol code
When bDeviceClass = 0x00, the host inspects each Interface Descriptor individually. Each interface has its own bInterfaceClass, bInterfaceSubClass, and bInterfaceProtocol fields. This interface-level approach is used for all standard class devices including CDC, HID, MSC, and Audio. The device-level approach (non-zero bDeviceClass) is reserved for devices where the class applies to the entire device — primarily CDC devices with a single interface (unusual) and some vendor-specific devices.
Rule of Thumb: For almost every practical embedded USB device, set bDeviceClass = 0x00, bDeviceSubClass = 0x00, bDeviceProtocol = 0x00 in the Device Descriptor. Define the actual class in the Interface Descriptor. The only exception is when you use a composite device with CDC interfaces — in that case you must set bDeviceClass = 0xEF (Miscellaneous), bDeviceSubClass = 0x02, bDeviceProtocol = 0x01 to tell the host that Interface Association Descriptors are present.
The Three Tiers: Class, Subclass, Protocol
Each class is further subdivided by subclass and protocol codes. The relationship is hierarchical:
- Class — the broad category (e.g., 0x0A = CDC Data, 0x02 = CDC Communications, 0x03 = HID)
- Subclass — the specific function within the class (e.g., CDC subclass 0x02 = ACM — Abstract Control Model, which is the virtual COM port model)
- Protocol — the specific protocol variant within the subclass (e.g., CDC-ACM protocol 0x01 = AT commands protocol, the de facto standard for virtual serial ports)
Together these three bytes uniquely identify the device's behaviour and determine exactly which OS class driver is instantiated. Getting any one of them wrong produces incorrect driver loading, which typically manifests as a device that enumerates successfully but does not appear correctly in Device Manager (Windows) or /dev/ (Linux).
All Major USB Device Classes
| Class Code |
Class Name |
Description |
Typical Use |
0x00 |
Use Interface |
Class defined at interface level |
Most composite and single-class devices |
0x01 |
Audio |
USB Audio Class (UAC) |
Microphones, speakers, audio interfaces |
0x02 |
Communications (CDC) |
CDC Control Interface |
Virtual COM ports, modems, Ethernet adapters |
0x03 |
HID |
Human Interface Device |
Keyboards, mice, gamepads, custom sensors |
0x05 |
Physical |
Force feedback devices |
Haptic feedback peripherals |
0x06 |
Image |
Still image capture (PTP/MTP) |
Digital cameras, scanners |
0x07 |
Printer |
USB Printer Class |
Inkjet, laser, receipt printers |
0x08 |
Mass Storage (MSC) |
Bulk-Only Transport + SCSI |
Flash drives, SD card readers, data loggers |
0x09 |
Hub |
USB Hub Class |
USB hubs (cannot be implemented by devices) |
0x0A |
CDC Data |
CDC Data Interface |
Paired with CDC Communications interface |
0x0B |
Smart Card |
USB Smart Card Reader |
Smart card readers, HSMs |
0x0D |
Content Security |
Content protection |
DRM hardware |
0x0E |
Video (UVC) |
USB Video Class |
Webcams, thermal cameras, frame grabbers |
0x0F |
Personal Healthcare |
Medical devices |
Pulse oximeters, glucose meters |
0x10 |
Audio/Video |
Combined AV devices |
Video conferencing systems |
0xDC |
Diagnostic |
USB compliance testing |
Debug probes, protocol analysers |
0xE0 |
Wireless Controller |
Bluetooth, WUSB |
Bluetooth dongles, Wi-Fi adapters |
0xEF |
Miscellaneous |
Multi-interface devices with IAD |
Composite devices with CDC + other classes |
0xFE |
Application Specific |
DFU, IrDA, Test & Measurement (USBTMC) |
Firmware upgrade, lab instruments |
0xFF |
Vendor-Specific |
Custom class, vendor-defined protocol |
Custom USB devices with proprietary drivers |
CDC — Communications Device Class
The Communications Device Class (CDC) is the class you will use more than any other in embedded development. CDC-ACM (Abstract Control Model) is what creates a virtual COM port — a USB device that appears to the host OS as a serial port (COMx on Windows, /dev/ttyACM0 or /dev/ttyUSB0 on Linux). No additional driver installation is needed on Linux or macOS, and Windows 10 and later includes a built-in CDC-ACM driver.
The PSTN Model and ACM Subclass
CDC is organised around the PSTN (Public Switched Telephone Network) model — a historical artifact from when USB was designed to replace modem connections. The ACM subclass (Abstract Control Model) is the PSTN subclass most relevant to embedded work. It abstracts away the physical telephone line and provides a clean bidirectional data pipe with optional line-coding notifications.
CDC-ACM requires two interfaces: a Control Interface and a Data Interface. This two-interface structure is where many beginners make mistakes — they forget one interface, or they assign class codes to the wrong interface.
CDC Interface Structure
| Interface |
bInterfaceClass |
bInterfaceSubClass |
bInterfaceProtocol |
Endpoints |
Purpose |
| CDC Control |
0x02 |
0x02 (ACM) |
0x01 (AT commands) |
1× IN Interrupt |
Send notifications (line state, serial state) |
| CDC Data |
0x0A |
0x00 |
0x00 |
1× IN Bulk + 1× OUT Bulk |
Bidirectional data transfer |
The Notification Endpoint (IN Interrupt on the Control Interface) is used to send notifications like SERIAL_STATE (indicating DCD, DSR, break detection) to the host. In practice most embedded CDC-ACM implementations have this endpoint but never actually send notifications — the host requires the endpoint to exist but tolerates it being silent.
CDC Class Descriptor Chain
Between the Standard Interface Descriptor and the first endpoint descriptor of the Control Interface, you must place a chain of CDC Functional Descriptors. At minimum, CDC-ACM requires a Header Functional Descriptor, a Call Management Functional Descriptor, an Abstract Control Management Functional Descriptor, and a Union Functional Descriptor:
/* CDC Control Interface — descriptor chain for CDC-ACM */
/* Standard Interface Descriptor */
0x09, 0x04, /* bLength, bDescriptorType (Interface) */
0x00, /* bInterfaceNumber: Interface 0 */
0x00, /* bAlternateSetting */
0x01, /* bNumEndpoints: 1 (Notification EP) */
0x02, /* bInterfaceClass: CDC */
0x02, /* bInterfaceSubClass: ACM */
0x01, /* bInterfaceProtocol: AT Commands */
0x00, /* iInterface */
/* CDC Header Functional Descriptor */
0x05, 0x24, 0x00, /* bLength=5, bDescriptorType=CS_INTERFACE, bDescriptorSubtype=HEADER */
0x10, 0x01, /* bcdCDC: 1.10 */
/* Call Management Functional Descriptor */
0x05, 0x24, 0x01, /* bLength=5, CS_INTERFACE, CALL_MANAGEMENT */
0x00, /* bmCapabilities: device does not handle call management itself */
0x01, /* bDataInterface: data interface number = 1 */
/* Abstract Control Management Functional Descriptor */
0x04, 0x24, 0x02, /* bLength=4, CS_INTERFACE, ABSTRACT_CONTROL_MANAGEMENT */
0x02, /* bmCapabilities: supports Set_Line_Coding, Set_Control_Line_State,
Get_Line_Coding, Serial_State notification */
/* Union Functional Descriptor */
0x05, 0x24, 0x06, /* bLength=5, CS_INTERFACE, UNION */
0x00, /* bMasterInterface: Interface 0 (this interface) */
0x01, /* bSlaveInterface0: Interface 1 (CDC Data) */
/* Notification Endpoint (IN Interrupt) */
0x07, 0x05, /* bLength=7, bDescriptorType=ENDPOINT */
0x81, /* bEndpointAddress: EP1 IN */
0x03, /* bmAttributes: Interrupt */
0x08, 0x00, /* wMaxPacketSize: 8 */
0x10, /* bInterval: 16ms polling interval */
CDC Use Cases
CDC-ACM is not just for virtual serial ports. The CDC class encompasses several subclasses used in different contexts:
- Virtual COM Port (CDC-ACM): The most common use. Acts as a serial bridge for debug output, AT command interfaces, and bootloader communication.
- USB-to-Ethernet (CDC-ECM/RNDIS): Subclasses 0x06 (ECM) and 0x0E (RNDIS) allow a USB device to appear as a network adapter. Used in embedded Linux (BeagleBone, STM32MP1) for USB gadget Ethernet.
- USB-to-Ethernet (CDC-NCM): Subclass 0x0D is the modern high-performance Ethernet model used in smartphones for USB tethering.
- OBEX: Subclass 0x0B for object exchange — used in legacy PDA synchronisation.
HID — Human Interface Device
The Human Interface Device (HID) class is the most self-contained of all USB classes. A correctly implemented HID device requires zero driver installation on any operating system — Windows, Linux, macOS, Android, iOS, and even gaming consoles all include built-in HID support. This makes HID the first choice for any device that sends user-input data to a host.
The Report Descriptor
The defining feature of HID is the Report Descriptor — a compact binary structure that tells the host exactly what data your device sends and receives. Unlike CDC where the data format is implicit (raw bytes), HID data is self-describing. The host reads the Report Descriptor once during enumeration and uses it to interpret all subsequent HID reports.
Report Descriptors are built from items: Short Items (1–5 bytes) and Long Items (up to 258 bytes). Short Items have a tag, type, and size encoded in the first byte, followed by the data. Common items include:
| Item |
Hex |
Purpose |
| USAGE_PAGE | 0x05 | Select the usage page (Generic Desktop, Game Controls, etc.) |
| USAGE | 0x09 | Select the specific usage within the page |
| COLLECTION | 0xA1 | Start a collection (Application, Physical, Logical) |
| END_COLLECTION | 0xC0 | Close the collection |
| REPORT_ID | 0x85 | Assign a report ID (required for multi-report devices) |
| REPORT_COUNT | 0x95 | Number of data fields |
| REPORT_SIZE | 0x75 | Size in bits of each field |
| LOGICAL_MINIMUM | 0x15 | Minimum logical value |
| LOGICAL_MAXIMUM | 0x25 | Maximum logical value |
| INPUT | 0x81 | Declare an input field (device to host) |
| OUTPUT | 0x91 | Declare an output field (host to device) |
| FEATURE | 0xB1 | Declare a feature field (bidirectional, config) |
A minimal keyboard Report Descriptor looks like this:
/* USB HID Report Descriptor — keyboard with modifier keys */
static const uint8_t hid_keyboard_report_descriptor[] = {
0x05, 0x01, /* USAGE_PAGE (Generic Desktop) */
0x09, 0x06, /* USAGE (Keyboard) */
0xA1, 0x01, /* COLLECTION (Application) */
/* Modifier keys: 8 bits, one per modifier (Ctrl, Shift, Alt, GUI x 2) */
0x05, 0x07, /* USAGE_PAGE (Keyboard/Keypad) */
0x19, 0xE0, /* USAGE_MINIMUM (Keyboard Left Control) */
0x29, 0xE7, /* USAGE_MAXIMUM (Keyboard Right GUI) */
0x15, 0x00, /* LOGICAL_MINIMUM (0) */
0x25, 0x01, /* LOGICAL_MAXIMUM (1) */
0x75, 0x01, /* REPORT_SIZE (1 bit) */
0x95, 0x08, /* REPORT_COUNT (8) */
0x81, 0x02, /* INPUT (Data, Variable, Absolute) */
/* Reserved byte */
0x95, 0x01, /* REPORT_COUNT (1) */
0x75, 0x08, /* REPORT_SIZE (8 bits) */
0x81, 0x01, /* INPUT (Constant) */
/* 6 keycodes (up to 6 simultaneous key presses) */
0x95, 0x06, /* REPORT_COUNT (6) */
0x75, 0x08, /* REPORT_SIZE (8 bits) */
0x15, 0x00, /* LOGICAL_MINIMUM (0) */
0x25, 0xFF, /* LOGICAL_MAXIMUM (255) */
0x05, 0x07, /* USAGE_PAGE (Keyboard/Keypad) */
0x19, 0x00, /* USAGE_MINIMUM (0) */
0x29, 0xFF, /* USAGE_MAXIMUM (255) */
0x81, 0x00, /* INPUT (Data, Array) */
/* LED output: Num Lock, Caps Lock, Scroll Lock, Compose, Kana */
0x95, 0x05, /* REPORT_COUNT (5) */
0x75, 0x01, /* REPORT_SIZE (1 bit) */
0x05, 0x08, /* USAGE_PAGE (LEDs) */
0x19, 0x01, /* USAGE_MINIMUM (Num Lock) */
0x29, 0x05, /* USAGE_MAXIMUM (Kana) */
0x91, 0x02, /* OUTPUT (Data, Variable, Absolute) */
/* LED padding: 3 bits to round to byte boundary */
0x95, 0x01, /* REPORT_COUNT (1) */
0x75, 0x03, /* REPORT_SIZE (3 bits) */
0x91, 0x01, /* OUTPUT (Constant) */
0xC0 /* END_COLLECTION */
};
Boot Protocol vs Report Protocol
HID devices can support two modes: Boot Protocol and Report Protocol. Boot Protocol is a fixed, simplified data format that the host BIOS can use before the OS loads and before it has parsed the Report Descriptor. Report Protocol is the full, flexible Report-Descriptor-driven mode.
- Boot Protocol is only relevant for keyboards (subclass 0x01, protocol 0x01) and mice (subclass 0x01, protocol 0x02). It uses an 8-byte fixed format for keyboards.
- Report Protocol is the mode used by all other HID devices and by keyboards/mice once the OS is running. Your firmware must handle
SET_PROTOCOL and GET_PROTOCOL control requests if you declare boot-protocol support.
- Custom HID devices (sensors, custom controllers) always use Report Protocol exclusively: set
bInterfaceSubClass = 0x00 and bInterfaceProtocol = 0x00.
Polling Interval and Performance
HID uses Interrupt endpoints. The bInterval field in the endpoint descriptor sets the polling interval — how often the host asks for new data. This is critically important for latency-sensitive applications:
| bInterval Value (FS) | Polling Interval | Use Case |
| 1 | 1 ms | Gaming mice, competitive keyboards — lowest latency |
| 2 | 2 ms | Standard gaming peripherals |
| 4 | 4 ms | General-purpose HID sensors |
| 10 | 10 ms | Slow control panels, status displays |
| 255 | 255 ms | Infrequent status updates |
Note that bInterval = 1 at Full Speed guarantees a maximum polling rate of 1 kHz (1000 reports/second). At High Speed, intervals are measured in microframes (125 µs), so bInterval = 1 gives 8 kHz — relevant only for latency-critical professional audio or gaming devices.
HID Report Types
- INPUT reports — device sends to host (key presses, mouse movement, sensor values). Transmitted via the Interrupt IN endpoint, but can also be requested via GET_REPORT control transfer.
- OUTPUT reports — host sends to device (keyboard LED state, motor speed, actuator control). Received via the Interrupt OUT endpoint, or SET_REPORT control transfer if no OUT endpoint is defined.
- FEATURE reports — configuration data exchanged via GET_REPORT/SET_REPORT control transfers only. Useful for device configuration, calibration, and firmware version queries.
MSC — Mass Storage Class
The Mass Storage Class (MSC) turns your embedded device into a USB storage device — the kind that appears as a removable drive in File Explorer or Finder. MSC is deceptively simple from the descriptor perspective but hides substantial complexity in its command protocol.
Bulk-Only Transport (BBB)
MSC exclusively uses the Bulk-Only Transport (BOT, also called BBB — Bulk/Bulk/Bulk) protocol. All communication happens through two Bulk endpoints (IN and OUT) — there are no Control or Interrupt endpoint transactions after enumeration (other than standard EP0 requests).
Every MSC transaction follows a three-phase protocol:
- CBW (Command Block Wrapper): Host sends a 31-byte CBW containing a SCSI command to the device via the Bulk OUT endpoint. The CBW identifies the direction, data length, and SCSI CDB (Command Descriptor Block).
- Data Phase: If the SCSI command requires data transfer, it happens here. Direction (IN or OUT) is determined by the
bmCBWFlags bit in the CBW.
- CSW (Command Status Wrapper): Device sends a 13-byte CSW back to the host via Bulk IN, indicating success, failure, or phase error.
/* MSC Bulk-Only Transport — CBW structure */
typedef struct __attribute__((packed)) {
uint32_t dCBWSignature; /* 0x43425355 — 'USBC' */
uint32_t dCBWTag; /* Host-assigned tag, echoed in CSW */
uint32_t dCBWDataTransferLength; /* Bytes to transfer in data phase */
uint8_t bmCBWFlags; /* Bit 7: 0=OUT(host->device), 1=IN(device->host) */
uint8_t bCBWLUN; /* Target LUN (Logical Unit Number) */
uint8_t bCBWCBLength; /* Length of CBWCB field (1-16) */
uint8_t CBWCB[16]; /* SCSI command block */
} msc_cbw_t;
/* MSC Bulk-Only Transport — CSW structure */
typedef struct __attribute__((packed)) {
uint32_t dCSWSignature; /* 0x53425355 — 'USBS' */
uint32_t dCSWTag; /* Same tag as the CBW this responds to */
uint32_t dCSWDataResidue; /* Difference between requested and actual bytes */
uint8_t bCSWStatus; /* 0x00=success, 0x01=command failed, 0x02=phase error */
} msc_csw_t;
SCSI Transparent Command Set
MSC uses the SCSI Transparent Command Set (bInterfaceProtocol = 0x50). Your firmware must respond to SCSI commands. At minimum, a functional MSC device must handle:
| SCSI Command | Opcode | Purpose |
| INQUIRY | 0x12 | Identify the device (vendor, product, version strings) |
| TEST UNIT READY | 0x00 | Check if the medium is ready (not ejected) |
| REQUEST SENSE | 0x03 | Return error information after a failed command |
| READ CAPACITY (10) | 0x25 | Return total number of logical blocks and block size |
| READ (10) | 0x28 | Read N blocks starting at LBA (Logical Block Address) |
| WRITE (10) | 0x2A | Write N blocks starting at LBA |
| MODE SENSE (6) | 0x1A | Return device mode parameters |
| PREVENT/ALLOW MEDIUM REMOVAL | 0x1E | Lock or unlock the medium |
LUN Concept
MSC supports multiple Logical Unit Numbers (LUNs), allowing a single USB connection to expose multiple storage volumes. For example, a card reader with SD and microSD slots exposes LUN 0 (SD card) and LUN 1 (microSD card) through a single MSC interface. The host enumerates all LUNs using the GET MAX LUN class-specific request.
For most embedded applications with a single storage medium, you will have exactly one LUN (LUN 0). TinyUSB's MSC implementation supports multiple LUNs — simply return the correct LUN count in the tud_msc_get_maxlun_cb() callback.
Critical Requirement: The host OS formats and reads MSC volumes using FAT12, FAT16, FAT32, or exFAT. Your MSC device must present storage that is already FAT-formatted, or you must format it on first use. An MSC device that exposes unformatted raw flash will confuse the OS into requesting format on every connect. Use FatFS or a pre-formatted ROM image to satisfy this requirement.
USB Audio Class
The USB Audio Class (UAC) is one of the most complex USB classes to implement correctly. It governs how USB is used to transmit audio streams — from microphones to speakers to professional audio interfaces. UAC exists in two major versions with significant differences in capability and implementation complexity.
UAC 1.0 vs UAC 2.0
| Feature |
UAC 1.0 |
UAC 2.0 |
| USB Speed Required | Full Speed sufficient | High Speed required for high-quality audio |
| Max Sample Rate (FS) | 96 kHz / 24-bit (bandwidth limited) | Up to 384 kHz (requires HS) |
| OS Driver Support | Built-in on all OS, BIOS-compatible | Built-in on Windows 10+, macOS, Linux |
| Clock Source Control | Limited (SET_SAMPLE_RATE request) | Full clock topology with Clock Source entities |
| Feedback Mechanism | Isochronous feedback endpoint | Improved explicit feedback + implicit feedback |
| Channel Count | Up to 8 channels | Up to 256 channels |
| Complexity | High | Very High |
Isochronous Endpoint Requirement
Audio class is the primary consumer of isochronous endpoints. Isochronous transfers guarantee a fixed bandwidth allocation every USB frame (1 ms at Full Speed, 125 µs at High Speed) but provide no error retransmission. Audio must arrive continuously and on time — a retransmitted packet arriving late is worse than a dropped one, because it disrupts the timing of subsequent packets.
For CD-quality audio (44.1 kHz, 16-bit stereo), the data rate is 44100 × 2 × 2 = 176,400 bytes/second = 176.4 bytes/frame at Full Speed. Since USB transfers integer bytes per frame, audio devices must handle the 44.1 kHz / 1 kHz = 44.1 frames/sample irregularity using a feedback endpoint — a separate isochronous IN endpoint that tells the host how many samples to send per frame to keep the device's audio clock synchronised.
Audio Function Topology
UAC descriptors define an audio processing topology using Units and Terminals. The key entities are:
- Input Terminal: The source of audio data (USB streaming from host = IT type 0x0101, or microphone = 0x0201)
- Output Terminal: The sink of audio data (USB streaming to host = OT type 0x0101, or speaker = 0x0301)
- Feature Unit: Processing entity — mute, volume control, bass, treble
- Mixer Unit: Combines multiple audio streams
- Selector Unit: Switches between audio sources
A typical USB speaker topology: USB Streaming Input Terminal → Feature Unit (volume/mute) → Speaker Output Terminal. A USB microphone: Microphone Input Terminal → Feature Unit → USB Streaming Output Terminal.
Practical Note: If you need simple audio in an embedded project — audio playback from an MCU or recording from a microphone — consider UAC 1.0 at Full Speed. For 48 kHz, 16-bit stereo, the data rate is 192 bytes/ms, comfortably within Full Speed's 1 MB/s theoretical maximum. UAC 2.0 is necessary only if you need high sample rates, high channel counts, or precise professional-grade clock synchronisation.
USB Video Class (UVC)
The USB Video Class (UVC) defines how USB is used to transport video streams — the class that makes webcams and USB capture cards work without driver installation on modern operating systems. UVC shares architectural DNA with UAC but is specifically designed for video frames rather than audio samples.
UVC Interface Structure
A UVC device uses multiple interfaces grouped under an Interface Association Descriptor:
- VideoControl Interface: Class 0x0E, SubClass 0x01. Handles device control — camera pan/tilt/zoom, brightness, contrast, white balance. Contains Unit and Terminal descriptors (very similar to UAC's topology model). Uses a single Interrupt IN endpoint for status notifications.
- VideoStreaming Interface(s): Class 0x0E, SubClass 0x02. Each streaming interface carries one video stream. Has an alternate setting 0 (zero bandwidth, for setting configuration) and one or more alternate settings with isochronous endpoints carrying the actual video data.
Video Format and Frame Descriptors
Within the VideoStreaming Interface, you declare what video formats your device supports using Format Descriptors and Frame Descriptors:
| Descriptor | Purpose | Key Fields |
| VS_FORMAT_MJPEG | Declare MJPEG compressed video support | bFormatIndex, bNumFrameDescriptors |
| VS_FRAME_MJPEG | Declare one MJPEG resolution | wWidth, wHeight, dwMinBitRate, dwMaxFrameInterval |
| VS_FORMAT_UNCOMPRESSED | Declare raw YUY2/NV12 video support | guidFormat (YUY2 = {32595559-...}, NV12 = {3231564E-...}) |
| VS_FRAME_UNCOMPRESSED | Declare one uncompressed resolution | wWidth, wHeight, dwMaxVideoFrameBufferSize |
Bandwidth Considerations
UVC's bandwidth requirements are substantial. Uncompressed video at 640×480 at 30 fps in YUY2 format requires 640 × 480 × 2 × 30 = 18.4 MB/s — far exceeding Full Speed USB's practical maximum of ~1 MB/s for isochronous. UVC with uncompressed video above thumbnail resolution requires High Speed USB (480 Mbit/s).
MJPEG video reduces bandwidth dramatically. A 1280×720 MJPEG stream at 30 fps might average 3–8 MB/s depending on compression quality — achievable at High Speed. For embedded applications (thermal cameras, machine vision), MJPEG at Full Speed is feasible only at low resolutions (160×120 or 320×240) and low frame rates (5–15 fps).
DFU — Device Firmware Upgrade
The Device Firmware Upgrade (DFU) class is a standardised mechanism for updating firmware over USB. It is defined in the USB DFU Specification 1.1 (class code 0xFE, subclass 0x01). Almost every serious USB device should implement DFU — it transforms firmware update from a fragile, hardware-dependent process into a robust, standardised USB transaction.
DFU Runtime Mode vs DFU Mode
DFU operates in two distinct modes:
- DFU Runtime Mode: The device is running its normal application firmware. It exposes a DFU Interface alongside its normal application interfaces. The host can use the DFU_DETACH request to ask the device to reset into DFU Mode.
- DFU Mode: The device is running its bootloader. Only the DFU Interface is visible — no application interfaces. The host downloads new firmware using DFU_DNLOAD requests.
Many STM32 devices include a built-in DFU bootloader in ROM (System Memory). When BOOT0 pin is held high at reset (or through a software-triggered reset to System Memory), the STM32 enters DFU mode and appears as a standard DFU device. The host tool dfu-util can then flash firmware without any additional bootloader code in your application.
DFU Descriptor and bmAttributes
The DFU Functional Descriptor is placed after the Standard Interface Descriptor and contains the bmAttributes field that controls DFU behaviour:
| bmAttributes Bit | Name | Meaning when Set |
| Bit 0 | bitCanDnload | Device can receive firmware download (host → device) |
| Bit 1 | bitCanUpload | Device can perform firmware upload (device → host) |
| Bit 2 | bitManifestationTolerant | Device can communicate after Manifestation phase without reset |
| Bit 3 | bitWillDetach | Device will detach+reattach on DFU_DETACH (no host USB reset needed) |
/* DFU Functional Descriptor */
static const uint8_t dfu_functional_descriptor[] = {
0x09, /* bLength */
0x21, /* bDescriptorType: DFU FUNCTIONAL */
0x0B, /* bmAttributes: bitCanDnload | bitCanUpload | bitWillDetach
(0x01 | 0x02 | 0x08 = 0x0B)
bitManifestationTolerant NOT set = device will reset after manifest */
0xFF, 0x00, /* wDetachTimeOut: 255 ms */
0x40, 0x00, /* wTransferSize: 64 bytes per DFU_DNLOAD block */
0x1A, 0x01 /* bcdDFUVersion: 1.1a */
};
DFU Transaction Sequence
| Step | Host Action | Device Response | State Transition |
| 1 | Send DFU_DETACH request | Reset and re-enumerate in DFU mode | appIDLE → appDETACH |
| 2 | Re-enumerate | DFU-only descriptor set visible | dfuIDLE |
| 3 | Send DFU_DNLOAD (block 0 = first 64 bytes) | Store in flash write buffer | dfuIDLE → dfuDNLOAD-SYNC |
| 4 | Send DFU_GETSTATUS | Return status (may include bwPollTimeout) | dfuDNLOAD-SYNC → dfuDNBUSY → dfuDNLOAD-IDLE |
| 5 | Repeat DNLOAD+GETSTATUS for all blocks | Continue writing flash | dfuDNLOAD-IDLE loop |
| 6 | Send DFU_DNLOAD (zero length = end of firmware) | Begin manifestation (program verification) | dfuDNLOAD-IDLE → dfuMANIFEST-SYNC |
| 7 | Send DFU_GETSTATUS | Return OK then reset | dfuMANIFEST → dfuMANIFEST-WAIT-RESET |
| 8 | Issue USB reset | Reset and boot new firmware | → appIDLE |
Using dfu-util
The open-source dfu-util tool is the standard host-side utility for DFU operations on Linux, macOS, and Windows (via Zadig WinUSB driver):
# List detected DFU devices
dfu-util --list
# Flash firmware to device (DFU mode, address 0x08000000 for STM32)
dfu-util --device 0483:df11 --alt 0 --dfuse-address 0x08000000 \
--download firmware.bin
# Upload (read back) firmware from device
dfu-util --device 0483:df11 --alt 0 --dfuse-address 0x08000000 \
--upload firmware_readback.bin
# Trigger DFU_DETACH (switch from runtime to DFU mode)
dfu-util --device 0483:5740 --detach
Vendor-Specific Class (0xFF)
When no standard USB class fits your device's requirements, you declare a Vendor-Specific class using bInterfaceClass = 0xFF. This gives you complete freedom to define your own protocol but requires you to solve the driver problem — a vendor-specific device has no OS class driver, so the host cannot communicate with it without something installing a driver or using a generic driver access layer.
When to Use Vendor-Specific Class
- Your device does not fit any standard class (e.g., an oscilloscope, a motor controller, a proprietary measurement instrument)
- You need high-throughput bulk transfers with a custom protocol that CDC cannot cleanly represent
- You are implementing USBTMC (USB Test and Measurement Class) or a similar application-specific protocol that still uses 0xFF at the device class level
- You want to add custom vendor control requests (
bRequest values ≥ 0x40) alongside a standard class
WinUSB: Driverless Vendor-Class Access on Windows
The conventional solution for driver-free Windows access to a vendor-specific device is WinUSB — Microsoft's built-in generic USB driver. WinUSB allows applications to talk directly to USB endpoints using the WinUSB API (or libusb on top of it) without installing a custom driver.
To enable WinUSB auto-installation on Windows 8.1 and later, you embed a Microsoft OS 2.0 Descriptor in your device. The host requests this descriptor via a vendor-specific GET_DESCRIPTOR control request (bRequest = 0x02, wIndex = 0x0007). The descriptor tells Windows to automatically install WinUSB for the device:
/* Microsoft OS 2.0 Platform Capability Descriptor — enables WinUSB auto-install */
/* Embedded in the BOS (Binary device Object Store) descriptor */
#define MS_OS_20_DESCRIPTOR_LENGTH 0x1E
static const uint8_t ms_os_20_descriptor[] = {
/* Microsoft OS 2.0 descriptor set header */
0x0A, 0x00, /* wLength: 10 */
0x00, 0x00, /* wDescriptorType: MS_OS_20_SET_HEADER_DESCRIPTOR */
0x00, 0x00, 0x03, 0x06, /* dwWindowsVersion: Windows 8.1 (0x06030000) */
MS_OS_20_DESCRIPTOR_LENGTH, 0x00, /* wTotalLength */
/* MS OS 2.0 compatible ID descriptor */
0x14, 0x00, /* wLength: 20 */
0x03, 0x00, /* wDescriptorType: MS_OS_20_FEATURE_COMPATIBLE_ID */
'W', 'I', 'N', 'U', 'S', 'B', 0x00, 0x00, /* CompatibleID: "WINUSB\0\0" */
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 /* SubCompatibleID: all zeros */
};
Vendor Control Requests and libusb
Vendor-specific devices typically use custom Control transfers on Endpoint 0 to send commands and query status. The host application sends a control request with bmRequestType indicating Vendor type and the target (device or interface). Your firmware handles these in the vendor control request callback.
On the host side, libusb is the cross-platform library for accessing vendor-specific USB devices from user-space applications on Linux, macOS, and Windows:
/* Host-side libusb example — send a vendor control request */
#include <libusb-1.0/libusb.h>
int send_vendor_command(libusb_device_handle *handle,
uint8_t bRequest, uint16_t wValue,
uint8_t *data, uint16_t length)
{
/* bmRequestType: 0x40 = Host-to-Device | Vendor | Device */
return libusb_control_transfer(
handle,
0x40, /* bmRequestType */
bRequest, /* bRequest (vendor-defined) */
wValue, /* wValue (vendor-defined parameter) */
0, /* wIndex (0 for device-level request) */
data, /* data buffer */
length, /* wLength */
5000 /* timeout ms */
);
}
Vendor-Specific vs CDC: A Comparison
| Consideration | CDC-ACM | Vendor-Specific + WinUSB |
| Driver required (Windows) | None (Windows 10+) | WinUSB (auto-install with MS OS 2.0) |
| Driver required (Linux) | None (built-in cdc_acm) | None (direct udev + libusb access) |
| OS presents as | Serial COM port / ttyACM device | Raw USB device (no system device) |
| Host access method | Standard serial APIs (COM port, termios) | libusb, WinUSB API |
| Throughput ceiling (FS) | ~1 MB/s (bulk, practical ~800 KB/s) | Same bulk endpoints, same ceiling |
| Custom control requests | Limited (CDC class requests only) | Full vendor control request space |
| Use when | You need a serial port in any language | You have a non-serial custom protocol |
Interface Association Descriptor (IAD)
The Interface Association Descriptor (IAD) solves a specific problem that arises with composite USB devices: how do you tell the host OS that two or more adjacent interfaces belong together as a single function? Without IAD, the OS might try to load separate, conflicting drivers for each interface — particularly problematic for CDC, which requires its Control and Data interfaces to be treated as a unit.
Why IAD Exists
The USB 2.0 base specification has no mechanism for grouping interfaces. CDC was designed before this limitation was well understood, and the original solution was to use the Device Descriptor class fields to signal CDC (setting bDeviceClass = 0x02). This works for single-function CDC devices but breaks down when you add a second function — the OS can no longer determine where the CDC function ends and the next function begins.
IAD, defined in ECN-InterfaceAssocDescriptor.pdf (2003), adds a new descriptor type (0x0B) that groups two or more consecutive interfaces. When IAD is present, the Device Descriptor must use the Miscellaneous class code to signal its presence:
/* Device Descriptor fields when IAD is used (composite device with CDC) */
/* bDeviceClass = 0xEF (Miscellaneous Device Class) */
/* bDeviceSubClass = 0x02 (Common Class) */
/* bDeviceProtocol = 0x01 (Interface Association Descriptor) */
/* IAD for a CDC function (interfaces 0 and 1) */
static const uint8_t iad_cdc[] = {
0x08, /* bLength: 8 bytes */
0x0B, /* bDescriptorType: Interface Association */
0x00, /* bFirstInterface: Interface 0 is the first interface in this function */
0x02, /* bInterfaceCount: 2 interfaces belong to this function (Control + Data) */
0x02, /* bFunctionClass: CDC */
0x02, /* bFunctionSubClass: ACM */
0x01, /* bFunctionProtocol: AT Commands */
0x00 /* iFunction: string index (0 = no string) */
};
IAD Placement in the Configuration Descriptor
The IAD must appear before the first Interface Descriptor of the group it describes. The correct order within the Configuration Descriptor body is:
/* Configuration Descriptor structure for CDC + HID composite device */
/* 1. Configuration Descriptor (9 bytes) */
/* 2. IAD for CDC function (8 bytes) ← tells OS: interfaces 0+1 = CDC */
/* 3. CDC Control Interface Descriptor (9 bytes) */
/* 4. CDC Header Functional Descriptor (5 bytes) */
/* 5. CDC Call Management Functional Descriptor (5 bytes) */
/* 6. CDC ACM Functional Descriptor (4 bytes) */
/* 7. CDC Union Functional Descriptor (5 bytes) */
/* 8. CDC Notification Endpoint (7 bytes) */
/* 9. CDC Data Interface Descriptor (9 bytes) */
/* 10. CDC Data Bulk OUT Endpoint (7 bytes) */
/* 11. CDC Data Bulk IN Endpoint (7 bytes) */
/* ─── No IAD needed for single-interface functions ─── */
/* 12. HID Interface Descriptor (9 bytes) */
/* 13. HID Descriptor (9 bytes) */
/* 14. HID Interrupt IN Endpoint (7 bytes) */
IAD Rule: An IAD is required for every multi-interface function (currently just CDC in common practice). Single-interface functions (HID, MSC, DFU, single-interface vendor class) do not need an IAD. If your device has only one CDC function and no other functions, an IAD is still recommended but technically optional if the Device Descriptor uses bDeviceClass = 0x02 directly — though this approach is incompatible with adding a second class later without restructuring the entire descriptor set.
Choosing the Right USB Device Class
With a thorough understanding of each class, the selection process becomes systematic. The following decision framework guides you from requirements to class selection:
Decision Flowchart
Start: What does your device need to do over USB?
│
├─ Firmware update capability?
│ └─ Add DFU interface to whatever class you choose below.
│
├─ Send/receive a serial data stream (like a UART)?
│ └─ → CDC-ACM (Virtual COM Port)
│
├─ Appear as a keyboard, mouse, gamepad, or custom input device?
│ └─ → HID
│ ├─ Simple input only? → Standard HID (no OUT endpoint)
│ └─ Input + output (LED feedback, actuators)? → HID with OUT endpoint
│
├─ Expose a file system to the host (drag-and-drop files)?
│ └─ → MSC
│ ├─ Have a FAT-formatted storage medium? → Straightforward MSC
│ └─ No storage? → RAM disk (512 KB typical) + FatFS
│
├─ Stream audio (microphone or speaker)?
│ └─ → UAC
│ ├─ ≤ 48 kHz, stereo, Full Speed? → UAC 1.0
│ └─ > 48 kHz or high channel count? → UAC 2.0 (requires High Speed)
│
├─ Stream video (camera or frame grabber)?
│ └─ → UVC
│ ├─ Low resolution (160×120), low fps, Full Speed? → UVC + MJPEG
│ └─ ≥ 640×480 @ 30fps? → UVC requires High Speed
│
├─ Custom protocol, not fitting any above?
│ └─ → Vendor-Specific (0xFF)
│ ├─ Need easy cross-platform access? → WinUSB + libusb
│ └─ Need SCPI-like instrument protocol? → USBTMC (subclass 0xFE, protocol 0x01)
│
└─ Multiple of the above simultaneously?
└─ → Composite device
├─ CDC present? → Set bDeviceClass=0xEF, use IAD
└─ CDC absent? → Set bDeviceClass=0x00, no IAD needed
Summary Table: Use Case to Class Mapping
| Use Case |
Recommended Class |
Driver Required? |
OS Support |
| Debug serial output, AT commands, printf over USB |
CDC-ACM |
None (Win 10+, Linux, macOS) |
Universal |
| Custom keyboard or gamepad |
HID |
None |
Universal (inc. BIOS, consoles) |
| Custom mouse or pointing device |
HID |
None |
Universal |
| Custom sensor data (low bandwidth) |
HID (custom usage page) |
None (Win HID API) |
Universal |
| Removable flash drive, data logger |
MSC |
None |
Universal |
| USB microphone or speaker |
UAC 1.0 / UAC 2.0 |
None |
Universal (UAC2: Win10+) |
| Webcam or thermal camera |
UVC |
None |
Win7+, Linux, macOS |
| Firmware update via USB |
DFU (+ application class) |
None / WinUSB via dfu-util |
Universal with dfu-util |
| Custom instrument / proprietary protocol |
Vendor (0xFF) + WinUSB |
WinUSB (auto via MS OS 2.0) |
Win8.1+, Linux, macOS |
| USB tethering / network adapter |
CDC-NCM or CDC-ECM |
None (built-in NDIS/usbnet) |
Win10+, Linux, macOS |
| CDC + HID composite |
CDC-ACM + HID, IAD for CDC |
None |
Universal |
| CDC + MSC composite |
CDC-ACM + MSC, IAD for CDC |
None |
Universal |
Practical Exercises
These exercises are structured in three tiers to match your current skill level and available hardware.
Tier 1 — Conceptual (No Hardware Required)
- Class Code Identification: Plug a USB keyboard, a USB flash drive, and a USB-to-serial adapter into a Linux machine. Run
lsusb -v and record the bInterfaceClass, bInterfaceSubClass, and bInterfaceProtocol values for each device. Verify them against the tables in this article.
- Descriptor Anatomy: Download a USB descriptor capture from USBlyzer or USB View on Windows for a known device (e.g., a cheap webcam). Identify the IAD, VideoControl interface, and VideoStreaming interface. Count the Format and Frame descriptors.
- Class Selection Exercise: For each of the following products, identify the correct USB class and justify your choice: (a) a USB foot pedal for Morse code input, (b) a USB-connected CO2 sensor that logs data to a file, (c) a USB firmware programmer for ATmega chips, (d) a USB LED matrix display.
Tier 2 — Firmware (Hardware Required)
- CDC + HID Composite: Using TinyUSB on an STM32F4 or RP2040, implement a composite device with both CDC-ACM (virtual serial port) and HID (custom 64-byte IN/OUT report). Verify that both functions enumerate correctly simultaneously. Check that the device descriptor uses
bDeviceClass = 0xEF with the IAD wrapping the CDC interfaces.
- DFU Bootloader Integration: Take an existing TinyUSB CDC application on STM32. Add a DFU interface to the descriptor set. Implement the DFU state machine so that receiving a DFU_DETACH request triggers a software reset to System Memory (STM32 ROM DFU bootloader). Verify with
dfu-util --list.
- Vendor Class with WinUSB: Implement a vendor-specific USB device on an STM32 with two Bulk endpoints. Add the BOS descriptor with the Microsoft OS 2.0 Platform Capability Descriptor to enable auto WinUSB installation. On the host, write a Python script using
pyusb to send and receive 512-byte test packets. Measure throughput.
Tier 3 — Advanced
- UAC 1.0 Microphone: Implement a USB microphone using a PDM MEMS microphone (e.g., MP34DT01) connected to an STM32F4 SAI or DFSDM peripheral. Configure UAC 1.0 with a 48 kHz, 16-bit, mono isochronous streaming interface. Verify the device appears in Windows Sound settings as an input device and that captured audio is intelligible.
- MSC RAM Disk with FatFS: Implement a TinyUSB MSC device that exposes a 128 KB RAM disk. Use FatFS to format it as FAT12 on first boot. Populate a
README.TXT file. The device should appear as a removable drive containing the pre-populated file. Implement read and write so file changes from the host side persist in RAM during the USB connection.
USB Class Selection Tool
Document your USB class selection decisions — primary class, secondary class for composite devices, class code rationale, and initial descriptor notes. Download as Word, Excel, PDF, or PPTX for design review or project documentation.
Conclusion & Next Steps
You have now surveyed the complete landscape of USB device classes. The key takeaways to carry forward:
- Class codes live in interface descriptors for standard classes (CDC, HID, MSC, Audio, Video). The Device Descriptor's class fields are only non-zero for specific scenarios (Miscellaneous when IAD is present, or legacy CDC-only devices).
- CDC-ACM is your workhorse class for serial communication — requires two interfaces, a chain of functional descriptors, and a notification endpoint. No driver installation needed on modern operating systems.
- HID is uniquely powerful because its Report Descriptor makes it self-describing — the OS needs no class-specific knowledge beyond the HID spec to understand any HID device. No driver installation on any OS.
- MSC looks simple but hides SCSI complexity. You must implement a minimum subset of SCSI commands and present a FAT-formatted medium.
- DFU should be on every serious embedded USB device — it's the standardised, reliable firmware update mechanism. The STM32 ROM bootloader provides it for free.
- Vendor-specific class plus the Microsoft OS 2.0 Descriptor gives you driver-free Windows access for custom protocols.
- IAD is required whenever CDC is part of a composite device. Set Device Descriptor class to 0xEF/0x02/0x01 and place IADs before the grouped interface descriptors.
Next in the Series
In Part 5: TinyUSB Deep Dive, we move from theory to implementation. You will understand TinyUSB's layered architecture, master its tusb_config.h configuration macros, implement the complete task loop and device callbacks, handle CDC and HID class callbacks with working code examples, navigate the memory placement requirements for DMA-capable SRAM, and use TinyUSB's debug output to diagnose enumeration failures before they become hours of debugging.
Related Articles in This Series
Part 3: Protocol & Enumeration
The complete USB enumeration sequence, USB packet structure, the descriptor hierarchy, and control transfer protocol that drives device configuration.
Read Article
Part 5: TinyUSB Deep Dive
TinyUSB architecture, initialization sequence, callback model, memory management, and porting to a new MCU target.
Read Article
Part 9: Composite Devices
Combining CDC, HID, and MSC into a single USB device — IAD placement, descriptor concatenation, and TinyUSB composite configuration.
Read Article