3. USB OTG协议:OTG规范、双角色设备与主机协商协议
聊到 USB OTG,我得先跟你说一句大实话:早期的 USB 世界其实挺“专制”的。一个设备要么是主机(Host),要么是外设(Peripheral),出厂就定死了。举个例子,你的 U 盘天生就是外设,插到电脑上才能用;反过来 U 盘没法当主机去读另一个 U 盘的数据。
但是移动设备普及之后,这个矛盾就炸了。你想想看,手机能不能直接插个 U 盘?能不能接个鼠标键盘?能不能反过来给另一台手机充电?这些需求催生了一个关键协议——USB OTG(On-The-Go)。说白了,它让设备拥有了“人格分裂”的能力,既能当主机,也能当外设。
3.1 OTG 的由来与初衷
USB 2.0 时代,标准规范里定义了两种角色:A 设备(Host)和 B 设备(Peripheral)。所有通信都由 A 发起,B 只能乖乖响应。这种主从模式在 PC 时代没问题,但到了手机、平板、相机这类便携设备上就尴尬了——你永远不知道对面插过来的东西是 U 盘还是另一台手机。
我记得刚开始做手机驱动那会儿,产品经理天天追着问:“为什么手机不能直接读 U 盘?” 当时的方案是加一颗 USB HUB 芯片或者用双重角色控制器,但那都是硬件层面的 hack。直到 OTG 1.0 规范出炉,事情才正式步入正轨。
OTG 规范的核心思路就一条:让设备通过一根线缆自动协商谁当主机、谁当外设。它引入了一个新概念——双角色设备(Dual-Role Device,简称 DRD)。一个 DRD 设备插上 OTG 线后,能根据对方的身份自动切换角色。
3.2 OTG 规范:关键角色与信号
OTG 规范定义了两个主要角色:
- A-Device:默认提供 VBus 供电,初始为主体(Host)。通常是插上 OTG 线后供电的那一端。
- B-Device:默认由 A-Device 供电,初始为外设(Peripheral)。需要时可以申请成为主机。
你想想看,这个定义很巧妙。它没有用“主机”和“外设”这种功能性的词,而是用“A”和“B”这种位置性的词。为什么?因为角色会变。A-Device 一开始是主机,但经过协商后可能变成外设;B-Device 同理。
这里有一个关键信号——ID 引脚。在标准 Micro-USB 和 Type-C 线缆里,ID 引脚就是角色的鉴定师:
| ID 引脚状态 | 设备角色 | 说明 |
|---|---|---|
| ID 接地(GND) | A-Device(主机) | 提供 VBus 5V 电源,检测 D+/D- 信号 |
| ID 悬空(Floating) | B-Device(外设) | 由 A-Device 供电,等待枚举 |
| ID 通过电阻下拉 | 被动角色指示 | 部分旧设备用于识别线缆类型 |
但光靠 ID 引脚还不够动态。真正的角色协商,得靠 HNP(Host Negotiation Protocol,主机协商协议)来完成。
3.3 双角色设备(DRD)的硬件结构
DRD 设备的硬件基础是一颗 双重角色控制器(Dual-Role Controller)。它既可以配置为 EHCI(增强主机控制器接口)模式,也可以配置为 UDC(USB 设备控制器)模式。切换时通常需要修改寄存器,甚至重新初始化端点。
我在项目里遇到过一个问题:某款手机平台的双重角色控制器,从 Host 切到 Device 时没有完全复位 DMA 通道,导致后续枚举时数据包 CRC 校验一直出错。排查了一整天,最后发现是 DMA 残留地址没清掉。嗯,这里要注意,切换角色时一定要做干净的软复位。
DRD 设备的典型工作流程如下:
- 检测连接:通过 ID 引脚(Micro-USB)或 CC 引脚(Type-C)判断插入的是主机还是外设。
- 初始角色分配:按照规范,插上线缆后立即决定谁是 A-Device(供电方),谁是 B-Device(受电方)。
- 角色协商:如果双方都是 DRD 设备,可以通过 HNP 交换角色。比如 B-Device 想变成主机去读取 A-Device 上的文件。
- 电源管理:角色切换时,VBus 的供电状态也要跟着切换。A-Device 必须持续供电,B-Device 在变成主机前要先拉高 VBus。
3.4 主机协商协议(HNP)详解
HNP 是 OTG 协议中最核心的动态机制。它允许 B-Device(初始为外设)主动申请变成主机,同时也允许 A-Device(初始为主机)放弃主机角色变回外设。这个协议在 USB 2.0 的帧头微帧(Micro-frame)里完成,不依赖单独的数据通路。
HNP 的完整流程,我习惯分成 6 步来记忆:
- 初始状态:A-Device 当主机,B-Device 当外设。A 拉高 VBus,B 在 D+/D- 上挂电阻表示连接。
- B-Device 发起请求:B 先释放 D+ 上的上拉电阻,让总线进入空闲状态(SE0)。然后 B 驱动 D- 为低,发送一个“角色切换请求”信号。
- A-Device 响应:A 检测到 B 发出的请求后,停止主机调度(把 SOF 包停下来),然后主动释放 D+ 上的下拉电阻,让总线进入空闲。
- 角色翻转:B 开始拉高 VBus(因为现在它要当主机了),然后初始化自己的主机控制器,在总线上发送 SOF 和复位序列。
- 枚举:A (现在变成外设) 检测到总线复位后,通过 D+ 的上拉电阻响应,完成枚举。此时角色互换成功。
- 回切:如果新主机(原 B)不想继续了,它可以再释放主机权,按同样的流程让角色换回来。
你可能会问:为什么需要这么麻烦的握手流程? 直接一个信号通知不就完了吗?
其实原因很简单——USB 总线是共享的。如果不经过协商就硬切换,总线上的数据包会乱飞,甚至烧坏 VBus 供电。HNP 保证在整个切换过程中,总线上最多只有一个人发号施令,其他人闭麦。这是所有串行总线通信的基本哲学。
usb_hcd_flush_endpoint() 就是干这个的。
HNP 的时序窗口非常小。USB 2.0 的速度是 480 Mbps,一个微帧只有 125 微秒。HNP 需要在连续几个微帧内完成 ID 检测和信号握手。如果驱动写得不够快,或者中断响应延迟过高,HNP 就会失败,总线直接进入挂起状态。所以我在做 HNP 驱动时,习惯把中断优先级调到最高,并且关掉不必要的 CPU 调频,避免中断抖动。
3.5 OTG 的系统视图:从硬件到驱动
最后,我用一张图把 OTG 整体的知识脉络串一下。这是我自己画的一个结构图,涵盖了从规范到实现的各个层面:
这张图里我把 OTG 的知识分成四层:最上面是协议规范,中间是三个核心机制(A/B 设备、DRD、HNP),下面是内核驱动实现,最下面是应用和调试。按这个顺序学,你会清楚每个知识点在整个栈里的位置。
CONFIG_USB_OTG 和 CONFIG_USB_DEBUG,然后通过 cat /sys/kernel/debug/usb/devices 观察角色变化前后描述符的差异。一旦 role 切换成功,设备文件里的 D(Device)和 H(Host)标志会立刻变化。这是最快验证 HNP 是否成功的方法。
好了,这一章的内容就到这儿。OTG 协议其实不复杂,但它是整个 USB 双角色系统的基础。后面我们聊驱动实现的时候,会频繁提到 A-Device、B-Device 和 HNP 这些概念。