An USB/IP server implementation for ESP32-S3 and ESP32-P4 based on usbipdcpp library. This project serves as a practical example of using usbipdcpp on ESP32 platforms.
Note: USB device compatibility depends on the ESP32 chip's USB PHY speed support:
- ESP32-S3: Supports Full Speed (12Mbps) and Low Speed (1.5Mbps) devices
- ESP32-P4: Supports High Speed (480Mbps), Full Speed (12Mbps) and Low Speed (1.5Mbps) devices
High Speed devices (480Mbps) may not work correctly on ESP32-S3 due to PHY limitations.
English | δΈζ
- π Transparent USB Forwarding - Export local USB devices to remote machines via USB/IP protocol
- π Hot-plug Support - Automatic device detection, enumeration, and cleanup on removal
- π Multi-device Support - USB hubs supported, multiple devices can be exported simultaneously
- β‘ Zero-Copy Performance - Direct DMA buffer access eliminates data copying overhead, achieving optimal throughput
- π‘οΈ Robust Connection Handling - Automatic cleanup when devices are unplugged during active sessions
- π₯οΈ Runtime WiFi Configuration - Change WiFi over a web UI or a dedicated serial console; credentials persist in NVS across reboots β no recompilation needed
- π Device Status Panel - Web UI and serial console list attached devices (busid, VID:PID, remote-client usage)
- ESP32-S3 or ESP32-P4 development board (USB OTG supported)
- USB devices (keyboards, mice, mass storage, etc.)
- USB hub (optional, for multiple devices)
One Board, Two Roles: Flash this firmware onto your ESP32-S3 dev board and it becomes a USB/IP dongle; flash something else and it's back to being a regular dev board. No hardware modifications needed β just swap the firmware whenever you switch projects.
USB OTG Power: Common ESP32-S3 DevKitC boards and their compatible counterparts have a USB OTG power solder pad on the back. Bridging this pad allows the board to supply power to connected USB devices. If you prefer not to modify the hardware, you can use a self-powered USB hub (one with an external power supply) instead β the hub will power the devices while the ESP32 handles data.
Speed Compatibility: Ensure your ESP32 chip's USB PHY supports the USB speed type of your device. For example, High Speed UVC webcams require ESP32-P4.
Flash Size: Default configuration assumes ESP32-S3 (8MB flash) and ESP32-P4 (32MB flash). Modify via
idf.py menuconfigβSerial flasher configβFlash sizeif needed.
- ESP-IDF v5.5
- Linux machine with
usbiptools installed (client side)
git clone --recursive https://github.com/yunsmall/usbipdcpp_esp32.git
cd usbipdcpp_esp32The checked-in per-target configs (sdkconfig.defaults.esp32s3 / esp32p4) intentionally contain no WiFi credentials β a fresh build boots without network. Provide credentials one of two ways:
Option A β compile-time default (connects on first boot):
idf.py menuconfigNavigate to Usbipdcpp WiFi Configuration and set Usbipd WiFi SSID / Usbipd WiFi Password.
Option B β configure at runtime after flashing via the web UI or the dedicated serial console (no recompile needed, credentials persist in NVS across reboots). See the Management & Configuration section below.
idf.py build flash monitorFor specific chip targets, use the appropriate sdkconfig defaults:
# For ESP32-S3
idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults.esp32s3" build flash monitor
# For ESP32-P4
idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults.esp32p4" build flash monitorIf the default sdkconfig doesn't take effect, explicitly specify it with
-DSDKCONFIG_DEFAULTS.
On your Linux machine:
# Load USB/IP kernel modules
sudo modprobe vhci-hcd
# List available devices
sudo usbip list -r <ESP32_IP>
# Attach to a device
sudo usbip attach -r <ESP32_IP> -b <BUSID>WiFi and device status are manageable at runtime. WiFi credentials are stored in NVS (namespace wifi) and applied automatically on every boot β recompiling to switch networks is never required.
Open http://<ESP32_IP>/ in a browser (HTTP port 80):
- USB devices card β attached devices with
busid,VID:PIDand usage state (idle / in use by a remote client). Auto-refreshes every 5 seconds. - WiFi card (collapsed by default) β current SSID/IP, change credentials (the device disconnects and reconnects to the new AP), plus a wiring hint for the fallback serial console. Empty password = open network.
REST API: GET /api/status (connection state + config-port GPIOs), GET /api/devices (device list), POST /api/wifi (form-urlencoded ssid=..&password=..).
If the network is unreachable, configure through the dedicated config UART β the pins differ per chip (Kconfig: USBIPD_CFG_UART_TX_GPIO / USBIPD_CFG_UART_RX_GPIO):
| Chip | Config UART TX | Config UART RX |
|---|---|---|
| ESP32-S3 | GPIO17 | GPIO18 |
| ESP32-P4 | GPIO4 | GPIO5 |
Wire a USB-UART adapter crossed: adapter RX β device TX, adapter TX β device RX, GND common. Open the terminal at 115200 8N1 and use the built-in commands (help lists all):
| Command | Purpose |
|---|---|
wifi_set <ssid> [password] |
Set WiFi and reconnect (saved to NVS; omit password for an open network) |
wifi_show / wifi_reset |
Show current config / clear NVS back to compile-time defaults |
devices |
List attached USB devices (busid / VID:PID / usage state) |
mem |
Print heap usage |
logs |
Mirror the main UART0 log stream to the config port (Ctrl-C to stop) |
about |
What this firmware is and how to manage it |
The mirrored log stream is written with \r\n line endings so it renders correctly on real serial terminals.
The web UI is a single static file (main/web/index.html) embedded at compile time. To iterate on layout/scripts without flashing the firmware: run python main/web/mock_server.py and open http://127.0.0.1:8000. The mock serves fake /api/status and /api/devices responses; edit the top of the script to preview different states (disconnected, disabled config port, long SSIDβ¦).
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Linux Client β
β βββββββββββββββ βββββββββββββββ βββββββββββββββ β
β β App A β β App B β β App C β β
β ββββββββ¬βββββββ ββββββββ¬βββββββ ββββββββ¬βββββββ β
β ββββββββββββββββββββΌβββββββββββββββββββ β
β β β
β βββββββββ΄ββββββββ β
β β usbip-vhci β β
β βββββββββ¬ββββββββ β
ββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββ
β TCP/IP Network
ββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββ
β βββββββββ΄ββββββββ β
β β ESP32-S3 β β
β β USB/IP Server β β
β βββββββββ¬ββββββββ β
β β β
β ββββββββββββββββββββΌβββββββββββββββββββ β
β β β β β
β ββββββ΄βββββ βββββββ΄ββββββ βββββββ΄ββββββ β
β β USB Hub β β USB Dev 1 β ... β USB Dev N β β
β βββββββββββ βββββββββββββ βββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
The USB/IP server core lives in the components/usbipdcpp component (usbipdcpp submodule + the esp32_handler glue layer on top of the IDF USB-host stack). It has no dependency on main/ β everything under main/ is just this firmware's interactive shell (WiFi management, serial console, web UI, status panel). Strip main/ to just the forwarding engine.
Bring the component into your project:
cp -r components/usbipdcpp <your_project>/components/
# the component embeds the usbipdcpp git submodule β init it inside your repo:
git submodule add https://github.com/yunsmall/usbipdcpp <your_project>/components/usbipdcpp/usbipdcppThe component's idf_component_register declares its own requirements (asio spdlog usb pthread lwip sock_utils), so your main only needs PRIV_REQUIRES usbipdcpp asio spdlog. USB devices are accessed through the standard IDF usb component (enable USB_HOST_HUBS_SUPPORTED if you use hubs).
Minimal working integration β no WiFi/console/UI; your application provides the network stack, the component only listens on TCP:
#include <thread>
#include <freertos/FreeRTOS.h>
#include <usb/usb_host.h>
#include "esp32_handler/Esp32Server.h"
// usb_host library-level event loop: needs its own task for the whole lifetime
static void usb_host_event_loop() {
while (true) {
uint32_t event_flags;
ESP_ERROR_CHECK(usb_host_lib_handle_events(portMAX_DELAY, &event_flags));
}
}
extern "C" void app_main() {
// run the main flow in a std::thread: spdlog/asio need a full pthread context
// (see the comments above thread_main in main/esp32_usbipdcpp.cpp)
std::thread main_thread([&] {
const usb_host_config_t host_cfg = {
.skip_phy_setup = false,
.intr_flags = ESP_INTR_FLAG_LEVEL3,
.enum_filter_cb = nullptr,
};
ESP_ERROR_CHECK(usb_host_install(&host_cfg));
std::thread(usb_host_event_loop).detach();
usbipdcpp::Esp32Server server;
server.init_client(); // registers the usb_host client (device hot-plug)
asio::ip::tcp::endpoint ep{asio::ip::tcp::v4(), 3240};
auto ec = server.start(ep); // starts listening + spawns internal threads
if (ec) { /* handle listen failure */ }
while (true) {
std::this_thread::sleep_for(std::chrono::seconds(1));
}
});
main_thread.join();
}Esp32Server::start() spawns the network and client-event threads internally; plugged-in USB devices are enumerated and exported automatically β from a computer run usbip list -r <esp32-ip> then usbip attach -r <esp32-ip> -b <busid>.
Notes:
- Networking is your job β the component does not initialise WiFi/Ethernet or any PHY; write your own network bring-up (STA connection, Ethernet, static/DHCP, whatever your project needs) and only then start the server on it.
- Scope β usbipdcpp covers exactly three things: registering the usb_host client (
Esp32Server::init_client), device hot-plug handling/binding, and the USB/IP protocol sessions over TCP. Everything else stays with your application, includingusb_host_install(shown in the example above) and the USB PHY setup it performs. usb_host_installis process-wide and must be called exactly once, before creatingEsp32Server.Esp32Server::startnever throws; errors come back via the returnederror_code. Device bind failures are logged and rolled back internally.- The
main/files are the reference usage βesp32_usbipdcpp.cpp(thread_main) shows the full sequence, with WiFi management / serial console / web UI as optional extras around the same core.
| Device Type | Status | Notes |
|---|---|---|
| USB Keyboard | β Working | |
| USB Mouse | β Working | |
| USB Flash Drive (MSC) | β Working | Bulk transfer tested |
| USB Audio | π Testing | |
| USB Webcam (UVC) | π Testing | Requires ESP32-P4 for High Speed |
Bulk and interrupt transfers have been verified to work correctly. Ensure your ESP32 chip's USB PHY supports the target device's speed type.
This implementation leverages usbipdcpp's zero-copy architecture for maximum throughput:
- Direct DMA Buffer Access: USB transfer buffers are allocated in DMA-capable memory and accessed directly for network I/O, eliminating intermediate data copies
- RAII Transfer Management:
TransferHandleautomatically manages buffer lifecycle, ensuring proper cleanup without manual memory management - ESP32-Specific Optimizations:
- Bulk/Interrupt IN transfers aligned to endpoint Max Packet Size for hardware efficiency
- Control transfer buffers pre-allocated with setup packet space
- Object pooling for callback structures reduces allocation overhead
- usbipdcpp - A cross-platform USB/IP protocol library. This project is an ESP32 implementation using usbipdcpp.
If you use this project in a product, please display the following information prominently in your product documentation or about page:
- Project URL:
https://github.com/yunsmall/usbipdcpp_esp32 - Author:
yunsmall(GitHub) - Contact:
yun_small@163.com
Large USB transfers (e.g. 65536 bytes for firmware flashing) require a single DMA-capable buffer of the same size. When DMA memory is fragmented, a large contiguous allocation can fail even though the total free space is sufficient.
Chunked transfer addresses this by splitting the large allocation into multiple smaller blocks (default: 16384 bytes each), trading one large contiguous allocation for several smaller ones β dramatically increasing the probability of successful allocation.
The chunked transfer implementation has been moved to the feature/chunked-transfer branch. To try it out, switch to that branch and build:
git checkout feature/chunked-transfer
idf.py build flash monitorHowever, chunking is disabled by default (enable_chunking = false in Esp32DeviceHandler.cpp) due to a persistent timeout issue: when enabled, some bulk transfer scenarios (e.g. remote JLINK firmware flashing) cause the first chunk to NAK indefinitely while waiting for device data. The host times out after ~1 second, sends CMD_UNLINK, and the cycle repeats. The root cause has not been fully identified.
If you know how to fix this, pull requests are welcome.
ESP32-P4 is strongly recommended over ESP32-S3 for USB/IP use. ESP32-S3's internal DMA-capable memory is very limited (~300KB), which can cause large USB transfer buffer allocations to fail under load. ESP32-P4 supports DMA access to PSRAM, which removes the need for chunking in most cases.
Apache License 2.0