3. USB接口基础:USB协议简介、USB控制器初始化、USB设备枚举流程

做机顶盒WiFi驱动,USB接口是绕不开的坎。说实话,现在市面上九成以上的外置WiFi模块都是走USB接口的。你想想看,USB协议虽然复杂,但我们搞驱动开发,真正需要吃透的就三块:协议基础、控制器初始化、设备枚举流程。今天我就把这三点掰开了讲清楚。

3.1 USB协议简介——别被那些术语吓到

USB协议,说白了就是一套主从通信的规矩。主机(Host)说了算,设备(Device)乖乖听话。我刚开始接触USB时,看到那些描述符、端点、管道之类的术语,头都大了。后来做多了才发现,核心就几个概念。

3.1.1 物理层与电气特性

USB使用差分信号传输,D+和D-两根线。速度不同,电气特性也不一样:

USB版本 速率 信号编码 线缆最大长度
USB 1.1 12 Mbps(全速) NRZI 5米
USB 2.0 480 Mbps(高速) NRZI + 位填充 5米
USB 3.0 5 Gbps 8b/10b 3米

机顶盒WiFi模块大多是USB 2.0高速设备。嗯,这里要注意:高速设备在D+线上会接一个1.5kΩ上拉电阻,全速设备则是在D-线上。这个细节在调试时很有用,我曾经因为上拉电阻焊错了位置,折腾了两天才找到原因。

3.1.2 协议分层结构

USB协议分三层,从上到下是:

  • 功能层:设备具体干什么,比如WiFi模块收发数据
  • 设备层:管理设备状态、配置、接口
  • 总线接口层:处理包传输、事务、传输类型

做驱动开发,我们最常打交道的是设备层。你得理解设备描述符、配置描述符、接口描述符、端点描述符这四件套。我习惯把它们比作一本书:描述符是目录,配置是章节,接口是段落,端点是句子。

3.1.3 传输类型

USB定义了四种传输类型,WiFi驱动里全用得上:

传输类型 特点 WiFi驱动中的应用
控制传输 可靠,双向,有保证 设备枚举、命令下发
批量传输 可靠,但延迟不确定 大数据收发(TCP数据)
中断传输 有保证的延迟,但数据量小 WiFi事件通知、扫描结果
等时传输 实时性高,但可能丢数据 音频视频流(WiFi模块很少用)

我个人习惯把批量传输用在数据通道上,中断传输用在事件通知上。控制传输嘛,初始化时用得多,跑起来后就很少碰了。

3.2 USB控制器初始化——让硬件准备好

USB控制器,在机顶盒里通常是集成在SoC中的。初始化流程各家芯片略有不同,但大框架是一样的。

3.2.1 控制器硬件初始化

以常见的EHCI(USB 2.0)控制器为例,初始化步骤大致如下:

// USB控制器初始化伪代码
void usb_hcd_init(struct usb_hcd *hcd)
{
    // 1. 使能时钟和电源
    clk_enable(hcd->clk);
    regulator_enable(hcd->vbus_reg);
    
    // 2. 复位控制器
    writel(USBCMD_HCRESET, hcd->regs + USBCMD);
    mdelay(10);  // 等待复位完成
    
    // 3. 设置控制器模式为主机模式
    writel(USBMODE_CM_HOST, hcd->regs + USBMODE);
    
    // 4. 配置中断
    writel(USBINTR_ALL, hcd->regs + USBINTR);
    
    // 5. 启动控制器
    writel(USBCMD_RUN_STOP, hcd->regs + USBCMD);
    
    // 6. 等待控制器就绪
    while (!(readl(hcd->regs + USBSTS) & USBSTS_HCH))
        cpu_relax();
}

这段代码看着简单,但坑不少。我遇到过最典型的问题是:复位后不等够时间就直接操作寄存器,结果控制器根本没准备好。还有一次是电源时序不对,先给了时钟再给电源,控制器直接罢工。

避坑指南: 我曾经在某个海思平台上一开始没注意VBUS使能时序,结果USB设备插上去没反应。后来发现是VBUS要在控制器复位完成后才稳定输出。记住:电源 -> 时钟 -> 复位 -> 配置,这个顺序别搞反了。

3.2.2 根Hub初始化

USB控制器内部集成了一个根Hub(Root Hub)。初始化完控制器,还得把根Hub搞起来:

  • 注册根Hub设备
  • 配置端口属性(比如端口数量、供电能力)
  • 使能端口状态变化检测中断

根Hub初始化完成后,控制器就等着设备插上来了。一旦检测到D+/D-上的电平变化,就会触发端口状态中断,然后开始枚举流程。

3.3 USB设备枚举流程——设备是怎么被认出来的

枚举,就是主机发现并配置设备的过程。这个过程很有意思,我当年第一次跟踪枚举流程时,感觉就像在看一场精心编排的舞蹈。

3.3.1 枚举的七个步骤

标准的USB枚举流程分七步:

  1. 设备连接:设备插入,D+或D-被拉高,主机检测到端口状态变化
  2. 复位设备:主机向端口发送复位信号,设备地址重置为0
  3. 获取设备描述符:主机发控制请求,获取前8字节(含最大包长度)
  4. 再次复位:根据设备速度重新复位(高速设备需要这一步)
  5. 设置地址:主机分配唯一地址给设备
  6. 获取完整描述符:获取设备、配置、接口、端点等全部描述符
  7. 选择配置:主机选择一个配置,设备进入配置状态

你想想看,从设备插上到能正常工作,也就几百毫秒的事。但这里面每一步都可能出问题。

3.3.2 枚举过程中的关键交互

枚举的核心是控制传输。主机通过默认端点(端点0)和设备通信。我画个简化的交互流程:

主机 --> 设备: GET_DESCRIPTOR (设备描述符, 8字节)
设备 --> 主机: 返回前8字节 (含bMaxPacketSize0)
主机 --> 设备: SET_ADDRESS (地址=1)
设备 --> 主机: 状态阶段完成
主机 --> 设备: GET_DESCRIPTOR (设备描述符, 完整18字节)
设备 --> 主机: 返回完整设备描述符
主机 --> 设备: GET_DESCRIPTOR (配置描述符)
设备 --> 主机: 返回配置描述符 + 接口描述符 + 端点描述符
主机 --> 设备: SET_CONFIGURATION (配置值=1)
设备 --> 主机: 状态阶段完成
关键点: 第一次获取设备描述符时只取8字节,是因为此时还不知道设备端点0的最大包长度。拿到bMaxPacketSize0后,后续传输才能用正确的包大小。这个细节在调试时特别重要,我见过有人直接请求18字节,结果设备返回的数据不全,枚举就卡住了。

3.3.3 枚举失败常见原因

做WiFi驱动调试,枚举失败是最常见的问题。我总结了几种典型情况:

现象 可能原因 排查方法
设备连接后无中断 VBUS没电、上拉电阻问题 量D+/D-电压,检查电源
获取描述符超时 地址冲突、设备未复位 抓USB总线数据,看复位时序
设置地址后设备无响应 地址分配错误、设备固件bug 换一个已知正常的设备对比测试
配置描述符解析失败 描述符长度不对、格式错误 用USB分析仪抓包,逐字节检查

说实话,枚举问题八成出在硬件上。我建议你先用示波器看看D+/D-的波形,确认设备确实发出了信号,再往软件层面排查。

3.4 实战经验:WiFi模块枚举的特殊之处

WiFi模块作为USB设备,枚举时有个特点:它通常需要加载固件后才能完成枚举。也就是说,模块刚插上时可能只是一个「空壳」,主机枚举到某个阶段后,需要先下载固件到模块里,然后模块复位,重新枚举一次。

我遇到过最头疼的情况是:模块第一次枚举成功了,固件也下载进去了,但第二次枚举时设备地址变了,导致上层应用找不到设备。后来发现是驱动里没有处理好设备移除和重新添加的事件。

小技巧: 调试WiFi模块枚举时,建议在驱动里加一些调试打印,把每次枚举的关键步骤和返回值都打出来。比如这样:
dev_info(dev, "Enumeration step %d: %s\n", step, 
         status == 0 ? "OK" : "FAIL");
有了这些日志,定位问题会快很多。

嗯,USB接口基础就讲到这里。下一章我们会深入WiFi模块的USB通信细节,包括批量传输的urb管理、中断传输的异步通知机制。到时候再聊。