第3章:USB接口基础
各位同学,今天我们来聊聊USB接口。说实话,USB这东西看起来简单——插上去就能用,对吧?但做驱动开发的人都知道,这里面的门道可不少。我在做POS机项目时,就遇到过USB蓝牙模块死活枚举不成功的情况,折腾了两天才找到原因。嗯,今天我就把这些经验揉碎了讲给你们听。
3.1 USB协议概述
USB,全称Universal Serial Bus,通用串行总线。说白了,它就是一套主机和设备之间的通信规则。为什么叫"通用"?因为它能接键盘、鼠标、U盘、打印机,还能接WiFi和蓝牙模块——我们POS机上的无线模块,绝大多数都是走USB接口的。
USB的物理层其实很简单,就4根线:VBUS(电源)、GND(地)、D+和D-(差分数据线)。但别小看这4根线,它们承载的协议可不简单。我个人习惯把USB协议分成三层来看:
- 物理层:电气特性、信号编码(NRZI编码,带位填充)
- 协议层:包结构、事务处理、传输类型
- 应用层:设备类规范(比如HID、CDC、Mass Storage)
我们做WiFi蓝牙模块驱动,最关心的是协议层和应用层。因为模块通常被枚举为CDC(通信设备类)或自定义设备,我们需要在驱动里处理好这些。
3.2 USB设备枚举过程
枚举,这个词听起来高大上,其实就是设备插上后,主机和它"握手认识"的过程。我刚开始做驱动时,总觉得枚举是硬件自动完成的,直到有一次设备枚举失败,我才被迫去研究它的每个细节。
枚举过程大致是这样的:
- 设备插入:D+或D-被拉高,主机检测到电平变化
- 复位:主机拉低D+和D-至少10ms,设备进入默认状态
- 获取设备描述符:主机发控制传输,要设备描述符(18字节)
- 设置地址:主机分配一个唯一地址给设备
- 获取配置描述符:主机再要配置描述符、接口描述符、端点描述符
- 选择配置:主机发Set Configuration,设备进入配置状态
- 加载驱动:主机根据设备类或VID/PID匹配驱动
这里有个坑,我曾经踩过:设备描述符里的bMaxPacketSize0字段,如果填错了,枚举就会卡在第3步。我遇到过一款蓝牙模块,厂商把最大包长写成了64,但实际只支持8,结果在Linux下枚举失败,Windows下却能用。为什么?因为Windows的USB栈更宽容,会尝试重试。Linux则直接放弃。嗯,这就是为什么我们做嵌入式Linux开发,必须对协议细节了如指掌。
3.3 USB传输类型
USB定义了四种传输类型,每种都有自己的脾气。你想想看,鼠标和U盘对传输的要求能一样吗?鼠标要低延迟,U盘要高吞吐。所以USB协议设计了四种类型来满足不同需求。
| 传输类型 | 特点 | 典型应用 | 我遇到的坑 |
|---|---|---|---|
| 控制传输 | 双向、可靠、数据量小 | 枚举、设备配置 | 控制传输超时导致枚举失败 |
| 批量传输 | 可靠、数据量大、延迟不确定 | U盘、打印机 | 批量端点带宽被占满,其他传输卡死 |
| 中断传输 | 可靠、延迟有保证、数据量小 | 鼠标、键盘 | 中断间隔设置太短,CPU占用高 |
| 同步传输 | 不可靠、延迟固定、数据量大 | 音频、视频 | 丢包导致音频爆音,需要加缓冲 |
我们做WiFi蓝牙模块,最常用的是批量传输和中断传输。WiFi数据走批量传输,因为要保证数据完整性;蓝牙的HCI命令走中断传输,因为需要低延迟。至于同步传输,蓝牙音频(A2DP)会用到,但那是音频驱动的事,我们做模块驱动时一般只提供数据通道。
3.4 Linux USB子系统架构
Linux的USB子系统,说白了就是三层架构:
- USB核心层(USB Core):负责USB协议栈、枚举、urb管理
- USB主机控制器驱动(HCD):负责和硬件打交道,比如EHCI、OHCI、xHCI
- USB设备驱动:我们写的就是这一层,比如蓝牙驱动、WiFi驱动
我画个简单的数据流:应用程序 → USB设备驱动 → USB核心 → HCD → 硬件。反过来也一样。
写驱动时,我们主要和USB核心层打交道。核心的数据结构是struct usb_device和struct usb_interface。每个USB设备对应一个usb_device,每个接口对应一个usb_interface。我们的WiFi蓝牙模块通常是一个复合设备,有多个接口——一个用于WiFi数据,一个用于蓝牙HCI,可能还有一个用于控制。
下面是一个简单的USB驱动框架,我写驱动时习惯先搭这个骨架:
#include <linux/usb.h>
static int my_probe(struct usb_interface *intf,
const struct usb_device_id *id)
{
struct usb_device *udev = interface_to_usbdev(intf);
struct usb_endpoint_descriptor *ep;
// 遍历端点,找到批量输入和批量输出端点
for (int i = 0; i < intf->cur_altsetting->desc.bNumEndpoints; i++) {
ep = &intf->cur_altsetting->endpoint[i].desc;
if (usb_endpoint_is_bulk_in(ep)) {
// 保存批量输入端点
}
if (usb_endpoint_is_bulk_out(ep)) {
// 保存批量输出端点
}
}
return 0;
}
static void my_disconnect(struct usb_interface *intf)
{
// 清理资源
}
static struct usb_driver my_driver = {
.name = "my_usb_driver",
.probe = my_probe,
.disconnect = my_disconnect,
.id_table = my_id_table,
};
module_usb_driver(my_driver);
这段代码看起来简单,但里面有个细节:usb_endpoint_is_bulk_in这个宏,它检查端点的方向(IN表示设备到主机)和传输类型(批量)。我刚开始写时,总是搞混IN和OUT的方向——IN是设备发数据给主机,OUT是主机发数据给设备。记住这个,能省不少调试时间。
1.
lsusb -t 查看USB设备树2.
cat /sys/kernel/debug/usb/devices 查看设备描述符3.
usbmon 抓USB数据包这三招能解决90%的问题。剩下的10%,嗯,那就得上USB分析仪了。
最后说一句,USB子系统在Linux内核里已经非常成熟了。我们写驱动,大部分时候是在写设备驱动层,核心层和HCD层基本不用动。但理解整个架构很重要——出了问题你知道该去查哪一层。我曾经遇到一个诡异的问题,urb提交后一直不返回,查了两天才发现是HCD的调度器有bug,升级内核版本就好了。所以,别只盯着自己的驱动代码,有时候问题出在别处。
好了,USB基础就讲到这里。下一章我们开始实战——写一个USB蓝牙模块的驱动。到时候我会带着你们一步步调试,把今天讲的理论用起来。