【蓝桥杯嵌入式】【模块】四、按键相关配置及代码模板
1. 前言
最近在准备16届的蓝桥杯嵌入式赛道的国赛,打算出一个系列的博客,记录STM32G431RBT6这块比赛用板上所有模块可能涉及到的所有考点,如果有错误或者遗漏欢迎各位大佬斧正。
本系列博客会分为以下两大类:
1.1. 单独模块的讲解
在这部分,我会分享自己总结的各个模块的相关配置、代码书写模板,涉及到的大致框架如下:
这个框架后续可能会不断更新,欢迎各位给出建议。
这一大类相关的文章链接如下(持续补充中):
【蓝桥杯嵌入式】【模块】一、系统初始化-CSDN博客
【蓝桥杯嵌入式】【模块】二、LED相关配置及代码模板-CSDN博客
【蓝桥杯嵌入式】【模块】三、LCD相关配置及代码模板-CSDN博客
【蓝桥杯嵌入式】【模块】四、按键相关配置及代码模板-CSDN博客
1.2. 蓝桥杯各届的真题、模拟题复盘及个人答案
在这一部分,我会分享个人练过的所有题的复盘思路及代码,每篇文章结构如下:
这一大类相关的文章链接如下(持续补充中):
【蓝桥杯嵌入式】【复盘】第13届国赛真题-CSDN博客
以下是本篇博客正文内容:
2. 在cubemx中配置按键
在开发板说明书中,案件相关的io口如下所示:
图1
简单分析电路可知,当按键按下时,对应的io口电平会被拉低。
所以,按键功能的实现,归根结底是对gpio口电平状态的扫描,当检测到io口为低电平时,便说明其对应的按键按下了。
需要注意的是,为了保证系统运行的并发性,io状态扫描这一步一般都放在定时器中断里进行。所以,按键的实质可以总结为io读取电平+定时器中断内扫描。
2.1. cubemx配置按键相关io
图2
根据图1,按键1,2,3,4对应的gpio口分别是PB0、PB1、PB2、PA0,所以,在cubemx中讲这几个io配置为gpio input模式即可,用于读取gpio电平。
2.2. cubemx配置定时器中断
图3
图4
我选择了定时器4,需要注意的是配置定时器中断的溢出周期,这里我选择的是80000000 / 80 / 10000 = 100hz,也就是10ms进一次定时器中断,这个频率用于按键测量一般是没什么问题的。注意,最后要记得使能中断。
3. 按键的单击/短按
3.1. 整体模板
按键单机是考察得最为频繁也是必考的一个点,我的整体模板代码如下:
struct keys {uint8_t key_step;bool key_pin_state;bool key_is_down;
};struct keys key_buf[4] = {0};void key_init(void)
{HAL_TIM_Base_Start_IT(&htim4);
}void key_scan(void)
{key_buf[0].key_pin_state = HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0);key_buf[1].key_pin_state = HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1);key_buf[2].key_pin_state = HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_2);key_buf[3].key_pin_state = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0);for(int i = 0;i < 4;i++){switch(key_buf[i].key_step){case 0:if(key_buf[i].key_pin_state == 0){key_buf[i].key_step = 1;} else{key_buf[i].key_step = 0;}break;case 1:if(key_buf[i].key_pin_state == 0){key_buf[i].key_step = 2;key_buf[i].key_is_down = 1;} else{key_buf[i].key_step = 0;key_buf[i].key_is_down = 0;}break;case 2:if(key_buf[i].key_pin_state == 0){
// key_buf[i].key_step = 1;} else{key_buf[i].key_step = 0;key_buf[i].key_is_down = 0;}break;}}
}void key_task(void)
{for(int i = 0; i < 4;i++){if(key_buf[i].key_is_down == 1){key_buf[i].key_is_down = 0;}}
}void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{if(htim->Instance == TIM4){key_scan();}
}
以下是一些重要函数设计:
3.2. 按键的初始化
按键的初始化非常简单,只需要开启定时器的中断就行。
HAL_TIM_Base_Start_IT(&htim4);
使用该函数打开对应计时器的基本定时功能,使得TIM4没10ms就会触发一次中断,进入中断回调函数。
此外,在初始化工作中,还需要声明一个按键结构体:
struct keys {uint8_t key_step; // 用于标记处于状态机的哪个阶段bool key_pin_state; // 用于标记按键对应io口的电平bool key_is_down; // 用于标记按键是否被按下
};struct keys key_buf[4] = {0}; // 有四个按键,所以声明的结构体数组大小为4
这个结构体会用于之后的按键判断及消抖状态机。
3.3. 按键的扫描及消抖
这部分的核心函数是key_scan,在该函数中大致做了以下几步:
1. 读取四个按键各自io口的引脚电平。
2. 构造状态机,实现按键的判定及消抖。
首先,读取引脚电平,通过HAL_GPIO_ReadPin函数便可实现:
key_buf[0].key_pin_state = HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0);key_buf[1].key_pin_state = HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1);key_buf[2].key_pin_state = HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_2);key_buf[3].key_pin_state = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0);
接着,便是最为重要的步骤,基于一个三步骤的状态机,实现按键的状态判断及消抖:
for(int i = 0;i < 4;i++){switch(key_buf[i].key_step){case 0:if(key_buf[i].key_pin_state == 0){key_buf[i].key_step = 1;} else{key_buf[i].key_step = 0;}break;case 1:if(key_buf[i].key_pin_state == 0){key_buf[i].key_step = 2;key_buf[i].key_is_down = 1;} else{key_buf[i].key_step = 0;key_buf[i].key_is_down = 0;}break;case 2:if(key_buf[i].key_pin_state == 0){
// key_buf[i].key_step = 1;} else{key_buf[i].key_step = 0;key_buf[i].key_is_down = 0;}break;}}
在一个4次的循环中,我们检测每一个按键对应的io口状态,如果为0,说明此时按键是按下了的,但是我们并不选择在此时便判定按键的状态为“按下”,而是让其进入状态机的第二层,在第二层中会再次判断一遍io口的状态,如果依旧为0,便可确认按键确实是按下了,将按键的状态更新为“按下”(key_buf[i].key_is_down = 1;)。
这个过程实现了消抖,因为按键在按下时可能会有一定的抖动,导致其电平并不稳定,采用双层状态机的设计,便可在抖动发生后进行第二步的判断,如果判定为抖动,则第二次测量的电平不会为0,此时便回到状态机的第一步,实现消抖。
那么为什么状态机还有第三层呢?实际上,这个第三层一般用来进行长按判断,后续会提到。
之后,将该key_scan函数放入定时器中断中,之后便可以没10ms运行一次该函数,使得按键状态的判断并不影响其他逻辑的进行:
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{if(htim->Instance == TIM4){key_scan();}
}
3.4. 按键业务函数
在进行代码编写时,我们一定要有一种异步处理的思想,即判断逻辑在中断中进行,只负责一些标志位的改变,复杂的业务逻辑放在主循环中进行,因此,设计一个key_task任务,将对应按键按下后的处理逻辑放在里面进行:
void key_task(void)
{for(int i = 0; i < 4;i++){if(key_buf[i].key_is_down == 1){key_buf[i].key_is_down = 0;// 根据题目编写该按键对应的业务逻辑}}
}
4. 按键的长按
与之前的代码相比,按键长按的基本框架不变,核心模板代码如下:
struct keys {uint8_t key_step;bool key_pin_state;bool key_is_down;uint32_t key_time;bool key_is_long;
};void key_scan(void)
{key_buf[0].key_pin_state = HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0);key_buf[1].key_pin_state = HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1);key_buf[2].key_pin_state = HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_2);key_buf[3].key_pin_state = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0);for(int i = 0;i < 4;i++){switch(key_buf[i].key_step){case 0:if(key_buf[i].key_pin_state == 0){key_buf[i].key_step = 1;} else{key_buf[i].key_step = 0;key_buf[i].key_is_long = 0;key_buf[i].key_time = 0;}break;case 1:if(key_buf[i].key_pin_state == 0){key_buf[i].key_step = 2;key_buf[i].key_is_down = 1;} else{key_buf[i].key_step = 0;key_buf[i].key_is_down = 0;key_buf[i].key_is_long = 0;key_buf[i].key_time = 0;}break;case 2:if(key_buf[i].key_pin_state == 0){
// key_buf[i].key_step = 1;key_buf[i].key_time++;if(key_buf[i].key_time >= 100){key_buf[i].key_is_long = 1;}} else{key_buf[i].key_step = 0;key_buf[i].key_is_down = 0;key_buf[i].key_is_long = 0;key_buf[i].key_time = 0;}break;}}
}
主要变动如下:
1. 在按键结构体中新增key_time用于计数按键按下的时间,用key_is_long用于标记长按标志位
2. 在状态机的第三层中,如果案件长按,则计数时间会累加,当时间超过题目要求的时间时(模板上是100 * 10ms = 1000ms = 1s),长按标志位便会置1,之后在业务函数中便可根据该标志位做判断。
这里的核心是key_time,实际在考试时,所运用的整体模板可能不同,具体见下面的易错点。
4.1. 易错点
在考试时,常常遇到的要求是按键长按后松开时,才触发对应的长按逻辑,此时模板代码中关于计数时间和计数标志的清0便可能发生改变,如下面的例子所示:
void key_scan(void)
{key_buffer[0].key_pin_state = HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0);key_buffer[1].key_pin_state = HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1);key_buffer[2].key_pin_state = HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_2);key_buffer[3].key_pin_state = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0);for(uint8_t i = 0; i < 4; i++){switch(key_buffer[i].key_step){case 0:if(key_buffer[i].key_pin_state == 0){key_buffer[i].key_step = 1;} else{key_buffer[i].key_step = 0;}break;case 1:if(key_buffer[i].key_pin_state == 0){key_buffer[i].key_step = 2;key_buffer[i].key_is_down = 1;} else{key_buffer[i].key_step = 0;key_buffer[i].key_is_down = 0;}break;case 2:if(key_buffer[i].key_pin_state == 0){key_buffer[i].key_time++;if(key_buffer[i].key_time >= 100){key_buffer[i].key_is_long = 1;}} else{key_buffer[i].key_step = 0;key_buffer[i].key_is_down = 0;}break;}}
}void key_task(void)
{for(uint8_t i = 0;i < 4;i++){if(key_buffer[i].key_is_down == 1){switch(i){case 0:// 按键1的业务逻辑break;case 1:// 按键2的业务逻辑 break;case 2:// 按键3的业务逻辑 break;case 3:if(show_num == 0){// 使用key4_flag 来标记按键4是否被按下key4_flag = 1;break;}}if(key_buffer[3].key_is_down == 0){if(key4_flag == 1){key4_flag = 0;if(key_buffer[3].key_is_long == 1){// 长按逻辑}else{// 短按逻辑}}// 相关标志位清0key_buffer[3].key_is_long = 0;key_buffer[3].key_time = 0;}}
}
上面是13届国赛的按键核心代码,可以发现的是,其与之前的模板代码的核心区别是将key_time和key_is_long的清零逻辑放到了key_task中进行, 并且使用了一个key4_flag标志位,这个标志位执行这样的作用:当按键4按下之后,该标志位会置1,但此时不进行任何的逻辑,之后等按键4松开之后,会判断该标志位,如果为1,说明是按下后松开,此时再判断长按标志位,实现对应的功能;如果此时key4_flag本身为0,说明此时按键4只是单纯的没有被按下,直接跳过即可。
5. 按键的双击
按键双击定义为在0.7s内连续按下按键两次,所以我的思路是在按键第一次按下之后开始计时,若700ms内按键再次按下,为双击;否则为单击。
我采用的模板代码如下:
void key_task(void)
{for(int i = 0; i < 4;i++){if(key_buf[i].key_is_down == 1){key_buf[i].key_is_down = 0;switch(i){case 0:key1_flag++;if(key1_flag == 2){printf("key1 double click\n");}break;}}}
}uint32_t key1_cnt = 0;
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{if(htim->Instance == TIM4){key_scan();if(key1_flag >= 1){key1_cnt++;if(key1_cnt >= 70){key1_cnt = 0;key1_flag--;}}}
}
使用key1_flag标志位,当按键1按下时,其值增加,同时在定时器中断中,会判断计数器,计数器每超过700ms都会让key1_flag减1,只有当0.7s内连续按下两次按键1,才能触发双击,printf打印日志。
总结
本文主要介绍了按键相关操作的模板代码,主要是按键的cubemx配置、单机、长按、双击。