STM32F103 GPIO之按键检测

本文详细解读了STM32F103中矩阵按键的配置、检测技术,包括基础的按键操作、去抖处理,以及矩阵键盘的原理和实现,还展示了如何利用矩阵按键实现密码锁功能。

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'时代表有按键按下,进入下一步对键值检测是否为特殊字符'#',若是则清空输入数组,否则储存输入键值。

按键检测 提取码:8866

往期内容

STM32F103基于固件库创建工程模板

STM32F103 GPIO之点亮一个LED灯

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

河狸子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值