环境配置
项目使用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优化版本,另经本人人为修改以适应本人编码习惯。