34、Linux 内核中 PCI 设备驱动开发全解析

Linux 内核中 PCI 设备驱动开发全解析

1. 核心功能与数据结构概述

在 Linux 内核里,PCI 核心部分( drivers/pci/probe.c )负责创建和初始化系统中总线、设备以及桥的数据结构树,处理总线和设备编号,创建设备条目并提供 proc/sysfs 信息。同时,它还为 PCI BIOS 和从设备(端点)驱动提供服务,并且在硬件支持的情况下,可提供热插拔支持。此外,它还提供 MSI 中断处理框架和 PCI Express 端口总线支持,这些功能极大地促进了 Linux 内核中设备驱动的开发。

PCI 设备驱动开发基于两个主要的数据结构: struct pci_dev 和 struct pci_driver 。 struct pci_dev 代表内核中的一个 PCI 设备,而 struct pci_driver 代表一个 PCI 驱动。

2. struct pci_dev 结构详解

struct pci_dev 用于内核实例化系统中的每个 PCI 设备,它描述设备并存储其部分状态参数。该结构定义在 include/linux/pci.h 中,部分代码如下:

struct pci_dev {
  struct pci_bus    *bus; /* Bus this device is on */
  struct pci_bus *subordinate; /* Bus this device bridges to */
    struct proc_dir_entry *procent;
    struct pci_slot *slot;
    unsigned short vendor;
    unsigned short device;
    unsigned short subsystem_vendor;
    unsigned short subsystem_device;
    unsigned int class;
   /* 3 bytes: (base,sub,prog-if) */
   u8 revision;     /* PCI revision, low byte of class word */
   u8 hdr_type; /* PCI header type (multi'  flag masked out) */
   u8 pin;                /* Interrupt pin this device uses */
   struct pci_driver *driver; /* Driver bound to this device */
   u64 dma_mask;
   struct device_dma_parameters dma_parms;
    struct device dev;
    int cfg_size;
    unsigned int irq;
    ...
    unsigned int no_msi:1; /* May not use MSI */
    unsigned int no_64bit_msi:1; /* May only use 32-bit MSIs */
    unsigned int msi_enabled:1;
    unsigned int msix_enabled:1;     atomic_t enable_cnt;
    ...
};

下面对部分元素进行详细解释:
| 元素 | 含义 |
| ---- | ---- |
| procent | 设备在 /proc/bus/pci/ 中的条目 |
| slot | 设备所在的物理插槽 |
| vendor | 设备制造商的供应商 ID,由 PCI 特殊兴趣小组维护全球注册表 |
| device | 探测到设备时用于标识该特定设备的 ID,依赖于供应商 |
| subsystem_vendor 和 subsystem_device | 指定 PCI 子系统供应商和子系统设备 ID,用于进一步识别设备 |
| class | 标识设备所属的类别,存储在 16 位寄存器中,高 8 位标识基类或组 |
| pin | 在传统基于 INTx 的中断情况下,设备使用的中断引脚 |
| driver | 与该设备关联的驱动 |
| dev | 此 PCI 设备的底层设备结构 |
| cfg_size | 配置空间的大小 |
| irq | 该字段值得重点关注。设备启动时,MSI(-X) 模式未启用,直到通过 pci_alloc_irq_vectors() API 显式启用。在不同中断模式下,其值或用法会发生变化:
- MSI 中断模式 :成功调用 pci_alloc_irq_vectors() 并设置 PCI_IRQ_MSI 标志后,该字段的预分配值将被新的 MSI 向量替换,向量 X 对应的 IRQ 号为 pci_dev->irq + X 。
- MSI - X 中断模式 :成功调用 pci_alloc_irq_vectors() 并设置 PCI_IRQ_MSIX 标志后,该字段的预分配值不变,但在该模式下 irq 无效。 |
| msi_enabled | 保存 MSI IRQ 模式的启用状态 |
| msix_enabled | 保存 MSI - X IRQ 模式的启用状态 |
| enable_cnt | 记录 pci_enable_device() 被调用的次数,确保在所有调用者都调用 pci_disable_device() 后才真正禁用设备 |

3. struct pci_device_id 结构详解

struct pci_device_id 用于识别设备,其定义如下:

struct pci_device_id {
    u32 vendor, device;
    u32 subvendor, subdevice;
    u32 class, class_mask;
    kernel_ulong_t driver_data;
};

各元素含义如下:
- vendor 和 device :分别代表设备的供应商 ID 和设备 ID,二者配对形成设备的唯一 32 位标识符,驱动依靠此标识符识别其设备。
- subvendor 和 subdevice :代表子系统 ID。
- class 和 class_mask :与类相关的 PCI 驱动使用,用于处理给定类别的所有设备。对于此类驱动, vendor 和 device 应设置为 PCI_ANY_ID 。
- driver_data :驱动的私有数据,不用于识别设备,而是用于传递不同数据以区分设备。

有三个宏可用于创建 struct pci_device_id 的特定实例:
- PCI_DEVICE :用于描述特定的 PCI 设备,通过给定供应商和设备 ID 匹配特定设备,子供应商、子设备和类相关字段设置为 PCI_ANY_ID 。
- PCI_DEVICE_CLASS :用于描述特定的 PCI 设备类,通过给定类和类掩码匹配特定类,供应商、设备、子供应商和子设备字段设置为 PCI_ANY_ID 。例如: PCI_DEVICE_CLASS(PCI_CLASS_STORAGE_EX PRESS, 0xffffff) 可匹配 NVMe 设备类。
- PCI_DEVICE_SUB :用于描述带有子系统的特定 PCI 设备,通过给定子系统信息匹配特定设备。

每个驱动支持的设备或类应放入同一个数组中,示例如下:

static const struct pci_device_id
bt8xxgpio_pci_tbl[] = {
  { PCI_DEVICE(PCI_VENDOR_ID_BROOKTREE, PCI_DEVICE_ID_BT848) },
  { PCI_DEVICE(PCI_VENDOR_ID_BROOKTREE, PCI_DEVICE_ID_BT849) },
  { PCI_DEVICE(PCI_VENDOR_ID_BROOKTREE, PCI_DEVICE_ID_BT878) },
  { PCI_DEVICE(PCI_VENDOR_ID_BROOKTREE, PCI_DEVICE_ID_BT879) },
  { 0, },
};

使用 MODULE_DEVICE_TABLE 宏将 pci_device_id 结构导出到用户空间,示例如下:

MODULE_DEVICE_TABLE(pci, bt8xxgpio_pci_tbl);

此宏会创建一个包含给定信息的自定义部分,编译时,构建过程会从驱动中提取该信息并构建一个人类可读的表 modules.alias ,位于 /lib/modules/<kernel_version>/ 目录下。当内核通知热插拔系统有新设备可用时,热插拔系统会参考 modules.alias 文件来查找要加载的合适驱动。

4. struct pci_driver 结构详解

struct pci_driver 代表一个 PCI 设备驱动的实例,每个 PCI 驱动都必须创建并填充该结构才能在核中注册。其定义如下:

struct pci_driver {
   const char *name;
   const struct pci_device_id *id_table;
   int (*probe)(struct pci_dev *dev, const struct pci_device_id *id);
   void (*remove)(struct pci_dev *dev);
   int (*suspend)(struct pci_dev *dev, pm_message_t state);
   int (*resume)(struct pci_dev *dev); /* Device woken up */
   void (*shutdown) (struct pci_dev *dev);
   ...
};

部分字段含义如下:
| 字段 | 含义 |
| ---- | ---- |
| name | 驱动的名称,必须在内核的所有 PCI 驱动中唯一,通常设置为驱动模块的名称。若同一子系统总线中已有同名驱动注册,注册将失败。 |
| id_table | 指向前面描述的 struct pci_device_id 表,该表必须非空,才能调用 probe 函数。 |
| probe | 驱动的探测函数指针,当 PCI 设备与驱动的 id_table 中的条目匹配时,由 PCI 核心调用。若成功初始化设备,该方法应返回 0,否则返回负错误码。 |
| remove | 当该驱动处理的设备从系统中移除(从总线消失)或驱动从内核卸载时,由 PCI 核心调用。 |
| suspend 、 resume 和 shutdown | 可选但推荐的电源管理函数,在这些回调中,可使用 PCI 相关的电源管理辅助函数,如 pci_save_state() 、 pci_restore_state() 等。这些回调分别在设备挂起、恢复和正常关闭时由 PCI 核心调用。 |

以下是一个 PCI 驱动结构初始化的示例:

static struct pci_driver bt8xxgpio_pci_driver =
{
    .name = " bt8xxgpio" ,
    .id_table = bt8xxgpio_pci_tbl,
    .probe = bt8xxgpio_probe,
    .remove = bt8xxgpio_remove,
    .suspend = bt8xxgpio_suspend,
    .resume = bt8xxgpio_resume,
};
5. 注册和注销 PCI 驱动

注册 PCI 驱动需要调用 pci_register_driver() ,并传入之前设置好的 struct pci_driver 结构的指针。这通常在模块的初始化方法中完成,示例如下:

static int init pci_foo_init(void)
{
    return pci_register_driver(&bt8xxgpio_pci_driver);
}

pci_register_driver() 注册成功返回 0,否则返回负错误码。

在模块卸载时,需要注销 struct pci_driver ,以防止系统尝试使用已不存在的模块对应的驱动。这通过调用 pci_unregister_driver() 完成,通常在模块退出函数中进行,示例如下:

static void exit pci_foo_exit(void)
{
    pci_unregister_driver(&bt8xxgpio_pci_driver);
}

为了简化操作,PCI 核心提供了 module_pci_driver() 宏,可自动处理注册和注销,示例如下:

module_pci_driver(bt8xxgpio_pci_driver);
6. PCI 驱动结构概述及设备启用

编写 PCI 设备驱动时,需要遵循一定的步骤,部分步骤有预定义的顺序。首先是启用设备,在对 PCI 设备进行任何操作(即使只是读取其配置寄存器)之前,必须显式启用该设备,可使用内核提供的 pci_enable_device() 函数,其原型如下:

int pci_enable_device(struct pci_dev *dev)

由于 pci_enable_device() 可能失败,需要检查其返回值,示例如下:

int err;
err = pci_enable_device(pci_dev);
if (err) {
    printk(KERN_ERR " foo_dev: Can' t enable device.\n" );
    return err;
}

pci_enable_device() 会初始化内存映射和 I/O BARs,但你可能只想初始化其中一个,可使用 pci_enable_device_mem() 仅初始化内存映射 BARs,或使用 pci_enable_device_io() 仅初始化 I/O BARs。

当需要禁用 PCI 设备时,使用 pci_disable_device() 方法,其原型如下:

void pci_disable_device(struct pci_dev *dev)

pci_disable_device() 还会在设备处于活动状态时禁用总线主控。但设备只有在所有调用 pci_enable_device() (或其变体)的调用者都调用 pci_disable_device() 后才会真正被禁用。

7. 总线主控能力

PCI 设备成为总线主控时,可发起总线上的事务。设备启用后,可通过设置相应配置寄存器中的总线主控位来启用 DMA,PCI 核心提供了 pci_set_master() 函数,其原型如下:

void pci_set_master(struct pci_dev *dev)

同时, pci_clear_master() 函数可通过清除总线主控位来禁用 DMA,其原型如下:

void pci_clear_master(struct pci_dev *dev)

如果设备要执行 DMA 操作,必须调用 pci_set_master() 。

8. 访问配置寄存器

设备绑定到驱动并启用后,通常会访问设备的内存空间,首先访问的往往是配置空间。传统 PCI 和 PCI - X 模式 1 设备有 256 字节的配置空间,PCI - X 模式 2 和 PCIe 设备有 4096 字节的配置空间。内核提供了标准和专用的 API 用于读写不同大小的数据配置空间,读取配置空间数据的原语如下:

int pci_read_config_byte(struct pci_dev *dev, int where, u8 *val);
int pci_read_config_word(struct pci_dev *dev, int where, u16 *val);
int pci_read_config_dword(struct pci_dev *dev, int where, u32 *val);

写入配置空间数据的原语如下:

int pci_write_config_byte(struct pci_dev *dev, int where, u8 val);
int pci_write_config_word(struct pci_dev *dev, int where, u16 val);
int pci_write_config_dword(struct pci_dev *dev, int where, u32 val);

其中, where 参数是配置空间起始处的字节偏移量。内核中定义了一些常用的配置偏移量的符号化宏,例如:

#define PCI_VENDOR_ID 0x00 /* 16 bits */
#define PCI_DEVICE_ID 0x02 /* 16 bits */
#define PCI_STATUS 0x06 /* 16 bits */
#define PCI_CLASS_REVISION  0x08  /* High 24 bits are class, low 8 revision */
#define    PCI_REVISION_ID   0x08  /* Revision ID */
#define    PCI_CLASS_PROG    0x09  /* Reg. Level Programming Interface */
#define    PCI_CLASS_DEVICE  0x0a  /* Device class */
...

获取给定 PCI 设备的修订 ID 的示例如下:

static unsigned char foo_get_revision(struct pci_dev *dev)
{
    u8 revision;
    pci_read_config_byte(dev, PCI_REVISION_ID, &revision);
    return revision;
}

需要注意的是,由于 PCI 设备以小端格式存储和读取数据,读取原语(字和双字变体)会将读取的数据转换为 CPU 的本地字节序,写入原语(字和双字变体)会在将数据写入设备之前将数据从 CPU 的本地字节序转换为小端格式。

9. 访问内存映射 I/O 资源

内存寄存器用于各种操作,如突发事务,它们对应于设备的内存基地址寄存器(BARs)。每个 BAR 会从系统地址空间分配一个内存区域,对这些区域的访问会被重定向到相应的设备。在 Linux 内核的内存映射 I/O 中,通常需要先请求一个内存区域,再创建映射,可使用 request_mem_region() 和 ioremap() 原语,其原型如下:

struct resource *request_mem_region (unsigned long start, unsigned long n, const char *name)
void iomem *ioremap(unsigned long phys_addr, unsigned long size);

request_mem_region() 是一个纯预留机制,不进行映射,依赖于其他驱动的配合,避免内存区域重叠。要获取给定 BAR 的信息,可使用 pci_resource_start() 、 pci_resource_len() 或 pci_resource_end() 函数,其原型如下:

unsigned long pci_resource_start (struct pci_dev *dev, int bar);
unsigned long pci_resource_len (struct pci_dev *dev, int bar);
unsigned long pci_resource_end (struct pci_dev *dev, int bar);
unsigned long pci_resource_flags (struct pci_dev *dev, int bar);

以下是映射给定设备的 bar0 的示例代码:

unsigned long bar0_base;
unsigned long bar0_size;
void iomem *bar0_map_membase;
/* Get the PCI Base Address Registers */
bar0_base = pci_resource_start(pdev, 0);
bar0_size = pci_resource_len(pdev, 0);
/* * think about managed version and use * devm_request_mem_regions() */
if (request_mem_region(bar0_base, bar0_size, " bar0-mapping" )) {
    /* there is an error */
    goto err_disable;
}
/* Think about managed version and use devm_ioremap instead */
bar0_map_membase = ioremap(bar0_base, bar0_size);
if (!bar0_map_membase) {
    /* error */
    goto err_iomap;
}
/* Now we can use ioread32()/iowrite32() on bar0_map_membase*/

为了简化操作,PCI 框架提供了更多相关函数,如 pci_request_region() 、 pci_request_regions() 、 pci_iomap() 等,使用这些辅助函数重写映射 BAR1 的代码示例如下:

#define DRV_NAME " foo-drv"
void iomem *bar1_map_membase;
int err;
err = pci_request_regions(pci_dev, DRV_NAME);
if (err) {
    /* an error occured */
    goto error;
}
bar1_map_membase = pci_iomap(pdev, 1, 0);
if (!bar1_map_membase) {
    /* an error occured */
    goto err_iomap;
}

内存区域被声明和映射后,可使用 ioread*() 和 iowrite*() API 访问映射的寄存器。

10. 访问 I/O 端口资源

访问 I/O 端口需要经过与 I/O 内存类似的步骤,包括请求 I/O 区域、映射 I/O 区域(可选)和访问 I/O 区域。 pci_requestregion*() 原语可处理 I/O 端口和 I/O 内存,它根据资源标志调用相应的底层辅助函数,示例如下:

unsigned long flags = pci_resource_flags(pci_dev, bar);
if (flags & IORESOURCE_IO)
    /* using request_region() */
else if (flags & IORESOURCE_MEM)
    /* using request_mem_region() */

因此,无论资源是 I/O 内存还是 I/O 端口,都可以安全地使用 pci_request_regions() 或其单 BAR 变体 pci_request_region() 。

pci_iomap*() 原语也能处理 I/O 端口或 I/O 内存,它们同样依赖资源标志调用相应的辅助函数创建映射。基于资源类型,底层映射函数对于 I/O 内存是 ioremap() ,对于 I/O 端口是 __pci_ioport_map() 。

访问 I/O 端口时,使用的 API 不同,内核提供的访问 I/O 端口的函数如下:

u8 inb(unsigned long port);
u16 inw(unsigned long port);
u32 inl(unsigned long port);
void outb(u8 value, unsigned long port);
void outw(u16 value, unsigned long port);
void outl(u32 value, unsigned long port);

in*() 系列函数从端口位置分别读取 1、2 或 4 个字节的数据, out*() 系列函数将 1、2 或 4 个字节的数据写入端口位置。

11. 处理中断

需要为设备服务中断的驱动首先需要请求这些中断,通常在 probe() 方法中进行。对于传统和非 MSI IRQ,驱动可以直接使用 pci_dev->irq 字段,该字段在设备探测时预先分配。

为了更通用的处理方式,建议使用 pci_alloc_irq_vectors() API,其定义如下:

int pci_alloc_irq_vectors(struct pci_dev *dev,
                          unsigned int min_vecs,
                          unsigned int max_vecs,
                          unsigned int flags);

该函数成功时返回分配的向量数量(可能小于 max_vecs ),失败时返回负错误码。分配的向量数量至少为 min_vecs ,如果设备可用的中断向量少于 min_vecs ,函数将返回 -ENOSPC 。

pci_alloc_irq_vectors() 的优点是可以处理传统中断、MSI 或 MSI - X 中断,根据 flags 参数,驱动可以指示 PCI 层为设备设置 MSI 或 MSI - X 功能。可能的标志定义在 include/linux/pci.h 中:
- PCI_IRQ_LEGACY :单个传统 IRQ 向量。
- PCI_IRQ_MSI :成功时, pci_dev->msi_enabled 设置为 1。
- PCI_IRQ_MSIX :成功时, pci_dev->msix_enabled 设置为 1。
- PCI_IRQ_ALL_TYPES :尝试按固定顺序分配上述任何一种中断,先尝试 MSI - X 模式,成功则立即返回;若失败,再尝试 MSI 模式;若两者都失败,则使用传统模式作为后备。驱动可通过 pci_dev->msi_enabled 和 pci_dev->msix_enabled 确定哪种模式成功。
- PCI_IRQ_AFFINITY :允许自动分配中断亲和性。

获取要传递给 request_irq() 和 free_irq() 的 Linux IRQ 编号的函数如下:

int pci_irq_vector(struct pci_dev *dev, unsigned int nr);

该函数根据不同的中断模式返回相应的 IRQ 编号,具体逻辑如下:

int pci_irq_vector(struct pci_dev *dev, unsigned int nr)
{
    if (dev->msix_enabled) {
        struct msi_desc *entry;
        int i = 0;
        for_each_pci_msi_entry(entry, dev) {
            if (i == nr)
                return entry->irq;
            i++;
        }
        WARN_ON_ONCE(1);
        return -EINVAL;
    }
    if (dev->msi_enabled) {
        struct msi_desc *entry = first_pci_msi_entry(dev);
        if (WARN_ON_ONCE(nr >= entry->nvec_used))
            return -EINVAL;
    } else {
        if (WARN_ON_ONCE(nr > 0))
            return -EINVAL;
    }
    return dev->irq + nr;
}

部分设备可能不支持传统线路中断,此时驱动可以指定只接受 MSI 或 MSI - X 中断,示例如下:

nvec = pci_alloc_irq_vectors(pdev, 1, nvec, PCI_IRQ_MSI | PCI_IRQ_MSIX);
if (nvec < 0)
    goto out_err;

需要注意的是,MSI/MSI - X 和传统中断是互斥的,参考设计默认支持传统中断。一旦设备启用了 MSI 或 MSI - X 中断,它将保持该模式,直到再次禁用。

12. 传统 INTx IRQ 分配

PCI 总线类型的探测方法 pci_device_probe() 会在每次有新的 PCI 设备添加到总线或新的 PCI 驱动注册到系统时被调用。该函数会调用 pci_assign_irq(pci_dev) 和 pcibios_alloc_irq(pci_dev) 为 PCI 设备分配 IRQ。

pci_assign_irq() 会读取 PCI 设备连接的引脚,示例如下:

u8 pin;
pci_read_config_byte(dev, PCI_INTERRUPT_PIN, &pin);
/* (1=INTA, 2=INTB, 3=INTD, 4=INTD) */

后续步骤依赖于 PCI 主机桥,其驱动应提供一些回调函数,包括 .map_irq ,用于根据设备插槽和读取的引脚创建 IRQ 映射, pci_assign_irq() 函数的部分代码如下:

void pci_assign_irq(struct pci_dev *dev)
{
    int irq = 0;
    u8 pin;
    struct pci_host_bridge *hbrg = pci_find_host_bridge(dev->bus);
    if (!(hbrg->map_irq)) {
        pci_dbg(dev, " runtime IRQ mapping not provided by arch\n" );
        return;
    }
    pci_read_config_byte(dev, PCI_INTERRUPT_PIN, &pin);
    if (pin) {
        ...
        irq = (*(hbrg->map_irq))(dev, slot, pin);
        if (irq == -1)
            irq = 0;
    }
    dev->irq = irq;
    pci_dbg(dev, " assign IRQ: got %d\n" , dev->irq);
    /* Always tell the device, so the driver knows what is the real IRQ to use; the device does not use it. */
    pci_write_config_byte(dev, PCI_INTERRUPT_LINE, irq);
}

pcibios_alloc_irq() 是一个弱且空的函数,仅由 AArch64 架构重载,它依赖 ACPI(如果启用)来处理分配的 IRQ。 pci_device_probe() 的最终代码如下:

static int pci_device_probe(struct device *dev)
{
    int error;
    struct pci_dev *pci_dev = to_pci_dev(dev);
    struct pci_driver *drv = to_pci_driver(dev->driver);
    pci_assign_irq(pci_dev);
    error = pcibios_alloc_irq(pci_dev);
    if (error < 0)
        return error;
    pci_dev_get(pci_dev);
    if (pci_device_can_probe(pci_dev)) {
        error = pci_device_probe(drv, pci_dev);
        if (error) {
            pcibios_free_irq(pci_dev);
            pci_dev_put(pci_dev);
        }
    }
    return error;
}

需要注意的是, PCI_INTERRUPT_LINE 中的 IRQ 值在调用 pci_enable_device() 之前是无效的,并且外设驱动不应修改 PCI_INTERRUPT_LINE ,因为它反映了 PCI 中断与中断控制器的连接方式,不可更改。

13. 模拟 INTx IRQ 交换

大多数处于传统 INTx 模式的 PCIe 设备默认使用本地 INTA “虚拟线输出”,许多通过 PCIe/PCI 桥连接的物理 PCI 设备也是如此。这会导致操作系统中所有外设共享 INTA 输入,所有设备共享同一 IRQ 线,可能会引发问题。解决方案是“虚拟线 INTx IRQ 交换”,在 pci_device_probe() 函数中调用的 pci_assign_irq() 函数中包含了一些交换操作来解决这个问题。

14. 锁定考虑

许多设备驱动会为每个设备设置一个自旋锁,并在中断处理程序中获取该锁。在基于 Linux 的系统中,由于中断保证是非重入的,处理基于引脚的中断或单个 MSI 时,不需要禁用中断。但如果设备使用多个中断,驱动在持有锁时必须禁用中断,以防止死锁。可使用 spin_lock_irqsave() 或 spin_lock_irq() 等锁定原语,这些原语会禁用本地中断并获取锁。

15. 旧版 API 说明

一些旧的驱动仍然使用现已弃用的 MSI 或 MSI - X API,如 pci_enable_msi() 、 pci_disable_msi() 等,新代码不应使用这些 API。以下是一个尝试使用 MSI 并在 MSI 不可用时回退到传统中断模式的代码示例:

int err;
/* Try using MSI interrupts */
err = pci_enable_msi(pci_dev);
if (err)
    goto intx;
err = devm_request_irq(&pci_dev->dev, pci_dev->irq, my_msi_handler, 0, " foo-msi" , priv);
if (err) {
    pci_disable_msi(pci_dev);
    goto intx;
}
return 0;
/* Try using legacy interrupts */
intx:
dev_warn(&pci_dev->dev, " Unable to use MSI interrupts, falling back to legacy\n" );
err = devm_request_irq(&pci_dev->dev, pci_dev->irq, ...);

综上所述,Linux 内核中 PCI 设备驱动的开发涉及多个方面,包括数据结构的使用、设备的启用和禁用、资源的访问以及中断的处理等。开发者需要深入理解这些概念和相关的 API,才能编写出高效、稳定的 PCI 设备驱动。

Linux 内核中 PCI 设备驱动开发全解析

16. 中断处理流程总结

为了更清晰地理解中断处理的流程,下面通过一个 mermaid 流程图来展示:

graph TD;
    A[驱动启动] --> B[调用 pci_alloc_irq_vectors];
    B --> C{根据 flags 选择中断模式};
    C -->|PCI_IRQ_LEGACY| D[使用传统 IRQ 向量];
    C -->|PCI_IRQ_MSI| E[设置 pci_dev->msi_enabled 为 1];
    C -->|PCI_IRQ_MSIX| F[设置 pci_dev->msix_enabled 为 1];
    C -->|PCI_IRQ_ALL_TYPES| G{先尝试 MSI - X};
    G -->|成功| F;
    G -->|失败| H{再尝试 MSI};
    H -->|成功| E;
    H -->|失败| D;
    D --> I[使用 pci_dev->irq 作为 IRQ];
    E --> J[使用 pci_dev->irq + X 作为 IRQ];
    F --> K[通过 pci_irq_vector 获取 IRQ];
    I --> L[请求中断服务];
    J --> L;
    K --> L;

从这个流程图可以看出,使用 pci_alloc_irq_vectors 函数可以根据不同的 flags 灵活选择中断模式,并且在不同模式下获取正确的 IRQ 号来请求中断服务。

17. 设备操作流程整合

在开发 PCI 设备驱动时,设备的启用、资源访问和中断处理等操作需要按照一定的顺序进行。下面通过一个表格来总结这些操作的流程:
| 步骤 | 操作 | 函数调用 | 说明 |
| ---- | ---- | ---- | ---- |
| 1 | 注册驱动 | pci_register_driver | 在模块初始化方法中调用,传入 struct pci_driver 指针 |
| 2 | 启用设备 | pci_enable_device | 对设备进行初始化,可选择使用 pci_enable_device_mem 或 pci_enable_device_io |
| 3 | 启用总线主控 | pci_set_master | 如果设备要执行 DMA 操作,必须调用 |
| 4 | 访问配置寄存器 | pci_read_config_* 和 pci_write_config_* | 可使用内核定义的宏来指定偏移量 |
| 5 | 访问内存映射 I/O 资源 | pci_request_regions 和 pci_iomap | 简化内存区域的请求和映射操作 |
| 6 | 访问 I/O 端口资源 | pci_request_region 和 pci_iomap | 根据资源标志处理 I/O 端口和 I/O 内存 |
| 7 | 处理中断 | pci_alloc_irq_vectors 和 pci_irq_vector | 选择合适的中断模式并获取 IRQ 号 |
| 8 | 注销驱动 | pci_unregister_driver | 在模块退出函数中调用,防止系统使用已卸载的驱动 |

18. 代码优化建议

在开发 PCI 设备驱动时,除了遵循上述流程,还可以进行一些代码优化,以提高代码的可读性和性能。以下是一些建议:
- 使用宏和辅助函数 :如 PCI_DEVICE 、 PCI_DEVICE_CLASS 等宏可以简化 struct pci_device_id 的创建, pci_request_regions 、 pci_iomap 等辅助函数可以简化资源的请求和映射操作。
- 错误处理 :在调用可能失败的函数时,如 pci_enable_device 、 pci_alloc_irq_vectors 等,要及时检查返回值并进行错误处理,避免程序出现未定义行为。
- 内存管理 :在使用 request_mem_region 和 ioremap 等函数请求和映射内存区域时,要确保在不再使用时及时释放资源,避免内存泄漏。
- 中断处理优化 :对于使用多个中断的设备,要注意锁定和中断禁用的问题,避免死锁。

19. 不同中断模式的比较

MSI、MSI - X 和传统 INTx 中断模式各有优缺点,下面通过一个表格来比较它们:
| 中断模式 | 优点 | 缺点 | 适用场景 |
| ---- | ---- | ---- | ---- |
| 传统 INTx | 简单易用,参考设计默认支持 | 多个设备可能共享同一 IRQ 线,容易产生冲突 | 对中断性能要求不高,设备数量较少的场景 |
| MSI | 减少 IRQ 共享冲突,提高中断处理效率 | 每个设备最多支持 32 个中断向量 | 对中断性能有一定要求,设备数量适中的场景 |
| MSI - X | 支持大量中断向量,可灵活分配 | 实现复杂度较高,需要硬件支持 | 对中断性能要求高,设备需要大量中断向量的场景 |

开发者可以根据设备的具体需求和硬件支持情况选择合适的中断模式。

20. 未来发展趋势

随着硬件技术的不断发展,PCI 设备驱动开发也将面临一些新的挑战和机遇。以下是一些可能的发展趋势:
- 更高的性能需求 :随着数据处理量的不断增加,对 PCI 设备的性能要求也越来越高,驱动需要更好地支持高速数据传输和低延迟中断处理。
- 更多的硬件特性支持 :新的 PCI 设备可能会支持更多的硬件特性,如 PCIe Gen5、CXL 等,驱动需要及时跟进并支持这些特性。
- 智能化和自动化 :未来的驱动可能会更加智能化和自动化,能够自动检测设备的硬件特性并选择合适的配置和中断模式。

总之,Linux 内核中 PCI 设备驱动开发是一个复杂而又充满挑战的领域,开发者需要不断学习和掌握新的技术和方法,才能开发出高效、稳定的驱动程序。通过深入理解本文介绍的数据结构、函数调用和操作流程,开发者可以更好地应对这些挑战,为 PCI 设备的正常运行提供有力的支持。

相关推荐

深入解析PCI总线中断机制:从INTx到MSI-X的演进与驱动开发实践

中断是计算机系统中实现异步事件处理的核心机制,它允许硬件设备在需要CPU处理时主动发出信号,从而避免CPU轮询带来的效率损失。其基本原理是通过中断控制器将设备请求传递给CPU,CPU保存当前上下文后转去执行中断服务程序。这一机制的技术价值在于极大提升了系统响应速度与整体吞吐量,是现代操作系统实现高并发、低延迟的基石,广泛应用于网络数据包处理、存储I/O、用户输入响应等实时性要求高的场景。随着PCI总线成为设备互联的主流标准,其中断机制也经历了从传统的INTx引脚共享到基于消息的MSI/MSI-X的现代化演进

weixin_34280237的博客 322

34、Linux内核中PCI设备驱动开发指南

本文详细介绍了Linux内核中PCI设备驱动的开发流程与关键技术,涵盖PCI核心功能、关键数据结构(如struct pci_dev和struct pci_driver)、驱动注册机制、设备启用、配置空间访问、内存与I/O资源操作、中断处理(包括传统INTx、MSI和MSI-X)以及锁定机制等内容。文章还提供了完整的代码示例、性能优化建议和未来发展趋势分析,帮助开发者全面掌握PCI驱动开发的最佳实践。

alice7model的博客 130

别再死记硬背HJB方程了!用Python数值求解一个简单最优控制问题(附完整代码)

本文通过Python数值求解Hamilton-Jacobi-Bellman(HJB)方程,帮助读者从工程实践角度理解最优控制问题。教程涵盖从建立简单车辆控制模型到离散化HJB方程、实现值迭代算法及可视化结果的完整流程,适合具备Python基础的开发者学习。

weixin_42520796的博客 371

用 SQL 分析不同用户群组留存率

一、前言 用户增长对企业非常重要,主要包括: 新增、留存、召回 。本文介绍一个实用的分析留存率的方法: Cohort Analysis (群组分析),APP的下载量越高并不是说产品就越好,产品若没有很好的留存,花费大量时间和资源来挖掘新客户也将是竹篮打水一场空。群组分析有利于衡量用户在每个漏斗的用户流失节点、用户流失数据,从而帮助你发现产品的用户的行为模式和用户增长点。从本质上讲,群组分析Cohort Analysis为你提供了一种数据驱动的方法,用群组分析Cohort Analysis方法,可以来帮助提

qq_39984050的博客 3174

CSP历年真题201503-1-图像旋转(C/C++)

一、问题描述 旋转是图像处理的基本操作,在这个问题中,你需要将一个图像逆时针旋转90度。 计算机中的图像表示可以用一个矩阵来表示,为了旋转一个图像,只需要将对应的矩阵旋转即可。 输入格式 二、输入格式 输入的第一行包含两个整数n, m,分别表示图像矩阵的行数和列数。 接下来n行每行包含m个整数,表示输入的图像。 三、输出格式 输出m行,每行包含n个整数,表示原始矩阵逆时针旋转90度后的矩阵。 四、...

GTboy100的博客 1671

Android PMS——概述(一)

PMS(Package Manager Service)是 Android 操作系统的一个重要组件,它负责管理应用程序的包信息以及与应用程序的安装、卸载、权限控制等相关功能,确保应用程序的安全性和稳定性。 Android PMS 负责维护一个应用程序包的清单信息,该清单是一个 XML 文件,包含应用程序的各种元数据,例如应用程序的包名、版本号、权限要求、组件信息等。PMS通过访问该清单文件来获取应用程序的信息。 此外,Android PMS 还负责应用程序的安装和卸载。在安装应用

小旭的专栏 1669

教程: 将 SolidWorks 设计导出为 ROS 的 URDF

请按照以下步骤应用自定义徽标纹理:创建徽标材质文件:techniquepass材质名称:测试/材质颜色属性:环境、漫反射、镜面和发射都设置为白色(1 1 1 1),这意味着该材质看起来完全是白色的。纹理 在表面应用纹理图像 logo.png。将材质文件复制到 gazebo matrial:复制徽标图像:将 “徽标 ”替换为机器人上徽标所在位置的适当参考值。按照以上步骤,您就可以在 Gazebo 中为机器人添加自定义材质和纹理,包括徽标。

机器人控制与仿真应用 2461

【系统分析师】2025年下半年真题:论文及解题思路

摘要 本文探讨软件测试技术的应用,重点关注静态测试与动态测试的比较。静态测试在不运行程序的情况下,通过审查、走查等方式分析需求、设计文档和代码,能早期发现高层次缺陷,降低修复成本。动态测试通过实际运行程序,使用单元测试、集成测试等方法验证功能性能,是发现运行时错误的唯一途径。二者在缺陷类型、介入阶段和修复成本上具有互补性,建议项目实践中结合使用以提升软件质量。

数据知道的博客 5789

Dify智能体开发:Dify文件上传

相较于聊天文本,文档文件能够承载大量的信息,例如学术报告、法律合同。受限于 LLM 自身仅能够支持文件或图片,难以获取文件内更加丰富的上下文信息,应用的使用者不得不手动复制粘贴大量信息与 LLM 对话,增加了许多不必要的使用成本。

王国平的博客 2592

学Simulink——基于直流电机的建模与控制场景实例:直流电机带负载突变时的动态响应仿真

在实际应用中,直流电机经常需要应对各种工作条件的变化,例如负载突变。了解电机在这种情况下的动态响应特性对于确保系统的稳定性和可靠性至关重要。通过仿真分析,可以预测电机在不同负载条件下的表现,并优化控制系统的设计。本文将详细介绍如何使用来构建一个直流电机带负载突变时的动态响应仿真模型,并通过仿真展示其应用效果。本文介绍了如何使用构建一个直流电机带负载突变时的动态响应仿真模型,并通过仿真展示了其应用效果。

xiaoheshang_123的博客 115

WINCC7.3破解文件

WINCC7.3硬件够破解文件,有需要的下载!WINCC7.3硬件够破解文件,有需要的下载!

pscad IEEE标准系统搭建说明

pscad IEEE标准系统搭建说明,包含典型的IEEE 9节点、14节点、30节点、39节点和118节点系统。内含标准测试系统的原始数据和潮流结果。

Statistics and Machine Learning Toolbox

Statistics and Machine Learning Toolbox(带书签),MATLAB统计学习工具

上一篇: 14、隧道场效应晶体管在低功耗存储器及外延层异质结线隧道FET的应用解析
下一篇: 15、基于外延层的 Si/SiGe 异质结线隧穿场效应晶体管:原理、特性与应用
人间清醒863
博客等级 码龄3年 33粉丝 1544原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值