蓝桥杯嵌入式(长天嵌入式)组织的模拟赛-赛题分析

Warning:写作不易,请勿转载,感谢。
PS:我讲解的只是编程题,并且只是我个人的一点点看法,不喜勿喷,感谢。

这次嵌入式比赛的赛题和我写的程序:传送门

1:赛题分析

废话不多说,直接分析就完事了。

1.1:基础部分要求

基本功能

  1. 通过竞赛板上电位器R37输出模拟电压信号,经微控制器内部AD采集处理后,通过液晶屏实时显示。
  2. 通过串口接收上位机指令,执行指令,并返回数据。
  3. 支持按键扫描功能,可识别当前各个按键状态。
  4. LED亮灭受控。

这就是赛题的基础要求部分。拿到赛题之后首先就要快速的去初始化号各个模块,具体代码模块,查看第二节。
在这里我分享一些我自己的习惯。

  1. 打开工程就把stm32所有的官方库都加进来,方便后续的使用,省的说在一个一个的找然后再加进来,记住先别编译。
  2. 然后再打开stm32的微库模式(当然如果说是不用串口的话可以不用选择)。具体什么是微库,为什么要打开。我在前面的文章已经讲过了,不懂得可以点击传送门
  3. 记住当32所有的库都添加了并选择了微库,再进行编译因为这样可以节省大部分的时间。

初始化这部分是很简答的,多余的部分我就不讲了。我想讲的是比较精彩的部分,也就是出题的扣分点。那就是串口和按键的时间判断!

1.2:按键功能要求

  1. 按键B1短按键操作:所有LED指示灯熄灭。
  2. 按键B1长按键操作(按下时长超过800ms):所有LED指示灯状态翻转。

这个功能的要求就是根据按键按下时间的长短,然后表现出不同的状态。具体检测按键的时间,可以看我之前的一篇文章有很详细的讲解,想要进一步了解的点击传送门。在这里我不过多去解读了,这个还是比较简单的

1.3:串口通信功能要求

  1. 串口基本配置
    使用竞赛板USART2完成全部串口通信功能.
    通信波特率配置为9600bps。

  2. LED亮灭控制指令
    指令格式:“LDn:0”、“LDn:1”或“LDn:2”
    指令解析:编号为n的LED指示灯点亮或熄灭,n的范围是1-8。0表示熄灭,1表示点亮,2控制指示灯状态翻转。
    指令举例:
    “LD1:0”,控制指示灯LD1熄灭。
    “LD1:1”,控制指示灯LD1点亮。
    “LD2:2”,控制指示灯LD2亮灭状态翻转。
    指令回复:
    本条指令不需要回复任何内容。

  3. 按键状态查询
    指令格式:“Bn?”
    指令解析:查询编号为n的按键状态。n的范围是1-4;
    指令举例:“B1?”
    指令回复:
    “B1:P”或“B1:R”,其中P表示B1按键处于按下的状态,R表示B1按键处于释放的状态。

  4. 模拟电压查询指令
    指令格式:“ADC?”
    指令解析:查询当前微控制器采集到的实时电压值。
    指令举例:“ADC?”
    指令回复:
    “ADC:3.02V”,表示当前采集到的电压值为3.02V,电压值保留小数点后两位有效数字。

  5. 未知指令
    当设备收到收到未知的错误指令时,返回“error”。

  6. 通信指令要求
    请严格按照上述1-5条中要求设计串口交互过程,注意指令格式、大小写等设计细节。

1.3.1:串口接受指令的结束判断

这次的串口模块的功能要求可以说是比较麻烦的了,因为他这次的发送指令缺少了一个结束符,所以我们就没有办法去判断这个指令到哪里才算是信息的结束。

可能有些人看起来有点不懂我的意思,在这我稍微解释一下。比如说PC机发送LDn:0LDn:1,这到底是不是错误的指令的呢??答案是这是个错误的指令,我们需要做的是串口发送"error"并且应该把接受数组重新初始化,然后等待下次的接受。但是问题就出来了,现在指令里缺少了’\n’,单片机就不知道什么时候结束,并且去发送。

可能有人说了,全部接受后然后用字符串比对,只要不符合要求的全部强制让串口结束,并且发送"error",那么恭喜你就进入他的一个陷阱(当然这是我自己的想的,不知道他们有没有在这挖坑,哈哈!也许我是与空气斗智斗勇)。假如说它用PC机首先发送一个"B1",然后过了0.5S发送一个"?",那么问题就来了,这到底算不算对呢,因为它们加起来是"B1?"。在我看来这是两个指令,都不符合规定因此都是错误的指令。那么单片机应该在PC机发送"B1"的时候,就应该发送出去"error"了,并且在PC机发送"?“的时候,这个时候也要发送"error”,我认为这样才是对的。

那么怎么样才算一个完整的指令呢?并且怎么去操作呢?其实很简单,这道编程题在最后面有这样一句话 “各类串口配置、查询指令响应时间要求:≤300ms。”,这句话给了我一个思路,他要求的串口响应要求是≤300s,那我们用定时器模块的配合。

具体操作:我们利用定时器模块进行计时,让它在串口接收到第一个数据的时候就立刻开始计时,然后10ms后强制关闭串口的接受中断,然后进行字符串比对,如果说正确就继续操作,如果说不对,就发送"error"即可,但是无论对还是不对都别忘了再次打开串口接收中断哦。但是为什么定时10ms呢?因为波特率要求是9600,根据计算9600/8/1000=1.2 symbol/ms,相当于10ms就能接收到12个字符。正确的接收指令最多也不会超过10个字符,所以说这里的10ms足够了。

因此我们只需要做的是接收到一个字符后,开启定时器,过了10ms后关闭串口,并且进行字符串比对和接受数组的初始化,然后再打开串口就可以了。PS:记住每次计时的时间在关闭串口后要进行清0,并且停止计时!

1.3.2:按键状态查询和模拟电压查询指令

这两个指令收到了做出的相应的反馈可以说是比较简单的,具体函数放到主函数中的while(1)就可以。
这里唯一要注意得就是发送出去指令的格式一定要严格的符合他的要求,他可是没有要求让你发送指令后面加"\n\r"的,千万别自作聪明加上,如果加上了,恭喜你分又没了。。。

if(RxBuffer1[0]=='A'&&RxBuffer1[1]=='D'&&RxBuffer1[2]=='C'&&RxBuffer1[3]=='?'&&RxBuffer1[4]=='`'&&RxBuffer1[5]=='`'){
	printf("ADC:%1.2fV",Adc_GetVal());
}
if(RxBuffer1[0]=='B'&&RxBuffer1[1]=='1'&&RxBuffer1[2]=='?'&&RxBuffer1[3]=='`'&&RxBuffer1[4]=='`'&&RxBuffer1[5]=='`'){
	if(Key1==0) printf("B1:P");
	else printf("B1:R");
}
if(RxBuffer1[0]=='B'&&RxBuffer1[1]=='2'&&RxBuffer1[2]=='?'&&RxBuffer1[3]=='`'&&RxBuffer1[4]=='`'&&RxBuffer1[5]=='`'){
	if(Key2==0) printf("B2:P");
	else printf("B2:R");
}
if(RxBuffer1[0]=='B'&&RxBuffer1[1]=='3'&&RxBuffer1[2]=='?'&&RxBuffer1[3]=='`'&&RxBuffer1[4]=='`'&&RxBuffer1[5]=='`'){
	if(Key3==0) printf("B3:P");
	else printf("B3:R");
}
if(RxBuffer1[0]=='B'&&RxBuffer1[1]=='4'&&RxBuffer1[2]=='?'&&RxBuffer1[3]=='`'&&RxBuffer1[4]=='`'&&RxBuffer1[5]=='`'){
	if(Key4==0) printf("B4:P");
	else printf("B4:R");
}

没错就那么粗暴。。。当然可以自己写一个字符串扫描函数如下:

_Bool Buffercmp(uint8_t* pBuffer1, uint8_t* pBuffer2)
{
  while(*pBuffer1!=0)
  {
    if(*pBuffer1 != *pBuffer2)
    {
      return 0;
    }
    pBuffer1++;
    pBuffer2++;
  }
  return 1;
}

这个完全是可以的,自己先初始化好相应的数组,与接收到的数组进行比对都可以的,看个人习惯罢了。

1.3.3:LED亮灭控制指令

这个串口指令可谓是整道题的灵魂,因为我们要保证某一位改变的同时,不能去影响其他位。其实如果写过寄存器就应该认为这是很简单的,因为这里只需要与,或和异或操作就能实现题目的要求了。

比如说如下要求:
“LD1:0”控制指示灯LD1熄灭。

 Led &= 0xfe;  //最低位变成0,其他位不变

“LD1:1”控制指示灯LD1点亮。

 Led |= 0x01; //最低位变成1,其他位不变

“LD1:2”,控制指示灯LD1亮灭状态翻转。
异或运算与0异或相当于保持不变,与1异或相当于取反。

Led ^= 0x01; //最低位的值取反,其他位不变

这样就可以对相应的位进行操作了,然后在进行下个操作就可以了。

Led_Control(Led_All,0);
Led_Control((u16)Led <<8,1);

第一行:先把所有的灯关掉。
第二行:Led左移8位,相当于就是引脚了,然后使引脚对应的LED亮起来。
比如说Led=0x01,左移8位就变成了0x0100这里对应的引脚就是PIN8,也就会LED1这个引脚。

至于为什么我要操作关闭所有的led这条指令呢?我的意思在这里,因为我第二行的代码只能使LED亮起来,并不可以关闭LED。我再打个比方,假如现在的Led=0x03,理论上是LD1和LD2在亮着呢,其他的都灭,然后我PC机发送一个指令“LD1:2”,这里就会使得Led的值变为0x02了。但是如果说我没有第一行的全灭,我直接操作第二行的代码,虽然说传的led的值为0x02,但是并不会使LED1灭,因为你只对引脚PIN9进行操作,并没有改变其他的引脚,因此我们需要先把之前的灯全部关掉,然后再去操作,就可以完美解决了。

可能有的人说了,我这是脱裤子放屁。。把与、或、和异或操作这些指令改为直接对单个led灯进行操作就可以,没必要改这个变量,可以是可以。。但是你就没办法把led的状态通过LCD显示出来了。。当然你也可以通过读取LED相应的引脚的值,也可以知道LED的状态的。。方法多的是,只要能成功就可以的。

1.3.4:串口功能要求总结

其实这个难度并不大,只要静下心,一步一步的去思考就可以了。。至于更多关于串口模块的功能的接受请看我之前的文章-蓝桥杯嵌入式基础板模块之串口模块的发送与接收

1.4:液晶显示功能

这个模块需要注意的是一定要把各个字符显示到固定的行列上,因为它是机审,这点是很重要的一点。

1.4.1:LCD之ADC数值显示

void Show_AdcVal(void){
	u8 str[20];
	float Adc_Number = 0;
	Adc_Number = Adc_GetVal();
	sprintf((char *)str," ADC:%1.2fV",Adc_Number);
	LCD_DisplayStringLine(Line2,str);
}

这里没有什么好说的,你只要把这个float型的数值,按相应的数值格式进行显示就可以了。

1.4.2:LCD之按键状态显示

void Show_KeyVal(void){
	u8 str[20];
	if(Key1_State)	sprintf((char *)str," B1:P");
	else sprintf((char *)str," B1:R");
	LCD_DisplayStringLine(Line3,str);
	
	if(Key2_State)	sprintf((char *)str," B2:P");
	else sprintf((char *)str," B2:R");
	LCD_DisplayStringLine(Line4,str);
	
	if(Key3_State)	sprintf((char *)str," B3:P");
	else sprintf((char *)str," B3:R");
	LCD_DisplayStringLine(Line5,str);
	
	if(Key4_State)	sprintf((char *)str," B4:P");
	else sprintf((char *)str," B4:R");
	LCD_DisplayStringLine(Line6,str);
}

这个也没什么可说的。。。

1.4.3:重点:LCD之LED状态显示!!

void Show_LedVal(void){
	u8 str[20];
	sprintf((char *)str," LED:%02X",Led);
	LCD_DisplayStringLine(Line7,str);
}

这是我唯一想说的一个点,还记得我之前那个Led这个变量吗?我这里就是利用这个变量,直接利用spinrtf()函数的功能,直接可以转换为大写的16进制,可以说是非常的便捷和简单!!!!

2:各个模块初始化和部分重点函数

2.1:Led模块或LED不受控制

没什么想说的,自己看。如果说碰到LED不受控制的情况,请参考我之前的博客-蓝桥杯嵌入式基础板模块之LED模块不受控制的解决方法

void Led_Init(void){
	GPIO_InitTypeDef GPIO_InitStructure;
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC|RCC_APB2Periph_GPIOD, ENABLE);

	GPIO_InitStructure.GPIO_Pin = Led_All;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
	GPIO_Init(GPIOC, &GPIO_InitStructure);

	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
	GPIO_Init(GPIOD, &GPIO_InitStructure);
	Led_Control(Led_All,0);
}

void Led_Control(u16 LED,u8 state){
	if(state==0){
		GPIO_SetBits(GPIOC,LED);
		GPIO_SetBits(GPIOD,GPIO_Pin_2);
		GPIO_ResetBits(GPIOD,GPIO_Pin_2);
	}
	else {
		GPIO_ResetBits(GPIOC,LED);
		GPIO_SetBits(GPIOD,GPIO_Pin_2);
		GPIO_ResetBits(GPIOD,GPIO_Pin_2);
	}
	if(LED==0){
		GPIO_SetBits(GPIOC,Led_All);
		GPIO_SetBits(GPIOD,GPIO_Pin_2);
		GPIO_ResetBits(GPIOD,GPIO_Pin_2);
	}
}

2.2:ADC模块

也没什么想说的,自己看。如果说想学更多关于ADC模块的知识,比如说32自带的温度芯片和多路DMA采集-请参考我之前的博客-蓝桥杯嵌入式基础板模块之ADC模块-温度传感器的单通道采集与多通道采集

void Adc_Init(void){
	ADC_InitTypeDef ADC_InitStructure;
	GPIO_InitTypeDef GPIO_InitStructure;

	RCC_ADCCLKConfig(RCC_PCLK2_Div6); 
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1|RCC_APB2Periph_GPIOB, ENABLE);// PB0 C8

	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
	GPIO_Init(GPIOB, &GPIO_InitStructure);

	ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
	ADC_InitStructure.ADC_ScanConvMode = ENABLE;
	ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;
	ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
	ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
	ADC_InitStructure.ADC_NbrOfChannel = 1;
	ADC_Init(ADC1, &ADC_InitStructure);

	ADC_RegularChannelConfig(ADC1, ADC_Channel_8, 1, ADC_SampleTime_239Cycles5);
	ADC_Cmd(ADC1, ENABLE);
	ADC_ResetCalibration(ADC1);
	while(ADC_GetResetCalibrationStatus(ADC1));
	ADC_StartCalibration(ADC1);
	while(ADC_GetCalibrationStatus(ADC1));
}

float Adc_GetVal(void){
	float temp=0;
	ADC_SoftwareStartConvCmd(ADC1, ENABLE);
	while(!ADC_GetFlagStatus(ADC1,ADC_FLAG_EOC));
	ADC_ClearFlag(ADC1,ADC_FLAG_EOC);
	temp = ADC_GetConversionValue(ADC1)*3.3/4095;
	return temp;
}

2.3:按键模块

也没什么想说的,自己看。如果说想知道怎么样判断按键时间-请参考我之前的博客STM32实现按键功能之短按加一次而长按连续加的功能


void Key_Init(void){
	GPIO_InitTypeDef GPIO_InitStructure;
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOB, ENABLE);

	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8|GPIO_Pin_0;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
	GPIO_Init(GPIOA, &GPIO_InitStructure);

	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1|GPIO_Pin_2;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
	GPIO_Init(GPIOB, &GPIO_InitStructure);
}
u8 KeyScan(u8 mode){
	static _Bool KeyScan_Flag=1;
	if(mode)	 KeyScan_Flag=1;
	if((Key1==0||Key2==0||Key3==0||Key4==0)&&KeyScan_Flag){
		Delay_Ms(10);
		KeyScan_Flag=0;
		if(Key1==0) return 1;
		else if(Key2==0) return 2;
		else if(Key3==0) return 3;
		else if(Key4==0) return 4;
	}
	else if(Key1==1&&Key2==1&&Key3==1&&Key4==1) KeyScan_Flag=1;
	return 0;
}

2.4:定时器模块

由于我定时器模块的中断函数内容过多,在这里我就不展示出来了,有兴趣的可以直接下载我的程序去看。如果说想学习TIM中的多路捕获和多路输出功能,请参考我之前的博客蓝桥杯嵌入式基础板模块之单个定时器的多路捕获和多路输出

void Timer2_Init(void){
	TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure; 	
	NVIC_InitTypeDef NVIC_InitStructure;

	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);

	NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 3;
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	NVIC_Init(&NVIC_InitStructure);

	TIM_TimeBaseStructure.TIM_Period = 4999;
	TIM_TimeBaseStructure.TIM_Prescaler = 71;
	TIM_TimeBaseStructure.TIM_ClockDivision = 0;
	TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
	TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);

	TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
	TIM_Cmd(TIM2, ENABLE);
}

2.5:串口模块和字符串比较函数

这个也没什么想说的,自己看。如果说学习关于更多串口模块的功能-请参考我之前的博客蓝桥杯嵌入式基础板模块之串口模块的发送与接收

void Usart2_Init(void){
	USART_InitTypeDef USART_InitStructure;
	GPIO_InitTypeDef GPIO_InitStructure;
	NVIC_InitTypeDef NVIC_InitStructure;
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_AFIO,ENABLE);
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2,ENABLE); 

	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
	GPIO_Init(GPIOA, &GPIO_InitStructure);

	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	  
	NVIC_InitStructure.NVIC_IRQChannel = USART2_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	NVIC_Init(&NVIC_InitStructure);

	USART_InitStructure.USART_BaudRate = 9600;
	USART_InitStructure.USART_WordLength = USART_WordLength_8b;
	USART_InitStructure.USART_StopBits = USART_StopBits_1;
	USART_InitStructure.USART_Parity = USART_Parity_No;
	USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
	USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
	USART_Init(USART2, &USART_InitStructure);
	USART_ITConfig(USART2,USART_IT_RXNE,ENABLE);
	USART_Cmd(USART2, ENABLE);
}
void USART2_IRQHandler(void)
{
	u8 temp=0;
	if(USART_GetITStatus(USART2,USART_IT_RXNE)!=RESET)
	{
		usart_State = 1;
		USART_ClearFlag(USART2, USART_IT_RXNE);
		temp= USART_ReceiveData(USART2);
		if(RxCounter1==999)
		{
			RxBuffer1[RxCounter1]=temp;
			Rx_Flag=1;
			USART_ITConfig(USART2,USART_IT_RXNE,DISABLE);
			USART_Cmd(USART2, DISABLE);
		}
		else{
			RxBuffer1[RxCounter1++] = temp;
		}
	}
}
_Bool Buffercmp(uint8_t* pBuffer1, uint8_t* pBuffer2)
{
  while(*pBuffer1!=0)
  {
    if(*pBuffer1 != *pBuffer2)
    {
      return 0;
    }
    pBuffer1++;
    pBuffer2++;
  }
  return 1;
}
PUTCHAR_PROTOTYPE
{
  USART_SendData(USART2, (uint8_t) ch);

  while (USART_GetFlagStatus(USART2, USART_FLAG_TXE) == RESET)
  {}
  return ch;
}

3:赛后总结

关于这次模拟赛,由于运气好,拿到了第二名。。关于这次比赛的编程题,我个人认为难度中等偏下吧,三个小时都可以说非常宽裕了,根本用不了5个小时。我这里说的三个小时可是包括所有模块的初始化的编写的。

其实我感觉这个比赛,就是考的是你发散的思维,千万不要局限于别人的思想中。我写着博客的作用更多的是给那些写出来题目但是解题思路跟我不同的人看得,从而使大脑更加的活跃。

这个比赛是没有技巧的,需要大量的经验。如果说你还没有做过往年省赛题和模拟题,那你就要赶紧加快速度了。我其实准备这个比赛也就用了一个月,我之前更多玩的是51和K60开发板,当然也玩过32但是就顶多算是入门。。在这一个月学的内容包括32的基础知识,基础板和拓展版的各个模块学习,并且写了往年所有的赛题。。。。至于为什么学那么快,可能因为我是个吃货吧,我想拿公费去北京吃烤鸭!!!哈哈,当然那只是动力罢了,其实是因为大二参加了智能车实验室(当然现在还在),我们实验室当时是每周一道综合编程题。。。

最后送给大家我喜欢的六个字:

越努力越幸运

发布了19 篇原创文章 · 获赞 17 · 访问量 4080

猜你喜欢

转载自blog.csdn.net/qq_43605009/article/details/105292710