本仓库是一个基于hal库,通过定时实现按键消抖,按键状态机实现按键单击、双击和长按,支持重复触发的按键状态机,仓库中案例是一个四个按键。主要目的是将硬件底层驱动在 BSP/,应用逻辑放在 Application/,tim.c里需在1ms中断添加key_tick(),main.c则表示主循环下如何使用
BSP/:外设底层驱动Application/:应用代码与任务
下面说明如何在本仓库基础上增加按键数量,并把按键映射改成你自己硬件上的引脚。内容适用于想把按键模块扩展到更多按键或更方便移植到新开发板的场景。
核心要点:
- 按键模块依赖一个固定的周期
key_tick()(默认 1 ms),以及采样间隔(默认 20 ms)。 - 要增加按键,只需扩展
KEY_COUNT、添加按键编号与对应的读引脚宏或使用更可扩展的端口/引脚数组。 - 确保
tim.c中产生的定时中断(目前 TIM6)以 1 ms 调用key_tick(),或根据KEY_TICK_MS的定义调整定时器。
下面给出两种实现方式:最小修改法(适合少量新增按键)与可扩展法(便于大量按键或经常移植)。
- 最小修改法(直接在现有代码基础上扩展)
- 修改
BSP/Key/Inc/key.h:- 把
#define KEY_COUNT 4改为你需要的数量,例如#define KEY_COUNT 6。 - 添加新的按键编号宏,例如:
#define KEY_5 4 #define KEY_6 5
- 添加对应的读引脚宏(如果你的按键接在不同引脚):
#ifndef KEY_READ_PIN5 #define KEY_READ_PIN5() HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_3) #endif #ifndef KEY_READ_PIN6 #define KEY_READ_PIN6() HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_5) #endif
- 把
- 修改
BSP/Key/Src/key.c:在Key_GetState()的switch里添加case KEY_5:、case KEY_6:分支,按现有模式读取对应宏并返回KEY_PRESSED或KEY_UNPRESSED。 - 更新应用层
KeyTask.c:在事件处理处增加新按键的处理分支(参考已注释的 KEY_2/3/4 示例)。
- 可扩展法—— 使用端口/引脚数组,移植时只要在头文件里改数组即可
- 优点:不需要每次改
switch,增加/减少按键只需修改数组与KEY_COUNT。 - 修改示例(替换
Key_GetState的实现):
// 在 key.h 中新增(或在 key_port.h 中统一定义)
extern GPIO_TypeDef* KEY_PORT[];
extern const uint16_t KEY_PIN[];
// 在 key.c 中定义(按你的硬件改端口与引脚)
GPIO_TypeDef* KEY_PORT[] = {GPIOB, GPIOB, GPIOB, GPIOA, GPIOA, GPIOB};
const uint16_t KEY_PIN[] = {GPIO_PIN_0, GPIO_PIN_1, GPIO_PIN_2, GPIO_PIN_0, GPIO_PIN_3, GPIO_PIN_5};
uint8_t Key_GetState(uint8_t key_number)
{
if (key_number >= KEY_COUNT) return KEY_UNPRESSED;
GPIO_PinState st = HAL_GPIO_ReadPin(KEY_PORT[key_number], KEY_PIN[key_number]);
return (st == GPIO_PIN_RESET) ? KEY_PRESSED : KEY_UNPRESSED; // 视你的电路为低电平按下
}- 在移植时只需修改
KEY_PORT与KEY_PIN数组即可映射到任意引脚。若设备按键为高电平有效,请把条件改成st == GPIO_PIN_SET。
- 确保
key_tick()的定时来源正确
- 当前例子在
Core/Src/tim.c中的HAL_TIM_PeriodElapsedCallback对 TIM6 的回调里调用key_tick()。确保:- TIM6 的时基配置能产生 1 ms 中断,或根据
KEY_TICK_MS修改 TIM6 产生相应周期。 - 如果你不使用 TIM6,也可以在 SysTick 或 RTOS 定时器里调用
key_tick()(保持稳定周期)。
- TIM6 的时基配置能产生 1 ms 中断,或根据
示例:如果 KEY_TICK_MS == 1,TIM6 应配置为 1 ms 周期。若你改为 5 ms,需把 KEY_SAMPLE_MS 与 KEY_SAMPLE_TICKS 一并检查(采样间隔 = KEY_SAMPLE_TICKS * KEY_TICK_MS)。
- 采样与响应时间调整
KEY_SAMPLE_MS(默认 20 ms)与KEY_TIME_*(单击、双击、长按等)定义在key.h中。若增加更多按键或不同按键反应需求,可按需调整:- 更短的
KEY_SAMPLE_MS能稍微降低按键响应延迟,但可能增加误触风险; KEY_TIME_DOUBLE决定双击最大间隔;KEY_TIME_LONG决定长按判定时间。
- 更短的
- 测试流程(建议步骤)
- 运行:下载到板子,观察串口打印或 LED 指示,验证每个按键的 SINGLE/DOUBLE/LONG/REPEAT 是否触发正确。
- 若某个按键不响应:
- 检查
KEY_PORT/KEY_PIN是否正确; - 检查
key_tick()是否按预期周期调用。
- 检查
keyflag 是一个按键事件位掩码数组:uint8_t keyflag[KEY_COUNT],每一位代表一种按键状态或事件,通过 Key_Check() 读取并消费相应位。
位定义与含义(在 BSP/Key/Inc/key.h 中)
KEY_HOLD(0x01):按键当前处于按住状态(物理按下,经采样确认)。在key_tick()的采样段按下时置位,未按时清除。KEY_DOWN(0x02):按下的边沿事件(从未按到按下)。在key_tick()采样时检测到上升边(按下)时置位。KEY_UP(0x04):松开的边沿事件(从按下到松开)。在key_tick()检测到下降边(松开)时置位。KEY_SINGLE(0x08):高层“单击”事件(在等待双击窗口超时后产生)。由状态机在超时处置位。KEY_DOUBLE(0x10):高层“双击”事件(在双击窗口内第二次按下时置位)。KEY_LONG(0x20):高层“长按”事件(按下持续到长按阈值时置位,并进入重复模式)。KEY_REPEAT(0x40):长按后的“重复”事件,在进入重复模式后每隔一段时间置位一次。
在哪儿设置/清除
- 设置:上述位均由
BSP/Key/Src/key.c的key_tick()内的采样逻辑与状态机按条件设置(keyflag[i] |= ...)。 - 清除:
KEY_HOLD在下一次采样读到未按时由采样逻辑自动清除(keyflag[i] &= ~KEY_HOLD)。- 高层事件位(
KEY_SINGLE/DOUBLE/LONG/REPEAT/DOWN/UP)通常由应用通过Key_Check()读取后清除;Key_Check()内部实现会清除对应位。
使用示例(应用层)
if (Key_Check(KEY_1, KEY_SINGLE)) { /* 处理单击 */ }
if (Key_Check(KEY_1, KEY_DOUBLE)) { /* 处理双击 */ }
if (Key_Check(KEY_1, KEY_LONG)) { /* 处理长按 */ }
if (Key_Check(KEY_1, KEY_REPEAT)) { /* 处理长按重复 */ }
// 若想查询当前是否按住(不消费)
if (keyflag[KEY_1] & KEY_HOLD) { /* 正在按住 */ }