Skip to content

BSP USB Serial Module

The bsp_usb_serial module provides the low-level USB OTG host driver for USB-to-serial communication on the Pibot CNC Pendant firmware. Running on the ESP32-S3’s built-in USB OTG peripheral, it initializes the USB host stack, manages a dedicated FreeRTOS event task, and registers a set of Virtual COM Port (VCP) drivers that cover the most common USB-to-serial adapter chipsets found on CNC controllers and their accessories.

This module sits at the bottom of the USB serial transport stack. The upper layer — ESP3DUsbSerialClient (see serial_transports.md) — builds on this driver to implement the full connection lifecycle, RX/TX buffering, baud-rate negotiation, and GCode host integration.


Application Layer

Hardware Abstraction Layer (bsp_usb_serial)

connectDevice() / VCP::open()

Custom VCP Drivers (Project)

PL2303 (Prolific)

CH9102 (WCH)

STM32 VCP

(STMicroelectronics)

Built-in VCP Drivers (ESP-IDF)

FT23x (FTDI)

CP210x (Silicon Labs)

CH34x (WCH)

USB OTG PHY

(Internal, Host mode)

ESP-IDF USB Host Library

usb_host_install / usb_host_uninstall

usb_lib_task

(FreeRTOS - usb_lib)

CDC-ACM Host Driver

cdc_acm_host_install

VCP Service

esp_usb::VCP

ESP3DUsbSerialClient

(serial_transports)


FileRole
hardware/drivers_usb_otg/usb_serial/usb_serial.cppHost init/deinit, USB lib task, VCP driver registration
hardware/drivers_usb_otg/usb_serial/ch9102/usb_host_ch9102_vcp.cCustom VCP driver — WCH CH9102(F/X)
hardware/drivers_usb_otg/usb_serial/pl2303/usb_host_pl2303_vcp.cCustom VCP driver — Prolific PL2303
hardware/drivers_usb_otg/usb_serial/stm32/usb_host_stm32_vcp.cCustom VCP driver — STM32 native USB VCP

Initializes the internal USB OTG PHY in host mode then installs the ESP-IDF USB host library.

usb_new_phy() — USB_PHY_CTRL_OTG, USB_PHY_TARGET_INT, USB_OTG_MODE_HOST
usb_host_install() — skip_phy_setup=true, ESP_INTR_FLAG_LEVEL1

Must be called before usb_serial_create_task(). Returns ESP_FAIL if the host cannot be installed.

Creates the usb_lib FreeRTOS task and performs full CDC+VCP stack bringup:

  1. Pins the usb_lib_task to the configured core at ESP3D_USB_LIB_TASK_PRIORITY / ESP3D_USB_LIB_TASK_SIZE.
  2. Calls cdc_acm_host_install(NULL).
  3. Registers all six VCP drivers (see VCP Driver Registration below).

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

Permanent FreeRTOS task (runs on ESP3D_USB_LIB_TASK_CORE). Calls usb_host_lib_handle_events() in a blocking loop — wrapped in try/catch to absorb spurious exceptions that can occur on device disconnect. Reacts to two event flags:

FlagAction
USB_HOST_LIB_EVENT_FLAGS_NO_CLIENTSCalls usb_host_device_free_all()
USB_HOST_LIB_EVENT_FLAGS_ALL_FREELogs, continues (supports reconnection)

Tears down the stack in reverse order:

usb_serial_delete_task() → cdc_acm_host_uninstall / vTaskDelete
usb_host_device_free_all()
usb_host_uninstall()
usb_del_phy()

Note: usb_host_uninstall() returns ESP_ERR_INVALID_STATE if a device is still connected; the error is logged but deinit continues.

Attempts cdc_acm_host_uninstall() first. If that fails, force-deletes the task handle and returns ESP_OK to allow the caller to proceed with teardown regardless.


All six drivers are registered during usb_serial_create_task() via the esp_usb::VCP::register_driver<T>() template:

VCP Service

esp_usb::VCP

FT23x - FTDI

(ESP-IDF built-in)

CP210x - Silicon Labs

(ESP-IDF built-in)

CH34x - WCH

(ESP-IDF built-in)

PL2303 - Prolific

(project custom)

CH9102 - WCH

(project custom)

STM32VCP - STMicroelectronics

(project custom)

When VCP::open() is called by the upper layer, the service probes registered drivers in order and selects the first one whose VID/PID matches the connected device. Custom drivers hook their own line_coding_set / set_control_line_state (and optionally line_coding_get) callbacks directly into the cdc_acm_dev_hdl_t interface function table at open time.


Targets WCH CH9102F and CH9102X chipsets (VID 0x1A86). These chips use a vendor-specific register interface (CMD_WRITE = 0x9A) rather than standard CDC class requests.

PIDVariant
CH9102F_PIDCH9102F
CH9102X_PIDCH9102X

A 16-bit register value is computed from a custom clock divisor formula. The clock source is selected in bands:

Baud rangeClock sourceDivisor byte b
> 23 529 bps6 000 0003
> 2 941 bps750 0002
> 367 bps93 7501
≤ 367 bps11 7190

Special cases: 921 600 bps → (0xF3, 7) and 307 200 bps → (0xD9, 7).

The resulting value is written to register 0x1312 with bit 7 set: wValue = (factor << 8) | divisor | 0x80.

Bit fieldMeaning
0x80Enable RX
0x40Enable TX
0x20Mark/space parity select
0x10Even parity
0x08Parity enable
0x042 stop bits
0x03/02/01/008/7/6/5 data bits

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


Targets Prolific PL2303 and variants (VID 0x067B).

PID ConstantVariant
PL2303_PIDStandard
PL2303_PID_HXDHXD variant
PL2303_PID_RSAQ2RSAQ2
PL2303_PID_DCU11DCU11

Baud rate is split across the wValue (high 16 bits) and wIndex (low 16 bits) fields of a vendor SET_LINE_CTL (0x20) request, matching the PL2303 HX Linux driver protocol.

All line parameters (data bits, parity, stop bits) are encoded into a 16-bit value and written via a second SET_LINE_CTL request:

ParameterEncoding
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/cleared with separate vendor requests:

SignalSet requestClear request
DTR0x120x11
RTS0x140x13

Targets STM32 microcontrollers configured as USB VCP (VID 0x0483). Unlike the WCH and Prolific chips, STM32 firmware implements standard CDC class requests — no vendor commands are required.

PID ConstantBoard
STM32F4_DISCOVERY_PIDSTM32F4 Discovery
STM32F1_BLUEPILL_PIDSTM32F1 Blue Pill
STM32G0_NUCLEO_PIDSTM32G0 Nucleo
STM32H7_NUCLEO_PIDSTM32H7 Nucleo
STM32L4_NUCLEO_PIDSTM32L4 Nucleo
FunctionCDC RequestDirection
stm32_line_coding_setSET_LINE_CODINGclass, interface OUT
stm32_line_coding_getGET_LINE_CODINGclass, interface IN
stm32_set_control_line_stateSET_CONTROL_LINE_STATEbitmap: DTR=bit0, RTS=bit1

STM32VCP is the only driver in this module that also provides line_coding_get.


ChipsetVendorVIDDriver sourceNotes
FT23xFTDI0x0403ESP-IDF built-inCommon USB-UART bridge
CP210xSilicon Labs0x10C4ESP-IDF built-inVery common on dev boards
CH34xWCH0x1A86ESP-IDF built-inCH340/CH341 series
PL2303Prolific0x067BProject customPL2303, HXD, RSAQ2, DCU11
CH9102F/XWCH0x1A86Project customNewer WCH bridge
STM32 VCPSTMicroelectronics0x0483Project customF1/F4/G0/H7/L4 boards

VCP ServiceCDC-ACM Driverusb_lib_taskUSB Host LibraryUSB OTG PHYusb_serial.cppESP3DUsbSerialClient(Application Layer)VCP ServiceCDC-ACM Driverusb_lib_taskUSB Host LibraryUSB OTG PHYusb_serial.cppESP3DUsbSerialClient(Application Layer)alt[NO_CLIENTS][ALL_FREE]loop[USB event loop]usb_serial_init()usb_new_phy() - host modeusb_host_install()ESP_OKusb_serial_create_task()xTaskCreatePinnedToCore(usb_lib_task)cdc_acm_host_install()register FT23x, CP210x, CH34xregister PL2303, CH9102, STM32VCPESP_OKusb_host_lib_handle_events()event_flagsusb_host_device_free_all()log & continue (reconnect support)usb_serial_deinit()cdc_acm_host_uninstall()vTaskDelete(usb_serial_xHandle)usb_host_device_free_all()usb_host_uninstall()usb_del_phy()
CDC-ACM HostMatched VCP Driver(CH9102 / PL2303 / STM32 / …)VCP ServiceESP3DUsbSerialClientConnection Task(esp3d_usb_serial_connection_task)CDC-ACM HostMatched VCP Driver(CH9102 / PL2303 / STM32 / …)VCP ServiceESP3DUsbSerialClientConnection Task(esp3d_usb_serial_connection_task)loop[RX data]alt[Device disconnect event]connectDevice()VCP::open(pid, interface, config)xxx_vcp_open(pid, interface, config)cdc_acm_host_open(VID, PID, ...)cdc_hdlhook intf_func callbacks\n(line_coding_set, set_control_line_state)ESP_OK + cdc_hdlCdcAcmDevice handleline_coding_set() - baud / formatset_control_line_state() - DTR / RTSrx_callback(data, len)handle_rx() → push to RX queuehandle_event(DEVICE_DISCONNECTED)setConnected(false)\n_disconnect_pending = true

ESP-IDF Components

bsp_usb_serial

Logging

esp3d_log

Core Platform

Application Layer

ESP3DUsbSerialClient

serial_transports

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

usb/cdc_acm_host.h

esp_private/usb_phy.h

esp_private/cdc_host_common.h

usb/usb_types_ch9.h

usb/vcp.hpp

vcp_ch34x / vcp_cp210x / vcp_ftdi

DependencySourcePurpose
usb/usb_host.hESP-IDFUSB host library (install, event loop, device management)
usb/cdc_acm_host.hESP-IDFCDC-ACM class host driver
esp_private/usb_phy.hESP-IDFInternal USB OTG PHY control
esp_private/cdc_host_common.hESP-IDFcdc_acm_dev_hdl_t internals for hooking intf_func
usb/vcp.hpp + chip headersESP-IDFVCP abstraction + built-in FT23x, CP210x, CH34x
freertos/task.hESP-IDFTask create, pin, delete
esp3d_logCore PlatformStructured logging (see logging.md)

Task nameFunctionStackPriorityCore
usb_libusb_lib_taskESP3D_USB_LIB_TASK_SIZEESP3D_USB_LIB_TASK_PRIORITYESP3D_USB_LIB_TASK_CORE

Task constants are defined in usb_serial_def.h. The task runs for the entire lifetime of the USB serial service and must not be starved, as it drives all USB host events including device enumeration and disconnection.


The call to usb_host_lib_handle_events() is wrapped in try/catch(...). This is intentional: a known ESP-IDF issue can produce spurious C++ exceptions on device hot-unplug events. Catching them prevents the task from crashing and preserves reconnection capability.

ESP3DUsbSerialClient::handle_event() does not destroy the CdcAcmDevice directly when CDC_ACM_HOST_DEVICE_DISCONNECTED fires (that event arrives from the USB host task context). Instead it sets _connected = false and _disconnect_pending = true. The actual device destruction is deferred to handle() or end(), which own the TX mutex and can safely release the object without racing the blocking tx_blocking() path.

usb_serial_init() takes exclusive ownership of the internal USB OTG PHY via usb_new_phy() and passes skip_phy_setup = true to usb_host_install() to prevent the host library from re-configuring the PHY. The handle is stored in the module-private phy_hdl static and released only in usb_serial_deinit().

All three project-custom drivers share the same integration pattern:

  1. Open the CDC-ACM device via cdc_acm_host_open() using the chip’s specific VID and PID.
  2. Obtain the raw cdc_acm_dev_hdl_t.
  3. Overwrite intf_func.line_coding_set and intf_func.set_control_line_state (and line_coding_get for STM32) with chip-specific implementations.

This means the CDC-ACM host layer’s public cdc_acm_host_line_coding_set() and cdc_acm_host_set_control_line_state() calls transparently dispatch to the correct chip protocol without any changes in the upper layer.