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 驱动加载的典型流程

了解了架构,我们看看驱动加载时到底发生了什么。这个流程我建议你背下来——后面分析加载失败案例时,每一步都可能有对应的错误现象。

  1. PHY初始化: 上电后,PHY驱动先配置时钟、电源,并检测VBUS和ID电平。如果这一步失败,后面全部停摆。
  2. 角色判定: OTG控制器根据ID引脚状态,决定当前是A设备(Host)还是B设备(Device)。
  3. 模式切换: Linux内核根据判定的角色,加载对应的驱动栈。Host侧加载HCD(Host Controller Driver),Device侧加载UDC和Gadget驱动。
  4. 枚举/配置: Host侧进行标准USB枚举,Device侧等待Host发起的配置请求。

这四个步骤,每一步都有坑。比如第一步,有些PHY需要固件才能工作,但驱动没及时加载固件,导致PHY初始化超时。第三步,模式切换的时机很关键——如果角色判定和模式加载之间有竞态问题,就可能出现驱动加载失败。

小提示: 调试时多用 usbmon 工具。它能帮你抓到URB层面的细节。我习惯先把 usbmon 打开,再复现问题。很多“无头悬案”是靠这个工具破的。

1.5 一张图看懂整个体系

下面这张图,我把OTG上电后的角色判定、驱动加载、以及用户态看到的效果串在了一起。

USB OTG 驱动加载流程结构图 USB OTG 驱动加载流程结构图 硬件连接(ID / VBUS) PHY 驱动 + extcon 角色判定 Host 模式:加载 HCD、HUB 驱动 Device 模式:加载 UDC、Gadget 驱动 枚举 / 配置 + 用户态接管

警告: 千万别只读文档。文档说支持OTG,实际可能只是硬件上引出了ID和VBUS引脚。软件栈甚至PHY固件都可能不完善。我遇到过一款芯片,文档写着“支持OTG”,但Linux主线内核里UDC驱动根本没提交。这种“半成品”最致命。

好了,这一章的基础打完了。你理解了OTG的硬件信号、驱动分层、以及加载流程,后面的章节里,我会带着这些知识,一例一例地分析那些让你头疼的驱动加载失败案例。

我个人的习惯: 拿到一个新板子,先读 drivers/usb/phy/ 和 drivers/usb/dwc3/ 目录下的代码。这两个地方是OTG调试的核心战场。多数加载失败的问题,都能在这两处找到线索。