不少人在學習微控制器的過程當中,都會碰到4×4的鍵盤掃描,而4×4的鍵盤也是許多產品都擁有的周邊電路之一。透過掃描的方式可以僅使用8隻接腳,完成16鍵的按鍵判讀。
下文將以STM32L053R8Tx為示範,以輪詢(Polling)的方式進行鍵值判讀,並使用意法半導體官方所提供的Nucleo開發板完成該實驗,使用的IDE為Keil uVision5(MDK-ARM),而STM32CubeMX的版本為6.2.1。
4×4矩陣式鍵盤,具有4列、4行的接線。行列交集處即是一個按鍵,最多可以控制16個按鍵,其鍵盤內部電路圖如下所示。
初學時對「掃描」的理解很模糊,因此要先介紹一下鍵盤掃描的原理。我們使用Low Active,因此預設都是High。假設R1~R4是輸出,C1~C4是輸入,則當R1輸出”0”的時候,R1~R4為0111,R1這列的四顆按鈕若按下,分別會在C1~C4讀取到”0”的訊號,若沒有按下,則會讀取到”1”的訊號,接著R1~R4輸出1011,則可以定位到R2這列的四顆按鈕,若按下R2xC2這顆按鈕,則會在C2讀取到”0”,其餘為”1”。以此類推由R1~R4依序輸出”0”的訊號,並讀取C1~C4來定位哪一顆按鈕被按下。聽起來很饒口,下方會有完整的條列掃描流程,完成程式碼後可以透過Debug模式讓自己更熟悉掃描的原理與操作,未來若有使用七段顯示器或是LED矩陣,也會使用到掃描,因此熟悉掃描的原理是相當重要的一環。
- R1~R4:0111
讀取C1是否為0,若是則R1xC1該按鈕被按下,反之無按下。
讀取C2是否為0,若是則R1xC2該按鈕被按下,反之無按下。
讀取C3是否為0,若是則R1xC3該按鈕被按下,反之無按下。
讀取C4是否為0,若是則R1xC4該按鈕被按下,反之無按下。 - R1~R4:1011
讀取C1是否為0,若是則R2xC1該按鈕被按下,反之無按下。
讀取C2是否為0,若是則R2xC2該按鈕被按下,反之無按下。
讀取C3是否為0,若是則R2xC3該按鈕被按下,反之無按下。
讀取C4是否為0,若是則R2xC4該按鈕被按下,反之無按下。 - R1~R4:1101
讀取C1是否為0,若是則R3xC1該按鈕被按下,反之無按下。
讀取C2是否為0,若是則R3xC2該按鈕被按下,反之無按下。
讀取C3是否為0,若是則R3xC3該按鈕被按下,反之無按下。
讀取C4是否為0,若是則R3xC4該按鈕被按下,反之無按下。 - R1~R4:1110
讀取C1是否為0,若是則R4xC1該按鈕被按下,反之無按下。
讀取C2是否為0,若是則R4xC2該按鈕被按下,反之無按下。
讀取C3是否為0,若是則R4xC3該按鈕被按下,反之無按下。
讀取C4是否為0,若是則R4xC4該按鈕被按下,反之無按下。
因為前面兩篇文章已經熟悉了STM32CubeMX的使用了,在此因為使用8隻腳,可以直接從原本產生的程式碼進行改寫,則不用每隻接腳單獨設定,會較有效率,但若需返回STM32CubeMX重新設定和產生程式碼,在Keil所改寫的部分則會被覆蓋掉哦!
預計使用PC0~PC3作為列(Row) Output掃描訊號,而PB0~PB3作為欄(Column) Input讀取訊號,先透過STM32CubeMX設置PC0、PB0後,即按下產生程式碼。進入Keil後,可以移至MX_GPIO_Init(void)查看。
我們設定鍵盤以Low Active,因此PB0~PB3需設置Pull-up的提升電阻。在GPIO_Init中,修改PC0的設置,PIN腳改為0x0f,轉為二進制是0b1111,所代表的是PC0~PC3都進行Output的設置,並同樣設置Pull-up的提升電阻。且在Output Level預設需輸出為HIGH(SET)。接著修改PB0的設置,PIN腳改為0x0f,且需有提升電阻Pull-up,如下所示
static void MX_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
/* GPIO Ports Clock Enable */
__HAL_RCC_GPIOC_CLK_ENABLE();
__HAL_RCC_GPIOH_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
/*Configure GPIO pin Output Level */
HAL_GPIO_WritePin(GPIOC, 0x0f, GPIO_PIN_SET);
/*Configure GPIO pin Output Level */
HAL_GPIO_WritePin(LD2_GPIO_Port, LD2_Pin, GPIO_PIN_RESET);
/*Configure GPIO pin : B1_Pin */
GPIO_InitStruct.Pin = B1_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(B1_GPIO_Port, &GPIO_InitStruct);
/*Configure GPIO pin : PC0 */
GPIO_InitStruct.Pin = 0x0f;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);
/*Configure GPIO pin : LD2_Pin */
GPIO_InitStruct.Pin = LD2_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(LD2_GPIO_Port, &GPIO_InitStruct);
/*Configure GPIO pin : PB0 */
GPIO_InitStruct.Pin = 0x0f;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
}
調整完GPIO的設定後,點選Application/User/Core資料夾右鍵Add New Item to Group。
並選擇C File,名字可以自己定義,因為主要是鍵盤的副程式,因此Name為Keyboard,而路徑一定要在Src當中,才可以直接引用。
接著重複上述,新增標頭檔Header File,檔名也是Keyboard,路徑需在Inc當中。
打開Keyboard.h,新增#ifndef、#define、#endif,並引用HAL的函式庫,以利下方的參數定義。
接著我們需要定義一些參數,以利我們後面的程式撰寫更簡潔及順利。首先需要定義一個Row皆為1的參數,名字可以自己決定。因為使用PC0~PC3作為R1~R4,因此定義KB_R_Blank為GPIOC->ODR|=(0x000f)。
#define KB_R_Blank GPIOC->ODR|=(0x000f) //Blank Row
並定義掃描的參數,這部分可以在.c檔寫成一個sub function,但因為程式碼僅有一行,因此在這邊直接定義即可。
#define KB_R_Scan(x) GPIOC->ODR&=~(1<<x)
完成了Row的參數定義後,接著定義Column的參數。這邊定義的主因是讓我們更容易了解程式碼的運作,因此不直接使用暫存器的運算式做為後續程式碼判斷的內容,也因為使用Low Active,所以參數前方都新增驚嘆號作為邏輯反向。
#define KB_C1 !((GPIOB->IDR & (1<<0))>>0) //C1=0?
#define KB_C2 !((GPIOB->IDR & (1<<1))>>1) //C2=0?
#define KB_C3 !((GPIOB->IDR & (1<<2))>>2) //C3=0?
#define KB_C4 !((GPIOB->IDR & (1<<3))>>3) //C4=0?
#ifndef _KEYBOARD_H_
#define _KEYBOARD_H_
#include "stm32l0xx_hal.h" //HAL lib.
#define KB_R_Blank GPIOC->ODR|=(0x000f) //Blank Row
#define KB_R_Scan(x) GPIOC->ODR&=~(1<<x) //R1~R4 Output Low
#define KB_C1 !((GPIOB->IDR & (1<<0))>>0) //C1=0?
#define KB_C2 !((GPIOB->IDR & (1<<1))>>1) //C2=0?
#define KB_C3 !((GPIOB->IDR & (1<<2))>>2) //C3=0?
#define KB_C4 !((GPIOB->IDR & (1<<3))>>3) //C4=0?
#endif
完成了Header File的定義後,接著就可以使用這些定義的參數依照上方掃描的原理進行程式的撰寫。在Keyboard.c中引用Keyboard.h後,需要宣告我們鍵盤每一個按鈕對應的鍵值,利用查表法的方式完成鍵值的回傳,可以自行定義每個按鍵需要回傳的數值,也可以參考我下方的宣告。
#include "Keyboard.h"
uint8_t KeyBoard[]={ 0x01 , 0x02 , 0x03 , 0x0c,
0x04 , 0x05 , 0x06 , 0x0d,
0x07 , 0x08 , 0x09 , 0x0e,
0x0a , 0x00 , 0x0b , 0x0f};
/* -------------------------
| 1 | 2 | 3 | c |
-------------------------
| 4 | 5 | 6 | d |
-------------------------
| 7 | 8 | 9 | e |
-------------------------
| a | 0 | b | f |
-------------------------
*/
接著就可以使用Header File的定義撰寫了,首先需要依序將R1~R4輸出0,並在每一個Row輸出0的時候,判讀4個Column是否有被按下(讀到0),完成判讀後則遮蔽關閉整個Row,以避免判讀錯誤。使用For迴圈依序掃描,並將讀取到的鍵值直接回傳。
若C1讀取到0,則回傳KeyBoard陣列中0~3的資料;C2讀到0,回傳陣列中4~7的資料,依此類推。若都沒有按下,則回傳0xff,代表無按鍵按下。
uint8_t Keyboard_Read(void)
{
for(uint8_t scan=0;scan<=3;scan++)
{
KB_R_Blank;
KB_R_Scan(scan); //R1~R4 Output Low
HAL_Delay(1);
if(KB_C1)return KeyBoard[scan*4]; //Read C1
if(KB_C2)return KeyBoard[scan*4+1]; //Read C2
if(KB_C3)return KeyBoard[scan*4+2]; //Read C3
if(KB_C4)return KeyBoard[scan*4+3]; //Read C4
}
return 0xff;
}
完成後至Keyboard.h中,新增副程式的宣告。
完成後在main.c的/* USER CODE BEGIN Includes */及/* USER CODE END Includes */中間插入#include “Keyboard.h”,才可以取用剛剛所使宣告的參數及副程式。
在/* USER CODE BEGIN 0 */及/* USER CODE END 0 */之間可以宣告一個全域變數,以利我們可以使用Debug模式來監測鍵值回傳的數值為何。
在while迴圈使用稍早在Keyborad.c的副程式,將回傳值傳到keynumber,並使用Debug模式從Watch視窗查看回傳值。
while (1)
{
keynumber=Keyboard_Read();
HAL_Delay(1);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
以上就是以輪詢法實現4×4鍵盤掃描的教學,實際燒錄完成後的結果未來會再更新在此,也歡迎各位自行測試看看哦!