2. PCIe配置空间:配置空间结构(Type0/Type1 Header)、Capability链表、BAR空间(Memory/IO)与地址映射
PCIe配置空间是主机软件(RC)发现、枚举和配置下游设备(EP/Switch)的核心入口。每个PCIe功能(Function)在逻辑上拥有独立的4KB配置空间,前64字节为预定义的Header区,剩余部分用于存放Capability结构和设备特定寄存器。理解配置空间的布局、Header类型差异、Capability链表遍历机制以及BAR空间的地址映射规则,是编写正确PCIe驱动的基础。
2.1 配置空间整体结构
PCIe兼容PCI的配置空间规范,每个Function的配置空间大小为4KB(0x000~0xFFF)。前256字节(0x000~0xFF)为PCI兼容配置区,后3840字节(0x100~0xFFF)为PCIe扩展配置区。主机通过配置读写TLP(Type0/Type1)访问这些空间。
2.2 Type0与Type1 Header结构
配置空间的前64字节(0x000~0x03F)称为Header区,根据设备类型分为两种:Type0 Header用于Endpoint设备,Type1 Header用于Switch的Upstream/Downstream Port以及Root Port。两种Header的前16字节(0x000~0x00F)布局相同,从0x010开始出现差异。
| 偏移(字节) | 寄存器名称 | Type0(Endpoint) | Type1(Switch/Root Port) |
|---|---|---|---|
| 0x00 | Vendor ID / Device ID | Vendor ID[15:0] + Device ID[31:16] | Vendor ID[15:0] + Device ID[31:16] |
| 0x04 | Command / Status | Command[15:0] + Status[31:16] | Command[15:0] + Status[31:16] |
| 0x08 | Revision ID / Class Code | Rev ID[7:0] + Class Code[31:8] | Rev ID[7:0] + Class Code[31:8] |
| 0x0C | Cache Line / Latency Timer / Header Type / BIST | 标准布局 | 标准布局 |
| 0x10 ~ 0x24 | Base Address Registers(BAR) | 6个BAR(每个32位或成对64位) | 2个BAR(BAR0/BAR1) |
| 0x18 | Secondary Latency Timer / Subordinate Bus / Secondary Bus / Primary Bus | —(Type0此处为BAR1) | Bus Number寄存器组 |
| 0x28 ~ 0x2C | Cardbus CIS Pointer / Subsystem ID / Subsystem Vendor ID | Subsystem ID + Subsystem Vendor ID | Subsystem ID + Subsystem Vendor ID |
| 0x30 | Expansion ROM Base Address | 支持 | 支持 |
| 0x34 | Capabilities Pointer | 指向第一个Capability结构 | 指向第一个Capability结构 |
| 0x3C ~ 0x3F | Interrupt Line / Interrupt Pin / Min Gnt / Max Lat | 标准布局 | 标准布局 |
2.3 Capability链表遍历
PCIe使用链表结构组织Capability,每个Capability结构包含Capability ID(1字节)、Next Capability Pointer(1字节)以及Capability特定数据。链表从Header的Capabilities Pointer(偏移0x34)开始,Next Pointer为0x00表示链表结束。
驱动遍历Capability链表的典型代码实现如下:
// 遍历PCIe Capability链表
static int pcie_find_capability(struct pci_dev *dev, int cap_id)
{
u8 cap_ptr;
u8 next_ptr;
u8 cap;
// 从Header的Capabilities Pointer开始
pci_read_config_byte(dev, PCI_CAPABILITY_LIST, &cap_ptr);
if (cap_ptr == 0x00)
return -ENOENT;
// 链表遍历
while (cap_ptr != 0x00) {
pci_read_config_byte(dev, cap_ptr + PCI_CAP_LIST_ID, &cap);
if (cap == cap_id)
return cap_ptr; // 找到目标Capability
pci_read_config_byte(dev, cap_ptr + PCI_CAP_LIST_NEXT, &next_ptr);
cap_ptr = next_ptr;
}
return -ENOENT;
}
2.4 BAR空间:Memory与IO类型
Base Address Register(BAR)是设备向系统声明所需地址空间的窗口。每个BAR寄存器(32位)的低位编码了地址空间类型和大小信息。驱动通过读取BAR值、掩码地址位来确定BAR类型和所需空间大小。
| BAR bit[2:1] | 地址空间类型 | 说明 |
|---|---|---|
| 00 | 32-bit Memory空间 | 标准Memory BAR,可预取(Prefetchable)由bit[3]控制 |
| 01 | 16-bit IO空间 | 已废弃,PCIe规范不推荐使用IO BAR |
| 10 | 64-bit Memory空间 | 占用连续两个BAR寄存器(如BAR0+BAR1),支持64位地址 |
| 11 | 保留 | 不使用 |
BAR空间大小探测算法:
- 保存BAR原始值。
- 向BAR写入全1(0xFFFFFFFF)。
- 回读BAR值,低有效位被硬件清零,高位保留的1表示可编程地址位。
- 根据低2位判断类型,计算地址掩码和空间大小。
- 恢复BAR原始值。
// BAR空间大小探测示例(Linux内核实现风格)
static int pci_bar_size(struct pci_dev *dev, int bar)
{
u32 orig, mask, size;
int is_64bit = 0;
pci_read_config_dword(dev, PCI_BASE_ADDRESS_0 + bar*4, &orig);
pci_write_config_dword(dev, PCI_BASE_ADDRESS_0 + bar*4, 0xFFFFFFFF);
pci_read_config_dword(dev, PCI_BASE_ADDRESS_0 + bar*4, &mask);
pci_write_config_dword(dev, PCI_BASE_ADDRESS_0 + bar*4, orig);
if (mask == 0)
return 0; // BAR未实现
// 判断类型
if ((mask & PCI_BASE_ADDRESS_SPACE) == PCI_BASE_ADDRESS_SPACE_IO) {
// IO BAR:低2位为01,掩码位[31:2]
size = (~(mask & 0xFFFFFFFC)) + 1;
} else {
// Memory BAR:低4位为类型/预取属性
if ((mask & PCI_BASE_ADDRESS_MEM_TYPE_MASK) == PCI_BASE_ADDRESS_MEM_TYPE_64)
is_64bit = 1;
size = (~(mask & 0xFFFFFFF0)) + 1;
}
return size;
}
2.5 地址映射:从PCIe总线地址到CPU物理地址
BAR空间经过地址映射后,CPU才能通过load/store指令访问设备寄存器。映射过程涉及三个地址域:PCIe总线地址(Bus Address)、CPU物理地址(Physical Address)和虚拟地址(Virtual Address)。
Linux驱动中BAR映射的标准流程:
// 标准BAR映射流程
static int my_pcie_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
struct my_dev *dev;
int ret;
int i;
// 1. 启用PCIe设备
ret = pci_enable_device(pdev);
if (ret)
return ret;
// 2. 设置DMA掩码(如果需要)
ret = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(64));
if (ret)
ret = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(32));
// 3. 请求BAR资源
ret = pci_request_regions(pdev, "my_pcie_driver");
if (ret)
goto err_disable;
// 4. 映射BAR到CPU虚拟地址空间
for (i = 0; i < 6; i++) {
if (pci_resource_len(pdev, i) == 0)
continue;
dev->bar_phys[i] = pci_resource_start(pdev, i);
dev->bar_size[i] = pci_resource_len(pdev, i);
dev->bar_virt[i] = ioremap(dev->bar_phys[i], dev->bar_size[i]);
if (!dev->bar_virt[i]) {
dev_err(&pdev->dev, "ioremap BAR%d failed\n", i);
goto err_unmap;
}
}
// 5. 注册中断(MSI-X / MSI / INTx)
// ...
return 0;
err_unmap:
for (i--; i >= 0; i--)
if (dev->bar_virt[i])
iounmap(dev->bar_virt[i]);
pci_release_regions(pdev);
err_disable:
pci_disable_device(pdev);
return -ENOMEM;
}
2.6 多队列中断与BAR空间的关系
在FPGA多队列中断场景中,BAR空间不仅用于数据路径的寄存器访问,还承载中断控制逻辑。典型布局如下:
| BAR | 地址范围 | 用途 | 中断关联 |
|---|---|---|---|
| BAR0 | 0x0000 ~ 0x0FFF | 控制/状态寄存器(CSR) | 中断使能/状态寄存器 |
| BAR1 | 0x1000 ~ 0x1FFF | 队列Doorbell寄存器 | 每个队列对应一个Doorbell |
| BAR2 | 0x2000 ~ 0x2FFF | MSI-X Table | MSI-X中断向量表 |
| BAR3 | 0x3000 ~ 0x3FFF | MSI-X PBA(Pending Bit Array) | 中断挂起状态 |
| BAR4 | 0x4000 ~ 0x4FFF | 数据缓冲区(可选) | — |
2.7 本章小结
- 配置空间结构:4KB空间分为PCI兼容区(0x000~0x0FF)和PCIe扩展区(0x100~0xFFF),Header区前64字节决定设备类型。
- Type0 vs Type1:Endpoint使用Type0 Header(6个BAR),Switch/Root Port使用Type1 Header(2个BAR + Bus Number寄存器)。
- Capability链表:从偏移0x34开始遍历,每个Capability包含ID和Next Pointer,0x00表示链表结束。
- BAR空间:支持32-bit Memory、64-bit Memory和IO三种类型,通过写入全1探测大小,驱动需正确映射到CPU虚拟地址空间。
- 地址映射:PCIe总线地址 → CPU物理地址(MMIO窗口) → 虚拟地址(ioremap),驱动通过readl/writel访问。
- 多队列中断:BAR空间布局需合理划分CSR、Doorbell、MSI-X Table和PBA区域,确保中断控制逻辑与数据路径解耦。