dw772 发表于 2025-2-14 09:10

分享一个单片机按键状态机实现键值与功能解耦的方法

本帖最后由 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); //根据按键值执行功能 这样就可以实现功能跟按键扫描的解耦,不需要把功能写到按键扫描内部
         
   }
   }
}



ayb_ice 发表于 2025-2-14 09:36

过于复杂,只要累加按下的次数即可(不支持多键同时按下),就可以识别短按,长按,连按,以及按下,释放,双击在此基础上再实现

dw772 发表于 2025-2-14 09:44

本帖最后由 dw772 于 2025-2-14 09:51 编辑

ayb_ice 发表于 2025-2-14 09:36
过于复杂,只要累加按下的次数即可(不支持多键同时按下),就可以识别短按,长按,连按,以及按下,释放,双击在此 ...
主要是想实现按键值与功能的解耦。 刚写代码的时候每次都把按键值与功能纠缠在一起,很难移植。此方法的好处是方便移植,不同项目修改头文件即可。也是个人想法,仅供参考。

ayb_ice 发表于 2025-2-14 10:51

本帖最后由 ayb_ice 于 2025-2-14 10:55 编辑

dw772 发表于 2025-2-14 09:44
主要是想实现按键值与功能的解耦。 刚写代码的时候每次都把按键值与功能纠缠在一起,很难移植。此方法的好 ...
解耦也容易啊,按键扫描后将按钮值存在变量中,用个API去获取按键值,程序调用API即可,或直接访问变量

xch 发表于 2025-2-14 13:22

看见switch...case 就知道还是幼儿园阶段

jobszheng 发表于 2025-2-14 13:25

我感觉着 您这代码还需要一些磨合

dw772 发表于 2025-2-14 13:34

xch 发表于 2025-2-14 13:22
看见switch...case 就知道还是幼儿园阶段

水平有限,不用switch一直不知道用什么方法识别按键比较好,请赐教

dw772 发表于 2025-2-14 13:40

jobszheng 发表于 2025-2-14 13:25
我感觉着 您这代码还需要一些磨合

按键跟功能解耦一度困扰了我很长一段时间。只是抛砖引玉,希望有好的思路也分享一下。

xch 发表于 2025-2-14 15:06

dw772 发表于 2025-2-14 13:40
按键跟功能解耦一度困扰了我很长一段时间。只是抛砖引玉,希望有好的思路也分享一下。 ...

用键值做索引函数指针的数组。一句话就搞定switch...case...。
如果切换不同GUI页面,就切换指向不同界面对应的数组的指针。
方便多层次嵌套修改GUI,也不容易出错。不需要改代码尸体,仅编辑数组内容即可。做到一次编程重复使用。

编程原理其实回到最原始的图灵机基础,在mcu之中虚构一个自己的图灵机。

jobszheng 发表于 2025-2-15 09:18

dw772 发表于 2025-2-14 13:34
水平有限,不用switch一直不知道用什么方法识别按键比较好,请赐教

就用switch,简单,易读。

不追求其它的所谓的C语言高阶应用

ayb_ice 发表于 2025-2-17 08:32

xch 发表于 2025-2-14 15:06
用键值做索引函数指针的数组。一句话就搞定switch...case...。
如果切换不同GUI页面,就切换指向不同界面 ...

任何情况下只有一个任务接管按键处理,在那个地方判断就可以了,这样就没有什么耦合,简单又高效,还用什么函数指针,我就不喜欢函数指针

xch 发表于 2025-2-17 09:35

ayb_ice 发表于 2025-2-17 08:32
任何情况下只有一个任务接管按键处理,在那个地方判断就可以了,这样就没有什么耦合,简单又高效,还用什么函 ...

如果是102个按键要case 102下。每切换个界面都case 102. MCU 也996

ayb_ice 发表于 2025-2-17 09:43

xch 发表于 2025-2-17 09:35
如果是102个按键要case 102下。每切换个界面都case 102. MCU 也996

你函数指针也少不了啊,何况MCU跑102个函数指针,估计都困难

xch 发表于 2025-2-17 12:27

#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:14

本帖最后由 ayb_ice 于 2025-2-17 13:18 编辑

xch 发表于 2025-2-17 12:27
#include
#include
#include

同一按键在不同的界面功能是不同的,哪有这么简单的,再说按键需要处理各个界面自己的变量,这些变量还可能是静态变量,甚至局部变量

dw772 发表于 2025-2-17 15:24

本帖最后由 dw772 于 2025-2-17 15:29 编辑

xch 发表于 2025-2-17 12:27
#include
#include
#include

不知道是不是我理解的问题,感觉这个按键键值跟功能没有完全解耦,移植和复制需要修改的东西比较多,基本等于重写一个按键了,本质上这种也是操作指针并无不同。每个人的习惯都有不同吧,作为参考就好

ayb_ice 发表于 2025-2-18 08:45

dw772 发表于 2025-2-17 15:24
不知道是不是我理解的问题,感觉这个按键键值跟功能没有完全解耦,移植和复制需要修改的东西比较多,基本 ...

在他眼里,你这也是小学生水平
页: [1]
查看完整版本: 分享一个单片机按键状态机实现键值与功能解耦的方法