第十七章按鍵輸入實驗
上一章,我們介紹了STM32MP157的IO口作為輸出的使用。本章,我們向大家介紹IO口作為輸入使用的操作方法,我們将利用闆載的3個按鍵來控制LED燈亮和滅以及和蜂鳴器的開和關。
本章将分為如下幾個小節:
12.1、按鍵輸入簡介;
12.2、硬體設計;
12.3、程式設計;
12.4、章節小結;
17.1 按鍵輸入簡介
17.1.1 按鍵檢測原理
幾乎每個開發闆都會闆載有獨立按鍵,因為按鍵用處很多。常态下,獨立按鍵是斷開的,按下的時候才閉合。每個獨立按鍵會單獨占用一個IO口,通過查詢IO口的高低電平來判斷按鍵的狀态。按鍵在閉合和斷開的時候,都存在抖動現象,即按鍵在閉合時不會馬上就穩定的連接配接,斷開時也不會馬上斷開,這是機械觸點,無法避免。獨立按鍵抖動波形圖如下:

圖17.1.1.1獨立按鍵抖動波形圖
圖中的按下抖動和釋放抖動的時間大概都為10ms。為了避免抖動可能帶來的誤操作,我們要做的措施就是給按鍵消抖。消抖方法分為硬體消抖和軟體消抖,我們常用軟體的方法消抖。
軟體消抖:檢測到按鍵按下後,一般進行10ms延時,用于跳過抖動的時間段,如果消抖效果不好可以調整這個10ms延時,因為不同類型的按鍵抖動時間可能有偏差。按鍵按下以後,待延時過後再檢測按鍵狀态,如果測試到按鍵沒有按下,那我們就判斷這是抖動或者幹擾造成的;如果測試到還是按下的狀态,那麼我們就認為這是按鍵真的按下了。對按鍵釋放的判斷同理。
硬體消抖:利用R-S觸發器或者單穩态觸發器構成消抖電路,或者利用RC積分電路吸收振蕩脈沖的特點來達到消抖的效果。
17.1.2 配置上下拉的原則
結合前面的實驗以及本章的實驗,相信會有部分小夥伴對什麼時候軟體要配置上拉什麼時候軟體要配置下拉有疑惑,下面我們來分析一下。
随着技術的迅速發展,晶片的內建度和複雜度也越來越高,功能也越來越強大,現在的晶片内部一般都內建了電阻和電容,為PCB設計極大地節省了空間。如下圖所示,STM32的GPIO口内部自帶了上拉和下拉電阻,電阻上有一個開關,可通過軟體配置來設定開或者關。
圖17.1.2.1 GPIO的基本結構圖
上拉就是将一個不确定的信号通過一個電阻嵌位在高電平,電阻同時起限流作用;下拉就是将一個不确定的信号通過一個電阻嵌位在低電平,電阻同時起限流作用。STM32的這個上下拉電阻阻值一般是30KΩ~50KΩ,上拉電阻,一般稱為弱上拉,下拉電阻,一般稱為弱下拉。這裡的弱是指電阻阻值比較大,電流就比較小,充電就比較慢,進而上下拉的速度就比較慢。
例如,晶片内部開啟了弱上拉(例如阻值是50KΩ),外部再加一個強上拉電阻10KΩ,電路中這兩個電阻相當于并聯,IO口的上拉電阻就小于10KΩ,電流就變大了,也就變“強”了。此電流可以供開漏電路使用,開漏輸出最主要的特性就是高電平沒有驅動能力,需要借助外部上拉電阻才能真正輸出高電平。
晶片複位以後,端口上拉下拉寄存器(PUPDR)的複位值為0x00000000,即預設不開啟内部上拉和下拉,IO口相當于浮空狀态,電平狀态是高還是低不确定。那麼問題來了,如果此IO口接的是一個按鍵,如果外部不做上下拉會怎樣?
圖17.1.2.2按鍵原理圖
我們是通過讀取按鍵對應的IO電平狀态來判斷按鍵是否有按下的,按鍵WKUP一端接的高電平,另一端接的PA0,按鍵按下以後IO口是高電平,是以認為此按鍵是高電平有效。如果PA0内部不開啟下拉,那麼IO口電平是未知的,當按鍵程式去讀取IO口電平時可能讀到值為0也可能為1,如果此時恰好沒有按下按鍵,而程式讀取IO口電平為1,程式就認為按鍵是已經按下了,這就錯了。在按鍵未按下前,我們當然希望PA0電平為0,是以對于按鍵WKUP一定要開啟弱下拉。對于按鍵KEY0和KEY1是低電平有效,IO口的一端實際上是已經接了10KΩ的上拉電阻,IO口電平已經預設為高電平了,是以内部上拉不開啟也是不影響的。
上下拉電阻的目的是給IO口設定一個确定的電平狀态,如果按鍵按下是高電平有效,我們就做下拉,讓該IO口在預設狀态下處于低電平狀态,即沒有按鍵按下時,IO口檢測到的總是低電平,隻有按鍵按下的時候IO口才會檢測到高電平;如果按鍵按下是低電平有效的話,我們就上拉,讓該IO口在預設狀态下處于高電平,即沒有按鍵按下時,IO口檢測到的總是高電平,隻有按鍵按下的時候IO口才會檢測到低電平。
17.2 硬體設計
1. 例程功能
通過開發闆上的三個獨立按鍵控制LED燈:WKUP控制LED0翻轉,KEY1控制LED1翻轉,KEY0控制蜂鳴器翻轉。
2. 硬體資源
LED0 | LED1 | WK_UP | KEY0 | KEY1 | BEEP |
PI0 | PF3 | PA0 | PG3 | PH7 | PC7 |
表17.2. 1硬體資源
3. 原理圖
LED和BEEP的原理圖我們前面已經涉及,獨立按鍵硬體部分的原理圖如下圖所示:
圖17.2. 1獨立按鍵與STM32MP157連接配接原理圖
這裡需要注意的是:KEY0和KEY1是低電平有效的,而WK_UP是高電平有效的,并且KEY0和KEY2接了一個10k的上拉電阻,而WK_UP外部沒有下拉電阻,是以WK_UP的配置中,一定要通過軟體來開啟STM32MP157内部下拉。
17.3 程式設計
本實驗配置好的實驗工程已經放到了開發闆CD光牒中,路徑為:開發闆CD光牒A-基礎資料\1、程式源碼\ 3、M4裸機驅動例程\庫V1.2\實驗6 按鍵輸入實驗。
17.3.1 程式設計流程
前面實驗我們學習了GPIO口作為輸出的使用方法,本節,我們來學習GPIO作為輸入的使用方法。實驗中,我們通過HAL庫的HAL_GPIO_ReadPin函數(實際上是操作IDR寄存器)來讀取按鍵對應GPIO引腳電平狀态,進而判斷按鍵是否有按下。KEY0和KEY1按下後引腳是低電平(低電平有效),WKUP按鍵按下後引腳是高電平。
程式設計中,我們要注意以下兩點:
1)按鍵程式要做消抖處理,消抖時間約為10ms;
2)除了按鍵WKUP配置為下拉以外,其它引腳均配置為上拉。
圖17.3.1. 1程式設計流程圖
17.3.2 添加驅動檔案
在上一章工程的Drivers\BSP\KEY下建立key.c和key.h檔案,并将key.c關聯到工程中:
圖17.3.2.1 建立key.c和key.h檔案
17.3.3 添加驅動代碼
1. 添加key.h代碼
key.h檔案代碼如下,這段代碼主要定義了3個按鍵引腳、使能按鍵引腳對應的IO口的時鐘、讀取三個按鍵引腳的電平。其中,通過HAL_GPIO_ReadPin函數讀取GPIO口的電平值,也就是讀取按鍵引腳電平值。最後定義了3個宏KEY0_PRES、KEY1_PRES和WKUP_PRES,這3個宏将用于按鍵掃描程式中,用于表示按鍵按下。
#ifndef __KEY_H
#define __KEY_H
#include "./SYSTEM/sys/sys.h"
/* 按鍵KEY0引腳定義 */
#define KEY0_GPIO_PORT GPIOG
#define KEY0_GPIO_PIN GPIO_PIN_3
/* 使能PG3時鐘使能 */
#define KEY0_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOG_CLK_ENABLE(); }while(0)
/* 按鍵KEY1引腳定義 */
#define KEY1_GPIO_PORT GPIOH
#define KEY1_GPIO_PIN GPIO_PIN_7
/* 使能PH7時鐘使能 */
#define KEY1_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOH_CLK_ENABLE(); }while(0)
/* 按鍵WK_UP引腳定義 */
#define WKUP_GPIO_PORT GPIOA
#define WKUP_GPIO_PIN GPIO_PIN_0
/* 使能PA0時鐘使能 */
#define WKUP_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOA_CLK_ENABLE(); }while(0)
/* 讀取KEY0引腳 */
#define KEY0 HAL_GPIO_ReadPin(KEY0_GPIO_PORT, KEY0_GPIO_PIN)
/* 讀取KEY1引腳 */
#define KEY1 HAL_GPIO_ReadPin(KEY1_GPIO_PORT, KEY1_GPIO_PIN)
/* 讀取WKUP引腳 */
#define WK_UP HAL_GPIO_ReadPin(WKUP_GPIO_PORT, WKUP_GPIO_PIN)
#define KEY0_PRES 1 /* KEY0按下 */
#define KEY1_PRES 2 /* KEY1按下 */
#define WKUP_PRES 3 /* KEY_UP按下 */
void key_init(void); /* 按鍵初始化函數 */
uint8_t key_scan(uint8_t mode); /* 按鍵掃描函數 */
#endif
2. 添加key.c檔案代碼
key.c檔案主要有兩部分内容:按鍵初始化和按鍵掃描。
(1)按鍵初始化
/**
* @brief按鍵初始化函數
* @param無
* @retval無
*/
void key_init(void)
{
GPIO_InitTypeDef gpio_init_struct;
KEY0_GPIO_CLK_ENABLE(); /* KEY0時鐘使能 */
KEY1_GPIO_CLK_ENABLE(); /* KEY1時鐘使能 */
WKUP_GPIO_CLK_ENABLE(); /* KEY_UP時鐘使能 */
gpio_init_struct.Pin = KEY0_GPIO_PIN; /* KEY0引腳 */
gpio_init_struct.Mode = GPIO_MODE_INPUT; /* 輸入 */
gpio_init_struct.Pull = GPIO_PULLUP; /* 上拉 */
gpio_init_struct.Speed = GPIO_SPEED_FREQ_VERY_HIGH; /* 高速 */
HAL_GPIO_Init(KEY0_GPIO_PORT, &gpio_init_struct); /* KEY0引腳初始化 */
gpio_init_struct.Pin = KEY1_GPIO_PIN; /* KEY1引腳 */
gpio_init_struct.Pull = GPIO_PULLUP; /* 上拉 */
HAL_GPIO_Init(KEY1_GPIO_PORT, &gpio_init_struct); /* KEY1引腳初始化 */
gpio_init_struct.Pin = WKUP_GPIO_PIN; /* KEY_UP引腳 */
gpio_init_struct.Pull = GPIO_PULLDOWN; /* 下拉 */
HAL_GPIO_Init(WKUP_GPIO_PORT, &gpio_init_struct); /* KEY_UP引腳初始化 */
}
按鍵初始化函數中,先使能按鍵的時鐘,再配置按鍵的模式,其中:
配置KEY0和KEY1為輸入、上拉、高速模式,配置WK_UP為輸入、下拉、高速模式。
(2)按鍵掃描函數
key.c檔案中記得添加如下代碼:
#include "./BSP/KEY/key.h"
#include "./SYSTEM/delay/delay.h"
按鍵掃描函數如下:
1 /**
2 * @brief按鍵掃描函數
3 * @note該函數響應優先級為(同時按下多個按鍵): WK_UP > KEY1 > KEY0!!
4 * @param可取 0 / 1, 具體含義如下:
5 * @arg不支援連續按(當按鍵按下不放時, 隻有第一次調用會傳回鍵值,
6必須松開按鍵以後, 再次按下才會傳回其他鍵值)
7 * @arg支援連續按(當按鍵按下不放時, 每次調用該函數都會傳回鍵值)
8 * @retval鍵值, 定義如下:
9按下
10按下
11按下
12 */
13 uint8_t key_scan(uint8_t mode)
14 {
15 static uint8_t key_up = 1; /* 按鍵按松開标志 */
16 uint8_t keyval = 0;
17 if (mode) key_up = 1; /* 支援連按 */
18 /* 按鍵松開标志為1, 且有任意一個按鍵按下了 */
19 if (key_up && (KEY0 == 0 || KEY1 == 0 || WK_UP == 1))
20 {
21 delay(2); /* 去抖動,後面會換成高精度延時函數! */
22 key_up = 0;
23 if (KEY0 == 0) keyval = KEY0_PRES;
24 if (KEY1 == 0) keyval = KEY1_PRES;
25 if (WK_UP == 1) keyval = WKUP_PRES;
26 }
27 /* 沒有任何按鍵按下, 标記按鍵松開 */
28 else if (KEY0 == 1 && KEY1 == 1 && WK_UP == 0)
29 {
30 key_up = 1;
31 }
32 return keyval; /* 傳回鍵值 */
33 }
key.c檔案主要是處理按鍵掃描函數,程式設計按鍵支援連續按和不支援連續按兩種情況。我們來看按鍵處理的程式設計過程:
第15行,key_up為按鍵松開标志,如果key_up等于1,表示按鍵已經松開。
第16行,定義程式的傳回值keyval。
第17行,如果函數的參數mode等于1,則key_up等于1,表示支援按鍵可連續按模式,也就是當按鍵按下不放時,每次調用該函數都會傳回鍵值。如果函數的參數mode等于0,表示不支援連續按,也就是當按鍵按下不放時,隻有第一次調用會傳回鍵值,必須松開按鍵以後, 再次按下才會傳回其鍵值。
第19行,如果标志位key_up等于1(表示按鍵是松開的,沒有按下),當KEY0等于0或者KEY1等于0或者WKUP等于1的時候,表示有按鍵按下了,具體是哪一個按鍵按下了,需經過後面的程式進行判斷。
第21行,程式延時約10ms,因為按鍵按下過程産生抖動,抖動周期大概10ms,是以我們先延時10ms以後再去讀取按鍵對應的IO口的狀态。這裡的delay函數是前面的實驗中我們自己定義的函數,如果想實作比較精确的10ms延時,我們可以直接調用HAL庫的HAL_Delay函數來實作,該函數是實作毫秒的延時,可以将第21行的代碼替換成HAL_Delay(10);
第22行,将标志位key_up置0,友善程式用于判斷下次按鍵是否有按下。
第23~25行,通過讀取到的按鍵對應的IO口電平來判斷是哪一個按鍵按下了。
我們前面通過原理圖分析知道,當按鍵WK_UP按下以後,對應的IO口為高電平,當KEY0或KEY1按鍵按下以後,對應的IO口為低電平。
第28行,如果讀取到KEY0和KEY1的電平為1,WKUP的電平為0,說明此時沒有按鍵按下, key_up值等于1,表示按鍵是松開的。
3. 添加main.c檔案代碼
main.c檔案函數如下:
1 #include "./SYSTEM/sys/sys.h"
2 #include "./SYSTEM/delay/delay.h"
3 #include "./BSP/LED/led.h"
4 #include "./BSP/BEEP/beep.h"
5 #include "./BSP/KEY/key.h"
6
7 /**
8 * @brief主函數
9 * @param無
10 * @retval無
11 */
12 int main(void)
13 {
14 uint8_t key;
15
16 HAL_Init(); /* 初始化HAL庫 */
17 led_init(); /* 初始化LED */
18 beep_init(); /* 初始化蜂鳴器 */
19 key_init(); /* 初始化按鍵 */
20 LED0(0); /* 先點亮LED0 */
21
22 while(1)
23 {
24 key = key_scan(0); /* 得到鍵值 */
25 if (key)
26 {
27 switch (key)
28 {
29 case WKUP_PRES: /* 控制LED0(RED)翻轉 */
30 LED0_TOGGLE(); /* LED0狀态取反 */
31 break;
32 case KEY1_PRES: /* 控制LED1(GREEN)翻轉 */
33 LED1_TOGGLE(); /* LED1狀态取反 */
34 break;
35 case KEY0_PRES: /* 控制蜂鳴器開關 */
36 BEEP_TOGGLE(); /* 蜂鳴器狀态取反 */
37 break;
38 }
39 }
40 else
41 {
42 delay(10);
43 //HAL_Delay(10); /* 也可以使用HAL庫自帶的毫秒級别延時函數 */
44 }
45 }
46 }
main.c檔案中也是先初始化HAL庫,我們前面多次提到,因為我們還沒手動添加時鐘配置的代碼,是以此時MCU的時鐘為64MHz。另外,延時函數的話,我們也可以使用HAL_Delay函數來代替。
第16~20行,初始化HAL庫、LED、蜂鳴器、按鍵,逛進入main函數的時候,LED0是亮的狀态;
第24行,擷取按鍵值,這裡key_scan(0)函數的參數是0,表示不支援連續按模式,如果大家想測試連續按模式,隻需要把函數的參數0改為1即可。
第25行,key_scan(0)的傳回值key可以是0、1、2或者3中的某一個,如果key的值是1、2和3的話,則表示有按鍵按下,通過第30~43行的case語句判斷按鍵的按下情況:
第一次WKUP按鍵按下,則蜂鳴器響,再次按下,蜂鳴器不響,再次按下蜂鳴器又響,如此循環,實作蜂鳴器翻轉。
第一次按下KEY1,LED1滅,再次按下KEY1,LED1又亮,如此循環。
第一次按下KEY0,LED0亮,再次按下KEY0,LED0滅,如此循環。
如果key的值是0,表示沒有按鍵按下,程式執行delay(10)函數延時一定時間以後再傳回到第24行往下執行,即在while中循環。