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