第四章:HCI传输层详解(USB)

好,咱们今天来啃一块硬骨头——USB传输层。说实话,很多做蓝牙的工程师一听到USB就头大,觉得它太复杂。但我个人觉得,USB其实是个很优雅的设计,只要你抓住了它的核心脉络。

4.1 USB硬件接口:不只是插拔那么简单

先说说物理层。USB硬件接口,说白了就是四根线:VBUS、D+、D-、GND。嗯,就这么简单。但这里有个坑——D+和D-是差分信号线,不是普通的IO口。

我记得刚入行那会儿,有个项目用USB连接蓝牙芯片,结果死活枚举不上。折腾了两天,最后发现是D+和D-的走线长度差了3mm。你想想看,高速信号对时序有多敏感。

避坑指南: 我曾经在Layout时把D+和D-走成了等长,但忘了做阻抗匹配。结果USB 2.0 High-Speed模式下数据包疯狂报错。后来加了45欧姆的串联电阻,问题才解决。

USB接口有几种常见的物理形态:

  • Type-A:最常见的方形口,主机端用得多
  • Type-B:方形的,设备端常见,比如打印机
  • Micro-B:小型的,很多蓝牙dongle在用
  • Type-C:现在的主流,正反都能插

对于蓝牙HCI来说,最常见的还是Micro-B和Type-C。我个人习惯用Type-C,因为它的CC引脚可以做更多控制,比如检测设备插入、协商供电等。

4.2 HCI USB传输协议:数据怎么跑起来的

USB传输协议,说白了就是一套数据打包和解包的规则。蓝牙HCI通过USB传输时,用的是USB的批量传输和中断传输两种方式。

为什么会这样?因为蓝牙HCI的数据包有两种:

  • 命令和事件:用中断传输,保证实时性
  • ACL数据:用批量传输,追求吞吐量

这里有个关键点——USB的端点(Endpoint)。每个端点就像一个邮箱,主机往里面放数据,设备从里面取数据。蓝牙HCI通常需要三个端点:

端点号 方向 传输类型 用途
0x00 双向 控制传输 USB枚举和配置
0x01 输入(设备到主机) 中断传输 HCI事件
0x02 输出(主机到设备) 批量传输 HCI命令和ACL数据
0x03 输入 批量传输 ACL数据

你看,这个结构其实很清晰。主机通过端点0x02发命令,设备通过端点0x01回事件。ACL数据则走批量端点,因为它的数据量大,但实时性要求没那么高。

核心要点: 中断传输和批量传输的区别,说白了就是「快」和「稳」的取舍。中断传输保证延迟,批量传输保证数据完整性。

4.3 USB中断传输与批量传输:选对路子

咱们深入聊聊这两种传输方式。

4.3.1 中断传输

中断传输,名字听着吓人,其实它并不是真的中断CPU。它是USB主机定期轮询设备,看看有没有数据要传。轮询间隔可以设置,比如1ms、2ms、8ms等。

对于蓝牙HCI事件来说,我建议轮询间隔设为1ms。为什么?因为蓝牙的ACL数据包最大也就几百字节,1ms的轮询足够应付。而且事件包通常很小,几十个字节就搞定了。

我曾经遇到过一个坑:把中断传输的轮询间隔设成了8ms,结果蓝牙连接断开后,主机要等8ms才能收到断开事件。用户体验极差。后来改成1ms,问题解决。

4.3.2 批量传输

批量传输,顾名思义,就是一次传一大包数据。它没有固定的轮询间隔,主机有空就来取数据。所以它的延迟不确定,但数据完整性有保证——USB协议会做CRC校验和重传。

对于蓝牙ACL数据,批量传输是首选。因为ACL数据包可能很大(比如传文件时),而且允许一定的延迟。你想想看,传一个MP3文件,延迟几毫秒根本感觉不到。

个人经验: 我建议在驱动初始化时,给批量端点分配较大的缓冲区,比如4KB。因为蓝牙ACL数据包最大是1024字节,加上USB协议头,3KB左右就够用了。但留点余量总是好的。

4.4 USB描述符配置:让主机认识你的设备

USB描述符,说白了就是设备的「身份证」。主机通过读取描述符,才知道你是什么设备、需要什么驱动、用哪些端点。

对于蓝牙HCI设备,我们需要配置以下几类描述符:

  • 设备描述符:描述设备的基本信息,比如VID、PID、USB版本等
  • 配置描述符:描述设备的供电方式、最大电流等
  • 接口描述符:描述设备的功能接口,蓝牙HCI通常用两个接口
  • 端点描述符:描述每个端点的属性,比如端点号、传输类型、最大包大小等

这里我贴一个典型的蓝牙HCI USB描述符配置示例:

// 设备描述符
0x12,           // bLength: 18字节
0x01,           // bDescriptorType: 设备描述符
0x00, 0x02,     // bcdUSB: USB 2.0
0xE0,           // bDeviceClass: 无线控制器
0x01,           // bDeviceSubClass: 蓝牙
0x01,           // bDeviceProtocol: HCI
0x40,           // bMaxPacketSize0: 64字节
0x0A, 0x12,     // idVendor: 0x120A
0x01, 0x00,     // idProduct: 0x0001
0x00, 0x01,     // bcdDevice: 1.0
0x01,           // iManufacturer: 厂商字符串索引
0x02,           // iProduct: 产品字符串索引
0x00,           // iSerialNumber: 无序列号
0x01            // bNumConfigurations: 1个配置

// 配置描述符
0x09,           // bLength: 9字节
0x02,           // bDescriptorType: 配置描述符
0x29, 0x00,     // wTotalLength: 41字节
0x02,           // bNumInterfaces: 2个接口
0x01,           // bConfigurationValue: 配置值1
0x00,           // iConfiguration: 无字符串
0x80,           // bmAttributes: 总线供电
0x32            // bMaxPower: 100mA

嗯,这里要注意一点:蓝牙HCI设备通常需要两个接口描述符。第一个接口用于HCI命令和事件,第二个接口用于ACL数据。但有些实现会把所有功能放在一个接口里,用不同的端点来区分。

重要提醒: 我曾经见过一个设备,它的端点描述符里最大包大小写错了。HCI事件端点写成了64字节,但实际只能传8字节。结果主机发命令后,设备回的事件包超过8字节就被截断了。调试了整整一天才发现是描述符的问题。

最后说说字符串描述符。虽然它不是必须的,但我建议加上厂商和产品字符串。这样在Windows的设备管理器里,你能看到「CSR Bluetooth Dongle」而不是「未知设备」。对调试和用户友好度都有帮助。

好了,USB传输层的内容就讲到这里。下一章咱们会深入HCI命令和事件的封装细节,到时候你会看到这些USB端点是怎么实际跑起来的。