UDC(USB Device Controller)驱动:UDC 的作用与常见实现

UDC,全称 USB Device Controller。说白了,它就是 USB 设备端最底层的硬件管家。每次你在开发板上插入 USB 线,主机发来一个控制传输,数据是怎么从物理引脚一路跑到你的 Gadget 函数里的?嗯,中间全靠 UDC 驱动在扛。

我个人习惯把 UDC 比作“硬件翻译官”——它负责把 USB 总线上的包拆开,然后按 USB 协议的标准格式,丢给上层处理。反过来,上层要回复的数据也由它打包发出去。如果没有 UDC 驱动,你的 OTG Gadget 连枚举的第一步都迈不出去。

UDC 的核心职责

  • 管理端点(Endpoint)的配置与数据传输
  • 处理总线事件(复位、挂起、唤醒等)
  • 维护 DMA 缓冲区与 FIFO
  • 向上层暴露标准的 Gadget API(usb_ep_ops、usb_gadget_ops)

UDC 在 USB Gadget 框架中的位置

先看整体层次:最下面是硬件控制器,上面是 UDC 驱动,再上面是 UDC Core(也称 Gadget Framework),最上面是各个功能驱动(如 f_mass_storage、f_serial)。UDC 驱动必须把底层寄存器操作封装成统一接口,否则 UDC Core 没法通用。

我早年调一个基于 DWC3 的板子时,系统死活枚举不到设备。后来发现是 UDC 驱动里端点映射写反了,导致控制传输的 EP0 根本没注册上来。这就是 UDC 驱动的坑——硬件手册没吃透,枚举就卡在第一关。

UDC 驱动在 USB Gadget 框架中的位置 Gadget Function(f_mass_storage, f_serial, f_ecm ...) UDC Core(Gadget API 框架) UDC 驱动(DWC3 / DWC2 / MUSB ...) USB 硬件控制器(DWC3 IP / DWC2 IP) ⚠️ 枚举失败排查关键路径 主机请求 → 硬件接收 → UDC 驱动解析 → UDC Core 分派 → Gadget 处理 任何一层断掉,枚举都会卡死

常见 UDC 控制器:DWC3 与 DWC2

目前市面上主流的 UDC 实现主要有两家:Synopsys 的 DWC3(DesignWare Cores USB 3.0)和 DWC2(USB 2.0)。另外还有 Chipidea、MUSB 等,但 DWC 系列占了八成以上的板子。我接触过的平台里,高通、瑞芯微、全志几乎全是 DWC3,只有一些低端 MCU 还在用 DWC2。

特性 DWC3 DWC2
USB 世代 USB 3.0 SuperSpeed + 2.0 USB 2.0 HighSpeed / FullSpeed
端点数量 最多 32 个(可配置) 最多 16 个
DMA 能力 内置 Scatter-Gather DMA 部分模式有 DMA,常需 CPU 搬运
驱动复杂度 较高,需要处理 TRB 环 相对简单,寄存器直接操作
常见平台 高端 SoC(骁龙、Exynos、RK35xx) 低功耗 MCU、老旧芯片(S3C2440、全志 F1C100s)

DWC3 驱动要点

DWC3 的内核驱动叫 dwc3.ko,它其实是两层结构:一个核心模块处理寄存器操作和 DMA 管理,一个平台驱动模块处理设备树匹配。我记得第一次看 DWC3 的源码时,被那个 TRB(Transfer Request Block)环搞晕了。简单说,每次数据传输都要先构造一串 TRB 描述符放到环形缓冲,然后硬件自动去抓取、搬运。

调试小技巧

在 /sys/kernel/debug/dwc3/ 下,你能看到端点状态、当前 TRB 指针、FIFO 占用等。我曾经靠这个目录里的信息,定位到一个 DMA 地址不对齐导致的枚举超时问题。

DWC3 驱动里有个重要的回调函数组 dwc3_gadget_ops:

static const struct usb_gadget_ops dwc3_gadget_ops = {
    .pullup          = dwc3_gadget_pullup,
    .udc_start       = dwc3_gadget_start,
    .udc_stop        = dwc3_gadget_stop,
    .get_frame       = dwc3_gadget_get_frame,
    .wakeup          = dwc3_gadget_wakeup,
    .set_selfpowered = dwc3_gadget_set_selfpowered,
    .vbus_session    = dwc3_gadget_vbus_session,
};

这些函数实现了 UDC Core 所需的接口。其中 pullup 是最关键的——它控制 D+ 上拉电阻。如果上拉时序不对,主机根本看不到设备插入。

DWC2 驱动要点

DWC2 的驱动在驱动树上叫 dwc2,支持 host、device、OTG 三种模式。它比 DWC3 简单得多,但也有不少坑。比如它的 FIFO 分配是静态的,你必须根据端点配置提前规划好。我调试一个串口 Gadget 项目时,因为 RXFIFO 分配太小,导致数据稍大就丢包,枚举成功后设备一直发送 STALL。

注意

DWC2 的驱动在 Linux 中有一个重要的配置选项:CONFIG_USB_DWC2,还需要选择 CONFIG_USB_DWC2_PERIPHERAL 或 CONFIG_USB_DWC2_DUAL_ROLE。千万别只勾了 Host 模式,否则 Gadget 功能根本编译不进去。

内核中的 UDC 驱动注册流程

无论是 DWC3 还是 DWC2,它们最终都要调用 usb_add_gadget_udc() 注册到 UDC Core。这个函数原型:

int usb_add_gadget_udc(struct device *parent, struct usb_gadget *gadget);

注册成功后,UDC Core 会在 /sys/devices/platform/*/udc/ 下创建设备节点。你可以用 ls /sys/class/udc/ 查看 UDC 是否被内核识别。如果这里为空,说明 UDC 驱动根本没加载成功,枚举失败就是必然的。

我遇到过一次奇葩情况:设备树里 UDC 节点没使能,但驱动模块已 insmod,结果 dwc3_probe 返回 -ENODEV,注册失败。嗯,这类问题通常从 dmesg 就能看出来,留意 "dwc3: failed to find dwc3 core" 之类的信息。

调试 UDC 的常用手段

  1. 检查 dmesg:看 UDC probe 是否成功,有没有错误打印。
  2. 查看 /sys/class/udc/:有设备文件说明 UDC 已注册。
  3. 使用 usb_gadget_debugfs:DWC3 下的 debug 文件能显示端点状态。
  4. 打开 Gadget 调试日志:在驱动里加 #define DEBUG 或动态调试。
  5. 逻辑分析仪抓包:硬件级确认总线事务是否正常。

枚举失败时快速自查清单

  • UDC 驱动加载了吗?lsmod | grep dwc3
  • UDC 设备存在吗?ls /sys/class/udc/
  • Gadget 函数绑定上了吗?cat /sys/kernel/config/usb_gadget/g1/UDC
  • 端点配置对吗?cat /sys/kernel/debug/dwc3/*.ep*/state

避坑指南

我曾经因为 UDC 电源管理问题排查了整整两天。 某款 SoC 在系统休眠后,UDC 驱动没有正确处理唤醒,导致 USB 总线复位后控制器不响应。解决方案是在设备树里加上 wakeup-source; 属性。

另一个常见陷阱是 DWC3 的 snps,dis_u3_susphy_quirk 和 snps,dis_u2_susphy_quirk 这两个 device tree 属性。如果没配好,USB PHY 可能始终处于省电模式,无法正常发起传输。你可以查一下 dmesg | grep phy 确认 PHY 状态。