USB 2.0 与 OTG 开发指南:协议抓包、电气测试与角色切换 简介面向嵌入式软硬件工程师、USB 驱动开发者与高校电子类学生的中文技术资料《USB 2.0 与 OTG 规范及开发指南》系统梳理 USB 2.0 协议体系与 OTG 补充规范可帮助读者从术语缩略词、设计目标与背景知识入手逐步理解总线布局、物理接口、电气与机械特性、电源分配与管理、总线协议及健壮性设计等核心内容。资源包仅含 1 个 PDF 文件约 1.03MB轻量便于离线查阅与打印全文为中文表述目录按章节组织涵盖绪论、体系结构概述、OTG 架构与实现机制并给出开发环境、开发工具与调试方法等开发指南适合在协议分析、接口设计与驱动调试时快速定位知识点。目前已有 1382 人学习下载。1. 一根 OTG 线插上去没反应问题往往不在驱动拿着手机插 OTG 读 U 盘屏幕上毫无动静dmesg 也是一片安静——多数人第一反应是内核驱动没编进去可真正卡住的常常是 ID 引脚悬空没接地、VBUS 没给到 5V或者对端设备压根没响应会话请求。《USB 2.0 与 OTG 规范及开发指南(全中文)》这份资料的价值就在这里它把 USB2.0 从术语表、体系结构、数据流模型一路铺到电气特性、协议层再接上 OTG 补充规范让你在翻寄存器手册之前先弄清握手到底是怎么发生的。它更适合做 BSP 移植、USB 设备固件、上位机与量产测试的工程师也适合被枚举失败、NAK 刷屏、眼图不过这些问题反复折腾的人。协议层看懂了再看 OTG 电路和 usb2.0 开关芯片的选型判断会快很多。2. 端点、通道与四种传输类型带宽怎么算、缓冲怎么配2.1 从总线拓扑到端点地址设备侧只有端点主机侧才有通道USB 的物理拓扑是一棵由集线器串起来的树但逻辑拓扑是星形——所有通信都必须经过主机控制器调度设备永远是被动响应方没有自己的时间基准去发起事务。这个前提决定了写设备固件时的基本心态你能做的是准备好端点缓冲等主机来取或来送然后按时回 ACK 或 NAK。设备侧能看到的抽象只有端点。端点零EP0是默认控制端点复位之后就必须可用枚举阶段的全部标准请求都走它。其余端点按方向拆开同一个端点号可以有一对 IN 和 OUT它们在设备内部是两份独立的缓冲。主机侧把「端点的缓冲区 传输类型 数据流方向」这三样打包成一个概念叫通道通道才是驱动里申请带宽、提交 URB 的对象。端点的关键描述字段只有几个bEndpointAddress 的高四位是端点号bit7 表示方向1 为 IN即设备到主机bmAttributes 的低两位决定传输类型wMaxPacketSize 是该端点单次事务能承载的最大字节数。低速设备批量端点最大包只有 8 字节全速 64 字节高速 512 字节同步端点则分别是 1023 和 1024。2.2 四种传输类型的边界四种传输类型不是并列的四个选项而是四组互相冲突的约束条件在不同场景下的取舍要不要保证带宽、能不能容忍重传、有没有固定服务间隔。选错了类型后期要么带宽不够要么实时性崩掉。传输类型bmAttributes 低 2 位典型用途全速最大包高速最大包带宽保证控制00枚举、类请求、配置8/16/32/6464否保留约 10%同步01音频、视频、实时采样10231024高带宽 3072是按微帧预留批量10U 盘、打印机、大块搬运64512否剩余带宽里抢中断11键鼠、HID 报告、状态上报641024是有服务间隔枚举一个现成设备最省事的办法是直接用 pyusb 把描述符树打印出来对照上面的字段核对比翻驱动源码快得多。import usb.core dev usb.core.find(idVendor0x1234, idProduct0x5678) if dev is None: raise SystemExit(device not found) for cfg in dev: # 配置描述符 print(fconfig {cfg.bConfigurationValue}, self-powered{cfg.bmAttributes 0x40}) for intf in cfg: # 接口描述符 print(f interface {intf.bInterfaceNumber}, class{hex(intf.bInterfaceClass)}) for ep in intf: # 端点描述符 ep_type ep.bmAttributes 0x03 direction IN if ep.bEndpointAddress 0x80 else OUT print(f EP {ep.bEndpointAddress:#04x} {direction} ftype{ep_type} maxpacket{ep.wMaxPacketSize})代码里三个字段值得单独拎出来bInterfaceClass配合bInterfaceSubClass决定内核挂哪个类驱动bmAttributes低两位映射到传输类型wMaxPacketSize直接决定你申请 URB 缓冲时的对齐粒度。如果打印出来的最大包是 64 而设备手册写的是 512多半是设备没成功切到高速退回到全速在跑问题就落到第 4 章的电气侧了。2.3 带宽预算全速 1ms 帧、高速 125us 微帧全速总线按 1ms 一帧切分高速总线把一帧再切成 8 个 125us 的微帧。同步传输和中断传输的带宽是在这些时间格子上预留出来的一旦排满新接口直接配置失败——这就是为什么同时插两个 USB 声卡第二个经常会报「资源不足」。同步端点在高带宽模式下一个微帧最多可以发起三个事务也就是 3 × 1024 3072 字节这是 480Mbps 下一个端点的天花板。算一下多声道音频占多少MICROFRAMES_PER_SEC 8000 # 125us 一个微帧 ISO_BUDGET_PER_MICROFRAME 3072 # 高带宽同步端点的微帧上限字节 def iso_profile(rate, channels, bytes_per_sample): per_sec rate * channels * bytes_per_sample # 每秒字节数 per_mf per_sec / MICROFRAMES_PER_SEC # 每个微帧要搬多少 return per_sec, per_mf, per_mf / ISO_BUDGET_PER_MICROFRAME for rate, ch, bps in [(48000, 2, 2), (96000, 8, 3), (192000, 16, 3)]: total, per_mf, ratio iso_profile(rate, ch, bps) print(f{rate}Hz x{ch}ch: {total/1024/1024:.2f} MB/s, f每微帧 {per_mf:.1f} B, 占预算 {ratio:.1%})跑出来的结果很直观48kHz 双声道 16bit 只占不到 1% 的微帧预算96kHz 八声道 24bit 跳到 18% 左右192kHz 十六声道 24bit 已经超过 37%。这意味着一条高速总线最多也就塞两三个这样的大流量同步设备再多就必须上多个主机控制器或者干脆走别的总线。设备固件侧对应的动作是同步端点的缓冲区必须严格按微帧节奏双缓冲或三缓冲一旦某个微帧没排上音频就会出现可听见的断点而协议层不会给你重传的机会。提示批量传输只吃剩余带宽但高速批量端点还有个 NYET 握手用来告诉主机「我上个包还没消化完先别发新的」。如果抓包看到大量 NYET说明端点缓冲大小和主机下发节奏不匹配。3. 协议层拆包PID、CRC 与位填充用抓包反推枚举卡在哪3.1 一个包的完整结构USB 的每一个包都从 SYNC 字段开始低速和全速是 8 位高速是 32 位作用是让接收端的时钟恢复电路锁定相位。接着是 PID 字段8 位里高四位是低四位的按位取反接收方一比就能判断这个包有没有被干扰坏。再往后是具体内容最后以 EOP 结束。PID 只有 16 种取值全部类型记熟了看抓包就像看日志PID 名称4 位值类别用途OUT0x1令牌主机向设备发数据IN0x9令牌主机向设备取数据SOF0x5令牌帧起始带 11 位帧号SETUP0xD令牌控制传送的建立阶段DATA00x3数据偶数序号数据包DATA10xB数据奇数序号数据包DATA20x7数据高速同步传输MDATA0xF数据高速同步传输ACK0x2握手接收无误NAK0xA握手暂时无法收发STALL0xE握手端点出错需要主机介入NYET0x6握手高速批量本包已收但未就绪PRE / ERR0xC特殊低速前导 / 错误SPLIT0x8特殊高速集线器事务分割PING0x4特殊高速流控探测握手包只有 ACK、NAK、STALL、NYET 四个成员却覆盖了排错时 80% 的信息量。NAK 是正常流控说明设备在忙STALL 是异常说明端点进入了错误状态PING 加 NYET 的组合是高速批量在问「准备好了吗」。3.2 CRC5 与 CRC16为什么令牌包只能检 5 位令牌包短只有 7 位地址加 4 位端点号用 CRC5 就够数据包可能长到 1024 字节必须上 CRC16。CRC5 的多项式是 x⁵ x² 1对应位模式 0b00101反射实现后的常数是 0x14。USB 规定数据以 LSB 优先送入最终结果取反后填入包中。def crc5_usb(value, nbits): USB 令牌包 CRC5多项式 x^5x^21LSB first结果取反 crc 0x1F # 初始值全 1 for i in range(nbits): bit (value i) 1 # 从最低位开始送入 feedback (crc ^ bit) 1 crc 1 if feedback: crc ^ 0x14 # 反射后的多项式 return (~crc) 0x1F # 输出取反 # 以 IN 令牌为例ADDR0x0AENDP0x02 addr, endp 0x0A, 0x02 field (endp 7) | addr # 11 位字段端点号在高位 print(hex(crc5_usb(field, 11)))三个参数必须和硬件对齐初始值 0x1F、送入顺序 LSB first、输出取反。自己写的软件解析器如果 CRC 校验全错但数据肉眼看着没问题九成是漏了最后那步取反。CRC16 的写法结构相同只是初始值变成 0xFFFF、多项式为 0x8005反射 0xA001输出同样取反。3.3 位填充与 NRZI连续六个 1 之后必须插 0USB 不对时钟单独走线靠 NRZI 编码把时钟嵌进数据流电平翻转表示 0电平保持不变表示 1。问题来了如果数据里出现一长串 1线上电平就会一直不翻转接收端时钟会漂。解决办法是位填充——数据里连续出现六个 1 之后发送端强行插入一个 0接收端在同样的规则下把这个 0 剥掉。def bit_stuff(bits): 连续 6 个 1 之后插入一个 0 out, ones, stuffed [], 0, 0 for b in bits: out.append(b) ones ones 1 if b else 0 if ones 6: out.append(0) ones 0 stuffed 1 return out, stuffed raw [1] * 7 [0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1] stuffed, n bit_stuff(raw) print(f原始 {len(raw)} 位填充后 {len(stuffed)} 位插入 {n} 个 0)这段代码的用处不是实现收发而是做最坏情况长度估算。一个满负载的 512 字节批量包如果数据全是 0xFF填充后要多出约 1/6 的位数事务在总线上的占用时间会明显拉长。做带宽预算时按最坏情况算才不会在跑压力测试时才暴露超时。3.4 抓包定位枚举失败的具体步骤硬件抓包器太贵的时候先软后硬。Linux 下打开 usbmon 就能拿到每个事务的 PID 和时间戳sudo modprobe usbmon ls /sys/kernel/debug/usb/usbmon/ # 找到目标总线号比如 1u sudo cat /sys/kernel/debug/usb/usbmon/1u trace.txt sleep 5 kill %1 grep -cE S (Ci|Co) trace.txt # 统计控制传输的建立阶段数量看 trace 的顺序是先找 SETUP 令牌确认主机发出去了再看对应的 IN/OUT 事务有没有收到 ACK如果 SETUP 之后一路 NAK 到超时说明设备的 EP0 没使能或者中断没打开如果直接 STALL通常是对标准请求的某个字段不支持比如设备只声明了配置 1主机却去取配置 2 的描述符。地址分配失败则表现为 SET_ADDRESS 之后所有包都带错地址多半是设备把地址写进了寄存器但没同步到收发逻辑里。注意usbmon 抓到的是主机控制器视角看不到位填充和 SYNC 层的错误。如果怀疑是信号质量问题必须换差分探头实测眼图软件侧只能给你「事务重试率」这种间接指标。4. 电气特性与信号完整性驱动器、终端电阻与眼图余量4.1 高速与全速驱动器的差异速率提上去之后驱动器的工作方式变了。全速驱动器的上升下降时间在 4 到 20ns 之间靠 1.5k 上拉电阻让主机识别到设备存在高速驱动器必须在 500ps 内完成跳变并且要在复位期间通过 chirp 握手设备先拉 D- 一段时间主机回应一串 chirp K来切换到高速模式切换成功后 1.5k 上拉被断开改由 45Ω 终端电阻匹配电缆阻抗。参数低速全速高速速率1.5 Mbps12 Mbps480 Mbps上拉位置D- 线上 1.5kD 线上 1.5k复位后切换D 上拉断开终端匹配无无45Ω 到地电缆最大长度3 m5 m5 m最大批量包8 B64 B512 B时间基准1 ms 帧1 ms 帧125 us 微帧差分上升/下降75 ns4–20 ns约 500 ps低速设备把上拉放在 D- 上这是主机区分低速和全速的唯一手段。画 OTG 电路时如果 D/D- 接反设备在主机看来速度类型就是错的表现为插入后反复复位但永远枚举不过去。4.2 抖动、电缆延迟与眼图高速信号的单位间隔UI只有 2.08ns留给发送端抖动、电缆衰减和接收端抖动的预算被切得非常薄。规范对发送端抖动的容忍度是 2ns 以内全速高速更严电缆延迟方面一根 5 米电缆引入的传播延迟可达 26ns往返就是 52ns这也是超时计时器必须留足余量的原因。眼图是判断信号质量最直接的手段。测试时把差分探头跨接在 D 和 D- 之间触发源选 SOF 包观察眼图的张开程度是否覆盖规范给出的模板。眼图闭合通常有三个来源驱动器输出阻抗不匹配导致反射、电缆屏蔽层处理不当引入共模噪声、走线长度差异导致差分对内偏斜超标。4.3 实测的几个工程细节第一探头地线要短。标准示波器探头那根几厘米长的地线在 480Mbps 下会引入明显的感性 ringing看起来像信号过冲其实是测量系统的问题。用弹簧地针或者直接焊同轴线。第二测试点选在连接器焊盘而不是芯片引脚。芯片引脚处信号当然最好看但用户插拔的是连接器真正的信号质量要在那里评估两者之间的走线、ESD 器件、共模电感都会劣化信号。第三ESD 器件要选低电容的。常见做法是选结电容小于 1pF 的型号普通 TVS 的几皮法寄生电容在 480Mbps 下足以把眼图削掉一大块症状是「常温能跑、温度一高就掉线」。第四看误码率而不是单次波形。用一致性测试套件跑几万个包统计 CRC 错误和重试次数比盯着一张波形图可靠得多。提示USB2.0 的高速模式对电源质量同样敏感。VBUS 上的纹波超过 100mV 时就可能出现随机的复位排查时先用示波器看 VBUS 和 3.3V 电源轨再看数据线。5. OTG 的 ID 引脚、SRP/HNP 与角色切换的工程落地OTG 在 USB2.0 之上加了两个能力一是让同一个连接器既能当主机又能当设备二是让总线在没有主机的时候能被设备唤醒。前者靠 ID 引脚后者靠 SRP 和 HNP 两套会话机制。ID 引脚的判定简单粗暴接地就是 A 设备默认当主机并提供 VBUS悬空就是 B 设备默认当外设。Micro-AB 插座之所以存在就是为了让两种线都能插进去。很多「OTG 不工作」的案例最后查出来是 A 设备的 VBUS 供电开关没有使能——ID 认出来了但 5V 没上去B 设备检测不到会话有效自然不会有任何反应。机制触发方关键动作工程要点ID 判定连接器A 设备 ID 接地B 设备 ID 悬空上拉/下拉电阻别接错否则角色判反SRPB 设备VBUS 脉冲或数据线脉冲唤醒 A 设备需要 PHY 支持会话结束检测HNPA/B 设备先SET_FEATURE(b_hnp_enable)协商再交换角色必须由软件显式使能默认关闭Linux 侧验证角色切换最直接的入口是 usb-role-switch 框架暴露的 sysfs 节点# 查看当前 OTG 角色和可用角色列表 cat /sys/class/usb_role/*/role cat /sys/class/usb_role/*/role_control # 手动切到 device 再切回 host验证 HNP 路径是否打通 echo device | sudo tee /sys/class/usb_role/fe800000.usb/role echo host | sudo tee /sys/class/usb_role/fe800000.usb/role # 持续观察 ID 引脚与 VBUS 事件 dmesg -w | grep -iE otg|role|vbus|id手动切换能成功、插线却不行说明问题在 ID 检测或 VBUS 供电而不是角色切换逻辑本身。反过来手动切换直接报错就去确认 PHY 驱动是否注册了 role switch以及设备树里有没有把role-switch-default-mode配成peripheral。做量产验证时把这三条命令串进脚本反复插拔几百次统计角色切换失败率比人工试几十次靠谱得多。最后说一个容易被忽略的点HNP 不是自动发生的。B 设备即使检测到 ID 变化也必须由 A 设备先通过SET_FEATURE下发b_hnp_enable双方进入协商状态后才允许交换角色。很多产品号称支持 OTG实际只实现了 SRP 唤醒HNP 根本没使能表现就是「能唤醒但换不了角色」。拿抓包器在切换瞬间抓一次控制传输看有没有那条SET_FEATURE请求答案立刻清楚。本文还有配套的精品资源点击获取