3、Linux内核驱动基础:字符设备驱动模型、platform驱动、设备树(DTS)解析、GPIO与中断处理

各位同学,今天我们来啃一块硬骨头——Linux内核驱动基础。说实话,这部分内容在车载传感器开发中,几乎是绕不过去的坎。我当年刚入行时,被设备树折腾得够呛,后来才慢慢摸清门道。今天我把这些经验掰开揉碎了讲给你听。

3.1 字符设备驱动模型:最基础的驱动形态

字符设备驱动,说白了就是按字节流读写数据的设备驱动。像串口、GPIO、I2C这些,都属于字符设备。为什么叫“字符”?因为它一次传输一个字符(字节),不像块设备那样按块操作。

驱动开发的核心,就是实现 file_operations 结构体。这个结构体定义了设备文件的操作方法,比如 open、read、write、ioctl 等。我习惯把它叫做“驱动与用户空间的桥梁”。

核心要点:字符设备驱动的注册流程

// 1. 定义 file_operations 结构体
static struct file_operations my_fops = {
    .owner   = THIS_MODULE,
    .open    = my_open,
    .read    = my_read,
    .write   = my_write,
    .release = my_release,
};

// 2. 注册字符设备
static int __init my_init(void) {
    int ret;
    dev_t dev_num;
    
    // 分配设备号
    ret = alloc_chrdev_region(&dev_num, 0, 1, "my_device");
    if (ret < 0) {
        printk("Failed to allocate device number\n");
        return ret;
    }
    
    // 初始化 cdev 并添加到内核
    cdev_init(&my_cdev, &my_fops);
    my_cdev.owner = THIS_MODULE;
    ret = cdev_add(&my_cdev, dev_num, 1);
    
    return 0;
}

// 3. 卸载时清理
static void __exit my_exit(void) {
    dev_t dev_num = MKDEV(major, 0);
    cdev_del(&my_cdev);
    unregister_chrdev_region(dev_num, 1);
}

嗯,这里要注意:cdev_add 之后,设备就“活”了。用户空间可以通过 open("/dev/my_device") 来访问。我在项目中遇到过一个问题——忘记检查 alloc_chrdev_region 的返回值,结果设备号冲突,驱动加载失败。这种低级错误,排查起来还挺费时间的。

3.2 platform驱动:设备与驱动的“红娘”

你想想看,在传统的嵌入式系统中,设备信息是硬编码在板级文件里的。每次换一个板子,就得改代码,多麻烦。platform 驱动机制就是为了解决这个问题——它把设备和驱动分离,通过名字匹配来“牵线搭桥”。

说白了,platform 驱动就是一套“设备-驱动”的匹配框架。设备端提供资源信息(地址、中断号等),驱动端提供操作函数。内核负责匹配,匹配成功就调用驱动的 probe 函数。

个人经验:我建议你在写 platform 驱动时,先把设备树节点写好,再写驱动。这样思路更清晰,不容易漏掉资源。

// platform_driver 结构体
static struct platform_driver my_platform_driver = {
    .probe  = my_probe,
    .remove = my_remove,
    .driver = {
        .name = "my_device",
        .of_match_table = my_of_match,
    },
};

// probe 函数:驱动匹配成功后的入口
static int my_probe(struct platform_device *pdev) {
    struct resource *res;
    void __iomem *base;
    int irq;
    
    // 获取内存资源
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    base = devm_ioremap_resource(&pdev->dev, res);
    
    // 获取中断号
    irq = platform_get_irq(pdev, 0);
    
    // 注册字符设备
    // ...
    
    return 0;
}

我曾经在调试一个车载摄像头驱动时,发现 probe 函数一直没被调用。查了半天,原来是设备树里的 compatible 属性和驱动里的 of_match_table 没对上。这种问题,说白了就是名字写错了,但排查起来真让人头大。

3.3 设备树(DTS)解析:硬件配置的“说明书”

设备树,英文叫 Device Tree,是描述硬件信息的文本文件。它用树形结构来描述 CPU、内存、外设等硬件资源。内核启动时,会解析 DTB(设备树二进制文件),然后根据这些信息来初始化设备。

为什么需要设备树?因为 ARM 架构下,硬件平台五花八门。没有设备树之前,每个平台都得写一堆板级文件,维护起来简直是噩梦。设备树把硬件描述和内核代码分离开,换平台只需要换 DTB 文件就行。

设备树的基本语法:

/dts-v1/;

/ {
    model = "My Car Sensor Board";
    compatible = "vendor,my-board";

    // CPU 节点
    cpu@0 {
        compatible = "arm,cortex-a7";
        reg = <0x0>;
    };

    // 外设节点
    i2c@1c00000 {
        compatible = "vendor,i2c-controller";
        reg = <0x1c00000 0x1000>;
        interrupts = <0 31 4>;
        clock-frequency = <100000>;

        // 子节点:I2C 设备
        temperature-sensor@48 {
            compatible = "vendor,tmp102";
            reg = <0x48>;
            interrupt-parent = <&gpio1>;
            interrupts = <5 2>;
        };
    };

    // GPIO 控制器
    gpio1: gpio@1c80000 {
        compatible = "vendor,gpio-controller";
        reg = <0x1c80000 0x1000>;
        gpio-controller;
        #gpio-cells = <2>;
    };
};

在驱动中解析设备树,常用的 API 有:

  • of_find_node_by_path():通过路径查找节点
  • of_property_read_u32():读取 32 位整数属性
  • of_get_named_gpio():获取 GPIO 编号
  • irq_of_parse_and_map():解析中断并映射

避坑指南:我曾经在解析设备树时,忘记检查 of_find_node_by_path 的返回值。结果节点不存在时,内核直接 panic 了。记住:设备树解析函数的返回值一定要检查,尤其是 IS_ERR()NULL 的情况。

3.4 GPIO与中断处理:与硬件“握手”

GPIO 是嵌入式开发中最基础的外设。控制一个 LED、读取一个按键,都离不开 GPIO。在 Linux 内核中,GPIO 操作有标准的 API 接口。

中断处理则是驱动开发的另一个核心。传感器数据变化、按键按下,都需要通过中断来通知 CPU。轮询?那太浪费 CPU 资源了,尤其是在车载系统中,实时性要求很高。

GPIO 操作示例

// 申请 GPIO
int gpio_num = of_get_named_gpio(np, "led-gpios", 0);
if (gpio_num < 0) {
    dev_err(dev, "Failed to get GPIO\n");
    return gpio_num;
}

// 设置方向为输出
ret = gpio_direction_output(gpio_num, 0);
if (ret < 0) {
    dev_err(dev, "Failed to set GPIO direction\n");
    return ret;
}

// 输出高电平
gpio_set_value(gpio_num, 1);

// 释放 GPIO
gpio_free(gpio_num);

中断处理示例

// 申请中断
int irq = platform_get_irq(pdev, 0);
if (irq < 0) {
    dev_err(dev, "Failed to get IRQ\n");
    return irq;
}

// 注册中断处理函数
ret = request_irq(irq, my_irq_handler, 
                  IRQF_TRIGGER_RISING, 
                  "my_device", dev);
if (ret) {
    dev_err(dev, "Failed to request IRQ\n");
    return ret;
}

// 中断处理函数
static irqreturn_t my_irq_handler(int irq, void *dev_id) {
    struct my_device *dev = dev_id;
    
    // 读取传感器数据
    // 处理数据
    // 唤醒等待队列
    
    return IRQ_HANDLED;
}

// 释放中断
free_irq(irq, dev);

个人经验:中断处理函数里不要做耗时操作。我习惯在中断里只做“标记”,把实际的数据处理放到工作队列或 tasklet 中。这样能保证中断响应速度,避免丢失中断。

为什么会这样?因为中断上下文是不能睡眠的。如果你在中断里调用 msleep()mutex_lock(),内核会直接报错。嗯,这个坑我踩过,调试时系统莫名其妙卡死,最后发现是中断里调了 kmalloc(GFP_KERNEL)——这个函数可能会睡眠。

3.5 实战经验总结

好了,讲了这么多,我来总结几个关键点:

  1. 字符设备驱动是基础,file_operations 结构体要烂熟于心
  2. platform 驱动是设备与驱动的桥梁,匹配机制要理解透彻
  3. 设备树是硬件配置的说明书,解析时要仔细检查返回值
  4. GPIO 和中断是驱动与硬件交互的“双手”,中断上下文不能睡眠

我在车载项目中,经常需要同时处理多个传感器。每个传感器都有自己的中断,GPIO 资源也有限。这时候,设备树就派上大用场了——硬件配置一目了然,换传感器只需要改 DTS 文件,驱动代码基本不用动。

最后说一句:驱动开发没有捷径,多写代码、多调试、多踩坑,自然就熟练了。下次我们讲 I2C 和 SPI 总线驱动,这两个在车载传感器中也很常见。到时候再聊!