1.直接上代码
#include "stm32f10x.h"
#include "led.h"
#include "key.h"
void KeyConfig(void)
{
GPIO_InitTypeDef GPIO_InitStructure; //定义结构体
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //打开GPIOB的外设时钟
/* 配置结构体并初始化到GPIOB */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; //选择需要使用的引脚
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD; //配置引脚输出模式
GPIO_Init(GPIOB, &GPIO_InitStructure); //初始化结构体
}
int main (void)
{
LedConfig();
KeyConfig();
while(1)
{
if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_9) == 1) //检测按键是否出现高电平状态
{
keyDelay(50); //延时去抖
if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_9) == 1) //再次检测
GPIO_ResetBits(GPIOC,GPIO_Pin_13); //点亮LED灯
}
else
{
GPIO_SetBits(GPIOC,GPIO_Pin_13); //熄灭LED灯
}
keyDelay(100); //普通延时
}
}
2.代码解析
代码的整体思路是,初始化LED灯以及按键检测IO,每隔100ms检测一次按键电平状态是否发生改变。当检测到按键按下时(即检测到高电平时),将LED灯点亮,否则熄灭LED灯。
在初始化代码中,相对比起LED减少了一步-配置IO口的频率(GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;),该代码是针对当IO口需要输出时的引脚输出速率,但本次工程需要使用的是IO的输入状态。
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
选择GPIOB_PIN_9引脚作为按键检测的输入引脚。
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD;
定义引脚模式,此处因为引脚需要作用的功能是输入检测,所以需要配置为输入模式,而输入模式在官方固件库代码中有三种模式,
GPIO_Mode_IN_FLOATING;GPIO_Mode_IPD;GPIO_Mode_IPU
第一种模式两种状况,其一是用于 IIC,SPI,UART...等通讯协议中,即数据状态不确定的情况下;其二是当外围硬件已经接有上拉/下拉电阻的情况下。
第二、三种模式一般是用于 引脚需要固定在某一种状态,前者是下拉输入,后者是上拉输入。
本次工程中因为是按键检测,需要引脚长期处于某一种状态,以确保LED灯不会因为引脚的漂浮不定所造成误触发,所以配置为GPIO_Mode_IPD。同时在确定引脚模式之前应该查原理图,检测按键不按下时的电平状态,此时的电平状态即为配置初始状态。

此时引脚PB9硬件外围并没有接上/下拉电阻,引脚处于漂浮状态,而当按键按下时3.3V电压直达PB9,状态改变为高电平,松开后引脚又恢复到漂浮状态,所以此时需要给它一个初始电平,根据原理图理解,该初始电平状态为低电平时,后续引脚电平检测才能检测到电平的改变。
if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_9) == 1) //检测按键是否出现高电平状态
{
keyDelay(50); //延时去抖
if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_9) == 1) //再次检测
GPIO_ResetBits(GPIOC,GPIO_Pin_13); //点亮LED灯
}
else
{
GPIO_SetBits(GPIOC,GPIO_Pin_13); //熄灭LED灯
}
传入形参中,前者为IO口所在的组,后者为具体IO。此语句是用于检测按键是否出现高电平。当出现高电平时,进入下一次检测,用以去除按键抖动,如果下一次检测依然出现高电平,则代表按键正常按下,执行点亮LED灯操作。当检测不到按键按下时熄灭LED灯。实现按键按下时点亮LED灯,松开熄灭LED灯。
3.花样按键
3.1矩阵按键
一个按键一个IO进行检测这种方法只适用于按键少的情况,但当按键的数量达到10+以上时,一IO一按键的状况就不实用了,因为IO口资源对于单片机来说是尤为珍贵的,对于这种情况,就有了一种应对方法——矩阵按键。
矩阵按键就是将N个按键,进行XY平面分布,组成一个阵列,随后分别将每一行每一列的引脚串联起来,让同一列的共用同一个引脚,同一行共用同一引脚,最后大致原理图如下:

我们用到的是3*4的矩阵键盘,根据原理图分析矩阵电路图的连接方式。
- 第一到四行分别连接单片机的PB6~PB9
- 第一到三列分别连接单片机的PA8-PA10
我们将PB6~PB9初始化配置为上拉输入模式,将PA8~PA10配置为推挽输出模式,并将其默认置0。在这种情况下当有按键按下时,引脚会被拉低,松开按键后引脚又会被芯片重新拉高。
举个例子,当按键1被按下时,PB9就会被拉低,此时如果按照51单片机的逻辑,我们只需要再次检测列引脚查看哪个被拉低,就可以确定按键在哪个位置。但在STM32中引脚的输入与输出是分开的,输入引脚的上拉电压在按键按下被接通后,在流入输出引脚后会被芯片内的NMOS对地放掉,所以引脚状态并不会发生改变。关于 I/O端口的基本结构可以看这位博主的文
在这种情况下我们需要将推挽输出的IO进行电平反转,再检测是哪一列,具体代码如下:
引脚初始化代码
void MatrixKeyConfig(void)
{
GPIO_InitTypeDef GPIO_InitStructure; //定义结构体
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //打开GPIOA的外设时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //打开GPIOB的外设时钟
/* 配置结构体并初始化到GPIOA */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9 | GPIO_Pin_10; //选择需要使用的引脚
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //配置引脚输出模式
GPIO_Init(GPIOA, &GPIO_InitStructure); //初始化结构体
GPIO_ResetBits(GPIOA, GPIO_Pin_8 | GPIO_Pin_9 | GPIO_Pin_10);
/* 配置结构体并初始化到GPIOB */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7 | GPIO_Pin_8 | GPIO_Pin_9; //选择需要使用的引脚
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //配置引脚输出模式
GPIO_Init(GPIOB, &GPIO_InitStructure);
}
引脚输入的初始化可以参考上面的按键初始化 ,输出初始化参考前面讲过的LED。
矩阵检测
char MatrixCheck(void)
{
uint16_t line;
uint16_t keydata = 0;
if((GPIO_ReadInputData(GPIOB)>>6) != 0x3ff)
{
keyDelay(1);
if((GPIO_ReadInputData(GPIOB)>>6) != 0x3ff)
{
keydata = GPIO_ReadInputData(GPIOB)>>6; //记录当前按键状态
line = 0x3ff-keydata; //得到当前被按下行数,8-4 4-3 2-2 1-1 line=0x08时,对应第四行被按下。
GPIO_SetBits(GPIOA,GPIO_Pin_8); //IO反转,
if((GPIO_ReadInputData(GPIOB)>>6) == keydata) //检测输入是否因为IO反转发生改变。
{
GPIO_ResetBits(GPIOA,GPIO_Pin_8);
GPIO_SetBits(GPIOA,GPIO_Pin_9);
if((GPIO_ReadInputData(GPIOB)>>6) == keydata)
{
GPIO_ResetBits(GPIOA,GPIO_Pin_9);
GPIO_SetBits(GPIOA,GPIO_Pin_10);
if((GPIO_ReadInputData(GPIOB)>>6) == keydata)
{
GPIO_ResetBits(GPIOA,GPIO_Pin_10);
}
else
line |= 0x10; //代表第一列按键被按下
}
else
line |= 0x20; //代表第二列按键被按下
}
else
line |= 0x30; //代表第三列按键被按下
GPIO_ResetBits(GPIOA, GPIO_Pin_8 | GPIO_Pin_9 | GPIO_Pin_10); //重新拉低输出IO
switch(line)
{
case 0x18:return '1';
case 0x14:return '4';
case 0x12:return '7';
case 0x11:return '*';
case 0x28:return '2';
case 0x24:return '5';
case 0x22:return '8';
case 0x21:return '0';
case 0x38:return '3';
case 0x34:return '6';
case 0x32:return '9';
case 0x31:return '#';
default:return 'n';
}
}
}
return 'n';
}
第一步检测按键状态是否发生改变,>>6 是为了将第一个按键所在的IO对齐数据的第一位。对于PB端口而言当高于PB5的端口没有被配置时都处于高阻态,所以他们的电平都是高电平,因此PB组IO在>>6后的默认数据为0x03ff。当检测到不为0x03ff时,代表有按键按下。使用keydata来储存按键状态,用0x03ff减去keydata就得到按键所在行数(行数对应8421,8代表在从下往上数第四行,4代表在第三行,以此类推),随后反转输出IO的电平,再检测PB口数据,将得到的数据与IO前的数据进行对比,如果不相等则代表上拉输入IO无法通过输出IO内部NMOS对地放电(有电势差才有电导通),因此可以确定列所在的位置。在完成对列的检测后仍需要对输出IO进行一个电平拉低。
上面我们得到一个完成的行列数据,随后将line参数传入switch进行一个选择,这完成了一整个矩阵按键的代码编写。
矩阵按键的具体应用
void MatrixUse(void)
{
char keyData;
keyData = MatrixCheck2();
if(keyData == '1')
GPIO_Write(GPIOA,GPIO_Pin_7); //点亮第一个LED灯
else if(keyData == '2')
GPIO_Write(GPIOA,GPIO_Pin_6); //点亮第二个LED灯
else if(keyData == '3')
GPIO_Write(GPIOA,GPIO_Pin_5); //点亮第三个LED灯
else if(keyData == '4')
GPIO_Write(GPIOA,GPIO_Pin_4); //点亮第四个LED灯
else if(keyData == '0')
GPIO_Write(GPIOA,0);
}
通过矩阵按键检测函数的返回值进行对应自定义操作,这里以点灯为例子。
3.2 矩阵按键在项目应用注意事项
在我们的具体项目应用中,引脚的电平状态不可能总是0x03ff,因此就必须要考虑只需要获得对应使用那一组引脚的数据,原理图中使用的是PB6-PB9,在二进制表中对应的位就是
0000 0011 1100 0000
将其转化为16进制数就是 0x03c0,得到这个数据由什么用呢,上面说到我们需要屏蔽其他位,只留下我们需要的位,此时我们只需要将GPIO_ReadInputData(GPIOB)函数得到的IO数据和0x03c0进行一个位与操作,就可以达到我们想要的效果,'&'运算符的特性是对0敏感,即有0必0。因此我们需要将3.1的代码进行一个修改。
char MatrixCheck2(void)
{
uint16_t line;
uint16_t keyDefault = 0x03c0; //需要用到引脚的所在位号
uint16_t keydata = (GPIO_ReadInputData(GPIOB) & keyDefault); //此处获取一次的目的是DEBUG模式查看数值
if((GPIO_ReadInputData(GPIOB) & keyDefault)!= keyDefault)
{
keyDelay(1); //滤波
if((GPIO_ReadInputData(GPIOB) & keyDefault)!= keyDefault)
{
keydata = (GPIO_ReadInputData(GPIOB) & keyDefault); //记录当前按键状态
line = (keyDefault-keydata)>>6; //得到当前被按下行数,">>6"将数据进行低位对齐 8-4 4-3 2-2 1-1 line=0x08时,对应第四行被按下。
GPIO_SetBits(GPIOA,GPIO_Pin_8); //IO反转
if((GPIO_ReadInputData(GPIOB) & keyDefault) == keydata)
{
GPIO_ResetBits(GPIOA,GPIO_Pin_8);
GPIO_SetBits(GPIOA,GPIO_Pin_9);
if((GPIO_ReadInputData(GPIOB) & keyDefault) == keydata)
{
GPIO_ResetBits(GPIOA,GPIO_Pin_9);
GPIO_SetBits(GPIOA,GPIO_Pin_10);
if((GPIO_ReadInputData(GPIOB) & keyDefault) == keydata)
{
GPIO_ResetBits(GPIOA,GPIO_Pin_10);
}
else
line |= 0x10; //代表第一列按键被按下
}
else
line |= 0x20; //代表第二列按键被按下
}
else
line |= 0x30; //代表第三列按键被按下
GPIO_ResetBits(GPIOA, GPIO_Pin_8 | GPIO_Pin_9 | GPIO_Pin_10); //重新拉低输出IO
switch(line)
{
case 0x18:return '1';
case 0x14:return '4';
case 0x12:return '7';
case 0x11:return '*';
case 0x28:return '2';
case 0x24:return '5';
case 0x22:return '8';
case 0x21:return '0';
case 0x38:return '3';
case 0x34:return '6';
case 0x32:return '9';
case 0x31:return '#';
default:return 'n';
}
}
}
return 'n';
}
代码的大体思路与3.1所述的一样,(GPIO_ReadInputData(GPIOB) & keyDefault) == keydata 这局代码是为了在去除其他IO口干扰后再进行数据的比较。
3.3密码锁
密码锁的大致原理是,通过矩阵按键将输入的数据存入数组中,随后进行密码的自动校验,校验的方法使用strcmp() 函数。代码如下
int main(void)
{
/*模块初始化*/
//GPIOinit...
uint16_t keys_scan_count = 0; //按键输入次数
char keys; //密码储存临时区域
char keys_data[KeyWords_MAX]={0}; //密码储存数组 KeyWords_MAX为5
char *openkeys = "13258"; //密码
while(1)
{
keys = MatrixCheck2();
if(keys != 'n') //检测不为非法输入
{
if(keys == '#') //此处 # 用作清空输入
{
memset(keys_data,0,strlen(keys_data)); //清空数组
GPIO_Write(GPIOA,0x00); //关闭数码管
GPIO_SetBits(GPIOC,GPIO_Pin_13); //关闭LED
keys_scan_count = 0; //计数清0
continue;
}
GPIO_Write(GPIOA,regLed[keys-48]); //数码管显示当前输入数,字符串1对应的ASCII为48,所以强转HEX需要-48
keys_data[keys_scan_count] = keys; //储存输入密码
if(strcmp(keys_data,openkeys) == 0) GPIO_ResetBits(GPIOC,GPIO_Pin_13); //点亮LED灯
keys_scan_count++; //输入次数+1
if(keys_scan_count >=strlen(openkeys))keys_scan_count = 0; //大于最大密码数清0
}
keyDelay(100);
}
}
从代码角度来理解就是,keys获取按键输入,随后进行字符检测,因为当无输入时MatrixCheck2()的返回值是'n',当不为'n'时代表有按键按下,进入下一步对键值检测是否为特殊字符'#',若是则清空输入数组,否则储存输入键值。
往期内容
本文详细解读了STM32F103中矩阵按键的配置、检测技术,包括基础的按键操作、去抖处理,以及矩阵键盘的原理和实现,还展示了如何利用矩阵按键实现密码锁功能。

3017

被折叠的 条评论
为什么被折叠?



