第四章: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空间的大小是怎么确定的?硬件设计时就已经固定了。驱动要做的是:
- 读取BAR寄存器,获取基地址
- 获取BAR空间的大小
- 调用
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中断的流程:
- 调用
pci_alloc_irq_vectors申请中断向量 - 为每个向量注册中断处理函数
- 在中断处理函数中,读取设备的中断状态寄存器,确认中断来源
代码示例:
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的基本流程:
- CPU在内存中分配一块缓冲区
- 将缓冲区的物理地址、大小、传输方向写入DMA控制器的寄存器
- 启动DMA传输
- 传输完成后,设备触发中断通知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_single和dma_unmap_single来管理一致性。
小技巧:调试DMA时,我习惯在传输前后打印缓冲区的第一个和最后一个字节。如果传输后数据没变,要么是DMA没启动,要么是地址写错了。
知识体系总览
下面这张图,是我自己总结的PCIe驱动开发核心逻辑。你看一眼,心里就有谱了。
你看,整个流程其实就是一个闭环:先通过配置空间找到设备,然后映射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驱动开发的基础就这些。下一节咱们会深入讲具体设备的寄存器编程,到时候再结合实战案例细聊。