上周调试一块新板子,遇到个邪门的问题------USB设备插上去能识别,但加载驱动后直接报-71错误(EPROTO)。抓dmesg看到一堆URB超时,硬件同事赌咒发誓PHY参数绝对没问题。最后发现是控制器驱动里一个DMA缓冲区对齐配置错了,32位系统里按64位对齐,直接导致传输描述符链表断裂。这种问题不摸清USB整体架构,根本无从下手。
USB那套"三层楼"模型
搞Linux驱动的都知道,USB子系统像栋三层楼。最底层是硬件控制器 ,要么是UHCI/OHCI这种老派方案,要么是现在主流的EHCI/xHCI。这层由usb_hcd(Host Controller Driver)管着,负责把USB协议时序变成实实在在的电气信号。有意思的是,xHCI驱动里经常能看到这种注释:"某厂商芯片的DMA引擎有坑,传输完成中断得延迟处理"------这就是硬件差异带来的"特色"。
中间那层是核心层(usbcore) ,相当于大楼的物业管理。它不关心你插的是U盘还是鼠标,只管提供通用服务:设备枚举、带宽分配、urb(USB Request Block)调度。这里有个关键数据结构struct usb_device,从插入那一刻就诞生,里面藏着设备描述符、配置描述符这些家底。记得早年调试时,我总喜欢在hub_event函数里加打印,看枚举过程到底卡在哪一步。
顶层是设备驱动层 ,这才是我们平时打交道最多的。键盘、U盘、USB转串口,各有各的驱动。它们通过usb_register_driver向核心层报到,靠id_table声明"我能管哪些设备"。这里容易踩的坑是:别在probe函数里假设端点一定存在 ,一定要先检查interface里的endpoint描述符。我就见过有个驱动没检查端点类型,把中断端点当批量端点用,传输能成功才怪。
那个绕不开的urb
urb(USB Request Block)是USB通信的原子操作单元。你可以把它理解为"USB封包快递单",里面写着收件地址(端点号)、货物内容(数据缓冲区)、送货方式(控制/批量/中断/等时)。提交urb后,它就进入HCD的调度队列,等控制器硬件搬数据。
写驱动时最常干的事就是填urb结构体。控制传输必须用usb_fill_control_urb,批量传输用usb_fill_bulk_urb,等等。这里有个细节:urb完成回调是在中断上下文执行的,所以不能在里面睡大觉。我习惯在回调里只做两件事:标记完成状态、唤醒等待进程。复杂处理交给工作队列。
c
/* 示例:批量传输的urb填充 */
static void bulk_callback(struct urb *urb)
{
/* 这里别搞复杂操作! */
struct usb_device *dev = urb->dev;
if (urb->status) {
/* 记录错误,但别打印太多,否则系统日志会被刷爆 */
if (urb->status != -ENOENT) // 主动取消的urb不算错误
dev_dbg(&dev->dev, "传输失败: %d\n", urb->status);
}
complete(urb->context); // 简单唤醒等待者
}
枚举:USB设备的"入职仪式"
新设备插入时,那套枚举流程堪称标准操作手册。核心层先读取设备描述符,分配地址,再获取配置描述符,最后选择配置。整个过程在hub_port_init里完成,代码有耐心的话可以跟一遍。
有个冷知识:第一次获取设备描述符只用默认地址0,而且只读前8字节。因为这时候还不知道端点0的最大包长,保守假设为8字节。等拿到真实包长后,才会重新读完整描述符。早年有款芯片在这里使坏,第一次回复64字节,导致内核解析越界------这种硬件bug能让驱动工程师怀疑人生。
调试时那些救命技巧
遇到USB问题,别急着改代码,先按这个顺序排查:
- 看dmesg | grep usb ,确认设备有没有被识别。连
New USB device found都没有的话,问题在硬件或控制器驱动。 - lsusb -v 把描述符dump出来,对照协议看有没有异常值。我见过厂家把bcdUSB填成0x0100(USB 1.0?),结果某些主机控制器直接拒绝。
- usbmon抓包 ,这是终极武器。
modprobe usbmon后,cat /sys/kernel/debug/usb/usbmon/0u能看到原始USB事务。曾经有个设备枚举到一半重置,靠usbmon发现是某个描述符请求超时------原来是固件没及时响应。 - 如果是自己写的驱动,在urb回调里加状态打印。注意控制urb的status字段:0是成功,-ENOENT通常是被主动取消,-EPIPE表示端点失能(stall),-EPROTO就是协议错误(开头那个坑)。
个人经验:别把USB当黑盒
USB协议栈确实复杂,但千万别把它当魔法黑盒。我的习惯是:
- 保持描述符一致性:设备固件里的描述符、内核驱动的id_table、用户空间的规则文件(如udev),这三者必须对齐。曾经因为产品升级改了PID(Product ID),驱动忘了更新,设备插上后只能当通用HID用。
- 注意电源管理:USB设备支持挂起/恢复,但很多驱动没好好处理。特别是自己做的USB外设,唤醒信号没发对的话,设备会睡死过去。
- 重视urb生命周期 :提交urb后一定要等回调完成再释放资源。有次我在disconnect函数里直接kfree设备结构体,结果urb回调访问了已释放内存,直接内核崩溃。现在我都用
usb_kill_urb确保安全。 - 理解传输类型本质:控制传输用于配置,批量传输追求可靠,中断传输要低延迟,等时传输只管实时性不重传。选错类型就像用卡车送外卖------不是不行,但很别扭。
最后说句实在话:USB驱动调试到后期,往往变成和硬件工程师的"友好交流"。你拿出usbmon证据,他查PHY配置,最后发现是PCB走线阻抗没控好。所以啊,做嵌入式这行,边界感不能太强。