FreeRTOS学习笔记-第一个项目工程(续)

续上一篇博客《FreeRTOS学习笔记-第一个项目工程》:
https://blog.youkuaiyun.com/shuang_sz/article/details/145697273
补充模拟IIC、TF卡文件系统的部分

1 模拟IIC

大多数程序猿都喜欢用模拟IIC来操作IIC设备,这在裸机系统中完全没有问题,但在实时操作系统中时,情况有所不同,因为EEPROM器件的时钟频率一般最高400KHz,而单片机模拟的时钟可能不会那么准确,也许达不到400KHz,就按400KHz算,假设读写50字节的数据,那么一次读写操作大约需要消耗13ms,而OS的调度频率一般10ms,因此在读写IIC的过程中,调度器可能随时打断读写过程,导致本次通讯失败,而为了避免这个问题,我采用的方法时,操作IIC时挂起任务调度器,操作完成后再恢复调度器。

1.1 规划EEPROM的存储内容

项目开始前一定要先规划好EEPROM中数据的存放规则,否则项目进行中需要修改的话工作量会比较大,本项目采用了以下方法:

  1. 用存储器的第一个单元标志该存储器有没有被初始化过(有没有写入过该项目的信息),若没有则擦除所有使用到的存储单元,并存入系统默认值,然后将该标志单元写入一个特殊字节的数据,表示该存储器已被初始化过,后面存放的数据都是可信的。
  2. 存储器后面存放的数据都封装到结构体中,避免读写零散的数据,一是要记住每个数据的地址,二是可能造成越界,误改了相邻单元的正常数据
  3. 本项目设计了一个存放系统信息的结构体,和一个系统配置信息的结构体,其实用一个也没问题,另外再创建更多的项目相关的结构体也没问题

1.2 创建EEPROM的数据结构体

在 work.h 文件中添加下面一些宏和两个结构体的定义:

/********************* EEPROM存储空间分配 每页128字节****************************************/

#define EEPROM_FLAG					0xAA								//有效标志
#define EEPROM_FLAG_ADDR			0									//存储器有效标志的存放地址
#define EEPROM_SYS_ADDR				10									//系统信息的起始地址

#define EEPROM_ID_SIZE				16									//定义设备ID的最大长度
#define EEPROM_ZONE_SIZE			16									//定义时区的最大长度
typedef struct
{
	char							ID[EEPROM_ID_SIZE + 1];				//设备ID
	char							Time_Zone[EEPROM_ZONE_SIZE + 1];	//时区
	char							Path_Spec[8];						//SD卡中规格文件夹的路径
	char							Path_Log[8];						//SD卡中Log文件夹的路径
}SystemInfo_t;

typedef struct
{
	unsigned char 					Debug_EN;
	unsigned char					Log_EN;
	//...
}SystemConfig_t;

extern SystemInfo_t					DevInfo;							//声明系统信息变量
extern SystemConfig_t				SysConfig;							//声明系统配置信息变量

1.3 初始化IIC

初始化IIC即创建EEPROM对象,打开init.c文件,首先定义相关的变量,然后调用MyIIC.lib中的函数创建EEPROM对象。
打开 init.c 文件,添加下面代码,定义EEPROM指针和两个结构体:

IIC_Device_t 		*EEPROM;											//定义EEPROM设备
SystemInfo_t		DevInfo;											//定义系统信息变量
SystemConfig_t		SysConfig;											//定义系统配置信息变量

还是在该文件中,添加如下代码,创建EEPROM指针并初始化:

//EEPROM初始化
void EEPROM_Init(void)
{
	uint8_t status;
	EEPROM = NewMyIIC(IIC_MCU_Type_F4, 168, 0xA0, 256, "PE0", "PE1");				//创建EEPROM设备
	if (EEPROM == NULL)
	{
		Error_Handle(1, "Creat EEPROM failed!");
	}
	
	if (IIC_ReadBytes(EEPROM, &status, EEPROM_FLAG_ADDR, 1) != OK)					//读出1个字节,存储器有效标志
	{
		Error_Handle(1, "Failed to read the FLAG!");
	}
	
	if (status != EEPROM_FLAG)														//存储器未初始化过,重新初始化
	{
		Clear(DevInfo.ID);
		Clear(DevInfo.Time_Zone);
		Append(DevInfo.Time_Zone, "CST");											//设置时区默认值
		Clear(DevInfo.Path_Spec);
		Append(DevInfo.Path_Spec, "/Spec");
		Clear(DevInfo.Path_Log);
		Append(DevInfo.Path_Log, "/Log");
		if (IIC_WriteBytes(EEPROM, (uint8_t *)&DevInfo, EEPROM_SYS_ADDR, sizeof(DevInfo)) != OK)//格式化系统信息,未成功写入
		{
			Error_Handle(1, "Failed to init the DevInfo!");
		}
		HAL_Delay(5);
		
		if (IIC_SetBytes(EEPROM, EEPROM_FLAG, EEPROM_FLAG_ADDR, 1) != OK)			//写入有效标志,未成功写入
		{
			Error_Handle(1, "Failed to write the FLAG!");
		}
		HAL_Delay(5);
	}
	if (IIC_ReadBytes(EEPROM, (uint8_t *)&DevInfo, EEPROM_SYS_ADDR, sizeof(DevInfo)) != OK)			//读出系统信息
	{
		Error_Handle(1, "Failed to read the DevInfo!");
	}
}
MSH_INIT_EXPORT(2, EEPROM_Init, Creat EEPROM);

函数 NewMyIIC(IIC_MCU_Type_F4, 168, 0xA0, 256, “PE0”, “PE1”) 用于创建EEPROM对象,传入参数说明如下:

  1. IIC_MCU_Type_F4,单片机类型,枚举,在MyIIC.h中有定义
  2. 168,主频,IIC库会根据该设置的主频将时钟频率调整到400KHz
  3. 0xA0,IIC器件地址
  4. 256,器件容量,单位Kbits,比如24C02,就是2Kbits,24M512就是512Kbits,IIC库会根据该容量值自动判断地址字节宽度(单字节或两字节)页大小(EEPROM不允许跨页写)。
  5. “PE0",SCL的引脚名,IIC库会根据该字符串计算SCL引脚地址
  6. “PE1",SDA的引脚名,IIC库会根据该字符串计算SDA引脚地址
    后面的流程是,先读一次标志字节,若不是设置的有效数据,如0xAA,则写入默认值(出厂初始参数),最后读出所有数据存入结构体中。

1.4 编写EEPROM读写命令

下面以设备ID为例,展示IIC库的用法,打开 work.c 文件,添加下面两条命令:

//命令,从EEPROM中读治具ID
void Get_ID(char * Payload)
{
	if (DevInfo.ID[0] != 0)														//检查是否已设置了ID
	{
		Printf_If_PC("OK,%s,%s\r\n@_@", __func__, DevInfo.ID);
		Printf_If_HMI("%s=\"%s\"%s", HMI_Ctr_Value, DevInfo.ID, HMI_End);
	}
	else
	{
		Printf_If_PC("NG,%s,No ID\r\n@_@", __func__);
		Printf_If_HMI("%s=\"%s\"%s", HMI_Ctr_Value, "No ID", HMI_End);
	}
}
MSH_CMD_EXPORT(Get_ID, Get fixture ID);

//命令,将治具ID写入EEPROM
void Set_ID(char * Payload)
{
	uint8_t ret;
	uint16_t len = StrLen(Payload);
	if ((len < 8) || (len > EEPROM_ID_SIZE))
	{
		Printf_If_PC("NG,%s,Wrong ID,[8 to %dbytes]\r\n@_@", __func__, EEPROM_ID_SIZE);
		Printf_If_HMI("%s=\"Wrong ID,[8 to %dbytes]\"%s", HMI_Ctr_UartRcv, EEPROM_ID_SIZE, HMI_End);
		return;
	}
	if (len < EEPROM_ID_SIZE) len++;											//如果字符串长度小于空间大小时,要多存一个结束符
	
	vTaskSuspendAll();															//下面的操作将用到模拟IIC,需要挂起调度器
	ret = EEPROM->Write((uint8_t *)Payload, EEPROM_SYS_ADDR + offsetof(SystemInfo_t, ID), len);
	xTaskResumeAll();															//操作完成,恢复调度器
	
	if (ret == OK)
	{
		Printf_If_PC("OK,%s\r\n@_@", __func__);
		Printf_If_HMI("%s=\"Set OK!\"%s", HMI_Ctr_UartRcv, HMI_End);
		for (uint8_t i = 0; i < len; i++) DevInfo.ID[i] = Payload[i];			//写入成功后更新全局变量
	}
	else
	{
		Printf_If_PC("NG,%s,Save failure!\r\n@_@", __func__);
		Printf_If_HMI("%s=\"Save failure!\"%s", HMI_Ctr_UartRcv, HMI_End);
	}
}
MSH_CMD_EXPORT(Set_ID, Set fixture ID);

读ID其实是从全局变量的系统信息结构体中直接读取的,并没有操作EEPROM。
设置ID的过程是将新的ID写入EEPROM,若写入成功,则同步更新到全局变量系统信息结构体中。
说明下EEPROM的读写方法:

  1. EEPROM->Write(数据指针,写入地址,写入长度)
  2. EEPROM->Read(数据指针,读取地址,读取长度)
    特别说明:
  3. offsetof,用于计算结构体中的指定的成员的偏移地址
  4. vTaskSuspendAll 用于挂起调度器,使IIC读写不会被打断
  5. xTaskResumeAll 用于恢复调度器,一定要与挂起函数成对使用
    其它的EEPROM操作命令不一一展示。

2 FATFS

2.1 定义一些相关全局变量

打开 work.h 文件,添加下列一些变量声明:

/********************* 文件系统 ****************************************************/

extern FATFS 						Fatfs;										//声明文件系统全局变量
extern const char 					*Root;										//声明根目录路径
extern DIR							Dir;										//声明文件夹全局变量
extern FIL 							File;										//声明文件全局变量
extern const char 					*File_Err[];								//声明文件系统的描述

并不是一定要都定义为全局变量,但这些变量一般都只在一个任务中使用,定义为全局可节省内存,不必在每个使用文件系统的函数中都定义一遍。
新建一个 file.c 文件,添加如下代码:

const char 	*File_Err[] = {
	"E00: Succeeded!",
	"E01: A hard error occurred in the low level disk I/O layer",
	"E02: Assertion failed",
	"E03: The physical drive cannot work",
	"E04: Could not find the file",
	"E05: Could not find the path",
	"E06: The path name format is invalid",
	"E07: Access denied due to prohibited access or directory full",
	"E08: Access denied due to prohibited access",
	"E09: The file/directory object is invalid",
	"E10: The physical drive is write protected",
	"E11: The logical drive number is invalid",
	"E12: The volume has no work area",
	"E13: There is no valid FAT volume",
	"E14: The f_mkfs() aborted due to any problem",
	"E15: Could not get a grant to access the volume within defined period",
	"E16: The operation is rejected according to the file sharing policy",
	"E17: LFN working buffer could not be allocated",
	"E18: Number of open files > _FS_LOCK",
	"E19: Given parameter is invalid"
};

const char 	*Root 		= "0:";										//定义根目录
FATFS 		Fatfs;													//定义文件系统全局变量
DIR			Dir;													//定义文件夹全局变量
FIL			File;													//定义文件全局变量
FRESULT 	ret;													//定义文件系统操作函数的返回值
const char 	Size_Unit[] = "BKMGTP";									//定义容量单位

2.1 挂载文件系统

打开 init.c 文件,添加文件系统挂载命令:

//文件系统初始化
void File_Init(void)
{
	if (SD_CD != 0) Error_Handle(2, "No SD card!");								//检查是否插入了SD卡
//	SD_PWR_OFF;
//	HAL_Delay(100);
//	SD_PWR_ON;																	//打开SD卡电源
//	HAL_Delay(200);
	FRESULT ret = f_mount(&Fatfs, Root, 1);										//尝试挂载文件系统
    if (ret == FR_NO_FILESYSTEM)												//如果没有文件系统就报错
    {
		Error_Handle(3, "No File system!");
	}
	if (ret != FR_OK) 															//挂载中遇到其它异常
    {
        printf("Mount error:%s\r\n", File_Err[ret]);
		Error_Handle(4, "Mount error!");
    }
}
MSH_INIT_EXPORT(3, File_Init, File system initialization);

2.2 编写文件操作命令

打开 file.c 文件,添加下列命令:

命令,格式化SD卡,参数是密码,密码正确才会执行,密码:123456
//void SD_Format(char * Payload)
//{
//	if (StrCompare(Payload, "123456") != 0)
//	{
//		Printf_If_PC("NG,%s,Password error!\r\n@_@", __func__);
//		Printf_If_HMI("%s=\"Password error!\"%s", HMI_Ctr_UartRcv, HMI_End);
//		return;
//	}
//	unsigned char *buf = (unsigned char *)malloc(512);				//为格式化操作申请内存
//	if (buf == NULL)
//	{
//		Printf_If_PC("NG,%s,Malloc fail!\r\n@_@", __func__);
//		Printf_If_HMI("%s=\"Malloc fail\"%s", HMI_Ctr_UartRcv, HMI_End);
//		return;
//	}
//	ret = f_mkfs(Root, FM_FAT32, 8192, buf, sizeof(buf));			//格式化SD卡
//	free(buf);														//释放内存
//	if(ret != FR_OK)												//格式化失败
//	{
//		Printf_If_PC("NG,%s,%s\r\n@_@", __func__, File_Err[ret]);
//		Printf_If_HMI("%s=\"%s\"%s", HMI_Ctr_UartRcv, File_Err[ret], HMI_End);
//	}
//	else
//	{
//		Printf_If_PC("OK,%s\r\n@_@", __func__);
//		Printf_If_HMI("%s=\"Format OK\"%s", HMI_Ctr_UartRcv, HMI_End);
//	}
//}
//MSH_CMD_EXPORT(SD_Format, Formatting SD Card);

//命令,查询剩余空间
void SD_Size (char * Payload)
{
	FATFS *fs;
    DWORD fre_clust = 0, fre_sect = 0, tot_sect = 0;
    ret = f_getfree(Root, &fre_clust, &fs);					//得到磁盘信息及空闲簇数量
    if (ret == FR_OK)
    {
        tot_sect = (fs->n_fatent - 2) * fs->csize;    		//计算总扇区数,簇数量 * 簇大小
        fre_sect = fre_clust * fs->csize;            		//得到空闲扇区数
		
		Printf_If_PC("OK,%s,", __func__);
		
		char unit = 1;
		float size = (float)(tot_sect >> 1);				//计算多少K,扇区大小为512字节,扇区数除以2就是K字节数
		while (size > 1024)
		{
			size /= 1024;
			unit++;
		}
		Printf_If_PC("Total: %.2f%c  ", size, Size_Unit[unit]);
		Printf_If_HMI("%s=\"Total: %.2f%c,", HMI_Ctr_UartRcv, size, Size_Unit[unit]);
		
		unit = 1;
		size = (float)(fre_sect >> 1);						//计算多少K,扇区大小为512字节,扇区数除以2就是K字节数
		while (size > 1024)
		{
			size /= 1024;
			unit++;
		}
		Printf_If_PC("Free: %.2f%c\r\n@_@", size, Size_Unit[unit]);
		Printf_If_HMI("Free: %.2f%c\"%s", size, Size_Unit[unit], HMI_End);
    }
	else
	{
		Printf_If_PC("NG,%s,%s\r\n@_@", __func__, File_Err[ret]);
		Printf_If_HMI("%s=\"%s\"%s", HMI_Ctr_UartRcv, File_Err[ret], HMI_End);
	}
}
MSH_CMD_EXPORT(SD_Size, Example Query the remaining disk space);

//命令,挂载文件系统
void File_Mount(char * Payload)
{
	f_mount(NULL, "", 1);									//先卸载文件系统
    ret = f_mount(&Fatfs, Root, 1);							//尝试挂载文件系统
    if (ret == FR_OK)
    {
		Printf_If_PC("OK,%s\r\n@_@", __func__);
		Printf_If_HMI("%s=\"Mount OK\"%s", HMI_Ctr_UartRcv, HMI_End);
    }
    else
    {
        Printf_If_PC("NG,%s,%s\r\n@_@", __func__, File_Err[ret]);
		Printf_If_HMI("%s=\"%s\"%s", HMI_Ctr_UartRcv, File_Err[ret], HMI_End);
    }	
}
MSH_CMD_EXPORT(File_Mount, Mount the file system);

//命令,获取当前目录地址
void Get_Path(char * Payload)
{
	char *path = pvPortMalloc(128);											//申请存放目录的内存
	if (path == NULL)
	{
		Printf_If_PC("NG,%s,malloc\r\n@_@", __func__);
		Printf_If_HMI("%s=\"malloc\"%s", HMI_Ctr_UartRcv, HMI_End);
		return;
	}
	ret = f_getcwd(path, 128);													//获取当前目录
	if (ret == FR_OK)
	{
		Printf_If_PC("OK,%s,%s\r\n@_@", __func__, path);
		Printf_If_HMI("%s=\"%s\"%s", HMI_Ctr_UartRcv, path, HMI_End);
	}
	else
	{
		Printf_If_PC("NG,%s,%s\r\n@_@", __func__, File_Err[ret]);
		Printf_If_HMI("%s=\"%s\"%s", HMI_Ctr_UartRcv, File_Err[ret], HMI_End);
	}
	vPortFree(path);
}
MSH_CMD_EXPORT(Get_Path, Get current directory);

//命令,进入指定目录
void Set_Path(char * Payload)
{
	ret = f_chdir(Payload);														//改变当前目录
	if (ret == FR_OK)
	{
		Printf_If_PC("OK,%s\r\n@_@", __func__);
		Printf_If_HMI("%s=\"OK\"%s", HMI_Ctr_UartRcv, HMI_End);
	}
	else
	{
		Printf_If_PC("NG,%s,%s\r\n@_@", __func__, File_Err[ret]);
		Printf_If_HMI("%s=\"%s\"%s", HMI_Ctr_UartRcv, File_Err[ret], HMI_End);
	}
}
MSH_CMD_EXPORT(Set_Path, Enter the specified directory);

//命令,创建一个新文件夹
void New_Dir(char * Payload)
{
	ret	= f_mkdir(Payload);														//创建文件夹
	if (ret == FR_OK)
	{
		Printf_If_PC("OK,%s\r\n@_@", __func__);
		Printf_If_HMI("%s=\"OK\"%s", HMI_Ctr_UartRcv, HMI_End);
	}
	else
	{
		Printf_If_PC("NG,%s,%s\r\n@_@", __func__, File_Err[ret]);
		Printf_If_HMI("%s=\"%s\"%s", HMI_Ctr_UartRcv, File_Err[ret], HMI_End);
	}
}
MSH_CMD_EXPORT(New_Dir, Create a new folder);

//命令,创建一个新文件,如果文件已存在,则报错
void New_File(char * Payload)
{
	FRESULT ret;
	ret = f_open(&File, Payload, FA_CREATE_ALWAYS);  			//创建文件
    if (ret != FR_OK)
    {
        Printf_If_PC("NG,%s,%s\r\n@_@", __func__, File_Err[ret]);
		Printf_If_HMI("%s=\"%s\"%s", HMI_Ctr_UartRcv, File_Err[ret], HMI_End);
		return;
    }
	f_close(&File);
	Printf_If_PC("OK,%s\r\n@_@", __func__);
	Printf_If_HMI("%s=\"OK\"%s", HMI_Ctr_UartRcv, HMI_End);
}
MSH_CMD_EXPORT(New_File, Create a new file);

//命令,显示指定路径下的文件和文件夹信息
void File_List(char * Payload)
{
	FILINFO fno;
	FRESULT ret = f_opendir(&Dir, Payload);										//以文件夹的形式打开指定的路径
	if (ret != FR_OK)
	{
		Printf_If_PC("NG,%s,%s\r\n@_@", __func__, File_Err[ret]);
		return;
	}
	ret = f_chdir(Payload);														//切换当前目录到该文件夹
	if (ret != FR_OK)
	{
		Printf_If_PC("NG,%s,%s\r\n@_@", __func__, File_Err[ret]);
		return;
	}
	while (1)
	{
		ret = f_readdir(&Dir, &fno);                   							/* Read a directory item */
		if (ret != FR_OK || fno.fname[0] == 0) break;  							/* Break on error or end of dir */
		if (fno.fattrib & AM_DIR)                      							/* It is a directory */
		{
			Printf_If_PC("Folder,%s,0,", fno.fname);
		} 
		else 																	/* It is a file. */
		{
			Printf_If_PC("File,%s,%u,", fno.fname, fno.fsize);
		}
		Printf_If_PC("%d/%02d/%02d %02d:%02d:%02d\r\n",
			((fno.fdate >> 9) & 0x007F) + 1980, 
			(fno.fdate >> 5) & 0x00F, 
			fno.fdate & 0x001F, 
			(fno.ftime >> 11) & 0x001F, 
			(fno.ftime >> 5) & 0x003F, 
			(fno.ftime & 0x001F) << 1);
	}
	f_closedir(&Dir);
	Printf_If_PC("OK,%s\r\n@_@", __func__);
}
MSH_CMD_EXPORT(File_List, Displays information about files and directories);
//命令,读取文件内容
void File_Read (char * Payload)
{
	unsigned int len, ReadLen, read_size;
	if (SENDER_IS_HMI) return;													//HMI不支持该命令
	ret = f_open(&File, Payload, FA_OPEN_EXISTING | FA_READ);					//以读取形式打开已存在的文件
    if (ret != FR_OK)
	{
		Printf_If_PC("NG,%s,%s\r\n@_@", __func__, File_Err[ret]);
		return;
	}
	len = f_size(&File);
	unsigned int buf_size = len > 256 ? 256 : len;
    unsigned char *buf = pvPortMalloc(buf_size);								//申请一块存放读取到的文件内容的内存
	if (buf == NULL)
	{
		Printf_If_PC("NG,%s,Malloc fail\r\n@_@", __func__);
		goto End;
	}
	while (len > 0)
	{
		read_size = len > buf_size ? buf_size : len;
		ret = f_read(&File, buf, read_size, &ReadLen);
		if ((ret != FR_OK) || (read_size != ReadLen))
		{
			Printf_If_PC("NG,%s,%s\r\n@_@", __func__, File_Err[ret]);
			goto End;
		}
		HAL_UART_Transmit(&UART_PC, buf, ReadLen, 50);
		len -= ReadLen;
	}
	HAL_Delay(20);
	Printf_If_PC("OK,%s\r\n@_@", __func__);
End:
	vPortFree(buf);
	f_close(&File);
}
MSH_CMD_EXPORT(File_Read, Read contents of the file);

//命令,以覆盖写的形式写一行文本到文件中,格式:path,content
void File_Write(char * Payload)
{
	unsigned int len1, len2;
	char *path, *content;
	path = StrSplit(Payload, ',');							//分割字符串,得到文件路径
	ret = f_open(&File, path, FA_OPEN_ALWAYS | FA_WRITE | FA_CREATE_ALWAYS);  	//以覆盖写的方式打开文件
    if (ret != FR_OK)
    {
        Printf_If_PC("NG,%s,Open error:%s\r\n@_@", __func__, File_Err[ret]);
		Printf_If_HMI("%s=\"Open error:%s\"%s", HMI_Ctr_UartRcv, File_Err[ret], HMI_End);
		return;
    }
	content = path;											//计算文本内容指针
	while (*content++);
	f_write(&File, content, StrLen(content), &len1);		//将指定存储区内容写入到文件内
	ret = f_write(&File, "\r\n", 2, &len2);					//追加写换行符
	if(ret == FR_OK)
	{
		Printf_If_PC("OK,%s\r\n@_@", __func__);
		Printf_If_HMI("%s=\"OK\"%s", HMI_Ctr_UartRcv, HMI_End);
	}
	else
	{
		Printf_If_PC("NG,%s,Write error:%s\r\n@_@", __func__, File_Err[ret]);
		Printf_If_HMI("%s=\"Write error:%s\"%s", HMI_Ctr_UartRcv, File_Err[ret], HMI_End);
	}
	f_close(&File);
}
MSH_CMD_EXPORT(File_Write, Write the contents to file);

这里展示了文件系统的大部分操作命令,还有一些如删除文件、删除文件夹及子项、文件追加写、重命名等命令不作展示。
然后用串口助手测试命令,比如发送”File_List",单片机将列出当前目录下的所有文件夹和文件。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值