GPIO口的輸入功能-機(jī)械按鍵狀態(tài)的識(shí)別
硬件: 深圳標(biāo)航科技有限公司 暴風(fēng) 開發(fā)板
開發(fā)環(huán)境:MDK(keil 5) + STM32CubeMX
1.1 GPIO口的輸入的作用
輸入,其意是指將處理器外部的邏輯信號(hào)0或者1輸入到處理器的內(nèi)部。輸入是每一個(gè)處理器的IO引腳的基本功能。利用處理器的輸入功能我們可以獲取外部電路的狀態(tài),進(jìn)而做出進(jìn)一步的判斷。GPIO的輸入功能的典型應(yīng)用是獲取機(jī)械按鍵的狀態(tài)—判斷按鍵是按下還是彈起。
1.2 機(jī)械按鍵狀態(tài)的識(shí)別
1.2.1 機(jī)械按鍵電路的設(shè)計(jì)
按鍵有兩個(gè)狀態(tài),一個(gè)是按下一個(gè)是彈起。通過巧妙的電路設(shè)計(jì),會(huì)使得按鍵的按下與彈起時(shí)IO引腳的邏輯電平不一樣。通過GPIO引腳的輸入功能將這些邏輯電平輸入到內(nèi)部供處理器識(shí)別,由此可知按鍵是按下還是彈起,并做出進(jìn)一步的判斷。
下面我們先來討論按鍵電路的設(shè)計(jì)。常用的按鍵電路設(shè)計(jì)如圖1的(a)和(b)所示。
(a)
(b)
圖1 按鍵電路設(shè)計(jì)
先來看圖1的(a)圖,在(a)圖中,按鍵的一端接地,另一端接IO引腳,接IO引腳這一端通過一個(gè)電阻連接到高電平VCC(這種電阻叫上拉電阻)。在沒有按鍵按下時(shí),由于處理器吸取的電流非常非常小,R1兩端可以認(rèn)為沒有電流流動(dòng),所以它們兩邊電位一樣,也即IO引腳的電平跟VCC基本一樣,此時(shí)IO引腳端為高電平。當(dāng)按鍵按下后,IO引腳和地端相連,IO引腳直接變?yōu)榱说碗娖?。通過這個(gè)分析,我們得出圖1(a)按鍵電路的特點(diǎn)如下:
(1)沒有按鍵按下,IO引腳為高電平;
(2)有按鍵按下,IO引腳為低電平。
所以,如果處理器某個(gè)時(shí)候讀到這個(gè)引腳信號(hào)為0,說明此時(shí)按鍵按下了,如果讀到為1,說明按鍵沒有按下。
再來看圖1的(b)圖,圖1(b)中,按鍵一端接高電平,另一端接IO引腳,其中接IO引腳這一端通過一個(gè)電阻接到地(這種電阻叫下拉電阻)。圖1(b)按鍵電路的特點(diǎn)如下:
(1)沒有按鍵按下時(shí),IO引腳為低電平;
(2)有按鍵按下時(shí),IO引腳為高電平。
所以,如果處理器某個(gè)時(shí)候讀到這個(gè)引腳信號(hào)為1,說明此時(shí)按鍵按下了,如果讀到為0,說明按鍵沒有按下。
暴風(fēng)開發(fā)板的按鍵電路如圖2所示,可以看到,在標(biāo)航的暴風(fēng)開發(fā)板中特意將兩個(gè)按鍵分別按圖1的(a)和(b)來連接,以便讀者學(xué)習(xí)這兩種按鍵電路按鍵狀態(tài)的識(shí)別,這個(gè)設(shè)計(jì)比較人性化,可以照顧不同開發(fā)者的不同應(yīng)用場(chǎng)合。
圖2 暴風(fēng)開發(fā)板按鍵電路圖
圖2中要注意,此時(shí)電路設(shè)計(jì)沒有在PE2和PA0兩個(gè)引腳分別連一個(gè)電阻到VCC和地,所以我們得在GD32的內(nèi)部這兩個(gè)引腳這里分別使能這兩個(gè)電阻(GD32和STM32的每個(gè)IO引腳內(nèi)部都配有一對(duì)受控的上下拉電阻)。
1.2.2 機(jī)械按鍵狀態(tài)識(shí)別的思路設(shè)計(jì)
通常,我們都是設(shè)計(jì)一個(gè)函數(shù)來單獨(dú)判斷按鍵是否按下,這個(gè)按鍵函數(shù)的思路設(shè)計(jì)如下:
uint8_t Key_Scan(void)
{
if(KEY0_Status == 0) || (WK_UP1_Status == 1) //說明有按鍵按下了
{
if(KEY0_Status == 0) return KEY0_Value;
if(WK_UP1_Status == 1) return WK_UP1_Value;
}
return KEY0_NO;
}
在函數(shù)Key_Scan中,我們先判斷KEY0的狀態(tài)是不是0或者WK_UP1的狀態(tài)是不是為1,如果KEY0的狀態(tài)是0或者WK_UP1的狀態(tài)是1,說明按鍵按下了,接下來進(jìn)行細(xì)分,看看是KEY0按下還是WK_UP1按下,并返回對(duì)應(yīng)的按鍵值。
對(duì)于Key_Scan函數(shù)的調(diào)用,我們可以在主函數(shù)中這樣調(diào)用
int main(void)
{
uint8 keyvalue = 0;
系統(tǒng)初始化;
while(1)
{
keyvalue = Key_Scan();
if(keyvalue == KEY0_Value) LED0 = ~LED0; // LED0狀態(tài)反轉(zhuǎn)
if(keyvalue == WK_UP1_Value) LED1 = ~LED1;// LED1狀態(tài)反轉(zhuǎn)
}
}
在主函數(shù)中,我們循環(huán)執(zhí)行按鍵掃描,如果發(fā)現(xiàn)按鍵掃描函數(shù)返回的是KEY0_Value,則將LED0的狀態(tài)反轉(zhuǎn),如果返回的是WK_UP1_Value,則將LED1的狀態(tài)反轉(zhuǎn)??偟膩碚f,我們是希望按下一次按鍵,對(duì)應(yīng)的LED的狀態(tài)就反轉(zhuǎn)。
上面這兩個(gè)函數(shù)的配合是否有問題呢?表面看來好像沒有問題,但是當(dāng)你用這個(gè)思路去完善程序并下載到開發(fā)板執(zhí)行的時(shí)候,你會(huì)發(fā)現(xiàn)按鍵按下時(shí),燈的狀態(tài)是不受控的,這個(gè)不受控的原因是什么呢?我們看一下整個(gè)執(zhí)行過程。
假設(shè)有按鍵KEY0按下,則整個(gè)過程為
①執(zhí)行語句“keyvalue = Key_Scan();”此時(shí)返回KEY0_Value,接著執(zhí)行判斷并使得LED0狀態(tài)反轉(zhuǎn)一次,這個(gè)過程持續(xù)時(shí)間非常短,1ms內(nèi)估計(jì)就能執(zhí)行完。
②又回來執(zhí)行語句“keyvalue = Key_Scan();”,此時(shí)由于按鍵仍然處于按下狀態(tài)(人為按下時(shí),按鍵的按下狀態(tài)通常會(huì)超過100ms,典型的是600ms左右),所以又會(huì)返回KEY0_Value,接著執(zhí)行判斷并使得LED0狀態(tài)又反轉(zhuǎn)一次。
注意,此時(shí)按鍵的狀態(tài)已經(jīng)變化兩次了,但是我們只是執(zhí)行一次按下而已!!!!!
繼續(xù)往下分析,你會(huì)發(fā)現(xiàn)按鍵按下一次時(shí),這個(gè)判斷系統(tǒng)會(huì)執(zhí)行多次返回,這是錯(cuò)誤的。錯(cuò)誤的原因在哪里呢?在Key_Scan這個(gè)函數(shù)中,這個(gè)函數(shù)里面只要KEY0_Status等于0,它就會(huì)返回一次KEY0_Value,所以我們需要加一個(gè)變量,用于描述按鍵的當(dāng)前狀態(tài),如果當(dāng)前按鍵已經(jīng)按下了,則這里就不需要再次判斷了。由于這個(gè)變量描述按鍵的按下與彈起狀態(tài),在Key_Scan執(zhí)行完后也不能釋放它的存儲(chǔ)空間,所以我們需要用static修飾它,此時(shí)的Key_Scan函數(shù)需要修改如下:
uint8_t Key_Scan(void)
{
static uint8_t flag = 0; //flag =0說明當(dāng)前是彈起,=1說明是按下
/*如果剛才是彈起但現(xiàn)在有按鍵按下則判斷是那個(gè)按鍵按下,同時(shí)將按鍵狀態(tài)置為1*/
if((flag == 0)&&((KEY0_Status == 0) || (WK_UP1_Status == 1))) {
flag = 1;
if(KEY0_Status == 0) return KEY0_Value;
if(WK_UP1_Status == 1) return WK_UP1_Value;
}
return KEY0_NO;
}
將Key_Scan函數(shù)修改為上面的樣子后,解決了按下一次就執(zhí)行一次返回,避免了按下一次則返回多次的問題。但是它仍然是有重大缺陷的,因?yàn)槲覀儼聪乱淮伟存I后,flag被設(shè)置為1了,當(dāng)按鍵再次被按下時(shí)里面的按下判斷再也得不到執(zhí)行,也即剛剛修改后的函數(shù)只能判斷一次按鍵按下。要想將flag恢復(fù)為0,我們要在Key_Scan中增加彈起的語句,如果彈起了,將flag設(shè)置為0,則就可以解決多次按下后都能觸發(fā)判斷的問題了。增加判斷后的Key_Scan函數(shù)如下:
uint8_t Key_Scan(void)
{
static uint8_t flag = 0; //flag =0說明當(dāng)前是彈起,=1說明是按下
/*如果剛才是彈起但現(xiàn)在有按鍵按下則判斷是那個(gè)按鍵按下,同時(shí)將按鍵狀態(tài)置為1*/
if((flag == 0)&&((KEY0_Status == 0) || (WK_UP1_Status == 1)))
{
flag = 1;
if(KEY0_Status == 0) return KEY0_Value;
if(WK_UP1_Status == 1) return WK_UP1_Value;
}
/*如果剛才按鍵按下,現(xiàn)在彈起了,則設(shè)置flag=0*/
if((flag == 1)&&((KEY0_Status == 1) && (WK_UP1_Status == 0))) {
flag = 0;
}
return KEY0_NO;
}
注意,按鍵彈起指的是所有按鍵的彈起,所以(KEY0_Status == 1) && (WK_UP1_Status == 0)這里要用邏輯與,體現(xiàn)出“而且”之意。
至此,機(jī)械按鍵狀態(tài)識(shí)別的關(guān)鍵問題就解決了。
1.2.3 機(jī)械按鍵的抖動(dòng)及其消除
雖然關(guān)鍵問題解決了,但還有一些細(xì)節(jié)要注意,這個(gè)細(xì)節(jié)就是按鍵的抖動(dòng)。
以圖3的按鍵電路為例,當(dāng)按鍵按下時(shí),PE2引腳的電平狀態(tài)如圖4所示。
圖3 按鍵電路圖
圖4 按鍵按下過程
由圖4可見,按鍵按下時(shí),PE2并不是馬上變?yōu)榈碗娖?,而是有一個(gè)漸變過程,而在彈起時(shí)也不是馬上變?yōu)楦唠娖?,它也有一個(gè)漸變過程。這些漸變過程我們叫做抖動(dòng)。在進(jìn)行按鍵的按下與彈起時(shí),我們都要進(jìn)行抖動(dòng)的消除。消除的方法非常簡(jiǎn)單,就是延時(shí)。通常,抖動(dòng)持續(xù)的時(shí)間在10ms之內(nèi),所有,我們只要進(jìn)行10ms的延時(shí)可以解決掉絕大部分機(jī)械按鍵的抖動(dòng)—如果解決不了,此時(shí)就要用示波器測(cè)一下你的按鍵按下與放開時(shí)的信號(hào),看看具體的抖動(dòng)是多少,然后增加延時(shí)消除它,筆者就曾遇到過需要延時(shí)20ms的情況……
這個(gè)消抖的過程如下:
1、按下的消抖
if(按鍵按下)
{
Delay(10ms);
if(按鍵按下)
{
按鍵是真的按下,執(zhí)行相應(yīng)的動(dòng)作;
}
}
2、彈起的消抖
if(按鍵彈起)
{
Delay(10ms);
if(按鍵彈起)
{
按鍵是真的彈起了,執(zhí)行相應(yīng)的動(dòng)作;
}
}
1.2.4 完整的按鍵判斷程序
加入消抖后,整個(gè)按鍵判斷的函數(shù)可以修改如下
uint8_t KEY_Scan(void)
{
static uint8_t flag=0; //按鍵彈起為0,按下為1
if(( flag == 0) && ((KEY0_Status == 0)||(WK_UP1_Status == 1)))
{
/*按鍵剛剛處于彈起狀態(tài),但現(xiàn)在有按下*/
HAL_Delay(10); //延時(shí)10ms,消除抖動(dòng)
if((KEY0_Status == 0)||(WK_UP1_Status == 1))
{
/*確實(shí)有按鍵按下*/
flag = 1; //按鍵為按下狀態(tài)
if(KEY0_Status == 0) return KEY0_Value;
if(WK_UP1_Status == 1) return WK_UP1_Value;
}
}
if((flag == 1) && ((KEY0_Status == 1) && (WK_UP1_Status == 0)))
{
/*按鍵處于彈起狀態(tài)而且剛才是按下狀態(tài)*/
HAL_Delay(10); //消除彈起抖動(dòng)
if((KEY0_Status == 1) && (WK_UP1_Status == 0))
{
flag = 0; //按鍵彈起了
}
}
return KEY_NO; //沒有按鍵按下返回KEY_NO
}
1.3 按鍵狀態(tài)判斷實(shí)驗(yàn)
下面我們通過一個(gè)例子來驗(yàn)證按鍵狀態(tài)的識(shí)別。
【例1】已知按鍵電路和LED電路如圖5所示,編寫程序?qū)崿F(xiàn)以下功能:
按下按鍵KEY0, LED0的狀態(tài)反轉(zhuǎn);按下按鍵WK_UP1,LED1的狀態(tài)反轉(zhuǎn)。
圖5 按鍵電路和LED電路示意圖
【實(shí)現(xiàn)過程】
1.配置RCC的高速時(shí)鐘來自于外部晶體陶瓷晶振,并且設(shè)置HCLK的頻率為72Mhz。
2.設(shè)置調(diào)式方式為Serial Wire。
以上兩步不懂如何設(shè)置的可以回頭看一下【模塊一 GPIO口的輸出功能-LED的閃爍實(shí)驗(yàn)】這一部分的步驟介紹。
3.設(shè)置PE12、PE13引腳的工作模式為輸出,PE12的User Label選項(xiàng)為L(zhǎng)ED0,PE13的User Label設(shè)置為L(zhǎng)ED1,以使能更加直觀方便。另外,一開始我們將這兩盞LED燈都點(diǎn)亮,以方便觀察結(jié)果。PE12和PE13的設(shè)置結(jié)果如圖6所示。
圖6 PE12和PE13的設(shè)置過程
4.設(shè)置PE2和PA0為輸入。PE2引腳上拉電阻使能,PA0引腳下拉電阻使能。同時(shí)設(shè)置PE2的用戶標(biāo)號(hào)為KEY0,設(shè)置PA0引腳的標(biāo)號(hào)為WK_UP1,。PE2引腳的設(shè)置如圖7所示,PA0引腳的設(shè)置如圖8所示。
圖7 PE2引腳的設(shè)置結(jié)果示意圖
圖8 PA0引腳的設(shè)置
5.設(shè)置好后,給工程取名,同時(shí)選擇IDE,并生成工程代碼。
6.添加代碼。
①編寫按鍵識(shí)別的C語言文件,其內(nèi)容如圖9所示。
圖9 key.c中內(nèi)容示意圖
②在key.h中定義KEY0_Value等宏名,如圖10所示。
圖10 KEY0_Value等的定義示意圖
③修改主函數(shù),其內(nèi)容如圖11所示。
圖11 主函數(shù)的內(nèi)容示意圖
在主函數(shù)中,注意要將頭文件key.h包含進(jìn)工程,如圖12所示。
至此,工程的代碼添加完畢,編譯后下載到開發(fā)板,按復(fù)位鍵,然后按KEY0或者WK_UP0,可以看到對(duì)應(yīng)的LED燈的狀態(tài)反轉(zhuǎn),任務(wù)目標(biāo)完成。
1.4 按鍵識(shí)別實(shí)驗(yàn)用到的HAL庫(kù)的函數(shù)解讀
1.引腳電平反轉(zhuǎn)函數(shù) HAL_GPIO_TogglePin()
在主函數(shù)main的while循環(huán)中,我們使用到了函數(shù)HAL_GPIO_TogglePin,這個(gè)函數(shù)的相關(guān)信息為
●作用:將某個(gè)IO引腳的輸出電平反轉(zhuǎn)。比如要反轉(zhuǎn)PE12引腳的電平,我們可以采用如下的方式來調(diào)用該函數(shù):
HAL_GPIO_TogglePin(GPIOE, GPIO_PIN_12);
●函數(shù)參數(shù),有兩個(gè),第一個(gè)用于指明要反轉(zhuǎn)信號(hào)的引腳位于第幾組GPIO口,第二個(gè)用于指明要反轉(zhuǎn)的是哪一個(gè)引腳的信號(hào)。
是不是很方便呢?
最后要注意,函數(shù)HAL_GPIO_TogglePin要使用于將IO引腳已經(jīng)配置為輸出的場(chǎng)合。
2.讀引腳信號(hào)函數(shù)HAL_GPIO_ReadPin()
在key.c函數(shù)中,有一個(gè)宏定義
#define KEY0_Status HAL_GPIO_ReadPin(KEY0_GPIO_Port, KEY0_Pin)
里面用到了一個(gè)函數(shù)HAL_GPIO_ReadPin,這個(gè)函數(shù)相關(guān)的信息如下:
●作用:讀取某個(gè)引腳的狀態(tài)。比如要讀取PA0的狀態(tài),我們可以采用如下的方式來調(diào)用該函數(shù)
HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0);
●函數(shù)參數(shù),有兩個(gè),第一個(gè)用于指明要讀取信號(hào)的引腳位于那組GPIO口,第二個(gè)參數(shù)用于指明是那個(gè)引腳。
注意,函數(shù)HAL_GPIO_ReadPin使用于IO引腳已經(jīng)設(shè)置為輸入的場(chǎng)合
1.5 GPIO輸入功能總結(jié)
在使用GD32/STM32的IO引腳時(shí)要注意以下兩點(diǎn):
1.如果引腳外部沒有上拉電阻或者下拉電阻,則可能需要在引腳內(nèi)部使能上拉電阻或者下拉電阻。
2.對(duì)按鍵狀態(tài)進(jìn)行識(shí)別時(shí),一定要注意防止按下一次時(shí)有多次返回值。
評(píng)論