ESC
输入关键词搜索文章标题和内容

USB驱动开发实战:从设备枚举到URB异步传输

本文由 linuxROS 整理发布,首发于 linuxros.cn,转载请注明出处。

USB驱动开发实战:从设备枚举到URB异步传输

导读:USB是最通用的外设接口,但USB驱动开发却藏着不少坑。URB是同步还是异步?四种传输类型怎么选?disconnect忘记kill_urb会怎样?本文从USB拓扑讲起,结合RK3506B平台,给出完整可运行驱动代码和避坑指南。


一、原理简析

USB 拓扑结构

USB采用主机-集线器-设备的树状拓扑,所有通信由Host发起,Device只能被动响应:

flowchart TB subgraph Host["主机侧"] CPU["CPU"] HC["USB Host Controller"] end subgraph Hub["集线器层"] RH["Root Hub"] H1["Hub 1"] H2["Hub 2"] end subgraph Device["设备层"] D1["键盘 EP"] D2["U盘 EP"] D3["摄像头 EP"] end CPU --> HC HC --> RH RH --> H1 RH --> H2 H1 --> D1 H1 --> D2 H2 --> D3 style Host fill:#E3F2FD,stroke:#1976D2 style Hub fill:#E8F5E9,stroke:#388E3C style Device fill:#FFEBEE,stroke:#D32F2F

Host Controller是USB总线的大脑,负责枚举设备、分配地址、发起传输。RK3506B使用DesignWare dwc2控制器,支持USB 2.0 OTG/Host模式。

USB 驱动分层架构

flowchart TB APP["应用层(用户程序)"] --> DRV DRV["USB Device Driver<br/>(usb_driver)"] --> CORE CORE["USB Core<br/>枚举/URB管理/驱动匹配"] --> HCD HCD["Host Controller Driver<br/>dwc2/ehci/xhci"] --> HW["USB 硬件控制器"] style APP fill:#E3F2FD,stroke:#1976D2 style DRV fill:#FFF8E1,stroke:#F57C00 style CORE fill:#E8F5E9,stroke:#388E3C style HCD fill:#FFEBEE,stroke:#D32F2F style HW fill:#F3E5F5,stroke:#7B1FA2

在Rockchip平台上,HCD层对应drivers/usb/dwc2/,基于Synopsys DesignWare USB 2.0 IP核。驱动开发者在设备驱动层工作,调用USB Core提供的标准API即可。

USB 2.0 vs USB 3.0 对比

特性 USB 2.0 USB 3.0
控制器 EHCI / dwc2 xHCI
速率 480Mbps(HS) 5Gbps(SS)
端点数量 最多15个IN+15个OUT 最多255个端点
传输方式 半双工(轮询) 全双工(异步通知)
电力供给 500mA 900mA
数据编码 NRZI 8b/10b
RK平台 所有Rockchip芯片均支持 RK356x/RK3588等中高端芯片支持

RK3506B集成USB 2.0 OTG控制器,支持Host/Device/Gadget三种模式,最高480Mbps。对于嵌入式场景,USB 2.0已足够覆盖绝大多数外设需求。

USB 描述符层级

描述符 大小 说明
Device 18B VID/PID、设备类、配置数
Config 9B 功耗、接口数、属性
Interface 9B 接口类/子类/协议、端点数量
Endpoint 7B 端点地址、方向、类型、最大包大小
String 可变 厂商名、产品名、序列号(可选)

描述符是USB设备的"身份证"。VID/PID决定驱动匹配,端点描述符决定数据传输方式。lsusb -v可查看完整描述符树。


二、传输类型与URB机制对比

四种传输类型

flowchart TB A(["选择传输类型"]) --> B{"数据传输需求"} B -->|"设备枚举<br/>配置访问"| C["控制传输<br/>双向,可靠,低带宽"] B -->|"大数据量<br/>可靠性优先"| D["批量传输<br/>单向,可靠,高带宽"] B -->|"小数据量<br/>周期性实时"| E["中断传输<br/>单向,可靠,周期性"] B -->|"音视频流<br/>实时性优先"| F["等时传输<br/>单向,不可靠,高带宽"] C --> C1["usb_control_msg<br/>同步API"] D --> D1["usb_fill_bulk_urb<br/>+ usb_submit_urb"] E --> E1["usb_fill_int_urb<br/>+ interval参数"] F --> F1["usb_submit_urb<br/>+ iso_packets"] C1 --> G(["数据传输完成"]) D1 --> G E1 --> G F1 --> G style A fill:#E3F2FD,stroke:#1976D2 style B fill:#FFF8E1,stroke:#F57C00 style C fill:#FFEBEE,stroke:#D32F2F style D fill:#E8F5E9,stroke:#388E3C style E fill:#F3E5F5,stroke:#7B1FA2 style F fill:#FFF8E1,stroke:#F57C00 style G fill:#E8F5E9,stroke:#388E3C

四种传输类型对比

传输类型 方向 可靠性 带宽 典型应用 内核API
控制传输 双向 可靠 低 枚举、描述符读取 usb_control_msg
批量传输 单向 可靠 高 U盘、打印机 usb_fill_bulk_urb
中断传输 单向 可靠 低 键盘、鼠标 usb_fill_int_urb
等时传输 单向 不可靠 高 摄像头、音频 手动填充URB+iso_packets

选型建议:控制传输用于枚举和配置,批量传输用于大数据块,中断传输用于周期性小数据(HID设备),等时传输用于实时音视频。

URB 异步 vs 同步传输对比

维度 异步URB 同步API
API usb_submit_urb usb_bulk_msg等
执行方式 非阻塞,回调通知 阻塞,等待完成
适用场景 高性能、持续数据流 简单控制、一次性传输
回调上下文 中断上下文(不可睡眠) 进程上下文(可睡眠)
内存分配注意 回调中用GFP_ATOMIC 可用GFP_KERNEL
取消方式 usb_kill_urb 超时返回或signal中断

USB驱动推荐:持续数据流(如批量传输)用异步URB,回调中重新提交形成循环。一次性控制操作用同步API更简洁。


三、驱动API速查总表

USB 驱动开发核心API对比

操作 核心API 释放函数 调用时机 关键注意
驱动注册 usb_register usb_deregister init/exit 或用module_usb_driver宏
设备匹配 USB_DEVICE(vid, pid) 无需释放 id_table 定义在usb_device_id表中
获取设备 interface_to_usbdev 无需释放 probe首步 从usb_interface获取设备
端点探测 usb_endpoint_is_bulk_in 无需释放 probe中 遍历cur_altsetting端点
URB分配 usb_alloc_urb usb_free_urb 端点探测后 等时传输设iso_packets参数
URB填充 usb_fill_bulk_urb 无需释放 分配后 指定管道、缓冲区、回调、context
URB提交 usb_submit_urb 无需释放 填充后 回调中用GFP_ATOMIC
URB取消 usb_kill_urb 无需释放 disconnect 必须等URB完全停止
管道创建 usb_rcvbulkpipe 无需释放 URB填充前 区分IN/OUT方向
同步控制 usb_control_msg 无需释放 任意时刻 阻塞等待,可设超时
同步批量 usb_bulk_msg 无需释放 任意时刻 返回实际传输长度
数据保存 usb_set_intfdata 无需释放 probe末尾 与usb_get_intfdata配对

四、实战步骤

RK3506B 设备树配置

Rockchip平台需要先配置设备树使能USB控制器。RK3506B使用DesignWare dwc2控制器,DTS文件位于kernel/arch/arm/boot/dts/目录下。以下为参考配置模板(具体寄存器地址和中断号请以实际SDK中rk3506.dtsi为准):

&usb20_otg {
    compatible = "rockchip,rk3506-usb", "snps,dwc2";
    reg = <0x0 0xff400000 0x0 0x40000>;
    interrupts = <GIC_SPI 62 IRQ_TYPE_LEVEL_HIGH>;
    clocks = <&cru HCLK_OTG>, <&cru HCLK_OTG_PMU>;
    clock-names = "otg", "otg_pmu";
    dr_mode = "otg";
    g-use-dma;
    g-rx-fifo-size = <512>;
    g-np-tx-fifo-size = <32>;
    phys = <&usb2phy_otg>;
    phy-names = "usb2-phy";
    status = "okay";
};

&usb2phy_otg {
    status = "okay";
};

关键属性说明:

来自 linuxros.cn · linuxROS
  • dr_mode:"host"纯主机模式、"otg"双角色模式、"peripheral"纯设备模式
  • g-use-dma:使能DMA传输,大幅降低CPU占用
  • phys:绑定USB PHY,RK3506B内置USB 2.0 PHY
  • compatible:"snps,dwc2"匹配DesignWare dwc2驱动,"rockchip,rk3506-usb"匹配Rockchip平台胶合层
  • g-rx-fifo-size / g-np-tx-fifo-size:Device模式下接收/发送FIFO大小,按需调整

内核配置需确保以下选项(RK3506B SDK默认已开启):

CONFIG_USB=y
CONFIG_USB_DWC2=y
CONFIG_USB_DWC2_HOST=y
CONFIG_USB_DWC2_DUAL_ROLE=y
CONFIG_USB_ANNOUNCE_NEW_DEVICES=y

完整驱动代码

以下为完整的USB设备驱动,涵盖设备结构、端点探测、URB异步传输、probe/remove和驱动注册:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/usb.h>

#define DRIVER_NAME "usb_demo"
#define VENDOR_ID   0x1234
#define PRODUCT_ID  0x5678

struct usb_demo_dev {
    struct usb_device *udev;
    struct usb_interface *intf;

    unsigned char *bulk_in_buf;
    size_t bulk_in_size;
    __u8 bulk_in_ep;
    __u8 bulk_out_ep;

    struct urb *bulk_in_urb;
};

static const struct usb_device_id usb_demo_ids[] = {
    { USB_DEVICE(VENDOR_ID, PRODUCT_ID) },
    { }
};
MODULE_DEVICE_TABLE(usb, usb_demo_ids);

static void usb_demo_bulk_in_cb(struct urb *urb)
{
    struct usb_demo_dev *dev = urb->context;
    int ret;

    if (urb->status) {
        if (urb->status != -ENOENT && urb->status != -ESHUTDOWN)
            dev_err(&dev->intf->dev,
                    "URB error: %d\n", urb->status);
        return;
    }

    dev_info(&dev->intf->dev,
             "received %d bytes\n", urb->actual_length);

    ret = usb_submit_urb(urb, GFP_ATOMIC);
    if (ret)
        dev_err(&dev->intf->dev,
                "resubmit URB failed: %d\n", ret);
}

static int usb_demo_probe(struct usb_interface *intf,
                          const struct usb_device_id *id)
{
    struct usb_device *udev = interface_to_usbdev(intf);
    struct usb_demo_dev *dev;
    struct usb_host_interface *iface_desc;
    struct usb_endpoint_descriptor *ep;
    int i, ret;

    dev = devm_kzalloc(&intf->dev, sizeof(*dev), GFP_KERNEL);
    if (!dev)
        return -ENOMEM;

    dev->udev = udev;
    dev->intf = intf;

    /* 1. 遍历端点,记录批量IN/OUT端点地址 */
    iface_desc = &intf->altsetting[0];
    for (i = 0; i < iface_desc->desc.bNumEndpoints; i++) {
        ep = &iface_desc->endpoint[i].desc;

        if (!dev->bulk_in_ep &&
            usb_endpoint_is_bulk_in(ep)) {
            dev->bulk_in_size = usb_endpoint_maxp(ep);
            dev->bulk_in_ep = ep->bEndpointAddress;
        }

        if (!dev->bulk_out_ep &&
            usb_endpoint_is_bulk_out(ep)) {
            dev->bulk_out_ep = ep->bEndpointAddress;
        }
    }

    if (!dev->bulk_in_ep || !dev->bulk_out_ep) {
        dev_err(&intf->dev, "endpoints not found\n");
        return -ENODEV;
    }

    /* 2. 分配接收缓冲区 */
    dev->bulk_in_buf = kmalloc(dev->bulk_in_size, GFP_KERNEL);
    if (!dev->bulk_in_buf)
        return -ENOMEM;

    /* 3. 分配并填充批量IN URB */
    dev->bulk_in_urb = usb_alloc_urb(0, GFP_KERNEL);
    if (!dev->bulk_in_urb) {
        ret = -ENOMEM;
        goto err_free_buf;
    }

    usb_fill_bulk_urb(dev->bulk_in_urb, udev,
                      usb_rcvbulkpipe(udev, dev->bulk_in_ep),
                      dev->bulk_in_buf, dev->bulk_in_size,
                      usb_demo_bulk_in_cb, dev);

    /* 4. 保存私有数据 */
    usb_set_intfdata(intf, dev);

    /* 5. 提交URB,启动异步传输 */
    ret = usb_submit_urb(dev->bulk_in_urb, GFP_KERNEL);
    if (ret) {
        dev_err(&intf->dev, "submit URB failed: %d\n", ret);
        goto err_free_urb;
    }

    dev_info(&intf->dev, "USB device probed, "
             "ep_in=0x%02x ep_out=0x%02x\n",
             dev->bulk_in_ep, dev->bulk_out_ep);
    return 0;

err_free_urb:
    usb_free_urb(dev->bulk_in_urb);
err_free_buf:
    kfree(dev->bulk_in_buf);
    return ret;
}

static void usb_demo_disconnect(struct usb_interface *intf)
{
    struct usb_demo_dev *dev = usb_get_intfdata(intf);

    usb_kill_urb(dev->bulk_in_urb);
    usb_free_urb(dev->bulk_in_urb);
    kfree(dev->bulk_in_buf);

    usb_set_intfdata(intf, NULL);

    dev_info(&intf->dev, "USB device removed\n");
}

static struct usb_driver usb_demo_driver = {
    .name = DRIVER_NAME,
    .id_table = usb_demo_ids,
    .probe = usb_demo_probe,
    .disconnect = usb_demo_disconnect,
};

module_usb_driver(usb_demo_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Driver Developer");
MODULE_DESCRIPTION("USB Demo Driver");

probe 5步走:遍历端点 → 分配缓冲区 → 分配URB → 填充URB → 提交URB。disconnect中必须先usb_kill_urb再释放资源,顺序反了内核崩。

批量OUT传输封装

当需要向设备发送数据时,封装同步批量OUT传输函数:

static int usb_demo_bulk_out(struct usb_demo_dev *dev,
                              void *data, size_t len)
{
    int ret, actual_len;

    ret = usb_bulk_msg(dev->udev,
                       usb_sndbulkpipe(dev->udev, dev->bulk_out_ep),
                       data, len, &actual_len, 5000);
    if (ret) {
        dev_err(&dev->intf->dev,
                "bulk out failed: %d\n", ret);
        return ret;
    }

    if (actual_len != len) {
        dev_warn(&dev->intf->dev,
                 "short write: %d/%zu\n", actual_len, len);
        return -EIO;
    }

    return 0;
}

usb_bulk_msg是同步API,内部封装了URB分配+提交+等待+释放的完整流程,适合不频繁的单次传输。注意检查actual_len防止短写。

调试命令速查

# 查看所有USB设备(含VID/PID和速度)
lsusb -v

# 查看USB设备树拓扑
lsusb -t

# 查看内核USB设备原始信息
cat /sys/kernel/debug/usb/devices

# 查看USB驱动绑定关系
ls -l /sys/bus/usb/drivers/

# 手动解绑/绑定设备(调试用)
echo "1-1:1.0" > /sys/bus/usb/drivers/usb_demo/unbind
echo "1-1:1.0" > /sys/bus/usb/drivers/usb_demo/bind

# 查看内核日志
dmesg | grep -i usb

# 抓取USB总线原始数据(需usbmon模块)
modprobe usbmon
cat /sys/kernel/debug/usb/usbmon/1u

# 查看dwc2控制器寄存器和参数(路径因平台而异)
ls /sys/kernel/debug/*.dwc2/
cat /sys/kernel/debug/*.dwc2/params

# 查看USB PHY注册信息
cat /sys/kernel/debug/usb/phy

五、常见问题解决

probe阶段

现象 根因 解决方案
probe未被调用 usb_device_id与设备VID/PID不匹配 lsusb确认设备ID,核对id_table
端点探测失败 接口选择错误或端点类型不匹配 确保遍历intf->altsetting[0],用usb_endpoint_is_*宏检查
usb_alloc_urb返回NULL 内存不足或等时参数错误 非等时传输iso_packets设为0,检查GFP标志
设备未出现在lsusb中 USB PHY未初始化或dwc2驱动未加载 检查dmesg \| grep dwc2,确认PHY供电和时钟配置

URB相关

现象 根因 解决方案
URB回调不触发 忘记调用usb_submit_urb probe末尾必须提交第一个URB
回调中usb_submit_urb失败 使用了GFP_KERNEL 回调运行在中断上下文,必须用GFP_ATOMIC
数据传输乱码 缓冲区大小与端点wMaxPacketSize不匹配 用usb_endpoint_maxp获取真实大小后再分配
usb_submit_urb返回-ENODEV 设备已断开 在disconnect中处理,检查urb->status是否为-ENOENT

中断相关

现象 根因 解决方案
usb_submit_urb返回-ENOMEM URB池耗尽 减少同时提交的URB数量或增大CONFIG_USB_MAX_URBS
中断传输丢包 interval设置过大 按端点描述符中的bInterval设置,单位为帧(1ms/125μs)

资源释放顺序

/* 错误!disconnect中先释放缓冲区再取消URB */
kfree(dev->bulk_in_buf);     // URB可能还在访问buffer!
usb_kill_urb(dev->bulk_in_urb);

/* 正确!先取消URB,再释放资源 */
usb_kill_urb(dev->bulk_in_urb);   // 1. 等待URB完全停止
usb_free_urb(dev->bulk_in_urb);   // 2. 释放URB结构
kfree(dev->bulk_in_buf);          // 3. 最后释放缓冲区

铁律:usb_kill_urb是disconnect的第一行,必须等URB完全停止后才能释放buffer和URB。usb_kill_urb会阻塞等待正在进行的传输完成,保证回调不再被调用。


六、总结

驱动开发决策流程

flowchart TB A(["需要开发USB驱动"]) --> B{"设备类型"} B -->|"标准类设备<br/>HID/存储/音频"| C["使用内核通用驱动<br/>无需自行开发"] B -->|"自定义厂商设备<br/>非标准协议"| D{"传输需求"} D -->|"控制+批量<br/>U盘/采集卡"| E["批量传输 + URB异步<br/>持续数据流"] D -->|"中断传输<br/>HID自定义设备"| F["中断传输 + URB<br/>周期性小数据"] D -->|"等时传输<br/>摄像头/音频"| G["等时传输 + URB<br/>实时不可靠"] E --> H{"需要同步API?"} F --> H G --> H H -->|"简单控制"| I["usb_control_msg<br/>同步阻塞"] H -->|"批量数据"| J["usb_bulk_msg<br/>同步阻塞"] E --> K(["注册module_usb_driver"]) F --> K G --> K I --> K J --> K style A fill:#E3F2FD,stroke:#1976D2 style B fill:#FFF8E1,stroke:#F57C00 style C fill:#E8F5E9,stroke:#388E3C style D fill:#FFF8E1,stroke:#F57C00 style E fill:#E8F5E9,stroke:#388E3C style F fill:#F3E5F5,stroke:#7B1FA2 style G fill:#FFEBEE,stroke:#D32F2F style H fill:#FFF8E1,stroke:#F57C00 style I fill:#E3F2FD,stroke:#1976D2 style J fill:#E3F2FD,stroke:#1976D2 style K fill:#E8F5E9,stroke:#388E3C

API速查

步骤 API 要点
驱动注册 module_usb_driver 一行搞定init/exit
设备匹配 USB_DEVICE(vid, pid) 定义在id_table中
端点探测 usb_endpoint_is_bulk_in 遍历altsetting所有端点
端点大小 usb_endpoint_maxp 获取wMaxPacketSize
URB分配 usb_alloc_urb 非等时iso_packets=0
URB填充 usb_fill_bulk_urb 指定管道、回调、context
URB提交 usb_submit_urb 回调中用GFP_ATOMIC
URB取消 usb_kill_urb disconnect第一行调用
同步传输 usb_bulk_msg / usb_control_msg 阻塞等待,适合一次性操作

经验总结

usb_kill_urb是disconnect的生命线——忘记调用它,URB回调可能访问已释放的buffer,导致内核panic。这是USB驱动开发第一坑。

GFP_ATOMIC是回调中的铁律——URB回调运行在中断上下文,使用GFP_KERNEL会触发内核睡眠警告甚至死锁。所有在回调中usb_submit_urb重新提交的代码,必须用GFP_ATOMIC。

端点遍历必须检查类型——不要假设端点编号,遍历altsetting后用usb_endpoint_is_*宏判断类型和方向。硬编码端点索引是bug的来源。

Rockchip上注意OTG模式——dr_mode配置错误(如写成"host"但硬件是OTG),会导致设备无法枚举。RK3506B的dwc2控制器默认OTG模式,需确认设备树配置与硬件设计一致。

urb->status检查不可省略——回调中忽略-ENOENT和-ESHUTDOWN以外的错误状态,会导致断连后无限重试提交URB。

版权声明

作者linuxROS
协议本作品采用 CC BY-NC-SA 4.0 许可协议:署名-非商业性使用-相同方式共享
关注欢迎关注微信公众号 linuxROS,获取更多机器人 / 嵌入式 / Linux 干货
返回首页