101_详解USB协议

//@TOC

  在Linux系统中,"注册一个USB设备"意味着让内核识别你的硬件,并将其与对应的驱动程序绑定。这个过程涉及USB核心子系统的probe机制、设备ID表以及用户空间节点的创建。本文以Linux驱动开发为背景,采用"三棒接力"的代码切片模式,手把手带你完成一个自定义USB设备的注册。

一、Linux USB设备注册的背景与演进

  从USB 1.0到如今的USB4,Linux内核的USB子系统经历了从usbfsusb core再到usb_gadget的完整演变。设备注册始终是连接硬件与用户空间的桥梁。

1. 诞生背景:统一设备模型

  Linux 2.6内核引入统一设备模型,所有设备都挂在bus上。USB设备注册的实质就是在usb_bus_type总线上创建一个struct usb_device实例,并由匹配的驱动接管。

2. 标准化进程:从hotplug到udev

  早期设备注册依靠hotplug脚本,如今由udev根据内核事件动态创建设备节点。驱动只需完成内核层的注册,用户空间节点的创建由device_create配合class完成。

3. 技术演进:关键API一览

|--------------------|-----------------------|
| 函数名 | 功能说明 |
| usb_register | 向USB子系统注册驱动(包含设备ID表) |
| usb_deregister | 注销USB驱动 |
| usb_register_dev | 注册USB次设备号,自动创建usb设备节点 |
| device_create | 在/dev或/sys下创建设备文件 |

二、Linux USB设备注册的工作原理

1. 通俗比喻:入职与工位分配

  USB设备插入就像新员工报到。内核(HR)读取其身份证(设备描述符),在员工花名册(设备树)里登记,然后喊来对应部门的驱动(probe),最后分配工位(设备节点)。

2. 核心机制:设备ID匹配

  每个USB驱动都有一个struct usb_device_id数组,包含支持的Vendor IDProduct ID。当设备插入时,USB核心调用驱动的probe函数进行初始化。

3. 关键特性:自动发现与热插拔

  注册驱动后,已插入的设备会被立即枚举;拔出时disconnect回调自动清理资源。这一切由khubd内核线程驱动。

4. 关键特性:端点管道抽象

  注册成功后,驱动通过usb_rcvbulkpipe等函数获取端点地址,发送URB请求进行数据传输。

三、核心概念与注册所需API

1. 设备描述符结构体

  在内核中,struct usb_device_descriptor包含了协议版本、设备类、VID/PID等信息,是注册匹配的基础。

|-----------------------------|-------------|
| 函数名 | 功能说明 |
| usb_get_device_descriptor | 从设备获取设备描述符 |
| usb_string | 获取USB字符串描述符 |

  代码示例:

c 复制代码
/*CN:读取设备描述符--EN:Read device descriptor*/
struct usb_device_descriptor desc;
int ret = usb_get_device_descriptor(dev, &desc);
if (ret < 0) {
    dev_err(&dev->dev, "CN:获取描述符失败--EN:Failed to get descriptor\n");
    return ret;
}

2. 驱动注册与注销

c 复制代码
/*CN:USB驱动结构体定义--EN:USB driver structure definition*/
static struct usb_driver my_driver = {
    .name = "my_usb",
    .id_table = my_id_table,
    .probe = my_probe,
    .disconnect = my_disconnect,
};

/*CN:在模块初始化函数中注册--EN:Register in module init*/
int ret = usb_register(&my_driver);

3. 设备节点创建

|-----------------------------|-------------------------------|
| 函数名 | 功能说明 |
| usb_register_dev | 注册USB设备(需要USB_MAJOR),自动创建设备节点 |
| device_create_with_groups | 高级创建设备节点,可附带sysfs属性 |

四、硬件视角:USB描述符与端点

1. 功能框图

  一个USB设备包含配置、接口、端点三层结构。注册驱动后,probe函数内需要遍历接口和端点,获取传输管道信息。

|---------------------------|-------------|
| 函数名 | 功能说明 |
| usb_find_interface | 根据接口设置查找接口 |
| usb_endpoint_is_bulk_in | 判断端点是否为批量输入 |

2. 管道配置示例

c 复制代码
/*CN:根据端点地址生成管道--EN:Generate pipe from endpoint address*/
if (usb_endpoint_is_bulk_in(endpoint)) {
    udev->bulk_in_pipe = usb_rcvbulkpipe(interface_to_usbdev(interface),
                                         endpoint->bEndpointAddress);
}

五、实践指南:三步完成USB设备注册

1. 配置流程概述

  Linux下注册一个USB设备的标准流程如下:

  1. 定义usb_device_id表,填写VID/PID
  2. 实现probe函数,完成字符设备注册与端点初始化
  3. 调用usb_register将驱动加载到内核

2. 以自定义USB设备为例 (VID=0x1234, PID=0x5678)

  假设硬件是一个简单的USB鼠标或自定义HID设备,下面用代码切片模式拆解probe中的初始化过程。

(1) 参数确定

  • 设备匹配:USB_DEVICE(0x1234, 0x5678)
  • 字符设备主设备号:动态分配
  • 设备文件名称:/dev/myusb0

(2) 初始化代码 --- 代码切片模式

  整个probe函数的结构如下,后续按"接力棒"顺序拆解:

c 复制代码
/**
 * Function:    my_probe
 * Description: CN:USB设备插入时的探测函数--EN:Probe function called when USB device is inserted
 * Parameters:  interface - CN:USB接口描述--EN:USB interface descriptor
 *              id - CN:匹配的设备ID项--EN:Matched device ID entry
 * Return value:CN:成功返回0,失败返回负值--EN:0 on success, negative on failure
 */
static int my_probe(struct usb_interface *interface,
                    const struct usb_device_id *id)
{
    /*CN:===== 第一棒:分配设备结构体,初始化字符设备 =====--EN:== First leg: Allocate device struct, init char device ==*/
    /*CN:===== 第二棒:配置USB端点,保存管道 =====--EN:== Second leg: Configure USB endpoints, store pipes ==*/
    /*CN:===== 第三棒:注册字符设备,创建设备节点 =====--EN:== Third leg: Register char device, create device node ==*/

    return 0;
}
  • 第一棒:alloc_chrdev_region + cdev_init ------ 准备字符设备

  用到的内核函数 API 及其功能说明如下:

|-----------------------|----------------------|
| 函数名 | 功能说明 |
| alloc_chrdev_region | 动态分配一个主设备号和次设备号范围 |
| cdev_init | 初始化cdev结构体并绑定文件操作集 |
| cdev_add | 将cdev添加到内核,使其对用户空间可用 |

c 复制代码
    /*CN:===== 第一棒:分配设备结构体,初始化字符设备 =====--EN:===== First leg: Allocate device struct, init char device =====*/
    int ret;
    dev_t devno;
    struct my_usb_dev *udev = kzalloc(sizeof(*udev), GFP_KERNEL);
    if (!udev)
        return -ENOMEM;

    ret = alloc_chrdev_region(&devno, 0, 1, "myusb");          /*CN:动态分配主次设备号--EN:Dynamically allocate major/minor*/
    if (ret < 0) {
        dev_err(&interface->dev, "CN:分配设备号失败--EN:Failed to alloc chrdev region\n");
        goto err_free;
    }

    udev->dev = devno;
    cdev_init(&udev->cdev, &my_fops);                          /*CN:绑定文件操作集--EN:Bind file operations*/
    udev->cdev.owner = THIS_MODULE;
    ret = cdev_add(&udev->cdev, devno, 1);
    if (ret) {
        dev_err(&interface->dev, "CN:cdev_add失败--EN:cdev_add failed\n");
        goto err_unreg_region;
    }
  • 第二棒:usb_set_intfdata + 端点解析 ------ 绑定数据并保存管道

  用到的内核函数 API 及其功能说明如下:

|--------------------|----------------------------|
| 函数名 | 功能说明 |
| usb_set_intfdata | 将私有数据指针绑定到interface,方便后续获取 |
| usb_rcvbulkpipe | 根据端点地址生成批量输入管道 |
| usb_sndbulkpipe | 根据端点地址生成批量输出管道 |

c 复制代码
    /*CN:===== 第二棒:配置USB端点,保存管道 =====--EN:===== Second leg: Configure USB endpoints, store pipes =====*/
    struct usb_host_interface *iface_desc = interface->cur_altsetting;
    int i;
    for (i = 0; i < iface_desc->desc.bNumEndpoints; i++) {
        struct usb_endpoint_descriptor *ep = &iface_desc->endpoint[i].desc;
        if (usb_endpoint_is_bulk_in(ep)) {
            udev->bulk_in_pipe = usb_rcvbulkpipe(interface_to_usbdev(interface),
                                                  ep->bEndpointAddress); /*CN:保存输入管道--EN:Store IN pipe*/
        } else if (usb_endpoint_is_bulk_out(ep)) {
            udev->bulk_out_pipe = usb_sndbulkpipe(interface_to_usbdev(interface),
                                                   ep->bEndpointAddress); /*CN:保存输出管道--EN:Store OUT pipe*/
        }
    }

    usb_set_intfdata(interface, udev);                         /*CN:绑定私有数据--EN:Bind private data*/
    udev->interface = interface;
  • 第三棒:device_create ------ 向用户空间暴露设备节点

  用到的内核函数 API 及其功能说明如下:

|-----------------|---------------------|
| 函数名 | 功能说明 |
| class_create | 在/sys/class下创建一个设备类 |
| device_create | 在/dev下创建对应的设备文件 |

c 复制代码
    /*CN:===== 第三棒:注册字符设备,创建设备节点 =====--EN:===== Third leg: Register char device, create device node =====*/
    udev->class = class_create(THIS_MODULE, "myusb_class");    /*CN:创建类--EN:Create class*/
    if (IS_ERR(udev->class)) {
        ret = PTR_ERR(udev->class);
        dev_err(&interface->dev, "CN:创建类失败--EN:class_create failed\n");
        goto err_del_cdev;
    }

    udev->device = device_create(udev->class, NULL, udev->dev,
                                 NULL, "myusb%d", 0);           /*CN:创建设备节点/dev/myusb0--EN:Create device node /dev/myusb0*/
    if (IS_ERR(udev->device)) {
        ret = PTR_ERR(udev->device);
        dev_err(&interface->dev, "CN:创建设备节点失败--EN:device_create failed\n");
        goto err_destroy_class;
    }

    dev_info(&interface->dev, "CN:设备注册成功--EN:Device registered successfully\n");
    return 0;

err_destroy_class:
    class_destroy(udev->class);
err_del_cdev:
    cdev_del(&udev->cdev);
err_unreg_region:
    unregister_chrdev_region(udev->dev, 1);
err_free:
    kfree(udev);
    return ret;
}

(3) 断开连接回调

c 复制代码
/**
 * Function:    my_disconnect
 * Description: CN:USB设备拔出时的清理函数--EN:Cleanup function when USB device is removed
 * Parameters:  interface - CN:USB接口--EN:USB interface
 * Return value:无
 */
static void my_disconnect(struct usb_interface *interface)
{
    struct my_usb_dev *udev = usb_get_intfdata(interface);
    device_destroy(udev->class, udev->dev);                   /*CN:销毁设备节点--EN:Destroy device node*/
    class_destroy(udev->class);
    cdev_del(&udev->cdev);
    unregister_chrdev_region(udev->dev, 1);
    kfree(udev);
    dev_info(&interface->dev, "CN:设备已注销--EN:Device deregistered\n");
}

六、多设备支持:一个驱动匹配多个USB设备

  通过扩展id_table,同一个驱动可以同时支持不同VID/PID的设备,并为每个设备实例创建独立的节点。

|--------|-----------------|---------------|--------|
| 设备 | VID:PID | 设备文件 | 说明 |
| 设备A | 0x1234:0x5678 | /dev/myusb0 | 第一个插入 |
| 设备B | 0x1234:0x9ABC | /dev/myusb1 | 第二个插入 |

  用到的API:

|--------------------|---------------------|
| 函数名 | 功能说明 |
| usb_register_dev | 另一种注册方式,自动处理USB次设备号 |

1. 统一初始化示例

c 复制代码
/**
 * Function:    module_usb_driver
 * Description: CN:宏,简化USB驱动模块加载/卸载--EN:Macro to simplify USB driver module init/exit
 * Parameters:  驱动结构体
 * Return value:无
 */
module_usb_driver(my_driver);

2. 各实例独立处理

  每个probe调用分配独立的my_usb_dev,通过usb_get_intfdatafile->private_data关联,实现多实例完全隔离。

七、跨平台视角:嵌入式Linux与无操作系统环境

  在资源受限的MCU上(如无Linux),注册USB设备意味着手动实现枚举应答。这里展示一种基于状态机的轻量级注册方法。

c 复制代码
/*========================================*/
/*CN:裸机USB注册状态机--EN:Bare-metal USB registration state machine*/
/*========================================*/

#define USB_STATE_IDLE       0
#define USB_STATE_ADDRESS    1
#define USB_STATE_CONFIGURED 2

1. 核心思想

  不使用操作系统驱动模型,直接在中断里解析Setup包,返回描述符,并记录分配的设备地址。

  • 要点一:用环形缓冲区存储端点数据
  • 要点二:根据bRequest类型分发标准请求
  • 要点三:手动管理配置状态

2. 硬件抽象宏

c 复制代码
/*========================================*/
/*CN:平台适配宏(按实际MCU修改)--EN:Platform adaptation macros (modify per MCU)*/
/*========================================*/

#define USB_EP0_TX(data, len)   hw_usb_ep0_transmit(data, len)   /*CN:端点0发送--EN:Endpoint 0 transmit*/
#define USB_EP0_RX(buf, len)    hw_usb_ep0_receive(buf, len)    /*CN:端点0接收--EN:Endpoint 0 receive*/
#define USB_SET_ADDRESS(addr)   hw_usb_set_address(addr)         /*CN:设置地址--EN:Set device address*/

3. 枚举处理核心

c 复制代码
/**
 * Function:    usb_handle_setup
 * Description: CN:处理USB标准请求,完成设备注册--EN:Handle standard USB requests to complete device registration
 * Parameters:  setup - CN:8字节SETUP包数据--EN:8-byte SETUP packet data
 * Return value:无
 */
void usb_handle_setup(uint8_t *setup)
{
    switch (setup[1]) {
    case USB_REQ_GET_DESCRIPTOR:                    /*CN:获取描述符--EN:Get descriptor*/
        if (setup[3] == USB_DT_DEVICE) {
            USB_EP0_TX(device_descriptor, sizeof(device_descriptor));
        }
        break;
    case USB_REQ_SET_ADDRESS:                       /*CN:设置地址--EN:Set address*/
        USB_SET_ADDRESS(setup[2]);
        USB_EP0_TX(NULL, 0);                        /*CN:回复零长度数据包--EN:Reply with zero-length packet*/
        break;
    case USB_REQ_SET_CONFIGURATION:                 /*CN:设置配置--EN:Set configuration*/
        /*CN:进入配置状态--EN:Enter configured state*/
        USB_EP0_TX(NULL, 0);
        break;
    default:
        USB_STALL();
    }
}

4. 裸机注册 vs Linux驱动注册

|------|------------------------------------|------------------|
| 维度 | Linux USB驱动 | 裸机枚举 |
| 注册方式 | usb_register,内核自动匹配 | 手动解析Setup包并应答 |
| 设备节点 | /dev/myusb0device_create 创建 | 不存在,由应用层自行定义通信接口 |
| 热插拔 | 内核自动通知 | 需要轮询或GPIO中断检测 |
| 开发难度 | 中等,需理解驱动模型 | 高,需深入理解USB协议 |
| 适用场景 | 标准Linux系统 | 超低功耗MCU,无操作系统 |

八、提高注册稳定性的技巧

1. 正确的错误处理与资源释放

  使用goto链式清理,确保probe失败时不会泄漏内存和设备号。每个goto标签按逆序执行清理操作,是Linux内核驱动的标准实践。

2. 使用dev_dbgdev_err进行分级日志

  统一使用dev_*系列打印函数,可在系统日志中快速定位问题。

c 复制代码
/*CN:带调试级别的输出--EN:Debug level print*/
dev_dbg(&udev->dev, "CN:当前状态=%d--EN:Current state=%d\n", udev->state);

3. 避免竞争条件

  在probe函数中,interface的引用计数已由USB核心持有,但注意URB回调可能在其他上下文中运行,需使用自旋锁或atomic_t保护共享数据。

  1. 断开回调中等待所有URB完成
  2. 文件操作函数中检查设备是否已断开
  3. 使用usb_autopm_get_interface进行电源管理

九、结语

  Linux下注册一个USB设备并不是一行insmod那么简单,它背后是一整套"驱动-核心-用户空间"的协作机制。从定义设备ID表,到"三棒接力"完成字符设备与端点配置,再到设备节点的诞生,每一步都清晰可控。希望本文为你点亮了USB驱动注册的完整路径。如果觉得有帮助,欢迎分享和讨论。

扩展阅读:

相关推荐
何中应18 小时前
Spring Boot整合Doris
java·数据库·spring boot
剑锋所指,所向披靡!18 小时前
数据结构之关键路径
数据结构·算法
J-Tony1119 小时前
【Redis】数据结构&&持久化
数据结构·数据库·redis
来一碗刘肉面19 小时前
队列的链式实现
数据结构·c++·算法·链表
遨游DATA19 小时前
Maven dependencyManagement 已声明却仍缺 Jar:如何验证最终运行包
java·spring boot·maven·故障排查·依赖管理
腻害兔19 小时前
【若依项目-产品经理视角】深度拆解 RuoYi-Vue-Pro 认证与权限:RBAC + 数据权限,这套“门禁系统“到底怎么设计的?
java·vue.js·人工智能·产品经理·ai编程
Summer-Bright19 小时前
深度 | Agent 协议标准化:一场决定了 AI 经济底层规则的基础设施战争
java·数据库·人工智能·ai
我叫张小白。19 小时前
LangChain 结构化输出(Structured Output)技术文档
java·数据库·langchain
猫猫不是喵喵.20 小时前
双亲委派机制与类加载过程
java·开发语言