以下是针对Linux下PCI设备驱动开发的详细步骤指南及示例代码,适合刚入门的小白逐步学习和实践:
一、开发环境准备
-
安装开发工具
sudo apt install build-essential linux-headers-$(uname -r)
-
创建项目目录
mkdir pci_driver && cd pci_driver
二、编写最简单的PCI驱动框架
1. 创建驱动源码文件 my_pci_driver.c
#include <linux/module.h>
#include <linux/pci.h>
/* 定义驱动支持的PCI设备ID列表 */
static const struct pci_device_id my_pci_ids[] = {
{ PCI_DEVICE(0x1234, 0x5678) }, // 替换为你的设备厂商ID和设备ID
{ 0, } // 结束标记
};
MODULE_DEVICE_TABLE(pci, my_pci_ids);
/* PCI设备探测函数(当系统发现匹配设备时调用) */
static int my_pci_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
printk(KERN_INFO "My PCI Driver: Device detected!\n");
return 0; // 返回0表示成功
}
/* PCI设备移除函数(驱动卸载或设备拔出时调用) */
static void my_pci_remove(struct pci_dev *pdev)
{
printk(KERN_INFO "My PCI Driver: Device removed.\n");
}
/* 定义PCI驱动结构体 */
static struct pci_driver my_pci_driver = {
.name = "my_pci_driver", // 驱动名称
.id_table = my_pci_ids, // 支持的设备ID表
.probe = my_pci_probe, // 探测函数
.remove = my_pci_remove, // 移除函数
};
/* 模块加载函数 */
static int __init my_pci_init(void)
{
return pci_register_driver(&my_pci_driver);
}
/* 模块卸载函数 */
static void __exit my_pci_exit(void)
{
pci_unregister_driver(&my_pci_driver);
}
module_init(my_pci_init);
module_exit(my_pci_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Simple PCI Driver Example");
2. 创建Makefile
obj-m += my_pci_driver.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
三、获取设备厂商ID与设备ID
-
查找PCI设备信息
lspci -nn # 显示所有PCI设备,例如: # 01:00.0 Ethernet controller [0200]: Intel Corporation Device [8086:1533]
-
8086
是厂商ID(Vendor ID) -
1533
是设备ID(Device ID)
-
-
修改代码中的PCI_DEVICE宏
{ PCI_DEVICE(0x8086, 0x1533) }, // 替换为你的设备ID
四、编译与加载驱动
-
编译驱动
make # 生成my_pci_driver.ko文件
-
加载驱动
sudo insmod my_pci_driver.ko
-
查看日志
dmesg | tail # 应显示"Device detected!"
-
卸载驱动
sudo rmmod my_pci_driver dmesg | tail # 显示"Device removed."
五、进阶功能实现
1. 启用PCI设备与映射内存
static int my_pci_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
int ret;
void __iomem *regs;
// 启用PCI设备
ret = pci_enable_device(pdev);
if (ret) {
printk(KERN_ERR "Failed to enable PCI device\n");
return ret;
}
// 请求内存区域(假设使用BAR0)
ret = pci_request_regions(pdev, "my_pci_driver");
if (ret) {
printk(KERN_ERR "Failed to request regions\n");
goto err_disable;
}
// 映射BAR0到内核虚拟地址
regs = pci_ioremap_bar(pdev, 0);
if (!regs) {
printk(KERN_ERR "Failed to map BAR0\n");
ret = -ENOMEM;
goto err_release;
}
// 示例:读取第一个寄存器(假设32位)
u32 value = ioread32(regs);
printk(KERN_INFO "Register value: 0x%x\n", value);
// 保存映射地址到设备私有数据
pci_set_drvdata(pdev, regs);
return 0;
err_release:
pci_release_regions(pdev);
err_disable:
pci_disable_device(pdev);
return ret;
}
static void my_pci_remove(struct pci_dev *pdev)
{
void __iomem *regs = pci_get_drvdata(pdev);
iounmap(regs);
pci_release_regions(pdev);
pci_disable_device(pdev);
}
2. 处理中断
#include <linux/interrupt.h>
static irqreturn_t my_pci_interrupt(int irq, void *dev_id)
{
struct pci_dev *pdev = dev_id;
void __iomem *regs = pci_get_drvdata(pdev);
// 读取中断状态寄存器
u32 status = ioread32(regs + 0x10);
if (status & 0x1) {
printk(KERN_INFO "Interrupt occurred!\n");
// 清除中断标志
iowrite32(status & 0x1, regs + 0x10);
return IRQ_HANDLED;
}
return IRQ_NONE;
}
// 在probe函数中添加:
ret = pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_LEGACY);
if (ret < 0) {
printk(KERN_ERR "Failed to allocate IRQ\n");
goto err_unmap;
}
ret = request_irq(pci_irq_vector(pdev, 0), my_pci_interrupt,
IRQF_SHARED, "my_pci_irq", pdev);
if (ret) {
printk(KERN_ERR "Failed to request IRQ\n");
goto err_irq;
}
// 在remove函数中添加:
free_irq(pci_irq_vector(pdev, 0), pdev);
pci_free_irq_vectors(pdev);
六、调试技巧
-
查看驱动日志
dmesg -w # 实时监控内核日志
-
检查设备是否被识别
lspci -v -s 01:00.0 # 替换为你的设备地址
-
查看驱动加载状态
lsmod | grep my_pci_driver
七、注意事项
-
内核版本兼容性:确保头文件路径正确(
/lib/modules/$(uname -r)/build
)。 -
内存安全:使用
ioread32
/iowrite32
访问寄存器,避免直接指针操作。 - 错误处理:所有内核函数调用必须检查返回值!
- 测试环境:建议在虚拟机或专用开发板测试,避免主机崩溃。
通过以上步骤,可以逐步构建一个完整的PCI设备驱动。实际开发中需根据具体硬件手册调整寄存器操作和中断处理逻辑。