GD32F103RCT6/GD32F303RCT6-UCOSIII底层移植(3)任务调度

  本文章基于兆易创新GD32 MCU所提供的2.2.4版本库函数开发

       后续项目主要在下面该专栏中发布:

手把手教你嵌入式国产化_不及你的温柔的博客-CSDN博客

       感兴趣的点个关注收藏一下吧!

       电机驱动开发可以跳转:

手把手教你嵌入式国产化-实战项目-无刷电机驱动(1)-CSDN博客

       BMS电源系统开发可以跳转:暂未放链接

 向上代码兼容GD32F303RCT6中使用

本项目配套开发板:

基于GD32F103RCT6国产GD32平台,以下教程编写基于该开发板

图片:

a83f44e3ba7542238ec2f3c3d9002bbe.jpeg​​​

原理图以及例程请联系客服获取!

注意:

本教程致力于解决所有在调试中出现的所有问题,如有未包含在的问题,请联系QQ:2049363803,有奖更新文档!

 19636cfb016d493580d5f3777aebf505.jpg

 群号:621154399

有问题欢迎大家加入我们一起交流,这个群是开源性技术交流群。

参考资料:

《UCOS-III开发指南_V1.5》

《µC/OS-III Documentation》

《ARM Cortex-M3 与 Cortex-M4 权威指南(第 3 版)》

《uCOS-III内核实现与应用开发实战指南—基于STM32》

介绍:

在上一小结,我们完成了对uCOSIII的任务创建,并且成功创建了三个LED任务,循环点亮,但是之前我们在完成创建任务函数以后,就立马对任务进行了启动,并且CPU重复调用我们创建的三个任务函数,没有涉及到对任务函数的挂起/删除/等待/延时等一系列操作。

uCOSIII中的每一个任务函数都有多种运行状态,例如从运行态变成阻塞态或者从阻塞态变成就绪态等;在正式使用之前,我们需要对任务的各自运行状态及其切换有一个深入的了解,才能确保我们后续在系统设计的时候不会出现灾难性错误。

任务运行状态:

1.创建任务  到  就绪态:

首先,任务在创建以后,该任务会紧接着进入就绪态,等待任务调度器进行调度。

如果该任务的优先级大于当前正在运行任务的优先级,任务调度器会将CPU使用权转接给新创建的任务;反之则等待CPU空闲或者更高优先级任务释放CPU使用权。

2.就绪态  到  运行态:

当系统发生任务切换的时候,就绪列表中优先级最高的任务会被调度器选中,从而进入运行态。

3.运行态  到  就绪态

当系统中有优先级比当前任务更高的任务创建后或者原本进入等待/阻塞等的更高优先级的任务恢复就绪态以后。任务调度器会进行调度,此时处于就绪列表中优先级最高的任务变为运行态,那么原先处于运行态的任务则进入就绪态!

但是任务依旧存储在就绪列表中,在等更高优先级的任务运行完毕或者进入阻塞/挂起等状态后恢复该任务的CPU使用权。

4.运行态  到  阻塞态

当系统中正在运行的任务发生阻塞(挂起/延时/等待信号量等)时,该任务就会从就绪列表中被系统删除,任务的状态也从运行态变为阻塞态;此时任务调度器会紧接着进行任务切换,将当前就绪列表中任务优先级最高的函数由就绪态变为运行态。

5.阻塞态  到  就绪态

当处于阻塞态的任务被恢复后(任务恢复、延时时间超时或到达、读信号量超时或者已经读取到信号量),此时被恢复的函数就会被加入到就绪列表中,由阻塞态变为就绪态;如果当前占用CPU使用权的任务的优先级小于此时被恢复的任务,就会发生任务调度,被恢复的任务又会被二次转变,由就绪态变为运行态。

6.就绪态、阻塞态、运行态  到  删除态

在uCOSIII系统中,任务通过调用系统提供的任务删除函数OSTaskDel();来将处于任何状态的任务删除,被删除后的任务将无法再次使用,其栈空间以及TCB等所有资源会被系统回收。

7.删除态  到  就绪态

任务删除后无法恢复,此过程相当于是重新创建任务。

uCOS系统的任务状态

在uCOS系统中,每一个任务都具有多种运行状态,系统在初始化完毕后,由任务管理器进行CPU资源分配。

通常存在一下几种任务状态:

就绪

任务存在于就绪列表中,就绪的任务已经可以被调度器调度执行。

延时

任务处于延时调度状态

等待

任务被用户API所使用的等待函数调用后进入等待状态,系统会设置一个等待超时时间让任务处于等待状态,如果时间为0,则代表任务将无限期的等待,直到其所需要的事件发生;如果超时时间大于0,则在该段时间内任务如果等待的事件或者信号都没有发生,就会自动退出等待状态,重新进入就绪状态。

运行

任务拥有CPU使用权,调度器会将就绪列表中任务优先级最高的任务转为运行状态,处于运行态的任务也是位于就绪列表中。

挂起

任务通过调用OSTaskSuspend()函数,允许任务挂起自己或者其他函数,但是无法挂起空闲任务;并且调用恢复函数OSTaskResume()函数将会是使得挂起的任务恢复运行的唯一办法。并且!!!挂起多少次就需要恢复多少次!!

延时+挂起

任务先产生一个延时,并且当延时还没结束的时候被其他任务挂起(挂起的次数可以叠加),当且仅当延时结束并且挂起被恢复的时候,任务才能再次运行。挂起多少次就需要恢复多少次!!

等待+挂起

当任务等待一个事件或者信号的发生(无限期等待),并且还没等待到的时候被其他任务所挂起(挂起的次数可以叠加),当且仅当任务等待到所需事件或者信号并且挂起已经被恢复了,该任务才能再次运行。挂起多少次就需要恢复多少次!!

超时等待+挂起

任务在其指定的时间内等待事件或者信号的发生,此时任务已经被其他任务挂起。

删除

从以上所有状态中删除,想恢复只能重新新建任务。

任务状态就绪延时等待挂起运行删除
就绪1.00000
延时010000
等待001000
挂起000100
运行000010
延时+挂起010100
等待加挂起001100
超时等待+挂起011100
删除000001

常用任务函数

任务挂起函数OS_TaskSuspend()

可以挂起用户所指定的任务,被挂起后的任务不论具有什么优先级,都无法获得CPU使用权。可以将处于任何状态的任务挂起,被挂起的函数对于调度器来说是不可见的,除非从挂起状态中被恢复。

使用该函数需要将宏定义:OS_CFG_TASK_SUSPEND_EN 使能

dd39fdd04b6d4fc2a0827232c0bf37d3.png

第一个参数指向需要挂起的任务,也可以是任务本身,其中空闲任务被禁止挂起。

第二个参数用于存放返回的错误代码用于分析发生对应什么错误。

例子:

static OS_TCB AppTaskLed1TCB;/* LED 任务句柄 */ 

static void KEY_Task(void* parameter)
{
OS_ERR err;
while (1) 
{
if ( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON ) 
{
/* KEY1 被按下 */
printf("挂起 LED 任务!\n");

/* 挂起 LED1 任务 */ 
OSTaskSuspend (& AppTaskLed1TCB, & err ); 

}
/* 延时 20 个 tick */
OSTimeDly ( 20, OS_OPT_TIME_DLY, & err ); 
}
}

任务恢复函数OSTaskResume()

既然有任务挂起函数,那么自然而然也就存在用于任务恢复的函数,不然处于挂起态的任务,我们怎么去恢复他?任务恢复函数,实际上就是让被挂起的任务重新变成就绪状态。若是此时我们被恢复的任务在就绪列表中是优先级最高的函数,那么调度器就会切换调用该函数。

2c303d9689d94b94bb192149c314826f.png

第一个参数指向需要恢复的任务,与挂起任务有一个很大的不同,该函数不被允许恢复本身;其实想想就明白,你都挂起了,CPU不调用你,你哪怕写了恢复自己也没办法;或者一个正在运行的任务,都运行了还需要恢复自己?等等之类的一听就好理解。

第二个参数还是存储错误代码。

例子:

static OS_TCB AppTaskLed1TCB;/* LED 任务句柄 */ 

static void KEY_Task(void* parameter)
{
OS_ERR err;
while (1) 
{
if ( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON ) 
{
/* KEY1 被按下 */
printf("恢复 LED 任务!\n");

/* 恢复 LED 任务 */ 
OSTaskResume ( & AppTaskLed1TCB, & err );

}
/* 延时 20 个 tick */
OSTimeDly ( 20, OS_OPT_TIME_DLY, & err ); 
}
}

删除任务函数OSTaskDel()

该函数用于删除一个任务,当一个任务调用该函数去删除另外一个任务时,形参为所要删除的任务在创建时返回的任务句柄。如果是删除自身,那么输入的形参则为NULL。

要使用该函数则需要在os_cfg.h中将 OS_CFG_TASK_DEL_EN 宏定义配置为1,所删除的任务会从所有就绪、阻塞、挂起和事件列表中删除。

c74eedda71b14563bd5dff656244afd0.png

例子:

static OS_TCB AppTaskLed1TCB;/* LED 任务句柄 */ 

static void KEY_Task(void* parameter)
{
OS_ERR err;
while (1) 
{
if ( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON ) 
{
/* KEY1 被按下 */
printf("删除 LED 任务!\n");

/* 删除 LED 任务 */ 
OSTaskDel( & AppTaskLed1TCB, & err );

}
/* 延时 20 个 tick */
OSTimeDly ( 20, OS_OPT_TIME_DLY, & err ); 
}
}

任务延时函数OSTimeDly() 和OSTimeDlyHMSM()

OSTimeDly()在我们任务中用得非常之多,每个任务都必须是死循环,并且是必须要 有阻塞的情况,否则低优先级的任务就无法被运行了,OSTimeDly() 函数常用于停止当前 任务进行的运行,延时一段时间后再运行。

参数分别为延时的节拍数、选项、返回错误类型

b1c6e01b0c5c4677a83a02450956ac35.png

其中选项可以有以下选择:

OS_OPT_TIME_DLY
dly 为相对时间,就是从现在起延时多长
时 间 , 到 时 钟 节 拍 总 计 数 OSTickCtr = OSTickCtr 当前 + dly 时延时结束。
OS_OPT_TIME_TIMEOUT
跟 OS_OPT_TIME_DLY 的作用情况
一样。
OS_OPT_TIME_MATCH
dly 为绝对时间,就是从系统开始运行
(调用 OSStart()) 时到节拍总计数 OSTickCtr = dly 时延时结束。
OS_OPT_TIME_PERIODIC
周 期 性 延 时 , 跟
OS_OPT_TIME_DLY 的作用差不多,如果是长时间延时,该选项更精准一些。

例子:

/* 调用相对延时函数,阻塞 1000 个 tick */ 
OSTimeDly ( 1000, OS_OPT_TIME_DLY, & err );

OSTimeDlyHMSM() 函数与 OSTimeDly() 函数的功能类似,也是用于停止当前任务进
行的运行,延时一段时间后再运行,但是 OSTimeDlyHMSM()函数会更加直观,延时多少
个小时、分钟、秒、毫秒。

若要使用该函数,则必须将宏 OS_CFG_TIME_DLY_HMSM_EN 设置为1。

8ea6554342f5495b8b54525008f17a9b.png

参数分别为:延时的小时数、分钟数、秒数、毫秒数、选项、返回的错误类型。

其中选项可以有以下选择:

OS_OPT_TIME_DLY
dly 为相对时间,就是从现在起延时多长
时 间 , 到 时 钟 节 拍 总 计 数 OSTickCtr = OSTickCtr 当前 + dly 时延时结束。
OS_OPT_TIME_TIMEOUT
跟 OS_OPT_TIME_DLY 的作用情况
一样。
OS_OPT_TIME_MATCH
dly 为绝对时间,就是从系统开始运行
(调用 OSStart()) 时到节拍总计数 OSTickCtr = dly 时延时结束。
OS_OPT_TIME_PERIODIC
周 期 性 延 时 , 跟
OS_OPT_TIME_DLY 的作用差不多,如果是长时间延时,该选项更精准一些。
OS_OPT_TIME_HMSM_STRICT
延时时间取值比较严格
OS_OPT_TIME_HMSM_NON_STRICT
延时时间取值比较宽松

例子:

OSTimeDlyHMSM( 0,0,1,0, OS_OPT_TIME_DLY, & err );

合理的任务设计

取自:《uCOS-III内核实现与应用开发实战指南—基于STM32》

作为一个嵌入式开发人员,要对自己设计的嵌入式系统要了如指掌,任务的优先级信息,任务与中断的处理,任务的运行时间、逻辑、状态等都要知道,才能设计出好的系统,所以,在设计的时候需要根据需求制定框架。在设计之初就应该考虑下面几点因素:任务运行的上下文环境、任务的执行时间合理设计。

uCOS 中程序运行的上下文包括:

1. 中断服务函数:

中断服务函数是一种需要特别注意的上下文环境,它运行在非任务的执行环境下(一般为芯片的一种特殊运行模式(也被称作特权模式)),在这个上下文环境中不能使用挂起当前任务的操作,不允许调用任何会阻塞运行的 API 函数接口。另外需要注意的是,中断服务程序最好保持精简短小,快进快出,一般在中断服务函数中只做标记事件的发生,
然后通知任务,让对应任务去执行相关处理,因为中断服务函数的优先级高于任何优先级的任务,如果中断处理时间过长,将会导致整个系统的任务无法正常运行。
所以在设计的时候必须考虑中断的频率、中断的处理时间等重要因素,以便配合对应中断处理任务的工作。
uCOS 支持中断延迟发布,使得原本在中断中发布的信息变成任务级发布,这样子会使得中断服务函数的处理更加快速,屏蔽中断的时间更短,这样子能快速响应其他的中断,真正称得上实时操作系统。

2. 任务:

任务看似没有什么限制程序执行的因素,似乎所有的操作都可以执行。但是做为一个优先级明确的实时系统,如果一个任务中的程序出现了死循环操作(此处的死循环是指没有阻塞机制的任务循环体),那么比这个任务优先级低的任务都将无法执行,当然也包括了空闲任务,因为死循环的时候,任务不会主动让出 CPU,低优先级的任务是不可能得到
CPU 的使用权的,而高优先级的任务就可以抢占 CPU。这个情况在实时操作系统中是必须
注意的一点,所以在任务中不允许出现死循环。如果一个任务只有就绪态而无阻塞态,势必会影响到其他低优先级任务的执行,所以在进行任务设计时,就应该保证任务在不活跃的时候,任务可以进入阻塞态以交出 CPU 使用权,这就需要我们自己明确知道什么情况下让任务进入阻塞态,保证低优先级任务可以正常运行。在实际设计中,一般会将紧急的处
理事件的任务优先级设置得高一些。

3. 空闲任务:

空闲任务(idle 任务)是 uCOS 系统中没有其他工作进行时自动进入的系统任务。因为处理器总是需要代码来执行——所以至少要有一个任务处于运行态。uCOS 为了保证这一点,当调用 OSInit()函数进行系统初始化时,系统会自动创建一个空闲任务,空闲任务是一个非常短小的循环。用户可以通过空闲任务钩子方式,在空闲任务上钩入自己的功能
函数。通常这个空闲任务钩子能够完成一些额外的特殊功能,例如系统运行状态的指示,系统省电模式等。空闲任务是唯一一个不允许出现阻塞情况的任务,因为 uCOS 需要保证系统永远都有一个可运行的任务。
对于空闲任务钩子上挂接的空闲钩子函数,它应该满足以下的条件:

1.永远不会挂起空闲任务;

2.不应该陷入死循环,需要留出部分时间用于统计系统的运行状态等。

4. 任务的执行时间:

任务的执行时间一般是指两个方面,一是任务从开始到结束的时间,二是任务的周期。
在系统设计的时候这两个时间候我们都需要考虑,例如,对于事件 A 对应的服务任务Ta,系统要求的实时响应指标是 10ms,而 Ta 的最大运行时间是 1ms,那么 10ms 就是任务Ta 的周期了,1ms 则是任务的运行时间,简单来说任务 Ta 在 10ms 内完成对事件 A 的响应即可。
此时,系统中还存在着以 50ms 为周期的另一任务 Tb,它每次运行的最大时间长度是 100us。在这种情况下,即使把任务 Tb 的优先级设置比 Ta 更高,对系统的实时性指标也没什么影响,因为即使在 Ta 的运行过程中,Tb 抢占了 Ta 的资源,等到 Tb 执行完毕,消耗的时间也只不过是 100us,还是在事件 A 规定的响应时间内(10ms),Ta 能够安全完成
对事件 A 的响应。但是假如系统中还存在任务 Tc,其运行时间为 20ms,假如将 Tc 的优先级设置比 Ta 更高,那么在 Ta 运行的时候,突然间被 Tc 打断,等到 Tc 执行完毕,那 Ta 已经错过对事件 A(10ms)的响应了,这是不允许的。所以在我们设计的时候,必须考虑任务的时间,一般来说处理时间更短的任务优先级应设置更高一些。

任务调度实验

本次实验我们将会对以上常用的任务函数进行实验,开发板还是选用我们自制的GD32开发板,创建LED1、LED2两个任务,两个任务的优先级均是三,采用默认时间片轮转调度运行。两个任务每隔一秒翻转一次其对应LED电平状态,其中,LED1在翻转五次后挂起LED2,LED1则继续翻转自身,当LED1翻转10次以后,恢复LED2的运行;

#include <includes.h>


/*
*************************************************************************
* 任务栈存放区域
*************************************************************************
*/


static CPU_STK AppTaskStartStk[APP_TASK_START_STK_SIZE];

static CPU_STK AppTaskLed1Stk [ APP_TASK_LED1_STK_SIZE ]; 
static CPU_STK AppTaskLed2Stk [ APP_TASK_LED2_STK_SIZE ]; 



/*
*************************************************************************
* 定义任务控制块存放区域
*************************************************************************
*/
static OS_TCB AppTaskStartTCB;

static OS_TCB AppTaskLed1TCB;
static OS_TCB AppTaskLed2TCB;


/*
*************************************************************************
* 函数原型存放区域
*************************************************************************
*/

static void AppTaskStart (void *p_arg);

static void AppTaskLed1 ( void * p_arg ); 
static void AppTaskLed2 ( void * p_arg ); 



/*
*************************************************************************
* main 函数
*************************************************************************
*/
/**
* @brief 主函数
* @param 无
* @retval 无
*/
int main(void)
{
	OS_ERR err;
	
	OSInit(&err);
	
	OSTaskCreate((OS_TCB *)&AppTaskStartTCB, 
							(CPU_CHAR *)"App Task Start",
							(OS_TASK_PTR ) AppTaskStart,
							(void *) 0,
							(OS_PRIO ) APP_TASK_START_PRIO,
							(CPU_STK *)&AppTaskStartStk[0],
							(CPU_STK_SIZE) APP_TASK_START_STK_SIZE / 10,
							(CPU_STK_SIZE) APP_TASK_START_STK_SIZE,
							(OS_MSG_QTY ) 5u,
							(OS_TICK ) 0u,
							(void *) 0,
							(OS_OPT )(OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR),
							(OS_ERR *)&err);
  OSStart(&err);
							
							
}


/*
*************************************************************************
* 空闲任务存放区域
*************************************************************************
*/

static void AppTaskStart (void *p_arg)
{
 CPU_INT32U cpu_clk_freq;
 CPU_INT32U cnts;
 OS_ERR err;
 
 
 (void)p_arg;
 
 BSP_Init(); /* Initialize BSP functions 
 */
 CPU_Init();
 
 cpu_clk_freq = BSP_CPU_ClkFreq(); /* Determine SysTick reference 
 freq. */
 cnts = cpu_clk_freq / (CPU_INT32U)OSCfg_TickRate_Hz; /* Determine 
 nbr SysTick increme nts */
 OS_CPU_SysTickInit(cnts); /*Init uC/OS periodic time src(SysTick).*/
 
 
 Mem_Init(); /* Initialize Memory Management Module 
 */
 
 #if OS_CFG_STAT_TASK_EN > 0u
 OSStatTaskCPUUsageInit(&err); /* Compute CPU capacity with no task 
 running */
 #endif
 
 CPU_IntDisMeasMaxCurReset();
 
 
 OSTaskCreate((OS_TCB *)&AppTaskLed1TCB,/*Create the Led1 task */ 
 (CPU_CHAR *)"App Task Led1", 
 (OS_TASK_PTR ) AppTaskLed1, 
 (void *) 0, 
 (OS_PRIO ) APP_TASK_LED1_PRIO, 
 (CPU_STK *)&AppTaskLed1Stk[0], 
 (CPU_STK_SIZE) APP_TASK_LED1_STK_SIZE / 10, 
 (CPU_STK_SIZE) APP_TASK_LED1_STK_SIZE, 
 (OS_MSG_QTY ) 5u, 
 (OS_TICK ) 0u, 
 (void *) 0, 
 (OS_OPT )(OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR), 
 (OS_ERR *)&err); 
 
 OSTaskCreate((OS_TCB *)&AppTaskLed2TCB, /*Create the Led2 task*/ 
 (CPU_CHAR *)"App Task Led2", 
 (OS_TASK_PTR ) AppTaskLed2, 
 (void *) 0, 
 (OS_PRIO ) APP_TASK_LED2_PRIO, 
 (CPU_STK *)&AppTaskLed2Stk[0], 
 (CPU_STK_SIZE) APP_TASK_LED2_STK_SIZE / 10, 
 (CPU_STK_SIZE) APP_TASK_LED2_STK_SIZE, 
 (OS_MSG_QTY ) 5u, 
 (OS_TICK ) 0u, 
 (void *) 0, 
 (OS_OPT )(OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR), 
 (OS_ERR *)&err); 
 


 OSTaskDel ( & AppTaskStartTCB, & err ); 

}


 /*
 ********************************************************************
 * LED1 TASK
 ********************************************************************
 */
 
static void AppTaskLed1 ( void * p_arg ) 
{ 
 OS_ERR err; 
 OS_REG time;
 
 (void)p_arg; 
 
 
 while (DEF_TRUE) { /* Task body, always written as an infinite 
 loop.*/ 
	 
 LED1_TOG; 
 time = OSTaskRegGet ( 0, 0, & err ); //获取自身任务寄存器值
 if(time<10)
 {
  OSTaskRegSet ( 0, 0, ++ time, & err ); //继续累加任务寄存器值
 }
 else
 {
 OSTaskRegSet ( 0, 0, 0, & err ); //将任务寄存器值归 0
 OSTaskResume ( & AppTaskLed2TCB, & err ); //恢复 LED2 任务
	 
 }
 
 OSTimeDly ( 1000, OS_OPT_TIME_DLY, & err ); 
 } 
 
 
} 
 
 
 /*
 ***********************************************************************
 * LED2 TASK
 ************************************************************************
 */
 
static void AppTaskLed2 ( void * p_arg ) 
{ 
 OS_ERR err; 
 OS_REG time;
 
 (void)p_arg; 
 
 
 while (DEF_TRUE) { /* Task body, always written as an 
 infinite loop. */ 
 LED2_TOG; 
 time = OSTaskRegGet ( 0, 0, & err ); //获取自身任务寄存器值
	 
 if(time<5)
 {
  OSTaskRegSet ( 0, 0, ++ time, & err ); //继续累加任务寄存器值
 }
 else
 {
 OSTaskRegSet ( 0, 0, 0, & err ); //将任务寄存器值归 0
	 
 OSTaskSuspend ( 0, & err ); //挂起自身
	 
 }
	 
 OSTimeDly ( 1000, OS_OPT_TIME_DLY, & err ); 
 }
}










实验结果:

完成预期目标,实验完成!

GD32UCOSIII任务调度测试视频

群号:621154399

有问题欢迎大家加入我们一起交流,这个群是开源性技术交流群。

  • 13
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

不及你的温柔

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

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

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

打赏作者

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

抵扣说明:

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

余额充值