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 最大设备槽位数配置 最终配置
编程提示: 所有寄存器操作必须使用 32 位读写(DWORD),且写入顺序不可颠倒。xHCI 对寄存器写入顺序有严格依赖,乱序写入可能导致控制器进入未定义状态。

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 = 使能系统错误中断

初始化步骤

  1. 复位控制器: 向 USBCMD 的 HCRST 位写入 1,然后轮询 USBSTS 的 HCHalted 位,等待其变为 1。
  2. 配置中断: 设置 INTE 和 HSEE 位为 1。
  3. 启动运行: 向 RS 位写入 1,然后轮询 USBSTS 的 HCHalted 位,等待其变为 0。
关键时序: 从写入 RS=1 到 HCHalted=0 的转换时间通常为微秒级,但规范允许最长 1ms。固件应实现超时机制,超时后执行错误恢复。
// 复位 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 未变化),应执行控制器复位或上报错误。
常见错误: 初学者容易忘记 USBSTS 是“写 1 清零”寄存器。如果直接写 0 来清除中断位,中断标志将永远不会被清除,导致中断风暴。
// 等待控制器停止(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 写入应尽量合并。如果多个端点需要通知,可以一次性写入多个 Doorbell 寄存器(每个端点对应一个独立的 Doorbell 寄存器,地址连续)。
// 发送命令 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 字节对齐)

初始化步骤

  1. 分配内存: 分配一块 64 字节对齐的 DMA 内存作为命令环。
  2. 初始化环: 将环中的所有条目初始化为 Link TRB,指向环的起始地址。
  3. 设置 CRCR: 将环基地址写入 CRCR 的高 24 位,并设置 RCS 为 1。
  4. 使能环: 清除 CS 和 CA 位,使命令环开始处理。
对齐要求: 命令环基地址必须 64 字节对齐(低 6 位为 0)。如果未对齐,xHCI 会忽略低 6 位,导致 DMA 访问错误地址。
// 设置命令环指针
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 的上下文
... ... ...

初始化步骤

  1. 分配数组: 分配一块 64 字节对齐的 DMA 内存,大小为 (MaxSlots + 1) * 8 字节。
  2. 初始化指针: 将所有指针初始化为 NULL。
  3. 设置 DCBAAP: 将数组基地址写入 DCBAAP 寄存器。
内存布局: DCBAAP 数组中的每个元素是 64 位指针(8 字节),即使系统是 32 位模式,也必须使用 64 位地址格式。高 32 位在 32 位系统中必须写 0。
// 设置 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

配置步骤

  1. 读取 HCSPARAMS1: 获取硬件支持的最大槽位数。
  2. 确定使能值: 根据系统需求选择一个不超过硬件最大值的槽位数。
  3. 写入 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 寄存器操作流程总图

下图展示了控制器初始化过程中六个寄存器的操作顺序和依赖关系:

xHCI 控制器初始化寄存器操作流程 xHCI 控制器初始化寄存器操作流程 阶段 1:复位 USBCMD.HCRST=1 轮询 USBSTS.HCHalted=1 阶段 2:描述符初始化 分配命令环 DMA 内存 分配 DCBAAP 数组 设置 CRCR = 命令环基址 设置 DCBAAP = 数组基址 阶段 3:配置 读取 HCSPARAMS1 写入 CONFIG 阶段 4:启动控制器 USBCMD.RS=1 轮询 USBSTS.HCHalted=0 阶段 5:运行状态 使用 DNCTRL 发送 Doorbell 关键依赖 CRCR 和 DCBAAP 必须在 USBCMD.RS=1 之前配置完成 CONFIG 必须在启动前写入

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 指向的内存是否已初始化
调试陷阱: 在读取 USBSTS 寄存器时,某些硬件实现会返回 0xFFFFFFFF 表示设备未就绪。固件应检测这种异常值并执行恢复流程,而不是将其当作有效状态。