USB驱动开发实战:从设备枚举到URB异步传输
导读:USB是最通用的外设接口,但USB驱动开发却藏着不少坑。URB是同步还是异步?四种传输类型怎么选?disconnect忘记kill_urb会怎样?本文从USB拓扑讲起,结合RK3506B平台,给出完整可运行驱动代码和避坑指南。
一、原理简析
USB 拓扑结构
USB采用主机-集线器-设备的树状拓扑,所有通信由Host发起,Device只能被动响应:
Host Controller是USB总线的大脑,负责枚举设备、分配地址、发起传输。RK3506B使用DesignWare dwc2控制器,支持USB 2.0 OTG/Host模式。
USB 驱动分层架构
在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机制对比
四种传输类型
四种传输类型对比
| 传输类型 | 方向 | 可靠性 | 带宽 | 典型应用 | 内核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 PHYcompatible:"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会阻塞等待正在进行的传输完成,保证回调不再被调用。
六、总结
驱动开发决策流程
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。