第十三届蓝桥杯嵌入式省赛真题演练——密码锁

题目分析

在这里插入图片描述
接下来站在博主的视角来完成这次模拟题。首先,通读题目后我大致能大致能获取到如下信息:要完成一个密码锁的项目,用户通过串口通讯来设置三位密码,使用按键来输入密码。那我的思路先根据硬件框图搭出程序的大致框架,比如他这里的框图包括LED指示灯、LCD显示、串口通讯、按键输入功能,至于控制信号输出我我不知道所指,但第一感觉是输出PWM信号,那就可以先不管,等后面完善题目具体要求的时候看到了再补充就行。
开始搭建框架,打开Cube根据手册配置相应的GPIO。新建一个工程,搜索选择所用的STM32G431RB芯片→在RCC中打开时钟,然后配置时钟树→工程文件设置(文件名等)→以硬件框图模块依次配置GPIO口,打开串口1→生成文件
在这里插入图片描述

在这里插入图片描述
在生成的工程文件目录下,新建一个文件夹命名为bsp,在其中新建各模块的.c和.h文件。
在这里插入图片描述
在工程中添加一个文件夹bsp并将上面的.c文件添加进文件夹。在各模块.c文件中包含其.h文件,在.h文件中把国际框架写好,编译。接下来依次写各模块的代码,依次编译下载验证。
首先key.c,在cube中配置TIM4(前面备赛的经验),

#include "key.h"

struct KEY_STA key_sta[4] = {0};		//定义四个按键


void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) //定时器溢出中断回调函数
{
	if(htim->Instance == TIM4)		//判断是不是定时器4导致的中断
	{
		key_sta[0].status = HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0);		//先将按键GPIO的电平读出
		key_sta[1].status = HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_1);
		key_sta[2].status = HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_2);
		key_sta[3].status = HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0);
	}
	
	for(uint8_t i = 0;i < 4;i++)		//分三个步骤来消抖
	{
		switch(key_sta[i].step)
		{
			case 0:
			{
				if(key_sta[i].status == 0)		//若检测到电平为低
					key_sta[i].step ++;				//进入状态二
			}
				break;
			
			case 1:
			{
				if(key_sta[i].status == 0)			//若到了下次定时器中断电平仍未低
				{
					key_sta[i].flag = 1;			//则断定是按键按下
					key_sta[i].step ++;				//进入状态三
				}	
				else
					key_sta[i].step = 0 ;		//若不是按键按下则回到状态一
			}
				break;
			
			case 2:
			{
				if(key_sta[i].status == 1)		//当按键松开后
					key_sta[i].step = 0 ;			//回到状态一
			}
				break;
		}
	}
}

#ifndef _KEY_H
#define _KEY_H
#include "main.h"
#include <stdbool.h>			//结构体定义的有布尔数据类型,需要包含这个头文件

struct KEY_STA			//定义一个储存按键状态的结构体
{
	bool flag;			//按键按下标志
	uint8_t status;			//电平值
	uint8_t step;			//状态
	
};


#endif

led.c文件,用一个函数来指定对应led亮灭

#include "led.h"

void led_dis(uint8_t num)		//定义一个选中led亮灯的函数,num为8位
{
	HAL_GPIO_WritePin(GPIOC,GPIO_PIN_All,GPIO_PIN_SET);		//拉高全部引脚,让led都熄灭
	HAL_GPIO_WritePin(GPIOC,num << 8,GPIO_PIN_RESET);			//因为led是PC8-15也就是高八位有效,所以先让num左移八位再拉低
	HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_SET);		//锁存
	HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_RESET);			
	
}
#ifndef _LED_H
#define _LED_H
#include "main.h"

void led_dis(uint8_t num);

#endif

有了按键和led模块,就可以结合起来验证一下实物是不是按照我们的想法实现的。先在main.c中包含所有模块的.h文件,发现警告没有找到文件(我打开了实时检测),要在魔术棒里把我们的文件所在路径添加到include path下。将

		if(key_sta[0].flag == 1)
		{
			led_dis(0x80);
			key_sta[0].flag = 0;
		}

加入while/* USER CODE BEGIN 3 */下,这里遇到key_sta的报错,因为这是在key.c中定义的结构体数组,要想在main.c中使用,需要在/* USER CODE BEGIN PTD */下加入extern struct KEY_STA key_sta[4];。编译无误,下载,显示NO LINK,需要在魔术棒的debug中选择。
在这里插入图片描述
再次下载,发现按下B1后LD8没有亮。发现在主函数中没有开启定时器中断,在/* USER CODE BEGIN 2 */下加入HAL_TIM_Base_Start_IT(&htim4);。编译下载,发现8个led上电全亮,在cube中将PC8-15设置为高电平后得以解决。
接下来写LCD显示模块。在比赛时给的数据包里会有LCD的例程,我们可以直接调用封装好的函数。首先将数据包里关于LCD的模块函数lcd.c/lcd.h/fonts.h复制到bsp文件夹中,并在工程文件中添加。打开其中LCD的工程文件,阅读代码查漏补缺,将初始化代码LCD_Init();加入main.c中。在/* USER CODE BEGIN WHILE */下加入 LCD_Clear(Black); LCD_SetBackColor(Black); LCD_SetTextColor(White);来初始化屏幕界面。按题目所给要求,需要显示两个界面以区分输入密码前后,具体可见代码注释。

#include "dis_pro.h"

uint8_t view = 0;			//定义一个变量来控制界面的切换
uint8_t temp[30] = {0}; 

void dis_pro()
{
	if(view == 0)			//通过按键来切换
	{
		sprintf((char *)temp,"       P S D");			//将待显示数据放入数组
		LCD_DisplayStringLine(Line2,temp);					//显示数组中的数据
		sprintf((char *)temp,"    B 1 :");
		LCD_DisplayStringLine(Line4,temp);	
		sprintf((char *)temp,"    B 2 :");
		LCD_DisplayStringLine(Line5,temp);	
		sprintf((char *)temp,"    B 3 :");
		LCD_DisplayStringLine(Line6,temp);			
	}
	
	if(view == 1)
	{
		sprintf((char *)temp,"       S T A");
		LCD_DisplayStringLine(Line2,temp);
		sprintf((char *)temp,"    F :");
		LCD_DisplayStringLine(Line4,temp);	
		sprintf((char *)temp,"    D :");
	}
}

#ifndef _DIS_PRO_H
#define _DIS_PRO_H

#include "stdio.h"
#include "main.h"
#include "lcd.h"

void dis_pro(void);

#endif

当然,如果有报错说某某函数或变量没有定义,那就在相应的.h文件中添加相关声明或即可。当然在key.c中还定义了一个按键功能函数

void key_pro()
{
	if(key_sta[3].flag == 1)
	{
		view ++;
		if(view > 1)
			view = 0;
		LCD_Clear(Black);
		key_sta[3].flag = 0;
		
	}
}

下载验证,结果可观。
接下来写串口通信模块。

#include "uart.h"

uint8_t dat = 0;		//创建变量来接收单个数据
uint8_t data[30] = {0};				//创建数组来储存接收的数据
uint8_t pointer = 0;					//创建一个光标来指示当前所在数组的位置
	
uint8_t a[2] = {'1'};				//接收串口设置的密码的第一位
uint8_t b[2] = {'2'};			//接收串口设置的密码的第二位
uint8_t c[2] = {'3'};				//接收串口设置的密码的第三位
uint8_t d[5] = {0};

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)		//串口接收中断回调函数
{
	data[pointer++] = dat;			//将当前接收到的单个数据存入数组
	HAL_UART_Receive_IT(huart,&dat,1);			//开启串口接收中断继续接收
	
}


void uart_pro()  //定义一个函数实现串口的具体功能
{
	if(pointer > 0)
	{
		if(pointer == 7)			//若接收到七个数据,就把数据分放便于后续比较
		{
			sscanf((char*)data,"%3s-%1s%1s%1s",d,a,b,c); //将data这个数组中的数据按照后面定义的格式分别放入dabc四个数组中
		}
		else
		{
			char text[30];
			sprintf(text,"ERROR");
			HAL_UART_Transmit(&huart1,(uint8_t *)text,strlen(text),50);//若未接收完则向用户发送一个erro
		}
		pointer = 0;		//光标回首位
		memset(data,0,30);			//数组归零
	}
}

#ifndef _UART_H
#define _UART_H

#include "main.h"
#include "stdio.h"
#include "usart.h"
#include "string.h"
void uart_pro(void);
#endif

前面分析的基本模块现已基本实现,接下来仔细去看题目的要求,对各模块的具体功能要求。
首先,按键输入@,0-9几个数来模拟输入密码的过程,与串口设置的密码进行比对,B4按下时若密码输入正确,则切换界面。初始状态下三位密码显示皆为@。主要需要修改的就是按键模块。

void key_pro()
{
	if(key_sta[0].flag == 1)			//B1按键
	{
		if(n_1[0] == '@')				//根据ASCII码表先将字符@改为0
			n_1[0] -= 17;
		n_1[0]++;					//按键自增
		if(n_1[0]>57)				//超过9后变回@,@-0-9
			n_1[0] = '@';
		key_sta[0].flag = 0;			//标志位归零
	}
	
		if(key_sta[1].flag == 1)		//B2
	{
		if(n_2[0] == '@')
			n_2[0] -= 17;
		n_2[0]++;
		if(n_2[0]>57)
			n_2[0] = '@';
		key_sta[1].flag = 0;
	}
	
		if(key_sta[2].flag == 1)			//B3
	{
		if(n_3[0] == '@')
			n_3[0] -= 17;
		n_3[0]++;
		if(n_3[0]>57)
			n_3[0] = '@';
		key_sta[2].flag = 0;
	}
		
	
	if(key_sta[3].flag == 1)		//B4
	{
		if((a[0] == n_1[0]) && (b[0] == n_2[0]) && (c[0] == n_3[0]))			//判断密码是否输入正确
		{
			view ++;				//切换界面
			if(view > 1)
				view = 0;
			LCD_Clear(Black);
			key_sta[3].flag = 0;
		}
		
	}
}

下载发现初始密码可以解锁,但无法通过按键进行修改。问题出在写串口的时候,没有在main.c中开启串口接收中断,在/* USER CODE BEGIN 2 */ 下加入HAL_UART_Receive_IT(&huart1,&dat,1);开启即可。验证无误。
题目要求,输入密码成功后要有信号输出,前面看不懂的那个模块用处就在于此。使用PA1完成脉冲输出功能,没输入正确密码前PA1输出的是1KHz的方波信号,密码正确则变为2KHz 占空比为10%的方波信号。持续五秒后,又变回原来的信号以及密码输入界面。
那我们去cube中先配置PA1为TIM_CH2。因为初始为1KHz,分频80,重装载1000。而因为我们要在LCD上显示频率和占空比,计算频率可以随便用上升沿或下降沿,捕获到的相当于PWM波一个周期的时间,用80000000(时钟频率)/80(分频系数)/time(捕获到的时间)=频率。而占空比则是利用下降沿的时间除以整个周期时间,则为占空比。所以可以配置一个输入捕获(如PB4),利用其中两个通道来分别捕获上升沿和下降沿的时间来计算频率值。
在这里插入图片描述
在这里插入图片描述

再次生成代码并打开工程文件。在key.c中写入输入捕获中断回调函数。

void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)		//输入捕获中断回调函数
{
		if(htim->Instance == TIM3)
	{
		time_1 = HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_1);		//读出计时值
		time_2 = HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_2);
		__HAL_TIM_SetCounter(htim,0);			//计时值归零
		frq_1 = (80000000/80)/time_1;		//计算当前频率
		zk = (time_2/time_1)*100;				//计算占空比(下降沿时间除以上升沿时间)
		HAL_TIM_IC_Start_IT(htim,TIM_CHANNEL_1);			//打开输入捕获定时器
		HAL_TIM_IC_Start_IT(htim,TIM_CHANNEL_2);	
	}
}

有了之前的教训,不要忘记在主函数中分别开启PWM信号和输入捕获中断。这里先不去管具体的功能,将frq_1和zk放入dis_pro.c中显示,想测试一下这个信号输出模块是否能成功。果不其然,代码逻辑是1000hz的频率,但显示出来就是233这个数,占空比为0。结果问题出在对变量的定义上,这里的代码没有给出变量定义,但文末我会附上整个工程文件。我写代码有个问题是在变量的定义上,不会去细想,这个数据的本质,只一心想着这个代码逻辑,问就是uint8_t类型,也不会报错,但出了问题也确实给我整嘛了。这里,time应该是double类型,而频率应该是uint16_t类型。
根据题目要求,密码输入正确后,频率会从4k变为2k且占空比为10%。在key_pro()函数中增改如下代码

	if(key_sta[3].flag == 1)			//B4
	{
		if((a[0] == n_1[0]) && (b[0] == n_2[0]) && (c[0] == n_3[0]))			//判断密码是否输入正确
		{
			LCD_Clear(Black);				//输入正确则清屏切换界面
			view ++;
			__HAL_TIM_SET_AUTORELOAD(&htim2,530);		//设置频率为2k
			__HAL_TIM_SET_COMPARE(&htim2,TIM_CHANNEL_2,50);			//设置占空比
			HAL_Delay(5000);
			__HAL_TIM_SET_AUTORELOAD(&htim2,999);			//恢复频率为1k

			if(view > 1)
				view = 0;
			key_sta[3].flag = 0;
		}
		
	}

编译无误,下载验证。再有,题目有信号输出精度要求,但是我按照这样的思路写出来他的频率已经超出精度范围了。我猜测是因为tim3开了两个通道的的缘故,因为一开始我没写占空比的测量,也就只开了一个捕获上升沿的通道,频率精度是正常的。这里可以调高函数中的参数值(如上面的代码__HAL_TIM_SET_AUTORELOAD的参数我设置为了530)。
最后,LED功能部分。题目要求密码验证成功,LED1点亮五秒后熄灭,而如果暑促密码>=3次,LED2以100毫秒的间隔闪烁,五秒后熄灭。在key_pro()函数中增改如下代码

	if(key_sta[3].flag == 1)			//B4
	{
		if((a[0] == n_1[0]) && (b[0] == n_2[0]) && (c[0] == n_3[0]))			//判断密码是否输入正确
		{
			LCD_Clear(Black);				//输入正确则清屏切换界面
			view ++;
			__HAL_TIM_SET_AUTORELOAD(&htim2,530);		//设置频率为2k
			__HAL_TIM_SET_COMPARE(&htim2,TIM_CHANNEL_2,50);			//设置占空比
			led_dis(0x01);			//LED1亮5s
			HAL_Delay(5000);
			__HAL_TIM_SET_AUTORELOAD(&htim2,999);			//恢复频率为1k
			led_dis(0x00);
			
			if(view > 1)
				view = 0;

		}
		else
		{
			wrong ++;			//错误次数
			if(wrong >= 3)
			{
				for(uint8_t i = 0; i < 50; i++)			//闪烁五秒
				{
					led_dis(0x02);
					HAL_Delay(100);
					led_dis(0x00);
					HAL_Delay(100);
				}
			}
		}
		key_sta[3].flag = 0;
	}

下载后的现象是,输入密码正确后会黑屏五秒,led会亮。问题就出在HAL_Delay()这个函数中,他利用的原理就是让程序一直卡在一个while循环中等待,导致其他的操作执行不了。具体可以参照这个视频的讲解。这里的修改也是参考了视频所述的代码逻辑,代码如下:

	if(key_sta[3].flag == 1)			//B4
	{
		if((a[0] == n_1[0]) && (b[0] == n_2[0]) && (c[0] == n_3[0]))			//判断密码是否输入正确
		{
				wrong = 0;
				__HAL_TIM_SET_AUTORELOAD(&htim2,530);		//设置频率为2k
				__HAL_TIM_SET_COMPARE(&htim2,TIM_CHANNEL_2,50);			//设置占空比
				led_dis(0x01);			//LED1亮5s
				view ++;
				LCD_Clear(Black);				//输入正确则清屏切换界面
				uwTick_pwm = uwTick;
				while(uwTick - uwTick_pwm < 5000)//定义了一个变量,uwTick属于配置文件中定义好的变量,每隔1ms增加1
				{
					dis_pro();			//一开始uwTick_pwm为0,所以这样就相当于延时了5s
				}
				__HAL_TIM_SET_AUTORELOAD(&htim2,999);
				led_dis(0x00);
				__HAL_TIM_SET_COMPARE(&htim2,TIM_CHANNEL_2,0);
				LCD_Clear(Black);				//输入正确则清屏切换界面
				view ++;
				if(view > 1)
				view = 0;
				n_1[0] = '@';
				n_2[0] = '@';
				n_3[0] = '@';
				key_sta[3].flag = 0;	
		}
		else
		{
			wrong ++;			//错误次数
			if(wrong >= 3)
			{
				for(uint8_t i = 0; i < 25; i++)			//闪烁五秒
				{
					led_dis(0x02);
					HAL_Delay(100);
					led_dis(0x00);
					HAL_Delay(100);
				}
			}
			key_sta[3].flag = 0;
		}

这样的操作解决了黑屏问题,但是在延时5s回到密码输入界面时,频率应改回1k,经测验频率不太对。这里我是试错来设置参数值调出的1k和50%的占空比,一直找不到问题在哪里,欢迎小伙伴来一起讨论。
全部文件附上:
链接:https://pan.baidu.com/s/1MFJNhFBoXwUAyG4MwiPW0A?pwd=olm6
提取码:olm6

  • 9
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值