3. Linux PCI子系统框架
PCI子系统是Linux内核中连接外设与驱动核心的桥梁。理解pci_bus/pci_dev数据结构、驱动注册与探测流程以及BAR映射机制,是排查DMA传输故障的基础。本章将深入剖析这三个核心模块,帮助你在驱动开发中精准定位问题。
3.1 pci_bus与pci_dev数据结构
Linux内核使用两个核心数据结构来描述PCI拓扑:pci_bus表示总线,pci_dev表示设备。它们共同构成了从根总线到末端设备的树形结构。
3.1.1 pci_bus结构
pci_bus代表一条物理PCI总线,包含总线编号、父总线、子总线链表以及该总线上的设备链表。关键字段如下:
| 字段 | 类型 | 说明 |
|---|---|---|
number |
unsigned char | 总线号(Bus Number) |
primary |
unsigned char | 主总线号(用于桥接设备) |
secondary |
unsigned char | 从属总线号(桥接设备下游) |
subordinate |
unsigned char | 最大从属总线号 |
bridge |
struct pci_dev * | 指向桥接设备的指针(若为根总线则为NULL) |
devices |
struct list_head | 该总线上所有设备的链表头 |
children |
struct list_head | 子总线链表头 |
3.1.2 pci_dev结构
pci_dev是单个PCI设备的抽象,包含配置空间信息、资源、驱动绑定状态等。核心字段包括:
- 标识字段:
vendor、device、subsystem_vendor、subsystem_device、class、revision - 拓扑字段:
bus(所属总线)、devfn(设备/功能号)、slot(槽位) - 资源字段:
resource[](最多6个BAR + ROM + 桥接窗口) - 驱动字段:
driver(绑定的驱动)、dev(内核通用设备) - DMA字段:
dma_mask、coherent_dma_mask、dev.dma_parms
排查要点:当DMA传输失败时,首先检查
pci_dev->dma_mask是否小于设备实际需要的地址范围。例如,64位设备若dma_mask被错误设置为32位,将导致地址截断。
3.1.3 PCI拓扑结构SVG
下图展示了PCI总线-设备树形结构,从根总线到末端功能设备的完整路径:
3.2 pci_driver注册与探测流程
PCI驱动通过pci_register_driver()注册到内核,内核在枚举设备时调用驱动的probe()函数完成绑定。理解这一流程对调试驱动加载失败至关重要。
3.2.1 驱动注册核心结构
struct pci_driver是驱动开发者需要填充的核心结构:
struct pci_driver {
const char *name; // 驱动名称
const struct pci_device_id *id_table; // 支持的设备ID表
int (*probe)(struct pci_dev *dev, const struct pci_device_id *id);
void (*remove)(struct pci_dev *dev);
int (*suspend)(struct pci_dev *dev, pm_message_t state);
int (*resume)(struct pci_dev *dev);
// ... 其他回调
};
3.2.2 探测流程步骤
- 设备枚举:PCI子系统扫描总线,为每个设备创建
pci_dev实例。 - ID匹配:遍历已注册驱动的
id_table,匹配vendor/device/class等字段。 - 资源分配:内核为设备分配I/O或MMIO资源(BAR映射)。
- 调用probe:匹配成功后调用驱动的
probe()函数。 - DMA初始化:在probe中设置dma_mask、申请DMA缓冲区。
- 设备注册:将设备注册到内核设备模型,生成设备文件或网络接口。
3.2.3 探测流程SVG
3.2.4 典型probe函数模板
static int my_pci_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
int ret;
struct my_dev *dev;
// 1. 启用PCI设备
ret = pci_enable_device(pdev);
if (ret) return ret;
// 2. 设置DMA掩码(先尝试64位,失败则回退32位)
ret = dma_set_mask(&pdev->dev, DMA_BIT_MASK(64));
if (ret) {
ret = dma_set_mask(&pdev->dev, DMA_BIT_MASK(32));
if (ret) goto err_disable;
}
// 3. 申请BAR资源映射
ret = pcim_iomap_regions(pdev, BIT(0), "my_driver");
if (ret) goto err_disable;
// 4. 分配驱动私有数据结构
dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
if (!dev) { ret = -ENOMEM; goto err_disable; }
// 5. 申请DMA缓冲区
dev->dma_buf = dma_alloc_coherent(&pdev->dev, BUF_SIZE,
&dev->dma_handle, GFP_KERNEL);
if (!dev->dma_buf) { ret = -ENOMEM; goto err_disable; }
// 6. 注册中断
ret = request_irq(pdev->irq, my_isr, 0, "my_driver", dev);
if (ret) goto err_free_dma;
// 7. 保存私有数据
pci_set_drvdata(pdev, dev);
return 0;
err_free_dma:
dma_free_coherent(&pdev->dev, BUF_SIZE, dev->dma_buf, dev->dma_handle);
err_disable:
pci_disable_device(pdev);
return ret;
}
常见故障:probe函数中忘记调用
pci_enable_device()会导致后续所有PCI配置空间访问失败;dma_set_mask()返回错误时未回退32位,会导致64位设备在32位系统上无法工作。
3.3 pci_resource与BAR映射
PCI设备的BAR(Base Address Register)定义了设备在系统中的地址空间。Linux通过pci_resource_*系列宏访问这些资源,驱动需要正确映射BAR才能访问设备寄存器。
3.3.1 BAR类型与标志
| BAR类型 | 标志位 | 地址空间 | 典型用途 |
|---|---|---|---|
| I/O BAR | bit 0 = 1 | I/O端口空间 | 配置寄存器、状态寄存器 |
| 32位MMIO BAR | bit 0 = 0, bit 2:1 = 00 | 32位内存空间 | 数据缓冲区、控制寄存器 |
| 64位MMIO BAR | bit 0 = 0, bit 2:1 = 10 | 64位内存空间 | 大容量DMA缓冲区、显存 |
| 扩展ROM BAR | 特殊类型 | 内存空间 | 设备固件/选项ROM |
3.3.2 资源访问API
Linux提供以下宏和函数操作BAR资源:
// 获取BAR的物理地址
resource_size_t pci_resource_start(struct pci_dev *dev, int bar);
resource_size_t pci_resource_end(struct pci_dev *dev, int bar);
// 获取BAR的大小
resource_size_t pci_resource_len(struct pci_dev *dev, int bar);
// 获取BAR的标志(IORESOURCE_IO / IORESOURCE_MEM)
unsigned long pci_resource_flags(struct pci_dev *dev, int bar);
// 映射BAR到内核虚拟地址
void __iomem *pcim_iomap(struct pci_dev *pdev, int bar, unsigned long maxlen);
void pcim_iounmap(struct pci_dev *pdev, void __iomem *addr);
// 批量映射(推荐)
int pcim_iomap_regions(struct pci_dev *pdev, int mask, const char *name);
3.3.3 BAR映射与DMA地址关系SVG
3.3.4 资源映射故障排查清单
- BAR未分配:检查BIOS/UEFI是否分配了地址空间,使用
lspci -vvv查看BAR区域是否为[disabled]。 - 地址冲突:多个设备BAR映射到相同地址范围,检查
/proc/iomem和/proc/ioports。 - 映射大小错误:
pci_resource_len()返回0或异常值,可能是设备配置空间损坏。 - 64位BAR截断:32位系统上访问64位BAR时地址被截断,需确认内核支持
CONFIG_PHYS_ADDR_T_64BIT。 - IO BAR误用:对I/O BAR使用
ioremap()而非ioport_map(),导致访问异常。
调试技巧:在probe函数中添加
dev_info(&pdev->dev, "BAR0: start=0x%llx, len=%llu, flags=0x%lx\n", pci_resource_start(pdev, 0), pci_resource_len(pdev, 0), pci_resource_flags(pdev, 0));,通过dmesg验证资源分配是否正确。
3.4 本章小结
本章深入剖析了Linux PCI子系统的三大核心模块:
- pci_bus/pci_dev数据结构:理解了总线-设备树形拓扑,以及关键字段在DMA调试中的作用。
- pci_driver注册与探测流程:掌握了从设备枚举到probe调用的完整链路,以及常见故障点。
- pci_resource与BAR映射:区分了BAR地址与DMA地址的本质差异,并提供了资源映射的排查清单。