环境配置

项目使用STM32CubeMX初始化框架

IDE: JetBrains Clion 2025.1.1

编译环境使用arm-none-eabi-gcc

使用嘉立创天空星STM32F407VGT6开发板进行测试

提要

使用链表及FSM对按键操作进行封装处理,实现常见的消抖,单击,双击,长按操作封装。

实现

Key.c文件内容如下

/*
 * @file    key.c
 * @brief   按键处理模块源文件
 * @version 0.1
 * @date    2025-05-27
 * @Author  Nymphaea0726
 * This module provides functions to handle key events such as single click,
 * double click, and long press. It uses a state machine to manage the key states.
 */

#include "key.h"

static key_t* key_list_head = NULL; // 按键链表头

/**
 * @brief 读取按键引脚电平
 */
static uint8_t read_key_level(key_t* key) {
    return HAL_GPIO_ReadPin(key->port, key->pin);
}

/**
 * @brief 初始化一个按键对象
 */
void key_init(key_t* key, GPIO_TypeDef* port, uint16_t pin, uint8_t active_level) {
    if (!key) return;

    key->port = port;
    key->pin = pin;
    key->state = STATE_IDLE; // 初始状态为空闲
    key->timer = 0;
    key->event = KEY_EVENT_NONE;
    key->active_level = active_level; // 设置按键有效电平(0:低电平有效,1:高电平有效)

    key->on_single_click = NULL;
    key->on_double_click = NULL;
    key->on_long_press_start = NULL;
    key->on_long_press_hold = NULL;
    key->on_long_press_release = NULL;
    key->next = NULL; // 初始化链表指针为NULL,该变量用于指定下一个按键对象,以便在链表中管理多个按键对象。
}

/**
 * @brief 注册按键
 */
void key_register(key_t* key) {
    key->next = key_list_head;
    key_list_head = key;
}

/**
 * @brief 为按键绑定事件回调函数
 */
void key_attach_event(key_t* key, key_event_t event, void (*callback)(void*)) {
    if (!key) return;
    switch (event) {
        case KEY_EVENT_SINGLE_CLICK:      key->on_single_click = callback; break; // 单击事件
        case KEY_EVENT_DOUBLE_CLICK:      key->on_double_click = callback; break; // 双击事件
        case KEY_EVENT_LONG_PRESS_START:  key->on_long_press_start = callback; break; // 长按
        case KEY_EVENT_LONG_PRESS_HOLD:   key->on_long_press_hold = callback; break;
        case KEY_EVENT_LONG_PRESS_RELEASE:key->on_long_press_release = callback; break;
        default: break;
    }
}

/**
 * @brief 处理单个按键的状态机,应被周期性调用,使用定时中断或xTask等方式
 */
static void key_process(key_t* key) {
    if (!key) return;

    uint8_t is_pressed = (read_key_level(key) == key->active_level);
    key->timer += KEY_SCAN_INTERVAL;

    switch (key->state) {
        case STATE_IDLE:
            if (is_pressed) {
                key->state = STATE_DEBOUNCE;
                key->timer = 0;
            }
            break;

        case STATE_DEBOUNCE:
            if (is_pressed) {
                if (key->timer >= KEY_DEBOUNCE_TIME) {
                    key->state = STATE_PRESSED;
                    key->timer = 0;
                }
            } else {
                key->state = STATE_IDLE;
            }
            break;

        case STATE_PRESSED:
            if (is_pressed) {
                if (key->timer >= KEY_LONG_PRESS_TIME) {
                    key->state = STATE_LONG_PRESS_START;
                    if (key->on_long_press_start) key->on_long_press_start(key);
                }
            } else {
                // 按键释放,开始等待双击
                key->state = STATE_RELEASED_WAIT;
                key->timer = 0;
            }
            break;

        case STATE_LONG_PRESS_START:
            key->state = STATE_LONG_PRESS_HOLD;
            key->timer = 0;
            // 这里不Break,目的是进入STATE_LONG_PRESS_HOLD

        case STATE_LONG_PRESS_HOLD:
            if (is_pressed) {
                if (key->on_long_press_hold) key->on_long_press_hold(key);
            } else {
                if (key->on_long_press_release) key->on_long_press_release(key);
                key->state = STATE_IDLE;
            }
            break;

        case STATE_RELEASED_WAIT:
            if (key->timer > KEY_DOUBLE_CLICK_INTERVAL) {
                // 超时,确认单击事件
                if (key->on_single_click) key->on_single_click(key);
                key->state = STATE_IDLE;
            } else {
                if (is_pressed) {
                    // 再次按下,判定双击
                    if (key->on_double_click) key->on_double_click(key);
                    // 进入等待释放状态,避免立即触发单击
                    key->state = STATE_WAIT_RELEASE;
                    key->timer = 0;
                }
            }
            break;

        case STATE_WAIT_RELEASE:
            // 等待双击释放完成
            if (!is_pressed) {
                key->state = STATE_IDLE;
            }
            break;
    }
}

/**
 * @brief 处理链表内所有按键的状态机
 */
void key_process_all(void) {
    for (key_t* current = key_list_head; current != NULL; current = current->next) {
        key_process(current);
    }
}

key.h文件内容如下

/*
 * @file    key.h
 * @brief   按键处理模块头文件
 * @version 0.1
 * @date    2025-05-27
 * @Author  Nymphaea0726
 * This module provides functions to handle key events such as single click,
 * double click, and long press. It uses a state machine to manage the key states.
 */

#ifndef KEY_H
#define KEY_H

#include <stdint.h>
#include "stm32f4xx_hal_gpio.h"

// 按键时间定义
#define KEY_DEBOUNCE_TIME         20      // 消抖时间 (ms)
#define KEY_LONG_PRESS_TIME       1000    // 长按检测时间 (ms)
#define KEY_DOUBLE_CLICK_INTERVAL 500     // 双击间隔时间 (ms)
#define KEY_SCAN_INTERVAL         10      // 按键扫描间隔 (ms),应在定时器中断中调用,且必须与定时中断时间同步。

// 按键事件类型定义
typedef enum {
    KEY_EVENT_NONE = 0,
    KEY_EVENT_SINGLE_CLICK,
    KEY_EVENT_DOUBLE_CLICK,
    KEY_EVENT_LONG_PRESS_START,
    KEY_EVENT_LONG_PRESS_HOLD,
    KEY_EVENT_LONG_PRESS_RELEASE,
} key_event_t;

// 按键状态机状态定义
typedef enum {
    STATE_IDLE,               // 空闲状态
    STATE_DEBOUNCE,           // 消抖状态
    STATE_PRESSED,            // 按下确认状态
    STATE_LONG_PRESS_START,   // 长按开始状态
    STATE_LONG_PRESS_HOLD,    // 长按保持状态
    STATE_RELEASED_WAIT,      // 释放后等待双击状态
    STATE_WAIT_RELEASE,       // 双击后等待释放状态
} key_state_t;

// 按键对象结构体定义
typedef struct key_t {
    GPIO_TypeDef*   port;           // 按键GPIO端口
    uint16_t        pin;            // 按键GPIO引脚
    key_state_t     state;          // 当前状态
    uint32_t        timer;          // 状态计时器
    key_event_t     event;          // 检测到的按键事件
    uint8_t         active_level;   // 按下电平(0:低电平有效,1:高电平有效)

    // 事件回调函数指针
    void (*on_single_click)(void*);
    void (*on_double_click)(void*);
    void (*on_long_press_start)(void*);
    void (*on_long_press_hold)(void*);
    void (*on_long_press_release)(void*);

    struct key_t* next;             // 链表指针,多个按键管理用
} key_t;

// 函数声明
void key_init(key_t* key, GPIO_TypeDef* port, uint16_t pin, uint8_t active_level);
void key_attach_event(key_t* key, key_event_t event, void (*callback)(void*));
void key_process_all(void);
void key_register(key_t* key);

#endif

原理

使用FSM对按键状态进行Switch判断,并在中断中调用,减少使用HAL_Delay进行延时对MCU的资源占用,同时使用链表形式对逻辑进行抽象,减少多按键处理时冗杂代码的编写。

用例

key_t usr_key;

void handle_single_click(void* key) {
    // 处理单击事件
    uart_printf("Single Click Detected\r\n");
}

void handle_double_click(void* key) {
    // 处理双击事件
    uart_printf("Double Click Detected\r\n");
}

void handle_long_press_start(void* key) {
    // 处理长按开始事件
    uart_printf("Long Press Start Detected\r\n");
}

key_init(&usr_key,Key_GPIO_Port, Key_Pin, 1); // 初始化按键对象,高电平有效。

key_attach_event(&usr_key, KEY_EVENT_SINGLE_CLICK, handle_single_click);
key_attach_event(&usr_key, KEY_EVENT_DOUBLE_CLICK, handle_double_click);
key_attach_event(&usr_key, KEY_EVENT_LONG_PRESS_START, handle_long_press_start);

key_register(&usr_key); // 注册按键到管理链表

最终呈现代码是经过Google Gemini 2.5 Pro优化版本,另经本人人为修改以适应本人编码习惯。