在嵌入式Linux开发中,按键输入是最基础也最频繁使用的外设交互方式之一。不同于裸机编程可以直接轮询GPIO状态,Linux系统下按键驱动开发需要处理中断上下文、用户空间交互、并发访问等一系列复杂问题。我在最近一个基于i.MX6UL处理器的工控项目中就遇到了这样的需求:需要实现4个物理按键的稳定检测,同时保证在多进程访问时不会出现竞态条件。
这个实验之所以选择Ubuntu 20.04作为开发环境,主要考虑到三点:首先LTS版本的工具链稳定;其次5.4内核已经包含完善的GPIO子系统支持;最重要的是在实际项目中,开发板驱动调试往往需要先在x86平台验证基础功能。通过QEMU或实际硬件,我们可以将验证过的驱动无缝移植到ARM平台。
典型的按键硬件电路有两种接法:
以常见的上拉电阻方案为例,硬件连接如下:
code复制GPIOx --- 10KΩ上拉电阻 --- VCC
|
按键开关
|
GND
在i.MX6UL开发板上,假设我们使用GPIO1_IO05作为按键输入引脚,对应的原理图标注可能是KEY1。实际项目中务必确认:
在开始编写驱动前,需要确保内核配置包含以下关键选项:
code复制CONFIG_INPUT=y
CONFIG_KEYBOARD_GPIO=y
CONFIG_GPIOLIB=y
CONFIG_DEBUG_FS=y # 用于调试GPIO状态
可以通过zcat /proc/config.gz | grep相关选项来验证,或直接在内核源码目录执行:
bash复制make menuconfig
在Device Drivers → Input device support → Keyboards路径下确认GPIO按键驱动已编译。
Linux内核的input子系统为按键驱动提供了标准框架,典型驱动结构如下:
c复制#include <linux/input.h>
#include <linux/gpio_keys.h>
static struct gpio_keys_button imx6ul_buttons[] = {
{
.code = KEY_POWER, // 按键键值,定义在input.h
.gpio = 5, // GPIO编号
.active_low = 1, // 低电平有效
.desc = "GPIO Key Power",
.type = EV_KEY, // 按键类型
.wakeup = 1, // 是否支持唤醒
},
};
static struct gpio_keys_platform_data imx6ul_button_data = {
.buttons = imx6ul_buttons,
.nbuttons = ARRAY_SIZE(imx6ul_buttons),
.poll_interval = 20, // 轮询间隔ms
};
static struct platform_device imx6ul_button_device = {
.name = "gpio-keys",
.id = -1,
.dev = {
.platform_data = &imx6ul_button_data,
},
};
当多个进程可能同时访问按键设备时,必须使用互斥锁保护共享资源。以下是完整实现示例:
c复制#include <linux/mutex.h>
static DEFINE_MUTEX(button_mutex); // 定义静态互斥体
static int button_press_count = 0; // 共享资源示例
static ssize_t button_read(struct file *filp, char __user *buf,
size_t count, loff_t *ppos)
{
int ret;
if (mutex_lock_interruptible(&button_mutex)) {
return -ERESTARTSYS; // 可中断的锁获取
}
// 临界区开始
ret = copy_to_user(buf, &button_press_count,
min(sizeof(button_press_count), count));
// 临界区结束
mutex_unlock(&button_mutex);
return ret ? -EFAULT : min(sizeof(button_press_count), count);
}
static irqreturn_t button_isr(int irq, void *dev_id)
{
if (mutex_trylock(&button_mutex)) { // 中断上下文使用trylock
button_press_count++;
mutex_unlock(&button_mutex);
}
return IRQ_HANDLED;
}
关键提示:在中断处理函数中必须使用mutex_trylock()而非mutex_lock(),因为中断上下文不能睡眠。如果获取锁失败,应根据业务需求决定是否记录此次按键。
驱动加载后,可以通过以下命令验证:
bash复制# 查看输入设备
ls /dev/input/
# 监听按键事件
evtest /dev/input/eventX # X替换为实际设备号
正常操作时终端会输出类似:
code复制Event: time 1234567.123456, type 1 (EV_KEY), code 116 (KEY_POWER), value 1
Event: time 1234567.123457, type 1 (EV_KEY), code 116 (KEY_POWER), value 0
编写多进程测试程序模拟竞态条件:
c复制#include <fcntl.h>
#include <sys/stat.h>
#define MAX_PROC 10
void reader_process(int id) {
int fd = open("/dev/button0", O_RDONLY);
int count;
for(int i=0; i<1000; i++) {
read(fd, &count, sizeof(count));
printf("Process %d read count: %d\n", id, count);
}
close(fd);
}
int main() {
for(int i=0; i<MAX_PROC; i++) {
if(fork() == 0) {
reader_process(i);
exit(0);
}
}
wait(NULL);
return 0;
}
没有互斥保护时,这个测试会频繁出现计数错误或进程卡死。加入mutex后,计数将保持严格一致。
机械按键存在5-10ms的抖动期,硬件防抖(RC电路)成本较高,推荐软件方案:
c复制#include <linux/timer.h>
static struct timer_list debounce_timer;
static void debounce_handler(struct timer_list *t)
{
int state = gpio_get_value(button_gpio);
if(state == pressed_state) {
input_report_key(input_dev, KEY_CODE, 1);
input_sync(input_dev);
}
}
static irqreturn_t button_isr(int irq, void *dev_id)
{
mod_timer(&debounce_timer, jiffies + msecs_to_jiffies(15));
return IRQ_HANDLED;
}
// 在probe函数中初始化
timer_setup(&debounce_timer, debounce_handler, 0);
对于便携设备,按键应支持唤醒功能:
c复制static int button_suspend(struct device *dev)
{
enable_irq_wake(button_irq);
return 0;
}
static int button_resume(struct device *dev)
{
disable_irq_wake(button_irq);
return 0;
}
static const struct dev_pm_ops button_pm_ops = {
.suspend = button_suspend,
.resume = button_resume,
};
bash复制cat /proc/bus/input/devices
bash复制echo 1 > /proc/sys/kernel/lockdep
insmod button.ko
dmesg | grep lockdep
bash复制echo function_graph > /sys/kernel/debug/tracing/current_tracer
echo gpio_keys_isr > /sys/kernel/debug/tracing/set_ftrace_filter
cat /sys/kernel/debug/tracing/trace_pipe
对于需要更高性能的场景,可以考虑:
在最近的一个工业HMI项目中,我们最终采用的混合方案是:基本按键检测使用内核驱动,业务逻辑处理通过Netlink套接字通知用户态守护进程。这种架构既保证了实时性,又避免了复杂的内核态开发。