蓝桥杯嵌入式比赛 STM32G431 停车计费系统真题及程序设计详解
文章目录
前言
最近,我报名了今年的蓝桥杯嵌入式比赛,为此刷了一下以往的真题。以下是我对十二届蓝桥杯省赛真题的一些思路和心得,还有一些具体代码的实现。
一、题目介绍
二、相关模块及重难点分析
1、相关模块
第十二届比赛主要用到的模块包括:LED、KEY、LCD、TIM、USART
2、重难点分析
这道题主要目的是做一个停车管理收费系统,初始化两个界面来显示CNBR和VNBR两种类型的停车情况,以及剩余的空闲车位和两种类型的停车费用。通过上面的信息来判断LED的亮灭,以及PA7输出的状态。最后通过串口接受车辆信息,来分析是入库停车,还是出库离去。若是入库停车,则寻找车位,并保存信息。反之,则计算停车费用并通过串口打印,最后删除车辆信息。
**难点1:**串口处理接收的车辆信息,然后再判断是出库还是入库。
**难点2:**时间处理函数,将时间统一转换成秒数再计算出停车的时间,转换成小时,再通过单价得出时间的费用。(最麻烦的是其中还要考虑年份是否为闰年)。
**难点3:**整体代码的一个思路和逻辑,这可能是本届题目最大的一个考点和难点吧!
三、题解
1、变量定义
//gocar判断是否来车,serial判断是否车辆信息是否合格
uchar Gocar,serial=0;
uint16_t Compare; /*设置比较值,来确保输出为20%的占空比*/
uint8_t led; /*LED值*/
uint8_t key_val; /*按键值*/
uint8_t view=0; /*界面值*/
uint8_t CNBR=2,VNBR=4; /*两种类型车辆的初始值*/
uint8_t IDLE=8; /*初始化空余车位*/
uint8_t arr[23]; /*接受车辆信息的数组*/
uint8_t ass[20]; /*打印错误信息数组*/
char temp[30]; /*展示具体信息数组*/
float CNBR_fee=3.50,VNBR_fee=2.50;/*不同类型车辆的停车单价*/
extern struct Scinfo now_car; /*构造当前车辆信息的结构体*/
extern struct Scinfo car[9]; /*已有车辆信息结构体*/
2、各类函数声明
void key_proc(void); /*声明按键处理函数*/
void dis_proc(void); /*声明界面切换函数*/
uint8_t uart_proc(void);/*声明车辆信息判断函数*/
3、主函数以及while中的代码
在这里面一定要记得先去处理LED1、2的亮灭,因为可能在后面写一些其他的要求时从而忘记不同情况下的LED的亮灭状态。
//初始化LCD
LCD_Init();
LCD_Clear(Black);
LCD_SetBackColor(Black);
LCD_SetTextColor(White);
//开启TIM3通道捕获2,捕获PWM脉冲
HAL_TIM_PWM_Start(&htim3,TIM_CHANNEL_2);
while (1)
{
LED_Disp(led);
//开始接受数据
HAL_UART_Receive_IT(&huart1,arr,22);
//判断是否有还有车位剩余
IDLE=8-CNBR-VNBR;
key_proc();
dis_proc();
//判断是否来车
if(Gocar)
{
Gocar=0;
//判断车辆信息是否有误
serial=uart_proc();
if(serial==0)
{
//通过串口 打印ERROR
sprintf((char *)ass,"ERROR\r\n");
HAL_UART_Transmit(&huart1,ass,sizeof(ass),50);
//清除ass数组数据
memset(ass,0,sizeof(ass));
}
}
//判断是否还有剩余车位来控制LED1的亮灭
if(IDLE>0)
{
led |=1;
}
else
{
//因为控制LED灯是八位,~是安位取反操作,这里操作将最低位置为零。
led&=~1;
}
}
4、扫描按键及按键处理函数
这里按键的小细节可能就是避免单次按下,按键多次响应。至于PA7输出低电平脉冲,直接设为零,肯定是低电平脉冲了。不要被迷惑了,我一开始就是被迷惑了,然后想的太复杂了。
uint8_t keynum=0;
uint8_t key_read(void)
{
//按键锁
static unsigned char keyLock = 1;
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;
//这个是HAL库自带函数不会影响实时性
//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;
}
//按键处理函数
void key_proc()
{
key_val=key_read();
//判断按下哪个按键,进行不同的操作
switch(key_val)
{
case 1:
//切换界面
view=!view;
//界面刷新
LCD_Clear(Black);
key_val=0;
break;
case 2:
if(view==1)
{
CNBR_fee+=0.5f;
VNBR_fee+=0.5f;
key_val=0;
}
break;
case 3:
if(view==1)
{
CNBR_fee-=0.5f;
VNBR_fee-=0.5f;
key_val=0;
}
break;
case 4:
//按下B4,然后卡其LED2来设置比较值确保输出为20%
m^=1;
led^=2;
if(m==1)
{
LED_Disp(led);
Compare=400;
__HAL_TIM_SetCompare(&htim3,TIM_CHANNEL_2,Compare);
}
else
{
LED_Disp(led);
Compare=0;
__HAL_TIM_SetCompare(&htim3,TIM_CHANNEL_2,Compare);
}
}
}
5、LED函数
void LED_Disp(uint8_t dsLED)
{
HAL_GPIO_WritePin(GPIOC,GPIO_PIN_All,GPIO_PIN_SET);//先让所有灯熄灭
HAL_GPIO_WritePin(GPIOC,dsLED<<8,GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_SET);//开启锁存器
HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_RESET);
}
6、界面处理函数
void dis_proc()
{
if(view==0)
{
sprintf(temp," Data");
LCD_DisplayStringLine(Line0,(uint8_t*)temp);
sprintf(temp," CNBR=%d",CNBR);
LCD_DisplayStringLine(Line2,(uint8_t*)temp);
sprintf(temp," VNBR=%d",VNBR);
LCD_DisplayStringLine(Line4,(uint8_t*)temp);
sprintf(temp," IDLE=%d",IDLE);
LCD_DisplayStringLine(Line6,(uint8_t*)temp);
}
if(view==1)
{
sprintf(temp," Para");
LCD_DisplayStringLine(Line0,(uint8_t*)temp);
sprintf(temp," CNBR=%f",CNBR_fee);
LCD_DisplayStringLine(Line2,(uint8_t*)temp);
sprintf(temp," VNBR=%f",VNBR_fee);
LCD_DisplayStringLine(Line4,(uint8_t*)temp);
}
}
7、串口回调函数
这里就是难点1:车辆详细信息的处理,解决方法是通过构造结构体来简化这个信息处理的难度。还有一个不容忽视的问题,就是回调函数中千万别写太多,题目中有一定要求。
extern uchar Gocar;
extern uint8_t arr[23];
struct Scinfo
{
uint8_t NBR;
uint8_t Name[5];
uint8_t Year;
uint8_t Month;
uint8_t Day;
uint8_t Hour;
uint8_t Minute;
uint8_t Second;
};
struct Scinfo now_car;
//回调函数
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
//记录车辆信息
now_car.NBR=arr[0];
now_car.Name[0]=arr[5];
now_car.Name[1]=arr[6];
now_car.Name[2]=arr[7];
now_car.Name[3]=arr[8];
now_car.Year=(arr[10]-'0')*10+(arr[11]-'0');
now_car.Month=(arr[12]-'0')*10+(arr[13]-'0');
now_car.Day=(arr[14]-'0')*10+(arr[15]-'0');
now_car.Hour=(arr[16]-'0')*10+(arr[17]-'0');
now_car.Minute=(arr[18]-'0')*10+(arr[19]-'0');
now_car.Second=(arr[20]-'0')*10+(arr[21]-'0');
Gocar =1;
//清楚arr数据,为下次接收做准备
memset(arr,0,sizeof(arr));
}
8、车辆信息判断处理函数
判断车辆信息来确保输入格式正确,以及闰年的判断来确保时间以及年、月、日时间格式的正确。然后再通过判断车库是否有这台车,从而来判定调用哪个函数。
uint8_t uart_proc(void)
{
uint16_t Year;
uint8_t i;
if((now_car.NBR=='C'||now_car.NBR=='V')&&now_car.Hour<24&&now_car.Minute<60&&now_car.Second<60)
{
Year=2000+Year;
if(now_car.Month==1||now_car.Month==3||now_car.Month==5||now_car.Month==7||now_car.Month==8||now_car.Month==10||now_car.Month==12)
{
if(now_car.Day>31) return 0;
}
else if(now_car.Month==4||now_car.Month==6||now_car.Month==9||now_car.Month==11)
{
if(now_car.Day>30) return 0;
}
else
{
if((Year%4==0&&Year%100!=0)||Year%400==0)//判断是否为闰年
{
if(now_car.Day>28) return 0;
}
else
{
if(now_car.Day>29) return 0;
}
}
//判断是否存在这辆车 若存在则调用出库离开函数
for(i=0;i<8;i++)
{
if(car[i].NBR==now_car.NBR && strcmp((char *)car[i].Name,(char *)now_car.Name)==0)
{
if(output_car(i))
{
return 1;
}
else
{
return 0;
}
}
}
//寻找剩余车位 找到车位调用入库停车函数
for(i=0;i<8;i++)
{
if(car[i].NBR!='C'||car[i].NBR!='V')
{
input_car(i);
return 1;
}
}
}
return 0;
}
9、入库停车和出库离开的处理函数
入库函数:
判断是哪种车辆类型,并保存车辆详细信息
extern struct Scinfo now_car;
extern uint8_t IDLE,CNBR,VNBR;
struct Scinfo car[9]={0};//具体枚举在回调函数那里定义
uint8_t tip[30]={0};
void input_car(uint8_t location)
{
if(IDLE>0)
{
if(now_car.NBR=='C')
{
CNBR++;
}
else
{
VNBR++;
}
car[location].NBR=now_car.NBR;
car[location].Name[0]=now_car.Name[0];
car[location].Name[1]=now_car.Name[1];
car[location].Name[2]=now_car.Name[2];
car[location].Name[3]=now_car.Name[3];
car[location].Year=now_car.Year;
car[location].Month=now_car.Month;
car[location].Day=now_car.Day;
car[location].Hour=now_car.Hour;
car[location].Minute=now_car.Minute;
car[location].Second=now_car.Second;
}
}
出库函数:
判断哪种车辆离开 并计算停车费用
uint8_t output_car(uint8_t location)
{
float fee=0;
uint32_t second=0,second_last;
uint32_t time;
//把时间换算成秒数 在算出停车时间
second=Time_to_Seconds(car[location].Year,car[location].Month,car[location].Day,car[location].Hour,car[location].Minute,car[location].Second);
second_last=Time_to_Seconds(now_car.Year,now_car.Month,now_car.Day,now_car.Hour,now_car.Minute,now_car.Second);
if(second_last==second)
{
return 0;
}
//计算出停车时间 在转换成小时
time=(second_last-second)/60/60;
//未满一个小时 按一个小时计算
if((second_last-second)>time*60*60)
{
time++; //余数补偿
}
if(now_car.NBR=='C')
{
fee=time*3.50;
CNBR--;
}
else
{
fee=time*2.50;
VNBR--;
}
//输出车辆具体费用 并删除它的新息
sprintf((char *)tip,"%cNBR:%4s:%.2d:%3.2f\r\n",car[location].NBR,car[location].Name,time,fee);
HAL_UART_Transmit(&huart1,tip,sizeof(tip),0xfff);
memset(tip,0,sizeof(tip));
car[location].NBR='\0';
car[location].Name[0]=0;
car[location].Name[1]=0;
car[location].Name[2]=0;
car[location].Name[3]=0;
car[location].Year=0;
car[location].Month=0;
car[location].Day=0;
car[location].Hour=0;
car[location].Minute=0;
car[location].Second=0;
return 1;
}
10、时间转换函数
这是本题的难点2:将时间统一转换成秒数,然后用还要判断年份是否为闰年。注意这里处理闰年的月份时,也只有二月份天数变化,所以这里还要判定月份时间是否大于2月份,大于二月份才对月份进行多加一天时间的处理。(这时间处理函数,也可以网上找,但是毕竟是练习,可以多练练)
//这里将12个月的天数用数组处理,可以变的更加方便。
uint8_t mon[13]={0,31,28,31,30,31,30,31,31,30,31,30,31};
uint32_t Time_to_Seconds(uint8_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute, uint8_t second)
{
uint16_t i;
uint32_t Second;
Second = 0;
uint16_t Y;
Y = 2000+year;
for(i = 2000; i < Y ; i++)
{
if(i % 400 == 0 || (i % 4 == 0 && i % 100 != 0))
{
Second += 366*24*60*60;
}
else
Second += 365*24*60*60;
}
//判断是否为闰年 若月份大于2月份则多加一天时间
if ((Y % 400 == 0 || (Y % 4 == 0 && Y % 100 != 0)) && month > 2)
{
Second += 24*60*60;
}
for(i = 1; i < month ; i++)
{
Second += mon[i]*24*60*60;
}
for(i = 1; i < day; i++)
{
Second += 24*60*60;
}
for(i = 1; i < hour; i++)
{
Second += 60*60;
}
for(i = 1; i < minute; i++)
{
Second += 60;
}
Second += second;
return Second;
}
四、总结
总的来说,这届试题最大的难点还是车辆信息的处理和计费部分的逻辑,可能一时间有些处理不清。但是我从官方的解法发现,他们在解题之前都画了流程图,对我这个手残来说实属为难,不过当我画个草图来分析时,确实头脑变得清晰了很多,所以这个难点还是很好解决的。当然啦,除了本题难点外,题目还是有很多细节部分像我在函数开头或者注释的时候所说的那样,容易忽视,所以比赛做题时还是要格外注意。
最后想说,这是我第一次在CSDN上写文章,如果有什么不足之处,请多多见谅。毕竟,我写这篇文章,只是想记录和总结一下我的做题心得和步骤。
五、源码
源码