当前位置: 首页 > news >正文

【蓝桥杯嵌入式】【模块】四、按键相关配置及代码模板

 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配置、单机、长按、双击。

相关文章:

  • (6)python开发经验
  • CSRF攻击 + 观测iframe加载时间利用时间响应差异侧信道攻击 -- reelfreaks DefCamp 2024
  • MySQL数据类型之VARCHAR和CHAR使用详解
  • 湖北理元理律师事务所:债务优化如何实现“减负不降质”?
  • fscanf 读取问题指南
  • 【递归、搜索和回溯】穷举vs暴搜vs深搜vs回溯vs剪枝
  • (独家)SAP CO模块中 销售发票对应的Cost Document中的PSG对象是什么东东??
  • 网站漏洞存在哪些危害,该如何做好预防?
  • SQL练习——day01
  • 告别碎片化!MCP 带来 AI Agent 开发生态的革命性突破
  • Makefile 详解
  • 电商热销榜的5种实现方案
  • 平替BioLegend品牌-Elabscience PE/Cyanine5.5标记CD11b抗体,高性价比解决方案!
  • MySQL 8.0 OCP 1Z0-908 题目解析(4)
  • 2025 OceanBase 开发者大会全议程指南
  • Console Importer浏览器插件的编译 及 制作成.crx浏览器插件的步骤
  • Trae - 国人Cursor的免费平替产品
  • Unity基础学习(十五)核心系统——音效系统
  • Scratch作品 | 3D原野漫游
  • 数据分析NumPy
  • 南昌上饶领导干部任前公示:2人拟提名为县(市、区)长候选人
  • 李成钢出席中国与《数字经济伙伴关系协定》成员部级会议
  • 南方降水频繁暴雨连连,北方高温再起或现40°C酷热天气
  • 人民日报:从“轻微免罚”看涉企执法方式转变
  • 金砖国家召开经贸联络组司局级特别会议,呼吁共同抵制单边主义和贸易保护主义
  • 白玉兰奖征片综述丨动画的IP生命力