2. xHCI 控制器架构:寄存器空间、中断映射与设备上下文基地址

好,咱们这一章开始深入 xHCI 的“躯体”——它的寄存器空间、中断怎么走、设备上下文在哪儿放。这三个东西搞明白了,你抓 Trace 的时候才能一眼看出控制器在干什么。

我最初调 USB 3.0 设备枚举时,翻着 xHCI 的 Spec 头大,后来发现其实核心就三块:寄存器决定了控制器怎么配、中断决定了事件怎么通知、上下文决定了设备长什么样。咱们一个个拆开讲。

2.1 xHCI 寄存器空间概览

xHCI 把寄存器分成了四个区域,在物理地址上连续排列。你写驱动或者读 Trace 时,需要知道每个区域的基地址。

区域 英文简称 偏移范围 主要作用
能力寄存器 CAP 0x00 – 0x3F 读取控制器版本、结构参数、支持的协议
操作寄存器 OP 由 CAP 中的 CAPLENGTH 指定 控制运行、端口管理、命令执行
Doorbell 寄存器 DB 由 DBOFF 指定 通知控制器有新的工作项(TRB)需要处理
运行时寄存器 RTS 由 RTSOFF 指定 中断管理、事件环控制

我见过不少同学拿到 Trace 只盯着 OP 寄存器看,忽略了 DB 和 RTS。其实很多枚举失败的原因,是中断没配置对,或者 Doorbell 没往对的地方敲。

实践提醒:看 Trace 时,先定位 CAPLENGTH、DBOFF、RTSOFF 三个字段,算出各区域真实地址。有一次我抓到一个奇怪的枚举超时,后来发现是驱动把 DBOFF 算错了,Doorbell 写到了操作寄存器区,结果啥反应都没有。

2.2 中断映射:从事件环到 CPU

xHCI 支持两种中断方式:MSI/MSI-X 和 传统引脚中断。在实际主机上,99% 都是 MSI-X,因为它可以分配多个中断向量,让不同的事件环走不同的 CPU。

咱们重点看中断映射是怎么配的。在 RTS 区域里,有一组 中断寄存器组(Interrupter Register Set),每个 Interrupter 对应一个事件环。中断寄存器组里最重要的两个字段:

  • IMAN(中断管理寄存器):有没有待处理的中断,以及是否使能。
  • IMOD(中断调节寄存器):中断节流,控制最大中断频率。
  • ERST(事件环段表基址寄存器):指向当前事件环段表。

注意,xHCI 允许最多 1024 个 Interrupter,但实际常见的就配 8 个。我常用 Interrupter 0 做命令完成事件,Interrupter 1 做传输事件,分开看不会乱。

我的调试习惯:在 Trace 里找写入 IMAN 寄存器的记录,如果看到写 1 到 Pending 位后没有清除,说明硬件有中断没被处理。这时候多半是中断映射没配、MSI-X 表项没写对、或者 CPU 没有响应。

中断还会涉及 中断分组(Interrupt Group)?其实 xHCI 没它。但 PCIe 层面的 MSI-X 有个表,控制每个向量映射到哪个 CPU。你如果在 Trace 里发现中断总是落在同一个核上,可能是驱动没调 MSI-X 的 affinity。

2.3 设备上下文基地址:DCBAA 与 Device Context

这是 xHCI 里最绕但最关键的概念。每个 USB 设备,在控制器里都有一个 设备上下文(Device Context),存放该设备所有端点的状态、TR 指针、槽状态等。

控制器怎么找到这些上下文呢?通过 设备上下文基地址数组(DCBAA)。你在 OP 寄存器里写 DCBAAP(Device Context Base Address Array Pointer),它就指向一个物理地址,这个地址存放了一个数组,数组每一项是一个指针,指向某一个设备的上下文。

举个例子:

DCBAAP  -->  指针数组(物理地址)
   [0]  -->  槽 0 的设备上下文(通常不用)
   [1]  -->  槽 1 的设备上下文
   [2]  -->  槽 2 的设备上下文
   ...   -->  ...
   [255] -->  槽 255 的设备上下文

设备上下文本身是 1024 字节对齐的(xHCI 1.1 及以后要求 16 字节对齐,但通常建议 64 字节对齐)。每个设备上下文内又包含了:

  • 槽上下文:设备速度、路由字符串、入口点等。
  • 端点上下文:每个端点拥有自己的上下文区域,最多 31 个输入端点和 31 个输出端点。

我曾经踩过的坑:有一次调试枚举失败,发现主机发完 SET_ADDRESS 后,设备上下文里的 Slot State 一直没有从 0(Disabled)跳到 1(Enabled)。后来检查 Trace,发现 DCBAAP 指向的物理地址是 32 位,而设备实际是 64 位地址空间,高 32 位丢了。结果控制器读上下文时全是 0,中断也不来了。所以配 DCBAAP 时一定要:1)确认地址写完整;2)确认内存是 Non-Cacheable。

2.4 三者如何协作

把寄存器、中断、上下文串起来,就是一次 USB 枚举的底层路径:

  1. 主机软件通过 操作寄存器 写命令 TRB 到命令环。
  2. 敲 Doorbell 通知控制器。
  3. 控制器读命令,操作 DCBAA 找到目标设备上下文。
  4. 执行命令后,事件 TRB 写入 事件环,通过 中断寄存器 触发 MSI-X。
  5. CPU 响应中断,读事件环,完成一次事务。

你抓 Trace 时,如果看到 Doorbell 敲了但没事件回来,就查中断映射和 DCBAAP 是否配对了。如果看到事件回来了但状态码不对,就查设备上下文里的端点状态。

一句话总结:xHCI 的脑子是 🧠 操作寄存器+Device Context,手脚是 🖐️ Doorbell,嘴巴是 💬 中断。嘴巴张不开,手脚再快也没用。

xHCI 控制器核心架构图 CAP 能力寄存器 CAPLENGTH DBOFF / RTSOFF 结构参数 OP 操作寄存器 DCBAAP CRCR / PORTSC 命令执行 DB Doorbell 按槽号敲 触发控制器工作 RTS 运行时寄存器 Interrupter 0..N IMAN / IMOD / ERST 事件环管理 DCBAA 设备上下文指针数组 Slot 0 Slot 1 Slot 2 ... Device Context 槽上下文 端点上下文 0..31 TR 指针 状态 中断映射 软件通过 OP 配 DCBAAP → Doorbell 敲 DB → 控制器读 DCBAA → 操作 Device Context → 事件写 RTS 事件环 → 中断通知 CPU (虚线表示 RTS 中断映射到 CPU,实线表示数据流路径)

Trace 实战技巧:用专业工具(比如 Ellisys 或 LeCroy USB Advisor)抓 xHCI 寄存器读写时,重点关注三个地方——DCBAAP 写入值(确认地址正确)、IMAN 的 Pending 位变化(检查中断是否被处理)、Doorbell 写操作的目的槽号(是不是你预期的设备槽)。