干嵌入式十多年最让我头疼的不是代码 bug而是调试阶段串口不够用。一块板上塞了 GPS、LoRa、4G 模组、传感器采集再加一个调试串口单片机自带的三四个 UART 根本不够分。以前我习惯用 IO 模拟串口或 SPI 转串口芯片硬扩但通信一多、波特率一上来就不稳定。后来我换了个思路让 STM32 本身去当 USB Host外面挂一个 USB Hub再接多个 CH340 模块这样一块开发板上就能轻松堆出 4 路、8 路甚至更多的虚拟串口。这篇文章就是把这套方案的完整细节整理出来包含硬件选型、CH340 的 USB 私有协议、STM32CubeMX 配置、自定义 USBH Class 的移植以及我在实际调试中踩过的各种坑。适合正在做多路串口网关、数据采集设备或者想深入了解 STM32 USB Host 底层开发的朋友参考。1. 为什么用 STM32 的 USB HOST 扩展串口1.1 串口扩展的几种常见方案对比在做多路串口扩展前我先盘点过市面上几类主流方案各有各的适用边界。第一种是 IO 口模拟 UART也叫 bit-banging。这种方式在 F103 这类低主频单片机上做两路 9600 波特率的小数据量收发还能勉强应付但波特率一旦超过 38400定时器中断就会把 CPU 吃满而且多路收发同时发生时每路之间的时序抖动非常明显误码率会高到没法用。它适合的场景只有临时调试、对可靠性要求极低的实验。第二种是 SPI/I2C 转串口芯片比如 SC16IS752、WK2124、CH438 这些。一片芯片能扩展 2 到 8 路 UART稳定性很好硬件流控、波特率自动适配也做得不错。缺点是外围电路复杂要处理芯片地址、中断引脚、寄存器映射PCB 布线和驱动代码的工作量都不小。而且扩展路数受限于 SPI/I2C 总线的从设备数量想做到 8 路以上得用多片级联硬件成本并不低。第三种就是本文要讲的 USB Host 方案。STM32 的 USB OTG 接口加上一个几块钱的 USB Hub 芯片就能挂载多个 USB 转串口模块。CH340 模块现在非常便宜、货源充足一根 USB 线就是一个串口扩展多少路只取决于 Hub 的端口数量。这种方式硬件连接极其简洁核心工作量全部集中在软件层。方案扩展路数硬件复杂度软件工作量可靠性适用场景IO 模拟串口2-3 路极低低较差低速临时调试SPI/I2C 转串口芯片2-8 路中中高固定端口数量的产品USB Host CH3404-16 路低高高多路动态扩展、串口服务器对比下来USB Host 方案最适合“串口数量不确定、后续可能还要往上加”的场景。比如我做数据采集网关时客户今天说接 4 台仪表下周又说要接 8 台硬件上只需要换一个更多端口的 USB Hub软件上把一个实例数组从 4 改成 8重新编译就完事了。这在传统方案里是想都不敢想的。1.2 这套方案能解决什么问题把 USB Host 方案做成产品形态最典型的应用是串口服务器和数据采集网关。STM32 作为 USB Host 挂载多个 CH340每个 CH340 连接一台下位机设备STM32 再把收到的数据通过以太网、WiFi 或者自身的多个物理 UART 转发出去。这样一台设备就相当于把传统工控机要干的事全部接管了功耗低、体积小、成本也压得住。另外这个方案天然适合做“测试治具”。我以前调试一批传感器模组要同时监听多个串口的日志输出电脑 USB 口不够用一个 USB Hub 能解决一部分问题但治具本身也要和被测设备通信。用 STM32 做 USB Host 收集多路串口数据再通过一个标准 USB 虚拟串口把汇总日志传给 PC整个测试流程就非常干净。总带宽这块大家也不用担心。USB Full Speed 是 12Mbps 的总线带宽实际有效载荷能跑到 6-8Mbps。按每路 115200bps 算一路满载大约是 11.5KB/s纯理论能跑到 50 路以上实际考虑协议开销和缓冲稳定跑 8-12 路 115200 是完全没问题的。如果哪路波特率只有 9600那挂二三十路都不在话下。1.3 做之前必须确认的三个硬件条件第一STM32 必须带 USB OTG Host 能力。很多新手拿了 F103C8T6 就想做这个方案但普通 F103 的 USB 外设只是 Device不带 Host 功能这是硬件上的硬伤软件救不回来。要选 F105/F107、F4、F7、H7 这些带 OTG 外设的型号。第二USB Hub 芯片或成品 Hub 必须得是自供电设计。CH340 模块虽然单路功耗不高但 8 个模块同时工作峰值电流能到 500mA 以上抢总线供电会直接导致枚举失败、设备随机掉线。第三CH340 模块本身要是正经厂家的芯片。市面上的 CH340G、CH340C、CH340N 都行但那些打磨掉丝印的山寨芯片有时 VID/PID 都不标准后面驱动匹配会非常麻烦。2. 硬件选型与连接细节2.1 STM32 选型F103 普通版不行别走弯路这是我的第一个深刻教训。最初我图省事手头正好有 F103C8T6看到网上有人用 USB 虚拟串口就以为也能做 Host结果查了参考手册才发现 F103 的 USB 是 Device-only根本没有 Host 控制器。真正能用的最低成本方案是 STM32F105RBT6 或者 F107这两个型号自带 USB OTG FS支持 Host 模式。再往上就是大家更熟悉的 STM32F407VET6它有两路 USB OTG一路 FS 一路 HS可以把 FS 配成 Host、HS 配成 Device这样既能当主机挂 CH340又能把自己的调试日志通过虚拟串口发给 PC非常灵活。STM32H750 系列资源更充裕适合同时跑网络协议栈和 USB Host 协议栈的情况。STM32 型号USB 外设是否支持 Host备注F103 普通系列USB Device不支持只能做 DeviceF105/F107USB OTG FS支持成本最低的选择F407/F429OTG FS OTG HS支持HS 需外接 ULPI PHYH750/H743多路 OTG支持适合复杂产品关于 HS 接口提醒一句F407 的 OTG HS 如果在内部 PHY 模式下本质上还是 Full Speed 速率想要真正跑 High Speed必须外接 USB3300 这类 ULPI PHY 芯片。做多路 CH340 扩展其实 Full Speed 就够了没必要加 PHY省一颗料是一颗料。2.2 USB Hub 芯片与 CH340 模块的搭配USB Hub 芯片我用过 GL850G、FE1.1s、USB2513 这几款。GL850G 是入门首选四端口版本很便宜外围就几个电阻电容但要注意它个别批次的兼容性一般如果遇到 CH340 枚举不稳定先换一个 Hub 芯片试试。FE1.1s 是台湾旺玖的芯片稳定性比 GL850G 好一截价格差别不大我中后期项目基本固定用它。USB2513 是 Microchip 的经典款可靠性最好但外围电路要求更严格适合用在出货量大的产品上。CH340 模块选型时我一般用 CH340C它内置晶振不依赖外部 12MHz 晶振少两个元件少一桩心事。老款的 CH340G 需要一个外部晶振而且晶振质量不好会直接导致 USB 枚举不稳定这是我踩过的大坑。连接关系很直接STM32 的 OTG_FS_DP/OTG_FS_DM 引脚连到 Hub 的上行端口Hub 下行端口接各个 CH340 模块。注意 OTG_FS_ID 引脚要拉低表示 Host 模式VBUS 引脚要接一个使能控制信号不能让它悬空。2.3 供电与信号完整性的实际教训这个方案里九成的问题都是供电引起的。USB 标准规定一个端口最大提供 500mA 电流一个 CH340 模块空闲时大概 10-20mA但通信时瞬时电流会突增。4 个模块在同一个 Hub 下总线供电就已经很紧张了更别说 8 路以上。所以必须采用自供电 Hub外部 5V 电源直接进 HubHub 的每个下行端口独立输出 5V 给 CH340 模块这样总线只负责数据通信不承担供电压力。我在自制 PCB 时还会在每个 CH340 模块的 5V 输入处加一个 10uF 钽电容和一个 0.1uF 陶瓷电容并联去耦。USB 的 D/D- 差分对要尽量等长走线不要在中间打孔穿层否则高速信号容易失真。如果只是用杜邦线连模块做实验那也尽量让杜邦线短一点超过 20cm 就会出现随机的枚举失败。实验阶段我发现CH340 模块离 Hub 的 USB 座越近整体越稳。3. CH340 在 USB 总线上的真实身份为什么不能直接用 CDC 类驱动3.1 它不是标准 CDC ACM 设备很多人在 STM32 上移植 USB Host 时第一反应是去看 ST 官方的 usbh_cdc.c觉得既然是 USB 转串口肯定属于通信设备类CDC。但 CH340 恰恰不是。我实测过把 CH340 模块插到运行标准 CDC 驱动的 STM32 Host 上枚举阶段能读到设备描述符但到了接口描述符发现接口类代码是 0xFF也就是 vendor-specific厂商自定义不是 CDC 类的 0x02。所以 ST 的 CDC 类驱动根本无法关联匹配它。这也是为什么 Windows 下 CH340 要单独装驱动、Linux 下要走 ch341 内核驱动而 CP2102、FT232 在大部分系统上可以直接用自带驱动的原因。CP2102 是标准的 CDC ACM 设备FT232 有些型号也遵循标准类但 CH340 从底层就自己搞了一套私有协议。想要 STM32 和它通信就得自己把这套协议实现出来。3.2 CH340 私有协议的核心内容CH340 的协议可以从 Linux 内核源码 drivers/usb/serial/ch341.c 里找到完整答案我移植时就是对着这个文件逐行翻译的。协议的主体分三块设备初始化、波特率设置、数据收发。初始化阶段Host 需要发一个 vendor request请求号是 0xA1方向是 Host to Device也就是 OUT 传输让 CH340 内部的串口状态机复位并进入工作状态。之后还要通过写寄存器请求把芯片配置成串口模式。波特率设置是协议里最核心的部分。CH340 内部通过一个分频因子来生成串口时钟不同芯片版本的时钟基准略有差异。设置波特率时Host 要计算出对应的分频因子然后通过写寄存器请求0xA6把因子的低 16 位和高 16 位分别写入特定的寄存器地址。如果只设置了其中一个寄存器波特率就完全错误串口出来的数据直接乱码。控制请求请求号方向用途CH341_REQ_SERIAL_INIT0xA1OUT初始化串口内部状态机CH341_REQ_WRITE_REG0xA6OUT写内部寄存器波特率、模式CH341_REQ_READ_REG0xA7IN读内部寄存器查询状态CH341_REQ_MODEM_CTRL0xA4OUT控制 RTS/DTR 等 Modem 信号数据收发则走 USB 批量传输端点CH340 通常暴露一个批量 IN 端点和一个批量 OUT 端点直接从这两个端点搬运数据即可不需要额外协议封装。3.3 “虚拟串口”在这里有两层含义第一层CH340 本身在 USB 上模拟了一个串口USB Host 通过批量端点把数据送到 CH340CH340 再从它的 UART TX 引脚输出效果等同于一个真实串口。第二层当 STM32 把多路 CH340 的数据采集起来再转发到网络上PC 端用虚拟串口软件可以把网络端口映射成本地 COM 口这就是第七节要展开的网络串口服务器方案。明白这个概念你就知道为什么整个标题叫“多个 CH340 虚拟串口”了。4. 基于 STM32CubeMX 的软件框架搭建4.1 CubeMX 配置 USB OTG 与中间件我以 STM32F407VET6 为例讲配置流程。打开 STM32CubeMX先把 RCC 的外部高速时钟打开USB OTG FS 正常工作时需要 48MHz 时钟这个时钟一般由 PLLQ 输出提供在 Clock Configuration 页面确认 USB 时钟源等于 48MHz否则枚举必然失败。然后在 Connectivity 选项卡里选择 USB_OTG_FS模式选 Host Only。VBUS 相关的使能引脚根据硬件原理图配置成 GPIO 输出。接下来在 Middleware and Software Packs 里勾选 USB_HOSTClass for FS IP 一定要选 Custom Class这样 HAL 库不会自动生成 CDC 或 HID 类代码而是留一个空白类模板给用户自己填。这个选择非常关键选成 CDC 反而会给后面带来类代码冲突。还需要注意堆栈大小。USB Host 协议栈本身要吃掉不少 RAM裸机环境下我把堆栈都设到 4KB 以上否则运行到 USBH_Process 时容易莫名 HardFault。4.2 自定义类驱动的挂载方式HAL 库的 USB Host 中间件设计了一套类驱动注册机制。开发者要自己定义 USBH_ClassTypeDef 结构体实现 Init、DeInit、Requests、DataIn、DataOut 这几个回调然后把结构体指针传给 USBH_RegisterClass最后调用 USBH_Start 启动主机。我移植 CH340 时就是在 Custom Class 框架里填入 CH340 的枚举匹配逻辑和控制传输代码。核心代码骨架如下注意 API 细节要以你用的 HAL 库版本头文件为准USBH_ClassTypeDef CH340_Class { .Name CH340, .Init CH340_Init, .DeInit CH340_DeInit, .Requests CH340_Requests, .BgndProcess CH340_BackgroundProcess, }; void USBH_UserProcess(USBH_HandleTypeDef *phost, uint8_t id) { switch (id) { case HOST_USER_SELECT_CONFIGURATION: USBH_RegisterClass(phost, CH340_Class); break; default: break; } }主循环里只需要不断调用 USBH_Process 驱动状态机数据的到达和处理全部由类驱动回调触发。这种事件驱动的方式配合 FreeRTOS 使用非常顺手USB 中断和任务之间通过消息队列解耦即可。4.3 与 FreeRTOS 配合的数据分发思路多路 CH340 同时通信时数据流的组织是软件设计的关键。我的做法是给每一路 CH340 分配一个独立的环形缓冲区USB 的 DataIn 回调只负责把数据拷入对应路的环形缓冲然后发一个信号量给串口转发任务。转发任务负责把缓冲区的数据通过目标物理 UART 或其他通道发送出去。这样 USB 中断里不做耗时的字符串解析只做内存搬运即使 8 路同时满负荷接收CPU 占用也能控制在 30% 以内。发送方向同理。各路物理 UART 收到的数据先进入各自的发送环形缓冲USB 主循环在轮询阶段把缓冲数据通过批量 OUT 端点发给对应的 CH340。正是因为收发两边都用环形缓冲做了缓冲瞬时突发流量才不会互相阻塞这也是多路稳定通信的前提。5. CH340 设备驱动的移植与实现5.1 枚举流程中的设备识别逻辑USB Host 枚举完成后系统会拿到设备的 VID、PID 以及接口描述符。CH340 最常见的 VID 是 0x1A86PID 根据型号不同有 0x7523、0x5523 等。对于 CH340G、CH340C我直接匹配 0x1A86:0x7523。匹配成功后还要保存这个设备挂在哪个 Hub 端口上作为它的逻辑通道号。需要特别注意的是多个相同的 CH340 同时插入时USB 枚举不会保证端口号顺序固定。有的 Hub 物理端口 3 的设备会被枚举为第 2 个设备完全取决于插入时序。所以产品设计时必须通过 Hub 物理端口号来区分逻辑通道而不是依赖枚举顺序。我一般会在类驱动的 Init 回调里把 phost-device 的父级 Hub 端口信息读出来记录到实例结构体中。5.2 关键代码CH340 初始化与波特率设置下面这段代码是我移植的核心部分基于 Linux ch341 驱动的逻辑改写。首先发送初始化请求static void CH340_Init(USBH_HandleTypeDef *phost) { // 0xA1: SERIAL_INIT重置 CH340 内部串口状态机 USBH_CtrlReq(phost, NULL, 0, USB_EP_TYPE_CONTROL, CH341_REQ_SERIAL_INIT, 0, 0, USB_H2D | USB_REQ_TYPE_VENDOR | USB_REQ_RECIPIENT_DEVICE); // 写寄存器把芯片配置为标准串口模式 CH340_WriteReg(phost, 0x1312, 0x0000); CH340_WriteReg(phost, 0x1313, 0x0001); CH340_WriteReg(phost, 0x1314, 0x0000); } static void CH340_WriteReg(USBH_HandleTypeDef *phost, uint16_t reg, uint16_t val) { uint8_t buf[2]; buf[0] val 0xFF; buf[1] (val 8) 0xFF; // 0xA6: WRITE_REG把 val 写入 reg 指定的寄存器 USBH_CtrlReq(phost, buf, 2, USB_EP_TYPE_CONTROL, CH341_REQ_WRITE_REG, reg, 0, USB_H2D | USB_REQ_TYPE_VENDOR | USB_REQ_RECIPIENT_DEVICE); }波特率设置的算法在各型号 CH340 上略有差异。Linux ch341.c 里维护了一个因子换算关系大概流程是先根据目标波特率算出分频因子再拆成两个 16 位分别写入两个相邻寄存器。下面给出我移植的参考实现具体因子一定要对照你手里芯片的版本用示波器实测输出的 TX 波形确认static void CH340_SetBaud(USBH_HandleTypeDef *phost, uint32_t baud) { uint32_t factor 1532620800u / baud; // 以 Linux ch341 驱动为参考 // 先写低 16 位寄存器再写高 16 位寄存器 CH340_WriteReg(phost, 0x0002, factor 0xFFFF); CH340_WriteReg(phost, 0x0003, (factor 16) 0xFFFF); }如果在实际项目里发现某路波特率不准最直接的验证办法是用另一块单片机以相同波特率回读数据或者把 CH340 的 TX 接到逻辑分析仪上看波形周期。5.3 批量收发与多路实例管理CH340 的数据端点通常是端点 1 的批量 IN 和批量 OUT。批量传输本身有 CRC 校验和重传机制USB 协议层面的可靠性可以放心真正要小心的是应用层的缓冲管理。我在类驱动的 DataIn 回调里这样组织数据static void CH340_DataIn(USBH_HandleTypeDef *phost, uint8_t *data, uint16_t len) { CH340_Device_t *dev CH340_Devices[dev_index]; RingBuf_Write(dev-rx_ring, data, len); osSemaphoreRelease(dev-rx_sem); }其中 dev_index 在 Init 回调里根据 USB Hub 端口确定。多路数据各自独立互不干扰。发送时直接从对应通道的发送缓冲取数据调用 USBH_BulkSendData 发起批量 OUT 传输。如果发送缓冲为空就跳过本次发送避免发空包。实际调试中发现HAL 库的批量发送函数在数据量大的时候要注意发送完成标志不要在没完成上一次传输时就发起下一次否则 USB 状态机会错乱。我会加一个简单的忙标志来串行化同一端点的发送请求。6. 实测中遇到的高频问题与排查技巧6.1 常见问题速查表现象直接原因处理办法枚举失败设备地址始终为 0VBUS 供电不足或 48MHz 时钟缺失检查外部电源和时钟树配置只能识别第一个 CH340总线供电被拉垮改为自供电 Hub串口数据全乱码波特率因子寄存器写入不完整核对寄存器高低 16 位是否都写了插拔 CH340 时系统死机热插拔没有做去抖和错误恢复在状态迁移处增加延时和复位逻辑多路同时收发时丢包环形缓冲太小每路接收缓冲加大到 2KB 以上USB Host 初始化 HardFault堆栈溢出堆栈至少 4KB检查 malloc 配置6.2 枚举失败的排查套路枚举失败是这类方案最常遇到的问题。我一般按三步来排查第一步把 CH340 模块插到 PC 上确认模块本身能正常工作、设备管理器里能识别到串口号这能排除模块坏的嫌疑。第二步用逻辑分析仪抓 OTG_FS 的 DP/DM 波形看主机是否发起了复位和设置地址请求如果一根线都没波形问题在 STM32 配置侧重点核对 USB 时钟是否 48MHz、VBUS 使能是否正常。第三步在 USBH_Process 前面加串口日志打印当前状态机的状态值看卡在哪个环节。还有一个隐蔽问题很多开发板的 OTG_FS 接口需要外部跳线或者电阻来区分 Host 和 Device。ID 引脚不拉低的话即使代码配成 Host Only硬件上也起不来。我会在硬件设计时加一个 ID 引脚到 GND 的默认下拉电阻避免漏接。6.3 数据乱码与丢包的进一步排查波特率设置错误是乱码的首要原因。CH340 的波特率是通过写内部寄存器实现的和 CP2102 的标准 CDC 设置方式完全不同。如果你移植时只写了低 16 位因子而忽略了高 16 位数据就是乱码。还有少数 CH340 山寨芯片的时钟基准不一样同样因子出来的波特率会偏差 2% 以上串口只支持 ±2% 的误差窗口所以一旦乱码优先怀疑波特率因子而不是怀疑协议。丢包问题则要看缓冲。批量端点的单次最大传输长度是 64 字节如果应用层处理慢USB 收完一个包后下一次 IN 令牌可能被主机错过设备端的 FIFO 就会溢出丢数据。解决办法有两条一是把接收环形缓冲做大二是把 USB 数据接收任务的优先级调到足够高。实测 8 路 115200 同时收发每路 2KB 的环形缓冲就很稳了。7. 把方案再往前推一步网络串口服务器与虚拟串口软件7.1 从本地扩展串口到网络映射串口当 STM32 通过 USB Host 把多路 CH340 的数据都拿到手之后最大的价值不是只转发给本机串口而是通过网络发出去形成一个多串口服务器。硬件上加一个 W5500 模块或者 LAN8720 以太网 PHY每路 CH340 对应一个 TCP Server 端口PC 端用虚拟串口软件比如上海卓岚的 ZLVircom把这些 TCP 端口映射成本地 COM 口。这样调试者坐在办公室就能远程访问几十公里外设备的串口和插在本机上的物理 COM 口体验几乎一样。我在实际项目中做过类似设计STM32F407 挂 4 路 CH340通过 W5500 把数据上传PC 端映射成 COM3-COM6。唯一要留意的是 TCP 是流式协议没有串口的帧边界概念若上位机要按帧解析需要在应用层加帧头帧尾或长度字段不能在传输层期待它自动分包。7.2 MQTT 虚拟串口与物联网场景如果不想维护 TCP 长连接可以直接走 MQTT。每路 CH340 在 MQTT Broker 下对应一个主题比如dev/serial1/tx和dev/serial1/rx。其他设备或者云平台通过订阅/发布主题就能和串口设备双向通信。网上有些 MQTT 虚拟串口软件本质也是把 MQTT 消息桥接成 COM 口。这种方案特别适合动环监控、远程传感器读取这类 IoT 场景优点是天然跨平台、支持发布订阅解耦缺点是实时性不如 TCP适合非实时数据采集。7.3 固件升级与后续维护软件做大了以后OTA 升级能力就很重要。搜“STM32 OTA”能找到很多参考这里只提一条建议在做 USB Host 网络协议栈这种大型固件时一定要把 Bootloader 和 App 分区独立设计。Bootloader 负责从网络或 USB 升级 AppApp 跑业务逻辑。因为多路 CH340 的驱动、协议栈、应用代码加在一起体积不小没有 OTA 能力每改一版都要开壳刷程序后期维护成本很高。8. 关于方案取舍我最想叮嘱的一段经验这套方案我前后迭代了三版从最早的 F105 裸机小样到后来 F407 FreeRTOS 网络协议栈的多路串口服务器期间踩的最深的坑就是 CH340 的私有协议。网上很多文章想当然地把它当标准 CDC 设备处理照着写驱动结果枚举成功但数据就是不来折腾很久才发现方向错了。所以大家动手前先搞清楚目标芯片在 USB 协议栈中的真实身份再去选现成驱动还是自研驱动这一步价值千金。另外如果只是为了临时调试、不打算产品化那直接用现成 USB Hub 插电脑是最省事的但如果你想做的是可量产的多路串口网关设备那么把 STM32 USB Host、CH340 私有驱动、网络协议栈整合在一起软硬件成本都能压到最低。最后再分享一个小技巧量产阶段我会在每路 CH340 的通道管理结构体里保存一份“设备指纹”包括 VID/PID、Hub 端口号、实例使能标志这样现场排查问题的时候能直接通过串口命令行查询每路设备的状态效率会高很多。
