Repository files navigation

FlexibleButton

FlexibleButton 是一个基于标准 C 语言的小巧灵活的按键处理库,支持单击、连击、短按、长按、自动消抖,可以自由设置组合按键,可用于中断和低功耗场景。

该按键库解耦了具体的按键硬件结构,理论上支持轻触按键与自锁按键,并可以无限扩展按键数量。另外,FlexibleButton 使用扫描的方式一次性读取所有所有的按键状态,然后通过事件回调机制上报按键事件。核心的按键扫描代码仅有三行,没错,就是经典的 三行按键扫描算法。使用 C 语言标准库 API 编写,也使得该按键库可以无缝兼容任意的处理器平台,并且支持任意 OS 和 non-OS(裸机编程)。

获取

Git 方式

git clone https://github.com/murphyzhao/FlexibleButton.git

RT-Thread menuconfig 方式

RT-Thread online packages --->
miscellaneous packages --->
[*] FlexibleButton: Small and flexible button driver --->
[*] Enable flexible button demo
version (latest) --->

配置完成后,输入 pkgs --update 下载软件包。

资源统计

ARMCC -O0 优化的情况下,FlexibleButton 资源占用如下:

  • CODE:798 字节
  • RO DATA:0
  • RW DATA:13 字节
  • ZI DATA:0

快速体验

FlexibleButton 库中提供了一个测试例程 ./examples/demo_rtt_iotboard.c,该例程基于 RT-Thread OS 进行测试,硬件平台选择了 RT-Thread IoT Board Pandora v2.51 开发板。当然你可以选择使用其他的 OS,或者使用裸机测试,只需要移除 OS 相关的特性即可。

如果你使用自己的硬件平台,只需要将 FlexibleButton 库源码和例程加入你既有的工程下即可。

DEMO 程序说明

该示例程序可以直接在 RT-Thread stm32l475-atk-pandora BSP 中运行,可以在该 BSP 目录下,使用 menuconfig 获取本软件包。

确定用户按键

typedefenum
{
USER_BUTTON_0=0, // 对应 IoT Board 开发板的 PIN_KEY0USER_BUTTON_1, // 对应 IoT Board 开发板的 PIN_KEY1USER_BUTTON_2, // 对应 IoT Board 开发板的 PIN_KEY2USER_BUTTON_3, // 对应 IoT Board 开发板的 PIN_WK_UPUSER_BUTTON_MAX
} user_button_t;
staticflex_button_tuser_button[USER_BUTTON_MAX];

上述代码定义了 4 个按键,数据结构存储在 user_button 数组中。

程序入口

intflex_button_main(void)
{
rt_thread_ttid=RT_NULL;
user_button_init();
/* 创建按键扫描线程 flex_btn,线程栈 1024 byte,优先级 10 */tid=rt_thread_create("flex_btn", button_scan, RT_NULL, 1024, 10, 10);
if(tid!=RT_NULL)
{
rt_thread_startup(tid);
}
return0;
}
/* 使用 RT-Thread 的自动初始化 */INIT_APP_EXPORT(flex_button_main);

如上代码所示,首先使用 user_button_init(); 初始化用户按键硬件,该步骤将用户按键绑定到 FlexibleButton 库。然后,使用 RT-Thread 的 INIT_APP_EXPORT 接口导出为上电自动初始化,创建了一个 “flex_btn” 名字的按键扫描线程,线程里扫描检查按键事件。

按键初始化代码

user_button_init(); 初始化代码如下所示:

staticvoiduser_button_init(void)
{
inti;
/* 初始化按键数据结构 */rt_memset(&user_button[0], 0x0, sizeof(user_button));
/* 初始化 IoT Board 按键引脚,使用 rt-thread PIN 设备框架 */rt_pin_mode(PIN_KEY0, PIN_MODE_INPUT_PULLUP); /* 设置 GPIO 为上拉输入模式 */rt_pin_mode(PIN_KEY1, PIN_MODE_INPUT_PULLUP); /* 设置 GPIO 为上拉输入模式 */rt_pin_mode(PIN_KEY2, PIN_MODE_INPUT_PULLUP); /* 设置 GPIO 为上拉输入模式 */rt_pin_mode(PIN_WK_UP, PIN_MODE_INPUT_PULLDOWN); /* 设置 GPIO 为下拉输入模式 */for (i=0; i<USER_BUTTON_MAX; i++)
{
user_button[i].id=i;
user_button[i].usr_button_read=common_btn_read;
user_button[i].cb=common_btn_evt_cb;
user_button[i].pressed_logic_level=0;
user_button[i].short_press_start_tick=FLEX_MS_TO_SCAN_CNT(1500);
user_button[i].long_press_start_tick=FLEX_MS_TO_SCAN_CNT(3000);
user_button[i].long_hold_start_tick=FLEX_MS_TO_SCAN_CNT(4500);
if (i==USER_BUTTON_3)
{
user_button[USER_BUTTON_3].pressed_logic_level=1;
}
flex_button_register(&user_button[i]);
}
}

核心的配置如下:

配置项说明
id按键编号
usr_button_read设置按键读值回调函数
cb设置按键事件回调函数
pressed_logic_level设置按键按下时的逻辑电平
short_press_start_tick短按起始 tick,使用 FLEX_MS_TO_SCAN_CNT 宏转化为扫描次数
long_press_start_tick长按起始 tick,使用 FLEX_MS_TO_SCAN_CNT 宏转化为扫描次数
long_hold_start_tick超长按起始 tick,使用 FLEX_MS_TO_SCAN_CNT 宏转化为扫描次数

注意,short_press_start_tick、long_press_start_tick 和 long_hold_start_tick 必须使用 FLEX_MS_TO_SCAN_CNT 将毫秒时间转化为扫描次数。

user_button[i].short_press_start_tick = FLEX_MS_TO_SCAN_CNT(1500); 表示按键按下开始计时,1500 ms 后按键依旧是按下状态的话,就断定为短按开始。

事件处理代码

staticvoidcommon_btn_evt_cb(void*arg)
{
flex_button_t*btn= (flex_button_t*)arg;
rt_kprintf("id: [%d - %s] event: [%d - %30s] repeat: %d\n", btn->id, enum_btn_id_string[btn->id],
btn->event, enum_event_string[btn->event],
btn->click_cnt);
if (flex_button_event_read(&user_button[USER_BUTTON_0]) ==flex_button_event_read(&user_button[USER_BUTTON_1]) ==FLEX_BTN_PRESS_CLICK)
{
rt_kprintf("[combination]: button 0 and button 1\n");
}
}

示例代码中,将所有的按键事件回调均绑定到 common_btn_evt_cb 函数,在该函数中打印了按键 ID 和按键事件,以及按键连击次数,并演示了如何使用组合按键。

FlexibleButton 代码说明

按键事件定义

按键事件的定义并没有使用 Windows 驱动上的定义,主要是方便嵌入式设备中的应用场景(也可能是我理解的偏差),按键事件定义如下:

typedefenum
{
FLEX_BTN_PRESS_DOWN=0, // 按下事件FLEX_BTN_PRESS_CLICK, // 单击事件FLEX_BTN_PRESS_DOUBLE_CLICK, // 双击事件FLEX_BTN_PRESS_REPEAT_CLICK, // 连击事件,使用 flex_button_t 中的 click_cnt 断定连击次数FLEX_BTN_PRESS_SHORT_START, // 短按开始事件FLEX_BTN_PRESS_SHORT_UP, // 短按抬起事件FLEX_BTN_PRESS_LONG_START, // 长按开始事件FLEX_BTN_PRESS_LONG_UP, // 长按抬起事件FLEX_BTN_PRESS_LONG_HOLD, // 长按保持事件FLEX_BTN_PRESS_LONG_HOLD_UP, // 长按保持的抬起事件FLEX_BTN_PRESS_MAX,
FLEX_BTN_PRESS_NONE,
} flex_button_event_t;

其中 FLEX_BTN_PRESS_LONG_HOLD 事件可以用来实现长按累加的应用场景。

按键数据结构

typedefstructflex_button
{
structflex_button*next;
uint8_t (*usr_button_read)(void*);
flex_button_response_callbackcb;
uint16_tscan_cnt;
uint16_tclick_cnt;
uint16_tmax_multiple_clicks_interval;
uint16_tdebounce_tick;
uint16_tshort_press_start_tick;
uint16_tlong_press_start_tick;
uint16_tlong_hold_start_tick;
uint8_tid;
uint8_tpressed_logic_level : 1;
uint8_tevent : 4;
uint8_tstatus : 3;
} flex_button_t;
序号数据成员是否需要用户初始化说明
1next按键库使用单向链表串起所有的按键
2usr_button_read用户设备的按键引脚电平读取函数,重要
3cb设置按键事件回调,用于应用层对按键事件的分类处理
4scan_cnt用于记录扫描次数,按键按下是开始从零计数
5click_cnt记录单击次数,用于判定单击、连击
6max_multiple_clicks_interval连击间隙,用于判定是否结束连击计数,有默认值 MAX_MULTIPLE_CLICKS_INTERVAL
7debounce_tick消抖时间,暂未使用,依靠扫描间隙进行消抖
8short_press_start_tick设置短按事件触发的起始 tick
9long_press_start_tick设置长按事件触发的起始 tick
10long_hold_start_tick设置长按保持事件触发的起始 tick
11id当多个按键使用同一个回调函数时,用于断定属于哪个按键
12pressed_logic_level设置按键按下的逻辑电平。1:标识按键按下的时候为高电平;0:标识按键按下的时候未低电平,重要
13event用于记录当前按键事件
14status用于记录当前按键的状态,用于内部状态机

注意,在使用 max_multiple_clicks_intervaldebounce_tickshort_press_start_ticklong_press_start_ticklong_hold_start_tick 的时候,注意需要使用宏 **FLEX_MS_TO_SCAN_CNT(ms)** 将毫秒值转换为扫描次数。因为按键库基于扫描次数运转。示例如下:

user_button[1].short_press_start_tick = FLEX_MS_TO_SCAN_CNT(1500); // 1500 毫秒

上述代码表示:表示按键按下后开始计时,1500ms 的时候,按键依旧按下,则断定为短按开始,并上报 FLEX_BTN_PRESS_SHORT_START 事件。

按键注册接口

使用该接口注册一个用户按键,入参为一个 flex_button_t 结构体实例的地址。

int8_tflex_button_register(flex_button_t*button);

按键事件读取接口

使用该接口获取指定按键的事件。

flex_button_event_tflex_button_event_read(flex_button_t*button);

按键扫描接口

按键扫描的核心函数,需要放到应用程序中定时扫描,扫描间隔建议 20 毫秒。

voidflex_button_scan(void);

注意事项

  • 阻塞问题

    因为按键事件回调函数以及按键键值读取函数是在按键扫描的过程中执行的,因此请不要在这类函数中使用阻塞接口,不要进行延时操作。

  • 按键扫描函数栈需求

    按键扫描函数本身对栈的需求小于 300 字节,但是按键事件回调函数和按键键值读取函数都是在按键扫描函数的上下文中执行的,请格外关心按键事件回调函数与按键键值读取函数对栈空间的需求。

其它

关于低功耗

本按键库是通过不间断扫描的方式来检查按键状态,因此会一直占用 CPU 资源,这对低功耗应用场景是不友好的。为了降低正常工作模式下的功耗,建议合理配置扫描周期(5ms - 20ms),扫描间隙里 CPU 可以进入轻度睡眠。

该按键库不在底层实现低功耗处理,应用层可以根据自己的功耗模式灵活处理,通常会有以下两种方式:

  1. 进入低功耗前,挂起按键扫描线程;退出低功耗后,唤醒按键扫描。
  2. 增加按键中断模式,所有的按键中断来,就触发一次按键扫描,以确认所有的按键状态。

低功耗相关的探讨参考 issue 1 中的讨论。

关于按键中断模式

由于该按键库一次扫描可以确定所有的按键状态,因此可以将所有的按键中断通过 “” 的方式转化为一个中断,然后在中断处理函数中执行一次按键扫描。

中断 “” 的方式可以通过硬件来完成,也可以通过软件来完成。

硬件方式,需要使用一个 或门 芯片,多个输入条件转化为一个输出条件,然后通过一个外部中断即可完成所有按键的中断方式检测。

软件方式,需要为每一个按键配置为中断触发模式,然后在每一个按键中断的中断处理函数中执行按键扫描。

为了在降低中断处理函数中执行按键扫描带来的时延,可以通过信号量的方式来异步处理,仅在中断处理函数中释放一个按键扫描的信号量,然后在按键扫描线程中监测该信号量。

关于组合按键

该按键库仅做了底层的按键扫描处理,一次扫描可以确定所有的按键状态,并上报对应的按键事件,如果需要支持组合按键,请再封一层,根据按键库返回的事件封装需要的组合按键。示例程序提供了简单的实现。

关于矩阵键盘

不管你的矩阵键盘是通过什么通信方式获取按键状态的,只要你将读取按键状态的函数对接到 Flexible_button 数据结构中的 uint8_t (*usr_button_read)(void*); 函数上即可。

参考 issue 2 中的讨论。

问题和建议

如果有什么问题或者建议欢迎提交 Issue 进行讨论。

维护

感谢

感谢所有一起探讨的朋友,感谢所有使用 flexible_button 的朋友,感谢你们的 Star 和 Fork,谢谢你们的支持。

友情链接

About

灵活的按键处理库(Flexible Button)| 按键驱动 | 支持单击、双击、连击、长按、自动消抖 | 灵活适配中断和低功耗 | 按需实现组合按键

Topics

Resources

Stars

950 stars

Watchers

26 watching

Forks

Releases

Sponsor this project

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

FlexibleButton

FlexibleButton 是一个基于标准 C 语言的小巧灵活的按键处理库,支持单击、连击、短按、长按、自动消抖,可以自由设置组合按键,可用于中断和低功耗场景。

该按键库解耦了具体的按键硬件结构,理论上支持轻触按键与自锁按键,并可以无限扩展按键数量。另外,FlexibleButton 使用扫描的方式一次性读取所有所有的按键状态,然后通过事件回调机制上报按键事件。核心的按键扫描代码仅有三行,没错,就是经典的 三行按键扫描算法。使用 C 语言标准库 API 编写,也使得该按键库可以无缝兼容任意的处理器平台,并且支持任意 OS 和 non-OS(裸机编程)。

获取

Git 方式

git clone https://github.com/murphyzhao/FlexibleButton.git

RT-Thread menuconfig 方式

RT-Thread online packages --->
miscellaneous packages --->
[*] FlexibleButton: Small and flexible button driver --->
[*] Enable flexible button demo
version (latest) --->

配置完成后,输入 pkgs --update 下载软件包。

资源统计

ARMCC -O0 优化的情况下,FlexibleButton 资源占用如下:

  • CODE:798 字节
  • RO DATA:0
  • RW DATA:13 字节
  • ZI DATA:0

快速体验

FlexibleButton 库中提供了一个测试例程 ./examples/demo_rtt_iotboard.c,该例程基于 RT-Thread OS 进行测试,硬件平台选择了 RT-Thread IoT Board Pandora v2.51 开发板。当然你可以选择使用其他的 OS,或者使用裸机测试,只需要移除 OS 相关的特性即可。

如果你使用自己的硬件平台,只需要将 FlexibleButton 库源码和例程加入你既有的工程下即可。

DEMO 程序说明

该示例程序可以直接在 RT-Thread stm32l475-atk-pandora BSP 中运行,可以在该 BSP 目录下,使用 menuconfig 获取本软件包。

确定用户按键

typedefenum
{
USER_BUTTON_0=0, // 对应 IoT Board 开发板的 PIN_KEY0USER_BUTTON_1, // 对应 IoT Board 开发板的 PIN_KEY1USER_BUTTON_2, // 对应 IoT Board 开发板的 PIN_KEY2USER_BUTTON_3, // 对应 IoT Board 开发板的 PIN_WK_UPUSER_BUTTON_MAX
} user_button_t;
staticflex_button_tuser_button[USER_BUTTON_MAX];

上述代码定义了 4 个按键,数据结构存储在 user_button 数组中。

程序入口

intflex_button_main(void)
{
rt_thread_ttid=RT_NULL;
user_button_init();
/* 创建按键扫描线程 flex_btn,线程栈 1024 byte,优先级 10 */tid=rt_thread_create("flex_btn", button_scan, RT_NULL, 1024, 10, 10);
if(tid!=RT_NULL)
{
rt_thread_startup(tid);
}
return0;
}
/* 使用 RT-Thread 的自动初始化 */INIT_APP_EXPORT(flex_button_main);

如上代码所示,首先使用 user_button_init(); 初始化用户按键硬件,该步骤将用户按键绑定到 FlexibleButton 库。然后,使用 RT-Thread 的 INIT_APP_EXPORT 接口导出为上电自动初始化,创建了一个 “flex_btn” 名字的按键扫描线程,线程里扫描检查按键事件。

按键初始化代码

user_button_init(); 初始化代码如下所示:

staticvoiduser_button_init(void)
{
inti;
/* 初始化按键数据结构 */rt_memset(&user_button[0], 0x0, sizeof(user_button));
/* 初始化 IoT Board 按键引脚,使用 rt-thread PIN 设备框架 */rt_pin_mode(PIN_KEY0, PIN_MODE_INPUT_PULLUP); /* 设置 GPIO 为上拉输入模式 */rt_pin_mode(PIN_KEY1, PIN_MODE_INPUT_PULLUP); /* 设置 GPIO 为上拉输入模式 */rt_pin_mode(PIN_KEY2, PIN_MODE_INPUT_PULLUP); /* 设置 GPIO 为上拉输入模式 */rt_pin_mode(PIN_WK_UP, PIN_MODE_INPUT_PULLDOWN); /* 设置 GPIO 为下拉输入模式 */for (i=0; i<USER_BUTTON_MAX; i++)
{
user_button[i].id=i;
user_button[i].usr_button_read=common_btn_read;
user_button[i].cb=common_btn_evt_cb;
user_button[i].pressed_logic_level=0;
user_button[i].short_press_start_tick=FLEX_MS_TO_SCAN_CNT(1500);
user_button[i].long_press_start_tick=FLEX_MS_TO_SCAN_CNT(3000);
user_button[i].long_hold_start_tick=FLEX_MS_TO_SCAN_CNT(4500);
if (i==USER_BUTTON_3)
{
user_button[USER_BUTTON_3].pressed_logic_level=1;
}
flex_button_register(&user_button[i]);
}
}

核心的配置如下:

配置项说明
id按键编号
usr_button_read设置按键读值回调函数
cb设置按键事件回调函数
pressed_logic_level设置按键按下时的逻辑电平
short_press_start_tick短按起始 tick,使用 FLEX_MS_TO_SCAN_CNT 宏转化为扫描次数
long_press_start_tick长按起始 tick,使用 FLEX_MS_TO_SCAN_CNT 宏转化为扫描次数
long_hold_start_tick超长按起始 tick,使用 FLEX_MS_TO_SCAN_CNT 宏转化为扫描次数

注意,short_press_start_tick、long_press_start_tick 和 long_hold_start_tick 必须使用 FLEX_MS_TO_SCAN_CNT 将毫秒时间转化为扫描次数。

user_button[i].short_press_start_tick = FLEX_MS_TO_SCAN_CNT(1500); 表示按键按下开始计时,1500 ms 后按键依旧是按下状态的话,就断定为短按开始。

事件处理代码

staticvoidcommon_btn_evt_cb(void*arg)
{
flex_button_t*btn= (flex_button_t*)arg;
rt_kprintf("id: [%d - %s] event: [%d - %30s] repeat: %d\n", btn->id, enum_btn_id_string[btn->id],
btn->event, enum_event_string[btn->event],
btn->click_cnt);
if (flex_button_event_read(&user_button[USER_BUTTON_0]) ==flex_button_event_read(&user_button[USER_BUTTON_1]) ==FLEX_BTN_PRESS_CLICK)
{
rt_kprintf("[combination]: button 0 and button 1\n");
}
}

示例代码中,将所有的按键事件回调均绑定到 common_btn_evt_cb 函数,在该函数中打印了按键 ID 和按键事件,以及按键连击次数,并演示了如何使用组合按键。

FlexibleButton 代码说明

按键事件定义

按键事件的定义并没有使用 Windows 驱动上的定义,主要是方便嵌入式设备中的应用场景(也可能是我理解的偏差),按键事件定义如下:

typedefenum
{
FLEX_BTN_PRESS_DOWN=0, // 按下事件FLEX_BTN_PRESS_CLICK, // 单击事件FLEX_BTN_PRESS_DOUBLE_CLICK, // 双击事件FLEX_BTN_PRESS_REPEAT_CLICK, // 连击事件,使用 flex_button_t 中的 click_cnt 断定连击次数FLEX_BTN_PRESS_SHORT_START, // 短按开始事件FLEX_BTN_PRESS_SHORT_UP, // 短按抬起事件FLEX_BTN_PRESS_LONG_START, // 长按开始事件FLEX_BTN_PRESS_LONG_UP, // 长按抬起事件FLEX_BTN_PRESS_LONG_HOLD, // 长按保持事件FLEX_BTN_PRESS_LONG_HOLD_UP, // 长按保持的抬起事件FLEX_BTN_PRESS_MAX,
FLEX_BTN_PRESS_NONE,
} flex_button_event_t;

其中 FLEX_BTN_PRESS_LONG_HOLD 事件可以用来实现长按累加的应用场景。

按键数据结构

typedefstructflex_button
{
structflex_button*next;
uint8_t (*usr_button_read)(void*);
flex_button_response_callbackcb;
uint16_tscan_cnt;
uint16_tclick_cnt;
uint16_tmax_multiple_clicks_interval;
uint16_tdebounce_tick;
uint16_tshort_press_start_tick;
uint16_tlong_press_start_tick;
uint16_tlong_hold_start_tick;
uint8_tid;
uint8_tpressed_logic_level : 1;
uint8_tevent : 4;
uint8_tstatus : 3;
} flex_button_t;
序号数据成员是否需要用户初始化说明
1next按键库使用单向链表串起所有的按键
2usr_button_read用户设备的按键引脚电平读取函数,重要
3cb设置按键事件回调,用于应用层对按键事件的分类处理
4scan_cnt用于记录扫描次数,按键按下是开始从零计数
5click_cnt记录单击次数,用于判定单击、连击
6max_multiple_clicks_interval连击间隙,用于判定是否结束连击计数,有默认值 MAX_MULTIPLE_CLICKS_INTERVAL
7debounce_tick消抖时间,暂未使用,依靠扫描间隙进行消抖
8short_press_start_tick设置短按事件触发的起始 tick
9long_press_start_tick设置长按事件触发的起始 tick
10long_hold_start_tick设置长按保持事件触发的起始 tick
11id当多个按键使用同一个回调函数时,用于断定属于哪个按键
12pressed_logic_level设置按键按下的逻辑电平。1:标识按键按下的时候为高电平;0:标识按键按下的时候未低电平,重要
13event用于记录当前按键事件
14status用于记录当前按键的状态,用于内部状态机

注意,在使用 max_multiple_clicks_intervaldebounce_tickshort_press_start_ticklong_press_start_ticklong_hold_start_tick 的时候,注意需要使用宏 **FLEX_MS_TO_SCAN_CNT(ms)** 将毫秒值转换为扫描次数。因为按键库基于扫描次数运转。示例如下:

user_button[1].short_press_start_tick = FLEX_MS_TO_SCAN_CNT(1500); // 1500 毫秒

上述代码表示:表示按键按下后开始计时,1500ms 的时候,按键依旧按下,则断定为短按开始,并上报 FLEX_BTN_PRESS_SHORT_START 事件。

按键注册接口

使用该接口注册一个用户按键,入参为一个 flex_button_t 结构体实例的地址。

int8_tflex_button_register(flex_button_t*button);

按键事件读取接口

使用该接口获取指定按键的事件。

flex_button_event_tflex_button_event_read(flex_button_t*button);

按键扫描接口

按键扫描的核心函数,需要放到应用程序中定时扫描,扫描间隔建议 20 毫秒。

voidflex_button_scan(void);

注意事项

  • 阻塞问题

    因为按键事件回调函数以及按键键值读取函数是在按键扫描的过程中执行的,因此请不要在这类函数中使用阻塞接口,不要进行延时操作。

  • 按键扫描函数栈需求

    按键扫描函数本身对栈的需求小于 300 字节,但是按键事件回调函数和按键键值读取函数都是在按键扫描函数的上下文中执行的,请格外关心按键事件回调函数与按键键值读取函数对栈空间的需求。

其它

关于低功耗

本按键库是通过不间断扫描的方式来检查按键状态,因此会一直占用 CPU 资源,这对低功耗应用场景是不友好的。为了降低正常工作模式下的功耗,建议合理配置扫描周期(5ms - 20ms),扫描间隙里 CPU 可以进入轻度睡眠。

该按键库不在底层实现低功耗处理,应用层可以根据自己的功耗模式灵活处理,通常会有以下两种方式:

  1. 进入低功耗前,挂起按键扫描线程;退出低功耗后,唤醒按键扫描。
  2. 增加按键中断模式,所有的按键中断来,就触发一次按键扫描,以确认所有的按键状态。

低功耗相关的探讨参考 issue 1 中的讨论。

关于按键中断模式

由于该按键库一次扫描可以确定所有的按键状态,因此可以将所有的按键中断通过 “” 的方式转化为一个中断,然后在中断处理函数中执行一次按键扫描。

中断 “” 的方式可以通过硬件来完成,也可以通过软件来完成。

硬件方式,需要使用一个 或门 芯片,多个输入条件转化为一个输出条件,然后通过一个外部中断即可完成所有按键的中断方式检测。

软件方式,需要为每一个按键配置为中断触发模式,然后在每一个按键中断的中断处理函数中执行按键扫描。

为了在降低中断处理函数中执行按键扫描带来的时延,可以通过信号量的方式来异步处理,仅在中断处理函数中释放一个按键扫描的信号量,然后在按键扫描线程中监测该信号量。

关于组合按键

该按键库仅做了底层的按键扫描处理,一次扫描可以确定所有的按键状态,并上报对应的按键事件,如果需要支持组合按键,请再封一层,根据按键库返回的事件封装需要的组合按键。示例程序提供了简单的实现。

关于矩阵键盘

不管你的矩阵键盘是通过什么通信方式获取按键状态的,只要你将读取按键状态的函数对接到 Flexible_button 数据结构中的 uint8_t (*usr_button_read)(void*); 函数上即可。

参考 issue 2 中的讨论。

问题和建议

如果有什么问题或者建议欢迎提交 Issue 进行讨论。

维护

感谢

感谢所有一起探讨的朋友,感谢所有使用 flexible_button 的朋友,感谢你们的 Star 和 Fork,谢谢你们的支持。

友情链接

About

灵活的按键处理库(Flexible Button)| 按键驱动 | 支持单击、双击、连击、长按、自动消抖 | 灵活适配中断和低功耗 | 按需实现组合按键

Topics

Resources

Stars

950 stars

Watchers

26 watching

Forks

Releases

Sponsor this project

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

FlexibleButton

FlexibleButton 是一个基于标准 C 语言的小巧灵活的按键处理库,支持单击、连击、短按、长按、自动消抖,可以自由设置组合按键,可用于中断和低功耗场景。

该按键库解耦了具体的按键硬件结构,理论上支持轻触按键与自锁按键,并可以无限扩展按键数量。另外,FlexibleButton 使用扫描的方式一次性读取所有所有的按键状态,然后通过事件回调机制上报按键事件。核心的按键扫描代码仅有三行,没错,就是经典的 三行按键扫描算法。使用 C 语言标准库 API 编写,也使得该按键库可以无缝兼容任意的处理器平台,并且支持任意 OS 和 non-OS(裸机编程)。

获取

Git 方式

git clone https://github.com/murphyzhao/FlexibleButton.git

RT-Thread menuconfig 方式

RT-Thread online packages --->
miscellaneous packages --->
[*] FlexibleButton: Small and flexible button driver --->
[*] Enable flexible button demo
version (latest) --->

配置完成后,输入 pkgs --update 下载软件包。

资源统计

ARMCC -O0 优化的情况下,FlexibleButton 资源占用如下:

  • CODE:798 字节
  • RO DATA:0
  • RW DATA:13 字节
  • ZI DATA:0

快速体验

FlexibleButton 库中提供了一个测试例程 ./examples/demo_rtt_iotboard.c,该例程基于 RT-Thread OS 进行测试,硬件平台选择了 RT-Thread IoT Board Pandora v2.51 开发板。当然你可以选择使用其他的 OS,或者使用裸机测试,只需要移除 OS 相关的特性即可。

如果你使用自己的硬件平台,只需要将 FlexibleButton 库源码和例程加入你既有的工程下即可。

DEMO 程序说明

该示例程序可以直接在 RT-Thread stm32l475-atk-pandora BSP 中运行,可以在该 BSP 目录下,使用 menuconfig 获取本软件包。

确定用户按键

typedefenum
{
USER_BUTTON_0=0, // 对应 IoT Board 开发板的 PIN_KEY0USER_BUTTON_1, // 对应 IoT Board 开发板的 PIN_KEY1USER_BUTTON_2, // 对应 IoT Board 开发板的 PIN_KEY2USER_BUTTON_3, // 对应 IoT Board 开发板的 PIN_WK_UPUSER_BUTTON_MAX
} user_button_t;
staticflex_button_tuser_button[USER_BUTTON_MAX];

上述代码定义了 4 个按键,数据结构存储在 user_button 数组中。

程序入口

intflex_button_main(void)
{
rt_thread_ttid=RT_NULL;
user_button_init();
/* 创建按键扫描线程 flex_btn,线程栈 1024 byte,优先级 10 */tid=rt_thread_create("flex_btn", button_scan, RT_NULL, 1024, 10, 10);
if(tid!=RT_NULL)
{
rt_thread_startup(tid);
}
return0;
}
/* 使用 RT-Thread 的自动初始化 */INIT_APP_EXPORT(flex_button_main);

如上代码所示,首先使用 user_button_init(); 初始化用户按键硬件,该步骤将用户按键绑定到 FlexibleButton 库。然后,使用 RT-Thread 的 INIT_APP_EXPORT 接口导出为上电自动初始化,创建了一个 “flex_btn” 名字的按键扫描线程,线程里扫描检查按键事件。

按键初始化代码

user_button_init(); 初始化代码如下所示:

staticvoiduser_button_init(void)
{
inti;
/* 初始化按键数据结构 */rt_memset(&user_button[0], 0x0, sizeof(user_button));
/* 初始化 IoT Board 按键引脚,使用 rt-thread PIN 设备框架 */rt_pin_mode(PIN_KEY0, PIN_MODE_INPUT_PULLUP); /* 设置 GPIO 为上拉输入模式 */rt_pin_mode(PIN_KEY1, PIN_MODE_INPUT_PULLUP); /* 设置 GPIO 为上拉输入模式 */rt_pin_mode(PIN_KEY2, PIN_MODE_INPUT_PULLUP); /* 设置 GPIO 为上拉输入模式 */rt_pin_mode(PIN_WK_UP, PIN_MODE_INPUT_PULLDOWN); /* 设置 GPIO 为下拉输入模式 */for (i=0; i<USER_BUTTON_MAX; i++)
{
user_button[i].id=i;
user_button[i].usr_button_read=common_btn_read;
user_button[i].cb=common_btn_evt_cb;
user_button[i].pressed_logic_level=0;
user_button[i].short_press_start_tick=FLEX_MS_TO_SCAN_CNT(1500);
user_button[i].long_press_start_tick=FLEX_MS_TO_SCAN_CNT(3000);
user_button[i].long_hold_start_tick=FLEX_MS_TO_SCAN_CNT(4500);
if (i==USER_BUTTON_3)
{
user_button[USER_BUTTON_3].pressed_logic_level=1;
}
flex_button_register(&user_button[i]);
}
}

核心的配置如下:

配置项说明
id按键编号
usr_button_read设置按键读值回调函数
cb设置按键事件回调函数
pressed_logic_level设置按键按下时的逻辑电平
short_press_start_tick短按起始 tick,使用 FLEX_MS_TO_SCAN_CNT 宏转化为扫描次数
long_press_start_tick长按起始 tick,使用 FLEX_MS_TO_SCAN_CNT 宏转化为扫描次数
long_hold_start_tick超长按起始 tick,使用 FLEX_MS_TO_SCAN_CNT 宏转化为扫描次数

注意,short_press_start_tick、long_press_start_tick 和 long_hold_start_tick 必须使用 FLEX_MS_TO_SCAN_CNT 将毫秒时间转化为扫描次数。

user_button[i].short_press_start_tick = FLEX_MS_TO_SCAN_CNT(1500); 表示按键按下开始计时,1500 ms 后按键依旧是按下状态的话,就断定为短按开始。

事件处理代码

staticvoidcommon_btn_evt_cb(void*arg)
{
flex_button_t*btn= (flex_button_t*)arg;
rt_kprintf("id: [%d - %s] event: [%d - %30s] repeat: %d\n", btn->id, enum_btn_id_string[btn->id],
btn->event, enum_event_string[btn->event],
btn->click_cnt);
if (flex_button_event_read(&user_button[USER_BUTTON_0]) ==flex_button_event_read(&user_button[USER_BUTTON_1]) ==FLEX_BTN_PRESS_CLICK)
{
rt_kprintf("[combination]: button 0 and button 1\n");
}
}

示例代码中,将所有的按键事件回调均绑定到 common_btn_evt_cb 函数,在该函数中打印了按键 ID 和按键事件,以及按键连击次数,并演示了如何使用组合按键。

FlexibleButton 代码说明

按键事件定义

按键事件的定义并没有使用 Windows 驱动上的定义,主要是方便嵌入式设备中的应用场景(也可能是我理解的偏差),按键事件定义如下:

typedefenum
{
FLEX_BTN_PRESS_DOWN=0, // 按下事件FLEX_BTN_PRESS_CLICK, // 单击事件FLEX_BTN_PRESS_DOUBLE_CLICK, // 双击事件FLEX_BTN_PRESS_REPEAT_CLICK, // 连击事件,使用 flex_button_t 中的 click_cnt 断定连击次数FLEX_BTN_PRESS_SHORT_START, // 短按开始事件FLEX_BTN_PRESS_SHORT_UP, // 短按抬起事件FLEX_BTN_PRESS_LONG_START, // 长按开始事件FLEX_BTN_PRESS_LONG_UP, // 长按抬起事件FLEX_BTN_PRESS_LONG_HOLD, // 长按保持事件FLEX_BTN_PRESS_LONG_HOLD_UP, // 长按保持的抬起事件FLEX_BTN_PRESS_MAX,
FLEX_BTN_PRESS_NONE,
} flex_button_event_t;

其中 FLEX_BTN_PRESS_LONG_HOLD 事件可以用来实现长按累加的应用场景。

按键数据结构

typedefstructflex_button
{
structflex_button*next;
uint8_t (*usr_button_read)(void*);
flex_button_response_callbackcb;
uint16_tscan_cnt;
uint16_tclick_cnt;
uint16_tmax_multiple_clicks_interval;
uint16_tdebounce_tick;
uint16_tshort_press_start_tick;
uint16_tlong_press_start_tick;
uint16_tlong_hold_start_tick;
uint8_tid;
uint8_tpressed_logic_level : 1;
uint8_tevent : 4;
uint8_tstatus : 3;
} flex_button_t;
序号数据成员是否需要用户初始化说明
1next按键库使用单向链表串起所有的按键
2usr_button_read用户设备的按键引脚电平读取函数,重要
3cb设置按键事件回调,用于应用层对按键事件的分类处理
4scan_cnt用于记录扫描次数,按键按下是开始从零计数
5click_cnt记录单击次数,用于判定单击、连击
6max_multiple_clicks_interval连击间隙,用于判定是否结束连击计数,有默认值 MAX_MULTIPLE_CLICKS_INTERVAL
7debounce_tick消抖时间,暂未使用,依靠扫描间隙进行消抖
8short_press_start_tick设置短按事件触发的起始 tick
9long_press_start_tick设置长按事件触发的起始 tick
10long_hold_start_tick设置长按保持事件触发的起始 tick
11id当多个按键使用同一个回调函数时,用于断定属于哪个按键
12pressed_logic_level设置按键按下的逻辑电平。1:标识按键按下的时候为高电平;0:标识按键按下的时候未低电平,重要
13event用于记录当前按键事件
14status用于记录当前按键的状态,用于内部状态机

注意,在使用 max_multiple_clicks_intervaldebounce_tickshort_press_start_ticklong_press_start_ticklong_hold_start_tick 的时候,注意需要使用宏 **FLEX_MS_TO_SCAN_CNT(ms)** 将毫秒值转换为扫描次数。因为按键库基于扫描次数运转。示例如下:

user_button[1].short_press_start_tick = FLEX_MS_TO_SCAN_CNT(1500); // 1500 毫秒

上述代码表示:表示按键按下后开始计时,1500ms 的时候,按键依旧按下,则断定为短按开始,并上报 FLEX_BTN_PRESS_SHORT_START 事件。

按键注册接口

使用该接口注册一个用户按键,入参为一个 flex_button_t 结构体实例的地址。

int8_tflex_button_register(flex_button_t*button);

按键事件读取接口

使用该接口获取指定按键的事件。

flex_button_event_tflex_button_event_read(flex_button_t*button);

按键扫描接口

按键扫描的核心函数,需要放到应用程序中定时扫描,扫描间隔建议 20 毫秒。

voidflex_button_scan(void);

注意事项

  • 阻塞问题

    因为按键事件回调函数以及按键键值读取函数是在按键扫描的过程中执行的,因此请不要在这类函数中使用阻塞接口,不要进行延时操作。

  • 按键扫描函数栈需求

    按键扫描函数本身对栈的需求小于 300 字节,但是按键事件回调函数和按键键值读取函数都是在按键扫描函数的上下文中执行的,请格外关心按键事件回调函数与按键键值读取函数对栈空间的需求。

其它

关于低功耗

本按键库是通过不间断扫描的方式来检查按键状态,因此会一直占用 CPU 资源,这对低功耗应用场景是不友好的。为了降低正常工作模式下的功耗,建议合理配置扫描周期(5ms - 20ms),扫描间隙里 CPU 可以进入轻度睡眠。

该按键库不在底层实现低功耗处理,应用层可以根据自己的功耗模式灵活处理,通常会有以下两种方式:

  1. 进入低功耗前,挂起按键扫描线程;退出低功耗后,唤醒按键扫描。
  2. 增加按键中断模式,所有的按键中断来,就触发一次按键扫描,以确认所有的按键状态。

低功耗相关的探讨参考 issue 1 中的讨论。

关于按键中断模式

由于该按键库一次扫描可以确定所有的按键状态,因此可以将所有的按键中断通过 “” 的方式转化为一个中断,然后在中断处理函数中执行一次按键扫描。

中断 “” 的方式可以通过硬件来完成,也可以通过软件来完成。

硬件方式,需要使用一个 或门 芯片,多个输入条件转化为一个输出条件,然后通过一个外部中断即可完成所有按键的中断方式检测。

软件方式,需要为每一个按键配置为中断触发模式,然后在每一个按键中断的中断处理函数中执行按键扫描。

为了在降低中断处理函数中执行按键扫描带来的时延,可以通过信号量的方式来异步处理,仅在中断处理函数中释放一个按键扫描的信号量,然后在按键扫描线程中监测该信号量。

关于组合按键

该按键库仅做了底层的按键扫描处理,一次扫描可以确定所有的按键状态,并上报对应的按键事件,如果需要支持组合按键,请再封一层,根据按键库返回的事件封装需要的组合按键。示例程序提供了简单的实现。

关于矩阵键盘

不管你的矩阵键盘是通过什么通信方式获取按键状态的,只要你将读取按键状态的函数对接到 Flexible_button 数据结构中的 uint8_t (*usr_button_read)(void*); 函数上即可。

参考 issue 2 中的讨论。

问题和建议

如果有什么问题或者建议欢迎提交 Issue 进行讨论。

维护

感谢

感谢所有一起探讨的朋友,感谢所有使用 flexible_button 的朋友,感谢你们的 Star 和 Fork,谢谢你们的支持。

友情链接

About

灵活的按键处理库(Flexible Button)| 按键驱动 | 支持单击、双击、连击、长按、自动消抖 | 灵活适配中断和低功耗 | 按需实现组合按键

Topics

Resources

Stars

950 stars

Watchers

26 watching

Forks

Releases

Sponsor this project

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

FlexibleButton

FlexibleButton 是一个基于标准 C 语言的小巧灵活的按键处理库,支持单击、连击、短按、长按、自动消抖,可以自由设置组合按键,可用于中断和低功耗场景。

该按键库解耦了具体的按键硬件结构,理论上支持轻触按键与自锁按键,并可以无限扩展按键数量。另外,FlexibleButton 使用扫描的方式一次性读取所有所有的按键状态,然后通过事件回调机制上报按键事件。核心的按键扫描代码仅有三行,没错,就是经典的 三行按键扫描算法。使用 C 语言标准库 API 编写,也使得该按键库可以无缝兼容任意的处理器平台,并且支持任意 OS 和 non-OS(裸机编程)。

获取

Git 方式

git clone https://github.com/murphyzhao/FlexibleButton.git

RT-Thread menuconfig 方式

RT-Thread online packages --->
miscellaneous packages --->
[*] FlexibleButton: Small and flexible button driver --->
[*] Enable flexible button demo
version (latest) --->

配置完成后,输入 pkgs --update 下载软件包。

资源统计

ARMCC -O0 优化的情况下,FlexibleButton 资源占用如下:

  • CODE:798 字节
  • RO DATA:0
  • RW DATA:13 字节
  • ZI DATA:0

快速体验

FlexibleButton 库中提供了一个测试例程 ./examples/demo_rtt_iotboard.c,该例程基于 RT-Thread OS 进行测试,硬件平台选择了 RT-Thread IoT Board Pandora v2.51 开发板。当然你可以选择使用其他的 OS,或者使用裸机测试,只需要移除 OS 相关的特性即可。

如果你使用自己的硬件平台,只需要将 FlexibleButton 库源码和例程加入你既有的工程下即可。

DEMO 程序说明

该示例程序可以直接在 RT-Thread stm32l475-atk-pandora BSP 中运行,可以在该 BSP 目录下,使用 menuconfig 获取本软件包。

确定用户按键

typedefenum
{
USER_BUTTON_0=0, // 对应 IoT Board 开发板的 PIN_KEY0USER_BUTTON_1, // 对应 IoT Board 开发板的 PIN_KEY1USER_BUTTON_2, // 对应 IoT Board 开发板的 PIN_KEY2USER_BUTTON_3, // 对应 IoT Board 开发板的 PIN_WK_UPUSER_BUTTON_MAX
} user_button_t;
staticflex_button_tuser_button[USER_BUTTON_MAX];

上述代码定义了 4 个按键,数据结构存储在 user_button 数组中。

程序入口

intflex_button_main(void)
{
rt_thread_ttid=RT_NULL;
user_button_init();
/* 创建按键扫描线程 flex_btn,线程栈 1024 byte,优先级 10 */tid=rt_thread_create("flex_btn", button_scan, RT_NULL, 1024, 10, 10);
if(tid!=RT_NULL)
{
rt_thread_startup(tid);
}
return0;
}
/* 使用 RT-Thread 的自动初始化 */INIT_APP_EXPORT(flex_button_main);

如上代码所示,首先使用 user_button_init(); 初始化用户按键硬件,该步骤将用户按键绑定到 FlexibleButton 库。然后,使用 RT-Thread 的 INIT_APP_EXPORT 接口导出为上电自动初始化,创建了一个 “flex_btn” 名字的按键扫描线程,线程里扫描检查按键事件。

按键初始化代码

user_button_init(); 初始化代码如下所示:

staticvoiduser_button_init(void)
{
inti;
/* 初始化按键数据结构 */rt_memset(&user_button[0], 0x0, sizeof(user_button));
/* 初始化 IoT Board 按键引脚,使用 rt-thread PIN 设备框架 */rt_pin_mode(PIN_KEY0, PIN_MODE_INPUT_PULLUP); /* 设置 GPIO 为上拉输入模式 */rt_pin_mode(PIN_KEY1, PIN_MODE_INPUT_PULLUP); /* 设置 GPIO 为上拉输入模式 */rt_pin_mode(PIN_KEY2, PIN_MODE_INPUT_PULLUP); /* 设置 GPIO 为上拉输入模式 */rt_pin_mode(PIN_WK_UP, PIN_MODE_INPUT_PULLDOWN); /* 设置 GPIO 为下拉输入模式 */for (i=0; i<USER_BUTTON_MAX; i++)
{
user_button[i].id=i;
user_button[i].usr_button_read=common_btn_read;
user_button[i].cb=common_btn_evt_cb;
user_button[i].pressed_logic_level=0;
user_button[i].short_press_start_tick=FLEX_MS_TO_SCAN_CNT(1500);
user_button[i].long_press_start_tick=FLEX_MS_TO_SCAN_CNT(3000);
user_button[i].long_hold_start_tick=FLEX_MS_TO_SCAN_CNT(4500);
if (i==USER_BUTTON_3)
{
user_button[USER_BUTTON_3].pressed_logic_level=1;
}
flex_button_register(&user_button[i]);
}
}

核心的配置如下:

配置项说明
id按键编号
usr_button_read设置按键读值回调函数
cb设置按键事件回调函数
pressed_logic_level设置按键按下时的逻辑电平
short_press_start_tick短按起始 tick,使用 FLEX_MS_TO_SCAN_CNT 宏转化为扫描次数
long_press_start_tick长按起始 tick,使用 FLEX_MS_TO_SCAN_CNT 宏转化为扫描次数
long_hold_start_tick超长按起始 tick,使用 FLEX_MS_TO_SCAN_CNT 宏转化为扫描次数

注意,short_press_start_tick、long_press_start_tick 和 long_hold_start_tick 必须使用 FLEX_MS_TO_SCAN_CNT 将毫秒时间转化为扫描次数。

user_button[i].short_press_start_tick = FLEX_MS_TO_SCAN_CNT(1500); 表示按键按下开始计时,1500 ms 后按键依旧是按下状态的话,就断定为短按开始。

事件处理代码

staticvoidcommon_btn_evt_cb(void*arg)
{
flex_button_t*btn= (flex_button_t*)arg;
rt_kprintf("id: [%d - %s] event: [%d - %30s] repeat: %d\n", btn->id, enum_btn_id_string[btn->id],
btn->event, enum_event_string[btn->event],
btn->click_cnt);
if (flex_button_event_read(&user_button[USER_BUTTON_0]) ==flex_button_event_read(&user_button[USER_BUTTON_1]) ==FLEX_BTN_PRESS_CLICK)
{
rt_kprintf("[combination]: button 0 and button 1\n");
}
}

示例代码中,将所有的按键事件回调均绑定到 common_btn_evt_cb 函数,在该函数中打印了按键 ID 和按键事件,以及按键连击次数,并演示了如何使用组合按键。

FlexibleButton 代码说明

按键事件定义

按键事件的定义并没有使用 Windows 驱动上的定义,主要是方便嵌入式设备中的应用场景(也可能是我理解的偏差),按键事件定义如下:

typedefenum
{
FLEX_BTN_PRESS_DOWN=0, // 按下事件FLEX_BTN_PRESS_CLICK, // 单击事件FLEX_BTN_PRESS_DOUBLE_CLICK, // 双击事件FLEX_BTN_PRESS_REPEAT_CLICK, // 连击事件,使用 flex_button_t 中的 click_cnt 断定连击次数FLEX_BTN_PRESS_SHORT_START, // 短按开始事件FLEX_BTN_PRESS_SHORT_UP, // 短按抬起事件FLEX_BTN_PRESS_LONG_START, // 长按开始事件FLEX_BTN_PRESS_LONG_UP, // 长按抬起事件FLEX_BTN_PRESS_LONG_HOLD, // 长按保持事件FLEX_BTN_PRESS_LONG_HOLD_UP, // 长按保持的抬起事件FLEX_BTN_PRESS_MAX,
FLEX_BTN_PRESS_NONE,
} flex_button_event_t;

其中 FLEX_BTN_PRESS_LONG_HOLD 事件可以用来实现长按累加的应用场景。

按键数据结构

typedefstructflex_button
{
structflex_button*next;
uint8_t (*usr_button_read)(void*);
flex_button_response_callbackcb;
uint16_tscan_cnt;
uint16_tclick_cnt;
uint16_tmax_multiple_clicks_interval;
uint16_tdebounce_tick;
uint16_tshort_press_start_tick;
uint16_tlong_press_start_tick;
uint16_tlong_hold_start_tick;
uint8_tid;
uint8_tpressed_logic_level : 1;
uint8_tevent : 4;
uint8_tstatus : 3;
} flex_button_t;
序号数据成员是否需要用户初始化说明
1next按键库使用单向链表串起所有的按键
2usr_button_read用户设备的按键引脚电平读取函数,重要
3cb设置按键事件回调,用于应用层对按键事件的分类处理
4scan_cnt用于记录扫描次数,按键按下是开始从零计数
5click_cnt记录单击次数,用于判定单击、连击
6max_multiple_clicks_interval连击间隙,用于判定是否结束连击计数,有默认值 MAX_MULTIPLE_CLICKS_INTERVAL
7debounce_tick消抖时间,暂未使用,依靠扫描间隙进行消抖
8short_press_start_tick设置短按事件触发的起始 tick
9long_press_start_tick设置长按事件触发的起始 tick
10long_hold_start_tick设置长按保持事件触发的起始 tick
11id当多个按键使用同一个回调函数时,用于断定属于哪个按键
12pressed_logic_level设置按键按下的逻辑电平。1:标识按键按下的时候为高电平;0:标识按键按下的时候未低电平,重要
13event用于记录当前按键事件
14status用于记录当前按键的状态,用于内部状态机

注意,在使用 max_multiple_clicks_intervaldebounce_tickshort_press_start_ticklong_press_start_ticklong_hold_start_tick 的时候,注意需要使用宏 **FLEX_MS_TO_SCAN_CNT(ms)** 将毫秒值转换为扫描次数。因为按键库基于扫描次数运转。示例如下:

user_button[1].short_press_start_tick = FLEX_MS_TO_SCAN_CNT(1500); // 1500 毫秒

上述代码表示:表示按键按下后开始计时,1500ms 的时候,按键依旧按下,则断定为短按开始,并上报 FLEX_BTN_PRESS_SHORT_START 事件。

按键注册接口

使用该接口注册一个用户按键,入参为一个 flex_button_t 结构体实例的地址。

int8_tflex_button_register(flex_button_t*button);

按键事件读取接口

使用该接口获取指定按键的事件。

flex_button_event_tflex_button_event_read(flex_button_t*button);

按键扫描接口

按键扫描的核心函数,需要放到应用程序中定时扫描,扫描间隔建议 20 毫秒。

voidflex_button_scan(void);

注意事项

  • 阻塞问题

    因为按键事件回调函数以及按键键值读取函数是在按键扫描的过程中执行的,因此请不要在这类函数中使用阻塞接口,不要进行延时操作。

  • 按键扫描函数栈需求

    按键扫描函数本身对栈的需求小于 300 字节,但是按键事件回调函数和按键键值读取函数都是在按键扫描函数的上下文中执行的,请格外关心按键事件回调函数与按键键值读取函数对栈空间的需求。

其它

关于低功耗

本按键库是通过不间断扫描的方式来检查按键状态,因此会一直占用 CPU 资源,这对低功耗应用场景是不友好的。为了降低正常工作模式下的功耗,建议合理配置扫描周期(5ms - 20ms),扫描间隙里 CPU 可以进入轻度睡眠。

该按键库不在底层实现低功耗处理,应用层可以根据自己的功耗模式灵活处理,通常会有以下两种方式:

  1. 进入低功耗前,挂起按键扫描线程;退出低功耗后,唤醒按键扫描。
  2. 增加按键中断模式,所有的按键中断来,就触发一次按键扫描,以确认所有的按键状态。

低功耗相关的探讨参考 issue 1 中的讨论。

关于按键中断模式

由于该按键库一次扫描可以确定所有的按键状态,因此可以将所有的按键中断通过 “” 的方式转化为一个中断,然后在中断处理函数中执行一次按键扫描。

中断 “” 的方式可以通过硬件来完成,也可以通过软件来完成。

硬件方式,需要使用一个 或门 芯片,多个输入条件转化为一个输出条件,然后通过一个外部中断即可完成所有按键的中断方式检测。

软件方式,需要为每一个按键配置为中断触发模式,然后在每一个按键中断的中断处理函数中执行按键扫描。

为了在降低中断处理函数中执行按键扫描带来的时延,可以通过信号量的方式来异步处理,仅在中断处理函数中释放一个按键扫描的信号量,然后在按键扫描线程中监测该信号量。

关于组合按键

该按键库仅做了底层的按键扫描处理,一次扫描可以确定所有的按键状态,并上报对应的按键事件,如果需要支持组合按键,请再封一层,根据按键库返回的事件封装需要的组合按键。示例程序提供了简单的实现。

关于矩阵键盘

不管你的矩阵键盘是通过什么通信方式获取按键状态的,只要你将读取按键状态的函数对接到 Flexible_button 数据结构中的 uint8_t (*usr_button_read)(void*); 函数上即可。

参考 issue 2 中的讨论。

问题和建议

如果有什么问题或者建议欢迎提交 Issue 进行讨论。

维护

感谢

感谢所有一起探讨的朋友,感谢所有使用 flexible_button 的朋友,感谢你们的 Star 和 Fork,谢谢你们的支持。

友情链接

About

灵活的按键处理库(Flexible Button)| 按键驱动 | 支持单击、双击、连击、长按、自动消抖 | 灵活适配中断和低功耗 | 按需实现组合按键

Topics

Resources

Stars

950 stars

Watchers

26 watching

Forks

Releases

Sponsor this project

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

FlexibleButton

FlexibleButton 是一个基于标准 C 语言的小巧灵活的按键处理库,支持单击、连击、短按、长按、自动消抖,可以自由设置组合按键,可用于中断和低功耗场景。

该按键库解耦了具体的按键硬件结构,理论上支持轻触按键与自锁按键,并可以无限扩展按键数量。另外,FlexibleButton 使用扫描的方式一次性读取所有所有的按键状态,然后通过事件回调机制上报按键事件。核心的按键扫描代码仅有三行,没错,就是经典的 三行按键扫描算法。使用 C 语言标准库 API 编写,也使得该按键库可以无缝兼容任意的处理器平台,并且支持任意 OS 和 non-OS(裸机编程)。

获取

Git 方式

git clone https://github.com/murphyzhao/FlexibleButton.git

RT-Thread menuconfig 方式

RT-Thread online packages --->
miscellaneous packages --->
[*] FlexibleButton: Small and flexible button driver --->
[*] Enable flexible button demo
version (latest) --->

配置完成后,输入 pkgs --update 下载软件包。

资源统计

ARMCC -O0 优化的情况下,FlexibleButton 资源占用如下:

  • CODE:798 字节
  • RO DATA:0
  • RW DATA:13 字节
  • ZI DATA:0

快速体验

FlexibleButton 库中提供了一个测试例程 ./examples/demo_rtt_iotboard.c,该例程基于 RT-Thread OS 进行测试,硬件平台选择了 RT-Thread IoT Board Pandora v2.51 开发板。当然你可以选择使用其他的 OS,或者使用裸机测试,只需要移除 OS 相关的特性即可。

如果你使用自己的硬件平台,只需要将 FlexibleButton 库源码和例程加入你既有的工程下即可。

DEMO 程序说明

该示例程序可以直接在 RT-Thread stm32l475-atk-pandora BSP 中运行,可以在该 BSP 目录下,使用 menuconfig 获取本软件包。

确定用户按键

typedefenum
{
USER_BUTTON_0=0, // 对应 IoT Board 开发板的 PIN_KEY0USER_BUTTON_1, // 对应 IoT Board 开发板的 PIN_KEY1USER_BUTTON_2, // 对应 IoT Board 开发板的 PIN_KEY2USER_BUTTON_3, // 对应 IoT Board 开发板的 PIN_WK_UPUSER_BUTTON_MAX
} user_button_t;
staticflex_button_tuser_button[USER_BUTTON_MAX];

上述代码定义了 4 个按键,数据结构存储在 user_button 数组中。

程序入口

intflex_button_main(void)
{
rt_thread_ttid=RT_NULL;
user_button_init();
/* 创建按键扫描线程 flex_btn,线程栈 1024 byte,优先级 10 */tid=rt_thread_create("flex_btn", button_scan, RT_NULL, 1024, 10, 10);
if(tid!=RT_NULL)
{
rt_thread_startup(tid);
}
return0;
}
/* 使用 RT-Thread 的自动初始化 */INIT_APP_EXPORT(flex_button_main);

如上代码所示,首先使用 user_button_init(); 初始化用户按键硬件,该步骤将用户按键绑定到 FlexibleButton 库。然后,使用 RT-Thread 的 INIT_APP_EXPORT 接口导出为上电自动初始化,创建了一个 “flex_btn” 名字的按键扫描线程,线程里扫描检查按键事件。

按键初始化代码

user_button_init(); 初始化代码如下所示:

staticvoiduser_button_init(void)
{
inti;
/* 初始化按键数据结构 */rt_memset(&user_button[0], 0x0, sizeof(user_button));
/* 初始化 IoT Board 按键引脚,使用 rt-thread PIN 设备框架 */rt_pin_mode(PIN_KEY0, PIN_MODE_INPUT_PULLUP); /* 设置 GPIO 为上拉输入模式 */rt_pin_mode(PIN_KEY1, PIN_MODE_INPUT_PULLUP); /* 设置 GPIO 为上拉输入模式 */rt_pin_mode(PIN_KEY2, PIN_MODE_INPUT_PULLUP); /* 设置 GPIO 为上拉输入模式 */rt_pin_mode(PIN_WK_UP, PIN_MODE_INPUT_PULLDOWN); /* 设置 GPIO 为下拉输入模式 */for (i=0; i<USER_BUTTON_MAX; i++)
{
user_button[i].id=i;
user_button[i].usr_button_read=common_btn_read;
user_button[i].cb=common_btn_evt_cb;
user_button[i].pressed_logic_level=0;
user_button[i].short_press_start_tick=FLEX_MS_TO_SCAN_CNT(1500);
user_button[i].long_press_start_tick=FLEX_MS_TO_SCAN_CNT(3000);
user_button[i].long_hold_start_tick=FLEX_MS_TO_SCAN_CNT(4500);
if (i==USER_BUTTON_3)
{
user_button[USER_BUTTON_3].pressed_logic_level=1;
}
flex_button_register(&user_button[i]);
}
}

核心的配置如下:

配置项说明
id按键编号
usr_button_read设置按键读值回调函数
cb设置按键事件回调函数
pressed_logic_level设置按键按下时的逻辑电平
short_press_start_tick短按起始 tick,使用 FLEX_MS_TO_SCAN_CNT 宏转化为扫描次数
long_press_start_tick长按起始 tick,使用 FLEX_MS_TO_SCAN_CNT 宏转化为扫描次数
long_hold_start_tick超长按起始 tick,使用 FLEX_MS_TO_SCAN_CNT 宏转化为扫描次数

注意,short_press_start_tick、long_press_start_tick 和 long_hold_start_tick 必须使用 FLEX_MS_TO_SCAN_CNT 将毫秒时间转化为扫描次数。

user_button[i].short_press_start_tick = FLEX_MS_TO_SCAN_CNT(1500); 表示按键按下开始计时,1500 ms 后按键依旧是按下状态的话,就断定为短按开始。

事件处理代码

staticvoidcommon_btn_evt_cb(void*arg)
{
flex_button_t*btn= (flex_button_t*)arg;
rt_kprintf("id: [%d - %s] event: [%d - %30s] repeat: %d\n", btn->id, enum_btn_id_string[btn->id],
btn->event, enum_event_string[btn->event],
btn->click_cnt);
if (flex_button_event_read(&user_button[USER_BUTTON_0]) ==flex_button_event_read(&user_button[USER_BUTTON_1]) ==FLEX_BTN_PRESS_CLICK)
{
rt_kprintf("[combination]: button 0 and button 1\n");
}
}

示例代码中,将所有的按键事件回调均绑定到 common_btn_evt_cb 函数,在该函数中打印了按键 ID 和按键事件,以及按键连击次数,并演示了如何使用组合按键。

FlexibleButton 代码说明

按键事件定义

按键事件的定义并没有使用 Windows 驱动上的定义,主要是方便嵌入式设备中的应用场景(也可能是我理解的偏差),按键事件定义如下:

typedefenum
{
FLEX_BTN_PRESS_DOWN=0, // 按下事件FLEX_BTN_PRESS_CLICK, // 单击事件FLEX_BTN_PRESS_DOUBLE_CLICK, // 双击事件FLEX_BTN_PRESS_REPEAT_CLICK, // 连击事件,使用 flex_button_t 中的 click_cnt 断定连击次数FLEX_BTN_PRESS_SHORT_START, // 短按开始事件FLEX_BTN_PRESS_SHORT_UP, // 短按抬起事件FLEX_BTN_PRESS_LONG_START, // 长按开始事件FLEX_BTN_PRESS_LONG_UP, // 长按抬起事件FLEX_BTN_PRESS_LONG_HOLD, // 长按保持事件FLEX_BTN_PRESS_LONG_HOLD_UP, // 长按保持的抬起事件FLEX_BTN_PRESS_MAX,
FLEX_BTN_PRESS_NONE,
} flex_button_event_t;

其中 FLEX_BTN_PRESS_LONG_HOLD 事件可以用来实现长按累加的应用场景。

按键数据结构

typedefstructflex_button
{
structflex_button*next;
uint8_t (*usr_button_read)(void*);
flex_button_response_callbackcb;
uint16_tscan_cnt;
uint16_tclick_cnt;
uint16_tmax_multiple_clicks_interval;
uint16_tdebounce_tick;
uint16_tshort_press_start_tick;
uint16_tlong_press_start_tick;
uint16_tlong_hold_start_tick;
uint8_tid;
uint8_tpressed_logic_level : 1;
uint8_tevent : 4;
uint8_tstatus : 3;
} flex_button_t;
序号数据成员是否需要用户初始化说明
1next按键库使用单向链表串起所有的按键
2usr_button_read用户设备的按键引脚电平读取函数,重要
3cb设置按键事件回调,用于应用层对按键事件的分类处理
4scan_cnt用于记录扫描次数,按键按下是开始从零计数
5click_cnt记录单击次数,用于判定单击、连击
6max_multiple_clicks_interval连击间隙,用于判定是否结束连击计数,有默认值 MAX_MULTIPLE_CLICKS_INTERVAL
7debounce_tick消抖时间,暂未使用,依靠扫描间隙进行消抖
8short_press_start_tick设置短按事件触发的起始 tick
9long_press_start_tick设置长按事件触发的起始 tick
10long_hold_start_tick设置长按保持事件触发的起始 tick
11id当多个按键使用同一个回调函数时,用于断定属于哪个按键
12pressed_logic_level设置按键按下的逻辑电平。1:标识按键按下的时候为高电平;0:标识按键按下的时候未低电平,重要
13event用于记录当前按键事件
14status用于记录当前按键的状态,用于内部状态机

注意,在使用 max_multiple_clicks_intervaldebounce_tickshort_press_start_ticklong_press_start_ticklong_hold_start_tick 的时候,注意需要使用宏 **FLEX_MS_TO_SCAN_CNT(ms)** 将毫秒值转换为扫描次数。因为按键库基于扫描次数运转。示例如下:

user_button[1].short_press_start_tick = FLEX_MS_TO_SCAN_CNT(1500); // 1500 毫秒

上述代码表示:表示按键按下后开始计时,1500ms 的时候,按键依旧按下,则断定为短按开始,并上报 FLEX_BTN_PRESS_SHORT_START 事件。

按键注册接口

使用该接口注册一个用户按键,入参为一个 flex_button_t 结构体实例的地址。

int8_tflex_button_register(flex_button_t*button);

按键事件读取接口

使用该接口获取指定按键的事件。

flex_button_event_tflex_button_event_read(flex_button_t*button);

按键扫描接口

按键扫描的核心函数,需要放到应用程序中定时扫描,扫描间隔建议 20 毫秒。

voidflex_button_scan(void);

注意事项

  • 阻塞问题

    因为按键事件回调函数以及按键键值读取函数是在按键扫描的过程中执行的,因此请不要在这类函数中使用阻塞接口,不要进行延时操作。

  • 按键扫描函数栈需求

    按键扫描函数本身对栈的需求小于 300 字节,但是按键事件回调函数和按键键值读取函数都是在按键扫描函数的上下文中执行的,请格外关心按键事件回调函数与按键键值读取函数对栈空间的需求。

其它

关于低功耗

本按键库是通过不间断扫描的方式来检查按键状态,因此会一直占用 CPU 资源,这对低功耗应用场景是不友好的。为了降低正常工作模式下的功耗,建议合理配置扫描周期(5ms - 20ms),扫描间隙里 CPU 可以进入轻度睡眠。

该按键库不在底层实现低功耗处理,应用层可以根据自己的功耗模式灵活处理,通常会有以下两种方式:

  1. 进入低功耗前,挂起按键扫描线程;退出低功耗后,唤醒按键扫描。
  2. 增加按键中断模式,所有的按键中断来,就触发一次按键扫描,以确认所有的按键状态。

低功耗相关的探讨参考 issue 1 中的讨论。

关于按键中断模式

由于该按键库一次扫描可以确定所有的按键状态,因此可以将所有的按键中断通过 “” 的方式转化为一个中断,然后在中断处理函数中执行一次按键扫描。

中断 “” 的方式可以通过硬件来完成,也可以通过软件来完成。

硬件方式,需要使用一个 或门 芯片,多个输入条件转化为一个输出条件,然后通过一个外部中断即可完成所有按键的中断方式检测。

软件方式,需要为每一个按键配置为中断触发模式,然后在每一个按键中断的中断处理函数中执行按键扫描。

为了在降低中断处理函数中执行按键扫描带来的时延,可以通过信号量的方式来异步处理,仅在中断处理函数中释放一个按键扫描的信号量,然后在按键扫描线程中监测该信号量。

关于组合按键

该按键库仅做了底层的按键扫描处理,一次扫描可以确定所有的按键状态,并上报对应的按键事件,如果需要支持组合按键,请再封一层,根据按键库返回的事件封装需要的组合按键。示例程序提供了简单的实现。

关于矩阵键盘

不管你的矩阵键盘是通过什么通信方式获取按键状态的,只要你将读取按键状态的函数对接到 Flexible_button 数据结构中的 uint8_t (*usr_button_read)(void*); 函数上即可。

参考 issue 2 中的讨论。

问题和建议

如果有什么问题或者建议欢迎提交 Issue 进行讨论。

维护

感谢

感谢所有一起探讨的朋友,感谢所有使用 flexible_button 的朋友,感谢你们的 Star 和 Fork,谢谢你们的支持。

友情链接

About

灵活的按键处理库(Flexible Button)| 按键驱动 | 支持单击、双击、连击、长按、自动消抖 | 灵活适配中断和低功耗 | 按需实现组合按键

Topics

Resources

Stars

950 stars

Watchers

26 watching

Forks

Releases

Sponsor this project

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

FlexibleButton

FlexibleButton 是一个基于标准 C 语言的小巧灵活的按键处理库,支持单击、连击、短按、长按、自动消抖,可以自由设置组合按键,可用于中断和低功耗场景。

该按键库解耦了具体的按键硬件结构,理论上支持轻触按键与自锁按键,并可以无限扩展按键数量。另外,FlexibleButton 使用扫描的方式一次性读取所有所有的按键状态,然后通过事件回调机制上报按键事件。核心的按键扫描代码仅有三行,没错,就是经典的 三行按键扫描算法。使用 C 语言标准库 API 编写,也使得该按键库可以无缝兼容任意的处理器平台,并且支持任意 OS 和 non-OS(裸机编程)。

获取

Git 方式

git clone https://github.com/murphyzhao/FlexibleButton.git

RT-Thread menuconfig 方式

RT-Thread online packages --->
miscellaneous packages --->
[*] FlexibleButton: Small and flexible button driver --->
[*] Enable flexible button demo
version (latest) --->

配置完成后,输入 pkgs --update 下载软件包。

资源统计

ARMCC -O0 优化的情况下,FlexibleButton 资源占用如下:

  • CODE:798 字节
  • RO DATA:0
  • RW DATA:13 字节
  • ZI DATA:0

快速体验

FlexibleButton 库中提供了一个测试例程 ./examples/demo_rtt_iotboard.c,该例程基于 RT-Thread OS 进行测试,硬件平台选择了 RT-Thread IoT Board Pandora v2.51 开发板。当然你可以选择使用其他的 OS,或者使用裸机测试,只需要移除 OS 相关的特性即可。

如果你使用自己的硬件平台,只需要将 FlexibleButton 库源码和例程加入你既有的工程下即可。

DEMO 程序说明

该示例程序可以直接在 RT-Thread stm32l475-atk-pandora BSP 中运行,可以在该 BSP 目录下,使用 menuconfig 获取本软件包。

确定用户按键

typedefenum
{
USER_BUTTON_0=0, // 对应 IoT Board 开发板的 PIN_KEY0USER_BUTTON_1, // 对应 IoT Board 开发板的 PIN_KEY1USER_BUTTON_2, // 对应 IoT Board 开发板的 PIN_KEY2USER_BUTTON_3, // 对应 IoT Board 开发板的 PIN_WK_UPUSER_BUTTON_MAX
} user_button_t;
staticflex_button_tuser_button[USER_BUTTON_MAX];

上述代码定义了 4 个按键,数据结构存储在 user_button 数组中。

程序入口

intflex_button_main(void)
{
rt_thread_ttid=RT_NULL;
user_button_init();
/* 创建按键扫描线程 flex_btn,线程栈 1024 byte,优先级 10 */tid=rt_thread_create("flex_btn", button_scan, RT_NULL, 1024, 10, 10);
if(tid!=RT_NULL)
{
rt_thread_startup(tid);
}
return0;
}
/* 使用 RT-Thread 的自动初始化 */INIT_APP_EXPORT(flex_button_main);

如上代码所示,首先使用 user_button_init(); 初始化用户按键硬件,该步骤将用户按键绑定到 FlexibleButton 库。然后,使用 RT-Thread 的 INIT_APP_EXPORT 接口导出为上电自动初始化,创建了一个 “flex_btn” 名字的按键扫描线程,线程里扫描检查按键事件。

按键初始化代码

user_button_init(); 初始化代码如下所示:

staticvoiduser_button_init(void)
{
inti;
/* 初始化按键数据结构 */rt_memset(&user_button[0], 0x0, sizeof(user_button));
/* 初始化 IoT Board 按键引脚,使用 rt-thread PIN 设备框架 */rt_pin_mode(PIN_KEY0, PIN_MODE_INPUT_PULLUP); /* 设置 GPIO 为上拉输入模式 */rt_pin_mode(PIN_KEY1, PIN_MODE_INPUT_PULLUP); /* 设置 GPIO 为上拉输入模式 */rt_pin_mode(PIN_KEY2, PIN_MODE_INPUT_PULLUP); /* 设置 GPIO 为上拉输入模式 */rt_pin_mode(PIN_WK_UP, PIN_MODE_INPUT_PULLDOWN); /* 设置 GPIO 为下拉输入模式 */for (i=0; i<USER_BUTTON_MAX; i++)
{
user_button[i].id=i;
user_button[i].usr_button_read=common_btn_read;
user_button[i].cb=common_btn_evt_cb;
user_button[i].pressed_logic_level=0;
user_button[i].short_press_start_tick=FLEX_MS_TO_SCAN_CNT(1500);
user_button[i].long_press_start_tick=FLEX_MS_TO_SCAN_CNT(3000);
user_button[i].long_hold_start_tick=FLEX_MS_TO_SCAN_CNT(4500);
if (i==USER_BUTTON_3)
{
user_button[USER_BUTTON_3].pressed_logic_level=1;
}
flex_button_register(&user_button[i]);
}
}

核心的配置如下:

配置项说明
id按键编号
usr_button_read设置按键读值回调函数
cb设置按键事件回调函数
pressed_logic_level设置按键按下时的逻辑电平
short_press_start_tick短按起始 tick,使用 FLEX_MS_TO_SCAN_CNT 宏转化为扫描次数
long_press_start_tick长按起始 tick,使用 FLEX_MS_TO_SCAN_CNT 宏转化为扫描次数
long_hold_start_tick超长按起始 tick,使用 FLEX_MS_TO_SCAN_CNT 宏转化为扫描次数

注意,short_press_start_tick、long_press_start_tick 和 long_hold_start_tick 必须使用 FLEX_MS_TO_SCAN_CNT 将毫秒时间转化为扫描次数。

user_button[i].short_press_start_tick = FLEX_MS_TO_SCAN_CNT(1500); 表示按键按下开始计时,1500 ms 后按键依旧是按下状态的话,就断定为短按开始。

事件处理代码

staticvoidcommon_btn_evt_cb(void*arg)
{
flex_button_t*btn= (flex_button_t*)arg;
rt_kprintf("id: [%d - %s] event: [%d - %30s] repeat: %d\n", btn->id, enum_btn_id_string[btn->id],
btn->event, enum_event_string[btn->event],
btn->click_cnt);
if (flex_button_event_read(&user_button[USER_BUTTON_0]) ==flex_button_event_read(&user_button[USER_BUTTON_1]) ==FLEX_BTN_PRESS_CLICK)
{
rt_kprintf("[combination]: button 0 and button 1\n");
}
}

示例代码中,将所有的按键事件回调均绑定到 common_btn_evt_cb 函数,在该函数中打印了按键 ID 和按键事件,以及按键连击次数,并演示了如何使用组合按键。

FlexibleButton 代码说明

按键事件定义

按键事件的定义并没有使用 Windows 驱动上的定义,主要是方便嵌入式设备中的应用场景(也可能是我理解的偏差),按键事件定义如下:

typedefenum
{
FLEX_BTN_PRESS_DOWN=0, // 按下事件FLEX_BTN_PRESS_CLICK, // 单击事件FLEX_BTN_PRESS_DOUBLE_CLICK, // 双击事件FLEX_BTN_PRESS_REPEAT_CLICK, // 连击事件,使用 flex_button_t 中的 click_cnt 断定连击次数FLEX_BTN_PRESS_SHORT_START, // 短按开始事件FLEX_BTN_PRESS_SHORT_UP, // 短按抬起事件FLEX_BTN_PRESS_LONG_START, // 长按开始事件FLEX_BTN_PRESS_LONG_UP, // 长按抬起事件FLEX_BTN_PRESS_LONG_HOLD, // 长按保持事件FLEX_BTN_PRESS_LONG_HOLD_UP, // 长按保持的抬起事件FLEX_BTN_PRESS_MAX,
FLEX_BTN_PRESS_NONE,
} flex_button_event_t;

其中 FLEX_BTN_PRESS_LONG_HOLD 事件可以用来实现长按累加的应用场景。

按键数据结构

typedefstructflex_button
{
structflex_button*next;
uint8_t (*usr_button_read)(void*);
flex_button_response_callbackcb;
uint16_tscan_cnt;
uint16_tclick_cnt;
uint16_tmax_multiple_clicks_interval;
uint16_tdebounce_tick;
uint16_tshort_press_start_tick;
uint16_tlong_press_start_tick;
uint16_tlong_hold_start_tick;
uint8_tid;
uint8_tpressed_logic_level : 1;
uint8_tevent : 4;
uint8_tstatus : 3;
} flex_button_t;
序号数据成员是否需要用户初始化说明
1next按键库使用单向链表串起所有的按键
2usr_button_read用户设备的按键引脚电平读取函数,重要
3cb设置按键事件回调,用于应用层对按键事件的分类处理
4scan_cnt用于记录扫描次数,按键按下是开始从零计数
5click_cnt记录单击次数,用于判定单击、连击
6max_multiple_clicks_interval连击间隙,用于判定是否结束连击计数,有默认值 MAX_MULTIPLE_CLICKS_INTERVAL
7debounce_tick消抖时间,暂未使用,依靠扫描间隙进行消抖
8short_press_start_tick设置短按事件触发的起始 tick
9long_press_start_tick设置长按事件触发的起始 tick
10long_hold_start_tick设置长按保持事件触发的起始 tick
11id当多个按键使用同一个回调函数时,用于断定属于哪个按键
12pressed_logic_level设置按键按下的逻辑电平。1:标识按键按下的时候为高电平;0:标识按键按下的时候未低电平,重要
13event用于记录当前按键事件
14status用于记录当前按键的状态,用于内部状态机

注意,在使用 max_multiple_clicks_intervaldebounce_tickshort_press_start_ticklong_press_start_ticklong_hold_start_tick 的时候,注意需要使用宏 **FLEX_MS_TO_SCAN_CNT(ms)** 将毫秒值转换为扫描次数。因为按键库基于扫描次数运转。示例如下:

user_button[1].short_press_start_tick = FLEX_MS_TO_SCAN_CNT(1500); // 1500 毫秒

上述代码表示:表示按键按下后开始计时,1500ms 的时候,按键依旧按下,则断定为短按开始,并上报 FLEX_BTN_PRESS_SHORT_START 事件。

按键注册接口

使用该接口注册一个用户按键,入参为一个 flex_button_t 结构体实例的地址。

int8_tflex_button_register(flex_button_t*button);

按键事件读取接口

使用该接口获取指定按键的事件。

flex_button_event_tflex_button_event_read(flex_button_t*button);

按键扫描接口

按键扫描的核心函数,需要放到应用程序中定时扫描,扫描间隔建议 20 毫秒。

voidflex_button_scan(void);

注意事项

  • 阻塞问题

    因为按键事件回调函数以及按键键值读取函数是在按键扫描的过程中执行的,因此请不要在这类函数中使用阻塞接口,不要进行延时操作。

  • 按键扫描函数栈需求

    按键扫描函数本身对栈的需求小于 300 字节,但是按键事件回调函数和按键键值读取函数都是在按键扫描函数的上下文中执行的,请格外关心按键事件回调函数与按键键值读取函数对栈空间的需求。

其它

关于低功耗

本按键库是通过不间断扫描的方式来检查按键状态,因此会一直占用 CPU 资源,这对低功耗应用场景是不友好的。为了降低正常工作模式下的功耗,建议合理配置扫描周期(5ms - 20ms),扫描间隙里 CPU 可以进入轻度睡眠。

该按键库不在底层实现低功耗处理,应用层可以根据自己的功耗模式灵活处理,通常会有以下两种方式:

  1. 进入低功耗前,挂起按键扫描线程;退出低功耗后,唤醒按键扫描。
  2. 增加按键中断模式,所有的按键中断来,就触发一次按键扫描,以确认所有的按键状态。

低功耗相关的探讨参考 issue 1 中的讨论。

关于按键中断模式

由于该按键库一次扫描可以确定所有的按键状态,因此可以将所有的按键中断通过 “” 的方式转化为一个中断,然后在中断处理函数中执行一次按键扫描。

中断 “” 的方式可以通过硬件来完成,也可以通过软件来完成。

硬件方式,需要使用一个 或门 芯片,多个输入条件转化为一个输出条件,然后通过一个外部中断即可完成所有按键的中断方式检测。

软件方式,需要为每一个按键配置为中断触发模式,然后在每一个按键中断的中断处理函数中执行按键扫描。

为了在降低中断处理函数中执行按键扫描带来的时延,可以通过信号量的方式来异步处理,仅在中断处理函数中释放一个按键扫描的信号量,然后在按键扫描线程中监测该信号量。

关于组合按键

该按键库仅做了底层的按键扫描处理,一次扫描可以确定所有的按键状态,并上报对应的按键事件,如果需要支持组合按键,请再封一层,根据按键库返回的事件封装需要的组合按键。示例程序提供了简单的实现。

关于矩阵键盘

不管你的矩阵键盘是通过什么通信方式获取按键状态的,只要你将读取按键状态的函数对接到 Flexible_button 数据结构中的 uint8_t (*usr_button_read)(void*); 函数上即可。

参考 issue 2 中的讨论。

问题和建议

如果有什么问题或者建议欢迎提交 Issue 进行讨论。

维护

感谢

感谢所有一起探讨的朋友,感谢所有使用 flexible_button 的朋友,感谢你们的 Star 和 Fork,谢谢你们的支持。

友情链接

About

灵活的按键处理库(Flexible Button)| 按键驱动 | 支持单击、双击、连击、长按、自动消抖 | 灵活适配中断和低功耗 | 按需实现组合按键

Topics

Resources

Stars

950 stars

Watchers

26 watching

Forks

Releases

Sponsor this project

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

FlexibleButton

FlexibleButton 是一个基于标准 C 语言的小巧灵活的按键处理库,支持单击、连击、短按、长按、自动消抖,可以自由设置组合按键,可用于中断和低功耗场景。

该按键库解耦了具体的按键硬件结构,理论上支持轻触按键与自锁按键,并可以无限扩展按键数量。另外,FlexibleButton 使用扫描的方式一次性读取所有所有的按键状态,然后通过事件回调机制上报按键事件。核心的按键扫描代码仅有三行,没错,就是经典的 三行按键扫描算法。使用 C 语言标准库 API 编写,也使得该按键库可以无缝兼容任意的处理器平台,并且支持任意 OS 和 non-OS(裸机编程)。

获取

Git 方式

git clone https://github.com/murphyzhao/FlexibleButton.git

RT-Thread menuconfig 方式

RT-Thread online packages --->
miscellaneous packages --->
[*] FlexibleButton: Small and flexible button driver --->
[*] Enable flexible button demo
version (latest) --->

配置完成后,输入 pkgs --update 下载软件包。

资源统计

ARMCC -O0 优化的情况下,FlexibleButton 资源占用如下:

  • CODE:798 字节
  • RO DATA:0
  • RW DATA:13 字节
  • ZI DATA:0

快速体验

FlexibleButton 库中提供了一个测试例程 ./examples/demo_rtt_iotboard.c,该例程基于 RT-Thread OS 进行测试,硬件平台选择了 RT-Thread IoT Board Pandora v2.51 开发板。当然你可以选择使用其他的 OS,或者使用裸机测试,只需要移除 OS 相关的特性即可。

如果你使用自己的硬件平台,只需要将 FlexibleButton 库源码和例程加入你既有的工程下即可。

DEMO 程序说明

该示例程序可以直接在 RT-Thread stm32l475-atk-pandora BSP 中运行,可以在该 BSP 目录下,使用 menuconfig 获取本软件包。

确定用户按键

typedefenum
{
USER_BUTTON_0=0, // 对应 IoT Board 开发板的 PIN_KEY0USER_BUTTON_1, // 对应 IoT Board 开发板的 PIN_KEY1USER_BUTTON_2, // 对应 IoT Board 开发板的 PIN_KEY2USER_BUTTON_3, // 对应 IoT Board 开发板的 PIN_WK_UPUSER_BUTTON_MAX
} user_button_t;
staticflex_button_tuser_button[USER_BUTTON_MAX];

上述代码定义了 4 个按键,数据结构存储在 user_button 数组中。

程序入口

intflex_button_main(void)
{
rt_thread_ttid=RT_NULL;
user_button_init();
/* 创建按键扫描线程 flex_btn,线程栈 1024 byte,优先级 10 */tid=rt_thread_create("flex_btn", button_scan, RT_NULL, 1024, 10, 10);
if(tid!=RT_NULL)
{
rt_thread_startup(tid);
}
return0;
}
/* 使用 RT-Thread 的自动初始化 */INIT_APP_EXPORT(flex_button_main);

如上代码所示,首先使用 user_button_init(); 初始化用户按键硬件,该步骤将用户按键绑定到 FlexibleButton 库。然后,使用 RT-Thread 的 INIT_APP_EXPORT 接口导出为上电自动初始化,创建了一个 “flex_btn” 名字的按键扫描线程,线程里扫描检查按键事件。

按键初始化代码

user_button_init(); 初始化代码如下所示:

staticvoiduser_button_init(void)
{
inti;
/* 初始化按键数据结构 */rt_memset(&user_button[0], 0x0, sizeof(user_button));
/* 初始化 IoT Board 按键引脚,使用 rt-thread PIN 设备框架 */rt_pin_mode(PIN_KEY0, PIN_MODE_INPUT_PULLUP); /* 设置 GPIO 为上拉输入模式 */rt_pin_mode(PIN_KEY1, PIN_MODE_INPUT_PULLUP); /* 设置 GPIO 为上拉输入模式 */rt_pin_mode(PIN_KEY2, PIN_MODE_INPUT_PULLUP); /* 设置 GPIO 为上拉输入模式 */rt_pin_mode(PIN_WK_UP, PIN_MODE_INPUT_PULLDOWN); /* 设置 GPIO 为下拉输入模式 */for (i=0; i<USER_BUTTON_MAX; i++)
{
user_button[i].id=i;
user_button[i].usr_button_read=common_btn_read;
user_button[i].cb=common_btn_evt_cb;
user_button[i].pressed_logic_level=0;
user_button[i].short_press_start_tick=FLEX_MS_TO_SCAN_CNT(1500);
user_button[i].long_press_start_tick=FLEX_MS_TO_SCAN_CNT(3000);
user_button[i].long_hold_start_tick=FLEX_MS_TO_SCAN_CNT(4500);
if (i==USER_BUTTON_3)
{
user_button[USER_BUTTON_3].pressed_logic_level=1;
}
flex_button_register(&user_button[i]);
}
}

核心的配置如下:

配置项说明
id按键编号
usr_button_read设置按键读值回调函数
cb设置按键事件回调函数
pressed_logic_level设置按键按下时的逻辑电平
short_press_start_tick短按起始 tick,使用 FLEX_MS_TO_SCAN_CNT 宏转化为扫描次数
long_press_start_tick长按起始 tick,使用 FLEX_MS_TO_SCAN_CNT 宏转化为扫描次数
long_hold_start_tick超长按起始 tick,使用 FLEX_MS_TO_SCAN_CNT 宏转化为扫描次数

注意,short_press_start_tick、long_press_start_tick 和 long_hold_start_tick 必须使用 FLEX_MS_TO_SCAN_CNT 将毫秒时间转化为扫描次数。

user_button[i].short_press_start_tick = FLEX_MS_TO_SCAN_CNT(1500); 表示按键按下开始计时,1500 ms 后按键依旧是按下状态的话,就断定为短按开始。

事件处理代码

staticvoidcommon_btn_evt_cb(void*arg)
{
flex_button_t*btn= (flex_button_t*)arg;
rt_kprintf("id: [%d - %s] event: [%d - %30s] repeat: %d\n", btn->id, enum_btn_id_string[btn->id],
btn->event, enum_event_string[btn->event],
btn->click_cnt);
if (flex_button_event_read(&user_button[USER_BUTTON_0]) ==flex_button_event_read(&user_button[USER_BUTTON_1]) ==FLEX_BTN_PRESS_CLICK)
{
rt_kprintf("[combination]: button 0 and button 1\n");
}
}

示例代码中,将所有的按键事件回调均绑定到 common_btn_evt_cb 函数,在该函数中打印了按键 ID 和按键事件,以及按键连击次数,并演示了如何使用组合按键。

FlexibleButton 代码说明

按键事件定义

按键事件的定义并没有使用 Windows 驱动上的定义,主要是方便嵌入式设备中的应用场景(也可能是我理解的偏差),按键事件定义如下:

typedefenum
{
FLEX_BTN_PRESS_DOWN=0, // 按下事件FLEX_BTN_PRESS_CLICK, // 单击事件FLEX_BTN_PRESS_DOUBLE_CLICK, // 双击事件FLEX_BTN_PRESS_REPEAT_CLICK, // 连击事件,使用 flex_button_t 中的 click_cnt 断定连击次数FLEX_BTN_PRESS_SHORT_START, // 短按开始事件FLEX_BTN_PRESS_SHORT_UP, // 短按抬起事件FLEX_BTN_PRESS_LONG_START, // 长按开始事件FLEX_BTN_PRESS_LONG_UP, // 长按抬起事件FLEX_BTN_PRESS_LONG_HOLD, // 长按保持事件FLEX_BTN_PRESS_LONG_HOLD_UP, // 长按保持的抬起事件FLEX_BTN_PRESS_MAX,
FLEX_BTN_PRESS_NONE,
} flex_button_event_t;

其中 FLEX_BTN_PRESS_LONG_HOLD 事件可以用来实现长按累加的应用场景。

按键数据结构

typedefstructflex_button
{
structflex_button*next;
uint8_t (*usr_button_read)(void*);
flex_button_response_callbackcb;
uint16_tscan_cnt;
uint16_tclick_cnt;
uint16_tmax_multiple_clicks_interval;
uint16_tdebounce_tick;
uint16_tshort_press_start_tick;
uint16_tlong_press_start_tick;
uint16_tlong_hold_start_tick;
uint8_tid;
uint8_tpressed_logic_level : 1;
uint8_tevent : 4;
uint8_tstatus : 3;
} flex_button_t;
序号数据成员是否需要用户初始化说明
1next按键库使用单向链表串起所有的按键
2usr_button_read用户设备的按键引脚电平读取函数,重要
3cb设置按键事件回调,用于应用层对按键事件的分类处理
4scan_cnt用于记录扫描次数,按键按下是开始从零计数
5click_cnt记录单击次数,用于判定单击、连击
6max_multiple_clicks_interval连击间隙,用于判定是否结束连击计数,有默认值 MAX_MULTIPLE_CLICKS_INTERVAL
7debounce_tick消抖时间,暂未使用,依靠扫描间隙进行消抖
8short_press_start_tick设置短按事件触发的起始 tick
9long_press_start_tick设置长按事件触发的起始 tick
10long_hold_start_tick设置长按保持事件触发的起始 tick
11id当多个按键使用同一个回调函数时,用于断定属于哪个按键
12pressed_logic_level设置按键按下的逻辑电平。1:标识按键按下的时候为高电平;0:标识按键按下的时候未低电平,重要
13event用于记录当前按键事件
14status用于记录当前按键的状态,用于内部状态机

注意,在使用 max_multiple_clicks_intervaldebounce_tickshort_press_start_ticklong_press_start_ticklong_hold_start_tick 的时候,注意需要使用宏 **FLEX_MS_TO_SCAN_CNT(ms)** 将毫秒值转换为扫描次数。因为按键库基于扫描次数运转。示例如下:

user_button[1].short_press_start_tick = FLEX_MS_TO_SCAN_CNT(1500); // 1500 毫秒

上述代码表示:表示按键按下后开始计时,1500ms 的时候,按键依旧按下,则断定为短按开始,并上报 FLEX_BTN_PRESS_SHORT_START 事件。

按键注册接口

使用该接口注册一个用户按键,入参为一个 flex_button_t 结构体实例的地址。

int8_tflex_button_register(flex_button_t*button);

按键事件读取接口

使用该接口获取指定按键的事件。

flex_button_event_tflex_button_event_read(flex_button_t*button);

按键扫描接口

按键扫描的核心函数,需要放到应用程序中定时扫描,扫描间隔建议 20 毫秒。

voidflex_button_scan(void);

注意事项

  • 阻塞问题

    因为按键事件回调函数以及按键键值读取函数是在按键扫描的过程中执行的,因此请不要在这类函数中使用阻塞接口,不要进行延时操作。

  • 按键扫描函数栈需求

    按键扫描函数本身对栈的需求小于 300 字节,但是按键事件回调函数和按键键值读取函数都是在按键扫描函数的上下文中执行的,请格外关心按键事件回调函数与按键键值读取函数对栈空间的需求。

其它

关于低功耗

本按键库是通过不间断扫描的方式来检查按键状态,因此会一直占用 CPU 资源,这对低功耗应用场景是不友好的。为了降低正常工作模式下的功耗,建议合理配置扫描周期(5ms - 20ms),扫描间隙里 CPU 可以进入轻度睡眠。

该按键库不在底层实现低功耗处理,应用层可以根据自己的功耗模式灵活处理,通常会有以下两种方式:

  1. 进入低功耗前,挂起按键扫描线程;退出低功耗后,唤醒按键扫描。
  2. 增加按键中断模式,所有的按键中断来,就触发一次按键扫描,以确认所有的按键状态。

低功耗相关的探讨参考 issue 1 中的讨论。

关于按键中断模式

由于该按键库一次扫描可以确定所有的按键状态,因此可以将所有的按键中断通过 “” 的方式转化为一个中断,然后在中断处理函数中执行一次按键扫描。

中断 “” 的方式可以通过硬件来完成,也可以通过软件来完成。

硬件方式,需要使用一个 或门 芯片,多个输入条件转化为一个输出条件,然后通过一个外部中断即可完成所有按键的中断方式检测。

软件方式,需要为每一个按键配置为中断触发模式,然后在每一个按键中断的中断处理函数中执行按键扫描。

为了在降低中断处理函数中执行按键扫描带来的时延,可以通过信号量的方式来异步处理,仅在中断处理函数中释放一个按键扫描的信号量,然后在按键扫描线程中监测该信号量。

关于组合按键

该按键库仅做了底层的按键扫描处理,一次扫描可以确定所有的按键状态,并上报对应的按键事件,如果需要支持组合按键,请再封一层,根据按键库返回的事件封装需要的组合按键。示例程序提供了简单的实现。

关于矩阵键盘

不管你的矩阵键盘是通过什么通信方式获取按键状态的,只要你将读取按键状态的函数对接到 Flexible_button 数据结构中的 uint8_t (*usr_button_read)(void*); 函数上即可。

参考 issue 2 中的讨论。

问题和建议

如果有什么问题或者建议欢迎提交 Issue 进行讨论。

维护

感谢

感谢所有一起探讨的朋友,感谢所有使用 flexible_button 的朋友,感谢你们的 Star 和 Fork,谢谢你们的支持。

友情链接

About

灵活的按键处理库(Flexible Button)| 按键驱动 | 支持单击、双击、连击、长按、自动消抖 | 灵活适配中断和低功耗 | 按需实现组合按键

Topics

Resources

Stars

950 stars

Watchers

26 watching

Forks

Releases

Sponsor this project

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

FlexibleButton

FlexibleButton 是一个基于标准 C 语言的小巧灵活的按键处理库,支持单击、连击、短按、长按、自动消抖,可以自由设置组合按键,可用于中断和低功耗场景。

该按键库解耦了具体的按键硬件结构,理论上支持轻触按键与自锁按键,并可以无限扩展按键数量。另外,FlexibleButton 使用扫描的方式一次性读取所有所有的按键状态,然后通过事件回调机制上报按键事件。核心的按键扫描代码仅有三行,没错,就是经典的 三行按键扫描算法。使用 C 语言标准库 API 编写,也使得该按键库可以无缝兼容任意的处理器平台,并且支持任意 OS 和 non-OS(裸机编程)。

获取

Git 方式

git clone https://github.com/murphyzhao/FlexibleButton.git

RT-Thread menuconfig 方式

RT-Thread online packages --->
miscellaneous packages --->
[*] FlexibleButton: Small and flexible button driver --->
[*] Enable flexible button demo
version (latest) --->

配置完成后,输入 pkgs --update 下载软件包。

资源统计

ARMCC -O0 优化的情况下,FlexibleButton 资源占用如下:

  • CODE:798 字节
  • RO DATA:0
  • RW DATA:13 字节
  • ZI DATA:0

快速体验

FlexibleButton 库中提供了一个测试例程 ./examples/demo_rtt_iotboard.c,该例程基于 RT-Thread OS 进行测试,硬件平台选择了 RT-Thread IoT Board Pandora v2.51 开发板。当然你可以选择使用其他的 OS,或者使用裸机测试,只需要移除 OS 相关的特性即可。

如果你使用自己的硬件平台,只需要将 FlexibleButton 库源码和例程加入你既有的工程下即可。

DEMO 程序说明

该示例程序可以直接在 RT-Thread stm32l475-atk-pandora BSP 中运行,可以在该 BSP 目录下,使用 menuconfig 获取本软件包。

确定用户按键

typedefenum
{
USER_BUTTON_0=0, // 对应 IoT Board 开发板的 PIN_KEY0USER_BUTTON_1, // 对应 IoT Board 开发板的 PIN_KEY1USER_BUTTON_2, // 对应 IoT Board 开发板的 PIN_KEY2USER_BUTTON_3, // 对应 IoT Board 开发板的 PIN_WK_UPUSER_BUTTON_MAX
} user_button_t;
staticflex_button_tuser_button[USER_BUTTON_MAX];

上述代码定义了 4 个按键,数据结构存储在 user_button 数组中。

程序入口

intflex_button_main(void)
{
rt_thread_ttid=RT_NULL;
user_button_init();
/* 创建按键扫描线程 flex_btn,线程栈 1024 byte,优先级 10 */tid=rt_thread_create("flex_btn", button_scan, RT_NULL, 1024, 10, 10);
if(tid!=RT_NULL)
{
rt_thread_startup(tid);
}
return0;
}
/* 使用 RT-Thread 的自动初始化 */INIT_APP_EXPORT(flex_button_main);

如上代码所示,首先使用 user_button_init(); 初始化用户按键硬件,该步骤将用户按键绑定到 FlexibleButton 库。然后,使用 RT-Thread 的 INIT_APP_EXPORT 接口导出为上电自动初始化,创建了一个 “flex_btn” 名字的按键扫描线程,线程里扫描检查按键事件。

按键初始化代码

user_button_init(); 初始化代码如下所示:

staticvoiduser_button_init(void)
{
inti;
/* 初始化按键数据结构 */rt_memset(&user_button[0], 0x0, sizeof(user_button));
/* 初始化 IoT Board 按键引脚,使用 rt-thread PIN 设备框架 */rt_pin_mode(PIN_KEY0, PIN_MODE_INPUT_PULLUP); /* 设置 GPIO 为上拉输入模式 */rt_pin_mode(PIN_KEY1, PIN_MODE_INPUT_PULLUP); /* 设置 GPIO 为上拉输入模式 */rt_pin_mode(PIN_KEY2, PIN_MODE_INPUT_PULLUP); /* 设置 GPIO 为上拉输入模式 */rt_pin_mode(PIN_WK_UP, PIN_MODE_INPUT_PULLDOWN); /* 设置 GPIO 为下拉输入模式 */for (i=0; i<USER_BUTTON_MAX; i++)
{
user_button[i].id=i;
user_button[i].usr_button_read=common_btn_read;
user_button[i].cb=common_btn_evt_cb;
user_button[i].pressed_logic_level=0;
user_button[i].short_press_start_tick=FLEX_MS_TO_SCAN_CNT(1500);
user_button[i].long_press_start_tick=FLEX_MS_TO_SCAN_CNT(3000);
user_button[i].long_hold_start_tick=FLEX_MS_TO_SCAN_CNT(4500);
if (i==USER_BUTTON_3)
{
user_button[USER_BUTTON_3].pressed_logic_level=1;
}
flex_button_register(&user_button[i]);
}
}

核心的配置如下:

配置项说明
id按键编号
usr_button_read设置按键读值回调函数
cb设置按键事件回调函数
pressed_logic_level设置按键按下时的逻辑电平
short_press_start_tick短按起始 tick,使用 FLEX_MS_TO_SCAN_CNT 宏转化为扫描次数
long_press_start_tick长按起始 tick,使用 FLEX_MS_TO_SCAN_CNT 宏转化为扫描次数
long_hold_start_tick超长按起始 tick,使用 FLEX_MS_TO_SCAN_CNT 宏转化为扫描次数

注意,short_press_start_tick、long_press_start_tick 和 long_hold_start_tick 必须使用 FLEX_MS_TO_SCAN_CNT 将毫秒时间转化为扫描次数。

user_button[i].short_press_start_tick = FLEX_MS_TO_SCAN_CNT(1500); 表示按键按下开始计时,1500 ms 后按键依旧是按下状态的话,就断定为短按开始。

事件处理代码

staticvoidcommon_btn_evt_cb(void*arg)
{
flex_button_t*btn= (flex_button_t*)arg;
rt_kprintf("id: [%d - %s] event: [%d - %30s] repeat: %d\n", btn->id, enum_btn_id_string[btn->id],
btn->event, enum_event_string[btn->event],
btn->click_cnt);
if (flex_button_event_read(&user_button[USER_BUTTON_0]) ==flex_button_event_read(&user_button[USER_BUTTON_1]) ==FLEX_BTN_PRESS_CLICK)
{
rt_kprintf("[combination]: button 0 and button 1\n");
}
}

示例代码中,将所有的按键事件回调均绑定到 common_btn_evt_cb 函数,在该函数中打印了按键 ID 和按键事件,以及按键连击次数,并演示了如何使用组合按键。

FlexibleButton 代码说明

按键事件定义

按键事件的定义并没有使用 Windows 驱动上的定义,主要是方便嵌入式设备中的应用场景(也可能是我理解的偏差),按键事件定义如下:

typedefenum
{
FLEX_BTN_PRESS_DOWN=0, // 按下事件FLEX_BTN_PRESS_CLICK, // 单击事件FLEX_BTN_PRESS_DOUBLE_CLICK, // 双击事件FLEX_BTN_PRESS_REPEAT_CLICK, // 连击事件,使用 flex_button_t 中的 click_cnt 断定连击次数FLEX_BTN_PRESS_SHORT_START, // 短按开始事件FLEX_BTN_PRESS_SHORT_UP, // 短按抬起事件FLEX_BTN_PRESS_LONG_START, // 长按开始事件FLEX_BTN_PRESS_LONG_UP, // 长按抬起事件FLEX_BTN_PRESS_LONG_HOLD, // 长按保持事件FLEX_BTN_PRESS_LONG_HOLD_UP, // 长按保持的抬起事件FLEX_BTN_PRESS_MAX,
FLEX_BTN_PRESS_NONE,
} flex_button_event_t;

其中 FLEX_BTN_PRESS_LONG_HOLD 事件可以用来实现长按累加的应用场景。

按键数据结构

typedefstructflex_button
{
structflex_button*next;
uint8_t (*usr_button_read)(void*);
flex_button_response_callbackcb;
uint16_tscan_cnt;
uint16_tclick_cnt;
uint16_tmax_multiple_clicks_interval;
uint16_tdebounce_tick;
uint16_tshort_press_start_tick;
uint16_tlong_press_start_tick;
uint16_tlong_hold_start_tick;
uint8_tid;
uint8_tpressed_logic_level : 1;
uint8_tevent : 4;
uint8_tstatus : 3;
} flex_button_t;
序号数据成员是否需要用户初始化说明
1next按键库使用单向链表串起所有的按键
2usr_button_read用户设备的按键引脚电平读取函数,重要
3cb设置按键事件回调,用于应用层对按键事件的分类处理
4scan_cnt用于记录扫描次数,按键按下是开始从零计数
5click_cnt记录单击次数,用于判定单击、连击
6max_multiple_clicks_interval连击间隙,用于判定是否结束连击计数,有默认值 MAX_MULTIPLE_CLICKS_INTERVAL
7debounce_tick消抖时间,暂未使用,依靠扫描间隙进行消抖
8short_press_start_tick设置短按事件触发的起始 tick
9long_press_start_tick设置长按事件触发的起始 tick
10long_hold_start_tick设置长按保持事件触发的起始 tick
11id当多个按键使用同一个回调函数时,用于断定属于哪个按键
12pressed_logic_level设置按键按下的逻辑电平。1:标识按键按下的时候为高电平;0:标识按键按下的时候未低电平,重要
13event用于记录当前按键事件
14status用于记录当前按键的状态,用于内部状态机

注意,在使用 max_multiple_clicks_intervaldebounce_tickshort_press_start_ticklong_press_start_ticklong_hold_start_tick 的时候,注意需要使用宏 **FLEX_MS_TO_SCAN_CNT(ms)** 将毫秒值转换为扫描次数。因为按键库基于扫描次数运转。示例如下:

user_button[1].short_press_start_tick = FLEX_MS_TO_SCAN_CNT(1500); // 1500 毫秒

上述代码表示:表示按键按下后开始计时,1500ms 的时候,按键依旧按下,则断定为短按开始,并上报 FLEX_BTN_PRESS_SHORT_START 事件。

按键注册接口

使用该接口注册一个用户按键,入参为一个 flex_button_t 结构体实例的地址。

int8_tflex_button_register(flex_button_t*button);

按键事件读取接口

使用该接口获取指定按键的事件。

flex_button_event_tflex_button_event_read(flex_button_t*button);

按键扫描接口

按键扫描的核心函数,需要放到应用程序中定时扫描,扫描间隔建议 20 毫秒。

voidflex_button_scan(void);

注意事项

  • 阻塞问题

    因为按键事件回调函数以及按键键值读取函数是在按键扫描的过程中执行的,因此请不要在这类函数中使用阻塞接口,不要进行延时操作。

  • 按键扫描函数栈需求

    按键扫描函数本身对栈的需求小于 300 字节,但是按键事件回调函数和按键键值读取函数都是在按键扫描函数的上下文中执行的,请格外关心按键事件回调函数与按键键值读取函数对栈空间的需求。

其它

关于低功耗

本按键库是通过不间断扫描的方式来检查按键状态,因此会一直占用 CPU 资源,这对低功耗应用场景是不友好的。为了降低正常工作模式下的功耗,建议合理配置扫描周期(5ms - 20ms),扫描间隙里 CPU 可以进入轻度睡眠。

该按键库不在底层实现低功耗处理,应用层可以根据自己的功耗模式灵活处理,通常会有以下两种方式:

  1. 进入低功耗前,挂起按键扫描线程;退出低功耗后,唤醒按键扫描。
  2. 增加按键中断模式,所有的按键中断来,就触发一次按键扫描,以确认所有的按键状态。

低功耗相关的探讨参考 issue 1 中的讨论。

关于按键中断模式

由于该按键库一次扫描可以确定所有的按键状态,因此可以将所有的按键中断通过 “” 的方式转化为一个中断,然后在中断处理函数中执行一次按键扫描。

中断 “” 的方式可以通过硬件来完成,也可以通过软件来完成。

硬件方式,需要使用一个 或门 芯片,多个输入条件转化为一个输出条件,然后通过一个外部中断即可完成所有按键的中断方式检测。

软件方式,需要为每一个按键配置为中断触发模式,然后在每一个按键中断的中断处理函数中执行按键扫描。

为了在降低中断处理函数中执行按键扫描带来的时延,可以通过信号量的方式来异步处理,仅在中断处理函数中释放一个按键扫描的信号量,然后在按键扫描线程中监测该信号量。

关于组合按键

该按键库仅做了底层的按键扫描处理,一次扫描可以确定所有的按键状态,并上报对应的按键事件,如果需要支持组合按键,请再封一层,根据按键库返回的事件封装需要的组合按键。示例程序提供了简单的实现。

关于矩阵键盘

不管你的矩阵键盘是通过什么通信方式获取按键状态的,只要你将读取按键状态的函数对接到 Flexible_button 数据结构中的 uint8_t (*usr_button_read)(void*); 函数上即可。

参考 issue 2 中的讨论。

问题和建议

如果有什么问题或者建议欢迎提交 Issue 进行讨论。

维护

感谢

感谢所有一起探讨的朋友,感谢所有使用 flexible_button 的朋友,感谢你们的 Star 和 Fork,谢谢你们的支持。

友情链接

About

灵活的按键处理库(Flexible Button)| 按键驱动 | 支持单击、双击、连击、长按、自动消抖 | 灵活适配中断和低功耗 | 按需实现组合按键

Topics

Resources

Stars

950 stars

Watchers

26 watching

Forks

Releases

Sponsor this project

Packages

Contributors

Languages