單片機上的按鍵通常一端鏈接高電位,一端鏈接低電位,二者其一鏈接GPIO管脚。

按鍵按下,高低電位導通,引起GPIO管脚電位改變。GPIO管脚処電位變化説明按鍵狀態改變,可以透過讀取GPIO引脚電平來獲取當前按鍵狀態。而考慮到 按鍵機械結構設計,按下后按鍵兩端電平會發生抖動,可能引發判斷錯誤,所以需要一定的操作來“消抖”實現準確檢測。

本文中按鍵按下時GPIO管脚為低電位,鬆開為高電位。

如下述代碼所示:透過直接讀取GPIO電平得到當前按鍵狀態,實測會發生一次按下引發多次響應的現象,判斷會出錯,需要一定的處理。

1
2
3
4
5
6
// 不合理的寫法
bool state = 0, if_press = 0;
state = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0);
// state = GPIO_ReadOutputDataBit(GPIOA, GPIO_Pin_0);
if_press = (!state) ? 1 : 0;
// 1: press 0: not

按鍵單擊

按鍵單擊判斷的思路是:

如若檢測按下,延時一段時間,再次檢測,如果依舊按下,那麽認爲按鍵按下,直到按鍵鬆開,再重新開始檢測。

我們可以使用延時判定的方法,跳過抖動部分,以獲得更准確的結果。如若抖動部分持續過久,需要適當增加延時,或給按鍵并聯電容來減輕抖動帶來的影響。本案例使用judge_sta來記錄判斷到哪一階段,key_sta記錄GPIO引脚電位,single_flag記錄判斷結果,使用結構體封裝這三個參數。

按鍵狀態結構體定義如下:

1
2
3
4
5
6
7
8
struct Keys {
unsigned char judge_sta;
// 多步驟判定中表示步數
bool key_sta;
// GPIO讀取到的電位
bool single_flag;
// 最終結果,表示到底按沒按下
};

在實例化結構體時,强烈建議使用volatile修飾struct,以確保主函數每次能從中斷讀取到最新的數據,因爲key[]共享於中斷與主循環,未用volatile會因編譯器優化導致讀取過期值。如若使用舊版本編譯器或不使用中斷函數進行按鍵判斷,則無需考慮這種情況。對volatile關鍵字的介紹詳見此處

1
volatile struct keys key[4] = {0, 0, 0};

本例使用中斷,使判斷代碼在外部自動循環執行,減少對主函數的影響。在使用中斷前,需要進行時鐘配置,使用STM32 CubeMX進行配置大致流程如下:

Clock Configuration -> HCLK = 80MHz
Clock Configuration -> PLL Source = HSE
Clock Configuration -> Input Source = 晶振頻率
RCC -> HSE = Crystal/Ceramic Resonator
TIM2 -> Clock Source = Internal clock
Prescaler = 80 - 1
Counter Period = 10000 - 1

這樣可以實現每10ms進入一次中斷進行判斷。
10ms <–> 100Hz = 80,000,000 / 80 / 10000
上述操作并不普適,具體操作因電路不同而有差異。配置完成後透過初始化HAL_TIM_Base_Start_IT(&htim2)啓動中斷函數,否則中斷内的代碼不會執行。中斷内具體實現代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//代碼寫在中斷的囘調函數HAL_TIM_PeriodElapsedCallback裏
void HAL_TIM_PeriodElapsedCallback(TIM_handleTypeDef *htim) {
if(htim -> Instance == TIM3) {
//讀取GPIO電位(B0/B1/B2/A0)
key[0].key_sta = HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0);
key[1].key_sta = HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1);
key[2].key_sta = HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_2);
key[3].key_sta = HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_3);
}
for(int i = 0; i < 4; ++i) {
switch(key[i].judge_sta) {
case 0:
//第一步判斷
if(key[i].key_sta == 0)
//如果為按下狀態
key[i].judge_sta = 1;
//下次直接進行第二步判斷(case 1)
break;
case 1:
if(key[i].key_sta == 0) {
//10ms后仍舊按下
key[i].judge_sta = 2;
//進入下一步判斷(case 2)
key[i].single_flag = 1;
//這回確實是按下了
}
else key[i].judge_sta = 0;
//否則認爲是抖動造成case 0時的低電平
break;
case 2:
if(key[i].key_sta == 1) {
//鬆開按鍵后,重新開始判斷流程
key[i].judge_sta = 0;
}
break;
}
}
}
注意:`single_flag`在使用后需要重置為0.

使用中斷可以避免按鍵檢測阻塞主函數進程,但如若認爲10ms對程序影響可以忽略,那麽可以不使用中斷函數,直接在main.c中添加如下所示Key_Scan按鍵掃描函數進行判斷,這種情況下無需volatile關鍵字來保證數據時效性。

1
2
3
4
5
6
7
8
9
10
11
12
uint8_t Key_Scan(void) { 
if(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0) == 0) {
//PB0被按下
HAL_Delay(10);
//延时消抖
if(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0) == 0) {
while(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0) == 0);
//等待按键抬起
return 1;//返回1,代表第一個按鍵按下
}
}
}

按鍵長按

類似地,我們有按鍵長按的判斷邏輯。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
case 0: 
if(key[i].key_sta == 0) {
key[i].judge_sta = 1;
key[i].key_time = 0;
}
break;
case 1:
if(key[i].key_sta == 0) {
key[i].judge_sta = 2;
}
else key[i].judge_sta = 0;
break;

case 2:
if(key[i].key_sta == 1) {
key[i].judge_sta = 0;
if(key[i].key_time < 70)
key[i].single_flag = 1;
}else {
key[i].key_time++;
if(key[i].key_time > 70)
key[i].long_flag = 1;

}
break;