欧美极品高清xxxxhd,国产日产欧美最新,无码AV国产东京热AV无码,国产精品人与动性XXX,国产传媒亚洲综合一区二区,四库影院永久国产精品,毛片免费免费高清视频,福利所导航夜趣136

 找回密碼
 立即注冊

QQ登錄

只需一步,快速開始

搜索
查看: 1511|回復: 19
收起左側

僅需40行!可實現按鍵長按單擊雙擊三擊四擊,挑戰行數最少代碼

  [復制鏈接]
回帖獎勵 2 黑幣 回復本帖可獲得 2 黑幣獎勵! 每人限 1 次
ID:1155837 發表于 2026-2-5 01:11 | 顯示全部樓層 |閱讀模式
差不多一年前我發了個帖子,內容是“使用掃描后輸出結果進行邏輯運算的按鍵處理方法”,就在這里http://m.raoushi.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,問題就可以完全解決了。
回復

使用道具 舉報

ID:1084416 發表于 2026-3-5 10:54 | 顯示全部樓層
感謝分享思路
回復

使用道具 舉報

ID:1084416 發表于 2026-3-5 11:22 | 顯示全部樓層
感謝分享思路
回復

使用道具 舉報

您需要登錄后才可以回帖 登錄 | 立即注冊

本版積分規則

小黑屋|51黑電子論壇 |51黑電子論壇6群 QQ 管理員QQ:125739409;技術交流QQ群281945664

Powered by 單片機教程網

快速回復 返回頂部 返回列表