☰
RT-Thread USB CDC ACM实战:GD32H759工控虚拟串口开发指南
2026/10/18 1:04:30 网站建设 项目流程

1. 项目背景与核心需求拆解

1.1 为什么要在工控场景下折腾USB CDC ACM

先说说这个项目的来龙去脉。手头这块GD32H759是兆易创新的一款高性能MCU,Cortex-M7内核,主频能跑到600MHz,外设资源相当丰富。整个系列文章围绕RT-Thread实时操作系统在这颗芯片上的工控实战展开,前面几篇分别搞定了环境搭建、GPIO、串口、定时器、ADC、PWM、SPI Flash这些基础外设。到了第8篇,轮到USB CDC ACM了。

CDC ACM全称Communication Device Class - Abstract Control Model,说白了就是让MCU通过USB接口模拟成一个串口设备。你插上USB线,电脑上就多出来一个COM口,用串口助手就能收发数据。这个功能在工控领域太常见了——设备调试口、参数配置口、日志输出口,用USB虚拟串口比传统UART转USB芯片方案省一颗CH340或者CP2102,BOM成本直接降下来,PCB面积也省了。

但为什么要在RT-Thread上做这件事?因为工控设备不是跑裸机就完事了。多任务调度、设备管理、协议栈分层,这些都需要RTOS来撑。RT-Thread本身对USB协议栈有支持,但GD32H759这颗芯片的USB外设跟STM32系列有差异,官方RT-Thread的BSP包不一定能直接跑通,需要自己适配。这就是这篇要解决的核心问题。

1.2 目标读者与前置知识

这篇内容适合谁看?如果你正在用GD32系列MCU做项目,或者打算从STM32迁移到GD32平台,又或者你已经在RT-Thread上跑通了基础外设但还没碰USB,那这篇就是给你准备的。前置知识方面,你需要了解RT-Thread的基本操作(线程创建、信号量、设备模型),知道USB的基本概念(端点、描述符、枚举),会用Keil或者IAR这类IDE。如果这些还不熟,建议先翻翻前面几篇的基础内容。

我假设你已经有一个能跑的RT-Thread工程,GD32H759的BSP已经配好,串口控制台能正常输出。接下来我们要做的,就是把USB Device功能加进去,让它枚举成一个CDC ACM设备。

1.3 方案选型:RT-Thread USB Device协议栈 vs 裸机USB库

在RT-Thread上实现USB CDC ACM,有两条路可以走。第一条是用RT-Thread自带的USB Device协议栈,这是RT-Thread官方维护的组件,跟RT-Thread的设备框架深度集成,用起来比较统一。第二条是直接用GD32官方的USB库,绕过RT-Thread的USB框架,自己封装成RT-Thread设备。

我选的是第一条路,原因有几个。RT-Thread的USB Device协议栈已经实现了CDC ACM的类驱动,你只需要提供底层硬件操作接口就行,上层几乎不用动。而且它跟RT-Thread的设备模型打通了,注册之后可以直接用rt_device_find、rt_device_open这些标准接口操作,跟用普通串口一样。如果用GD32官方库,虽然底层控制更直接,但需要自己处理枚举过程、描述符管理、端点配置这些细节,工作量翻倍不说,还容易出bug。

当然,RT-Thread的USB协议栈也不是没有代价。它对底层驱动的抽象层要求比较严格,GD32H759的USB外设跟STM32的USB外设在寄存器层面有差异,需要仔细对照手册来适配。但这是一次性的工作,适配好了之后,上层应用开发就很舒服了。

2. USB CDC ACM核心原理与RT-Thread协议栈架构

2.1 USB CDC ACM的工作机制

USB CDC ACM本质上是一个USB设备类规范。USB设备插到主机上之后,主机会先读取设备描述符,知道这是一个CDC类的设备,然后加载对应的类驱动。在Windows上,系统自带的usbser.sys就是CDC ACM的驱动,所以不需要额外装驱动就能识别成串口。Linux上则是cdc_acm模块。

CDC ACM设备内部有两个接口:一个通信接口(Communication Interface),一个数据接口(Data Interface)。通信接口用来传输控制命令,比如设置波特率、握手信号这些;数据接口用来传输实际的数据。每个接口下面有若干个端点,通信接口通常用一个中断端点来上报状态,数据接口用两个批量端点分别做收发。

枚举过程中,主机会发送一系列标准请求,比如GET_DESCRIPTOR、SET_CONFIGURATION、SET_LINE_CODING等。设备端需要正确响应这些请求,主机才会认为设备正常工作。RT-Thread的USB Device协议栈已经把这些标准请求的处理都封装好了,你只需要提供描述符和底层端点操作。

2.2 RT-Thread USB Device协议栈的分层结构

RT-Thread的USB Device协议栈大致分三层。最底层是硬件驱动层,也就是我们要适配的部分,负责操作GD32H759的USB外设寄存器,处理中断,完成端点的数据收发。中间是协议栈核心层,处理USB标准请求、设备状态管理、端点调度。最上层是类驱动层,CDC ACM的类驱动就在这一层,它实现了CDC ACM规范定义的所有请求处理和数据通道管理。

这种分层的好处是,换一款MCU只需要改硬件驱动层,上面的协议栈和类驱动完全不用动。RT-Thread官方已经为STM32、GD32等系列提供了部分BSP的USB驱动,但GD32H759这颗比较新,可能需要自己动手。

2.3 GD32H759 USB外设的特点

GD32H759内部集成了一个USB 2.0 OTG控制器,支持高速和全速模式。在CDC ACM场景下,全速模式(12Mbps)就够用了,虚拟串口的波特率可以设到很高,实际传输速度受限于USB全速的带宽,大概能跑到1MB/s左右,比传统UART快得多。

这颗芯片的USB外设寄存器布局跟STM32F4系列的USB OTG有些相似,但不完全一样。比如端点FIFO的分配方式、中断标志位的定义、DMA控制的细节都有差异。适配的时候不能直接照搬STM32的代码,必须对照GD32H759的用户手册来改。

另外,GD32H759的USB外设时钟来源需要配置正确。USB OTG需要48MHz的时钟,这个时钟通常由PLL提供。在RT-Thread的时钟初始化代码里,需要确保USB时钟被正确使能,否则USB外设根本不工作。

3. 硬件驱动层适配实操

3.1 工程目录结构与文件准备

在开始写代码之前,先把工程目录理清楚。RT-Thread的USB Device协议栈在rt-thread/components/drivers/usb/usb_device目录下。我们需要在这个目录下新建一个GD32H759的硬件驱动文件,比如叫usb_dc_gd32h7.c。同时,在BSP的board目录下,需要配置USB相关的引脚和时钟。

具体来说,GD32H759的USB OTG引脚通常是PA11(DM)和PA12(DP),这两个引脚需要配置成复用功能,并且使能内部上拉。有些封装可能引脚不同,需要根据实际原理图确认。时钟方面,USB OTG的48MHz时钟来自PLL,需要在system_gd32h7xx.c或者BSP的时钟配置函数里确保PLL配置正确。

3.2 端点FIFO分配策略

USB OTG的端点FIFO是一块共享的RAM区域,需要手动分配给各个端点。GD32H759的USB外设总FIFO大小是4KB左右,具体数值查手册。CDC ACM需要用到三个端点:一个中断端点(EP1 IN)用于通信接口的状态上报,一个批量输出端点(EP2 OUT)用于接收主机发来的数据,一个批量输入端点(EP2 IN)用于向主机发送数据。

FIFO分配的原则是:批量端点需要较大的FIFO来保证吞吐量,中断端点可以小一些。我一般给EP1 IN分配64字节,EP2 OUT分配512字节,EP2 IN分配512字节。剩下的FIFO留给其他可能用到的端点。分配的时候要注意FIFO地址不能重叠,每个端点的FIFO起始地址需要按4字节对齐。

注意:FIFO分配错误是USB枚举失败最常见的原因之一。如果设备插上电脑后没有任何反应,或者提示“设备描述符请求失败”,优先检查FIFO分配。

3.3 中断处理与端点数据收发

USB中断的处理是整个驱动层最核心的部分。GD32H759的USB外设有多个中断向量,包括OTG_FS_IRQHandler等。在中断服务函数里,需要读取中断标志位,判断是哪种事件,然后分别处理。

常见的中断事件包括:枚举完成(ENUMDONE)、端点输入完成(EPx_IN)、端点输出完成(EPx_OUT)、USB复位(USBRST)、唤醒(WKUP)等。对于CDC ACM来说,最重要的是端点输入和输出中断。当主机发来数据时,EP2 OUT端点会产生中断,我们需要从FIFO里把数据读出来,放到RT-Thread的接收缓冲区里。当我们要发送数据时,把数据写入EP2 IN的FIFO,然后触发发送。

这里有个细节:GD32H759的USB外设支持DMA,但RT-Thread的USB Device协议栈默认用的是中断模式。如果数据量大,中断模式可能会丢数据,这时候可以考虑启用DMA。但DMA的配置更复杂,建议先把中断模式跑通,再考虑优化。

3.4 描述符配置与枚举过程

描述符是USB设备的“身份证”,主机通过读取描述符来了解设备的能力。CDC ACM需要提供设备描述符、配置描述符、接口描述符、端点描述符、字符串描述符等。RT-Thread的USB Device协议栈已经提供了CDC ACM的默认描述符模板,我们只需要修改VID、PID、厂商字符串、产品字符串这些信息。

VID和PID可以自己定义,但要注意不要跟已有设备冲突。如果是内部项目,随便设一个不常见的值就行。字符串描述符需要用UTF-16LE编码,RT-Thread的协议栈会自动处理编码转换,我们只需要提供ASCII字符串。

枚举过程中,主机会先读取设备描述符的前8个字节,获取端点0的最大包长,然后再读取完整的设备描述符。接着读取配置描述符,设置配置,然后读取字符串描述符。每一步都需要设备正确响应,任何一步出错都会导致枚举失败。

4. 应用层开发与调试

4.1 注册CDC ACM设备到RT-Thread设备框架

硬件驱动层适配好之后,接下来就是把CDC ACM设备注册到RT-Thread的设备框架里。RT-Thread的USB Device协议栈提供了rt_usb_device_cdc_acm_register之类的接口,调用之后会创建一个名为“vcom”的设备。之后就可以用标准设备接口来操作了。

注册的时机很重要。一般是在USB中断里检测到枚举完成事件后,再调用注册函数。如果注册太早,设备还没枚举成功,注册会失败。注册太晚,主机可能已经超时了。

4.2 数据收发线程设计

注册成功之后,就可以像操作普通串口一样操作虚拟串口了。我一般会创建一个独立的线程来处理数据收发。接收方向,用rt_device_read从虚拟串口读数据,然后根据协议解析。发送方向,用rt_device_write往虚拟串口写数据。

这里有个坑:RT-Thread的USB Device协议栈在接收数据时,默认是阻塞模式。如果接收缓冲区满了,rt_device_read会阻塞,直到有数据可读。如果不想阻塞,可以设置成非阻塞模式,用信号量或者消息队列来通知。

4.3 与工控协议的对接

在工控场景下,虚拟串口通常用来传输Modbus、自定义二进制协议等。以Modbus RTU为例,虚拟串口收到的数据需要先做帧同步,然后解析功能码、寄存器地址、数据长度,最后执行相应的操作。发送响应时,按照Modbus RTU的格式组帧,通过虚拟串口发回去。

由于USB虚拟串口的传输速度远高于传统UART,Modbus RTU的3.5字符间隔在USB上其实不太适用。实际项目中,我一般会在应用层做超时判断,而不是依赖物理层的间隔。

4.4 调试工具与常见问题排查

调试USB CDC ACM,最常用的工具就是串口助手。Windows上可以用SSCOM、XCOM这些,Linux上直接用minicom或者screen。插上USB线之后,如果设备管理器里能看到“USB串行设备”并且分配了COM口,说明枚举成功了。

如果枚举失败,排查思路是这样的:先看设备管理器里有没有未知设备,如果有,说明描述符有问题;如果连未知设备都没有,说明硬件连接或者时钟配置有问题。可以用USB分析仪抓包,看看枚举卡在哪一步。没有分析仪的话,可以在代码里加打印,看看中断有没有触发,端点有没有收到数据。

5. 常见问题与避坑经验

5.1 枚举失败问题速查表

现象可能原因排查方法
设备管理器无反应USB时钟未使能检查PLL配置和USB时钟使能位
提示“设备描述符请求失败”端点0 FIFO配置错误检查EP0 FIFO大小和地址
提示“未知USB设备”描述符内容错误用USB分析仪抓包对比标准描述符
枚举成功但无法收发数据端点FIFO分配冲突检查各端点FIFO地址是否重叠
数据收发不稳定中断优先级配置不当调整USB中断优先级,避免被其他中断打断

5.2 实操心得与注意事项

第一个坑是时钟配置。GD32H759的USB OTG需要精确的48MHz时钟,如果PLL配置有偏差,USB枚举会不稳定甚至失败。我建议用示波器或者频率计测量一下USB时钟输出,确保在48MHz±0.25%以内。

第二个坑是中断优先级。USB中断的优先级不能太低,否则在大量数据传输时可能丢包。但也不能太高,否则会影响其他关键中断的响应。我一般把USB中断设成中等优先级,比SysTick低,比普通外设中断高。

第三个坑是FIFO分配。前面提过,FIFO分配错误是枚举失败的主要原因。我的经验是,先把所有端点的FIFO需求列出来,算一下总大小,确保不超过USB外设的总FIFO容量。然后按照端点的重要性分配,批量端点优先。

第四个坑是字符串描述符的编码。RT-Thread的协议栈要求字符串描述符用UTF-16LE编码,如果直接填ASCII字符串,主机会显示乱码。虽然不影响功能,但看起来不专业。建议用协议栈提供的转换函数来处理。

5.3 性能优化建议

如果虚拟串口的数据吞吐量比较大,可以考虑几个优化方向。一是启用USB DMA,减少CPU中断开销。二是增大端点FIFO,减少数据分包。三是优化应用层的数据处理流程,避免在USB中断里做耗时操作。

实测下来,在中断模式下,GD32H759的USB CDC ACM可以稳定跑到500KB/s左右。启用DMA之后,能到800KB/s以上。对于大多数工控场景,这个速度已经绰绰有余了。

6. 完整代码框架与关键片段

6.1 硬件驱动层初始化代码

#include <rtthread.h> #include <rtdevice.h> #include "usb_dc.h" /* USB OTG FS 初始化 */ rt_err_t usb_dc_init(void) { /* 使能USB时钟 */ rcu_periph_clock_enable(RCU_USBFS); /* 配置USB引脚 */ gpio_af_set(GPIOA, GPIO_AF_10, GPIO_PIN_11 | GPIO_PIN_12); gpio_mode_set(GPIOA, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_11 | GPIO_PIN_12); gpio_output_options_set(GPIOA, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_11 | GPIO_PIN_12); /* 复位USB外设 */ usb_global_reset(); /* 配置FIFO */ usb_fifo_config(USB_EP0, 64); usb_fifo_config(USB_EP1_IN, 64); usb_fifo_config(USB_EP2_OUT, 512); usb_fifo_config(USB_EP2_IN, 512); /* 使能中断 */ nvic_irq_enable(USBFS_IRQn, 6, 0); return RT_EOK; }

6.2 中断服务函数关键逻辑

void USBFS_IRQHandler(void) { rt_uint32_t intsts = usb_interrupt_flag_get(); /* 枚举完成 */ if (intsts & USB_INT_ENUMDONE) { usb_interrupt_flag_clear(USB_INT_ENUMDONE); rt_usb_device_cdc_acm_register(); } /* 端点输出中断 - 收到主机数据 */ if (intsts & USB_INT_EP2_OUT) { usb_interrupt_flag_clear(USB_INT_EP2_OUT); rt_uint32_t len = usb_ep_out_data_len_get(USB_EP2); usb_ep_read(USB_EP2, usb_rx_buffer, len); rt_mq_send(&usb_rx_mq, usb_rx_buffer, len); } /* 端点输入中断 - 数据发送完成 */ if (intsts & USB_INT_EP2_IN) { usb_interrupt_flag_clear(USB_INT_EP2_IN); rt_sem_release(&usb_tx_sem); } }

6.3 应用层数据收发示例

static void vcom_thread_entry(void *parameter) { rt_device_t vcom = rt_device_find("vcom"); rt_device_open(vcom, RT_DEVICE_FLAG_RDWR | RT_DEVICE_FLAG_INT_RX); rt_uint8_t buf[256]; while (1) { rt_size_t len = rt_device_read(vcom, 0, buf, sizeof(buf)); if (len > 0) { /* 处理收到的数据,比如解析Modbus帧 */ process_modbus_frame(buf, len); } } }

7. 后续扩展方向

这套USB CDC ACM的框架跑通之后,可以往几个方向扩展。一是增加第二个CDC ACM接口,实现复合设备,同时提供两个虚拟串口。二是把CDC ACM跟RNDIS结合起来,做成USB网卡加串口的复合设备。三是把虚拟串口跟无线模块对接,实现远程调试通道。

我在实际项目中还遇到过一种需求:设备正常工作时虚拟串口用来输出日志,进入配置模式后虚拟串口用来接收参数。这种场景下,需要在应用层做模式切换,根据当前模式决定数据的流向。实现起来不难,但要注意模式切换时的数据缓冲,避免丢数据。

最后分享一个小技巧:如果虚拟串口的数据量很大,建议在应用层加一个环形缓冲区,把USB中断收到的数据先存起来,再由低优先级的线程慢慢处理。这样可以避免在中断里做复杂操作,提高系统的实时性。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询