嵌入式Linux按键监控模块详解:实现设备重启与长按检测

嵌入式Linux按键监控模块详解:实现设备重启与长按检测

在嵌入式Linux设备开发中,物理按键仍然是用户与设备交互的重要方式。本文将分享一个轻量级但功能完整的按键监控模块,它可以精确识别按键的短按和长按事件,并执行对应操作如系统重启。

需求背景

许多嵌入式设备需要提供物理重启按钮,一般有两种常见的交互模式:

  • 短按:重启设备(常见操作)
  • 长按:执行特殊功能,如恢复出厂设置(避免误触发)

这些看似简单的需求,实现起来需要考虑多线程、事件检测、资源管理等多方面因素。

技术方案

我们的方案基于以下技术组件:

  • Linux Input子系统:捕获设备按键事件
  • epoll机制:高效IO复用,降低资源消耗
  • pthread:创建独立线程处理按键事件
  • 回调函数:灵活处理不同类型的按键事件

代码实现

1. 模块头文件定义

首先,定义模块对外接口和数据类型:

// sl_key.h
#ifndef SL_KEY_H
#define SL_KEY_H

#include <stdint.h>

// 按键事件类型
typedef enum {
    KEY_EVENT_SHORT_PRESS,    // 短按事件
    KEY_EVENT_LONG_PRESS,     // 长按事件
} key_event_type_t;

// 按键事件回调函数类型
typedef void (*key_callback_t)(key_event_type_t event);

// 初始化按键检测模块
int sl_key_init(const char *gpio_path, key_callback_t callback);

// 停止按键检测
void sl_key_deinit(void);

// 快速初始化函数
void reset_key_listen_init(void);

#endif // SL_KEY_H

这个简洁的接口定义了两种按键事件类型和必要的初始化/反初始化函数。

2. 核心模块实现

模块主体实现包含以下几个关键部分:

2.1 配置参数与全局状态
// 配置参数
#define KEY_LONG_PRESS_TIME   5000    // 长按阈值(毫秒)
#define KEY_SHORT_PRESS_TIME  1000    // 短按最小时间(毫秒)
#define EPOLL_TIMEOUT         500     // epoll超时时间(毫秒)

// 模块状态
static struct {
    int fd;                     // 设备文件描述符
    int epoll_fd;              // epoll文件描述符
    pthread_t thread;          // 监听线程
    volatile int running;      // 运行标志
    key_callback_t callback;   // 回调函数
} g_key_ctx = {
    .fd = -1,
    .epoll_fd = -1,
    .running = 0,
    .callback = NULL
};

这里定义了短按和长按的时间阈值:短按至少1秒,长按需要5秒以上。同时使用静态结构体集中管理模块状态,便于维护。

2.2 按键监听线程
static void *key_monitor_thread(void *arg)
{
    struct epoll_event events[1];
    struct input_event ev;
    uint64_t press_time = 0;
    int key_pressed = 0;

    while (g_key_ctx.running) {
        int ret = epoll_wait(g_key_ctx.epoll_fd, events, 1, EPOLL_TIMEOUT);

        if (ret < 0) {
            if (errno != EINTR) {
                SL_ERR("epoll_wait failed: %s\n", strerror(errno));
                break;
            }
            continue;
        }

        if (ret == 0) {
            continue;  // 超时,继续等待
        }

        if (events[0].events & (EPOLLERR | EPOLLHUP)) {
            SL_ERR("epoll error event\n");
            break;
        }

        if (!(events[0].events & EPOLLIN)) {
            continue;
        }

        // 读取按键事件
        if (read(g_key_ctx.fd, &ev, sizeof(ev)) != sizeof(ev)) {
            if (errno != EAGAIN && errno != EWOULDBLOCK) {
                SL_ERR("read event failed: %s\n", strerror(errno));
                break;
            }
            continue;
        }

        // 只处理按键事件
        if (ev.type != EV_KEY) {
            continue;
        }

        // 按键按下
        if (ev.value == 1 && !key_pressed) {
            press_time = get_current_ms();
            key_pressed = 1;
            SL_LOG("Key pressed\n");
        }
        // 按键释放
        else if (ev.value == 0 && key_pressed) {
            uint64_t duration = get_current_ms() - press_time;
            key_pressed = 0;

            SL_LOG("Key released, duration: %lu ms\n", duration);

            // 回调处理
            if (g_key_ctx.callback) {
                if (duration >= KEY_LONG_PRESS_TIME) {
                    g_key_ctx.callback(KEY_EVENT_LONG_PRESS);
                } else if (duration >= KEY_SHORT_PRESS_TIME) {
                    g_key_ctx.callback(KEY_EVENT_SHORT_PRESS);
                }
            }
        }
    }

    return NULL;
}

这是模块的核心逻辑,通过epoll高效等待按键事件,记录按下时间并计算持续时间,然后根据时长判断按键类型并触发回调。

2.3 模块初始化与清理
int sl_key_init(const char *gpio_path, key_callback_t callback)
{
    if (!gpio_path || !callback) {
        return -1;
    }

    // 防止重复初始化
    if (g_key_ctx.running) {
        return -1;
    }

    // 打开设备
    g_key_ctx.fd = open(gpio_path, O_RDONLY | O_NONBLOCK);
    if (g_key_ctx.fd < 0) {
        SL_ERR("Failed to open %s: %s\n", gpio_path, strerror(errno));
        return -1;
    }

    // 创建epoll实例
    g_key_ctx.epoll_fd = epoll_create1(0);
    if (g_key_ctx.epoll_fd < 0) {
        SL_ERR("Failed to create epoll: %s\n", strerror(errno));
        close(g_key_ctx.fd);
        g_key_ctx.fd = -1;
        return -1;
    }

    // 添加到epoll监听
    struct epoll_event ev = {
        .events = EPOLLIN,
        .data.fd = g_key_ctx.fd
    };

    if (epoll_ctl(g_key_ctx.epoll_fd, EPOLL_CTL_ADD, g_key_ctx.fd, &ev) < 0) {
        SL_ERR("Failed to add to epoll: %s\n", strerror(errno));
        close(g_key_ctx.epoll_fd);
        close(g_key_ctx.fd);
        g_key_ctx.fd = -1;
        g_key_ctx.epoll_fd = -1;
        return -1;
    }

    // 设置回调和运行标志
    g_key_ctx.callback = callback;
    g_key_ctx.running = 1;

    // 创建监听线程
    if (pthread_create(&g_key_ctx.thread, NULL, key_monitor_thread, NULL) != 0) {
        SL_ERR("Failed to create thread: %s\n", strerror(errno));
        close(g_key_ctx.epoll_fd);
        close(g_key_ctx.fd);
        g_key_ctx.fd = -1;
        g_key_ctx.epoll_fd = -1;
        g_key_ctx.running = 0;
        return -1;
    }

    SL_LOG("Key module initialized successfully\n");
    return 0;
}

void sl_key_deinit(void)
{
    if (!g_key_ctx.running) {
        return;
    }

    // 停止线程
    g_key_ctx.running = 0;
    pthread_join(g_key_ctx.thread, NULL);

    // 关闭文件描述符
    if (g_key_ctx.epoll_fd >= 0) {
        close(g_key_ctx.epoll_fd);
        g_key_ctx.epoll_fd = -1;
    }

    if (g_key_ctx.fd >= 0) {
        close(g_key_ctx.fd);
        g_key_ctx.fd = -1;
    }

    g_key_ctx.callback = NULL;
    SL_LOG("Key module deinitialized\n");
}

初始化函数完成资源分配和线程创建,每一步都有错误检查和资源释放,确保不会出现资源泄露。反初始化函数则负责清理所有资源。

2.4 按键事件处理函数
void key_handler(key_event_type_t event)
{
    switch (event) {
        case KEY_EVENT_SHORT_PRESS:
            SL_LOG("Short press detected\r\n");
            sl_reboot();  // 短按重启系统
            break;

        case KEY_EVENT_LONG_PRESS:
            SL_LOG("Long press detected\r\n");
            // 处理长按(预留)
            break;
    }
}

void reset_key_listen_init(void)
{
     // 初始化按键模块
    if (sl_key_init("/dev/input/event1", key_handler) < 0) {
        SL_ERR("Failed to initialize key module\n");
    }
}

这部分定义了具体的按键处理逻辑:短按触发系统重启,长按目前只记录日志(预留扩展)。reset_key_listen_init是一个便捷的初始化函数,使用固定路径和处理函数。

技术要点解析

1. 为什么使用epoll而非轮询?

在嵌入式系统中,资源通常受限,使用epoll机制有以下优势:

  • 资源效率高:只在有事件时才被唤醒,降低CPU占用
  • 扩展性好:即使监控多个按键,性能也不会明显下降
  • 响应及时:事件触发立即被通知,不依赖轮询间隔

2. 为什么使用独立线程?

按键处理使用独立线程的好处:

  • 不阻塞主程序:即使按键处理耗时,也不影响主程序执行
  • 实时性好:按键事件能够及时处理,不依赖主程序调度
  • 模块化设计:按键模块可以独立工作,便于集成和移植

3. 按键时间阈值选择

  • 短按最小时间(1秒):过滤意外触碰,确保是有意识的操作
  • 长按阈值(5秒):足够长以区分短按,避免误触发重要功能
  • 超时时间(500毫秒):平衡响应速度和资源占用

使用方法

使用该模块很简单,只需在系统初始化时调用:

#include "sl_key.h"

int main()
{
    // 方法1:使用便捷函数(固定设备路径)
    reset_key_listen_init();

    // 或者方法2:自定义设备路径和回调
    // sl_key_init("/dev/input/eventX", custom_key_handler);

    // 主程序逻辑...

    // 退出前清理
    sl_key_deinit();
    return 0;
}

应用场景

该按键模块适用于多种嵌入式设备场景:

  1. 网络设备:路由器、交换机等需要重启/复位功能
  2. 智能家居:需要物理按键控制的智能设备
  3. 工控设备:需要紧急重启或模式切换的控制设备
  4. 医疗设备:需要物理按键操作的设备

改进方向

虽然这个模块已经能满足基本需求,但仍有以下改进空间:

  1. 多按键支持:扩展为支持多个按键及组合按键
  2. 按键防抖:添加软件防抖动算法,提高可靠性
  3. 更多事件类型:如双击、三击等复杂操作
  4. 动态配置:支持运行时调整按键时间阈值
  5. 低功耗优化:结合系统电源管理,降低待机功耗

总结

这个按键监控模块虽然代码量不大,但涵盖了Linux设备交互、事件处理、多线程编程等多个技术要点。它通过精心设计,在资源消耗与功能实现之间取得了很好的平衡,适合在各类嵌入式Linux系统中使用。

对于需要物理按键交互的嵌入式设备,这个模块提供了一个可靠、高效、易于集成的解决方案。它不仅支持基本的短按重启功能,还预留了长按接口,可根据实际需求进行扩展。

参考资源

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值