4. 驱动开发基础

驱动开发,说白了就是让内核认识你的硬件。我刚开始接触Linux驱动时,总觉得这东西玄乎——不就是写几个函数吗?后来踩了无数坑才明白,驱动模型的设计哲学,远比写代码本身重要。

这一章,我们聚焦三个核心:设备驱动模型、字符设备框架、platform驱动机制。这三块是嵌入式Linux驱动的基石,搞懂了它们,后面写任何驱动都有章可循。

4.1 Linux设备驱动模型:为什么需要它?

先问一个问题:为什么Linux不直接让驱动操作硬件,非要搞出一套设备模型?

你想想看,一个嵌入式系统里可能有几十个设备。每个设备有自己的地址、中断、时钟。如果没有统一的管理方式,驱动之间互相打架、资源冲突,那场面简直灾难。

Linux设备驱动模型的核心,就是把设备、驱动、总线三者解耦。它引入了几个关键概念:

概念 角色 我个人的理解
bus_type 总线类型 设备和驱动之间的“媒人”
device_driver 驱动抽象 描述“谁能驱动这个设备”
device 设备抽象 描述“这个设备长什么样”
class 设备类 按功能分类,比如input、tty

模型的核心流程是:总线匹配驱动和设备。当系统发现一个新设备时,总线会遍历所有注册的驱动,看看谁能驱动它。匹配上了,就调用驱动的probe函数。

关键点:驱动不直接操作设备,而是通过总线来“认识”设备。这种设计让驱动代码可以跨平台复用。

我在项目中遇到过一个问题:一个I2C触摸屏驱动在A平台跑得好好的,换到B平台就不工作了。排查了半天,发现是设备树里中断号配置错了。这就是设备模型的好处——硬件描述和驱动逻辑分离,改设备树就行,不用动驱动代码。

4.2 字符设备驱动框架:最基础的驱动形态

字符设备是Linux驱动里最简单的形式。它按字节流读写数据,典型的例子有串口、GPIO、LED。

字符设备驱动的核心结构是struct file_operations。这个结构体里放了一堆函数指针:open、read、write、ioctl、release……你实现这些函数,内核就知道怎么跟你的设备打交道。

来看一个最简单的字符设备驱动框架:

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>

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

static int major;
static struct class *my_class;
static struct cdev my_cdev;

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

static ssize_t my_read(struct file *file, char __user *buf,
                       size_t len, loff_t *off) {
    pr_info("Read called\n");
    return 0;
}

static ssize_t my_write(struct file *file, const char __user *buf,
                        size_t len, loff_t *off) {
    pr_info("Write called with %zu bytes\n", len);
    return len;
}

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

static int __init my_init(void) {
    // 1. 分配设备号
    major = register_chrdev(0, DEVICE_NAME, &fops);
    if (major < 0) {
        pr_err("Failed to register device\n");
        return major;
    }

    // 2. 初始化cdev
    cdev_init(&my_cdev, &fops);
    cdev_add(&my_cdev, MKDEV(major, 0), 1);

    // 3. 创建类
    my_class = class_create(THIS_MODULE, CLASS_NAME);
    device_create(my_class, NULL, MKDEV(major, 0),
                  NULL, DEVICE_NAME);

    pr_info("Module loaded with major %d\n", major);
    return 0;
}

static void __exit my_exit(void) {
    device_destroy(my_class, MKDEV(major, 0));
    class_destroy(my_class);
    cdev_del(&my_cdev);
    unregister_chrdev(major, DEVICE_NAME);
    pr_info("Module unloaded\n");
}

module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");

这段代码虽然简单,但包含了字符设备驱动的完整骨架:

  1. 分配设备号——告诉内核“我要用这个号码”
  2. 注册cdev——把file_operations挂到内核
  3. 创建设备节点——让用户空间能访问

我的习惯:写字符设备驱动时,先实现open和release,再补read/write。因为open/release是调试入口,能快速验证设备节点是否创建成功。

我曾经犯过一个低级错误:在write函数里忘了用copy_from_user,直接解引用用户空间指针。结果内核直接panic。嗯,这里要注意——用户空间指针不能在内核里直接访问,必须用专用的拷贝函数。

4.3 platform驱动机制:设备树时代的标配

字符设备驱动虽然简单,但它有个问题:设备信息写死在代码里。换一个板子,地址变了,就得改代码重新编译。

Platform驱动机制就是为了解决这个问题。它把设备信息放到设备树里,驱动只关心“怎么操作”,不关心“操作谁”。

Platform驱动的核心是两个结构体:

  • struct platform_driver——描述驱动本身
  • struct platform_device——描述设备信息(通常由设备树生成)

匹配流程是这样的:

Platform驱动匹配流程 设备树节点 compatible = "my,device" 内核解析 生成platform_device 匹配驱动 of_match_table比对 调用probe函数 驱动开始工作 💡 关键点 驱动不直接创建设备,而是等待内核匹配后触发probe

写platform驱动时,你只需要做两件事:

  1. 实现probe和remove函数——probe里做硬件初始化,remove里做清理
  2. 注册platform_driver——告诉内核“我能驱动哪些设备”

来看一个实际的platform驱动模板:

#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>

static int my_probe(struct platform_device *pdev) {
    struct resource *res;
    void __iomem *base;

    // 从设备树获取内存资源
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    if (!res) {
        dev_err(&pdev->dev, "No memory resource\n");
        return -ENXIO;
    }

    // 映射物理地址到虚拟地址
    base = devm_ioremap(&pdev->dev, res->start, resource_size(res));
    if (!base) {
        dev_err(&pdev->dev, "ioremap failed\n");
        return -ENOMEM;
    }

    dev_info(&pdev->dev, "Device probed at 0x%llx\n",
             (unsigned long long)res->start);
    return 0;
}

static int my_remove(struct platform_device *pdev) {
    dev_info(&pdev->dev, "Device removed\n");
    return 0;
}

static const struct of_device_id my_of_match[] = {
    { .compatible = "my,device" },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_of_match);

static struct platform_driver my_driver = {
    .probe  = my_probe,
    .remove = my_remove,
    .driver = {
        .name = "my_device",
        .of_match_table = my_of_match,
    },
};

module_platform_driver(my_driver);
MODULE_LICENSE("GPL");

我曾经踩过的坑:of_match_table里的compatible字符串必须和设备树里的一模一样,多一个空格都不行。有一次我设备树里写的是"my,device",驱动里写的是"my, device"(多了个空格),结果驱动死活不probe。查了两天才发现。

4.4 三种机制的对比与选择

学完这三个概念,你可能会问:什么时候用字符设备?什么时候用platform?

我的建议是:

场景 推荐机制 理由
纯软件虚拟设备 字符设备 不需要硬件资源,简单直接
挂载在总线上的硬件 platform驱动 设备树描述硬件,驱动可复用
需要用户空间交互 字符设备 + platform platform管理硬件,字符设备提供接口

说白了,字符设备是“接口”,platform是“机制”。它们不是互斥的,而是互补的。实际项目中,我经常在platform驱动的probe函数里注册一个字符设备,这样既利用了设备树的灵活性,又提供了用户空间接口。

核心总结:

  • 设备驱动模型:解耦设备、驱动、总线
  • 字符设备框架:提供file_operations给用户空间
  • platform驱动:通过设备树匹配硬件,自动触发probe

嗯,这一章的内容就到这里。记住一个原则:驱动是服务于硬件的,不是服务于代码的。把硬件理解透了,驱动自然就好写了。