1. USB OTG 协议基础与驱动架构概述
做USB驱动调试这些年,我见过太多工程师一上来就追着错误日志跑。结果呢?绕了一大圈,最后发现是基础概念没搞清。尤其是OTG——它可不是简单的“手机插U盘”那么回事。
这一章,我们先打好地基。把OTG的协议本质、硬件架构、驱动软件栈理清楚。后面分析加载失败案例时,你才能一眼看出问题出在哪一层。
1.1 OTG到底在解决什么问题?
标准的USB是主从模式。Host永远是主,Device永远是从。这在PC时代没问题,但到了嵌入式设备,比如手机、平板、开发板,大家既想当Host(插U盘),也想当Device(连电脑传数据)。
OTG就是用来解决这个矛盾的。它让一个USB口能动态切换角色。注意我的用词——“动态切换”。这是OTG的核心,也是后面驱动调试最头疼的地方。
OTG的两个关键能力:
- 双角色支持:同一个端口既可以做Host,也可以做Device。
- 角色协商:通过硬件引脚和协议握手,决定谁是Host谁是Device。
我记得有一次,客户说他们的板子插上U盘没反应。我查了半天,发现硬件上ID引脚没拉对,板子一直认为自己是个Device,根本没启动Host驱动。这种问题,光看log是看不出来的。
1.2 硬件架构:不只是“多根线”
OTG在标准USB的四根线(VBUS、D+、D-、GND)基础上,增加了两条关键信号线。
| 信号线 | 作用 | 典型状态 |
|---|---|---|
| ID | 身份识别引脚。接地表示A设备(默认Host),悬空表示B设备(默认Device)。 | GND 或 高阻 |
| VBUS | 由A设备提供5V电源。B设备在检测到VBUS有效后开始枚举。 | 0V 或 5V |
这里有个坑,我踩过: 有些芯片的ID引脚内部有上拉电阻,你直接量电平不一定准。最好用示波器抓上电瞬间的波形。我曾经在一个项目里,因为ID引脚电平没检测对,导致驱动加载总失败,折腾了两个晚上。
另外,OTG还定义了SRP(会话请求协议)和HNP(主机协商协议)。SRP用于省电场景——B设备想用总线时,先发个脉冲唤醒A设备。HNP则是在双角色都支持的情况下,通过USB总线交换角色。说实话,HNP在实际产品里用得很少。大部分时候,硬件通过ID引脚就决定了角色,不需要软件再折腾。
1.3 驱动软件架构:三层分离
驱动这一块,我习惯把它分成三层。这样调试时思路清晰:问题出在哪一层,就查哪一层的代码。
Linux USB OTG 驱动架构(简化版):
- 上层: Gadget Function 驱动(比如存储、串口、RNDIS)。
- 中层: UDC 驱动(USB Device Controller)。负责控制器的底层操作。
- 底层: PHY 驱动 + OTG 控制器。负责物理层信号和角色切换逻辑。
为什么这么分层?你想想看,我们做产品时,可能换不同的芯片,但上层功能逻辑是通用的。比如一个“U盘模式”的gadget驱动,在STM32上和在全志芯片上,代码几乎一样。真正需要改的,是中间的UDC驱动和底层的PHY配置。
嗯,这里要注意:很多开发板把OTG的角色切换逻辑放在一个单独的“extcon”驱动里。extcon是External Connector的缩写,它负责检测ID引脚状态,然后通知上层切换驱动模式。我曾经遇到一个问题——extcon驱动检测到的状态是对的,但通知机制没处理好,导致Host驱动和Gadget驱动打架。这种问题最恶心,因为log里看起来参数都对,但就是不干活。
// 一个典型的 extcon 设备树节点
&usb_otg1 {
dr_mode = "otg"; // 支持双角色
extcon = &extcon_usb1; // 关联外部连接器
status = "okay";
};
&extcon_usb1 {
compatible = "linux,extcon-usb-gpio";
id-gpio = &gpio3 15 GPIO_ACTIVE_HIGH; // ID引脚
vbus-gpio = &gpio3 16 GPIO_ACTIVE_HIGH; // VBUS控制
};
这段代码很常见。但注意,dr_mode 可以取三种值:host、peripheral、otg。很多踩坑的案例,就是这里配成了 host,但硬件上又接了双角色线缆,导致驱动加载时逻辑混乱。
1.4 驱动加载的典型流程
了解了架构,我们看看驱动加载时到底发生了什么。这个流程我建议你背下来——后面分析加载失败案例时,每一步都可能有对应的错误现象。
- PHY初始化: 上电后,PHY驱动先配置时钟、电源,并检测VBUS和ID电平。如果这一步失败,后面全部停摆。
- 角色判定: OTG控制器根据ID引脚状态,决定当前是A设备(Host)还是B设备(Device)。
- 模式切换: Linux内核根据判定的角色,加载对应的驱动栈。Host侧加载HCD(Host Controller Driver),Device侧加载UDC和Gadget驱动。
- 枚举/配置: Host侧进行标准USB枚举,Device侧等待Host发起的配置请求。
这四个步骤,每一步都有坑。比如第一步,有些PHY需要固件才能工作,但驱动没及时加载固件,导致PHY初始化超时。第三步,模式切换的时机很关键——如果角色判定和模式加载之间有竞态问题,就可能出现驱动加载失败。
小提示: 调试时多用 usbmon 工具。它能帮你抓到URB层面的细节。我习惯先把 usbmon 打开,再复现问题。很多“无头悬案”是靠这个工具破的。
1.5 一张图看懂整个体系
下面这张图,我把OTG上电后的角色判定、驱动加载、以及用户态看到的效果串在了一起。
警告: 千万别只读文档。文档说支持OTG,实际可能只是硬件上引出了ID和VBUS引脚。软件栈甚至PHY固件都可能不完善。我遇到过一款芯片,文档写着“支持OTG”,但Linux主线内核里UDC驱动根本没提交。这种“半成品”最致命。
好了,这一章的基础打完了。你理解了OTG的硬件信号、驱动分层、以及加载流程,后面的章节里,我会带着这些知识,一例一例地分析那些让你头疼的驱动加载失败案例。
我个人的习惯: 拿到一个新板子,先读 drivers/usb/phy/ 和 drivers/usb/dwc3/ 目录下的代码。这两个地方是OTG调试的核心战场。多数加载失败的问题,都能在这两处找到线索。