sku图是什么意思| 什么像什么什么| 木乐念什么| 80分贝相当于什么声音| 乳和霜有什么区别| neighborhood什么意思| 为什么端午节要吃粽子| 4a广告公司什么意思| 垮掉是什么意思| 脱肛是什么原因造成的| 子宫肌瘤是什么原因引起的| 梅肉是什么肉| 开车穿什么鞋子| 腮腺炎吃什么药好得快| 绝经一般在什么年龄| 散漫是什么意思| 国家电网需要什么专业| 阴瑜伽是什么意思| 什么树没有叶子| 宫颈癌吃什么好| 细菌性阴道炎用什么药效果好| 正常的包皮什么样子| 前列腺钙化什么意思| 小孩自闭症是什么原因引起的| 12月份是什么星座| 远视储备是什么意思| 孤寡老人是什么意思| 蜂蜜喝了有什么好处| 言字五行属什么| 牙齿一吸就出血是什么原因| 什么叫幽门螺旋杆菌| 列文虎克发明了什么| 前列腺增大伴钙化是什么意思| 吃什么最补血而且最快| 河南有什么特色美食| 黄曲霉菌是什么颜色| 银杏叶像什么| 蘖是什么意思| 耳朵突然听不见是什么原因| 血糖低什么症状| 特步属于什么档次| 城五行属什么| 蜜蜂是什么牌子| 舌头麻木是什么原因| 康熙姓什么| 8月15号什么星座| 什么是调和油| 吃香蕉有什么好处| 巴氏征阳性是什么意思| 为什么射出的精子里有淡红色| 梦到高考是什么意思| 淋巴细胞百分比偏低是什么意思| 天地人和是什么意思| 男人交生育保险有什么用| 什么情况下需要做宫腔镜| noa是什么意思| 10月13号是什么星座| 烤冷面是什么做的| 眉毛附近长痘痘是什么原因| 3月7日是什么星座| 归元寺求什么最灵验| 王者风范是什么意思| 一什么冰箱| 项韧带钙化是什么意思| 珍珠五行属什么| 血酮体高代表什么| 为什么会肚子痛| 低压高什么原因| 肝囊肿是什么病| 立春之后是什么节气| 胃窦是什么| 浊气是什么意思| 怀孕能吃什么水果| 落叶像什么飘落下来| 风寒吃什么药| 夜间睡觉流口水是什么原因| 5月9号什么星座| 剑齿虎为什么会灭绝| 黄药是什么| 5月21日什么星座| 甲状腺功能减退是什么原因引起的| 阿迪达斯和三叶草有什么区别| 什么是皮炎| 射手和什么星座最配| 孕晚期吃什么水果好| 息风止痉是什么意思| 忽什么忽什么| 627是什么星座| 脾大吃什么药能缩小| 肉桂粉是什么做的| 粘液阳性是什么意思| 甲胄是什么意思| 收录是什么意思| 热症是什么意思| 肝回声稍密是什么意思| 扁桃体发炎是什么引起的| 经常胃胀是什么原因| 女人梦见大蟒蛇是什么征兆| 中医考证需要什么条件| 宝子是什么意思| 贲门ca是什么意思| 什么时候测血压最准确| 囊壁钙化是什么意思| 车水马龙是什么生肖| 殳是什么意思| 三月二十是什么星座| 腋下有异味用什么药| 老是犯困想睡觉是什么原因| 筑基是什么意思| 孕妇吃冰的东西对胎儿有什么影响| 3月20是什么星座| 王字旁的字与什么有关| 左眼跳是什么意思| 犹太人为什么叫犹太人| 阿玛尼是什么意思| 束缚是什么意思| 徐娘半老是什么意思| 发泡胶用什么能洗掉| 地道战在河北什么地方| 阑尾炎吃什么| 五险一金的一金是什么| 反差萌是什么意思| 早泄吃什么中成药| 四肢厥逆是什么意思| 尿白细胞3十什么意思| 孟字五行属什么| 总爱放屁是什么原因| 一级军士长什么待遇| 本科二批是什么意思| aid是什么意思| 色丁布是什么面料| 什么的色彩| 倍增是什么意思| 1954年属什么| 酸入肝是什么意思| 逐年是什么意思| 手指甲紫色是什么原因| 最坚固的锁怕什么| 二型血糖高吃什么药好| 1月18是什么星座| 孕早期是什么时候| 你说什么| 嘴唇没有血色是什么原因| 已所不欲勿施于人是什么意思| 搪瓷杯为什么被淘汰了| 水什么| 口腔溃疡反反复复是什么原因| 地图舌吃什么好得快| 什么水晶招财旺事业| 什么降糖药效果最好| 香椿是什么| 什么东西清肺止咳| 意尔康属于什么档次| 留置针是什么| 为什么喜欢春天| 胃肠感冒吃什么食物比较好| 月季黑斑病用什么药| 丰年虾是什么| 现字五行属什么| 腰酸是什么原因| 大伽是什么意思| 中午1点是什么时辰| 手指盖空了是什么原因| 什么是白细胞| 脑动脉硬化是什么意思| 天蝎座男生喜欢什么样的女生| 高危性行为是什么意思| 子宫病变有什么症状| 闺蜜生日送什么礼物好| 手心发痒是什么原因| 每延米是什么意思| 孕妇梦见龙是什么征兆| 跑步穿什么衣服| 梦见被追杀预示什么| 以身相许是什么意思| 喉咙里老是有痰是什么原因| ph值小于7是什么意思| 牛骨煲汤搭配什么最好| 皮肤软组织感染用什么消炎药| 十一月是什么月| 疝外科是治什么病的| 夜不能寐什么意思| 梦到自己怀孕是什么意思| sk-ll是什么牌子| 什么是考生号| 全身发麻是什么原因| 什么情况下怀疑白血病| 筋膜炎挂什么科| premier是什么牌子| 送女生礼物送什么好| 七手八脚是什么意思| 令堂是什么意思| 耳火念什么| 才高八斗是什么动物| 金牛座是什么星象| 76年出生属什么生肖| 星巴克是什么| 六月十三日是什么日子| 派大星是什么动物| 2.4号是什么星座| 助产是干什么的| 腹部胀疼是什么原因| 什么的镜子| 尿尿疼是什么原因| 宫颈糜烂什么症状| 牙疼吃什么饭| 莜面是什么面| 五十坐地能吸土是什么意思| wbc是什么意思医学| 6月12是什么星座| 唏嘘是什么意思| OD是什么| 十月二十七是什么星座| 骨头疼是什么病的征兆| 海参有什么营养价值| 女人吃桑葚有什么好处| 女性经常手淫有什么危害| 六根不净是什么意思| PT医学上是什么意思| 经常感觉口渴口干是什么原因| 钾低是什么原因造成的| 中风是什么原因引起的| 隐翅虫是什么| 什么水果含铁| 鲈鱼吃什么| 改姓需要什么手续| 重庆有什么区| t是什么火车| 虫草是什么| 死于非命是什么意思| 170是什么号码| 为什么叫五十肩| 放臭屁是什么原因| sunglasses什么意思| 甲木是什么意思| 肾结石可以吃什么| 为什么会牙痛| 白天尿少晚上尿多什么原因| 涉三什么意思| 反流性咽喉炎吃什么药最好| 暖对什么| 心房颤动是什么意思| 寒碜是什么意思| 脉浮是什么意思| 尿急吃什么药效果最好| 嘌呤高会引起什么症状| 感冒咳嗽挂号挂什么科| panerai是什么牌子| 玉米什么时候传入中国| 门昌念什么| 三点水一个兆读什么| 检查胃镜需要提前做什么准备| 一劳永逸什么意思| 清真是什么意思| 什么是adhd| 什么猪没有嘴| 三金片有什么副作用| 值太岁是什么意思| 人的肝脏在什么位置| 儿童支原体感染吃什么药| 天上九头鸟地上湖北佬是什么意思| 胡萝卜什么时候成熟| 护理学是干什么的| 什么的什么是什么的伞| 百度
Skip to content

嵌入式按键处理驱动(Button Driver),支持单击、双击、多击、自动消抖、长按、长长按、超长按 | 低功耗支持 | 组合按键支持 | 静态/动态注册支持

License

Notifications You must be signed in to change notification settings

bobwenstudy/easy_button

Repository files navigation

简介

在嵌入式裸机开发中,经常有按键的管理需求,GitHub上已经有蛮多成熟的按键驱动了,但是由于这样那样的问题,最终还是自己实现了一套。本项目地址:bobwenstudy/easy_button (github.com)。 一个网友的分享,MCU应用实践可以参考:easy_button-Application

项目开发过程中参考了如下几个项目murphyzhao/FlexibleButton: 灵活的按键处理库(Flexible Button)| 按键驱动 | 支持单击、双击、连击、长按、自动消抖 | 灵活适配中断和低功耗 | 按需实现组合按键 (github.com)0x1abin/MultiButton: Button driver for embedded system (github.com)MaJerle/lwbtn: Lightweight button handler for embedded systems (github.com)

其中核心的按键管理机制借鉴的是lwbtn,并在其基础上做了比较多的改动,部分事件上报行为和原本处理有些不同。

对比分析

下面从几个维度来对比几个开源库的差异。

注意:分析纯属个人观点,如有异议请随时与我沟通。

easy_button FlexibleButton MultiButton lwbtn
最大支持按键数 无限 32 无限 无限
按键时间参数独立配置 支持 支持 部分支持 支持
单个按键RAM Size(Bytes) 20(ebtn_btn_t) 28(flex_button_t) 44(Button) 48(lwbtn_btn_t)
支持组合按键 支持 不支持 不支持 不支持
支持静态注册(可以省Code Size) 支持 不支持 不支持 支持
支持动态注册 支持 支持 支持 不支持
单击最大次数 无限 无限 2 无限
长按种类 无限 1 1 无限
批量扫描支持 支持 不支持 不支持 不支持

可以看出easy_button功能是最全的,并且使用的RAM Size也是最小的,这个在键盘之类有很多按键场景下非常有意义。

组合按键支持

现有的项目基本都不支持组合按键,基本都是要求用户根据ID在应用层将多个按键作为一个ID来实现,虽然这样也能实现组合按键的功能需要。

但是这样的实现逻辑不够优雅,并且扫描按键行为的逻辑不可避免有重复的部分,增加了mips。

本项目基于bit_array_static实现了优雅的组合按键处理机制,无需重复定义按键扫描逻辑,驱动会利用已经读取到的按键状态来实现组合按键的功能逻辑。

长按支持

实际项目中会遇到各种功能需求,如长按3s是功能A,长按5s是功能B,长按30s是功能C。通过keepalive_cnttime_keepalive_period的设计,能够支持各种场景的长按功能需要。

如定义time_keepalive_period=1000,那么每隔1s会上报一个KEEPALIVE(EBTN_EVT_KEEPALIVE)事件,应用层在收到上报事件后,当keepalive_cnt==3时,执行功能A;当keepalive_cnt==5时,执行功能B;当keepalive_cnt==30时,执行功能C。

批量扫描支持

现有的按键库都是一个个按键扫描再单独处理,这个在按键比较少的时候,比较好管理,但是在多按键场景下,尤其是矩阵键盘下,这个会大大增加扫描延迟,通过批量扫描支持,可以先在用户层将所有按键状态记录好(用户层根据具体应用优化获取速度),而后一次性将当前状态传给(ebtn_process_with_curr_state)驱动。

嵌入式按键处理驱动,支持单击、双击、多击、自动消抖、长按、长长按、超长按 | 低功耗支持 | 组合按键支持 | 静态/动态注册支持

简易但灵活的事件类型

参考lwbtn实现,当有按键事件发生时,所上报的事件类型只有4种,通过click_cntkeepalive_cnt来支持灵活的按键点击和长按功能需要,这样的设计大大简化了代码行为,也大大降低了后续维护成本。

如果用户觉得不好用,也可以在该驱动基础上再封装出自己所需的驱动。

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;

关于低功耗

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

一般MCU都有深度睡眠模式,这是CPU只能被IO切换唤醒,所以驱动为大家提供了int ebtn_is_in_process(void)接口来判断是否可以进入深度睡眠模式。

代码结构

代码结构如下所示:

  • ebtn:驱动库,主要包含BitArray管理和EasyButton管理。
  • example_user.c:捕获windows的0-9作为按键输入,测试用户交互的例程。
  • example_test.c:模拟一些场景的按键事件,对驱动进行测试。
  • main.c:程序主入口,配置进行测试模式函数用户交互模式。
  • build.mkMakefile:Makefile编译环境。
  • README.md:说明文档
easy_button
 ├── ebtn
 │   ├── bit_array.h
 │   ├── ebtn.c
 │   └── ebtn.h
 ├── build.mk
 ├── example_user.c
 └── example_test.c
 ├── main.c
 ├── Makefile
 └── README.md

使用说明

使用简易步骤

Step1:定义KEY_ID、按键参数和按键数组和组合按键数组。

typedef enum
{
    USER_BUTTON_0 = 0,
    USER_BUTTON_1,
    USER_BUTTON_2,
    USER_BUTTON_3,
    USER_BUTTON_4,
    USER_BUTTON_5,
    USER_BUTTON_6,
    USER_BUTTON_7,
    USER_BUTTON_8,
    USER_BUTTON_9,
    USER_BUTTON_MAX,

    USER_BUTTON_COMBO_0 = 0x100,
    USER_BUTTON_COMBO_1,
    USER_BUTTON_COMBO_2,
    USER_BUTTON_COMBO_3,
    USER_BUTTON_COMBO_MAX,
} user_button_t;

/* User defined settings */
static const ebtn_btn_param_t defaul_ebtn_param = EBTN_PARAMS_INIT(20, 0, 20, 300, 200, 500, 10);

static ebtn_btn_t btns[] = {
        EBTN_BUTTON_INIT(USER_BUTTON_0, &defaul_ebtn_param),
        EBTN_BUTTON_INIT(USER_BUTTON_1, &defaul_ebtn_param),
        EBTN_BUTTON_INIT(USER_BUTTON_2, &defaul_ebtn_param),
        EBTN_BUTTON_INIT(USER_BUTTON_3, &defaul_ebtn_param),
        EBTN_BUTTON_INIT(USER_BUTTON_4, &defaul_ebtn_param),
        EBTN_BUTTON_INIT(USER_BUTTON_5, &defaul_ebtn_param),
};


static ebtn_btn_combo_t btns_combo[] = {
        EBTN_BUTTON_COMBO_INIT(USER_BUTTON_COMBO_0, &defaul_ebtn_param),
        EBTN_BUTTON_COMBO_INIT(USER_BUTTON_COMBO_1, &defaul_ebtn_param),
};

Step2:初始化按键驱动

ebtn_init(btns, EBTN_ARRAY_SIZE(btns), btns_combo, EBTN_ARRAY_SIZE(btns_combo),
              prv_btn_get_state, prv_btn_event);

Step3:配置组合按键comb_key,必须在按键注册完毕后再配置,不然需要ebtn_combo_btn_add_btn_by_idx用这个接口。

ebtn_combo_btn_add_btn(&btns_combo[0], USER_BUTTON_0);
ebtn_combo_btn_add_btn(&btns_combo[0], USER_BUTTON_1);

ebtn_combo_btn_add_btn(&btns_combo[1], USER_BUTTON_2);
ebtn_combo_btn_add_btn(&btns_combo[1], USER_BUTTON_3);

Step4:动态注册所需按键,并配置comb_key。

// dynamic register
for (int i = 0; i < (EBTN_ARRAY_SIZE(btns_dyn)); i++)
{
    ebtn_register(&btns_dyn[i]);
}

ebtn_combo_btn_add_btn(&btns_combo_dyn[0].btn, USER_BUTTON_4);
ebtn_combo_btn_add_btn(&btns_combo_dyn[0].btn, USER_BUTTON_5);

ebtn_combo_btn_add_btn(&btns_combo_dyn[1].btn, USER_BUTTON_6);
ebtn_combo_btn_add_btn(&btns_combo_dyn[1].btn, USER_BUTTON_7);

for (int i = 0; i < (EBTN_ARRAY_SIZE(btns_combo_dyn)); i++)
{
    ebtn_combo_register(&btns_combo_dyn[i]);
}

Step5:启动按键扫描,具体实现可以用定时器做,也可以启任务或者轮询处理。需要注意需要将当前系统时钟get_tick()传给驱动接口ebtn_process

while (1)
{
    /* Process forever */
    ebtn_process(get_tick());

    /* Artificial sleep to offload win process */
    Sleep(5);
}

具体可以参考example_user.cexample_test.c的实现。

key_id和key_idx的说明

为了更好的实现组合按键以及批量扫描的支持,驱动引入了BitArray来管理按键的历史状态和组合按键信息。这样就间接引入了key_index的概念,其代表独立按键在驱动的位置,该值不可直接设置,是按照一定规则隐式定义的。

key_id是用户定义的,用于标识按键的,该值可以随意更改,但是尽量保证该值独立。

如下图所示,驱动有2个静态注册的按键,还有3个动态注册的按键。每个按键的key_id是随意定义的,但是key_idx却是驱动内部隐式定义的,先是静态数组,而后按照动态数组顺先依次定义。

注意:由于组合按键也会用到key_idx的信息,所以动态按键目前并不提供删除按键的行为,这个可能引发一些风险。

image-20240223103317921

结构体说明

按键配置参数结构体说明-ebtn_btn_param_t

按键根据不同的时间触发不同的事件,目前每个按键可以配置的参数如下。

名称 说明
time_debounce 防抖处理,按下防抖超时,配置为0,代表不启动
time_debounce_release 防抖处理,松开防抖超时,配置为0,代表不启动
time_click_pressed_min 按键超时处理,按键最短时间,配置为0,代表不检查最小值
time_click_pressed_max 按键超时处理,按键最长时间,配置为0xFFFF,代表不检查最大值,用于区分长按和按键事件。
time_click_multi_max 多击处理,两个按键之间认为是连击的超时时间
time_keepalive_period 长按处理,长按周期,每个周期增加keepalive_cnt计数
max_consecutive 最大连击次数,配置为0,代表不进行连击检查。
typedef struct ebtn_btn_param
{
    /**
     * \brief           Minimum debounce time for press event in units of milliseconds
     *
     * This is the time when the input shall have stable active level to detect valid *onpress*
     * event.
     *
     * When value is set to `> 0`, input must be in active state for at least
     * minimum milliseconds time, before valid *onpress* event is detected.
     *
     * \note            If value is set to `0`, debounce is not used and *press* event will be
     * triggered immediately when input states goes to *inactive* state.
     *
     *                  To be safe not using this feature, external logic must ensure stable
     * transition at input level.
     *
     */
    uint16_t time_debounce; /*!< Debounce time in milliseconds */

    /**
     * \brief           Minimum debounce time for release event in units of milliseconds
     *
     * This is the time when the input shall have minimum stable released level to detect valid
     * *onrelease* event.
     *
     * This setting can be useful if application wants to protect against
     * unwanted glitches on the line when input is considered "active".
     *
     * When value is set to `> 0`, input must be in inactive low for at least
     * minimum milliseconds time, before valid *onrelease* event is detected
     *
     * \note            If value is set to `0`, debounce is not used and *release* event will be
     * triggered immediately when input states goes to *inactive* state
     *
     */
    uint16_t time_debounce_release; /*!< Debounce time in milliseconds for release event  */

    /**
     * \brief           Minimum active input time for valid click event, in milliseconds
     *
     * Input shall be in active state (after debounce) at least this amount of time to even consider
     * the potential valid click event. Set the value to `0` to disable this feature
     *
     */
    uint16_t time_click_pressed_min; /*!< Minimum pressed time for valid click event */

    /**
     * \brief           Maximum active input time for valid click event, in milliseconds
     *
     * Input shall be pressed at most this amount of time to still trigger valid click.
     * Set to `-1` to allow any time triggering click event.
     *
     * When input is active for more than the configured time, click even is not detected and is
     * ignored.
     *
     */
    uint16_t time_click_pressed_max; /*!< Maximum pressed time for valid click event*/

    /**
     * \brief           Maximum allowed time between last on-release and next valid on-press,
     *                  to still allow multi-click events, in milliseconds
     *
     * This value is also used as a timeout length to send the *onclick* event to application from
     * previously detected valid click events.
     *
     * If application relies on multi consecutive clicks, this is the max time to allow user
     * to trigger potential new click, or structure will get reset (before sent to user if any
     * clicks have been detected so far)
     *
     */
    uint16_t time_click_multi_max; /*!< Maximum time between 2 clicks to be considered consecutive
                                      click */

    /**
     * \brief           Keep-alive event period, in milliseconds
     *
     * When input is active, keep alive events will be sent through this period of time.
     * First keep alive will be sent after input being considered
     * active.
     *
     */
    uint16_t time_keepalive_period; /*!< Time in ms for periodic keep alive event */

    /**
     * \brief           Maximum number of allowed consecutive click events,
     *                  before structure gets reset to default value.
     *
     * \note            When consecutive value is reached, application will get notification of
     * clicks. This can be executed immediately after last click has been detected, or after
     * standard timeout (unless next on-press has already been detected, then it is send to
     * application just before valid next press event).
     *
     */
    uint16_t max_consecutive; /*!< Max number of consecutive clicks */
} ebtn_btn_param_t;

按键控制结构体说明-ebtn_btn_t

每个按键有一个管理结构体,用于记录按键当前状态,按键参数等信息。

名称 说明
key_id 用户定义的key_id信息,该值建议唯一
flags 用于记录一些状态,目前只支持EBTN_FLAG_ONPRESS_SENTEBTN_FLAG_IN_PROCESS
time_change 记录按键按下或者松开状态的时间点
time_state_change 记录按键状态切换时间点(并不考虑防抖,单纯记录状态切换时间点)
keepalive_last_time 长按最后一次上报长按时间的时间点,用于管理keepalive_cnt
click_last_time 点击最后一次松开状态的时间点,用于管理click_cnt
keepalive_cnt 长按的KEEP_ALIVE次数
click_cnt 多击的次数
param 按键时间参数,指向ebtn_btn_param_t,方便节省RAM,并且多个按键可公用一组参数
typedef struct ebtn_btn
{
    uint16_t key_id;         /*!< User defined custom argument for callback function purpose */
    uint16_t flags;          /*!< Private button flags 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;

组合按键控制结构体说明-ebtn_btn_combo_t

每个组合按键有一个管理结构体,用于记录组合按键组合配置参数,以及按键信息。

名称 说明
comb_key 用独立按键的key_idx设置的BitArray
btn ebtn_btn_t管理对象,管理按键状态
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_btn_dyn_t

动态注册需要维护一个列表,所以需要一个next指针。

名称 说明
next 用于链表链接每个节点
btn ebtn_btn_t管理对象,管理按键状态
typedef struct ebtn_btn_dyn
{
    struct ebtn_btn_dyn *next;

    ebtn_btn_t btn;
} ebtn_btn_dyn_t;

动态注册组合按键控制结构体说明-ebtn_btn_combo_dyn_t

动态注册需要维护一个列表,所以需要一个next指针。

名称 说明
next 用于链表链接每个节点
btn ebtn_btn_combo_t管理对象,管理组合按键状态
typedef struct ebtn_btn_combo_dyn
{
    struct ebtn_btn_combo_dyn *next; /*!< point to next combo-button */

    ebtn_btn_combo_t btn;
} ebtn_btn_combo_dyn_t;

按键驱动管理结构体-ebtn_t

按键驱动需要管理所有静态注册和动态注册的按键和组合按键信息,并且记录接口以及最后的按键状态。

名称 说明
btns 管理静态注册按键的指针
btns_cnt 记录静态注册按键的个数
btns_combo 管理静态注册组合按键的指针
btns_combo_cnt 记录静态注册组合按键的个数
btn_dyn_head 管理动态注册按键的列表指针
btn_combo_dyn_head 管理动态注册组合按键的列表指针
evt_fn 事件上报的回调接口
get_state_fn 按键状态获取的回调接口
old_state 记录按键上一次状态
typedef struct ebtn
{
    ebtn_btn_t *btns;             /*!< Pointer to buttons array */
    uint16_t btns_cnt;            /*!< Number of buttons in array */
    ebtn_btn_combo_t *btns_combo; /*!< Pointer to comb-buttons array */
    uint16_t btns_combo_cnt;      /*!< Number of comb-buttons in array */

    ebtn_btn_dyn_t *btn_dyn_head;             /*!< Pointer to btn-dynamic list */
    ebtn_btn_combo_dyn_t *btn_combo_dyn_head; /*!< Pointer to btn-combo-dynamic list */

    ebtn_evt_fn evt_fn;             /*!< Pointer to event function */
    ebtn_get_state_fn get_state_fn; /*!< Pointer to get state function */

    BIT_ARRAY_DEFINE(
            old_state,
            EBTN_MAX_KEYNUM); /*!< Old button state - `1` means active, `0` means inactive */
} ebtn_t;

操作API

核心API

主要的就是初始化和运行接口,加上动态注册接口。

void ebtn_process(ebtn_time_t mstime);
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);
int ebtn_register(ebtn_btn_dyn_t *button);
int ebtn_combo_register(ebtn_btn_combo_dyn_t *button);

组合按键注册key的API

用于给组合按键绑定btn使用,最终都是关联到key_idx上。

注意key_id注册接口必需先确保对应的Button已经注册到驱动中。

void ebtn_combo_btn_add_btn_by_idx(ebtn_btn_combo_t *btn, int idx);
void ebtn_combo_btn_remove_btn_by_idx(ebtn_btn_combo_t *btn, int idx);
void ebtn_combo_btn_add_btn(ebtn_btn_combo_t *btn, uint16_t key_id);
void ebtn_combo_btn_remove_btn(ebtn_btn_combo_t *btn, uint16_t key_id);

其他API

一些工具函数,按需使用。

void ebtn_process_with_curr_state(bit_array_t *curr_state, ebtn_time_t mstime);

int ebtn_get_total_btn_cnt(void);
int ebtn_get_btn_index_by_key_id(uint16_t key_id);
ebtn_btn_t *ebtn_get_btn_by_key_id(uint16_t key_id);
int ebtn_get_btn_index_by_btn(ebtn_btn_t *btn);
int ebtn_get_btn_index_by_btn_dyn(ebtn_btn_dyn_t *btn);

int ebtn_is_btn_active(const ebtn_btn_t *btn);
int ebtn_is_btn_in_process(const ebtn_btn_t *btn);
int ebtn_is_in_process(void);

其中ebtn_is_in_process()可以用于超低功耗业务场景,这时候MCU只有靠IO翻转唤醒。

按键核心处理逻辑说明

这里参考用户手册 — LwBTN 文档 (majerle.eu)对本驱动的按键实现机制进行说明。

在驱动运行中,应用程序可以会接收到如下事件:

  • EBTN_EVT_ONPRESS(简称:ONPRESS),每当输入从非活动状态变为活动状态并且最短去抖动时间过去时,都会将事件发送到应用程序
  • EBTN_EVT_ONRELEASE(简称:ONRELEASE),每当输入发送 ONPRESS事件时,以及当输入从活动状态变为非活动状态时,都会将事件发送到应用程序
  • EBTN_EVT_KEEPALIVE(简称:KEEPALIVE),事件在 ONPRESSONRELEASE事件之间定期发送
  • EBTN_EVT_ONCLICK(简称:ONCLICK),事件在ONRELEASE后发送,并且仅当活动按钮状态在有效单击事件的允许窗口内时发送。

ONPRESS事件

ONPRESS 事件是检测到按键处于活动状态时的第一个事件。 由于嵌入式系统的性质和连接到设备的各种按钮,有必要过滤掉潜在的噪音,以忽略无意的多次按下。 这是通过检查输入至少在一些最短时间内处于稳定水平来完成的,通常称为消抖时间,通常需要大约20ms

按键消抖时间分为按下消抖时间time_debounce和松开消抖时间time_debounce_release

image-20240223135908798

ONRELEASE事件

当按键从活动状态变为非活动状态时,才会立即触发 ONRELEASE事件,前提是在此之前检测到ONPRESS 事件。也就是 ONRELEASE事件是伴随着ONPRESS 事件发生的。

image-20240223143215840

ONCLICK事件

ONCLICK事件在多个事件组合后触发:

  • 应正确检测到ONPRESS 事件,表示按钮已按下
  • 应检测到ONRELEASE事件,表示按钮已松开
  • ONPRESSONRELEASE事件之间的时间必须在时间窗口内,也就是在time_click_pressed_mintime_click_pressed_max之间时。

当满足条件时,在ONRELEASE事件之后的time_click_multi_max时间,发送ONCLICK事件。

image-20240223143426179

下面显示了在 Windows 测试下的单击事件演示。

image-20240223173405665

Multi-Click事件

实际需求除了单击需求外,还需要满足多击需求。本驱动是靠time_click_multi_max来满足此功能,虽然有多次点击,但是只发送一次 ONCLICK事件

注意:想象一下,有一个按钮可以在单击时切换一盏灯,并在双击时关闭房间中的所有灯。 通过超时功能和单次点击通知,用户将只收到一次点击,并且会根据连续按压次数值,来执行适当的操作。

下面是Multi-Click的简化图,忽略了消抖时间。click_cnt表示检测到的Multi-Click 事件数,将在最终的ONCLICK事件中上报。

需要注意前一个按键的ONRELEASE事件和下次的ONPRESS事件间隔时间应小于time_click_multi_maxONCLICK事件会在最后一次按键的ONRELEASE事件之后time_click_multi_max时间上报。

image-20240223144701688

下面显示了在 Windows 测试下的三击事件演示。

image-20240223173435824

KEEPALIVE事件

KEEPALIVE事件在 ONPRESS事件和ONRELEASE事件之间定期发送,它可用于长按处理,根据过程中有多少KEEPALIVE事件以及time_keepalive_period可以实现各种复杂的长按功能需求。

需要注意这里根据配置的时间参数的不同,可能会出现KEEPALIVE事件和ONCLICK事件在一次按键事件都上报的情况。这个情况一般发生在按下保持时间(ONPRESS事件和ONRELEASE事件之间)大于time_keepalive_period却小于time_click_pressed_max的场景下。

image-20240223173042135

下面显示了在 Windows 测试下的KEEPALIVE事件和ONCLICK事件在一次按键事件出现的演示。

image-20240223173002558

而当按下保持时间大于time_click_pressed_max时,就不会上报ONCLICK事件,如下图所示。

image-20240227112945641

其他边界场景

example_test.c中对一些场景进行了覆盖性测试,具体可以看代码实现,测试都符合预期。

注意:time_overflow相关的case需要EBTN_CONFIG_TIMER_16宏,不然测试时间太长了。

测试说明

环境搭建

目前测试暂时只支持Windows编译,最终生成exe,可以直接在PC上跑。

目前需要安装如下环境:

编译说明

本项目都是由makefile组织编译的,编译整个项目只需要执行make all即可。

也就是可以通过如下指令来编译工程:

make all

而后运行执行make run即可运行例程,例程默认运行测试例程,覆盖绝大多数场景,从结果上看测试通过。

PS D:\workspace\github\easy_button> make run
Building   : "output/main.exe"
Start Build Image.
objcopy -v -O binary output/main.exe output/main.bin
copy from `output/main.exe' [pei-i386] to `output/main.bin' [binary]
objdump --source --all-headers --demangle --line-numbers --wide output/main.exe > output/main.lst
Print Size
   text    data     bss     dec     hex filename
  49616    6572    2644   58832    e5d0 output/main.exe
./output/main.exe
Test running
[     20][    20] ID(hex):   0, evt:   ONPRESS, keep-alive cnt:   0, click cnt:   0
[     42][    22] ID(hex):   0, evt: ONRELEASE, keep-alive cnt:   0, click cnt:   0
[    242][   200] ID(hex):   0, evt:   ONCLICK, keep-alive cnt:   0, click cnt:   1
Testing test_sequence_single_click ......................................... pass
[     20][    20] ID(hex):   0, evt:   ONPRESS, keep-alive cnt:   0, click cnt:   0
[     42][    22] ID(hex):   0, evt: ONRELEASE, keep-alive cnt:   0, click cnt:   0
[    163][   121] ID(hex):   0, evt:   ONPRESS, keep-alive cnt:   0, click cnt:   1
[    184][    21] ID(hex):   0, evt: ONRELEASE, keep-alive cnt:   0, click cnt:   1
[    384][   200] ID(hex):   0, evt:   ONCLICK, keep-alive cnt:   0, click cnt:   2
Testing test_sequence_double_click ......................................... pass
[     20][    20] ID(hex):   0, evt:   ONPRESS, keep-alive cnt:   0, click cnt:   0
[     42][    22] ID(hex):   0, evt: ONRELEASE, keep-alive cnt:   0, click cnt:   0
[    163][   121] ID(hex):   0, evt:   ONPRESS, keep-alive cnt:   0, click cnt:   1
[    184][    21] ID(hex):   0, evt: ONRELEASE, keep-alive cnt:   0, click cnt:   1
[    305][   121] ID(hex):   0, evt:   ONPRESS, keep-alive cnt:   0, click cnt:   2
[    326][    21] ID(hex):   0, evt: ONRELEASE, keep-alive cnt:   0, click cnt:   2
[    526][   200] ID(hex):   0, evt:   ONCLICK, keep-alive cnt:   0, click cnt:   3
Testing test_sequence_triple_click ......................................... pass
[     20][    20] ID(hex):   0, evt:   ONPRESS, keep-alive cnt:   0, click cnt:   0
[     42][    22] ID(hex):   0, evt: ONRELEASE, keep-alive cnt:   0, click cnt:   0
[    241][   199] ID(hex):   0, evt:   ONPRESS, keep-alive cnt:   0, click cnt:   1
[    262][    21] ID(hex):   0, evt: ONRELEASE, keep-alive cnt:   0, click cnt:   1
[    462][   200] ID(hex):   0, evt:   ONCLICK, keep-alive cnt:   0, click cnt:   2
Testing test_sequence_double_click_critical_time ........................... pass
[     20][    20] ID(hex):   0, evt:   ONPRESS, keep-alive cnt:   0, click cnt:   0
[     42][    22] ID(hex):   0, evt: ONRELEASE, keep-alive cnt:   0, click cnt:   0
[    243][   201] ID(hex):   0, evt:   ONCLICK, keep-alive cnt:   0, click cnt:   1
[    243][     0] ID(hex):   0, evt:   ONPRESS, keep-alive cnt:   0, click cnt:   0
[    264][    21] ID(hex):   0, evt: ONRELEASE, keep-alive cnt:   0, click cnt:   0
[    464][   200] ID(hex):   0, evt:   ONCLICK, keep-alive cnt:   0, click cnt:   1
Testing test_sequence_double_click_critical_time_over ...................... pass
[     20][    20] ID(hex):   0, evt:   ONPRESS, keep-alive cnt:   0, click cnt:   0
[     42][    22] ID(hex):   0, evt: ONRELEASE, keep-alive cnt:   0, click cnt:   0
[    163][   121] ID(hex):   0, evt:   ONPRESS, keep-alive cnt:   0, click cnt:   1
[    464][   301] ID(hex):   0, evt:   ONCLICK, keep-alive cnt:   0, click cnt:   1
[    663][   199] ID(hex):   0, evt: KEEPALIVE, keep-alive cnt:   1, click cnt:   0
[   1163][   500] ID(hex):   0, evt: KEEPALIVE, keep-alive cnt:   2, click cnt:   0
[   1663][   500] ID(hex):   0, evt: KEEPALIVE, keep-alive cnt:   3, click cnt:   0
[   1667][     4] ID(hex):   0, evt: ONRELEASE, keep-alive cnt:   3, click cnt:   0
Testing test_sequence_click_with_keepalive ................................. pass
[     20][    20] ID(hex):   0, evt:   ONPRESS, keep-alive cnt:   3, click cnt:   0
[     32][    12] ID(hex):   0, evt: ONRELEASE, keep-alive cnt:   0, click cnt:   0
Testing test_sequence_click_with_short ..................................... pass
[     20][    20] ID(hex):   0, evt:   ONPRESS, keep-alive cnt:   0, click cnt:   0
[     32][    12] ID(hex):   0, evt: ONRELEASE, keep-alive cnt:   0, click cnt:   0
[    153][   121] ID(hex):   0, evt:   ONPRESS, keep-alive cnt:   0, click cnt:   0
[    174][    21] ID(hex):   0, evt: ONRELEASE, keep-alive cnt:   0, click cnt:   0
[    295][   121] ID(hex):   0, evt:   ONPRESS, keep-alive cnt:   0, click cnt:   1
[    316][    21] ID(hex):   0, evt: ONRELEASE, keep-alive cnt:   0, click cnt:   1
[    516][   200] ID(hex):   0, evt:   ONCLICK, keep-alive cnt:   0, click cnt:   2
Testing test_sequence_click_with_short_with_multi .......................... pass
[     20][    20] ID(hex):   0, evt:   ONPRESS, keep-alive cnt:   0, click cnt:   0
[     42][    22] ID(hex):   0, evt: ONRELEASE, keep-alive cnt:   0, click cnt:   0
[    163][   121] ID(hex):   0, evt:   ONPRESS, keep-alive cnt:   0, click cnt:   1
[    184][    21] ID(hex):   0, evt: ONRELEASE, keep-alive cnt:   0, click cnt:   1
[    305][   121] ID(hex):   0, evt:   ONPRESS, keep-alive cnt:   0, click cnt:   2
[    316][    11] ID(hex):   0, evt: ONRELEASE, keep-alive cnt:   0, click cnt:   2
[    316][     0] ID(hex):   0, evt:   ONCLICK, keep-alive cnt:   0, click cnt:   2
Testing test_sequence_multi_click_with_short ............................... pass
[     60][    60] ID(hex):   0, evt:   ONPRESS, keep-alive cnt:   0, click cnt:   0
[     81][    21] ID(hex):   0, evt: ONRELEASE, keep-alive cnt:   0, click cnt:   0
[    281][   200] ID(hex):   0, evt:   ONCLICK, keep-alive cnt:   0, click cnt:   1
Testing test_sequence_onpress_debounce ..................................... pass
[  65547][ 65547] ID(hex):   0, evt:   ONPRESS, keep-alive cnt:   0, click cnt:   0
[  65568][    21] ID(hex):   0, evt: ONRELEASE, keep-alive cnt:   0, click cnt:   0
[  65768][   200] ID(hex):   0, evt:   ONCLICK, keep-alive cnt:   0, click cnt:   1
Testing test_sequence_time_overflow_onpress_debounce ....................... pass
[  65527][ 65527] ID(hex):   0, evt:   ONPRESS, keep-alive cnt:   0, click cnt:   0
[  65548][    21] ID(hex):   0, evt: ONRELEASE, keep-alive cnt:   0, click cnt:   0
[  65748][   200] ID(hex):   0, evt:   ONCLICK, keep-alive cnt:   0, click cnt:   1
Testing test_sequence_time_overflow_onpress ................................ pass
[  65507][ 65507] ID(hex):   0, evt:   ONPRESS, keep-alive cnt:   0, click cnt:   0
[  65528][    21] ID(hex):   0, evt: ONRELEASE, keep-alive cnt:   0, click cnt:   0
[  65728][   200] ID(hex):   0, evt:   ONCLICK, keep-alive cnt:   0, click cnt:   1
Testing test_sequence_time_overflow_onrelease_muti ......................... pass
[  65267][ 65267] ID(hex):   0, evt:   ONPRESS, keep-alive cnt:   0, click cnt:   0
[  65767][   500] ID(hex):   0, evt: KEEPALIVE, keep-alive cnt:   1, click cnt:   0
[  65789][    22] ID(hex):   0, evt: ONRELEASE, keep-alive cnt:   1, click cnt:   0
Testing test_sequence_time_overflow_keepalive .............................. pass
[     20][    20] ID(hex):   1, evt:   ONPRESS, keep-alive cnt:   0, click cnt:   0
[    164][   144] ID(hex):   1, evt: ONRELEASE, keep-alive cnt:   0, click cnt:   0
[    364][   200] ID(hex):   1, evt:   ONCLICK, keep-alive cnt:   0, click cnt:   1
Testing test_sequence_onrelease_debounce ................................... pass
[     20][    20] ID(hex):   1, evt:   ONPRESS, keep-alive cnt:   0, click cnt:   0
[    102][    82] ID(hex):   1, evt: ONRELEASE, keep-alive cnt:   0, click cnt:   0
[    123][    21] ID(hex):   1, evt:   ONPRESS, keep-alive cnt:   0, click cnt:   1
[    204][    81] ID(hex):   1, evt: ONRELEASE, keep-alive cnt:   0, click cnt:   1
[    404][   200] ID(hex):   1, evt:   ONCLICK, keep-alive cnt:   0, click cnt:   2
Testing test_sequence_onrelease_debounce_over .............................. pass
[  65497][ 65497] ID(hex):   1, evt:   ONPRESS, keep-alive cnt:   0, click cnt:   0
[  65578][    81] ID(hex):   1, evt: ONRELEASE, keep-alive cnt:   0, click cnt:   0
[  65778][   200] ID(hex):   1, evt:   ONCLICK, keep-alive cnt:   0, click cnt:   1
Testing test_sequence_onrelease_debounce_time_overflow ..................... pass
[     20][    20] ID(hex):   2, evt:   ONPRESS, keep-alive cnt:   0, click cnt:   0
[    120][   100] ID(hex):   2, evt: KEEPALIVE, keep-alive cnt:   1, click cnt:   0
[    203][    83] ID(hex):   2, evt: ONRELEASE, keep-alive cnt:   1, click cnt:   0
[    403][   200] ID(hex):   2, evt:   ONCLICK, keep-alive cnt:   1, click cnt:   1
Testing test_sequence_keepalive_with_click ................................. pass
[     20][    20] ID(hex):   2, evt:   ONPRESS, keep-alive cnt:   1, click cnt:   0
[    120][   100] ID(hex):   2, evt: KEEPALIVE, keep-alive cnt:   1, click cnt:   0
[    220][   100] ID(hex):   2, evt: KEEPALIVE, keep-alive cnt:   2, click cnt:   0
[    304][    84] ID(hex):   2, evt: ONRELEASE, keep-alive cnt:   2, click cnt:   0
[    504][   200] ID(hex):   2, evt:   ONCLICK, keep-alive cnt:   2, click cnt:   1
Testing test_sequence_keepalive_with_click_double .......................... pass
[     20][    20] ID(hex):   3, evt:   ONPRESS, keep-alive cnt:   0, click cnt:   0
[    102][    82] ID(hex):   3, evt: ONRELEASE, keep-alive cnt:   0, click cnt:   0
[    223][   121] ID(hex):   3, evt:   ONPRESS, keep-alive cnt:   0, click cnt:   1
[    304][    81] ID(hex):   3, evt: ONRELEASE, keep-alive cnt:   0, click cnt:   1
[    425][   121] ID(hex):   3, evt:   ONPRESS, keep-alive cnt:   0, click cnt:   2
[    506][    81] ID(hex):   3, evt: ONRELEASE, keep-alive cnt:   0, click cnt:   2
[    506][     0] ID(hex):   3, evt:   ONCLICK, keep-alive cnt:   0, click cnt:   3
Testing test_sequence_max_click_3 .......................................... pass
[     20][    20] ID(hex):   3, evt:   ONPRESS, keep-alive cnt:   0, click cnt:   0
[    102][    82] ID(hex):   3, evt: ONRELEASE, keep-alive cnt:   0, click cnt:   0
[    223][   121] ID(hex):   3, evt:   ONPRESS, keep-alive cnt:   0, click cnt:   1
[    304][    81] ID(hex):   3, evt: ONRELEASE, keep-alive cnt:   0, click cnt:   1
[    425][   121] ID(hex):   3, evt:   ONPRESS, keep-alive cnt:   0, click cnt:   2
[    506][    81] ID(hex):   3, evt: ONRELEASE, keep-alive cnt:   0, click cnt:   2
[    506][     0] ID(hex):   3, evt:   ONCLICK, keep-alive cnt:   0, click cnt:   3
[    627][   121] ID(hex):   3, evt:   ONPRESS, keep-alive cnt:   0, click cnt:   0
[    708][    81] ID(hex):   3, evt: ONRELEASE, keep-alive cnt:   0, click cnt:   0
[    908][   200] ID(hex):   3, evt:   ONCLICK, keep-alive cnt:   0, click cnt:   1
Testing test_sequence_max_click_3_over ..................................... pass
[     20][    20] ID(hex):   4, evt:   ONPRESS, keep-alive cnt:   0, click cnt:   0
[    102][    82] ID(hex):   4, evt: ONRELEASE, keep-alive cnt:   0, click cnt:   0
[    123][    21] ID(hex):   4, evt:   ONCLICK, keep-alive cnt:   0, click cnt:   1
[    123][     0] ID(hex):   4, evt:   ONPRESS, keep-alive cnt:   0, click cnt:   0
[    204][    81] ID(hex):   4, evt: ONRELEASE, keep-alive cnt:   0, click cnt:   0
[    225][    21] ID(hex):   4, evt:   ONCLICK, keep-alive cnt:   0, click cnt:   1
[    225][     0] ID(hex):   4, evt:   ONPRESS, keep-alive cnt:   0, click cnt:   0
[    306][    81] ID(hex):   4, evt: ONRELEASE, keep-alive cnt:   0, click cnt:   0
[    307][     1] ID(hex):   4, evt:   ONCLICK, keep-alive cnt:   0, click cnt:   1
Testing test_sequence_click_multi_max_0 .................................... pass
[     20][    20] ID(hex):   5, evt:   ONPRESS, keep-alive cnt:   0, click cnt:   0
[    303][   283] ID(hex):   5, evt: ONRELEASE, keep-alive cnt:   0, click cnt:   0
[    503][   200] ID(hex):   5, evt:   ONCLICK, keep-alive cnt:   0, click cnt:   1
Testing test_sequence_keep_alive_0 ......................................... pass
Executing 'run: all' complete!

当然可以用windows的按键进行交互测试,详见example_user.c的处理,main.c选择调用example_user()

About

嵌入式按键处理驱动(Button Driver),支持单击、双击、多击、自动消抖、长按、长长按、超长按 | 低功耗支持 | 组合按键支持 | 静态/动态注册支持

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published
md是什么牌子 emoji是什么意思 游泳有什么好处 仁爱是什么意思 装可以组什么词
白芷炖肉起什么作用 apd是什么意思 巴郎子是什么意思 布朗尼是什么 洛神花是什么
graff是什么牌子 净化心灵是什么意思 强回声斑块是什么意思 肺纤维化是什么症状 判处死刑缓期二年执行是什么意思
两肺纹理增多什么意思 乙丑是什么生肖 为所当为什么意思 美女胸部长什么样 一诺千金是什么生肖
天之骄子是什么意思hcv9jop7ns1r.cn 火车动车高铁有什么区别hcv8jop6ns8r.cn 大腿肌肉跳动是什么原因hcv8jop6ns8r.cn 什么品种的西瓜最好吃hcv7jop6ns2r.cn 消化不好吃什么药hcv9jop3ns4r.cn
马女和什么属相最配hcv8jop6ns0r.cn 格格不入什么意思hcv8jop2ns2r.cn 酸奶什么时候喝最好hcv9jop2ns2r.cn 效应什么意思sanhestory.com 知了叫什么cl108k.com
慈是什么意思hcv9jop1ns6r.cn 乳腺增生什么意思hcv8jop9ns4r.cn 什么是白癜风sanhestory.com 79年出生属什么生肖hcv8jop8ns8r.cn 小便短赤什么意思jinxinzhichuang.com
8月8号是什么日子hcv9jop2ns3r.cn 甲状腺有血流信号是什么意思gangsutong.com 葡萄糖氯化钠注射作用是什么xjhesheng.com 男人跑马是什么意思hcv9jop6ns7r.cn 脸上长痣是什么原因hcv8jop6ns3r.cn
百度