单片机状态机与定时器实现按键事件检测:短按,长按,单击,长单击(轮询方式)

    技术2022-07-14  78

    文章目录

    一、有限状态机(FSM)简介二、按键的状态机1. 按键的状态2. 按键的动作3. 按键的状态转换图2. 按键事件与定时器应用 三、状态图与事件四、程序设计1. 枚举变量:按键状态与事件2. 定义状态3. 事件的检测4. 事件处理(禁止/启用重复触发) 五、程序源码六、测试七、改进

    一、有限状态机(FSM)简介

    有限状态机FSM(Finite State Machine),通常指任意一个时刻在一种状态之中,不同状态的转移是通过动作来触发的,不同状态下,不同动作将触发不同的状态转移,当然也可以不发生转移。

    二、按键的状态机

    1. 按键的状态

    常用的按键只有2个状态,按下(press)和抬起(release)

    2. 按键的动作

    常用的按键只有2个动作:按下和抬起

    3. 按键的状态转换图

    当按键被按下时候,它的状态为按下,当按键松开时,它的状态为松开:

    2. 按键事件与定时器应用

    通常按键事件有短按和长按,本文定义了五种按键事件:

    事件(action)描述无事件按键没有动作发生按下按键被按下(按下时间<1s)短按(单击)按键被按下又抬起(按下时间<1s)长按(长单击)按键被按下又抬起(按下时间>1s)长按中按键被按下(按下时间>1s)

    可以发现,按键只有按下抬起两种动作和状态,但是可以产生五种事件,甚至可以更多,这些事件是根据按下的时长来区分的。所以,本文需要一个定时器,在按键第一次被按下时,记录时间,通过不断与后续的动作(按下或抬起)比较,即可区分各种状态

    三、状态图与事件

    如上所示,可综合绘制状态装换图与事件产生的条件:

    四、程序设计

    为简化程序,本文使用电容触摸模块,固不讨论按键抖动,程序也无滤波部分。另外,当电容被触摸时候,输出高电平,当电容未被触摸时,输出低电平。

    1. 枚举变量:按键状态与事件

    //按键状态 enum btn_sta{ RELEASE = 0, PRESSED = 1 }; //按键事件 enum { ACT_NO = 0, ACT_PRESS, ACT_SHORT_CLICKED, ACT_LONG_CLICKED, ACT_LONG_PRESSING, }_btn_evt;

    首先将按键状态与按键事件通过枚举变量的形式表达,提高程序的可读性。

    2. 定义状态

    static enum btn_sta _cur_sta = RELEASE, _last_sta = RELEASE;

    在状态机中,状态的转移需要当前状态,所以定义_cur_sta来存储,同时,事件的产生需要和上一状态对比,所以需要_last_sta来存储,在程序中,当一次读取按键结束后,当前的状态值,就成为了上次状态的值。

    3. 事件的检测

    关键变量定义完成以后,就需要具体的程序逻辑实现,通常状态机可以用swich case和if语句来实现:

    _cur_sta = (enum btn_sta)GET_BTN_STA(); //获取当前按键的动作,读取IO的值 if(_last_sta == RELEASE) //上次状态释放 { switch (_cur_sta) { case RELEASE: //当前状态为释放 _btn_evt = ACT_NO; //一直释放状态:无事件 break; case PRESSED: //当前状态为按下 _btn_evt = ACT_PRESS; //释放到按下:按下动作 time_last = HAL_GetTick(); //记录时刻 break; } } else if(_last_sta == PRESSED) //上次状态为抬起 { switch (_cur_sta) { case RELEASE: //当前状态为释放 if(HAL_GetTick() - time_last > 1000) { _btn_evt = ACT_LONG_CLICKED; //间隔<1s,短按(单击) } else { _btn_evt = ACT_SHORT_CLICKED; //间隔>1s,长按(长单击) } break; case PRESSED: //当前事件为按下 if(HAL_GetTick() - time_last > 1000) { _btn_evt = ACT_LONG_PRESSING; //事件间隔>1s,长按中 } break; } } _last_sta = _cur_sta; //本次状态更新为上一次状态, //为下次扫描做准备

    4. 事件处理(禁止/启用重复触发)

    当获取一次事件以后,需要对各个事件进行处理,首先需要定一个各个处理函数:

    void btn_evt_proc_short_click(void) { printf("%s\r\n",__FUNCTION__); } void btn_evt_proc_long_click(void) { printf("%s\r\n",__FUNCTION__); } void btn_evt_proc_long_pressing(void) { printf("%s\r\n",__FUNCTION__); } void btn_evt_proc_press(void) { printf("%s\r\n",__FUNCTION__); }

    接着使用switch语句可以很简单的处理各个事件:

    switch(_btn_evt) { case ACT_NO: break; case ACT_SHORT_CLICKED: btn_evt_proc_short_click(); break; case ACT_LONG_CLICKED: btn_evt_proc_long_click(); break; case ACT_PRESS: btn_evt_proc_press(); break; case ACT_LONG_PRESSING: btn_evt_proc_long_pressing(); break; } return;

    由于程序会不断的扫描按键,考虑一种情况:当用户按下按键时候,程序就会不断的检测到按下事件,用户若长时间不松开,程序还将一直检测到ACT_LONG_PRESSING(长按中)事件,那么程序将不断的调用btn_evt_proc_long_pressing();,若你需要在这个过程中,只调用一次函数,那么可以设计程序,禁止重复触发,注意到以下程序片段需要添加到按键事件处理之前:

    #define BAN_DECT_REPET 1 #if BAN_DECT_REPET uint8_t static last_evt = 0; if(last_evt == _btn_evt) { last_evt = _btn_evt; //若本次事件和上次相同 return; //程序返回 } last_evt = _btn_evt; #endif

    可以看到,当 BAN_DECT_REPET 为1时候,程序将会被程序,若2次事件一致,程序将返回(提前返回),此时,程序处理函数将不会被触发。

    五、程序源码

    程序完整源码(File:button.c):

    /****************************************************************************************** * @File: button.c * @Data:2020年7月2日 * @by :YonasLuo * @ver :1.0 ******************************************************************************************/ #include "button.h" #include "stdio.h" #include "main.h" #include "gpio.h" /****************************************************************************************** * @buttonCode ******************************************************************************************/ #define GET_BTN_STA() (HAL_GPIO_ReadPin(btn_GPIO_Port, btn_Pin)) #define BAN_DECT_REPET (1) //1:repet dectect 0: ban repet dectect /****************************************************************************************** * @API ******************************************************************************************/ void btn_evt_proc_short_click(void) { printf("%s\r\n",__FUNCTION__); } void btn_evt_proc_long_click(void) { printf("%s\r\n",__FUNCTION__); } void btn_evt_proc_long_pressing(void) { printf("%s\r\n",__FUNCTION__); } void btn_evt_proc_press(void) { printf("%s\r\n",__FUNCTION__); } /****************************************************************************************** * @buttonCode ******************************************************************************************/ enum btn_sta{ RELEASE = 0, PRESSED = 1 }; enum { ACT_NO = 0, ACT_PRESS, ACT_SHORT_CLICKED, ACT_LONG_CLICKED, ACT_LONG_PRESSING, }_btn_evt; static enum btn_sta _cur_sta = RELEASE, _last_sta = RELEASE; static uint16_t time_last = 0; void btn_proc_poll(void) { _cur_sta = (enum btn_sta)GET_BTN_STA(); if(_last_sta == RELEASE) { switch (_cur_sta) { case RELEASE: _btn_evt = ACT_NO; break; case PRESSED: _btn_evt = ACT_PRESS; time_last = HAL_GetTick(); break; } } else if(_last_sta == PRESSED) { switch (_cur_sta) { case RELEASE: if(HAL_GetTick() - time_last > 1000) { _btn_evt = ACT_LONG_CLICKED; } else { _btn_evt = ACT_SHORT_CLICKED; } break; case PRESSED: if(HAL_GetTick() - time_last > 1000) { _btn_evt = ACT_LONG_PRESSING; } break; } } _last_sta = _cur_sta; #if BAN_DECT_REPET uint8_t static last_evt = 0; if(last_evt == _btn_evt) { last_evt = _btn_evt; return; } last_evt = _btn_evt; #endif switch(_btn_evt) { case ACT_NO: break; case ACT_SHORT_CLICKED: btn_evt_proc_short_click(); break; case ACT_LONG_CLICKED: btn_evt_proc_long_click(); break; case ACT_PRESS: btn_evt_proc_press(); break; case ACT_LONG_PRESSING: btn_evt_proc_long_pressing(); break; } return; } /***************************** END OF FILE *****************************/

    六、测试

    ... void main(void) { .... extern void btn_proc_poll(void ); while (1) { btn_proc_poll(); } .... }

    测试函数时,在main()中不断调用处理函数btn_proc_poll()即可

    七、改进

    这个程序最明显的问题是移植不够简便:

    它使用了系统函数HAL_GetTick(),对于不同的工程,获取计数的函数通常是不同的,其值的单位也可能不是毫秒。用户需要在button.c 中添加自己的函数处理长按间隔时间未使用宏定义,若要修改间隔时间,需要每处都进行修改

    对于第1,第2个问题,通常可以使用函数指针,或者说回调函数来解决,其思路是设计一个指针,指向一个函数,这个指针在按键程序模块初始化的时候被传入,此时用户只需要将函数指针传入即可,无需改动此文件。这边降低了程序的耦合性。

    Processed: 0.013, SQL: 9