RK3568 Linux + GD32F303 双 USB CDC-ACM 虚拟串口 原理与编程实战总结
·
前言:
本项目基于瑞芯微 RK3568 Linux 主板作为 USB 主机,GD32F303CBT6 作为 USB 从设备,单 USB 物理链路实现双路独立 CDC-ACM 虚拟串口通信。
相较于传统物理 UART,USB 双虚拟串口布线简洁、无电平转换、通道隔离、扩展性强;
本文结合全程调试踩坑、硬件寄存器实测、Linux 驱动行为特性,整理双 CDC 核心原理、MCU 编程要点、Linux 主机适配、关键坑点解决方案,作为项目阶段性总结。
一、整体硬件架构
- 上位主机:RK3568(Linux 系统)
- 下位从机:GD32F303CBT6
- 通信链路:RK3568 USB Host → 直连 GD32 USB Device
- 设备形态:USB 复合设备(Composite Device)
- 最终节点:Linux 自动生成
/dev/ttyACM0、/dev/ttyACM1两路独立虚拟串口
二、双 CDC-ACM 核心基础原理
1. CDC-ACM 协议基础
- CDC:USB 通信设备类,专为串口类透传通信设计;
- ACM:抽象控制模型,是 Linux/Windows 系统原生识别虚拟串口 (VCOM) 的标准协议;
- 本质:利用 USB 控制端点 + 批量端点,封装串口帧数据,上层应用完全当作普通串口使用。
2. 双路 CDC 复合设备原理
- 单个 USB 物理设备,在设备描述符、接口描述符中,上报两套完整独立的 CDC 接口;
- 依靠 USB 标准字段
wIndex区分:wIndex=0对应 ACM0,wIndex=1对应 ACM1; - Linux 内核
cdc-acm驱动自动解析枚举信息,生成两个互不干扰的 ttyACM 设备节点; - 两路串口:波特率、停止位、校验位、收发数据完全独立配置、互不串扰。
3. 两类关键 USB 传输
- 控制传输(Endpoint 0)负责设备枚举、标准请求下发,核心命令:
SET_LINE_CODING:主机下发串口配置参数(波特率 / 数据位 / 停止位 / 奇偶校验)GET_LINE_CODING:主机读取当前设备串口配置SET_CONTROL_LINE_STATE:串口链路状态控制
- 批量传输(BULK IN/OUT)承载上下行业务数据,是虚拟串口收发报文的高速通道,双 CDC 各自分配独立端点。
4. 控制回调触发机制
req_process:解析主机发来的 USB 标准请求;ctlx_out:控制 OUT 数据包接收完成后自动回调;- 项目关键:SET_LINE_CODING 的 7 字节参数,仅在 ctlx_out 中完成最终生效。
三、GD32F303 从机 核心编程要点
1. 工程基础配置
- 开启 USB 外设时钟,配置为 USB Device 从机模式;
- 基于 GD32 官方 USB 设备库,移植 CDC 类驱动;
- 合理划分双 CDC 独立端点、独立接收 / 发送缓冲区、状态标记位。
/**
* @brief USB双CDC虚拟串口 总初始化函数(项目入口)
* @param 无
* @retval 无
*/
void Bsp_UsbVirtualSerialInit(void)
{
// 1. USB外设时钟配置:开启USB模块所需的RCU时钟(核心底层时钟)
usbd_rcu_config();
// 2. USB GPIO配置:配置USB_DM/USB_DP引脚为USB功能模式
usbd_gpio_config();
// 3. USB设备栈初始化:绑定USB设备句柄、双CDC描述符、CDC类回调接口
usbd_init(&usbd_dual_cdc, &dual_cdc_desc, &dual_cdc_class);
// 4. USB中断配置:配置USB中断优先级,使能USB中断响应
usbd_nvic_config();
// 5. 使能USB上拉电阻:硬件枚举上线,主机开始识别USB设备
usbd_connect(&usbd_dual_cdc);
}
2. 双接口隔离(重中之重)
- 所有串口配置请求,必须通过
req->wIndex识别当前是 ACM0 还是 ACM1; - 解析请求时保存接口索引,参数配置阶段绑定对应 CDC 实例与硬件 UART。
/*!
\brief handle the CDC ACM class-specific requests
\param[in] udev: pointer to USB device instance
\param[in] req: device class-specific request
\param[out] none
\retval USB device operation status
*/
static uint8_t dual_cdc_req_handler(usb_dev *udev, usb_req *req)
{
usb_cdc_handler *cdc = NULL;
uint8_t status = REQ_NOTSUPP, noti_buf[10] = {0U};
if(0x00U == (req->wIndex & 0xFFU)) {
cdc = (usb_cdc_handler *)udev->class_data[0];
} else {
cdc = (usb_cdc_handler *)udev->class_data[1];
}
acm_notification *notif = (void *)noti_buf;
switch(req->bRequest) {
case SET_LINE_CODING:
/* set the value of the current command to be processed */
udev->class_core->req_cmd = req->bRequest;
usb_transc_config(&udev->transc_out[0], (uint8_t *)&cdc->line_coding, req->wLength, 0U);
udev->class_core->req_reserved = req->wIndex & 0xFFU;
status = REQ_SUPP;
break;
case GET_LINE_CODING:
usb_transc_config(&udev->transc_in[0], (uint8_t *)&cdc->line_coding, 7U, 0U);
status = REQ_SUPP;
break;
case SET_CONTROL_LINE_STATE:
notif->bmRequestType = 0xA1U;
notif->bNotification = USB_CDC_NOTIFY_SERIAL_STATE;
notif->wIndex = req->wIndex; // 修复:绑定对应CDC接口
notif->wValue = 0U;
notif->wLength = 2U;
noti_buf[8] = (uint8_t)req->wValue & 3U;
noti_buf[9] = 0U;
status = REQ_SUPP;
break;
default:
break;
}
return status;
}
/*!
\brief handle CDC ACM data in DATA OUT transaction
\param[in] udev: pointer to USB device instance
\param[in] ep_num: endpoint number
\param[out] none
\retval none
*/
static void dual_cdc_data_out(usb_dev *udev, uint8_t ep_num)
{
usb_cdc_handler *cdc = NULL;
if((CDC_ACM0_DATA_OUT_EP & 0x7FU) == ep_num) {
cdc = (usb_cdc_handler *)udev->class_data[0];
} else {
cdc = (usb_cdc_handler *)udev->class_data[1];
}
cdc->packet_receive = 1U;
cdc->receive_length = udev->transc_out[ep_num].xfer_count;
}
3. 核心回调函数职责
usb_class dual_cdc_class = {
.req_cmd = 0xFFU,
.init = dual_cdc_init,
.deinit = dual_cdc_deinit,
.req_process = dual_cdc_req_handler, // 解析SET_LINE_CODING等标准请求
.ctlx_out = dual_cdc_ctlx_out, // 串口参数接收完成,执行硬件配置
.data_in = dual_cdc_data_in, // USB上行发送完成回调
.data_out = dual_cdc_data_out // USB下行接收完成回调
};
dual_cdc_req_handler:捕获校验 / 波特率 / 停止位配置请求,缓存当前接口编号;dual_cdc_ctlx_out:参数接收完毕,执行底层usb_comport_config硬件串口配置;- 数据收发回调:双路数据分开处理,避免缓冲区覆盖、数据错乱。
4. GD32F303 硬件特性(实测结论)
适用于 GD32F303 全系列,与 STM32 设计逻辑不同:
- 无校验模式:
- USART 数据位配置:WL=0(8 位数据)
- 奇校验 / 偶校验模式:
- 必须强制配置:WL=1(9 位数据位)
- 硬件逻辑:8 位有效数据 + 硬件自动附加 1 位校验位,组成 9 位完整帧
- 错误现象:开启校验但使用 8 位数据位,会直接触发硬件 PERR (校验错误) / FERR (帧错误),进入中断错误分支,直接丢包 + 乱码;
- 最终固定规范:GD32F303 只要开启奇偶校验,必须配置为 9 位数据位。
5. 串口参数映射逻辑
将 Linux 下发的 line_coding 结构体参数:
- 波特率
dwDTERate - 停止位
bStopBits - 校验位
bParityType精准映射到对应 USART 寄存器:USART_BAUD、USART_CTL0、USART_CTL1。
四、RK3568 Linux 主机端关键特性
-
设备节点双 CDC 枚举成功:
/dev/ttyACM0→ 对应 USB 接口 0/dev/ttyACM1→ 对应 USB 接口 1
- Linux cdc-acm 驱动固定行为打开任意一路 ttyACM 串口时,驱动会连续发送两次 SET_LINE_CODING 请求:
- 第一次:中间临时配置,参数还未传输完成,为驱动初始化流程;
- 第二次:用户串口工具真实配置参数,为最终生效参数;
- 处理方案:不做过滤、不屏蔽,直接二次覆盖配置,最终以第二次参数为准即可。
五、项目实战核心踩坑总结
坑 1:GD32F303 奇偶校验必乱码 / 帧错误
- 根因:GD32 硬件规则:开启校验必须设 9 位数据位(8 位数据 + 1 位校验位)
- 解决方案:配置函数中自动将数据位 + 1,强制切换 9 位模式
坑 2:USB 设置串口参数后,串口接收异常(核心 BUG)
- 现象:USB 下发串口配置后,发送正常、接收不到数据
- 根因:配置函数中执行了
usart_disable关闭串口,硬件自动关闭接收中断,代码未重新开启 - 解决方案:串口参数配置完成后,重新使能接收中断
USART_INT_RBNE
坑 3:双 CDC 通道串扰、参数错乱
- 根因:未通过 USB
wIndex区分双路接口,硬编码单一串口 - 解决方案:根据接口索引分别配置 USART0/USART1,双路完全隔离
坑 4:Linux 驱动两次 SET_LINE_CODING 请求
- 根因:Linux cdc-acm 驱动标准初始化行为
- 解决方案:不屏蔽、直接覆盖配置,第二次参数为最终生效值
六、最终总结
- 架构:RK3568 + GD32F303 单 USB 实现双路虚拟串口,方案稳定通用
- GD32F303 硬件:开启奇偶校验 → 数据位必须设为 9 位
- 核心 BUG 修复:USB 重配置串口参数后,必须重启接收中断
- Linux 适配:双 CDC 自动生成双 ttyACM 节点,两次配置请求直接覆盖即可
更多推荐




所有评论(0)