它是啥? 想象引脚内部有一根"橡皮筋"轻轻地把它往"高电平"(通常是电源 VCC
)拉。当没有更强的力量(比如按键按下接地)作用时,它就保持在高电平。
为啥用? 防止引脚"悬空"乱飘。就像一个开关,默认是开着的,只有你按下时才关断。
何时用? 当你需要引脚在空闲时稳定地输出高电平,或者连接的按键按下时会把引脚拉到低电平 (GND
)。这是最常见的按键连接方式之一。
它是啥? 和上拉相反,这次"橡皮筋"是把引脚往"低电平"(地线 GND
)拉。默认状态是低电平。
为啥用? 同样是为了防止悬空,提供一个明确的默认状态。
何时用? 当你需要引脚空闲时稳定在低电平,或者连接的按键按下时会把引脚接到高电平 (VCC
)。相对上拉来说,用得稍微少一些。
它是啥? 引脚内部既不上拉也不下拉,像个与世无争的"隐士"。它的电平完全由外部连接决定。如果什么都没接,它的状态就像空气中的羽毛,极其不稳定,容易受到各种电磁干扰。
为啥用? 有些特定场景需要引脚呈现高阻态,不影响外部电路(比如模拟信号输入,或者需要外部电路精确控制电平)。但在简单的数字输入,尤其是按键检测中,极少使用浮空模式,因为它太容易"受惊"了。
注意! 直接用浮空模式连接按键通常是不可靠的,你需要外部电路(比如外部上拉或下拉电阻)来明确默认电平。
在key中需要查看原理图后选择GPIO的上拉或者下拉电阻(本文选择下拉电阻)
它可以检测GPIO的引脚状态
GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin);
HAL_GPIO_Readpin的返回值有两个(通过枚举)
可以直接对HAL_GPIO_Readpin()直接判断使用
const ebtn_btn_param_t defaul_ebtn_param = EBTN_PARAMS_INIT
的解读const ebtn_btn_param_t defaul_ebtn_param = 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 (按下超过 500ms 后,每 500ms 触发一次)
5 // max_consecutive: 最多支持 5 连击
);
EBTN_PARAMS_INIT
的解读#define EBTN_PARAMS_INIT(_time_debounce, _time_debounce_release, _time_click_pressed_min, _time_click_pressed_max, _time_click_multi_max, \
_time_keepalive_period, _max_consecutive) \
{ \
.time_debounce = _time_debounce, .time_debounce_release = _time_debounce_release, .time_click_pressed_min = _time_click_pressed_min, \
.time_click_pressed_max = _time_click_pressed_max, .time_click_multi_max = _time_click_multi_max, .time_keepalive_period = _time_keepalive_period, \
.max_consecutive = _max_consecutive \
}
这段代码的用处是给 defaul_ebtn_param
的结构体赋值正好与ebtn_btn_param_t
定义的类型对应
defaul_ebtn_param
的解读defaul_ebtn_param
为被定义的变量它用于保存这些参数然后给用户的key
ebtn_btn_param_t
的解读它是用来定义按键参数的结构体
/* 2. 定义静态按键列表 */
// 宏: EBTN_BUTTON_INIT(按键ID, 参数指针)
ebtn_btn_t static_buttons[] = {
EBTN_BUTTON_INIT(1, &key_param_normal), // KEY1, ID=1, 使用 'key_param_normal' 参数
EBTN_BUTTON_INIT(2, &key_param_normal), // KEY2, ID=2, 也使用 'key_param_normal' 参数
};
ebtn_btn_t
是结构体主要功能是KEY_IDtypedef struct ebtn_btn
{
uint16_t key_id; /*!< User defined custom argument for callback function purpose */
uint8_t flags; /*!< Private button flags management */
uint8_t event_mask; /*!< Private button event mask management */
ebtn_time_t time_change; /*!< Time in ms when button state got changed last time after valid
debounce */
ebtn_time_t time_state_change; /*!< Time in ms when button state got changed last time */
ebtn_time_t keepalive_last_time; /*!< Time in ms of last send keep alive event */
ebtn_time_t click_last_time; /*!< Time in ms of last successfully detected (not sent!) click event
*/
uint16_t keepalive_cnt; /*!< Number of keep alive events sent after successful on-press
detection. Value is reset after on-release */
uint16_t click_cnt; /*!< Number of consecutive clicks detected, respecting maximum timeout
between clicks */
const ebtn_btn_param_t *param;
} ebtn_btn_t;
static_buttons
是被定义的结构体数组 EBTN_BUTTON_INIT
是将KEY_ID绑定参数的初始化宏定义/* 1. 实现获取按键状态的回调函数 */
// 函数原型: uint8_t (*ebtn_get_state_fn)(struct ebtn_btn *btn);
uint8_t my_get_key_state(struct ebtn_btn *btn) {
// 根据传入的按钮实例中的 key_id 判断是哪个物理按键
switch (btn->key_id) {
case 1: // 请求读取 KEY1 的状态
// 假设 KEY1 接在 PB0,按下为低电平 (返回 1 代表按下)
return (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0) == GPIO_PIN_RESET);
case 2: // 请求读取 KEY2 的状态
// 假设 KEY2 接在 PB1,按下为低电平
return (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1) == GPIO_PIN_RESET);
// ... 可以添加更多按键的读取逻辑 ...
default:
// 对于库内部处理组合键等情况,或者未知的 key_id,安全起见返回 0 (未按下)
return 0;
}
// 注意:返回值 1 表示 "活动/按下",0 表示 "非活动/释放"
}
my_get_key_state
的原型是typedef uint8_t (*ebtn_get_state_fn)(struct ebtn_btn *btn)
定义了一个函数指针类型。在int ebtn_init(ebtn_btn_t *btns, uint16_t btns_cnt, ebtn_btn_combo_t *btns_combo, uint16_t btns_combo_cnt, ebtn_get_state_fn get_state_fn, ebtn_evt_fn evt_fn);
中被调用。ebtn_btn
和上述的``ebtn_btn_t`一样的结构体参数(key_id 等等)。switch (btn->key_id)
这段代码将硬件和KEY_ID联系起来,每一个case都会返回给函数0或者1./* 2. 实现处理按键事件的回调函数 */
// 函数原型: void (*ebtn_evt_fn)(struct ebtn_btn *btn, ebtn_evt_t evt);
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 事件时有意义)
// uint16_t kalive_cnt = ebtn_keepalive_get_count(btn); // 获取长按计数 (仅在 KEEPALIVE 事件时有意义)
// 调试打印 (可选)
// printf("Key ID: %d, Event: %d", key_id, evt);
// 根据事件类型进行处理
switch (evt) {
case EBTN_EVT_ONPRESS: // 按下事件 (消抖成功后触发一次)
// printf(" - Pressed\n");
// 可以在这里处理按下即触发的操作,比如点亮提示灯
if (key_id == 1) { /* Do something for KEY1 press */ }
break;
case EBTN_EVT_ONRELEASE: // 释放事件 (消抖成功后触发一次)
// printf(" - Released\n");
// 可以在这里处理释放时触发的操作
if (key_id == 1) { /* Do something for KEY1 release */ }
break;
case EBTN_EVT_ONCLICK: // 单击/连击事件 (在释放后,或达到最大连击数,或超时后触发)
// printf(" - Clicked (%d times)\n", click_cnt);
// --- 根据 key_id 和 click_cnt 执行不同操作 ---
if (key_id == 1) { // 如果是 KEY1 触发的 CLICK
if (click_cnt == 1) {
// KEY1 单击
// printf(" Action: KEY1 Single Click - Toggle LED1\n");
// HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin);
} else if (click_cnt == 2) {
// KEY1 双击
// printf(" Action: KEY1 Double Click - Switch Mode\n");
}
// ... 可以继续判断 3击, 4击 ...
} else if (key_id == 2) { // 如果是 KEY2 触发的 CLICK
if (click_cnt == 1) {
// KEY2 单击
// printf(" Action: KEY2 Single Click - Toggle LED2\n");
}
} else if (key_id == 101) { // 如果是组合键 (KEY1+KEY2) 触发的 CLICK
if (click_cnt == 1) {
// 组合键单击
// printf(" Action: Combo Key 101 Single Click - Reset System\n");
}
}
break;
case EBTN_EVT_KEEPALIVE: // 保持活动/长按事件 (按下持续时间超过阈值后,按周期触发)
// printf(" - Keep Alive (Long Press, Count: %d)\n", kalive_cnt);
if (key_id == 1) {
// KEY1 长按
// printf(" Action: KEY1 Long Press - Increase Volume\n");
}
break;
default: // 未知事件 (理论上不应发生)
// printf(" - Unknown Event\n");
break;
}
}
ebtn_evt_t
是一个结构体他的代码如下typedef enum
{
EBTN_EVT_ONPRESS = 0x00, /*!< On press event - sent when valid press is detected */
EBTN_EVT_ONRELEASE, /*!< On release event - sent when valid release event is detected (from
active to inactive) */
EBTN_EVT_ONCLICK, /*!< On Click event - sent when valid sequence of on-press and on-release
events occurs */
EBTN_EVT_KEEPALIVE, /*!< Keep alive event - sent periodically when button is active */
} ebtn_evt_t;
参数用于判断事件为什么(按下,松开,连击,持续按压)
EBTN_EVT_ONPRESS
:表示按钮被按下的事件。
EBTN_EVT_ONRELEASE
:表示按钮被释放的事件(从按下状态恢复到未按下状态)。
EBTN_EVT_ONCLICK
:表示一次完整的点击事件(通常是按下后释放的组合)。
EBTN_EVT_KEEPALIVE
:表示按钮在按下状态下周期性发送的“保持活跃”事件。
uint16_t click_cnt = ebtn_click_get_count(btn);
// 获取连击次数 (仅在 ONCLICK 事件时有意义)void app_ebtn_init(void)
{
ebtn_init(btns, EBTN_ARRAY_SIZE(btns), NULL, 0, prv_btn_get_state, prv_btn_event);
}
ebtn_init(btns, EBTN_ARRAY_SIZE(btns), NULL, 0, prv_btn_get_state, prv_btn_event);
初始化ebtn库,上面的参数设置。
在int ebtn_init(ebtn_btn_t *btns, uint16_t btns_cnt, ebtn_btn_combo_t *btns_combo, uint16_t btns_combo_cnt, ebtn_get_state_fn get_state_fn, ebtn_evt_fn evt_fn);
是它的原型函数。
ebtn_btn_t *btns
: 指向按钮配置数组,定义需要管理的按钮。
uint16_t btns_cnt
: 按钮数组的长度(按钮数量)。
ebtn_btn_combo_t *btns_combo
: 指向组合键配置数组,定义支持的按钮组合。
uint16_t btns_combo_cnt
: 组合键数组的长度(组合键数量)。
ebtn_get_state_fn get_state_fn
: 函数指针,指定如何获取按钮状态(例如读取 GPIO)。
ebtn_evt_fn evt_fn
: 函数指针,指定如何处理按钮事件(例如按下、释放、点击)。
void btn_task(void)
{
ebtn_process(HAL_GetTick());
}
while (1) {
// *** 在主循环中调用 ebtn 处理函数 ***
// 注意:需要确保主循环执行频率足够快且稳定,否则会影响按键检测精度
ebtn_process(HAL_GetTick());
// ... 其他主循环任务 ...
// 比如检查事件回调函数设置的标志位并执行相应操作
}
ebtn
库内部的所有计时(去抖、单击超时、长按周期)都依赖于你通过 ebtn_process
传入的当前时间。如果调用间隔不规律或太长,会导致时间判断错误,按键事件也就无法被正确识别了。注意:组合键的配置必须在 ebtn_init
成功调用之后进行,因为它需要库内部已经知道了所有普通按键及其内部索引
/* 3. 定义静态组合按键列表 (可选) */
// 宏: EBTN_BUTTON_COMBO_INIT(按键ID, 参数指针)
ebtn_btn_combo_t static_combos[] = {
// 假设 KEY1+KEY2 组合键
EBTN_BUTTON_COMBO_INIT(101, &key_param_normal), // 组合键, ID=101 (必须与普通按键ID不同)
};
与上面定义的单个按键类似,注意名不一样,与上文单个结构体搭配构成组合按键 ,下面是原函数
typedef struct ebtn_btn_combo
{
BIT_ARRAY_DEFINE(comb_key, EBTN_MAX_KEYNUM); /*!< select key index - `1` means active, `0` means inactive */
ebtn_btn_t btn;//通过单个按键进行组合
} ebtn_btn_combo_t;
EBTN_BUTTON_COMBO_INIT(101, &key_param_normal)
// 组合键, ID=101 (必须与普通按键ID不同)
int key1_index = ebtn_get_btn_index_by_key_id(1); // 获取 KEY1 (ID=1) 的内部索引
int key2_index = ebtn_get_btn_index_by_key_id(2); // 获取 KEY2 (ID=2) 的内部索引
ebtn_combo_btn_add_btn_by_idx(&static_combos[0], key1_index); // 将 KEY1 添加到组合键
ebtn_combo_btn_add_btn_by_idx(&static_combos[0], key2_index); // 将 KEY2 添加到组合键
void my_handle_key_event(struct ebtn_btn *btn, ebtn_evt_t evt)
{
uint16_t key_id = btn->key_id;
if(key_id==101)
{
''''//触发事件
}
}
//按键0+按键1 复制ucled
//按键0+按键2 粘贴ucled
//按键0+按键3 剪切ucled(复制完后全部熄灭)
ebtn_btn_combo_t static_combos[] = {
// 假设 KEY1+KEY2 组合键
EBTN_BUTTON_COMBO_INIT(101, &key_param_normal), // 组合键, ID=101 (必须与普通按键ID不同)
EBTN_BUTTON_COMBO_INIT(102, &key_param_normal),
EBTN_BUTTON_COMBO_INIT(103, &key_param_normal),
};
int key1_index = ebtn_get_btn_index_by_key_id(1); // 获取 KEY1 (ID=1) 的内部索引
int key2_index = ebtn_get_btn_index_by_key_id(2); // 获取 KEY2 (ID=2) 的内部索引
int key3_index = ebtn_get_btn_index_by_key_id(3);
int key4_index = ebtn_get_btn_index_by_key_id(4);
//复制ucled
ebtn_combo_btn_add_btn_by_idx(&static_combos[0], key1_index); // 将 KEY1 添加到组合键
ebtn_combo_btn_add_btn_by_idx(&static_combos[0], key2_index); // 将 KEY2 添加到组合键
//粘贴led
ebtn_combo_btn_add_btn_by_idx(&static_combos[1], key1_index); // 将 KEY1 添加到组合键
ebtn_combo_btn_add_btn_by_idx(&static_combos[1], key3_index); // 将 KEY2 添加到组合键
//剪切led
ebtn_combo_btn_add_btn_by_idx(&static_combos[2], key1_index); // 将 KEY1 添加到组合键
ebtn_combo_btn_add_btn_by_idx(&static_combos[2], key4_index); // 将 KEY2 添加到组合键
if(evt==EBTN_EVT_ONCLICK){
switch(key_id)
{
case 101://ucled复制
memcpy(copy_ucLed, ucLed, sizeof(ucLed) );
// for(int i=0;i<6;i++)
// {
// copy_ucLed[i]=ucLed[i];
// }
break;
case 102://ucled粘贴
memcpy(ucLed, copy_ucLed, sizeof(copy_ucLed) );
// for(int i=0;i<6;i++)
// {
// ucLed[i]=copy_ucLed[i];
// }
break;
case 103://ucled剪切
memcpy(copy_ucLed, ucLed, sizeof(ucLed) );
memcpy(ucLed, clear_ucLed, sizeof(copy_ucLed) );
// for(int i=0;i<6;i++)
// {
// copy_ucLed[i]=ucLed[i];
// }
// for(int i=0;i<6;i++)
// {
// ucLed[i]=clear_ucLed[i];
// }
break;
}
}