按键是蓝桥杯比赛中的常驻嘉宾了,他和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;//记住用完后一定要清零
}
在这里,一定要用完后记住清零,这要有助于下次的使用,避免按键因此产生一些按下之后没有反应的事情发生。
四、结语:
以上便是对于蓝桥杯嵌入式比赛中,按键模块的介绍及讲解,其中我觉得按键的长短按是最重要的部分,如果你对于用定时器来实现按键扫描比较了解的话,这部分我相信是没有那么难以理解的。这一部分就结束了。