在嵌入式 Linux 开发中,按键是最基础的人机交互外设之一。本文以 NXP MX6UL 芯片为例,完整讲解从设备树配置、内核驱动开发(基于 input 子系统 + 平台驱动模型)到应用层测试的全部流程,实现 "按键中断触发 - 软中断延时消抖 - input 事件上报" 的逻辑。
一、背景
本次开发基于 MX6UL 芯片,需求是实现一个按键的驱动开发:
- 硬件层面:按键接 GPIO1_IO18,低电平触发(下降沿中断);
- 驱动层面:基于 Linux
input子系统上报按键事件(符合 Linux 输入子系统规范),基于平台驱动模型适配设备树,通过tasklet(软中断)+定时器实现按键消抖(避免机械抖动导致误触发); - 应用层面:读取 input 子系统的事件节点,解析按键状态。
技术栈:Linux 平台驱动、设备树(Device Tree)、GPIO 中断、input 子系统、tasklet、内核定时器、sysfs 属性暴露。
二、设备树配置解析
Linux 设备树(DTB)负责硬件信息的抽象描述,驱动通过设备树匹配硬件资源,无需硬编码 GPIO / 中断号,提升可移植性。以下是本次按键对应的设备树节点配置:
1. 按键核心节点(putekey)
dts
putekey {
#address-cells = <1>;
#size-cells = <1>;
compatible = "pute,putekey"; // 驱动匹配的核心标识
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_putekey>; // 绑定引脚复用配置
gpio-key = <&gpio1 18 0>; // 按键GPIO:GPIO1_IO18,属性0(输入模式)
interrupt-parent = <&gpio1>; // 中断父控制器(GPIO1)
interrupts = <18 IRQ_TYPE_EDGE_FALLING>; // 中断配置:GPIO1_IO18,下降沿触发
status = "okay"; // 启用该节点
};
关键属性说明:
compatible:平台驱动通过of_match_table匹配该字符串,是驱动与设备树绑定的核心;pinctrl-0:绑定引脚复用配置节点pinctrl_putekey,确保 GPIO1_IO18 引脚被正确配置为 GPIO 功能;gpio-key:自定义属性,驱动通过of_get_named_gpio读取该属性获取 GPIO 号;interrupts:中断触发方式(IRQ_TYPE_EDGE_FALLING = 下降沿),驱动通过of_irq_get读取中断号。
2. 引脚复用配置节点(pinctrl_putekey)
dts
pinctrl_putekey: putekey {
fsl,pins = <
MX6UL_PAD_UART1_CTS_B__GPIO1_IO18 0xF080
>;
};
MX6UL_PAD_UART1_CTS_B__GPIO1_IO18:MX6UL 芯片的引脚复用宏,将 UART1_CTS_B 引脚复用为 GPIO1_IO18 功能;0xF080:引脚电气属性配置(包含上拉 / 下拉、速率、驱动能力等),需根据硬件电路调整(本次配置为默认上拉,适配按键低电平触发)。
三、内核驱动代码解析(key_drv.c)
驱动代码基于 Linux 平台驱动框架实现,分为 "资源初始化、中断处理、消抖、input 事件上报" 四大模块,以下分模块拆解:
1. 头文件与全局变量
c
运行
// 涵盖input子系统、GPIO、中断、平台驱动、设备树、定时器等核心头文件
#include <linux/fs.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/init.h>
#include <linux/miscdevice.h>
#include <linux/of.h>
#include <asm/uaccess.h>
#include <linux/gpio.h>
#include <linux/of_gpio.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/irqreturn.h>
#include <linux/wait.h>
#include <linux/sched.h>
#include <linux/spinlock.h>
#include <linux/workqueue.h>
#include <linux/timer.h>
#include <uapi/asm-generic/poll.h>
#include <linux/poll.h>
#include <linux/input.h>
#include <asm/io.h>
#include <linux/device.h>
#include <linux/mutex.h>
#include <linux/slab.h>
#include <linux/platform_device.h>
#define KEY_ON 1
#define KEY_OFF 0
static struct input_dev *pinputdev = NULL; // input子系统核心结构体
static int gpiokeynum; // 按键GPIO号
struct device_node *pkeynode = NULL; // 设备树节点指针
static int irqno; // 中断号
static wait_queue_head_t wq; // 等待队列(本文未实际使用)
static struct work_struct my_workquene; // 工作队列(备用)
static struct timer_list my_timer; // 消抖定时器
input_dev:是 Linux 输入子系统的核心结构体,用于向内核上报按键、鼠标等输入事件;- 定时器
my_timer:用于按键消抖(机械按键按下 / 释放时会有 10ms 左右的抖动,需延时确认); - tasklet:软中断机制,用于延后处理中断上下文的耗时操作(中断上下文需尽可能短)。
2. 核心功能函数
(1)sysfs 属性函数(key_show)
暴露按键状态到用户空间(sysfs),方便调试:
c
运行
static ssize_t key_show(struct device *dev, struct device_attribute *attr, char *buf)
{
// 读取GPIO电平(!gpio_get_value:低电平表示按键按下)
if (KEY_ON == !gpio_get_value(gpiokeynum)) {
pr_info("KEY_ON\n");
}
else if (KEY_OFF == !gpio_get_value(gpiokeynum)) {
pr_info("KEY_OFF\n");
}
return 0;
}
static struct device_attribute key_attr = {
.attr = {
.name = "attr",
.mode = 0444, // 只读权限
},
.show = key_show,
};
驱动加载后,可通过cat /sys/devices/virtual/input/inputX/attr查看按键状态日志。
(2)定时器消抖函数(timerout_fun)
延时 10ms 后确认按键状态,避免抖动误触发:
c
运行
static void timerout_fun(unsigned long data)
{
// 再次读取GPIO,确认按键真的按下(消抖核心)
if(!gpio_get_value(gpiokeynum)){
// 上报KEY_0按下事件(value=1)
input_event(pinputdev, EV_KEY, KEY_0, 1);
input_sync(pinputdev); // 同步事件(input子系统必需)
// 上报KEY_0释放事件(value=0)
input_event(pinputdev, EV_KEY, KEY_0, 0);
input_sync(pinputdev);
}
return;
}
input_event:向 input 子系统上报事件,参数分别为:input 设备、事件类型(EV_KEY = 按键)、按键码(KEY_0)、状态(1 = 按下 / 0 = 释放);input_sync:同步事件,告知内核本次按键事件上报完成。
(3)tasklet 处理函数(tasklet_fun)
tasklet 是软中断机制,用于延后处理中断上下文的操作(中断上下文不能执行耗时操作):
c
运行
static void tasklet_fun(unsigned long data)
{
// 重置定时器:延时10ms触发消抖函数
mod_timer(&my_timer, jiffies + msecs_to_jiffies(10));
}
static DECLARE_TASKLET(my_tasklet, tasklet_fun, 0); // 定义并初始化tasklet
(4)中断处理函数(key_irq_handle)
按键中断触发后的入口函数,仅调度 tasklet,保证中断上下文极简:
c
运行
static irqreturn_t key_irq_handle(int irq, void *dev_id)
{
// 调度tasklet(软中断),延后处理消抖逻辑
tasklet_schedule(&my_tasklet);
// 工作队列(备用方案,本文未启用)
//schedule_work(&my_workquene);
return IRQ_HANDLED;
}
3. 平台驱动核心(probe/remove)
(1)probe 函数(设备匹配成功后执行)
probe 是平台驱动的核心入口,负责初始化硬件资源、注册 input 设备:
c
运行
static int key_probe(struct platform_device *pdevice)
{
int ret = 0;
// 1. 分配input设备
pinputdev = input_allocate_device();
if (pinputdev == NULL) {
pr_info("alloc input dev faikey\n");
return -1;
}
// 2. 配置input设备属性
pinputdev->name = "key_drv";
pinputdev->evbit[0] = EV_KEY | EV_REP; // 支持按键事件+重复事件
input_set_capability(pinputdev, EV_KEY, KEY_0); // 注册KEY_0按键
// 3. 从设备树获取GPIO资源
pkeynode = of_find_node_by_path("/putekey");
if (pkeynode == NULL) {
pr_info("can not find gpiokeynum");
input_free_device(pinputdev);
return -1;
}
gpiokeynum = of_get_named_gpio(pkeynode, "gpio-key", 0);
if (gpiokeynum < 0) {
pr_info("get gpiokeynum faikey\n");
input_free_device(pinputdev);
return -1;
}
// 4. 初始化定时器、工作队列
init_timer(&my_timer);
my_timer.function = timerout_fun;
add_timer(&my_timer);
INIT_WORK(&my_workquene, workquene_fun);
// 5. 获取中断号(从设备树读取)
irqno = of_irq_get(pkeynode, 0);
pr_info("get irq %d\n", irqno);
// 6. 请求中断(devm_xxx:设备资源管理,自动释放)
ret = devm_request_irq(&pinputdev->dev, irqno, key_irq_handle, IRQF_TRIGGER_FALLING, "key_drv", NULL);
if (ret != 0) {
pr_info("request irq faikey\n");
input_free_device(pinputdev);
return -1;
}
// 7. 请求GPIO并配置为输入
ret = devm_gpio_request(&pinputdev->dev, gpiokeynum, "key_drv");
if (ret != 0) {
pr_info("request gpio faikey\n");
input_free_device(pinputdev);
return -1;
}
gpio_direction_input(gpiokeynum);
// 8. 注册input设备(核心:向系统暴露input事件节点)
ret = input_register_device(pinputdev);
if (ret != 0) {
pr_info("input_register_device faikey\n");
input_free_device(pinputdev);
return -1;
}
// 9. 创建sysfs属性文件(调试用)
ret = device_create_file(&pinputdev->dev, &key_attr);
if (ret != 0) {
pr_info("device_create_file failed\n");
return -1;
}
pr_info("key drv init success!\n");
return 0;
}
关键要点:
devm_request_irq/devm_gpio_request:内核设备资源管理接口,无需手动释放资源,驱动卸载时自动回收,避免内存泄漏;input_register_device:注册后,系统会在/dev/input/目录下生成 eventX 节点(如 event1),应用层可通过该节点读取按键事件。
(2)remove 函数(驱动卸载时执行)
释放资源,保证内核资源不泄漏:
c
运行
static int key_remove(struct platform_device *pdevice)
{
input_unregister_device(pinputdev); // 注销input设备
del_timer(&my_timer); // 删除定时器
pr_info("Kernel:key remove success\n");
return 0;
}
4. 平台驱动注册
c
运行
// 设备树匹配表(与设备树compatible属性对应)
static const struct of_device_id key_of_match_table[] = {
{.compatible = "pute,putekey"},
{},
};
// 平台设备ID表(备用匹配方式)
static const struct platform_device_id key_id_table[] = {
{.name = "putekey"},
{},
};
// 平台驱动结构体
static struct platform_driver key_drv = {
.probe = key_probe,
.remove = key_remove,
.driver = {
.name = "putekey",
.of_match_table = key_of_match_table, // 优先匹配设备树
},
.id_table = key_id_table,
};
// 模块入口/出口
static int __init key_drv_init(void)
{
platform_driver_register(&key_drv); // 注册平台驱动
pr_info("key_drv_init success!\n");
return 0;
}
static void __exit key_drv_exit(void)
{
platform_driver_unregister(&key_drv); // 注销平台驱动
pr_info("key_drv_exit success!\n");
}
module_init(key_drv_init);
module_exit(key_drv_exit);
MODULE_LICENSE("GPL"); // 必须声明GPL协议,否则内核拒绝加载
MODULE_AUTHOR("pute");
四、应用层代码解析(key_app.c)
应用层通过读取/dev/input/eventX节点,解析 input 子系统上报的按键事件:
c
运行
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <signal.h>
#include <sys/select.h>
#include <sys/time.h>
#include <linux/input.h>
#define KEY_ON 1
#define KEY_OFF 0
void delay_ms(int ms)
{
usleep(ms * 1000);
}
int main(void)
{
int fd = 0;
int ret = 0;
struct input_event env;
// 打开input事件节点(需根据实际系统调整event1)
fd = open("/dev/input/event1", O_RDONLY);
if (-1 == fd)
{
printf("open error\n");
return -1;
}
// 循环读取按键事件
while (1)
{
ret = read(fd, &env, sizeof(env));
printf("===============================================\n");
printf("type:%d, code:%d, value:%d\n", env.type, env.code, env.value);
}
close(fd);
return 0;
}
struct input_event:input 子系统的事件结构体,包含type(事件类型)、code(按键码)、value(状态);- 关键值说明:
type=1:EV_KEY(按键事件);code=11:KEY_0(对应驱动中注册的 KEY_0);value=1:按键按下;value=0:按键释放。
五、编译与测试
1. 驱动编译(Makefile)
编写 Makefile,基于 MX6UL 内核源码编译驱动模块:
makefile
OBJ := key_drv
#内核路径
kerdir := (填自己的内核路径)
#当前驱动工程目录
curdir := $(shell pwd)
#代码添加到工程编译选项中
obj-m += $(OBJ).o
all:
make -C $(kerdir) M=$(curdir) modules
cp $(OBJ).ko ~/nfs/rootfs
.PHONY:
clean:
make -C $(kerdir) M=$(curdir) modules clean
distclean:
make -C $(kerdir) M=$(curdir) modules clean
rm ~/nfs/rootfs/$(OBJ).ko
执行make编译,生成key_drv.ko模块。
2. 应用层编译(交叉编译)
MX6UL 是 ARM 架构,需用交叉编译器:
bash
运行
自己的交叉编译工具 key_app.c -o key_app
3. 测试步骤
-
加载驱动 :将
key_drv.ko拷贝到开发板,执行insmod key_drv.ko; -
验证驱动加载 :执行
dmesg | grep key,查看日志:plaintext
key_drv_init success! get irq 48 key drv init success! -
确认 input 设备 :执行
cat /proc/bus/input/devices,找到Name="key_drv"的设备,记录其 event 节点(如 event1); -
运行应用程序 :执行
./key_app,按下按键,终端打印: -
调试 sysfs 属性 :执行
cat /sys/devices/virtual/input/inputX/attr,按下按键时会打印KEY_ON,释放时打印KEY_OFF。
4. 卸载驱动
bash
运行
rmmod key_drv
执行dmesg | grep key,可看到Kernel:key remove success日志。
六、关键注意事项
- 设备树匹配 :驱动中
of_match_table的compatible必须与设备树一致,否则平台驱动无法匹配; - 中断触发方式 :驱动中
IRQF_TRIGGER_FALLING需与设备树interrupts的IRQ_TYPE_EDGE_FALLING一致; - input 节点号 :
/dev/input/event1可能因系统不同变化,需通过/proc/bus/input/devices确认; - 消抖延时:10ms 是经验值,可根据硬件按键的机械特性调整(如 5ms/20ms);
- 中断上下文规范:中断处理函数仅做 "调度"(tasklet / 工作队列),不执行耗时操作,符合 Linux 内核编程规范;
- 资源释放 :优先使用
devm_xxx接口,避免手动释放资源导致的内存泄漏。
七、总结
本文基于 MX6UL 平台,完整实现了 "设备树 + 平台驱动 + input 子系统 + 中断 + 消抖" 的按键驱动开发流程