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 线后,能根据对方的身份自动切换角色。

核心变化:从“出厂定死”到“运行时协商”。OTG 不是一项完全独立的新技术,它是对 USB 2.0 协议的扩展,向下兼容所有标准 USB 设备和线缆。你手里的 Micro-USB OTG 线,其实就是把第 4 脚(ID pin)接地,告诉设备“你该当主机了”。

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 设备的典型工作流程如下:

  1. 检测连接:通过 ID 引脚(Micro-USB)或 CC 引脚(Type-C)判断插入的是主机还是外设。
  2. 初始角色分配:按照规范,插上线缆后立即决定谁是 A-Device(供电方),谁是 B-Device(受电方)。
  3. 角色协商:如果双方都是 DRD 设备,可以通过 HNP 交换角色。比如 B-Device 想变成主机去读取 A-Device 上的文件。
  4. 电源管理:角色切换时,VBus 的供电状态也要跟着切换。A-Device 必须持续供电,B-Device 在变成主机前要先拉高 VBus。
实践中,大部分 Android 设备在 USB 3.0 时代用的是 dwc3(DesignWare USB 3.0 Dual-Role Controller) 驱动。dwc3 内部有一个全局寄存器(GCTL),其中的 PRTCAPDIR 字段决定了当前角色是 Host、Device 还是 DRD。我个人习惯把 DRD 写在设备树里,让内核在 probe 阶段就初始化好双重角色能力。

3.4 主机协商协议(HNP)详解

HNP 是 OTG 协议中最核心的动态机制。它允许 B-Device(初始为外设)主动申请变成主机,同时也允许 A-Device(初始为主机)放弃主机角色变回外设。这个协议在 USB 2.0 的帧头微帧(Micro-frame)里完成,不依赖单独的数据通路。

HNP 的完整流程,我习惯分成 6 步来记忆:

  1. 初始状态:A-Device 当主机,B-Device 当外设。A 拉高 VBus,B 在 D+/D- 上挂电阻表示连接。
  2. B-Device 发起请求:B 先释放 D+ 上的上拉电阻,让总线进入空闲状态(SE0)。然后 B 驱动 D- 为低,发送一个“角色切换请求”信号。
  3. A-Device 响应:A 检测到 B 发出的请求后,停止主机调度(把 SOF 包停下来),然后主动释放 D+ 上的下拉电阻,让总线进入空闲。
  4. 角色翻转:B 开始拉高 VBus(因为现在它要当主机了),然后初始化自己的主机控制器,在总线上发送 SOF 和复位序列。
  5. 枚举:A (现在变成外设) 检测到总线复位后,通过 D+ 的上拉电阻响应,完成枚举。此时角色互换成功。
  6. 回切:如果新主机(原 B)不想继续了,它可以再释放主机权,按同样的流程让角色换回来。

你可能会问:为什么需要这么麻烦的握手流程? 直接一个信号通知不就完了吗?

其实原因很简单——USB 总线是共享的。如果不经过协商就硬切换,总线上的数据包会乱飞,甚至烧坏 VBus 供电。HNP 保证在整个切换过程中,总线上最多只有一个人发号施令,其他人闭麦。这是所有串行总线通信的基本哲学。

这里有一个坑。我曾经在某款平板电脑上遇到过:当 B-Device 申请变成主机时,A-Device 的 USB 控制器已经发出了一个未完成的数据传输(URB),而 HNP 切换完全无视了这个 pending 的 URB。结果就是,角色翻转后,A 的外设端点里还挂着 URB,导致后续枚举一直超时。解决方法是:在触发 HNP 之前,软件层要做一次 flush pending URBs 的操作。内核里的 usb_hcd_flush_endpoint() 就是干这个的。

HNP 的时序窗口非常小。USB 2.0 的速度是 480 Mbps,一个微帧只有 125 微秒。HNP 需要在连续几个微帧内完成 ID 检测和信号握手。如果驱动写得不够快,或者中断响应延迟过高,HNP 就会失败,总线直接进入挂起状态。所以我在做 HNP 驱动时,习惯把中断优先级调到最高,并且关掉不必要的 CPU 调频,避免中断抖动。

3.5 OTG 的系统视图:从硬件到驱动

最后,我用一张图把 OTG 整体的知识脉络串一下。这是我自己画的一个结构图,涵盖了从规范到实现的各个层面:

USB OTG 驱动栈知识结构 OTG 协议层 OTG 1.0 / OTG 2.0 / USB Type-C 规范 A-Device vs B-Device ID 引脚 / CC 引脚 双角色设备(DRD) dwc3 / 双重角色控制器 主机协商(HNP) 信号握手 / 角色翻转 Linux / Android 内核驱动层 usb gadget dwc3 / musb / chipidea extcon / ID 检测 角色切换流程:udc_start / usb_add_hcd 应用层 / 调试工具 adb / usbmon / sysfs 角色控制 / 获取描述符 本章核心:规范 → 机制 → 实现

这张图里我把 OTG 的知识分成四层:最上面是协议规范,中间是三个核心机制(A/B 设备、DRD、HNP),下面是内核驱动实现,最下面是应用和调试。按这个顺序学,你会清楚每个知识点在整个栈里的位置。

如果你在调试 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 这些概念。