3、PCI配置空间访问:通过PCI总线读取UHCI基地址寄存器(BAR)、启用总线主控(Bus Master)位

UHCI控制器作为PCI设备,其DMA描述符管理的第一步是正确获取控制器在内存中的映射地址,并确保其具备总线主控能力。本节将深入讲解如何通过PCI配置空间访问,读取基地址寄存器(BAR)以获取UHCI的I/O或内存基址,并设置命令寄存器中的总线主控位(Bus Master Enable, BME)。

3.1 PCI配置空间结构概述

每个PCI设备(包括UHCI控制器)都拥有256字节的配置空间,前64字节为预定义头部,包含设备识别、状态、命令、基地址寄存器等关键字段。UHCI控制器通常使用I/O空间映射,其BAR0存放I/O基地址。

偏移地址 寄存器名称 位宽 UHCI关键字段说明
0x00 Vendor ID / Device ID 32-bit 识别UHCI控制器厂商与型号
0x04 Command / Status 32-bit Command[2] = Bus Master Enable
0x08 Revision ID / Class Code 32-bit Class Code = 0x0C03(USB控制器)
0x10 BAR0 32-bit UHCI I/O基地址(bit0=1表示I/O空间)
0x3C Interrupt Line / Pin 32-bit 中断路由信息
关键识别点:UHCI控制器的Class Code为0x0C03(Serial Bus Controller -> USB Controller -> UHCI),在枚举PCI设备时可通过此码精确匹配。

3.2 访问PCI配置空间的两种方法

在x86平台上,访问PCI配置空间主要有两种方式:传统的I/O端口机制(CF8/CFC)和增强的MMCONFIG(Memory-Mapped Configuration)机制。在UHCI初始化阶段,通常使用CF8/CFC方法,因为它不依赖MMU映射,适合早期初始化环境。

3.2.1 CF8/CFC访问流程

  1. 构造配置地址:向I/O端口0xCF8写入32位地址,格式为:0x80000000 | (bus << 16) | (device << 11) | (function << 8) | offset
  2. 读取/写入数据:对I/O端口0xCFC进行32位读写操作。
  3. 字节/字访问:可通过0xCFC + (offset & 0x03) 进行字节或字访问。
优化要点:在循环读取多个配置寄存器时,只需更新0xCF8中的offset部分,避免重复写入高32位固定值,可减少I/O操作延迟。

3.2.2 读取BAR0的汇编实现

; 输入: bx = 总线号, si = 设备号, di = 功能号
; 输出: eax = BAR0 值
read_uhci_bar0:
    ; 构造配置地址 (offset = 0x10)
    mov     eax, 0x80000000
    movzx   ecx, bx          ; 总线号
    shl     ecx, 16
    or      eax, ecx
    movzx   ecx, si          ; 设备号
    shl     ecx, 11
    or      eax, ecx
    movzx   ecx, di          ; 功能号
    shl     ecx, 8
    or      eax, ecx
    or      eax, 0x10        ; BAR0 偏移

    ; 写入配置地址端口
    mov     dx, 0xCF8
    out     dx, eax

    ; 读取配置数据端口
    mov     dx, 0xCFC
    in      eax, dx

    ; 检查是否为I/O空间 (bit0 = 1)
    test    eax, 0x01
    jz      not_io_space

    ; 屏蔽低2位,得到I/O基地址
    and     eax, 0xFFFFFFFC
    ret

not_io_space:
    xor     eax, eax         ; 错误返回0
    ret

3.3 启用总线主控位(Bus Master Enable)

UHCI控制器必须通过DMA方式访问内存中的描述符链表和缓冲区。PCI命令寄存器的bit 2(Bus Master Enable)控制设备是否能够发起DMA事务。在读取BAR之后,必须立即设置此位。

常见错误:只读取BAR而不设置BME位,UHCI虽然能接收CPU的I/O命令,但无法通过DMA读取/写入Frame List或TD,导致传输超时或挂起。

3.3.1 设置BME位的操作步骤

  1. 读取命令寄存器(offset 0x04)当前值。
  2. 设置bit 2(BME),同时保留其他位(如I/O Space Enable bit 0)。
  3. 写回命令寄存器。
  4. 回读验证(可选,用于调试)。

3.3.2 启用BME的C语言封装

// PCI配置空间访问函数
uint32_t pci_config_read(uint8_t bus, uint8_t dev, uint8_t func, uint8_t offset) {
    uint32_t address = 0x80000000 | (bus << 16) | (dev << 11) | (func << 8) | (offset & 0xFC);
    outl(0xCF8, address);
    return inl(0xCFC);
}

void pci_config_write(uint8_t bus, uint8_t dev, uint8_t func, uint8_t offset, uint32_t value) {
    uint32_t address = 0x80000000 | (bus << 16) | (dev << 11) | (func << 8) | (offset & 0xFC);
    outl(0xCF8, address);
    outl(0xCFC, value);
}

// 启用UHCI总线主控
void uhci_enable_bus_master(uint8_t bus, uint8_t dev, uint8_t func) {
    uint16_t cmd_reg;
    // 读取命令寄存器(16位访问)
    cmd_reg = (uint16_t)pci_config_read(bus, dev, func, 0x04);
    // 设置Bus Master Enable (bit 2) 和 I/O Space Enable (bit 0)
    cmd_reg |= 0x05;  // 0x05 = bit0 | bit2
    // 写回
    pci_config_write(bus, dev, func, 0x04, (uint32_t)cmd_reg);
}

3.4 PCI配置空间访问流程图

下图展示了从PCI枚举到BAR读取、BME设置的完整流程,以及各步骤之间的依赖关系。

UHCI PCI配置空间访问流程 UHCI PCI配置空间访问与BME设置流程 1. 枚举PCI设备 2. 匹配Class Code = 0x0C03 3. 读取BAR0 (offset 0x10) 4. 解析I/O基地址 (屏蔽低2位) 5. 设置命令寄存器 BME位 (bit 2) 6. 回读验证 / 初始化完成 关键检查点 • BAR0 bit0=1 (I/O空间) • 基地址4字节对齐 • BME设置后不可清除

3.5 寄存器编程优化策略

在UHCI初始化阶段,PCI配置空间的访问效率直接影响系统启动时间。以下是针对BAR读取和BME设置的优化策略:

优化策略 实现方法 性能收益
批量配置读取 一次CF8写入后,连续读取CFC+0/4/8/12获取多个寄存器 减少I/O端口写操作约75%
寄存器缓存 将BAR0值缓存到局部变量,避免重复PCI访问 消除后续所有BAR读取延迟
原子化BME设置 读-改-写操作合并为一次PCI写(保留其他位) 避免两次PCI事务
延迟验证 BME设置后不立即回读,在首次DMA操作前统一验证 减少一次PCI读延迟
实战建议:在嵌入式系统中,PCI配置空间访问通常通过I/O端口进行,每次out/in指令约消耗几十个CPU周期。将BAR读取和BME设置合并到同一个PCI枚举循环中,可显著减少初始化时间。

3.6 常见问题与调试方法

  • BAR0读取为0xFFFFFFFF:表示PCI设备未正确枚举,检查总线/设备/功能号是否正确。
  • BAR0 bit0=0:UHCI控制器可能被配置为Memory空间映射,需检查BIOS设置或改用Memory BAR。
  • BME设置后DMA仍然失败:检查是否同时设置了I/O Space Enable (bit0),UHCI需要I/O空间和总线主控同时启用。
  • 配置空间访问超时:在QEMU或虚拟化环境中,CF8/CFC访问可能被拦截,需使用MMCONFIG或Hypercall。
调试技巧:使用逻辑分析仪或PCIe分析仪捕获配置空间访问事务,验证CF8地址格式是否正确(bit31必须为1),以及数据是否在CFC端口正确返回。