USB Device Example (CDC + Vendor/WebUSB + MSC, with the standard espp USB services)
The reference espp::UsbDevice example: one composite USB device with the
three interface classes the component provides, and the standard espp USB
services on every framed link, so the hosted web consoles, the Device Hub and
the espp_ota / espp_coredump command-line tools all work against it.
Interface |
What it carries |
Talk to it with |
|---|---|---|
CDC-ACM (a serial port) |
the espp framed protocol ( |
the hosted consoles over Web Serial; the |
vendor-specific (class 0xFF, WebUSB) |
the same framed protocol over bulk IN/OUT |
the hosted consoles over WebUSB (the BOS landing page points at the system console); the CLIs over libusb |
MSC (a USB drive) |
a wear-levelled FAT partition in flash ( |
mount it like any removable drive; eject it to hand it back to the firmware |
The device enumerates as VID 0x1209 / PID 0x0d38 (manufacturer “espp”,
product “espp USB Device”); the PID is distinct from the other espp examples so a
host-side filter can be specific. The log console is on UART0 (USB-Serial-JTAG
as the early-boot secondary): on the ESP32-S3 the USB-Serial-JTAG controller and
USB-OTG share the same native USB PHY, so the console cannot stay there once
TinyUSB owns the port.
Table of Contents
Standard USB services
Both framed links (CDC and vendor) serve the same set, each service registered on each link’s dispatcher and advertised through capability discovery, so the Device Hub lists them and every console finds its module by protocol id (the module ids below are the published defaults; hosts do not depend on them):
Service |
Protocol id |
Module |
Console / tool |
|---|---|---|---|
|
|
7 |
|
|
|
8 |
system console |
|
|
0 |
OTA console, |
|
|
4 |
coredump console, |
Reboot requests go through the example’s on_reboot_request callback, which
logs and permits them; an application would refuse or defer one while, say, the
host is writing to the drive.
The USB drive
The MSC function exposes the storage partition (a data, fat partition in
partitions.csv, accessed through wear levelling) as a removable drive named
ESPP USB. Ownership follows the usb_device MSC model: the firmware owns the
volume first (it formats it if needed and writes README.txt), the host takes
it when it mounts the device, and ejecting the drive on the host gives it back
to the firmware (logged by the heartbeat). The firmware and the host never
write the volume at the same time.
Partition layout
partitions.csv (4 MB flash): nvs, otadata, phy_init, two 1536K app slots
ota_0 / ota_1 (idf.py flash writes ota_0, each OTA update alternates to
the other slot), a 64K coredump partition and the 896K storage FAT volume.
Build, flash, run
cd components/usb_device/example
idf.py set-target esp32s3
idf.py build flash monitor # console is on UART0 (USB-UART adapter)
Then connect the native USB port: the host sees a serial port, a WebUSB interface and a drive. Open the system console (WebUSB or Web Serial) for the device info, the reboot buttons and the live heap / task view, the OTA and coredump consoles for updates and crash dumps, and mount the drive to read the README the firmware wrote.
How it works
espp::UsbDeviceinstalls the TinyUSB driver and builds the descriptors for the enabled CDC + vendor + MSC functions, allocating interfaces / endpoints sequentially; the vendor function advertises WebUSB + MS OS 2.0 descriptors so a browser (and Windows, via WinUSB) can bind it driverlessly.Each framed link has its own
espp::DispatcherWorker(a bounded receive queueworker task feeding one
espp::Dispatcher), fed from the TinyUSB receive callbacks; the four services are registered on each worker andserve_discovery()answers the hub’s query. Every device->host write on a transport goes through one application-level mutex, so a streamed monitor event and a reply from another service never interleave.
The OTA and core-dump engines (
espp::Ota,espp::CoreDump) are shared by the per-link services; an RX overflow on a link aborts an OTA transfer that link owned and tells the host.The MSC medium is configured with the application as the initial owner and
connect_on_initialize = false, so the README is written before the device presents itself to the host;auto_handoverthen moves the drive to the host on mount and back on eject.