如何實現按鍵的短按、長按檢測?
在電子產品中經常用到按鍵,尤其是經常需要MCU判斷 短按 和 長按 這兩種動作,本篇我們來專門聊下這個話題。
只談理論太無聊,我們還是結合著實際應用來說明。之前寫過一篇關於 《CH573第一篇:實現自拍杆藍芽遙控器1》 的文章,例子預設的功能是藍芽連線後不斷的傳送資料,從而不斷的拍照。而實際中的遙控器通常是按一次按鍵,控制一次,我們在來實現該功能。
板子上只有兩個按鍵,一個是RESET按鍵,一個是DOWNLOAD按鍵,我們使用DOWNLAOD按鍵,按鍵的一端接GND,另外一端接CH573的PB22引腳。
原理圖中有一個NC的C5,但是實際板子上我卻沒有找到它,可能是版本不一致。
提前說明一下:CH573的程式碼裡跑了TMOS(Task Management Operating System),可以理解為一個簡單的作業系統,所以下面的程式碼一般的裸機程式碼看著略有不同,不過核心思想都是一樣的,用在其他地方也很容易移植,只需要將其中的定時器部分改寫即可。
最初我是這麼做的,把PB22配置為上拉輸入,開啟下降沿中斷,在中斷服務函式裡,啟動一個事件,執行藍芽傳送。程式碼如下:
void Key_Init()
{
GPIOB_ModeCfg( GPIO_Pin_22, GPIO_ModeIN_PU );
GPIOB_ITModeCfg( GPIO_Pin_22, GPIO_ITMode_FallEdge );
PFIC_EnableIRQ( GPIO_B_IRQn );
}
void GPIOB_IRQHandler( void )
{
if(GPIOB_ReadPortPin(GPIO_Pin_22)==0)
{
GPIOB_ClearITFlagBit( GPIO_Pin_22);
tmos_set_event( hidEmuTaskId, START_REPORT_EVT );
}
}
這麼寫能工作,但是有問題,就是經常會出現按一下誤判為多次按下。原因大家應該都清楚,因為按鍵存在抖動,所以一次按下有可能進入多次進入中斷。
理想中的按下-彈起波形是這樣的:
但是實際由於按鍵抖動的存在,實際的波形可能是這樣的:
不信的話你可以接上示波器看看,或者軟體驗證,比如在GPIO中斷服務函式裡,設定一個全域性變數,讓它每次進入中斷後加1,按按鍵觀察這個變數的值。
那麼該如何消除抖動呢?一種方法是硬體消抖,即按鍵兩端並聯一個小電容(電容大小由按鍵的機械特性來決定),另外一種方法是我們今天要重點介紹的軟體消抖。
方法一:常用的加延時函式
在中斷服務函式中加一個比如10ms的延時函式,延時時間的長短取決於實際所用的按鍵特性,只要延時時間比抖動時間略大即可。原理很簡單,加了延時就避開了抖動的這段時間,在延時之後判斷引腳電平,如果為低電平就表示是按下。
void GPIOB_IRQHandler( void )
{
if(GPIOB_ReadPortPin(GPIO_Pin_22)==0)
{
mDelaymS(10);
if(GPIOB_ReadPortPin(GPIO_Pin_22)==0)
tmos_set_event( hidEmuTaskId, START_REPORT_EVT );
GPIOB_ClearITFlagBit( GPIO_Pin_22);
}
}
這個方法很簡單,但是不好的地方是延時佔用MCU資源。尤其是這裡的BLE應用,在中斷服務函式中執行時間長會引起藍芽連線中斷,所以這裡不能這麼用,我實際測試當按鍵按快一點就很容易引起藍芽連線中斷。
方法二:加定時器
它的原理和方法一類似,只不過是不在中斷服務函式中阻塞等待,而是用一個定時器,程式碼如下:
void GPIOB_IRQHandler( void )
{
if(GPIOB_ReadPortPin(GPIO_Pin_22)==0)
{
GPIOB_ClearITFlagBit( GPIO_Pin_22);
tmos_stop_task(hidEmuTaskId, START_DEBOUNCE_EVT);
tmos_start_task(hidEmuTaskId, START_DEBOUNCE_EVT,16);
}
}
if(events & START_DEBOUNCE_EVT)
{
if(GPIOB_ReadPortPin(GPIO_Pin_22)==0)
{
PRINT("short press\n");
tmos_set_event( hidEmuTaskId, START_REPORT_EVT );
}
return (events ^ START_DEBOUNCE_EVT);
}
它的邏輯是每次抖動的下降沿重新開啟10ms定時器,在定時器時間到之後判斷IO電平狀態來判斷按鍵是否按下。
需要注意的是:10ms定時器不是一個週期性的定時器,它是一次性的,即時間到了之後就停止計時了。另外每次進中斷後先讓定時器重新重頭開始計時。如果大家用其他程式碼實現時要注意這兩點。
此方法的好處不像加延時函式那樣佔用MCU資源。我實際測試這個方法可用,不會引起藍芽連線中斷。
以上介紹了使用中斷的方式來判斷按鍵短按,可以看到它判斷的依據是按鍵按下(由高電平變到低電平)這個狀態。下面在方法二的基礎上我們來實現長按的檢測,判斷長按的依據是按下後持續的維持一段時間低電平。程式碼如下:
if(events & START_DEBOUNCE_EVT)
{
if(GPIOB_ReadPortPin(GPIO_Pin_22)==0)
{
PRINT("short press\n");
tmos_set_event( hidEmuTaskId, START_REPORT_EVT );
tmos_start_task( hidEmuTaskId, START_LONGCHECK_TIMER,16 );
}
return (events ^ START_DEBOUNCE_EVT);
}
if(events & START_LONGCHECK_TIMER)
{
static int cnt=0;
if(GPIOB_ReadPortPin(GPIO_Pin_22)==0)
{
cnt++;
if(cnt>100)
{
PRINT("long press\n");
tmos_stop_task( hidEmuTaskId, START_LONGCHECK_TIMER);
cnt =0;
}
else
tmos_start_task( hidEmuTaskId, START_LONGCHECK_TIMER,16 );
}
else
{
cnt=0;
tmos_stop_task( hidEmuTaskId, START_LONGCHECK_TIMER );
}
return (events ^ START_LONGCHECK_TIMER);
}
實現的邏輯是:當檢測到短按時,再開啟一個10ms定時器,在定時器到時之中判斷電平狀態,如果為低電平,就讓cnt變數加1,否則cnt=0,當cnt>100,即低電平持續1s認為是長按。我在這裡當判斷到長按之後或者IO變高之後會停止掉這個定時器,否則週期定時,因為沒必要一直開著定時器。
除了上述的中斷方式,還可以使用 輪詢 的方式來實現,程式碼如下:
void Key_Init()
{
GPIOB_ModeCfg( GPIO_Pin_22, GPIO_ModeIN_PU );
}
if(events & START_KEYSCAN_EVT)
{
KeyScan();
tmos_start_task(hidEmuTaskId, START_KEYSCAN_EVT,160);// 100ms執行一次KeyScan()
return (events ^ START_KEYSCAN_EVT);
}
bool key_press_flag = false; // 按下標誌
bool key_long_press_flag = false; // 長按標誌
void KeyScan()
{
if(GPIOB_ReadPortPin(GPIO_Pin_22) == 0) // 低電平
{
if(key_press_flag == false)
tmos_start_task( hidEmuTaskId, START_LONGCHECK_TIMER, 1600 ); // 啟動1s定時器
key_press_flag = true; // 置位按下標誌
}
else if(key_press_flag == true) // 高電平同時按鍵被按下過 ,表示是按下後的彈起
{
key_press_flag = false; // 清除按下標誌
if(key_long_press_flag == false)// 短按後的彈起
{
tmos_stop_task(hidEmuTaskId, START_LONGCHECK_TIMER);
PRINT("short press\n");
tmos_set_event( hidEmuTaskId, START_REPORT_EVT );
}
else // 長按後的彈起
{
key_long_press_flag =false;
}
}
else
{
key_press_flag = false;
key_long_press_flag = false;
}
}
if(events & START_LONGCHECK_TIMER)
{
key_long_press_flag =true;
PRINT("long press\n");
return (events ^ START_LONGCHECK_TIMER);
}
上面的這段程式碼初次看著有點繞,但是看明白了之後會覺得這個實現邏輯還是挺好的,註釋寫了,這裡不再詳細解釋了,我在多個專案裡使用的都是它。它兼顧了去抖和短按/長按的檢測,並且長按可以判斷出長按按下/長按彈起。短按是檢測到彈起時認為是短按動作。另外如果想同時支援多個長按,也很方便新增。
輪詢和中斷各有優缺點,大家可以根據實際情況來選擇,你一般常用哪種方式呢?
end
一口Linux
關注,回覆【 1024 】海量Linux資料贈送
精彩文章合集
文章推薦
☞ 【專輯】 ARM
☞ 【專輯】 粉絲問答
☞ 【專輯】 所有原創
☞ 【 專輯 】 linux 入門
☞ 【 專輯 】 計算機網路
☞ 【 專輯 】 Linux驅動
☞ 【乾貨】 嵌入式驅動工程師學習路線
☞ 【乾貨】 Linux嵌入式所有知識點-思維導圖
- 如何實現按鍵的短按、長按檢測?
- 當代“程式猿”必備的神器有哪些?看看你用過幾個!
- 一文讀懂 | Linux系統啟動過程
- CAN協議詳解
- 手把手教你ubuntu下移植MJPG-streamer
- 一文搞懂Linux核心的4大IO排程演算法
- 成為架構師!必須掌握10種常見的架構模式
- Shell 指令碼中經典的13個面試題
- 圖解|Linux記憶體背後的那些神祕往事
- C語言和C 的區別和聯絡
- 如何從頭自己製作開發板?圖文並茂,一步步操作給你看。
- 資料結構中常見的樹
- 程式設計師必備高階技術之函式呼叫棧
- SPI 硬體 Linux驅動詳解
- 如何將一個模組檔案編譯到Linux核心中?
- 重新理解一波裝置驅動 | Linux 驅動
- 計算機是怎樣把資料通過網線傳輸出去的?
- Linux C基礎——” Makefile “ 檔案管理大師你拜訪過嘛?
- 路由器是如何工作的?
- Linux效能問題分析流程與效能優化思路