嵌入式学习笔记——使用寄存器编程实现按键输入功能

前言

昨天,通过配置通用输出模式,实现了LED灯的点亮、熄灭以及流水等操作,解决了通用输出的问题,今天我们再借用最常见的输入模块,按键来实现一个按键控制LED的功能,重点是配置GPIO为输入模式,以及如何检测GPIO的输入电平。

模块介绍

原理图

笔者用的这款最小系统有三个独立按键,可以操作,首先,第一步还是看原理图来确定我们需要使用的端口和管脚,可以看出K_UP使用的是PA0、K0使用的是PE4、KEY1使用的是PE3。
在这里插入图片描述
注意观察这三个按键的电路,其中KEY0和KEY1是没有上拉电阻的,只有按下按键直接接地这一个电平模式,这个我们在前面讲解GPIO模式的时候提到过,如果没有外部上拉的电路,想要实现高低电平的检测需要在内部编程实现上下拉,这两个按键就是需要使用到内部上拉,使得默认PE4、PE3端口默认是高电平,也就是1,只有按键按下,才会被拉到低电平,也就是0。
而K_UP,刚好相反,只有上拉电路,按下按键是高电平,不按下的时候应该要其默认状态是低,也就是说需要我们为其配置下拉。

编程思路

在看清楚检测原理后,就需要理清编程思路,根据昨天的按键技巧来,首先需要新建文件,命名保存key.c存在src文件夹下,key.h存在inc文件夹下,然后将Key.c添加到工程,再然后是定义头文件,编写初始化函数。
在这里插入图片描述
编写初始化代码:
伪代码:
①编写注释:
/***************************************************************************
*函数名 :Key_Init
*函数功能 :按键所用的管脚的初始化配置
*函数参数 :无
*函数返回值:无
*函数描述 :
KEY_UP------PA0------通用输入模式,默认状态采取内部下拉,按下按键为高电平
K0----------PE4------通用输入模式,默认状态采用内部上拉,按下按键为低电平
K1----------PE3------通用输入模式,默认状态采用内部上拉,按下按键为低电平
***************************************************************************/
②初始化函数
void Key_Init(void)
{
③使能对应端口的时钟,有两个,一个是GPIOA(昨天用过),一个是GPIOE;GPIOA对应第0位,GPIOE对应第4位。(先在数据手册查其挂接的时钟总线,然后再再第六章RCC找到对应使能进行配置)
④设置对应管脚的模式,为通用输入模式,分两组分别配置,A0:应该配置GPIOA的MODER 0 1两位,写入00;E3E4对应GPIOE的MODER的9 8 7 6 位,也都应该写入0000;
⑤设置上下拉,其中PA0设置为下拉模式,应该对GPIOA的PUPDR 的1 0两位写入10;PE4,PE3则应该将GPIOE的PUPDR 寄存器的9 8 7 6 位写入0101。
}
好了,可以发现整个配置过程比昨天的输入配置稍微简单一点,而且昨天输出使用的寄存器在按键输入上都是没有用上的。
接下来来看看代码吧。

//注释
void Key_Init(void)
{
    
    
	//打开AHB1上GPIOA端口
	RCC->AHB1ENR |= (1<<0);
	//打开GPIOE端口对应的AHB1时钟
	RCC->AHB1ENR |= (1<<4);
	//配置GPIOA0为通用输入模式
	GPIOA ->MODER &=~(3<<0);//清0  GPIOA_MODER寄存器为00通用输入模式
	GPIOA ->PUPDR &=~(3<<0);//清0  GPIOA_PUPDR寄存器为00 浮空
	GPIOA ->PUPDR|=(1<<1);//清0  GPIOA_PUPDR寄存器为10 下拉
	
	GPIOE->MODER &= ~(0XF<<6);//通用输入
	GPIOE->PUPDR &= ~(0XF<<6);//清零
	GPIOE->PUPDR |=  (0X5<<6);//写入0101配置为上拉模式
}

检测IO口的电平

在GPIO做输出的时候,是通过对对应端口的ODR寄存器的对应位进行写0与写1来实现输出低电平和高电平的,很明显,如果需要获取GPIO的输入状态,肯定只能采取读的方式,那么,该怎么读取呢,参照输出,肯定会有一个对应的输入数据寄存器用来获取GPIO的状态,前面介绍GPIO寄存器的时候提到过一个叫做IDR的寄存器,它的作用就是存储GPIO的高低电平的。也就是说,在获取输入信号时,需要直接操作的就是这个IDR寄存器的对应位。
在这里插入图片描述
其中Key_Up使用的是GPIOA->IDR的第0位;
K0使用的是GPIOE->IDR的第四位;K1使用的是GPIOE->IDR的第三位;
可以发现这个寄存器本身就是只读的,所以在编程的时候就不能对其使用‘=‘赋值语句。
由于需要获取IDR对应位的高和低,所以考虑使用 & 操作,在对应位 & 上1,当寄存器内部对弈位是1则输出1;当寄存器内部对应位是0则输出0;
具体的实现方式如下:

#define KEY_UP (GPIOA->IDR&(1<<0))//Key_Up的状态
#define KEY0 (GPIOE->IDR&(1<<3))  //KEY0的状态
#define KEY1 (GPIOE->IDR&(1<<4))  //KEY1的状态

采用宏定义的方式,这样通过条件判断KEY1、KEY0、KEY_UP是否为真即可知道按键是否按下,其中由于KEY1、KEY0是默认上拉,未按下时是高电平此时KEY1与KEY0是非0,只有按下按键才是低电平,此时KEY1和KEY0是0;而KEY_UP默认是下拉状态,默认是低电平,也就是KEY_UP是0;只有当按键按下,GPIO才变成高电平,KEY_UP是非0。
宏定义记得放回到Key.h的头文件中。
在这里插入图片描述
然后在主函数进行调用(记得添加Key.h进入main.h)。
1.检测按键状态,肯定是需要实时刷新才能检测到,因此有关判断必须放在While(1)主循环中,而不是上方的单次运行区;
2.三个按键的检测原理是不同的,KEY0与KEY1是需要检测低电平,所以判断按下的语句是if(!KEY0)和if(!KEY1);而KEY_UP需要检测的是高电平,所以语句是if(KEY_UP);
3.为了看见效果,需要定义三个变量,Key_Up、k0、k1三个变量(使用ST-LINK仿真,用iWatch查看数据,当然也可以使用LED灯);
4.使用一个模块之前一定要在初始化区调用对应模块的初始化函数。
在这里插入图片描述
main.c的代码:

#include "main.h"
u8 Led_Speed=5;
u8 Key_Up=0;
u8 k0=0;
u8 k1=0;
int main(void)
{
    
    
/*------------------变量定义区--------------------------*/

/*------------------初始化外设区------------------------*/
	Led_Init();
	Key_Init();
/*------------------单次运行区--------------------------*/	
	
	while(1)//防止程序跑飞
	{
    
    
/*------------------主循环区--------------------------*/		
			if(KEY_UP)Key_Up++;
			if(!KEY0)k0++;
			if(!KEY1)k1++;		
	}
}

按照上面的步骤和位置添加好代码后,编译通过即可,接下来开始使用iWatch调试。

Debug调试

编译通过后,点击1的位置,等待软件加载进入如下界面,这是调试与仿真的界面,为了直观地看见效果,现将上面定义的Key_Up、k0、k1添加到Watch中,这个框的作用就是可以方便查看仿真过程中变量的值,添加步骤如下图所示:
在这里插入图片描述
根据上面代码的逻辑,应该是按下对应按键这些值会对应自增1,按照想象来说,应该是按下一次数值自增1,实际效果会是这样吗。如下图所示:
在这里插入图片描述
可以发现,实际效果并不是按下一次按键变量自增一,而是按下一次自增了几十甚至几百,很明显这是有问题的。那么产生这个现象的原因是什么呢,其实就是前面提到过的主频问题,按照我们的代码逻辑,只要检测到对应IO是高或者是低,就自增,由于STM32F4的主频达到了168MHZ,也就是说它1秒钟可以跑完168M条机器指令,当我们按下按键的时候,它就开始增加,一直到我们松开按键,对于肉眼和感觉来说是很短暂,但是这个时间,单片机已经重复运算很多次了,这就会出现上面图片里的现象,那么要怎么解决这个问题呢。这就需要使用到按键扫描函数了。

按键扫描函数

出现上面的问题是因为在按下按键后if()的条件在一段时间内一直是真,导致变量一直自增,那么怎么解决一直自增的问题呢,这时候,前辈们想到了利用检测松手以及标志位锁住来解决这个问题,先看看代码:

/*******************************************
*函数名    :Key_Scanf
*函数功能  :按键扫描
*函数参数  :void 
*函数返回值:key_value键值
*函数描述  :
告诉主函数按下的是哪个按键。
判断案件是否按下,并返回对应键值。
*********************************************/
u8 Key_Scanf(void)
{
    
    
		u8 key_value=0;//初始键值
		static u8 Key_Flag=0;
		if(KEY_UP && Key_Flag == 0)//判断按键是否按下
		{
    
    
				key_value=1;//对按下的按键进行赋值
				Key_Flag=1;
		}
		if(!KEY_UP)//判断按键是否松开
		{
    
    
			Key_Flag =0;
		}
		return key_value;//返回键值
}

通过加加入了一个Key_Flag实现了对按键的判断锁定,检测到按键按下后,将标志位置位1,这样即使整个按下期间KEY_UP一直是真,由于后面的flag的限制,也无法造成影响,一直到检测到按键松开,才重新释放Key_Flag,这样也保证了下一次判断按键是否按下不受影响。在主函数调用,然后重复上面的Debug仿真。
![在这里插入图片描述](https://img-blog.csdnimg.cn/452ea8456e5f4f6f9277902350add3bd.png

实现效果如下:
在这里插入图片描述
这次可以明显看出了是每次自增1,但是还是有点问题,就是有时候明明只按下了一次,缺连着加了两三个数,这是由于按键的物理抖动造成的。
在这里插入图片描述
为了解决这个问题,也有两种方式,一种是通过在按键两端并联电容的方式硬件消除,但是,实际使用中很少有这么干的毕竟能省则省;主要是因为另外一种方式是使用代码延时解决,不需要物料成本。

延时函数

这里介绍一下STM32简单的延时函数。前面提到过,STM32F407一秒钟可以运行168M条机器指令,1ms运行168 000条,1us运行168条机器指令,需要注意的是一条机器指令不等于一条C语句,按照传言,一条C语句可以近似等价于4条机器指令,但是那毕竟是传言,不一定准确。
按照上面的逻辑,我们让STM32F407单片机运行168条机器指令就可以得到一个近似1us的函数,可是上哪找这个机器指令呢,其实在工程中有一个机器指令,它就是__nop,于是有了如下的us延时与毫秒延时函数:

/*******************************************
*函数名    :Delay_us
*函数功能  :机器指令微妙延时函数
*函数参数  :微秒数u32 us
*函数返回值:无
*函数描述  :
利用机器指令nop实现延时
1s---------168M条机器指令
1ms---------168 000 条机器指令
1us---------168条机器指令
*********************************************/
void Delay_us(u32 utime)
{
    
    
	while(utime--)
	{
    
    
		//1us的延时时长
		__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();
		__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();
		__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();
		__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();
		__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();
		__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();
		__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();
		__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();
		__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();
		__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();
		__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();
		__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();
		__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();
		__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();
		__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();
		__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();
		__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();
	}
}


/*******************************
函数名:Delay_ms
函数功能:机器指令实现ms延时
函数形参:u32 mtime
函数返回值:void
备注:
1s  168M  
1MS 168k
1us 168
********************************/
void Delay_ms(u32 mtime)
{
    
    
	while(mtime--)
	{
    
    
		//1ms的延时
		Delay_us(1000);
	}
}

同样的,为了方便后面管理,也需要新建Delay.c与Delay.h然后将.c加入工程,将.h包含进main.h的头文件。
然后在Key_Scanf()函数中添加延时消抖,并将剩下两个按键添加进来,最后的Key_Scanf函数如下所示:

/*******************************************
*函数名    :Key_Scanf
*函数功能  :按键扫描
*函数参数  :void 
*函数返回值:key_value键值
*函数描述  :
告诉主函数按下的是哪个按键。
判断案件是否按下,并返回对应键值。
*********************************************/
u8 Key_Scanf(void)
{
    
    
		u8 key_value=0;//初始键值
		static u8 Key_Flag=0;
		if(KEY_UP && Key_Flag == 0)//判断按键KEY_UP是否按下
		{
    
    
			Delay_ms(10);//消抖
			if(KEY_UP)
			{
    
    
				key_value=1;//对按下的按键进行赋值
				Key_Flag=1;
			}
		}
		
		else if(!KEY0 && Key_Flag == 0)//判断按键KEY0是否按下
		{
    
    
			Delay_ms(10);//消抖
			if(!KEY0)
			{
    
    
				key_value=2;
				Key_Flag=1;
			}
		}
		else if(!KEY1 && Key_Flag == 0)//判断按键KEY1是否按下
		{
    
    
			Delay_ms(10);//消抖
			if(!KEY1)
			{
    
    
				key_value=3;
				Key_Flag=1;
			}
		}
		
		if(!KEY_UP&& KEY0 && KEY1)//判断按键是否松开
		{
    
    
			Key_Flag =0;
		}
		return key_value;//返回键值
}

功能实现

通过上面的流程,基本搞定了按键的输入检测,接下里实现一个小的需求,按下按键KEY_UP小灯1亮,按下KEY0小灯2亮,按下KEY1两个小灯一起灭。
具体实现的main.c如下:

int main(void)
{
    
    
/*------------------变量定义区--------------------------*/
 u8 K_Value=0; 
/*------------------初始化外设区------------------------*/
	Led_Init();
	Key_Init();
/*------------------单次运行区--------------------------*/	
	
	while(1)//防止程序跑飞
	{
    
    
/*------------------主循环区--------------------------*/		
		K_Value=Key_Scanf();
		switch(K_Value)
		{
    
    
			case 1:Key_Up++;LED_1_ON;break;
			case 2:k0++;LED_2(1);break;
			case 3:k1++;LED_1_OFF;LED_2(0);break;
			default : break;
		}
		
	}
}

最终效果:
在这里插入图片描述

总结

本文主要是记录将GPIO配置为输入模式,检测按键输入的功能,如有疑问欢迎提出,另外文中如有不足,也欢迎批评指正。

猜你喜欢

转载自blog.csdn.net/qq_41954556/article/details/129430216