本文用法 :按函数框架顺序学习,每个函数直接给出生产推荐的写法,不做"低效教学"
你会学到什么
| 学会后能做什么 | 用到的内核机制 |
|---|---|
| 驱动自动适配不同板子(不改代码) | Platform Driver + 设备树 |
| 按键不丢事件 | 环形缓冲区(Ring Buffer) |
| 按键不抖动 | 内核定时器去抖 |
| 中断处理不卡顿 | threaded_IRQ(线程化中断,最推荐) |
| 一个进程同时监听多个设备 | poll 接口 |
| 内核主动通知用户程序 | fasync(SIGIO 异步通知) |
| 用户程序不阻塞 | O_NONBLOCK 非阻塞 IO |
完整流程预览
驱动启动全流程(目录树形式)
| device_node | platform_device | |
|---|---|---|
| 是什么 | 纯数据------设备树的解析结果 | 可操作的设备对象 |
| 系统路径 | /sys/firmware/devicetree/base/ 下的目录 |
/sys/bus/platform/devices/ 下的目录 |
| 具体例子 | /sys/firmware/devicetree/base/gpio_keys/ |
/sys/bus/platform/devices/gpio_keys/ |
| 何时创建 | 内核启动早期,解析 .dtb 时 | 内核启动后期,遍历 device_node 树时 |
| 能做什么 | 只读:查 compatible、gpios、reg 等属性 | 可操作:绑定驱动、电源管理、probe/remove |
| 生命周期 | 跟内核同生共死(不动) | 可以被 probe、remove、suspend、resume |
| 谁用 | 驱动 probe 里读硬件信息 | 总线匹配机制用 |
| 两者的关系 | 数据源(被引用) | 消费者(通过 of_node 指针指向 device_node) |
| 属性怎么查 | cat /sys/firmware/devicetree/base/gpio_keys/compatible |
通过 of_node 链接间接访问 |
内核不管你是谁,只管建 platform_device。你的驱动靠 compatible 认领,probe 里你把它当成按键处理,它就是按键。 名字 gpio_keys 只是给人看的,内核不看名字做判断。
驱动程序启动全流程
│
├── 开机阶段(内核自动完成,你什么都没干)
│ │
│ ├── U-Boot 把 .dtb 加载到内存
│ │ └── .dtb:设备树源码 (.dts) 编译后的二进制文件。
│ │ 类比:.dts = 你手写的硬件配置单(文本),.dtb = 打印成二维码。
│ │ 流程:你写 .dts → dtc 编译器 → .dtb → U-Boot 读到内存 → 传给内核。
│ │ 系统路径:没有固定路径,在内存里。
│ │
│ ├── 内核解析 .dtb → 建 device_node 树(在内存里)
│ │ └── device_node 树:
│ │ 内核把 .dtb 解析后,在内存里建的一棵"硬件档案树"。
│ │ 每个硬件节点(gpio4、gpio_keys 等)都是一个 device_node 对象。
│ │
│ │ 系统路径:/sys/firmware/devicetree/base/ 下的整个目录树
│ │ 比如 /sys/firmware/devicetree/base/gpio_keys/ 就是一个 device_node
│ │
│ │ 类比:每个 device_node 就是一张"硬件档案卡"。
│ │ 你写的 DTS:
│ │ gpio_keys {
│ │ compatible = "gpio-key,gpio_key"; ← 你是谁
│ │ gpios = <&gpio4 14 GPIO_ACTIVE_LOW>; ← 占了哪个引脚
│ │ };
│ │ 内核解析后在内存里存成:
│ │ device_node {
│ │ .name = "gpio_keys"; ← 设备名
│ │ .compatible = "gpio-key,gpio_key"; ← 驱动匹配靠这个
│ │ .properties = [ ← 属性链表
│ │ { "gpios", <&gpio4 14 1> },
│ │ ...
│ │ ];
│ │ };
│ │ compatible = "你的身份证号码",驱动靠它来认你。
│ │ gpios = "你用了哪几个引脚"的记录。
│ │ reg = "你的寄存器地址范围"(从 reg 属性解析,和 gpios 无关)。
│ │
│ └── 每个 DTS 节点(一个设备实例)→ 生成一个 platform_device → 挂到 platform 总线上
│ └── of_platform_populate():内核自己的函数,遍历 device_node 树,
│ 给每个节点创建一个 platform_device。你不用写,开机自动执行。
│
│ 系统路径:/sys/bus/platform/devices/ 下的目录
│ 比如 /sys/bus/platform/devices/gpio_keys/ 就是一个 platform_device
│
│ platform_device 和 device_node 的关系:
│ device_node = 纯数据档案卡(只读,记录硬件信息)
│ platform_device = 可操作设备对象(能绑定驱动,能 probe/remove)
│ 关系:platform_device 里有个 of_node 指针指向对应的 device_node
│ /sys/bus/platform/devices/gpio_keys/of_node →
| /sys/firmware/devicetree/base/gpio_keys/
│
│ platform 总线是什么?
│ ────────────────────
│ Linux 里 USB、I2C、SPI 设备有各自的通信协议,归各自总线管。
│ 但 GPIO 按键、LED 这些------直接焊在板子上、连在 CPU 引脚上,
│ 不需要任何通信协议,CPU 直接读寄存器就行。
│ 这些"无协议"设备的兜底归属,就是 platform 总线。硬件直接焊接到GPIO引脚
│ 系统路径:/sys/bus/platform/
│
├── insmod 阶段(加载你的 .ko,你要写的部分从这里开始)
│ │
│ ├── module_init 触发 → 调 gpio_key_init()
│ │ └── module_init:insmod 时内核自动调这个函数,不需要你手动调。
│ │ 整个 gpio_key_init() 只做一件事:调 platform_driver_register。
│ │
│ └── platform_driver_register(&gpio_keys_driver)
│ │ └── 向内核登记你的驱动,告诉内核"我来了,我可以管某些设备"。
│ │
│ │ platform_driver 是什么?
│ │ ────────────────────────
│ │ 你写的结构体,里面填了三样东西:
│ │ .probe ← 找到匹配设备后干什么(开业)
│ │ .remove ← 被卸载时干什么(关门)
│ │ .driver.of_match_table ← 我能管哪些设备(经营范围)
│ │
│ │ 系统路径:注册后出现在 /sys/bus/platform/drivers/gpio_key/
│ │
│ │ 类比:platform_driver = 开店资质证书。
│ │ of_match_table = 经营范围:"我能开 compatible 是 gpio-key,gpio_key 的店"
│ │ probe = 开业函数:找到匹配空店面 → 进去装修 → 开始营业
│ │
│ │ 流程:
│ │ 内核:总线上有 100 间空店面(platform_device),你看哪个符合?
│ │ 你: gpio_keys 这间 compatible 对得上!→ 进去 probe 开业
│ │ 其他的 compatible 不对,跳过。
│ │
│ ├── 内核遍历总线上每个 platform_device
│ │ └── 总线上挂着开机阶段生成的 platform_device,
│ │ 你的驱动登记后,内核逐个拿出来问:"这个你能管吗?"
│ │
│ ├── 通过 of_node 指针找到 device_node
│ │ └── 每个 platform_device 里有个 of_node 指针,指向对应的 device_node。
│ │ 拿到这个指针,就能翻那张"硬件档案卡"。
│ │
│ │ 类比:platform_device = 店面门牌号,
│ │ of_node 指针 = 门牌号后面压着的房产证 → 翻过来看 → 业主信息。
│ │
│ ├── 从 device_node 读出 compatible 字符串
│ │ └── 比如读到 "gpio-key,gpio_key"。
│ │
│ ├── 跟你的 of_match_table 逐条比对
│ │ └── of_match_table:
│ │ 驱动里写的一个数组,声明"我能管这些 compatible":
│ │ { .compatible = "gpio-key,gpio_key" }, ← 我能管
│ │ { .compatible = "another-key" }, ← 也能管
│ │ { } ← 空结尾,表示列表结束
│ │
│ │ 内核把读到的 "gpio-key,gpio_key" 跟表里每条用 strcmp 逐字符比对,
│ │ 完全一样就是命中。大小写、标点、下划线------差一个字符都不行。
│ │
│ │ 类比:你的经营范围列表。内核拿着房产证上的业主名,
│ │ 跟你的列表逐条比对,名字完全相同就认定这间店归你管。
│ │
│ └── 匹配成功 → 调用 probe(platform_device *)
│ └── probe 就是你写的 gpio_key_probe()。
│ 内核把匹配到的 platform_device 传给你,
│ 从这一刻起,这间空店面正式归你经营。
│
└── probe 阶段(真正初始化硬件)
│
├── of_gpio_count(node)
│ └── 从 device_node 的 gpios = <...> 属性里数有几个 GPIO 条目。
│ 比如 gpios = <&gpio4 14 1>, <&gpio1 18 0> → count = 2,两个按键。
│ 这个数字决定了后面分配几套资源(每个按键一套)。
│ 类比:翻房产证看"这间店配了几个停车位"。
│
├── kzalloc(sizeof(struct gpio_key) * count)
│ └── 内核版 malloc,z 后缀 = 同时清零。
│ 给每个按键分配一个 gpio_key 结构体,
│ 里面存 GPIO 编号、desc 句柄、中断号、定时器、环形缓冲区。
│ 内核实现路径:mm/slub.c,卸载时用 kfree 释放。
│ 类比:租了 count 个带锁的柜子,每个按键一个,东西分开放。
│
├── of_get_gpio_flags(node, i, &flag)
│ └── 从设备树取第 i 个 GPIO 的信息(只看档案,不碰硬件):
│ 返回值 = GPIO 编号(硬件身份证号,比如 110 = gpio4_14)
│ flag = 极性标志(ACTIVE_LOW 还是 ACTIVE_HIGH)
│ 类比:翻户口本查身份证号,不是测心跳。读的是"记录",不是"当前状态"。
│
├── devm_gpiod_get_index(&pdev->dev, NULL, i, GPIOD_IN)
│ └── 从 GPIO 子系统拿到操作这个引脚的"门禁卡"------gpio_desc 句柄。
│ gpio_desc 是内核里管理某个 GPIO 引脚的句柄对象。
│ 后续 gpiod_get_value(desc) 读电平、gpiod_set_value(desc) 写电平,
│ 全通过 gpio_desc 操作硬件。
│ devm_ 前缀 = 绑定设备生命周期,卸载时自动归还,不用手写释放。
│ 类比:GPIO 编号 = 身份证号,gpio_desc = 门禁卡。
│ 你刷卡进门,而不是报身份证号进门。
│
├── gpio_to_irq(gpio)
│ └── GPIO 编号 → 中断号的转换。
│ GPIO 控制器驱动里有一张映射表,内核自动查表。
│ GPIO 编号是硬件出厂定的,中断号是内核分配的工号,两者不同。
│ 类比:身份证号 → 公司内部工号,HR 帮你查表转的。
│
├── request_threaded_irq(irq, NULL, key_irq_handler, IRQF_TRIGGER_FALLING,
| "gpio_key", dev)
│ └── 向内核登记:"中断号来了,调我的 key_irq_handler 函数"。
│ IRQF_TRIGGER_FALLING = 下降沿触发(按键按下:高→低)。
│ 内核实现:kernel/irq/manage.c。
│ ⚠️ 必须配对 free_irq:不释放的话,卸载驱动后中断来了
│ → 跳转到已释放的函数地址 → 内核直接崩溃。
│ 类比:在传达室登记"我的工号被呼叫时打我手机"。
│ 退租时必须取消登记,不然呼叫打给空号。
│
├── timer_setup(&gpio_key->key_timer, key_timer_expire, 0) // 0默认配置
│ └── 初始化一个内核定时器(消抖用),此时不激活。
│ 完整消抖流程:mod_timer设置时间
│ 按键按下 → 中断 → ISR 立刻关中断 + mod_timer 设 20ms 后超时
│ → 20ms 后调 key_timer_expire → 读电平确认是真的按下
│ → 记录按键值到缓冲区 → 恢复中断
│ 内核实现:kernel/time/timer.c。
│ 类比:装一个闹钟,平时不响。有人按铃后启动倒计时 20ms,
│ 闹钟响了再去看是不是真有人------防止误触。
│
├── register_chrdev(0, "gpio_key", &gpio_key_drv)
│ └── 注册字符设备,0 = 让内核自动分配主设备号。
│ 然后 /proc/devices 里就出现一行 "xxx gpio_key",
│ 表示"主设备号 xxx 是 gpio_key 驱动在管"。
│ 类比:在工商局注册公司,拿到执照号(主设备号)。
│
├── class_create(THIS_MODULE, "gpio_key_class")
│ └── 在 sysfs 创建设备类目录:/sys/class/gpio_key_class/
│ udev 守护进程根据这个目录的信息,
│ 自动在 /dev/ 下创建设备文件。
│ 类比:在门口挂牌匾,"按键设备"------方便快递员(udev)找到你。
│
└── device_create(class, NULL, MKDEV(major, 0), NULL, "gpio_key")
└── 触发 udev 在 /dev/ 下创建设备文件:/dev/gpio_key
从现在起,用户程序 open("/dev/gpio_key") 就能访问你的驱动。
类比:装好门铃,顾客终于能按了。
┌─────────────────────────────────────────────────────┐
│ │
│ 最终结果
│
│ 用户程序: open("/dev/gpio_key")
│ ↓
│ read() / poll() / fasync
│ ↓
│ 等按键按下 → 返回按键值
│
│ 三处可见入口,作用各不相同:
│ /proc/devices ← 内核登记表(排查用)
│ /sys/class/gpio_key_class/ ← 给 udev 看的(自动建节点)
│ /dev/gpio_key ← 用户程序的入口(open/read)
|
│ │
└─────────────────────────────────────────────────────┘
你只负责写 insmod 之后的两棵子树,开机那棵是内核写好的。
| 注册 API | 你提供了什么 | 触发条件 | 谁回调你 | 回调函数 | |
|---|---|---|---|---|---|
| 1 | module_platform_driver |
of_device_id 表 + platform_driver 结构体 |
DTS 中 compatible 匹配成功 |
platform_bus 子系统 |
probe() → gpio_key_drv_probe |
| 2 | cdev_init + cdev_add + class_create + device_create |
file_operations 结构体(.open .read .poll .fasync) |
用户调用 open/read/poll |
VFS 层(系统调用→驱动) | gpio_key_drv_open() / _read() / _poll() |
| 3 | devm_gpio_request + gpio_direction_input |
GPIO 标号 | (无回调,仅资源占用) | --- | --- |
| 4 | devm_request_irq |
gpio_key_isr 函数指针 + 中断号 + 触发方式 |
GPIO 引脚电平跳变(按键按下/释放) | GIC 中断控制器→内核中断框架 | gpio_key_isr()(中断上下文,快进快出) |
| 5 | setup_timer + mod_timer |
key_timer_handler 函数指针 + 超时时间(200ms 消抖) |
ISR 中 mod_timer → 定时器到期 |
内核时钟软中断 TIMER_SOFTIRQ |
key_timer_handler()(消抖 + wake_up) |
| 6 | DECLARE_WAIT_QUEUE_HEAD + poll_wait / wake_up |
等待队列 gpio_key_wait |
timer_handler 调 wake_up 或 poll 调 poll_wait |
内核调度器 | wait_event_interruptible 宏(schedule→唤醒→继续循环) |
GPIO 按键驱动 --- 申请/注册资源总表
| # | API | 内核源码路径 | 申请/注册了什么 | 释放函数(remove 里用) |
|---|---|---|---|---|
| 1 | of_get_gpio_flags() |
drivers/of/of_gpio.c |
不申请,只读 DTS 属性,返回 GPIO 编号 | --- |
| 2 | gpio_to_desc() |
drivers/gpio/gpiolib.c |
不申请 ,编号查表返回 gpio_desc * |
--- |
| 3 | kzalloc() |
mm/slub.c |
分配内存 :count 个 gpio_key 结构体 |
kfree() |
| 4 | timer_setup() |
kernel/time/timer.c |
初始化定时器结构体(不分配新内存) | --- |
| 5 | add_timer() |
kernel/time/timer.c |
注册定时器到内核定时器链表 | del_timer_sync() |
| 6 | request_threaded_irq() |
kernel/irq/manage.c |
申请中断线 :注册 irqaction 到 irq_desc |
free_irq() |
| 7 | register_chrdev() |
fs/char_dev.c |
申请主设备号 :注册 file_operations |
unregister_chrdev() |
| 8 | class_create() |
drivers/base/class.c |
创建 class 对象 :/sys/class/gpio_key_class/ |
class_destroy() |
| 9 | device_create() |
drivers/base/core.c |
创建 device 对象 :触发 udev → /dev/gpio_key |
device_destroy() |
只保留"真正申请资源"的精简版
| # | API | 申请了什么 | 内核路径 | remove 释放 |
|---|---|---|---|---|
| 1 | kzalloc |
内核内存(gpio_key 数组) | mm/slub.c |
kfree |
| 2 | request_threaded_irq |
中断线(irqaction) | kernel/irq/manage.c |
free_irq |
| 3 | register_chrdev |
主设备号 | fs/char_dev.c |
unregister_chrdev |
| 4 | class_create |
class 对象(/sys/class/) | drivers/base/class.c |
class_destroy |
| 5 | device_create |
device 对象 + /dev 节点 | drivers/base/core.c |
device_destroy |
| 6 | add_timer |
定时器注册到内核链表 | kernel/time/timer.c |
del_timer_sync |
一句话记忆
内存 → kzalloc / kfree
中断线 → request_threaded_irq / free_irq
设备号 → register_chrdev / unregister_chrdev
sysfs类 → class_create / class_destroy
/dev节点 → device_create / device_destroy
定时器 → add_timer / del_timer_sync
如果用 devm_ 前缀的 API,release 函数全不用手写,设备卸载时内核自动按逆序释放。
第 1 步:platform 驱动注册(module_platform_driver)
- 提交内容:platform_driver + of 匹配表 (compatible 字符串)
- 触发时机:内核遍历设备树节点,节点 compatible 和驱动一致
- 调用者:platform 总线内核框架
- 执行回调:
gpio_key_drv_probe - probe 内部依次执行后面所有资源创建:字符设备、GPIO、中断、定时器、等待队列。
第 2 步:创建字符设备(cdev + class + device)
- 提交内容:file_operations 函数集(open、read、poll、fasync)
- 触发时机:用户程序执行
open("/dev/xxx")、read()、poll()系统调用 - 调用者:VFS 虚拟文件系统
- 对应回调:
- open:初始化私有数据
- read:调用
wait_event_interruptible,缓冲区为空就阻塞休眠 - poll:调用
poll_wait把进程挂入等待队列
第 3 步:申请 GPIO 引脚(devm_gpio_request)
- 提交内容:GPIO 编号
- 无触发事件、无回调
- 作用:占用硬件引脚资源,设为输入模式,防止其他驱动抢占。
第 4 步:注册硬件中断(devm_request_irq 设备托管式请求中断)
- 提交内容:中断服务函数
gpio_key_isr、中断号、边沿触发方式 - 触发条件:GPIO 电平跳变(按键按下 / 松开)
- 调用者:GIC 硬件中断控制器 + 内核中断子系统
- 执行回调:中断服务函数
gpio_key_isr - 中断上下文规则:不能休眠,只做一件事:启动消抖定时器
mod_timer,立刻退出 ISR。
第 5 步:定时器消抖(setup_timer + mod_timer)
- 提交内容:定时器处理函数
key_timer_handler - 触发条件:ISR 里启动定时器,等待 200ms 超时
- 调用者:内核时钟软中断
- 执行回调:定时器处理函数
- 再次读取 GPIO 真实电平,过滤机械按键抖动
- 确认按键状态后,把键值写入环形缓冲区
- 调用
wake_up(&gpio_key_wait)唤醒阻塞的进程
第 6 步:等待队列休眠与唤醒(wait_queue_head + poll_wait + wake_up)
- 提交内容:等待队列头
gpio_key_wait - 休眠流程: 用户 read →
wait_event_interruptible发现缓冲区为空 → 调用 schedule 让出 CPU 进入睡眠; poll 函数里执行poll_wait,同样将进程挂载到该等待队列。 - 唤醒条件:定时器处理函数执行
wake_up - 调用者:内核进程调度器
- 唤醒之后:进程重新运行,再次判断缓冲区非空,读出数据,read 系统调用返回给应用程序。
完整时序线
- 内核启动:驱动加载,platform_driver 注册
- 总线匹配 DTS 节点 → 执行 probe
- probe:创建字符设备 → 申请 GPIO → 注册中断 → 初始化定时器与等待队列
- APP:open 设备 → read 阻塞休眠
- 按键按下 → 硬件触发中断 → 进入 ISR
- ISR:启动消抖定时器,快速退出
- 200ms 后定时器到期 → 读取稳定电平,写入环形缓冲
- 执行 wake_up 唤醒等待队列上休眠的进程
- read 条件满足,跳出阻塞,读取键值返回用户态
- poll 机制同样依靠这条等待队列实现事件检测。
准备工作:设备树
一、为什么需要设备树?
反例:写死引脚的驱动(❌ 不要这样做)
#define KEY1_GPIO 142 // GPIO4_14
#define KEY2_GPIO 143 // GPIO4_15
static int __init gpio_key_init(void)
{
gpio_request(KEY1_GPIO, "key1");
gpio_request(KEY2_GPIO, "key2");
// ...
}
问题:换一块板子(比如 GPIO4_14 改成 GPIO1_18)→ 必须改代码、重新编译。同款芯片的不同板子需要不同的 .ko 文件 → 维护噩梦。
正确做法:硬件描述交给设备树
┌─────────────────────┐ ┌──────────────────────┐
│ 驱动代码 (.c) │ │ 设备树 (.dts) │
│ │ │ │
│ 只写"怎么操作GPIO" │◄─── │ 只写"用了哪些GPIO" │
│ 不写"用了几个GPIO" │匹配 │ GPIO数量、引脚号、标志 │
│ │ │ │
│ 一套代码 适配所有板 │ │ 不同板子 不同设备树 │
└─────────────────────┘ └──────────────────────┘
标志为有效电平标志
效果:换板子 → 只改设备树,驱动 .ko 不用动。硬件信息一目了然。
二、设备树长什么样?
设备树是一个树形结构的文件,描述整块板子的硬件:
/ ← 根节点(代表整块板子)
├── cpus { } ← CPU 信息
├── memory { } ← 内存信息
├── soc { ← 芯片内部总线
│ ├── gpio@0209c000 { } ← GPIO 控制器
│ ├── uart@021e8000 { } ← 串口控制器
│ └── i2c@021a0000 { } ← I2C 控制器
│
├── leds { } ← 板载 LED
└── gpio_keys { } ← 我们添加的按键节点
@0209c000 为起始地址
{}:包裹当前设备的所有硬件属性,构成一个完整设备节点,下面为他的成员。
compatible:匹配字符串,相当于设备的身份证号,驱动依靠该字符串找到对应硬件节点
reg:硬件寄存器的物理地址范围
gpios:引脚信息,记录硬件连接到 CPU 的哪一根引脚
interrupts:记录该硬件对应的中断编号,在 probe 函数中发起申请,申请函数request_irq()。
基本语法:
dts
节点名 {
reg = <0x0209c000 0x1000>; // 起始地址 + 长度
compatible = "Button,gpio-key"; // 字符串
gpios = <&gpio1 3 GPIO_ACTIVE_LOW>; // 引用其他节点(按键借用芯片引脚)
};
//compatible = "vendor,key", "generic,key";
1.compatible 属性保存设备匹配字符串,驱动依靠该字符串与设备节点完成配对。
2.&gpio1:引用 GPIO1 控制器节点
引脚编号为第 3 号引脚
GPIO_ACTIVE_LOW:低电平有效
三、我们的 GPIO 按键设备树
驱动代码不写死任何硬件引脚,硬件信息全部放在设备树(DTS)里:
dts
/ {
gpio_keys {
compatible = "gpio-key,gpio_key";
gpios = <&gpio4 14 GPIO_ACTIVE_LOW>,
<&gpio4 15 GPIO_ACTIVE_LOW>;
};
};
"gpio-key,gpio_key" = "厂商前缀,设备名称"
设备树写一份,驱动写一份
两边两个参数完全一致才匹配
| 属性 | 作用 |
|---|---|
compatible = "gpio-key,gpio_key" |
驱动的"配对暗号",必须和驱动代码里的一模一样 |
gpios = <...> |
描述用了哪组 GPIO,格式:&控制器 引脚 标志 |
GPIO_ACTIVE_LOW |
低电平时视为"按键按下" |
四、GPIO 属性两种写法
写法1:gpios(复数,一个属性包含所有 GPIO)------ 推荐入门用
dts
gpio_keys {
compatible = "gpio-key,gpio_key";
gpios = <&gpio4 14 GPIO_ACTIVE_LOW>,
<&gpio4 15 GPIO_ACTIVE_LOW>;
};
c
// 驱动里这样读
count = of_gpio_count(node); // 返回 设备节点,设备树当前设备2
for(i = 0; i < count; i++)
{
gpio = of_get_gpio_flags(node, i, &flag); // 取第 i 个
// i=0 拿到 14,i=1 拿到 15
}
- 所有按键的
gpios全部写在父节点 gpio_keys内部,没有子节点。 - 数据结构:父节点只有一条 gpios 数组,一次性存放所有引脚。
- 特点:多个按键挤在同一个属性里,只能批量读取引脚,不方便给每一个按键单独起名字。
写法2:子节点方式(可以给每个按键加 label,更规范)
dts
gpio_keys {
compatible = "gpio-key,gpio_key";
key1 { gpios = <&gpio4 14 GPIO_ACTIVE_LOW>; label = "KEY1"; };
key2 { gpios = <&gpio4 15 GPIO_ACTIVE_LOW>; label = "KEY2"; };
};
c
// 驱动里这样读
struct device_node *child;
for_each_child_of_node(node, child)
{
// 遍历 key1、key2
gpio = of_get_gpio_flags(child, 0, &flag); // child 依次指向 key1、key2节点取 flag
of_property_read_string(child, "label", &label); // label 读名字
// 第一次循环:key1,gpio=14,label="KEY1"
// 第二次循环:key2,gpio=15,label="KEY2"
}
- 父节点只保留
compatible匹配暗号。 - 每一个按键单独做成一个子节点:
key1{}、key2{}。 - 每个子节点独立拥有自己的
gpios(引脚)和label(按键名称)。
两种写法驱动都能用 of_get_gpio() 读到。
核心区别:传给 of_get_gpio_flags 的第 1 个参数 | gpios硬件占用那几个引脚
| 写法 | 传给 of_get_gpio_flags 的是 |
|---|---|
| 数组写法 | node(父节点 gpio_keys),index 用 0、1、2... |
| 子节点写法 | child(子节点 key1/key2),index 永远用 0(每个子节点只有一个 gpios) |
易混淆点:
-
Platform 总线匹配(靠
compatible) -
字符设备注册(靠
register_chrdev)
这是为了让用户程序能 open("/dev/gpio_key"),跟设备树匹配是两个独立流程。
五、GPIO 标志位详解
<&gpio控制器 引脚编号 标志位>
↓ ↓ ↓
控制器引用 第几个引脚 有效电平/其他配置
常用标志位(定义在 include/dt-bindings/gpio/gpio.h):
| 标志位 | 值 | 含义 | 什么时候用 |
|---|---|---|---|
GPIO_ACTIVE_HIGH |
0 | 高电平有效 | 按键按下时 GPIO 读到 1 |
GPIO_ACTIVE_LOW |
1 | 低电平有效 | 按键按下时 GPIO 读到 0 |
六、GPIO 控制器引用
这行不是驱动 C 代码 ,它写在设备树 imx6ull.dtsi 里。
在 i.MX6ULL 的 .dtsi【NXP 官方提供,定义芯片内部所有外设(GPIO4、UART、I2C...)】
里已经定义好了 GPIO 控制器:
dts
// imx6ull.dtsi 里已经有的
gpio4: gpio@020a8000 {
compatible = "fsl,imx6ul-gpio", "fsl,imx35-gpio";
reg = <0x020a8000 0x4000>;
gpio-controller; // 声明"我是 GPIO 控制器"
#gpio-cells = <2>; // 引用我需要 2 个参数
};
这行不是驱动 C 代码,它写在设备树 imx6ull.dtsi 里。
| 字段 | 值 | 来源 | 如果是自己写新芯片,去哪查 |
|---|---|---|---|
@020a8000 |
GPIO4 寄存器基地址 | 芯片手册 Memory Map 章节 | 芯片手册第 2 章 |
reg = <0x020a8000 0x4000> |
基地址 + 寄存器范围 | 芯片手册 | 芯片手册 |
compatible |
"fsl,imx6ul-gpio" |
NXP 的命名规范 + 内核绑定文档 | Documentation/devicetree/bindings/ 内核手册 |
gpio-controller |
写不写值都行 | Linux GPIO 子系统规范 | bindings/gpio/gpio.txt |
#gpio-cells |
2 |
i.MX GPIO 需要 2 个参数 | 同上 + 你看别的 i.MX dtsi 怎么写的 |
在 .dts 【自己写的设备树(硬件描述),只定义自己板子的外接设备(按键、LED、屏幕...)】
里用 &gpio4 引用它:
dts
gpios = <&gpio4 14 GPIO_ACTIVE_LOW>;
// ↑ ↑ ↑
// 标签引用 引脚号 标志位
#gpio-cells = <2> 意思是引用这个 GPIO 控制器时,后面要跟 2 个参数(引脚号 + 标志)。
GPIO 编号计算(i.MX6ULL):
GPIO 编号 = Bank号 × 32 + 引脚号
例:GPIO4_14 → 4 × 32 + 14 = 142
七、compatible 是配对的暗号
设备树 (.dts) 驱动代码 (.c)
┌──────────────────────────┐ ┌─────────────────────────────────┐
│ gpio_keys { │ │ static const struct of_device_id │
│ compatible = │ │ gpio_key_match_table[] = { │
│ "gpio-key,gpio_key"; │◄──►│ { .compatible = │
│ │ 匹配│ "gpio-key,gpio_key" }, │
│ }; │ │ { }, │
└──────────────────────────┘ │ }; │
└─────────────────────────────────┘
规则 :两边的字符串必须完全一样(包括大小写、逗号、下划线)。多一个空格都不行。
如果不匹配会怎样?加载驱动后 dmesg 里什么都不会打印(probe (匹配初始化函数) ,匹配成功后调用,初始化硬件)。
八、验证设备树是否生效
bash
# 第1步:编译设备树 自动生成设备树文件 .dbt
make dtbs
# 第2步:反编译检查(看看内核实际解析的结果)
dtc -I dtb -O dts xxx.dtb -o check.dts
cat check.dts | grep -A5 gpio_keys
# 第3步:在目标板上查看
ls /proc/device-tree/gpio_keys/
cat /proc/device-tree/gpio_keys/compatible # 应输出:gpio-key,gpio_key
# 第4步:加载驱动后看 dmesg
insmod gpio_key_drv.ko
dmesg | tail
# 应看到:gpio_key: found 2 gpios in device tree
九、设备树常见错误排查
| 错误现象 | 原因 | 排查方法 |
|---|---|---|
| 加载驱动后 probe 没被调用 | compatible 不匹配 | cat /proc/device-tree/gpio_keys/compatible 对比驱动代码 |
of_gpio_count 返回 0 |
属性名写错(写了 gpio 少了 s) |
ls /proc/device-tree/gpio_keys/ 看看有没有 gpios |
of_get_gpio 返回负数 |
GPIO 引脚号写错/控制器引用错误 | 检查编号计算:GPIO4_14 = 4×32+14 = 142,不是 110 |
request_irq 失败 |
GPIO 引脚被其他驱动占用 | `cat /sys/kernel/debug/gpio |
第 1 步:全局变量声明区
这里要做什么:声明驱动用到的所有全局变量 --- 按键数据结构、环形缓冲区、等待队列头、设备号等。
步骤框架
c
// 1. 定义每个按键的数据结构 struct gpio_key
// 成员:gpio[老接口:只存编号]、gpiod[新接口:完整信息包结构体内核用该结构体操作对象]、
flag[高、低电平有效]、irq[每个引脚唯一中断号]、key_timer(去抖定时器)
// 2. 全局指针:指向所有按键数据数组(probe【匹配初始化函数】 里才分配)
static struct gpio_key *gpio_keys_all;
// 3. 主设备号(0 = 让内核自动分配)
static int major = 0;
// 4. 设备类指针(用于 /sys/class/)
static struct class *gpio_key_class;
// 5. 环形缓冲区
// BUF_LEN = 128、g_keys[BUF_LEN]、读指针 r、写指针 w
// 6. fasync 异步通知结构指针(登记要接收按键信号的应用进程,是实现驱动异步通知的唯一载体)
struct fasync_struct *button_fasync;
// 7. 等待队列头
static DECLARE_WAIT_QUEUE_HEAD(gpio_key_wait);
同步:应用主动等待硬件;
异步:硬件做好准备,主动叫醒应用。
详解
c 中断上下文不能恢复现场,不能休眠
/*
* struct gpio_key:描述一个按键的完整信息
*
* 为什么需要这个结构体?
* 设备树里可能配置了多个按键,每个按键都有自己的
* GPIO 编号、中断号、去抖定时器等。
* 用一个结构体数组来管理,比散落的一堆全局变量清晰得多。
*
* 为什么用环形缓冲区而不是单个 g_key 变量?
* 单个变量 → 用户程序来不及读时,新按键覆盖旧按键 → 丢事件
* 环形缓冲区 → 能缓存 128 个按键事件 → 不丢事件
*/
struct gpio_key {
int gpio; // GPIO 硬件编号(如 142 = GPIO4_14)
struct gpio_desc *gpiod; // GPIO 描述符指针,内核内部用这个对象操作 GPIO
int flag; // 标志位,OF_GPIO_ACTIVE_LOW 表示"低电平时按键被按下"
int irq; // 中断号
struct timer_list key_timer; // 去抖定时器
};
/*
* 全局指针,指向按键数据数组
*
* 为什么是指针而不是数组?
* 因为设备树里配了几个按键是运行时才知道的,
* 必须在 probe(初始化硬件) 里用 kzalloc() 动态分配。
*/
static struct gpio_key *gpio_keys_all;
static int major = 0; // 主设备号(0 = 让内核自动分配)
static struct class *gpio_key_class; // 设备类指针,用于 /sys/class/
/*
* 环形缓冲区:解决"丢事件"问题
*
* 为什么用环形缓冲区?
* 如果用户程序在两次按键之间
* 来不及调用 read(),第一次按键的键值就被覆盖了。
*
* 环形缓冲区的优点:
* - 固定大小数组,不动态分配【ISR (中断服务子函数) 里安全】
* - 读写指针分离,ISR 只写入缓冲区、read 只读返回给用户,无锁竞争
* - 满了就丢弃新数据(宁可丢新数据,不覆盖未读数据)
*/
#define BUF_LEN 128 // 缓冲区容量:能存 128 个按键事件
static int g_keys[BUF_LEN]; // 缓冲区数组本体
static int r = 0, w = 0; // r = 读指针(出队位置), w = 写指针(入队位置)
/* fasync 异步通知结构:解决"必须主动问"问题,直接通知用户 */
struct fasync_struct *button_fasync;
/*
* DECLARE_WAIT_QUEUE_HEAD:声明一个等待队列头
*
* 这是 Linux 阻塞 IO 的核心机制。
* 类比:医院排号系统,gpio_key_wait 就是那个"排队等候区"。
* 所有因为 read() 而等待按键的进程,都被加入这个队列排队。
*
* 为什么需要等待队列?
* 如果用户程序调用 read() 时没有按键事件,
* 不能让 CPU 空转轮询(太浪费),
* 而是让进程睡眠,等中断来了再唤醒它。
*/
//不需要声明变量,直接返回
static DECLARE_WAIT_QUEUE_HEAD(gpio_key_wait);
/* ========== 环形缓冲区辅助函数 ========== */
/*
* NEXT_POS(x):计算环形缓冲区的下一个位置
*
* 例:BUF_LEN=128,当前位置 x=127 时,next = (127+1) % 128 = 0
* ------从数组末尾绕回到开头,形成"环"
*/
#define NEXT_POS(x) ((x+1) % BUF_LEN)
/* 判断缓冲区是否为空:读写指针指向同一个位置 */
static int is_key_buf_empty(void)
{
return (r == w);
}
/* 判断缓冲区是否已满:写指针的下一个位置等于读指针 */
static int is_key_buf_full(void)
{
return (r == NEXT_POS(w));
}
/* put_key:放入一个按键(ISR / 定时器回调里调用) */
static void put_key(int key)
{
if(!is_key_buf_full())
{
g_keys[w] = key; //赋值
w = NEXT_POS(w); //移动
}
// 如果满了就丢弃------宁可丢新数据,不覆盖未读数据
}
/* get_key:取出一个按键(read 里调用) */
static int get_key(void)
{
int key = 0;
if(!is_key_buf_empty())
{
key = g_keys[r]; //取值
r = NEXT_POS(r); //移动
}
return key;
}
环形缓冲区图解,结尾将拓展动态环形缓冲区
初始状态:r == w == 0(空)
┌────┬────┬────┬────┬────┬────┬────┐
│ │ │ │ │ │ │ │
└────┴────┴────┴────┴────┴────┴────┘
↑
r, w
放入 3 个按键后:r=0, w=3
┌────┬────┬────┬────┬────┬────┬────┐
│ A │ B │ C │ │ │ │ │
└────┴────┴────┴────┴────┴────┴────┘
↑ ↑
r w
读出 A 后:r=1, w=3
┌────┬────┬────┬────┬────┬────┬────┐
│ │ B │ C │ │ │ │ │
└────┴────┴────┴────┴────┴────┴────┘
↑ ↑
r w
持续放入直到 w 绕回:r=1, w=0(即将满)
┌────┬────┬────┬────┬────┬────┬────┐
│ │ B │ C │ D │ E │ F │ G │
└────┴────┴────┴────┴────┴────┴────┘
↑ ↑
r w
第 1 步:read 函数
项目 说明 函数作用 用户程序调用 read(fd, buf, size)时,内核执行这个函数。从环形缓冲区取出一个按键值,安全地拷贝给用户程序。参数 file--- 打开的文件对象;buf--- 用户空间缓冲区指针(__user表示不能直接用*ptr访问);size--- 用户想读多少字节;offset--- 文件偏移量(字符设备通常不用)返回值 实际读到的字节数(成功时为 4),负值表示错误(如 -EAGAIN表示非阻塞模式下暂无数据)
步骤框架
c
static ssize_t gpio_key_drv_read(struct file *file, char __user *buf,
size_t size, loff_t *offset)
{
// 1. 声明 err 变量,接收 copy_to_user 的返回值
// 2. 非阻塞模式检查:
// 如果缓冲区为空 且 文件标志有 O_NONBLOCK → 立刻返回 -EAGAIN
// 3. 调用 wait_event_interruptible(wq, condition)
// 作用:如果缓冲区为空,当前进程睡眠,让出 CPU
// 如果缓冲区不空,立即继续执行
// 4. 调用 get_key() 从缓冲区取出一个按键值
// 5. 调用 copy_to_user(to, from, n)
// 作用:把内核空间的键值安全地拷贝到用户空间 buf
// 6. 返回 4(表示成功读取了 4 个字节,即一个 int)
}
详解
c
static ssize_t gpio_key_drv_read(struct file *file, char __user *buf,
size_t size, loff_t *offset)
{
int err;
int key;
/*
* 非阻塞模式检查
*
* 如果用户程序用 O_NONBLOCK 标志打开设备,
* file->f_flags 里就会有 O_NONBLOCK 标志。
*
* 此时如果缓冲区为空,不睡眠,直接返回 -EAGAIN。
* 用户程序收到 -1 且 errno == EAGAIN 时知道"现在没数据,稍后再试"。
*/
if(is_key_buf_empty() && (file->f_flags & O_NONBLOCK))
return -EAGAIN;
/*
* wait_event_interruptible(队列, 条件)
* 中文含义:"在指定队列中等待,直到条件成立"
*
* 执行过程分两步:
* 1. 检查条件 !is_key_buf_empty():
* - 如果缓冲区不空(有按键事件)→ 直接继续执行,不睡觉
* - 如果缓冲区为空(没按键)→ 当前进程被标记为"可中断睡眠"状态,
* 加入 gpio_key_wait 队列,CPU 立刻切换去运行其他进程
* 2. 被唤醒后,重新检查条件:
* - 条件成立 → 跳出等待,继续往下执行
* - 条件不成立 → 继续睡(这是"虚假唤醒"的防护机制)
*
* interruptible 的含义:这种睡眠可以被信号(Signal)打断
* 比如用户按 Ctrl+C 时会收到 SIGINT 信号,进程会被唤醒并返回 -ERESTARTSYS
*/
//条件不成立阻塞
wait_event_interruptible(gpio_key_wait, !is_key_buf_empty());
/*
* 被唤醒后,从缓冲区取出一个按键值
*/
key = get_key();
/*
* copy_to_user(目标用户空间地址, 源内核空间地址, 字节数)
* 中文含义:"安全地把内核数据复制给用户程序"
*
* 为什么不能直接用 memcpy(buf, &key, 4)?
* 答:buf 是用户空间的地址,内核不能直接访问用户空间的内存
* copy_to_user 会做权限检查、地址合法性校验,防止越权和崩溃
*
* 返回值:未复制完的字节数,0 表示全部成功
*/
err = copy_to_user(buf, &key, 4);
return 4; // 告诉用户程序"我给你读了 4 个字节(int)"
}
深入:wait_event_interruptible 内部是怎么让进程睡觉的?
wait_event_interruptible(wq, condition) 宏的展开逻辑(简化):
if (condition)
return; // 条件已满足,不休眠
// 条件不满足,准备休眠
prepare_to_wait(&wq, &wait, TASK_INTERRUPTIBLE);
// 把自己加入等待队列,状态设为 TASK_INTERRUPTIBLE
for(;;)
{
// current 内核的全局宏,指向当前正在 CPU 上跑的进程
if (signal_pending(current)) // 有无未决信号
return -ERESTARTSYS; // 信号为假,重新执行内核read
if (condition) // 再检查一次条件
break;
schedule(); // 保存当前所有寄存器主动让出 CPU,调度其他进程运行
}
finish_wait(&wq, &wait); // 醒来后,从等待队列移除自己
第 2 步:poll / fasync 回调 & file_operations 结构体
这里包含三个函数和最终的 file_operations 注册表格,每个都有独立的作用/参数说明。
注意:检测按键按下的是中断 + 定时器消抖。 poll 和 fasync 都只是"通知手段",让应用层知道"按键事件已经发生了,快来取数据"。
- gpio_key_drv_poll 函数
项目 说明 函数作用 实现 poll接口。用户程序调用select()/poll()/epoll()时,内核调用此函数检查数据是否就绪。参数 fp--- 文件指针;wait--- poll 等待队列,需要调用poll_wait把当前进程注册上去返回值 0--- 数据未就绪;`POLLIN
-
BAND(带外(紧急)数据,高优先级) 宏:
POLLRDBAND(读)、POLLWRBAND(写) 对应数据:TCP OOB 紧急数据、加急控制指令。 特点:独立缓冲区,插队处理,不跟普通数据排队。 -
NORM(普通常规数据,低优先级) 宏:
POLLRDNORM(读)、POLLWRNORM(写) 对应数据:普通业务报文、字符流、按键、串口普通字符。 特点:进入主缓冲区,按先后顺序排队。
汇总关系
- 总读事件:
POLLIN = POLLRDNORM | POLLRDBAND - 总写事件:
POLLOUT = POLLWRNORM | POLLWRBAND
执行规则
只要 BAND 紧急数据就绪,程序优先处理带外数据,再处理 NORM 普通数据。
2. gpio_key_drv_fasync 函数
项目 说明 函数作用 处理 fasync 注册/注销。用户程序调用 fcntl(fd, F_SETOWN, pid)+ `fcntl(fd, F_SETFL, flags参数 fd--- 文件描述符;file--- 文件指针;on--- 1=注册异步通知,0=注销异步通知返回值 0 成功, -EIO失败
3. file_operations 结构体
项目 说明 作用 驱动向内核注册的"我支持哪些操作"表格。用户程序调用 open/read/poll时,内核查这张表找到对应的函数去执行。关键成员 .owner = THIS_MODULE--- 防止模块正在使用时被 rmmod 卸载;.read--- read 回调;.poll--- poll 回调;.fasync--- fasync 回调
步骤框架
c
// poll 回调框架
static unsigned int gpio_key_drv_poll(struct file *fp, poll_table *wait)
{
// 1. 调用 poll_wait(fp, &gpio_key_wait, wait) 把进程注册到等待队列
// 2. 检查缓冲区是否有数据
// - 有数据 → 返回 POLLIN | POLLRDNORM
// - 无数据 → 返回 0
}
// fasync 回调框架 struct file 文件打开状态
static int gpio_key_drv_fasync(int fd, struct file *file, int on)
{
// 1. 调用 fasync_helper(fd, file, on, &button_fasync)
// 2. 返回 0(成功)或 -EIO(失败)
}
// file_operations 框架
static struct file_operations gpio_key_drv = {
// 1. .owner = THIS_MODULE
// 2. .read = gpio_key_drv_read
// 3. .poll = gpio_key_drv_poll
// 4. .fasync = gpio_key_drv_fasync
};
gpio_key_drv_read() ← 驱动里只写了这一个 read 函数,三种触发路径,调用的是同一个函数
三个通路共享同一个触发源(按键事件),但互不相干:
把当前进程(current)加入 gpio_key_wait 链表 ,被 wake_up_interruptible 叫醒后
1.gpio_key_drv_read 这里简称(read):把按键值从内核缓冲区拷贝到用户空间,然后返回。
2.poll 做的事:返回 POLLIN 标志,告诉用户"有数据可读",用户收到后再去调 read()。
3.fasync 做的事:发 SIGIO 信号给订阅的进程,进程的信号处理函数被异步调用,在 handler 里调 read() 取数据。
用户态与内核态虽然内存地址空间严格隔离 ,但内核拥有管理所有进程的最高权限,信号本身就是内核提供的跨态通信机制 。
隔离只限制:用户代码不能直接读写内核内存;不限制内核主动向用户进程下发事件。
| | 方式 | 谁调用 gpio_key_drv_read | 触发路径 | |---------|-----------------------------------|--------------------------------------| | 阻塞 read | 用户进程主动调 read() | sys_read → VFS → gpio_key_drv_read | | poll | poll 返回 POLLIN 后,用户进程主动调 read() | 同上 | | fasync | SIGIO handler 里用户进程主动调 read() | 同上 | |
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 三种方式最终走的都是同一条路 :用户空间 read() → VFS → gpio_key_drv_read |
三个通路不是固定流程,是教学展示 。实际项目三选一 :普通程序用阻塞 read,有事件循环的用 poll,极端实时场景才用 fasync。驱动里写 wake_up_interruptible 一行就能覆盖 80% 的需求。
| 场景 | 选哪个 | 要不要 kill_fasync |
|---|---|---|
| 简单的单按键测试程序 | 阻塞 read | 不要 |
| Qt/LVGL 事件循环 | poll 或 epoll | 不要 |
| 网络服务器(一个线程管多个连接) | epoll | 不要 |
| 对实时性要求极高的嵌入式程序 | fasync (SIGIO) | 要 |
| 只想学习驱动全貌 | 都写上 | 写上看看 |
绝大多数嵌入式项目只需要 wait_event + poll_wait,不需要 kill_fasync。 用信号做异步通知在 Linux 用户态编程里也极少使用,更多是历史遗留或特定实时场景。
按键事件丢失 / 覆盖,问题不出在 poll_wait/wait,而是出在内核缓冲区。
详解
c
/*
* gpio_key_drv_poll:poll 回调函数
*
* poll_wait:把当前进程加入 poll 等待列表(不阻塞,只是注册)
*
* 返回值:
* 0 --- 数据未就绪(没有按键事件)
* POLLIN | POLLRDNORM --- 数据就绪,可以非阻塞地读
* POLLIN = 有数据可读
* POLLRDNORM = 有"普通"数据可读(网络数据常用)
* 两个标志相同,兼容性兜底
*/
static unsigned int gpio_key_drv_poll(struct file *fp, poll_table *wait)
{
// fp谁是调用者 gpio_key_wait挂到哪个队列 wait用哪个表单登记
poll_wait(fp, &gpio_key_wait, wait); // 注册到这个等待队列上
return is_key_buf_empty() ? 0 : POLLIN | POLLRDNORM;
}
/*
* gpio_key_drv_fasync:处理 fasync 注册/注销
*
* fasync_helper:内核提供的辅助函数,管理 fasync_struct 链表
*
* 当用户程序调用 fcntl(fd, F_SETOWN, pid) 和 fcntl(fd, F_SETFL, flags | FASYNC) 时,
* 内核会调用这个回调函数,把当前进程加入异步通知链表。
* 函数作用:将当前进程从 fasync 订阅链表中移除
*/
static int gpio_key_drv_fasync(int fd, struct file *file, int on)
{
return fasync_helper(fd, file, on, &button_fasync) >= 0 ? 0 : -EIO;
}
/*
* file_operations 结构体:驱动向内核注册的"我支持哪些操作"表格
*
* .owner = THIS_MODULE:告诉内核"这个 file_operations 属于当前模块"
* 作用:防止模块正在被使用时被 rmmod 卸载,是一种保护机制。
* 如果没有 .owner,用户在 read() 阻塞时 rmmod,
* 模块被卸载后 ISR 还在跑,会访问已释放的内存 → 内核崩溃。
*
* .poll:实现 poll 接口,让用户程序可以用 select/poll/epoll 同时监听多个 fd
*
* .fasync:实现 fasync 接口,让用户程序可以注册 SIGIO 异步通知
*/
static struct file_operations gpio_key_drv = {
.owner = THIS_MODULE,
.read = gpio_key_drv_read,
.poll = gpio_key_drv_poll, // 支持多路复用(一个进程监听多个fd)
.fasync = gpio_key_drv_fasync, // 支持异步通知
};
| poll | fasync (SIGIO) | |
|---|---|---|
| 通知方式 | 进程主动查,内核帮你排队 | 内核主动发信号打断你 |
| 进程状态 | 阻塞等待(可以同时等多个 fd) | 该干嘛干嘛,被信号打断 |
| 数据获取 | poll 返回 → 你调 read | 信号 handler 里调 read |
| 能否同时管多个 fd | 能(这是它存在的意义) | 不能(信号只有一个 SIGIO,分不清谁发的) |
| 信号丢失 | 不会(查的是缓冲区状态) | 会(信号不排队,多次按键可能只收到一次) |
| 实时性 | 中等(取决于 poll 间隔/唤醒机制) | 高(信号立即打断) |
| 编程复杂度 | 低(标准 poll/epoll 用法) | 高(信号处理 + 可重入问题) |
第 3 步:中断服务函数 ISR(最好用 threaded_IRQ)
这里涉及三个函数:上半部 ISR 、定时器去抖回调 、下半部线程函数。
gpio_key_isr (Interrupt Service Routine) --- 中断上半部 - 只做硬件操作
项目 说明 函数作用 硬件中断触发时 CPU 跳转执行的函数。运行在中断上下文,绝对不能休眠!只做极简操作:启动去抖定时器。 参数 irq--- 中断号(nterrupt Request 硬件的中断请求);dev_id--- 注册中断时传入的私有数据(这里是&gpio_keys_all[i]),通过它找回是哪个按键触发了中断返回值 IRQ_WAKE_THREAD--- 告诉内核"上半部做完了,请启动下半部线程"
key_timer_expire (到期) --- 定时器去抖回调
项目 说明 函数作用 20ms 去抖时间到、没有新中断续期时,内核调用此函数。此时电平已稳定,可以安全读取 GPIO 值。在这里做真正的重活:读电平、存缓冲区、唤醒等待进程、发送 SIGIO。 参数 t--- 触发超时的timer_list对象,通过from_timer反推出包含它的gpio_key结构体返回值 无(void)
gpio_key_thread_func --- 中断下半部线程
项目 说明 函数作用 运行在独立的内核线程( irq/xxx-thread)里,像普通进程一样可以休眠。可以在这里做日志记录等重活。参数 irq--- 中断号;data--- 注册中断时传入的私有数据(同 dev_id)返回值 IRQ_HANDLED--- 中断已处理
步骤框架
c
// 上半部 ISR 框架:
static irqreturn_t gpio_key_isr(int irq, void *dev_id)
{
// 1. 通过 dev_id 找回是哪个按键触发了中断
// 用:struct gpio_key *gpio_key = dev_id;
// 2. 不在这里读 GPIO!
// 只做:启动去抖定时器(调度定时器)
// 3. 返回 IRQ_WAKE_THREAD(告诉内核"上半部做完了,请启动下半部线程")
}
// 定时器去抖回调框架:
static void key_timer_expire(struct timer_list *t)
{
// 1. 内核只传进来 timer 成员的地址,不给设备结构体指针;
// 必须用 from_timer 反向算出外层 struct key_dev,才能拿到硬件资源
// 每一路按键对应独立结构体,自动锁定当前触发的这一路设备
// 2. gpiod_get_value(gpio_key->gpiod)
// 要从结构体里取出当前按键对应的GPIO句柄,读取真实稳定电平
// 3. 编码键值:(gpio_key->gpio << 8) | val
// 从结构体取出引脚编号,和电平拼成一条完整按键事件
// 4. put_key(key) 存入环形缓冲区
// 缓冲区也是当前dev结构体内部资源,多路按键互不干扰
// 5. wake_up_interruptible(&gpio_key->key_wait)
// 取出本设备专属的等待队列,只唤醒等待该按键的进程,不影响其他按键
// 6. kill_fasync(&gpio_key->fasync, SIGIO, POLL_IN)
// 取出当前设备的异步通知结构体,只给打开此按键的程序发信号
}
// 下半部线程框架:
static irqreturn_t gpio_key_thread_func(int irq, void *data)
{
// 1. 通过 data 找回是哪个按键
// 2. 在这里读 GPIO 电平(因为在线程上下文,可以休眠)
// 3. 打印日志
// 4. 返回 IRQ_HANDLED
}
一、为什么必须反推出结构体?
- 回调函数入参被内核写死,只有
struct timer_list *t,没有 dev 指针。 t仅仅是结构体里一小块成员地址,不含 GPIO、队列、缓冲区。- 不做容器转换,你拿不到这一路按键的任何私有资源。
二、dev 结构体内成员各自的作用
gpiod:GPIO 句柄,用来读取按键电平;gpio:引脚号,用来编码事件,区分多路按键;- 环形缓冲区:保存本次按键事件数据;
key_wait:当前按键专属等待队列,唤醒 poll 休眠;fasync:异步通知结构体,用来向上层应用发送 SIGIO 信号。
一句话总结
只有找回整个设备结构体,你才能拿到这一路按键独有的引脚、缓冲区、等待队列、异步节点,做到多路按键相互隔离,互不串扰。
注意中断上下文一旦休眠会发生什么:
- 中断没有归属进程,没有 task (进程任务结构体 struct task_struct);
- 调度器无法保存现场、无法切换任务;
- CPU 卡在中断处理函数里再也退不出来;
- 所有后续中断被屏蔽,整台机器硬死锁。
desc = GPIO 引脚的内核句柄,用来指定读哪一个引脚的电平。 
1.devm_gpiod_get_index() --- 解析 DTS,把 <&gpio4 14 GPIO_ACTIVE_LOW> 变成内核里的 gpio_desc *(引脚对应的结构体),存到结构体。
2.from_timer() --- 定时器回调只给你 timer_list *t,这个宏用地址减法从 t 反推出包含它的 gpio_key 结构体首地址。
3.gpiod_get_value() --- 传入 desc,读硬件寄存器返回当前电平(0/1),并自动处理 GPIO_ACTIVE_LOW 极性翻转。
详解
c
/*
* 为什么用 threaded_IRQ 而不是传统 ISR + tasklet + work_struct?
*
* 三种下半部方案对比:
* | 机制 | 运行上下文 | 能否休眠 | 推荐度 |
* |--------------|------------------|----------|--------|
* | tasklet | 软中断上下文 | 不能 | ⭐⭐ |
* | work_struct | 内核工作线程 | 能 | ⭐⭐⭐ |
* | threaded_IRQ | 独立内核线程 | 能! | ⭐⭐⭐⭐⭐ |
*
* threaded_IRQ 是最推荐的方案,因为:
* - 编写最简单(不需要手动管理 tasklet 或 work_struct)
* - 运行在独立线程里,可以休眠(可以调用 GFP_KERNEL[获取空闲页会休眠,不能在无进程的中断上下
文使用]、copy_to_user 等)
* - 优先级可调(用 chrt 或 nice 调整)
* - 不会拖慢其他中断的处理
*/
/*
* gpio_key_isr:中断上半部(Top Half)
*
* 当一个 GPIO 引脚的电平发生跳变时,硬件中断控制器会发信号给 CPU,
* CPU 暂停当前任务,跳转到这个函数执行。
*
* ⚠️ 关键约束:这个函数运行在"中断上下文",绝对不能休眠!
* - 不能用 kzalloc / kfree(可能触发内存回收,会休眠)
* - 不能用 copy_to_user(可能触发缺页异常,会休眠)
* - 不能用 mutex_lock(可能等待锁,会休眠)
* - 只能做极简操作:启动定时器、调度下半部
*
* 最佳实践:上半部只做"标记"或"启动定时器",
* 真正的重活留给下半部线程处理。
*/
static irqreturn_t gpio_key_isr(int irq, void *dev_id)
{
struct gpio_key *gpio_key = dev_id;
/*
* 启动去抖定时器
*
* mod_timer(&timer, expires):修改定时器的超时时间
*
* jiffies:内核维护的"滴答数",系统启动以来的时钟中断次数
* HZ:每秒的滴答数(通常为 100、250 或 1000,取决于内核配置)
* HZ/50:如果 HZ=100,则 HZ/50=2,即 2 个 jiffies ≈ 20ms
*
* 这个调用同时有"续期"效果:如果定时器已经启动,
* mod_timer 会先删除旧的超时设定,再设新的。
* 这就是去抖的核心原理:抖动期间的中断只是"续期",
* 只有 20ms 内没有新中断时,才认为电平稳定了。
*/
// 激活定时器
mod_timer(&gpio_key->key_timer, jiffies + HZ/50);
/*
* IRQ_WAKE_THREAD:告诉内核"上半部做完了,请启动下半部线程"
*
* 内核收到这个返回值后,会唤醒对应的中断线程(irq/xxx-thread),
* 在线程里执行 gpio_key_thread_func。
*/
return IRQ_WAKE_THREAD;
}
/*
* key_timer_expire:定时器超时回调函数
*
* 当 20ms 内没有新中断时,定时器超时,内核调用这个函数。
* 此时电平已经稳定,可以读取 GPIO 值了。
*
* 参数 t 是触发超时的定时器对象。
*
* from_timer 宏:根据成员指针反推出包含它的结构体指针
* 等价于:container_of(t, struct gpio_key, key_timer)
*/
static void key_timer_expire(struct timer_list *t)
{
struct gpio_key *gpio_key = from_timer(gpio_key, t, key_timer);
int val;
int key;
/*
* 读取稳定的 GPIO 电平
*
* gpiod_get_value(desc):读取 GPIO 当前电平
* 返回值:0(低电平)或 1(高电平)
*
* 注意具体含义取决于 flag:
* 普通模式:0 = 低电平,1 = 高电平
* OF_GPIO_ACTIVE_LOW:0 = 按键按下(有效),1 = 按键松开
*/
val = gpiod_get_value(gpio_key->gpiod);
/*
* 编码键值:用一个 int 同时携带"哪个按键"和"什么状态"
* (gpio_key->gpio << 8) | val
*
* 例:gpio=142, val=0(按下)→ (142<<8)|0 = 0x8E00 = 36352
*
* 用户程序解码:
* int key_val = *(int*)buf;
* int gpio_num = (key_val >> 8) & 0xFF; // = 142(GPIO4_14)
* int level = key_val & 0xFF; // = 0(按下)
*/
key = (gpio_key->gpio << 8) | val;
/*
* 存入环形缓冲区
*/
put_key(key);
/*
* 唤醒等待队列里所有睡眠的进程
*/
wake_up_interruptible(&gpio_key_wait);
/*
* 发送 SIGIO 信号给注册了异步通知的进程
*
* kill_fasync:向注册了异步通知的进程发送信号
* 参数:
* fasync --- 异步通知结构
* SIGIO --- 发送的信号类型
* POLL_IN --- 事件类型(数据可读)
*/
kill_fasync(&button_fasync, SIGIO, POLL_IN);
}
/*
* gpio_key_thread_func:中断下半部线程处理函数
*
* 运行在独立的内核线程(irq/xxx-thread)里,和普通进程几乎一样:
* - 可以休眠!
* - 可以用 GFP_KERNEL!
* - 可以调用 copy_to_user!
*
* 为什么还要线程化下半部?
* 因为我们的去抖方案是"定时器 + 线程化",
* 定时器回调里已经做了存数据、唤醒进程、发信号,
* 所以线程化函数这里可以做一些"更重的活",
* 比如打印进程名、记录日志等。
*
* 注意:在这个驱动里,线程化函数不是必须的
* (因为定时器回调已经做了所有事情)。
* 但保留它是最完整的做法,符合 threaded_IRQ 的标准用法。
*/
static irqreturn_t gpio_key_thread_func(int irq, void *data)
{
struct gpio_key *gpio_key = data;
int val;
val = gpiod_get_value(gpio_key->gpiod);
/*
* 打印当前进程名和 PID
* 你会发现它运行在一个叫 "irq/xxx-gpio_key" 的内核线程里
*/
printk("thread_func: process = %s pid = %d\n",
current->comm, current->pid);
printk("thread_func key %d %d\n", gpio_key->gpio, val);
return IRQ_HANDLED; //中断处理正常,否则IRQ_NONE处理异常
}
第 4 步:probe 函数(最核心,步骤最多)
项目 说明 函数作用 Platform 总线匹配成功后自动调用的初始化函数。按顺序完成:①读设备树获取 GPIO 信息 → ②分配内存 → ③初始化去抖定时器 → ④注册中断 → ⑤注册字符设备创建 /dev/gpio_key。参数 pdev--- 平台设备对象指针,通过pdev->dev.of_node拿到设备树节点信息返回值 0 成功,负值失败
步骤框架
c
static int gpio_key_probe(struct platform_device *pdev)
{
/*
* 第 1-2 步:搞清楚有几个按键
* 目的:DTS 里只写了文字,内核还不知道要管几个 GPIO。
* 这里从设备树读出数量,决定后续分配多少资源。
*/
struct device_node *node = pdev->dev.of_node; // 拿到 DTS 节点
int count = of_gpio_count(node); // 数一下定义了几个 GPIO
if(count == 0)
{
printk("gpio_key: 没有定义 GPIO\n");
return -1;
}
/*
* 第 3-4 步:给每个按键分配数据结构,记录它的"身份信息"
* 目的:一个 gpio_key 结构体 = 一个物理按键的身份证,
* 里面存好 GPIO 编号、描述符、中断号、极性、定时器,
* 后续 ISR、定时器回调、read 全都靠这张身份证找到对应的引脚。
*/
gpio_keys_all = kzalloc(sizeof(struct gpio_key) * count, GFP_KERNEL);
// kzalloc 同时分配 + 清零,比 kmalloc + memset 少写一行
int i;
for(i = 0; i < count; i++)
{
enum of_gpio_flags flag;
// ① 拿到 GPIO 编号(全局整数,如 110 = gpio4_14)
// node设备树节点
gpio_keys_all[i].gpio = of_get_gpio_flags(node, i, &flag);
if(gpio_keys_all[i].gpio < 0)
{
printk("gpio_key: 第 %d 个 GPIO 解析失败\n", i);
return -1;
}
// ② 把编号转成 gpio_desc(新 API 的引脚句柄,后续 定时器回调 用)
gpio_keys_all[i].gpiod = gpio_to_desc(gpio_keys_all[i].gpio);
// ③ 记录极性:GPIO_ACTIVE_LOW = 按下时低电平有效
gpio_keys_all[i].flag = flag & OF_GPIO_ACTIVE_LOW;
// ④ GPIO 编号 转换为 中断号(按键按下触发哪个硬件中断)
gpio_keys_all[i].irq = gpio_to_irq(gpio_keys_all[i].gpio);
// ⑤ 初始化消抖定时器(只是初始化,还没激活)
timer_setup(&gpio_keys_all[i].key_timer, key_timer_expire, 0);
gpio_keys_all[i].key_timer.expires = ~0; // 设为"永不到期"
add_timer(&gpio_keys_all[i].key_timer); // 挂入内核定时器链表
// 定时器处于"休眠"状态,需要 ISR 里 mod_timer 才能激活
}
/*
* 第 5 步:给每个按键注册中断
* 目的:告诉 CPU------"这个 GPIO 引脚电平一跳变,立刻调我的 ISR"。
* request_threaded_irq 支持上半部(硬中断)+ 下半部(线程),
* ISR 里只做 mod_timer(极轻),消抖和写缓冲区交给定时器回调。
*/
for(i = 0; i < count; i++)
{
request_threaded_irq(
gpio_keys_all[i].irq, // 中断号
gpio_key_isr, // 上半部:硬中断上下文(立刻执行)
NULL, // 下半部线程:NULL = 不用(消抖交给定时器)
IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,
//(中断请求标志) 上升沿 + 下降沿都触发
"gpio_key", // 中断名称(cat /proc/interrupts 可见)
&gpio_keys_all[i] // dev_id:传结构体指针,ISR 里用来找按键
);
}
/*
* 第 6-8 步:创建用户空间访问入口
* 目的:让应用层能 open("/dev/gpio_key") 然后 read/poll,
* 没有这三步,/dev 下就没有 gpio_key 这个文件。
*/
int major = register_chrdev(0, "gpio_key", &gpio_key_drv);
// 0:自动分配设备号 gpio_key: 名字(cat /proc/devices 可见)gpio_key_drv:读写操作表
gpio_key_class = class_create(THIS_MODULE, "gpio_key_class");
// 在 /sys/class/ 下创建目录,udev 靠这个目录来触发设备节点创建
if(IS_ERR(gpio_key_class))
{
unregister_chrdev(major, "gpio_key"); // 失败了要回退上一步
return PTR_ERR(gpio_key_class);
}
device_create(gpio_key_class, NULL, MKDEV(major, 0), NULL, "gpio_key");
// 触发 udev → 自动在 /dev/ 下创建 /dev/gpio_key 设备文件
return 0; // 一切就绪,设备可用
}
详解
c
/*
* gpio_key_probe:Platform 总线匹配成功后自动调用的初始化函数
*
* 这个函数的职责是按顺序完成 5 件事:
* ① 读设备树,获取 GPIO 信息
* ② 分配内存,保存每个按键的数据
* ③ 初始化去抖定时器
* ④ 注册中断(把 ISR 和 GPIO 绑定)------ 用 threaded_IRQ(最推荐)
* ⑤ 注册字符设备(创建 /dev/ 下的设备文件)
*/
static int gpio_key_probe(struct platform_device *pdev)
{
int err;
struct device_node *node = pdev->dev.of_node; // 获取设备树节点指针
int count;
int i;
enum of_gpio_flags flag;
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
/*
* of_gpio_count(node):统计设备树里 gpios 属性有几个 GPIO 引脚
*
* 假设设备树里写的是:
* gpios = <&gpio4 14 ...>, <&gpio4 15 ...>;
* 返回值就是 2,表示有 2 个按键。
*/
count = of_gpio_count(node);
if(!count)
{
printk("no gpio available\n");
return -1;
}
/*
* kzalloc(size, flags):在内核空间分配内存
*
* size --- 要分配的字节数(sizeof(struct gpio_key) * count)
* flags --- GFP_KERNEL(常规分配,允许休眠)| GFP_ATOMIC(原子分配,ISR 中用)
* 注:probe在驱动匹配成功后只执行一次,ISR只要引脚变化就执行一次
* kzalloc vs kmalloc:z = zero,分配后自动把内存全部清零
* 这避免了"读垃圾数据"的 Bug。
*
* 为什么不用全局数组?
* 因为设备树里配了几个按键是运行时才知道的,
* 编译时无法确定数组大小,必须动态分配。
*/
gpio_keys_all = kzalloc(sizeof(struct gpio_key) * count, GFP_KERNEL);
/* 遍历每个 GPIO,从设备树读取信息并初始化去抖定时器 */
for(i = 0; i < count; i++)
{
/* 解析 DTS,搞清楚引脚是谁、什么属性of_get_gpio_flags
* 设备树里怎么写:gpios = <&gpio4 14 GPIO_ACTIVE_LOW>;
* of_get_gpio_flags(node, index, &flags):读取第 N 个 GPIO 信息
*
* 参数:
* node --- 设备树节点
* index --- 索引,0 表示第一个 gpios 元素
* flags --- 输出参数,拿到 GPIO 的标志(如 OF_GPIO_ACTIVE_LOW)
*
* 返回值:GPIO 硬件编号(如 142)
* GPIO 编号计算:bank编号 × 32 + 引脚编号
* 例:GPIO4_14 → 4 × 32 + 14 = 142(不同芯片计算方式可能不同)
*/
gpio_keys_all[i].gpio = of_get_gpio_flags(node, i, &flag);
if(gpio_keys_all[i].gpio < 0)
{
printk("of_get_gpio_flags failed\n");
return -1;
}
/* gpio_to_desc:用 GPIO 编号换内核内部的 gpio_desc 对象 */
gpio_keys_all[i].gpiod = gpio_to_desc(gpio_keys_all[i].gpio);
/* 保存标志位 */
gpio_keys_all[i].flag = flag & OF_GPIO_ACTIVE_LOW;
/* gpio_to_irq:把这个 GPIO 编号转换成对应的中断号 */
gpio_keys_all[i].irq = gpio_to_irq(gpio_keys_all[i].gpio);
/*
* 初始化去抖定时器
*
* timer_setup(timer, callback, flags):初始化定时器
* 参数:定时器指针、超时回调函数、额外标志(0 = 普通定时器)
*
* timer.expires = ~0:设为"永远不超时"
* 让它闲置,等 ISR 触发才用 mod_timer 启动它。
*
* add_timer:把定时器加入内核的定时器管理链表
* 之后这个定时器就可以用 mod_timer 来启动了。
*/
timer_setup(&gpio_keys_all[i].key_timer, key_timer_expire, 0);
gpio_keys_all[i].key_timer.expires = ~0;
add_timer(&gpio_keys_all[i].key_timer);
}
/*
* 为每个按键注册中断(用 threaded_IRQ,最推荐的下半部方案)
*
* request_threaded_irq(irq, handler, thread_fn, irqflags, devname, dev_id)
*
* irq --- 中断号
* handler --- 上半部函数(硬件 ISR),返回 IRQ_WAKE_THREAD 时启动下半部
* thread_fn --- 下半部线程处理函数(运行在独立内核线程里,可以休眠!)
* irqflags --- 触发方式(IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING)
* devname --- 中断名称(cat /proc/interrupts 可见)
* dev_id --- 传给 ISR 和 thread_fn 的私有数据指针
* 在 ISR 里通过 dev_id 拿到,就能知道是哪个按键触发了中断
*
* 边沿触发:只抓跳变,防止电平一直保持时反复进中断。
* 下降沿控制中断什么时候来;低电平有效控制电平值怎么翻译,二者配合实现按键中断。
* IRQF_TRIGGER_RISING:上升沿触发(电平从 0 变 1,按键松开)
* IRQF_TRIGGER_FALLING:下降沿触发(电平从 1 变 0,按键按下)
* 同时写两个:按下和松开都会触发中断
*/
for (i = 0; i < count; i++)
{
err = request_threaded_irq(
gpio_keys_all[i].irq,
gpio_key_isr, // 上半部(硬件 ISR)
gpio_key_thread_func, // 下半部(线程化处理函数)
IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,
"gpio_key",
&gpio_keys_all[i] // 传给 ISR 和 thread_fn 的参数(dev_id)
);
}
/*
* register_chrdev(major, name, fops):注册字符设备
*
* 参数1:major=0 表示"请内核自动分配设备号"
* 参数2:设备名称,在 /proc/devices 里可见
* 参数3:file_operations 指针
* 返回值:分配的主设备号
*/
major = register_chrdev(0, "gpio_key", &gpio_key_drv);
/*
* class_create(owner, name):创建设备类
* 会在 /sys/class/ 下创建对应目录,让用户和 udev 看到这个设备
*/
gpio_key_class = class_create(THIS_MODULE, "gpio_key_class");
if(IS_ERR(gpio_key_class))
{
unregister_chrdev(major, "gpio_key");
return PTR_ERR(gpio_key_class);
}
/*
* device_create(class, parent, devt, drvdata, name):创建设备节点
*
* MKDEV(major, 0):把主设备号和次设备号 0 组合成一个 dev_t
* 主设备号:区分不同驱动(如按键是 200,串口是 204)
* 次设备号:同一驱动下的不同设备实例(如 4 个按键可以分别是 0,1,2,3)
*
* 这个函数调用后,udev 会自动在 /dev/ 下创建 /dev/gpio_key
*/
device_create(gpio_key_class, NULL, MKDEV(major, 0), NULL, "gpio_key");
return 0;
}
| 函数 | 名字参数 | 出现在哪里 | 给谁看 | 干什么用 |
|---|---|---|---|---|
register_chrdev 驱动注册登记表 |
"gpio_key" |
/proc/devices |
内核 / 用户排查 | 告诉内核"这个主设备号对应哪个驱动" |
class_create 设备分类目录 |
"gpio_key_class" |
/sys/class/gpio_key_class/ |
udev / 用户空间工具 | 告诉系统"这类设备是什么类型" |
device_create 打开的设备文件 |
"gpio_key" |
/dev/gpio_key |
用户程序 | 用户 open() 时用的设备文件名 |
驱动读设备树的核心函数链速查
pdev->dev.of_node → 拿到设备树节点指针
↓
of_gpio_count(node) → 统计有几个 GPIO
↓
of_get_gpio_flags(node,i) → 读第 i 个 GPIO 的编号 + 标志
↓
gpio_to_desc(gpio) → 编号 → 内核描述符
gpio_to_irq(gpio) → 编号 → 中断号
第 5 步:remove 函数
项目 说明 函数作用 驱动卸载时调用,负责释放所有资源。释放顺序必须是"后申请的先释放"(堆栈式后进先出),和 probe 正好反过来。 参数 pdev--- 平台设备对象指针 (用来识别唯一设备)返回值 0 成功
步骤框架
c
| 顺序 | probe 里申请 | remove 里释放 | 说明 |
|---|---|---|---|
| ① | kzalloc |
kfree |
内存最先申请,最后释放 |
| ② | request_threaded_irq |
free_irq |
中断 |
| ③ | timer_setup + mod_timer |
del_timer_sync |
定时器 |
| ④ | register_chrdev |
unregister_chrdev |
字符设备 |
| ⑤ | class_create |
class_destroy |
设备类 |
| ⑥ | device_create |
device_destroy |
设备节点,最先释放 |
static int gpio_key_remove(struct platform_device *pdev)
{
// 1. 从 pdev->dev.of_node 拿到设备树节点,存入 node
// 2. 调用 device_destroy(class, devt)
// 作用:删除 /dev/gpio_key 设备节点
// 3. 调用 class_destroy(class)
// 作用:删除 /sys/class/gpio_key_class/ 目录
// 4. 调用 unregister_chrdev(major, name)
// 作用:注销字符设备
// 5. 调用 of_gpio_count(node) 拿到 count
// for 循环调用 free_irq(irq, dev_id)
// 作用:释放每个按键注册的中断
// 6. for 循环调用 del_timer(&gpio_keys_all[i].key_timer)
// 作用:删除每个按键的去抖定时器
// 7. 调用 kfree(gpio_keys_all)
// 作用:释放 kzalloc 分配的内存
// 8. 返回 0
}
详解
c
/*
* gpio_key_remove:驱动卸载时调用,负责释放所有资源
*
* 释放顺序必须是"先释放后申请的"(堆栈式的后进先出)
* probe 的顺序: kzalloc → request_threaded_irq → register_chrdev → class_create →
device_create
* remove 的顺序: device_destroy → class_destroy → unregister_chrdev → free_irq →
del_timer → kfree
* (正好反过来)
*/
static int gpio_key_remove(struct platform_device *pdev)
{
struct device_node *node = pdev->dev.of_node;
int count;
int i;
device_destroy(gpio_key_class, MKDEV(major, 0)); // 删 /dev/ 下的节点
class_destroy(gpio_key_class); // 删 /sys/class/ 下的目录
unregister_chrdev(major, "gpio_key"); // 注销字符设备
count = of_gpio_count(node);
for(i = 0; i < count; i++)
{
free_irq(gpio_keys_all[i].irq, &gpio_keys_all[i]); // 释放中断号
del_timer(&gpio_keys_all[i].key_timer); // 删除去抖定时器
}
kfree(gpio_keys_all); // 释放内存
return 0;
}
第 6 步:of_device_id 匹配表 (驱动内可以匹配的设备节点)
项目 说明 作用 告诉内核"我能驱动哪些设备树节点"。内核用 compatible字符串在设备树和驱动之间做匹配。规则 compatible字符串必须和设备树里的一模一样;最后一个空元素{ }是数组结束标记
步骤框架
c
static const struct of_device_id ????[] = {
// 1. 填写 compatible 字符串,必须和设备树里的 compatible 一模一样
// 格式:{ .compatible = "?????" },
// 2. 数组结束标记:{ },
};
详解
c
设备树内包含 厂家(gpio-key) + 设备名称(gpio_key)
|-----------|----------------------------------------------|
| 设备树文件大概路径 | arch/arm/boot/dts/100ask_imx6ull-14x14.dts |
/*
* of_device_id 数组:告诉内核"我能驱动哪些设备树节点"
*
* compatible = "gpio-key,gpio_key" 必须和设备树里写的一模一样
* 内核会用这个字符串做匹配。
*
* 最后一个空元素 { } 是数组结束标记,内核遍历到这里就知道没有其他 compatible 了。
*
* 为什么用 "gpio-key,gpio_key" 而不是 "100ask,gpio_key"?
* 因为 Linux 官方设备树绑定规范推荐用连字符分隔厂商名和产品名,
* 这样看起来更规范,也和其他官方驱动保持一致。
*/
static const struct of_device_id gpio_key_match_table[] = {
{ .compatible = "gpio-key,gpio_key" },
{ },
};
第 7 步:platform_driver 结构体
项目 说明 作用 描述一个平台驱动:谁做 probe、谁做 remove、match 表是什么。内核由此知道匹配成功/卸载时该调哪个函数。 关键成员 .probe--- 匹配成功时的初始化函数;.remove--- 卸载时的清理函数;.driver.of_match_table--- 指向 compatible 匹配表
步骤框架
c
static struct platform_driver gpio_keys_driver = {
// 1. .probe = ????(设备匹配成功时调用)
// 2. .remove = ????(驱动卸载时调用)
// 3. .driver.name = "????";
// 4. .driver.of_match_table = ????(指向匹配表)
};
详解
c
| 名字 | 谁跟谁比 | 什么时候用 |
|---|---|---|
.name |
platform_device 的 name 跟驱动的 name | 没有设备树时 |
.of_match_table |
设备树的 compatible 跟表里的 compatible | 有设备树时(现代写法) |
/*
* platform_driver 结构体:描述一个平台驱动
*
* .probe = 设备匹配成功时调用(初始化)
* .remove = 驱动卸载时调用(清理)
* .driver.of_match_table = 指向 compatible 匹配表
*/
static struct platform_driver gpio_keys_driver = {
.probe = gpio_key_probe,
.remove = gpio_key_remove,
.driver = {
.name = "gpio_key",
.of_match_table = gpio_key_match_table,
},
};
内核启动代码是 Linux 内核自带的,你下载内核源码时就已经在里面了。
谁写了什么
| 内容 | 谁写的 | 你需要管吗 |
|---|---|---|
| 内核启动代码(解析设备树、建 device_node 树) | Linus 和内核社区 | ❌ 不用管 |
设备树 .dts 文件 |
芯片厂家 / 板子厂家提供 | 改一改就行 |
你的驱动 .c 文件 |
你自己写 | ✅ 这是你要干的 |
文件在内核源码哪里
内核源码/
├── init/main.c ← 内核启动入口(start_kernel)
├── drivers/of/base.c ← 设备树解析代码
├── drivers/of/fdt.c ← 把 .dtb 解析成 device_node 树
├── arch/arm/boot/dts/ ← 设备树 .dts 文件(厂家提供的)
└── 你的驱动.c ← 你写的,放在这里或外部单独编译
你作为驱动开发者只需要做两件事
① 写 .dts 里加一个节点(描述你的硬件)
compatible = "gpio-key,gpio_key";
② 写 .c 驱动(处理这个硬件)
of_device_id 里写一样的 compatible
platform_driver_register 注册
内核启动时怎么解析设备树、怎么建树、怎么匹配------这些基础设施内核早就写好了,你直接用就行。就像开饭店不需要自己修路,路政府早就修好了,只管开店。
第 8 步:入口函数 insmod → register → 匹配 → probe
项目 说明 函数作用 insmod时自动执行。把驱动注册到 Platform 总线,内核开始匹配设备树。参数 无 返回值 platform_driver_register的返回值(0 成功,负值失败)
步骤框架
c
static int __init gpio_key_init(void)
{
// 1. 打印一句日志(可选,方便 dmesg 查看加载情况)
// 2. 调用 platform_driver_register(&drv)
// 作用:把驱动注册到 Platform 总线,内核开始匹配设备树
// 返回 platform_driver_register 的返回值
}
详解
c
/*
* gpio_key_init:模块入口函数,insmod 时自动执行
*
* __init 宏:告诉内核这个函数只在初始化时用,之后可以回收它的内存
*
* 原理:链接器把所有 __init 标记的函数放到一个特殊的"初始化代码段",
* 初始化完成后,内核把这个段的内存释放掉(节省内存)。
*/
static int __init gpio_key_init(void)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
return platform_driver_register(&gpio_keys_driver);
}
第 9 步:出口函数
项目 说明 函数作用 rmmod时自动执行。从 Platform 总线注销驱动,内核自动调用 remove 释放所有资源。参数 无 返回值 无(void)
先自己写------步骤框架
c
static void __exit gpio_key_exit(void)
{
// 1. 打印一句日志(可选)
// 2. 调用 platform_driver_unregister(&drv)
// 作用:从 Platform 总线注销驱动,内核自动调用 remove
}
详解
c
/*
* gpio_key_exit:模块出口函数,rmmod 时自动执行
*
* __exit 宏:告诉内核这个函数只在卸载时用,不用常驻内存
*
* 对于内置编译(built-in)的驱动,__exit 标记的函数会被直接丢弃
*(因为不会卸载)。
*/
static void __exit gpio_key_exit(void)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
platform_driver_unregister(&gpio_keys_driver);
}
module_init |
platform_driver_register |
|
|---|---|---|
| 注册的是 | 一个函数(init函数) | 一个驱动对象(platform_driver结构体) |
| 注册到哪 | 内核模块系统 | Platform 总线 |
| 作用 | 告诉内核:insmod 时调这个函数 | 告诉内核:我是个平台驱动,去总线上找匹配的设备 |
| 后续动作 | insmod 直接执行 init 里的代码 | 内核去匹配设备树 → 匹配成功再调 probe |
一句话:
module_init--- 注册函数,insmod 时直接执行platform_driver_register--- 注册驱动,注册完还要等内核匹配设备树才能执行 probe
第 10 步:模块宏
项目 说明 module_init(fn)指定模块入口函数, insmod时调用module_exit(fn)指定模块出口函数, rmmod时调用MODULE_LICENSE("GPL")声明许可证。不写 GPL 会导致内核标记污染(tainted),且无法使用 GPL-only 的内核符号
c
module_init(gpio_key_init);
module_exit(gpio_key_exit);
MODULE_LICENSE("GPL");
第三部分:完整代码(生产级)
把上面所有内容合并起来的完整驱动代码。
c
#include <linux/module.h>
#include <linux/poll.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <linux/gpio/consumer.h>
#include <linux/platform_device.h>
#include <linux/of_gpio.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/slab.h>
#include <linux/fcntl.h>
#include <linux/timer.h>
#include <linux/workqueue.h>
#include <asm/current.h>
/* ===== 数据结构 ===== */
struct gpio_key {
int gpio;
struct gpio_desc *gpiod;
int flag;
int irq;
struct timer_list key_timer; // 去抖定时器
};
static struct gpio_key *gpio_keys_all;
static int major = 0;
static struct class *gpio_key_class;
/* ===== 环形缓冲区 ===== */
#define BUF_LEN 128
static int g_keys[BUF_LEN];
static int r = 0, w = 0;
#define NEXT_POS(x) ((x+1) % BUF_LEN)
static int is_key_buf_empty(void) { return (r == w); }
static int is_key_buf_full(void) { return (r == NEXT_POS(w)); }
static void put_key(int key) { if (!is_key_buf_full()) { g_keys[w] = key; w = NEXT_POS(w); } }
static int get_key(void) { int key = 0; if (!is_key_buf_empty()) { key = g_keys[r]; r = NEXT_POS(r); } return key; }
struct fasync_struct *button_fasync;
static DECLARE_WAIT_QUEUE_HEAD(gpio_key_wait);
/* ===== 定时器去抖回调 ===== */
static void key_timer_expire(struct timer_list *t)
{
struct gpio_key *gpio_key = from_timer(gpio_key, t, key_timer);
int val;
int key;
val = gpiod_get_value(gpio_key->gpiod);
printk("key_timer_expire key %d %d\n", gpio_key->gpio, val);
key = (gpio_key->gpio << 8) | val;
put_key(key);
wake_up_interruptible(&gpio_key_wait);
kill_fasync(&button_fasync, SIGIO, POLL_IN);
}
/* ===== 中断上半部 ===== */
static irqreturn_t gpio_key_isr(int irq, void *dev_id)
{
struct gpio_key *gpio_key = dev_id;
mod_timer(&gpio_key->key_timer, jiffies + HZ/50); // 去抖:续期 20ms
return IRQ_WAKE_THREAD; // 启动下半部线程
}
/* ===== 中断下半部线程 ===== */
static irqreturn_t gpio_key_thread_func(int irq, void *data)
{
struct gpio_key *gpio_key = data;
int val = gpiod_get_value(gpio_key->gpiod);
printk("thread_func: process=%s pid=%d\n", current->comm, current->pid);
printk("thread_func key %d %d\n", gpio_key->gpio, val);
return IRQ_HANDLED;
}
/* ===== file_operations ===== */
static ssize_t gpio_key_drv_read(struct file *file, char __user *buf,
size_t size, loff_t *offset)
{
int err;
int key;
if (is_key_buf_empty() && (file->f_flags & O_NONBLOCK))
return -EAGAIN;
wait_event_interruptible(gpio_key_wait, !is_key_buf_empty());
key = get_key();
err = copy_to_user(buf, &key, 4);
return 4;
}
static unsigned int gpio_key_drv_poll(struct file *fp, poll_table *wait)
{
poll_wait(fp, &gpio_key_wait, wait);
return is_key_buf_empty() ? 0 : POLLIN | POLLRDNORM;
}
static int gpio_key_drv_fasync(int fd, struct file *file, int on)
{
return fasync_helper(fd, file, on, &button_fasync) >= 0 ? 0 : -EIO;
}
static struct file_operations gpio_key_drv = {
.owner = THIS_MODULE,
.read = gpio_key_drv_read,
.poll = gpio_key_drv_poll,
.fasync = gpio_key_drv_fasync,
};
/* ===== probe / remove ===== */
static int gpio_key_probe(struct platform_device *pdev)
{
int err;
struct device_node *node = pdev->dev.of_node;
int count;
int i;
enum of_gpio_flags flag;
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
count = of_gpio_count(node);
if (!count) {
printk("no gpio available\n");
return -1;
}
gpio_keys_all = kzalloc(sizeof(struct gpio_key) * count, GFP_KERNEL);
for (i = 0; i < count; i++) {
gpio_keys_all[i].gpio = of_get_gpio_flags(node, i, &flag);
if (gpio_keys_all[i].gpio < 0) {
printk("of_get_gpio_flags failed\n");
return -1;
}
gpio_keys_all[i].gpiod = gpio_to_desc(gpio_keys_all[i].gpio);
gpio_keys_all[i].flag = flag & OF_GPIO_ACTIVE_LOW;
gpio_keys_all[i].irq = gpio_to_irq(gpio_keys_all[i].gpio);
timer_setup(&gpio_keys_all[i].key_timer, key_timer_expire, 0);
gpio_keys_all[i].key_timer.expires = ~0;
add_timer(&gpio_keys_all[i].key_timer);
}
for (i = 0; i < count; i++) {
err = request_threaded_irq(
gpio_keys_all[i].irq,
gpio_key_isr,
gpio_key_thread_func,
IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,
"gpio_key",
&gpio_keys_all[i]
);
}
major = register_chrdev(0, "gpio_key", &gpio_key_drv);
gpio_key_class = class_create(THIS_MODULE, "gpio_key_class");
if (IS_ERR(gpio_key_class)) {
unregister_chrdev(major, "gpio_key");
return PTR_ERR(gpio_key_class);
}
device_create(gpio_key_class, NULL, MKDEV(major, 0), NULL, "gpio_key");
return 0;
}
static int gpio_key_remove(struct platform_device *pdev)
{
struct device_node *node = pdev->dev.of_node;
int count;
int i;
device_destroy(gpio_key_class, MKDEV(major, 0));
class_destroy(gpio_key_class);
unregister_chrdev(major, "gpio_key");
count = of_gpio_count(node);
for (i = 0; i < count; i++) {
free_irq(gpio_keys_all[i].irq, &gpio_keys_all[i]);
del_timer(&gpio_keys_all[i].key_timer);
}
kfree(gpio_keys_all);
return 0;
}
static const struct of_device_id gpio_key_match_table[] = {
{ .compatible = "gpio-key,gpio_key" },
{ },
};
static struct platform_driver gpio_keys_driver = {
.probe = gpio_key_probe,
.remove = gpio_key_remove,
.driver = {
.name = "gpio_key",
.of_match_table = gpio_key_match_table,
},
};
static int __init gpio_key_init(void)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
return platform_driver_register(&gpio_keys_driver);
}
static void __exit gpio_key_exit(void)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
platform_driver_unregister(&gpio_keys_driver);
}
module_init(gpio_key_init);
module_exit(gpio_key_exit);
MODULE_LICENSE("GPL");
第四部分:测试程序
测试一:阻塞模式
c
/* test_blocking.c */
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main(void)
{
int fd = open("/dev/gpio_key", O_RDONLY);
if (fd < 0) {
perror("open");
return 1;
}
printf("Waiting for key press...\n");
while (1) {
int key_val;
int ret = read(fd, &key_val, sizeof(key_val));
if (ret == sizeof(key_val)) {
int gpio = (key_val >> 8) & 0xFF;
int level = key_val & 0xFF;
printf("GPIO %d: %s\n", gpio, level ? "RELEASED" : "PRESSED");
}
}
close(fd);
return 0;
}
测试二:poll/select 多路复用
c
/* test_poll.c */
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/select.h>
int main(void)
{
int fd = open("/dev/gpio_key", O_RDONLY);
if (fd < 0) {
perror("open");
return 1;
}
fd_set readfds;
printf("select waiting for key...\n");
while (1) {
FD_ZERO(&readfds);
FD_SET(fd, &readfds);
int ret = select(fd + 1, &readfds, NULL, NULL, NULL);
if (ret > 0 && FD_ISSET(fd, &readfds)) {
int key_val;
read(fd, &key_val, sizeof(key_val));
int gpio = (key_val >> 8) & 0xFF;
printf("GPIO %d: %s\n", gpio,
(key_val & 0xFF) ? "RELEASED" : "PRESSED");
}
}
close(fd);
return 0;
}
测试三:SIGIO 异步通知
c
/* test_async.c */
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <signal.h>
int fd;
void sigio_handler(int signum)
{
int key_val;
if (read(fd, &key_val, 4) == 4) {
int gpio = (key_val >> 8) & 0xFF;
printf("[SIGIO] GPIO %d: %s\n", gpio,
(key_val & 0xFF) ? "RELEASED" : "PRESSED");
}
}
int main(void)
{
fd = open("/dev/gpio_key", O_RDONLY);
if (fd < 0) {
perror("open");
return 1;
}
signal(SIGIO, sigio_handler);
fcntl(fd, F_SETOWN, getpid());
int flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags | FASYNC);
printf("SIGIO mode started, press key...\n");
while (1) pause();
close(fd);
return 0;
}
Makefile(交叉编译用)
makefile
CC = arm-linux-gnueabihf-gcc
CFLAGS = -Wall -O2
TARGETS = test_blocking test_poll test_async
all: $(TARGETS)
%: %.c
$(CC) $(CFLAGS) -o $@ $<
clean:
rm -f $(TARGETS)
.PHONY: all clean
第五部分:全文词汇表
以下按字母顺序列出本文出现的所有英文宏/函数/术语,配有中文解释和首次出现位置。
数据结构与宏
| 英文 | 中文解释 | 作用 |
|---|---|---|
struct gpio_key |
自定义结构体 | 描述一个按键的 GPIO、中断、定时器等信息 |
struct platform_device |
平台设备 | 内核抽象的一个"挂在平台总线上的设备" |
struct platform_driver |
平台驱动 | 描述驱动的 probe/remove 入口和 compatible 匹配表 |
struct device_node |
设备树节点 | 指向设备树中某个节点的指针,of_xxx 系列函数的操作对象 |
struct file_operations |
文件操作表 | 驱动向内核注册的"可支持用户层使用的系统调用" |
struct fasync_struct |
异步通知结构 | 存储哪些进程注册了 SIGIO 异步通知 |
struct timer_list |
内核定时器 | 代表一个"在 x 毫秒后做某件事"的定时任务 |
DECLARE_WAIT_QUEUE_HEAD |
声明等待队列头 | 创建阻塞 IO 用的排队区 |
NEXT_POS(x) |
自定义宏 | 环形缓冲区下标前移并绕回 |
BUF_LEN |
自定义宏 | 环形缓冲区容量 |
MKDEV(major, minor) |
宏:组合设备号 | 把主/次设备号拼成 dev_t |
THIS_MODULE |
当前模块指针 | 指向当前内核模块的 struct module |
GFP_KERNEL |
内存分配标志 | "分配空闲页,允许休眠" ,中断内不允许休眠! |
OF_GPIO_ACTIVE_LOW |
设备树标志 | "这个 GPIO 低电平时视为'激活/按下'" |
IRQF_TRIGGER_RISING |
中断触发方式 | 上升沿触发(0→1,松开按键) |
IRQF_TRIGGER_FALLING |
中断触发方式 | 下降沿触发(1→0,按下按键) |
IRQ_HANDLED |
ISR 返回值 | "我处理了这个中断" |
| IRQ_NONE | 不是我 | 设备没触发中断(可能是共享中断号的别人触发的) |
IRQ_WAKE_THREAD |
ISR 返回值 | "上半部完成,请启动下半部线程" |
O_NONBLOCK |
文件打开标志 | 非阻塞模式,没有数据可读时 read 不等待 |
POLLIN |
poll 事件 | 有数据可读 |
POLLRDNORM |
poll 事件 | 有"普通"数据可读 |
SIGIO |
信号编号 | 异步 IO 信号,收到说明有数据就绪 |
POLL_IN |
fasync 事件 | 数据可读事件(和 POLLIN 不同场景下的不同写法) |
EAGAIN |
错误码 | "资源暂时不可用,请稍后重试" |
EIO |
错误码 | "IO 错误" |
GPL |
许可证类型 | GNU General Public License |
jiffies |
内核全局变量 | 系统启动以来的滴答计数,所有定时器的时间基准 |
HZ |
内核编译常量 | 每秒的滴答数(100/250/1000) |
函数
| 英文 | 中文解释 | 首次出现章节 |
|---|---|---|
module_init(fn) |
指定模块入口 | 第 8 步 |
module_exit(fn) |
指定模块出口 | 第 9 步 |
platform_driver_register(drv) |
把驱动注册到 Platform 总线 | 第 8 步 |
platform_driver_unregister(drv) |
从 Platform 总线移除驱动 | 第 9 步 |
of_gpio_count(node) |
统计设备树里 gpios 写的GPIO引脚数 | 第 4 步 |
of_get_gpio_flags(node, i, &flag) |
读取设备树里第 i 个 GPIO 的引脚编号 | 第 4 步 |
gpio_to_desc(gpio_num) |
GPIO 编号 → 内核描述符 (读写引脚) | 第 4 步 |
gpio_to_irq(gpio_num) |
GPIO 编号 → 中断号 (配置为中断源) | 第 4 步 |
gpiod_get_value(desc) |
读取 GPIO 当前电平 | 第 3 步 |
request_threaded_irq(...) |
注册中断,指定上半部和下半部线程 | 第 4 步 |
free_irq(irq, dev_id) |
释放中断 | 第 5 步 |
kzalloc(size, flags) |
内核内存分配(自动清零) | 第 4 步 |
kfree(ptr) |
释放内核内存 | 第 5 步 |
copy_to_user(to, from, n) |
内核空间 → 用户空间数据拷贝 | 第 1 步 |
register_chrdev(major, name, fops) |
注册字符设备 | 第 4 步 |
unregister_chrdev(major, name) |
注销字符设备 | 第 5 步 |
class_create(owner, name) |
创建设备类(/sys/class/) | 第 4 步 |
class_destroy(cls) |
销毁设备类 | 第 5 步 |
device_create(...) |
创建设备节点(触发 udev) | 第 4 步 |
device_destroy(...) |
删除设备节点 | 第 5 步 |
wait_event_interruptible(wq, cond) |
等待条件成立(可被信号打断) | 第 1 步 |
wake_up_interruptible(wq) |
唤醒等待队列 | 第 3 步 |
poll_wait(fp, wq, wait) |
把进程加入 poll 等待列表 | 第 2 步 |
fasync_helper(fd, file, on, fasync) |
处理 fasync 注册/注销 | 第 2 步 |
kill_fasync(fasync, sig, band) |
向注册进程发送信号 | 第 3 步 |
timer_setup(timer, cb, flags) |
初始化定时器 | 第 4 步 |
add_timer(timer) |
添加定时器到内核管理链表 | 第 4 步 |
mod_timer(timer, expires) |
修改(续期)定时器 | 第 3 步 |
del_timer(timer) |
删除定时器 | 第 5 步 |
from_timer(p, t, member) |
从定时器指针反推包含它的设备结构体 | 第 3 步 |
IS_ERR(ptr) |
判断指针是不是错误码 | 第 4 步 |
PTR_ERR(ptr) |
把错误指针转成错误码 | 第 4 步 |
第六部分:全文总结
按键驱动完整流程 · 速记
注册阶段
平台驱动先挂号,设备树对上就跑 probe。
建设备、绑操作表,申请 GPIO 把引脚占牢。
注册中断设边沿,中断里只启动定时器,别的都不搞。
环形队列先备好,万事就绪,等信号来报到。
运行阶段
用户 open 打开设备,缓冲区空?那就 read 进去睡觉。
按键一按触发中断,中断里一秒都不能耗。
只改定时器做消抖,重活,休眠在中断里不能搞。
定时一到读电平,数据塞进环形队列刚刚好。
wake_up 一声叫醒,进程读出键值,收工回家。
核心规矩短句
中断只发令,干活交给定时器; 空了就休眠,有数据再把人唤起。
-
中断不能睡,定时器能睡。 中断上下文没有进程身份,调用任何可能休眠的函数(
msleep、copy_to_user、mutex_lock)内核直接崩。mod_timer只改一个超时时间,纯寄存器操作,绝不阻塞,所以中断里能安全调用。定时器回调跑在软中断/进程上下文,可以慢慢读电平、压队列。 -
空转浪费 CPU,睡觉让出 CPU。
read没数据时不 sleep 就只能while死循环轮询,把一个核跑满。wait_event_interruptible一调,进程挂到等待队列上,调度器把它踢出运行列表,CPU 去干别的------数据来了wake_up才把它拎回来。
完整运行时数据流
Linux 驱动通用运行流程
══════════════════════
一、总览
开机 注册 运行
──── ──── ────
内核解析 .dtb insmod xxx.ko 用户 open/read/write
→ device_node 树 → module_init() → file_operations
→ 生成 platform_device → 匹配 compatible → 中断 + 下半部 + 缓冲区
→ probe() → copy_to_user
→ /dev/xxx 出现 → close
卸载
────
rmmod xxx.ko
→ module_exit()
→ 倒序释放资源
二、开机阶段(内核自动)
.dts ──编译──→ .dtb ──U-Boot加载──→ 内核展开 device_node 树
→ 注册 platform_device
/sys/firmware/devicetree/base/ device_node 只读档案
/sys/firmware/devicetree/base/<节点>/compatible 兼容字符串存放处
三、注册阶段(insmod → probe)
insmod xxx.ko
module_init(xxx_init)
platform_driver_register(&xxx_driver)
内核匹配:遍历 device_node,拿 driver.of_match_table[i].compatible
和 device_node 的 compatible 做 strcmp
├─ 对上 → 调用 xxx_driver.probe()
└─ 没对上 → 驱动挂着,等热插拔
platform_driver 和旧式字符设备区别:
platform_driver: compatible 自动匹配,硬件信息来自设备树
旧式: 无匹配,module_init 里硬编码寄存器地址
probe() 六步:
① 读设备树属性
of_get_gpio(node, 0)
→ 从设备树 gpios 属性里读第 0 个成员 GPIO 的编号(int)
of_get_gpio_flags(node, 0, &flags)
→ 同上,同时把边沿触发标志(上升沿/下降沿)读到 flags
gpio_request(gpio, "xxx")
→ 向内核声明独占这个 GPIO 编号
gpio_to_irq(gpio)
→ GPIO 编号 → 中断号,查表转换
of_property_read_u32(node, "debounce-interval", &val)
→ 从设备树读一个 u32 属性值(比如消抖时间 20ms),存进 val
② 分配 per-device 私有数据
devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL)
→ 内核自动管理生命周期,rmmod 时自动 free,不用手写 kfree
platform_set_drvdata(pdev, dev)
→ 把 dev 指针存进 pdev,后面 probe 各处通过 platform_get_drvdata 取出来
③ 初始化内核原语(每个都要 init 才能用)
spin_lock_init(&dev->lock)
→ 自旋锁,保护中断和进程共享的数据
mutex_init(&dev->mutex)
→ 互斥锁,保护进程间共享的数据(能休眠,中断里不能用)
init_waitqueue_head(&dev->wq)
→ 等待队列头,read 睡在这,下半部通过它叫醒
timer_setup(&dev->timer, callback, 0)
→ 绑定定时器回调函数,第三个参数 0 表示不设初始 flags
④ 申请硬件资源
devm_request_irq(&pdev->dev, irq, xxx_isr,
IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING,
"xxx", dev)
→ 注册中断:双边沿触发,isr 是上半部,"xxx" 是 /proc/interrupts 里显示的名字
→ dev 作为 dev_id 传给 isr,isr 里通过它拿到私有数据
⑤ 创建 /dev 节点
device_create(cls, NULL, devt, NULL, "xxx")
→ /dev/xxx 出现,用户 open 的就是这个文件
→ 触发 udev 自动创建设备文件、设权限
⑥ return 0
→ probe 成功,平台设备绑定完成,驱动就绪
四、运行阶段
用户态 内核态 硬件
────── ────── ────
fd = open("/dev/xxx")
────────────────────→ xxx_open()
return 0
←────────────────────
read(fd, buf, len)
────────────────────→ xxx_read()
if (缓冲区为空)
wait_event_interruptible(wq, 有数据)
进程挂到等待队列,调度器踢出 CPU
║
╔═══════ 进程在此睡眠 ═══════╣
║ ║
║ 硬件事件触发
║ 中断控制器 → CPU
║ ║
║ 上半部 (硬中断上下文) ↓
║ xxx_isr()
║ 只做:mod_timer → 设 20ms 后回调
║ 或 schedule_work → 把 work 排进队列
║ return IRQ_HANDLED
║ ⚠ 不能:sleep / mutex / copy_to_user
║ ║
║ 下半部 (softirq / 内核线程)
║ timer 回调 或 work 回调
║ 读硬件 → 处理数据 → 写缓冲区
║ wake_up_interruptible(wq)
║ ║
╚═══ 进程被踢回运行列表 ═════╝
xxx_read() 继续
从缓冲区取数据
copy_to_user(buf, &val, sizeof(val))
return sizeof(val)
←────────────────────
close(fd)
────────────────────→ xxx_release()
return 0
中断上下文的硬红线:
场景 能用 不能用 原因
──── ──── ──── ────
中断上半部 GFP_ATOMIC GFP_KERNEL 中断无进程上下文
mod_timer msleep 调度器无法调度"无进程"
spin_lock mutex_lock 死锁
(irqsave) copy_to_user 需要 current 指针
timer 回调 GFP_ATOMIC GFP_KERNEL softirq 上下文
(softirq)
work 回调 GFP_KERNEL --- 内核线程,等于进程
三种下半部选型:
方式 上下文 可休眠 延迟 适用
──── ──── ──── ──── ────
timer_list softirq ❌ 精确 消抖、超时、延时后处理
work_struct 内核线程 ✅ 稍高 重活:文件 I/O、内存分配、复杂计算
tasklet softirq ❌ 低 轻量但已不推荐
五、卸载阶段(rmmod)
rmmod xxx.ko
module_exit(xxx_exit)
① free_irq(irq, dev) 停中断(最先)
② del_timer_sync(&dev->timer) 确保定时器回调已退出
③ cancel_work_sync(&dev->work) 确保 work 回调已退出
④ device_destroy(cls, devt) 删 /dev/xxx
⑤ cdev_del(&dev->cdev) 注销 cdev
⑥ unregister_chrdev_region(devt, 1) 释放设备号
⑦ gpiod_put(dev->gpiod) 释放 GPIO
⑧ platform_driver_unregister(&xxx_driver) 注销驱动
顺序铁律:先停中断 → 再停下半部 → 再释放资源。反了就会崩溃。
六、驱动骨架结构
xxx_driver.c
│
├─ module_init(xxx_init) → platform_driver_register
├─ module_exit(xxx_exit) → platform_driver_unregister
│
├─ MODULE_LICENSE("GPL")
├─ MODULE_AUTHOR("...")
│
├─ static const struct of_device_id xxx_of_match[] = {
│ { .compatible = "vendor,xxx" }, ← 和设备树配对
│ {}
│ };
│
├─ static struct platform_driver xxx_driver = {
│ .probe = xxx_probe,
│ .remove = xxx_remove,
│ .driver = {
│ .name = "xxx", ← /sys/bus/platform/drivers/xxx/
│ .of_match_table = xxx_of_match,
│ },
│ };
│
├─ xxx_probe(struct platform_device *pdev) {
│ 六步初始化(见第三节)
│ }
│
├─ static const struct file_operations xxx_fops = {
│ .owner = THIS_MODULE,
│ .open = xxx_open,
│ .read = xxx_read,
│ .write = xxx_write,
│ .unlocked_ioctl = xxx_ioctl,
│ .release = xxx_release,
│ };
│
├─ xxx_open(struct inode *i, struct file *f) { return 0; }
│
├─ xxx_read(struct file *f, char __user *buf, size_t n, loff_t *off) {
│ wait_event_interruptible(dev->wq, 缓冲区非空);
│ copy_to_user(buf, &data, sizeof(data));
│ return sizeof(data);
│ }
│
├─ xxx_isr(int irq, void *dev_id) {
│ struct xxx_dev *dev = dev_id;
│ mod_timer(&dev->timer, jiffies + msecs_to_jiffies(20));
│ return IRQ_HANDLED;
│ }
│
├─ xxx_timer_cb(struct timer_list *t) {
│ struct xxx_dev *dev = from_timer(dev, t, timer); ← 反推结构体指针
│ 读硬件 → 处理 → 写缓冲区 → wake_up_interruptible(&dev->wq);
│ }
│
└─ struct xxx_dev { ← 驱动私有数据
struct gpio_desc *gpiod;
int irq;
struct timer_list timer;
wait_queue_head_t wq;
spinlock_t lock;
// 环形缓冲区 + 读写指针
};
七、关键路径速查
问题 答案
──── ────
device_node 在哪? /sys/firmware/devicetree/base/
platform_device 在哪? /sys/bus/platform/devices/
platform_driver 在哪? /sys/bus/platform/drivers/<name>/
设备节点在哪? /dev/xxx
compatible 匹配谁和谁? driver.of_match_table ↔ device_node.compatible
device_node 怎么传给 probe? 通过 platform_device.dev.of_node
of_get_gpio 返回什么? gpio 编号(int),不是电平,不是句柄
gpiod_get 返回什么? gpio_desc * 句柄,一步到位
from_timer 干什么? container_of 定时器专用版,从 timer_list* 反推外层结构体
wait_event_interruptible 干什么? 进程挂等待队列,让出 CPU,被 wake_up 叫醒
中断里为什么不能 copy_to_user? current 指针可能无效,页表可能不对
为什么 timer 回调也不能睡? softirq 上下文,不是独立进程
probe 用 GFP_KERNEL 还是 GFP_ATOMIC? GFP_KERNEL,probe 跑在进程上下文
可直接套用的技术骨架。换硬件逻辑只改变 probe 里读什么资源、下半部里处理什么数据,其他全部复用。