8 FreeRTOS 列表和列表项

1 列表和列表项简介

列表是FreeRTOS中的一个数据结构,概念上和链表有点类似,列表被用来跟踪FreeRTOS中的任务,列表项就是存放在列表中的项目。

列表相当于链表,列表项相当于节点,FreeRTOS中的列表是一个双向环形链表。

列表的特点:列表项间的地址是非连续的,是人为连接到一起的列表项的数目随时可以改变,是由后期添加到个数决定的。

数组的特点:数组成员地址是连续的,数组在最初确定了成员数量后期无法改变。

因此在OS中任务的数量是不确定的,并且任务状态是会发生改变的,所以非常适用列表(链表)这种数据结构。

1)列表结构体

列表项是列表中用于存放数据的地方

typedef struct xLIST
{
    listFIRST_LIST_INTEGRITY_CHECK_VALUE			/* 校验值 */
    volatile UBaseType_t uxNumberOfItems;			/* 列表中的列表项数量 */
   	ListItem_t * configLIST_VOLATILE pxIndex		/* 用于遍历列表项的指针 */
    MiniListItem_t xListEnd					        /* 末尾列表项 */
    listSECOND_LIST_INTEGRITY_CHECK_VALUE		    /* 校验值 */
} List_t;

1、在该结构体中,包含了两个宏,这两个宏是确定的已知常量,FreeRTOS通过检查这两个常量的值来判断列表的数据在程序运行过程中,是否遭到破坏,该功能一般用于调试,默认是不开启的。

2、成员uxNumberOfItems,用于记录列表中列表项的个数(不包含xListEnd)

3、成员pxIndex用于指向列表中的某个列表项,一般用于遍历列表中的所有列表项

4、成员变量xListEnd是一个迷你列表项,排在最末尾

列表结构示意图:

 2)列表项结构体

struct xLIST_ITEM
{
    listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE			/* 用于检测列表项的数据完整性 */
    configLIST_VOLATILE TickType_t xItemValue			/* 列表项的值 */
    struct xLIST_ITEM * configLIST_VOLATILE pxNext		/* 下一个列表项 */
  	struct xLIST_ITEM * configLIST_VOLATILE pxPrevious	/* 上一个列表项 */
    void * pvOwner							            /* 列表项的拥有者 */
    struct xLIST * configLIST_VOLATILE pxContainer; 	/* 列表项所在列表 */
   	listSECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE			/* 用于检测列表项的数据完整性 */
};
typedef struct xLIST_ITEM ListItem_t; 

1、成员变量xItemValue为列表项的值,这个值多用于按升序对列表中的列表项进行排序

2、成员变量pxNext和pxPrevious分别用于指向列表中列表项的下一个列表项和上一个列表项

3、成员变量pxOwner用过指向包含列表项的对象(通常是任务控制块)

4、成员变量pxContainer用于指向列表项所在列表

列表项结构示意图:

 3)迷你列表项结构体

迷你列表项也是列表项,但迷你列表项仅用于标记列表的末尾和挂载其他插入列表中的列表项。

struct xMINI_LIST_ITEM
{
    listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE 			/* 用于检测数据完整性 */
	configLIST_VOLATILE TickType_t xItemValue;			/* 列表项的值 */
    struct xLIST_ITEM * configLIST_VOLATILE pxNext;		/* 上一个列表项 */
   	struct xLIST_ITEM * configLIST_VOLATILE pxPrevious; /* 下一个列表项 */
};
typedef struct xMINI_LIST_ITEM MiniListItem_t;

1、成员变量xItemValue为列表项的值,这个值多用于按升序对列表中的列表项进行排序

2、成员变量pxNext和pxPrevious分别用于指向列表中列表项的下一个列表项和上一个列表项

3、迷你列表项只用于标记列表的末尾和挂载其他插入列表中的列表项,因此不需要成员变量pxOwner和pxContainer,以节省内存开销

迷你列表项结构体示意图:
 

 2 列表相关API函数

函数描述
vListInitialise()初始化列表
vListInitialiseItem()初始化列表项
vListInsertEnd()列表末尾插入列表项
vListInsert()列表插入列表项
uxListRemove()列表移除列表项

1)初始化列表vListInitialise()

void vListInitialise(List_t * const pxList)   
/*函数 vListInitialise()无返回值, pxList为待初始化列表*/
{
  pxList->pxIndex = ( ListItem_t * ) &( pxList->xListEnd );
  /*pxList->pxIndex用于指向某一个列表项,由于一开始只有末尾列表项,因此指向pxList的末尾列表项*/

  pxList->xListEnd.xItemValue = portMAX_DELAY;
 /*把末尾列表项 xListEnd 的值初始化为最大值0XFFFF FFFF,用于列表项升序排序时,排在最后 */
 
  pxList->xListEnd.pxNext = ( ListItem_t * ) &( pxList->xListEnd );
  pxList->xListEnd.pxPrevious = ( ListItem_t * ) &( pxList->xListEnd );
  /* 初始化时,列表中只有末尾列表项 xListEnd,因此上一个和下一个列表项都指向 xListEnd 本身 */

  pxList->uxNumberOfItems = ( UBaseType_t ) 0U;
  /*初始化时,列表中的列表项数量为 0(不包含 xListEnd) */

  listSET_LIST_INTEGRITY_CHECK_1_VALUE( pxList );
  listSET_LIST_INTEGRITY_CHECK_2_VALUE( pxList );
  /* 初始化用于检测列表数据完整性的校验值 */
}

函数 vListInitialise()初始化后的列表结构示意图

 2)初始化列表项vListInitialiseItem()

void vListInitialiseItem( ListItem_t * const pxItem )
/*函数 vListInitialiseItem()无返回值, pxItem为待初始化列表项*/
{
    pxItem->pxContainer = NULL;
    /* 这句程序主要设置列表项属于哪一个列表,由于在初始化阶段,因此列表所属列表项为空 */

    listSET_FIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE( pxItem );
    listSET_SECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE( pxItem );
    /* 初始化用于检测列表项数据完整性的校验值 */
}

函数 vListInitialiseItem()初始化后的列表项结构示意图

3) 列表末尾插入列表项vListInsertEnd()

void vListInsertEnd( List_t * const pxList, ListItem_t * const pxNewListItem )
/*函数 vListInsertEnd()无返回值, pxList为目标列表,pxNewListItem为待插入列表项*/
{
    ListItem_t * const pxIndex = pxList->pxIndex;
    /* 获取目标列表所指向的列表项(假设指向末尾列表项pxIndex,后续简称P) */

    listTEST_LIST_INTEGRITY( pxList );
    listTEST_LIST_ITEM_INTEGRITY( pxNewListItem );
    /* 检查参数是否正确 */

    pxNewListItem->pxNext = pxIndex;
    pxNewListItem->pxPrevious = pxIndex->pxPrevious;
    /*将待插入列表项的Next指向P末尾列表项,将待插入列表项的Previous指向P的上一个列表项*/
    /*到此,待插入列表项的Next和Previous都已经连接,还需要将前后的列表项连向自己*/ 

    mtCOVERAGE_TEST_DELAY();
    /* 测试使用,不用理会 */

    pxIndex->pxPrevious->pxNext = pxNewListItem;
    pxIndex->pxPrevious = pxNewListItem;
    /* 将末尾列表项P的上一个列表项的Next指向待插入列表项,
     *然后将末尾列表项的Previous连接到待插入列表项 ,到此,待插入列表项已经完全插入目标列表中*/
    
    pxNewListItem->pxContainer = pxList;
    /*由于已经插入,因此更新待插入列表项所属列表*/

    ( pxList->uxNumberOfItems )++;
    /*目标列表中列表项数目更新*/
}

从上面的程序中可以看出,此函数就是将待插入的列表项插入到列表pxIndex指向的列表项前面,需要注意的是,pxIndex不一定指向xListEnd,而是有可能指向列表中任意一个列表项。

函数 vListInsertEnd()插入列表项后的列表结构示意图

4) 列表插入列表项vListInsert()

此函数用于将待插入列表的列表项按照列表项值升序排序的顺序,有序地插入到列表中

void vListInsert( List_t * const pxList, ListItem_t * const pxNewListItem )
/*函数vListInsert()无返回值,pxList为目标列表,pxNewListItem为待插入的列表项*/
{
    ListItem_t * pxIterator;
    const TickType_t xValueOfInsertion = pxNewListItem->xItemValue;、
    /*读取到待插入列表项的数值(假设为30)*/

    listTEST_LIST_INTEGRITY( pxList );
    listTEST_LIST_ITEM_INTEGRITY( pxNewListItem );
    /* 检查参数是否正确 */

    if( xValueOfInsertion == portMAX_DELAY )
    /*判断待插入的列表项的数值是不是等于最大值(0XFFFF FFFF)*/
    {
        pxIterator = pxList->xListEnd.pxPrevious;
        /* 插入的位置为列表 xListEnd 前面 */
    }
    else
    {
        /* 遍历列表中的列表项,找到插入的位置 */
        for( pxIterator = ( ListItem_t * ) &( pxList->xListEnd ); 
        pxIterator->pxNext->xItemValue <= xValueOfInsertion; 
        pxIterator = pxIterator->pxNext ) 
        /*首先,让pxIterator指向末尾列表项,判断末尾列表项的下一个列表项的值是否 <= 待插入列表项 
         *的值,若满足条件则继续向下遍历,当不满足条件即pxIterator 的下一个列表项的值 >= 待插入            
         *列表项的值,则退出循环。此时pxIterator指向的就是待插入列表项的前一个列表项*/
        {
            /* There is nothing to do here, just iterating to the wanted
             * insertion position. */
        }
    }

    pxNewListItem->pxNext = pxIterator->pxNext;
    /*将待插入列表项的next指向pxIterator所指向的下一个列表项*/

    pxNewListItem->pxNext->pxPrevious = pxNewListItem;
    /*将待插入列表项的下一个列表项的Previous指向待插入列表项*/
    
    pxNewListItem->pxPrevious = pxIterator;
    /*将待插入列项的Previous指向pxIterator*/

    pxIterator->pxNext = pxNewListItem;
   /*将pxIterator的Next指向待插入列表项*/

    pxNewListItem->pxContainer = pxList;
    /* 更新待插入列表项所在列表 */

    ( pxList->uxNumberOfItems )++;
    /* 更新列表中列表项的数量 */
}

从上面程序可以看出,此函数在将待插入列表项之前,会先遍历列表,找到待插入列表项需要插入的位置,待插入列表需要插入的位置是依照列表中列表项的值按照升序排序确定的。

5)列表移除列表项uxListRemove()

此函数用于将列表项从列表项所在列表中移除

UBaseType_t uxListRemove( ListItem_t * const pxItemToRemove )
{
    List_t * const pxList = pxItemToRemove->pxContainer;
    /* 获取待删除项所属列表 */

    pxItemToRemove->pxNext->pxPrevious = pxItemToRemove->pxPrevious;
    pxItemToRemove->pxPrevious->pxNext = pxItemToRemove->pxNext;
    /* 从列表中移除列表项 */

    mtCOVERAGE_TEST_DELAY();
    /* 测试使用,不用理会 */

    if( pxList->pxIndex == pxItemToRemove )
    /* 如果 pxIndex 正指向待移除的列表项 */
    {
        pxList->pxIndex = pxItemToRemove->pxPrevious;
        /* pxIndex 指向上一个列表项 */
    }
    else
    {
        mtCOVERAGE_TEST_MARKER();
    }

    pxItemToRemove->pxContainer = NULL;
    /* 将待移除列表项的所在列表指针清空 */

    ( pxList->uxNumberOfItems )--;
    /* 更新列表中列表项的数量 */

    return pxList->uxNumberOfItems;
    /* 返回列表项移除后列表中列表项的数量 */
}

要注意uxListRemove()移除后的列表项,依然于列表有着单向联系,即移除后列表项中用于指向上一个和下一个列表项的指针,依然指向列表中的列表项。

函数 uxListRemove()移除列表项后的列表结构示意图

3 列表项的插入和删除实验

3.1 实验设计

设计三个任务:start_task 、task1、task2

start_task : 用来创建其他2个任务

task1 : 实现LED1每500ms闪烁一次,用来提示系统正在运行

task2: 调用列表和列表选项相关API函数,并且通过串口输出相应的信息,进行观察

3.2 程序源码

#include "freertos_demo.h"
#include "./SYSTEM/usart/usart.h"
#include "./BSP/LED/led.h"
#include "./BSP/LCD/lcd.h"
#include "./BSP/KEY/key.h"
/*FreeRTOS*********************************************************************************************/
#include "FreeRTOS.h"
#include "task.h"

/******************************************************************************************************/
/*FreeRTOS配置*/

/* START_TASK 任务 配置
 * 包括: 任务句柄 任务优先级 堆栈大小 创建任务
 */
#define START_TASK_PRIO 1                   /* 任务优先级 */
#define START_STK_SIZE  128                 /* 任务堆栈大小 */
TaskHandle_t            StartTask_Handler;  /* 任务句柄 */
void start_task(void *pvParameters);        /* 任务函数 */

/* TASK1 任务 配置
 * 包括: 任务句柄 任务优先级 堆栈大小 创建任务
 */
#define TASK1_PRIO      2                   /* 任务优先级 */
#define TASK1_STK_SIZE  128                 /* 任务堆栈大小 */
TaskHandle_t            Task1Task_Handler;  /* 任务句柄 */
void task1(void *pvParameters);             /* 任务函数 */

/* TASK2 任务 配置
 * 包括: 任务句柄 任务优先级 堆栈大小 创建任务
 */
#define TASK2_PRIO      3                   /* 任务优先级 */
#define TASK2_STK_SIZE  128                 /* 任务堆栈大小 */
TaskHandle_t            Task2Task_Handler;  /* 任务句柄 */
void task2(void *pvParameters);             /* 任务函数 */

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

//第一步,定义测试列表项
List_t      TestList;

//第二步,定义测试列表项
ListItem_t 	ListItem_1;
ListItem_t 	ListItem_2;
ListItem_t 	ListItem_3;





void freertos_demo(void)
{
   
    xTaskCreate((TaskFunction_t )start_task,            /* 任务函数 */
                (const char*    )"start_task",          /* 任务名称 */
                (uint16_t       )START_STK_SIZE,        /* 任务堆栈大小 */
                (void*          )NULL,                  /* 传入给任务函数的参数 */
                (UBaseType_t    )START_TASK_PRIO,       /* 任务优先级 */
                (TaskHandle_t*  )&StartTask_Handler);   /* 任务句柄 */
    vTaskStartScheduler();
}

void start_task(void *pvParameters)
{
    taskENTER_CRITICAL();           /* 进入临界区 */
    
	  /* 创建任务1 */
    xTaskCreate((TaskFunction_t )task1,
                (const char*    )"task1",
                (uint16_t       )TASK1_STK_SIZE,
                (void*          )NULL,
                (UBaseType_t    )TASK1_PRIO,
                (TaskHandle_t*  )&Task1Task_Handler);

	  /* 创建任务2 */
    xTaskCreate((TaskFunction_t )task2,
                (const char*    )"task2",
                (uint16_t       )TASK2_STK_SIZE,
                (void*          )NULL,
                (UBaseType_t    )TASK2_PRIO,
                (TaskHandle_t*  )&Task2Task_Handler);
 								
    vTaskDelete(StartTask_Handler); /* 删除开始任务 */
								
    taskEXIT_CRITICAL();            /* 退出临界区 */
}

//任务一,实现每500ms翻转一次
void task1(void *pvParameters)
{
    while(1)
    {
				LED1_TOGGLE();                                                  /* LED0闪烁 */
        vTaskDelay(500);                                               /* 延时500ms */
    }
}

//任务二,列表项的插入和删除实验
void task2(void *pvParameters)
{
	 //第三步,初始化列表
	vListInitialise( &TestList );
	
	//第四步,初始化列表项
	vListInitialiseItem( &ListItem_1 );
	vListInitialiseItem( &ListItem_2 );
	vListInitialiseItem( &ListItem_3 );
	
	ListItem_1.xItemValue = 40;
	ListItem_2.xItemValue = 60;
	ListItem_3.xItemValue = 50;
	
	/* 第五步:打印列表和其他列表项的地址 */
  printf("/**************第五步:打印列表和列表项的地址**************/\r\n");
  printf("项目\t\t\t地址\r\n");
  printf("TestList\t\t0x%p\t\r\n", &TestList);               					//打印列表地址
  printf("TestList->pxIndex\t0x%p\t\r\n", TestList.pxIndex);  				//打印列表PxIndex所指向的列表项的地址
  printf("TestList->xListEnd\t0x%p\t\r\n", (&TestList.xListEnd));			
  printf("ListItem1\t\t0x%p\t\r\n", &ListItem_1);        							//打印列表项1地址
  printf("ListItem2\t\t0x%p\t\r\n", &ListItem_2);											//打印列表项2地址
  printf("ListItem3\t\t0x%p\t\r\n", &ListItem_3);											//打印列表项3地址
  printf("/**************************结束***************************/\r\n");
	
	/* 第六步:列表项1插入列表 */
  printf("\r\n/*****************第六步:列表项1插入列表******************/\r\n");
  vListInsert((List_t*    )&TestList,(ListItem_t*)&ListItem_1);                         //调用升序插入函数,在TestList列表中插入ListItem_1列表项1
  printf("项目\t\t\t\t地址\r\n");
  printf("TestList->xListEnd->pxNext\t0x%p\r\n", (TestList.xListEnd.pxNext));           //打印末尾列表项的下一个列表项的地址
  printf("ListItem1->pxNext\t\t0x%p\r\n", (ListItem_1.pxNext));                         //打印列表项1的下一个列表项地址
  printf("TestList->xListEnd->pxPrevious\t0x%p\r\n", (TestList.xListEnd.pxPrevious));   //打印末尾列表项的上一个列表项地址
  printf("ListItem1->pxPrevious\t\t0x%p\r\n", (ListItem_1.pxPrevious));                 //打印列表项1的上一个列表项地址
  printf("/**************************结束***************************/\r\n");

	/* 第七步:列表项2插入列表 */
  printf("\r\n/*****************第七步:列表项2插入列表******************/\r\n");
  vListInsert((List_t*    )&TestList,(ListItem_t*)&ListItem_2);                          //调用升序插入函数,在TestList列表中插入ListItem_2列表项2
  printf("项目\t\t\t\t地址\r\n");
  printf("TestList->xListEnd->pxNext\t0x%p\r\n", (TestList.xListEnd.pxNext));
  printf("ListItem1->pxNext\t\t0x%p\r\n", (ListItem_1.pxNext));
  printf("ListItem2->pxNext\t\t0x%p\r\n", (ListItem_2.pxNext));
  printf("TestList->xListEnd->pxPrevious\t0x%p\r\n", (TestList.xListEnd.pxPrevious));
  printf("ListItem1->pxPrevious\t\t0x%p\r\n", (ListItem_1.pxPrevious));
  printf("ListItem2->pxPrevious\t\t0x%p\r\n", (ListItem_2.pxPrevious));
  printf("/**************************结束***************************/\r\n");
	
	/* 第八步:列表项3插入列表 */
  printf("\r\n/*****************第八步:列表项3插入列表******************/\r\n");
  vListInsert((List_t*    )&TestList,(ListItem_t*)&ListItem_3);          //调用升序插入函数,在TestList列表中插入ListItem_3列表项3
  printf("项目\t\t\t\t地址\r\n");
  printf("TestList->xListEnd->pxNext\t0x%p\r\n", (TestList.xListEnd.pxNext));
  printf("ListItem1->pxNext\t\t0x%p\r\n", (ListItem_1.pxNext));
  printf("ListItem2->pxNext\t\t0x%p\r\n", (ListItem_2.pxNext));
  printf("ListItem3->pxNext\t\t0x%p\r\n", (ListItem_3.pxNext));
  printf("TestList->xListEnd->pxPrevious\t0x%p\r\n", (TestList.xListEnd.pxPrevious));
  printf("ListItem1->pxPrevious\t\t0x%p\r\n", (ListItem_1.pxPrevious));
  printf("ListItem2->pxPrevious\t\t0x%p\r\n", (ListItem_2.pxPrevious));
  printf("ListItem3->pxPrevious\t\t0x%p\r\n", (ListItem_3.pxPrevious));
	printf("/**************************结束***************************/\r\n");
	
	/* 第九步:移除列表项2 */
  printf("\r\n/*******************第九步:移除列表项2********************/\r\n");
  uxListRemove((ListItem_t*   )&ListItem_2);   /* 移除列表项 */
  printf("项目\t\t\t\t地址\r\n");
  printf("TestList->xListEnd->pxNext\t0x%p\r\n", (TestList.xListEnd.pxNext));
  printf("ListItem1->pxNext\t\t0x%p\r\n", (ListItem_1.pxNext));
  printf("ListItem3->pxNext\t\t0x%p\r\n", (ListItem_3.pxNext));
  printf("TestList->xListEnd->pxPrevious\t0x%p\r\n", (TestList.xListEnd.pxPrevious));
  printf("ListItem1->pxPrevious\t\t0x%p\r\n", (ListItem_1.pxPrevious));
  printf("ListItem3->pxPrevious\t\t0x%p\r\n", (ListItem_3.pxPrevious));
  printf("/**************************结束***************************/\r\n");
	
  /* 第十步:列表末尾添加列表项2 */
  printf("\r\n/****************第十步:列表末尾添加列表项2****************/\r\n");
  vListInsertEnd((List_t*     )&TestList,(ListItem_t* )&ListItem_2);   
  printf("项目\t\t\t\t地址\r\n");
  printf("TestList->pxIndex\t\t0x%p\r\n", TestList.pxIndex);
  printf("TestList->xListEnd->pxNext\t0x%p\r\n", (TestList.xListEnd.pxNext));
  printf("ListItem1->pxNext\t\t0x%p\r\n", (ListItem_1.pxNext));
  printf("ListItem2->pxNext\t\t0x%p\r\n", (ListItem_2.pxNext));
  printf("ListItem3->pxNext\t\t0x%p\r\n", (ListItem_3.pxNext));
  printf("TestList->xListEnd->pxPrevious\t0x%p\r\n", (TestList.xListEnd.pxPrevious));
  printf("ListItem1->pxPrevious\t\t0x%p\r\n", (ListItem_1.pxPrevious));
  printf("ListItem2->pxPrevious\t\t0x%p\r\n", (ListItem_2.pxPrevious));
  printf("ListItem3->pxPrevious\t\t0x%p\r\n", (ListItem_3.pxPrevious));
  printf("/************************实验结束***************************/\r\n");
	
  /* 第十一步:修改P指针的指向,再插入列表项2 */
  printf("\r\n/****************第十一步:修改P指针的指向,再插入列表项2****************/\r\n");
	TestList.pxIndex = &ListItem_1;
  vListInsertEnd((List_t*     )&TestList,(ListItem_t* )&ListItem_2);   
  printf("项目\t\t\t\t地址\r\n");
  printf("TestList->pxIndex\t\t0x%p\r\n", TestList.pxIndex);
  printf("TestList->xListEnd->pxNext\t0x%p\r\n", (TestList.xListEnd.pxNext));
  printf("ListItem1->pxNext\t\t0x%p\r\n", (ListItem_1.pxNext));
  printf("ListItem2->pxNext\t\t0x%p\r\n", (ListItem_2.pxNext));
  printf("ListItem3->pxNext\t\t0x%p\r\n", (ListItem_3.pxNext));
  printf("TestList->xListEnd->pxPrevious\t0x%p\r\n", (TestList.xListEnd.pxPrevious));
  printf("ListItem1->pxPrevious\t\t0x%p\r\n", (ListItem_1.pxPrevious));
  printf("ListItem2->pxPrevious\t\t0x%p\r\n", (ListItem_2.pxPrevious));
  printf("ListItem3->pxPrevious\t\t0x%p\r\n", (ListItem_3.pxPrevious));
  printf("/************************实验结束***************************/\r\n");
	
	while(1)
  {
        vTaskDelay(10);
  }
}


程序现象:

  • 10
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值