USB 虚拟串口源码改造与 FreeRTOS 适配

目录

一、前言

在上一篇笔记中,我们完成了 USBX 组件的手工移植并实现了基础的 USB 虚拟串口功能,能够实现与 PC 端的简单数据收发。但原有源码的收发逻辑缺乏可靠的同步机制与数据缓存机制,在实际工程应用中容易出现数据丢失、发送超时无响应等问题。本次笔记将基于 FreeRTOS 的信号量与消息队列,对 USB 虚拟串口的源码进行改造,实现可靠的阻塞式发送与队列化接收,让 USB 串口功能更符合嵌入式工程的实战需求,同时夯实 FreeRTOS 与 USBX 结合使用的核心技巧。

二、回顾:原有 USB 虚拟串口收发逻辑

我们此前已经了解,USB 设备能够被 PC 准确识别,核心依赖于预配置的设备描述符、配置描述符、接口描述符与端点描述符,这些描述符在 USBX 移植的配置文件中已完成配置,为虚拟串口功能打下了基础。

在上一篇的移植实践中,我们使用以下两个函数实现 USB 串口的数据发送功能:

c 复制代码
// 发送完成回调函数,通知发送状态
static UINT ux_device_class_cdc_acm_write_callback(struct UX_SLAVE_CLASS_CDC_ACM_STRUCT *cdc_acm, UINT status, ULONG length)

// 启动USB串口数据发送,配合回调函数使用
UINT _ux_device_class_cdc_acm_write_with_callback(UX_SLAVE_CLASS_CDC_ACM *cdc_acm, UCHAR *buffer,
                                ULONG requested_length)

当开发板接收到来自 PC 的 USB 串口数据时,以下回调函数会被自动触发,完成数据的接收处理:

c 复制代码
// 数据接收回调函数,处理从PC端接收的数据
static UINT ux_device_class_cdc_acm_read_callback(struct UX_SLAVE_CLASS_CDC_ACM_STRUCT *cdc_acm, UINT status, UCHAR *data_pointer, ULONG length)

原有逻辑能够实现基础的收发,但缺乏工程化的同步与缓存机制,在高频率数据交互场景下存在局限性。

三、改造方向:可靠阻塞收发与队列缓存

本次源码改造的核心方向是提升 USB 虚拟串口收发的可靠性与工程实用性,具体规划如下:

  1. 发送功能改造 :使用_ux_device_class_cdc_acm_write_with_callback启动数据发送,随后通过阻塞机制等待ux_device_class_cdc_acm_write_callback回调函数唤醒,避免无效轮询,提升 CPU 利用率;
  2. 接收功能改造:将接收回调函数中获取的数据写入 FreeRTOS 消息队列,实现数据的缓存与解耦,后续业务任务可从队列中读取数据,避免数据丢失,同时简化业务逻辑编写。

补充:FreeRTOS 的信号量与消息队列是嵌入式工程中实现同步与数据缓存的常用工具,信号量适合实现任务与回调函数的同步,消息队列适合实现数据的异步缓存与传递。

四、发送功能改造:基于二进制信号量

发送功能改造的核心是基于二进制信号量实现阻塞同步,整体分为三个步骤,确保发送过程的可靠性。

步骤 1:定义发送功能的核心函数接口

实现阻塞式发送函数,提供数据缓冲区、数据长度、超时时间三个参数,满足工程使用需求:

c 复制代码
// USB串口阻塞式发送函数,等待发送完成或超时
int ux_device_cdc_acm_send(uint8_t *datas, uint32_t len, uint32_t timeout)

步骤 2:创建二进制信号量全局变量与初始化

采用二进制信号量作为发送完成的同步标志(信号量比单纯的标志位同步更准确、更可靠),首先定义全局信号量变量,再在USBD_CDC_ACM_Activate函数中完成信号量的创建

c 复制代码
// 定义USB串口发送同步用的二进制信号量全局变量
static SemaphoreHandle_t g_xUSBUARTSend;

// USB串口激活函数,初始化同步信号量
VOID USBD_CDC_ACM_Activate(VOID *cdc_acm_instance)
{
    // 信号量未创建时,完成二进制信号量的创建
	if(!g_xUSBUARTSend)
	{
		g_xUSBUARTSend = xSemaphoreCreateBinary();
	}
}

步骤 3:回调函数释放信号量,发送函数实现阻塞等待

  1. 发送完成回调函数中释放二进制信号量,通知发送任务已完成:
c 复制代码
// 发送完成回调函数,释放信号量唤醒阻塞的发送任务
static UINT ux_device_class_cdc_acm_write_callback(struct UX_SLAVE_CLASS_CDC_ACM_STRUCT *cdc_acm, UINT status, ULONG length)
{
	xSemaphoreGive(g_xUSBUARTSend); // 释放信号量,标记发送完成
	return 0;
}
  1. 完善阻塞式发送函数的完整逻辑,实现 "启动发送→阻塞等待→判断结果" 的流程:
c 复制代码
// USB串口阻塞式发送函数完整实现
int ux_device_cdc_acm_send(uint8_t *datas, uint32_t len, uint32_t timeout)
{
    // 校验cdc_acm实例是否有效
    if (cdc_acm)
    {
        // 调用USBX接口启动数据发送
        if (UX_SUCCESS == ux_device_class_cdc_acm_write_with_callback(cdc_acm, datas, len))
        {
            // 阻塞等待信号量(发送完成)或超时
            if(pdTRUE == xSemaphoreTake(g_xUSBUARTSend, timeout))
				return 0; // 发送成功
			else
				return -1; // 超时失败
        }
        else
        {
            return -1; // 发送启动失败
        }
    }
    else
    {
        return -1; // cdc_acm实例无效
    }
	return 0;
}

发送功能改造核心逻辑总结

调用ux_device_cdc_acm_send发送数据时,会先启动 USBX 底层发送,随后通过xSemaphoreTake阻塞等待;发送完成后回调函数释放信号量,唤醒阻塞任务,最终返回发送结果(成功或超时失败),全程无需轮询,提升了可靠性与 CPU 利用率。

五、接收功能改造:基于 FreeRTOS 消息队列

接收功能改造的核心是基于FreeRTOS 消息队列实现数据缓存,整体分为 "创建队列→写入队列→读取队列" 三个步骤,避免数据丢失。

步骤 1:创建接收队列全局变量与初始化

USBD_CDC_ACM_Activate函数中,与发送信号量一起完成接收队列的创建,定义队列长度为 200,每个数据单元大小为 1 字节:

c 复制代码
// 定义USB串口接收数据缓存队列全局变量
static QueueHandle_t g_xUSBUART_Rx_Queue;

// USB串口激活函数,初始化同步信号量与接收队列
VOID USBD_CDC_ACM_Activate(VOID *cdc_acm_instance)
{
    // 信号量与队列未创建时,完成初始化
	if(!g_xUSBUARTSend)
	{
		g_xUSBUARTSend = xSemaphoreCreateBinary();
		g_xUSBUART_Rx_Queue = xQueueCreate(200, 1); // 创建接收队列,200字节缓存
	}
}

步骤 2:接收回调函数写入队列

修改接收回调函数,将接收到的每一字节数据写入消息队列,实现数据缓存:

c 复制代码
// 接收回调函数,将数据写入FreeRTOS消息队列
static UINT ux_device_class_cdc_acm_read_callback(struct UX_SLAVE_CLASS_CDC_ACM_STRUCT *cdc_acm, UINT status, UCHAR *data_pointer, ULONG length)
{
    // 遍历接收数据,逐字节写入队列
	for(int i = 0; i < length; i++)
	{
		xQueueSend(g_xUSBUART_Rx_Queue, (const void *)&data_pointer[i], 0);
	}
		return 0;
}

步骤 3:提供接收队列的读函数接口

实现队列读取函数,供业务任务调用,获取缓存的接收数据:

c 复制代码
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "semphr.h"

// USB串口接收读队列函数,获取缓存的接收数据
int ux_device_cdc_acm_getchar(uint8_t *pdata, uint32_t timeout)
{
	// 校验接收队列是否有效
	if(g_xUSBUART_Rx_Queue)
	{
		// 从队列中读取一个字节数据
		if(pdPASS == xQueueReceive(g_xUSBUART_Rx_Queue, pdata, 0))
			return 0; // 读取成功
		else
			return -1; // 队列无数据
	}		
	else
	{
		return -1; // 接收队列无效
	}
}

接收功能改造核心逻辑总结

PC 发送的数据通过回调函数接收后,会被缓存到消息队列中,业务任务可通过ux_device_cdc_acm_getchar函数从队列中读取数据,实现了 "接收回调" 与 "业务处理" 的解耦,避免了高频率数据交互时的数据丢失。

六、改造验证:LCD 显示任务测试

完成收发功能的源码改造后,创建一个 LCD 显示任务进行功能测试,任务核心逻辑如下:

  1. 开发板定时自动向 PC 发送数据,验证发送功能的可靠性;
  2. 当接收到 PC 发送的数据时,将计数器cnt加 1,验证接收功能的有效性;
  3. 相关结果通过 LCD 屏幕展示,直观查看测试效果。

测试结果符合预期,USB 虚拟串口的收发功能稳定可靠,无数据丢失与超时异常,实际运行效果如下图所示:

七、总结

  1. USB 虚拟串口改造核心:发送用二进制信号量实现阻塞同步,接收用消息队列实现数据缓存;
  2. 信号量使用流程:定义全局变量→创建信号量→发送函数等待→回调函数释放;
  3. 队列使用流程:定义全局变量→创建队列→回调函数写入→业务函数读取;
  4. 改造后实现了可靠阻塞收发,符合嵌入式工程实战需求,无数据丢失风险。

八、结尾

本次 USB 虚拟串口源码改造,是 USBX 与 FreeRTOS 结合使用的核心实战案例,通过信号量与消息队列解决了原有收发逻辑的局限性,让 USB 串口功能更具工程实用性。吃透这种 "协议栈 + RTOS" 的结合开发思路,能够大幅提升嵌入式项目的稳定性与可维护性。感谢各位的阅读,持续关注本系列笔记,一起深耕嵌入式 USB 实战开发,解锁更多高级功能与工程技巧!

相关推荐
曦月逸霜2 小时前
深入理解计算机系统——学习笔记(持续更新~)
笔记·学习·计算机系统
koo3642 小时前
pytorch深度学习笔记18
pytorch·笔记·深度学习
hetao17338372 小时前
2026-01-22~23 hetao1733837 的刷题笔记
c++·笔记·算法
curry____3032 小时前
数据结构学习笔记
数据结构·笔记·学习
向前V3 小时前
Flutter for OpenHarmony轻量级开源记事本App实战:笔记编辑器
开发语言·笔记·python·flutter·游戏·开源·编辑器
望眼欲穿的程序猿3 小时前
SDCC+Ai8051U 中断点灯
stm32·单片机·嵌入式硬件
youcans_3 小时前
【动手学STM32G4】(15)三路互补带死区 PWM 输出
stm32·单片机·嵌入式硬件·pwm·死区
snow_star_dream3 小时前
(笔记)VSC python应用--函数补全注释添加
笔记·python
小慧10243 小时前
外部中断与回调函数
stm32·单片机·嵌入式硬件