Linux USB 子系统架构与 OTG 角色切换
做 USB 驱动开发,第一件事就是摸清子系统的骨架。我个人习惯把 Linux USB 子系统比作一个三层小楼——每层管的事不一样,但层与层之间靠标准接口通信。你如果搞不清哪层该干什么,遇到驱动加载失败、角色切不过去的问题,根本无从下手。
2.1 三层架构,各司其职
这三层从上往下分别是:USB 设备驱动层、USB 核心层、USB 控制器驱动层。它们在内核里的位置和职责非常清晰。
| 层次 | 所处空间 | 核心职责 | 关键数据结构 / API |
|---|---|---|---|
| 设备驱动层 (USBD) | 用户空间 / 内核上层 | 实现具体设备协议(U 盘、键盘、鼠标等) | usb_driver、probe()、usb_register() |
| USB 核心层 (USB Core) | 内核空间 | 协议栈、设备枚举、配置管理、OTG 角色管理 | usb_otg、usb_otg_ops、usb_create_hcd() |
| 控制器驱动层 (HCD) | 内核底层 / 硬件层 | 与 USB 控制器硬件交互、处理 URB 传输 | hc_driver、usb_hcd、hcd_register() |
设备驱动层只管「怎么跟设备说话」,核心层管「设备怎么连上来、怎么切角色」,控制器驱动层管「怎么把数据发到物理总线上」。三层各干各的,但接口是固定的。
我遇到过不少驱动加载失败的 case,最后发现是控制器驱动层的 probe 没跑完,核心层等不到 hcd 注册,直接超时退出了。你想想看,底层没就绪,上层就是纸上谈兵。
2.2 USB 核心层——承上启下
核心层是整栋楼的「腰杆子」。它不仅要处理设备枚举、配置分配、电源管理,还负责 OTG 角色的仲裁。说白了,上层驱动收发包、底层控制器收发数据,中间全靠核心层调度。
举个例子,当你插上一个 USB 设备时,核心层会依次做这些事:
- 检测端口状态变化,触发 hub 事件
- 分配地址,发起设备描述符请求
- 解析配置,选择合适的驱动进行绑定
如果这一层卡住了,上层 probe 根本不会执行。我记得第一次调 USB 驱动时,dmesg 里只看到 reset high-speed USB device number 2 反复出现,最后发现是核心层的枚举超时参数配得太短了。
重点记住:核心层维护着整个 USB 总线状态,OTG 角色切换的入口就在这里。你看到的 /sys/kernel/debug/usb/devices 信息,也是由核心层导出的。
2.3 OTG 角色切换的底层逻辑
OTG 允许一个端口既能当 Host,也能当 Device。切换的关键是 id 引脚的硬件电平。标准 OTG 线缆的一端把 id 引脚接地,另一端悬空——插入哪一端,就决定当前角色。
角色切换的典型流程:
- 检测 id 引脚电平变化(上升沿或下降沿)
- 核心层调用
usb_otg_ops中的回调 - Host 模式:打开 VBUS 电源,启动 HCD
- Device 模式:关闭 VBUS,连接 USB 控制器作为设备
下面这个结构体就是 OTG 角色切换的核心接口:
struct usb_otg_ops {
int (*start_host)(struct usb_otg *otg);
int (*start_device)(struct usb_otg *otg);
int (*set_vbus)(struct usb_otg *otg, bool enabled);
int (*set_id)(struct usb_otg *otg, int id);
int (*set_power)(struct usb_otg *otg, unsigned int mA);
};
每个回调都对应一个硬件操作。start_host 和 start_device 是切换的主入口,set_vbus 控制电源,set_id 一般用于软件强制设定角色。我见过不少国产芯片的 OTG 驱动,set_vbus 里还藏着电源管理的互斥锁——一个不小心就死锁了。
调试小技巧:在 set_id 回调里加一条 printk,打印出 id 引脚的电平值和切换方向。这是判断角色切换是否被正确触发的第一道关卡。
为什么会用到 set_id 软件强制设定?有些嵌入式硬件没有 OTG 检测引脚,只能靠用户态写 sysfs 文件来切换角色。这时候驱动里会把 set_id 实现成一个状态机入口。
2.4 实战中的角色切换陷阱
讲几个我踩过的坑,给你提个醒。
曾经有个项目,OTG 口插上 U 盘后 dmesg 毫无反应。排查了一下午,发现是硬件设计把 id 引脚悬空了,默认为高电平——控制器一直认为自己是 Device。后来在 board.dts 里强行将 id 引脚配置为下拉,才切到 Host 模式。
这类问题很隐蔽。你以为是驱动没加载,实际上是硬件根本没告诉芯片「该切换了」。我建议在调试时先确认 id 引脚电平:用万用表量一下,或者看 GPIO 寄存器值,别一上来就追代码。
另一个常见问题是驱动模块加载顺序。控制器驱动层必须先于 OTG 角色管理注册,否则 start_host 回调里拿不到 hcd 指针。我曾经在一个方案里看到,module_init 和 fs_initcall 的优先级没拉开,导致角色切换永远超时。
| 模式 | id 引脚电平 | VBUS 电源 | 典型使用场景 |
|---|---|---|---|
| Host 模式 | 低电平(接地) | 芯片对外供电 | 连接 U 盘、USB 键盘 |
| Device 模式 | 高电平(悬空) | 由外部主机供电 | 连接 PC、充电 |
这张表看起来简单,但实际项目中因为 VBUS 电源控制不到位导致设备无法枚举的案例非常多。有的芯片需要额外延迟 10ms 才能稳定供电,有的则要在 set_vbus 里先等上一轮电源状态机完成。
我的习惯:在 start_host 和 start_device 的入口和出口各加一条 dev_dbg,打印当前状态和返回值。日志一开,角色切换成没成功、卡在哪个回调里,一目了然。
其实 OTG 角色切换的底层逻辑并不复杂,复杂的是硬件时序和驱动耦合。你只要把 id 检测、VBUS 控制、HCD 启停这三个环节理清楚,排查故障时就有了方向。嗯,这里要注意:别忽略设备树里的引脚配置,很多所谓的「驱动 bug」最后都是 pinmux 没设对。
我个人觉得,理解清楚三层架构和 OTG 切换的关系,是上手 USB 驱动调试的第一道坎。跨过这道坎,后面看 URB 传输、电源管理那些细节,就不会觉得乱了。