驱动框架入门:Linux设备驱动模型

说实话,我刚接触Linux驱动开发那会儿,最头疼的就是搞不懂设备驱动模型到底是个啥。一堆结构体、注册函数、回调接口,看着就头大。后来做了几个项目,踩了不少坑,才慢慢摸清楚门道。

这一章,我就带你从最核心的platform总线开始,一步步把Linux设备驱动模型拆开来看。你想想看,驱动开发说白了就是让内核认识你的硬件,然后应用程序能通过文件操作来使用它。就这么简单。

1. Linux设备驱动模型的核心三要素

Linux内核把设备驱动抽象成了三个角色:总线(Bus)、设备(Device)、驱动(Driver)。这三者怎么配合呢?

  • 总线:负责匹配设备和驱动。比如platform总线,它会遍历所有注册的设备,看看有没有驱动愿意接管它。
  • 设备:描述硬件的信息,比如寄存器地址、中断号、时钟频率等。设备本身不干活,它等着驱动来认领。
  • 驱动:真正干活的代码。它实现了probe、remove等回调函数,负责初始化硬件、提供操作接口。

我个人习惯把这三者的关系想象成「婚介所」:总线是红娘,设备是待嫁的姑娘,驱动是来相亲的小伙子。红娘负责匹配,匹配上了就调用probe(相当于结婚),设备开始工作。

核心要点:驱动不直接操作设备,而是通过总线来绑定。这种解耦设计让代码复用性大大提高。

2. platform总线:最常用的虚拟总线

在嵌入式系统中,很多设备并不挂载在PCI、USB这类物理总线上。比如SoC内部的UART、I2C控制器、GPIO等。它们怎么注册呢?答案就是platform总线。

platform总线是Linux内核提供的一种虚拟总线。它不依赖具体的硬件连接,而是通过设备树或代码静态定义来匹配。

2.1 platform_device

设备端,我们需要填充一个platform_device结构体:

struct platform_device {
    const char  *name;      // 设备名,用于匹配驱动
    int         id;         // 设备ID,-1表示只有一个
    struct device   dev;    // 内嵌的通用设备结构体
    struct resource *resource;  // 资源数组(地址、中断等)
    int         num_resources;
};

我记得第一次写platform设备时,忘了设置name字段,结果驱动死活匹配不上。查了两天才发现是名字拼写不一致。嗯,这里要注意:设备名和驱动名必须完全一致,大小写敏感。

2.2 platform_driver

驱动端,我们需要实现platform_driver结构体:

struct platform_driver {
    int (*probe)(struct platform_device *);
    int (*remove)(struct platform_device *);
    void (*shutdown)(struct platform_device *);
    int (*suspend)(struct platform_device *, pm_message_t);
    int (*resume)(struct platform_device *);
    struct device_driver driver;
};

probe函数是驱动的入口。当总线匹配成功时,内核会调用probe。你在probe里做硬件初始化、申请内存、注册字符设备等操作。

避坑指南:我曾经在probe里申请了DMA缓冲区,但忘了检查返回值。结果内存不足时直接崩溃。记住:probe里每一步都要做错误处理,一旦失败就返回负的错误码。

3. module_init/exit:驱动的加载与卸载

驱动写好了,怎么让内核加载它?答案就是module_init和module_exit宏。

static int __init my_driver_init(void)
{
    return platform_driver_register(&my_platform_driver);
}

static void __exit my_driver_exit(void)
{
    platform_driver_unregister(&my_platform_driver);
}

module_init(my_driver_init);
module_exit(my_driver_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple platform driver");

这里有几个细节:

  • __init__exit 是内核的优化标记。__init标记的函数在初始化完成后会被释放,节省内存。
  • module_init不一定要做platform_driver_register,你也可以注册其他类型的驱动。
  • MODULE_LICENSE必须声明,否则内核会报「tainted kernel」警告。

你可能会问:为什么用宏而不是直接定义函数指针?说白了,这是内核为了兼容不同编译器和架构做的封装。你照着写就行。

4. file_operations结构体:连接用户态和内核态

驱动最终是要给应用程序用的。应用程序通过open、read、write、ioctl等系统调用访问设备。这些系统调用怎么映射到驱动代码?答案就是file_operations结构体。

struct file_operations {
    struct module *owner;
    loff_t (*llseek) (struct file *, loff_t, int);
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
    long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
    int (*open) (struct inode *, struct file *);
    int (*release) (struct inode *, struct file *);
    // ... 还有很多
};

每个字段对应一个系统调用。比如应用程序调用read(),内核最终会调用你注册的read函数。

关键点:read/write函数的buf参数是__user指针,表示用户空间地址。你不能直接访问,必须用copy_to_usercopy_from_user来拷贝数据。我见过新手直接解引用__user指针,结果内核崩溃——因为用户空间的地址在内核态是无效的。

5. 字符设备驱动模板:一个完整的例子

光说不练假把式。下面我给你一个完整的字符设备驱动模板,结合platform总线:

#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>

#define DEVICE_NAME "my_char_dev"
#define CLASS_NAME  "my_class"

static int major;
static struct class *my_class = NULL;
static struct device *my_device = NULL;
static struct cdev my_cdev;

// file_operations 实现
static int my_open(struct inode *inode, struct file *file)
{
    pr_info("Device opened\n");
    return 0;
}

static int my_release(struct inode *inode, struct file *file)
{
    pr_info("Device closed\n");
    return 0;
}

static ssize_t my_read(struct file *file, char __user *buf, 
                       size_t len, loff_t *offset)
{
    char kernel_buf[100] = "Hello from kernel!";
    size_t to_copy = min(len, strlen(kernel_buf));
    
    if (copy_to_user(buf, kernel_buf, to_copy)) {
        pr_err("Failed to copy to user\n");
        return -EFAULT;
    }
    return to_copy;
}

static ssize_t my_write(struct file *file, const char __user *buf,
                        size_t len, loff_t *offset)
{
    char kernel_buf[100];
    
    if (len > sizeof(kernel_buf))
        return -EINVAL;
    
    if (copy_from_user(kernel_buf, buf, len)) {
        pr_err("Failed to copy from user\n");
        return -EFAULT;
    }
    pr_info("Received: %s\n", kernel_buf);
    return len;
}

static struct file_operations my_fops = {
    .owner   = THIS_MODULE,
    .open    = my_open,
    .release = my_release,
    .read    = my_read,
    .write   = my_write,
};

// platform driver probe
static int my_probe(struct platform_device *pdev)
{
    dev_t dev_num;
    int ret;
    
    // 1. 分配设备号
    ret = alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME);
    if (ret < 0) {
        pr_err("Failed to allocate device number\n");
        return ret;
    }
    major = MAJOR(dev_num);
    
    // 2. 初始化cdev
    cdev_init(&my_cdev, &my_fops);
    my_cdev.owner = THIS_MODULE;
    
    // 3. 添加cdev到内核
    ret = cdev_add(&my_cdev, dev_num, 1);
    if (ret < 0) {
        pr_err("Failed to add cdev\n");
        unregister_chrdev_region(dev_num, 1);
        return ret;
    }
    
    // 4. 创建设备类
    my_class = class_create(THIS_MODULE, CLASS_NAME);
    if (IS_ERR(my_class)) {
        pr_err("Failed to create class\n");
        cdev_del(&my_cdev);
        unregister_chrdev_region(dev_num, 1);
        return PTR_ERR(my_class);
    }
    
    // 5. 创建设备节点
    my_device = device_create(my_class, NULL, dev_num, 
                              NULL, DEVICE_NAME);
    if (IS_ERR(my_device)) {
        pr_err("Failed to create device\n");
        class_destroy(my_class);
        cdev_del(&my_cdev);
        unregister_chrdev_region(dev_num, 1);
        return PTR_ERR(my_device);
    }
    
    pr_info("My char device probed successfully\n");
    return 0;
}

static int my_remove(struct platform_device *pdev)
{
    dev_t dev_num = MKDEV(major, 0);
    
    device_destroy(my_class, dev_num);
    class_destroy(my_class);
    cdev_del(&my_cdev);
    unregister_chrdev_region(dev_num, 1);
    
    pr_info("My char device removed\n");
    return 0;
}

static struct platform_driver my_platform_driver = {
    .probe  = my_probe,
    .remove = my_remove,
    .driver = {
        .name = "my_platform_device",
        .of_match_table = NULL,  // 设备树匹配,暂不启用
    },
};

static int __init my_driver_init(void)
{
    return platform_driver_register(&my_platform_driver);
}

static void __exit my_driver_exit(void)
{
    platform_driver_unregister(&my_platform_driver);
}

module_init(my_driver_init);
module_exit(my_driver_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Embedded Driver Expert");
MODULE_DESCRIPTION("Platform based char device driver example");

这个模板涵盖了:

  • platform_driver的注册和注销
  • 字符设备号的申请和释放
  • cdev的初始化和添加
  • 设备类和设备节点的自动创建
  • 基本的open/read/write/release实现

注意事项:

  • probe函数中每一步都要检查返回值,失败时要回滚之前的所有操作。
  • copy_to_user和copy_from_user可能会睡眠,不能在中断上下文调用。
  • 设备节点名称不要和系统已有的冲突,否则insmod会失败。

6. 知识体系总览

下面这张图帮你理清整个驱动框架的脉络:

Linux设备驱动模型核心框架 用户空间 (User Space) 应用程序通过 open/read/write/ioctl 系统调用访问设备 VFS (虚拟文件系统) + 系统调用接口 file_operations 结构体 .open / .read / .write / .release / .unlocked_ioctl 字符设备驱动 (Char Device Driver) cdev_init / cdev_add / alloc_chrdev_region / device_create platform 总线 (虚拟总线) platform_driver_register / platform_device_register → probe匹配 调用链 关键函数 module_init module_exit probe remove copy_to/from_user cdev_add

从这张图可以清楚看到:应用程序通过VFS调用file_operations,file_operations由字符设备驱动实现,而字符设备驱动又通过platform总线与硬件设备绑定。每一层各司其职,这就是Linux驱动模型的精髓。

7. 调试小技巧

最后分享几个我常用的调试方法:

  • 查看设备是否注册成功ls /sys/bus/platform/devices/ 能看到所有platform设备。
  • 查看驱动是否匹配ls /sys/bus/platform/drivers/ 能看到已注册的驱动。
  • 查看设备节点ls -l /dev/my_char_dev 确认主次设备号正确。
  • 动态测试:用echo "test" > /dev/my_char_devcat /dev/my_char_dev 快速验证读写。

个人经验:我习惯在probe函数开头加一句pr_info("probe called for %s\n", pdev->name);。这样insmod后看dmesg,能第一时间知道probe有没有被调用。如果没调用,八成是设备名和驱动名不匹配。

好了,这一章的内容就到这里。你把这个模板理解透,再配合实际的硬件调试,Linux驱动开发的大门就算正式迈进去了。


公众号:蓝海资料掘金营,微信deep3321