打印
[程序源码]

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

[复制链接]
2555|22
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
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_t  btn_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[BUTTON_MAX] = { NULL};        //定义一个数组存放按键的地址,方便扫描按键的时候按顺序执行。

TypeButton  Power_key; //电源
TypeButton  Light_key; //灯
TypeButton  Funct_key; //功能
TypeButton  Incre_key; //增加
TypeButton  Decre_key; //减少
还需要一个注册按键的函数,注册按键并初始化按键对应的变量,并将读取按键IO的函数指针存放到按键结构体变量内,函数原型如下。
/**
* [url=home.php?mod=space&uid=247401]@brief[/url] bsp_button_register  按键注册,并初始化相应按键的参数
*
*
* @param  id 按键的顺序
* @param (*read_cb)() 按键读取回调函数 返回值为1表示按键按下
* @param  *btn        指向 存放按键数值的变量  指针
* [url=home.php?mod=space&uid=120043]@reval[/url]  none
*/
void  bsp_button_register( uint8_t  id, flag_status (*read_cb)(), TypeButton *btn)
{
   bt[id] = 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_t  i;
    for(i=0;i< BUTTON_MAX;i++)
    {      
       bsp_read_key_value(bt[i]);
      
    }

}
在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即可,或直接访问变量

使用特权

评论回复
5
xch| | 2025-2-14 13:22 | 只看该作者
看见switch...case 就知道还是幼儿园阶段

使用特权

评论回复
评论
xionghaoyun 2025-2-17 08:47 回复TA
那你写一个? 
6
jobszheng| | 2025-2-14 13:25 | 只看该作者
我感觉着 您这代码还需要一些磨合

使用特权

评论回复
7
dw772|  楼主 | 2025-2-14 13:34 | 只看该作者
xch 发表于 2025-2-14 13:22
看见switch...case 就知道还是幼儿园阶段

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

使用特权

评论回复
评论
xionghaoyun 2025-2-17 14:48 回复TA
@xch :那你来写一个 
xch 2025-2-17 09:30 回复TA
@xionghaoyun : 居然蠢到以为别人会用if。 
xionghaoyun 2025-2-17 08:49 回复TA
LZ没问题啊 那用if的人是不是幼儿园? 
8
dw772|  楼主 | 2025-2-14 13:40 | 只看该作者
jobszheng 发表于 2025-2-14 13:25
我感觉着 您这代码还需要一些磨合

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

使用特权

评论回复
9
xch| | 2025-2-14 15:06 | 只看该作者
dw772 发表于 2025-2-14 13:40
按键跟功能解耦一度困扰了我很长一段时间。只是抛砖引玉,希望有好的思路也分享一下。 ...

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

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

使用特权

评论回复
10
jobszheng| | 2025-2-15 09:18 | 只看该作者
dw772 发表于 2025-2-14 13:34
水平有限,不用switch一直不知道用什么方法识别按键比较好,请赐教

就用switch,简单,易读。

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

使用特权

评论回复
11
ayb_ice| | 2025-2-17 08:32 | 只看该作者
xch 发表于 2025-2-14 15:06
用键值做索引函数指针的数组。一句话就搞定switch...case...。
如果切换不同GUI页面,就切换指向不同界面 ...

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

使用特权

评论回复
评论
xch 2025-2-17 09:32 回复TA
喜欢就好。 
12
xch| | 2025-2-17 09:35 | 只看该作者
ayb_ice 发表于 2025-2-17 08:32
任何情况下只有一个任务接管按键处理,在那个地方判断就可以了,这样就没有什么耦合,简单又高效,还用什么函 ...

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

使用特权

评论回复
13
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 09:58 回复TA
我在小学等你 
14
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[10])(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[key]();
   }

return0;
}

///这里仅简单举例简单单个GUI界面情况,如果需要切换多界面,把界面也编个号,搞个指针数组指向对应的不同KeyFun。

使用特权

评论回复
15
ayb_ice| | 2025-2-17 13:14 | 只看该作者
本帖最后由 ayb_ice 于 2025-2-17 13:18 编辑
xch 发表于 2025-2-17 12:27
#include
#include
#include

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

使用特权

评论回复
16
dw772|  楼主 | 2025-2-17 15:24 | 只看该作者
本帖最后由 dw772 于 2025-2-17 15:29 编辑
xch 发表于 2025-2-17 12:27
#include
#include
#include

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

使用特权

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

在他眼里,你这也是小学生水平

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

28

主题

495

帖子

1

粉丝