第四章:PCIe驱动开发基础

说实话,PCIe驱动开发这块,我当年刚接触的时候也是一头雾水。配置空间、BAR映射、中断注册、DMA传输——这些名词听着就头大。但等你真正上手调过几块板子,就会发现,其实核心就那么几件事。

今天咱们就掰开揉碎,把PCIe驱动开发的四个基本功讲透。我个人习惯是从配置空间开始,一步步往下走。

4.1 PCIe配置空间访问

PCIe设备上电后,系统会分配一个唯一的BDF(Bus:Device:Function)号。通过这个号,我们就能访问它的配置空间。配置空间是啥?说白了就是设备的“身份证”加“控制面板”。

标准配置空间有256字节,前64字节是必须的,后面是扩展的。我刚开始做驱动时,经常需要读Vendor ID和Device ID来确认设备是否在位。

核心寄存器一览:

  • Vendor ID (0x00):厂商编号,比如0x8086是Intel
  • Device ID (0x02):设备型号
  • Command (0x04):控制位,比如开启IO空间、内存空间、总线主控
  • Status (0x06):状态位,比如是否收到中断
  • BAR0~BAR5 (0x10~0x24):基地址寄存器,映射用的

在Linux下,访问配置空间最常用的方式是通过pci_read_config_*pci_write_config_*系列函数。举个例子:

#include <linux/pci.h>

struct pci_dev *pdev;
u16 vendor_id, device_id;

// 假设pdev已经通过pci_get_device获取
pci_read_config_word(pdev, PCI_VENDOR_ID, &vendor_id);
pci_read_config_word(pdev, PCI_DEVICE_ID, &device_id);

printk(KERN_INFO "Vendor: 0x%04x, Device: 0x%04x\n", vendor_id, device_id);

嗯,这里要注意:读取配置空间时,一定要检查返回值。我曾经遇到过一块有问题的FPGA板卡,读配置空间时偶尔会超时,如果不做错误处理,驱动直接崩掉。

避坑指南:我曾经在调试时发现,某些PCIe桥片在热插拔场景下,配置空间访问会返回全F。所以每次读取后,最好判断一下是不是0xFFFF,如果是,说明设备可能已经不存在了。

4.2 BAR空间映射

配置空间里的BAR寄存器,决定了设备的地址空间映射到系统内存的哪个位置。每个BAR可以映射内存空间或IO空间,但现在的AI芯片基本都用内存映射。

BAR空间的大小是怎么确定的?硬件设计时就已经固定了。驱动要做的是:

  1. 读取BAR寄存器,获取基地址
  2. 获取BAR空间的大小
  3. 调用ioremap将物理地址映射到内核虚拟地址空间

看代码:

unsigned long bar_start, bar_len;
void __iomem *bar_virt;

bar_start = pci_resource_start(pdev, 0);  // BAR0的物理起始地址
bar_len   = pci_resource_len(pdev, 0);    // BAR0的大小

bar_virt = ioremap(bar_start, bar_len);
if (!bar_virt) {
    printk(KERN_ERR "ioremap failed\n");
    return -ENOMEM;
}

// 现在可以通过bar_virt读写设备寄存器了
writel(0x1, bar_virt + 0x100);  // 写某个控制寄存器

我个人习惯在映射完成后,先读一下BAR空间的某个已知寄存器,验证映射是否正确。比如读版本号寄存器,如果返回0xFFFFFFFF,那八成是映射地址不对。

小技巧:调试BAR映射时,可以用lspci -vvv查看当前BAR的分配情况。如果发现BAR地址是0或者全F,说明设备没有被正确枚举。

4.3 MSI/MSI-X中断注册

PCIe中断有两种:传统的INTx和消息信号中断(MSI/MSI-X)。INTx是共享中断线,性能差,容易冲突。MSI/MSI-X则是通过写特定地址来触发中断,效率高得多。

MSI和MSI-X的区别?MSI最多支持32个中断向量,MSI-X可以支持到2048个。对于AI芯片这种需要大量中断的场景,MSI-X是首选。

注册MSI-X中断的流程:

  1. 调用pci_alloc_irq_vectors申请中断向量
  2. 为每个向量注册中断处理函数
  3. 在中断处理函数中,读取设备的中断状态寄存器,确认中断来源

代码示例:

int nr_irqs;
int i;

nr_irqs = pci_alloc_irq_vectors(pdev, 1, 4, PCI_IRQ_MSIX);
if (nr_irqs < 0) {
    printk(KERN_ERR "Failed to allocate MSI-X vectors\n");
    return nr_irqs;
}

for (i = 0; i < nr_irqs; i++) {
    ret = request_irq(pci_irq_vector(pdev, i), my_irq_handler,
                      IRQF_SHARED, "my_device", &my_data[i]);
    if (ret) {
        printk(KERN_ERR "Failed to register IRQ %d\n", i);
        goto err;
    }
}

中断处理函数里,记得要清除设备的中断状态位,否则会一直触发中断。我曾经犯过这个错,结果CPU被中断风暴直接打满,系统几乎卡死。

避坑指南:我曾经在调试MSI-X时,发现中断注册成功了,但就是触发不了。查了半天,原来是设备端的MSI-X Table和PBA(Pending Bit Array)没有正确配置。记住:MSI-X需要设备端也开启对应的中断使能位。

4.4 DMA传输原理与编程

DMA(直接内存访问)是AI芯片数据传输的核心。没有DMA,CPU得亲自搬数据,那效率就太低了。

DMA的基本流程:

  1. CPU在内存中分配一块缓冲区
  2. 将缓冲区的物理地址、大小、传输方向写入DMA控制器的寄存器
  3. 启动DMA传输
  4. 传输完成后,设备触发中断通知CPU

在Linux驱动中,DMA缓冲区需要是物理连续的。用dma_alloc_coherent可以分配这样的缓冲区:

dma_addr_t dma_handle;
void *cpu_addr;

cpu_addr = dma_alloc_coherent(&pdev->dev, size, &dma_handle, GFP_KERNEL);
if (!cpu_addr) {
    printk(KERN_ERR "DMA allocation failed\n");
    return -ENOMEM;
}

// dma_handle是设备看到的物理地址
// cpu_addr是CPU看到的虚拟地址
// 把dma_handle写入设备的DMA源地址寄存器
writel(lower_32_bits(dma_handle), bar_virt + DMA_SRC_ADDR_LOW);
writel(upper_32_bits(dma_handle), bar_virt + DMA_SRC_ADDR_HIGH);
writel(size, bar_virt + DMA_SIZE);
writel(0x1, bar_virt + DMA_START);  // 启动传输

DMA传输完成后,设备会发中断。在中断处理函数里,检查DMA状态寄存器,确认传输是否成功。

关键点:DMA传输涉及Cache一致性。如果CPU修改了缓冲区内容,但Cache没有刷到内存,DMA读到的就是旧数据。反过来,DMA写入了新数据,CPU读到的可能是Cache里的旧数据。

解决办法:使用dma_alloc_coherent分配的缓冲区默认是Cache一致的。如果要用kmalloc分配的缓冲区,需要手动调用dma_map_singledma_unmap_single来管理一致性。

小技巧:调试DMA时,我习惯在传输前后打印缓冲区的第一个和最后一个字节。如果传输后数据没变,要么是DMA没启动,要么是地址写错了。

知识体系总览

下面这张图,是我自己总结的PCIe驱动开发核心逻辑。你看一眼,心里就有谱了。

PCIe驱动开发核心流程 1. 配置空间访问 读取Vendor/Device ID 2. BAR空间映射 ioremap物理地址 3. MSI/MSI-X中断 注册中断处理函数 4. DMA传输 分配一致内存 配置DMA描述符 源地址/目的地址/长度 启动DMA引擎 写控制寄存器 等待中断完成 检查状态寄存器 核心要点 配置空间是入口 → BAR映射是桥梁 → 中断是通知机制 → DMA是数据传输引擎 四者环环相扣,缺一不可 公众号:蓝海资料掘金营,微信deep3321

你看,整个流程其实就是一个闭环:先通过配置空间找到设备,然后映射BAR空间来访问设备寄存器,接着注册中断来接收设备通知,最后用DMA来高效传输数据。每一步都依赖上一步的成果。

在实际项目中,这四个步骤往往需要反复调试。我记得有一次调一块AI加速卡,BAR映射没问题,中断也能触发,但DMA就是传不对数据。最后发现是DMA描述符里的地址没有做64位拆分——设备只支持32位地址,而我传了个64位地址过去。

嗯,这种坑踩多了,经验就来了。PCIe驱动开发,说白了就是跟配置空间、BAR、中断、DMA这四个老朋友打交道。把它们摸透了,剩下的就是具体设备的寄存器细节了。

总结一下:

  • 配置空间访问:用pci_read_config_*系列函数,注意检查返回值
  • BAR映射:用ioremap,映射后先验证
  • 中断注册:优先用MSI-X,记得清除中断状态位
  • DMA传输:用dma_alloc_coherent分配缓冲区,注意Cache一致性

好了,PCIe驱动开发的基础就这些。下一节咱们会深入讲具体设备的寄存器编程,到时候再结合实战案例细聊。