3. 配置空间与 BAR:PCIe 设备的“身份证”和“地址本”
配置空间,说白了就是 PCIe 设备的“身份证”。你想想看,CPU 要跟一个设备打交道,总得先知道它是什么、需要多大地址空间、怎么触发中断吧?这些信息,全都存在配置空间里。
我个人习惯把配置空间比作“酒店前台”。你入住(设备上电)后,前台会告诉你房间号(BDF)、你有多少行李(BAR 大小)、以及怎么联系你(中断引脚)。而咱们驱动工程师,就是那个要跟所有“房间”打交道的管家。
3.1 配置空间的布局:Type 0 与 Type 1
PCIe 配置空间总共有 4096 字节(4KB),这是 PCI 时代 256 字节的 16 倍。前 256 字节是“兼容区”,跟 PCI 完全一样;后面的 3840 字节是 PCIe 扩展空间,存放着 Capability 结构。
这里有个关键区分:Type 0 和 Type 1。我刚开始学的时候也搞混过,其实记住一句话就行——Endpoint 用 Type 0,Switch/Bridge 用 Type 1。
| 特性 | Type 0 (Endpoint) | Type 1 (Bridge/Switch) |
|---|---|---|
| 设备类型 | 网卡、显卡、NVMe SSD | PCIe 桥、Switch 上行/下行端口 |
| Header 长度 | 64 字节 | 64 字节 |
| BAR 数量 | 6 个 (BAR0 ~ BAR5) | 2 个 (BAR0 ~ BAR1) |
| 特有字段 | 无 | Primary/Secondary/Subordinate Bus Number |
| 主要作用 | 暴露自身资源 | 转发事务、管理下游总线 |
关键偏移量速查(前 64 字节):
- 0x00:Vendor ID + Device ID(厂商和设备号)
- 0x04:Command + Status 寄存器
- 0x08:Revision ID + Class Code(告诉你是啥设备)
- 0x10 ~ 0x24:BAR0 ~ BAR5(Type 0)
- 0x3C:Interrupt Line + Interrupt Pin
3.2 Base Address Register (BAR) 详解
BAR,全称 Base Address Register,是配置空间里最重要的字段之一。它告诉系统:“我需要这么大一块地址空间,请给我分配基址”。
我在项目中遇到过一块 FPGA 加速卡,BAR0 申请了 256MB 的 MMIO 空间,结果 BIOS 分配时发现地址窗口不够,直接把设备挂在了一个不可用的地址上。那次排查花了我整整两天——最后发现是 BIOS 的 Above 4G Decoding 没开。
3.2.1 BAR 的硬件探测机制
你可能会问:驱动怎么知道一个 BAR 需要多大空间?其实方法很简单——先往 BAR 寄存器写全 1,再读回来。
// 伪代码:探测 BAR 大小
uint32_t bar_orig = pci_read_config_dword(dev, offset);
pci_write_config_dword(dev, offset, 0xFFFFFFFF);
uint32_t bar_mask = pci_read_config_dword(dev, offset);
pci_write_config_dword(dev, offset, bar_orig); // 恢复原值
// 解析大小
if (bar_mask == 0) {
// 该 BAR 未实现
} else {
uint32_t size = (~(bar_mask & 0xFFFFFFF0)) + 1;
// size 就是该 BAR 需要的地址空间大小
}
经验之谈:写全 1 时一定要先保存原值,探测完立刻恢复。我见过有同事在调试时忘了恢复,结果系统直接挂死——因为 BAR 被写成了非法值,所有 DMA 操作都找不到目标地址了。
3.2.2 BAR 的类型:MMIO 与 IO Space
BAR 寄存器的 bit 0 决定了地址空间类型:
- bit 0 = 0:MMIO 空间(Memory Mapped IO)
- bit 0 = 1:IO 空间(IO Space,x86 特有)
对于 MMIO,bit 2~1 还指示了宽度:
| bit[2:1] | 地址宽度 | 说明 |
|---|---|---|
| 00 | 32 位 | 最常见,地址空间在 4GB 以下 |
| 10 | 64 位 | 需要两个连续的 BAR 寄存器(如 BAR0+BAR1) |
| 01 / 11 | 保留 | 不使用 |
注意:64 位 MMIO BAR 会占用两个连续的 BAR 槽位。比如 BAR0 是 64 位的,那么 BAR1 就不能独立使用了。我调试过一块 GPU,它的 BAR0 和 BAR1 合起来表示一个 64 位地址空间,结果我在代码里只配置了 BAR0,BAR1 没动——显存映射直接失败。
3.3 MMIO 与 IO Space 映射
配置空间解析完 BAR 之后,下一步就是让 CPU 能访问到设备的寄存器。这个过程叫“映射”。
3.3.1 MMIO 映射
MMIO 就是把设备的寄存器直接映射到 CPU 的物理地址空间。CPU 用普通的 load/store 指令就能读写设备寄存器,不需要特殊的 IO 指令。
// Linux 内核中映射 MMIO 的标准做法
#include <linux/io.h>
// 从配置空间读取 BAR 的物理地址
resource_size_t mmio_start = pci_resource_start(pdev, bar_index);
resource_size_t mmio_len = pci_resource_len(pdev, bar_index);
// 将物理地址映射到内核虚拟地址空间
void __iomem *mmio_base = ioremap(mmio_start, mmio_len);
if (!mmio_base) {
dev_err(&pdev->dev, "ioremap failed\n");
return -ENOMEM;
}
// 读写寄存器
uint32_t val = ioread32(mmio_base + REG_OFFSET);
iowrite32(val | BIT(0), mmio_base + REG_OFFSET);
// 使用完毕后解除映射
iounmap(mmio_base);
我个人的习惯:每次 ioremap 之后,立刻在日志里打印出映射的虚拟地址和物理地址。这样万一后续访问出问题,看一眼 dmesg 就能确认映射是否成功。别问我怎么知道的——有一次我映射了 0 地址,读寄存器直接触发了一页错误。
3.3.2 IO Space 映射
IO Space 是 x86 架构的历史遗留。它使用独立的 IO 地址空间(64KB),需要用 in/out 指令访问。在 Linux 中,对应的接口是:
// IO Space 映射(x86 专用)
#include <asm/io.h>
unsigned long io_start = pci_resource_start(pdev, bar_index);
unsigned long io_len = pci_resource_len(pdev, bar_index);
// 请求 IO 端口区域
if (!request_region(io_start, io_len, "my_device")) {
dev_err(&pdev->dev, "request_region failed\n");
return -EBUSY;
}
// 读写 IO 端口
uint8_t val = inb(io_start + REG_OFFSET);
outb(val | 0x01, io_start + REG_OFFSET);
// 使用完毕后释放
release_region(io_start, io_len);
避坑指南:IO Space 在现代 PCIe 设备中已经很少用了。我建议新设计一律用 MMIO。原因有三:一是 IO Space 只有 64KB,很容易耗尽;二是 IO 指令比内存访问慢;三是 ARM/RISC-V 等非 x86 平台根本不支持 IO Space。如果你在写跨平台驱动,直接用 MMIO 最省心。
3.4 知识结构总览
下面这张图总结了本章的核心逻辑:从配置空间读取 BAR,解析类型和大小,最后映射到 CPU 地址空间。
3.5 实战中的常见陷阱
嗯,这里要注意几个容易踩坑的地方:
- BAR 未使能:配置空间的 Command 寄存器 bit 0(IO Space Enable)和 bit 1(Memory Space Enable)必须置 1,否则 BAR 映射无效。我见过有人配好了 BAR,但忘了写 Command 寄存器,结果读回来全是 0xFF。
- 64 位 BAR 的对齐要求:64 位 BAR 的起始地址必须 64 位对齐。如果 BIOS 分配时没对齐,设备可能无法正常工作。
- IO Space 的 16 位限制:IO Space 地址只有 16 位,所以 BAR 的高 16 位必须为 0。如果设备报告了一个超过 64KB 的 IO BAR,那一定是硬件设计有问题。
- Prefetchable 标记:BAR 的 bit 3 表示该空间是否可预取(Prefetchable)。可预取空间允许 CPU 缓存读取结果,适合存放帧缓冲等只读数据。但如果你对可预取空间做了写操作,一定要小心缓存一致性问题。
调试小技巧:在 Linux 下,用 lspci -vvv -s 01:00.0 可以查看设备的完整配置空间,包括所有 BAR 的地址和大小。我每次拿到新板卡,第一件事就是跑这个命令,确认 BAR 分配是否符合预期。