ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

GD32H759工控项目USB CDC ACM虚拟串口实现与RT-Thread实战

GD32H759工控项目USB CDC ACM虚拟串口实现与RT-Thread实战

1. 为什么在工控项目里要折腾 USB CDC ACM

搞工控的兄弟大多有个共识:串口这东西,永远不嫌多。调试口、通讯口、传感器数据口,一个项目下来 UART 经常被占得满满当当。尤其是用 GD32H759 这种高性能 MCU 做中高端工控设备时,UART 资源更是紧张——上位机要通讯、触摸屏要通讯、几个传感器模块也要通讯,等你把该接的都接上,发现调试口没地方放了。

这时候 USB CDC ACM 虚拟串口就是救命稻草。CDC 是 Communication Device Class 的缩写,ACM 是 Abstract Control Model,说白了就是让 MCU 通过 USB 接口模拟出一个标准串口。上位机插上 USB 线,设备管理器里直接多出一个 COM 口,用串口助手就能收发数据,跟操作物理串口一模一样,不需要装任何驱动(Windows 10 以上自带 CDC 驱动)。

我在一个工控采集项目里就吃过 UART 不够用的亏。当时板子上三个 UART 全被 RS485 和调试模块占了,临时想加个日志输出通道,翻遍原理图发现只剩 USB 口空着。后来用 RT-Thread 的 USB Device 框架把 CDC ACM 跑起来,从写代码到上位机收到数据,前后不到两小时。这个方案的好处很直接:不占 UART 资源、速率比普通串口高得多(全速 USB 12Mbps,实际跑个几百 KB/s 没问题)、上位机兼容性好、还能顺便给板子供电。

这篇文章适合两类人看:一类是正在用 GD32H759 或类似高性能 MCU 做工控项目,UART 资源紧张想找替代方案的;另一类是想在 RT-Thread 上把 USB Device 跑起来,但被各种描述符、端点配置搞得头大的。我会把整个实现过程拆开讲,包括 RT-Thread 的 USB 框架怎么配、CDC ACM 的描述符怎么写、收发数据怎么处理,以及我踩过的那些坑。

2. 先搞清楚 RT-Thread USB Device 框架的套路

2.1 框架分层与数据流向

RT-Thread 的 USB Device 框架分三层,从上到下依次是:设备类驱动层(CDC、HID、MSC 等)、USB Device Core 层(负责枚举、端点管理、事件分发)、硬件驱动层(对接具体 MCU 的 USB 外设寄存器)。这个分层设计的好处是,你换 MCU 只需要改硬件驱动层,上层设备类代码基本不用动。

数据流向是这样的:上位机发数据 → USB 硬件收到 → 硬件驱动层中断处理 → Core 层解析端点 → 分发到 CDC 类的接收回调 → 你的应用线程从环形缓冲区读数据。反过来发送时,应用线程调用 CDC 的发送接口 → Core 层打包 → 硬件驱动层写入端点 FIFO → USB 硬件自动发送。

理解这个流向很重要,因为调试的时候你要知道数据卡在哪一层。我遇到过上位机发数据但应用收不到的情况,最后查出来是 CDC 接收回调里的环形缓冲区满了,应用线程读得太慢。如果你不清楚数据流向,就会在硬件层瞎折腾。

2.2 GD32H759 的 USB 外设特点

GD32H759 带的是 USB FS(全速)和 USB HS(高速)控制器,做 CDC ACM 用 FS 就够了。它的 USB 外设有几个特点需要留意:端点 FIFO 是共享的,总共 1.25KB 左右,分配给各个端点时要算好;支持 DMA 模式,但 RT-Thread 的驱动默认用中断模式,数据量大时可以考虑改 DMA;中断优先级要配好,别被其他高优先级中断打断太久导致 USB 超时。

这里有个关键点:GD32H759 的 USB FS 控制器和某些外设共用引脚或时钟,配置前一定要看数据手册的引脚复用表。我在一个项目里就遇到过 USB 引脚和 SPI 冲突,后来把 SPI 换到别的引脚才解决。

2.3 为什么选 CDC ACM 而不是其他方案

有人会问,虚拟串口方案那么多,为什么偏偏选 CDC ACM?我对比过几种常见方案:

方案驱动需求速率实现难度上位机兼容性
CDC ACM系统自带高中极好
自定义 HID系统自带低中需自己写上位机
自定义 Bulk需装驱动极高高需装驱动
USB 转串口芯片需装驱动中低好

CDC ACM 的优势在于免驱、速率够用、上位机直接用串口助手就能调。对于工控场景,调试和中等速率数据通讯完全够用。如果你要传大量数据(比如图像),那得考虑 USB HS 加自定义 Bulk 传输,但那是另一个话题了。

3. 动手前的准备工作

3.1 软件环境与版本选择

我用的环境是 RT-Thread 5.0.2 版本,GD32H759 的 BSP 包从官方仓库拉的。这里提醒一句:RT-Thread 的 USB Device 框架在不同版本间有 API 变化,4.1.x 和 5.0.x 的初始化流程略有不同。如果你用的是 4.1.x,rt_usb_device_init的调用方式要查对应版本的文档。

开发工具链我用的是 Keil MDK 5.38 加 ARM Compiler 6,也可以用 GCC 加 VSCode,看个人习惯。RT-Thread Studio 也是个选择,但它的工程管理有时候会抽风,我最后还是回到 Keil 手动管理工程。

需要确认的配置项:

  • RT_USB_DEVICE_CDC宏要打开
  • RT_USB_DEVICE_CDC_ACM子选项要打开
  • USB 设备栈的线程优先级要合理,别设太低被其他线程饿死
  • 堆栈大小给够,USB 描述符和缓冲区都占内存

3.2 硬件连接与注意事项

硬件上,GD32H759 的 USB FS 引脚是 PA11(DM)和 PA12(DP),这是标准配置。如果你用的是 USB HS,引脚不一样,要查手册。PCB 布线时注意几点:USB 差分线要等长、阻抗控制 90 欧姆、尽量少过孔、远离高频信号线。我见过一个板子 USB 死活枚举不了,最后发现是差分线走了两个过孔还跨了地平面分割,重新布线就好了。

供电方面,USB 设备模式是从 VBUS 取电,但 GD32H759 的 USB 控制器需要 3.3V 供电,所以板子上要有 LDO 或 DC-DC 把 5V 转 3.3V。如果板子自己供电,那 VBUS 检测引脚要处理好,不然设备模式识别不了。

注意:调试 USB 时,先确认硬件没问题再动软件。用示波器看 DM 和 DP 上有没有信号,或者用 USB 协议分析仪抓包。软件调半天最后发现是硬件问题,这种事我干过不止一次。

3.3 工程配置的关键步骤

在 RT-Thread 的 menuconfig 里,路径是RT-Thread Components → Device Drivers → Using USB,然后选Using USB Device,再选Enable USB CDC Device。保存后重新生成工程。

如果你用的是 Keil,还要在工程选项里把 USB 相关的源文件加进来。RT-Thread 的 SCons 构建系统会自动处理,但手动管理工程的话要自己加。需要加的文件包括usb_device.c、usb_cdc.c、usb_cdc_vcom.c以及 GD32 的 USB 底层驱动。

时钟配置也要注意,USB FS 需要 48MHz 时钟,GD32H759 的 USB 时钟源要选对。我一般用 PLL 分频得到 48MHz,具体分频系数看你的主频设置。

4. CDC ACM 描述符配置与代码实现

4.1 USB 描述符的结构与配置

USB 描述符是枚举的核心,主机通过读描述符来识别设备。CDC ACM 需要这几类描述符:设备描述符、配置描述符、接口描述符(两个接口:通讯接口和数据接口)、端点描述符、CDC 功能描述符、字符串描述符。

设备描述符里关键字段:idVendor和idProduct可以自定义,但别用知名厂商的 VID,不然可能被系统识别成别的设备。bDeviceClass设为 0x02(CDC),bDeviceSubClass设为 0x00,bDeviceProtocol设为 0x00。

配置描述符里,bNumInterfaces设为 2,因为 CDC ACM 需要通讯接口和数据接口各一个。通讯接口用中断端点上报状态,数据接口用 Bulk 端点收发数据。

端点分配上,我一般这样配:

  • 端点 1 IN:中断端点,用于通讯接口的状态上报,最大包 8 或 16 字节
  • 端点 2 IN:Bulk 端点,用于数据发送,最大包 64 字节(FS)
  • 端点 2 OUT:Bulk 端点,用于数据接收,最大包 64 字节

CDC 功能描述符(Union Functional Descriptor 和 Call Management Functional Descriptor)是 CDC 特有的,主机靠它来识别这是 CDC 设备。这部分描述符如果写错,设备管理器里可能显示成未知设备或者枚举失败。

4.2 初始化代码与线程配置

初始化流程分几步:先初始化 USB 硬件,再注册 CDC 设备,最后启动 USB 设备栈。代码大概长这样:

#include <rtthread.h> #include <rtdevice.h> #include "usb_device.h" #include "usb_cdc.h" static struct rt_serial_device serial; static struct usb_cdc_acm acm; int usb_cdc_init(void) { rt_usb_device_init(); usb_cdc_acm_init(&acm, "vcom"); return RT_EOK; } INIT_APP_EXPORT(usb_cdc_init);

这里usb_cdc_acm_init是 RT-Thread 封装好的接口,它会自动注册一个名为 "vcom" 的串口设备。之后你就可以用rt_device_open、rt_device_read、rt_device_write这些标准串口 API 来操作它。

线程配置上,我建议单独开一个线程处理 USB 数据收发,优先级设成中等偏上。别在 USB 回调里做耗时操作,回调里只做数据搬运,复杂处理放到线程里。

static void cdc_thread_entry(void *param) { char buf[128]; rt_size_t len; rt_device_t dev = rt_device_find("vcom"); rt_device_open(dev, RT_DEVICE_FLAG_RDWR); while (1) { len = rt_device_read(dev, 0, buf, sizeof(buf)); if (len > 0) { // 处理收到的数据 process_data(buf, len); } rt_thread_mdelay(10); } }

4.3 数据收发与缓冲区管理

数据发送比较简单,调用rt_device_write就行。但要注意,如果 USB 主机没准备好接收,写操作可能会阻塞或返回错误。我一般用非阻塞模式加超时处理。

数据接收是重点。RT-Thread 的 CDC 驱动内部有个环形缓冲区,USB 中断收到数据后先存到缓冲区,应用线程再从缓冲区读。缓冲区大小可以配置,默认好像是 512 字节。如果你的数据量大或者处理慢,要加大缓冲区,不然会丢数据。

我踩过一个坑:应用线程读数据太慢,缓冲区满了之后新数据直接丢弃,上位机那边看起来就是发出去的数据没反应。后来把缓冲区加到 2KB,并且优化了处理逻辑,问题解决。

提示:调试时可以在 CDC 接收回调里加个计数器,统计收到多少字节,和应用线程读到的字节数对比,就能判断是不是缓冲区溢出。

5. 上位机联调与性能测试

5.1 上位机识别与驱动安装

板子插上电脑后,Windows 10/11 会自动识别并安装 CDC 驱动,设备管理器里会出现一个 COM 口。如果没识别,先检查硬件连接,再看设备管理器里有没有未知设备。有未知设备的话,可能是描述符写错了,用 USB 协议分析仪抓包看枚举过程卡在哪一步。

Linux 下更简单,插上就有/dev/ttyACM0,直接用 minicom 或 picocom 就能通讯。macOS 下是/dev/tty.usbmodem*。

如果设备管理器里显示的是其他类型的设备(比如 HID),那说明描述符里的设备类字段配错了。CDC 的设备类应该是 0x02,配置描述符里的接口类也要对应。

5.2 通讯测试与性能数据

我用串口助手做了几轮测试,数据如下:

测试项结果
波特率设置任意值均可,CDC 不受波特率限制
单次发送 1KB正常,无丢包
连续发送 100KB正常,速率约 800KB/s
双向同时收发正常,互不干扰
长时间运行 24 小时稳定,无断连

这里有个细节:CDC ACM 的波特率设置其实是个"摆设",因为 USB 传输不依赖波特率。但有些上位机软件会检查波特率是否设置成功,所以驱动里要正确响应 SET_LINE_CODING 请求,返回成功就行。

性能方面,全速 USB 理论带宽 12Mbps,实际 Bulk 传输能跑到 8Mbps 左右,也就是 1MB/s 上下。我实测 800KB/s 左右,受限于 MCU 的处理速度和缓冲区管理。如果你要更高速度,得上 USB HS,但 GD32H759 的 HS 需要外接 ULPI PHY,硬件成本上去了。

5.3 稳定性与异常处理

长时间运行测试中,我遇到过几次 USB 断连,原因有几个:一是上位机休眠导致 USB 挂起,二是电源波动导致 USB 复位,三是代码里有内存越界把 USB 缓冲区踩了。

针对这些情况,代码里要做好异常处理:USB 断连回调里要清理状态,重新枚举后能自动恢复;接收缓冲区要做边界检查,防止越界;电源方面,如果板子自己供电,VBUS 检测要做好滤波。

RT-Thread 的 USB 框架有断连回调,可以注册一个回调函数,在断连时打印日志或者做状态复位。这样调试的时候能快速定位问题。

6. 常见问题排查与避坑经验

6.1 枚举失败问题速查

枚举失败是最常见的问题,表现是插上电脑没反应或者提示未知设备。排查思路按这个顺序来:

现象可能原因排查方法
完全没反应硬件连接问题查 DM/DP 电压,正常时 DP 上拉 3.3V
未知设备描述符错误用 USB 分析仪抓包看枚举卡在哪
识别成其他设备设备类字段错误检查 bDeviceClass 和接口类
枚举成功但无 COM 口CDC 功能描述符缺失检查 Union 和 Call Management 描述符
时好时坏电源或时钟不稳查 48MHz 时钟和 VBUS 供电

我遇到最多的是描述符错误,尤其是 CDC 功能描述符。这部分描述符不是标准的 USB 描述符,格式容易写错。建议直接参考 RT-Thread 的示例代码,别自己从头写。

6.2 数据丢包与乱码排查

数据丢包一般三个原因:缓冲区溢出、端点配置错误、上位机读取超时。乱码的话,先确认上位机的数据位、停止位、校验位设置,虽然 CDC 不受这些影响,但有些上位机软件会做检查。

我遇到过一次乱码,查了半天发现是上位机软件把 CDC 当成普通串口,设置了奇偶校验,而 CDC 驱动没正确处理这个请求。后来在驱动里把 SET_LINE_CODING 请求的响应改成固定返回成功,问题解决。

丢包问题我建议在应用层加个简单的协议,比如每包数据带长度和校验,这样能快速判断是丢包还是数据错误。

6.3 性能优化与资源占用

如果发现速率上不去,可以从这几个方面优化:加大 USB 缓冲区、提高 USB 中断优先级、用 DMA 模式传输、优化应用层数据处理逻辑。

资源占用方面,CDC ACM 大概占 4-6KB RAM(包括缓冲区和描述符),Flash 占用 8-10KB。对于 GD32H759 这种大容量 MCU 来说不算什么,但如果你同时跑很多功能,还是要算一下内存。

注意:USB 中断优先级别设太高,不然会影响其他实时性要求高的中断。我一般把 USB 中断设成中等优先级,比 systick 低,比普通外设高。

7. 几个实操中总结的小技巧

第一个技巧:调试 USB 时,先别急着写应用代码,用 RT-Thread 的 USB 示例代码把枚举跑通,确认硬件和驱动没问题,再改描述符和加应用逻辑。这样能把问题范围缩小。

第二个技巧:CDC 的接收回调里别做复杂处理,只做数据搬运。我见过有人在回调里做协议解析,结果 USB 中断被占用太久,导致主机端超时断连。

第三个技巧:如果项目里同时用 USB 和 SD 卡、以太网这些高速外设,注意总线带宽和中断优先级分配。GD32H759 的 USB FS 和以太网共用某些资源,配置时要查手册。

第四个技巧:产品化的时候,VID 和 PID 要申请正规的,别用示例里的默认值。字符串描述符里的厂商名和产品名也要改,不然设备管理器里显示的是示例名称,显得不专业。

第五个技巧:如果要做固件升级功能,CDC ACM 可以配合自定义协议实现,比 UART 快得多。我做过一个项目,通过 CDC 升级 1MB 固件,大概 3 秒就传完了,用 UART 得传一分多钟。

这套 CDC ACM 方案我在三个工控项目里用过,最长的已经稳定运行两年多,没出过 USB 相关的问题。关键还是描述符要配对、缓冲区要够大、异常处理要完善。把这三点做好,CDC ACM 在工控场景里是个非常可靠的通讯方案。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进