4. PCIe驱动开发入门
本章将引导你从零开始构建一个完整的 PCIe 字符设备驱动。该驱动将注册为一个标准的 Linux 字符设备,并实现 open、read、write 和 ioctl 接口,为上层应用程序提供与 PCIe 硬件交互的通道。这是 PCIe 驱动开发中最基础、最核心的实践环节。
学习目标: 掌握 PCIe 驱动的基本框架、字符设备注册流程、以及如何通过
file_operations 结构体将系统调用与硬件操作绑定。
4.1 PCIe 字符设备驱动架构总览
一个典型的 PCIe 字符设备驱动在 Linux 内核中分为三个逻辑层:PCIe 设备层、字符设备层和用户接口层。下图展示了这三层之间的调用关系与数据流向。
架构要点: 用户空间的系统调用通过 VFS 层路由到字符设备驱动注册的
file_operations,再由字符设备层调用 PCIe 设备层提供的硬件访问函数,最终通过 PCIe 总线配置空间和 BAR 空间与物理设备通信。
4.2 驱动核心数据结构与初始化流程
编写 PCIe 字符设备驱动,首先需要定义并初始化两个核心结构体:struct pci_driver 和 struct file_operations。下表列出了这两个结构体中必须实现的成员及其作用。
| 结构体 | 成员 | 作用描述 |
|---|---|---|
| pci_driver | .name |
驱动名称,用于与设备 ID 表匹配 |
.id_table |
指向 pci_device_id 数组,声明支持的设备 VID/PID |
|
.probe |
设备发现时调用,完成硬件初始化、字符设备注册 | |
.remove |
设备移除时调用,释放资源、注销字符设备 | |
| file_operations | .owner |
通常设为 THIS_MODULE |
.open |
打开设备文件时调用,可进行权限检查或递增引用计数 | |
.read |
从设备读取数据,通常通过 ioread 系列函数访问 BAR 空间 | |
.write |
向设备写入数据,通常通过 iowrite 系列函数操作硬件寄存器 | |
.unlocked_ioctl |
实现自定义控制命令,如复位、配置读取、DMA 触发等 |
4.3 驱动入口与出口函数实现
驱动的生命周期由 module_init 和 module_exit 管理。在入口函数中,我们调用 pci_register_driver 将驱动注册到 PCI 核心子系统。
#include <linux/module.h>
#include <linux/pci.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#define DRIVER_NAME "pcie_char_dev"
#define DEVICE_NAME "pcie_char"
#define CLASS_NAME "pcie_char_class"
static struct class *pcie_char_class;
static int major_num;
// 设备私有结构体
struct pcie_char_dev {
struct pci_dev *pdev;
struct cdev cdev;
void __iomem *bar0_addr;
unsigned long bar0_len;
};
static int pcie_char_probe(struct pci_dev *pdev, const struct pci_device_id *ent);
static void pcie_char_remove(struct pci_dev *pdev);
// PCI 设备 ID 表
static const struct pci_device_id pcie_char_ids[] = {
{ PCI_DEVICE(0x10EE, 0x9034) }, // 示例 VID=0x10EE, PID=0x9034
{ 0, }
};
MODULE_DEVICE_TABLE(pci, pcie_char_ids);
static struct pci_driver pcie_char_driver = {
.name = DRIVER_NAME,
.id_table = pcie_char_ids,
.probe = pcie_char_probe,
.remove = pcie_char_remove,
};
static int __init pcie_char_init(void)
{
int ret;
// 动态分配主设备号
ret = alloc_chrdev_region(&major_num, 0, 1, DEVICE_NAME);
if (ret < 0) {
pr_err("Failed to allocate major number\n");
return ret;
}
// 创建设备类
pcie_char_class = class_create(THIS_MODULE, CLASS_NAME);
if (IS_ERR(pcie_char_class)) {
unregister_chrdev_region(major_num, 1);
return PTR_ERR(pcie_char_class);
}
// 注册 PCI 驱动
ret = pci_register_driver(&pcie_char_driver);
if (ret) {
class_destroy(pcie_char_class);
unregister_chrdev_region(major_num, 1);
pr_err("Failed to register PCI driver\n");
}
return ret;
}
static void __exit pcie_char_exit(void)
{
pci_unregister_driver(&pcie_char_driver);
class_destroy(pcie_char_class);
unregister_chrdev_region(major_num, 1);
}
module_init(pcie_char_init);
module_exit(pcie_char_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("PCIe Debug Course");
MODULE_DESCRIPTION("Simple PCIe Character Device Driver");
注意事项:
alloc_chrdev_region 必须在 pci_register_driver 之前调用,因为 probe 回调中需要用到已分配的设备号来初始化 cdev。如果顺序颠倒,probe 中将无法创建设备节点。
4.4 probe 函数:字符设备与 PCIe 硬件绑定
probe 函数是驱动初始化的核心。它负责启用 PCIe 设备、映射 BAR 空间、初始化字符设备并创建设备节点。下面是完整的 probe 实现。
static int pcie_char_probe(struct pci_dev *pdev, const struct pci_device_id *ent)
{
struct pcie_char_dev *dev;
struct device *device;
int ret;
// 1. 分配设备私有结构体
dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
if (!dev)
return -ENOMEM;
dev->pdev = pdev;
// 2. 启用 PCIe 设备
ret = pcim_enable_device(pdev);
if (ret) {
dev_err(&pdev->dev, "Failed to enable PCI device\n");
return ret;
}
// 3. 请求 BAR0 资源并映射到内核虚拟地址空间
ret = pcim_iomap_regions(pdev, BIT(0), DRIVER_NAME);
if (ret) {
dev_err(&pdev->dev, "Failed to iomap BAR0\n");
return ret;
}
dev->bar0_addr = pcim_iomap_table(pdev)[0];
dev->bar0_len = pci_resource_len(pdev, 0);
// 4. 设置 DMA 掩码(可选,用于 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, "Failed to set DMA mask\n");
return ret;
}
}
// 5. 初始化字符设备
cdev_init(&dev->cdev, &pcie_char_fops);
dev->cdev.owner = THIS_MODULE;
ret = cdev_add(&dev->cdev, major_num, 1);
if (ret) {
dev_err(&pdev->dev, "Failed to add cdev\n");
return ret;
}
// 6. 创建设备节点 /dev/pcie_char
device = device_create(pcie_char_class, &pdev->dev, major_num,
dev, DEVICE_NAME);
if (IS_ERR(device)) {
cdev_del(&dev->cdev);
return PTR_ERR(device);
}
// 7. 保存私有数据到 pci_dev
pci_set_drvdata(pdev, dev);
dev_info(&pdev->dev, "Probe successful, BAR0 at %p (len=%lu)\n",
dev->bar0_addr, dev->bar0_len);
return 0;
}
4.5 file_operations 接口实现
字符设备的核心交互接口定义在 file_operations 结构体中。下面给出 open、read、write 和 unlocked_ioctl 的典型实现。
static int pcie_char_open(struct inode *inode, struct file *filp)
{
struct pcie_char_dev *dev = container_of(inode->i_cdev,
struct pcie_char_dev, cdev);
filp->private_data = dev;
return 0;
}
static ssize_t pcie_char_read(struct file *filp, char __user *buf,
size_t count, loff_t *f_pos)
{
struct pcie_char_dev *dev = filp->private_data;
size_t bytes_to_read;
u32 data;
// 限制读取长度不超过 BAR0 空间大小
bytes_to_read = min(count, (size_t)(dev->bar0_len - *f_pos));
if (bytes_to_read < 4)
return 0;
// 从 BAR0 偏移 f_pos 处读取一个 32 位寄存器
data = ioread32(dev->bar0_addr + *f_pos);
if (copy_to_user(buf, &data, sizeof(data)))
return -EFAULT;
*f_pos += sizeof(data);
return sizeof(data);
}
static ssize_t pcie_char_write(struct file *filp, const char __user *buf,
size_t count, loff_t *f_pos)
{
struct pcie_char_dev *dev = filp->private_data;
u32 data;
if (count < sizeof(data))
return -EINVAL;
if (copy_from_user(&data, buf, sizeof(data)))
return -EFAULT;
// 写入 BAR0 偏移 f_pos 处的寄存器
iowrite32(data, dev->bar0_addr + *f_pos);
*f_pos += sizeof(data);
return sizeof(data);
}
static long pcie_char_ioctl(struct file *filp, unsigned int cmd,
unsigned long arg)
{
struct pcie_char_dev *dev = filp->private_data;
u32 val;
switch (cmd) {
case IOCTL_READ_REG:
// 从用户空间获取寄存器偏移地址
if (copy_from_user(&val, (u32 __user *)arg, sizeof(val)))
return -EFAULT;
val = ioread32(dev->bar0_addr + val);
if (copy_to_user((u32 __user *)arg, &val, sizeof(val)))
return -EFAULT;
break;
case IOCTL_WRITE_REG:
// 从用户空间获取 {偏移, 值} 结构体
// 此处省略具体结构体定义
break;
case IOCTL_RESET_DEVICE:
// 触发设备软复位
iowrite32(0x1, dev->bar0_addr + RESET_OFFSET);
break;
default:
return -ENOTTY;
}
return 0;
}
static struct file_operations pcie_char_fops = {
.owner = THIS_MODULE,
.open = pcie_char_open,
.read = pcie_char_read,
.write = pcie_char_write,
.unlocked_ioctl = pcie_char_ioctl,
};
设计建议: 在实际项目中,
read 和 write 通常用于流式数据传输(如 DMA 缓冲区),而 ioctl 更适合寄存器级别的配置和状态查询。建议将 BAR 空间的随机访问封装在 ioctl 中,保持 read/write 语义的清晰。
4.6 用户空间测试程序示例
编写完成后,可以通过一个简单的用户空间程序验证驱动功能。以下程序打开设备、读取一个寄存器并打印其值。
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#define DEVICE_PATH "/dev/pcie_char"
#define IOCTL_READ_REG _IOR('p', 1, unsigned int)
int main(void)
{
int fd;
unsigned int offset = 0x00;
unsigned int value;
fd = open(DEVICE_PATH, O_RDWR);
if (fd < 0) {
perror("Failed to open device");
return -1;
}
// 通过 ioctl 读取 BAR0 偏移 0x00 处的寄存器
if (ioctl(fd, IOCTL_READ_REG, &offset) < 0) {
perror("ioctl failed");
close(fd);
return -1;
}
value = offset; // ioctl 将读取结果写回 offset 变量
printf("Register at offset 0x00: 0x%08X\n", value);
close(fd);
return 0;
}
4.7 常见问题与调试技巧
- 设备节点未创建: 检查
udev规则或手动使用mknod /dev/pcie_char c <major> 0创建。 - ioread32 返回 0xFFFFFFFF: 通常表示 BAR 空间未正确映射或设备未使能,检查
lspci -vv中的 BAR 地址是否有效。 - probe 未被调用: 确认
pci_device_id表中的 VID/PID 与目标设备一致,并检查驱动是否已加载。 - ioctl 返回 -ENOTTY: 检查命令编号是否与用户空间定义的宏一致,确保
_IOR/_IOW的方向和大小匹配。
调试命令速查:
# lspci -vv -s 01:00.0 — 查看 PCIe 设备配置空间和 BAR 信息# cat /proc/devices | grep pcie_char — 确认主设备号已注册# ls -l /dev/pcie_char — 确认设备节点存在# dmesg | tail -20 — 查看驱动打印的调试信息