在電子產(chǎn)品中經(jīng)常用到按鍵,尤其是經(jīng)常需要MCU判斷短按和長(zhǎng)按這兩種動(dòng)作,本篇我們來(lái)專門聊下這個(gè)話題。
只談理論太無(wú)聊,我們還是結(jié)合著實(shí)際應(yīng)用來(lái)說(shuō)明。之前寫過(guò)一篇關(guān)于《CH573第一篇:實(shí)現(xiàn)自拍桿藍(lán)牙遙控器1》的文章,例子默認(rèn)的功能是藍(lán)牙連接后不斷的發(fā)送數(shù)據(jù),從而不斷的拍照。而實(shí)際中的遙控器通常是按一次按鍵,控制一次,我們?cè)趤?lái)實(shí)現(xiàn)該功能。
板子上只有兩個(gè)按鍵,一個(gè)是RESET按鍵,一個(gè)是DOWNLOAD按鍵,我們使用DOWNLAOD按鍵,按鍵的一端接GND,另外一端接CH573的PB22引腳。
原理圖中有一個(gè)NC的C5,但是實(shí)際板子上我卻沒(méi)有找到它,可能是版本不一致。
提前說(shuō)明一下:CH573的代碼里跑了TMOS(Task Management Operating System),可以理解為一個(gè)簡(jiǎn)單的操作系統(tǒng),所以下面的代碼一般的裸機(jī)代碼看著略有不同,不過(guò)核心思想都是一樣的,用在其他地方也很容易移植,只需要將其中的定時(shí)器部分改寫即可。
最初我是這么做的,把PB22配置為上拉輸入,開啟下降沿中斷,在中斷服務(wù)函數(shù)里,啟動(dòng)一個(gè)事件,執(zhí)行藍(lán)牙發(fā)送。代碼如下:
voidKey_Init()
{
GPIOB_ModeCfg(GPIO_Pin_22,GPIO_ModeIN_PU);
GPIOB_ITModeCfg(GPIO_Pin_22,GPIO_ITMode_FallEdge);
PFIC_EnableIRQ(GPIO_B_IRQn);
}
voidGPIOB_IRQHandler(void)
{
if(GPIOB_ReadPortPin(GPIO_Pin_22)==0)
{
GPIOB_ClearITFlagBit(GPIO_Pin_22);
tmos_set_event(hidEmuTaskId,START_REPORT_EVT);
}
}
這么寫能工作,但是有問(wèn)題,就是經(jīng)常會(huì)出現(xiàn)按一下誤判為多次按下。原因大家應(yīng)該都清楚,因?yàn)榘存I存在抖動(dòng),所以一次按下有可能進(jìn)入多次進(jìn)入中斷。
理想中的按下-彈起波形是這樣的:
但是實(shí)際由于按鍵抖動(dòng)的存在,實(shí)際的波形可能是這樣的:
不信的話你可以接上示波器看看,或者軟件驗(yàn)證,比如在GPIO中斷服務(wù)函數(shù)里,設(shè)置一個(gè)全局變量,讓它每次進(jìn)入中斷后加1,按按鍵觀察這個(gè)變量的值。
那么該如何消除抖動(dòng)呢?一種方法是硬件消抖,即按鍵兩端并聯(lián)一個(gè)小電容(電容大小由按鍵的機(jī)械特性來(lái)決定),另外一種方法是我們今天要重點(diǎn)介紹的軟件消抖。
方法一:常用的加延時(shí)函數(shù)
在中斷服務(wù)函數(shù)中加一個(gè)比如10ms的延時(shí)函數(shù),延時(shí)時(shí)間的長(zhǎng)短取決于實(shí)際所用的按鍵特性,只要延時(shí)時(shí)間比抖動(dòng)時(shí)間略大即可。原理很簡(jiǎn)單,加了延時(shí)就避開了抖動(dòng)的這段時(shí)間,在延時(shí)之后判斷引腳電平,如果為低電平就表示是按下。
voidGPIOB_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);
}
}
這個(gè)方法很簡(jiǎn)單,但是不好的地方是延時(shí)占用MCU資源。尤其是這里的BLE應(yīng)用,在中斷服務(wù)函數(shù)中執(zhí)行時(shí)間長(zhǎng)會(huì)引起藍(lán)牙連接中斷,所以這里不能這么用,我實(shí)際測(cè)試當(dāng)按鍵按快一點(diǎn)就很容易引起藍(lán)牙連接中斷。
方法二:加定時(shí)器
它的原理和方法一類似,只不過(guò)是不在中斷服務(wù)函數(shù)中阻塞等待,而是用一個(gè)定時(shí)器,代碼如下:
voidGPIOB_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("shortpress
");
tmos_set_event(hidEmuTaskId,START_REPORT_EVT);
}
return(events^START_DEBOUNCE_EVT);
}
它的邏輯是每次抖動(dòng)的下降沿重新開啟10ms定時(shí)器,在定時(shí)器時(shí)間到之后判斷IO電平狀態(tài)來(lái)判斷按鍵是否按下。
需要注意的是:10ms定時(shí)器不是一個(gè)周期性的定時(shí)器,它是一次性的,即時(shí)間到了之后就停止計(jì)時(shí)了。另外每次進(jìn)中斷后先讓定時(shí)器重新重頭開始計(jì)時(shí)。如果大家用其他代碼實(shí)現(xiàn)時(shí)要注意這兩點(diǎn)。
此方法的好處不像加延時(shí)函數(shù)那樣占用MCU資源。我實(shí)際測(cè)試這個(gè)方法可用,不會(huì)引起藍(lán)牙連接中斷。
以上介紹了使用中斷的方式來(lái)判斷按鍵短按,可以看到它判斷的依據(jù)是按鍵按下(由高電平變到低電平)這個(gè)狀態(tài)。下面在方法二的基礎(chǔ)上我們來(lái)實(shí)現(xiàn)長(zhǎng)按的檢測(cè),判斷長(zhǎng)按的依據(jù)是按下后持續(xù)的維持一段時(shí)間低電平。代碼如下:
if(events&START_DEBOUNCE_EVT)
{
if(GPIOB_ReadPortPin(GPIO_Pin_22)==0)
{
PRINT("shortpress
");
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)
{
staticintcnt=0;
if(GPIOB_ReadPortPin(GPIO_Pin_22)==0)
{
cnt++;
if(cnt>100)
{
PRINT("longpress
");
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);
}
實(shí)現(xiàn)的邏輯是:當(dāng)檢測(cè)到短按時(shí),再開啟一個(gè)10ms定時(shí)器,在定時(shí)器到時(shí)之中判斷電平狀態(tài),如果為低電平,就讓cnt變量加1,否則cnt=0,當(dāng)cnt>100,即低電平持續(xù)1s認(rèn)為是長(zhǎng)按。我在這里當(dāng)判斷到長(zhǎng)按之后或者IO變高之后會(huì)停止掉這個(gè)定時(shí)器,否則周期定時(shí),因?yàn)闆](méi)必要一直開著定時(shí)器。
除了上述的中斷方式,還可以使用輪詢的方式來(lái)實(shí)現(xiàn),代碼如下:
voidKey_Init()
{
GPIOB_ModeCfg(GPIO_Pin_22,GPIO_ModeIN_PU);
}
if(events&START_KEYSCAN_EVT)
{
KeyScan();
tmos_start_task(hidEmuTaskId,START_KEYSCAN_EVT,160);//100ms執(zhí)行一次KeyScan()
return(events^START_KEYSCAN_EVT);
}
boolkey_press_flag=false;//按下標(biāo)志
boolkey_long_press_flag=false;//長(zhǎng)按標(biāo)志
voidKeyScan()
{
if(GPIOB_ReadPortPin(GPIO_Pin_22)==0)//低電平
{
if(key_press_flag==false)
tmos_start_task(hidEmuTaskId,START_LONGCHECK_TIMER,1600);//啟動(dòng)1s定時(shí)器
key_press_flag=true;//置位按下標(biāo)志
}
elseif(key_press_flag==true)//高電平同時(shí)按鍵被按下過(guò),表示是按下后的彈起
{
key_press_flag=false;//清除按下標(biāo)志
if(key_long_press_flag==false)//短按后的彈起
{
tmos_stop_task(hidEmuTaskId,START_LONGCHECK_TIMER);
PRINT("shortpress
");
tmos_set_event(hidEmuTaskId,START_REPORT_EVT);
}
else//長(zhǎng)按后的彈起
{
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("longpress
");
return(events^START_LONGCHECK_TIMER);
}
上面的這段代碼初次看著有點(diǎn)繞,但是看明白了之后會(huì)覺(jué)得這個(gè)實(shí)現(xiàn)邏輯還是挺好的,注釋寫了,這里不再詳細(xì)解釋了,我在多個(gè)項(xiàng)目里使用的都是它。它兼顧了去抖和短按/長(zhǎng)按的檢測(cè),并且長(zhǎng)按可以判斷出長(zhǎng)按按下/長(zhǎng)按彈起。短按是檢測(cè)到彈起時(shí)認(rèn)為是短按動(dòng)作。另外如果想同時(shí)支持多個(gè)長(zhǎng)按,也很方便添加。
輪詢和中斷各有優(yōu)缺點(diǎn),大家可以根據(jù)實(shí)際情況來(lái)選擇,你一般常用哪種方式呢?
-
單片機(jī)
+關(guān)注
關(guān)注
6030文章
44499瀏覽量
632174 -
操作系統(tǒng)
+關(guān)注
關(guān)注
37文章
6698瀏覽量
123147 -
定時(shí)器
+關(guān)注
關(guān)注
23文章
3234瀏覽量
114358
原文標(biāo)題:?jiǎn)纹瑱C(jī)短按、長(zhǎng)按實(shí)現(xiàn)方法
文章出處:【微信號(hào):strongerHuang,微信公眾號(hào):strongerHuang】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論