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 设备的正常运行提供有力的支持。
超级会员免费看
568

被折叠的 条评论
为什么被折叠?



