STM32综合-基于HAL库(第十二届蓝桥杯嵌入式省赛)


前言

相关说明:

开发板:CT117E-M4(STM32G431RB 蓝桥杯嵌入式比赛板)
开发环境: CubeMX+Keil5
涉及题目:第十二届蓝桥杯嵌入式省赛
题目难点:停车管理系统逻辑编写;数据接收,解析,判定,更新。
代码思路:(使用usart1时需要修改引脚为PA8 PA9 PA10)串口接收到数据后,先判定数据接收长度是否正确,即每接收到一个字节都重新开启定时器,最后一字节数据接收完且进入定时器中断后判断接收数据长度,准确无误则进行数据解析;解析时将数据分段保存:车类型,车牌号,时间;保存好后再对数据的合法性进行判定,车类型是否为规定类型之一,类型、车牌号数据长度是否为四位。时间是否合法(月份对应天数,时分秒对应进制);最后是存储数据的更新,车牌号是否已经存在?不存在的话判断是否还有空余车位?有则将类型、车牌号、时间等数据存储在数组中;存在的话考虑现在接收时间是否大于到达时间?时间合法则对存储在数组组中的数据进行计算和输出。


CubeMX配置、主要函数代码及说明:

一、CubeMX配置(第十二届省赛完整版)

1.使能外部高速时钟:在这里插入图片描述

2.配置时钟树:在这里插入图片描述

3.GPIO:

在这里插入图片描述

4.TIM3(PWM):在这里插入图片描述在这里插入图片描述

5.TIM6(串口在接收到最后一字节数据5us后进入定时器中断函数):在这里插入图片描述

6.USART1:在这里插入图片描述

7.NVIC(中断配置):在这里插入图片描述

二、代码相关定义、声明

1.函数声明

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显示更新

2.宏定义

#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

3.变量定义

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的输出。)

1.按键扫描

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);
			}
		}
	}
}

接下来是数据的处理,分别为接收,解析,判定和更新。

2.串口接收中断、定时器中断(接收)

串口在接收到一个字节数据时进入串口中断函数,每次进入串口中断函数都需要重新开启判定,在最后一字节数据接收完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));//重新开启串口接收中断
}

3.数据解析

将保存在二维数组中的数据进行字符串组合,数组中第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,欢迎懂的大佬留言)
}

4.判定数据正误

车类型是否为规定类型之一,类型、车牌号数据长度是否为四位(这里用冒号的位置进行判断)。时间是否合法(月份对应天数是否准确,,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
}

5.数据更新

接收数据判定无误后进入数据更新步骤。首先判断车牌号是否已经存在,如不存在则为进入,进入时需要判断是否有空余车位,有则将接收数据保存在数组中,并更新车位信息;如果车牌号存在则为离开,离开需判断时间是否大于车辆到达时间,若合法则将离开车辆信息传递给结算函数进行结算并更新车位信息,不合法返回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--;//下标-- 为储存下一辆车做准备
	}
}

6.结算

打印到达和离开信息,并用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);//串口打印时长及费用信息
}

7.Main函数

在主循环之前需要做好初始化工作。
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 */
}

四、实验结果

1.数据长度有误

a.数据过长
在这里插入图片描述
b.数据过短
在这里插入图片描述
c.返回
在这里插入图片描述

2.数据不合法

a.类型错误
在这里插入图片描述
b.时间不合法
在这里插入图片描述
c.离开时间小于到达时间
在这里插入图片描述

在这里插入图片描述
d.返回
在这里插入图片描述

3.数据正常

a.输入车辆到达信息
在这里插入图片描述
b.输入车辆离开信息
在这里插入图片描述
c.返回
在这里插入图片描述

五、源码(转载请注明出处)

在这里插入图片描述


总结

以上就是全部内容,如有错误请批评指正。

  • 13
    点赞
  • 80
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 28
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

AゞOctopus๊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值