相关说明:
开发板:CT117E-M4(STM32G431RB 蓝桥杯嵌入式比赛板)
开发环境: CubeMX+Keil5
涉及题目:第十二届蓝桥杯嵌入式省赛
题目难点:停车管理系统逻辑编写;数据接收,解析,判定,更新。
代码思路:(使用usart1时需要修改引脚为PA8 PA9 PA10)串口接收到数据后,先判定数据接收长度是否正确,即每接收到一个字节都重新开启定时器,最后一字节数据接收完且进入定时器中断后判断接收数据长度,准确无误则进行数据解析;解析时将数据分段保存:车类型,车牌号,时间;保存好后再对数据的合法性进行判定,车类型是否为规定类型之一,类型、车牌号数据长度是否为四位。时间是否合法(月份对应天数,时分秒对应进制);最后是存储数据的更新,车牌号是否已经存在?不存在的话判断是否还有空余车位?有则将类型、车牌号、时间等数据存储在数组中;存在的话考虑现在接收时间是否大于到达时间?时间合法则对存储在数组组中的数据进行计算和输出。
CubeMX配置、主要函数代码及说明:
3.GPIO:
5.TIM6(串口在接收到最后一字节数据5us后进入定时器中断函数):
main.c
void Car_Change(char *type,char *carNum,time_t *time,char *str); //Car数组改变
uint8_t Dat_Check(char *type,char *carNum); //判断接收数据正误(格式 时间)
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim); //定时器6中断 数据接收超时判定(避免一个合法数据分多次发送)
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart); //串口接收中断函数(每接收一字节中断一次)
void Settle_Accounts(struct car outCar); //结账
void Switch_RecBuff(char *type,char *carNum,time_t *time,char *timStr); //CNBR:A392:200202120000 数据解析(类型 车牌 时间)
void LCD_Init_Show(); //LCD初始化显示
void LCD_Refresh(uint8_t page); //LCD更新显示
void LED_Change(); //LED状态改变
gpio.h
void KEY_Scan(void); //按键扫描
void LED_AllClose(uint8_t *LCD_Close); //LED显示更新
#define LED_GPIO_PORT GPIOC
#define LED1_GPIO_PIN GPIO_PIN_8
#define LED2_GPIO_PIN GPIO_PIN_9
#define LED3_GPIO_PIN GPIO_PIN_10
#define LED4_GPIO_PIN GPIO_PIN_11
#define LED5_GPIO_PIN GPIO_PIN_12
#define LED6_GPIO_PIN GPIO_PIN_13
#define LED7_GPIO_PIN GPIO_PIN_14
#define LED8_GPIO_PIN GPIO_PIN_15
#define ON GPIO_PIN_RESET
#define OFF GPIO_PIN_SET
#define LED1(a) HAL_GPIO_WritePin(LED_GPIO_PORT,LED1_GPIO_PIN,a)
#define LED2(a) HAL_GPIO_WritePin(LED_GPIO_PORT,LED2_GPIO_PIN,a)
#define LED3(a) HAL_GPIO_WritePin(LED_GPIO_PORT,LED3_GPIO_PIN,a)
#define LED4(a) HAL_GPIO_WritePin(LED_GPIO_PORT,LED4_GPIO_PIN,a)
#define LED5(a) HAL_GPIO_WritePin(LED_GPIO_PORT,LED5_GPIO_PIN,a)
#define LED6(a) HAL_GPIO_WritePin(LED_GPIO_PORT,LED6_GPIO_PIN,a)
#define LED7(a) HAL_GPIO_WritePin(LED_GPIO_PORT,LED7_GPIO_PIN,a)
#define LED8(a) HAL_GPIO_WritePin(LED_GPIO_PORT,LED8_GPIO_PIN,a)
#define KEY1_GPIO_PORT GPIOB
#define KEY1_GPIO_PIN GPIO_PIN_0
#define KEY2_GPIO_PORT GPIOB
#define KEY2_GPIO_PIN GPIO_PIN_1
#define KEY3_GPIO_PORT GPIOB
#define KEY3_GPIO_PIN GPIO_PIN_2
#define KEY4_GPIO_PORT GPIOA
#define KEY4_GPIO_PIN GPIO_PIN_0
main.c
uint8_t CNBR=0; //CNBR类型车辆数
uint8_t VNBR=0; //VNBR类型车辆数
uint8_t IDLE=8; //空闲位置
double CNBR_Price=3.5; //CNBR类型停车费用
double VNBR_Price=2.0; //VNBR类型停车费用
char str[30]; //用于组合字符串
uint8_t LED_Close[3]={1,0,1}; //LED关闭数组
uint8_t recDatBuff[3][20]={0,0,0,0}; //数据接收数组(recDatBuff[0]存储停车类型,recDatBuff[1]存储车牌号,recDatBuff[2]存储时间,冒号存储在数组0,1行的最后一个位置)
uint8_t recDex=0; //接收数组下标
uint8_t recNum=0; //接收数组行号
uint8_t recDat; //本次接收数据(一次一字节)
uint32_t recLong=0; //数接收长度
uint8_t firstByte=1; //接收到本次传输数据的第一个字节(用于判定传输超时)
uint8_t switch_flag=0; //接收数据转换标志,完整接收一次数据后置1,随后进行转换、判断、存储
int Error; //接收数据错误标志
int recYear; //接收年
int recMon; //接收月
int recDay; //接收日
int recHour; //接收时
int recMin; //接收分
int recSec; //接收秒
struct car //车辆结构体
{
char num[10]; //车牌号
char type[10]; //车辆类型
int dftime; //时间差
double EndPrice; //最终价格
double type_price; //停车单价(元/小时)
char reach_time[60];//到达时间字符串
char leave_time[60];//离开时间字符串
int reach; //到达时间时间戳
int leave; //离开时间时间戳
};
struct car car[9]; //车辆存储结构体数组
uint8_t car_dex=0; //存储数组下标
首先是按键按下对数据以及输出PWM的更改;更改PWM输出时,按键按下后,先判断LED_Close[2]存储的状态,为灭则使LED2亮,开启PWM,输出1KHz信号。为亮则使LED2灭,暂停PWM,输出持续的低电平。(PWM在MX的配置为1KHz的输出。)
gpio.c
void Data_Change(uint8_t mode)//数据改变
{
switch(mode)
{
case ADD:
CNBR_Price+=Price_step;
VNBR_Price+=Price_step;
break;
case SUB:
CNBR_Price-=Price_step;
VNBR_Price-=Price_step;
break;
}
}
void Setting_Mode()//设置模式
{
uint8_t delay=0;
while(1)
{
if(HAL_GPIO_ReadPin(KEY1_GPIO_PORT,KEY1_GPIO_PIN)==GPIO_PIN_RESET)//change mode
{
HAL_Delay(10);
if(HAL_GPIO_ReadPin(KEY1_GPIO_PORT,KEY1_GPIO_PIN)==GPIO_PIN_RESET)
{
while(HAL_GPIO_ReadPin(KEY1_GPIO_PORT,KEY1_GPIO_PIN)==GPIO_PIN_RESET);
LCD_Refresh(1);
break;
}
}
else if(HAL_GPIO_ReadPin(KEY2_GPIO_PORT,KEY2_GPIO_PIN)==GPIO_PIN_RESET)//++
{
HAL_Delay(10);
if(HAL_GPIO_ReadPin(KEY2_GPIO_PORT,KEY2_GPIO_PIN)==GPIO_PIN_RESET)
{
while(HAL_GPIO_ReadPin(KEY2_GPIO_PORT,KEY2_GPIO_PIN)==GPIO_PIN_RESET);
Data_Change(ADD);
LCD_Refresh(2);
}
}
else if(HAL_GPIO_ReadPin(KEY3_GPIO_PORT,KEY3_GPIO_PIN)==GPIO_PIN_RESET)//--
{
HAL_Delay(10);
if(HAL_GPIO_ReadPin(KEY3_GPIO_PORT,KEY3_GPIO_PIN)==GPIO_PIN_RESET)
{
while(HAL_GPIO_ReadPin(KEY3_GPIO_PORT,KEY3_GPIO_PIN)==GPIO_PIN_RESET);
Data_Change(SUB);
LCD_Refresh(2);
}
}
}
}
void KEY_Scan()//按键扫描
{
if(HAL_GPIO_ReadPin(KEY1_GPIO_PORT,KEY1_GPIO_PIN)==GPIO_PIN_RESET)//change mdoe
{
HAL_Delay(10);
if(HAL_GPIO_ReadPin(KEY1_GPIO_PORT,KEY1_GPIO_PIN)==GPIO_PIN_RESET)
{
while(HAL_GPIO_ReadPin(KEY1_GPIO_PORT,KEY1_GPIO_PIN)==GPIO_PIN_RESET);
LCD_Refresh(2);
Setting_Mode();
}
}
else if(HAL_GPIO_ReadPin(KEY4_GPIO_PORT,KEY4_GPIO_PIN)==GPIO_PIN_RESET)//PWM
{
HAL_Delay(10);
if(HAL_GPIO_ReadPin(KEY4_GPIO_PORT,KEY4_GPIO_PIN)==GPIO_PIN_RESET)
{
while(HAL_GPIO_ReadPin(KEY4_GPIO_PORT,KEY4_GPIO_PIN)==GPIO_PIN_RESET);
if(LED_Close[2]==1)//如果灭 即输出低电平
{
LED_Close[2]=0;
HAL_TIM_PWM_Start(&htim3,TIM_CHANNEL_2);
}
else
{
LED_Close[2]=1;
HAL_TIM_PWM_Stop(&htim3,TIM_CHANNEL_2);
HAL_GPIO_WritePin(GPIOA,GPIO_PIN_7,GPIO_PIN_RESET);
}
}
}
}
接下来是数据的处理,分别为接收,解析,判定和更新。
串口在接收到一个字节数据时进入串口中断函数,每次进入串口中断函数都需要重新开启判定,在最后一字节数据接收完5us后进入定时器中断函数,在定时器中断函数中判定接收数据长度是否符合要求,不符合则返回Error,符合要求则按照设定好的规则进行保存。我使用的是二维数组,遇到冒号就换行,最后根据数据长度来设定接收结束的标志(接收结束后将开始解析)即可。
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)//定时器6中断 数据接收长度判定
{
HAL_TIM_Base_Stop_IT(&htim6);//关闭判定
HAL_UART_Receive_IT(&huart1,&recDat,sizeof(recDat));//重新开启串口接收中断
if(recLong!=22)
{
Error=1;//错误标志位置1
}
else
{
switch_flag=1;//字符串转换标志位置1
}
recLong=0;//接收长度重置
recNum=0;//recNum重置
recDex=0;//recDex重置
}
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)//串口接收中断函数(每接收一字节中断一次) 在最后一字节接收完5us后判断数据长度
{
HAL_TIM_Base_Stop_IT(&htim6);//关闭判定
recLong++;
recDatBuff[recNum][recDex++]=recDat;//将接收到的数据存储进数组
if(recDat==':')//如果本次接收数据为冒号则数组换行,下标置为0
{
recNum++;
recDex=0;
}
TIM6->CNT=0;
HAL_TIM_Base_Start_IT(&htim6);//重新开启判定
HAL_UART_Receive_IT(&huart1,&recDat,sizeof(recDat));//重新开启串口接收中断
}
将保存在二维数组中的数据进行字符串组合,数组中第0行数据为车的类型+冒号,第1行数据为车牌号+冒号,第2行数据为时间。
这里用到time.h中的函数对时间进行转换,先将时间进行类型转换(字符型转整型),转换后根据mktime函数规则进行调整(年份减1900,月份减1),再将调整后的结果赋值给时间结构体,再调用mktime函数进行时间转换,转换后的时间为自1970年1月1日以来持续时间的秒数 (为什么用一个错误的时间测试,mktime不会返回-1,欢迎懂的大佬留言)
void Switch_RecBuff(char *type,char *carNum,time_t *time,char *timStr)//CNBR:A392:200202120000 数据解析 类型 车牌 时间
{
struct tm timeTem;//定义时间结构体
sprintf(type,"%c%c%c%c",recDatBuff[0][0],recDatBuff[0][1],recDatBuff[0][2],recDatBuff[0][3]);//解析类型
sprintf(carNum,"%c%c%c%c",recDatBuff[1][0],recDatBuff[1][1],recDatBuff[1][2],recDatBuff[1][3]);//解析车牌号
sprintf(timStr,"%c%c%c%c%c%c%c%c%c%c%c%c",recDatBuff[2][0],recDatBuff[2][1],recDatBuff[2][2],recDatBuff[2][3],recDatBuff[2][4],recDatBuff[2][5],recDatBuff[2][6],recDatBuff[2][7],recDatBuff[2][8],recDatBuff[2][9],recDatBuff[2][10],recDatBuff[2][11]);//解析时间
recYear=2000+(recDatBuff[2][0]-48)*10+(recDatBuff[2][1]-48)-1900;//将时间转换为int类型数据 如'0'要转换成0,字符0对应的ASCII码为48,则0为'0'-48 1~9以此类推
recMon=(recDatBuff[2][2]-48)*10+(recDatBuff[2][3]-48)-1;//时间结构体存储规则,月份减1,年份减1900
recDay=(recDatBuff[2][4]-48)*10+(recDatBuff[2][5]-48);
recHour=(recDatBuff[2][6]-48)*10+(recDatBuff[2][7]-48);
recMin=(recDatBuff[2][8]-48)*10+(recDatBuff[2][9]-48);
recSec=(recDatBuff[2][10]-48)*10+(recDatBuff[2][11]-48);
timeTem.tm_year=recYear;//一一对应赋值
timeTem.tm_mon=recMon;
timeTem.tm_mday=recDay;//刚开始这里成员选成了tm_yday,需要注意,排查了好久,yday是代表一年中的第几天,mday代表一月中的第几天
timeTem.tm_hour=recHour;
timeTem.tm_min=recMin;
timeTem.tm_sec=recSec;
*time=mktime(&timeTem);//将时间结构体用mktime函数转化为自1970年1月1日以来持续时间的秒数 (为什么用一个错误的时间测试,mktime不会返回-1,欢迎懂的大佬留言)
}
车类型是否为规定类型之一,类型、车牌号数据长度是否为四位(这里用冒号的位置进行判断)。时间是否合法(月份对应天数是否准确,,2月份还需考虑闰年;时分秒对应进制有误);
uint8_t Dat_Check(char *type,char *carNum)//判定接收数据正误 格式 时间
{
recMon+=1;//月份在上面为了转换减了1 这里需要加回来 recYear同理
recYear-=100;
if(strcmp(type,"CNBR")!=0 && strcmp(type,"VNBR")!=0)//如果类型不是其中之一(判断类型)
{
return 1;
}
if(recDatBuff[0][4]!=':' || recDatBuff[1][4]!=':')//数组最后一个是否为:(判断格式)
{
return 1;
}
if(recMon>12 || recMon<0)//判断时间合法性 下同 很好理解
{
return 1;
}
else if(recMon==2)//2月
{
if(recYear%4==0)//闰年
{
if(recDay>28 ||recDay<0)
{
return 1;
}
}
else//非闰年
{
if(recDay>29 ||recDay<0)
{
return 1;
}
}
}
else if(recMon==1 || recMon==3 || recMon==5 || recMon==7 || recMon==8 || recMon==10 || recMon==12)//大月
{
if(recDay>31 ||recDay<0)
{
return 1;
}
}
else if(recMon==4 || recMon==6 || recMon==9 || recMon==11)//小月
{
if(recDay>30 ||recDay<0)
{
return 1;
}
}
if(recHour>23 || recHour<0)//时
{
return 1;
}
if(recMin>59 || recMin<0)//分
{
return 1;
}
if(recSec>59 || recSec<0)//秒
{
return 1;
}
return 0;//无误返回0
}
接收数据判定无误后进入数据更新步骤。首先判断车牌号是否已经存在,如不存在则为进入,进入时需要判断是否有空余车位,有则将接收数据保存在数组中,并更新车位信息;如果车牌号存在则为离开,离开需判断时间是否大于车辆到达时间,若合法则将离开车辆信息传递给结算函数进行结算并更新车位信息,不合法返回Error。
void Car_Change(char *type,char *carNum,time_t *time,char *str)//Car数组改变
{
uint8_t i;
uint8_t dir=1;//方向标志位 1为进 0为出
uint8_t outcar_dex;//离开车辆下标
for(i=0;i<9;i++)//判断停车场是否存在该车
{
if(strcmp(carNum,car[i].num)==0)//如存在
{
dir=0;//方向为出
outcar_dex=i;//记录下标
if(*time<car[outcar_dex].reach)//与到达时间对比 判断时间是否合法
{
printf("Error\n");
return;
}
break;
}
}
if(dir==1)//in
{
if(IDLE==0)//无空闲车位
{
return;
}
if(strcmp(type,"CNBR")==0)//如果车辆类型为CNBR
{
CNBR++;
IDLE--;
car[car_dex].type_price=CNBR_Price;//存储价格
}
else if(strcmp(type,"VNBR")==0)//如果车辆类型为VNBR
{
VNBR++;
IDLE--;
car[car_dex].type_price=VNBR_Price;//存储价格
}
strcpy(car[car_dex].type,type);//存储车类型
strcpy(car[car_dex].num,carNum);//存储车牌号
strcpy(car[car_dex].reach_time,str);//存储车到达时间字符串
car[car_dex].reach=*time;//存储车到达时间
car_dex++;//下标++ 为储存下一辆车做准备
}
else//out
{
if(strcmp(type,"CNBR")==0)
{
CNBR--;
IDLE++;
}
else if(strcmp(type,"VNBR")==0)
{
VNBR--;
IDLE++;
}
strcpy(car[outcar_dex].leave_time,str);//存储车离开时间字符串
car[outcar_dex].leave=*time;//存储车离开时间
Settle_Accounts(car[outcar_dex]);//结算
for(i=outcar_dex;i<car_dex;i++)//存储数组前移
{
car[i]=car[i+1];
}
car_dex--;//下标-- 为储存下一辆车做准备
}
}
打印到达和离开信息,并用difftime函数计算时间差,单位为秒,再将时间单位化为小时,最后计算费用并将时间和费用信息进行打印。
void Settle_Accounts(struct car outCar)//结账
{
printf("%s:%s:%s\n",outCar.type,outCar.num,outCar.reach_time);//串口打印到达信息
printf("%s:%s:%s\n",outCar.type,outCar.num,outCar.leave_time);//串口打印离开信息
outCar.dftime=difftime(outCar.leave,outCar.reach)/60/60;//difftime函数返回两时间的差值 单位为秒
if(outCar.dftime==0)outCar.dftime=1;//不足一小时记为一小时
outCar.EndPrice=outCar.dftime*outCar.type_price; //计算费用
printf("%s:%s:%d:%.2f\n\n",outCar.type,outCar.num,outCar.dftime,outCar.EndPrice);//串口打印时长及费用信息
}
在主循环之前需要做好初始化工作。
1.要先重置定时器更新标志位(TIMX->SR=0),否则程序运行后将立刻进入定时器中断函数。
2.开启串口接收中断
3.LCD初始化显示
4.PWM初始化
主循环逻辑大体就是实时更新LCD和LED,并检测是否需要对数据进行转换,转换完后判断数据是否合法,合法的话是车辆进入还是车辆离开。
int main(void)
{
/* USER CODE BEGIN 1 */
time_t time;//保存传输的时间
char type[10];//保存传输的类型
char carNum[20];//车牌号
char timStr[60];//时间字符串
// memset(&time, 0, sizeof(time));
/* 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_TIM3_Init();
MX_USART1_UART_Init();
MX_TIM6_Init();
/* USER CODE BEGIN 2 */
LCD_Init();//LCD初始化
LCD_Init_Show();//LCD初始化显示
HAL_UART_Receive_IT(&huart1,&recDat,sizeof(recDat));//开启串口接收中断
TIM6->SR=0;//中断标志位清零
LED_Close[2]=1;//LED2默认为灭
HAL_TIM_PWM_Stop(&htim3,TIM_CHANNEL_2);//PWM关闭
HAL_GPIO_WritePin(GPIOA,GPIO_PIN_7,GPIO_PIN_RESET);//输出持续低电平
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
KEY_Scan();//按键扫描
LCD_Refresh(1);//LCD更新显示
if(switch_flag==1 && !Error)//接收数组转换标志为1并且无错误就继续执行
{
switch_flag=0;//转换标志位重置
Switch_RecBuff(type,carNum,&time,timStr);//进行数据转换
Error=Dat_Check(type,carNum);//判定接收数据是否合法
if(!Error)//如果无错误
{
Car_Change(type,carNum,&time,timStr);//对存储车辆信息进行更新
}
else//接收数据不合法
{
Error=0;//重置错误标志位
printf("Error\n");//串口输出提示信息
}
LED_Change();
}
else if(Error)//接收数据长度不符
{
Error=0;//重置错误标志位
printf("Error\n");//串口输出提示信息
}
LED_AllClose(LED_Close);//LED显示更新
}
/* USER CODE END 3 */
}
以上就是全部内容,如有错误请批评指正。