从硬编码按键驱动到 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)
- [6.1 中断上半部:`gpio_key_isr`](#6.1 中断上半部:
- 七、文件操作接口详解
-
- [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:异步通知)
- [7.1 `read`:阻塞与非阻塞](#7.1
- [八、设备树深度理解:从 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)
- [6.1 中断上半部:`gpio_key_isr`](#6.1 中断上半部:
- 七、文件操作接口详解
-
- [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:异步通知)
- [7.1 `read`:阻塞与非阻塞](#7.1
- [八、设备树深度理解:从 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 引脚号 131 和 132 写死在数组里。换个板子就要改代码、重新编译,毫无灵活性可言。
二、改造的目标: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),依次尝试四种方式:
- 强制覆盖 (
driver_override字段) - 设备树匹配 (比较
of_node和of_match_table) - ACPI 匹配
- 平台设备 ID 表匹配 (
id_table) - 兜底 :直接比较设备名和驱动名(
pdev->namevsdrv->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=100则HZ/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信号。
这样,一个完整的硬件中断 → 消抖 → 数据分发的链条就打通了。
七、文件操作接口详解
驱动最终是要让用户程序使用的,因此还需要实现 read、write、poll、fasync。
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,同时提取 reg 和 interrupts 属性作为设备资源。
哪些节点会被转换?
- 根节点的子节点且具备
compatible属性; - 当父节点
compatible为"simple-bus"等值时,其子节点也会被转换; - I2C、SPI 控制器下的子节点不作为
platform_device,而是由对应总线驱动生成i2c_client或spi_device。
8.3 驱动里使用设备树 API
在我们的 probe 中已经用到了 of_gpio_count 和 of_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;
}
加上对应的 remove 和 platform_driver 定义,以及已经讲过的文件操作函数,这就是一份符合规范的、支持设备树的外部按键驱动。
十、总结:从硬编码到 platform 驱动的思维跃迁
- 设备树只描述硬件,不写逻辑;驱动程序通过 API 读取硬件信息。
- platform 驱动模型 分离了设备与驱动,通过
compatible等机制自动匹配。 - 中断上下半部是处理实时事件的惯用模式:上半部快进快出,下半部完成实际数据处理。
- 环形缓冲区 + 等待队列 + 异步通知构成了用户空间获取数据的三种模式(阻塞、非阻塞、异步)。
掌握了这些,再去研究 I2C、SPI、USB 驱动,你会发现框架如出一辙。
十一、自测题(检验你到底学会没)
- 概念 :在设备树中,
compatible属性的作用是什么? - 填空 :
#address-cells = <1>; #size-cells = <1>;表示子节点的reg属性用 _____ 个 32 位数描述地址,用 _____ 个 32 位数描述大小。 - 代码分析 :在
gpio_drv_probe的设备树分支中,如果of_gpio_count返回 0,函数会怎么做? - 找错 :旧代码
write中的边界检查是ker_buf[0] >= sizeof(gpios)/sizeof(gpios[0]),为什么在新代码中会出错?应该改成什么? - 流程 :描述一个按键按下后,从硬件中断到用户
read返回数据的完整路径。 - 设计 :如果想增加 LED 控制,复用现有
write接口,你会如何约定用户写入的数据格式? - 代码补全 :在
remove函数中,释放 GPIO 数组内存的正确语句是什么? - 匹配顺序 :请按顺序写出
platform_match函数尝试的 4 种匹配方式。 - 实战 :在开发板上执行
ls /sys/firmware/devicetree/,找到对应compatible = "100ask,gpiodemo"的节点,查看它的gpios属性值,写出hexdump或cat的输出。 - 综合改造 :如果将驱动改为同时支持 5 个按键,且键码分别为
KEY_A、KEY_B...,你需要改动哪些地方?(不需要具体代码,列出改动点即可)
参考答案(请先尽力完成后核对):
- 用于与驱动匹配,内核根据它找到对应的
platform_driver。 - 1 个,1 个。
- 返回
-EINVAL,表明没有可用的 GPIO 资源。 gpios变成了指针,sizeof(gpios)是指针长度,应改为ker_buf[0] >= count。- 硬件中断 → ISR 里
mod_timer→ 200ms 后定时器回调 → 读取电平、存缓冲区 →wake_up唤醒read→ 用户层read返回键值。 - 写两个字节:
[LED索引, 亮度/状态],驱动根据索引调用gpio_set_value或 PWM 接口。 kfree(gpios);- ①
driver_override强制匹配;② 设备树兼容性匹配;③ ACPI 匹配;④ ID 表匹配或名字直接匹配。 - 具体输出取决于板子的设备树,例如
cat compatible得到100ask,gpiodemo,hexdump gpios显示 GPIO 编号。 - 改动点:设备树中增加 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 引脚号 131 和 132 写死在数组里。换个板子就要改代码、重新编译,毫无灵活性可言。
二、改造的目标: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),依次尝试四种方式:
- 强制覆盖 (
driver_override字段) - 设备树匹配 (比较
of_node和of_match_table) - ACPI 匹配
- 平台设备 ID 表匹配 (
id_table) - 兜底 :直接比较设备名和驱动名(
pdev->namevsdrv->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=100则HZ/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信号。
这样,一个完整的硬件中断 → 消抖 → 数据分发的链条就打通了。
七、文件操作接口详解
驱动最终是要让用户程序使用的,因此还需要实现 read、write、poll、fasync。
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,同时提取 reg 和 interrupts 属性作为设备资源。
哪些节点会被转换?
- 根节点的子节点且具备
compatible属性; - 当父节点
compatible为"simple-bus"等值时,其子节点也会被转换; - I2C、SPI 控制器下的子节点不作为
platform_device,而是由对应总线驱动生成i2c_client或spi_device。
8.3 驱动里使用设备树 API
在我们的 probe 中已经用到了 of_gpio_count 和 of_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;
}
加上对应的 remove 和 platform_driver 定义,以及已经讲过的文件操作函数,这就是一份符合规范的、支持设备树的外部按键驱动。
十、总结:从硬编码到 platform 驱动的思维跃迁
- 设备树只描述硬件,不写逻辑;驱动程序通过 API 读取硬件信息。
- platform 驱动模型 分离了设备与驱动,通过
compatible等机制自动匹配。 - 中断上下半部是处理实时事件的惯用模式:上半部快进快出,下半部完成实际数据处理。
- 环形缓冲区 + 等待队列 + 异步通知构成了用户空间获取数据的三种模式(阻塞、非阻塞、异步)。
掌握了这些,再去研究 I2C、SPI、USB 驱动,你会发现框架如出一辙。
十一、自测题(检验你到底学会没)
- 概念 :在设备树中,
compatible属性的作用是什么? - 填空 :
#address-cells = <1>; #size-cells = <1>;表示子节点的reg属性用 _____ 个 32 位数描述地址,用 _____ 个 32 位数描述大小。 - 代码分析 :在
gpio_drv_probe的设备树分支中,如果of_gpio_count返回 0,函数会怎么做? - 找错 :旧代码
write中的边界检查是ker_buf[0] >= sizeof(gpios)/sizeof(gpios[0]),为什么在新代码中会出错?应该改成什么? - 流程 :描述一个按键按下后,从硬件中断到用户
read返回数据的完整路径。 - 设计 :如果想增加 LED 控制,复用现有
write接口,你会如何约定用户写入的数据格式? - 代码补全 :在
remove函数中,释放 GPIO 数组内存的正确语句是什么? - 匹配顺序 :请按顺序写出
platform_match函数尝试的 4 种匹配方式。 - 实战 :在开发板上执行
ls /sys/firmware/devicetree/,找到对应compatible = "100ask,gpiodemo"的节点,查看它的gpios属性值,写出hexdump或cat的输出。 - 综合改造 :如果将驱动改为同时支持 5 个按键,且键码分别为
KEY_A、KEY_B...,你需要改动哪些地方?(不需要具体代码,列出改动点即可)
参考答案(请先尽力完成后核对):
- 用于与驱动匹配,内核根据它找到对应的
platform_driver。 - 1 个,1 个。
- 返回
-EINVAL,表明没有可用的 GPIO 资源。 gpios变成了指针,sizeof(gpios)是指针长度,应改为ker_buf[0] >= count。- 硬件中断 → ISR 里
mod_timer→ 200ms 后定时器回调 → 读取电平、存缓冲区 →wake_up唤醒read→ 用户层read返回键值。 - 写两个字节:
[LED索引, 亮度/状态],驱动根据索引调用gpio_set_value或 PWM 接口。 kfree(gpios);- ①
driver_override强制匹配;② 设备树兼容性匹配;③ ACPI 匹配;④ ID 表匹配或名字直接匹配。 - 具体输出取决于板子的设备树,例如
cat compatible得到100ask,gpiodemo,hexdump gpios显示 GPIO 编号。 - 改动点:设备树中增加 5 个
gpios条目;在probe中为每个gpio_desc.key赋值(如gpios[i].key = KEY_A + i);确认定时器回调中key组合正确;无需改动驱动框架代码。
这篇博客到这里就结束了。希望它能成为你常翻常新的参考手册。如果觉得有用,欢迎分享给正在学习 Linux 驱动的朋友。