4x4矩陣鍵盤實拍照如下圖。其構成是4行(L1:4)x 4列(R1:4)共16個按鍵,當第n行、第m列的按鈕(n, m)按下時,引腳 Ln 與 Rm 導通: 有一篇文章,對矩陣鍵盤的介面講解得很詳細。概括起來說,按鍵檢測分為3個階段。第一個階段,掃描行。行I/O口設為input模式,使用上拉電阻。列 ...
4x4矩陣鍵盤實拍照如下圖。其構成是4行(L1:4)x 4列(R1:4)共16個按鍵,當第n行、第m列的按鈕(n, m)按下時,引腳 Ln 與 Rm 導通:
有一篇文章,對矩陣鍵盤的介面講解得很詳細。概括起來說,按鍵檢測分為3個階段。第一個階段,掃描行。行I/O口設為input模式,使用上拉電阻。列I/O口設為output模式,輸出0。逐行掃描,某一行若沒有按鍵按下,則在上拉電阻的作用下pin值讀取為1;若該行任一按鍵按下,則被按鍵短路到列I/O口,因此pin值讀為0。檢測到有按鍵被按下後,進入第二階段,列掃描,以確定被按下的按鍵的列。列掃描階段,行/列的I/O模式互換,即:行I/O口設置為output模式,輸出0;列I/O口設為input模式,使用上拉電阻。類似於行掃描,逐列進行掃描,當讀取到pin值為0則表明被按下的按鍵屬於該列。通過第一、二階段,就能確定被按下的按鍵。第三階段,監聽被按下的按鍵的列I/O口,直到pin值為1,即表明按鍵被鬆開。
關於上拉/下拉電阻,這裡有一篇介紹文章。上拉電阻的作用在於,在常態下,按鈕開放,IO口被“往上拉”到VDD,讀數為1;當按鈕閉合,I/O口通過按鈕短路到VSS,讀數為0;而VDD通過上拉電阻和按鈕與VSS連通。若沒有上拉電阻的存在,則VDD與VSS短路,會造成災難性的後果,這顯然是必須避免的。使用上拉電阻時,按鈕開放時,pin值為1;當按鈕閉合時,pin值為0。即,pin值與按鈕閉合狀態相反,這稱為“負邏輯”。
在前述矩陣鍵盤 的介面演算法中,三個階段都使用了上拉電阻。其檢測邏輯為負邏輯。
STM32的I/O口內部電路中包含有上拉電阻和下拉電阻,可以通過程式啟用或禁用。
在流水燈實驗的硬體基礎上,增加矩陣鍵盤介面。4x4矩陣鍵盤共有16個按鍵,4個LED剛好可以顯示16個二進位值(0-0x0F)。
矩陣鍵盤的按鍵檢測是分階段進行的,因此,程式的主體結構特別適合使用“狀態機”設計模式。下列代碼中,4個行I/O口的Label依次為R1:4,列I/O口為C1:4。首先定義狀態結構體及3個實例:
typedef struct { void (*enter)(); uint8_t (*loop)(); } App_ScanningState; #define App_STAY 0 #define App_LEAVE 1 void rowScanningEnter(); uint8_t rowScanningLoop(); void colScanningEnter(); uint8_t colScanningLoop(); void colScanningPressedEnter(); uint8_t colScanningPressedLoop(); App_ScanningState rowScanning = { rowScanningEnter, rowScanningLoop }; App_ScanningState colScanning = { colScanningEnter, colScanningLoop }; App_ScanningState colScanningPressed = { colScanningPressedEnter, colScanningPressedLoop }; App_ScanningState *currState = &rowScanning;
結構體 App_ScanningState 表示1個狀態,當進入該狀態時,調用其 (函數指針)成員enter() 。在程式主迴圈中,則調用其 loop() 成員。loop() 函數返回值為 App_STAY 或 App_LEAVE,若返回前者,則表明應該停留在該狀態,下次主迴圈將再次調用此狀態的 loop() 函數;反之,若返回後者,則表明應該切換到下一個狀態。
rowScanning, colScanning, colScanningPressed 3個App_ScanningState實例,分別為行掃描階段、列掃描階段及第三階段(檢測按鍵鬆開)。程式初始時為行掃描狀態,例如,使用CubeMX自動生成的初始化代碼。程式主迴圈內的代碼為:
if (App_LEAVE != currState->loop()) { return; } // Button released if (currState == &colScanningPressed) { lightLedsUp(key); } // Next state currState = currState == &rowScanning ? &colScanning // : currState == &colScanning ? &colScanningPressed // : &rowScanning; currState->enter();
首先,調用當前狀態的 loop() 函數,其返回值表明是否應該切換到下一個狀態。如果切換到下一個狀態,則調用其 enter() 函數。如果是離開第三階段,則已檢測到一次按鍵事件(按下並鬆開),根據按鍵鍵值(0-15)點亮LED。點亮LED的函數定義如下,其無外乎按位依次點亮或熄滅每一個LED:
#define BIT_TO_PIN_VALUE(key, bit) ( (1 & (key >> bit)) ? GPIO_PIN_SET : GPIO_PIN_RESET ) void lightLedsUp(uint8_t key) { HAL_GPIO_WritePin(LED1_GPIO_Port, LED1_Pin, BIT_TO_PIN_VALUE(key, 3)); HAL_GPIO_WritePin(LED2_GPIO_Port, LED2_Pin, BIT_TO_PIN_VALUE(key, 2)); HAL_GPIO_WritePin(LED3_GPIO_Port, LED3_Pin, BIT_TO_PIN_VALUE(key, 1)); HAL_GPIO_WritePin(LED4_GPIO_Port, LED4_Pin, BIT_TO_PIN_VALUE(key, 0)); }
對於行掃描狀態,進入該狀態時,應該對行、列的I/O口進行設置。也即,在其enter() 實現中設置行I/O口為input模式,並啟用其內部上拉電阻;列I/O為output模式,並輸出0。其 loop() 實現則依次檢測行I/O口是否讀數為0,若讀數為0,則表明該行有按鍵按下,記下行號,並離開本狀態:
#define configInputPullUp(port, pin, GPIO_InitStruct) { \ /* HAL_GPIO_WritePin(port, pin, GPIO_PIN_RESET); */ \ (GPIO_InitStruct)->Pin = pin ; \ (GPIO_InitStruct)->Mode = GPIO_MODE_INPUT ; \ (GPIO_InitStruct)->Pull = GPIO_PULLUP ; \ (GPIO_InitStruct)->Speed = GPIO_SPEED_FREQ_LOW; \ HAL_GPIO_Init(port, GPIO_InitStruct) ; \ } #define configOutputLow(port, pin, GPIO_InitStruct) { \ (GPIO_InitStruct)->Pin = pin ; \ (GPIO_InitStruct)->Mode = GPIO_MODE_OUTPUT_PP ; \ (GPIO_InitStruct)->Pull = GPIO_NOPULL ; \ (GPIO_InitStruct)->Speed = GPIO_SPEED_FREQ_LOW; \ HAL_GPIO_Init(port, GPIO_InitStruct) ; \ HAL_GPIO_WritePin(port, pin, GPIO_PIN_RESET); \ } #define DEBOUNCE_DELAY 5 void rowScanningEnter() { GPIO_InitTypeDef GPIO_InitStruct; // Row pins: input, pull-up enabled configInputPullUp(R1_GPIO_Port, R1_Pin, &GPIO_InitStruct); configInputPullUp(R2_GPIO_Port, R2_Pin, &GPIO_InitStruct); configInputPullUp(R3_GPIO_Port, R3_Pin, &GPIO_InitStruct); configInputPullUp(R4_GPIO_Port, R4_Pin, &GPIO_InitStruct); // Col pins: output 0 configOutputLow(C1_GPIO_Port, C1_Pin, &GPIO_InitStruct); configOutputLow(C2_GPIO_Port, C2_Pin, &GPIO_InitStruct); configOutputLow(C3_GPIO_Port, C3_Pin, &GPIO_InitStruct); configOutputLow(C4_GPIO_Port, C4_Pin, &GPIO_InitStruct); } GPIO_PinState checkPressedLow(GPIO_TypeDef *port, uint16_t pin) { if (GPIO_PIN_RESET == HAL_GPIO_ReadPin(port, pin)) { // Delay & read again HAL_Delay(DEBOUNCE_DELAY); return HAL_GPIO_ReadPin(port, pin); } return GPIO_PIN_SET; } uint8_t rowScanningLoop() { if (GPIO_PIN_RESET == checkPressedLow(R1_GPIO_Port, R1_Pin)) { key = 0; return App_LEAVE; } if (GPIO_PIN_RESET == checkPressedLow(R2_GPIO_Port, R2_Pin)) { key = 1 << 2; return App_LEAVE; } if (GPIO_PIN_RESET == checkPressedLow(R3_GPIO_Port, R3_Pin)) { key = 2 << 2;key return App_LEAVE; } if (GPIO_PIN_RESET == checkPressedLow(R4_GPIO_Port, R4_Pin)) { key = 3 << 2; return App_LEAVE; } return App_STAY; }
註意,在讀取pin值時,為了de-bouncing,增加了一個5ms的延時重讀。一般,de-bouncing延時取5-10ms。
列掃描狀態的實現與行掃描相類似,這裡便不再給出代碼了。需要說明的是,程式中使用了一個位元組型全局變數 key 用來保存鍵值,其第2-3位為行號(0-3),第0-1位為列號(0-3),因此,key 的值為0-0x0F,依次對應16個按鍵。
而第三階段無需改變I/O口設置,只需檢測被按下按鍵所在的列是否讀取pin值為1。讀取pin值為1表明按鍵被鬆開,應該離開此狀態,切換回行掃描狀態:
uint8_t colScanningPressedLoop() { int col = 3 & key; if (0 == col) { if (GPIO_PIN_SET == HAL_GPIO_ReadPin(C1_GPIO_Port, C1_Pin)) { return App_LEAVE; } } else if (1 == col) { if (GPIO_PIN_SET == HAL_GPIO_ReadPin(C2_GPIO_Port, C2_Pin)) { return App_LEAVE; } } else if (2 == col) { if (GPIO_PIN_SET == HAL_GPIO_ReadPin(C3_GPIO_Port, C3_Pin)) { return App_LEAVE; } } else { // 3== col if (GPIO_PIN_SET == HAL_GPIO_ReadPin(C4_GPIO_Port, C4_Pin)) { return App_LEAVE; } } return App_STAY; }