创作原因

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

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

正文部分

题目要求

解题思路

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

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

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

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

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

4、LCD显示完成,接下来实现ADC功能

5、ADC部分完成,现在写按键控制改变变量值

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}
};

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

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界面显示大框架

题目要求

在这里三个界面我们在之后以1、2、3界面代替

代码部分如下

void key_ctr(void)
{
	//key1控制界面切换
	if(key[0].single_flag==1)
	{
		LCD_Clear(Black);
		key[0].single_flag=0;
		LCD_sta++;
		if(LCD_sta>3)
		{
			LCD_sta=1;
		}
	}
}

void LCD_Disp(void)
{
	if(LCD_sta==1)			//产品参数界面
	{
		sprintf(text,"      GOODS ");
		LCD_DisplayStringLine(Line1,(uint8_t*)text);
		sprintf(text,"    R37:%.2lfV",r37_data);
		LCD_DisplayStringLine(Line3,(uint8_t*)text);
		sprintf(text,"    R38:%.2lfV",r38_data);
		LCD_DisplayStringLine(Line4,(uint8_t*)text);
	}
	if(LCD_sta==2)			//标准设置界面
	{
		sprintf(text,"     STANDARD");
		LCD_DisplayStringLine(Line1,(uint8_t*)text);
		sprintf(text,"   SR37:%.1lf-%.1lf",r37_low,r37_hig);
		LCD_DisplayStringLine(Line3,(uint8_t*)text);
		sprintf(text,"   SR38:%.1lf-%.1lf",r38_low,r38_hig);
		LCD_DisplayStringLine(Line4,(uint8_t*)text);
	}
	if(LCD_sta==3)			//合格率界面
	{
		sprintf(text,"       PASS");
		LCD_DisplayStringLine(Line1,(uint8_t*)text);
		sprintf(text,"    PR37:%.1lf%%",r37_pass);
		LCD_DisplayStringLine(Line3,(uint8_t*)text);
		sprintf(text,"    PR38:%.1lf%%",r38_pass);
		LCD_DisplayStringLine(Line4,(uint8_t*)text);
	}
}

这里有两个函数,一个是对按键操作进行处理的key_ctr函数

一个是LCD显示的LCD_Disp函数

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

代码逻辑解释

在key_ctr函数中,检测到按键按下之后就改变标志位LCD_sta,改标志位初始化为1,大于3后重置为1

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

需要注意的是,在标志位被按键改变的同时注意进行一个清屏操作,且由于之前写的按键检测函数中并没有重置判断按键按下标志位key[i].single_flag的操作,所以检测到按键被按下之后作一个判断按键按下标志位清零的操作

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

 第四步、ADC采集

CubeMx配置

选择相应的引脚,在引脚配置中配置为ADC输入,再在侧边Analog下打开相应的ADC采集通道

CubeMx配置完成 

ADC采集函数
double getADC(ADC_HandleTypeDef *pin)                //该函数输入只能是&hadc1和&hadc2
{
	uint adc;
	HAL_ADC_Start(pin);
	adc=HAL_ADC_GetValue(pin);
	return adc*3.3/4096;
}

在这里有一点需要注意地地方就是,如果想额外定义ADC的子文件,子文件名称不能是adc,因为该名称文件已经存在

关于最后给出的返回值,*3.3是因为电压是3.3,除4096是因为被分成了4096份,之所以是4096是因为该开发板用到的是一个12位的ADC,2的12次方是4096,故ADC的分辨率就是4096,也就是分成了4096份

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

题目要求

我总结了一下题目对于按键操作的反馈,在总结中三个界面用1、2、3依次表示

Key1:实现三个界面的切换,顺序从1->2->3->1
Key2:在1界面下,定义为R37检测按钮,在2界面下定义为选择按键,选择需要调整的上下限
Key3:在1界面下,定义为R38检测按钮,在2界面下定义为加按钮,每次加0.2
Key4:在3界面下,定义为清零按键,按下后清零当前得出的产品合格率,在2界面下定义为减按钮,每次减0.2

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

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

代码实现
void key_ctr(void)
{
	//key1控制界面切换
	if(key[0].single_flag==1)
	{
		LCD_Clear(Black);
		key[0].single_flag=0;
		LCD_sta++;
		if(LCD_sta>3)
		{
			LCD_sta=1;
		}
	}
	
	//注意若某一产品标准改变,对应产品的合格率清零;
    //当前界面下无功能的按键按下,不触发其它界面的功能;
	
	//在界面一情况下,key2控制检查r37,key3控制检查r38
	//在界面二情况下,key2控制选择数,key3控制对数进行加操作,key4控制对数进行减操作
	//在界面三情况下,key4控制结果清零
	//界面二选择顺序,37->38,上限->下限 
	if(key[1].single_flag==1)
	{
		key[1].single_flag=0;
		//界面一下功能,检查r37
		if(LCD_sta==1)
		{
			if((r37_data>=r37_low)&&(r37_data<=r37_hig))
			{
				nums[0].pass_Num+=1;
				r37pass_sta=1;
			}
			else
			{
				nums[0].error_Num+=1;
			}
		}
		
		//界面二下功能,选择数
		if(LCD_sta==2)
		{
			index_sta+=1;
			if(index_sta>4)
			{
				index_sta=1;
			}
		}
	}
	
	if(key[2].single_flag==1)
	{
		key[2].single_flag=0;
		//界面一功能,检查r38
		if(LCD_sta==1)
		{
			if((r38_data>=r38_low)&&(r38_data<=r38_hig))
			{
				nums[1].pass_Num+=1;
				r38pass_sta=1;
			}
			else
			{
				nums[1].error_Num+=1;
			}
		}
		
		//界面二功能,控制数加
		if(LCD_sta==2)
		{
			switch(index_sta)
			{
				case 1:
				{
					r37_hig+=0.2;
					if(r37_hig>3)
					{
						r37_hig=2.2;
					}
					memset(&nums[0],0,sizeof(struct num));
					break;
				}
				case 2:
				{
					r37_low+=0.2;
					if(r37_low>2)
					{
						r37_low=1.2;
					}
					memset(&nums[0],0,sizeof(struct num));
					break;
				}
				case 3:
				{
					r38_hig+=0.2;
					if(r38_hig>3.8)
					{
						r38_hig=3.0;
					}
					memset(&nums[1],0,sizeof(struct num));
					break;
				}
				case 4:
				{
					r38_low+=0.2;
					if(r38_low>2.2)
					{
						r38_low=1.4;
					}
					memset(&nums[1],0,sizeof(struct num));
					break;
				}
			}
		}
		
	}

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

第六步、内部算法实现

当完成了以上内容之后,大多数题目都会进入到内部算法实现的步骤,这次题目内部算法实现就只是计算一个合格率,相当简单

题目要求

实现思路 

看了要求之后就知道这部分很好实现,只需要知道总数以及通过数即可

我这里的处理方法是定义了一个结构体数组

struct num{
	uchar pass_Num;
	uchar error_Num;
};

struct num nums[2]=
{
	{0,0},
	{0,0}
};

第七步、LED显示

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

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

实现难点

根据题目的要求来看,LED实现部分的难点在于检测通过后的LED1、2点亮1s后熄灭

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

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

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

代码实现
//R37检测通过之后r37_pass置一

//界面一下功能,检查r37
if(LCD_sta==1)
{
	if((r37_data>=r37_low)&&(r37_data<=r37_hig))
	{
		nums[0].pass_Num+=1;
		r37pass_sta=1;
	}
	else
	{
		nums[0].error_Num+=1;
	}
}


//R38检测通过之后r38_pass置一

//界面一功能,检查r38
if(LCD_sta==1)
{
	if((r38_data>=r38_low)&&(r38_data<=r38_hig))
	{
		nums[1].pass_Num+=1;
		r38pass_sta=1;
	}
	else
	{
		nums[1].error_Num+=1;
	}
}

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

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

if(r37pass_sta==1)
{
	pass_time++;
	if(pass_time>100)
	{
		pass_time=0;
		r37pass_sta=0;		
	}
}
if(r38pass_sta==1)
{
	pass_time++;
	if(pass_time>100)
	{
		pass_time=0;
		r38pass_sta=0;
	}
}

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

10ms产生一次中断,当他执行到100次,pass_time变成101,这个时候进入判断语句,将pass_time及r37/38pass_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 Data_proc(void)
{
	uint number=0;
	char temp[30];
	if(strcmp(rxdata,"R37")==0)
	{
		number=nums[0].pass_Num+nums[0].error_Num;
		sprintf(temp,"R37:%d,%d,%.1lf%%\r\n",number,nums[0].pass_Num,r37_pass);
		HAL_UART_Transmit(&huart1,(uint8_t*)temp,strlen(temp),50);
	}
	if(strcmp(rxdata,"R38")==0)
	{
		number=nums[1].pass_Num+nums[1].error_Num;
		sprintf(temp,"R38:%d,%d,%.1lf%%\r\n",number,nums[1].pass_Num,r38_pass);
		HAL_UART_Transmit(&huart1,(uint8_t*)temp,strlen(temp),50);
	}
}

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

效果演示 

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

VID_20240310_143924

工程文件

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

结语

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

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

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐