USBX虚拟串口源码分析与改造笔记

这份笔记的目标是理解并改造USBX的CDC ACM(虚拟串口)类代码,使其数据收发更易用、更可靠。

第一部分:USB如何被电脑识别------描述符

电脑插入USB设备后,第一件事就是读取"描述符"。这是一份介绍设备身份和功能的"说明书"。

1. 设备描述符(我是谁)

代码通常在 ux_device_descriptors.c 中。它定义了设备的全局信息:

  • idVendor (VID) 和 idProduct (PID):就像品牌的商标和型号,电脑靠这个识别具体设备。

  • bDeviceClass, bDeviceSubClass, bDeviceProtocol:这里常设为0,因为功能由后面的接口描述符定义。

  • iManufacturer, iProduct:指向描述厂商和产品名称的字符串索引。

为什么要改这里?

如果你想更改设备在电脑中显示的名称,或者使用特定的VID/PID,就需要修改这里的对应值。

2. 配置描述符与接口描述符(我能做什么)

一个设备可以有多种配置。对于虚拟串口,我们常用一个配置,里面包含"通信接口"和"数据接口"。

  • 配置描述符:定义了此配置下的接口数量、功耗等。

  • 接口描述符:定义了这是一个"CDC ACM"类设备,并指定了使用的端点。

小结 :描述符是USB通信的基石。USBX框架已经帮我们写好了大部分描述符构建代码。我们通常只需要在头文件里修改 USBD_VIDUSBD_PID 等宏定义来自定义设备信息,一般不需要改动构建逻辑。

第二部分:数据收发的核心机制与问题

USBX原始代码使用"回调"机制处理数据,这对于应用程序来说不够直接。我们需要把它改造成更易用的"阻塞发送"和"队列接收"模式。

1. 发送数据流程(设备 -> 电脑)

原始流程:

  • 应用程序 调用 ux_device_class_cdc_acm_write_with_callback()。这个函数只是启动发送,然后立刻返回,不会等待发送完成。

  • 当硬件真正完成数据发送后,USBX框架会自动调用 ux_device_class_cdc_acm_write_callback() 函数。

原始问题:应用程序不知道数据何时发完,不方便处理。

改造思路

  1. 我们创建一个二进制信号量(例如 g_xUSBUARTSend),初始状态为"不可用"。

  2. 应用程序在启动发送后,立刻等待这个信号量,从而进入阻塞状态。

  3. ux_device_class_cdc_acm_write_callback() 里,数据发送完成时,我们释放这个信号量。

  4. 信号量释放后,等待它的应用程序任务就被唤醒,知道发送已经完成。

这样,我们就实现了一个 ux_device_cdc_acm_send() 函数,它内部启动发送并等待完成,对应用程序来说就像调用了一个普通的阻塞式发送函数。

2. 接收数据流程(电脑 -> 设备)

原始流程:

  • 当电脑通过USB发来数据时,USBX框架在处理好数据后,会自动调用 ux_device_class_cdc_acm_read_callback() 函数,并把数据指针和长度传进来。

原始问题:数据在回调函数里一闪而过,应用程序必须在这个函数里立刻处理完,否则数据就丢了。

改造思路

  1. 我们创建一个FreeRTOS队列(例如 g_xUSBUART_RX_Queue)。

  2. ux_device_class_cdc_acm_read_callback() 里,我们不再直接处理数据(如显示到屏幕),而是将接收到的每一个字节,依次存入队列。

  3. 我们为应用程序提供一个 ux_device_cdc_acm_getchar() 函数。应用程序可以随时调用这个函数,它从队列里读取数据。如果队列为空,应用程序可以选择等待或做别的事。

这样,我们就将"中断式的数据接收"转换成了"可随时读取的数据缓冲池",非常灵活。

第三部分:关键工具------信号量

信号量在这里是一个"通知工具"。我们主要使用二进制信号量。

  • 创建xSemaphoreCreateBinary()。创建后,它默认是"空的"或"不可用"状态。

  • 获取 (Take)xSemaphoreTake()。任务调用它来尝试获取信号量。如果信号量是"不可用"状态,任务就会进入阻塞等待(如果设定了超时时间)。

  • 释放 (Give)xSemaphoreGive()。在另一个地方(如回调函数)调用它,使信号量变为"可用"状态。这会唤醒正在等待该信号量的任务。

在我们的改造中:

  • 发送完成回调函数里 Give 信号量。

  • 发送函数在启动传输后 Take 信号量。

  • 一Give一Take,就完成了一次同步通知。

第四部分:代码改造实战

假设所有代码在 ux_device_cdc_acm.c 中。

第1步:定义全局通信工具

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

/* 发送完成信号量 */
SemaphoreHandle_t g_xUSBUARTSend = NULL;
/* 接收数据队列 */
QueueHandle_t g_xUSBUART_RX_Queue = NULL;

第2步:初始化这些工具(在设备初始化阶段调用)

cs 复制代码
void USB_CommunicationTools_Init(void)
{
    /* 创建二进制信号量,初始状态为"不可用" */
    g_xUSBUARTSend = xSemaphoreCreateBinary();
    /* 创建队列,能存储例如256个字节 */
    g_xUSBUART_RX_Queue = xQueueCreate(256, sizeof(uint8_t));
}

第3步:改造发送回调函数

cs 复制代码
static UINT ux_device_class_cdc_acm_write_callback(struct UX_SLAVE_CLASS_CDC_ACM_STRUCT *cdc_acm, UINT status, ULONG length)
{
    (void)cdc_acm; (void)length; // 避免未使用参数警告

    /* 当发送完成时,无论成功或失败(检查status),都释放信号量唤醒等待的任务 */
    if(g_xUSBUARTSend != NULL)
    {
        xSemaphoreGive(g_xUSBUARTSend);
    }
    return 0;
}

第4步:实现阻塞式发送函数

cs 复制代码
cint ux_device_cdc_acm_send(uint8_t *datas, uint32_t len, uint32_t timeout)
{
    UINT result;

    /* 1. 启动非阻塞发送 */
    result = ux_device_class_cdc_acm_write_with_callback(cdc_acm, datas, len);
    if (result != UX_SUCCESS) {
        return -1; // 启动失败
    }

    /* 2. 等待发送完成信号量 */
    if (xSemaphoreTake(g_xUSBUARTSend, pdMS_TO_TICKS(timeout)) == pdTRUE) {
        return 0; // 发送成功完成
    } else {
        return -2; // 等待超时
    }
}

第5步:改造接收回调函数

cs 复制代码
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)
{
    (void)cdc_acm; // 避免未使用参数警告

    /* 如果接收成功 */
    if (status == UX_SUCCESS && g_xUSBUART_RX_Queue != NULL)
    {
        /* 将收到的每个字节存入队列 */
        for (ULONG i = 0; i < length; i++)
        {
            // 如果队列满了,数据会被丢弃。你可以根据需求调整队列大小或处理方式。
            xQueueSend(g_xUSBUART_RX_Queue, &(data_pointer[i]), 0);
        }
    }
    return 0;
}

第6步:实现应用程序接收函数

cs 复制代码
int ux_device_cdc_acm_getchar(uint8_t *pData, uint32_t timeout)
{
    if(g_xUSBUART_RX_Queue != NULL)
    {
        /* 从队列中取一个字节,如果队列为空,则等待指定的超时时间 */
        if(xQueueReceive(g_xUSBUART_RX_Queue, pData, pdMS_TO_TICKS(timeout)) == pdPASS)
        {
            return 0; // 成功读到数据
        }
    }
    return -1; // 队列未初始化或等待超时
}
第五部分:应用与验证

1. 应用程序发送数据

cs 复制代码
char buf[100];
sprintf(buf, "Hello USB Serial!\r\n");
ux_device_cdc_acm_send((uint8_t *)buf, strlen(buf), 1000); // 阻塞等待最多1秒

2. 应用程序接收数据(在一个任务中循环)

cs 复制代码
uint8_t received_byte;
while(1) {
    if(ux_device_cdc_acm_getchar(&received_byte, 100) == 0) {
        // 成功收到一个字节,处理它
        // 例如,存入另一个缓冲区,或直接处理
    } else {
        // 没有收到数据,可以执行其他任务
    }
}

3. 最终效果

  • 电脑端:设备管理器识别出一个新的COM口(如COM3)。

  • 串口工具:打开这个COM口。

    • 可以接收到开发板周期性发送的测试数据。

    • 在串口工具里发送字符串,开发板能通过 ux_device_cdc_acm_getchar 函数读到,并可在LCD上显示或进行其他处理。

相关推荐
BlackWolfSky2 小时前
鸿蒙中级课程笔记3—ArkUI进阶6—ArkUI性能优化实践(长列表加载性能优化)
笔记·华为·harmonyos
马猴烧酒.2 小时前
智能协图云图库学习笔记day6-主流图片优化技术
笔记·学习
静小谢2 小时前
前端mock假数据工具JSON Server使用笔记
前端·笔记·json
白白白飘3 小时前
【书籍课程】强化学习的数学原理
笔记
今儿敲了吗3 小时前
计算机网络第四章笔记(三)
笔记·计算机网络
宵时待雨3 小时前
数据结构(初阶)笔记归纳8:栈和队列
数据结构·笔记
朗迹 - 张伟4 小时前
UE5 City Traffic Pro 交通插件学习笔记
笔记·学习·ue5
@––––––4 小时前
论文学习笔记:FAST - 高效的视觉-语言-动作模型动作分词技术
笔记·学习
Gain_chance4 小时前
22-学习笔记尚硅谷数仓搭建-日志表建表语句解析、数据装载及脚本装载数据
数据仓库·笔记·学习