资讯详情

GD32H759 RT-Thread USB CDC ACM虚拟串口实战:从枚举到稳定通信

📅 2026/10/18 17:30:39 | 华诺云谱 👁 阅读
GD32H759 RT-Thread USB CDC ACM虚拟串口实战:从枚举到稳定通信
1. 为什么在工控板卡上折腾USB CDC ACM而不是继续用UART做工业控制这行时间长了你会发现一个很现实的问题调试口永远不够用。一块主控板上UART0接了HMI屏UART1挂了RS485总线UART2留给了一个Modbus从站等到你想再引一路出来打日志、传参数、做固件升级的时候引脚已经排满了。更麻烦的是很多工控现场机箱封死之后你根本不想再拧开盖子去接那三根杜邦线。我在几个基于GD32H759的项目里都遇到过这个场景。GD32H759这颗片子本身外设很足USB FS/HS控制器是片上自带的RT-Thread又对USB协议栈有比较完整的支持所以把USB口做成一个CDC ACM虚拟串口在PC端看起来就是一个标准的COM口这件事的性价比其实非常高。你不需要装驱动Windows 10以上自带usbser.inf不需要额外的USB转串口芯片一根Type-C线插上去设备管理器里就多出来一个串口波特率随便设实际速率远超物理UART。这篇内容就是围绕GD32H759 RT-Thread USB CDC ACM这个组合把从零跑通一个虚拟串口的过程拆开讲。适合两类人看一类是刚接触RT-Thread USB协议栈、想找个能落地的例程照着做的嵌入式工程师另一类是已经在用GD32H7系列、想把调试通道从UART迁移到USB的老手。我会把设备栈的注册逻辑、描述符的坑、收发缓冲的设计、以及实际联调时那些文档里不会写的问题都摊开说。先说结论USB CDC ACM在RT-Thread上跑通不难难的是跑稳。枚举成功只是第一步真正在生产环境里用起来你要处理的是热插拔、PC端串口软件打开关闭时的状态机跳变、以及大数据量下的流控问题。下面按我的实际调试顺序来展开。2. GD32H759的USB外设底子与RT-Thread USB设备栈的对接逻辑2.1 先搞清楚这颗片子的USB控制器能力边界GD32H759系列内部集成了USB FS全速12Mbps和USB HS高速480Mbps控制器其中HS控制器通常需要外接ULPI PHY才能跑到480Mbps而FS控制器可以直接用片内PHYD和D-直接引到连接器上。对于CDC ACM这种应用如果你只是做调试串口和参数传输FS的12Mbps其实已经够用了——换算下来理论峰值大概1.2MB/s实际能跑到700KB/s到900KB/s比115200的UART快了将近一个数量级。但这里有个选型上的细节要注意GD32H759的USB FS和USB HS在引脚复用上是分开的你在做原理图阶段就要确定用哪个。如果板子已经打样了只能看D D-接在哪组引脚上。我手上这块板子用的是FSPA11/PA12这也是最常见的接法。HS那组一般走ULPI引脚多布线要求也高除非你要做视频或者大容量存储否则CDC ACM没必要上HS。另外一个容易被忽略的点是VBUS检测。很多工控板为了省事USB座子的VBUS引脚直接悬空或者只接了个电容结果就是设备插拔的时候固件根本感知不到。RT-Thread的USB设备栈里USB_DEVICE_ATTACH和USB_DEVICE_DETACH这两个状态是靠VBUS或者SOF中断来驱动的。如果你不做VBUS检测至少也要保证在USB中断里能正确响应复位和挂起事件否则会出现插上去没反应拔了再插才行这种玄学问题。2.2 RT-Thread USB Device栈的分层结构RT-Thread的USB设备协议栈大致分三层最底下是USB Device Controller驱动层也就是usbd_dcd这一层跟具体芯片相关GD32的DCD驱动需要你根据RT-Thread的接口去适配中间是USB Device Core层负责端点管理、描述符解析、标准请求处理最上面是Class层CDC ACM就属于这一层它实现了USB CDC类规范里定义的通信接口和数据接口。CDC ACM这个类比较特殊它实际上由两个接口组成一个Communication Class Interface用来做线路控制、波特率设置这些管理功能另一个Data Class Interface用来真正传数据。每个接口下面挂不同的端点。通信接口通常用一个中断端点做通知数据接口用一对批量端点做收发。你在配置描述符的时候这两个接口要一起描述而且接口号、端点地址都不能冲突。RT-Thread里CDC ACM的实现在usbdevice_cdc.c这个文件里它已经帮你把标准请求、类请求、端点回调都搭好了框架。你要做的主要是两件事一是把DCD驱动对接好让底层能正确响应SETUP包和端点中断二是在应用层调用rt_usb_device_cdc_init之类的接口把设备注册进去然后拿到一个rt_device句柄像操作普通串口一样去读写。2.3 端点资源的分配与FIFO规划GD32H759的USB FS控制器内部有专门的PMAPacket Memory Area也就是端点FIFO缓冲区。这个区域大小有限FS模式下通常是1.25KB左右你要给每个用到的端点分配FIFO。CDC ACM至少要用到三个端点一个控制端点EP0双向一个中断端点IN用于通知一对批量端点IN和OUT用于数据。这里有个实操经验批量端点的FIFO不要分配得太小。我一开始按默认的64字节分配结果在PC端用串口助手连续发大包数据的时候经常出现丢包。后来把批量OUT端点的FIFO加到256字节IN端点也加到256字节丢包问题就消失了。原因是FS模式下批量端点的最大包长就是64字节但如果你的FIFO只有64字节那么当一个包还没被应用层读走下一个包就来了硬件层面就会NAK甚至覆盖。给FIFO留出几个包的余量能显著改善突发流量的处理能力。具体分配的时候EP0一般固定给64字节中断端点给16或32字节就够CDC通知包很小剩下的全部分给批量端点。GD32的USB库里有类似usbd_ep_fifo_config的接口RT-Thread的DCD驱动里也会有对应的配置入口你要根据实际使用的端点号去设置。3. 从零搭建CDC ACM设备的完整操作链路3.1 工程配置与依赖项检查在开始写代码之前先把RT-Thread的配置工具打开确认几个关键选项。在RT-Thread Components - USB - USB Device下面要勾选Enable USB Device然后在Class里面勾选CDC ACM device。同时Using USB Device Controller要选对GD32H759对应的DCD驱动如果官方没有现成的你可能需要自己移植一份。另外RT_USB_DEVICE_CDC这个宏要打开它决定了CDC类的代码是否参与编译。还有一个容易漏掉的是RT_USB_DEVICE_CDC_ACM有些版本的RT-Thread把它单独拆出来了不勾的话编译能过但注册的时候会找不到符号。堆栈方面USB设备线程的栈建议给到2048字节以上。CDC ACM在枚举阶段会处理不少描述符和字符串栈太小容易在枚举过程中HardFault。我实测下来1536字节在简单场景下能跑但一旦PC端发送了比较长的类请求就有风险。3.2 描述符的定制VID、PID和字符串描述符是USB枚举的灵魂也是新手最容易翻车的地方。RT-Thread的CDC ACM例程里通常给了一套默认描述符VID和PID是随便填的。你在实际项目里如果不想跟PC上已有的驱动冲突最好改成自己申请的VID/PID或者至少用一个不常见的组合。设备描述符里bDeviceClass要填0x02CDCbDeviceSubClass填0bDeviceProtocol填0。配置描述符里bNumInterfaces要填2因为CDC ACM有两个接口。接口关联描述符IAD在CDC ACM里是可选的但Windows对IAD比较敏感加上它枚举会更稳。字符串描述符里iManufacturer、iProduct、iSerialNumber这三个索引要跟字符串描述符数组对应上。我踩过一个坑字符串描述符的长度计算错了少算了一个字符结果Windows枚举到一半就报设备描述符请求失败。后来用USB抓包工具一看是字符串长度字段和实际数据对不上。这种问题在Linux下可能能容忍但Windows的USB栈比较严格一点错就拒绝枚举。提示改完描述符之后一定要用USB分析仪或者软件抓包工具确认一下枚举流程不要只看设备管理器有没有认出来。有些错误枚举能过但后续通信会不稳定。3.3 设备注册与线程模型的建立描述符准备好之后就可以在应用层注册CDC ACM设备了。典型的调用顺序是先初始化USB设备栈然后调用CDC ACM的初始化函数拿到rt_device句柄最后打开设备并设置接收回调。rt_device_t cdc_dev; cdc_dev rt_device_find(vcom); if (cdc_dev RT_NULL) { rt_kprintf(find vcom failed\n); return -RT_ERROR; } rt_device_open(cdc_dev, RT_DEVICE_FLAG_RDWR | RT_DEVICE_FLAG_INT_RX); rt_device_set_rx_indicate(cdc_dev, cdc_rx_callback);这里的vcom是设备名称RT-Thread的CDC ACM默认注册成这个名字。接收回调里不要做太重的处理最好只是发个信号量或者消息队列让专门的线程去读数据。因为USB的接收回调是在中断上下文或者USB线程上下文里执行的你如果在里面直接调用可能阻塞的API整个USB栈都会卡住。发送的时候直接用rt_device_write就行。但要注意rt_device_write返回的是实际写入的字节数如果底层FIFO满了它可能只写入一部分。所以对于大块数据你要么循环写直到写完要么自己做一个发送缓冲队列在USB IN端点完成中断里继续发下一包。3.4 收发缓冲区的设计环形缓冲还是消息队列在实际工控场景里USB CDC ACM收到的数据往往是变长的命令帧发送的可能是周期性的状态上报。我比较推荐的做法是接收方向用一个环形缓冲区USB回调里只管往环形缓冲里塞数据应用线程从环形缓冲里按帧解析发送方向用一个消息队列或者链式缓冲把待发送的数据块排队USB IN完成中断里触发下一块发送。环形缓冲的大小要根据你的最大帧长和突发量来定。我一般给到2KB到4KB。太小了容易在高速收发时溢出太大了浪费RAM。GD32H759的RAM很充裕但如果你还跑了其他中间件也要算总账。发送方向有个细节CDC ACM的批量IN端点在FS模式下最大包长64字节如果你一次要发1KB的数据底层会自动拆成16个包。但RT-Thread的CDC实现里rt_device_write可能不会帮你做这个拆分它只是把数据拷贝到发送缓冲然后启动第一次传输后续的包要在IN完成中断里继续发。所以你要确认你用的RT-Thread版本里CDC ACM的发送逻辑是否支持自动续传。如果不支持你就得自己在应用层分片每次最多写64字节然后在回调里写下一片。4. 联调阶段那些让人抓狂的典型问题与排查路径4.1 枚举成功但PC端串口打不开这个现象很常见设备管理器里能看到USB串行设备但用串口助手打开的时候提示拒绝访问或者端口被占用。原因通常有两个一是Windows自带的usbser驱动没有正确绑定二是CDC ACM的通信接口里线路控制相关的类请求没有正确响应。排查的时候先在设备管理器里看设备属性如果驱动是未知设备或者带了黄色感叹号那就是描述符或者类请求的问题。如果驱动正常但串口打不开那多半是SET_LINE_CODING这个类请求处理有问题。PC端在打开串口的时候会先发SET_LINE_CODING设置波特率、数据位、停止位如果你的固件没有正确ACK这个请求PC端就会认为设备没准备好。我在GD32H759上遇到过这个问题后来发现是DCD驱动里对SETUP包的处理有遗漏类请求被当成了标准请求。修复方法是在DCD的SETUP阶段中断里正确区分请求类型把类请求转发给CDC类层去处理。4.2 数据收发一段时间后卡死这个问题的表现是刚开始通信正常跑几分钟或者几十分钟之后PC端发数据没反应设备端也收不到。用调试器看USB线程可能卡在某个信号量上或者端点状态寄存器显示NAK。根因通常是端点FIFO溢出或者NAK超时没有恢复。FS模式下如果OUT端点的FIFO满了硬件会自动回NAKPC端会重试。但如果你的应用层长时间不读数据FIFO一直是满的PC端的重试次数超限之后有些主机会直接放弃这个传输甚至重新枚举。这时候如果你没有正确处理重新枚举设备就假死了。解决办法有两个层面一是应用层要保证及时读取数据不要在接收回调里做耗时操作二是可以在DCD驱动里加一个超时检测如果某个端点连续NAK超过一定次数就主动清一下FIFO或者触发一次端点复位。RT-Thread的USB栈里有一些调试宏打开之后可以打印端点状态变化对定位这类问题很有帮助。4.3 热插拔之后设备不重新枚举工控现场经常需要带电插拔USB线如果固件对DETACH事件处理不当第二次插上去的时候PC端就认不到设备了。这个问题的核心在于USB设备被拔出时DCD驱动要能检测到VBUS掉电或者SOF中断停止然后调用rt_usb_device_detach把设备状态复位。如果没做这一步设备栈还停留在已配置状态再次插入时就不会重新走枚举流程。GD32H759的USB控制器支持VBUS检测中断如果你硬件上把VBUS接到了检测引脚那就在中断里处理。如果硬件没接那就只能靠SOF中断超时来判断。RT-Thread的DCD驱动里一般会有SOF中断处理你可以在里面加一个计数器超过一定时间没有SOF就认为设备被拔出了。注意热插拔处理不好轻则枚举失败重则导致USB控制器进入异常状态需要复位整个USB外设才能恢复。所以在产品化阶段一定要把DETACH处理做扎实。4.4 波特率设置无效但数据能通CDC ACM的波特率其实是个名义参数USB传输本身不依赖波特率你设成9600还是115200实际传输速率都是一样的。但PC端的串口软件会把这个值显示出来有些上位机还会根据波特率做超时计算。所以即使波特率不影响实际通信你也要在SET_LINE_CODING请求里正确保存这个值并且在GET_LINE_CODING的时候返回回去。我见过一个案例固件里没有保存波特率PC端设置115200之后再读回来发现是9600上位机就认为设备没配置成功一直重试。这种问题不影响底层数据但会让上位机逻辑混乱。所以类请求的处理要完整该保存的字段都要保存。5. 把虚拟串口真正用起来性能调优与工程化建议5.1 吞吐量实测与瓶颈定位在GD32H759的FS模式下我用CDC ACM做过实测PC端用脚本连续发送1KB的数据包设备端收到后原样回传统计下来稳定在600KB/s到800KB/s之间。这个数字受几个因素影响PC端USB主控制器的调度、端点FIFO大小、应用层读取的及时性。如果你发现吞吐量明显偏低比如只有几十KB/s那就要检查是不是每个包都在等应用层处理。USB批量传输本身是异步的硬件可以在应用层还没读走上一个包的时候继续接收下一个包只要FIFO有空间。所以提高吞吐的关键是让FIFO尽量满应用层尽量快地消费。另一个瓶颈可能是RT-Thread的USB线程优先级。如果USB线程优先级太低被其他线程抢占端点中断得不到及时处理也会拉低吞吐。我一般把USB设备线程的优先级设得比普通应用线程高但比硬实时线程低。5.2 与工控协议栈的对接方式CDC ACM虚拟串口在系统里就是一个rt_device你可以把它当成普通串口来用。如果你的工控协议栈比如Modbus RTU、自定义帧协议是基于串口设备抽象写的那基本上不用改代码把设备名从uart2换成vcom就行。但有一点要注意USB CDC ACM的帧边界跟UART不一样。UART有字符间隔你可以用空闲中断来判断一帧结束USB是包传输每个包有明确的边界但包与包之间不一定对应你的协议帧。所以协议解析层最好用环形缓冲加状态机的方式不要依赖一次read就是一帧这种假设。我在一个项目里把Modbus RTU跑在CDC ACM上一开始按UART的思路每次read返回的数据就当一帧处理结果发现有时候两个Modbus帧被合并到一个USB包里有时候一个帧被拆到两个包里。后来改成环形缓冲加状态机解析问题就解决了。5.3 产品化阶段的几个加固点如果这个虚拟串口是要出货的有几个地方必须加固。第一是描述符的序列号每台设备最好有唯一的序列号这样PC端能区分不同的设备不会因为插了多台就混淆。序列号可以从MCU的UID或者外部Flash里读。第二是通信超时和错误恢复。USB通信虽然比UART可靠但也不是万无一失。应用层要有超时重传机制底层要有端点异常恢复机制。我一般会在USB线程里加一个看门狗式的检测如果发现端点状态异常超过一定时间就重新初始化USB设备栈。第三是功耗管理。工控设备有时候要求低功耗USB挂起状态要能正确进入和退出。RT-Thread的USB栈支持挂起回调你可以在挂起的时候关掉一些外设恢复的时候再打开。但要注意挂起期间如果有数据要发要等恢复之后再发否则会丢数据。5.4 调试工具与抓包方法最后说一下调试手段。USB问题如果只靠打印效率很低。我建议至少准备两种工具一个是软件抓包工具可以在PC端看到USB的枚举过程和传输内容另一个是硬件USB分析仪虽然贵一点但在排查电气层和协议层问题时无可替代。软件抓包在Windows上可以用Wireshark配合USBPcapLinux下直接用usbmon。抓包的时候重点看枚举阶段的描述符请求和响应以及数据传输阶段的端点号和包长。很多时候问题就藏在某个描述符字段或者某个端点的NAK响应里。如果条件有限至少要在固件里加足够的日志。RT-Thread的USB栈有调试开关打开之后会打印枚举状态机的变化。但要注意日志本身会占用USB带宽和CPU时间调试完之后要记得关掉否则会影响正常通信的性能。我在实际项目里从第一次跑通CDC ACM到最终稳定出货大概花了三周时间其中大部分时间都花在处理热插拔和异常恢复上。枚举成功真的只是开始后面那些边界情况才是真正考验人的地方。希望这些经验能让你少走一些弯路。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑