找回密码
 立即注册

QQ登录

只需一步,快速开始

搜索
查看: 921|回复: 17
收起左侧

仅需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为全局按键变量,在函数内部不会清零,需要手动在使用完毕之后清零。变相实现了保持输出状态的功能。
除过变量声明还有注释,确实只有几十行,不是吗?
回复

使用道具 举报

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);
这样才能让函数读取到你结构体中的数据。
回复

使用道具 举报

ID:1142711 发表于 2026-2-5 08:14 | 显示全部楼层
获益匪浅!
回复

使用道具 举报

ID:143767 发表于 2026-2-6 11:16 | 显示全部楼层
从程序里看不出按键接在哪个IO口了
回复

使用道具 举报

ID:1073939 发表于 2026-2-6 11:19 | 显示全部楼层
想法很新颖,不过很难用于工程实践:
采用固定32次(640ms)采样窗口,无法滚动更新数据。感觉双击,长按会有问题。
常规工程中长按功能通常设置为3秒左右(且可配置1~20秒)。
三连击、四连击在实际项目中很少用到;
回复

使用道具 举报

ID:584814 发表于 2026-2-6 12:06 | 显示全部楼层
感谢分享一种好思路,实用情况待测。
回复

使用道具 举报

ID:1073939 发表于 2026-2-6 14:12 | 显示全部楼层
下面是ai分析的结果,虽不完全赞同,但部分是正确的。
你希望我分析这段按键采样代码的逻辑,并结合 20ms 的执行间隔,指出它与常规按键处理相比的优缺点,特别是针对单击、双击等场景的问题。
一、代码核心逻辑梳理
这段代码的核心是基于定时器(20ms 间隔)的连续采样机制,通过检测按键引脚的电平变化,统计 32 次采样内的上升沿数量,来判断按键事件类型,核心流程如下:
触发采样:检测到按键下降沿(按下瞬间),启动 32 次连续采样;
采样过程:每 20ms 采样一次引脚电平,存入 32 位的Sampling_Temp缓存;
结果判定:32 次采样完成后,调用Rise_Edge_Counter统计上升沿数量,对应不同按键事件(0 = 长按、1 = 单击、2 = 双击等);
状态保持:全局变量Key_Event会保持判定结果,直到手动清零。
简单来说,它的判定逻辑是:32 次采样(共 640ms)内的上升沿数量 = 按键点击次数。
二、结合 20ms 间隔的核心缺点分析
1. 判定延迟严重,实时性差
代码必须完成32 次采样(32×20ms=640ms) 后,才能输出按键事件结果。
单击场景:用户按下并松开按键后,需要等待 640ms 才能触发SINGLEKEY事件,远慢于常规按键 “松开即触发” 的逻辑,操作反馈滞后明显;
长按场景:长按超过 640ms 才会判定为LONGKEY,常规长按通常 200~300ms 即可触发,这段代码的长按判定过于迟钝。
2. 双击 / 多击逻辑不符合用户习惯,易误判
常规双击的核心逻辑是:第一次单击松开后,500ms 内再次单击,判定为双击;超过 500ms 则判定为两次独立单击。但这段代码完全违背这一习惯:
代码的判定窗口是固定的 640ms,只要两次单击的时间间隔≤640ms,就会被判定为双击;
你提到的 “单击后间隔 500ms 再单击”,在代码中会被判定为双击(32 次采样内包含 2 个上升沿),而非用户预期的两次单击,完全不符合日常操作直觉;
多击(三击、四击)同理,只要 640ms 内完成多次点击,就会被合并判定,无法识别 “间隔稍长的多次独立单击”。
3. 采样窗口固定,缺乏灵活性,易漏判 / 误判
32 次采样的 640ms 窗口是固定的,没有动态调整机制,存在明显缺陷:
漏判短按:如果用户点击速度极快(如 < 100ms),可能在 32 次采样开始前就完成按下松开,导致采样未捕捉到有效电平,判定为NOKEY;
误判长按:如果用户按下按键后,在 640ms 内未松开(如按住 300ms),会被判定为LONGKEY,但常规逻辑中这属于 “短按未松开”,而非长按;
无法处理 “长按后松开”:代码中LONGKEY仅由 “32 次采样内无上升沿” 判定,即 “按下后 640ms 未松开”,但用户长按后松开的动作,代码无法区分 “长按过程中” 和 “长按结束”,事件粒度太粗。
4. 状态管理混乱,易导致逻辑异常
全局变量依赖:Key_Event是全局变量,且不会自动清零,必须手动处理。如果主程序忘记清零,会导致后续采样结果被覆盖,或重复触发同一事件;
采样状态锁死:一旦启动 32 次采样,期间无法响应新的按键动作(Is_Sampling为 true 时,下降沿检测被屏蔽)。如果用户在采样过程中再次点击,会被完全忽略,导致按键响应丢失;
无防抖优化:代码声称 “自带消抖”,但仅依赖 “不足 20ms 的下降沿无法触发采样”,实际未对按键物理抖动(通常 10~20ms)做专门处理,抖动电平可能被计入采样缓存,导致上升沿统计错误,引发误判。
5. 事件类型覆盖不全,实用性低
代码仅支持 “无键、单击、双击、三击、四击、长按”6 种事件,缺乏常规按键的核心事件类型:
无 “按键按下”“按键松开” 基础事件,无法实现 “按下执行、松开停止” 的交互逻辑;
无 “长按保持” 事件(如长按持续调节音量),仅能判定 “长按触发一次”,无法满足连续操作需求;
多击上限仅到四击,且判定逻辑不符合习惯,实际应用中几乎无法适配双击、三击的正常交互。
三、与常规按键处理方案的核心差异
特性        这段代码        常规按键处理(状态机 / 定时器方案)
判定时机        固定 640ms 采样结束后        松开即触发单击,500ms 内二次按下触发双击
双击逻辑        640ms 内两次点击 = 双击        第一次松开后 500ms 内二次按下 = 双击
实时性        延迟 640ms,反馈滞后        无延迟,操作即响应
采样过程        锁死状态,忽略新按键        实时扫描,响应新动作
防抖处理        依赖采样间隔,无专门优化        连续采样(如 10ms)确认电平,过滤抖动
事件粒度        仅 6 种复合事件        支持按下、松开、单击、双击、长按、长按保持
四、总结
这段代码的核心问题是用 “固定时长的连续采样 + 上升沿统计” 替代了常规按键的 “状态时序判定”,导致实时性差、交互逻辑不符合用户习惯、状态管理混乱,且缺乏实用的基础事件类型。
如果要适配常规按键交互,建议改用按键状态机 + 定时器超时判定方案:通过扫描引脚电平检测按下 / 松开,用定时器记录点击间隔(如 500ms 双击窗口、300ms 长按阈值),实时输出按下、松开、单击、双击、长按等事件,既保证实时性,又符合用户操作习惯。
总结
核心缺陷:640ms 固定采样窗口导致判定延迟、双击逻辑违背用户习惯,且采样过程锁死新按键;
关键问题:缺乏基础按键事件、无有效防抖、全局状态管理易出错,实用性极低;
改进方向:替换为状态机 + 超时判定的常规按键方案,适配单击、双击、长按的标准交互逻辑。
回复

使用道具 举报

ID:1155837 发表于 2026-2-6 19:02 | 显示全部楼层
ydatou 发表于 2026-2-6 11:19
想法很新颖,不过很难用于工程实践:
采用固定32次(640ms)采样窗口,无法滚动更新数据。感觉双击,长按会 ...

你可以调节定时器中断间隔来延长时间,我测过其实30ms一次中断下比较可靠,单击双击长按所有功能都稳定触发,另外我做的开关机没有用过这么长时间,最多的也就是1.2秒,所以确实没考虑过3秒开机的情况。虽然也可以用三个uint16拼出来更长的采样时间,也可以实现超时退出采样,但是因为那样代码就复杂了,还不如老实用多层switch呢。
回复

使用道具 举报

ID:1155837 发表于 2026-2-6 19:05 | 显示全部楼层
dj3365191 发表于 2026-2-6 11:16
从程序里看不出按键接在哪个IO口了

Read_Temp = digitalRead(IO_Set);//采样一次
这一行是读取IO电平。这是arduino的写法,你如果是51的话直接写Read_Temp = PXX就可以了
回复

使用道具 举报

ID:1155837 发表于 2026-2-6 20:01 | 显示全部楼层
ydatou 发表于 2026-2-6 11:19
想法很新颖,不过很难用于工程实践:
采用固定32次(640ms)采样窗口,无法滚动更新数据。感觉双击,长按会 ...

说很难用于工程实践有些过了,实际上即使是640ms的采样串口下,我进行几十次输入测试也都可以稳定触发长按单击双击三击,只是时间太短,没那么快四连击,你可以试试,不要光感觉,复制代码,只需要改一下按键的引脚输入就可以测试了,用串口回传keyevent变量即可。
回复

使用道具 举报

ID:1155837 发表于 2026-2-6 20:29 | 显示全部楼层
更新一下,相比传统switch状态机,目前来说固定32次采样确实不够灵活,因此我简单修改了一下,实现了按键输入超时检测。在实时性上能达到switch状态机的水平了,那种写法可以规定每个状态的等待时间,还可以在到达状态后立即返回,比如三击后立即返回三击状态,确实实时性更高。
本采访法按键处理函数,实现超时检查的方法就是如果按键没按下就计数一次,按下了就清空计数,连续12次没按下就停止采样,没采完的直接全部补1。总共增加了10行,依然比多层switch方案简短。
void Key_Sampling(unsigned char IO_Set){//传入需要进行采样的引脚
  //实际上本函数自带消抖,因为不足一个定时器间隔的下降沿无法触发采样
  //在20ms以上的定时器中断中调用本函数,推荐30ms一次中断
  static unsigned char Sampling_Counter = 0;//采样次数计数器
  static unsigned long Sampling_Temp = 0;//采样结果缓存
  static bool Previous_Key = false;//之前的按键采样记录
  static bool Now_Key = false;//当前的按键采样记录
  static bool Is_Sampling = false;//正在采样标志
  static bool Start_Sampling = false;//开始采样标志,由下降沿触发
  static bool Read_Temp = false;//按键状态缓存,每次进入函数时读取
  static unsigned char Key_Release_Counter = 0;//超时计数器,可以更快的响应单击和双击操作
  unsigned char Rise_Edge_Result = 0;//上升沿计算的结果,非static变量,重入自动清零
  Read_Temp = digitalRead(IO_Set);//采样一次
  //采样触发(检测下降沿)
  if(!Is_Sampling){//如果没有在采样,才做判断
  Previous_Key = Now_Key;//存储上一次的结果
  Now_Key = Read_Temp;//读取本次结果
  //Key_Event = NOKEY;//未在采样状态,全局按键事件为无键
  if(!Now_Key && Previous_Key){//上一次为1,这一次为0,是下降沿
  Start_Sampling = true;//开始采样
  }
  else{
    return;}//未触发采样,直接返回
  }
  //开始采样与结果处理
  if(Start_Sampling){
    if(Sampling_Counter < 32){
      Is_Sampling = true;
      Sampling_Temp <<= 1;//整体左移一位,低位补0
      if (Read_Temp){//采样引脚,如果为高电平,最低位置1
        Sampling_Temp |= 0x01;//如果引脚为高才计1,如果为低就继续左移一位,也就是补0
        Key_Release_Counter++;
        if(Key_Release_Counter >= 12){//超过240ms没有按键输入
        Key_Release_Counter = 0;
        //采样超时,提前结束,未采样的部分全部补1
        Sampling_Temp = (Sampling_Temp << (32-Sampling_Counter)) | ((1UL << (32-Sampling_Counter)) - 1);
        Sampling_Counter = 32;//标记为采样结束
        }
        }
        else{//如果按键为低电平
        Key_Release_Counter = 0;
        }
        Sampling_Counter++;
        }
      else{//采满32次,采样结束,开始计算
        Is_Sampling = false;
        Start_Sampling = false;
        Sampling_Counter = 0;//清空计数器
        Rise_Edge_Result = Rise_Edge_Counter(Sampling_Temp);//计算上升沿数量
        switch(Rise_Edge_Result){
          case 0: Key_Event = LONGKEY;  break;
          case 1: Key_Event = SINGLEKEY;break;
          case 2: Key_Event = DOUBLEKEY;break;
          case 3: Key_Event = TRIPLEKEY;break;
          case 4: Key_Event = QUADRAKEY;break;
        }
        }
  }
}
回复

使用道具 举报

ID:1155837 发表于 2026-2-7 13:45 | 显示全部楼层
ydatou 发表于 2026-2-6 14:12
下面是ai分析的结果,虽不完全赞同,但部分是正确的。
你希望我分析这段按键采样代码的逻辑,并结合 20ms  ...

这个ai的说法不太对,因为一个要实现单击双击三击的按键处理函数,等待下次按键输入的时间是必须的,不可能输入单击不等待就直接输出。而传统按键状态机确实可以减少在单击输入下的等待时间,比如单击后等待300ms,双击后再等待300ms。但是你可以看看我更新后的另一条评论,已经实现了超时退出,在实时性上已经不输了。
至于这个ai说的什么用户习惯,完全狗屁不通,两次单击和一次双击完全是同一个事件,哪里来的习惯不同一说?
至于采样窗口固定?你可以调整定时器中断间隔,实际上30ms是最合适的,结合我新增加的超时退出,实时性也是很好的。
总之,各个方案有各个方案的优势,我的采样32次的方法,其实就是从另一个角度去看待按键输入。
回复

使用道具 举报

ID:1155837 发表于 2026-2-8 13:49 | 显示全部楼层
千早爱音爱玩51 发表于 2026-2-6 20:29
更新一下,相比传统switch状态机,目前来说固定32次采样确实不够灵活,因此我简单修改了一下,实现了按键输 ...

其实超时后没必要对数据全部补1,直接输出就行了,默认全是0
Sampling_Temp = (Sampling_Temp << (32-Sampling_Counter)) | ((1UL << (32-Sampling_Counter)) - 1);
这行可以删掉,不需要这个计算
回复

使用道具 举报

ID:344848 发表于 2026-2-9 17:22 | 显示全部楼层
对于爱好者提高个人掌握单片机技能来说,有一定的参考价值,但对于实际工程来说,意义不大,对于每个实际用户来说,每个用户认为长、短时间实际有相当大的差异。有雷同的功能产品在市场有一款产品:某厂家的温度控制器,为了压低产品价格和追求小体积。减少按键个数,厂家将两类不同功能合并为一键来实现,即长按键和短按键来实现,通过显示功能的来确定用户选择。短按键是普通用户需要调整的功能,长按键是提高产品性能的高级用户使用——这些功能普通用户不常使用。
回复

使用道具 举报

ID:1155837 发表于 2026-2-12 02:07 | 显示全部楼层
本帖最后由 千早爱音爱玩51 于 2026-2-13 00:37 编辑

突然发现如果用我的采样法按键处理来做开关机的话,有一个bug(划掉),或者说特性。
这个状态机,进行按键采样的条件必须是有下降沿,而通过外部中断唤醒后才开始检测,单片机检测到的Nowkey和previouskey都是低电平,也就是没有下降沿,这导致无法真正的检测按键,导致超时关机。
而只要单击后长按,就可以给一个下降沿,开始采样,最后得到长按信号。
下面是休眠的测试代码,用到了TM1650来显示参数,没有的话一个LED就够了、#include "STC8G.H"
#include "intrins.H"
#include "TM1650.H"
unsigned long Main_FOSC = 6000000L;
bit poweron = 1;
bit was_enter_sleep = 0;
bit need_wakeup_sampling = 0;
void IO_CONFIG(void){
//IO模式设置
//P31电源开关开漏,低电平开
//P32外部按键高阻,启用内部上拉
//P33LEDPWM信号输出,推挽
//P54风扇开关信号输出推挽,高电平开
//P55NTC电源开关推挽,低电平开,同时控制ETA9741的按键,开漏
    P_SW2 = 0X80;//允许访问扩展寄存器(设置上拉,转换速度等扩展寄存器)
    P3M1 = 0XF7;//P30高阻,P31开漏,P32高阻,P33推挽
    P3M0 = 0X0A;
    P5M1 = 0XEF;//P54推挽,P55开漏
    P5M0 = 0X30;
    P3SR = 0XF7;//P33引脚电平转换速度为快
    P3DR = 0XF7;//P33引脚驱动电流增强
//省电设置//
//高阻输入的引脚如果无上拉下拉,电平不确定就会导致漏电
    P3IE = 0X05;//P3只启用P30和P32的数字输入功能,允许读取外部电平,防止休眠漏电
    P5IE = 0XFF;//P5全部关闭数字输入,不需要读取外部电平
//规定引脚初始电平//
    P31 = 1;//打开电源
    P33 = 0;//初始化为关闭LED
    P54 = 0;//N管高电平开
    P55 = 1;//P管低电平开
}
void TIMER_INIT(void){
    AUXR &= 0x7F;
    TCON = 0X01;//中断标志清零,外部中断0设置为下降沿触发。
    TMOD = 0X00;//00000000,GATE置零,TR=1就开始计数,T_CT=0,作为定时器,T0T1均十六位自动重载
    //AUXR辅助寄存器不用配置,复位值就是12T模式,即系统时钟12分频
    //定时器周期配置
    //定时器0,10ms一次溢出中断
    TL0 = 0x68;                //30ms@6MHZ
    TH0 = 0xC5;               
    TF0 = 1;
    //定时器1,125ms一次溢出中断
    TH1 = 0X0B;
    TL1 = 0XDC;
    TF1 = 0;
    //STC8G1K08A SOP8只有定时器0和1两个定时器
}
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;
}
#define NOKEY 0
#define SINGLEKEY 1
#define DOUBLEKEY 2
#define TRIPLEKEY 3
#define QUADRAKEY 4
#define LONGKEY 5
unsigned char Key_Event = NOKEY;
void Key_Sampling(void){//传入需要进行采样的引脚
  //实际上本函数自带消抖,因为不足一个定时器间隔的下降沿无法触发采样
  //在20ms以上的定时器中断中调用本函数,推荐30ms一次中断
  static unsigned char Sampling_Counter = 0;//采样次数计数器
  static unsigned long Sampling_Temp = 0;//采样结果缓存
  static bit Previous_Key = false;//之前的按键采样记录
  static bit Now_Key = false;//当前的按键采样记录
  static bit Is_Sampling = false;//正在采样标志
  static bit Start_Sampling = false;//开始采样标志,由下降沿触发
  static unsigned char Key_Release_Counter = 0;//超时计数器,可以更快的响应单击和双击操作
  unsigned char Rise_Edge_Result = 0;//上升沿计算的结果,非static变量,重入自动清零
  bit Read_Temp = P32;//按键状态缓存,每次进入函数时读取
  //采样触发(检测下降沿)
  if(!Is_Sampling){//如果没有在采样,才做判断
  Previous_Key = Now_Key;//存储上一次的结果
  Now_Key = Read_Temp;//读取本次结果
  //Key_Event = NOKEY;//未在采样状态,全局按键事件为无键
  if((!Now_Key && Previous_Key) || need_wakeup_sampling){//上一次为1,这一次为0,是下降沿
  Start_Sampling = true;//开始采样
  }
  else{
    return;}//未触发采样,直接返回
  }
  //开始采样与结果处理
  if(Start_Sampling){
    if(Sampling_Counter < 32){
      Is_Sampling = true;
      Sampling_Temp <<= 1;//整体左移一位,低位补0
      if (Read_Temp){//采样引脚,如果为高电平,最低位置1
        Sampling_Temp |= 0x01;//如果引脚为高才计1,如果为低就继续左移一位,也就是补0
        Key_Release_Counter++;
        if(Key_Release_Counter >= 12){//超过240ms没有按键输入
        Key_Release_Counter = 0;
        //采样超时,提前结束,未采样的部分全部补1
        Sampling_Temp = (Sampling_Temp << (32-Sampling_Counter)) | ((1UL << (32-Sampling_Counter)) - 1);
        Sampling_Counter = 32;//标记为采样结束
        }
        }
        else{//如果按键为低电平
        Key_Release_Counter = 0;
        }
        Sampling_Counter++;
        }
      else{//采满32次,采样结束,开始计算
        Is_Sampling = false;
        Start_Sampling = false;
        need_wakeup_sampling = false;
        Sampling_Counter = 0;//清空计数器
        Rise_Edge_Result = Rise_Edge_Counter(Sampling_Temp);//计算上升沿数量
        switch(Rise_Edge_Result){
          case 0: Key_Event = LONGKEY;  break;
          case 1: Key_Event = SINGLEKEY;break;
          case 2: Key_Event = DOUBLEKEY;break;
          case 3: Key_Event = TRIPLEKEY;break;
          case 4: Key_Event = QUADRAKEY;break;
        }
        }
  }
}
volatile unsigned char sleep_timer = 0;//睡眠计时器,用于避免误触唤醒
bit wakeup_flag = 1;
bit backsleep_flag = 0;
void ENTER_SLEEP(void){
    while(1){//外层while
    TR0 = 0;
    P33 = 0;
    TM1650_Init(0,0);
    sleep_timer = 0;
    poweron = false;
    Key_Event = NOKEY;
    was_enter_sleep = true;
    PCON = 0X02;//进入掉电模式
/////唤醒后从这里继续执行/////
    _nop_();_nop_();_nop_();_nop_();//空指令,避免CPU上电后的不稳定
    TR0 = 1;//打开定时器0,开始计时,在中断中执行按键检测
    TM1650_Init(0,4);
    TM1650_Display_Word("FUnC");
    while(!poweron){//内层while
        WDT_CONTR |= 0X35;//喂狗
        if(Key_Event ==LONGKEY){
            Key_Event = NOKEY;
            poweron ^= 1;}
        if(backsleep_flag){
            backsleep_flag = 0;
            break;//跳出内层while,重新执行休眠。
        }
    }
    if(poweron){
        break;//跳出外层while
    }
    }
/////从这里开始执行恢复程序/////
    TR0 = 1;
    P33 = 1;
    TM1650_Display_Word("On  ");
}
void TIMER0_ROUTINE(void) interrupt 1 {//定时器0 30ms中断服务函数
    Key_Sampling();
    if(!poweron && sleep_timer <= 63){//误唤醒超时检测
        sleep_timer++;}
    else if(!poweron && sleep_timer > 63){
        sleep_timer = 0;//超过1600ms,清空状态,重新睡眠
        backsleep_flag = 1;
    }
    }
void INT0_ROUTINE(void) interrupt 0 {//定时器1 125ms中断服务函数
    if(was_enter_sleep){
        need_wakeup_sampling = true;
        was_enter_sleep = false;
    }
    }
void main(void){
    unsigned int a = 0;
    IO_CONFIG();
    TIMER_INIT();
    TM1650_PinSet(54,55);
    TM1650_Init(100,4);
    TR0 = 1;
    WDT_CONTR = 0X25;
    IE = 0X8B;
    while(1){
        a = 65535;
        while(a--);
        if(!poweron){
            ENTER_SLEEP();
        }
        if(Key_Event ==LONGKEY){
            TM1650_Display_Word("ToSL");
            Key_Event = NOKEY;
            poweron ^= 1;
        }
        else{
        TM1650_Display_Num(Key_Event,0);
        Key_Event = NOKEY;
        }
    }
}
回复

使用道具 举报

ID:1155837 发表于 2026-2-12 02:38 | 显示全部楼层
tips:上面的tm1650驱动库,点进我的主页找相关帖子里有
如果你真的不需要单击后休眠唤醒的话,也是很容易解决的。只需要增加两个变量即可。
bit was_enter_sleep = false;
bit need_wakeup_sampling = false;
在休眠时设置 was_enter_sleep =true;
在唤醒时int0中断服务函数中检测flag:
void INT0_ROUTINE(void) interrupt 0 {//定时器1 125ms中断服务函数
    if(was_enter_sleep){
        need_wakeup_sampling = true;
        was_enter_sleep = false;
    }
    }
在采样函数中增加额外条件:
  if((!Now_Key && Previous_Key) || need_wakeup_sampling){//上一次为1,这一次为0,是下降沿
  Start_Sampling = true;//开始采样
  }
然后在采样结束后清零need flag即可。
如此以来就可以规避单击后长按唤醒。
但我真的不推荐你这么搞,单击后长按唤醒更稳定而且不容易误触。
说句题外话,这个代码在51上编译大概需要800字节 ,因为大量使用了ulong 32位量,51是不支持直接计算32位变量的,都是软件模拟的,所以尺寸大,速度也慢(即使使用了swar算法,24mhz下依然需要50us才能算完上升沿)
但是一旦你将眼光投到32单片机上就会发现,32单片机天生支持32位 移位运算,和32位逻辑运算,不需要多行汇编模拟,因此速度会快的惊人,十几个时钟周期就能算完上升沿。
但你按键数量多了之后就会发现本库函数的优点,变量都集中在一个函数中,仅仅一个简单的结构体,就可以无限扩展按键,每个按键只需要多9字节内存(全部变量占用,当然,在51上使用bit变量更节省),无疑是相当有优势的。
回复

使用道具 举报

ID:1155837 发表于 2026-2-21 01:55 | 显示全部楼层
我发现这个代码在30ms定时器中断的情况下无法稳定运行,容易出现将单击误判为长按的问题,但在20ms下完全不会有问题,但是不知道为什么,我在arduino esp32上始终无法复现这个问题,可能是和51单片机有关系。但是至少20ms下是完全稳定可用的
回复

使用道具 举报

ID:1155837 发表于 2026-2-21 18:03 | 显示全部楼层
好吧,我已经找到了问题原因,是之前加入的提前退出功能导致的
我之前写的是这样
Sampling_Temp = (Sampling_Temp << (32-Sampling_Counter)) | ((1UL << (32-Sampling_Counter)) - 1);
这导致其实多左移了一位,如果按键低电平时间持续很短的话,采样结果为01111111...,再执行这个就会导致丢失最高位,输出0xffffffff,进而导致上升沿数量为0。
将这里改成31,问题就可以完全解决了。
回复

使用道具 举报

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

本版积分规则

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

Powered by 单片机教程网

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