ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

嵌入式Linux按键驱动源码与测试程序实战:从设备树到input子系统

嵌入式Linux按键驱动源码与测试程序实战:从设备树到input子系统

简介:这份资源面向Linux内核驱动初学者与嵌入式开发人员,提供按键驱动从实现到验证的完整参考,帮助理解字符设备注册、中断处理与输入子系统之间的协作关系。压缩包共2个文件,均为C语言源码,整体约2KB,其中一份承担驱动主体实现,另一份负责用户空间的事件读取与验证,体量轻便,适合直接编译加载后观察运行效果。已有362人学习下载,说明其在入门驱动实践中具有一定参考价值。读者可从中梳理设备注册、中断请求线关联、input_event结构体填充与input_report_key上报等关键环节,并借助测试程序掌握打开设备、读取按键事件、识别键码与按下释放状态、关闭设备的完整流程,同时结合内核日志排查加载与中断问题,为后续编写更复杂的输入设备驱动打下基础。

1. 从一次按键失灵说起:linux按键驱动源码及测试程序到底在解决什么

做过嵌入式 Linux 的人大概率都遇到过这种场景:板子跑起来了,串口能登录,LED 能点,但一按按键,dmesg里什么都没有,evtest也抓不到事件。更玄学的是,同一份设备树,换一块板子就正常,换回来又不行。这时候你翻遍应用层代码也没用,问题往往埋在 linux 按键驱动源码及测试程序这一层——GPIO 有没有被别的驱动占用、中断触发沿配没配对、input 子系统有没有把事件上报出去。

这个标题讲的不是「写一个能跑的 demo」,而是把一条完整的链路打通:设备树描述硬件 → 平台驱动 probe → 中断或轮询读电平 → input 子系统上报键值 → 用户态用测试程序验证。它适合三类人:刚接手一块新板子要验证按键通路的驱动工程师;想从应用层往下钻、搞懂/dev/input/eventX背后发生了什么的人;以及需要给产线写一个按键自检工具的人。下面我按自己实际调试的顺序,把源码结构、关键参数和测试程序一步步拆开。

2. 按键驱动源码的骨架:从设备树到 input 子系统

2.1 为什么按键要走 input 子系统而不是自己造字符设备

很多新手第一反应是写个 misc 设备,read返回键值就完事。这在单按键、单进程的场景能跑,但一旦系统里有触摸屏、遥控器、音量键,应用层就要为每个设备写一套解析逻辑,还得自己处理阻塞、非阻塞、多路复用。input 子系统把这些统一了:驱动只管上报「哪个键、按下还是抬起」,内核负责把事件塞进/dev/input/eventX,应用层用同一套struct input_event结构读取。

选型上,按键驱动常见两种实现:中断方式和轮询方式。中断方式省 CPU,但要求 GPIO 支持中断且触发沿稳定;轮询方式用input_setup_polling或定时器,适合按键抖动大、或者 GPIO 控制器中断资源紧张的情况。我一般优先中断,只有在硬件确实不给力时才退到轮询。

2.2 设备树里按键节点怎么写才不会被别的驱动抢走

设备树是第一个翻车点。按键节点必须挂在正确的父节点下,compatible要和驱动里的of_match_table对上,gpios属性引用的引脚不能被 pinctrl 里别的功能复用。

/* 按键节点:挂在根节点下,compatible 与驱动匹配 */ keys { compatible = "demo,gpio-keys"; pinctrl-names = "default"; pinctrl-0 = <&key_pins>; key_power { label = "Power"; gpios = <&gpio1 12 GPIO_ACTIVE_LOW>; /* 低电平有效 */ linux,code = <KEY_POWER>; /* 上报的键值 */ debounce-interval = <20>; /* 去抖 20ms */ }; };

这里三个参数最容易出问题。GPIO_ACTIVE_LOW表示按下时引脚为低,如果你硬件是按下拉高却写了 LOW,现象就是「不按有事件、按了没反应」。linux,code决定上报的键值,写错的话应用层收到的就是另一个键。debounce-interval是软件去抖,机械按键一般 10~30ms,设太小会一次按下报多次,设太大快速连按会丢。

2.3 驱动 probe 里必须做的四件事

拿到 GPIO 之后,probe 函数里要做的事其实很固定:申请 GPIO、配置输入方向、申请中断、注册 input 设备。下面是一段精简后的核心代码。

#include <linux/module.h> #include <linux/platform_device.h> #include <linux/gpio/consumer.h> #include <linux/interrupt.h> #include <linux/input.h> #include <linux/of.h> struct demo_key { struct gpio_desc *gpiod; /* GPIO 描述符 */ struct input_dev *input; /* input 设备 */ int irq; /* 中断号 */ int code; /* 键值 */ }; /* 中断处理:上报按键事件 */ static irqreturn_t demo_key_isr(int irq, void *dev_id) { struct demo_key *key = dev_id; int val = gpiod_get_value(key->gpiod); /* 读当前电平 */ /* 低电平有效:val=0 表示按下 */ input_report_key(key->input, key->code, !val); input_sync(key->input); /* 同步,否则事件不提交 */ return IRQ_HANDLED; } static int demo_key_probe(struct platform_device *pdev) { struct demo_key *key; struct device *dev = &pdev->dev; int ret; key = devm_kzalloc(dev, sizeof(*key), GFP_KERNEL); if (!key) return -ENOMEM; /* 1. 获取 GPIO,第二个参数 0 表示不强制输出 */ key->gpiod = devm_gpiod_get(dev, NULL, GPIOD_IN); if (IS_ERR(key->gpiod)) return PTR_ERR(key->gpiod); /* 2. 从设备树读键值 */ ret = device_property_read_u32(dev, "linux,code", &key->code); if (ret) key->code = KEY_ENTER; /* 读不到给默认值 */ /* 3. 申请中断,双边沿触发 */ key->irq = gpiod_to_irq(key->gpiod); ret = devm_request_irq(dev, key->irq, demo_key_isr, IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING, "demo_key", key); if (ret) return ret; /* 4. 注册 input 设备 */ key->input = devm_input_allocate_device(dev); if (!key->input) return -ENOMEM; key->input->name = "demo-key"; input_set_capability(key->input, EV_KEY, key->code); ret = input_register_device(key->input); if (ret) return ret; platform_set_drvdata(pdev, key); return 0; }

逻辑上,devm_gpiod_get拿到的 GPIO 已经自动配置成输入,不用再手动调方向。gpiod_to_irq把 GPIO 映射成中断号,这一步在部分平台上会返回负值,说明该 GPIO 不支持中断,得改用轮询。input_report_key的第三个参数是「按下为 1、抬起为 0」,所以低电平有效时要取反。input_sync绝对不能省,它相当于一次提交,少了它应用层读不到完整事件。

参数方面,IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING表示双边沿都触发,这样按下和抬起都能上报。如果只写 FALLING,抬起事件就丢了,应用层会一直以为键按着。devm_前缀的资源是自动释放的,probe 失败或模块卸载时不用手动 free,这是新驱动应该优先用的写法。

3. 测试程序怎么写:从 evtest 到自己的验证工具

3.1 先用 evtest 确认事件到底有没有上来

在写自己的测试程序之前,先用现成工具确认链路通不通。evtest会列出所有 input 设备,选中你的按键设备后,按下按键应该能看到类似输出。

# 列出 input 设备,找到 demo-key 对应的 event 号 cat /proc/bus/input/devices | grep -A 5 "demo-key" # 假设是 event2,用 evtest 监听 evtest /dev/input/event2

如果evtest里能看到Event: time ... type 1 (EV_KEY), code 116 (KEY_POWER), value 1,说明驱动和 input 子系统都没问题,问题在应用层。如果什么都看不到,回到驱动层查中断和上报。这一步能省掉大量瞎猜。

3.2 自己写一个能跑在产线自检里的测试程序

evtest适合调试,但产线自检需要程序化判断。下面这个测试程序读取指定 event 设备,统计按键次数,超时退出。

#include <stdio.h> #include <fcntl.h> #include <unistd.h> #include <string.h> #include <linux/input.h> #include <sys/time.h> int main(int argc, char *argv[]) { int fd, ret; struct input_event ev; int count = 0; const char *dev = (argc > 1) ? argv[1] : "/dev/input/event2"; fd = open(dev, O_RDONLY); if (fd < 0) { perror("open input device"); return -1; } printf("listening on %s, press key within 10s\n", dev); while (1) { fd_set fds; struct timeval tv = { .tv_sec = 10, .tv_usec = 0 }; FD_ZERO(&fds); FD_SET(fd, &fds); /* 用 select 做超时,避免永久阻塞 */ ret = select(fd + 1, &fds, NULL, NULL, &tv); if (ret == 0) { printf("timeout, no key pressed\n"); break; } if (ret < 0) { perror("select"); break; } ret = read(fd, &ev, sizeof(ev)); if (ret != sizeof(ev)) continue; /* 只关心按键事件,且 value=1 表示按下 */ if (ev.type == EV_KEY && ev.value == 1) { printf("key code %d pressed\n", ev.code); count++; if (count >= 3) { /* 按满 3 次退出 */ printf("test pass\n"); break; } } } close(fd); return count >= 3 ? 0 : -1; }

这段代码的关键在select超时。产线自检不能让程序卡死,10 秒没按键就判失败退出。read读到的struct input_event里,type为EV_KEY才是按键,value为 1 是按下、0 是抬起、2 是长按重复。只统计value == 1可以避免抬起事件干扰计数。编译时用gcc test_key.c -o test_key即可,不需要额外库。

参数上,select的超时时间按产线节拍调整,一般 5~15 秒。退出条件count >= 3也可以改成按特定键值退出,比如只认KEY_POWER,这样能验证键值映射对不对。

3.3 用 shell 快速验证而不写 C 程序

如果只是临时验证,不想编译,可以用hexdump直接看事件流。

# 每个 input_event 是 16 字节(64 位系统),直接看原始数据 hexdump -e '16/1 "%02x " "\n"' /dev/input/event2

按下按键时,你会看到一串数据,其中包含01 00(EV_KEY)、键值、01 00 00 00(按下)。这种方式不用装任何工具,适合在精简的根文件系统里应急。缺点是数据要自己解析,不如evtest直观。

4. 避坑与排查:按键驱动最常见的五个翻车现场

4.1 中断申请失败,probe 返回 -EINVAL

现象是dmesg里看到demo_key: probe failed with error -22。原因通常是gpiod_to_irq返回了负值,说明这个 GPIO 控制器不支持中断,或者引脚被配置成了普通 GPIO 功能。解决办法是先确认硬件手册里这个引脚是否接到中断控制器,如果确实不支持,改用轮询方式,用input_setup_polling配合定时器读电平。

4.2 按下一次上报多次,应用层收到连击

现象是evtest里按一下出现好几组按下抬起。原因是机械按键抖动,而debounce-interval没设或者设得太小。解决是在设备树里把debounce-interval调到 20~30ms。注意这个属性是 input 子系统层面的软件去抖,不是硬件 RC 去抖,两者可以叠加。如果调大后快速连按丢键,说明设过头了,往下调 5ms 试试。

4.3 按键事件被别的进程抢走

现象是自己的测试程序读不到事件,但evtest能读到。原因是 input 事件是「谁先读谁拿到」,多个进程同时打开同一个 event 设备会互相抢。解决办法是确保同一时间只有一个进程读,或者在驱动里用input_grab_device独占。产线自检程序启动前先fuser /dev/input/event2看看有没有别的进程占着。

4.4 键值对不上,按 A 键出来 B 键

现象是应用层收到的ev.code和设备树里写的linux,code不一致。原因通常是设备树里linux,code写成了十进制,而内核头文件里的KEY_*是十六进制宏。比如KEY_POWER是 116,你写linux,code = <116>没问题,但写成<0x74>也对,写成<74>就变成 74 了。解决是统一用内核头文件里的宏名对应的数值,或者直接在驱动里用KEY_POWER宏而不是从设备树读。

4.5 系统休眠后按键失效

现象是系统进入 suspend 后按键没反应,唤醒后也不恢复。原因是 suspend 时 GPIO 中断被关闭,resume 后没有重新使能。解决是在驱动的suspend和resume回调里分别调disable_irq和enable_irq,或者用dev_pm_ops把中断状态管起来。如果按键本身要作为唤醒源,还得在设备树里给对应中断节点加wakeup-source属性。

5. 进阶技巧:把按键驱动做成可复用的输入设备框架

上面讲的单按键驱动能跑,但一个板子上往往有多个按键,每个都写一遍 probe 太蠢。我一般的做法是把多个按键做成一个 input 设备,每个按键一个子节点,probe 时遍历子节点统一注册。

static int demo_keys_probe(struct platform_device *pdev) { struct device *dev = &pdev->dev; struct input_dev *input; struct device_node *child; int ret, nkeys = 0; input = devm_input_allocate_device(dev); if (!input) return -ENOMEM; input->name = "demo-keys"; /* 遍历所有子节点,每个子节点是一个按键 */ for_each_available_child_of_node(dev->of_node, child) { struct gpio_desc *gpiod; u32 code; gpiod = devm_gpiod_get_from_of_node(dev, child, "gpios", 0, GPIOD_IN, NULL); if (IS_ERR(gpiod)) continue; if (device_property_read_u32(&child->dev, "linux,code", &code)) continue; input_set_capability(input, EV_KEY, code); nkeys++; /* 这里省略中断申请,实际每个按键都要单独申请 */ } if (!nkeys) return -ENODEV; ret = input_register_device(input); if (ret) return ret; return 0; }

这个框架的好处是新增按键只改设备树,驱动不用动。for_each_available_child_of_node会跳过status = "disabled"的节点,方便硬件裁剪。input_set_capability要为每个键值调一次,否则应用层看不到这个键。

验证时,除了功能测试,还要做压力测试。我习惯用脚本连续触发 1000 次,看有没有丢事件或内存泄漏。

# 连续监听,统计 1000 次按键事件,检查是否有丢失 count=0 while [ $count -lt 1000 ]; do # 这里用 evtest 的 --grab 模式或自己写的程序统计 count=$((count + 1)) done echo "stress test done"

实际压测时,我会用cat /proc/interrupts | grep demo_key看中断计数和实际按键次数是否一致。如果中断数远大于按键数,说明抖动没去干净;如果中断数小于按键数,说明有丢中断,得查中断处理里有没有耗时操作。

最后说个血泪经验:调试按键驱动时,先别急着写代码,拿万用表量一下按下和抬起时引脚的实际电平,确认和GPIO_ACTIVE_LOW/HIGH对得上。我见过太多人在这上面耗半天,最后发现是硬件上拉电阻没焊。把电平确认了,再去调设备树和中断,能省下大把时间。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进