|
本帖最后由 dxgdsx 于 2018-9-24 19:41 编辑
论坛已有几位大神发布了关于按键处理的C代码,写的都非常好,本人很受启发。
比如https://www.amobbs.com/forum.php ... =%E6%8C%89%E9%94%AE
中秋闲来无事,我也自己动手写一写。
事先声明:本代码参考了论坛里面几位大神的开源代码,以及STM32库函数代码风格,此外也加入了个人的一点想法,欢迎批评指正!
首先简单单片机系统中,常见的按键操作有短按(点击)、长按、连击。
个人对“连击”操作的定义并不是很清晰,个人理解是在长按基础上继续按着,然后超过一段时间视为连击1次,...,直到弹起。
下图是个人对按键操作的理解:
那么源代码中定义一些操作判定用的时间常数:
- //建议10ms扫描一次
- //这些常数大小,可以根据项目实际需求进行修改
- //按键按下消抖计数
- #define KEY_PRESS_DB_CNT 3
- //判断按键长按计数
- #define KEY_LONG_PRESS_CNT 150
- //判断按键连击计数
- #define KEY_REPEAT_PRESS_CNT 50
- //按键释放消抖计数
- #define KEY_RELEASE_DB_CNT 3
复制代码
设计了一个按键扫描处理状态机,共有5个状态编码:
- //按键扫描状态机状态编码
- typedef enum
- {
- //按键状态机:“按键等待被按下”处理
- KEY_FSM_WAIT_PRESS = 0,
- //按键状态机:“按键短按”处理
- KEY_FSM_SHORT_PRESS,
- //按键状态机:“按键长按”处理
- KEY_FSM_LONG_PRESS,
- //按键状态机:“按键连击”处理
- KEY_FSM_REPEAT_PRESS,
- //按键状态机:“按键释放”处理
- KEY_FSM_RELEASE
- }KEY_FsmTypeDef;
复制代码
定义按键的不同,共有6个状态编码:
- //按键状态编码
- typedef enum
- {
- //按键状态:释放状态
- KEY_RELEASED = 0,
- //按键状态:短按状态
- KEY_SHORT_PRESSED,
- //按键状态:等待长按状态
- KEY_LONG_PRESS_WAIT,
- //按键状态:长按状态
- KEY_LONG_PRESSED,
- //按键状态:等待连击状态
- KEY_REPEAT_PRESS_WAIT,
- //按键状态:一次连击状态
- KEY_REPEAT_PRESSED
- }KEY_StateTypeDef;
复制代码
定义一个独立按键的按键句柄结构体:
- //按键句柄结构体
- typedef struct
- {
- uint8_t KeyId; //按键编号
- KEY_StateTypeDef KeyState; //按键当前状态(高4bit为上一个状态,低4bit为当前状态)
- uint16_t KeyCnt; //内部计数器(高2bit给按下/弹出计数用,低14bit给长按/连击计数用)
- KEY_FsmTypeDef KeyFsmState; //状态机状态
- }KEY_HandleTypeDef;
复制代码
声明一些函数:
- //按键端口读取函数声明,需要用户根据硬件情况自行实现
- uint8_t KEY_Read_Port(uint8_t KeyId);
- //按键初始化函数声明
- void KEY_Init(void);
- //按键扫描函数声明
- void KEY_Scan(KEY_HandleTypeDef* keyHandle);
- //按键状态获取函数声明
- KEY_StateTypeDef KEY_Get_State(KEY_HandleTypeDef* keyHandle);
复制代码
其中,关键函数是KEY_Scan函数,事实上其它几个函数无关紧要,存在更多只是为了形式上的完整
KEY_Scan函数就是一个状态机处理函数,源代码如下:
- /**
- * @brief 单个按键处理函数,需要被周期性调用,建议10ms扫描一次
- * @param 指定单个按键的句柄
- * @retval None
- */
- void KEY_Scan(KEY_HandleTypeDef* keyHandle)
- {
- //读取按键端口电平
- uint8_t IsKeyDown = KEY_Read_Port(keyHandle->KeyId);
-
- //获取状态机的当前状态
- switch(keyHandle->KeyFsmState)
- {
- case KEY_FSM_WAIT_PRESS:
- if(IsKeyDown) //检测到按键按下
- {
- keyHandle->KeyCnt = KEY_FULL_CNT(KEY_HIGH_CNT(keyHandle->KeyCnt)+1, 0);
- //按键按下消抖:连续几次检测到按键按下,可以确认按键按下
- if(KEY_HIGH_CNT(keyHandle->KeyCnt) >= KEY_PRESS_DB_CNT)
- {
- keyHandle->KeyCnt = 0; //复位计数器,状态跳转
- keyHandle->KeyFsmState = KEY_FSM_SHORT_PRESS;
- keyHandle->KeyState = KEY_FULL_STATE(KEY_RELEASED, KEY_SHORT_PRESSED);
- }
- }
- else
- keyHandle->KeyCnt = 0; //检测到按键释放,复位计数器
- break;
-
- case KEY_FSM_SHORT_PRESS:
- keyHandle->KeyState = KEY_FULL_STATE(KEY_SHORT_PRESSED, KEY_LONG_PRESS_WAIT);
- if(!IsKeyDown) //检测到按键释放
- {
- keyHandle->KeyCnt = KEY_FULL_CNT(KEY_HIGH_CNT(keyHandle->KeyCnt)+1, 0);
- //按键释放消抖:连续几次检测到按键释放,可以确认按键释放
- if(KEY_HIGH_CNT(keyHandle->KeyCnt) >= KEY_RELEASE_DB_CNT)
- {
- keyHandle->KeyCnt = 0; //复位计数器,状态跳转
- keyHandle->KeyFsmState = KEY_FSM_RELEASE;
- keyHandle->KeyState = KEY_FULL_STATE(KEY_SHORT_PRESSED, KEY_RELEASED);
- }
- }
- else
- {
- keyHandle->KeyCnt = KEY_FULL_CNT(0, KEY_LOW_CNT(keyHandle->KeyCnt)+1);
- if(KEY_LOW_CNT(keyHandle->KeyCnt) >= KEY_LONG_PRESS_CNT) //可以确认按键长按
- {
- keyHandle->KeyCnt = 0; //复位计数器,状态跳转
- keyHandle->KeyFsmState = KEY_FSM_LONG_PRESS;
- keyHandle->KeyState = KEY_FULL_STATE(KEY_LONG_PRESS_WAIT, KEY_LONG_PRESSED);
- }
- }
- break;
-
- case KEY_FSM_LONG_PRESS:
- keyHandle->KeyState = KEY_FULL_STATE(KEY_LONG_PRESSED, KEY_REPEAT_PRESS_WAIT);
- if(!IsKeyDown) //检测到按键释放
- {
- keyHandle->KeyCnt = KEY_FULL_CNT(KEY_HIGH_CNT(keyHandle->KeyCnt)+1, 0);
- //按键释放消抖:连续几次检测到按键释放,可以确认按键释放
- if(KEY_HIGH_CNT(keyHandle->KeyCnt) >= KEY_RELEASE_DB_CNT)
- {
- keyHandle->KeyCnt = 0; //复位计数器,状态跳转
- keyHandle->KeyFsmState = KEY_FSM_RELEASE;
- keyHandle->KeyState = KEY_FULL_STATE(KEY_LONG_PRESSED, KEY_RELEASED);
- }
- }
- else
- {
- keyHandle->KeyCnt = KEY_FULL_CNT(0, KEY_LOW_CNT(keyHandle->KeyCnt)+1);
- if(KEY_LOW_CNT(keyHandle->KeyCnt) >= KEY_REPEAT_PRESS_CNT) //可以确认按键连击
- {
- keyHandle->KeyCnt = 0; //复位计数器,状态跳转
- keyHandle->KeyFsmState = KEY_FSM_REPEAT_PRESS;
- keyHandle->KeyState = KEY_FULL_STATE(KEY_REPEAT_PRESS_WAIT, KEY_REPEAT_PRESSED);
- }
- }
- break;
-
- case KEY_FSM_REPEAT_PRESS:
- keyHandle->KeyState = KEY_FULL_STATE(KEY_REPEAT_PRESSED, KEY_REPEAT_PRESS_WAIT);
- if(!IsKeyDown) //检测到按键释放
- {
- keyHandle->KeyCnt = KEY_FULL_CNT(KEY_HIGH_CNT(keyHandle->KeyCnt)+1, 0);
- //按键释放消抖:连续几次检测到按键释放,可以确认按键释放
- if(KEY_HIGH_CNT(keyHandle->KeyCnt) >= KEY_RELEASE_DB_CNT)
- {
- keyHandle->KeyCnt = 0; //复位计数器,状态跳转
- keyHandle->KeyFsmState = KEY_FSM_RELEASE;
- keyHandle->KeyState = KEY_FULL_STATE(KEY_REPEAT_PRESS_WAIT, KEY_RELEASED);
- }
- }
- else
- {
- keyHandle->KeyCnt = KEY_FULL_CNT(0, KEY_LOW_CNT(keyHandle->KeyCnt)+1);
- if(KEY_LOW_CNT(keyHandle->KeyCnt) >= KEY_REPEAT_PRESS_CNT) //可以确认按键连击
- {
- keyHandle->KeyCnt = 0; //复位计数器,状态跳转
- keyHandle->KeyFsmState = KEY_FSM_REPEAT_PRESS;
- keyHandle->KeyState = KEY_FULL_STATE(KEY_REPEAT_PRESS_WAIT, KEY_REPEAT_PRESSED);
- }
- }
- break;
-
- case KEY_FSM_RELEASE:
- keyHandle->KeyCnt = 0; //复位计数器,状态跳转
- keyHandle->KeyFsmState = KEY_FSM_WAIT_PRESS;
- keyHandle->KeyState = KEY_FULL_STATE(KEY_RELEASED, KEY_RELEASED);
- break;
- default:
- break;
- }
- }
复制代码
状态迁移图如下(部分默认分支没有画出):
KEY_Init函数是用于初始化按键对应的IO,若已有类似功能函数,则该函数就不需要再实现。
KEY_Read_Port函数是读取指定按键对应的IO口电平值,不需要消抖,一次读取即可,该函数需要用户根据硬件自行实现。
- /**
- * @brief 按键初始化操作,如端口配置、按键句柄结构体初始化等
- * @param None
- * @retval None
- */
- __weak void KEY_Init(void)
- {
- /*
- hkey.KeyId = KEY_ID_1;
- hkey.KeyReadFuncPtr = KeyReadFunc;
- hkey.KeyState = KEY_RELEASED;
- hkey.KeyCnt = 0;
- hkey.KeyFsmState = KEY_FSM_FSTATE(KEY_FSM_WAIT_PRESS, KEY_FSM_WAIT_PRESS);
- */
- }
- /**
- * @brief 读取指定按键对应的IO口电平值,不需要消抖,一次读取即可,该函数需要用户根据硬件自行实现
- * @param 按键的ID值,用户自行编码
- * @retval IO口电平值,本实现中1:按键按下,0:按键按下,
- */
- __weak uint8_t KEY_Read_Port(uint8_t KeyId)
- {
- /*
- //根据KeyId读取指定IO口电平值
- switch(KeyId)
- {
- case KEY_ID_1:
- return (HAL_GPIO_ReadPin(USEKEY_GPIO_Port, USEKEY_Pin));
- break;
- default:
- return 0;
- break;
- }
- */
- return 0;
- }
复制代码
另外,本代码中最重要的,或者说与其他实现方式不同的地方在于,按键动作识别。
关键在于按键句柄结构体中的KeyState成员,其高4bit为上一个状态,低4bit为当前状态,通过对这个组合字可以识别出按键的具体动作。
定义了短按、长按、连击的组合识别码:
- //按键状态组合字(8位),ls为上一个状态(高4位),cs为当前状态(低4位)
- #define KEY_FULL_STATE(ls, cs) (KEY_StateTypeDef)(((ls)<<4)|(cs))
- //按键的当前状态值,取按键状态组合字的低4位
- #define KEY_CURT_STATE(s) (KEY_StateTypeDef)((s)&0x0F)
- //按键上一个状态值,取按键状态组合字的高4位
- #define KEY_LAST_STATE(s) (KEY_StateTypeDef)(((s)&0xF0)>>4)
- //按键内部计数组合字(16位),h2用于按下/释放消抖计数(高2位),l14用于长按/连击计数(低14位)
- #define KEY_FULL_CNT(h2, l14) ((((h2)&0x0003)<<14)|((l14)&0x3FFF))
- //按键按下/释放消抖计数值
- #define KEY_LOW_CNT(c) ((c)&0x3FFF)
- //按键长按/连击计数值
- #define KEY_HIGH_CNT(c) (((c)&0xC000)>>14)
- //按键短按动作识别字
- #define KEY_SHORT_ACTION() KEY_FULL_STATE(KEY_SHORT_PRESSED, KEY_RELEASED)
- //按键长按动作识别字
- #define KEY_LONG_ACTION() KEY_FULL_STATE(KEY_LONG_PRESSED, KEY_RELEASED)
- //按键连击动作识别字
- #define KEY_REPEAT_ACTION() KEY_FULL_STATE(KEY_REPEAT_PRESS_WAIT, KEY_REPEAT_PRESSED)
复制代码
上述代码最后形成两个文件:key_driver.c和key_driver.h
如何使用该模块:
- /**
- * @brief main.c--以单个按键以例,仅把与本模块相关的代码写出,其余代码未写
- * @param
- * @retval
- */
- //包含其他头文件
- //包含按键处理模块头文件
- #include "key_driver.h"
- //定义一个按键句柄结构体,并进行初始化
- #define KEY_ID_1 1
- KEY_HandleTypeDef hkey = {KEY_ID_1, KEY_FULL_STATE(KEY_RELEASED, KEY_RELEASED), 0, KEY_FSM_WAIT_PRESS};
- /*若有多个按键,可以定义按键句柄结构体数组
- #define KEY_ID_1 1
- #define KEY_ID_2 2
- #define KEY_ID_3 3
- KEY_HandleTypeDef hkey[3] = {{KEY_ID_1, KEY_FULL_STATE(KEY_RELEASED, KEY_RELEASED), 0, KEY_FSM_WAIT_PRESS},
- {KEY_ID_2, KEY_FULL_STATE(KEY_RELEASED, KEY_RELEASED), 0, KEY_FSM_WAIT_PRESS},
- {KEY_ID_3, KEY_FULL_STATE(KEY_RELEASED, KEY_RELEASED), 0, KEY_FSM_WAIT_PRESS}};
- */
- //用户根据硬件情况自行实现指定按键的IO电平读取,该函数将覆盖模块中的同名函数
- uint8_t KEY_Read_Port(uint8_t KeyId)
- {
-
- switch(KeyId)
- {
- case KEY_ID_1:
- return (HAL_GPIO_ReadPin(USEKEY_GPIO_Port, USEKEY_Pin));
- break;
- default:
- return 0;
- break;
- }
- }
- //main函数
- int main(void)
- {
- //按键端口配置,若其他代码已对其进行了配置,则可忽略
- KEY_Init();
-
- //其余代码
-
- while(1)
- {
- //其余代码
-
- //周期性调用按键扫描
- KEY_Scan(&hkey);
-
- //按键动作识别
- if(KEY_SHORT_ACTION() == KEY_Get_State(&hkey)) //短按,点亮led
- HAL_GPIO_WritePin(LD3_GPIO_Port, LD3_Pin, GPIO_PIN_SET);
- if(KEY_LONG_ACTION() == KEY_Get_State(&hkey)) //长按,熄灭led
- HAL_GPIO_WritePin(LD3_GPIO_Port, LD3_Pin, GPIO_PIN_RESET);
- if(KEY_REPEAT_ACTION() == KEY_Get_State(&hkey)) //连击1次,led翻转1下
- HAL_GPIO_TogglePin(LD3_GPIO_Port, LD3_Pin);
-
- //扫描间隔10ms
- HAL_Delay(10);
- }
-
- return 0;
- }
复制代码
本按键代码模块在STM32L152 Discovery硬件上进行了验证,附源码供参考,欢迎批评指正!
|
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有帐号?注册
x
阿莫论坛20周年了!感谢大家的支持与爱护!!
如果天空是黑暗的,那就摸黑生存;
如果发出声音是危险的,那就保持沉默;
如果自觉无力发光,那就蜷伏于牆角。
但是,不要习惯了黑暗就为黑暗辩护;
也不要为自己的苟且而得意;
不要嘲讽那些比自己更勇敢的人。
我们可以卑微如尘土,但不可扭曲如蛆虫。
|