Linux内核中PCI设备驱动开发指南
1. PCI核心功能概述
PCI核心在Linux内核中承担着诸多重要任务,它负责创建和初始化系统中总线、设备以及桥接器的数据结构树,处理总线和设备编号,创建设备条目并提供
proc/sysfs
信息。同时,它还为PCI BIOS和从设备(端点)驱动提供服务,并在硬件支持的情况下提供热插拔支持。此外,它还负责查询(EP)驱动接口,并初始化枚举过程中发现的相应设备,提供MSI中断处理框架和PCI Express端口总线支持,这些功能极大地促进了Linux内核中设备驱动的开发。
2. PCI数据结构
Linux内核的PCI框架基于两个主要数据结构来辅助PCI设备驱动的开发:
struct pci_dev
和
struct pci_driver
。
2.1 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;
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特殊兴趣小组维护全球注册表,制造商需申请唯一编号,该ID存储在设备配置空间的16位寄存器中。
-
device
:设备被探测后用于标识该特定设备的ID,依赖于供应商,无官方注册表,也存储在16位寄存器中。
-
irq
:该字段值得关注。设备启动时,MSI(-X)模式未启用,直到通过
pci_alloc_irq_vectors()
API(旧驱动使用
pci_enable_msi()
)显式启用。初始时,
irq
对应默认预分配的非MSI IRQ,其值或用法可能根据以下情况改变:
- 在MSI中断模式下(成功调用
pci_alloc_irq_vectors()
并设置
PCI_IRQ_MSI
标志),该字段的预分配值将被新的MSI向量替换,向量X(索引从0开始)对应的IRQ号为
pci_dev->irq + X
。
- 在MSI - X中断模式下(成功调用
pci_alloc_irq_vectors()
并设置
PCI_IRQ_MSIX
标志),该字段的预分配值不变,但在该模式下
irq
无效,使用它请求服务中断可能导致不可预测的行为。
| 成员 | 含义 |
|---|---|
procent
|
/proc/bus/pci/
中的设备条目
|
slot
| 设备所在的物理插槽 |
vendor
| 设备制造商的供应商ID |
device
| 设备被探测后的特定ID |
irq
| 中断号,根据不同中断模式有不同表现 |
2.2 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设备,将子供应商、子设备和类相关字段设置为
PCI_ANY_ID
。
-
PCI_DEVICE_CLASS
:用于描述特定PCI设备类,将供应商、设备、子供应商和子设备字段设置为
PCI_ANY_ID
。例如:
PCI_DEVICE_CLASS(PCI_CLASS_STORAGE_EX PRESS, 0xffffff)
对应NVMe设备的PCI类。
-
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
宏将这些信息导出到用户空间,示例如下:
MODULE_DEVICE_TABLE(pci, bt8xxgpio_pci_tbl);
2.3 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相关的电源管理辅助函数。
以下是一个初始化示例:
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,
};
3. 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,否则返回负错误码。在模块卸载时,需要调用
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);
4. PCI驱动结构概述及设备操作
4.1 启用设备
在对PCI设备进行任何操作之前,必须显式启用该设备,可使用
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()
或
pci_enable_device_io()
。禁用设备时,使用
pci_disable_device()
,示例如下:
void pci_disable_device(struct pci_dev *dev)
4.2 总线主控能力
PCI设备成为总线主控时可发起总线事务,启用总线主控即启用设备的DMA功能,可使用
pci_set_master()
函数,禁用则使用
pci_clear_master()
函数:
void pci_set_master(struct pci_dev *dev)
void pci_clear_master(struct pci_dev *dev)
4.3 访问配置寄存器
驱动需要访问设备的配置空间,以读取必要信息或设置重要参数。内核提供了不同大小数据的读写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);
// 获取设备修订ID示例
static unsigned char foo_get_revision(struct pci_dev *dev) {
u8 revision;
pci_read_config_byte(dev, PCI_REVISION_ID, &revision);
return revision;
}
4.4 访问内存映射I/O资源
访问内存映射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);
// 映射bar0示例
unsigned long bar0_base;
unsigned long bar0_size;
void iomem *bar0_map_membase;
bar0_base = pci_resource_start(pdev, 0);
bar0_size = pci_resource_len(pdev, 0);
if (request_mem_region(bar0_base, bar0_size, " bar0-mapping")) {
goto err_disable;
}
bar0_map_membase = ioremap(bar0_base, bar0_size);
if (!bar0_map_membase) {
goto err_iomap;
}
PCI框架还提供了许多辅助函数,简化了这些操作:
int pci_request_region(struct pci_dev *pdev, int bar, const char *res_name)
int pci_request_regions(struct pci_dev *pdev, const char *res_name)
void iomem *pci_iomap(struct pci_dev *dev, int bar, unsigned long maxlen)
void iomem *pci_iomap_range(struct pci_dev *dev, int bar, unsigned long offset, unsigned long maxlen)
void iomem *pci_ioremap_bar(struct pci_dev *pdev, int bar)
void pci_iounmap(struct pci_dev *dev, void iomem *addr)
void pci_release_regions(struct pci_dev *pdev)
以下是使用辅助函数映射BAR1的示例:
#define DRV_NAME " foo-drv"
void iomem *bar1_map_membase;
int err;
err = pci_request_regions(pci_dev, DRV_NAME);
if (err) {
goto error;
}
bar1_map_membase = pci_iomap(pdev, 1, 0);
if (!bar1_map_membase) {
goto err_iomap;
}
5. 访问I/O端口资源
访问I/O端口资源需要经过请求I/O区域、映射I/O区域(可选)和访问I/O区域这几个步骤。
pci_requestregion*()
和
pci_iomap*()
函数可处理I/O端口和I/O内存,它们根据资源标志调用相应的底层辅助函数。访问I/O端口的API如下:
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);
6. 处理中断
需要处理设备中断的驱动通常在
probe()
方法中请求中断。对于更通用的方法,推荐使用
pci_alloc_irq_vectors()
函数,示例如下:
int pci_alloc_irq_vectors(struct pci_dev *dev, unsigned int min_vecs, unsigned int max_vecs, unsigned int flags);
该函数根据
flags
参数可以处理传统中断、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模式。
-
PCI_IRQ_AFFINITY
:允许自动分配中断亲和力。
获取Linux IRQ编号可使用
pci_irq_vector()
函数:
int pci_irq_vector(struct pci_dev *dev, unsigned int nr);
以下是处理中断的mermaid流程图:
graph TD
A[开始] --> B[调用pci_alloc_irq_vectors]
B --> C{是否成功}
C -- 是 --> D{中断模式}
C -- 否 --> E[处理错误]
D -- MSI-X --> F[使用pci_dev->msix_enabled判断]
D -- MSI --> G[使用pci_dev->msi_enabled判断]
D -- 传统 --> H[使用pci_dev->irq]
F --> I[获取对应IRQ编号]
G --> J[获取对应IRQ编号]
H --> K[使用pci_dev->irq]
I --> L[请求中断]
J --> L
K --> L
L --> M[处理中断]
M --> N[结束]
E --> N
7. 传统INTx IRQ分配及相关问题处理
PCI总线类型的探测方法
pci_device_probe()
会调用
pci_assign_irq()
和
pcibios_alloc_irq()
来分配IRQ。
pci_assign_irq()
读取设备连接的引脚,并根据PCI主机桥的
.map_irq
回调函数创建IRQ映射。
需要注意的是,
PCI_INTERRUPT_LINE
中的IRQ值在调用
pci_enable_device()
之前是无效的,且外设驱动不应更改该值。
为了解决大多数PCIe设备在传统INTx模式下共享INTA输入的问题,采用了“虚拟线INTx IRQ交换”技术,在
pci_device_probe()
调用的
pci_assign_irq()
函数中有相关处理。
8. 锁定考虑
许多设备驱动在中断处理程序中使用每个设备的自旋锁。在基于Linux的系统中,处理基于引脚的中断或单个MSI时,不需要禁用中断。但如果设备使用多个中断,驱动在持有锁时必须禁用中断,可使用
spin_lock_irqsave()
或
spin_lock_irq()
函数。
9. 旧API说明
一些旧驱动仍使用已弃用的MSI或MSI - X API,如
pci_enable_msi()
、
pci_disable_msi()
等,新代码不应使用这些API。以下是一个尝试使用MSI并在失败时回退到传统中断模式的示例:
int err;
/* 尝试使用MSI中断 */
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;
/* 尝试使用传统中断 */
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设备驱动需要深入理解PCI核心功能、数据结构以及各种操作方法,合理使用相关API和辅助函数,同时注意中断处理、锁定和旧API的使用问题,这些步骤和技巧对于成功开发PCI设备驱动至关重要。
Linux内核中PCI设备驱动开发指南
10. 总结与最佳实践
在开发Linux内核中的PCI设备驱动时,我们需要遵循一系列的步骤和最佳实践,以确保驱动的正确性和性能。以下是一个总结性的列表,涵盖了从设备初始化到中断处理的关键步骤:
-
驱动注册
-
创建并初始化
struct pci_driver结构。 -
使用
pci_register_driver()进行注册,使用pci_unregister_driver()进行注销,推荐使用module_pci_driver()宏自动处理。
-
创建并初始化
-
设备启用
-
在操作设备前,使用
pci_enable_device()启用设备,根据需要使用pci_enable_device_mem()或pci_enable_device_io()。 -
使用
pci_disable_device()禁用设备。
-
在操作设备前,使用
-
总线主控能力
-
若设备需要进行DMA操作,使用
pci_set_master()启用总线主控,使用pci_clear_master()禁用。
-
若设备需要进行DMA操作,使用
-
配置寄存器访问
-
使用
pci_read_config_*()和pci_write_config_*()函数访问设备配置空间。
-
使用
-
内存映射I/O资源访问
-
使用
request_mem_region()和ioremap()或PCI框架提供的辅助函数请求和映射内存区域。 -
使用
ioread*()和iowrite*()访问映射的寄存器。
-
使用
-
I/O端口资源访问
-
使用
pci_requestregion*()和pci_iomap*()函数处理I/O端口请求和映射。 -
使用
in*()和out*()函数访问I/O端口。
-
使用
-
中断处理
-
使用
pci_alloc_irq_vectors()分配中断向量。 -
使用
pci_irq_vector()获取Linux IRQ编号。
-
使用
-
锁定考虑
-
若设备使用多个中断,在中断处理程序中使用
spin_lock_irqsave()或spin_lock_irq()禁用中断并获取锁。
-
若设备使用多个中断,在中断处理程序中使用
| 操作步骤 | 相关函数 |
|---|---|
| 驱动注册 |
pci_register_driver()
、
pci_unregister_driver()
、
module_pci_driver()
|
| 设备启用 |
pci_enable_device()
、
pci_enable_device_mem()
、
pci_enable_device_io()
、
pci_disable_device()
|
| 总线主控能力 |
pci_set_master()
、
pci_clear_master()
|
| 配置寄存器访问 |
pci_read_config_*()
、
pci_write_config_*()
|
| 内存映射I/O资源访问 |
request_mem_region()
、
ioremap()
、
pci_request_region()
、
pci_iomap()
等
|
| I/O端口资源访问 |
pci_requestregion*()
、
pci_iomap*()
、
in*()
、
out*()
|
| 中断处理 |
pci_alloc_irq_vectors()
、
pci_irq_vector()
|
| 锁定考虑 |
spin_lock_irqsave()
、
spin_lock_irq()
|
11. 代码示例整合
以下是一个完整的PCI设备驱动开发示例,整合了上述的关键步骤:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/pci.h>
#define DRV_NAME "foo-drv"
// 定义pci_device_id表
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, bt8xxgpio_pci_tbl);
// 定义probe函数
static int bt8xxgpio_probe(struct pci_dev *dev, const struct pci_device_id *id) {
int err;
void iomem *bar1_map_membase;
// 启用设备
err = pci_enable_device(dev);
if (err) {
dev_err(&dev->dev, "Can't enable device\n");
return err;
}
// 启用总线主控
pci_set_master(dev);
// 请求并映射内存区域
err = pci_request_regions(dev, DRV_NAME);
if (err) {
dev_err(&dev->dev, "Can't request regions\n");
goto error;
}
bar1_map_membase = pci_iomap(dev, 1, 0);
if (!bar1_map_membase) {
dev_err(&dev->dev, "Can't map BAR1\n");
goto err_iomap;
}
// 分配中断向量
err = pci_alloc_irq_vectors(dev, 1, 1, PCI_IRQ_ALL_TYPES);
if (err < 0) {
dev_err(&dev->dev, "Can't allocate IRQ vectors\n");
goto err_irq;
}
// 请求中断
err = devm_request_irq(&dev->dev, pci_irq_vector(dev, 0), my_irq_handler, 0, DRV_NAME, dev);
if (err) {
dev_err(&dev->dev, "Can't request IRQ\n");
goto err_irq;
}
return 0;
err_irq:
pci_iounmap(dev, bar1_map_membase);
err_iomap:
pci_release_regions(dev);
error:
pci_disable_device(dev);
return err;
}
// 定义remove函数
static void bt8xxgpio_remove(struct pci_dev *dev) {
pci_iounmap(dev, pci_iomap(dev, 1, 0));
pci_release_regions(dev);
pci_disable_device(dev);
}
// 定义pci_driver结构
static struct pci_driver bt8xxgpio_pci_driver = {
.name = DRV_NAME,
.id_table = bt8xxgpio_pci_tbl,
.probe = bt8xxgpio_probe,
.remove = bt8xxgpio_remove,
};
// 模块初始化函数
static int __init pci_foo_init(void) {
return pci_register_driver(&bt8xxgpio_pci_driver);
}
// 模块退出函数
static void __exit pci_foo_exit(void) {
pci_unregister_driver(&bt8xxgpio_pci_driver);
}
module_init(pci_foo_init);
module_exit(pci_foo_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("PCI device driver example");
12. 性能优化与调试建议
在开发PCI设备驱动时,性能优化和调试是非常重要的环节。以下是一些建议:
12.1 性能优化
- 减少内存访问次数 :尽量批量读取和写入数据,避免频繁的单次访问。
- 合理使用中断 :根据设备特性选择合适的中断模式,避免不必要的中断开销。
- 优化DMA操作 :确保DMA缓冲区的合理分配和使用,提高数据传输效率。
12.2 调试建议
-
使用内核日志
:使用
printk()函数输出调试信息,注意日志级别。 -
使用调试工具
:如
gdb、kgdb等进行内核调试。 - 检查硬件连接 :确保PCI设备的物理连接正常,避免硬件故障导致的问题。
13. 未来发展趋势
随着硬件技术的不断发展,PCI设备驱动开发也面临着新的挑战和机遇。以下是一些未来可能的发展趋势:
- 更高的带宽和速度 :PCIe标准不断演进,未来的设备将支持更高的带宽和数据传输速度,驱动需要适应这些变化。
- 更多的功能和特性 :新的PCI设备可能会具备更多的功能和特性,如智能电源管理、安全加密等,驱动需要支持这些新功能。
- 软件定义硬件 :软件定义硬件的趋势将使得PCI设备的配置和管理更加灵活,驱动需要提供相应的接口和支持。
14. 结论
开发Linux内核中的PCI设备驱动是一个复杂而又富有挑战性的任务。通过深入理解PCI核心功能、数据结构和操作方法,遵循最佳实践,合理使用相关API和辅助函数,我们可以开发出高效、稳定的PCI设备驱动。同时,关注性能优化、调试技巧和未来发展趋势,将有助于我们更好地应对不断变化的硬件环境和需求。希望本文能够为开发者提供一个全面的指南,帮助他们在PCI设备驱动开发的道路上取得成功。
以下是开发PCI设备驱动的整体mermaid流程图:
graph TD
A[开始] --> B[驱动注册]
B --> C[设备启用]
C --> D[总线主控启用]
D --> E[访问配置寄存器]
E --> F[访问内存映射I/O资源]
F --> G[访问I/O端口资源]
G --> H[分配中断向量]
H --> I[请求中断]
I --> J[处理中断]
J --> K[设备操作]
K --> L[设备禁用]
L --> M[驱动注销]
M --> N[结束]
超级会员免费看
60

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



