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

嵌入式linux PCIe驱动开发实战:从BAR映射到MSI-X中断

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

嵌入式linux PCIe驱动开发实战:从BAR映射到MSI-X中断

导读:PCIe驱动是嵌入式Linux开发的高阶技能。BAR映射到底怎么选?MSI和MSI-X哪个更适合?DMA一致性还是流式?本文从PCIe拓扑讲起,结合RK3506B平台实战,给出完整可运行驱动代码和避坑指南。


一、原理简析

PCIe 拓扑结构

PCIe采用点对点串行连接,核心组件包括Root Complex(RC)、Switch和Endpoint(EP):

flowchart TB subgraph Host["主机侧"] CPU["CPU"] MEM["内存"] end subgraph RC["Root Complex"] RC_CHIP["RC 芯片"] end subgraph Switch["Switch 交换机"] SW1["Port 1"] SW2["Port 2"] SW3["Port 3"] end subgraph EP["端点设备"] NIC["网卡 EP"] SSD["SSD EP"] GPU["显卡 EP"] end subgraph Bridge["PCIe-PCI 桥"] P2P["PCIe-PCI 桥"] end subgraph Legacy["传统 PCI"] OLD["旧设备"] end CPU --> RC_CHIP MEM --> RC_CHIP RC_CHIP --> SW1 RC_CHIP --> SW2 SW1 --> NIC SW2 --> SSD SW3 --> P2P P2P --> GPU P2P --> OLD style Host fill:#E3F2FD,stroke:#1976D2 style RC fill:#FFF8E1,stroke:#F57C00 style Switch fill:#E8F5E9,stroke:#388E3C style EP fill:#FFEBEE,stroke:#D32F2F style Bridge fill:#FFF8E1,stroke:#F57C00 style Legacy fill:#F3E5F5,stroke:#7B1FA2

RC是CPU和PCIe总线之间的桥梁,负责枚举设备、配置空间访问和中断路由。Switch扩展PCIe链路数量,EP是最终的功能设备(网卡、SSD、GPU等)。

PCIe 分层架构

flowchart TB APP["软件应用层"] --> DRV DRV["PCIe 设备驱动"] --> CORE CORE["PCI Core<br/>枚举/资源/中断管理"] --> HCD HCD["Host Controller Driver<br/>pcie-rockchip.c"] --> HW["PCIe 硬件控制器"] style APP fill:#E3F2FD,stroke:#1976D2 style DRV fill:#FFF8E1,stroke:#F57C00 style CORE fill:#E8F5E9,stroke:#388E3C style HCD fill:#FFEBEE,stroke:#D32F2F style HW fill:#F3E5F5,stroke:#7B1FA2

在Rockchip平台上,Host Controller Driver对应drivers/pci/controller/dwc/pcie-dw-rockchip.c,基于Synopsys DesignWare PCIe IP核。驱动开发者在设备驱动层工作,调用PCI Core提供的标准API即可。

PCIe vs 传统 PCI 对比

特性 PCI PCIe
拓扑 共享并行总线 点对点树状拓扑
带宽 133 MB/s(最大值) x1=250MB/s, x16=4GB/s(Gen1)
配置空间 256 字节 4KB(含扩展Capability)
中断机制 4条IRQ线共享(INTx#A\~D) MSI/MSI-X消息中断
热插拔 不支持 原生支持
传输方式 并行信号 串行差分信号(LVDS)

核心差异:PCIe用消息中断替代了传统的引脚中断,避免了中断共享带来的性能损失和处理复杂度。

PCIe 配置空间关键寄存器

偏移 大小 字段 说明
0x00 2B Vendor ID 厂商ID(PCI-SIG分配)
0x02 2B Device ID 设备ID(厂商定义)
0x04 2B Command 命令寄存器(使能MEM/IO/Master)
0x06 2B Status 状态寄存器(Capability支持等)
0x08 1B Revision ID 芯片修订版本号
0x09 3B Class Code 设备类别(如网卡=0x020000)
0x10 4B BAR0 基地址寄存器0(MEM/IO空间)
0x14 4B BAR1 基地址寄存器1
0x2C 2B Subsystem VID 子系统厂商ID
0x34 1B CapPtr Capabilities链表指针
0x3C 1B IRQ Line 传统INTx中断线

BAR空间分为MEM(内存映射)和IO(端口映射)两种。Linux驱动中优先使用MEM类型BAR,IO类型在现代PCIe设备中已基本淘汰。


二、中断与DMA机制对比

中断方式全景

flowchart TB A(["中断方式选择"]) --> B{"设备能力与场景"} B -->|"传统设备<br/>兼容优先"| C["INTx 传统中断<br/>引脚复用,共享IRQ"] B -->|"单向量<br/>简单场景"| D["MSI 中断<br/>1-32个向量"] B -->|"多向量<br/>高性能场景"| E["MSI-X 中断<br/>1-2048个向量"] C --> C1["无需额外配置<br/>中断自动分配"] D --> D1["pci_alloc_irq_vectors<br/>PCI_IRQ_MSI"] E --> E1["pci_alloc_irq_vectors<br/>PCI_IRQ_MSIX"] C1 --> F(["设备就绪"]) D1 --> F E1 --> F style A fill:#E3F2FD,stroke:#1976D2 style B fill:#FFF8E1,stroke:#F57C00 style C fill:#FFEBEE,stroke:#D32F2F style D fill:#E8F5E9,stroke:#388E3C style E fill:#F3E5F5,stroke:#7B1FA2 style C1 fill:#E3F2FD,stroke:#1976D2 style D1 fill:#E8F5E9,stroke:#388E3C style E1 fill:#F3E5F5,stroke:#7B1FA2 style F fill:#E8F5E9,stroke:#388E3C

INTx vs MSI vs MSI-X 对比

特性 INTx MSI MSI-X
向量数 1(多设备共享) 1-32 1-2048
中断共享 是(影响性能) 否(独享) 否(独享)
可屏蔽 否 是(PCI 3.0+) 是
CPU亲和性 不支持 有限 支持(每向量独立)
配置复杂度 无需配置 中等 较高
典型场景 老式设备、兼容模式 简单PCIe设备 高性能网卡/SSD/NVMe
内核API 自动fallback pci_alloc_irq_vectors pci_alloc_irq_vectors

选型建议:新驱动优先使用pci_alloc_irq_vectors配合PCI_IRQ_ALL_TYPES让内核自动选择最佳方式。内核会按MSI-X → MSI → INTx的优先级分配。

DMA 映射方式对比

PCIe驱动中DMA是核心数据通路,选择正确的映射方式直接影响性能和稳定性:

维度 一致性DMA 流式DMA
API dma_alloc_coherent dma_map_single
内存来源 自分配(内核DMA池) 已有buffer(如kmalloc)
Cache一致性 自动(硬件保证) 需手动dma_sync_*同步
生命周期 长期持有(驱动生命周期) 单次传输,用完即释放
分配开销 一次分配,较大 每次map/unmap
适用场景 描述符环、命令队列 网络包、磁盘数据块
CPU访问 直接读写 map时需注意方向

PCIe驱动推荐:设备描述符/队列用一致性DMA长期持有,单次数据传输用流式DMA配合scatter-gather。


三、驱动API速查总表

PCIe 驱动开发核心API对比

操作 核心API 释放函数 调用时机 关键注意
设备激活 pci_enable_device pci_disable_device probe最前面 激活前设备未通电
资源占用 pci_request_regions pci_release_regions enable之后 声明BAR所有权
BAR映射 pci_iomap pci_iounmap request之后 先检查IORESOURCE_MEM
DMA掩码 dma_set_mask_and_coherent 无需释放 DMA分配之前 32位=0xFFFFFFFF
一致性DMA dma_alloc_coherent dma_free_coherent 长期持有 自动Cache一致
流式DMA dma_map_single dma_unmap_single 单次传输 手动dma_sync_*
总线主控 pci_set_master 无需释放 DMA之前 忘记调用DMA不工作
中断向量 pci_alloc_irq_vectors pci_free_irq_vectors probe末尾 返回实际分配数量
中断注册 request_irq free_irq vectors之后 用pci_irq_vector取IRQ号
配置读写 pci_read/write_config_* 无需释放 任意时刻 _word/_dword按位宽选择

四、实战步骤

RK3506B 设备树配置

Rockchip平台需要先配置设备树使能PCIe控制器,以下为RK3506B典型配置:

&pcie_phy {
    status = "okay";
};

&pcie {
    reset-gpios = <&gpio4 RK_PA5 GPIO_ACTIVE_HIGH>;
    num-lanes = <1>;
    max-link-speed = <2>;
    vpcie3v3-supply = <&vcc3v3_pcie>;
    status = "okay";
};

关键属性说明:

来自 linuxros.cn · linuxROS
  • reset-gpios:设备复位引脚,常见问题源头
  • num-lanes:链路宽度(1/2/4),RK3506B支持x1
  • max-link-speed:1=Gen1(2.5GT/s), 2=Gen2(5GT/s)
  • vpcie3v3-supply:部分WiFi模块依赖3.3V供电

内核配置需确保以下选项:

CONFIG_PCI=y
CONFIG_PCI_MSI=y
CONFIG_PCIE_DW=y
CONFIG_PCIE_ROCKCHIP=y
CONFIG_PCIE_DW_ROCKCHIP=y

完整驱动代码

以下为完整的PCIe设备驱动,涵盖设备结构、probe/remove、中断处理和驱动注册:

#include <linux/module.h>
#include <linux/pci.h>
#include <linux/interrupt.h>
#include <linux/dma-mapping.h>

#define DRIVER_NAME "pcie_demo"
#define VENDOR_ID   0x1234
#define DEVICE_ID   0x5678

struct pcie_demo_dev {
    struct pci_dev *pdev;
    void __iomem *mmio_base;
    resource_size_t mmio_len;
    int irq;
    int num_vecs;

    dma_addr_t dma_handle;
    void *dma_buffer;
    size_t dma_size;
};

static const struct pci_device_id pcie_demo_ids[] = {
    { PCI_DEVICE(VENDOR_ID, DEVICE_ID) },
    { }
};
MODULE_DEVICE_TABLE(pci, pcie_demo_ids);

static irqreturn_t pcie_demo_irq(int irq, void *dev_id)
{
    struct pcie_demo_dev *dev = dev_id;
    u32 status;

    status = readl(dev->mmio_base + 0x04);

    if (!(status & 0x01))
        return IRQ_NONE;

    writel(0x01, dev->mmio_base + 0x04);

    pr_info("pcie_demo: interrupt handled\n");

    return IRQ_HANDLED;
}

static int pcie_demo_probe(struct pci_dev *pdev,
                          const struct pci_device_id *ent)
{
    struct pcie_demo_dev *dev;
    int ret;

    dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
    if (!dev)
        return -ENOMEM;

    dev->pdev = pdev;
    pci_set_drvdata(pdev, dev);

    /* 1. 激活设备 */
    ret = pci_enable_device(pdev);
    if (ret) {
        dev_err(&pdev->dev, "failed to enable device: %d\n", ret);
        return ret;
    }

    /* 2. 声明BAR资源 */
    ret = pci_request_regions(pdev, DRIVER_NAME);
    if (ret) {
        dev_err(&pdev->dev, "failed to request regions: %d\n", ret);
        goto err_disable;
    }

    /* 3. 映射BAR0为内核可访问的虚拟地址 */
    dev->mmio_len = pci_resource_len(pdev, 0);
    dev->mmio_base = pci_iomap(pdev, 0, dev->mmio_len);
    if (!dev->mmio_base) {
        ret = -ENOMEM;
        goto err_release;
    }

    /* 4. 设置DMA寻址能力(RK3506B为32位平台) */
    ret = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(32));
    if (ret) {
        dev_err(&pdev->dev, "DMA mask set failed: %d\n", ret);
        goto err_unmap;
    }

    /* 5. 分配一致性DMA缓冲区 */
    dev->dma_size = 4096;
    dev->dma_buffer = dma_alloc_coherent(&pdev->dev, dev->dma_size,
                                          &dev->dma_handle, GFP_KERNEL);
    if (!dev->dma_buffer) {
        ret = -ENOMEM;
        goto err_unmap;
    }

    /* 6. 开启总线主控(DMA前提) */
    pci_set_master(pdev);

    /* 7. 分配MSI-X中断向量 */
    ret = pci_alloc_irq_vectors(pdev, 1, 4,
                                 PCI_IRQ_MSIX | PCI_IRQ_AFFINITY);
    if (ret < 0) {
        dev_err(&pdev->dev, "failed to alloc IRQ vectors: %d\n", ret);
        goto err_dma;
    }
    dev->num_vecs = ret;

    /* 8. 注册中断处理函数 */
    dev->irq = pci_irq_vector(pdev, 0);
    ret = request_irq(dev->irq, pcie_demo_irq, 0, DRIVER_NAME, dev);
    if (ret) {
        dev_err(&pdev->dev, "failed to request IRQ: %d\n", ret);
        goto err_irq;
    }

    dev_info(&pdev->dev, "PCIe device probed, vectors=%d\n",
             dev->num_vecs);
    return 0;

err_irq:
    pci_free_irq_vectors(pdev);
err_dma:
    dma_free_coherent(&pdev->dev, dev->dma_size,
                       dev->dma_buffer, dev->dma_handle);
err_unmap:
    pci_iounmap(pdev, dev->mmio_base);
err_release:
    pci_release_regions(pdev);
err_disable:
    pci_disable_device(pdev);
    return ret;
}

static void pcie_demo_remove(struct pci_dev *pdev)
{
    struct pcie_demo_dev *dev = pci_get_drvdata(pdev);

    /* 释放顺序与probe相反 */
    free_irq(pci_irq_vector(pdev, 0), dev);
    pci_free_irq_vectors(pdev);

    dma_free_coherent(&pdev->dev, dev->dma_size,
                      dev->dma_buffer, dev->dma_handle);

    pci_iounmap(pdev, dev->mmio_base);
    pci_release_regions(pdev);
    pci_disable_device(pdev);

    dev_info(&pdev->dev, "PCIe device removed\n");
}

static struct pci_driver pcie_demo_driver = {
    .name = DRIVER_NAME,
    .id_table = pcie_demo_ids,
    .probe = pcie_demo_probe,
    .remove = pcie_demo_remove,
};

module_pci_driver(pcie_demo_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Driver Developer");
MODULE_DESCRIPTION("PCIe Demo Driver");

probe 8步走:enable → request → iomap → dma_mask → dma_alloc → set_master → alloc_irq → request_irq。务必按此顺序,否则资源依赖关系会出错。

MSI-X 多向量配置

当设备需要多条独立中断通道时(如分离数据中断和错误中断):

#define MSIX_VECTORS 4

static int pcie_demo_enable_msix(struct pci_dev *pdev,
                                  struct pcie_demo_dev *dev)
{
    int i, ret;

    ret = pci_alloc_irq_vectors(pdev, 1, MSIX_VECTORS,
                                 PCI_IRQ_MSIX | PCI_IRQ_AFFINITY);
    if (ret < 0) {
        dev_err(&pdev->dev, "failed to enable MSI-X: %d\n", ret);
        return ret;
    }

    for (i = 0; i < ret; i++) {
        int irq = pci_irq_vector(pdev, i);
        ret = request_irq(irq, pcie_demo_msix_irq, 0,
                          dev_name(&pdev->dev), dev);
        if (ret) {
            dev_err(&pdev->dev, "failed to request IRQ %d\n", irq);
            goto err_free;
        }
    }

    return 0;

err_free:
    while (--i >= 0)
        free_irq(pci_irq_vector(pdev, i), dev);
    pci_free_irq_vectors(pdev);
    return ret;
}

PCI_IRQ_AFFINITY让内核自动将中断分散到不同CPU核心,对多核平台性能提升明显。

调试命令速查

# 列出所有PCIe设备(含vendor/device ID)
lspci -vv

# 查看特定设备BAR信息
lspci -v -s 01:00.0

# 查看配置空间裸数据
lspci -xxx -s 01:00.0

# 查看PCIe拓扑树
lspci -t

# 查看驱动绑定关系
ls -l /sys/bus/pci/drivers/

# 手动解绑/绑定设备(调试用)
echo "0000:01:00.0" > /sys/bus/pci/drivers/pcie_demo/unbind
echo "0000:01:00.0" > /sys/bus/pci/drivers/pcie_demo/bind

# 查看MSI中断分配
cat /proc/interrupts | grep -i pci

# 查看内核日志
dmesg | grep pcie

# Rockchip特有:查看PCIe链路状态
cat /sys/kernel/debug/pcie/*/link_status

# Rockchip特有:设备重扫描(热复位后使用)
echo 1 > /sys/bus/pci/rescan

五、常见问题解决

probe阶段

现象 根因 解决方案
probe未被调用 pci_device_id与设备VID/PID不匹配 lspci -n确认设备ID,核对id_table
pci_enable_device失败 设备电源未就绪或复位未释放 检查reset-gpios和vpcie3v3-supply配置
pci_iomap返回NULL BAR0不是MEM类型 先用pci_resource_flags(pdev, 0) & IORESOURCE_MEM检查
设备未出现在lspci中 PCIe链路训练失败 检查dmesg \| grep "PCIe link",确认PHY初始化

DMA相关

现象 根因 解决方案
DMA不工作 忘记调用pci_set_master 在pci_alloc_irq_vectors之前调用
DMA数据错误 使用了vmalloc分配的buffer做流式映射 改用kmalloc或dma_alloc_coherent分配源buffer
dma_alloc_coherent失败 DMA区域耗尽或mask太小 降低DMA缓冲区大小,或检查DMA zone配置

中断相关

现象 根因 解决方案
中断风暴/CPU满载 中断未清标志或返回IRQ_HANDLED误判 先读状态寄存器确认是否本设备中断,非本设备返回IRQ_NONE
pci_alloc_irq_vectors返回-22 内核CONFIG_PCI_MSI未使能 检查.config中CONFIG_PCI_MSI=y
MSI-X分配数量少于预期 系统连续向量不足 减少max_vecs或改用MSI模式fallback

资源释放顺序

/* 错误!释放顺序与申请顺序相同 */
pci_disable_device(pdev);     // 还有中断未释放!
free_irq(...);

/* 正确!释放顺序是申请的逆序 */
free_irq(...);                // 1. 先释放中断
pci_free_irq_vectors(pdev);   // 2. 释放中断向量
dma_free_coherent(...);       // 3. 释放DMA
pci_iounmap(pdev, ...);       // 4. 解除BAR映射
pci_release_regions(pdev);    // 5. 释放区域
pci_disable_device(pdev);     // 6. 最后禁用设备

铁律:remove中的释放顺序必须是probe中申请顺序的严格逆序,否则可能导致内核崩溃。


六、总结

驱动开发决策流程

flowchart TB A(["需要开发PCIe驱动"]) --> B{"设备类型"} B -->|"标准Endpoint<br/>网卡/SSD/FPGA"| C{"中断需求"} B -->|"自定义桥接<br/>PCIe-SPI/I2C"| D["参考标准驱动模板<br/>修改设备ID表即可"] C -->|"单中断<br/>简单设备"| E["MSI模式<br/>1个向量"] C -->|"多中断<br/>高性能设备"| F["MSI-X模式<br/>多向量+CPU亲和"] D --> G{"需要DMA?"} E --> G F --> G G -->|"是"| H["dma_alloc_coherent<br/>+ pci_set_master"] G -->|"否"| I["跳过DMA步骤"] H --> J(["注册module_pci_driver"]) I --> J style A fill:#E3F2FD,stroke:#1976D2 style B fill:#FFF8E1,stroke:#F57C00 style C fill:#FFF8E1,stroke:#F57C00 style D fill:#E8F5E9,stroke:#388E3C style E fill:#E8F5E9,stroke:#388E3C style F fill:#F3E5F5,stroke:#7B1FA2 style G fill:#FFF8E1,stroke:#F57C00 style H fill:#E8F5E9,stroke:#388E3C style I fill:#FFEBEE,stroke:#D32F2F style J fill:#E8F5E9,stroke:#388E3C

API速查

步骤 API 要点
驱动注册 module_pci_driver 一行搞定init/exit
设备激活 pci_enable_device probe首步
BAR映射 pci_iomap 先检查IORESOURCE_MEM
DMA掩码 dma_set_mask_and_coherent 32位=DMA_BIT_MASK(32)
DMA分配 dma_alloc_coherent 自动Cache一致
总线主控 pci_set_master DMA前必须调用
中断分配 pci_alloc_irq_vectors 返回实际分配数量
获取IRQ号 pci_irq_vector(pdev, idx) 传给request_irq

经验总结

pci_alloc_irq_vectors是统一入口——弃用pci_enable_msi()等旧API,一个函数搞定MSI/MSI-X/INTx,让内核自动选择最佳模式。

pci_set_master是DMA的开关——忘记它DMA静默失败,这是最隐蔽也最常见的坑。

释放顺序=申请逆序——remove函数中free_irq在前,pci_disable_device在最后,顺序反了系统崩。

BAR映射前先判类型——不是所有BAR都是MEM映射,IO类型的BAR用pci_iomap会出问题,先用pci_resource_flags检查。

Rockchip上注意电源时序——vpcie3v3-supply和reset-gpios配置不正确是RK平台PCIe不认设备的第一大原因。

版权声明

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