FreeRTOS从入门到精通 第八章(任务调度)

参考教程:【正点原子】手把手教你学FreeRTOS实时系统_哔哩哔哩_bilibili

一、开启任务调度器

1、vTaskStartScheduler函数

(1)作用:启动任务调度器,任务调度器启动后,FreeRTOS便会开始进行任务调度。

(2)函数的执行流程:

①创建空闲任务。

②如果使能软件定时器,则创建定时器任务。

③关闭中断,防止调度器开启之前或过程中,受中断干扰,会在运行第一个任务时打开中断。

④初始化全局变量,并将任务调度器的运行标志设置为已运行。

⑤初始化任务运行时间统计功能的时基定时器。

⑥调用函数xPortStartScheduler。

(3)源码剖析:

void vTaskStartScheduler( void )
{
    BaseType_t xReturn;

    traceENTER_vTaskStartScheduler();

    #if ( configUSE_CORE_AFFINITY == 1 ) && ( configNUMBER_OF_CORES > 1 )
    {
        configASSERT( ( sizeof( UBaseType_t ) * taskBITS_PER_BYTE ) >= configNUMBER_OF_CORES );
    }
    #endif /* #if ( configUSE_CORE_AFFINITY == 1 ) && ( configNUMBER_OF_CORES > 1 ) */

    xReturn = prvCreateIdleTasks();      //创建空闲任务

    #if ( configUSE_TIMERS == 1 )     //如果使能软件定时器,则创建定时器任务
    {
        if( xReturn == pdPASS )
        {
            xReturn = xTimerCreateTimerTask();   //创建软件定时器任务
        }
        else
        {
            mtCOVERAGE_TEST_MARKER();
        }
    }
    #endif /* configUSE_TIMERS */

    if( xReturn == pdPASS )   //判断任务是否创建成功
    {
        #ifdef FREERTOS_TASKS_C_ADDITIONS_INIT
        {
            freertos_tasks_c_additions_init();
        }
        #endif

        portDISABLE_INTERRUPTS();   //关中断,防止调度器开启之前或过程中受中断干扰,会在运行第一个任务时打开中断

        #if ( configUSE_C_RUNTIME_TLS_SUPPORT == 1 )
        {
            configSET_TLS_BLOCK( pxCurrentTCB->xTLSBlock );
        }
        #endif

        xNextTaskUnblockTime = portMAX_DELAY;    //下一个任务的阻塞时间初始化为最大值,因为当前调度器还未开启
        xSchedulerRunning = pdTRUE;   //开启任务调度器(置调度器的运行状态位为1)
        xTickCount = (TickType_t) configINITIAL_TICK_COUNT;  //节拍计数初始化为0
        
        portCONFIGURE_TIMER_FOR_RUN_TIME_STATS();  //任务运行时间统计(需要用户实现,可选)
        traceTASK_SWITCHED_IN();   //供调试使用(需要用户实现,可选)
        ( void ) xPortStartScheduler();  //完成启动任务调度器中与硬件架构相关的配置部分,以及启动第一个任务
    }
    else
    {
        configASSERT( xReturn != errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY );
    }

    ( void ) xIdleTaskHandles;
    ( void ) uxTopUsedPriority;

    traceRETURN_vTaskStartScheduler();
}

2、xPortStartScheduler函数

(1)作用:完成启动任务调度器中与硬件架构相关的配置部分,以及启动第一个任务。

(2)函数的执行流程:

①检测用户在FreeRTOSConfig.h文件中对中断的相关配置是否有误。

②配置PendSV和SysTick的中断优先级为最低优先级。

③调用函数vPortSetupTimerInterrupt配置SysTick。

④初始化临界区嵌套计数器为0。

⑤调用函数prvEnableVFP使能FPU。(ARM Cortex-M3内核MCU无FPU)

⑥调用函数prvStartFirstTask启动第一个任务。

(3)源码剖析:

BaseType_t xPortStartScheduler( void )
{
    #if ( configASSERT_DEFINED == 1 )   //检测用户在FreeRTOSConfig.h文件中对中断的相关配置是否有误
    {
        volatile uint8_t ucOriginalPriority;
        volatile uint32_t ulImplementedPrioBits = 0;
        volatile uint8_t * const pucFirstUserPriorityRegister = ( uint8_t * ) ( portNVIC_IP_REGISTERS_OFFSET_16 + portFIRST_USER_INTERRUPT_NUMBER );
        volatile uint8_t ucMaxPriorityValue;

        ucOriginalPriority = *pucFirstUserPriorityRegister;

        *pucFirstUserPriorityRegister = portMAX_8_BIT_VALUE;

        ucMaxPriorityValue = *pucFirstUserPriorityRegister;

        ucMaxSysCallPriority = configMAX_SYSCALL_INTERRUPT_PRIORITY & ucMaxPriorityValue;

        configASSERT( ( configMAX_SYSCALL_INTERRUPT_PRIORITY & ( ~ucMaxPriorityValue ) ) == 0U );

        while( ( ucMaxPriorityValue & portTOP_BIT_OF_BYTE ) == portTOP_BIT_OF_BYTE )
        {
            ulImplementedPrioBits++;
            ucMaxPriorityValue <<= ( uint8_t ) 0x01;
        }

        if( ulImplementedPrioBits == 8 )
        {
            configASSERT( ( configMAX_SYSCALL_INTERRUPT_PRIORITY & 0x1U ) == 0U );
            ulMaxPRIGROUPValue = 0;
        }
        else
        {
            ulMaxPRIGROUPValue =portMAX_PRIGROUP_BITS -ulImplementedPrioBits;
        }

        ulMaxPRIGROUPValue <<= portPRIGROUP_SHIFT;
        ulMaxPRIGROUPValue &= portPRIORITY_GROUP_MASK;

        *pucFirstUserPriorityRegister = ucOriginalPriority;
    }
    #endif /* configASSERT_DEFINED */

    portNVIC_SHPR3_REG |= portNVIC_PENDSV_PRI;  //配置PendSV的中断优先级为最低优先级
    portNVIC_SHPR3_REG |= portNVIC_SYSTICK_PRI;  //配置SysTick的中断优先级为最低优先级
    vPortSetupTimerInterrupt();    //初始化滴答定时器
    uxCriticalNesting = 0;        //临界区嵌套数初始化为0
    prvStartFirstTask();           //启动第一个任务

    return 0;
}

二、启动第一个任务

1、prvStartFirstTask函数

(1)要想启动第一个任务,就需要将该任务的值恢复到CPU的寄存器中,而任务的值在创建任务时已经保存到任务堆栈中。(中断产生时,硬件自动将xPSR、PC(R15)、LR(R14)、R12、R3-R0保存和恢复,而R4-R11需要手动保存和恢复)

(2)prvStartFirstTask函数的作用:初始化启动第一个任务前的环境,主要是重新设置MSP指针,并使能全局中断。

(3)函数的执行流程:

①首先使用PRESERVE8进行8字节对齐,这是因为在任何时候都是需要4字节对齐的,而在调用入口得8字节对齐,在进行C编程的时候,编译器会自动完成的对齐的操作,而对于汇编,就需要开发者手动进行对齐。

②获取MSP的初始值。(进入中断后会强制使用MSP指针)

        程序在运行过程中需要一定的栈空间来保存局部变量等一些信息,当有信息保存到栈中时,MCU会自动更新SP指针,使SP指针指向最后一个入栈的元素,那么程序就可以根据SP指针来从栈中存取信息。

        ARMCortex-M提供了两个栈空间,这两个栈空间的堆栈指针分别是MSP(主堆栈指针)和PSP(进程堆指针)。在FreeRTOS中,MSP是给系统栈空间使用的,而PSP是给任务栈使用的,也就是说,FreeRTOS任务的空间是通过PSP指向的,而在进入中断服务函数时,则是使用MSP指针。当使用不同的堆栈指针时,SP会等于当前使用的堆栈指针。

③在获取了栈顶指针后,将MSP指针重新赋值为栈底指针(相当于逻辑删除程序之前保存在栈中的数据)。

④在vTaskStartScheduler函数中关闭了受FreeRTOS控制的中断,这里需要重新将它们打开。

⑤使用SVC命令,传入系统调用号0,触发SVC中断,以启动第一个任务。

(4)源码剖析:

__asm void prvStartFirstTask( void )
{
    PRESERVE8           //8字节对齐

    ldr r0, =0xE000ED08   //0xE000ED08是向量表偏移量寄存器VTOR的地址
    ldr r0, [ r0 ]          //获取VTOR的值
    ldr r0, [ r0 ]          //获取MSP的初始值

    /* 初始化MSP */
    msr msp, r0
    /* 使能全局中断 */
    cpsie i
    cpsie f
    dsb
    isb
    /* 使用SVC命令,传入系统调用号0,触发SVC中断,以启动第一个任务 */
    svc 0
    nop
    nop
}

2、vPortSVCHandler函数

(1)作用:当使能了全局中断,并且手动触发SVC中断后,就会进入到SVC的中断服务函数中,SVC中断只在启动第一次任务时会调用一次,以后均不调用。

(2)函数的执行流程:

①通过pxCurrentTCB获取优先级最高的就绪态任务的任务栈地址,优先级最高的就绪态任务是系统将要运行的任务。

②通过任务的栈顶指针将任务栈中的内容出栈到CPU寄存器中,任务栈中的内容在调用任务创建函数的时候已初始化,然后设置PSP指针。

③往BASEPRI寄存器中写0,允许中断。

④在SVC中断服务函数中,r14中的值为EXC_RETURN,这是一个特殊的值,有如下6个合法的选项,Cortex-M3中未使用浮点单元,且SVC中断结束后将使用PSP,并进入线程模式,故该值应为0xFFFFFFFD。

描述

使用浮点单元

未使用浮点单元

中断返回后进入Hamdler模式,并使用MSP

0xFFFFFFE1

0xFFFFFFF1

中断返回后进入线程模式,并使用 MSP

0xFFFFFFE9

0xFFFFFFF9

中断返回后进入线程模式,并使用 PSP

0xFFFFFFED

0xFFFFFFFD

(3)源码剖析:

__asm void vPortSVCHandler( void )
{
    PRESERVE8            //8字节对齐

	/* 获取任务栈地址 */
    ldr r3, =pxCurrentTCB    //获取优先级最高的任务的任务控制块地址
    ldr r1, [ r3 ]            //获取其首成员的地址
    ldr r0, [ r1 ]            //获取首成员的值(该任务栈空间的栈顶)
    
	/* 模拟出栈,并设置PSP */
    ldmia r0 !, { r4 - r11 }  //任务栈弹出到CPU寄存器
    msr psp, r0             //使PSP指向R0寄存器
    isb
    
	/* 使能所有中断 */
	mov r0, # 0
msr basepri, r0

	/* 使用PSP指针,并跳转到任务函数 */
    orr r14, # 0xd           //将R14中的值设置为0xFFFFFFFD
    bx r14                  //从PSP指向的栈中出栈R0-xPSR寄存器
}

三、任务切换

1、任务切换概述

(1)任务切换的本质其实就是CPU寄存器的切换(又叫上下文切换),这个过程在PendSV中断服务函数里完成。

(2)假设由任务A切换到任务B,这个过程主要分为两步:

①需暂停任务A的执行,并将此时任务A的寄存器值保存到任务堆栈,这个过程叫做保存现场。

②将任务B的各个寄存器值(被存于任务堆栈中)恢复到CPU寄存器中,这个过程叫做恢复现场。

2、PendSV中断

(1)PendSV中断的触发时机:

①滴答定时器中断调用。

②执行FreeRTOS提供的相关API函数,主要是portYIELD。

(2)启动PendSV中断的底层逻辑:通过向中断控制和状态寄存器ICSR的bit28写入1,挂起PendSV,以启动PendSV中断。

(3)滴答定时器中断调用可以触发PendSV中断,如下是xPortSysTickHandler函数(滴答定时器中断服务函数)的源码剖析:

①xPortSysTickHandler函数本体:

void xPortSysTickHandler( void )
{
    vPortRaiseBASEPRI();
    traceISR_ENTER();
    {
        if( xTaskIncrementTick() != pdFALSE )    //如果需要任务切换(上下文切换)
        {
            traceISR_EXIT_TO_SCHEDULER();

            portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;   //向中断控制和状态寄存器ICSR的bit28写入1,触发PendSV中断
        }
        else
        {
            traceISR_EXIT();
        }
    }

    vPortClearBASEPRIFromISR();
}

②xTaskIncrementTick函数:

BaseType_t xTaskIncrementTick( void )
{
    TCB_t * pxTCB;
    TickType_t xItemValue;
    BaseType_t xSwitchRequired = pdFALSE;

    #if ( configUSE_PREEMPTION == 1 ) && ( configNUMBER_OF_CORES > 1 )
    BaseType_t xYieldRequiredForCore[ configNUMBER_OF_CORES ] = { pdFALSE };
    #endif /* #if ( configUSE_PREEMPTION == 1 ) && ( configNUMBER_OF_CORES > 1 ) */

    traceENTER_xTaskIncrementTick();

    traceTASK_INCREMENT_TICK( xTickCount );

    if( uxSchedulerSuspended == ( UBaseType_t ) 0U )
    {
        const TickType_t xConstTickCount = xTickCount + ( TickType_t ) 1;

        xTickCount = xConstTickCount;

        if( xConstTickCount == ( TickType_t ) 0U )
            taskSWITCH_DELAYED_LISTS();
        else
            mtCOVERAGE_TEST_MARKER();

        if( xConstTickCount >= xNextTaskUnblockTime )  //判断是否到达下一个任务的阻塞超时时间
        {
            for( ; ; )
            {
                if( listLIST_IS_EMPTY( pxDelayedTaskList ) != pdFALSE )
                {
                    xNextTaskUnblockTime = portMAX_DELAY;
                    break;
                }
                else
                {
                    pxTCB = listGET_OWNER_OF_HEAD_ENTRY(pxDelayedTaskList);
                    xItemValue =listGET_LIST_ITEM_VALUE( &( pxTCB->xStateListItem ) );

                    if( xConstTickCount < xItemValue )
                    {
                        xNextTaskUnblockTime = xItemValue;
                        break;
                    }
                    else
                    {
                        mtCOVERAGE_TEST_MARKER();
                    }

                    listREMOVE_ITEM( &( pxTCB->xStateListItem ) );

                    if(listLIST_ITEM_CONTAINER(&(pxTCB->xEventListItem))!=NULL)
                        listREMOVE_ITEM( &( pxTCB->xEventListItem ) );
                    else
                        mtCOVERAGE_TEST_MARKER();

                    prvAddTaskToReadyList( pxTCB );  //将任务添加进就绪列表中

                    #if ( configUSE_PREEMPTION == 1 )
                    {
                        #if ( configNUMBER_OF_CORES == 1 )
                        {
							//判断唤醒的阻塞任务优先级是否高于当前运行任务的优先级,是则需要任务切换
                            if( pxTCB->uxPriority > pxCurrentTCB->uxPriority )
                                xSwitchRequired = pdTRUE;
                            else
                                mtCOVERAGE_TEST_MARKER();
                        }
                        #else /* #if( configNUMBER_OF_CORES == 1 ) */
                        {
                            prvYieldForTask( pxTCB );
                        }
                        #endif /* #if( configNUMBER_OF_CORES == 1 ) */
                    }
                    #endif /* #if ( configUSE_PREEMPTION == 1 ) */
                }
            }
        }

        //如果使能了抢占式调度和时间片调度,编译如下代码块
        #if ( (configUSE_PREEMPTION == 1) && (configUSE_TIME_SLICING == 1) )
        {
            #if ( configNUMBER_OF_CORES == 1 )
            {
                //如果同等优先级的就绪列表中的任务数量大于1,则需要进行任务切换
                if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ pxCurrentTCB->uxPriority ] ) ) > 1U )
                    xSwitchRequired = pdTRUE;
                else
                    mtCOVERAGE_TEST_MARKER();
            }
            #else /* #if ( configNUMBER_OF_CORES == 1 ) */
            {
                BaseType_t xCoreID;

                for( xCoreID = 0; xCoreID < ( ( BaseType_t ) configNUMBER_OF_CORES ); xCoreID++ )
                {
                    if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ pxCurrentTCBs[ xCoreID ]->uxPriority ] ) ) > 1U )
                        xYieldRequiredForCore[ xCoreID ] = pdTRUE;
                    else
                        mtCOVERAGE_TEST_MARKER();
                }
            }
            #endif /* #if ( configNUMBER_OF_CORES == 1 ) */
        }
        #endif /* #if ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) ) */

        #if ( configUSE_TICK_HOOK == 1 )
        {
            if( xPendedTicks == ( TickType_t ) 0 )
                vApplicationTickHook();
            else
                mtCOVERAGE_TEST_MARKER();
        }
        #endif /* configUSE_TICK_HOOK */

        #if ( configUSE_PREEMPTION == 1 )
        {
            #if ( configNUMBER_OF_CORES == 1 )
            {
                if( xYieldPendings[ 0 ] != pdFALSE )
                    xSwitchRequired = pdTRUE;
                else
                    mtCOVERAGE_TEST_MARKER();
            }
            #else /* #if ( configNUMBER_OF_CORES == 1 ) */
            {
                BaseType_t xCoreID, xCurrentCoreID;
                xCurrentCoreID = ( BaseType_t ) portGET_CORE_ID();

                for( xCoreID = 0; xCoreID < ( BaseType_t ) configNUMBER_OF_CORES; xCoreID++ )
                {
                    #if ( configUSE_TASK_PREEMPTION_DISABLE == 1 )
                        if( pxCurrentTCBs[xCoreID]->xPreemptionDisable ==pdFALSE )
                    #endif
                    {
                        if( ( xYieldRequiredForCore[ xCoreID ] != pdFALSE ) || ( xYieldPendings[ xCoreID ] != pdFALSE ) )
                        {
                            if( xCoreID == xCurrentCoreID )
                                xSwitchRequired = pdTRUE;
                            else
                                prvYieldCore( xCoreID );
                        }
                        else
                        {
                            mtCOVERAGE_TEST_MARKER();
                        }
                    }
                }
            }
            #endif /* #if ( configNUMBER_OF_CORES == 1 ) */
        }
        #endif /* #if ( configUSE_PREEMPTION == 1 ) */
    }
    else
    {
        xPendedTicks += 1U;

        #if ( configUSE_TICK_HOOK == 1 )
        {
            vApplicationTickHook();
        }
        #endif
    }

    traceRETURN_xTaskIncrementTick( xSwitchRequired );

    return xSwitchRequired;
}

(4)portYIELD函数源码剖析:

#define portYIELD()                                   	  			\
    {                                                 	  				\
        /*向中断控制和状态寄存器ICSR的bit28写入1,触发PendSV中断*/  	\
        portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;			 	\
                                                        			\
        __dsb( portSY_FULL_READ_WRITE );                           	\
        __isb( portSY_FULL_READ_WRITE );                           	\
    }

(5)PendSV的中断服务函数:

①PendSV的中断服务函数本体:

__asm void xPortPendSVHandler( void )
{
    extern uxCriticalNesting;
    extern pxCurrentTCB;
    extern vTaskSwitchContext;

/* *INDENT-OFF* */
    PRESERVE8      //8字节对齐

    mrs r0, psp       //将psp赋给r0(硬件自动压栈后,指针指向EXC_RETURN/R14)
    isb

    ldr r3, =pxCurrentTCB   //获取当前运行任务的控制块首成员(栈顶指针)的地址并赋给r3
    ldr r2, [ r3 ]           //根据当前运行任务的栈顶指针地址获取栈顶指针并赋给r2

    stmdb r0 !, { r4 - r11 } //手动保存R4到R11中的值
    str r0, [ r2 ]           //将新的堆栈顶部保存到TCB的第一个成员中

    stmdb sp !, { r3, r14 }  //对R14及R0-R3的内容进行压栈操作
    mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY  //FreeRTOS可管理的最高中断优先级写入R0
    msr basepri, r0  //将FreeRTOS可管理的最高中断优先级写入中断屏蔽寄存器(关中断)
    dsb
    isb
    bl vTaskSwitchContext    //寻找下一个运行的任务的任务控制块(后面都是操作下一个任务了)
    mov r0, #0
    msr basepri, r0          //开中断
    ldmia sp !, { r3, r14 }   //对R14及R0-R3的内容进行出栈操作

    ldr r1, [ r3 ]      //获取当前运行任务的控制块首成员(栈顶指针)的地址并赋给r1
    ldr r0, [ r1 ]      //根据当前运行任务的栈顶指针地址获取栈顶指针并赋给r0
    ldmia r0 !, { r4 - r11 } //将R4-R11的内容进行出栈操作
    msr psp, r0      //将psp赋给r0(硬件自动压栈后,指针指向EXC_RETURN/R14)
    isb
    bx r14          //返回线程模式,后续即可真正地执行下一个任务了
    nop
/* *INDENT-ON* */
}

②vTaskSwitchContext函数:

void vTaskSwitchContext( void )
{
    traceENTER_vTaskSwitchContext();

    if( uxSchedulerSuspended != ( UBaseType_t ) 0U )
    {
        xYieldPendings[ 0 ] = pdTRUE;
    }
    else
    {
        xYieldPendings[ 0 ] = pdFALSE;
        traceTASK_SWITCHED_OUT();

        #if ( configGENERATE_RUN_TIME_STATS == 1 )
        {
            #ifdef portALT_GET_RUN_TIME_COUNTER_VALUE
                portALT_GET_RUN_TIME_COUNTER_VALUE( ulTotalRunTime[ 0 ] );
            #else
                ulTotalRunTime[ 0 ] = portGET_RUN_TIME_COUNTER_VALUE();
            #endif

            if( ulTotalRunTime[ 0 ] > ulTaskSwitchedInTime[ 0 ] )
            {
                pxCurrentTCB->ulRunTimeCounter += ( ulTotalRunTime[ 0 ] - ulTaskSwitchedInTime[ 0 ] );
            }
            else
            {
                mtCOVERAGE_TEST_MARKER();
            }

            ulTaskSwitchedInTime[ 0 ] = ulTotalRunTime[ 0 ];
        }
        #endif /* configGENERATE_RUN_TIME_STATS */

        taskCHECK_FOR_STACK_OVERFLOW();

        #if ( configUSE_POSIX_ERRNO == 1 )
        {
            pxCurrentTCB->iTaskErrno = FreeRTOS_errno;
        }
        #endif

        taskSELECT_HIGHEST_PRIORITY_TASK();   //运行任务需要是优先级最高的任务(阻塞/挂起除外)
        traceTASK_SWITCHED_IN();

        portTASK_SWITCH_HOOK( pxCurrentTCB );

        #if ( configUSE_POSIX_ERRNO == 1 )
        {
            FreeRTOS_errno = pxCurrentTCB->iTaskErrno;
        }
        #endif

        #if ( configUSE_C_RUNTIME_TLS_SUPPORT == 1 )
        {
            configSET_TLS_BLOCK( pxCurrentTCB->xTLSBlock );
        }
        #endif
    }

    traceRETURN_vTaskSwitchContext();
}

③taskSELECT_HIGHEST_PRIORITY_TASK函数:

/*获取已有就绪任务的最高优先级,将其就绪列表中的一个任务调出来运行*/
    #define taskSELECT_HIGHEST_PRIORITY_TASK()      \
    do {                                               \
        UBaseType_t uxTopPriority;                       \
        portGET_HIGHEST_PRIORITY( uxTopPriority, uxTopReadyPriority );  \
        configASSERT( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ uxTopPriority ] ) ) > 0 );  \
        listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) );   \
    } while( 0 )

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Zevalin爱灰灰

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

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

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

打赏作者

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

抵扣说明:

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

余额充值