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访问流程
- 构造配置地址:向I/O端口0xCF8写入32位地址,格式为:
0x80000000 | (bus << 16) | (device << 11) | (function << 8) | offset - 读取/写入数据:对I/O端口0xCFC进行32位读写操作。
- 字节/字访问:可通过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位的操作步骤
- 读取命令寄存器(offset 0x04)当前值。
- 设置bit 2(BME),同时保留其他位(如I/O Space Enable bit 0)。
- 写回命令寄存器。
- 回读验证(可选,用于调试)。
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设置的完整流程,以及各步骤之间的依赖关系。
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端口正确返回。