ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

GD32H759 RT-Thread USB CDC ACM虚拟串口实战

GD32H759 RT-Thread USB CDC ACM虚拟串口实战 1. 项目背景与需求拆解1.1 为什么要在工控板子上折腾USB虚拟串口做过工控设备的朋友都知道调试口和通信口是两码事。调试口通常就是UART转USB芯片比如CH340、CP2102这类插上电脑装个驱动就能看log。但产品一旦出货客户可不想在设备外面再挂一个USB转串口小板子他们希望一根USB线插上去既能看调试信息又能下发控制指令最好还能给设备升级固件。这就是USB CDC ACM出场的时候了。CDC是Communication Device Class的缩写ACM是Abstract Control Model说白了就是让MCU的USB外设模拟成一个标准的串口设备。电脑端不需要装任何厂商驱动Windows 10以上自带usbser.sysLinux和macOS更是原生支持。插上去就是一个COM口或者/dev/ttyACMx用串口助手就能直接通信。我这次用的硬件平台是GD32H759这是国产MCU里性能相当猛的一颗Cortex-M7内核主频跑到600MHz带USB HS外设。软件层面跑的是RT-Thread国产RTOS里生态最完善的一个。选这个组合的原因很简单工控场景对实时性和可靠性要求高RT-Thread有完整的设备驱动框架USB协议栈也有现成的CDC ACM类驱动不用从零造轮子。这一篇要解决的问题很具体在GD32H759上基于RT-Thread的USB Device协议栈把CDC ACM虚拟串口跑通并且要能稳定收发数据能跟工控上位机做双向通信。适合谁看适合那些已经能把RT-Thread跑起来、但对USB协议栈还比较陌生的嵌入式工程师也适合正在选型工控通信方案的架构师。1.2 工控场景下虚拟串口的真实需求工控现场跟消费电子不一样几个硬性要求摆在那里。第一是稳定性设备可能连续运行几个月不重启USB连接不能动不动就掉线。第二是实时性下发一条控制指令从上位机到MCU响应延迟要可控。第三是兼容性客户的电脑可能是Win7、Win10、Win11也可能是各种Linux发行版虚拟串口要能即插即用。CDC ACM这个方案恰好能满足这三点。它是USB-IF定义的标准类操作系统原生支持不需要额外驱动。USB 2.0 Full Speed下理论带宽12Mbps实际有效吞吐做到几百KB/s没问题对于工控指令和日志传输绰绰有余。而且RT-Thread的USB Device协议栈把CDC ACM的枚举、配置、端点管理都封装好了我们只需要关注数据收发逻辑。但这里有个坑要注意GD32H759的USB外设跟STM32的USB外设寄存器不兼容RT-Thread的驱动层需要做适配。我一开始直接拿STM32的CDC ACM例程往GD32H759上搬枚举都过不了后来查手册才发现USB PHY的配置寄存器地址完全不一样。这个后面会详细说。2. 技术选型与方案设计2.1 GD32H759的USB外设能力摸底GD32H759带的是USB HSHigh Speed外设但也可以配置成FSFull Speed模式。HS模式下需要外接ULPI PHY芯片FS模式下可以直接用内部PHY。工控板子为了省成本和简化设计通常用FS模式就够了。我这次也是用的FS模式内部PHYD和D-直接接到USB座子上。这里有个细节GD32H759的USB FS内部PHY需要配置一个校准寄存器叫USB_HS_PHY_CTL或者类似的具体名字我记不太清了反正是在RCU或者USB全局寄存器里。如果不配置USB信号质量会很差枚举时好时坏。我实测下来这个校准值跟PCB走线和晶振精度都有关系需要根据实际情况微调。另外GD32H759的USB中断向量和STM32不一样RT-Thread的USB驱动里中断服务函数的名字要改。这个在移植的时候要特别注意不然编译能过但中断进不去USB枚举直接卡死。2.2 RT-Thread USB Device协议栈的架构RT-Thread的USB Device协议栈分三层最底层是USB Device Controller驱动跟具体MCU相关中间是USB Core层处理标准USB请求和描述符最上面是Class层CDC ACM就属于这一层。CDC ACM类在RT-Thread里对应的是usbdevice_cdcacm组件它实现了两个接口一个是通信接口用来传控制指令一个是数据接口用来传实际数据。数据接口有两个端点一个IN一个OUT都是批量传输端点。批量传输的好处是不占带宽有数据就传没数据就不传适合工控这种突发性通信。RT-Thread的CDC ACM驱动还实现了一个虚拟串口设备注册到RT-Thread的设备框架里应用层可以用rt_device_open、rt_device_read、rt_device_write这些标准接口来操作跟操作普通UART一模一样。这个设计很舒服应用层代码不用关心底层是USB还是UART。2.3 为什么不用HID或者自定义类有人可能会问为什么不用HID做通信HID免驱兼容性更好。但HID有报告描述符的限制单次传输数据量小而且带宽优先级低工控场景下传大块数据或者要求实时响应的时候HID就不够用了。自定义类呢自定义类需要装驱动客户现场装驱动是个麻烦事尤其是工控机可能没有管理员权限。CDC ACM免驱这个优势在工控场景下太重要了所以最终选它。3. 核心细节解析与实操要点3.1 硬件连接与时钟配置GD32H759的USB FS内部PHY需要48MHz时钟这个时钟可以从PLL分频得到也可以用外部晶振。我板子上用的是25MHz晶振PLL配置成600MHz系统时钟然后分频出48MHz给USB。这里要注意USB时钟必须是精确的48MHz偏差超过0.25%就会导致枚举失败。时钟配置的代码大概长这样/* 使能USB时钟 */ rcu_periph_clock_enable(RCU_USBHS); /* 配置USB PHY时钟 */ rcu_usbhs_phy_clock_config(); /* 等待时钟稳定 */ while(0 rcu_flag_get(RCU_FLAG_USBHS_PHY_CLK));这段代码是我根据GD32H7的固件库手册写的实际寄存器名字可能略有出入但逻辑就是这样。配置完时钟之后还要配置USB PHY的校准值这个值在GD32的固件库里有默认推荐值但最好根据实际信号质量调整。3.2 RT-Thread USB驱动移植的关键修改点RT-Thread官方仓库里有GD32的USB驱动但主要是针对GD32F系列GD32H759的USB HS外设跟F系列差别很大。我移植的时候主要改了三个地方第一是中断向量表。GD32H759的USB HS中断向量号跟F系列不一样需要在rt_hw_usb_init里改成正确的向量号。这个查一下GD32H759的用户手册就能找到。第二是端点FIFO分配。GD32H759的USB HS有专用的FIFO RAM每个端点的FIFO大小需要手动配置。CDC ACM需要三个端点控制端点0、数据IN端点、数据OUT端点。控制端点0的FIFO至少要64字节数据端点各分配512字节比较稳妥。第三是PHY初始化。GD32H759的USB HS PHY需要额外的初始化序列包括设置HS模式还是FS模式、配置内部PHY还是外部PHY、设置校准值等。这部分代码在RT-Thread的驱动里原本是没有的需要自己加。3.3 CDC ACM描述符的配置细节CDC ACM的USB描述符包括设备描述符、配置描述符、接口关联描述符、通信接口描述符、数据接口描述符、端点描述符等。这些描述符在RT-Thread的CDC ACM驱动里都有模板但有几个参数需要根据实际情况改。VID和PID可以随便填但建议不要用默认的免得跟其他设备冲突。我一般用0x1234和0x5678这种测试用的值。字符串描述符里可以填厂商名和产品名上位机识别设备的时候会显示这些信息。还有一个细节CDC ACM的通信接口里有一个Call Management描述符和一个Abstract Control Management描述符这两个描述符决定了设备支持哪些控制指令。如果不需要支持调制解调器指令可以把Call Management描述符里的参数设成0这样上位机就不会发那些没用的指令过来。4. 实操过程与核心环节实现4.1 环境搭建与工程配置我用的开发环境是RT-Thread Studio基于Eclipse的用起来还算顺手。新建工程的时候选GD32H759的BSP然后在外设配置里勾选USB Device和CDC ACM。RT-Thread Studio会自动把相关的驱动和组件加进来省了不少事。但自动生成的工程里USB时钟配置和PHY初始化是不完整的需要手动补。我是在board.c的rt_hw_board_init函数里加的USB时钟配置在USB驱动初始化之前调用。工程配置里还要注意堆栈大小。USB协议栈和CDC ACM驱动会用到不少栈空间尤其是中断服务函数里。我把主线程栈设成4096字节USB中断的栈也设成2048字节实测下来比较稳。4.2 USB CDC ACM初始化的完整流程初始化流程分几步走。第一步是初始化USB Device Controller包括时钟、PHY、中断等。第二步是初始化CDC ACM类注册虚拟串口设备。第三步是启动USB设备等待枚举。代码大概长这样/* 第一步初始化USB设备控制器 */ rt_usb_device_init(); /* 第二步初始化CDC ACM */ rt_usb_cdcacm_init(); /* 第三步启动USB设备 */ rt_usb_device_start();实际代码里rt_usb_device_init会调用GD32H759的USB驱动初始化函数rt_usb_cdcacm_init会创建虚拟串口设备并注册到设备框架。启动之后USB设备会等待主机枚举枚举成功后虚拟串口设备就可以读写了。这里有个坑枚举过程是异步的rt_usb_device_start返回的时候枚举可能还没完成。如果这时候就去写数据会丢包。我的做法是在应用层等一个信号量USB驱动在枚举完成的中断里释放这个信号量应用层等到信号量之后再开始通信。4.3 数据收发测试与性能调优枚举成功之后我用串口助手打开虚拟串口波特率随便设CDC ACM的波特率是虚拟的设多少都行。然后发了一条测试指令MCU收到之后回了一条响应通信正常。但测试大块数据传输的时候发现吞吐量上不去只有几十KB/s。查了一下原因是RT-Thread的CDC ACM驱动里每次读写只处理一个USB包没有做批量处理。USB FS的批量端点最大包长是64字节每次中断只传64字节中断频率太高CPU开销大。优化方法是在驱动里加一个缓冲区中断里把数据存到缓冲区应用层从缓冲区读。写的时候也是先写到缓冲区攒够一定数量再通过USB发出去。这样吞吐量能提升到几百KB/s满足工控需求。5. 常见问题与排查技巧实录5.1 枚举失败问题排查枚举失败是最常见的问题表现是插上USB线之后电脑没反应或者提示“未知USB设备”。排查思路如下现象可能原因排查方法电脑完全没反应USB时钟没配置用示波器测USB时钟引脚应该是48MHz提示未知USB设备描述符配置错误用USB分析仪抓包看描述符返回是否正确枚举时好时坏PHY校准值不对调整PHY校准寄存器观察信号质量枚举成功但无法通信端点FIFO配置错误检查端点FIFO分配确保大小足够我遇到过一次枚举时好时坏的问题折腾了好久最后发现是USB座子的D上拉电阻没焊好虚焊了。所以硬件问题也要排查别光盯着软件。5.2 数据丢包问题排查数据丢包的表现是上位机发的数据MCU收不全或者MCU发的数据上位机收不全。原因通常有三个一是缓冲区太小数据溢出二是中断优先级配置不当USB中断被其他中断打断三是应用层读写速度跟不上。我的做法是把USB中断优先级设成最高确保USB数据能及时处理。然后在驱动里加环形缓冲区应用层用单独的线程读写避免在中断里做耗时操作。环形缓冲区的大小根据实际数据量调整我一般设成4KB。5.3 长时间运行稳定性问题工控设备要求长时间稳定运行USB连接不能掉线。我做过一个72小时连续运行测试中间发现过几次掉线排查下来是USB挂起Suspend导致的。USB协议规定如果主机3ms内没有传输设备可以进入挂起状态。但有些工控机的USB主控器会主动发挂起信号设备如果响应不当就会掉线。解决方法是在USB驱动里正确处理挂起和恢复中断挂起的时候不要关闭USB时钟恢复的时候重新初始化端点。另外可以在应用层定期发心跳包保持USB连接活跃。5.4 上位机兼容性问题不同操作系统的USB CDC ACM驱动行为有差异。Windows 10以上自带usbser.sys即插即用。Windows 7需要装一个补丁或者用厂商提供的INF文件。Linux下通常是/dev/ttyACM0但有些发行版需要把用户加到dialout组才能访问。macOS下CDC ACM设备是/dev/tty.usbmodemXXXX用screen或者minicom都能打开。但macOS对USB描述符的校验比较严格如果描述符里有不合规的地方macOS会拒绝枚举。我遇到过一次macOS枚举失败最后发现是字符串描述符的编码格式不对改成UTF-16LE就好了。6. 实操心得与避坑指南6.1 调试工具的选择与使用调试USB问题光靠打印log是不够的最好有一个USB分析仪。我用的是某品牌的USB协议分析仪能抓取USB总线上的所有包包括令牌包、数据包、握手包。枚举失败的时候抓包一看就知道是哪个描述符返回错了或者哪个握手包没响应。如果没有USB分析仪也可以用软件工具比如Windows上的USBView能查看设备描述符和配置描述符。Linux下用lsusb -v也能看到详细的描述符信息。这些工具虽然不如硬件分析仪强大但排查大部分问题够用了。6.2 代码结构组织建议USB CDC ACM的代码最好分层组织。底层是USB驱动跟硬件相关中间是CDC ACM类驱动跟USB协议相关上层是应用逻辑跟业务相关。这样分层之后换MCU或者换RTOS的时候只需要改底层驱动上层代码不用动。我在项目里把USB驱动放在drivers目录CDC ACM类驱动放在components目录应用逻辑放在applications目录。每个目录下再按功能分子目录结构很清晰。6.3 性能测试与优化经验性能测试我主要测三个指标吞吐量、延迟、CPU占用率。吞吐量用大文件传输测试延迟用ping-pong测试CPU占用率用RT-Thread的CPU使用率统计功能。优化的时候优先优化中断处理。USB中断里只做最必要的事情比如把数据从端点FIFO搬到内存缓冲区其他事情交给线程处理。另外批量传输的包长可以适当调大USB FS最大64字节USB HS最大512字节包长越大中断频率越低CPU占用率越小。6.4 工控现场部署注意事项工控现场电磁环境复杂USB线缆要选带屏蔽的长度不要超过3米。如果设备外壳是金属的USB座子要做好接地不然容易受干扰。另外USB供电要稳定工控机的USB口供电能力参差不齐最好用带电源的USB Hub或者设备自己供电。还有一点工控现场经常有热插拔的需求USB设备要能正确处理插拔事件。RT-Thread的USB驱动里有插拔检测机制但需要配置VBUS检测引脚。如果硬件上没有VBUS检测可以在软件里定期检查USB连接状态掉线了自动重新初始化。7. 后续扩展方向CDC ACM跑通之后还可以做几个扩展。一个是复合设备把CDC ACM和MSC大容量存储组合在一起插上USB之后既能看到串口又能看到U盘方便现场升级固件和导出日志。另一个是RNDIS或者ECM把USB虚拟成网卡工控设备通过USB跟上位机做网络通信速度比串口快得多。还有一个方向是USB DFU设备固件升级通过USB直接给MCU升级固件不用拆机。GD32H759支持USB DFURT-Thread也有DFU的组件结合起来用很方便。我在实际项目里最深的体会是USB协议栈的调试硬件和软件要一起查。很多时候问题出在硬件上比如时钟不准、信号质量差、上拉电阻不对但表现却是软件层面的枚举失败。所以手边最好有示波器和USB分析仪能省很多排查时间。另外RT-Thread的USB驱动虽然好用但针对具体MCU的适配工作少不了尤其是国产MCU寄存器跟STM32不兼容的地方很多要有耐心一点点啃手册。
RELATED READING

延伸阅读

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