【蓝桥杯嵌入式16届备赛】(三)按键篇

按键是蓝桥杯比赛中的常驻嘉宾了,他和LED以及LCD被称为蓝桥杯嵌入式赛道的御三家,开了个玩笑。玩笑归玩笑,这一部分的考点也是比较多的,难点也是较多,所以理解起来也是会有一些困难相较于前两篇的LCD和LED。

一、按键的STM32CubeMX工程文件的配置:

要配置STM32CubeMX的工程文件,那么我们就要先了解按键的硬件电路图

在图中,我们可以清楚的看到比赛中涉及的按键引脚是PA0、PB0~PB2,所以 我们的工程文件配置就围绕着几个引脚进行,下面就展示一下具体的配置(指的是引脚配置,时钟树和其他的过于公式就不展示了)

这个地方解释一下,因为他这个地方和LED的选择项比较像,关于GPIO_Output和GPIO_Input的区别,前者是用于输出的,因为LED要进行这种外放的显示,所以要用到都这个;后者是用于输入,因为按键是要将信号传给单片机,让其接收到指令。这就是两者的区别,当然也能从两者的英文名显示出来。

 这个地方,我们解释一下为何要配置这个Pull-up这个选项,意思是上拉输出。在这个地方我们要结合硬件电路图进行分析了,在硬件电路图中,我们能清楚的看见当按键没有按下的时候,他是处于强上拉的一个状态,当按键按下时,处于低电平,当我们通电时,按键并没有按下,所以要处于上拉状态,这就是原因。当然,这个地方不给他设置这个上拉也是可以的,因为她本身就保持这个特性,但这么写要明白他的这么写的原理,也是一种保障。

二、按键普通功能的实现:

我们蓝桥杯嵌入式的比赛开发板无非就是五个按键,其中还有一个是复位的,这个按键就不用我们管了,所以总体的难度还是偏小的。在这里我会分为三种方法来进行按键普通功能的实现:

1、第一种方法:

之所以把他作为第一种方法,是因为我觉他是这里面最简单的,所以把它放在第一位,话不多说,直接进入正题

void Key_Scan(void)
{
	//读取按键的引脚状态
	uint8_t B1_KeyNum = HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0);
	uint8_t B2_KeyNum = HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_1);
	uint8_t B3_KeyNum = HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_2);
	uint8_t B4_KeyNum = HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0);
	
	//按键引脚之后的状态
	uint8_t B1_last_KeyNum;
	uint8_t B2_last_KeyNum;
	uint8_t B3_last_KeyNum;
	uint8_t B4_last_KeyNum;
	
	//按键1
	if(B1_KeyNum == 0 && B1_last_KeyNum == 1)
	{
		//这个地方就据题目的要求来写要实现的功能
	}
	//按键2
	if(B2_KeyNum == 0 && B2_last_KeyNum == 1)
	{
		//这个地方就据题目的要求来写要实现的功能
	}
	//按键3
	if(B3_KeyNum == 0 && B3_last_KeyNum == 1)
	{
		//这个地方就据题目的要求来写要实现的功能
	}
	//按键4
	if(B4_KeyNum == 0 && B4_last_KeyNum == 1)
	{
		//这个地方就据题目的要求来写要实现的功能
	}
	
	//按键按完后接着读取的状态赋值给这些变量
	B1_last_KeyNum = B1_KeyNum;
	B2_last_KeyNum = B2_KeyNum;
	B3_last_KeyNum = B3_KeyNum;
	B4_last_KeyNum = B4_KeyNum;
}

这种方法比较简单,但是写起来他的内容过多了,有时候配上一些功能的时候容易能混,所以我不是经常用这种方法。还有一点就是他这里的这个判断条件,可能对于新手来说有点不太友好,我怕我注释不太清楚,我就在这里继续说明一下,因为我们在前面提到了这个配置的工程文件的原理,关于这个上拉和低电平,所以在这个判断条件这里,我们就应用的这一点,当按键按下,他是处于低电平状态,所以这里B1_KeyNum == 0就是说明他是否为低电平状态,而B1_last_KeyNum == 1则是当按键松开后,B1_KeyNum会处于高电平,将这个值赋值给last,判断他是否符合。这就是对于这个判断部分的解释。

还有这个HAL_GPIO_ReadPin()这个函数,我们进行下列说明


/**
  * @brief  Read the specified input port pin.
  * @param  GPIOx where x can be (A..G) to select the GPIO peripheral for STM32G4xx family
  * @param  GPIO_Pin specifies the port bit to read.
  *         This parameter can be any combination of GPIO_PIN_x where x can be (0..15).
  * @retval The input port pin value.
  */
GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin)

2、第二种方法:

这种方法我认为是比较实用的,像他的调用起来,按键判断起来都是比较好用的,我是经常用这种方法的

uint8_t Key_Scan(void)
{
	uint8_t B1_KeyNum = HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0);
	uint8_t B2_KeyNum = HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_1);
	uint8_t B3_KeyNum = HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_2);
	uint8_t B4_KeyNum = HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0);
	
	//按键1
	if(B1_KeyNum == 0)
	{
		HAL_Delay(10); //进行一个延时消抖
		if(B1_KeyNum == 0)
		{
			while(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0) == 0); //等待按键抬起
			return 1;  //返回1
		}
	}
	//按键2
	else if(B2_KeyNum == 0)
	{
		HAL_Delay(10);
		if(B2_KeyNum == 0)
		{
			while(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_1) == 0);
			return 2;
		}
	}
	//按键3
	else if(B3_KeyNum == 0)
	{
		HAL_Delay(10);
		if(B3_KeyNum == 0)
		{
			while(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_2) == 0);
			return 3;
		}
	}
	//按键4
	else if(B4_KeyNum == 0)
	{
		HAL_Delay(10);
		if(B4_KeyNum == 0)
		{
			while(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0) == 0);
			return 4;
		}
	}
	return 0;//没有按键按下返回0
}

这个地方可能会有一个疑问,就是为什么while和if内部的内容性质一样,但是表现得不一样呢,这一点我们进行解释一下。这个地方,首先我们要清楚一点,while函数在这个地方起到一个什么作用,他起到的是一个判断按键是否抬起的作用,了解了这一点,我们就能很好的理解这个问题了,判断按键是否抬起是需要进行实时扫描的,但是if中只是判断他是否按的这个按键,这就是两者直接这么写的区别。

3、第三种方法:

我们前面了解的第二种方法,虽然我认为比较好,但是也有缺点,那就是他是一直去扫描这四个引脚端口的,这一点是比较占用单片机的工作效率的,用于实战项目中是比较浪费资源的,所以我们接下来介绍第三种方法,利用定时器进行定时按键扫描。那么,接下来我们配置一下定时器。

在这个地方,我们配置了一个周期为0.01s,其PSC为79HZ,(AAR+1)为10000的定时器。在这里,我们要明白为何要这样配置

这个地方,我们就是根据这个公式进行配置的,我们配置完成后,老样子,在main和while之间调用下列函数

    HAL_TIM_Base_Start_IT(&htim4);//初始化定时器中断

 当我们把这个初始化函数调用好后,那我们的第三种的代码就要写了

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
	if(htim->Instance == TIM4)
	{
		//按键1
		if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0) == GPIO_PIN_RESET)
		{
			//这个地方就据题目的要求来写要实现的功能
		}
		//按键2
		if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_1) == GPIO_PIN_RESET)
		{
			//这个地方就据题目的要求来写要实现的功能
		}
		//按键3
		if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_2) == GPIO_PIN_RESET)
		{
			//这个地方就据题目的要求来写要实现的功能
		}
		//按键4
		if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0) == GPIO_PIN_RESET)
		{
			//这个地方就据题目的要求来写要实现的功能
		}
	}
}

以上便是实现按键普通功能的三种方法,下面将介绍蓝桥杯嵌入式按键中最难的一部分,实现按键的长按和短按的功能。

三、按键的长按和短按功能的实现:

这一部分算是蓝桥杯嵌入式比赛里关于按键部分最难的一部分了,其实长按和短按无非就是时间的长短问题了,我们不妨利用定时器来确定一个周期,来定时的扫描检测一下按键,并规定一下长按和短按的时间,这样就会很好的实现两者的判断。这里要注意的是,有时在比赛中,他可能会涉及到双击和这两者的判断,所以我们也将双击加入其中。

因为上面部分我们介绍了定时器定时扫描按键的方法,就以上面为基础,继续让周期为0.01s。由于这个功能较于复杂,涉及的变量较多,我们就使用结构体变量的方式(不明白结构体变量的可以自行搜索),我就先列举下来

struct keys
{
	bool signed_flag;  //短按
	bool long_flag;    //长按
	bool double_flag;  //双击
	bool key_status;   //端口的电平状态
	uint8_t click_status;//按键按下是否稳定(消抖)
	int click_time;    //按键按下时长
	uint8_t double_satus;//判断双击与否
	int double_time;    //按键之间间隔
}Key[4];

在上面的结构体变量中,对于bool数据类型,我们需要调用stdbool.h才能使用布尔数据类型,这一点是要知道的,不然会报错。

在声明完变量后,我们就要正式的进行此功能的实现了,代码过于长,我尽量进行解读,要是不理解的话,那就最好背过。

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
	if(htim->Instance == TIM4)
	{
        //赋值按键状态,进行后续判断
		Key[0].key_status = HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0);
		Key[1].key_status = HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_1);
		Key[2].key_status = HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_2);
		Key[3].key_status = HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0);
        //因为是四个按键,要进行四次循环
		for(uint8_t i = 0;i < 4;i++)
		{
            //此处是判断按键是否稳定,即题目的要求是否消抖
			switch(Key[i].click_status)
			{
				case 0://状态0:第一次按下
					if(Key[i].key_status == GPIO_PIN_RESET)
					{
						Key[i].click_status = 1;//跳转状态1
					}
					break;
				case 1://状态1:电平已稳定
					if(Key[i].key_status == GPIO_PIN_RESET)
					{
						Key[i].click_status = 2;
						Key[i].click_time = 0; //计时器清零,准备调用
					}
					else
					{
						Key[i].click_status = 0;
					}
					break;
				case 2:
					//若B1按下,则计时器一直增加
					if(Key[i].key_status == GPIO_PIN_RESET)
					{
						Key[i].click_time ++;
					}
					//当端口电平状态为高电平时,且计数超过了某个值(根据题目而定)
					if(Key[i].key_status == GPIO_PIN_SET && Key[i].click_time >= 70)
					{
						Key[i].long_flag = 1;
						Key[i].click_status = 0;//重置状态
					}
					//剩下情况就要区分短按和双击的区别
					else if(Key[i].key_status == GPIO_PIN_SET && Key[i].click_time < 70)
					{
						switch(Key[i].double_satus)
						{
							case 0://状态0:第一次松开按键
								Key[i].double_satus = 1;
								Key[i].double_time = 0;
								break;
							case 1://状态1:第二次松开按键
								Key[i].double_flag = 1;
								Key[i].double_satus = 0;
								break;
						}
						Key[i].click_status = 0;
					}
					break;
			}
			if(Key[i].double_satus == 1)//状态1:第一次松开后未按下按键
			{
				Key[i].double_time ++;//若一直未按下第二次,则计数器一直计数
				if(Key[i].double_time >= 35)
				{
					Key[i].signed_flag = 1; //为短按
					Key[i].double_satus = 0;
				}
			}
		}
	}
}

以上便是此功能的实现,关于为何这么写,我已经尽量的在代码中添加注释了。之后我们来说明一下该如何使用它,例如,我们要求短按下B1按键之后使LD1点亮,如下

    if(Key[0].signed_flag == 1)
    {
        LED_On(1);
        Key[0].signed_flag = 0;//记住用完后一定要清零
    }

在这里,一定要用完后记住清零,这要有助于下次的使用,避免按键因此产生一些按下之后没有反应的事情发生。

四、结语:

以上便是对于蓝桥杯嵌入式比赛中,按键模块的介绍及讲解,其中我觉得按键的长短按是最重要的部分,如果你对于用定时器来实现按键扫描比较了解的话,这部分我相信是没有那么难以理解的。这一部分就结束了。

### 蓝桥杯嵌入式中的按键设计与实现 在蓝桥杯嵌入式中,按键的设计和实现是一个常见的考点。通常情况下,参者需要基于指定的硬件平台(如CT117E-M4开发板)完成按键的功能配置和逻辑处理[^3]。 #### 按键的工作原理 按键是一种常用的输入设,在嵌入式系统中用于触发特定事件或改变系统的运行状态。其基本工作原理是通过检测电平的变化来判断按键的状态。当按键按下时,对应的GPIO引脚会从高电平变为低电平(或者相反),从而触发中断或轮询机制下的状态变化检测[^1]。 #### GPIO配置 为了实现按键功能,首先需要对微控制器上的通用输入/输出端口(GPIO)进行初始化设置。以下是典型的GPIO配置过程: ```c // 配置GPIO引脚作为输入模式 void GPIO_Init(void) { RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 启用GPIOA时钟 GPIO_InitTypeDef GPIO_InitStruct; // 设置PA0为输入模式 GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0; // 选择引脚 GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING; // 浮动输入模式 GPIO_Init(GPIOA, &GPIO_InitStruct); } ``` 上述代码片段展示了如何将STM32的一个GPIO引脚配置为浮动输入模式,以便读取外部按键信号。 #### 中断方式实现按键检测 除了简单的轮询方法外,还可以利用外部中断的方式来提高效率并减少CPU占用率。以下是一个使用EXTI(External Interrupt Line)模块的例子: ```c // 初始化外部中断线 void EXTI_Init(void) { NVIC_InitTypeDef NVIC_InitStruct; EXTI_InitTypeDef EXTI_InitStruct; // 配置NVIC优先级组 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); // 配置EXTI线路0 EXTI_InitStruct.EXTI_Line = EXTI_Line0; // 使用EXTI线0 EXTI_InitStruct.EXTI_Mode = EXTI_Mode_Interrupt; // 中断模式 EXTI_InitStruct.EXTI_Trigger = EXTI_Trigger_Falling; // 下降沿触发 EXTI_InitStruct.EXTI_LineCmd = ENABLE; // 开启EXTI线 EXTI_Init(&EXTI_InitStruct); // 配置NVIC使能对应中断通道 NVIC_InitStruct.NVIC_IRQChannel = EXTI0_IRQn; // 对应EXTI0中断向量 NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 0x01; // 抢占优先级 NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0x01; // 子优先级 NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE; // 使能中断通道 NVIC_Init(&NVIC_InitStruct); } // 外部中断服务函数 void EXTI0_IRQHandler(void) { if (EXTI_GetITStatus(EXTI_Line0) != RESET) { // 判断是否有中断发生 // 执行按键响应操作 KeyPressedHandler(); // 自定义按键处理函数 EXTI_ClearITPendingBit(EXTI_Line0); // 清除中断标志位 } } ``` 此部分代码实现了对外部按键按压事件的实时捕获,并调用了`KeyPressedHandler()`函数来进行具体业务逻辑处理[^2]。 #### 去抖动处理 实际应用中,机械按键可能存在接触不稳定的情况,即所谓的“抖动”。这会导致多次误触的发生。因此,在编写按键控制程序时,必须加入去抖动措施。一种简单有效的方法是在检测到一次有效的高低电平转换之后延迟一段时间再重新确认当前状态是否仍然保持不变。 ```c #define DEBOUNCE_DELAY_MS 20U uint8_t IsKeyStable(uint8_t pinState) { uint32_t startTime = HAL_GetTick(); while ((HAL_GetTick() - startTime) < DEBOUNCE_DELAY_MS) { if (ReadPinState() != pinState) return FALSE; // 如果中途发生变化,则认为未稳定 } return TRUE; // 经过延时期间无变化则视为已稳定 } ``` 以上代码提供了一种基于时间戳的方式去除按键抖动的影响。 ---
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值