日本語版: usb-host-guide.ja.md
How to get a USB device working on an ESP32. The first half covers USB Host itself, the second half covers what is specific to the ESP32 series and how to actually run the experiments.
The deeper material -- descriptor byte layouts, channels and the FIFO split, error recovery, throughput design, callback context, and implementing your own class -- is in the advanced guide.
It is written so that someone who has never touched USB can follow it, but the goal is that you can diagnose "why doesn't this work" yourself, so the mechanics are not skipped. If you are in a hurry, start at Running the experiments and come back when a term is unfamiliar.
- USB Host fundamentals
- Power and hubs
- ESP32-specific limits
- Running the experiments
- Working out a protocol
- Troubleshooting
- Tool index
- References
USB is not a peer-to-peer bus. There is one host and the devices hanging off it, and every transfer is started by the host. A device cannot send data on its own; it answers when the host asks.
A PC is the host; a keyboard or a flash drive is a device. With this library the ESP32 takes the PC's role: it is the host.
The first thing that confuses people is the USB connector on the ESP32 board. Two physically identical USB-C connectors can have completely different roles:
- The connector you plug into a PC — the ESP32 is the device here; used for flashing and the serial monitor.
- The connector you plug USB devices into — the ESP32 is the host here; this is the one this library uses.
Which is which differs per board, and the silkscreen (USB, OTG, UART) is not reliable. Check the schematic.
A standard USB 2.0 cable has four wires.
| Wire | Role |
|---|---|
| VBUS | +5 V power. The host supplies it to the device |
| D+ / D- | The differential data pair |
| GND | Ground |
Being the host also means being the power source. This is where embedded boards trip people up first: if the board's connector does not put 5 V on VBUS, no amount of correct wiring will start the device (chapter 2).
Charge-only cables have no D+/D-. When a device is not detected at all, suspect the cable first.
| Speed | Short | Rate | Typical use |
|---|---|---|---|
| Low Speed | LS | 1.5 Mbps | Older mice and keyboards |
| Full Speed | FS | 12 Mbps | Keyboards, mice, serial bridges, MIDI, most small devices |
| High Speed | HS | 480 Mbps | Flash drives, cameras, audio interfaces, Ethernet adapters |
| SuperSpeed | SS | 5 Gbps and up | USB 3.x. Not usable on ESP32 |
Most USB 3.x devices fall back to USB 2.0, but that is up to the device. SS-only devices cannot be used.
The device decides the speed; the host does not choose it — but a device whose speed the host does not implement will not work. The ESP32-S2/S3 host is full speed only; the ESP32-P4 has both a full-speed and a high-speed port (chapter 3).
Between plugging a device in and the application being able to use it, the host goes through these steps. Knowing them lets you read where in the log it failed.
- Attach detection — the D+/D- pull-up tells the host that a device is present, and at what speed
- Reset — the bus is reset
- Address assignment — the device is given an address between 1 and 127
- Device descriptor — VID/PID, USB version, class
- Configuration descriptor — interfaces and endpoints, read as one block
- Set configuration — normally the first one
- Interface claim — the driver (this library) claims the interfaces it handles and opens their endpoints
usb.onDeviceConnected() fires after step 7. If you never get there, the problem is electrical (1–2), a descriptor that cannot be read or is too long (4–5), or a resource shortage (7).
A device declares what it is through descriptors:
Device (the whole device: VID/PID, EP0 size)
└── Configuration (current draw, bus- or self-powered)
├── Interface 0 (class / subclass / protocol <- "what it is" lives here)
│ ├── class-specific descriptors (HID descriptor, CDC functional descriptors, ...)
│ ├── Endpoint 0x81 (IN, interrupt, max packet 8, interval 10)
│ └── Endpoint 0x01 (OUT, ...)
└── Interface 1
└── ...
Three things matter here:
- The class belongs to the interface, not the device. The device descriptor's class is often
0x00("see the interfaces"), so you have to look at the interfaces to know what a device really is. - One device can have several interfaces (a composite device). Keyboard plus mouse, or keyboard plus a vendor-specific function, is entirely normal. An Interface Association Descriptor (
bDescriptorType=0x0b) may group several interfaces into one function. - Class-specific descriptors do not appear in a parsed dump. The HID descriptor's report-descriptor reference, CDC functional descriptors, the CCID class descriptor and so on require walking the raw configuration descriptor bytes.
EspUsbHostDeviceExplorerdoes that.
An endpoint is a communication port. It has an address (0x81: bit 7 is the direction, the low 4 bits are the number), a direction (IN = device to host, OUT = host to device), a transfer type, a max packet size (MPS), and a polling interval.
| Transfer type | Character | Used by |
|---|---|---|
| Control | Request/response on EP0. Every device has it | Enumeration, class requests, HID SET_REPORT |
| Interrupt | Polled by the host at a fixed interval. Small and low-latency | Keyboards, mice, CCID status notifications |
| Bulk | Bulk data. No bandwidth guarantee, errors are retried | Flash drives, printers, serial bridges, vendor protocols |
| Isochronous | Reserved bandwidth, no retries | Audio, video |
Directions are from the host's point of view: "IN" means data coming from the device.
Each endpoint consumes a hardware resource (a channel) on the host side, which matters on the ESP32 (3.4).
USB defines standard classes; devices with the same class code are driven the same way.
| Class | Code | How this library handles it |
|---|---|---|
| Audio / MIDI | 0x01 |
Library API (Audio / MIDI) |
| CDC (serial, Ethernet) | 0x02 / 0x0a |
Library API (Serial / UsbNetwork) |
| HID | 0x03 |
Library API (HID) |
| Printer | 0x07 |
Example (EspUsbHostPrinterEscPos) |
| Mass Storage | 0x08 |
Library API (Storage) |
| Hub | 0x09 |
Library API |
| Smart Card (CCID) | 0x0b |
Library API (Ccid) |
| Video (UVC) | 0x0e |
Not supported (3.5) |
| Application Specific (USBTMC, DFU) | 0xfe |
Example (EspUsbHostUsbtmcScpi) |
| Vendor Specific | 0xff |
Library API (Vendor) |
The thing every beginner hits is that two products with the same purpose can look completely different on USB:
| Product | What it commonly looks like |
|---|---|
| Barcode scanner | HID keyboard (scans arrive as keystrokes) / CDC serial / vendor HID reports. Often switchable by model or by scanning a configuration barcode |
| Receipt printer | Printer class 0x07 / CDC serial 0x02 / vendor-specific 0xff. Some models switch with a DIP switch or a config tool |
| USB serial bridge | CDC-ACM 0x02 / vendor-specific 0xff (FTDI, CP210x, CH34x are the latter) |
| Test instrument | USBTMC 0xfe/0x03 / CDC serial / vendor-specific |
| Gamepad | HID / vendor-specific (XInput family) |
| Power supply, measurement module | A private protocol carried inside HID reports (e.g. DP100) |
So there is no such feature as "barcode scanner support" — only "support for how that barcode scanner presents itself". This is why the first step is always to find out what the device in your hand looks like (chapter 4).
A device that looks like a keyboard works straight away with onKeyboard(). That is by far the easiest case, so if your device can be switched into HID-keyboard or CDC mode, it is worth checking before writing any code.
Most USB Host trouble is power, not software.
Check this first. Some boards do not put 5 V on the host connector at all.
- Espressif's official ESP32-S3-DevKitC-1 does not power an attached device through its USB OTG connector. Either wire a separate supply to the device or use a self-powered hub.
- Some M5Stack products can switch USB connector power in software. Check the schematic and procedure for your product.
- Boards such as the Freenove ESP32-S3-WROOM Board can power a device from the OTG Type-C connector. For a straightforward start, use a board like this.
Measuring VBUS to GND with a meter is the reliable check. If it reads 0 V, nothing you do in firmware will help on that connector.
During development, prefer a board with two USB connectors so that flashing/serial monitor (USB-UART) and the host port (OTG) stay separate. The finished product can use a single-connector board such as an AtomS3.
bMaxPower in the configuration descriptor is what the device declares it draws from the bus (in 2 mA units). Actual peaks during operation can be much higher.
Typical symptoms of insufficient current:
- Nothing enumerates
- Enumeration works, but the device drops the moment it is loaded (writing, printing, a motor starting)
- The ESP32 itself resets (
Brownout detector was triggered)
The fix is a self-powered (mains-powered) USB hub. A bus-powered hub only splits the ESP32's own supply and solves nothing. 2.5-inch hard drives, printers, wireless adapters and USB displays deserve particular care.
A hub is needed for several devices at once. In practice:
- Use a self-powered USB 2.0 hub. USB 3.x hubs and products with internally cascaded hubs behave differently and are not fully validated.
- A hub itself consumes channels and an address (3.4).
- Some hub/device pairs are known to crash the ESP-IDF hub driver. See "Hub combinations that crash ESP-IDF" in tests/manual/README.md.
- With a per-port-power-switching (PPPS) hub,
EspUsbHostHubPPPScan power a device down and force re-enumeration, which is useful while debugging.
| Chip | Host capability |
|---|---|
| ESP32-S2 | One FS host. Little internal RAM, so ESP_USB_HOST_MAX_DEVICES defaults to 3 |
| ESP32-S3 | One FS host. The primary target of this library |
| ESP32-P4 | FS OTG and HS OTG — but only one of them can be the host at a time |
| ESP32 (original), C3, C6, ... | No USB OTG, or no host capability. This library does not run on them |
On ESP32-P4, EspUsbHostConfig::port selects ESP_USB_HOST_PORT_FULL_SPEED or ESP_USB_HOST_PORT_HIGH_SPEED. The ESP-IDF host stack can drive only one host peripheral, so FS and HS cannot both be hosts.
The P4's FS port shares its PHY with USB Serial/JTAG and is not fixed to one pin pair. See the ESP32-P4 notes in README.md and EspUsbHostP4FsPhyRouting.
This is the choice that decides what you can actually plug in.
With the FS port
- Every attached device runs at full (or low) speed.
- Several devices through a hub work. Keyboard + mouse + serial belongs here.
- Some devices do not work at full speed at all: HS-only devices, and devices with no full-speed configuration — an endpoint with a 1024-byte max packet size (which only exists at high speed), or a UAC2 audio device with no FS configuration. A concrete case: the Mirabox N3 family macro pad has a 1024-byte interrupt OUT endpoint, so it works only on the P4's HS port.
With the HS port
- HS devices run at their real speed. Measured bulk OUT throughput is about 36 MB/s against about 1.1 MB/s at full speed (
vendor_bulk_throughput). - Hubs are effectively unusable. The USB 2.0 specification allows FS/LS devices below a high-speed hub through its Transaction Translator, but that path is not usable in the current ESP-IDF environment, so a hub on the HS port does not work in practice.
- Consequently an HS device and an FS keyboard cannot be used at the same time. Treat the HS port as one directly attached device.
So the choice is speed versus simultaneous devices. When in doubt, try the FS port first and move only the devices that need it — those that do not work at FS, or that need the bandwidth — to the HS port.
The ESP32-S2/S3 have no HS port, so the choice does not exist there. A fast device that will not work on an S3 may still work on a P4's HS port.
This library runs on the precompiled ESP-IDF USB Host stack shipped by Arduino-ESP32. That build's configuration (sdkconfig) cannot be changed from a sketch or from the library. This is why some things are simply unavailable in the Arduino environment.
The control transfer size limit below is the main example, and it is the direct reason USB cameras cannot be used.
The ESP32-S3 USB host has 8 channels (OTG_NUM_HOST_CHAN). EP0, every claimed endpoint, and hubs all consume them.
Composite devices, hubs, audio, MSC and several serial devices exhaust them quickly. One recorded example: through a hub, FTDI + CP210x works, while FTDI + CH34x fails on channel exhaustion (tests/manual/README.md).
Any of these lines in the log means you ran out:
No more HCD channels available
EP Alloc error: ESP_ERR_NOT_SUPPORTED
Claiming interface error: ESP_ERR_NOT_SUPPORTED
usb.endpointChannelCount(), usb.maxEndpointChannelCount() and usb.estimatedHcdChannelCount() report usage; EspUsbHostBringUpCheck and EspUsbHostDeviceInfo print it.
Arduino-ESP32's precompiled host stack is built with CONFIG_USB_HOST_CONTROL_TRANSFER_MAX_SIZE=256. One control transfer carries at most 256 bytes including the 8-byte setup packet. Two consequences:
- A device whose configuration descriptor exceeds 256 bytes in total fails to enumerate, before any class driver runs. This is why USB cameras (UVC) do not work — a Logitech C920's configuration descriptor is 1,974 bytes. No sketch or library option can work around it; it would take a change on the Arduino-ESP32 side.
- One GET_DESCRIPTOR can read at most 248 bytes. Tools that dump raw descriptor bytes stop there.
The ESP32-S2 has much less internal RAM, so ESP_USB_HOST_MAX_DEVICES defaults to 3 (8 on the S3). Raise it with -DESP_USB_HOST_MAX_DEVICES=N and watch for dram0_0_seg overflowed. Larger examples (such as UsbNetwork) do not fit on an S2.
- P4 cache coherency — on the P4, the DMA memory used for USB transfers is cached. The library writes those lines back before every IN submit, so no application-side workaround is needed (verified by
msc_cache_coherency). - Core version — ESP32-S2/S3 need arduino-esp32 3.2.0 or later, ESP32-P4 needs 3.3.1 or later.
- Log level — the reason enumeration failed usually appears only in the ESP-IDF log, so set Core Debug Level to
Verbosewhile investigating.
The route from an unknown device to working code. Go top to bottom without skipping — most wasted time comes from debugging a later step while an earlier one (power, connector) is still broken.
- Measure 5 V on VBUS of the host connector
- Confirm the cable carries data (not charge-only)
- Confirm the device works when plugged into a PC
Flash examples/Info/EspUsbHostBringUpCheck.
- Does
usb.begin(): okappear? If not, it is a build / target / core version problem - Does
ENUMERATEDappear when you plug the device in? If not, it is power, connector, cable, or the device itself — the sketch prints the checklist - Note the speed (FS/HS/LS) and the VID:PID
Try a plain USB keyboard or flash drive first. If that enumerates, the board is fine and the problem is specific to your device.
Flash examples/Info/EspUsbHostDeviceExplorer. It prints:
- The parsed device / interface / endpoint state
- Per interface: what it is, and which API or example drives it
- The raw device and configuration descriptor bytes, plus a block-by-block walk
What to look for:
- How many interfaces there are, and each one's class / subclass / protocol
- Endpoint types (interrupt / bulk / isochronous), directions and max packet sizes
- Which interfaces the library claimed (
claimed=yes) - Any unfamiliar class-specific descriptors (
CS_INTERFACEand friends)
The developer-side equivalents are tests/manual/device_dump and tests/manual/raw_descriptor, run from pytest so the output is logged:
cd tests
uv run --env-file .env pytest manual/device_dump/device_dump.py -v -s
uv run --env-file .env pytest manual/raw_descriptor/raw_descriptor.py -v -sCapturing lsusb -v -d <vid>:<pid> (Linux) or USB Tree Viewer (Windows) output on a PC as well makes later comparison easier.
Once you know the class, flash the matching example as-is (the table in 1.7). Most devices work at this point.
- HID →
EspUsbHostHIDRawDumpfor raw reports; keyboard/mouse have dedicated examples - CDC / VCP →
EspUsbHostUSBSerial - MSC →
EspUsbHostMSCFatList - CCID →
EspUsbHostCcidReader - MIDI / Audio → MIDI / Audio
A HID device declares the meaning of its data in a report descriptor. EspUsbHostHIDReportDescriptor fetches and decodes it.
For gamepads and custom devices, the Usage Page / Usage / Report Size / Report Count items tell you which bits of an input report mean what. A vendor-defined usage page (0xff00 and above) means the report content is a private protocol. A private protocol wrapped in HID is common (the DP100 and the macro pad both work this way). In that case continue with Step 5.
Once you know it is a private protocol, look for existing work before analysing it yourself. Most devices have already been worked out by someone.
- Vendor protocol documents, SDKs, API documentation
- Linux kernel drivers (
drivers/usb/,drivers/hid/,drivers/net/usb/). Working code is a specification - libusb-based open source projects, Python implementations,
sigrokprotocol decoders - Libraries for other microcontrollers that target the same device
- Industry-standard protocols such as ESC/POS, SCPI, CCID and ADB. The class may be
0xffwhile the payload is standard
The examples in this repository are built exactly that way: open the interface with the generic API, then speak a documented protocol over it — printers (ESC/POS), instruments (USBTMC + SCPI), Android (ADB), DisplayLink, AX206. The protocol notes live under docs/ (printer-spec.ja.md, usbtmc-spec.ja.md, ccid-api-spec.ja.md, dp100-spec.ja.md, ...).
When nothing is published, or what you find is incomplete, capture what the vendor's own software says to the device on a PC and read it back out. The procedure is in chapter 5.
Replay the transfers you read out of the capture with examples/Vendor/EspUsbHostProtocolConsole. One transfer at a time, with no rebuild in between, which keeps the analysis cycle fast:
> ctl 80 06 0100 0000 12 # GET_DESCRIPTOR(DEVICE) first, to confirm the path
> open 0 # claim interface 0
> out 10 04 01 # send the command seen in the capture
> in 40 # read the answer
When the answer matches the capture, that exchange is understood. Failures are informative too — see Reading failures.
Turn the console session into vendorOpen() / vendorWrite() / vendorReadSync() / vendorControlTransfer() calls. EspUsbHostVendorBulk is the minimal skeleton.
Things to get right:
- Reproduce the init sequence completely, including order and delays. When it does not work, look for a request in the capture that you skipped.
- ZLP (zero-length packet) — when a transfer length is a multiple of the max packet size, some protocols need a zero-length packet to terminate it (
vendorWriteZlp()/vendorSetAutoZlp()). - Read mode — open a request/response device with
ESP_USB_HOST_VENDOR_READ_ON_DEMANDand read after writing. A device that streams unprompted wantsCONTINUOUS. - Max packet size and splitting — large payloads may have to be split by the caller.
Once it is reproduced, record it in the following form — it helps both your future self and others.
- Put a minimal working sketch under
examples/(in this repository, with README.md, README.ja.md and sketch.yaml) - Add anything that needs a human to judge it as a manual test under
tests/manual/ - Write the protocol findings into
docs/<device>-spec.ja.md - Record the exact VID:PID you verified, and any model-specific behaviour
Windows: USBPcap + Wireshark
- Install USBPcap (the Wireshark installer may include it)
- Start Wireshark and pick a
USBPcap1-style interface. These are per root hub, so check which hub the device is on - Start the capture before plugging the device in, so the whole enumeration is recorded
- Start the vendor software and perform exactly one operation
- Stop and save
Linux: usbmon
sudo modprobe usbmon
sudo wireshark # pick a usbmonX interface
# or as text
sudo cat /sys/kernel/debug/usb/usbmon/0uFor descriptors alone, lsusb -v -d <vid>:<pid> is quicker, and usbhid-dump gives HID report descriptors.
macOS: USB Prober from Xcode's Additional Tools, or Wireshark's USB capture.
The practical order to work through a capture in Wireshark:
- Filter down to the device —
usb.device_address == 5. The address is visible in the enumeration packets - Separate enumeration from operation — the first GET_DESCRIPTOR burst is enumeration; the protocol starts after it
- Work by transfer type
URB_CONTROL— notebmRequestType/bRequest/wValue/wIndex/wLengthfrom the setup packet. Those are exactly the arguments of the console'sctlcommandURB_BULK/URB_INTERRUPT— the payloads. Follow OUT (host to device) and IN (device to host) as pairs
- Spot the repetition — a request that repeats on a timer is polling, and can be left for later
The most effective way to read an unknown protocol is to compare two captures that differ in exactly one thing.
- Set brightness to 10 in the vendor app, capture; set it to 11, capture → the byte that changed is the brightness
- Idle capture versus one button press → the difference is the event
- Output on versus output off → the state bit
That is how you pin down the frame layout (header, command id, length, payload, checksum) one byte at a time.
Common clues:
- Fixed leading bytes (a magic number)
- A length field (look for a value that matches the payload length)
- A sequence number (increments by one)
- A trailing checksum (a plain sum, XOR, or CRC-16/MODBUS are common — see the DP100 notes)
- Little-endian 16/32-bit values (all standard USB fields are little-endian)
- Keep to analysing hardware you own, for your own use. Do not do anything that breaks software licence terms or local law.
- Captures can contain what you typed or scanned (card IDs, entered text). Check the content before sharing a capture.
- Be careful with write commands. An unverified command can corrupt settings or brick firmware. Establish the read side first, and only send writes whose meaning you are sure of — especially where the effect is physical, such as switching a power supply's output on.
| Symptom | Likely cause | Check / fix |
|---|---|---|
usb.begin() fails |
Target has no USB host, or the core is too old | Is it an S2/S3/P4? Is arduino-esp32 3.2.0+ (P4: 3.3.1+)? |
| Nothing happens on plug-in | No VBUS | Measure it. Add a self-powered hub |
| Nothing happens on plug-in | Wrong connector (plugged into the UART side) | Check the schematic; use BringUpCheck |
| Nothing happens on plug-in | Charge-only cable | Use a data cable |
| Only one device never enumerates | Configuration descriptor over 256 bytes | Check the verbose log. UVC cameras cannot be supported (3.5) |
| Only one device never enumerates | The device does not work at full speed | Try the P4 HS port (3.2) |
| Drops out under load | Not enough current | Self-powered hub, external supply |
| The ESP32 reboots | Brownout from current draw | As above; look for Brownout detector in the log |
| Fails with several devices | Out of channels | Look for No more HCD channels available; use fewer devices (3.4) |
| Hub does not work on the HS port | Current limitation | Use one device directly on HS; use the FS port if you need a hub (3.2) |
Enumerates but supported=no |
No class driver in the library for it | Inspect it with DeviceExplorer and drive it with the vendor API |
vendorOpen() fails |
The library already claimed that interface | Check claimed; name a different interface number |
| Bulk IN times out | Polling a request/response device | Open with ESP_USB_HOST_VENDOR_READ_ON_DEMAND, write first, then read |
| Transfers stall part way | The protocol needs a ZLP | vendorWriteZlp() / vendorSetAutoZlp() |
| Reboot loop when a hub is attached | A known hub/device combination | See the table in tests/manual/README.md |
| No idea what is happening | The ESP-IDF log is not visible | Reproduce with Core Debug Level set to Verbose |
| Tool | Purpose |
|---|---|
Info/EspUsbHostBringUpCheck |
Run this first. Does the host start, does the device enumerate, at what speed — with a checklist when nothing does |
Info/EspUsbHostDeviceExplorer |
Run this second. The whole layout, per-interface instructions on how to drive it, raw descriptors and a block walk |
Info/EspUsbHostDeviceInfo |
Periodic dump of every connected device, hub info and channel usage |
Info/EspUsbHostHIDReportDescriptor |
Fetch and decode HID report descriptors |
HID/EspUsbHostHIDRawDump |
Raw HID input report dump |
Vendor/EspUsbHostProtocolConsole |
For protocol work. Type control/bulk transfers from the serial monitor and watch the answers |
Vendor/EspUsbHostVendorBulk |
The skeleton to turn your findings into a sketch |
Info/EspUsbHostHubPPPS |
Power a hub port off and on to force re-enumeration |
Info/EspUsbHostP4FsPhyRouting |
Route the ESP32-P4 FS OTG to the GPIO24/25 connector |
Run from pytest so the output is logged. See tests/manual/README.md for how to run them.
| Tool | Purpose |
|---|---|
smoke |
Checks the manual-test workflow itself. Run it first on a new machine |
device_dump |
Parsed dump of every device, plus the bulk/interrupt endpoints of unclaimed interfaces |
raw_descriptor |
Raw DEVICE/CONFIGURATION descriptors and a block walk, for comparing against USBPcap |
hid_report_descriptor |
HID report descriptor retrieval |
hotplug |
Survives repeated connect/disconnect cycles |
hub_info / hub_power |
Hub topology and per-port power control |
vendor_bulk_throughput |
Effective bulk OUT throughput — the practical ceiling of the board |
| others | Per-class and per-device manual tests (catalog) |
- USB 2.0 Specification
- USB Class Codes
- USB Device Class Documents — HID, CDC, MSC, CCID, UAC and the rest
- USB Made Simple — a readable introduction
- USBPcap / Wireshark — capturing on Windows
- Linux USB project — kernel drivers as de-facto specifications
- ESP-IDF USB Host Library — what this library is built on
- Tested Devices and Boards — verified units with their VID:PIDs and conditions, and board notes
- README.md in this repository — API reference and per-class status
- docs/ — per-device protocol notes