题目与概要

 本文为蓝桥杯十七届嵌入式省赛真题解析,因为4T上评测估计要明年才开启,因此本文涉及的代码与得分项可能会有误差与错误,代码仅作参考用,如有其他错误请谅解、

CUBEMX配置如下

一般会开启一个定时器用于整体计时或者中断任务处理

外设模板代码与逻辑处理

定义的结构体  我的定义变量的方法是不太好的,例如文章中的十七届真题,逻辑复杂且多需要定义的变量就非常多,会导致写到后面忘记前面的变量,所以这种定义结构体的方法慎用。

#ifndef _FUN_H_
#define _FUN_H_
#include "config.h"
void LCD_Proc(void);
void Key_Scan(void);
void Sys_Init(void);
void IC_Proc(void);
void Interrupt(void);
void ADC_Proc(void);
void PWM_Proc(void);
void LED_Proc(uint16_t LED,uint16_t Mode);
void LED_Test(void);
typedef struct{
	uint16_t page;
	char text[30];
	uint16_t LCD_ClearFlag;
	uint16_t Select_Tar;
	uint16_t Select_Para;
	//PAGE1
	uint16_t Mode;
	float Pre_Realtime;
	float Tra_Ins;
	float Tra_Sum;
	float Pa1_Duty;
	float V_Offset;
	
	//PAGE2
	uint16_t TarD_Duty;
	float TarP_Pre;
	
	//PAGE3
	float FH_Para;
	float FL_Para;
	float PL_Para;
	uint16_t DH_Para;
	
	//SYS
	uint32_t Runtime_IC;
	uint32_t Runtime_Auto;
	uint32_t Runtime_Man;
	uint32_t Runtime_KEY1;
	uint32_t Runtime_KEY2;
	uint32_t Runtime_LED2;
	uint32_t Runtime_LED3;
	
	//KEY
	uint16_t Key_State[4];
	uint16_t Key_Last_State[4];
	uint16_t KEY_Flag;
	//ADC
	uint32_t DMA_ADC_Buff[30];
	float ADC_Value;
	
	//IC
	uint16_t TIM16_FRE;
	float Pa6_Duty;
	uint16_t Pa6_Fre;
	
	//PWM
	uint16_t Auto_Flag;
	uint16_t Man_Flag;
	
	//LED
	uint16_t LED_Flag1;
	uint16_t LED_Flag2;
}Pro;
extern Pro pro;
#endif

LCD

void LCD_Proc(void){
			if(pro.LCD_ClearFlag){
				pro.LCD_ClearFlag=0;
				LCD_Clear(Black);
			}
	switch(pro.page){
		case 0:				    //12345678901234567890
			sprintf(pro.text,"       MAIN         ");
		    LCD_DisplayStringLine(Line1,(uint8_t*)pro.text);
			sprintf(pro.text,"   M=%s             ",pro.Mode?"AUTO":"MAN");
		    LCD_DisplayStringLine(Line3,(uint8_t*)pro.text);
			sprintf(pro.text,"   P=%.1fBAR        ",pro.Pre_Realtime);
		    LCD_DisplayStringLine(Line4,(uint8_t*)pro.text);
			sprintf(pro.text,"   F=%.1fL/M        ",pro.Tra_Ins);
		    LCD_DisplayStringLine(Line5,(uint8_t*)pro.text);
			sprintf(pro.text,"   Q=%.0fL          ",pro.Tra_Sum);
		    LCD_DisplayStringLine(Line6,(uint8_t*)pro.text);
			sprintf(pro.text,"   D=%.0f%%         ",pro.Pa1_Duty);
		    LCD_DisplayStringLine(Line7,(uint8_t*)pro.text);
			sprintf(pro.text,"   V=%.1fV          ",pro.V_Offset);
		    LCD_DisplayStringLine(Line8,(uint8_t*)pro.text);
			break;
		case 1:						//12345678901234567890
			sprintf(pro.text,"       OUTP         ");
		    LCD_DisplayStringLine(Line1,(uint8_t*)pro.text);
			sprintf(pro.text,"   TarD=%d%%        ",pro.TarD_Duty);
		    LCD_DisplayStringLine(Line3,(uint8_t*)pro.text);
			sprintf(pro.text,"   TarP=%.1fBAR     ",pro.TarP_Pre/10);
		    LCD_DisplayStringLine(Line4,(uint8_t*)pro.text);
			break;
		case 2:	          //12345678901234567890
			sprintf(pro.text,"       PARA         ");
		    LCD_DisplayStringLine(Line1,(uint8_t*)pro.text);
			sprintf(pro.text,"   FH=%.1fL/M       ",pro.FH_Para);
		    LCD_DisplayStringLine(Line3,(uint8_t*)pro.text);
			sprintf(pro.text,"   FL=%.1fL/M       ",pro.FL_Para);
		    LCD_DisplayStringLine(Line4,(uint8_t*)pro.text);
			sprintf(pro.text,"   PL=%.1fBAR       ",pro.PL_Para/10);
		    LCD_DisplayStringLine(Line5,(uint8_t*)pro.text);
			sprintf(pro.text,"   DH=%d%%          ",pro.DH_Para);
		    LCD_DisplayStringLine(Line6,(uint8_t*)pro.text);
			break;
	}
}

LCD代码模板有很多这里不过多赘述

按键 

void Key_Scan(void){
	pro.Key_State[0]=HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0);
	pro.Key_State[1]=HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_1);
	pro.Key_State[2]=HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_2);
	pro.Key_State[3]=HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0);
	if(pro.Key_State[0]==0 && pro.Key_Last_State[0]==1){
		pro.page++;
		if(pro.page>2){
			pro.page=0;
		}
		pro.Select_Tar=0;
		pro.Select_Para=0;
		pro.LCD_ClearFlag=1;
	}
	if(pro.Key_State[1]==0 && pro.Key_Last_State[1]==1){
		if(pro.page==0){
			pro.Mode=!pro.Mode;
		}
		else if(pro.page==1){
			pro.Select_Tar=!pro.Select_Tar;
		}
		else if(pro.page==2){
			pro.Select_Para++;
			if(pro.Select_Para>3){
				pro.Select_Para=0;
			}
		}
	}
	if(pro.Key_State[2]==0 && pro.Key_Last_State[2]==1){
		if(pro.page==0){
			pro.KEY_Flag=1;
		}
		else if(pro.page==1){
			if(pro.Select_Tar==0){
				pro.TarD_Duty+=5;
				if(pro.TarD_Duty>95){
					pro.TarD_Duty=95;
				}
			}
			else{
				pro.TarP_Pre+=5;
				if(pro.TarP_Pre>95){
					pro.TarP_Pre=95;
				}
			}
		}
		else if(pro.page==2){
			switch(pro.Select_Para){
				case 0:
					pro.FH_Para+=1;
				  if(pro.FH_Para>40){
						pro.FH_Para=40;
					}
					break;
				case 1:
					pro.FL_Para+=1;
				  if(pro.FL_Para>40){
						pro.FL_Para=40;
					}
					break;
				case 2:
					pro.PL_Para+=5;
					if(pro.PL_Para>95){
						pro.PL_Para=95;
					}
					break;
				case 3:
					pro.DH_Para+=10;
					if(pro.DH_Para>95){
						pro.DH_Para=95;
					}
					break;
			}
		}
	}
	else if(pro.Key_State[2]==1 && pro.Key_Last_State[2]==0){
		if(pro.page==0 && pro.Runtime_KEY1>=2000){
			pro.V_Offset=pro.ADC_Value;
		}
		pro.KEY_Flag=0;
		pro.Runtime_KEY1=0;
	}
	if(pro.Key_State[3]==0 && pro.Key_Last_State[3]==1){
		if(pro.page==0){
			pro.KEY_Flag=2;
		}
		else if(pro.page==1){
			if(pro.Select_Tar==0){
				pro.TarD_Duty-=5;
				if(pro.TarD_Duty<5){
					pro.TarD_Duty=5;
				}
			}
			else{
				pro.TarP_Pre-=5;
				if(pro.TarP_Pre<5){
					pro.TarP_Pre=5;
				}
			}
		}
		else if(pro.page==2){
			switch(pro.Select_Para){
				case 0:
					pro.FH_Para-=1;
				  if(pro.FH_Para<4){
						pro.FH_Para=4;
					}
					break;
				case 1:
					pro.FL_Para-=1;
				  if(pro.FL_Para<4){
						pro.FL_Para=4;
					}
					break;
				case 2:
					pro.PL_Para-=5;
					if(pro.PL_Para<10){
						pro.PL_Para=10;
					}
					break;
				case 3:
					pro.DH_Para-=10;
					if(pro.DH_Para<5){
						pro.DH_Para=5;
					}
					break;
			}
		}
	}
	else if(pro.Key_State[3]==1 && pro.Key_Last_State[3]==0){
		if(pro.page==0 && pro.Runtime_KEY2>=2000){
			pro.Tra_Sum=0;
		}
		pro.KEY_Flag=0;
		pro.Runtime_KEY2=0;
	}
	for(int i=0;i<4;i++){
		pro.Key_Last_State[i]=pro.Key_State[i];
	}
	
}

我更喜欢这个软件消抖的代码模板,用法都是差不多的。

就是用到了pro.Key_State[1]==0 && pro.Key_Last_State[1]==1 通过短暂延迟进行按键判断后改变当前状态。

具体的逻辑处理

这个功能其实挺简单其实就是动态变化斜率,在坐标轴上取两点分别是(3.3,10) (V_offset,0)

实时计算 y=kx+b 斜率与纵坐标的值,既

 ADC

void ADC_Proc(void){
	for(int i=0;i<30;i++){
		pro.ADC_Value+=pro.DMA_ADC_Buff[i];
	}
	pro.ADC_Value=pro.ADC_Value/30*3.3f/4096;
	if(pro.ADC_Value<pro.V_Offset){
		pro.Pre_Realtime=0;
	}    //y=kx+b;  k=10/(3.3-pro.V_Offset)
	else{// b=-10*pro.V_Offset/(3.3-pro.V_Offset)
		pro.Pre_Realtime=10*(pro.ADC_Value-pro.V_Offset)/(3.3f-pro.V_Offset);
	}
}

一般我使用串口或者ADC都会开启DMA因为可以减小误差

还有一个功能是将当前电压值赋值给 V_offset 按键按下两秒后赋值一般我会开启一个10ms的定时器TIM1作为处理有时间变化的任务中断

//以下为KEY部分代码
if(pro.Key_State[2]==0 && pro.Key_Last_State[2]==1){
		if(pro.page==0){
			pro.KEY_Flag=1;
		}
	}
	else if(pro.Key_State[2]==1 && pro.Key_Last_State[2]==0){
		if(pro.page==0 && pro.Runtime_KEY1>=2000){
			pro.V_Offset=pro.ADC_Value;
		}
		pro.KEY_Flag=0;
		pro.Runtime_KEY1=0;
	}

void Interrupt(void){
	//篇幅限制其余中断判断代码删去
	
	if(pro.LED_Flag1==1){
		pro.Runtime_LED2+=10;
	}
	if(pro.LED_Flag2==1){
		pro.Runtime_LED3+=10;
	}
}

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim){
		Interrupt();
}

通过每10ms扫描一次中断任务即可实现按键按下2s后进行赋值

这个功能其实有需要细心的点,流量其实是以L/M单位,既每分钟的流量,在考场上我以为只需要除60既化为每秒,然后扔进中断每100ms判断累加一次,但是以100m为采样周期实际上已经给了流量的单位为ms,对于流量的处理应该是除600 既将 Min->Ms 这是我考场上比较明显的错误,

void IC_Proc(void){
	
	if(pro.TIM16_FRE<800){
		pro.Tra_Ins=0;
	}
	else if(pro.TIM16_FRE>=800 && pro.TIM16_FRE<=8000){
		pro.Tra_Ins=(float)pro.TIM16_FRE/200;
	}
	else{
		pro.Tra_Ins=40;
	}
}
void Interrupt(void){
	pro.Runtime_IC+=10;
	if(pro.Runtime_IC>=100){
		pro.Tra_Sum+=pro.Tra_Ins/600;//L/M -> L/ms
		pro.Runtime_IC=0;
	}
}

这个功能其实考的是1s的中断配置与对于的逻辑判断,我的方法是多设立变量不怕多,就怕错

void PWM_Proc(void){
	if(pro.Mode && pro.page!=1){
		pro.Man_Flag=0;
		LED_Proc(4,1);
		if(pro.Pre_Realtime<(pro.TarP_Pre-5)/10){
			pro.Auto_Flag=1;
		}
		else if(pro.Pre_Realtime>(pro.TarP_Pre+5)/10){
			pro.Auto_Flag=2;
		}
		else{
			pro.Auto_Flag=0;
			LED_Proc(4,0);
		}
	}
	else if(!pro.Mode && pro.page!=1){
		pro.Auto_Flag=0;
		LED_Proc(4,1);
		if(pro.Pa1_Duty>pro.TarD_Duty){
			pro.Man_Flag=1;
		}
		else if(pro.Pa1_Duty<pro.TarD_Duty){
			pro.Man_Flag=2;
		}
		else{
			pro.Man_Flag=0;
			LED_Proc(4,0);
		}
	}
	TIM2->CCR2=pro.Pa1_Duty;
}

void Interrupt(void){
	if(pro.Auto_Flag==1){
		pro.Runtime_Auto+=10;
		if(pro.Runtime_Auto>1000){
			pro.Runtime_Auto=0;
			pro.Pa1_Duty--;
			if(pro.Pa1_Duty<5){
				pro.Pa1_Duty=5;
			}
		}
	}
	else if(pro.Auto_Flag==2){
		pro.Runtime_Auto+=10;
		if(pro.Runtime_Auto>1000){
			pro.Runtime_Auto=0;
			pro.Pa1_Duty++;
			if(pro.Pa1_Duty>95){
				pro.Pa1_Duty=95;
			}
		}
	}
	else{
		pro.Runtime_Auto=0;
	}
	
	if(pro.Man_Flag==1){
		pro.Runtime_Man+=10;
		if(pro.Runtime_Man>=1000){
			pro.Runtime_Man=0;
			pro.Pa1_Duty--;
		}
		if(pro.Pa1_Duty<=pro.TarD_Duty){
			pro.Man_Flag=0;
		}
	}
	else if(pro.Man_Flag==2){
		pro.Runtime_Man+=10;
		if(pro.Runtime_Man>=1000){
			pro.Runtime_Man=0;
			pro.Pa1_Duty++;
		}
		if(pro.Pa1_Duty>=pro.TarD_Duty){
			pro.Man_Flag=0;
		}
	}
	else{
		pro.Runtime_Man=0;
	}
}

LED功能其实是比较简单的,没有复杂的闪烁与逻辑

void LED_Proc(uint16_t LED,uint16_t Mode){
	HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_SET);
	if(Mode){
		HAL_GPIO_WritePin(GPIOC,GPIO_PIN_8<<(LED-1),GPIO_PIN_RESET);
	}
	else{
		HAL_GPIO_WritePin(GPIOC,GPIO_PIN_8<<(LED-1),GPIO_PIN_SET);
	}
	HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_RESET);
}

void LED_Test(void){
	if(pro.TIM16_FRE>8000){
		LED_Proc(1,1);
	}
	else{
		LED_Proc(1,0);
	}
	
	if(pro.Tra_Ins<pro.FL_Para && pro.Pa1_Duty>pro.DH_Para){
		pro.LED_Flag1=1;
		if(pro.Runtime_LED2>2000){
			LED_Proc(2,1);
		}
	}
	else{
		pro.LED_Flag1=0;
		pro.Runtime_LED2=0;
		LED_Proc(2,0);
	}
	
	if(pro.Tra_Ins>pro.FH_Para && pro.Pre_Realtime<pro.PL_Para/10){
		pro.LED_Flag2=1;
		if(pro.Runtime_LED3>2000){
			LED_Proc(3,1);
		}
	}
	else{
		pro.LED_Flag2=0;
		pro.Runtime_LED3=0;
		LED_Proc(3,0);
	}
	LED_Proc(5,0);
	LED_Proc(6,0);
	LED_Proc(7,0);
	LED_Proc(8,0);
}
void Interrupt(void){
	if(pro.LED_Flag1==1){
		pro.Runtime_LED2+=10;
	}
	if(pro.LED_Flag2==1){
		pro.Runtime_LED3+=10;
	}
}

至此大体的逻辑差不多都完成了

有一些我比较喜欢的小细节,在比赛中实时测量一下现在运行的占空比与频率

开启PA6的TIM3通道1和2的捕获功能,其中通道1为直接捕获上升沿,通道2为间接捕获下降沿,

对于的数据为TIM3的CCR1和CCR2

void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim){
		if(htim->Instance==TIM3){
			if(htim->Channel==HAL_TIM_ACTIVE_CHANNEL_1){
				uint16_t CCR1=TIM3->CCR1;
				uint16_t CCR2=TIM3->CCR2;
				TIM3->CNT=0;
				pro.Pa6_Duty=(100*CCR2)/CCR1;
				pro.Pa6_Fre=1e6/CCR1;
			}
		}
}

这样只需通过杜邦线连接PA1和PA6在LCD上显示pro.Pa6_Duty与pro.Pa6_Fre即可知道当前输出的占空比与频率,更进一步可以在LCD上显示各个Runtime的值,避免配置错误引发的定时混乱

最后以下为main.c的部分代码

int main(void)
{

  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_DMA_Init();
  MX_ADC2_Init();
  MX_TIM1_Init();
  MX_TIM2_Init();
  MX_TIM3_Init();
  MX_TIM16_Init();
  /* USER CODE BEGIN 2 */
	HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_RESET);
	LCD_Init();
	LCD_Clear(Black);
  LCD_SetTextColor(White);
  LCD_SetBackColor(Black);
	
	HAL_ADCEx_Calibration_Start(&hadc2,ADC_SINGLE_ENDED);
	HAL_ADC_Start_DMA(&hadc2,pro.DMA_ADC_Buff,30);
	HAL_TIM_Base_Start_IT(&htim1);
	HAL_TIM_PWM_Start(&htim2,TIM_CHANNEL_2);
	TIM2->CCR2=5;
	HAL_TIM_IC_Start_IT(&htim3,TIM_CHANNEL_1);
	HAL_TIM_IC_Start_IT(&htim3,TIM_CHANNEL_2);
	HAL_TIM_IC_Start_IT(&htim16,TIM_CHANNEL_1);
  Sys_Init();
	HAL_Delay(50);
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
		LCD_Proc();
    Key_Scan();
    IC_Proc();
		ADC_Proc();
		PWM_Proc();
		LED_Test();
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

小结

        这份相对于十六届和十五届还是比较简单的,就是单纯的量大,没有复杂的逻辑处理与时间变化,没有像十六届的步进值,比较考验数据处理,也没有十五届的频率判断逻辑与题目误导,只是单纯量大,对变量的管理与数据变化需要比较细心

如需全部的代码,请私信或下载文章资源

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐