找回密码
 立即注册

QQ登录

只需一步,快速开始

搜索
查看: 49|回复: 1
打印 上一主题 下一主题
收起左侧

仅需40行!可实现按键长按单击双击三击四击,挑战行数最少代码

[复制链接]
回帖奖励 2 黑币 回复本帖可获得 2 黑币奖励! 每人限 1 次
跳转到指定楼层
楼主
ID:1155837 发表于 2026-2-5 01:11 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
差不多一年前我发了个帖子,内容是“使用扫描后输出结果进行逻辑运算的按键处理方法”,就在这里http://www.51hei.com/bbs/dpj-240563-1.html
当时因为刚开始学单片机,没那个实力也没时间,所以今天再翻老底的时候才发现了之前的创意
现在的我也是有能力把这个代码写出来了,说白了也没什么难的,当时其实也写完大半了,只是当时课设赶着交,所以也就没搞出来。
这个代码实现的功能从原理上非常容易理解,既:以定时器中断为时基,每次定时器中断都采样一次,总共采样32次,存为一个32位的ulong量。然后通过逻辑与位运算,计算采样结果中的上升沿数量,进而得到按键按下的次数与时间。
那么应该如何采样呢?在回答这个问题之前,我们必须明白,采样应该如何触发毕竟中断无时无刻都在调用,如果一直采样,不仅逻辑上不好处理,对CPU时间消耗也会更多。
答案很简单,第一个下降沿就代表按键已经按下,要判断下降沿,我们需要两个变量和一个if判断
  Previous_Key = Now_Key;//存储上一次的结果
  Now_Key = Read_Temp;//读取本次结果
  //Key_Event = NOKEY;//未在采样状态,全局按键事件为无键
  if(!Now_Key && Previous_Key){//上一次为1,这一次为0,是下降沿
  Start_Sampling = true;//开始采样
  }


上面的代码很简单是不是?先保存之前的按键状态,再读取现在的,只要之前是1,现在是0,那么就是一个下降沿
明白了采样如何触发之后,就该考虑如何采样了,这个也很简单,只需要一个uchar记录采样次数,一个ulong变量缓存采样结果即可
if(Sampling_Counter < 32){
      Is_Sampling = true;
      Sampling_Temp <<= 1;//整体左移一位,低位补0
      if (Read_Temp){//采样引脚,如果为高电平,最低位置1
        Sampling_Temp |= 0x01;//如果引脚为高才计1,如果为低就继续左移一位,也就是补0
        }
        Sampling_Counter++;
        }


是不是也很简单?每次采样,缓存数据左移一位,填入低位,执行32次就得到了结果
有了结果之后,怎么计算上升沿数量呢?这是本方案里最巧妙的一点。我们用一个掩码Mask表示运算结果。
Mask = ~Input & (Input << 1);

即:先对data取反,将取反后的值,与data左移一位后的值,做逻辑与就可得到上升沿掩码,mask中的1的数量,就是采样结果中上升沿的数量
这样做到底对不对?我们来简单验证。
假设一个八位采样结果为: 01101000
取反:1 0 0 1 0 1 1 1
左移一位:1 1 0 1 0 0 0 0
逻辑与:1 0 0 1 0 0 0 0
可以看到,结果中有两个1,也就是两个上升沿。而我们手动找出原数据中的上升沿(0→1)同样是两个。
说明该算法正确。实际测试结果也是可以正确判断上升沿数量的,并且上升沿的数量完全按键按下次数相同。
而如何统计出掩码的中的1的数量呢?笨办法是直接while循环遍历。
    while(Mask){//当mask = 0时,循环结束
        Count_Temp += Mask & 0X01;//取最低位,与count相加
        Mask >>= 1;//右移一位,抛弃最低位,最高位补0
    }

这是最容易理解的,代码行数也最少。但是过于消耗CPU时间。每次循环都需要几个CPU周期,总共需要消耗160个周期左右。那有没有更省性能的办法呢?有的兄弟,有的,那就是著名的SIMD Within A Register算法,这个算法我也不太明白原理,但是能用。
unsigned char Rise_Edge_Counter(unsigned long Input){
  unsigned long Mask;//掩码
    Mask = ~Input & (Input << 1);
    Mask = (Mask & 0x55555555) + ((Mask >> 1) & 0x55555555);
    Mask = (Mask & 0x33333333) + ((Mask >> 2) & 0x33333333);
    Mask = (Mask + (Mask >> 4)) & 0x0F0F0F0F;
    Mask = Mask + (Mask >> 8);
    Mask = Mask + (Mask >> 16);
    Mask = Mask & 0x0000003F;
    return Mask;
}

将消耗时间的while循环变成了CPU可以轻松执行的逻辑与移位运算,整体不会超过15个时钟周期!
上面已经将基本原理说清楚了,也没什么好说的了,直接上源码!需要和上面那个riseedgecounter函数一块使用。
  1. #define NOKEY 0
  2. #define SINGLEKEY 1
  3. #define DOUBLEKEY 2
  4. #define TRIPLEKEY 3
  5. #define QUADRAKEY 4
  6. #define LONGKEY 5
  7. unsigned char Key_Event = NOKEY;//全局按键状态
  8. //函数不会主动清零,需要在使用之后手动清零,可以保持输出状态
  9. void Key_Sampling(unsigned char IO_Set){//传入需要进行采样的引脚
  10.   //实际上本函数自带消抖,因为不足一个定时器间隔(20ms)的下降沿无法触发采样
  11.   static unsigned char Sampling_Counter = 0;//采样次数计数器
  12.   static unsigned long Sampling_Temp = 0;//采样结果缓存
  13.   static bool Previous_Key = false;//之前的按键采样记录
  14.   static bool Now_Key = false;//当前的按键采样记录
  15.   static bool Is_Sampling = false;//正在采样标志
  16.   static bool Start_Sampling = false;//开始采样标志,由下降沿触发
  17.   static bool Read_Temp = false;//按键状态缓存,每次进入函数时读取*/
  18.   unsigned char Rise_Edge_Result = 0;//上升沿计算的结果,非static变量,重入自动清零
  19.   Read_Temp = digitalRead(IO_Set);//采样一次
  20.   //采样触发(检测下降沿)
  21.   if(!Is_Sampling){//如果没有在采样,才做判断
  22.   Previous_Key = Now_Key;//存储上一次的结果
  23.   Now_Key = Read_Temp;//读取本次结果
  24.   //Key_Event = NOKEY;//未在采样状态,全局按键事件为无键
  25.   if(!Now_Key && Previous_Key){//上一次为1,这一次为0,是下降沿
  26.   Start_Sampling = true;//开始采样
  27.   }
  28.   else{
  29.     return;}//未触发采样,直接返回
  30.   }
  31.   //开始采样与结果处理
  32.   if(Start_Sampling){
  33.     if(Sampling_Counter < 32){
  34.       Is_Sampling = true;
  35.       Sampling_Temp <<= 1;//整体左移一位,低位补0
  36.       if (Read_Temp){//采样引脚,如果为高电平,最低位置1
  37.         Sampling_Temp |= 0x01;//如果引脚为高才计1,如果为低就继续左移一位,也就是补0
  38.         }
  39.         Sampling_Counter++;
  40.         }
  41.       else{//采满32次,采样结束,开始计算
  42.         Is_Sampling = false;
  43.         Start_Sampling = false;
  44.         Sampling_Counter = 0;//清空计数器
  45.         Rise_Edge_Result = Rise_Edge_Counter(Sampling_Temp);//计算上升沿数量
  46.         switch(Rise_Edge_Result){
  47.           case 0: Key_Event = LONGKEY; break;
  48.           case 1: Key_Event = SINGLEKEY;break;
  49.           case 2: Key_Event = DOUBLEKEY;break;
  50.           case 3: Key_Event = TRIPLEKEY;break;
  51.           case 4: Key_Event = QUADRAKEY;break;
  52.         }
  53.         }
  54.   }
  55. }
复制代码

其中,Key_Event为全局按键变量,在函数内部不会清零,需要手动在使用完毕之后清零。变相实现了保持输出状态的功能。
除过变量声明还有注释,确实只有几十行,不是吗?
分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友
收藏收藏2 分享淘帖 顶 踩
回复

使用道具 举报

沙发
ID:1155837 发表于 2026-2-5 01:30 | 只看该作者
下面为使用教程,本函数使用了一个Key_Event为全局按键事件变量,需要在其他函数中读取这个Key_Event。
你需要在定时器中断中调用采样函数,结果保存在Key_Event中。
void TIMER_ISR onTimer() {
  Key_Sampling(Button);
}

如果你想通过串口测试按键结果?
void loop() {
    delay(500);
    Serial.print("Key Event is");
    Serial.println(Key_Event);
    Key_Event = NOKEY;
}

另外,这是Arduino平台的测试方法,如果你想移植,也非常简单,只需要修改下这一行。
  Read_Temp = digitalRead(IO_Set);//采样一次
如果你是51单片机,那你可以写
  Read_Temp = P32;//采样一次
如果你是32单片机,你可以写
  Read_Temp = GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0);//采样一次
实际上这种不涉及外设的功能代码都是通用的,可以随意移植的,不同平台,也只不过是初始化的时候不同,51用寄存器初始化,32单片机用hal库或者标准库初始化,arduino同理。
另外,如果你想用在多个按键上,怎么处理?
这个比较复杂,需要用到结构体:
  1. typedef struct{//多个按键存储不同结构体
  2.   unsigned char Key_IO;
  3.   unsigned char Sampling_Counter;//采样次数计数器
  4.   unsigned long Result_Temp;//采样结果缓存
  5.   bool Previous_Key;//之前的按键采样记录
  6.   bool Now_Key;//当前的按键采样记录
  7.   bool Is_Sampling;//正在采样标志
  8.   bool Start_Sampling;//开始采样标志,由下降沿触发
  9.   bool Key_Temp;//按键状态缓存,每次进入函数时读取
  10.   unsigned char Rise_Edge_Result;
  11. } Key_Sampling_Group;
  12. Key_Sampling_Group IO_15{15,0,0,0,0,0,0,0};//IO15所接按键
  13. Key_Sampling_Group IO_7{7,0,0,0,0,0,0,0};//IO7所接按键
复制代码
通过这个结构体,我们可以用同一个函数,实现多个IO并行检测,函数的变量分别保存,不会互相干扰,节省flash,虽然其实这么短的代码根本不会超过500字节代码吧,但是如果按键比较多,占用还是很可观的。
使用结构体,传入时需要传入指针(地址),否则会导致直接将结构体中的数值复制一份到函数中,你在定时器中断中这么写:
Key_Sampling(Key_Sampling_Group *IO_Set)
这样就可以传入结构体的地址,函数会使用结构体中的变量,也会自动保存在结构体中。
然后你需要在函数中每个用到对应变量的地方修改,比如:
原本是digitalRead(Key_IO);
现在你需要改为digitalRead(IO_Set -> Key_IO);
这样才能让函数读取到你结构体中的数据。
回复

使用道具 举报

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

本版积分规则

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

Powered by 单片机教程网

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