ESC
输入关键词搜索文章标题和内容

PCIe驱动开发:从拓扑结构到DMA传输的完整实战

本文由 linuxROS 整理发布,首发于 linuxros.cn,转载请注明出处。

PCIe驱动开发:从拓扑结构到DMA传输的完整实战

PCIe是现代x86和ARM服务器上最主流的高速总线,NVMe SSD、网卡、GPU、FPGA加速卡全挂在PCIe上。和I2C、SPI那种简单总线不同,PCIe有独立的配置空间、复杂的地址映射、MSI-X中断和DMA引擎,驱动写起来门槛高不少。本文基于Linux 6.8内核,从PCIe拓扑和配置空间讲起,过一遍核心数据结构,用完整驱动示例把probe/remove/BAR映射/MSI-X/DMA串起来,最后给调试方法和常见坑点。

一、PCIe核心概念

PCIe拓扑结构

PCIe是点对点串行总线,拓扑结构是树形的:

Root Complex (RC, 集成在CPU/芯片组内)
  ├── Endpoint (网卡)
  ├── Switch
    ├── Endpoint (NVMe SSD)
    └── Endpoint (FPGA加速卡)
  └── Endpoint (GPU)
  • **Root Complex(RC):PCIe树的根节点,CPU通过RC访问PCIe设备,RC内部集成Host Bridge
  • Switch:扩展端口,类似网络交换机,把一条上行端口拆成多条下行端。- Endpoint:最终的功能设备,NVMe、网卡、GPU都是Endpoint

和USB的主从模式不同,PCIe的点对点连接意味着RC和Endpoint之间是专用链路,不共享带宽。Switch下面的设备互不干扰。

PCIe与PCI的区别

对比 PCI PCIe
信号方式 并行总线(32/64位共享) 串行差分线
连接方式 共享总线,设备争用 点对点,独享带宽
时钟 共享33/66MHz时钟 每条Lane内嵌时钟,8b/10b,128b/130b编码
带宽 133MB/s(32位33MHz) 单Lane最高~4GB/s(Gen5 x1)
配置空间 256字节 256字节 + 4KB扩展
中断 INTx引脚共享 MSI/MSI-X消息中断
热插拔 不支持 原生支持

PCIe不是简单升级版PCI,物理层和链路层完全重写了,软件层面保持了向后兼容——配置空间前64字节和PCI一致,驱动模型也沿用了pci_driver框架。

PCIe版本与带宽

PCIe版本 编码方式 单Lane速率 单Lane有效带宽 x16带宽
Gen1 8b/10b 2.5 GT/s ~250 MB/s ~4.0 GB/s
Gen2 8b/10b 5.0 GT/s ~500 MB/s ~8.0 GB/s
Gen3 128b/130b 8.0 GT/s ~985 MB/s ~15.8 GB/s
Gen4 128b/130b 16.0 GT/s ~1.97 GB/s ~31.5 GB/s
Gen5 128b/130b 32.0 GT/s ~3.94 GB/s ~63.0 GB/s

注意GT/s和GB/s的区别:GT/s是原始线路速率,8b/10b编码,20%开销,128b/130b编码开销1.5%。Gen3之后换了编码方式,有效带宽提升明显。

TLP事务层包

PCIe设备之间通过TLP(Transaction Layer Packet)通信,TLP是PCIe协议的核心:

TLP类型 方向 用途
Memory Read RC→Endpoint 读取设备寄存器或DMA内存
Memory Write RC→Endpoint 写设备寄存器或DMA内存
Completion Endpoint→RC 响应Read请求,返回数据
Configuration Read/Write RC→Endpoint 读写配置空间
Message 双向 中断、错误报告、电源管理

驱动开发者不需要手动构造TLP,PCIe控制器硬件自动完成TLP的打包和解包,但理解TLP有助于调试——当DMA传输失败或MMIO读取返回全F时,问题往往出在TLP路由上。

二、PCIe配置空间

配置空间布局

每个PCIe功能(Function)有256字节的传统配置空间,PCIe扩展4KB。前64字节是PCI兼容的Type 0 Header(Endpoint用),Type 1 Header是Bridge用的。

关键寄存器

偏移 寄存器名 宽度 说明
0x00 Vendor ID 16bit 厂商ID,0xFFFF表示未用
0x02 Device ID 16bit 设备ID
0x04 Command 16bit 总线命令:I/O空间使能、内存空间使能、总线主控制器
0x06 Status 16bit 状态标志:中断状态、错误标志等
0x08 Revision ID 8bit 芯片版本
0x09 Class Code 24bit 设备分类码(3字节:基类+子类+协议)
0x0C Cache Line Size 8bit 缓存行大小
0x0D Latency Timer 8bit 延迟定时器
0x0E Header Type 8bit 头类型:bit7=多功能,bit6:0=0(Type0)/1(Type1)
0x0F BIST 8bit 内建自测
0x10-0x24 BAR[0:5] 32bit 基地址寄存器,6个
0x2C Subsystem Vendor ID 16bit 子系统厂商ID
0x2E Subsystem Device ID 16bit 子系统设备ID
0x30 Expansion ROM Base 32bit 扩展ROM基址
0x34 Capabilities Pointer 8bit 能力链表指针
0x3C Interrupt Line 8bit 中断线号(INTx用)
0x3D Interrupt Pin 8bit 中断引脚0=无,1=INTA0=INTB...
0x3E-0x3F Min Grant / Max Latency 8bit 总线仲裁参数

配置空间布局图(Header Type 0。

flowchart TB subgraph 配置空间["PCIe配置空间 (Header Type 0)"] A["0x00-0x01<br/>Vendor ID"] --- B["0x02-0x03<br/>Device ID"] B --- C["0x04-0x05<br/>Command"] C --- D["0x06-0x07<br/>Status"] D --- E["0x08<br/>Revision ID"] E --- F["0x09-0x0B<br/>Class Code"] F --- G["0x0C-0x0D<br/>Cache Line / Latency"] G --- H["0x0E<br/>Header Type"] H --- I["0x0F<br/>BIST"] end subgraph BAR区域["BAR寄存器区"] J["0x10<br/>BAR0"] --- K["0x14<br/>BAR1"] K --- L["0x18<br/>BAR2"] --- M["0x1C<br/>BAR3"] M --- N["0x20<br/>BAR4"] --- O["0x24<br/>BAR5"] end subgraph 扩展区域["扩展信息"] P["0x2C-0x2F<br/>Subsystem ID"] Q["0x30-0x33<br/>Expansion ROM"] R["0x34<br/>Capabilities Ptr"] S["0x3C<br/>Interrupt Line"] T["0x3D<br/>Interrupt Pin"] end 配置空间 --> BAR区域 BAR区域 --> 扩展区域 style 配置空间 fill:#E3F2FD style BAR区域 fill:#FFF8E1 style 扩展区域 fill:#E8F5E9

BAR是驱动开发打交道最多的寄存器——它告诉CPU设备的寄存器或内存映射到哪个物理地址。BAR的低几位是只读的属性位:bit0=1表示I/O空间,bit0=0表示内存空间;bit2:1表示内存空间类型。0=32位,10=64位);bit3表示是否可预取。

三、PCIe核心数据结构

pci_driver:驱动注册入。

// include/linux/pci.h(简化)
struct pci_driver {
    const char *name;           // 驱动名,/sys/bus/pci/drivers/下可。    const struct pci_device_id *id_table;  // 设备匹配置    int  (*probe)(struct pci_dev *pdev,
                  const struct pci_device_id *ent);  // 设备发现时调试    void (*remove)(struct pci_dev *pdev);             // 设备移除时调试    int  (*suspend)(struct pci_dev *pdev, pm_message_t state);
    int  (*resume)(struct pci_dev *pdev);
    void (*shutdown)(struct pci_dev *pdev);
    struct device_driver driver;  // 内嵌device_driver
};

注册宏:

// 注册PCIe驱动
module_pci_driver(my_pci_driver);

// 等价于:
// static int __init my_init(void) { return pci_register_driver(&my_pci_driver); }
// module_init(my_init);
// static void __exit my_exit(void) { pci_unregister_driver(&my_pci_driver); }
// module_exit(my_exit);

pci_device_id:设备匹配规。

// include/linux/mod_devicetable.h
struct pci_device_id {
    __u32 vendor;       // Vendor ID,PCI_ANY_ID表示通配
    __u32 device;       // Device ID
    __u32 subvendor;    // Subsystem Vendor ID
    __u32 subdevice;    // Subsystem Device ID
    __u32 class;        // Class Code
    __u32 class_mask;   // Class匹配掩码
    kernel_ulong_t driver_data;  // 驱动私有数据
};

匹配规则:vendor/device/subvendor/subdevice/class依次匹配,PCI_ANY_ID0xFFFFFFFF)表示忽略该字段。

// 匹配示例
static const struct pci_device_id my_pci_ids[] = {
    // 精确匹配:某厂商某设。    { PCI_DEVICE(0x10ee, 0x8030) },
    // 匹配某厂商所有设。    { PCI_DEVICE(0x10ee, PCI_ANY_ID) },
    // 匹配某类设备(网络控制器。    { PCI_DEVICE_CLASS(PCI_CLASS_NETWORK_ETHERNET, 0xffff00) },
    // 子系统精确匹。    { PCI_DEVICE_SUB(0x10ee, 0x8030, 0x10ee, 0x0001) },
    { 0, }  // 结尾哨兵
};
MODULE_DEVICE_TABLE(pci, my_pci_ids);

MODULE_DEVICE_TABLE(pci, my_pci_ids) 这行不能省——它把设备ID表导出到模块信息段,modprobe才能根据设备ID自动加载驱动。

pci_dev:内核表示PCI设备

// include/linux/pci.h(关键字段)
struct pci_dev {
    struct device dev;          // 内嵌device,统一设备模型
    u16 vendor;                 // Vendor ID
    u16 device;                 // Device ID
    u16 subsystem_vendor;       // Subsystem Vendor ID
    u16 subsystem_device;       // Subsystem Device ID
    u8  hdr_type;               // Header Type
    u8  irq;                    // 中断号(INTx。    struct resource resource[DEVICE_COUNT_RESOURCE];  // BAR资源
    // ...
};

resource[]数组对应6个BAR,每个resource包含物理地址、大小和标志。驱动通过pci_resource_start(pdev, bar)等宏获取BAR信息。

四、简单PCIe驱动示例

完整驱动框架

// my_pcie_drv.c - 简单PCIe驱动框架(Linux 6.8。#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/pci.h>
#include <linux/interrupt.h>
#include <linux/io.h>

#define DRV_NAME "my_pcie_drv"

/* 设备ID匹配置*/
static const struct pci_device_id my_pci_ids[] = {
    { PCI_DEVICE(0x1234, 0x5678) },  // 替换为实际VID/DID
    { 0, }
};
MODULE_DEVICE_TABLE(pci, my_pci_ids);

/* 设备私有数据 */
struct my_pcie_dev {
    struct pci_dev *pdev;
    void __iomem *hw_regs;      // MMIO映射地址
    dma_addr_t dma_handle;      // DMA缓冲区总线地址
    void *dma_buf;              // DMA缓冲区虚拟地址
    size_t dma_size;            // DMA缓冲区大。};

/* 中断处理函数 */
static irqreturn_t my_pcie_irq_handler(int irq, void *dev_id)
{
    struct my_pcie_dev *mydev = dev_id;
    u32 status;

    /* 读中断状态寄存器 */
    status = readl(mydev->hw_regs + 0x04);
    if (!status)
        return IRQ_NONE;

    /* 清中*/
    writel(status, mydev->hw_regs + 0x08);

    dev_info(&mydev->pdev->dev, "irq: status=0x%x\n", status);
    return IRQ_HANDLED;
}

/* probe:设备发现时调用 */
static int my_pcie_probe(struct pci_dev *pdev,
                         const struct pci_device_id *ent)
{
    struct my_pcie_dev *mydev;
    int ret;

    dev_info(&pdev->dev, "probe: found device %04x:%04x\n",
             pdev->vendor, pdev->device);

    /* 1. 分配私有数据 */
    mydev = devm_kzalloc(&pdev->dev, sizeof(*mydev), GFP_KERNEL);
    if (!mydev)
        return -ENOMEM;
    mydev->pdev = pdev;
    pci_set_drvdata(pdev, mydev);

    /* 2. 启用PCIe设备 */
    ret = pcim_enable_device(pdev);
    if (ret) {
        dev_err(&pdev->dev, "pcim_enable_device failed: %d\n", ret);
        return ret;
    }

    /* 3. 请求MMIO区域 */
    ret = pcim_iomap_regions(pdev, BIT(0), DRV_NAME);
    if (ret) {
        dev_err(&pdev->dev, "pcim_iomap_regions failed: %d\n", ret);
        return ret;
    }

    /* 4. 获取映射地址 */
    mydev->hw_regs = pcim_iomap_table(pdev)[0];
    if (!mydev->hw_regs) {
        dev_err(&pdev->dev, "pcim_iomap_table returned NULL\n");
        return -ENOMEM;
    }

    /* 5. 设置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));
        if (ret) {
            dev_err(&pdev->dev, "dma_set_mask failed: %d\n", ret);
            return ret;
        }
    }

    /* 6. 请求MSI中断 */
    ret = pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_MSI);
    if (ret < 0) {
        dev_warn(&pdev->dev, "MSI failed, trying INTx\n");
        ret = pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_INTX);
        if (ret < 0) {
            dev_err(&pdev->dev, "irq alloc failed: %d\n", ret);
            return ret;
        }
    }

    ret = devm_request_irq(&pdev->dev, pci_irq_vector(pdev, 0),
                           my_pcie_irq_handler, IRQF_SHARED,
                           DRV_NAME, mydev);
    if (ret) {
        dev_err(&pdev->dev, "request_irq failed: %d\n", ret);
        return ret;
    }

    /* 7. 启用总线主控(DMA必需*/
    pci_set_master(pdev);

    dev_info(&pdev->dev, "probe: device initialized\n");
    return 0;
}

/* remove:设备移除时调用 */
static void my_pcie_remove(struct pci_dev *pdev)
{
    struct my_pcie_dev *mydev = pci_get_drvdata(pdev);

    /* devm资源自动释放,无需手动free */
    dev_info(&pdev->dev, "remove: device released\n");
}

/* 电源管理(可选) */
static int my_pcie_suspend(struct pci_dev *pdev, pm_message_t state)
{
    pci_save_state(pdev);
    pci_disable_device(pdev);
    return 0;
}

static int my_pcie_resume(struct pci_dev *pdev)
{
    pci_restore_state(pdev);
    return pcim_enable_device(pdev);
}

/* 驱动结构建*/
static struct pci_driver my_pci_driver = {
    .name      = DRV_NAME,
    .id_table  = my_pci_ids,
    .probe     = my_pcie_probe,
    .remove    = my_pcie_remove,
    .suspend   = my_pcie_suspend,
    .resume    = my_pcie_resume,
};

module_pci_driver(my_pci_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("linuxros");
MODULE_DESCRIPTION("Simple PCIe driver for Linux 6.8");

probe流程。

flowchart TB A(["probe入口"]) --> B["devm_kzalloc<br/>分配私有数据"] B --> C["pcim_enable_device<br/>启用PCIe设备"] C --> D["pcim_iomap_regions<br/>映射BAR0"] D --> E["dma_set_mask_and_coherent<br/>设置DMA掩码"] E --> F{"MSI可用?"} F -->|"| G["pci_alloc_irq_vectors<br/>PCI_IRQ_MSI"] F -->|"| H["pci_alloc_irq_vectors<br/>PCI_IRQ_INTX"] G --> I["devm_request_irq<br/>注册中断"] H --> I I --> J["pci_set_master<br/>启用总线主控"] J --> K(["probe成功返回0"]) style A fill:#E3F2FD style K fill:#E8F5E9 style F fill:#FFF8E1 style H fill:#FFEBEE

注意上面大量使用了devm_*和pcim_*函数。Linux 6.8推荐用设备资源管理(devm)版本——probe失败或remove时自动释放资源,不会遗漏。老式写法需要手动调pci_iounmap、pci_release_regions、free_irq等,容易在错误路径上忘记释放。

五、BAR映射与MMIO

BAR是什么?

BAR(Base Address Register)是PCIe设备向CPU暴露寄存器或内存的窗口。CPU通过BAR映射的地址读写设备寄存器,这就是MMIO(Memory-Mapped I/O)。
每个PCIe设备最。个BAR(Type 0),每个BAR可以是:
- 内存空间BAR:映射到CPU的物理内存地址空间,用readl/writel访问
- I/O空间BAR:映射到x86的I/O端口空间,用inb/outb访问(现代驱动基本不用)

pci_iomap系列函数

// 映射整个BAR(老式写法。.8仍可用但不推荐)
void __iomem *pci_iomap(struct pci_dev *pdev, int bar, unsigned long maxlen);

// 映射BAR的指定范。void __iomem *pci_iomap_range(struct pci_dev *pdev, int bar,
                               unsigned long offset, unsigned long maxlen);

// 推荐写法:pcim_iomap_regions + pcim_iomap_table
// 一步完成:请求区域 + 映射
int pcim_iomap_regions(struct pci_dev *pdev, int mask, const char *name);

// 获取映射后的虚拟地址。void __iomem * const *pcim_iomap_table(struct pci_dev *pdev);

MMIO寄存器读。

// 32位读。u32 val = readl(regs + OFFSET);
writel(val, regs + OFFSET);

// 16位读。u16 val = readw(regs + OFFSET);
writew(val, regs + OFFSET);

// 8位读。u8 val = readb(regs + OFFSET);
writeb(val, regs + OFFSET);

// 。。写(设置某一位)
u32 val = readl(regs + CTRL_OFFSET);
val |= BIT(3);          // 设置bit3
val &= ~BIT(7);         // 清除bit7
writel(val, regs + CTRL_OFFSET);

readl/writel有内存屏障语义,保证读写顺序。不要用ioread32/iowrite32的memcpy变体来访问寄存器——那些是给大块内存用的。

来自 linuxros.cn · linuxROS

完整BAR操作示例

/* 寄存器偏移定*/
#define REG_CTRL    0x00
#define REG_STATUS  0x04
#define REG_DATA    0x08

/* 控制寄存器位定义 */
#define CTRL_START  BIT(0)
#define CTRL_RESET  BIT(1)
#define CTRL_IRQ_EN BIT(4)

static int my_device_init(struct my_pcie_dev *mydev)
{
    void __iomem *regs = mydev->hw_regs;
    u32 status;

    /* 先复位设*/
    writel(CTRL_RESET, regs + REG_CTRL);
    usleep_range(1000, 2000);

    /* 清状态,使能中断,启*/
    writel(0xffffffff, regs + REG_STATUS);  // 。清零
    writel(CTRL_IRQ_EN | CTRL_START, regs + REG_CTRL);

    /* 等待就绪 */
    status = readl(regs + REG_STATUS);
    if (!(status & BIT(0))) {
        dev_err(&mydev->pdev->dev, "device not ready, status=0x%x\n", status);
        return -EIO;
    }

    return 0;
}

BAR信息查询

// 获取BAR的物理起始地址
resource_size_t pci_resource_start(struct pci_dev *pdev, int bar);

// 获取BAR的大。resource_size_t pci_resource_end(struct pci_dev *pdev, int bar);

// 获取BAR的标志unsigned long pci_resource_flags(struct pci_dev *pdev, int bar);
// IORESOURCE_MEM  - 内存空间
// IORESOURCE_IO   - I/O空间
// IORESOURCE_MEM_64 - 64位内存空。// IORESOURCE_PREFETCH - 可预。
// 判断BAR是否为内存空。if (pci_resource_flags(pdev, bar) & IORESOURCE_MEM) {
    // 这是内存空间BAR
}

六、MSI/MSI-X中断

三种中断机制对比

对比 INTx MSI MSI-X
触发方式 电平触发/边沿触发 写特定地址触发 写特定地址触发
向量数 1个(4个引脚共享) 1/2/4/8/16/32 最。048
中断号 固定IRQ 动态分配 动态分配
多向量 不支持 最多2个(2的幂 最多2048
共享 多设备共享IRQ 不共享 不共享
性能 低(需共享、需扫描)
配置复杂 简单 中等 较高

INTx是传统PCI中断。根引脚(INTA~INTD)被所有设备共享,中断来了要遍历链表找是谁发的。MSI和MSI-X都是消息中断——设备往特定地址写一个数据,CPU就收到中断,不需要IRQ线。MSI-X比MSI灵活:向量数更多,每个向量可以独立配置地址和数据。

MSI/MSI-X使用方法

/* 请求MSI中断。个向量) */
int nvec = pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_MSI);
if (nvec < 0) {
    dev_err(&pdev->dev, "MSI alloc failed: %d\n", nvec);
    return nvec;
}

/* 请求MSI-X中断(最。个向量) */
int nvec = pci_alloc_irq_vectors(pdev, 1, 8, PCI_IRQ_MSIX);
if (nvec < 0) {
    dev_err(&pdev->dev, "MSI-X alloc failed: %d\n", nvec);
    return nvec;
}
dev_info(&pdev->dev, "got %d MSI-X vectors\n", nvec);

/* 获取中断号并注册 */
int irq = pci_irq_vector(pdev, 0);  // 。个向量的IRQ。ret = devm_request_irq(&pdev->dev, irq, my_handler, 0, DRV_NAME, mydev);

/* 降级策略:先尝试MSI-X,再MSI,最后INTx */
int nvec = pci_alloc_irq_vectors(pdev, 1, 8,
                                  PCI_IRQ_MSIX | PCI_IRQ_MSI | PCI_IRQ_INTX);

多向量MSI-X示例

#define MAX_MSIX_VECTORS  4

struct my_pcie_dev {
    struct pci_dev *pdev;
    void __iomem *hw_regs;
    int num_vectors;
    /* 每个向量对应一个队*/
    struct my_queue queues[MAX_MSIX_VECTORS];
};

static irqreturn_t my_queue_irq_handler(int irq, void *dev_id)
{
    struct my_queue *q = dev_id;
    u32 status = readl(q->regs + REG_QUEUE_STATUS);

    if (!status)
        return IRQ_NONE;

    writel(status, q->regs + REG_QUEUE_STATUS);  // 清中。    /* 处理队列事件 */
    return IRQ_HANDLED;
}

static int my_setup_msix(struct my_pcie_dev *mydev)
{
    struct pci_dev *pdev = mydev->pdev;
    int i, ret;

    /* 请求MSI-X向量 */
    ret = pci_alloc_irq_vectors(pdev, 1, MAX_MSIX_VECTORS, PCI_IRQ_MSIX);
    if (ret < 0) {
        dev_warn(&pdev->dev, "MSI-X failed (%d), fallback to MSI\n", ret);
        ret = pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_MSI);
        if (ret < 0)
            return ret;
    }
    mydev->num_vectors = ret;

    /* 为每个向量注册中*/
    for (i = 0; i < mydev->num_vectors; i++) {
        int irq = pci_irq_vector(pdev, i);
        mydev->queues[i].regs = mydev->hw_regs + i * 0x100;
        mydev->queues[i].idx = i;

        ret = devm_request_irq(&pdev->dev, irq, my_queue_irq_handler,
                               0, DRV_NAME, &mydev->queues[i]);
        if (ret) {
            dev_err(&pdev->dev, "request_irq %d failed: %d\n", i, ret);
            return ret;
        }
    }

    dev_info(&pdev->dev, "setup %d irq vectors\n", mydev->num_vectors);
    return 0;
}

pci_alloc_irq_vectors的第二个参数是最小向量数,第三个是最大向量数。返回实际分配的向量数,可能小于请求的最大值。如果返回值小于最小值,说明分配失败。

七、DMA操作

PCIe设备的DMA引擎能直接读写系统内存,CPU不参与搬运。NVMe、网卡的高性能全靠它。

DMA地址设置

/* 设置DMA掩码——告诉内核设备能访问的地址宽度 */
/* 先尝。4位,不行降级32*/
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));
    if (ret) {
        dev_err(&pdev->dev, "no usable DMA mask\n");
        return ret;
    }
}

dma_set_mask_and_coherent同时设置流式DMA和一致性DMA的掩码。4位掩码表示设备能访问4GB以上内存32位则DMA缓冲区被限制。GB以下。

一致性DMA(Coherent DMA。

一致性DMA缓冲区对CPU和设备始终可见且一致,不需要手动同步。适合设备频繁访问的控制结构建

/* 分配一致性DMA缓冲*/
mydev->dma_size = 4096;
mydev->dma_buf = dma_alloc_coherent(&pdev->dev, mydev->dma_size,
                                     &mydev->dma_handle, GFP_KERNEL);
if (!mydev->dma_buf) {
    dev_err(&pdev->dev, "dma_alloc_coherent failed\n");
    return -ENOMEM;
}

dev_info(&pdev->dev, "dma buf: virt=%p bus=%pad size=%zu\n",
         mydev->dma_buf, &mydev->dma_handle, mydev->dma_size);

/* 使用:CPU写数据,设备DMA读取 */
memset(mydev->dma_buf, 0, mydev->dma_size);
/* 告诉设备DMA地址 */
writel(lower_32_bits(mydev->dma_handle), regs + REG_DMA_ADDR_LO);
writel(upper_32_bits(mydev->dma_handle), regs + REG_DMA_ADDR_HI);
writel(mydev->dma_size, regs + REG_DMA_SIZE);
writel(CTRL_DMA_START, regs + REG_CTRL);

/* 释放(remove时) */
dma_free_coherent(&pdev->dev, mydev->dma_size,
                  mydev->dma_buf, mydev->dma_handle);

流式DMA(Streaming DMA。

流式DMA用于一次性传输,需要手动映射和同步。适合大块数据的收发。

/* 发送方向:CPU→设*/
/* 1. 分配普通内存*/
void *tx_buf = kmalloc(buf_size, GFP_KERNEL);
/* 2. 填充数据 */
memcpy(tx_buf, data, data_len);
/* 3. 映射 */
dma_addr_t tx_dma = dma_map_single(&pdev->dev, tx_buf, buf_size,
                                    DMA_TO_DEVICE);
if (dma_mapping_error(&pdev->dev, tx_dma)) {
    dev_err(&pdev->dev, "dma_map_single failed\n");
    kfree(tx_buf);
    return -EIO;
}
/* 4. 告诉设备DMA地址,启动传递*/
writel(lower_32_bits(tx_dma), regs + REG_DMA_SRC_LO);
writel(upper_32_bits(tx_dma), regs + REG_DMA_SRC_HI);
writel(data_len, regs + REG_DMA_LEN);
writel(CTRL_DMA_START, regs + REG_CTRL);

/* 5. 传输完成后取消映*/
dma_unmap_single(&pdev->dev, tx_dma, buf_size, DMA_TO_DEVICE);
kfree(tx_buf);

/* 接收方向:设备→CPU */
dma_addr_t rx_dma = dma_map_single(&pdev->dev, rx_buf, buf_size,
                                    DMA_FROM_DEVICE);
if (dma_mapping_error(&pdev->dev, rx_dma))
    return -EIO;

/* 设备写完后,同步缓存 */
dma_unmap_single(&pdev->dev, rx_dma, buf_size, DMA_FROM_DEVICE);
/* 此时rx_buf中的数据对CPU可见 */

DMA传输完整示例

/* DMA寄存器偏*/
#define REG_DMA_CTRL     0x100
#define REG_DMA_SRC_LO   0x104
#define REG_DMA_SRC_HI   0x108
#define REG_DMA_DST_LO   0x10C
#define REG_DMA_DST_HI   0x110
#define REG_DMA_LEN      0x114
#define REG_DMA_STATUS   0x118

#define DMA_CTRL_START   BIT(0)
#define DMA_CTRL_DIR     BIT(1)  // 0=设备到内存,1=内存到设。#define DMA_STATUS_DONE  BIT(0)

static int my_dma_transfer(struct my_pcie_dev *mydev,
                           void *buf, size_t len, bool to_device)
{
    void __iomem *regs = mydev->hw_regs;
    dma_addr_t dma_addr;
    u32 ctrl, status;
    int ret;

    /* 映射DMA缓冲*/
    dma_addr = dma_map_single(&mydev->pdev->dev, buf, len,
                               to_device ? DMA_TO_DEVICE : DMA_FROM_DEVICE);
    if (dma_mapping_error(&mydev->pdev->dev, dma_addr)) {
        dev_err(&mydev->pdev->dev, "dma_map_single failed\n");
        return -EIO;
    }

    /* 设置DMA源地址(系统内存) */
    writel(lower_32_bits(dma_addr), regs + REG_DMA_SRC_LO);
    writel(upper_32_bits(dma_addr), regs + REG_DMA_SRC_HI);

    /* 设置传输长度 */
    writel(len, regs + REG_DMA_LEN);

    /* 启动传输 */
    ctrl = DMA_CTRL_START;
    if (to_device)
        ctrl |= DMA_CTRL_DIR;
    writel(ctrl, regs + REG_DMA_CTRL);

    /* 等待完成(简化版,实际应该用中断*/
    ret = readl_poll_timeout(regs + REG_DMA_STATUS, status,
                              status & DMA_STATUS_DONE,
                              10, 100000);  // 10us间隔。00ms超时
    if (ret) {
        dev_err(&mydev->pdev->dev, "DMA timeout\n");
        dma_unmap_single(&mydev->pdev->dev, dma_addr, len,
                         to_device ? DMA_TO_DEVICE : DMA_FROM_DEVICE);
        return -ETIMEDOUT;
    }

    /* 取消映射 */
    dma_unmap_single(&mydev->pdev->dev, dma_addr, len,
                     to_device ? DMA_TO_DEVICE : DMA_FROM_DEVICE);

    return 0;
}

readl_poll_timeout是Linux内核提供的轮询辅助宏,定义在linux/iopoll.h中。生产环境不要用轮询等DMA完成——应该用中断通知。

DMA操作速查

操作 一致性DMA 流式DMA
分配 dma_alloc_coherent kmalloc + dma_map_single
访问 CPU直接读写,无需同步 映射后CPU写完需dma_sync_single_for_device
释放 dma_free_coherent dma_unmap_single + kfree
适用 控制结构、描述符号 大块数据、一次性传递
缓存一致 硬件保证 软件手动同步

八、调试技。

lspci:查看PCIe设备

# 列出所有PCIe设备
lspci

# 详细信息。v详细。vv更详细,-vvv最详细。lspci -v -s 03:00.0

# 查看设备树形拓扑
lspci -t

# 只看某类设备(网络控制器。lspci -d ::0200

# 以数字方式显示(VID/DID。lspci -n -s 03:00.0

# 查看DMA地址宽度
lspci -vvv -s 03:00.0 | grep "DevCap"

setpci:读写配置空。

# 读取Vendor ID
setpci -s 03:00.0 VENDOR_ID.w

# 读取Device ID
setpci -s 03:00.0 DEVICE_ID.w

# 读取Command寄存。setpci -s 03:00.0 COMMAND.w

# 设置Command寄存器(使能内存空间和总线主控制setpci -s 03:00.0 COMMAND=0x0006

# 读取BAR0
setpci -s 03:00.0 BASE_ADDRESS_0.l

# 读取扩展配置空间(偏。x100。setpci -s 03:00.0 0x100.l

/sys/bus/pci/devices/:设备属。

# 查看设备目录
ls /sys/bus/pci/devices/0000:03:00.0/

# 查看Vendor/Device ID
cat /sys/bus/pci/devices/0000:03:00.0/vendor
cat /sys/bus/pci/devices/0000:03:00.0/device

# 查看IRQ。cat /sys/bus/pci/devices/0000:03:00.0/irq

# 查看NUMA节点
cat /sys/bus/pci/devices/0000:03:00.0/numa_node

# 查看当前链路速度和宽。cat /sys/bus/pci/devices/0000:03:00.0/current_link_speed
cat /sys/bus/pci/devices/0000:03:00.0/current_link_width

# 手动绑定/解绑驱动
echo "0000:03:00.0" > /sys/bus/pci/drivers/my_pcie_drv/bind
echo "0000:03:00.0" > /sys/bus/pci/drivers/my_pcie_drv/unbind

# 查看资源(BAR地址和大小)
cat /sys/bus/pci/devices/0000:03:00.0/resource

PCIe链路训练状态

PCIe设备上电后需要链路训练(Link Training)才能正常通信。如果链路训练失败,设备不可用。

# 查看链路训练状态lspci -vvv -s 03:00.0 | grep "LnkSta"

# 输出示例。# LnKSta: Speed 8GT/s, Width x4, TrErr- Train- SlotClk+ DLActive- BWMgmt- ABWMgmt-
# DLActive- 表示链路未激。
# 查看链路能力
lspci -vvv -s 03:00.0 | grep "LnkCap"

# 强制重新训练(调试用。setpci -s 03:00.0 CAP_EXP+10.w=0x0020  # 设置Retrain Link```

`DLActive`是关键标志——它。说明链路训练成功,设备可以通信。如果一直是0,检查硬件连接、时钟和复位信号。
## 九、常见问题
### Q1:probe不被调用。
驱动加载了但probe没执行,99%是`pci_device_id`匹配失败。
排查步骤。
```bash
# 1. 确认设备的VID/DID
lspci -n -s 03:00.0
# 输出。3:00.0 0200: 10ee:8030 (rev 01)

# 2. 检查驱动id_table是否包含这个VID/DID
modinfo my_pcie_drv.ko | grep alias
# 输出:alias:          pci:v000010EEd00008030sv*sd*bc*sc*i*

# 3. 检查设备是否已被其他驱动绑。ls -l /sys/bus/pci/devices/0000:03:00.0/driver

# 4. 手动解绑旧驱动再绑定新驱动echo "0000:03:00.0" > /sys/bus/pci/drivers/old_driver/unbind
echo "0000:03:00.0" > /sys/bus/pci/drivers/my_pcie_drv/bind

常见原因:VID/DID写错、子系统ID不匹配、设备已被内核内置驱动占用。

Q2:pci_iomap返回NULL。

BAR映射失败,几种可能:

// 1. 检查BAR是否存在
if (!(pci_resource_flags(pdev, bar) & IORESOURCE_MEM)) {
    dev_err(&pdev->dev, "BAR%d is not memory space\n", bar);
}

// 2. 检查设备是否已启用
if (!pci_device_is_present(pdev)) {
    dev_err(&pdev->dev, "device not present\n");
}

// 3. 检查Command寄存器的内存空间使能。u16 cmd;
pci_read_config_word(pdev, PCI_COMMAND, &cmd);
if (!(cmd & PCI_COMMAND_MEMORY)) {
    dev_err(&pdev->dev, "memory space not enabled\n");
}

用pcim_iomap_regions的话,它会自动启用内存空间位。手动pci_iomap前需要先调pci_enable_device和pci_request_regions。

Q3:MSI分配失败。

MSI/MSI-X分配失败通常是因为平台不支持或中断控制器限制。

/* 降级策略 */
int nvec;

/* 先尝试MSI-X */
nvec = pci_alloc_irq_vectors(pdev, 1, 8, PCI_IRQ_MSIX);
if (nvec < 0) {
    /* 降级到MSI */
    nvec = pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_MSI);
    if (nvec < 0) {
        /* 降级到INTx */
        nvec = pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_INTX);
        if (nvec < 0) {
            dev_err(&pdev->dev, "all interrupt types failed\n");
            return nvec;
        }
        dev_info(&pdev->dev, "using INTx\n");
    } else {
        dev_info(&pdev->dev, "using MSI\n");
    }
} else {
    dev_info(&pdev->dev, "using %d MSI-X vectors\n", nvec);
}

减少请求的向量数也可能成功——有些平台MSI-X向量有限,请。个可能失败但1个能成功能

Q4:DMA传输数据不对。

DMA数据错误排查看

/* 1. 检查DMA地址是否超出设备可访问范*/
u64 dma_addr = mydev->dma_handle;
dev_info(&pdev->dev, "dma_addr=%pad, mask=%llx\n",
         &dma_addr, pdev->dev.dma_mask);

/* 2. 流式DMA必须同步缓存 */
dma_addr_t map = dma_map_single(dev, buf, len, DMA_FROM_DEVICE);
/* 设备写完后,CPU读之*/
dma_sync_single_for_cpu(dev, map, len, DMA_FROM_DEVICE);
/* CPU写完后,设备读之*/
dma_sync_single_for_device(dev, map, len, DMA_TO_DEVICE);

/* 3. 检查是否启用了总线主控 */
u16 cmd;
pci_read_config_word(pdev, PCI_COMMAND, &cmd);
if (!(cmd & PCI_COMMAND_MASTER)) {
    dev_err(&pdev->dev, "bus master not enabled!\n");
    pci_set_master(pdev);  // 重新启用
}

最常见的原因:忘了调pci_set_master启用总线主控,或者流式DMA忘了同步缓存。

十、总结

PCIe驱动开发速查看

步骤 函数 说明
启用设备 pcim_enable_device devm版本,自动释放
映射BAR pcim_iomap_regions + pcim_iomap_table devm版本,一步完
设置DMA掩码 dma_set_mask_and_coherent 。4位后32
请求中断 pci_alloc_irq_vectors + devm_request_irq MSI-X > MSI > INTx
启用总线主控 pci_set_master DMA必需
MMIO读写 readl / writel 有内存屏
一致性DMA dma_alloc_coherent / dma_free_coherent 控制结构体
流式DMA dma_map_single / dma_unmap_single 大块数据
注册驱动 module_pci_driver 一行搞

PCIe驱动probe流程速查

flowchart TB A(["probe入口"]) --> B["pcim_enable_device"] B --> C["pcim_iomap_regions<br/>映射BAR"] C --> D["pcim_iomap_table<br/>获取虚拟地址"] D --> E["dma_set_mask_and_coherent"] E --> F["pci_alloc_irq_vectors"] F --> G["devm_request_irq"] G --> H["pci_set_master"] H --> I(["初始化完"]) style A fill:#E3F2FD style I fill:#E8F5E9 style F fill:#FFF8E1

PCIe驱动的套路很固定:启用设备→映射BAR→设DMA掩码→请求中断→启用总线主控。把这五步走对,剩下的就是填寄存器操作和业务逻辑。调试时善用lspci -vvv和/sys/bus/pci/devices/,大部分问题一看配置空间就明白。

本文首发于linuxros.cn,转载请注明出处。

版权声明

作者linuxROS
协议本作品采用 CC BY-NC-SA 4.0 许可协议:署名-非商业性使用-相同方式共享
关注欢迎关注微信公众号 linuxROS,获取更多机器人 / 嵌入式 / Linux 干货
返回首页