按键收集单击,双击和长按

引言

在我们生活中, 按键是必不可少的, 不同的电器, 有不同的按键, 但是按键总有不够用的时候, 那么给与一个按键赋予不同的功能,就必不可少了. 一个按键可以通过按下的时间长短和频次, 来定义其类型。

一次按键收集, 都是在一个按键收集周期的, 比如500毫秒内, 我要收集一个按键类型, 在500毫秒内,按键按下一次按键, 那么到达500毫秒后, 我们就宣布答案, 此次按键是单击。

同理, 在500毫秒内, 如果我们按下了, 两次按键,那么就是双击, 以此类推。

那长按呢? 长按就是, 在500毫秒内, 我们按下了按键,并且500ms时间到时, 按键还未松开, 就代表着长按。

本博客最终修改项目链接下载:

点击下载

https://wwyz.lanzoul.com/itXC327v9dra

按键收集模型

image-20240818183801773

单片机实现方法

因为按键, 需要每时每刻的去检测, 是否按下, 如果使用while阻塞循环, 则会大大影响程序效率, 所以我们使用外部中断去检测中断。

按键事件触发检测

按键按下的瞬间, 单片机的io口, 就会被从高电平拉到低电平, 然后单片机io口就会检测到, 从而触发下降沿中断。

此时,就代表着按键事件已经触发了。

按键类型判断

​ 按键事件触发,仅仅代表着我们按下了按键, 但是我们想区分单击,双击和长按, 那么我们就得从按键按下开始计算,直到按键计算周期结束,这一段时间我们定义成 500毫秒。

所以, 我们在按键事件触发后,我们需要启动定时器,计时500ms, 然后500ms时间到后, 判断按键按下的次数, 还有按键此时是否仍然被按下.

在这 500毫秒内, 我们重复按下按键, 就需要重复的检测按键按下的事件, 然后计入按键次数内.

所以我们的处理模型是

image-20240818193546975

代码实现

按键中断

按键中断初始化

我们初始化io端口PB1,配置下降沿中断

image-20240819125528156

void Key_Init(void)
{

	GPIO_InitTypeDef gpio_initstruct;
	EXTI_InitTypeDef exti_initstruct; 
	NVIC_InitTypeDef nvic_initstruct;
	
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);		//打开GPIOB的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);		//打开复用时钟
	
	gpio_initstruct.GPIO_Mode = GPIO_Mode_IPU;				//设置为输出
	gpio_initstruct.GPIO_Pin = GPIO_Pin_1;						//将初始化的Pin脚
	gpio_initstruct.GPIO_Speed = GPIO_Speed_50MHz;				//可承载的最大频率
	
	GPIO_Init(GPIOB, &gpio_initstruct);							//初始化GPIO
	
	GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource1);
	
	exti_initstruct.EXTI_Line = EXTI_Line1;
	exti_initstruct.EXTI_Mode = EXTI_Mode_Interrupt;
	exti_initstruct.EXTI_Trigger = EXTI_Trigger_Falling;
	exti_initstruct.EXTI_LineCmd = ENABLE;
	EXTI_Init(&exti_initstruct);
	
	nvic_initstruct.NVIC_IRQChannel = EXTI1_IRQn;
	nvic_initstruct.NVIC_IRQChannelPreemptionPriority = 2;
	nvic_initstruct.NVIC_IRQChannelSubPriority = 2;
	nvic_initstruct.NVIC_IRQChannelCmd = ENABLE;
	NVIC_Init(&nvic_initstruct);
    
}
触发中断,按键标志位赋1

image-20240819125747163

extern _Bool key_down;
void EXTI1_IRQHandler(void)
{

	if(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0)
	{
		//代表按键按下(按键标志清除--一次时间处理完成)
		key_down = 1;

	}
	EXTI_ClearITPendingBit(EXTI_Line1);
}	

定时器中断扫描判断按键是否按下

当触发中断后,我们把key_down置成1后, 就会被定时器扫描,检测到, 我们就 把按键次数加1, 并且启动 500ms的计时, count = 1, 然后if(count >0), 就会启动 count++计时, 因为我们定时器是1ms运行一次,所以当count == 500的时候,进行按键类型检测

image-20240819125930008

按键类型判断

当到达500ms后, 我们就可以通过判断按键次数, 区分单双击, 单击的情况下,通过判断500ms时,io口的状态,从而判断单击还是长按。

单击和长按区分

单击和长按, 按键次数 button_count 都是1, 不同的是,500毫秒之后,单击按键类型, 已经松开, 长按,则是按下状态, 所以我们通过读取检测500ms时间到时, io的状态, 就可以区分长按和单击了。

长按, io口是低电平, 单击,io是高电平, (我们是下降沿触发, 按键一端连io口,一端连 地, io口输出状态是, 推挽输出)

button_mode = 1代表 单击 ,

button_mode = 3代表 长按

image-20240819130853341

image-20240819141941257

uint8_t key_scan(void)
{
	uint8_t value;
	value = 0;	//默认是不是一直低电平
	//检测是否长按
	if(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0)// 判断按键是否仍然在按下
	{
		//这时候还是按下状态, 不一定是真正的按键按下, 有可能是抖动造成的,接下来延时消抖
		//再次判断按键是否按下, 如果按下, 代表真正的按键仍然在按下
		if(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0)
		{
			//那就等抬起了再做事
			value = 1;//这时候才可以判断是 长按下
		}
	}
	return value;
}
双击区分

通过判断, 按键的次数, 就可以得知双击, 记得按键类型判断完毕后, 要把按键次数置0, 为下次判断做准备。 button_mode = 2, 代表按键双击

image-20240819142123007

定时器扫描函数代码

void TIM4_IRQHandler(void)
{
	if(TIM_GetFlagStatus(TIM4,TIM_FLAG_Update))//一毫秒运行一次
	{
		

			//当 button_atom的时候,才能进行读取按键信息
			if(key_down == 1)
			{	
				key_down = 0;
				button_count++;
				count = 1;	//开始计时
			}
			
			if(count > 0)
			{
				count++;
				if(count > 500)
				{
					count = 0;
					if(button_count == 1)
					{
						button_count = 0;
						if(key_scan() == 0)	//短按
							button_mode = 1;
						else if(key_scan() == 1)	//长按
							button_mode = 3; 
					}
					else 
					if(button_count == 2)
					{
						button_count = 0;
						
						button_mode = 2;		
					}
					else
					{
						button_count = 0;//判断完,到时间照样得button_count 清零,为后面做准备
					}
				}
			}		
		
		TIM_ClearFlag(TIM4,TIM_FLAG_Update);
	}
}

现象调试

判断按键类型方式

我们通过代码实操 ,已经可以得知我们按下的按键是单击,双击还是长按了,通过判断 button_mode 的数值。

image-20240819143221376

调用相关现象

我们选用PA0, PA1, PA2, 来分别代表 单击,双击和长按的小灯。

我们在检测到 button_mode 的数值改变的时候, 就赋值对应的小灯亮灭,这样我们就可以看到现象了

image-20240819144039916

初始化PA0,PA1,PA2

设置这三个io口分别为推挽输出模式

image-20240819181926289

void Led_Init(void)
{
	/*开启时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);	//开启GPIOA的时钟
															//使用各个外设前必须开启时钟,否则对外设的操作无效
	
	/*GPIO初始化*/
	GPIO_InitTypeDef GPIO_InitStructure;					//定义结构体变量
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;		//GPIO模式,赋值为推挽输出模式
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;				//GPIO引脚,赋值为第0号引脚
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;		//GPIO速度,赋值为50MHz
	
	GPIO_Init(GPIOA, &GPIO_InitStructure);					//将赋值后的构体变量传递给GPIO_Init函数
															//函数内部会自动根据结构体的参数配置相应寄存器
															//实现GPIOA的初始化
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;		//GPIO模式,赋值为推挽输出模式
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1;				//GPIO引脚,赋值为第0号引脚
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;		//GPIO速度,赋值为50MHz
	
	GPIO_Init(GPIOA, &GPIO_InitStructure);					//将赋值后的构体变量传递给GPIO_Init函数
															//函数内部会自动根据结构体的参数配置相应寄存器
															//实现GPIOA的初始化
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;		//GPIO模式,赋值为推挽输出模式
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;				//GPIO引脚,赋值为第0号引脚
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;		//GPIO速度,赋值为50MHz
	
	GPIO_Init(GPIOA, &GPIO_InitStructure);					//将赋值后的构体变量传递给GPIO_Init函数
															//函数内部会自动根据结构体的参数配置相应寄存器

	//引脚初始化
	led_go(0,0);
	led_go(1,0);
	led_go(2,0);		
}	

单独设置对应的小灯亮灭

void led_go(uint8_t led,_Bool go)

比如我们控制 PA0 io口, 我们传入 0, 代表我们要控制位序为0的小灯, 第二个参数 go = 1, 代表着让灯两, go = 0, 代表着让灯灭。这样我们就可以灵活的控制小灯亮灭了。

image-20240819182225221
void led_go(uint8_t led,_Bool go)
{
	if(led == 0)
	{
		if(go == 1)
		{
			GPIO_SetBits(GPIOA, GPIO_Pin_0);	//亮
		}
		else
		{
			GPIO_ResetBits(GPIOA, GPIO_Pin_0); //灭
		}
	}
	else
	if(led == 1)
	{
		if(go == 1)
		{
			GPIO_SetBits(GPIOA, GPIO_Pin_1);	//亮
		}
		else
		{
			GPIO_ResetBits(GPIOA, GPIO_Pin_1); //灭
		}		
	}	
	else
	if(led == 2)
	{
		if(go == 1)
		{
			GPIO_SetBits(GPIOA, GPIO_Pin_2);	//亮
		}
		else
		{
			GPIO_ResetBits(GPIOA, GPIO_Pin_2); //灭
		}		
	}	
}

根据 button_mode亮灭小灯

还是那个图, 我们有了底层构建, 我们根据单双击,然后会有不同的butoon_mode 按键模式, 所以我们根据这个数值, 赋值对应的小灯亮灭, 这样我们就可以看到现象了.

led控制图(ctrl 加鼠标左键,快速跳转)

image-20240819182628204
void chose_led(void)
{
		if(button_mode == 0)
		{
			led_go(0,0);
			led_go(1,0);
			led_go(2,0);
			
		}
		else 
		if(button_mode == 1)
		{
			led_go(0,1);
			led_go(1,0);
			led_go(2,0);
			
		}
		else 
		if(button_mode == 2)
		{
			led_go(0,0);
			led_go(1,1);
			led_go(2,0);
			
		}
		else 
		if(button_mode == 3)
		{
			led_go(0,0);
			led_go(1,0);
			led_go(2,1);
			
		}	
}	

main.c主函数调用

初始化,按键和定时器, 还有调试的小灯, 我们根据按键模式, 控制对应的小灯亮灭, 观察现象

image-20240819182827540
_Bool key_down = 0;	//按键是否按下
uint16_t count;	//定时器计数毫秒数(按键按下时长)
uint8_t button_count = 0; 
uint8_t button_mode = 0;	//按键模式:无-0 , 单击- 1, 双击 - 2, 长按 - 3
int main(void)
{
	Key_Init();
	TIM4_Init();
	Led_Init();
	while(1)
	{
		chose_led();
	}
}	

工程代码构建

我们上面, 代码的逻辑思路, 已经构建完毕, 同时我们也提供构建完成的工程, 我们这里带领大家, 复制黏贴

1.基本工程构建方法

创建stm32f103c8t6基本工程_stm32f103c8启动文件配置-CSDN博客

2.我们工程名字叫做

Key_detection_model

点击品字,然后修改工程名字

image-20240819183230023

3.我们用到main函数, 所以有User用户文件夹

我们用到按键检测, 所以用到 Key_model按键模型

我们用到系统的延时函数, 所以用到System系统文件夹

我们用到定时器,所以用到Time定时器文件夹

User
Key_model
System
Time

4.具体文件添加方式, 看最小例程构建,从第九步开始

我们User文件夹下, 添加

main.c
led.c
led.h

image-20240819183721379

Key_model文件夹下添加

key.c
key.h
image-20240819183757371

System文件夹下添加

delay.c
delay.h
sys.h
image-20240819200115950

Time文件夹下添加

TIM4.c
TIM4.h
image-20240819200130023

5.记得包含对应的文件夹路径

image-20240819200342838

6.按键收集模型代码快速复制

各文件对应的代码

跳转复制

  1. 构建完成如图

image-20240819201343274

8.编译运行,烧录

烧录方法

9.小灯辨别正负极方法

灯芯放在光下看, 大头是负极,小头是正极, 我们正极插在io口, 负极插在地, io口分别是 PA0, PA1, PA2

10.按键演示视频

按键演示视频_哔哩哔哩_bilibili

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐