从硬编码按键驱动到 Linux Platform 设备树驱动:逐行解剖与融会贯通

从硬编码按键驱动到 Linux Platform 设备树驱动:逐行解剖与融会贯通

这是一篇万字长文,用"费曼学习法"的方式,带你从最原始的按键驱动一步步升级为标准 platform 驱动,并彻底弄懂设备树的来龙去脉。

你会看到每一行新增代码的含义、每一条匹配规则的用意、每一个内核机制的背后逻辑。

如果你能坚持读完并完成最后的自测题,你将有自信说:"我把 Linux 设备驱动模型的核心吃透了。"


文章目录

  • [从硬编码按键驱动到 Linux Platform 设备树驱动:逐行解剖与融会贯通](#从硬编码按键驱动到 Linux Platform 设备树驱动:逐行解剖与融会贯通)
    • 一、从哪里开始?一份写死的按键驱动
    • [二、改造的目标:platform 驱动 + 设备树](#二、改造的目标:platform 驱动 + 设备树)
    • [三、platform 驱动匹配机制------让内核自动调用你的 probe](#三、platform 驱动匹配机制——让内核自动调用你的 probe)
    • [四、逐行解剖 `probe`:硬件初始化的新家](#四、逐行解剖 probe:硬件初始化的新家)
      • [4.1 函数原型与局部变量](#4.1 函数原型与局部变量)
      • [4.2 分支一:设备树方式](#4.2 分支一:设备树方式)
      • [4.3 分支二:传统平台设备方式](#4.3 分支二:传统平台设备方式)
      • [4.4 统一的硬件初始化](#4.4 统一的硬件初始化)
      • [4.5 字符设备注册](#4.5 字符设备注册)
    • [五、`remove` 函数------有借有还](#五、remove 函数——有借有还)
    • 六、中断与去抖动:上半部与下半部的经典组合
      • [6.1 中断上半部:`gpio_key_isr`](#6.1 中断上半部:gpio_key_isr)
      • [6.2 下半部:定时器回到函数 `key_timer_expire`](#6.2 下半部:定时器回到函数 key_timer_expire)
    • 七、文件操作接口详解
      • [7.1 `read`:阻塞与非阻塞](#7.1 read:阻塞与非阻塞)
      • [7.2 `write`:控制 GPIO 输出](#7.2 write:控制 GPIO 输出)
      • [7.3 `poll`](#7.3 poll)
      • [7.4 `fasync`:异步通知](#7.4 fasync:异步通知)
    • [八、设备树深度理解:从 DTS 到 platform_device](#八、设备树深度理解:从 DTS 到 platform_device)
      • [8.1 设备树的结构](#8.1 设备树的结构)
      • [8.2 内核处理设备树流程](#8.2 内核处理设备树流程)
      • [8.3 驱动里使用设备树 API](#8.3 驱动里使用设备树 API)
    • [九、将新旧代码合二为一:你的 driver 就该这么写](#九、将新旧代码合二为一:你的 driver 就该这么写)
    • [十、总结:从硬编码到 platform 驱动的思维跃迁](#十、总结:从硬编码到 platform 驱动的思维跃迁)
    • 十一、自测题(检验你到底学会没)
    • 一、从哪里开始?一份写死的按键驱动
    • [二、改造的目标:platform 驱动 + 设备树](#二、改造的目标:platform 驱动 + 设备树)
    • [三、platform 驱动匹配机制------让内核自动调用你的 probe](#三、platform 驱动匹配机制——让内核自动调用你的 probe)
    • [四、逐行解剖 `probe`:硬件初始化的新家](#四、逐行解剖 probe:硬件初始化的新家)
      • [4.1 函数原型与局部变量](#4.1 函数原型与局部变量)
      • [4.2 分支一:设备树方式](#4.2 分支一:设备树方式)
      • [4.3 分支二:传统平台设备方式](#4.3 分支二:传统平台设备方式)
      • [4.4 统一的硬件初始化](#4.4 统一的硬件初始化)
      • [4.5 字符设备注册](#4.5 字符设备注册)
    • [五、`remove` 函数------有借有还](#五、remove 函数——有借有还)
    • 六、中断与去抖动:上半部与下半部的经典组合
      • [6.1 中断上半部:`gpio_key_isr`](#6.1 中断上半部:gpio_key_isr)
      • [6.2 下半部:定时器回到函数 `key_timer_expire`](#6.2 下半部:定时器回到函数 key_timer_expire)
    • 七、文件操作接口详解
      • [7.1 `read`:阻塞与非阻塞](#7.1 read:阻塞与非阻塞)
      • [7.2 `write`:控制 GPIO 输出](#7.2 write:控制 GPIO 输出)
      • [7.3 `poll`](#7.3 poll)
      • [7.4 `fasync`:异步通知](#7.4 fasync:异步通知)
    • [八、设备树深度理解:从 DTS 到 platform_device](#八、设备树深度理解:从 DTS 到 platform_device)
      • [8.1 设备树的结构](#8.1 设备树的结构)
      • [8.2 内核处理设备树流程](#8.2 内核处理设备树流程)
      • [8.3 驱动里使用设备树 API](#8.3 驱动里使用设备树 API)
    • [九、将新旧代码合二为一:你的 driver 就该这么写](#九、将新旧代码合二为一:你的 driver 就该这么写)
    • [十、总结:从硬编码到 platform 驱动的思维跃迁](#十、总结:从硬编码到 platform 驱动的思维跃迁)
    • 十一、自测题(检验你到底学会没)

一、从哪里开始?一份写死的按键驱动

我们先上一个最常见的按键驱动写法(为了聚焦改动,这里只保留核心逻辑):

c

复制代码
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/gpio.h>
#include <linux/interrupt.h>
#include <linux/timer.h>
// ...... 其他头文件省略

struct gpio_desc {
    int gpio;
    int irq;
    char *name;
    int key;
    struct timer_list key_timer;
};

static struct gpio_desc gpios[2] = {
    {131, 0, "gpio_100ask_1", },
    {132, 0, "gpio_100ask_2", },
};

static int major;
static struct class *gpio_class;

#define BUF_LEN 128
static int g_keys[BUF_LEN];
static int r, w;
static DECLARE_WAIT_QUEUE_HEAD(gpio_wait);
struct fasync_struct *button_fasync;

// 环形缓冲区、读、写、poll、fasync 等函数
// ...

static irqreturn_t gpio_key_isr(int irq, void *dev_id)
{
    struct gpio_desc *gpio_desc = dev_id;
    mod_timer(&gpio_desc->key_timer, jiffies + HZ/5);
    return IRQ_HANDLED;
}

static void key_timer_expire(unsigned long data)
{
    struct gpio_desc *gpio_desc = (struct gpio_desc *)data;
    int val = gpio_get_value(gpio_desc->gpio);
    int key = (gpio_desc->key) | (val << 8);
    put_key(key);
    wake_up_interruptible(&gpio_wait);
    kill_fasync(&button_fasync, SIGIO, POLL_IN);
}

static int __init gpio_drv_init(void)
{
    int i, count = ARRAY_SIZE(gpios);

    for (i = 0; i < count; i++) {
        gpios[i].irq = gpio_to_irq(gpios[i].gpio);
        setup_timer(&gpios[i].key_timer, key_timer_expire,
                    (unsigned long)&gpios[i]);
        gpios[i].key_timer.expires = ~0;
        add_timer(&gpios[i].key_timer);
        request_irq(gpios[i].irq, gpio_key_isr,
                    IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,
                    "100ask_gpio_key", &gpios[i]);
    }

    major = register_chrdev(0, "100ask_gpio_key", &gpio_key_drv);
    gpio_class = class_create(THIS_MODULE, "100ask_gpio_key_class");
    device_create(gpio_class, NULL, MKDEV(major, 0), NULL, "100ask_gpio");
    return 0;
}

static void __exit gpio_drv_exit(void)
{
    int i;
    device_destroy(gpio_class, MKDEV(major, 0));
    class_destroy(gpio_class);
    unregister_chrdev(major, "100ask_gpio_key");
    for (i = 0; i < ARRAY_SIZE(gpios); i++) {
        free_irq(gpios[i].irq, &gpios[i]);
        del_timer(&gpios[i].key_timer);
    }
}

module_init(gpio_drv_init);
module_exit(gpio_drv_exit);
MODULE_LICENSE("GPL");

这份代码能跑,但有个致命问题:GPIO 引脚号 131132 写死在数组里。换个板子就要改代码、重新编译,毫无灵活性可言。

二、改造的目标:platform 驱动 + 设备树

我们希望达到的效果是:驱动程序不变,硬件信息由设备树或板级配置文件给出。这样同一份驱动可以支持不同板卡。

新代码的整体骨架变成了:

旧代码 新代码
固定数组 gpios[2] 动态指针 struct gpio_desc *gpios + 全局 int count
module_init 里完成所有初始化 module_init 只注册 platform_driver
出口函数直接释放资源 出口函数只注销 platform_driver,释放资源放到 .remove 函数里
无匹配机制 通过 compatible 或平台设备名进行匹配

结构体也做了优化:

c

复制代码
struct gpio_desc {
    int gpio;
    int irq;
    char name[128];
    int key;
    struct timer_list key_timer;
};

char *name 改成 char name[128],是因为我们后面要动态生成类似 "button_pin_0" 的名字,如果还是用指针,就需要额外为每个名字单独分配内存。把名字内嵌到结构体里,省心省力。


三、platform 驱动匹配机制------让内核自动调用你的 probe

在 Linux 内核中,platform 总线是一个虚拟总线,挂载着许多 platform 设备和驱动。匹配规则在 platform_match() 函数中(源码位于 drivers/base/platform.c),依次尝试四种方式:

  1. 强制覆盖driver_override 字段)
  2. 设备树匹配 (比较 of_nodeof_match_table
  3. ACPI 匹配
  4. 平台设备 ID 表匹配id_table
  5. 兜底 :直接比较设备名和驱动名(pdev->name vs drv->driver.name

在我们的新驱动里,我们要让内核根据设备树信息来匹配硬件和驱动。做法是定义一个 of_device_id 表,并把它嵌入到 platform_driver 结构体中:

c

复制代码
static const struct of_device_id gpio_dt_ids[] = {
    { .compatible = "100ask,gpiodemo", },
    { /* sentinel */ }
};

static struct platform_driver gpio_platform_driver = {
    .driver = {
        .name = "100ask_gpio_plat_drv",
        .of_match_table = gpio_dt_ids,
    },
    .probe  = gpio_drv_probe,
    .remove = gpio_drv_remove,
};

当设备树里有这样的节点:

dts

复制代码
button {
    compatible = "100ask,gpiodemo";
    gpios = <&gpio5 5 GPIO_ACTIVE_HIGH>,
            <&gpio5 3 GPIO_ACTIVE_HIGH>;
};

内核解析设备树时,会为 button 节点创建一个 platform_device,其 dev.of_node 指向该节点。

然后当我们的驱动通过 platform_driver_register 注册时,匹配机制就会找到 compatible 匹配,于是内核调用 gpio_drv_probe()


四、逐行解剖 probe:硬件初始化的新家

probe 函数是整个新架构的核心,它把原先 gpio_drv_init 中的硬件初始化部分迁移了过来,并增加了动态获取 GPIO 的逻辑。

4.1 函数原型与局部变量

c

复制代码
static int gpio_drv_probe(struct platform_device *pdev)
{
    int err = 0;
    int i;
    struct device_node *np = pdev->dev.of_node;
    struct resource *res;
  • pdev:内核为我们匹配成功的 platform_device 指针,包含了设备资源与设备树节点信息。
  • np:如果设备来自设备树,pdev->dev.of_node 非空;若来自传统的 C 文件注册的平台设备,则为 NULL。这个 NULL 值将决定我们走哪条分支。
  • res:在处理平台设备资源时的临时变量。

4.2 分支一:设备树方式

c

复制代码
if (np) {
    count = of_gpio_count(np);
    if (!count)
        return -EINVAL;

    gpios = kmalloc(count * sizeof(struct gpio_desc), GFP_KERNEL);
    for (i = 0; i < count; i++) {
        gpios[i].gpio = of_get_gpio(np, i);
        sprintf(gpios[i].name, "%s_pin_%d", np->name, i);
    }
}

逐一解释:

  • count = of_gpio_count(np);
    解析设备树节点的 gpios 属性,统计里面包含多少个 GPIO 定义。如果节点写的是 gpios = <...>, <...>; ,那 count 就是 2。
  • if (!count) return -EINVAL;
    如果一个 GPIO 都没有,驱动无法工作,报错返回。
  • gpios = kmalloc(...)
    按照实际个数动态分配内存。注意:后面在 remove 里必须用 kfree(gpios) 释放。
  • gpios[i].gpio = of_get_gpio(np, i);
    从设备树的 gpios 属性中取出第 i 个引脚的 GPIO 编号。比如 gpios = <&gpio5 5 ...>,经过 of_get_gpio 就会翻译成内核内部的 GPIO 号(比如 133)。
  • sprintf(gpios[i].name, "%s_pin_%d", np->name, i);
    生成一个可读的名字,如 "button_pin_0""button_pin_1"。这就是为何我们结构体中将 name 改成了 128 字节数组的原因------直接用 sprintf 写入,无需额外分配。

4.3 分支二:传统平台设备方式

如果板级文件用老式方法注册了 platform_device(例如定义了 resource 数组),np 为 NULL,我们走 else 分支:

c

复制代码
else {
    count = 0;
    while (1) {
        res = platform_get_resource(pdev, IORESOURCE_IRQ, count);
        if (res) count++;
        else break;
    }

    if (!count) return -EINVAL;

    gpios = kmalloc(count * sizeof(struct gpio_desc), GFP_KERNEL);
    for (i = 0; i < count; i++) {
        res = platform_get_resource(pdev, IORESOURCE_IRQ, i);
        gpios[i].gpio = res->start;
        sprintf(gpios[i].name, "%s_pin_%d", pdev->name, i);
    }
}

解释:

  • 第一个 while 循环:通过不断调用 platform_get_resource(pdev, IORESOURCE_IRQ, index) 来数出有多少个 IRQ 类型的资源。这里约定:老平台文件用 IRQ 资源的 start 字段来存放 GPIO 编号(一种历史包袱的做法)。
  • 计数结束后,同样用 kmalloc 分配内存。
  • for 循环里,再次调用 platform_get_resource 获得每个资源,取出 res->start 填入 gpios[i].gpio;名字则用 pdev->name 加上索引生成。

4.4 统一的硬件初始化

不管哪个分支,最后我们都得到了一个填充好的 gpios 数组,以及全局变量 count。接下来这些代码和旧版本几乎一样:

c

复制代码
for (i = 0; i < count; i++) {
    gpios[i].irq  = gpio_to_irq(gpios[i].gpio);

    setup_timer(&gpios[i].key_timer, key_timer_expire,
                (unsigned long)&gpios[i]);
    gpios[i].key_timer.expires = ~0;
    add_timer(&gpios[i].key_timer);
    err = request_irq(gpios[i].irq, gpio_key_isr,
                      IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,
                      "100ask_gpio_key", &gpios[i]);
}
  • gpio_to_irq:将 GPIO 号转换成对应的硬件中断号。
  • setup_timer:初始化定时器,绑定回调函数 key_timer_expire,并把当前 gpio_desc 的地址作为参数传给回调。
  • expires = ~0:将定时器的到期时间设为最大值(unsigned long 全 1),相当于激活了一个永不到期的定时器。真正触发靠的是 ISR 里的 mod_timer
  • request_irq:注册中断处理函数 gpio_key_isr,触发方式为双边沿(上升沿和下降沿都触发),最后一个参数 &gpios[i] 会作为 dev_id 传给 ISR,以便区分是哪个按键。

4.5 字符设备注册

紧接着是字符设备的注册:

c

复制代码
major = register_chrdev(0, "100ask_gpio_key", &gpio_key_drv);

gpio_class = class_create(THIS_MODULE, "100ask_gpio_key_class");
if (IS_ERR(gpio_class)) {
    unregister_chrdev(major, "100ask_gpio_key");
    return PTR_ERR(gpio_class);
}

device_create(gpio_class, NULL, MKDEV(major, 0), NULL, "100ask_gpio");

这里的逻辑和老版本一模一样,只是把它从 init 移到了 probe 里,确保在硬件资源确认可用后,才创建用户接口 /dev/100ask_gpio


五、remove 函数------有借有还

新架构的 remove 对应旧的 exit,但必须增加 kfree 释放动态内存:

c

复制代码
static int gpio_drv_remove(struct platform_device *pdev)
{
    int i;

    device_destroy(gpio_class, MKDEV(major, 0));
    class_destroy(gpio_class);
    unregister_chrdev(major, "100ask_gpio_key");

    for (i = 0; i < count; i++) {
        free_irq(gpios[i].irq, &gpios[i]);
        del_timer(&gpios[i].key_timer);
    }
    kfree(gpios);   // 对应 probe 里的 kmalloc
    return 0;
}

注意销毁顺序与 probe 相反:先销毁设备节点,再释放中断(避免 ISR 触发时设备节点已不存在),然后删除定时器,最后释放内存。


六、中断与去抖动:上半部与下半部的经典组合

按键存在机械抖动,硬件触发一次按下可能产生多次电平跳变。我们的驱动采用"中断上半部 + 定时器下半部"的思路来消除抖动。

6.1 中断上半部:gpio_key_isr

c

复制代码
static irqreturn_t gpio_key_isr(int irq, void *dev_id)
{
    struct gpio_desc *gpio_desc = dev_id;
    mod_timer(&gpio_desc->key_timer, jiffies + HZ/5);
    return IRQ_HANDLED;
}
  • dev_id 就是 request_irq 时传入的 &gpios[i],这样 ISR 就知道是哪个 GPIO 发生了中断。
  • mod_timer 将定时器的到期时间改为 当前时间 + 200ms(假设 HZ=100HZ/5=20 滴答)。即便在 200ms 内来了多次抖动中断,也只是反复覆盖同一个定时器的到期时间,保证只会在最后一次跳变后的 200ms 才真正读取电平。

6.2 下半部:定时器回到函数 key_timer_expire

200ms 后,内核调用:

c

复制代码
static void key_timer_expire(unsigned long data)
{
    struct gpio_desc *gpio_desc = (struct gpio_desc *)data;
    int val = gpio_get_value(gpio_desc->gpio);
    int key = (gpio_desc->key) | (val << 8);
    put_key(key);
    wake_up_interruptible(&gpio_wait);
    kill_fasync(&button_fasync, SIGIO, POLL_IN);
}
  • gpio_get_value 读取稳定的按键电平。
  • 键码 key 低 8 位是按键标识(本例中 gpio_desc->key 一直为 0,实际产品可赋值为 KEY_A 等),bit8 为当前电平(1 表示高电平)。
  • put_key 将键码存入环形缓冲区。
  • wake_up_interruptible 唤醒因 read 阻塞的进程。
  • kill_fasync 向设置了异步通知的进程发送 SIGIO 信号。

这样,一个完整的硬件中断 → 消抖 → 数据分发的链条就打通了。


七、文件操作接口详解

驱动最终是要让用户程序使用的,因此还需要实现 readwritepollfasync

7.1 read:阻塞与非阻塞

c

复制代码
static ssize_t gpio_drv_read (struct file *file, char __user *buf,
                              size_t size, loff_t *offset)
{
    int key;

    if (is_key_buf_empty() && (file->f_flags & O_NONBLOCK))
        return -EAGAIN;

    wait_event_interruptible(gpio_wait, !is_key_buf_empty());
    key = get_key();
    copy_to_user(buf, &key, 4);
    return 4;
}
  • 若缓冲区空且用户以非阻塞(O_NONBLOCK)方式打开,则立即返回 -EAGAIN
  • 否则,进程在 gpio_wait 上睡眠,直到缓冲区非空(被定时器回调唤醒)。
  • 唤醒后取出一个 int 型按键值,通过 copy_to_user 拷贝给用户空间,返回 4 字节。

7.2 write:控制 GPIO 输出

c

复制代码
static ssize_t gpio_drv_write(struct file *file, const char __user *buf,
                              size_t size, loff_t *offset)
{
    unsigned char ker_buf[2];
    if (size != 2) return -EINVAL;

    copy_from_user(ker_buf, buf, 2);
    if (ker_buf[0] >= count) return -EINVAL;   // 用 count 而不是 sizeof(gpios)!

    gpio_set_value(gpios[ker_buf[0]].gpio, ker_buf[1]);
    return 2;
}
  • 用户写入两个字节:第一个是 GPIO 索引,第二个是输出值(0 或非 0)。
  • 注意 :旧代码里用的是 ker_buf[0] >= sizeof(gpios)/sizeof(gpios[0]),但新代码中 gpios 是指针,sizeof(gpios) 只会得到指针长度,必须改用全局的 count

7.3 poll

c

复制代码
static unsigned int gpio_drv_poll(struct file *fp, poll_table *wait)
{
    poll_wait(fp, &gpio_wait, wait);
    return is_key_buf_empty() ? 0 : POLLIN | POLLRDNORM;
}
  • poll_wait 把当前进程加入到驱动私有的等待队列 gpio_wait,但不立即休眠。
  • 返回值为 0 表示没有数据可读;返回 POLLIN | POLLRDNORM 表示有数据,应用程序的 select/poll 可以继续处理。

7.4 fasync:异步通知

c

复制代码
static int gpio_drv_fasync(int fd, struct file *file, int on)
{
    return fasync_helper(fd, file, on, &button_fasync) >= 0 ? 0 : -EIO;
}

用户程序通过 fcntl(fd, F_SETFL, FASYNC) 调用时,fasync_helper 会把进程记录到 button_fasync 链表中。之后在定时器回调执行 kill_fasync 时,会向链表里的进程发送 SIGIO 信号,通知它们有数据可读。


八、设备树深度理解:从 DTS 到 platform_device

设备树是驱动硬件配置分离的关键技术。它的基本语法我们已经在前文片段里看到过,现在更系统地梳理。

8.1 设备树的结构

dts

复制代码
/dts-v1/;

/ {
    model = "fsl,mpc8572ds";
    compatible = "fsl,mpc8572ds";
    #address-cells = <1>;
    #size-cells = <1>;

    cpus {
        cpu@0 { ... };
        cpu@1 { ... };
    };

    memory@0 {
        device_type = "memory";
        reg = <0 0x20000000>;
    };

    uart@fe001000 {
        compatible = "ns16550";
        reg = <0xffe001000 0x100>;
    };

    chosen { bootargs = "..."; };
    aliases { serial0 = "/uart@fe001000"; };
};

节点命名形式为 名称[@地址]。属性值可以是整数、字符串、字节序列,以及它们的混合。compatible 是最重要的匹配属性,形式为 "厂商,型号"

8.2 内核处理设备树流程

DTS 编译成 DTB,由 U-Boot 传递给内核。内核解析 DTB 为 device_node 树,然后挑选一部分节点转换为 platform_device,同时提取 reginterrupts 属性作为设备资源。

哪些节点会被转换?

  • 根节点的子节点且具备 compatible 属性;
  • 当父节点 compatible"simple-bus" 等值时,其子节点也会被转换;
  • I2C、SPI 控制器下的子节点不作为 platform_device,而是由对应总线驱动生成 i2c_clientspi_device

8.3 驱动里使用设备树 API

在我们的 probe 中已经用到了 of_gpio_countof_get_gpio,这些都是设备树辅助函数。此外常用的还有:

  • of_property_read_u32(np, "propname", &val)
  • of_find_node_by_path("/memory")
  • irq_of_parse_and_map(np, index) 直接从设备树中断属性获取 IRQ 号。

这些函数都定义在 include/linux/of_*.h 中。


九、将新旧代码合二为一:你的 driver 就该这么写

把上面的所有改动拼合起来,完整的 gpio_drv_probe 应该像下面这样:

c

复制代码
static int gpio_drv_probe(struct platform_device *pdev)
{
    int err = 0, i;
    struct device_node *np = pdev->dev.of_node;
    struct resource *res;

    if (np) {
        count = of_gpio_count(np);
        if (!count) return -EINVAL;
        gpios = kmalloc(count * sizeof(*gpios), GFP_KERNEL);
        for (i = 0; i < count; i++) {
            gpios[i].gpio = of_get_gpio(np, i);
            sprintf(gpios[i].name, "%s_pin_%d", np->name, i);
        }
    } else {
        count = 0;
        while (platform_get_resource(pdev, IORESOURCE_IRQ, count))
            count++;
        if (!count) return -EINVAL;
        gpios = kmalloc(count * sizeof(*gpios), GFP_KERNEL);
        for (i = 0; i < count; i++) {
            res = platform_get_resource(pdev, IORESOURCE_IRQ, i);
            gpios[i].gpio = res->start;
            sprintf(gpios[i].name, "%s_pin_%d", pdev->name, i);
        }
    }

    for (i = 0; i < count; i++) {
        gpios[i].irq = gpio_to_irq(gpios[i].gpio);
        setup_timer(&gpios[i].key_timer, key_timer_expire,
                    (unsigned long)&gpios[i]);
        gpios[i].key_timer.expires = ~0;
        add_timer(&gpios[i].key_timer);
        err = request_irq(gpios[i].irq, gpio_key_isr,
                          IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,
                          "100ask_gpio_key", &gpios[i]);
        // 实际产品中应检查 err 并处理错误
    }

    major = register_chrdev(0, "100ask_gpio_key", &gpio_key_drv);
    gpio_class = class_create(THIS_MODULE, "100ask_gpio_key_class");
    if (IS_ERR(gpio_class)) {
        unregister_chrdev(major, "100ask_gpio_key");
        return PTR_ERR(gpio_class);
    }
    device_create(gpio_class, NULL, MKDEV(major, 0), NULL, "100ask_gpio");

    return 0;
}

加上对应的 removeplatform_driver 定义,以及已经讲过的文件操作函数,这就是一份符合规范的、支持设备树的外部按键驱动。


十、总结:从硬编码到 platform 驱动的思维跃迁

  1. 设备树只描述硬件,不写逻辑;驱动程序通过 API 读取硬件信息。
  2. platform 驱动模型 分离了设备与驱动,通过 compatible 等机制自动匹配。
  3. 中断上下半部是处理实时事件的惯用模式:上半部快进快出,下半部完成实际数据处理。
  4. 环形缓冲区 + 等待队列 + 异步通知构成了用户空间获取数据的三种模式(阻塞、非阻塞、异步)。

掌握了这些,再去研究 I2C、SPI、USB 驱动,你会发现框架如出一辙。


十一、自测题(检验你到底学会没)

  1. 概念 :在设备树中,compatible 属性的作用是什么?
  2. 填空#address-cells = <1>; #size-cells = <1>; 表示子节点的 reg 属性用 _____ 个 32 位数描述地址,用 _____ 个 32 位数描述大小。
  3. 代码分析 :在 gpio_drv_probe 的设备树分支中,如果 of_gpio_count 返回 0,函数会怎么做?
  4. 找错 :旧代码 write 中的边界检查是 ker_buf[0] >= sizeof(gpios)/sizeof(gpios[0]),为什么在新代码中会出错?应该改成什么?
  5. 流程 :描述一个按键按下后,从硬件中断到用户 read 返回数据的完整路径。
  6. 设计 :如果想增加 LED 控制,复用现有 write 接口,你会如何约定用户写入的数据格式?
  7. 代码补全 :在 remove 函数中,释放 GPIO 数组内存的正确语句是什么?
  8. 匹配顺序 :请按顺序写出 platform_match 函数尝试的 4 种匹配方式。
  9. 实战 :在开发板上执行 ls /sys/firmware/devicetree/,找到对应 compatible = "100ask,gpiodemo" 的节点,查看它的 gpios 属性值,写出 hexdumpcat 的输出。
  10. 综合改造 :如果将驱动改为同时支持 5 个按键,且键码分别为 KEY_AKEY_B...,你需要改动哪些地方?(不需要具体代码,列出改动点即可)

参考答案(请先尽力完成后核对)

  1. 用于与驱动匹配,内核根据它找到对应的 platform_driver
  2. 1 个,1 个。
  3. 返回 -EINVAL,表明没有可用的 GPIO 资源。
  4. gpios 变成了指针,sizeof(gpios) 是指针长度,应改为 ker_buf[0] >= count
  5. 硬件中断 → ISR 里 mod_timer → 200ms 后定时器回调 → 读取电平、存缓冲区 → wake_up 唤醒 read → 用户层 read 返回键值。
  6. 写两个字节:[LED索引, 亮度/状态],驱动根据索引调用 gpio_set_value 或 PWM 接口。
  7. kfree(gpios);
  8. driver_override 强制匹配;② 设备树兼容性匹配;③ ACPI 匹配;④ ID 表匹配或名字直接匹配。
  9. 具体输出取决于板子的设备树,例如 cat compatible 得到 100ask,gpiodemohexdump gpios 显示 GPIO 编号。
  10. 改动点:设备树中增加 5 个 gpios 条目;在 probe 中为每个 gpio_desc.key 赋值(如 gpios[i].key = KEY_A + i);确认定时器回调中 key 组合正确;无需改动驱动框架代码。

这篇博客到这里就结束了。希望它能成为你常翻常新的参考手册。如果觉得有用,欢迎分享给正在学习 Linux 驱动的朋友。# 从硬编码按键驱动到 Linux Platform 设备树驱动:逐行解剖与融会贯通

这是一篇万字长文,用"费曼学习法"的方式,带你从最原始的按键驱动一步步升级为标准 platform 驱动,并彻底弄懂设备树的来龙去脉。

你会看到每一行新增代码的含义、每一条匹配规则的用意、每一个内核机制的背后逻辑。

如果你能坚持读完并完成最后的自测题,你将有自信说:"我把 Linux 设备驱动模型的核心吃透了。"


一、从哪里开始?一份写死的按键驱动

我们先上一个最常见的按键驱动写法(为了聚焦改动,这里只保留核心逻辑):

c

复制代码
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/gpio.h>
#include <linux/interrupt.h>
#include <linux/timer.h>
// ...... 其他头文件省略

struct gpio_desc {
    int gpio;
    int irq;
    char *name;
    int key;
    struct timer_list key_timer;
};

static struct gpio_desc gpios[2] = {
    {131, 0, "gpio_100ask_1", },
    {132, 0, "gpio_100ask_2", },
};

static int major;
static struct class *gpio_class;

#define BUF_LEN 128
static int g_keys[BUF_LEN];
static int r, w;
static DECLARE_WAIT_QUEUE_HEAD(gpio_wait);
struct fasync_struct *button_fasync;

// 环形缓冲区、读、写、poll、fasync 等函数
// ...

static irqreturn_t gpio_key_isr(int irq, void *dev_id)
{
    struct gpio_desc *gpio_desc = dev_id;
    mod_timer(&gpio_desc->key_timer, jiffies + HZ/5);
    return IRQ_HANDLED;
}

static void key_timer_expire(unsigned long data)
{
    struct gpio_desc *gpio_desc = (struct gpio_desc *)data;
    int val = gpio_get_value(gpio_desc->gpio);
    int key = (gpio_desc->key) | (val << 8);
    put_key(key);
    wake_up_interruptible(&gpio_wait);
    kill_fasync(&button_fasync, SIGIO, POLL_IN);
}

static int __init gpio_drv_init(void)
{
    int i, count = ARRAY_SIZE(gpios);

    for (i = 0; i < count; i++) {
        gpios[i].irq = gpio_to_irq(gpios[i].gpio);
        setup_timer(&gpios[i].key_timer, key_timer_expire,
                    (unsigned long)&gpios[i]);
        gpios[i].key_timer.expires = ~0;
        add_timer(&gpios[i].key_timer);
        request_irq(gpios[i].irq, gpio_key_isr,
                    IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,
                    "100ask_gpio_key", &gpios[i]);
    }

    major = register_chrdev(0, "100ask_gpio_key", &gpio_key_drv);
    gpio_class = class_create(THIS_MODULE, "100ask_gpio_key_class");
    device_create(gpio_class, NULL, MKDEV(major, 0), NULL, "100ask_gpio");
    return 0;
}

static void __exit gpio_drv_exit(void)
{
    int i;
    device_destroy(gpio_class, MKDEV(major, 0));
    class_destroy(gpio_class);
    unregister_chrdev(major, "100ask_gpio_key");
    for (i = 0; i < ARRAY_SIZE(gpios); i++) {
        free_irq(gpios[i].irq, &gpios[i]);
        del_timer(&gpios[i].key_timer);
    }
}

module_init(gpio_drv_init);
module_exit(gpio_drv_exit);
MODULE_LICENSE("GPL");

这份代码能跑,但有个致命问题:GPIO 引脚号 131132 写死在数组里。换个板子就要改代码、重新编译,毫无灵活性可言。

二、改造的目标:platform 驱动 + 设备树

我们希望达到的效果是:驱动程序不变,硬件信息由设备树或板级配置文件给出。这样同一份驱动可以支持不同板卡。

新代码的整体骨架变成了:

旧代码 新代码
固定数组 gpios[2] 动态指针 struct gpio_desc *gpios + 全局 int count
module_init 里完成所有初始化 module_init 只注册 platform_driver
出口函数直接释放资源 出口函数只注销 platform_driver,释放资源放到 .remove 函数里
无匹配机制 通过 compatible 或平台设备名进行匹配

结构体也做了优化:

c

复制代码
struct gpio_desc {
    int gpio;
    int irq;
    char name[128];
    int key;
    struct timer_list key_timer;
};

char *name 改成 char name[128],是因为我们后面要动态生成类似 "button_pin_0" 的名字,如果还是用指针,就需要额外为每个名字单独分配内存。把名字内嵌到结构体里,省心省力。


三、platform 驱动匹配机制------让内核自动调用你的 probe

在 Linux 内核中,platform 总线是一个虚拟总线,挂载着许多 platform 设备和驱动。匹配规则在 platform_match() 函数中(源码位于 drivers/base/platform.c),依次尝试四种方式:

  1. 强制覆盖driver_override 字段)
  2. 设备树匹配 (比较 of_nodeof_match_table
  3. ACPI 匹配
  4. 平台设备 ID 表匹配id_table
  5. 兜底 :直接比较设备名和驱动名(pdev->name vs drv->driver.name

在我们的新驱动里,我们要让内核根据设备树信息来匹配硬件和驱动。做法是定义一个 of_device_id 表,并把它嵌入到 platform_driver 结构体中:

c

复制代码
static const struct of_device_id gpio_dt_ids[] = {
    { .compatible = "100ask,gpiodemo", },
    { /* sentinel */ }
};

static struct platform_driver gpio_platform_driver = {
    .driver = {
        .name = "100ask_gpio_plat_drv",
        .of_match_table = gpio_dt_ids,
    },
    .probe  = gpio_drv_probe,
    .remove = gpio_drv_remove,
};

当设备树里有这样的节点:

dts

复制代码
button {
    compatible = "100ask,gpiodemo";
    gpios = <&gpio5 5 GPIO_ACTIVE_HIGH>,
            <&gpio5 3 GPIO_ACTIVE_HIGH>;
};

内核解析设备树时,会为 button 节点创建一个 platform_device,其 dev.of_node 指向该节点。

然后当我们的驱动通过 platform_driver_register 注册时,匹配机制就会找到 compatible 匹配,于是内核调用 gpio_drv_probe()


四、逐行解剖 probe:硬件初始化的新家

probe 函数是整个新架构的核心,它把原先 gpio_drv_init 中的硬件初始化部分迁移了过来,并增加了动态获取 GPIO 的逻辑。

4.1 函数原型与局部变量

c

复制代码
static int gpio_drv_probe(struct platform_device *pdev)
{
    int err = 0;
    int i;
    struct device_node *np = pdev->dev.of_node;
    struct resource *res;
  • pdev:内核为我们匹配成功的 platform_device 指针,包含了设备资源与设备树节点信息。
  • np:如果设备来自设备树,pdev->dev.of_node 非空;若来自传统的 C 文件注册的平台设备,则为 NULL。这个 NULL 值将决定我们走哪条分支。
  • res:在处理平台设备资源时的临时变量。

4.2 分支一:设备树方式

c

复制代码
if (np) {
    count = of_gpio_count(np);
    if (!count)
        return -EINVAL;

    gpios = kmalloc(count * sizeof(struct gpio_desc), GFP_KERNEL);
    for (i = 0; i < count; i++) {
        gpios[i].gpio = of_get_gpio(np, i);
        sprintf(gpios[i].name, "%s_pin_%d", np->name, i);
    }
}

逐一解释:

  • count = of_gpio_count(np);
    解析设备树节点的 gpios 属性,统计里面包含多少个 GPIO 定义。如果节点写的是 gpios = <...>, <...>; ,那 count 就是 2。
  • if (!count) return -EINVAL;
    如果一个 GPIO 都没有,驱动无法工作,报错返回。
  • gpios = kmalloc(...)
    按照实际个数动态分配内存。注意:后面在 remove 里必须用 kfree(gpios) 释放。
  • gpios[i].gpio = of_get_gpio(np, i);
    从设备树的 gpios 属性中取出第 i 个引脚的 GPIO 编号。比如 gpios = <&gpio5 5 ...>,经过 of_get_gpio 就会翻译成内核内部的 GPIO 号(比如 133)。
  • sprintf(gpios[i].name, "%s_pin_%d", np->name, i);
    生成一个可读的名字,如 "button_pin_0""button_pin_1"。这就是为何我们结构体中将 name 改成了 128 字节数组的原因------直接用 sprintf 写入,无需额外分配。

4.3 分支二:传统平台设备方式

如果板级文件用老式方法注册了 platform_device(例如定义了 resource 数组),np 为 NULL,我们走 else 分支:

c

复制代码
else {
    count = 0;
    while (1) {
        res = platform_get_resource(pdev, IORESOURCE_IRQ, count);
        if (res) count++;
        else break;
    }

    if (!count) return -EINVAL;

    gpios = kmalloc(count * sizeof(struct gpio_desc), GFP_KERNEL);
    for (i = 0; i < count; i++) {
        res = platform_get_resource(pdev, IORESOURCE_IRQ, i);
        gpios[i].gpio = res->start;
        sprintf(gpios[i].name, "%s_pin_%d", pdev->name, i);
    }
}

解释:

  • 第一个 while 循环:通过不断调用 platform_get_resource(pdev, IORESOURCE_IRQ, index) 来数出有多少个 IRQ 类型的资源。这里约定:老平台文件用 IRQ 资源的 start 字段来存放 GPIO 编号(一种历史包袱的做法)。
  • 计数结束后,同样用 kmalloc 分配内存。
  • for 循环里,再次调用 platform_get_resource 获得每个资源,取出 res->start 填入 gpios[i].gpio;名字则用 pdev->name 加上索引生成。

4.4 统一的硬件初始化

不管哪个分支,最后我们都得到了一个填充好的 gpios 数组,以及全局变量 count。接下来这些代码和旧版本几乎一样:

c

复制代码
for (i = 0; i < count; i++) {
    gpios[i].irq  = gpio_to_irq(gpios[i].gpio);

    setup_timer(&gpios[i].key_timer, key_timer_expire,
                (unsigned long)&gpios[i]);
    gpios[i].key_timer.expires = ~0;
    add_timer(&gpios[i].key_timer);
    err = request_irq(gpios[i].irq, gpio_key_isr,
                      IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,
                      "100ask_gpio_key", &gpios[i]);
}
  • gpio_to_irq:将 GPIO 号转换成对应的硬件中断号。
  • setup_timer:初始化定时器,绑定回调函数 key_timer_expire,并把当前 gpio_desc 的地址作为参数传给回调。
  • expires = ~0:将定时器的到期时间设为最大值(unsigned long 全 1),相当于激活了一个永不到期的定时器。真正触发靠的是 ISR 里的 mod_timer
  • request_irq:注册中断处理函数 gpio_key_isr,触发方式为双边沿(上升沿和下降沿都触发),最后一个参数 &gpios[i] 会作为 dev_id 传给 ISR,以便区分是哪个按键。

4.5 字符设备注册

紧接着是字符设备的注册:

c

复制代码
major = register_chrdev(0, "100ask_gpio_key", &gpio_key_drv);

gpio_class = class_create(THIS_MODULE, "100ask_gpio_key_class");
if (IS_ERR(gpio_class)) {
    unregister_chrdev(major, "100ask_gpio_key");
    return PTR_ERR(gpio_class);
}

device_create(gpio_class, NULL, MKDEV(major, 0), NULL, "100ask_gpio");

这里的逻辑和老版本一模一样,只是把它从 init 移到了 probe 里,确保在硬件资源确认可用后,才创建用户接口 /dev/100ask_gpio


五、remove 函数------有借有还

新架构的 remove 对应旧的 exit,但必须增加 kfree 释放动态内存:

c

复制代码
static int gpio_drv_remove(struct platform_device *pdev)
{
    int i;

    device_destroy(gpio_class, MKDEV(major, 0));
    class_destroy(gpio_class);
    unregister_chrdev(major, "100ask_gpio_key");

    for (i = 0; i < count; i++) {
        free_irq(gpios[i].irq, &gpios[i]);
        del_timer(&gpios[i].key_timer);
    }
    kfree(gpios);   // 对应 probe 里的 kmalloc
    return 0;
}

注意销毁顺序与 probe 相反:先销毁设备节点,再释放中断(避免 ISR 触发时设备节点已不存在),然后删除定时器,最后释放内存。


六、中断与去抖动:上半部与下半部的经典组合

按键存在机械抖动,硬件触发一次按下可能产生多次电平跳变。我们的驱动采用"中断上半部 + 定时器下半部"的思路来消除抖动。

6.1 中断上半部:gpio_key_isr

c

复制代码
static irqreturn_t gpio_key_isr(int irq, void *dev_id)
{
    struct gpio_desc *gpio_desc = dev_id;
    mod_timer(&gpio_desc->key_timer, jiffies + HZ/5);
    return IRQ_HANDLED;
}
  • dev_id 就是 request_irq 时传入的 &gpios[i],这样 ISR 就知道是哪个 GPIO 发生了中断。
  • mod_timer 将定时器的到期时间改为 当前时间 + 200ms(假设 HZ=100HZ/5=20 滴答)。即便在 200ms 内来了多次抖动中断,也只是反复覆盖同一个定时器的到期时间,保证只会在最后一次跳变后的 200ms 才真正读取电平。

6.2 下半部:定时器回到函数 key_timer_expire

200ms 后,内核调用:

c

复制代码
static void key_timer_expire(unsigned long data)
{
    struct gpio_desc *gpio_desc = (struct gpio_desc *)data;
    int val = gpio_get_value(gpio_desc->gpio);
    int key = (gpio_desc->key) | (val << 8);
    put_key(key);
    wake_up_interruptible(&gpio_wait);
    kill_fasync(&button_fasync, SIGIO, POLL_IN);
}
  • gpio_get_value 读取稳定的按键电平。
  • 键码 key 低 8 位是按键标识(本例中 gpio_desc->key 一直为 0,实际产品可赋值为 KEY_A 等),bit8 为当前电平(1 表示高电平)。
  • put_key 将键码存入环形缓冲区。
  • wake_up_interruptible 唤醒因 read 阻塞的进程。
  • kill_fasync 向设置了异步通知的进程发送 SIGIO 信号。

这样,一个完整的硬件中断 → 消抖 → 数据分发的链条就打通了。


七、文件操作接口详解

驱动最终是要让用户程序使用的,因此还需要实现 readwritepollfasync

7.1 read:阻塞与非阻塞

c

复制代码
static ssize_t gpio_drv_read (struct file *file, char __user *buf,
                              size_t size, loff_t *offset)
{
    int key;

    if (is_key_buf_empty() && (file->f_flags & O_NONBLOCK))
        return -EAGAIN;

    wait_event_interruptible(gpio_wait, !is_key_buf_empty());
    key = get_key();
    copy_to_user(buf, &key, 4);
    return 4;
}
  • 若缓冲区空且用户以非阻塞(O_NONBLOCK)方式打开,则立即返回 -EAGAIN
  • 否则,进程在 gpio_wait 上睡眠,直到缓冲区非空(被定时器回调唤醒)。
  • 唤醒后取出一个 int 型按键值,通过 copy_to_user 拷贝给用户空间,返回 4 字节。

7.2 write:控制 GPIO 输出

c

复制代码
static ssize_t gpio_drv_write(struct file *file, const char __user *buf,
                              size_t size, loff_t *offset)
{
    unsigned char ker_buf[2];
    if (size != 2) return -EINVAL;

    copy_from_user(ker_buf, buf, 2);
    if (ker_buf[0] >= count) return -EINVAL;   // 用 count 而不是 sizeof(gpios)!

    gpio_set_value(gpios[ker_buf[0]].gpio, ker_buf[1]);
    return 2;
}
  • 用户写入两个字节:第一个是 GPIO 索引,第二个是输出值(0 或非 0)。
  • 注意 :旧代码里用的是 ker_buf[0] >= sizeof(gpios)/sizeof(gpios[0]),但新代码中 gpios 是指针,sizeof(gpios) 只会得到指针长度,必须改用全局的 count

7.3 poll

c

复制代码
static unsigned int gpio_drv_poll(struct file *fp, poll_table *wait)
{
    poll_wait(fp, &gpio_wait, wait);
    return is_key_buf_empty() ? 0 : POLLIN | POLLRDNORM;
}
  • poll_wait 把当前进程加入到驱动私有的等待队列 gpio_wait,但不立即休眠。
  • 返回值为 0 表示没有数据可读;返回 POLLIN | POLLRDNORM 表示有数据,应用程序的 select/poll 可以继续处理。

7.4 fasync:异步通知

c

复制代码
static int gpio_drv_fasync(int fd, struct file *file, int on)
{
    return fasync_helper(fd, file, on, &button_fasync) >= 0 ? 0 : -EIO;
}

用户程序通过 fcntl(fd, F_SETFL, FASYNC) 调用时,fasync_helper 会把进程记录到 button_fasync 链表中。之后在定时器回调执行 kill_fasync 时,会向链表里的进程发送 SIGIO 信号,通知它们有数据可读。


八、设备树深度理解:从 DTS 到 platform_device

设备树是驱动硬件配置分离的关键技术。它的基本语法我们已经在前文片段里看到过,现在更系统地梳理。

8.1 设备树的结构

dts

复制代码
/dts-v1/;

/ {
    model = "fsl,mpc8572ds";
    compatible = "fsl,mpc8572ds";
    #address-cells = <1>;
    #size-cells = <1>;

    cpus {
        cpu@0 { ... };
        cpu@1 { ... };
    };

    memory@0 {
        device_type = "memory";
        reg = <0 0x20000000>;
    };

    uart@fe001000 {
        compatible = "ns16550";
        reg = <0xffe001000 0x100>;
    };

    chosen { bootargs = "..."; };
    aliases { serial0 = "/uart@fe001000"; };
};

节点命名形式为 名称[@地址]。属性值可以是整数、字符串、字节序列,以及它们的混合。compatible 是最重要的匹配属性,形式为 "厂商,型号"

8.2 内核处理设备树流程

DTS 编译成 DTB,由 U-Boot 传递给内核。内核解析 DTB 为 device_node 树,然后挑选一部分节点转换为 platform_device,同时提取 reginterrupts 属性作为设备资源。

哪些节点会被转换?

  • 根节点的子节点且具备 compatible 属性;
  • 当父节点 compatible"simple-bus" 等值时,其子节点也会被转换;
  • I2C、SPI 控制器下的子节点不作为 platform_device,而是由对应总线驱动生成 i2c_clientspi_device

8.3 驱动里使用设备树 API

在我们的 probe 中已经用到了 of_gpio_countof_get_gpio,这些都是设备树辅助函数。此外常用的还有:

  • of_property_read_u32(np, "propname", &val)
  • of_find_node_by_path("/memory")
  • irq_of_parse_and_map(np, index) 直接从设备树中断属性获取 IRQ 号。

这些函数都定义在 include/linux/of_*.h 中。


九、将新旧代码合二为一:你的 driver 就该这么写

把上面的所有改动拼合起来,完整的 gpio_drv_probe 应该像下面这样:

c

复制代码
static int gpio_drv_probe(struct platform_device *pdev)
{
    int err = 0, i;
    struct device_node *np = pdev->dev.of_node;
    struct resource *res;

    if (np) {
        count = of_gpio_count(np);
        if (!count) return -EINVAL;
        gpios = kmalloc(count * sizeof(*gpios), GFP_KERNEL);
        for (i = 0; i < count; i++) {
            gpios[i].gpio = of_get_gpio(np, i);
            sprintf(gpios[i].name, "%s_pin_%d", np->name, i);
        }
    } else {
        count = 0;
        while (platform_get_resource(pdev, IORESOURCE_IRQ, count))
            count++;
        if (!count) return -EINVAL;
        gpios = kmalloc(count * sizeof(*gpios), GFP_KERNEL);
        for (i = 0; i < count; i++) {
            res = platform_get_resource(pdev, IORESOURCE_IRQ, i);
            gpios[i].gpio = res->start;
            sprintf(gpios[i].name, "%s_pin_%d", pdev->name, i);
        }
    }

    for (i = 0; i < count; i++) {
        gpios[i].irq = gpio_to_irq(gpios[i].gpio);
        setup_timer(&gpios[i].key_timer, key_timer_expire,
                    (unsigned long)&gpios[i]);
        gpios[i].key_timer.expires = ~0;
        add_timer(&gpios[i].key_timer);
        err = request_irq(gpios[i].irq, gpio_key_isr,
                          IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,
                          "100ask_gpio_key", &gpios[i]);
        // 实际产品中应检查 err 并处理错误
    }

    major = register_chrdev(0, "100ask_gpio_key", &gpio_key_drv);
    gpio_class = class_create(THIS_MODULE, "100ask_gpio_key_class");
    if (IS_ERR(gpio_class)) {
        unregister_chrdev(major, "100ask_gpio_key");
        return PTR_ERR(gpio_class);
    }
    device_create(gpio_class, NULL, MKDEV(major, 0), NULL, "100ask_gpio");

    return 0;
}

加上对应的 removeplatform_driver 定义,以及已经讲过的文件操作函数,这就是一份符合规范的、支持设备树的外部按键驱动。


十、总结:从硬编码到 platform 驱动的思维跃迁

  1. 设备树只描述硬件,不写逻辑;驱动程序通过 API 读取硬件信息。
  2. platform 驱动模型 分离了设备与驱动,通过 compatible 等机制自动匹配。
  3. 中断上下半部是处理实时事件的惯用模式:上半部快进快出,下半部完成实际数据处理。
  4. 环形缓冲区 + 等待队列 + 异步通知构成了用户空间获取数据的三种模式(阻塞、非阻塞、异步)。

掌握了这些,再去研究 I2C、SPI、USB 驱动,你会发现框架如出一辙。


十一、自测题(检验你到底学会没)

  1. 概念 :在设备树中,compatible 属性的作用是什么?
  2. 填空#address-cells = <1>; #size-cells = <1>; 表示子节点的 reg 属性用 _____ 个 32 位数描述地址,用 _____ 个 32 位数描述大小。
  3. 代码分析 :在 gpio_drv_probe 的设备树分支中,如果 of_gpio_count 返回 0,函数会怎么做?
  4. 找错 :旧代码 write 中的边界检查是 ker_buf[0] >= sizeof(gpios)/sizeof(gpios[0]),为什么在新代码中会出错?应该改成什么?
  5. 流程 :描述一个按键按下后,从硬件中断到用户 read 返回数据的完整路径。
  6. 设计 :如果想增加 LED 控制,复用现有 write 接口,你会如何约定用户写入的数据格式?
  7. 代码补全 :在 remove 函数中,释放 GPIO 数组内存的正确语句是什么?
  8. 匹配顺序 :请按顺序写出 platform_match 函数尝试的 4 种匹配方式。
  9. 实战 :在开发板上执行 ls /sys/firmware/devicetree/,找到对应 compatible = "100ask,gpiodemo" 的节点,查看它的 gpios 属性值,写出 hexdumpcat 的输出。
  10. 综合改造 :如果将驱动改为同时支持 5 个按键,且键码分别为 KEY_AKEY_B...,你需要改动哪些地方?(不需要具体代码,列出改动点即可)

参考答案(请先尽力完成后核对)

  1. 用于与驱动匹配,内核根据它找到对应的 platform_driver
  2. 1 个,1 个。
  3. 返回 -EINVAL,表明没有可用的 GPIO 资源。
  4. gpios 变成了指针,sizeof(gpios) 是指针长度,应改为 ker_buf[0] >= count
  5. 硬件中断 → ISR 里 mod_timer → 200ms 后定时器回调 → 读取电平、存缓冲区 → wake_up 唤醒 read → 用户层 read 返回键值。
  6. 写两个字节:[LED索引, 亮度/状态],驱动根据索引调用 gpio_set_value 或 PWM 接口。
  7. kfree(gpios);
  8. driver_override 强制匹配;② 设备树兼容性匹配;③ ACPI 匹配;④ ID 表匹配或名字直接匹配。
  9. 具体输出取决于板子的设备树,例如 cat compatible 得到 100ask,gpiodemohexdump gpios 显示 GPIO 编号。
  10. 改动点:设备树中增加 5 个 gpios 条目;在 probe 中为每个 gpio_desc.key 赋值(如 gpios[i].key = KEY_A + i);确认定时器回调中 key 组合正确;无需改动驱动框架代码。

这篇博客到这里就结束了。希望它能成为你常翻常新的参考手册。如果觉得有用,欢迎分享给正在学习 Linux 驱动的朋友。

相关推荐
小周技术驿站2 小时前
Linux 权限管理细节详解
linux·运维·服务器·ubuntu·centos
思麟呀2 小时前
Select多路转接
linux·网络·c++·网络协议·http
cen__y3 小时前
Linux04(重定向)
linux·服务器·c语言
senijusene3 小时前
I2C 总线框架下LM75A 温度传感器 Linux驱动开发:
linux·运维·驱动开发
专注VB编程开发20年3 小时前
工控成套控制柜厂家 / 自动化小工厂 对外市场价
运维·自动化·工控·上位机开发
片酷3 小时前
【Isaacsim&Isaaclab】安装教程
linux·开发语言·python
Magic@3 小时前
Redis学习[1] ——基本概念和数据类型
linux·开发语言·数据库·c++·redis·学习
microxiaoxiao3 小时前
Aeroshell:2026 年,支持AI的SSH 终端
运维·人工智能·ssh
大腕先生3 小时前
通用分页超详细介绍(附带源代码解析&页面展示效果)
xml·java·linux·服务器·开发语言·前端·idea