3. xHCI 操作寄存器:USBCMD、USBSTS、DNCTRL、CRCR、DCBAAP、CONFIG
控制器初始化过程中,软件必须按照严格的时序操作一组关键寄存器。这些寄存器控制着 xHCI 的运行状态、设备连接检测、DMA 描述符基址以及最终的系统配置。本章将逐一解析 USBCMD、USBSTS、DNCTRL、CRCR、DCBAAP 和 CONFIG 这六个核心寄存器,并给出对应的编程实战要点。
3.1 寄存器总览与功能分组
下表列出了本章涉及的寄存器及其在初始化流程中的角色:
| 寄存器 | 偏移地址 | 核心功能 | 初始化阶段 |
|---|---|---|---|
| USBCMD | 0x00 | 运行/停止控制、复位、中断使能 | 启动与复位 |
| USBSTS | 0x04 | 控制器状态指示、事件通知 | 状态轮询 |
| DNCTRL | 0x14 | 设备通知控制(Doorbell) | 运行阶段 |
| CRCR | 0x18 | 命令环指针(DMA 描述符基址) | 描述符初始化 |
| DCBAAP | 0x50 | 设备上下文基址数组指针 | 描述符初始化 |
| CONFIG | 0x38 | 最大设备槽位数配置 | 最终配置 |
3.2 USBCMD —— 控制器运行/停止与复位
USBCMD 是 xHCI 的“总开关”。软件通过该寄存器控制控制器的运行状态、执行复位以及配置中断行为。
关键位域
| 位域 | 名称 | 描述 |
|---|---|---|
| Bit 0 | RS (Run/Stop) | 1 = 运行;0 = 停止。写入 1 启动调度,写入 0 停止调度 |
| Bit 1 | HCRST (Host Controller Reset) | 写入 1 触发复位,复位完成后硬件自动清零 |
| Bit 2 | INTE (Interrupt Enable) | 1 = 使能主中断;0 = 禁用 |
| Bit 3 | HSEE (Host System Error Enable) | 1 = 使能系统错误中断 |
初始化步骤
- 复位控制器: 向 USBCMD 的 HCRST 位写入 1,然后轮询 USBSTS 的 HCHalted 位,等待其变为 1。
- 配置中断: 设置 INTE 和 HSEE 位为 1。
- 启动运行: 向 RS 位写入 1,然后轮询 USBSTS 的 HCHalted 位,等待其变为 0。
// 复位 xHCI 控制器
void xhci_reset(volatile uint32_t *usbcmd, volatile uint32_t *usbsts) {
*usbcmd |= (1 << 1); // 设置 HCRST
while ((*usbsts & (1 << 0)) == 0) { // 等待 HCHalted=1
// 轮询或延时
}
}
// 启动控制器
void xhci_start(volatile uint32_t *usbcmd, volatile uint32_t *usbsts) {
*usbcmd |= (1 << 0); // 设置 RS=1
while ((*usbsts & (1 << 0)) != 0) { // 等待 HCHalted=0
// 轮询或延时
}
}
3.3 USBSTS —— 状态轮询与事件通知
USBSTS 寄存器反映控制器的当前状态,包括运行状态、复位完成、事件中断等。软件通过轮询或中断方式读取该寄存器以获取控制器状态变化。
关键位域
| 位域 | 名称 | 描述 |
|---|---|---|
| Bit 0 | HCHalted | 1 = 控制器已停止;0 = 正在运行 |
| Bit 1 | Reset | 1 = 复位正在进行;0 = 复位完成 |
| Bit 2 | Port Change Detect | 1 = 端口状态发生变化 |
| Bit 3 | Event Interrupt (EINT) | 1 = 事件环中有待处理事件 |
编程要点
- 轮询 HCHalted: 在启动和停止控制器时,必须轮询该位以确认状态转换完成。
- 清除中断位: 读取 USBSTS 后,软件必须写回 1 来清除已处理的中断位(W1C 语义)。
- 超时处理: 如果轮询超时(例如 10ms 内 HCHalted 未变化),应执行控制器复位或上报错误。
// 等待控制器停止(HCHalted=1)
void wait_hchalted(volatile uint32_t *usbsts, uint32_t timeout_us) {
while ((*usbsts & (1 << 0)) == 0) {
if (timeout_us-- == 0) {
// 超时处理
break;
}
udelay(1);
}
}
// 清除事件中断位
void clear_event_interrupt(volatile uint32_t *usbsts) {
*usbsts |= (1 << 3); // 写 1 清除 EINT
}
3.4 DNCTRL —— 设备通知(Doorbell)机制
DNCTRL(Doorbell Control)寄存器用于软件向 xHCI 发送“门铃”信号,通知控制器有新的命令或传输请求需要处理。每个设备槽位和端点都有对应的 Doorbell 寄存器。
寄存器结构
DNCTRL 是一个 32 位寄存器,其位域定义如下:
| 位域 | 名称 | 描述 |
|---|---|---|
| Bits 7:0 | DB Target | 目标设备槽位号(0 = 命令环,1-255 = 设备槽位) |
| Bits 15:8 | DB Stream ID | 流 ID(用于流式传输) |
| Bits 31:16 | Reserved | 保留,必须写 0 |
使用场景
- 命令 Doorbell: 向 DB Target 写入 0,通知控制器处理命令环上的命令。
- 传输 Doorbell: 向 DB Target 写入设备槽位号,通知控制器处理该设备的传输环。
// 发送命令 Doorbell
void ring_command_doorbell(volatile uint32_t *dnctrl) {
*dnctrl = 0; // DB Target = 0
}
// 发送设备传输 Doorbell
void ring_transfer_doorbell(volatile uint32_t *dnctrl_base, uint8_t slot_id) {
volatile uint32_t *doorbell = dnctrl_base + slot_id; // 每个槽位偏移 4 字节
*doorbell = slot_id;
}
3.5 CRCR —— 命令环指针寄存器
CRCR(Command Ring Control Register)保存命令环的基地址和控制信息。命令环是一个由软件维护的 DMA 描述符环,xHCI 通过 DMA 从该环中读取命令。
寄存器布局
| 位域 | 名称 | 描述 |
|---|---|---|
| Bits 5:0 | RCS (Ring Cycle State) | 环循环状态位,用于生产者/消费者同步 |
| Bit 6 | CS (Command Stop) | 1 = 停止命令环处理 |
| Bit 7 | CA (Command Abort) | 1 = 中止当前命令 |
| Bits 31:8 | Command Ring Pointer | 命令环基地址(64 字节对齐) |
初始化步骤
- 分配内存: 分配一块 64 字节对齐的 DMA 内存作为命令环。
- 初始化环: 将环中的所有条目初始化为 Link TRB,指向环的起始地址。
- 设置 CRCR: 将环基地址写入 CRCR 的高 24 位,并设置 RCS 为 1。
- 使能环: 清除 CS 和 CA 位,使命令环开始处理。
// 设置命令环指针
void setup_command_ring(volatile uint32_t *crcr, void *ring_base) {
uint32_t addr = (uint32_t)ring_base;
// 确保 64 字节对齐
if (addr & 0x3F) {
// 错误处理:地址未对齐
return;
}
// 写入 CRCR:基地址 + RCS=1
*crcr = (addr & 0xFFFFFF00) | (1 << 0); // RCS=1
}
3.6 DCBAAP —— 设备上下文基址数组指针
DCBAAP(Device Context Base Address Array Pointer)寄存器指向一个由软件维护的指针数组,该数组的每个元素指向一个设备上下文数据结构。xHCI 通过该数组访问每个设备的上下文信息。
数据结构
DCBAAP 指向的数组结构如下:
| 偏移 | 内容 | 描述 |
|---|---|---|
| 0x00 | Device Context Pointer 0 | 保留(必须为 NULL) |
| 0x08 | Device Context Pointer 1 | 指向设备槽位 1 的上下文 |
| 0x10 | Device Context Pointer 2 | 指向设备槽位 2 的上下文 |
| ... | ... | ... |
初始化步骤
- 分配数组: 分配一块 64 字节对齐的 DMA 内存,大小为 (MaxSlots + 1) * 8 字节。
- 初始化指针: 将所有指针初始化为 NULL。
- 设置 DCBAAP: 将数组基地址写入 DCBAAP 寄存器。
// 设置 DCBAAP
void setup_dcbaap(volatile uint32_t *dcbaap, void *array_base) {
uint64_t addr = (uint64_t)array_base;
// 写入低 32 位
*dcbaap = (uint32_t)(addr & 0xFFFFFFFF);
// 写入高 32 位(如果寄存器支持 64 位访问)
*(dcbaap + 1) = (uint32_t)(addr >> 32);
}
3.7 CONFIG —— 最大设备槽位数配置
CONFIG 寄存器用于设置控制器支持的最大设备槽位数。该值必须在控制器启动前写入,且不能超过 HCSPARAMS1 寄存器中报告的 MaxSlots 值。
位域定义
| 位域 | 名称 | 描述 |
|---|---|---|
| Bits 7:0 | Max Device Slots Enabled | 使能的设备槽位数(1-255) |
| Bits 31:8 | Reserved | 保留,必须写 0 |
配置步骤
- 读取 HCSPARAMS1: 获取硬件支持的最大槽位数。
- 确定使能值: 根据系统需求选择一个不超过硬件最大值的槽位数。
- 写入 CONFIG: 将使能值写入 CONFIG 寄存器。
// 配置最大设备槽位数
void configure_max_slots(volatile uint32_t *config, uint32_t max_slots) {
// 确保不超过硬件限制
uint32_t hw_max = read_hcsparams1() & 0xFF;
if (max_slots > hw_max) {
max_slots = hw_max;
}
*config = max_slots & 0xFF;
}
3.8 寄存器操作流程总图
下图展示了控制器初始化过程中六个寄存器的操作顺序和依赖关系:
3.9 编程实战:完整初始化序列
以下代码展示了上述六个寄存器的完整初始化序列,包含错误处理和超时机制:
// xHCI 控制器初始化序列
int xhci_controller_init(volatile uint32_t *reg_base) {
volatile uint32_t *usbcmd = reg_base + 0x00;
volatile uint32_t *usbsts = reg_base + 0x04;
volatile uint32_t *dnctrl = reg_base + 0x14;
volatile uint32_t *crcr = reg_base + 0x18;
volatile uint32_t *config = reg_base + 0x38;
volatile uint32_t *dcbaap = reg_base + 0x50;
// 1. 复位控制器
*usbcmd |= (1 << 1); // HCRST=1
uint32_t timeout = 10000; // 10ms 超时
while ((*usbsts & (1 << 0)) == 0) { // 等待 HCHalted=1
if (--timeout == 0) {
return -1; // 复位超时
}
udelay(1);
}
// 2. 分配并初始化命令环
void *cmd_ring = dma_alloc_align(4096, 64); // 64 字节对齐
if (!cmd_ring) return -2;
init_command_ring(cmd_ring);
// 3. 分配并初始化 DCBAAP 数组
uint32_t max_slots = read_hcsparams1() & 0xFF;
void *dcbaap_array = dma_alloc_align((max_slots + 1) * 8, 64);
if (!dcbaap_array) return -3;
memset(dcbaap_array, 0, (max_slots + 1) * 8);
// 4. 设置 CRCR
*crcr = ((uint32_t)cmd_ring & 0xFFFFFF00) | (1 << 0); // RCS=1
// 5. 设置 DCBAAP
uint64_t dcbaap_addr = (uint64_t)dcbaap_array;
*dcbaap = (uint32_t)(dcbaap_addr & 0xFFFFFFFF);
*(dcbaap + 1) = (uint32_t)(dcbaap_addr >> 32);
// 6. 配置最大槽位数
*config = max_slots & 0xFF;
// 7. 启动控制器
*usbcmd |= (1 << 0); // RS=1
timeout = 10000;
while ((*usbsts & (1 << 0)) != 0) { // 等待 HCHalted=0
if (--timeout == 0) {
return -4; // 启动超时
}
udelay(1);
}
// 8. 使能中断
*usbcmd |= (1 << 2); // INTE=1
return 0; // 初始化成功
}
- 所有 DMA 内存必须使用
dma_alloc_align分配,确保物理地址连续且对齐。 - CRCR 和 DCBAAP 的写入顺序可以互换,但必须在 RS=1 之前完成。
- CONFIG 寄存器写入后,控制器会立即分配内部资源,因此必须在启动前写入。
- DNCTRL 在初始化阶段不需要操作,它只在运行阶段用于发送 Doorbell。
3.10 常见错误与调试建议
| 错误现象 | 可能原因 | 调试方法 |
|---|---|---|
| 控制器无法启动(HCHalted 不变化) | CRCR 或 DCBAAP 未正确设置 | 读取 CRCR 和 DCBAAP 寄存器,确认地址值正确 |
| 命令环无响应 | 命令环地址未 64 字节对齐 | 检查 CRCR 的低 6 位是否为 0 |
| 中断风暴 | USBSTS 中断位未正确清除 | 确认使用 W1C 方式清除中断位 |
| 设备无法枚举 | DCBAAP 数组指针为 NULL | 检查 DCBAAP 指向的内存是否已初始化 |