分享一个单片机按键状态机实现键值与功能解耦的方法
本帖最后由 dw772 于 2025-2-14 09:32 编辑1:搞单片机程序有好几年了,回想出入门时写程序时,对按键处理一直理解不太透彻,每做一个项目就需要写一遍按键函数,虽然能解决问题,但是一直存在一个问题,按键识别跟功能一直纠缠在一起,无法将功能与按键的键值解耦。 2:随着项目的增加和经验的积累,逐渐理解了按键状态机和函数指针后,参考一些前辈的思路,总结了一个比较好用的按键思路。
3:在裸机系统中,我们一般采用while(1){}大循环,所有功能按顺序执行。在while循环内一般有个定时器标志位,比如10ms执行一次控制函数执行节奏,同时也可以做一下非精确的定时,比如按键长按时间,指示灯闪烁周期。
先定义一个按键状态的枚举类型结构体
typedef enum
{
BT_EVENT_RELEASED = 0, /**< 按键已经释放 */
BT_EVENT_PRESSED, /**< 按键按下边沿 */
BT_EVENT_PRESSING, /**< 按键按住保持 */
BT_EVENT_SHORT_CLICKED, /**< 按键短按释放边沿 */
BT_EVENT_LONG_PRESSED, /**< 按键识别为长按边缘 press_cont ==最小长按时间 */
BT_EVENT_LONG_PRESSED_REPEAT, /**< 按键长按重复 press_cont -上次计数 == 重复时间 */
BT_EVENT_CLICKED, /**< 按键释放 长按/短按 */
BT_EVENT_DOUBLE_CLICKED, /**< 按键双击 两次间隙<= 500MS */
}bt_event_value_t;再定义一个按键类型参数,用来存放按键的各个参数typedef struct
{
flag_status (*read_gpio)(); //读取按键GPIO的函数指针返回值为TRUE& FALSE
bt_event_value_tbtn_event; //枚举为 BT_EVENT_RELEASED ~ BT_EVENT_DOUBLE_CLICKED任意值
uint8_t btn_stat; //按键有限状态机
uint8_t btn_double_en; //双击是否使能
uint8_t btn_act_sta; //有效状态
uint32_t press_cont; //按下计数,判断长短按
uint32_t idl_cont; //空闲计数,判断双击 组合按键需要另外处理
}TypeButton;根据需要几个按键定义几个按键变量,一个变量对应一个按键。该按键的所有数据都保存在这个变量内部。同时还需要定义一个数组存放这些变量的地址,方便扫描的时候顺序扫描按键。TypeButton *bt = { NULL}; //定义一个数组存放按键的地址,方便扫描按键的时候按顺序执行。
TypeButtonPower_key; //电源
TypeButtonLight_key; //灯
TypeButtonFunct_key; //功能
TypeButtonIncre_key; //增加
TypeButtonDecre_key; //减少还需要一个注册按键的函数,注册按键并初始化按键对应的变量,并将读取按键IO的函数指针存放到按键结构体变量内,函数原型如下。
/**
* @brief bsp_button_register按键注册,并初始化相应按键的参数
*
*
* @paramid 按键的顺序
* @param (*read_cb)() 按键读取回调函数 返回值为1表示按键按下
* @param*btn 指向 存放按键数值的变量指针
* @revalnone
*/
voidbsp_button_register( uint8_tid, flag_status (*read_cb)(), TypeButton *btn)
{
bt = btn;
btn->read_gpio = read_cb;
btn->btn_stat = 0;
btn->idl_cont = 0;
btn->btn_double_en = 0;
btn->press_cont = 0;
btn->btn_event = BT_EVENT_RELEASED;
}注册按键实例如下,在程序初始化时调用,注册之前先定义好读取按键GPIO的回调函数实体
<blockquote>void bsp_btn_register(void)下面需要用到按键状态机,主要是按下,按下等待,短按识别,长按识别等状态的切换。代码如下:void bsp_read_key_value( TypeButton *btn)
{
switch(btn->btn_stat)
{
case 0://空闲
//btn->press_cont = 0;
btn->btn_event= BT_EVENT_RELEASED;
if( btn->read_gpio())
{
if(btn->press_cont ++ >=2)
{
btn->btn_stat = 1;
}
}
break;
case 1://按下
if( btn->read_gpio())
{
btn->btn_event = BT_EVENT_PRESSED; //BT_EVENT_PRESSED;
btn->btn_stat = 2;
}
else
{
btn->btn_stat = 0;
}
break;
case 2://长按等待
btn->btn_event= BT_EVENT_PRESSING ; //按住不放
if( btn->read_gpio())
{
if( btn->press_cont++ > SHORT_CLICK_TIME )
{
btn->btn_event = BT_EVENT_LONG_PRESSED;
btn->btn_stat= 3;
}
}
else
{
btn->btn_stat= 3;
btn->btn_event = BT_EVENT_SHORT_CLICKED; //短按弹起
}
break;
case 3://长按
btn->press_cont++;
btn->btn_event = BT_EVENT_PRESSING;
if( (btn->press_cont-SHORT_CLICK_TIME)%REPEAT_PRESS_TIME ==0)
{
btn->btn_event = BT_EVENT_LONG_PRESSED_REPEAT;
}
if(!( btn->read_gpio() ))
{
btn->press_cont = 0;
btn->btn_event= BT_EVENT_RELEASED;
btn->btn_stat = 0;
}
break;
default: btn->btn_stat = 0; break;
}
}最后就是按键扫描函数,定义好按键的个数,每次扫描从第一个按键开始,用for循环,遍历每个按键变量内的读取回IO调函数,根据IO状态切换按键的状态,此函数放在主循环调用。
void bsp_btnton_scanf(void)
{
uint8_ti;
for(i=0;i< BUTTON_MAX;i++)
{
bsp_read_key_value(bt);
}
}在main函数中结构如下,在功能函数中,只要根据按键的event事件来执行不同的功能,这样就实现了功能与按键扫描的解耦:int main(void)
{
bsp_btn_register(); //注册按键
while(1)
{
if (Flag_10ms)
{
Flag_10ms = 0;
bsp_btnton_scanf(); //按键扫描,得到键值
Funct_1(Power_key.btn_event); //根据按键值执行功能 这样就可以实现功能跟按键扫描的解耦,不需要把功能写到按键扫描内部
}
}
}
过于复杂,只要累加按下的次数即可(不支持多键同时按下),就可以识别短按,长按,连按,以及按下,释放,双击在此基础上再实现 本帖最后由 dw772 于 2025-2-14 09:51 编辑
ayb_ice 发表于 2025-2-14 09:36
过于复杂,只要累加按下的次数即可(不支持多键同时按下),就可以识别短按,长按,连按,以及按下,释放,双击在此 ...
主要是想实现按键值与功能的解耦。 刚写代码的时候每次都把按键值与功能纠缠在一起,很难移植。此方法的好处是方便移植,不同项目修改头文件即可。也是个人想法,仅供参考。 本帖最后由 ayb_ice 于 2025-2-14 10:55 编辑
dw772 发表于 2025-2-14 09:44
主要是想实现按键值与功能的解耦。 刚写代码的时候每次都把按键值与功能纠缠在一起,很难移植。此方法的好 ...
解耦也容易啊,按键扫描后将按钮值存在变量中,用个API去获取按键值,程序调用API即可,或直接访问变量
看见switch...case 就知道还是幼儿园阶段 我感觉着 您这代码还需要一些磨合 xch 发表于 2025-2-14 13:22
看见switch...case 就知道还是幼儿园阶段
水平有限,不用switch一直不知道用什么方法识别按键比较好,请赐教 jobszheng 发表于 2025-2-14 13:25
我感觉着 您这代码还需要一些磨合
按键跟功能解耦一度困扰了我很长一段时间。只是抛砖引玉,希望有好的思路也分享一下。 dw772 发表于 2025-2-14 13:40
按键跟功能解耦一度困扰了我很长一段时间。只是抛砖引玉,希望有好的思路也分享一下。 ...
用键值做索引函数指针的数组。一句话就搞定switch...case...。
如果切换不同GUI页面,就切换指向不同界面对应的数组的指针。
方便多层次嵌套修改GUI,也不容易出错。不需要改代码尸体,仅编辑数组内容即可。做到一次编程重复使用。
编程原理其实回到最原始的图灵机基础,在mcu之中虚构一个自己的图灵机。
dw772 发表于 2025-2-14 13:34
水平有限,不用switch一直不知道用什么方法识别按键比较好,请赐教
就用switch,简单,易读。
不追求其它的所谓的C语言高阶应用 xch 发表于 2025-2-14 15:06
用键值做索引函数指针的数组。一句话就搞定switch...case...。
如果切换不同GUI页面,就切换指向不同界面 ...
任何情况下只有一个任务接管按键处理,在那个地方判断就可以了,这样就没有什么耦合,简单又高效,还用什么函数指针,我就不喜欢函数指针 ayb_ice 发表于 2025-2-17 08:32
任何情况下只有一个任务接管按键处理,在那个地方判断就可以了,这样就没有什么耦合,简单又高效,还用什么函 ...
如果是102个按键要case 102下。每切换个界面都case 102. MCU 也996 xch 发表于 2025-2-17 09:35
如果是102个按键要case 102下。每切换个界面都case 102. MCU 也996
你函数指针也少不了啊,何况MCU跑102个函数指针,估计都困难 #include <stdio.h>
#include <stdlib.h>
#include <math.h>
;
void Key0Proc(void)
{//按键处理
printf("Key 0\r\n");
}
void Key1Proc(void)
{
//按键处理
printf("Key 1\r\n");
}
void KeynProc(void)
{
//按键处理
printf("Key n\r\n");
}
void (*KeyFun)(void)=
{
//把用到的各种按键函数罗列
Key0Proc,
Key1Proc,
KeynProc
};
enum aa{
//键值按顺序罗列
Key_0,
Key_1,
//.........
Key_n,
}KEY_VALUE;
int main() {
for(int key=Key_0;key<(Key_n+1);key++)
{
//不需要switch..case.. ,也不需要if,一句话调用键值对应功能函数
KeyFun();
}
return0;
}
///这里仅简单举例简单单个GUI界面情况,如果需要切换多界面,把界面也编个号,搞个指针数组指向对应的不同KeyFun。
本帖最后由 ayb_ice 于 2025-2-17 13:18 编辑
xch 发表于 2025-2-17 12:27
#include
#include
#include
同一按键在不同的界面功能是不同的,哪有这么简单的,再说按键需要处理各个界面自己的变量,这些变量还可能是静态变量,甚至局部变量 本帖最后由 dw772 于 2025-2-17 15:29 编辑
xch 发表于 2025-2-17 12:27
#include
#include
#include
不知道是不是我理解的问题,感觉这个按键键值跟功能没有完全解耦,移植和复制需要修改的东西比较多,基本等于重写一个按键了,本质上这种也是操作指针并无不同。每个人的习惯都有不同吧,作为参考就好 dw772 发表于 2025-2-17 15:24
不知道是不是我理解的问题,感觉这个按键键值跟功能没有完全解耦,移植和复制需要修改的东西比较多,基本 ...
在他眼里,你这也是小学生水平
页:
[1]