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。
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流程。
注意上面大量使用了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变体来访问寄存器——那些是给大块内存用的。
完整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流程速查
PCIe驱动的套路很固定:启用设备→映射BAR→设DMA掩码→请求中断→启用总线主控。把这五步走对,剩下的就是填寄存器操作和业务逻辑。调试时善用lspci -vvv和/sys/bus/pci/devices/,大部分问题一看配置空间就明白。
本文首发于linuxros.cn,转载请注明出处。