STM32HAL 移植 EasyLogger 极简美开源打印文件日志输出(裸机开发神器)

        目录

概述

一、使用方法

二、STM32CubeMx配置​

三、Examples

四、运行结果​

五、总结


概述

        嵌入式也能像纯软开发那样输出级别,方便查找Bug,有了这个神器可以减少很多排查问题的时间。好了,下面引用官方的简述:
        EasyLogger 是一款超轻量级(ROM<1.6K, RAM<0.3K)、高性能的 C/C++ 日志库,非常适合对资源敏感的软件项目,例如: IoT 产品、可穿戴设备、智能家居等等。相比 log4c、zlog 这些知名的 C/C++ 日志库, EasyLogger 的功能更加简单,提供给用户的接口更少,但上手会很快,更多实用功能支持以插件形式进行动态扩展。
这篇文章没有引用Flash保存日志功能,只是简单的打印输出功能,后期再写一篇使用Flash功能,谢谢各位看管赏阅 ^_^ 。

github:GitHub - armink/EasyLogger: An ultra-lightweight(ROM<1.6K, RAM<0.3k), high-performance C/C++ log library. | 一款超轻量级(ROM<1.6K, RAM<0.3k)、高性能的 C/C++ 日志库
 

硬件:STM32F103CBT6最小系统板
软件:Keil 5.29  + STM32CubeMX6.01

一、使用方法

1、下载源码

点击此链接即可直接下载位于Github上的源码。软件版本号位于 \easylogger\inc\elog.h 的 ELOG_SW_VERSION 宏定义。当然你也可以选择使用Git工具直接克隆到本地。

建议:点击项目主页 GitHub - armink/EasyLogger: An ultra-lightweight(ROM<1.6K, RAM<0.3k), high-performance C/C++ log library. | 一款超轻量级(ROM<1.6K, RAM<0.3k)、高性能的 C/C++ 日志库 右上角 Watch & Star,这样项目有更新时,会及时以邮件形式通知你。

如果Github下载太慢,也可以点击项目位于的国内仓库下载的链接(OSChina|Coding)。

2、导入项目

在导入到项目前,先打开\demo\文件夹,检查下有没有与项目平台一致的Demo。如果有则先直接跳过2、3、4章节,按照第5章的要求设置参数,并运行、验证Demo。验证通过再按照下面的导入项目要求,将Demo中的移植文件直接导入到项目中即可。

  • 1、先解压下载好的源码包,文件的目录结构大致如下:
源文件描述
\easylogger\src\elog.c核心功能源码
\easylogger\src\elog_async.c核心功能异步输出模式源码
\easylogger\src\elog_buf.c核心功能缓冲输出模式源码
\easylogger\src\elog_utils.cEasyLogger常用小工具
\easylogger\port\elog_port.c不同平台下的EasyLogger移植接口
\easylogger\plugins\插件源码目录
\docs\zh\所有中文文档目录
\demo\non_os\stm32f10x\stm32f10x裸机的 demo
\demo\os\linux\linux平台 demo
\demo\os\windows\windows平台 demo
\demo\os\rt-thread\stm32f10x\stm32f10x基于RT-Thread的demo(包含Flash插件demo)
  • 2、将\easylogger\(里面包含incsrcport的那个)文件夹拷贝到项目中;
  • 3、添加\easylogger\src\elog.c\easylogger\src\elog_utils.c\easylogger\port\elog_port.c这些文件到项目的编译路径中(elog_async.c 及 elog_buf.c 视情况选择性添加);
  • 4、添加\easylogger\inc\文件夹到编译的头文件目录列表中;

3、移植接口

3.1 移植初始化

EasyLogger移植初始化。初始化EasyLogger移植所需的资源等等。

ElogErrCode elog_port_init(void)

3.2 日志输出接口

日志最终输出的末端接口。可以在里面增加输出到终端、输出到文件、输出到Flash等方法。

void elog_port_output(const char *log, size_t size)
参数描述
log日志内容
size日志大小

例子:

void elog_port_output(const char *log, size_t size) {
    /* output to terminal */
    printf("%.*s", size, log);
    /* output to flash */
    elog_flash_write(log, size);
}

3.3 对日志输出加锁

对日志输出方法进行加锁,保证日志在并发输出时的正确性。有操作系统时可以使用获取信号量来加锁,裸机时可以通过关闭全局中断来加锁。

void elog_port_output_lock(void)

3.4 对日志输出解锁

与日志输出加锁功能对应。

void elog_port_output_unlock(void)

3.5 获取当前时间

返回当前时间,将会显示在日志中。

const char *elog_port_get_time(void)

3.6 获取进程信息

返回进程信息,将会显示在日志中。(没有则可以返回 "" )

const char *elog_port_get_p_info(void)

3.7 获取线程信息

返回线程信息,将会显示在日志中。(没有则可以返回 ""

const char *elog_port_get_t_info(void)

4、设置参数

配置时需要修改项目中的elog_cfg.h文件,开启、关闭、修改对应的宏即可。

4.1 输出开关

开启后,日志才会被输出。如果关闭,所有日志输出代码都将会被替换为空。

  • 操作方法:开启、关闭ELOG_OUTPUT_ENABLE宏即可

4.2 输出级别

此方法为静态设置输出级别。设置后,比当前输出级别低的日志输出代码将会被替换为空。

输出控制级别: ELOG_OUTPUT_ENABLE(总开关) > ELOG_OUTPUT_LVL(静态) > elog_set_filter(动态)

  • 操作方法:修改ELOG_OUTPUT_LVL宏对应值即可

可选的设置级别如下:

/* output log's level */
#define ELOG_LVL_ASSERT                      0
#define ELOG_LVL_ERROR                       1
#define ELOG_LVL_WARN                        2
#define ELOG_LVL_INFO                        3
#define ELOG_LVL_DEBUG                       4
#define ELOG_LVL_VERBOSE                     5

4.3 断言开关

开启后,将会启动断言检查功能。如果关闭,所有断言检查代码都将会被替换为空。

  • 操作方法:开启、关闭ELOG_ASSERT_ENABLE宏即可

4.4 每行日志的缓冲区大小

该配置决定了日志一行最多输出多少字符,单位:byte。

  • 操作方法:修改ELOG_LINE_BUF_SIZE宏对应值即可

4.5 行号最大长度

建议设置5较为合适,用户可以根据自己的文件行号最大值进行设置,例如最大行号为:9999,则可以设置行号最大长度为4

  • 操作方法:修改ELOG_LINE_NUM_MAX_LEN宏对应值即可

4.6 过滤标签最大长度

日志中标签内容及用户设置过滤标签的最大长度,单位:byte。

  • 操作方法:修改ELOG_FILTER_TAG_MAX_LEN宏对应值即可

4.7 过滤关键词最大长度

用户可设置过滤关键字的最大长度,单位:byte。

  • 操作方法:修改ELOG_FILTER_KW_MAX_LEN宏对应值即可

4.8 标签 + 级别过滤器的最大数目

最大支持的动态日志级别过滤的模块(标签)数量,详见 :elog_set_filter_tag_lvl

  • 操作方法:修改ELOG_FILTER_TAG_LVL_MAX_NUM宏对应值即可

4.9 换行符

用户可以根据自己的使用场景自定义换行符,例如:"\r\n""\n"

  • 操作方法:修改ELOG_NEWLINE_SIGN宏对应值即可

4.10 颜色

注意 :启用颜色功能需先定义 ELOG_COLOR_ENABLE

每个级别的日志均有默认颜色。如果想修改,请先查看在 elog.c 的头部定义的各种颜色及字体风格,这里以修改 VERBOSE 级别日志来举例:

首先选择前景色为白色,再选择背景色为黑色,最后字体风格为粗体

那么最终的配置如下:

#define ELOG_COLOR_VERBOSE         (F_WHITE B_BLACK S_BOLD)
  • 操作方法:增加并修改ELOG_COLOR_VERBOSE宏对应值即可,其他级别日志颜色的修改以此类推

4.11 异步输出模式

开启异步输出模式后,将会提升用户应用程序的执行效率。应用程序在进行日志输出时,无需等待日志彻底输出完成,即可直接返回。

  • 操作方法:开启、关闭ELOG_ASYNC_OUTPUT_ENABLE宏即可

4.11.1 异步输出日志的最高级别

日志低于或等于该级别时,才会通过异步输出。高于该级别的日志都将按照默认的同步方式输出。这样的好处是,提升了较高级别的日志输出的实时性。

  • 默认级别:ELOG_LVL_ASSERT ,不定义此宏,将会自动按照默认值设置
  • 操作方法:修改ELOG_ASYNC_OUTPUT_LVL宏对应值即可

4.11.2 异步输出模式缓冲区大小

  • 默认大小:(ELOG_LINE_BUF_SIZE * 10) ,不定义此宏,将会自动按照默认值设置
  • 操作方法:修改ELOG_ASYNC_OUTPUT_BUF_SIZE宏对应值即可

4.11.3 异步按行输出日志

由于异步输出方式内部拥有缓冲区,所以直接输出缓冲区中积累的日志时,日志移植输出方法 (elog_port_output) 输出的日志将不会按照 行日志 (以换行符结尾)的格式进行输出。这使得无法在移植输出方法中完成日志的分析处理。开启此功能后,将会最大限度保证移植输出方法每次输出的日志格式都为行日志。

  • 操作方法:开启、关闭ELOG_ASYNC_LINE_OUTPUT宏即可

4.11.4 启用 pthread 库

异步输出模式默认是使用 POSIX 的 pthread 库来实现,用户的平台如果支持 pthread ,则可以开启此宏。对于一些缺少 pthread 的支持平台,可以关闭此宏,参考 elog_async.c 中关于日志异步输出线程的实现方式,自己动手实现此功能。

  • 操作方法:开启、关闭ELOG_ASYNC_OUTPUT_USING_PTHREAD宏即可

4.12 缓冲输出模式

开启缓冲输出模式后,如果缓冲区不满,用户线程在进行日志输出时,无需等待日志彻底输出完成,即可直接返回。但当日志缓冲区满以后,将会占用用户线程,自动将缓冲区中的日志全部输出干净。同时用户也可以在非日志输出线程,通过定时等机制使用 void elog_flush(void) 将缓冲区中的日志输出干净。

  • 操作方法:开启、关闭ELOG_BUFF_OUTPUT_ENABLE宏即可

4.12.1 缓冲输出模式缓冲区大小

  • 默认大小:(ELOG_LINE_BUF_SIZE * 10) ,不定义此宏,将会自动按照默认值设置
  • 操作方法:修改ELOG_BUF_OUTPUT_BUF_SIZE宏对应值即可

5、测试验证

如果\demo\文件夹下有与项目平台一致的Demo,则直接编译运行,观察测试结果即可。无需关注下面的步骤。

每次使用前,务必先执行elog_init()方法对EasyLogger库进行初始化,保证初始化没问题后,再设置输出格式、过滤级别、断言钩子等,最后记得调用elog_start()方法启动EasyLogger,否则EasyLogger将不会开始工作。启动后接上终端就即可日志的输出信息,可以参考并运行这里的日志测试函数。如果出现错误或断言,需根据提示信息检查移植配置及接口。

下面为常见初始化方式(点击查看源码

/* close printf buffer */
setbuf(stdout, NULL);
/* initialize EasyLogger */
elog_init();
/* set EasyLogger log format */
elog_set_fmt(ELOG_LVL_ASSERT, ELOG_FMT_ALL);
elog_set_fmt(ELOG_LVL_ERROR, ELOG_FMT_LVL | ELOG_FMT_TAG | ELOG_FMT_TIME);
elog_set_fmt(ELOG_LVL_WARN, ELOG_FMT_LVL | ELOG_FMT_TAG | ELOG_FMT_TIME);
elog_set_fmt(ELOG_LVL_INFO, ELOG_FMT_LVL | ELOG_FMT_TAG | ELOG_FMT_TIME);
elog_set_fmt(ELOG_LVL_DEBUG, ELOG_FMT_ALL & ~ELOG_FMT_FUNC);
elog_set_fmt(ELOG_LVL_VERBOSE, ELOG_FMT_ALL & ~ELOG_FMT_FUNC);
/* start EasyLogger */
elog_start();

二、STM32CubeMx配置

三、Examples

简单粗暴,直接上代码

打开STM32CubeMX生成的Keil工程,如下所示:

把代码下载下来,把easylogger文件夹中文件添加到项目中来,刪掉plugins文件夹与src文件夹里面elog_async.c、elog_buf.c这两个文件即可。


以上步骤执行完后,编译,此时会出现以下错误:


解决方法:
打开elog_cfg.h文件,把 //#define ELOG_ASYNC_OUTPUT_ENABLE、//#define ELOG_BUF_OUTPUT_ENABLE 这两个宏定义的注释掉即可。

再次编译,没有报错

只需在main.c、elog_cfg.c文件,添加代码即可,其它.h与.c文件无需做其它修改。

1、elog_cfg.c文件

/*
 * This file is part of the EasyLogger Library.
 *
 * Copyright (c) 2015, Armink, <armink.ztl@gmail.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * 'Software'), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 *
 * Function: Portable interface for each platform.
 * Created on: 2015-04-28
 */
 
#include <elog.h>
#include <stdio.h>

/**
 * EasyLogger port initialize
 *
 * @return result
 */
ElogErrCode elog_port_init(void) {
    ElogErrCode result = ELOG_NO_ERR;

    /* add your code here */
    
    return result;
}

/**
 * EasyLogger port deinitialize
 *
 */
void elog_port_deinit(void) {

    /* add your code here */

}

/**
 * output log port interface
 *
 * @param log output of log
 * @param size log size
 */
void elog_port_output(const char *log, size_t size) {
    
    /* add your code here */
    printf("%.*s", size, log);
}

/**
 * output lock
 */
void elog_port_output_lock(void) {
    
    /* add your code here */
    __disable_irq();
}

/**
 * output unlock
 */
void elog_port_output_unlock(void) {
    
    /* add your code here */
    __enable_irq();
}

/**
 * get current time interface
 *
 * @return current time
 */
const char *elog_port_get_time(void) {
    
    /* add your code here */
    return "10:08:12";
}

/**
 * get current process name interface
 *
 * @return current process name
 */
const char *elog_port_get_p_info(void) {
    
    /* add your code here */
    return "pid:1008";
}

/**
 * get current thread name interface
 *
 * @return current thread name
 */
const char *elog_port_get_t_info(void) {
    
    /* add your code here */
    return "tid:24";
}

2、main.c文件

/* USER CODE BEGIN Header */
/**
  ******************************************************************************
  * @file           : main.c
  * @brief          : Main program body
  ******************************************************************************
  * @attention
  *
  * <h2><center>&copy; Copyright (c) 2021 STMicroelectronics.
  * All rights reserved.</center></h2>
  *
  * This software component is licensed by ST under BSD 3-Clause license,
  * the "License"; You may not use this file except in compliance with the
  * License. You may obtain a copy of the License at:
  *                        opensource.org/licenses/BSD-3-Clause
  *
  ******************************************************************************
  */
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "usart.h"
#include "gpio.h"

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include <elog.h>
#include <stdio.h>
/* USER CODE END Includes */

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */

/* USER CODE END PTD */

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
/* USER CODE END PD */

/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */

/* USER CODE END PM */

/* Private variables ---------------------------------------------------------*/

/* USER CODE BEGIN PV */

/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */

/* USER CODE END PFP */

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
#include "stdio.h"
 
#ifdef __GNUC__
  /* With GCC/RAISONANCE, small printf (option LD Linker->Libraries->Small printf
     set to 'Yes') calls __io_putchar() */
  #define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#else
  #define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#endif /* __GNUC__ */
/**
  * @brief  Retargets the C library printf function to the USART.
  * @param  None
  * @retval None
  */
PUTCHAR_PROTOTYPE
{
  /* Place your implementation of fputc here */
  /* e.g. write a character to the EVAL_COM1 and Loop until the end of transmission */
  HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF);
 
  return ch;
}
 
int fgetc(FILE * f)
{
  uint8_t ch = 0;
  HAL_UART_Receive(&huart1, (uint8_t *)&ch, 1, 0xffff);
  return ch;
}


void test_elog(void) {
    /* test log output for all level */
    log_a("Hello EasyLogger!");
    log_e("Hello EasyLogger!");
    log_w("Hello EasyLogger!");
    log_i("Hello EasyLogger!");
    log_d("Hello EasyLogger!");
    log_v("Hello EasyLogger!");
//    elog_raw("Hello EasyLogger!");
}

/* USER CODE END 0 */

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{
  /* USER CODE BEGIN 1 */

  /* 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_USART1_UART_Init();
  /* USER CODE BEGIN 2 */
	elog_init();
	/* set EasyLogger log format */
	elog_set_fmt(ELOG_LVL_ASSERT, ELOG_FMT_ALL);
	elog_set_fmt(ELOG_LVL_ERROR, ELOG_FMT_LVL | ELOG_FMT_TAG | ELOG_FMT_TIME);
	elog_set_fmt(ELOG_LVL_WARN, ELOG_FMT_LVL | ELOG_FMT_TAG | ELOG_FMT_TIME);
	elog_set_fmt(ELOG_LVL_INFO, ELOG_FMT_LVL | ELOG_FMT_TAG | ELOG_FMT_TIME);
	elog_set_fmt(ELOG_LVL_DEBUG, ELOG_FMT_ALL & ~(ELOG_FMT_FUNC | ELOG_FMT_T_INFO | ELOG_FMT_P_INFO));
	elog_set_fmt(ELOG_LVL_VERBOSE, ELOG_FMT_ALL & ~(ELOG_FMT_FUNC | ELOG_FMT_T_INFO | ELOG_FMT_P_INFO));
	/* start EasyLogger */
	elog_start();
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
		HAL_Delay(1000);
		HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
		test_elog();
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

/**
  * @brief System Clock Configuration
  * @retval None
  */
void SystemClock_Config(void)
{
  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

  /** Initializes the RCC Oscillators according to the specified parameters
  * in the RCC_OscInitTypeDef structure.
  */
  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
  RCC_OscInitStruct.HSEState = RCC_HSE_ON;
  RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
  RCC_OscInitStruct.HSIState = RCC_HSI_ON;
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
  RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
  {
    Error_Handler();
  }
  /** Initializes the CPU, AHB and APB buses clocks
  */
  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
                              |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;

  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
  {
    Error_Handler();
  }
}

/* USER CODE BEGIN 4 */

/* USER CODE END 4 */

/**
  * @brief  This function is executed in case of error occurrence.
  * @retval None
  */
void Error_Handler(void)
{
  /* USER CODE BEGIN Error_Handler_Debug */
  /* User can add his own implementation to report the HAL error return state */
  __disable_irq();
  while (1)
  {
  }
  /* USER CODE END Error_Handler_Debug */
}

#ifdef  USE_FULL_ASSERT
/**
  * @brief  Reports the name of the source file and the source line number
  *         where the assert_param error has occurred.
  * @param  file: pointer to the source file name
  * @param  line: assert_param error line source number
  * @retval None
  */
void assert_failed(uint8_t *file, uint32_t line)
{
  /* USER CODE BEGIN 6 */
  /* User can add his own implementation to report the file name and line number,
     ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
  /* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */

/************************ (C) COPYRIGHT STMicroelectronics *****END OF FILE****/

四、运行结果

传送门->代码

五、总结

        好了,就介绍到此,有了这个神器,裸机开发就简单很多了,有点像开发纯软(Android、ios、前端、Java后台等等)的味道,非常方便打印输出各个信息。

  • 8
    点赞
  • 96
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Ch_champion

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

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

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

打赏作者

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

抵扣说明:

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

余额充值