Linux GPIO 驱动全链路剖析:从设备树、Platform Driver 到生产级中断处理一站式讲解

本文用法 :按函数框架顺序学习,每个函数直接给出生产推荐的写法,不做"低效教学"


你会学到什么

学会后能做什么 用到的内核机制
驱动自动适配不同板子(不改代码) 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_handlerwake_uppollpoll_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 分配内存countgpio_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 申请中断线 :注册 irqactionirq_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)

  1. 提交内容:platform_driver + of 匹配表 (compatible 字符串)
  2. 触发时机:内核遍历设备树节点,节点 compatible 和驱动一致
  3. 调用者:platform 总线内核框架
  4. 执行回调:gpio_key_drv_probe
  5. probe 内部依次执行后面所有资源创建:字符设备、GPIO、中断、定时器、等待队列。

第 2 步:创建字符设备(cdev + class + device)

  1. 提交内容:file_operations 函数集(open、read、poll、fasync)
  2. 触发时机:用户程序执行 open("/dev/xxx")read()poll() 系统调用
  3. 调用者:VFS 虚拟文件系统
  4. 对应回调:
    • open:初始化私有数据
    • read:调用wait_event_interruptible,缓冲区为空就阻塞休眠
    • poll:调用poll_wait把进程挂入等待队列

第 3 步:申请 GPIO 引脚(devm_gpio_request)

  1. 提交内容:GPIO 编号
  2. 无触发事件、无回调
  3. 作用:占用硬件引脚资源,设为输入模式,防止其他驱动抢占。

第 4 步:注册硬件中断(devm_request_irq 设备托管式请求中断)

  1. 提交内容:中断服务函数gpio_key_isr、中断号、边沿触发方式
  2. 触发条件:GPIO 电平跳变(按键按下 / 松开)
  3. 调用者:GIC 硬件中断控制器 + 内核中断子系统
  4. 执行回调:中断服务函数gpio_key_isr
  5. 中断上下文规则:不能休眠,只做一件事:启动消抖定时器 mod_timer,立刻退出 ISR。

第 5 步:定时器消抖(setup_timer + mod_timer)

  1. 提交内容:定时器处理函数key_timer_handler
  2. 触发条件:ISR 里启动定时器,等待 200ms 超时
  3. 调用者:内核时钟软中断
  4. 执行回调:定时器处理函数
    • 再次读取 GPIO 真实电平,过滤机械按键抖动
    • 确认按键状态后,把键值写入环形缓冲区
    • 调用wake_up(&gpio_key_wait)唤醒阻塞的进程

第 6 步:等待队列休眠与唤醒(wait_queue_head + poll_wait + wake_up)

  1. 提交内容:等待队列头 gpio_key_wait
  2. 休眠流程: 用户 read → wait_event_interruptible发现缓冲区为空 → 调用 schedule 让出 CPU 进入睡眠; poll 函数里执行poll_wait,同样将进程挂载到该等待队列。
  3. 唤醒条件:定时器处理函数执行wake_up
  4. 调用者:内核进程调度器
  5. 唤醒之后:进程重新运行,再次判断缓冲区非空,读出数据,read 系统调用返回给应用程序。

完整时序线

  1. 内核启动:驱动加载,platform_driver 注册
  2. 总线匹配 DTS 节点 → 执行 probe
  3. probe:创建字符设备 → 申请 GPIO → 注册中断 → 初始化定时器与等待队列
  4. APP:open 设备 → read 阻塞休眠
  5. 按键按下 → 硬件触发中断 → 进入 ISR
  6. ISR:启动消抖定时器,快速退出
  7. 200ms 后定时器到期 → 读取稳定电平,写入环形缓冲
  8. 执行 wake_up 唤醒等待队列上休眠的进程
  9. read 条件满足,跳出阻塞,读取键值返回用户态
  10. 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)

易混淆点:

  1. Platform 总线匹配(靠 compatible

  2. 字符设备注册(靠 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 都只是"通知手段",让应用层知道"按键事件已经发生了,快来取数据"。

  1. gpio_key_drv_poll 函数
项目 说明
函数作用 实现 poll 接口。用户程序调用 select() / poll() / epoll() 时,内核调用此函数检查数据是否就绪。
参数 fp --- 文件指针;wait --- poll 等待队列,需要调用 poll_wait 把当前进程注册上去
返回值 0 --- 数据未就绪;`POLLIN
  1. BAND(带外(紧急)数据,高优先级) 宏:POLLRDBAND(读)、POLLWRBAND(写) 对应数据:TCP OOB 紧急数据、加急控制指令。 特点:独立缓冲区,插队处理,不跟普通数据排队。

  2. 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
}

一、为什么必须反推出结构体?

  1. 回调函数入参被内核写死,只有 struct timer_list *t,没有 dev 指针
  2. t 仅仅是结构体里一小块成员地址,不含 GPIO、队列、缓冲区。
  3. 不做容器转换,你拿不到这一路按键的任何私有资源。

二、dev 结构体内成员各自的作用

  1. gpiod:GPIO 句柄,用来读取按键电平;
  2. gpio:引脚号,用来编码事件,区分多路按键;
  3. 环形缓冲区:保存本次按键事件数据;
  4. key_wait:当前按键专属等待队列,唤醒 poll 休眠;
  5. 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 一声叫醒,进程读出键值,收工回家。
核心规矩短句

中断只发令,干活交给定时器; 空了就休眠,有数据再把人唤起。

  • 中断不能睡,定时器能睡。 中断上下文没有进程身份,调用任何可能休眠的函数(msleepcopy_to_usermutex_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 里读什么资源、下半部里处理什么数据,其他全部复用。