野火串口调试助手PID功能(文末有工程链接)

一、项目功能介绍

野火串口调试助手PID功能是一款多功能调试助手,不仅能看数据的波形,还能通过上位机给单片机发送一些PID的参数来进行动态调节,避免了繁琐的反复的下载程序验证,大概的移植和使用方法如下:

一:移植protocol.c 和 protocol.h文件到工程中,其中的过中

protocol.h


#ifndef __PID_PROTOCOL_H__
#define __PID_PROTOCOL_H__


#include "stdint.h"

#ifdef _cplusplus
extern "C" {
#endif   


/* 数据接收缓冲区大小 */
#define PROT_FRAME_LEN_RECV  128

/* 校验数据的长度 */
#define PROT_FRAME_LEN_CHECKSUM    1

#pragma pack (1)  
/* 数据头结构体 */
typedef __packed struct
{
  uint32_t head;    // 包头
  uint8_t ch;       // 通道
  uint32_t len;     // 包长度
  uint8_t cmd;      // 命令
//  uint8_t sum;      // 校验和
  
}packet_head_t;

#pragma pack ()               //作用:取消自定义字节对齐方式。

#define FRAME_HEADER     0x59485A53    // 帧头

/* 通道宏定义 */
#define CURVES_CH1      0x01
#define CURVES_CH2      0x02
#define CURVES_CH3      0x03
#define CURVES_CH4      0x04
#define CURVES_CH5      0x05

/* 指令(下位机 -> 上位机) */
#define SEND_TARGET_CMD      0x01     // 发送上位机通道的目标值
#define SEND_FACT_CMD        0x02     // 发送通道实际值
#define SEND_P_I_D_CMD       0x03     // 发送 PID 值(同步上位机显示的值)
#define SEND_START_CMD       0x04     // 发送启动指令(同步上位机按钮状态)
#define SEND_STOP_CMD        0x05     // 发送停止指令(同步上位机按钮状态)
#define SEND_PERIOD_CMD      0x06     // 发送周期(同步上位机显示的值)

/* 指令(上位机 -> 下位机) */
#define SET_P_I_D_CMD        0x10     // 设置 PID 值
#define SET_TARGET_CMD       0x11     // 设置目标值
#define START_CMD            0x12     // 启动指令
#define STOP_CMD             0x13     // 停止指令
#define RESET_CMD            0x14     // 复位指令
#define SET_PERIOD_CMD       0x15     // 设置周期

/* 空指令 */
#define CMD_NONE             0xFF     // 空指令

/* 索引值宏定义 */
#define HEAD_INDEX_VAL       0x3u     // 包头索引值(4字节)
#define CHX_INDEX_VAL        0x4u     // 通道索引值(1字节)
#define LEN_INDEX_VAL        0x5u     // 包长索引值(4字节)
#define CMD_INDEX_VAL        0x9u     // 命令索引值(1字节)

#define EXCHANGE_H_L_BIT(data)      ((((data) << 24) & 0xFF000000) |\
                                     (((data) <<  8) & 0x00FF0000) |\
                                     (((data) >>  8) & 0x0000FF00) |\
                                     (((data) >> 24) & 0x000000FF))     // 交换高低字节

#define COMPOUND_32BIT(data)        (((*(data-0) << 24) & 0xFF000000) |\
                                     ((*(data-1) << 16) & 0x00FF0000) |\
                                     ((*(data-2) <<  8) & 0x0000FF00) |\
                                     ((*(data-3) <<  0) & 0x000000FF))      // 合成为一个字
                                     
/**
 * @brief   接收数据处理
 * @param   *data:  要计算的数据的数组.
 * @param   data_len: 数据的大小
 * @return  void.
 */
void protocol_data_recv(uint8_t *data, uint16_t data_len);

/**
 * @brief   初始化接收协议
 * @param   void
 * @return  初始化结果.
 */
int32_t protocol_init(void);

/**
 * @brief   接收的数据处理
 * @param   void
 * @return  -1:没有找到一个正确的命令.
 */
int8_t receiving_process(void);

/**
  * @brief 设置上位机的值
  * @param cmd:命令
  * @param ch: 曲线通道
  * @param data:参数指针
  * @param num:参数个数
  * @retval 无
  */
void set_computer_value(uint8_t cmd, uint8_t ch, void *data, uint8_t num);


/**
  * @brief  最终发送数据输出的底层接口函数,需要用户实现
  * @param data : 发送的数据
  * @param num: 参数大小
  * @retval none
  */
__weak void port_send_data_to_computer(void *data, uint8_t num);

/**
  * @brief  处理上位机发送过来的PID参数
  * @param  p : PID p参数
  * @param  i: PID i参数
  * @param  d : PID d参数
  * @retval none
  * @note  需要用户实现
  */
__weak void set_pid_paramter_cmd(float p, float i, float d);

/**
 * @brief:  处理上位机开始PID控制命令
 * @param:  none
 * @retval: none
 * @note:   需要用户实现
 */
__weak void pid_start_cmd(void);


/**
 * @brief:  处理上位机停止PID控制命令
 * @param:  none
 * @retval: none
 * @note:   需要用户实现
 */
__weak void pid_stop_cmd(void);


/**
 * @brief:  处理上位机复位命令
 * @param:  none
 * @retval: none
 * @note:   需要用户实现
 */
__weak void pid_reset_cmd(void);

/**
 * @brief:  处理上位机设置目标值命令
 * @param:  actual_val - 目标值
 * @retval: none
 * @note:   需要用户实现
 */
__weak void set_pid_actual_val_cmd(int actual_val);

/**
 * @brief: 设置PID周期数  
 * @param: period - 周期数
 * @retval:  
 * @note:  需要用户实现
 */
__weak void set_pid_period_cmd(uint32_t period);

#ifdef _cplusplus
}
#endif   

#endif

protocol.c

/*
 * @brief:  PID调试助手协议  
 * @author: Ares
 * @date: 2021-05-xx
 * @version: v1.0.0
 * @copyright(c) 2020: OptoMedic Technologies Co.,Ltd. All rights reserved
 */


#include "pid-protocol.h"
#include "string.h"
#include "sys.h"
//#include "insufflator.h"
//#include "pid.h"
//#include "insufflator-pid.h"

struct prot_frame_parser_t
{
	uint8_t *recv_ptr;
	uint16_t r_oft;
	uint16_t w_oft;
	uint16_t frame_len;
	uint16_t found_frame_head;
};

static struct prot_frame_parser_t parser;

static uint8_t recv_buf[PROT_FRAME_LEN_RECV];

/**
  * @brief 计算校验和
  * @param ptr:需要计算的数据
  * @param len:需要计算的长度
  * @retval 校验和
  */
uint8_t check_sum(uint8_t init, uint8_t *ptr, uint8_t len)
{
	uint8_t sum = init;

	while (len--)
	{
		sum += *ptr;
		ptr++;
	}

	return sum;
}

/**
 * @brief   得到帧类型(帧命令)
 * @param   *frame:  数据帧
 * @param   head_oft: 帧头的偏移位置
 * @return  帧长度.
 */
static uint8_t get_frame_type(uint8_t *frame, uint16_t head_oft)
{
	return (frame[(head_oft + CMD_INDEX_VAL) % PROT_FRAME_LEN_RECV] & 0xFF);
}

/**
 * @brief   得到帧长度
 * @param   *buf:  数据缓冲区.
 * @param   head_oft: 帧头的偏移位置
 * @return  帧长度.
 */
static uint16_t get_frame_len(uint8_t *frame, uint16_t head_oft)
{
	return ((frame[(head_oft + LEN_INDEX_VAL + 0) % PROT_FRAME_LEN_RECV] << 0) |
			(frame[(head_oft + LEN_INDEX_VAL + 1) % PROT_FRAME_LEN_RECV] << 8) |
			(frame[(head_oft + LEN_INDEX_VAL + 2) % PROT_FRAME_LEN_RECV] << 16) |
			(frame[(head_oft + LEN_INDEX_VAL + 3) % PROT_FRAME_LEN_RECV] << 24)); // 合成帧长度
}

/**
 * @brief   获取 crc-16 校验值
 * @param   *frame:  数据缓冲区.
 * @param   head_oft: 帧头的偏移位置
 * @param   head_oft: 帧长
 * @return  帧长度.
 */
static uint8_t get_frame_checksum(uint8_t *frame, uint16_t head_oft, uint16_t frame_len)
{
	return (frame[(head_oft + frame_len - 1) % PROT_FRAME_LEN_RECV]);
}

/**
 * @brief   查找帧头
 * @param   *buf:  数据缓冲区.
 * @param   ring_buf_len: 缓冲区大小
 * @param   start: 起始位置
 * @param   len: 需要查找的长度
 * @return  -1:没有找到帧头,其他值:帧头的位置.
 */
static int32_t recvbuf_find_header(uint8_t *buf, uint16_t ring_buf_len, uint16_t start, uint16_t len)
{
	uint16_t i = 0;

	for (i = 0; i < (len - 3); i++)
	{
		if (((buf[(start + i + 0) % ring_buf_len] << 0) |
			 (buf[(start + i + 1) % ring_buf_len] << 8) |
			 (buf[(start + i + 2) % ring_buf_len] << 16) |
			 (buf[(start + i + 3) % ring_buf_len] << 24)) == FRAME_HEADER)
		{
			return ((start + i) % ring_buf_len);
		}
	}
	return -1;
}

/**
 * @brief   计算为解析的数据长度
 * @param   *buf:  数据缓冲区.
 * @param   ring_buf_len: 缓冲区大小
 * @param   start: 起始位置
 * @param   end: 结束位置
 * @return  为解析的数据长度
 */
static int32_t recvbuf_get_len_to_parse(uint16_t frame_len, uint16_t ring_buf_len, uint16_t start, uint16_t end)
{
	uint16_t unparsed_data_len = 0;

	if (start <= end)
		unparsed_data_len = end - start;
	else
		unparsed_data_len = ring_buf_len - start + end;

	if (frame_len > unparsed_data_len)
		return 0;
	else
		return unparsed_data_len;
}

/**
 * @brief   接收数据写入缓冲区
 * @param   *buf:  数据缓冲区.
 * @param   ring_buf_len: 缓冲区大小
 * @param   w_oft: 写偏移
 * @param   *data: 需要写入的数据
 * @param   *data_len: 需要写入数据的长度
 * @return  void.
 */
static void recvbuf_put_data(uint8_t *buf, uint16_t ring_buf_len, uint16_t w_oft,
							 uint8_t *data, uint16_t data_len)
{
	if ((w_oft + data_len) > ring_buf_len) // 超过缓冲区尾
	{
		uint16_t data_len_part = ring_buf_len - w_oft; // 缓冲区剩余长度

		/* 数据分两段写入缓冲区*/
		memcpy(buf + w_oft, data, data_len_part);					 // 写入缓冲区尾
		memcpy(buf, data + data_len_part, data_len - data_len_part); // 写入缓冲区头
	}
	else
		memcpy(buf + w_oft, data, data_len); // 数据写入缓冲区
}

/**
 * @brief   查询帧类型(命令)
 * @param   *data:  帧数据
 * @param   data_len: 帧数据的大小
 * @return  帧类型(命令).
 */
static uint8_t protocol_frame_parse(uint8_t *data, uint16_t *data_len)
{
	uint8_t frame_type = CMD_NONE;
	uint16_t need_to_parse_len = 0;
	int16_t header_oft = -1;
	uint8_t checksum = 0;

	need_to_parse_len = recvbuf_get_len_to_parse(parser.frame_len, PROT_FRAME_LEN_RECV, parser.r_oft, parser.w_oft); // 得到为解析的数据长度
	if (need_to_parse_len < 9)																						 // 肯定还不能同时找到帧头和帧长度
		return frame_type;

	/* 还未找到帧头,需要进行查找*/
	if (0 == parser.found_frame_head)
	{
		/* 同步头为四字节,可能存在未解析的数据中最后一个字节刚好为同步头第一个字节的情况,
           因此查找同步头时,最后一个字节将不解析,也不会被丢弃*/
		header_oft = recvbuf_find_header(parser.recv_ptr, PROT_FRAME_LEN_RECV, parser.r_oft, need_to_parse_len);
		if (0 <= header_oft)
		{
			/* 已找到帧头*/
			parser.found_frame_head = 1;
			parser.r_oft = header_oft;

			/* 确认是否可以计算帧长*/
			if (recvbuf_get_len_to_parse(parser.frame_len, PROT_FRAME_LEN_RECV,
										 parser.r_oft, parser.w_oft) < 9)
				return frame_type;
		}
		else
		{
			/* 未解析的数据中依然未找到帧头,丢掉此次解析过的所有数据*/
			parser.r_oft = ((parser.r_oft + need_to_parse_len - 3) % PROT_FRAME_LEN_RECV);
			return frame_type;
		}
	}

	/* 计算帧长,并确定是否可以进行数据解析*/
	if (0 == parser.frame_len)
	{
		parser.frame_len = get_frame_len(parser.recv_ptr, parser.r_oft);
		if (need_to_parse_len < parser.frame_len)
			return frame_type;
	}

	/* 帧头位置确认,且未解析的数据超过帧长,可以计算校验和*/
	if ((parser.frame_len + parser.r_oft - PROT_FRAME_LEN_CHECKSUM) > PROT_FRAME_LEN_RECV)
	{
		/* 数据帧被分为两部分,一部分在缓冲区尾,一部分在缓冲区头 */
		checksum = check_sum(checksum, parser.recv_ptr + parser.r_oft,
							 PROT_FRAME_LEN_RECV - parser.r_oft);
		checksum = check_sum(checksum, parser.recv_ptr, parser.frame_len - PROT_FRAME_LEN_CHECKSUM + parser.r_oft - PROT_FRAME_LEN_RECV);
	}
	else
	{
		/* 数据帧可以一次性取完*/
		checksum = check_sum(checksum, parser.recv_ptr + parser.r_oft, parser.frame_len - PROT_FRAME_LEN_CHECKSUM);
	}

	if (checksum == get_frame_checksum(parser.recv_ptr, parser.r_oft, parser.frame_len))
	{
		/* 校验成功,拷贝整帧数据 */
		if ((parser.r_oft + parser.frame_len) > PROT_FRAME_LEN_RECV)
		{
			/* 数据帧被分为两部分,一部分在缓冲区尾,一部分在缓冲区头*/
			uint16_t data_len_part = PROT_FRAME_LEN_RECV - parser.r_oft;
			memcpy(data, parser.recv_ptr + parser.r_oft, data_len_part);
			memcpy(data + data_len_part, parser.recv_ptr, parser.frame_len - data_len_part);
		}
		else
		{
			/* 数据帧可以一次性取完*/
			memcpy(data, parser.recv_ptr + parser.r_oft, parser.frame_len);
		}
		*data_len = parser.frame_len;
		frame_type = get_frame_type(parser.recv_ptr, parser.r_oft);

		/* 丢弃缓冲区中的命令帧*/
		parser.r_oft = (parser.r_oft + parser.frame_len) % PROT_FRAME_LEN_RECV;
	}
	else
	{
		/* 校验错误,说明之前找到的帧头只是偶然出现的废数据*/
		parser.r_oft = (parser.r_oft + 1) % PROT_FRAME_LEN_RECV;
	}
	parser.frame_len = 0;
	parser.found_frame_head = 0;

	return frame_type;
}

/**
 * @brief   接收数据处理
 * @param   *data:  要计算的数据的数组.
 * @param   data_len: 数据的大小
 * @return  void.
 */
void protocol_data_recv(uint8_t *data, uint16_t data_len)
{
	recvbuf_put_data(parser.recv_ptr, PROT_FRAME_LEN_RECV, parser.w_oft, data, data_len); // 接收数据
	parser.w_oft = (parser.w_oft + data_len) % PROT_FRAME_LEN_RECV;						  // 计算写偏移
}

/**
 * @brief   初始化接收协议
 * @param   void
 * @return  初始化结果.
 */
int32_t protocol_init(void)
{
	memset(&parser, 0, sizeof(struct prot_frame_parser_t));

	/* 初始化分配数据接收与解析缓冲区*/
	parser.recv_ptr = recv_buf;

	return 0;
}


/**
 * @brief   接收的数据处理
 * @param   void
 * @return  -1:没有找到一个正确的命令.
 */
int8_t receiving_process(void)
{
	uint8_t cmd_type = CMD_NONE; // 命令类型
	uint8_t frame_data[128];	 // 要能放下最长的帧
	uint16_t frame_len = 0;		 // 帧长度

	while (1)
	{
		cmd_type = protocol_frame_parse((uint8_t *)frame_data, &frame_len);
		switch (cmd_type)
		{
		case CMD_NONE:
		{
			return -1;
		}

		case SET_P_I_D_CMD:
		{
			uint32_t temp0 = COMPOUND_32BIT(&frame_data[13]);
			uint32_t temp1 = COMPOUND_32BIT(&frame_data[17]);
			uint32_t temp2 = COMPOUND_32BIT(&frame_data[21]);

			float p_temp, i_temp, d_temp;

			p_temp = *(float *)&temp0;
			i_temp = *(float *)&temp1;
			d_temp = *(float *)&temp2;

			set_pid_paramter_cmd(p_temp, i_temp, d_temp);
			/* 在这里填写自己的处理函数 */
		}
		break;

		case SET_TARGET_CMD:
		{
			int actual_temp = COMPOUND_32BIT(&frame_data[13]); // 得到数据

			/* 在这里填写自己的处理函数 */
			set_pid_actual_val_cmd(actual_temp);
		}
		break;

		case START_CMD:
		{
			/* 在这里填写自己的处理函数 */
			pid_start_cmd();
		}
		break;

		case STOP_CMD:
		{
			/* 在这里填写自己的处理函数 */
			pid_stop_cmd();
		}
		break;

		case RESET_CMD:
		{
			/* 在这里填写自己的处理函数 */
			//NVIC_SystemReset(); // 复位系统
			pid_reset_cmd();
		}
		break;

		case SET_PERIOD_CMD:
		{
			uint32_t temp = COMPOUND_32BIT(&frame_data[13]);   //周期数

			/* 在这里填写自己的处理函数 */
			set_pid_period_cmd(temp);
		}
		break;

		default:
			return -1;
		}
	}
}

__weak void set_pid_paramter_cmd(float p, float i, float d)
{

}

__weak void set_pid_actual_val_cmd(int actual_val)
{

}

__weak void set_pid_period_cmd(uint32_t period)
{

}

__weak void pid_start_cmd(void)
{

}

__weak void pid_stop_cmd(void)
{

}

__weak void pid_reset_cmd(void)
{

}

//__weak void port_send_data_to_computer(void *data, uint8_t num)

__weak void UART_Send_Byte(char Byte)
{
	
}
void port_send_data_to_computer(void *data, uint8_t num)
{
	u8 i=0;
	char *tp=(char *)data;
	for(i=0;i<num;i++)
	{		
		UART_Send_Byte(*tp++);        //通过库函数  发送数据 
		//等待发送完成。   检测 USART_FLAG_TC 是否置1;    //见库函数 P359 介绍       
	}

}

/**
  * @brief 设置上位机的值
  * @param cmd:命令
  * @param ch: 曲线通道
  * @param data:参数指针
  * @param num:参数个数
  * @retval 无
  */
void set_computer_value(uint8_t cmd, uint8_t ch, void *data, uint8_t num)
{
	static packet_head_t set_packet;
	
	uint8_t sum = 0; // 校验和
	num *= 4;		 // 一个参数 4 个字节
	
	set_packet.head = FRAME_HEADER; // 包头 0x59485A53
	set_packet.len = 0x0B + num;	// 包长
	set_packet.ch = ch;				// 设置通道
	set_packet.cmd = cmd;			// 设置命令

	sum = check_sum(0, (uint8_t *)&set_packet, sizeof(set_packet)); // 计算包头校验和
	sum = check_sum(sum, (uint8_t *)data, num);						// 计算参数校验和
	
	port_send_data_to_computer(&set_packet, sizeof(set_packet));
//	HAL_UART_Transmit(&UartHandle, (uint8_t *)&set_packet, sizeof(set_packet), 0xFFFFF); // 发送数据头
//	HAL_UART_Transmit(&UartHandle, (uint8_t *)data, num, 0xFFFFF);						 // 发送参数
	port_send_data_to_computer(data, num);
//	HAL_UART_Transmit(&UartHandle, (uint8_t *)&sum, sizeof(sum), 0xFFFFF);				 // 发送校验和
	port_send_data_to_computer(&sum, sizeof(sum));
}

/**********************************************************************************************/

重要的就是对这几个弱函数的改写,当然你也可以使用函数指针来改,

//处理上位机发送过来的PID参数
__weak void set_pid_paramter_cmd(float p, float i, float d)

__weak void set_pid_actual_val_cmd(int actual_val)

__weak void set_pid_period_cmd(uint32_t period)

__weak void pid_start_cmd(void)

__weak void pid_stop_cmd(void)

__weak void pid_reset_cmd(void)

__weak void UART_Send_Byte(char Byte)

最重要的就是

__weak void UART_Send_Byte(char Byte)

__weak void set_pid_paramter_cmd(float p, float i, float d)

这两个了

改写其实也非常简单,只需将该函数的__weak去调,然后加入到需要的文件中。我是加到main.c

里面写自己的发送一字节的函数。PID的参数处理和这个一样,只需将对应的值给到我们想要的值即可。我写的实例如下

#include "led.h"
#include "delay.h"
#include "key.h"
#include "sys.h"
#include "usart.h"
#include "timer.h"
#include "oled.h"
#include "string.h"
#include "protocol.h"

float P,I,D;
//串口发送一字节函数的改写
void UART_Send_Byte(char Byte)
{
	Usart_SendByte(USART1,Byte);
}
//PID参数的在线修改改写
void set_pid_paramter_cmd(float p, float i, float d){
	P= p;
	I = i;
	D = d;
}




到此基本的配置工作就完成了,就开始准备波形的显示和串口中断的

然后把三个函数放在对应的执行位置,缺一不可


/**
 * @brief   接收数据处理
 * @param   *data:  要计算的数据的数组.
 * @param   data_len: 数据的大小
 * @return  void.
 */
void protocol_data_recv(uint8_t *data, uint16_t data_len);
/**
 * @brief   初始化接收协议
 * @param   void
 * @return  初始化结果.
 */
int32_t protocol_init(void);

/**
 * @brief   接收的数据处理
 * @param   void
 * @return  -1:没有找到一个正确的命令.
 */
int8_t receiving_process(void);

int32_t protocol_init(void);

放到while(1)之前,就当是一个外设的初始化,

int8_t receiving_process(void);

放到while(1)中,时刻处理来自中断的数据

 int main(void)
 {		
    //放在这里即可
	protocol_init();
   	while(1)
	{
	   receiving_process();
	}	 
 
}
	

void protocol_data_recv(uint8_t *data, uint16_t data_len);

放到串口中断函数中,将接受到的一个字节作为实际参数放到protocol_data_recv()中并解析

void USART1_IRQHandler(void)                	//串口1中断服务程序
	{
	u8 Res;
	if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)  //接收中断(接收到的数据必须是0x0d 0x0a结尾)
		{
			Res =USART_ReceiveData(USART1);	//读取接收到的数据 	
			
			USART_SendData(USART1,Res);
			protocol_data_recv(&Res,1);
    } 
} 

接下来就是发送波形的函数了

/**
  * @brief 设置上位机的值
  * @param cmd:命令
  * @param ch: 曲线通道
  * @param data:参数指针
  * @param num:参数个数
  * @retval 无
  */
void set_computer_value(uint8_t cmd, uint8_t ch, void *data, uint8_t num)

使用案例:

将set和num1改改成自己想要查看的波形数据就行了,当然这是一个通道的数据,好像一共有五个通道来着

set_computer_value(SEND_TARGET_CMD,CURVES_CH1,&set,1);
set_computer_value(SEND_FACT_CMD,CURVES_CH1,&num1,1);

视频展示:当然你也 可以可以通过OLED来看效果

视频​​​​​​

基本的使用就是这样了,感谢观看

最后给上工程的链接:

链接:https://pan.baidu.com/s/1pxyy6dn5iZ1YCMjcMheFBg?pwd=l2m0 
提取码:l2m0

  • 15
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 11
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值