【蓝桥杯嵌入式】第十二届蓝桥杯嵌入式省赛[模拟赛]程序设计题以及详细题解

原题展示

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

原题分析

本模拟试题还算比较简单的,需要用到的模块有USARTLEDLCD按键ADC五个部分,虽然总体上来说比较简单,但是其难点在于图4与图5计时说明图的转化,即我们应该明白计时始末的条件——当ADC数值在曲线图中处于上升沿中,当此时ADC数值等于Vmin时才启动;当ADC数值等于Vmin时终止,中间过程中可以多次触发计时起始值
这里还需要说明,由于STM32G431RBT6的串口2无法直接与PC连通收发数据,因此小编使用USART1替代USART2。
大家可以去产品手册上找找相关的信息,看能否找到解决方法。其实小编找过将串口1替换成串口2做这套试题的方法,但是最终没找到解决方法🤣🤣🤣,因此咱还是使用串口1吧!下面是产品手册上的提示:
在这里插入图片描述

原题解析

LED相关

通过查询产品手册知,LED的引脚为PC8~PC15,外加锁存器74HC573需要用到的引脚PD2。(由于题目要求除题目要求需要使用的LED外其他LED都处于熄灭状态,此处特意将所有的LED都初始化以便于管理其他的LED灯)
CubeMX配置:

代码样例
由于G431的所有LED都跟锁存器74HC573连接,因此每次更改LED状态时都需要先打开锁存器,写入数据后再关闭锁存器。

/*****************************************************
* 函数功能:改变所有LED的状态
* 函数参数:
*			char LEDSTATE: 0-表示关闭 1-表示打开
* 函数返回值:无
******************************************************/
void changeAllLedByStateNumber(char LEDSTATE)
{
	HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13|GPIO_PIN_14|GPIO_PIN_15|GPIO_PIN_8
					|GPIO_PIN_9|GPIO_PIN_10|GPIO_PIN_11|GPIO_PIN_12,(LEDSTATE==1?GPIO_PIN_RESET:GPIO_PIN_SET));
	//打开锁存器    准备写入数据
	HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, GPIO_PIN_SET);
	//关闭锁存器 锁存器的作用为 使得锁存器输出端的电平一直维持在一个固定的状态
	HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, GPIO_PIN_RESET);
}

/*****************************************************
* 函数功能:根据LED的位置打开或者是关闭LED
* 函数参数:
*			uint16_t LEDLOCATION:需要操作LED的位置
*			char LEDSTATE: 0-表示关闭 1-表示打开
* 函数返回值:无
******************************************************/
void changeLedStateByLocation(uint16_t LEDLOCATION,char LEDSTATE)
{
	HAL_GPIO_WritePin(GPIOC,LEDLOCATION,(LEDSTATE==1?GPIO_PIN_RESET:GPIO_PIN_SET));
	HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, GPIO_PIN_SET);
	HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, GPIO_PIN_RESET);
}

试题要求的LED显示其条件都比较单一,在满足点亮条件时直接点亮,否则,就直接熄灭即可,至于闪烁的周期控制,可以借助与sysTick中断实现,如果就因此再开一个定时器就有点浪费资源了。(虽然小编以前经常这样子干🤣🤣🤣)

小编写的LED工作逻辑函数:

/***************************************
* 函数功能:LED显示逻辑函数
* 函数参数:无
* 函数返回值:无
***************************************/
static void ledWork(void)
{
		// 触发计时功能 且计时未结束 LED点亮 
	changeLedStateByLocation(LED1,LEDFlag[0]);
	
	// 参数设置不正确就点亮LED2 直到其正确
	changeLedStateByLocation(LED2,LEDFlag[1]);
	
	// 串口一旦收到不正确或者是不满足要求的指令就点亮LED3
	changeLedStateByLocation(LED3,LEDFlag[2]);	
}

这里因为使用了一个变量来控制LED的状态,因此这里的LED显示函数显得格外简单。😂😂😂

LCD相关

样例代码
由于LCD的相关代码在官方给的比赛资源数据包中存在,因此,可以直接调用资源包中的.c、.h文件来完成LCD的相关初始化以及显示。这是一个简单的LCD初始化函数,其功能是将LCD显示屏初始化为一个背景色为黑色、字体颜色为白色的屏幕,具体代码如下:

/******************************************************************************
* 函数功能:LCD初始化
* 函数参数:无
* 函数返回值:无
*******************************************************************************/
void lcdInit(void)
{
	//HAL库的初始化
	LCD_Init();
	//设置LCD的背景色
	LCD_Clear(Black);
	//设置LCD字体颜色
	LCD_SetTextColor(White);
	//设置LCD字体的背景色
	LCD_SetBackColor(Black);
}

在显示时,可以借助于sprintf()函数将需要显示的数据格式成一个字符串,再在LCD上显示这个字符串。

	char temp[20];
	LCD_DisplayStringLine(Line1,(u8*) "       DATA    ");
	sprintf(temp,"   VR37:%4.2fV    ",r37);

为了操作LED与LCD显示方便,不让其相互干扰,小编这里对LCD进行了部分源码改写,使得每次LCD显示时不改变LED的显示状态,具体的方法各位可以点击查看【蓝桥杯】一文解决蓝桥杯嵌入式开发板(STM32G431RBT6)LCD与LED显示冲突问题,并讲述LCD翻转显示

下面附上小编完成模拟赛的LCD部分的详细代码:

/**
  * @Name    lcdDisplay
  * @brief   LCD显示数据
  * @param   char mod:显示模式 可以切换显示数据
  * @retval  None
  * @author  黑心萝卜三条杠
  * @Data    2023-04-02
 **/
static void lcdDisplay(void)
{
	char temp[20];
	// 数据显示界面
	if(mod == 0)
	{
		sprintf(temp,"      Data  ");
		LCD_DisplayStringLine(Line0,(u8*)temp);
		sprintf(temp," V:%.2fV      ",V);
		LCD_DisplayStringLine(Line2,(u8*)temp);
		sprintf(temp," T:%ds        ",T);
		LCD_DisplayStringLine(Line3,(u8*)temp);
	}
	// 参数显示界面
	else if(mod == 1)
	{
		sprintf(temp,"      Para  ");
		LCD_DisplayStringLine(Line0,(u8*)temp);
		sprintf(temp," Vmax:%.1fV      ",Vmax);
		LCD_DisplayStringLine(Line2,(u8*)temp);
		sprintf(temp," Vmin:%.1fV        ",Vmin);
		LCD_DisplayStringLine(Line3,(u8*)temp);
	}
}

(是不是非常简单粗暴。哈哈哈哈)

按键相关

    通过查询产品手册知,开发板上的四个按键引脚为PB0~PB2、PA0
CubeMX配置

样例代码
由于主板上的按键数量较少,因此小编这里的按键读取操作相对简单粗暴,其实现步骤为:

  • 步骤一:判断按键是否按下以及按键锁是否打开,在两者同时满足的情况下进入下一步;
  • 步骤二:关闭按键锁并且延时10ms,实现按键的延时消抖;
  • 步骤三:再次读取每个按键的值,判断按键按下的位置;
  • 步骤四:读取每个按键的状态,如果都处于松开状态就打开按键锁;

具体代码实现:

/*********************************************
 * 函数功能:按键扫描 含按键消抖 无长按短按设计
 * 函数参数:无
 * 函数返回值:按键的位置
 *            返回值说明:B1-1 B2-2 B3-3 B4-4
*********************************************/
unsigned char scanKey(void)
{
	//按键锁
	static unsigned char keyLock = 1;
    //记录按键消抖时间
    // static uint16_t keyCount = 0;

	//按键按下
    if((HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0) == RESET || HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_1) == RESET
      || HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_2) == RESET || HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0) == RESET) 
      && keyLock == 1){
        //给按键上锁 避免多次触发按键
        keyLock = 0;
        
        //按键消抖 这里最好不要使用延时函数进行消抖 会影响系统的实时性
        // if(++keyCount % 10 < 5) return 0;
        // if(HAL_GetTick()%15 < 10) return 0;
        HAL_Delay(10);

        //按键B1
        if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0) == RESET){
            return 1;
        }
        //按键B2
        if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_1) == RESET){
            return 2;
        }
        //按键B3
        if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_2) == RESET){
            return 3;
        }
        //按键B4
        if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0) == RESET){
            return 4;
        }
    }
    //按键松开
    if((HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0) == SET && HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_1) == SET
      && HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_2) == SET && HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0) == SET) 
      && keyLock == 0){
        //开锁
        keyLock = 1;
    }
    return 0;
}

调用上述函数后,即可判断每次按键按下的位置,之后的按键逻辑函数就相对简单了,大家一起来看看吧!

/***************************************
* 函数功能:按键逻辑函数
* 函数参数:无
* 函数返回值:无
***************************************/
static void keyPro(void)
{
	// 按键扫描
	unsigned char keyRising = scanKey();
	switch(keyRising)
	{
		// 按键B1
		case 1:
			mod ^= 1;
		  //即将进入数据界面 判断参数是否合理
			if(mod == 0  ) 
			{
				// 判断数据是否合理
				if(VmaxTemp >= VminTemp+1)
				{
					Vmax = VmaxTemp;
					Vmin = VminTemp;
					LEDFlag[1] = 0;
				}
				else
					LEDFlag[1] = 1;
			}
			break;
		// 按键B2
		case 2:
			if(mod == 1)
			{
				VmaxTemp += 0.1;
				if(VmaxTemp >= 3.4) VmaxTemp = 0;
			}
			break;
		// 按键B3
		case 4:
			if(mod == 1)
			{
				VminTemp += 0.1;
				if(VminTemp >= 3.4) VminTemp = 0;
			}
			break;
		default:
			break;
	}
}

ADC相关

CubeMX配置
ADC配置非常简单,大家一起来看看ADC的CubeMX配置方式吧!
在这里插入图片描述

样例代码
ADC获取数据时,为了获取ADC数据更加准确,小编采用连续读取10次数据然后取平均值作为本轮ADC数据采集的值!😉😉😉

/*******************************************************************
* 函数功能:获取ADC的值
* 函数参数:
* 			ADC_HandleTypeDef *hadc:ADC的通道值
* 函数返回值:
* 			double:转换后的ADC值
*******************************************************************/
double getADC(ADC_HandleTypeDef *hadc)
{
	unsigned int value = 0,i = 0;
	//开启转换ADC并且获取值
	HAL_ADC_Start(hadc);
	while(LL_ADC_IsActiveFlag_EOC(ADC2) == 0) ;
	// HAL_ADC_PollForConversion(hadc,50);
	for(i=0;i<10;++i)
		value += HAL_ADC_GetValue(hadc);
	
	//ADC值的转换 3.3V是电压 4096是ADC的精度为12位也就是2^12=4096
	return value/10*3.3/4096;
}

大家是不是对:获取到ADC值后的处理非常感兴趣,让我们大家一起来看看吧!!!🤤🤤🤤
下面的处理逻辑也相对简单:

首先,可以定义一个变量VoldData保存上一次ADC获取的值,另一变量V保存本次ADC的值,然后使用VoldData-V的值是否大于0.2,来确定是否处于上升沿,如果其值大于0.2,当然是处于上升沿。

同理,可以使用V - Vmin Vmax - V的值是否能够处于起始或者是终止状态。至于时间计数可以直接使用sysTick定时器的HAL_GetTick()函数来计数获取ADC“测量”时间。
(这里的0.2是小编故意留下的误差,如果是0,可能会多次处于起始或者是结束状态)

/* -------------------------------- begin  -------------------------------- */
/**
  * @Name    ADCHandle
  * @brief   ADC数据获取到后的处理函数
  * @param   None
  * @retval  None
  * @author  黑心萝卜三条杠
  * @Data    2023-03-24
 **/
/* -------------------------------- end -------------------------------- */
void ADCHandle(void)
{
	// ADC获取
	V = getADC(&hadc2);
	
	// 触发计时
	if(V - VoldData > 0.02 && V -  Vmin >= 0 && V -  Vmin <= 0.03 )
	{
		LEDFlag[0] = 1;
		T = 0;
	}
	// 结束计时
	else if(V - VoldData > 0.02 &&  Vmax -  V >= 0 && Vmax -  V <= 0.03)
		LEDFlag[0] = 0;
	// 保存上一次的历史数据
	VoldData = V;
}

至于计时,小编采用定时器完成,定时器中断中的处理demo如下:

  if(LEDFlag[0] == 1 && ++count%1010 >= 1000)
  {
	  ++T;
	  count = 0;
  }

串口相关

CubeMX配置
    配置时一定一定记得改引脚!!!在这里插入图片描述
样例代码
本程序中小编使用的是中断接收PC发送的数据其函数原型为:

// 函数原型:
HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
// 参数解析:
UART_HandleTypeDef *huart:串口通道;
uint8_t *pData:存放数据的buff;
uint16_t Size:一次接收数据的长度

在使用时还需要使用该函数“中断初始化”,否则不能够进入中断接收数据;

下面就是一个串口接收定长数据的demo:

/**********************************************串口相关************************************/

//定义一个串口信息的结构
uint8_t ucRxbuff[10];
uint8_t _ucRxbuff[1],lenBuff = 0;

/***使用HAL_UART_Receive_IT中断接收数据 每次接收完成数据后就会执行该函数***/
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{	
	if(huart->Instance == USART1){
		ucRxbuff[lenBuff++%10] = _ucRxbuff[0];
		// 重新使能中断		
		HAL_UART_Receive_IT(huart,(uint8_t *)&_ucRxbuff,sizeof(_ucRxbuff)); 
	}
}

本届试题要求的是定长数据,因此我们只要使用HAL_UART_Receive_IT(huart,(uint8_t *)&_ucRxbuff,sizeof(_ucRxbuff)); 触发中断即可,不需要改变串口接收数据的长度。

题中要求串口功能不仅仅是接收数据这么简单,其还需要能够解析串口接收的数据,并且以此为指令将合适的结果发送给PC。下面就是小编写的一个简单的数据处理demo:

/**
  * @Name    usartPro
  * @brief   串口处理逻辑函数
  * @param   None
  * @retval  None
  * @author  黑心萝卜三条杠
  * @Data    2023-04-02
 **/
static void usartPro(void)
{
	// 未收到数据
	if(strlen((char*)ucRxbuff) == 0) return ;
	// 收到数据并且判断其格式是否正确
	if(strlen((char*)ucRxbuff) == 9 && ucRxbuff[0] == '"' && '0' <= ucRxbuff[1] && ucRxbuff[1] <= '9' && ucRxbuff[2] == '.' && '0' <= ucRxbuff[3] && ucRxbuff[3] <= '9' \
		&& ucRxbuff[4] == ',' && '0' <= ucRxbuff[5] && ucRxbuff[5] <= '9' && ucRxbuff[6] == '.' && '0' <= ucRxbuff[7] && ucRxbuff[7] <= '9' && ucRxbuff[8] == '"')
	{
		VmaxTemp = ((ucRxbuff[1]-'0')*10 + ucRxbuff[3]-'0')*1.0/10;
		VminTemp = ((ucRxbuff[5]-'0')*10 + ucRxbuff[7]-'0')*1.0/10;
		if(VmaxTemp > 3.3 || VminTemp>3.3)
			LEDFlag[2] = 1;
		else
			LEDFlag[2] = 0;
	}
	else
		LEDFlag[2] = 1;
	
	memset(ucRxbuff,0,sizeof((char*)ucRxbuff));
	lenBuff = 0;
}

文章福利

下边是小编个人整理出来免费的蓝桥杯嵌入式福利,有需要的童鞋可以自取哟!🤤🤤🤤
省赛:

国赛:

其他:

这是小编自创的嵌入式交流群Q:726128226,欢迎各位大佬加入交流哟!😁😁😁

也欢迎大家留言或私信交流,共同进步哟!😉😉😉

  • 4
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值