找回密码
 立即注册

QQ登录

只需一步,快速开始

搜索
查看: 7903|回复: 25
收起左侧

单片机基于事件的按键处理编程思想(原创)

  [复制链接]
ID:471574 发表于 2021-9-25 09:39 | 显示全部楼层 |阅读模式
本帖最后由 zyhlove813 于 2021-9-25 10:57 编辑

单片机处理按键网上的思路也是五花八门。入门的,可能是直接判断端口,老手的,可能是通过键值计算;不管是谁学单片机,都逃不了做按键处理的程序。我在做项目的过程中,参考一些网上的思路,结合自己的算法,通过项目调试和验证,终于做出了比较优化和满意的按键处理程序,功能有如下几个方面:
1、多键扫描处理,提高处理速度
2、支持长按处理(单次触发或一直触发)
3、支持按下、弹起、按住、松开、长按的事件
4、项目中只需要修改扫描键值,然后在各事件中判断对应键值(单键或多键)
主要编程思路如下:
1、变量的说明
  1. //长按键的时长
  2. #define longkey_times 2000
  3. //长按单次模式定义,如果要长按时一直执行,请注释下一行
  4. #define LONG_PROCESS_ONCE
  5. uint8_t  KEY_PRESS;  //当前按下的键值
  6. uint8_t  KEY_NOT_PRESS;  //当前未被按的键值
  7. uint8_t  KEY_LAST;  //上一次的键值
  8. uint8_t  KEY_LONG;  //长按的键值
  9. uint8_t  KEY_DOWN;  //按下的键值
  10. uint8_t  KEY_UP; //弹起的键值
  11. uint8_t  KEY_UP_NL; //弹起的键值不带长按键
  12. uint32_t KEY_TICKS;  //按键时间,用于长按计时
复制代码

2、按键相关函数说明
  1. //按键处理程序
  2. void JUDGE_KEY(bool SINGLE_KEY); //键值扫描及逻辑处理
  3. void KEY_LONG_PROCESS(void);  //长按事件
  4. void KEY_PRESS_PROCESS(void);  //按住状态事件
  5. void KEY_NOT_PRESS_PROCESS(void); //松开状态事件
  6. void KEY_DOWN_PROCESS(void); //按下事件
  7. void KEY_UP_PROCESS(void);  //弹起事件
复制代码
3、按键扫描及逻辑处理思路
  1. //bool SINGLE_KEY 防抖开关,True时打开
  2. void JUDGE_KEY(bool SINGLE_KEY)
  3. {
  4.     uint8_t TEMP_KEY;  //临时的键值缓存
  5.     TEMP_KEY = PIND & 0x0C; //批量扫描IO,并生成键值,用户需结合项目自已修改,PIND
  6.                             //此处表示PD0-7的端口,不同单片机不一样,0x0C只取出
  7. //PD2 PD3的值  
  8.     TEMP_KEY ^= 0x0C;       //此处主要是把键值取反,如果你的按键是低电平触发的话
  9.                             //如果你的按键是高电平触发,则删除此行,不需要取反
  10.     if(TEMP_KEY > 0)  //键值大于0,表示有按键按着
  11.     {
  12.      delay(10); //防抖延时
  13.      //以下再一次批量扫描键值
  14.     KEY_PRESS = PIND & 0x0C;         
  15.     KEY_PRESS ^= 0x0C;

  16.     //如果防抖开关有效且两次键值不一致,返回不处理
  17.     if(TEMP_KEY!=KEY_PRESS && SINGLE_KEY)
  18.    {
  19.       return;
  20.     }
  21.     }
  22.     else //无按键动作,当前按下的键值=0
  23.     {
  24.     KEY_PRESS=0;
  25.     }
  26.    //核心按键逻辑判断
  27.     KEY_DOWN=(KEY_LAST^KEY_PRESS) & KEY_PRESS;   //按下的键值
  28.     KEY_UP=(KEY_LAST^KEY_PRESS) & KEY_LAST;//弹起的键值(包含长按键)
  29.     KEY_UP_NL=(~KEY_LONG) & KEY_UP; //弹起的键值(不包含长按键)
  30.     KEY_NOT_PRESS=~KEY_PRESS;  //未按的键状态值
  31.           if(KEY_LONG & KEY_UP)
  32.     {
  33.      KEY_LONG=0;
  34.     }
  35.     if(KEY_PRESS > 0)  //当前有按键值按下
  36.     {
  37.        if(KEY_LAST & KEY_PRESS)     //如果当前的值与上次按下的值有相同的地方
  38.                                               //表示有键一直按着,否则可能只是切换了其他按键
  39.        {
  40.             //millis()函数是Arduino的开机时间毫秒计数,其他单片机自己实现
  41.              if(millis() - KEY_TICKS > longkey_times)   //按键时间大于长按时间
  42.             {
  43.                  KEY_LONG =KEY_LAST & KEY_PRESS;    //长按键值等于一直按住的值
  44.                  KEY_LONG_PROCESS();   //长按键处理
  45.                   #ifdef LONG_PROCESS_ONCE  //如果是长按单次处理
  46.                   KEY_TICKS=millis(); //更新长按时间标记,避免进入长按判断
  47.                  #endif
  48.              }
  49.         }
  50.        else
  51.        {
  52.             KEY_TICKS=millis();  //切换了其他键,更新长按时间标记,避免进入长按判断
  53.         }
  54.     }
  55.     else   //当前无按键按下
  56.     {
  57.       KEY_TICKS=millis();  //更新长按时间标记,避免进入长按判断
  58.     }
  59.     if(KEY_UP > 0)  //如果有弹起的按键值
  60.     {
  61.     KEY_UP_PROCESS();    //按键弹起时处理
  62.     KEY_UP = 0;  //复位弹起的键值
  63.     }
  64.     if(KEY_DOWN > 0)
  65.     {
  66.     KEY_DOWN_PROCESS();  //按键按下时处理
  67.     }
  68.     if(KEY_PRESS > 0)
  69.     {
  70.     KEY_PRESS_PROCESS();  //按键按着状态处理
  71.     }               
  72.                 if(KEY_NOT_PRESS)
  73.                 {
  74.      KEY_NOT_PRESS_PROCESS();  //按键弹起状态处理
  75.     }
  76.      KEY_LAST=KEY_PRESS; //更新上一次的键值
  77. }
复制代码


4、按键逻辑处理算法详解
   4.1首次按下的键,先用异或^进行上次扫描键值和本次扫描键值计算,取得不一样的键位,不一样的键位和本次扫描键位相同,则表示首次按下。
0000 0010表示上次扫描的键,第1位是按下的状态
   0000 0110 表示本次扫描的键,第1位和第2位是按下的,
   我们要算出第2位是首次按下,则
  0000 0010 ^ 0000 0110=0000 0100
  0000 0100 & 0000 0110=0000 0100

又如 000 0010表示上次扫描的键,第1位是按下的
     0000 0100表示本次扫描的键,第2位是按下的,第1位已经松开
     我们要算出第2位是首次按下,则
     0000 0010 ^ 0000 0100=0000 0110
     0000 0110 & 0000 0100=0000 0100  
   (所以与本次扫描的键值与,可以得到首次按下的键值)
       KEY_DOWN=(KEY_LAST^KEY_PRESS) & KEY_PRESS;   //按下的键值

   4.2弹起的键值
      与按下的原理一样,不同的是要和上次扫描的键值相与
          0000 0010表示上次扫描的键,第1位是按下的状态
          0000 0100 表示本次扫描的键,第2位是按下的,
          我们要算出第1位是弹起,则
          0000 0010 ^ 0000 0100=0000 0110
          0000 0110 & 0000 0010=0000 0010

          KEY_UP=(KEY_LAST^KEY_PRESS) & KEY_LAST;//弹起的键值(包含长按键)
   4.3长按键一般单独处理,弹起时如果要排除,避免多次触发事件,需要计算出
      不包含长按键的键值,用如下公式
           KEY_UP_NL=(~KEY_LONG) & KEY_UP; //弹起的键值(不包含长按键)
   4.4 长按键的计算逻辑,见程序注释
5、如何使用

    5.1 设置好长按的时间
       #define longkey_times 2000  //这里表示2秒
    5.2 修改扫描键值
        TEMP_KEY = PIND & 0x0C; //批量扫描IO,并生成键值,用户需结合项目自已修改,PIND
                            //此处表示PD0-7的端口,不同单片机不一样,0x0C只取出 PD2 PD3的值  
         TEMP_KEY ^= 0x0C;       //此处主要是把键值取反,如果你的按键是低电平触发的话
        //还有一处地方也要一起改
        KEY_PRESS = PIND & 0x0C;         
        KEY_PRESS ^= 0x0C;

注意:51或其他单片机中,如果按键不在同一序列,比如P01 P03 P14 P16,则可以如下设置
    TEMP_KEY = P0 & 0x0A; //取出 P01 P03
    TEMP_KEY |=(P1 & 0x50); //取出 P14 P16

    TEMP_KEY ^= (0x0A|0x50);       //此处主要是把键值取反,如果你的按键是低电平触发的话,
                                    //如果你的按键是高电平触发,则删除此行,不需要取反
    //还有一处地方也要一起改
    KEY_PRESS = P0 & 0x0A; //取出 P01 P03   
    KEY_PRESS |=(P1 & 0x50); //取出 P14 P16
    KEY_PRESS ^= (0x0A|0x50);      //此处主要是把键值取反,如果你的按键是低电平触发的话,
                                 //如果你的按键是高电平触发,则删除此行,不需要取反
为了编程方便,尽量使用同一序列的口,如果不同序列的口,那端口号也要能错开,如用了P01,就不要用P11了。
这样的话,才能方便计算,提高扫描效率,如果非要用,只能通过移位处理
如51或其他单片机中,想判断 P01 P02 P12 P13的键
TEMP_KEY = P1 & 0x0C; //取出 P12 P13
TEMP_KEY =TEMP_KEY<<1; //左移1位,避开P12和P02交叉重叠
TEMP_KEY |= (P0 & 0x06); //取出 P01 P02
TEMP_KEY ^= (0x18|0x06);       //此处主要是把键值取反,如果你的按键是低电平触发的话
                               //如果你的按键是高电平触发,则删除此行,不需要取反
这样键值里,0x02表示P01,0x04表示P02,0x08表示P12,0x10表示P13

5.3在单片机循环程序或定时器里,周期性调用扫描程序
void loop()
{
   JUDGE_KEY(true);
}
5.4在对应事件里进行其他编程

如:
void KEY_NOT_PRESS_PROCESS()   //按键弹起状态处理
{
  if(KEY_NOT_PRESS & 0x04)
  {
    //Serial.println("KEY PD2 is NOT PRESSING");
  }
}
void KEY_PRESS_PROCESS() //按键按着状态处理
{
  if(KEY_PRESS & 0x04)
  {
    //Serial.println("KEY PD2 is PRESSING");
  }
}
void KEY_LONG_PROCESS() //长按键处理
{
  if(KEY_LONG & 0x04)
  {
    Serial.println("KEY PD2 is LONG PRESS");
  }
  if(KEY_LONG & 0x08)
  {
    Serial.println("KEY PD3 is LONG PRESS");
  }  
}
void KEY_DOWN_PROCESS()  //按键按下时处理
{
  if(KEY_DOWN & 0x04)
  {
    Serial.println("KEY PD2 is DOWN NOW");
  }
  if(KEY_DOWN & 0x08)
  {
    Serial.println("KEY PD3 is DOWN NOW");
  }         
  Serial.println("---------------------");
}
void KEY_UP_PROCESS()    //按键弹起时处理
{
  if(KEY_UP_NL & 0x04)
  {
    Serial.println("KEY PD2 is UP_NL NOW");
  }        
  if(KEY_UP_NL & 0x08)
  {
    Serial.println("KEY PD3 is UP NOW");
  }  
  Serial.println("---------------------");
}
附上Arduino的测试程序,注意Arduino Uno中PD2表示数字脚2,PD3表示数字脚3
KEY_TEST-Arduino.rar (1.35 KB, 下载次数: 122)

评分

参与人数 2黑币 +62 收起 理由
sadv + 12 好帖
admin + 50 共享资料的黑币奖励!

查看全部评分

回复

使用道具 举报

ID:968484 发表于 2021-9-27 04:11 | 显示全部楼层
谢了好资料,51黑有你更精彩!!!
回复

使用道具 举报

ID:471574 发表于 2021-9-27 10:16 | 显示全部楼层
ximao 发表于 2021-9-27 04:11
谢了好资料,51黑有你更精彩!!!

有用就好
回复

使用道具 举报

ID:194006 发表于 2021-10-6 17:53 来自手机 | 显示全部楼层
先收藏一下,以后用得上
回复

使用道具 举报

ID:971477 发表于 2021-10-12 16:19 | 显示全部楼层
好资料,51黑有你更精彩!!!
回复

使用道具 举报

ID:971822 发表于 2021-10-13 20:27 | 显示全部楼层
不错,值得学习
回复

使用道具 举报

ID:471574 发表于 2021-10-25 11:16 | 显示全部楼层

一起学习,如果你有好的建议也不妨提出,给大家学习一下
回复

使用道具 举报

ID:941265 发表于 2021-11-1 17:00 | 显示全部楼层
好资料,51黑有你更精彩!!!
回复

使用道具 举报

ID:370415 发表于 2021-11-9 12:47 | 显示全部楼层
一起学习,51黑一定辉煌起来
回复

使用道具 举报

ID:850575 发表于 2021-11-19 08:18 | 显示全部楼层
很深奥的内容。
回复

使用道具 举报

ID:814879 发表于 2022-1-12 11:01 | 显示全部楼层
刚好,我这个小白去学习学习!
回复

使用道具 举报

ID:8222 发表于 2022-1-31 09:28 | 显示全部楼层
很好的资料!
回复

使用道具 举报

ID:258676 发表于 2022-3-11 09:54 | 显示全部楼层
这个模块很方便
回复

使用道具 举报

ID:22218 发表于 2022-3-19 03:39 | 显示全部楼层
本帖最后由 chinomango 于 2022-3-19 03:41 编辑

这个适合别的uP和C编译吗?最好前面有#if以自动适合不同的C编译。
看到晚了,上个月还自己写了一个,不过支持连续按键2次,以即时关闭电源;单次则是延迟3分钟关闭。不知用你这个要如何改动? 先谢过。这个是堵塞式的吗还是定时中断扫描键盘?
回复

使用道具 举报

ID:471574 发表于 2022-3-30 08:10 | 显示全部楼层
chinomango 发表于 2022-3-19 03:39
这个适合别的uP和C编译吗?最好前面有#if以自动适合不同的C编译。
看到晚了,上个月还自己写了一个,不过 ...

这个只是把按键值用计算的方式来产生按下、弹长、长按等事件动作,连续两次按键,你可以在按下或弹起的事件里,用一个值来判断按了几次,再加上一个时长,如果超时或者中间按了其他按键,这个值就初始化到初值;
这个程序主要作用是产生事件,至少逻辑处理,要做什么事情,是你要在事件里进行编程。我这个程序,如果 JUDGE_KEY(true)在定时器里中断调用,就变成是中断式,如果在Loop循环里调用,就成了堵塞式。看你的程序需求
回复

使用道具 举报

ID:933601 发表于 2022-5-19 18:29 | 显示全部楼层
收藏一下
回复

使用道具 举报

ID:966468 发表于 2022-6-23 09:35 | 显示全部楼层
非常好的设计思路,如果能提高实时性就更好了
回复

使用道具 举报

ID:471574 发表于 2022-6-25 11:37 | 显示全部楼层
mrzhou 发表于 2022-6-23 09:35
非常好的设计思路,如果能提高实时性就更好了

多个项目应用证明,一般的项目都能应对,如果有担心的话,也可以把调用放在定时中断里
回复

使用道具 举报

ID:364137 发表于 2022-8-19 22:45 | 显示全部楼层
很好的经验,谢谢分享。
回复

使用道具 举报

ID:509408 发表于 2022-8-20 09:03 | 显示全部楼层
按键IO那里还是要做的更通用些就好了,我自己的项目应用中因为IO口紧张,是不太可能按键分布在连续IO口上的。
回复

使用道具 举报

ID:137736 发表于 2022-9-3 17:52 | 显示全部楼层
太好了,也上个51的源码好了
回复

使用道具 举报

ID:1032507 发表于 2022-11-2 15:48 | 显示全部楼层
楼主写的很详细,谢谢分享。
回复

使用道具 举报

ID:979603 发表于 2022-11-10 07:54 | 显示全部楼层

好资料,51黑有你更精彩!!!
回复

使用道具 举报

ID:1013961 发表于 2022-12-3 17:28 | 显示全部楼层
很不错的按键逻辑处理。收藏了。
回复

使用道具 举报

ID:27536 发表于 2023-5-12 16:47 | 显示全部楼层
既然都用事件了,为什么还要延时防抖呢?
回复

使用道具 举报

ID:224580 发表于 2023-8-21 23:26 | 显示全部楼层
值得慢慢研究,加入到我的代码上去
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

手机版|小黑屋|51黑电子论坛 |51黑电子论坛6群 QQ 管理员QQ:125739409;技术交流QQ群281945664

Powered by 单片机教程网

快速回复 返回顶部 返回列表