第1章:USB协议基础

大家好,我是你们的讲师。做嵌入式这么多年,我有个习惯——不管做什么系统,先把物理层搞明白。USB这东西,看着简单,真要出起问题来,能把人折腾得怀疑人生。这一章,咱们先把地基打牢。

1.1 USB发展史:从1.0到4.0

USB(Universal Serial Bus)从诞生那天起,目的就很明确——统一外设接口。你想想看,早年间键盘用PS/2,鼠标用串口,打印机用并口,那叫一个乱。所以我一直觉得,USB最大的贡献不是速度,是「统一」这两个字。

版本 发布时间 最高速率 核心变化
USB 1.0 / 1.1 1996 / 1998 1.5 Mbps(低速)/ 12 Mbps(全速) 定义了基础协议框架,Host-Device模型
USB 2.0 2000 480 Mbps(高速) 引入微帧、事务拆分,兼容1.x
USB 3.0 / 3.1 / 3.2 2008 / 2013 / 2017 5 Gbps(3.0)/ 10 Gbps(3.1 Gen2)/ 20 Gbps(3.2×2) 新增SDP差分对,全双工,独立物理层
USB4 2019 40 Gbps 基于Thunderbolt 3协议,强制Type-C接口

个人经验:我做项目时,遇到过很多开发人员只关心USB2.0的480Mbps理论值,结果实际吞吐率连40MB/s都跑不到。为什么?协议开销比你想象的大得多。别被数字骗了。

USB 1.0/1.1:定义了两根数据线D+和D-,采用差分传输。低速(1.5 Mbps)和全速(12 Mbps)靠D+/D-的不同上拉电阻来区分。我记得第一次调试USB 1.1设备时,拿示波器看D+线上的波形,那会儿觉得这协议真简单——现在看来,真是天真。

USB 2.0:真正的转折点。480 Mbps的高速速率,引入了微帧(microframe)概念,把原来的1ms帧拆成8个125μs的微帧。为什么会这样?为了满足等时传输(比如音频)的实时性要求。

USB 3.x:从半双工变成了全双工。说白了,就是加了独立的发送和接收通道,能同时读写。物理层也变了,不在用传统的D+/D-,而是用了SDP(SuperSpeed Differential Pair)。嗯,这里要注意:USB 3.0向下兼容2.0,但物理连接完全独立。

USB4:基于Thunderbolt 3架构,强制Type-C接口。我最欣赏的一点是,它引入了隧道协议——把PCIe、DisplayPort和USB数据包直接塞进一个通道里。这给故障注入带来了新挑战,咱们后面章节会细聊。

避坑指南:我曾经有个客户,拿着USB 3.0的线去跑USB 2.0的高速设备,结果死活协商不上。检查半天,发现焊盘上D+/D-的差分阻抗没控制好。别以为线材不重要——差分阻抗控制不好,信号反射能淹了你的眼图。

1.2 物理层与电气特性

做硬件安全,物理层是第一道防线,也是最后一道。很多故障注入的手法,都是先搞乱物理层的信号。所以这一节,必须扎扎实实吃透。

1.2.1 信号定义

USB 2.0及以下,使用一对差分信号(D+ / D-)。电压标准是3.3V(驱动端实际在3.0~3.6V之间)。D+和D-不是随便接的——线缆的差分阻抗要控制在90Ω±15%。

  • 空闲状态(Idle):D+和D-同时被下拉到GND(全速/低速的设备通过上拉电阻区分)。
  • J状态 / K状态:差分数据流的两种逻辑电平。
  • SE0(Single-Ended Zero):D+和D-都拉低,用于复位和帧起始(SOF)信号。

我刚开始学USB时,一直搞不懂怎么判断设备是低速还是高速。其实很简单:看D+还是D-被上拉。全速/高速设备:D+上拉一个1.5kΩ电阻到3.3V。低速设备:D-上拉同样的电阻。这个上拉电阻在上电后会被Host检测到,用来识别设备类型。

设备类型 上拉电阻位置 位速率 驱动器类型
低速(LS) D-上拉 1.5 Mbps 电流源
全速(FS) D+上拉 12 Mbps 电流源
高速(HS) D+上拉(先FS握手再切HS) 480 Mbps 电流源 + 终端匹配

警告:千万不要用万用表去量D+/D-的电压来判断信号状态。差分信号是高速跳变的,你的万用表看到的是平均电压,毫无意义。必须用示波器——而且采样率至少1GSa/s,带宽不低于200MHz。

1.2.2 终端匹配与眼图

USB 2.0高速模式下,Host和Device内部都有45Ω的终端匹配电阻(连到GND)。为什么是45Ω?因为差分阻抗90Ω,单端对地就是45Ω。匹配好了,反射就小,眼图就干净。

我在项目中遇到过一个人,总是抱怨USB 2.0链路误码率高。我让他量眼图,他说「眼图很好啊」。结果我一看截图——眼宽只有300ps,眼高只有200mV,这能好吗?做硬件安全,眼图是衡量物理层健壮性的金标准。我个人的习惯是:眼高至少留30%的裕量(至少400mV),眼宽至少留20%的裕量(至少500ps)。

// 一段伪代码,用来理解眼图测量逻辑
// 不是真正的USB协议代码,只是个概念演示
uint32_t sample_eye(uint8_t *rx_buffer, int length) {
    int eye_width = 0;
    int eye_height = 0;
    for (int i = 0; i < length; i++) {
        // 在眼图中心位置采样
        if (rx_buffer[i] & 0x80) {
            eye_width++;
        }
        if (rx_buffer[i] & 0x40) {
            eye_height++;
        }
    }
    return (eye_width << 16) | eye_height;
}

1.3 拓扑结构:Host-Device-Hub

USB的拓扑结构,说白了就是一级一级往下挂。一个Host下面可以挂Hub,Hub下面再接设备或Hub。层级最多7层(包括Root Hub在内),设备数量最多127个(包括Hub本身)。

  • Host(主机):唯一的主控者。所有传输由Host发起,设备不能主动发数据。你想想看,为什么USB键盘按一下键就能被电脑识别?因为Host每秒轮询设备无数次。
  • Device(设备):被动响应。有四种传输类型:控制传输、批量传输、中断传输、等时传输。
  • Hub(集线器):中继转发。负责信号整形、电源管理、设备连接/断开检测。

我记得有一次调试USB Hub的兼容性问题,设备在直连Host时正常,一旦经过Hub就断连。查了两天才发现——Hub的上游端口(Upstream Port)在复位时没等够100ms。这就是时序问题。所以说,做USB不仅要懂协议,还要懂硬件时序。

USB拓扑结构(含故障注入点示意) Host 主机 Root Hub F1 Hub 1 F2 Device 1 F3 Device 2 Device 3 典型故障注入点 F1: 物理层/包修改 | F2: Hub转发篡改 | F3: 设备响应干扰

上图标注了故障注入点的位置。你想想看,如果我要模拟一次物理层攻击,我会在F1位置注入信号抖动或共模干扰。如果要做协议层篡改,F2位置最合适——Hub是透明转发,在这里修改数据包很难被Host直接发现。

1.3.1 枚举过程

USB设备上电后,Host通过枚举来识别设备。这个过程是故障注入的黄金窗口——很多攻击都在枚举阶段搞手脚。

  1. 复位(Reset):Host把D+/D-拉到SE0(至少10ms),设备收到复位信号后进入默认状态。
  2. 获取设备描述符(Get Device Descriptor):Host发送默认地址0,请求64字节的设备描述符。这里我提醒一句——很多设备在复位后只能返回8字节,必须分两次获取。
  3. 分配地址(Set Address):Host给设备分配一个唯一地址(1~127)。注意,地址不能重复,否则链路直接瘫痪。
  4. 获取配置描述符(Get Configuration Descriptor):读取设备的完整描述符集(配置、接口、端点)。
  5. 配置设备(Set Configuration):Host选择一个配置,设备进入就绪状态。

实用技巧:我自己调试时,拿USB分析仪抓枚举阶段的报文,会重点关注「Set Address」之后Host有没有收到设备的确认包(ACK)。如果缺了ACK,多半是物理层或协议层出了问题。你试着想象一下:设备明明回复了ACK,但Host没收到,链路就不通了。这就是故障注入的目标之一——把正常ACK淹没掉,模拟一个错误。

1.3.2 电源管理

USB的电源管理是个容易被忽视的细节。VBUS标准是5V,但允许的波动范围是4.75V~5.25V。我遇到过很多次,因为VBUS压降太大导致设备重启。

  • 低功率模式:单个端口最大供电100mA(USB 2.0规范)。
  • 高功率模式:设备枚举后可以申请到500mA(USB 2.0)或900mA(USB 3.0)。
  • 挂起(Suspend):3ms无总线活动,设备自动进入挂起状态,电流小于2.5mA。

危险:千万不要在设备枚举期间突然拉低VBUS。这么做不仅会导致设备掉电,还可能损伤Host的USB控制器。我在实验室里亲眼见过一个学生这么做——直接把Host的USB端口烧了。故障注入也要讲究分寸,不是乱来。