按键驱动开发指南 (基于 STM32CubeMX)
本文档旨在说明如何使用 STM32CubeMX 工具进行基础的按键(Key/Button)硬件配置,并提供一套基础及一套高级的软件驱动方案。
一、 CubeMX 硬件配置
1. 设置引脚为输入模式
- 在
Pinout & Configuration视图中,找到并选择您计划用作按键的 GPIO 引脚。 - 点击该引脚,将其功能设置为
GPIO_Input。
2. 配置上拉模式
- 选中该引脚后,在下方的 “GPIO Mode and Configuration” 面板中,将引脚模式设置为 上拉 (Pull-up)。
- 重要说明:
- 为何选择上拉? 配置为上拉后,当按键未按下时,引脚稳定在高电平。当按键按下(通常是接地)时,引脚电平被拉低至低电平。这可以有效防止因引脚悬空而导致的程序误判。
- 内部与外部上拉: 即使硬件已设计外部上拉电阻,也强烈建议开启 MCU 的内部上拉。这可以作为冗余保护,确保在外部硬件异常时,按键功能依然稳定。
3. 后续步骤:生成代码与驱动编写
- 完成上述配置后,即可生成项目代码。
- 后续需要在代码中编写驱动逻辑以实现按键检测。
二、 按键驱动示例代码 (基础方案)
此方案适用于简单的单按键检测场景,逻辑简单,资源占用极低。
1. 变量定义
// 假设已在全局范围内定义以下变量
uint8_t Key_Val; // 用于存储 Key_Read() 函数返回的当前按键值
uint8_t Key_Down; // 用于标记哪个按键被按下 (产生下降沿)
uint8_t Key_Up; // 用于标记哪个按键被抬起 (产生上升沿)
uint8_t Key_Old; // 用于存储上一次的按键值,用于与当前值比较
2. 按键扫描与状态处理函数
/**
* @brief 读取当前按下的按键值
* @note 此函数为原始实现,同一时间只能返回一个按键值。
* 如果多个按键同时按下,将返回最后一个被检测到的按键值。
* @retval uint8_t 返回按键编号 (1-6),无按键按下则返回 0。
*/
uint8_t Key_Read()
{
uint8_t temp = 0; // 初始化临时变量为0,表示无按键
// 依次检测每个按键,如果按下,则将对应的键值赋给temp
if(HAL_GPIO_ReadPin(KEY1_GPIO_Port, KEY1_Pin) == GPIO_PIN_RESET) temp = 1; // KEY1
if(HAL_GPIO_ReadPin(KEY2_GPIO_Port, KEY2_Pin) == GPIO_PIN_RESET) temp = 2; // KEY2
if(HAL_GPIO_ReadPin(KEY3_GPIO_Port, KEY3_Pin) == GPIO_PIN_RESET) temp = 3; // KEY3
if(HAL_GPIO_ReadPin(KEY4_GPIO_Port, KEY4_Pin) == GPIO_PIN_RESET) temp = 4; // KEY4
if(HAL_GPIO_ReadPin(KEY5_GPIO_Port, KEY5_Pin) == GPIO_PIN_RESET) temp = 5; // KEY5
if(HAL_GPIO_ReadPin(KEY6_GPIO_Port, KEY6_Pin) == GPIO_PIN_RESET) temp = 6; // KEY6
return temp; // 返回最终检测到的按键值
}
/**
* @brief 核心按键处理任务,计算按键的按下和抬起事件
* @note 此函数需要被周期性调用 (例如在您自己的任务调度器中)
*/
void key_task()
{
// 步骤1: 读取当前按键的原始值
Key_Val = Key_Read();
// 步骤2: 计算“按下”事件 (下降沿检测)
Key_Down = Key_Val & (Key_Old ^ Key_Val);
// 步骤3: 计算“抬起”事件 (上升沿检测)
Key_Up = ~Key_Val & (Key_Old ^ Key_Val);
// 步骤4: 更新旧值,为下一次循环做准备
Key_Old = Key_Val;
}
3. 代码逻辑解读
-
Key_Read()函数:- 该函数通过逐个检查按键引脚来确定哪个按键被按下。
- 它的实现非常简单,但存在一个特性:由于连续使用赋值
=操作,如果多个按键同时按下,函数只会返回最后一个被检测到的按键编号。
-
key_task()函数:- 这是非常经典的“四行代码”按键状态机,其核心思想是通过比较本次和上次的按键状态差异,来精确捕捉事件。
- 它能将持续的电平状态(按键被一直按着)转换为瞬时的事件触发(“刚刚被按下的那一刻”和“刚刚被松开的那一刻”)。
-
重要逻辑说明:
key_task()中的位运算逻辑是为位掩码 (bitmask) 处理而设计的。然而,当前的Key_Read()函数返回的是按键编号。- 这种组合导致该方案仅在单按键操作时可靠。如果存在组合按键或按键切换,
key_task可能会产生错误的Key_Up(抬起)事件。
三、 高级按键驱动方案 (基于 easy_button 库)
为了实现更复杂的按键功能,如单击、双击、长按等,同时保持代码的整洁和可移植性,可以采用功能强大的开源按键库 easy_button。
- 开源库链接: Easy_Button
跨平台移植核心要点
easy_button库本身是平台无关的,移植到其他芯片(如 ESP32, TI 等)非常简单,只需修改与硬件相关的部分即可:
GPIO读取函数: 在
my_get_key_state()中,将HAL_GPIO_ReadPin()替换为您目标平台的 GPIO 读取 API。时间获取函数: 在
btn_task()中,将HAL_GetTick()替换为您目标平台的毫秒级计时 API。引脚定义: 根据新硬件,修改
KEY1_GPIO_Port,KEY1_Pin等相关宏定义。库本身无需修改: 只需正确实现上述硬件抽象层,
ebtn核心代码无需任何改动。
下面是 ebtn 库的使用代码示例,已按照五步法拆分展示。
第一步:包含头文件
在你的 C 文件顶部,包含 ebtn 库的头文件。
#include "ebtn.h"
第二步:定义参数与按键列表
这是配置按键行为的核心步骤。这个优化版本使用枚举(enum)来定义按键 ID,极大地增强了代码的可读性和可维护性。
/* 1. 定义按键参数实例 */
const ebtn_btn_param_t key_param_normal = EBTN_PARAMS_INIT(
20, // time_debounce: 按下消抖 20ms
20, // time_debounce_release: 释放消抖 20ms
50, // time_click_pressed_min: 有效最短单击按下 50ms
500, // time_click_pressed_max: 有效最长单击按下 500ms
300, // time_click_multi_max: 多次单击最大间隔 300ms
500, // time_keepalive_period: 长按事件周期 500ms
5 // max_consecutive: 最多支持 5 连击
);
/* 前置:为每个按键注册ID,便于直接阅读按键作用 */
typedef enum
{
KEY1 = 1, //注册单个按键ID
KEY2,
KEY3,
KEY4,
KEY5,
KEY6,
KEY_MAX,
COM_KEY1 = 101, //注册组合按键ID
COM_KEY2 = 102,
}user_button_t;
/* 2. 定义静态按键列表 */
ebtn_btn_t my_buttons[] = {
EBTN_BUTTON_INIT(KEY1, &key_param_normal), // KEY1, ID=1
EBTN_BUTTON_INIT(KEY2, &key_param_normal), // KEY2, ID=2
EBTN_BUTTON_INIT(KEY3, &key_param_normal), // KEY3, ID=3
EBTN_BUTTON_INIT(KEY4, &key_param_normal), // KEY4, ID=4
EBTN_BUTTON_INIT(KEY5, &key_param_normal), // KEY5, ID=5
EBTN_BUTTON_INIT(KEY6, &key_param_normal), // KEY6, ID=6
};
/* 3. 定义静态组合按键列表 (可选) */
ebtn_btn_combo_t my_combos[] = {
EBTN_BUTTON_COMBO_INIT(COM_KEY1, &key_param_normal), // 组合键, ID=101
EBTN_BUTTON_COMBO_INIT(COM_KEY2, &key_param_normal), // 组合键, ID=102
};
第三步:编写回调函数
你需要提供两个函数给 ebtn 库:一个用于读取硬件状态,一个用于处理库检测到的事件。
/* 1. 实现获取按键状态的回调函数 */
uint8_t my_get_key_state(struct ebtn_btn *btn) {
// 根据传入的按钮实例中的 key_id 判断是哪个物理按键
switch (btn->key_id) {
case KEY1:
// 假设按下为低电平 (返回 1 代表按下)
return (HAL_GPIO_ReadPin(KEY1_GPIO_Port, KEY1_Pin) == GPIO_PIN_RESET);
case KEY2:
return (HAL_GPIO_ReadPin(KEY2_GPIO_Port, KEY2_Pin) == GPIO_PIN_RESET);
case KEY3:
return (HAL_GPIO_ReadPin(KEY3_GPIO_Port, KEY3_Pin) == GPIO_PIN_RESET);
case KEY4:
return (HAL_GPIO_ReadPin(KEY4_GPIO_Port, KEY4_Pin) == GPIO_PIN_RESET);
case KEY5:
return (HAL_GPIO_ReadPin(KEY5_GPIO_Port, KEY5_Pin) == GPIO_PIN_RESET);
case KEY6:
return (HAL_GPIO_ReadPin(KEY6_GPIO_Port, KEY6_Pin) == GPIO_PIN_RESET);
default:
// 对于未知的 key_id,安全起见返回 0 (未按下)
return 0;
}
}
/* 2. 实现处理按键事件的回调函数 */
void my_handle_key_event(struct ebtn_btn *btn, ebtn_evt_t evt) {
uint16_t key_id = btn->key_id; // 获取触发事件的按键 ID
uint16_t click_cnt = ebtn_click_get_count(btn); // 获取连击次数 (仅在 ONCLICK 事件时有意义)
// 根据事件类型进行处理
switch (evt) {
case EBTN_EVT_ONPRESS: // 按下事件
if (key_id == KEY1) { /* Do something for KEY1 press */ }
break;
case EBTN_EVT_ONRELEASE: // 释放事件
if (key_id == KEY1) { /* Do something for KEY1 release */ }
break;
case EBTN_EVT_ONCLICK: // 单击/连击事件
if (key_id == KEY1) {
if (click_cnt == 1) { /* KEY1 单击 */ }
else if (click_cnt == 2) { /* KEY1 双击 */ }
} else if (key_id == KEY2) {
if (click_cnt == 1) { /* KEY2 单击 */ }
} else if (key_id == COM_KEY1) {
if (click_cnt == 1) { /* 组合键1 单击 */ }
}
break;
case EBTN_EVT_KEEPALIVE: // 长按事件
if (key_id == KEY1) { /* KEY1 长按 */ }
break;
default:
break;
}
}
第四步:初始化 ebtn 库
在系统启动的初始化阶段,调用一个封装好的初始化函数,将之前准备好的按键列表和回调函数"注册"给 ebtn 库,并绑定组合键。
void my_ebtn_init(void)
{
/* 绑定按键列表、回调函数 */
ebtn_init(my_buttons, EBTN_ARRAY_SIZE(my_buttons),
my_combos, EBTN_ARRAY_SIZE(my_combos),
my_get_key_state, my_handle_key_event);
/* 配置组合键 (如果使用了) */
// 1. 找到参与组合的普通按键的内部索引 (Index)
int key1_index = ebtn_get_btn_index_by_key_id(KEY1);
int key2_index = ebtn_get_btn_index_by_key_id(KEY2);
// 2. 将这些索引对应的按键添加到组合键定义中
if (key1_index >= 0 && key2_index >= 0) {
// 假设 my_combos[0] 是 COM_KEY1 (KEY1+KEY2)
ebtn_combo_btn_add_btn_by_idx(&my_combos[0], key1_index); // 将 KEY1 添加到组合键
ebtn_combo_btn_add_btn_by_idx(&my_combos[0], key2_index); // 将 KEY2 添加到组合键
}
}
第五步:周期性调用处理函数
这是让 ebtn 库运转起来的最后一步。你需要在一个固定时间间隔(推荐 5ms 到 20ms)调用的地方,不断地调用 ebtn 的核心处理函数。
/* 封装一个周期任务函数 */
void btn_task(void)
{
ebtn_process(HAL_GetTick());
}
如何调用 btn_task():
-
推荐方式 (定时器中断): 在一个周期为 5-20ms 的定时器中断服务程序中调用
btn_task()。// 例如,在 HAL 库的定时器周期溢出回调中 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIMx) { // TIMx 是你的定时器实例 btn_task(); // 调用按键处理任务 } } -
简单方式 (主循环): 在
main函数的while(1)循环中直接调用btn_task(),但要确保主循环没有长时间阻塞。int main(void) { // ... 初始化 ... my_ebtn_init(); while (1) { btn_task(); // ... 其他主循环任务 ... } }