蓝桥杯十三届省赛(全CSDN最详细!!!)

创作原因

本人大二在读,目前备赛蓝桥杯中,出于巩固自己所学知识以及为寻找解题思路的同胞们提供参考的目的,写下了这篇博客

在这篇博客中我会一步一步完善代码,最后给出完整工程文件

也许有的朋友会发现我这篇文章的大框架和上一次写蓝桥杯文章的大框架相似,这是为了让第一次看文章的朋友有更好的阅读体验也是为了偷懒(手动狗头)

正文部分

题目要求

解题思路

在我们写蓝桥杯嵌入式的题目的时候,我们应该明确一个适合自己的大致流程:先写什么再写什么

接下来为各位奉上我这次写题目时候的思路

1、先完成LED和KEY函数部分

2、当LED和KEY部分配置及函数都写完之后开始配置LCD

3、当LCD也配置完之后开始写LCD显示操作,先把大框架全部搭好,具体的变量处理之类的之后再处理

4、LCD显示完成,接下来实现按键控制密码选择及密码确认

5、按键控制密码选择及确认部分完成,接下来实现脉冲输出频率和占空比设置

6、按键进行操作全部完毕,接下来是内部算法,就只是一个判断密码是否相等

7、算法完毕,接下来做LED

8、LED显示部分完毕,接下来做串口

大概解释一些我思路是这样的原因吧,首先LED和KEY和LCD是所有模块里面最基本且一定会用到的模块

在我的代码中为LED部分和KEY部分都专门写了处理的函数,而之所以把他们放在LCD之前是因为LED和LCD存在引脚复用问题,我需要先处理好LED及与其相关的锁存器才能让整个工程的LED和LCD不存在问题

配置完LED、KEY、LCD之后就随缘了,根据题目的具体情况进行分析,总而言之就是先完成简单的功能,然后再一步步实现复杂的功能

解题过程

第一步、LED、KEY配置及函数部分

LED
CubeMx配置

在CubeMx中把以上与LED相关引脚设置为Output即可(记得带上锁存器PD2)

控制函数

介绍代码之前,我想先说明一些基本原理方便大家理解代码

从LED原理图可以看出,LED的连接方式为共阳极连接,而我们想要点亮一个LED就必须形成一个电位差,所以既然是共阳极连接,点亮方式就是低电平点亮

关于锁存器,该开发板(STM32G431RBT6)是高电平打开,低电平关闭,且控制的是LED引脚

uchar,这是一个我typedef的变量类型,实际为unsigned char

void LED_Proc(uchar LED_Bit)            //这个变量传入特别妙
{
    HAL_GPIO_WritePin(GPIOC,GPIO_PIN_All,GPIO_PIN_SET);
    HAL_GPIO_WritePin(GPIOC,LED_Bit<<8,GPIO_PIN_RESET);
    HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_SET);
    HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_RESET);
}

首先我们传入一个八位数据,关于为什么这个数据是八位,大有讲究

由于一个寄存器GPIOC有十六位,而LED的引脚刚好在后八位,所以我们只需要传入一个八位数据再进行位移操作即可实现功能,是不是很完美

在代码中我先关闭了所有LED,这样做的效果相当于做一个初始化

之后再传入整个寄存器的位值,根据位分布来操作每个位,位为0赋低电平,为1赋高电平

接着开启寄存器并关闭寄存器,之后开启寄存器之后,才能改变LED引脚的状态

LED控制函数完成

KEY
CubeMx配置

KEY相关引脚配置为Input,之后选择模式为上拉输入

然后选择一个通用定时器(高级定时器也可以),将周期配置为10ms,开启中断

CubeMx配置完成

控制函数

由于有四个按键,每个按键的判断都需要用到相同数量的变量,所以直接建立一个结构体来存放数据,之后再创建一个结构体数组来解决变量需求

struct key{
    bool key_sta;
    uchar key_judge;
    bool single_flag;
}

struct key keys[4]={
    {0,0,0},
    {0,0,0},
    {0,0,0},
    {0,0,0}
};

这里定义了一个有三个变量的结构体,这三个变量分别用于

key_sta:存储按键I/O口状态

key_judge:用于函数运行过程中进程判断

single_flag:判断按键是否被按下

准备工作完成,接下来放控制部分代码

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
	if(htim->Instance==TIM4)    //以后按键中断就用定时器四给,写完所有的模块之后发现定时器四没有其他功能需要用到
	{
		//读取按键I/O状态
		key[0].key_sta=HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0);
		key[1].key_sta=HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_1);
		key[2].key_sta=HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_2);
		key[3].key_sta=HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0);
		
		for(int i=0;i<4;i++)
		{
			switch(key[i].key_judge)
			{
				case 0:
				{
					if(key[i].key_sta==0)
					{
						key[i].key_judge=1;
					}
					break;
				}
				case 1:     //此时判断按下
				{
					if(key[i].key_sta==0)
					{
						key[i].key_judge=2;
					}
					else
					{
						key[i].key_judge=0;
					}
					break;
				}
				case 2:
				{
					if(key[i].key_sta==1)    //此时判断松开
					{
						key[i].single_flag=1;
						key[i].key_judge=0;
					}
					break;
					
				}
				
			}
			
		}
}

说一下代码思路

首先按键写在了中断回调函数之中,在CubeMx中我们开启了定时器4(通用定时器),周期为10ms,所以在中断回调函数之中来自定时器四的中断就是10ms扫描一次

需要两次判断按键持续处于低电平之后key_judge=2之后才判断按下,这样的目的是做一个防误触操作

判断按下的同时将判断标志位置零,按键判断按下的标志位在这里不作置零,在使用部分才作置零操作

按键部分的代码我也是从其他地方借鉴过来的,我只能说这个按键写的相当的好

第二步、配置LCD

文件导入

要使用LCD要导入官方提供的文件:lcd.h、lcd.c、fonts.h

直接复制后添加就可以

CubeMx配置

对应引脚配置成Output就可以,注意部分引脚在LED配置的时候就已经配置过了

LCD显示 需要操作

首先需要在初始化部分写初始化函数,官方提供的lcd.c文件中提供了几乎所有你需要用到的函数

LCD_Init();

 在初始化完成之后,我们要初始化LCD屏幕的界面

看一下题目要求

LCD_Clear(Black);
LCD_SetBackColor(Black);
LCD_SetTextColor(White);	

根据题目要求我们初始化LCD屏幕的操作就是

清空界面为黑色(题目要求背景色为黑色)

设置背景颜色为黑色

设置文本颜色为白色

LCD打印需要操作
sprintf(text,"      GOODS ");
LCD_DisplayStringLine(Line1,(uint8_t*)text);

首先使用sprintf写入一个字符串到字符数组里,然后对数组做一个强制类型转换就可以打印出来了

第三步、LCD界面显示大框架

题目要求

特殊要求如下

关于第一个界面,定义了一个有三个元素的一维数组,用来存储密码的值;定义了一个有三个元素的一维数组来存储标志位数据(关于”@“显示)

关于第二个界面,并没有定义任何变量用来传参,因为通过仔细观察可以发现其实第二个界面的值都是固定的,所以直接打印就可以,不需要定义变量 

代码部分如下

//关于改变LCD显示标志位的部分函数
if(key[3].single_flag==1)
{
	//按下确认之后无论结果都必须要做一个清空处理
	key[3].single_flag=0;
	LCD_Key_sta[0]=0;
	LCD_Key_sta[1]=0;
	LCD_Key_sta[2]=0;
			
	//判断是否相等及相关操作
	Data_Proc();
	passw[0]=0;
	passw[1]=0;
	passw[2]=0;
}
//数据处理函数
void Data_Proc(void)
{
	sprintf(enter_pas,"%d%d%d",passw[0],passw[1],passw[2]);
	if(strcmp(enter_pas,pas)==0)
	{
		LCD_sta=1;
		error_num=0;
		LCD_Clear(Black);
		__HAL_TIM_SetAutoreload(&htim2,5000-1);
		__HAL_TIM_SetCompare(&htim2,TIM_CHANNEL_2,500);
        HAL_TIM_GenerateEvent(&htim2, TIM_EVENTSOURCE_UPDATE);
	}
	if(strcmp(enter_pas,pas)!=0)
	{
		error_num++;
	}
	
}


//LCD显示函数
void LCD_Disp(void)
{
	if(LCD_sta==0)
	{
		sprintf(text,"       PSD ");
		LCD_DisplayStringLine(Line1,(uint8_t*)text);
		if(LCD_Key_sta[0]==0)
		{
			sprintf(text,"    B1:@    ");
			LCD_DisplayStringLine(Line3,(uint8_t*)text);
		}
		if(LCD_Key_sta[0]==1)
		{
			sprintf(text,"    B1:%d",passw[0]);
			LCD_DisplayStringLine(Line3,(uint8_t*)text);
		}
		if(LCD_Key_sta[1]==0)
		{
			sprintf(text,"    B2:@      ");
			LCD_DisplayStringLine(Line4,(uint8_t*)text);
		}
		if(LCD_Key_sta[1]==1)
		{
			sprintf(text,"    B2:%d",passw[1]);
			LCD_DisplayStringLine(Line4,(uint8_t*)text);
		}
		if(LCD_Key_sta[2]==0)
		{
			sprintf(text,"    B3:@       ");
			LCD_DisplayStringLine(Line5,(uint8_t*)text);
		}
		if(LCD_Key_sta[2]==1)
		{
			sprintf(text,"    B3:%d",passw[2]);
			LCD_DisplayStringLine(Line5,(uint8_t*)text);
		}
		//频率1K,占空比50%
		__HAL_TIM_SetAutoreload(&htim2,10000-1);
		__HAL_TIM_SetCompare(&htim2,TIM_CHANNEL_2,5000);
        HAL_TIM_GenerateEvent(&htim2, TIM_EVENTSOURCE_UPDATE);
		
	}
	if(LCD_sta==1)
	{
		sprintf(text,"       STA ");
		LCD_DisplayStringLine(Line1,(uint8_t*)text);
		sprintf(text,"    F:%dHz",freq_data);
		LCD_DisplayStringLine(Line3,(uint8_t*)text);
		sprintf(text,"    D:10%%");
		LCD_DisplayStringLine(Line4,(uint8_t*)text);
	}
}

这里放了三个函数,一个是处于按键部分的判断key4按下的函数,一个是判断密码是否正确的数据处理函数,再就是LCD的显示函数

在LCD的显示函数中,为了识别特殊字符的显示,我用了很多的条件判断语句,这样看起来有点像屎山代码

由于我所有的变量定义都遵循一定的命名规则,所以很容易看懂,这里我就不放出变量定义部分以及变量说明

代码逻辑解释

在LCD_Disp中根据LCD_sta的不同进入不同的条件判断语句进而显示不同的页面

需要注意的是,在标志位被按键改变的同时一般都会进行一个清屏操作,但是在这里我只在LCD_sta改变成1这里作了一个清屏操作,也就是从界面1转换到界面2的时候做了一个清屏操作,但是从界面二转换为界面一时作的清屏操作有点麻烦

由于题目要求,界面二只显示5s,于是我用中断来计时,所以我将LCD_sta改变成0的操作在中断回调函数里进行,而在中断回调函数里是无法调用清屏函数的,因为清屏函数的时序过长,而中断无法调用时序过长的操作

所以这里就直接用了一个不太好的方法来实现清屏操作,那就是使用空格来刷新屏幕的显示,所以可以看到我在界面一的显示后面敲了一些空格,目的就在于屏幕刷新

在这里我们所有需要显示的变量都先放上来,具体变量的值先不管,只管把变量形式正确地打印出来

 第四步、脉冲频率输出及占空比改变

题目要求

CubeMx配置

题目要求的时PA1输出PWM,我们在引脚PA1选择相应的定时器通道,即TIM2_CH2

接下来,打开定时器

以”以定时器通道二生成PWM“打开通道二

然后根据公式以及题目要求设置 

公式如下

 参考公式:
(定时)频率计算公式:定时工作频率=外部总线频率/(PSC+1)
                    定时频率=定时工作频率/counter(计数周期)+1
 占空比计算公式:占空比(%)=100xpulse(设置的值)/(计数周期+1)

CubeMx配置完成 

相关函数
__HAL_TIM_SetAutoreload(&htim17,9999);       //设置重转载值
HAL_TIM_GenerateEvent(&htim17, TIM_EVENTSOURCE_UPDATE);           //更新定时器
__HAL_TIM_GetAutoreload(&htim17);           //读取重装载值
__HAL_TIM_GET_COUNTER(&htim);                //读取定时器的pulse,用于读取占空比

之所以这里与频率相关的参数只有重装载值是因为,只通过改变重装载值就可以改变频率

代码实现
//处于界面一时的频率和占空比设置
freq_data=10000000/(__HAL_TIM_GetAutoreload(&htim2)+1);
HAL_TIM_GenerateEvent(&htim2, TIM_EVENTSOURCE_UPDATE);

//处于界面二的频率和占空比设置
//频率1K,占空比50%
__HAL_TIM_SetAutoreload(&htim2,10000-1);
__HAL_TIM_SetCompare(&htim2,TIM_CHANNEL_2,5000);
		

在这里,第一段代码放在了LCD_sta=0下LCD显示函数的最后

第二段代码放在了按键把LCD_sta从零变成1的操作之后

第五步、按键操作导致的改变

题目要求

我总结了一下题目对于按键操作的反馈,前三个按键控制密码选择,第四个按键控制密码验证

Key1、2、3:分别实现密码位1、2、3的密码选择
选择顺序:@->0->1->2->3->4->……->9->0->1->2
Key4:验证密码是否正确,若正确,转到界面二,不正确,刷新密码数据

这次的题目里面关于对按键的反馈还有一个需要注意的点就是,按键改变什么时候到达临界条件,往往一个不小心就会忽略

所有值的初始显示也要认真观察

代码实现
void key_Proc(void)
{
	//前三个按键功能类似
	if(key[0].single_flag==1)
	{
		key[0].single_flag=0;
		LCD_Key_sta[0]=1;
		passw[0]++;
		if(passw[0]==10)
		{
			passw[0]=0;
		}
	}
	if(key[1].single_flag==1)
	{
		key[1].single_flag=0;
		LCD_Key_sta[1]=1;
		passw[1]++;
		if(passw[1]==10)
		{
			passw[1]=0;
		}
	}
	if(key[2].single_flag==1)
	{
		key[2].single_flag=0;
		LCD_Key_sta[2]=1;
		passw[2]++;
		if(passw[2]==10)
		{
			passw[2]=0;
		}
	}
	
	//关于确认部分,我还没想好怎么写,先把其他地方写好
	//密码正确之后,屏幕作一个刷新
	if(key[3].single_flag==1)
	{
		
		//按下确认之后无论结果都必须要做一个清空处理
		key[3].single_flag=0;
		LCD_Key_sta[0]=0;
		LCD_Key_sta[1]=0;
		LCD_Key_sta[2]=0;
		
		
		//判断是否相等及相关操作
		Data_Proc();
		passw[0]=0;
		passw[1]=0;
		passw[2]=0;
	}
}

按键部分代码没有太多好说的,理清思路,注意细节,就可以了(其实是细节太多不想说了)

第六步、内部算法实现

当完成了以上内容之后,大多数题目都会进入到内部算法实现的步骤,这次题目内部算法实现就只是判断密码是否相等,相当简单

实现代码

看了要求之后就知道这部分很好实现

void Data_Proc(void)
{
	sprintf(enter_pas,"%d%d%d",passw[0],passw[1],passw[2]);
	if(strcmp(enter_pas,pas)==0)
	{
		LCD_sta=1;
		error_num=0;
		LCD_Clear(Black);
		__HAL_TIM_SetAutoreload(&htim2,5000-1);
		__HAL_TIM_SetCompare(&htim2,TIM_CHANNEL_2,500);
	}
	if(strcmp(enter_pas,pas)!=0)
	{
		error_num++;
	}
	freq_data=10000000/(__HAL_TIM_GetAutoreload(&htim2)+1);
	HAL_TIM_GenerateEvent(&htim2, TIM_EVENTSOURCE_UPDATE);
	
}

我在这里首先把三个密码整合在一起,然后再用strcmp函数判断是否相等,如果相等就证明密码正确,再进行下一步操作 

第七步、LED显示

不知道大家有没有遇到过这样的问题:在多个地方对LED进行操作,不知道为什么总是得不到自己想要的效果

我就经常遇到这样的问题,后来我想的方法是定义标志位,单独写一个函数根据标志位的情况来点亮LED

实现思路

LED的规定时间点亮我们要怎么写呢?用延时函数吗,显然是不适合的,一用延时函数,整个代码就都卡在这里,能不用延时函数就别用延时函数

既然不用延时函数,那我们怎么解决这个延时问题?我想到的是——中断

还记得之前在按键部分初始化的定时器四中断吗,我们就利用这个定时器中断就可以了,这个定时器中断被 设置为10ms触发一次,想要1s只需要触发一百次定时器四中断即可

代码实现

关于LED1的亮灭,我和LCD界面使用了同一个标志位,因为它们的生存域完全相同

关于LED2的亮灭,由于题目要求错误三次之后才开始闪烁状态,所以在按键部分,如果密码验证失败,一个我定义的变量就会有加操作,当验证成功后该操作置零

//error_num即为LED2的标志位
void Data_Proc(void)
{
	sprintf(enter_pas,"%d%d%d",passw[0],passw[1],passw[2]);
	if(strcmp(enter_pas,pas)==0)
	{
		LCD_sta=1;
		error_num=0;
		LCD_Clear(Black);
		__HAL_TIM_SetAutoreload(&htim2,5000-1);
		__HAL_TIM_SetCompare(&htim2,TIM_CHANNEL_2,500);
        HAL_TIM_GenerateEvent(&htim2, TIM_EVENTSOURCE_UPDATE);
	}
	if(strcmp(enter_pas,pas)!=0)
	{
		error_num++;
	}
	
	
}

这段代码是写在之前的按键操作导致改变的部分里的,我在这里再拿出来方便大家理解

接下来就是在中断函数中对该标志位进行改变操作

if(LCD_sta==1)
{
	time_sta++;
	if(time_sta>500)
	{
		time_sta=0;
		LCD_sta=0;
	}
}
if(error_num>=3)
{
	time_sta2++;
	if(time_sta2>10)
	{
		led_sta=!led_sta;
		time_sta2=0;
	}
}

要注意我这段代码是写在中断回调函数之中判断中断为定时器四产生的中断的里面

10ms产生一次中断,当他执行到500次,time_sta变成501,这个时候进入判断语句,将time_sta及LCD_sta置零

当进入关于LED2判断之后,执行10次led_sta取反

既然难点处理完了,我就把显示代码放上来了

void LED_Disp(void)
{
	//0000 0000
	//0000 0100     4
	//0000 1000     8
	//0001 0000     10
	if(LCD_sta==1)
	{
		LED_Proc(0x04);
		if((r37pass_sta==1)&&(r38pass_sta==0))
		{
			LED_Proc(0x05);
		}
		if((r37pass_sta==0)&&(r38pass_sta==1))
		{
			LED_Proc(0x06);
		}
		if((r37pass_sta==1)&&(r38pass_sta==1))
		{
			LED_Proc(0x07);
		}
	}
	if(LCD_sta==2)
	{
		LED_Proc(0x08);
	}
	if(LCD_sta==3)
	{
		LED_Proc(0x10);
	}
	
}

第八步、串口收发

CubeMx配置

点击Connection(通信)选择usart1,选择异步模式(Asynchronous),根据题目设置好波特率,这个题目的要求是9600

设置完成

串口接收代码
char rxdata[30];
uint8_t rxdat;
uchar rx_pointer;
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *hurat)
{
	rxdata[rx_pointer]=rxdat;
	rx_pointer++;
	HAL_UART_Receive_IT(&huart1,&rxdat,1);
}

在串口中断回调函数中写下接收的操作,记住接收代码每次只能接一个数据

串口发送代码

串口发送代码相当简单,只需要一行代码就可以实现

HAL_UART_Transmit(&huart1,(uint8_t*)temp,strlen(temp),50);
题目效果实现代码
void usart_Proc(void)
{
	if(rx_pointer==7)
	{
		if(rxdata[3]=='-')
		{
			sscanf(rxdata,"%3s-%3s",usart_pas,new_pas);
			if(strcmp(usart_pas,pas)==0)
			{
				strcpy(pas,new_pas);
			}
		}
		
	}
	rx_pointer=0;
	memset(rxdata,0,sizeof(rxdata));
}

//在处理之前为了防止接收不完整,我在循环里写了这样一段代码
if(rx_pointer!=0)                       //防止接收不完整
{
	int temp=rx_pointer;
	HAL_Delay(10);
    if(temp==rx_pointer)
	{
	    rx_pointer=0;
		Data_proc();            //处理串口数据的函数
	}
}

在这里我首先判断接受的数据格式是否有问题,判断没问题之后再进入循环 

当判断完了之后,将串口指针位和数据接收数组全部恢复初始状态

效果演示 

所有部分已经完成,接下来上演示效果

VID_20240316_190100

工程文件

链接放在这里了,有需要的拿阿里云盘分享

由于阿里云盘不能上传.zip文件,所以我上传之前利用了一个软件把这个文件伪装了,下载之后可以通过这篇教程获取原来文件【已解决】阿里云盘压缩包无法分享压缩文件分享限制_aliyunpansharer-CSDN博客

结语

在之后到蓝桥杯开始的一段时间内,我会陆续更新蓝桥杯真题的文章,大家可以关注一下以防错过

创作不易,大家点点赞收收藏鼓励一下我吧

CSDN蓝桥杯单片第十三届是面向单片机爱好者和从事嵌入式开发的参赛者的一项比赛。本次比赛由CSDN蓝桥杯国赛联合主办,旨在挖掘和推广优秀的单片机程序设计人才,促进单片机应用技术的发展。 参赛者需要通过线下选拔赛和线上网络赛的多轮比拼,最终脱颖而出。比赛的题目涉及单片机应用开发的各个方面,包括硬件设计、软件编程和电路调试等内容。参赛者需要在有限的时间内解决一系列与单片机相关的实际问题,并编写出高效、可靠的程序。 本届比赛的题目难度适中,考察了参赛者的综合能力和技术水平。比赛中,选手们需要应用单片机进行各类任务,如控制电机、读取传感器数据、控制显示屏等。他们需要具备扎实的单片机知识、熟练的编程技巧,以及创新思维和团队合作能力。 通过参与蓝桥杯单片第十三届比赛,参赛者将获得更深入地了解和应用单片机技术的机会。他们将学习到更多实践经验,提升自己的技术能力和解决问题的能力。此外,比赛还为他们提供了与业内专家和其他参赛者交流的平台,有助于拓宽视野和提高专业素养。 总而言之,CSDN蓝桥杯单片第十三届比赛是一项重要的单片机程序设计竞赛,为单片机爱好者提供了锻炼和展示自己的机会。它不仅挖掘和培养了优秀的单片机程序设计人才,也推动了单片机技术的发展与创新。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值