Skip to content

USB Serial Module

The usb_serial module provides low-level USB OTG host-mode serial communication for the Pibot CNC Pendant firmware. It initializes the ESP32-S3’s built-in USB OTG peripheral in host mode, installs the ESP-IDF USB Host library and CDC-ACM host driver, and registers a set of Virtual COM Port (VCP) chip drivers that collectively cover the most common USB-to-serial adapters found on CNC controller boards. It is implemented in hardware/drivers_usb_otg/usb_serial/ and is consumed by the application-level ESP3DUsbSerialClient, which handles buffering, reconnection, and GCode pipeline integration.


ConcernOwner
USB PHY initialization (host mode)This module
USB Host library lifecycleThis module
CDC-ACM host driver installationThis module
VCP driver registration (6 chipsets)This module
USB host event loop (usb_lib_task)This module
Device open / baud-rate / control-line negotiationThis module (VCP drivers)
Connection polling, RX buffering, GCode pipelineESP3DUsbSerialClient
Board-level pinning and BSP integrationbsp_usb_serial

ESP-IDF USB Stack

USB Serial Hardware Driver (this module)

hardware/drivers_usb_otg/usb_serial/

usb_serial_init/create_task

VCP::open()

Custom VCP Drivers (project)

PL2303

(Prolific)

pl2303/

CH9102

(WCH)

ch9102/

STM32 VCP

(STMicroelectronics)

stm32/

Built-in VCP Drivers (ESP-IDF)

FT23x

(FTDI)

CP210x

(Silicon Labs)

CH34x

(WCH)

Application Layer

ESP3DUsbSerialClient

main/modules/usb_serial/

usb_serial_init()

usb_serial_create_task()

usb_serial_deinit()

usb_lib_task()

FreeRTOS - USB host event loop

CDC-ACM Host Driver

cdc_acm_host_install()

VCP Service

esp_usb::VCP

USB OTG PHY

(Internal, Host mode)

usb_host library


hardware/drivers_usb_otg/usb_serial/
├── usb_serial.cpp ← PHY init, host library install, task creation, VCP registration
├── usb_serial.h ← Public C API declarations
├── usb_serial_def.h ← Task stack / priority / core constants
├── ch9102/
│ ├── usb_host_ch9102_vcp.c ← CH9102 VCP driver (baud algorithm, LCR, modem control)
│ ├── vcp_ch9102.hpp ← C++ VCP wrapper (auto-detect + open)
│ └── vcp_ch9102.h ← VID/PID constants
├── pl2303/
│ ├── usb_host_pl2303_vcp.c ← PL2303 VCP driver
│ ├── vcp_pl2303.hpp
│ └── vcp_pl2303.h
└── stm32/
├── usb_host_stm32_vcp.c ← STM32 native USB CDC driver
├── vcp_stm32.hpp
└── vcp_stm32.h

FT23x, CP210x, and CH34x are handled by the ESP-IDF usb_host component’s built-in VCP drivers. No custom .c file is needed for those chipsets.


CNC Protocol & GCode Engine

Communication Transports

usb_serial module

Hardware Abstraction Layer

bsp_usb_serial

(BSP component group)

usb_serial.cpp

(init / task / registration)

ch9102/

usb_host_ch9102_vcp.c

pl2303/

usb_host_pl2303_vcp.c

stm32/

usb_host_stm32_vcp.c

ESP3DUsbSerialClient

main/modules/usb_serial/

ESP3DGCodeHostService


Bootstraps the USB host stack. Must be called once before any VCP device can be opened.

esp_err_t usb_serial_init();

Internal sequence:

usb_new_phy() ← USB_PHY_CTRL_OTG / USB_PHY_TARGET_INT / USB_OTG_MODE_HOST / speed auto
usb_host_install() ← skip_phy_setup=true, ESP_INTR_FLAG_LEVEL1, no enum filter

The PHY handle is stored in the module-private phy_hdl static variable; skip_phy_setup = true prevents the host library from reconfiguring the already-initialized PHY.


Creates the usb_lib FreeRTOS task and completes stack bringup.

esp_err_t usb_serial_create_task();

Steps performed:

  1. xTaskCreatePinnedToCore(usb_lib_task, "usb_lib", ...) — pinned to ESP3D_USB_LIB_TASK_CORE
  2. cdc_acm_host_install(NULL)
  3. Six VCP::register_driver<T>() calls (in order: FT23x, CP210x, CH34x, PL2303, CH9102, STM32VCP)

Returns ESP_FAIL if the task cannot be created or CDC-ACM installation fails.


Permanent FreeRTOS task. Blocks on usb_host_lib_handle_events(portMAX_DELAY, &event_flags) and reacts to library events:

Event flagAction
USB_HOST_LIB_EVENT_FLAGS_NO_CLIENTSCalls usb_host_device_free_all() to release device memory when no client holds an open handle
USB_HOST_LIB_EVENT_FLAGS_ALL_FREELogs the event; continues the loop to support hot-reconnection without a reboot

The usb_host_lib_handle_events() call is wrapped in a try/catch(...) block to absorb spurious C++ exceptions produced by the IDF USB stack on certain hot-unplug race conditions. Catching the exception prevents the task from crashing and preserves reconnection capability.


Tears down the full USB stack in reverse order:

esp_err_t usb_serial_deinit();
usb_serial_delete_task() → cdc_acm_host_uninstall() then vTaskDelete
usb_host_device_free_all()
usb_host_uninstall() ← may return ESP_ERR_INVALID_STATE if device still connected (logged, not fatal)
usb_del_phy(phy_hdl)
phy_hdl = NULL

Attempts cdc_acm_host_uninstall() first. If that fails (e.g., device still active), it force-deletes the task handle and returns ESP_OK to allow the caller to continue teardown.


VCP ServiceCDC-ACM DriverUSB Host LibraryUSB OTG PHYusb_serial.cppESP3DUsbSerialClientVCP ServiceCDC-ACM DriverUSB Host LibraryUSB OTG PHYusb_serial.cppESP3DUsbSerialClientalt[NO_CLIENTS][ALL_FREE]loop[USB host event loop (usb_lib_task)]usb_serial_init()usb_new_phy() [host mode]usb_host_install()ESP_OKusb_serial_create_task()xTaskCreatePinnedToCore(usb_lib_task)cdc_acm_host_install()register_driver<FT23x, CP210x, CH34x>()register_driver<PL2303, CH9102, STM32VCP>()ESP_OKevent_flagsusb_host_device_free_all()log & continue

Each VCP driver plugs into the CDC-ACM framework by overwriting three function pointers on the cdc_acm_dev_hdl_t handle after successfully calling cdc_acm_host_open():

cdc_hdl->intf_func.line_coding_set = <driver>_line_coding_set;
cdc_hdl->intf_func.set_control_line_state = <driver>_set_control_line_state;
cdc_hdl->intf_func.line_coding_get = <driver>_line_coding_get; // STM32 only

This hook pattern means the upper layer’s calls to cdc_acm_host_line_coding_set() and cdc_acm_host_set_control_line_state() transparently dispatch to the correct chip-specific protocol with no changes required in ESP3DUsbSerialClient.

yes

no

yes

no - all PIDs exhausted

xxx_vcp_open(pid, iface_idx, dev_config, &hdl)

pid == PID_AUTO?

Iterate supported_pids[]

call cdc_acm_host_open() for each

cdc_acm_host_open(VID, pid, ...)

ESP_OK?

Write intf_func callbacks:

line_coding_set

set_control_line_state

[line_coding_get - STM32 only]

Return ESP_OK + handle

Return ESP_ERR_NOT_FOUND


File: ch9102/usb_host_ch9102_vcp.c
VID: 0x1A86 | Auto-probe PIDs: CH9102F_PID, CH9102X_PID

Uses vendor-specific requests (CMD_WRITE = 0x9A) to a register interface — not standard CDC class requests.

A clock pre-scaler b is selected by baud-rate band, then an 8-bit factor a is computed with nearest-integer rounding:

Baud rangeClock sourcePre-scaler b
> 23 529 bps6 000 000 Hz3
> 2 941 bps750 000 Hz2
> 367 bps93 750 Hz1
≤ 367 bps11 719 Hz0

Two hard-coded special cases: 921 600 bps → (0xF3, 7), 307 200 bps → (0xD9, 7).

Result written to register 0x1312 as (factor << 8) | divisor | 0x80 via a CH9102_CMD_WRITE vendor request.

Bit(s)Meaning
0x80Enable RX
0x40Enable TX
0x20Mark/space parity select
0x10Even parity
0x08Parity enable
0x042 stop bits (1 stop bit when clear)
0x03/02/01/008/7/6/5 data bits

CMD_MODEM_OUT (0xA4) — DTR → bit 5 (0x20), RTS → bit 6 (0x40). Both written in a single vendor request.


File: pl2303/usb_host_pl2303_vcp.c
VID: 0x067B | Auto-probe PIDs: PL2303_PID, PL2303_PID_HXD, PL2303_PID_RSAQ2, PL2303_PID_DCU11

Uses vendor-class requests (request type 0x40) aligned with the Linux PL2303 HX driver protocol.

The 32-bit baud rate is split across two fields of a vendor SET_LINE_CTL (0x20) request:

  • wValue = upper 16 bits of baud rate
  • wIndex = lower 16 bits of baud rate

Data bits, parity, and stop bits are ORed into a 16-bit value and written via a second SET_LINE_CTL request:

ParameterValue encoding
Data bits5→0x00, 6→0x01, 7→0x02, 8→0x03
ParityNone/Odd/Even/Mark/Space → 0x00/0x08/0x18/0x28/0x38
Stop bits1→0x00, 2→0x04

DTR and RTS are set and cleared with four separate vendor write requests:

SignalSetClear
DTR0x120x11
RTS0x140x13

File: stm32/usb_host_stm32_vcp.c
VID: 0x0483 | Auto-probe PIDs: F4 Discovery, F1 Bluepill, G0/H7/L4 Nucleo

STM32 firmware that enables the USB VCP peripheral presents a standard CDC device. All control uses USB_BM_REQUEST_TYPE_TYPE_CLASS | USB_BM_REQUEST_TYPE_RECIP_INTERFACE — no vendor commands.

FunctionCDC requestDirection
stm32_line_coding_setUSB_CDC_REQ_SET_LINE_CODINGClass, interface OUT
stm32_line_coding_getUSB_CDC_REQ_GET_LINE_CODINGClass, interface IN
stm32_set_control_line_stateUSB_CDC_REQ_SET_CONTROL_LINE_STATEBitmap: DTR = bit 0, RTS = bit 1

STM32VCP is the only driver in this module that implements line_coding_get, enabling the host to read back the device’s live configuration.


ChipsetVendorVIDDriver sourceAuto-probe PIDs
FT23xFTDI0x0403ESP-IDF built-inYes
CP210xSilicon Labs0x10C4ESP-IDF built-inYes
CH34xWCH0x1A86ESP-IDF built-inYes
PL2303Prolific0x067BProject customPL2303, HXD, RSAQ2, DCU11
CH9102F/XWCH0x1A86Project customCH9102F, CH9102X
STM32 VCPSTMicroelectronics0x0483Project customF1/F4/G0/H7/L4 boards

CDC-ACM HostMatched VCP DriverVCP ServiceESP3DUsbSerialClientesp3d_usb_serial_connection_taskCDC-ACM HostMatched VCP DriverVCP ServiceESP3DUsbSerialClientesp3d_usb_serial_connection_taskloop[RX data reception]Deferred teardown in handle() / end()\nto avoid racing tx_blocking()alt[Hot disconnect]connectDevice()VCP::open(pid, iface, config, rx_callback, handle_event)xxx_vcp_open(pid, iface, dev_config, &hdl)cdc_acm_host_open(VID, PID, ...)cdc_hdlhook intf_func.line_coding_set\nhook intf_func.set_control_line_stateESP_OK + cdc_hdlCdcAcmDevice handle (_vcp_ptr)line_coding_set() - baud rate / data bits / parity / stop bitsset_control_line_state() - DTR=true, RTS=truesendInitCommand()rx_callback(data, data_len)handle_rx() → push to RX queue → GCode hosthandle_event(CDC_ACM_HOST_DEVICE_DISCONNECTED)setConnected(false)\n_disconnect_pending = true
Task nameFunctionStack constantPriority constantCore constant
usb_libusb_lib_taskESP3D_USB_LIB_TASK_SIZEESP3D_USB_LIB_TASK_PRIORITYESP3D_USB_LIB_TASK_CORE

All three constants are defined in usb_serial_def.h. The task runs for the entire lifetime of the USB serial service. It must not be starved — being blocked delays device enumeration, disconnection detection, and resource cleanup.

For a full view of the concurrent task topology see Communication Transports.


Logging

Application

ESP-IDF Components

usb_serial (hardware driver)

usb_serial.cpp

ch9102/

usb_host_ch9102_vcp.c

pl2303/

usb_host_pl2303_vcp.c

stm32/

usb_host_stm32_vcp.c

usb/usb_host.h

(install/uninstall/events)

usb/cdc_acm_host.h

(install/open/send)

esp_private/usb_phy.h

(PHY control)

esp_private/cdc_host_common.h

(intf_func hook)

usb/usb_types_ch9.h

(request type masks)

usb/vcp.hpp + vcp_ftdi / vcp_cp210x / vcp_ch34x

freertos/task.h

ESP3DUsbSerialClient

(serial_transports)

esp3d_log / esp3d_log_e

(core_platform)

HeaderESP-IDF componentPurpose
usb/usb_host.husb_hostHost library install, event dispatch, device free
usb/cdc_acm_host.husb_hostCDC-ACM class driver, custom request API
esp_private/usb_phy.husb_hostInternal OTG PHY lifecycle
esp_private/cdc_host_common.husb_hostcdc_acm_dev_hdl_t internals for intf_func hooking
usb/vcp.hpp + chip headersusb_hostVCP service registry + FT23x / CP210x / CH34x drivers
freertos/task.hFreeRTOSTask creation, pinning, deletion
esp3d_logesp3d_log componentStructured logging — see logging

  • Own answers sent at once. Received messages go through processRxMsg(), and process() first calls sendOwnAnswer(): a long answer to a command received on this link ([ESP400], [ESP0], [ESP740]…) is written at once instead of overflowing the TX queue. The transport write is factored into writeTxMsg(), shared with the handle() TX drain.
  • Low-memory admission and lost lines. Each received line first goes through rxAdmit() (a refused line is dropped on purpose); allocation failures call esp3d_rx_line_lost(), and a line not sent to the CNC calls esp3d_tx_line_lost() (its ok will never come, S21). See CNC_Firmware_Integration.md, “Lost Lines, Input Overflow and Resets”.
  • Messages larger than the transfer buffer (ESP3D_USB_SERIAL_TX_BUFFER_SIZE) are sent in chunks; the write runs on the task that owns the TX path (handle() or the direct answer path).
  • Host (this OTG client) and device (the new USB CDC device client) share one OTG controller and are exclusive at build time; see Communication_Transports.md.

The usb_host_lib_handle_events() call is deliberately wrapped in try/catch(...). A known ESP-IDF USB stack issue can emit spurious C++ exceptions during device hot-unplug. Absorbing them keeps the task alive for reconnection without requiring a firmware restart.

When CDC_ACM_HOST_DEVICE_DISCONNECTED fires, the event arrives in the USB host task context — not the application task that owns the TX mutex. handle_event() therefore only sets _connected = false and _disconnect_pending = true. The CdcAcmDevice destructor is called later from handle() or end(), which hold _tx_mutex, preventing a race against an in-flight tx_blocking() call.

usb_serial_init() takes exclusive ownership of the internal OTG PHY via usb_new_phy(). The handle is stored in the module-private phy_hdl static and is not released until usb_serial_deinit(). Passing skip_phy_setup = true to usb_host_install() ensures the host library does not re-configure the already-initialized PHY.

usb_host_install() is a global singleton in ESP-IDF. Calling usb_serial_init() a second time without first calling usb_serial_deinit() returns ESP_ERR_INVALID_STATE. The module has no guard against this; the caller is responsible for correct lifecycle management.


The USB serial driver is conditionally compiled via the feature flag system (see Build Toolchain). The controlling option is typically USB_SERIAL_SERVICE in the board’s CMakeLists.txt.

Platform constraint: USB OTG host mode requires the ESP32-S3’s built-in OTG PHY. It is not available on the original ESP32. Enabling this feature on an unsupported board will cause a PHY initialization failure at boot.

Mutual exclusion with Bluetooth: On variants without PSRAM, the USB host stack and the Bluetooth stack cannot coexist due to RAM pressure. The cmake/sanity_check.cmake enforces this exclusion at build time (see Hardware Abstraction Layer for the full constraint matrix).


  • bsp_usb_serial.md — BSP-level component group view of this module (sibling drivers, board init hooks)
  • serial_transports.md — ESP3DUsbSerialClient: application layer that consumes this driver; covers connection task, RX/TX buffering, init-command sequence, and ping keep-alive
  • bsp.md — Full Hardware Abstraction Layer overview and sibling driver groups
  • bsp_bsp_board_initialization.md — Board initialization sequence and transport selection
  • logging.md — esp3d_log / esp3d_log_e macro conventions used throughout this module