3. 硬件接口层适配:DWC3 / EHCI / OHCI 控制器差异、寄存器映射与 DMA 配置、PHY 初始化与时钟树调试

硬件接口层适配,说白了就是让 USB 协议栈跟具体的芯片“对上话”。

我早年刚接触 USB 驱动时,总觉得这部分就是填几个寄存器。后来踩的坑多了才明白——控制器差异、DMA 配置、PHY 初始化,这三件事但凡有一个没对齐,整个 USB 栈就跑不起来。

3.1 三大控制器:DWC3 / EHCI / OHCI 到底差在哪?

先看一张总览图,把三者的定位理清楚。

USB 控制器架构对比 DWC3 (Synopsys) USB 3.0 / 3.1 / 3.2 双角色 (DRD) 32 位 / 64 位 DMA TRB 环形队列 内部 PHY 接口 (PIPE3 / ULPI) 常见于: 手机 SoC 高端嵌入式 SoC EHCI (Enhanced HCI) USB 2.0 高速 (480 Mbps) 主机模式 (HCD) 32 位 DMA 周期 / 异步列表调度 外部 PHY (ULPI) 常见于: PC 主板 旧款嵌入式 SoC OHCI (Open HCI) USB 1.1 全速 / 低速 主机模式 (HCD) 32 位 DMA ED / TD 链表 外部 PHY 常见于: 旧款 PC 兼容性控制器 注意:DWC3 可同时兼容 USB 2.0/3.0,EHCI 和 OHCI 常以“组合控制器”形式出现

3.2 寄存器映射:从 Datasheet 到代码

拿到一份新的 SoC 手册,我习惯先做一件事——找到 USB 控制器的基地址。

DWC3 的寄存器空间通常分为两部分:

  • Global Registers:全局控制,包括版本号、中断使能、DMA 控制
  • Device / Host Registers:根据当前角色切换,映射到同一段地址偏移

EHCI 和 OHCI 就简单多了。它们各自有独立的 CAPLENGTH 和 HCCPARAMS 寄存器,用来描述能力指针。

我的习惯:移植时先打印版本寄存器。如果读出来是 0xFFFFFFFF 或 0x00000000,说明基地址或时钟没通,别急着往下调。

3.3 DMA 配置:环形队列 vs 链表

DMA 是 USB 高性能的关键。但不同控制器的 DMA 模型差别很大。

控制器 DMA 模型 描述符格式 对齐要求
DWC3 TRB 环形队列 16 字节 / 条目 16 字节对齐
EHCI 周期 / 异步列表 qTD (32 字节) 32 字节对齐
OHCI ED / TD 链表 ED (16 字节) + TD (16 字节) 16 字节对齐

我在项目中遇到过一个问题:DWC3 的 TRB 必须放在 非 Cacheable 的内存区域。否则 CPU 写好了描述符,DMA 读到的却是脏数据。

避坑:DMA 缓冲区必须使用一致性内存分配(如 dma_alloc_coherent),或者手动做 Cache 刷新。我曾经因为忘了刷新,导致 USB 3.0 批量传输随机丢包,查了整整两天。

3.4 PHY 初始化:让物理层“活”过来

PHY 是 USB 的物理层,负责把数字信号变成差分线上的模拟信号。

初始化步骤通常包括:

  1. 上电复位:释放 PHY 的复位信号
  2. 时钟选择:配置参考时钟频率(24 MHz / 26 MHz / 19.2 MHz 等)
  3. 接口模式:选择 UTMI / ULPI / PIPE3
  4. 校准:阻抗校准、摆幅调整
  5. 状态轮询:等待 PHY 进入 Ready 状态

DWC3 内部集成了 PIPE3 接口,可以直接连接 USB 3.0 PHY。而 EHCI 通常通过 ULPI 接口连接外部 PHY。

关键点:PHY 的时钟必须与控制器侧匹配。如果 PHY 输出 60 MHz,但控制器配置成 30 MHz,链路协商会直接失败。

3.5 时钟树调试:从源头排查

时钟树是 USB 调试的“第一道关”。我总结了一个排查顺序:

  1. 检查参考时钟:用示波器测 PHY 的 XI 引脚,确认频率和幅度
  2. 检查 PLL 锁定:读 PHY 的状态寄存器,看 PLL Lock 位是否置 1
  3. 检查接口时钟:UTMI 需要 60 MHz,ULPI 需要 60 MHz,PIPE3 需要 125 MHz
  4. 检查总线时钟:AHB / AXI 时钟必须满足控制器的最低频率要求

举个例子。有一次客户反馈 USB 3.0 设备无法识别。我远程看了 log,发现 PHY 的 PLL Lock 位一直为 0。最后定位到是参考时钟的晶振负载电容配错了,导致频率偏了 0.5%。换了个电容就好了。

小技巧:在驱动初始化时,把 PHY 和控制器的主要时钟频率打印出来。对比 Datasheet 的允许范围,一眼就能看出问题。

3.6 实战:DWC3 寄存器映射示例

下面是一段 DWC3 初始化代码的骨架,展示了基地址映射和核心寄存器操作:

/* DWC3 全局寄存器基地址 */
#define DWC3_GLOBALS_BASE    0x4A000000
#define DWC3_GSNPSID         0x00  /* 版本 ID 寄存器 */
#define DWC3_GHWPARAMS0      0x08  /* 硬件参数寄存器 */
#define DWC3_GCTL            0x10  /* 全局控制寄存器 */

static void __iomem *dwc3_base;

/* 1. 映射寄存器空间 */
dwc3_base = ioremap(DWC3_GLOBALS_BASE, 0x10000);

/* 2. 读取版本号,验证映射是否正确 */
u32 id = readl(dwc3_base + DWC3_GSNPSID);
pr_info("DWC3 Version: 0x%08x\n", id);

/* 3. 读取硬件参数 */
u32 hwparams0 = readl(dwc3_base + DWC3_GHWPARAMS0);
u32 num_eps = (hwparams0 >> 10) & 0x1F;
pr_info("Number of endpoints: %d\n", num_eps);

/* 4. 配置全局控制寄存器 */
u32 gctl = readl(dwc3_base + DWC3_GCTL);
gctl |= DWC3_GCTL_SCALEDOWN(0) | DWC3_GCTL_DISSCRAMBLE(0);
writel(gctl, dwc3_base + DWC3_GCTL);

这段代码看起来简单,但我在实际项目中踩过一个坑:ioremap 的 size 不够大。DWC3 的寄存器空间可能跨越 64KB,如果只映射 4KB,访问后面的 endpoint 寄存器就会触发异常。

3.7 小结

硬件接口层适配,核心就三件事:

  • 控制器差异:DWC3 是全能选手,EHCI 是 2.0 主力,OHCI 是 1.1 老将
  • DMA 配置:描述符格式和对齐要求必须严格匹配
  • PHY 与时钟:初始化顺序和频率匹配是成败关键

嗯,这部分内容比较“硬”,但它是整个 USB 协议栈的基石。基础打牢了,后面移植协议栈和调试驱动才会顺手。