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 个实例:
结构体 App_ScanningState 表示 1 个状态,当进入该状态时,调用其 (函数指针)成员 enter() .在程序主循环中,则调用其 loop() 成员.loop() 函数返回值为 App_STAY 或 App_LEAVE,若返回前者,则表明应该停留在该状态,下次主循环将再次调用此状态的 loop() 函数;反之,若返回后者,则表明应该切换到下一个状态.
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;
rowScanning, colScanning, colScanningPressed 3 个 App_ScanningState 实例,分别为行扫描阶段,列扫描阶段及第三阶段(检测按键松开).程序初始时为行扫描状态,例如,使用 CubeMX 自动生成的初始化代码.程序主循环内的代码为:
首先,调用当前状态的 loop() 函数,其返回值表明是否应该切换到下一个状态.如果切换到下一个状态,则调用其 enter() 函数.如果是离开第三阶段,则已检测到一次按键事件(按下并松开),根据按键键值(0-15)点亮 LED.点亮 LED 的函数定义如下,其无外乎按位依次点亮或熄灭每一个 LED:
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();
对于行扫描状态,进入该状态时,应该对行,列的 I/O 口进行设置.也即,在其 enter() 实现中设置行 I/O 口为 input 模式,并启用其内部上拉电阻;列 I/O 为 output 模式,并输出 0.其 loop() 实现则依次检测行 I/O 口是否读数为 0,若读数为 0,则表明该行有按键按下,记下行号,并离开本状态:
#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));
}
注意,在读取 pin 值时,为了 de-bouncing,增加了一个 5ms 的延时重读.一般,de-bouncing 延时取 5-10ms.
#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;
}
列扫描状态的实现与行扫描相类似,这里便不再给出代码了.需要说明的是,程序中使用了一个字节型全局变量 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;
}
来源: http://www.bubuko.com/infodetail-2457889.html