Free RTOS学习笔记 - 队列,信号量,互斥量,事件组,任务通知

一、队列

1、队列的特点

        数据的操作采用先进先出的方法(FIFO),写数据时放到尾部,读数据时从头部开始读取。

        也可以强制写队列头部,但不会覆盖头部数据。

        队列中每个数据的大小时固定的。

        创建队列时要指定长度,数据大小。

2、队列的使用

在创建队列时,会创建两个链表,分别存放等待数据的任务和等待向队列中写的数据,buf中存放队列的数据。

1)创建队列

#define xQueueCreate( uxQueueLength, uxItemSize )    xQueueGenericCreate( ( uxQueueLength ), ( uxItemSize ), ( queueQUEUE_TYPE_BASE ) )
//uxQueueLength 队列长度
//uxItemSize 每个项大小

可以看到xQueueXCreat()最后调用的是xQueueGenericCreate()。

创建队列时,会创建几个指针,pcHead始终指向头部,不会移动;pcWriteto在初始化时指向头部,后面指向要写入的下一个位置;pcReadFrom指向上一次读的位置,使用前要指向这一次读的位置。

2)向队列中写入数据

//向队列中写数据
#define xQueueSend( xQueue, pvItemToQueue, xTicksToWait ) \
    xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), ( xTicksToWait ), queueSEND_TO_BACK )
//向队列尾部写数据
#define xQueueSendToBack( xQueue, pvItemToQueue, xTicksToWait ) \
    xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), ( xTicksToWait ), queueSEND_TO_BACK )
//向队列首部写数据
#define xQueueSendToFront( xQueue, pvItemToQueue, xTicksToWait ) \
    xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), ( xTicksToWait ), queueSEND_TO_FRONT )
//向队列尾部写数据,可以在中断函数中使用,不可阻塞
#define xQueueSendToBackFromISR( xQueue, pvItemToQueue, pxHigherPriorityTaskWoken ) \
    xQueueGenericSendFromISR( ( xQueue ), ( pvItemToQueue ), ( pxHigherPriorityTaskWoken ), queueSEND_TO_BACK )
//向队列头部写数据,可以在中断函数中使用,不可阻塞
#define xQueueSendToFrontFromISR( xQueue, pvItemToQueue, pxHigherPriorityTaskWoken ) \
    xQueueGenericSendFromISR( ( xQueue ), ( pvItemToQueue ), ( pxHigherPriorityTaskWoken ), queueSEND_TO_FRONT )

向队列中写数据,默认就是向队列尾部写数据;向队列头部写数据实际上是队列其他数据不变,然后写到pcReadFrom的位置,pcReadFrom向后移动一个位置,这样就写到了头部,下一次读取的就是这个数据。

3)从队列中读数据

BaseType_t xQueueReceive( QueueHandle_t xQueue,
                          void * const pvBuffer,
                          TickType_t xTicksToWait )
BaseType_t xQueueReceiveFromISR( QueueHandle_t xQueue,
                                 void * const pvBuffer,
                                 BaseType_t * const pxHigherPriorityTaskWoken )

4)查询

查询队列中有多少个数据,有多少剩余空间。

//返回队列中可用数据的个数
UBaseType_t uxQueueMessagesWaiting( const QueueHandle_t xQueue )
//返回队列中可用空间的个数
UBaseType_t uxQueueSpacesAvailable( const QueueHandle_t xQueue )

5)覆盖/偷看

当队列长度为1时,可以使用xQueueOverwrite()或xQueueOverwriteFromISR()来覆盖数据。队列长度必须为1,当队列满时,这些函数会覆盖里面的数据,这也意味着这些函数不会被阻塞。

#define xQueueOverwrite( xQueue, pvItemToQueue ) \
    xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), 0, queueOVERWRITE )

#define xQueueOverwriteFromISR( xQueue, pvItemToQueue, pxHigherPriorityTaskWoken ) \
    xQueueGenericSendFromISR( ( xQueue ), ( pvItemToQueue ), ( pxHigherPriorityTaskWoken ), queueOVERWRITE )

偷看时不会移除数据,这些函数会从队列中复制出数据。这样意味着如果队列中没有数据,这些函数将会一直阻塞,一旦队列中有数据,每次偷看都会成功。

BaseType_t xQueuePeek( QueueHandle_t xQueue,
                       void * const pvBuffer,
                       TickType_t xTicksToWait )

BaseType_t xQueuePeekFromISR( QueueHandle_t xQueue,
                              void * const pvBuffer )

3、队列集

顾名思义,队列集就是队列的集合,当接收程序想从多个队列中读取数据的时候,如果采用轮训的方式会导致效率低下,可以假设要从mouse,key,touch,三个队列中读取数据,当三个队列中某个队列由数据写入的时候,就会将其句柄放入到Queue_Set中,当从队列集中读取数据的时候,只需获取到其句柄,然后从其对应的队列中读取数据即可。

使用队列集的步骤如下:

	//1.创建队列集
	xQueueSet  = xQueueCreateSet(4);
	//2.添加联系
	xQueueAddToSet(xQueueHandle1,xQueueSet);
	xQueueAddToSet(xQueueHandle2,xQueueSet);
	//3.创建三个任务
	xTaskCreate(Task1Function, "Task1", 100, NULL, 1, &xHandleTask1);
	xTaskCreate(Task2Function, "Task2", 100, NULL, 1, NULL);
	xTaskCreate(Task3Function, "Task3", 100, NULL, 1, NULL);
    //4.读取QueueSet
    handle = xQueueSelectFromSet(xQueueSet,portMAX_DELAY)
    //5.读取队列
    xQueueReceive(handle,&i,0);

二、信号量

1、特点

        只传递状态,不传递具体的信息。

        相较于队列,更节省内存。

2、信号量和队列的区别,信号量的分类如下

二进制信号量技术型信号量
被创建时初始值为0被创建时初始值可以设定
其他操作一样其它操作一样

3、信号量函数

1)创建信号量

//动态创建一个二进制信号量,返回它的句柄,可以看到实际上创建了一个长度为1,无符号字符型的队列
#define xSemaphoreCreateBinary()    xQueueGenericCreate( ( UBaseType_t ) 1, semSEMAPHORE_QUEUE_ITEM_LENGTH, queueQUEUE_TYPE_BINARY_SEMAPHORE )

//静态创建一个二进制信号量,返回它的句柄,可以看到实际上创建了一个长度为1,无符号字符型的队列
#define xSemaphoreCreateBinaryStatic( pxStaticSemaphore )    xQueueGenericCreateStatic( ( UBaseType_t ) 1, semSEMAPHORE_QUEUE_ITEM_LENGTH, NULL, pxStaticSemaphore, queueQUEUE_TYPE_BINARY_SEMAPHORE )

//动态创建一个计数型信号量
//返回它的句柄,
//uxMaxCount:最大计数值
//uxInitialCount :计数初始值
 #define xSemaphoreCreateCounting( uxMaxCount, uxInitialCount )    xQueueCreateCountingSemaphore( ( uxMaxCount ), ( uxInitialCount ) )

//静态创建一个技术型信号量
//返回它的句柄,非NULL代表成功
//uxMaxCount:最大计数值
//uxInitialCount:计数初始值
//pxSemaphoreBuffer :StaticSemaphore_t结构体指针
#define xSemaphoreCreateCountingStatic( uxMaxCount, uxInitialCount, pxSemaphoreBuffer )    xQueueCreateCountingSemaphoreStatic( ( uxMaxCount ), ( uxInitialCount ), ( pxSemaphoreBuffer ) )

2)删除

//删除一个信号量
//xSemaphore :信号量句柄
//实际上删除一个队列
#define vSemaphoreDelete( xSemaphore )  vQueueDelete( ( QueueHandle_t ) ( xSemaphore ) )

3)give/take

在任务中使用在ISR中使用
givexSemaphoreGivexSemaphoreGiveFromISR
takexSemaphoreTakexSemaphoreTakeFromISR
//释放信号量
//xSemaphore:释放哪个信号量
//返回值:pdTRUE,如果二进制信号量的计数值是1,再次调用则返回失败
                 如果计数型信号量的计数值已是最大值,再次调用则返回失败
#define xSemaphoreGive( xSemaphore )    xQueueGenericSend( ( QueueHandle_t ) ( xSemaphore ), NULL, semGIVE_BLOCK_TIME, queueSEND_TO_BACK )

//在ISR中释放信号量
//xSemaphore:信号量句柄,释放哪个信号量
//pxHigherPriorityTaskWoken :如果释放信号量导致更高优先级的任务变为了就绪态,则*pxHigherPriorityTaskWoken = pdTRUE
//返回值 pdTRUE表示成功,如果二进制信号量的计数值是1,再次调用则返回失败
                 如果计数型信号量的计数值已是最大值,再次调用则返回失败
#define xSemaphoreGiveFromISR( xSemaphore, pxHigherPriorityTaskWoken )    xQueueGiveFromISR( ( QueueHandle_t ) ( xSemaphore ), ( pxHigherPriorityTaskWoken ) )

//拿走信号量
//xSemaphore:拿走哪个信号量
//xBlockTime:等待时间,0:不阻塞,马上返回portMAX_DELAY:一直阻塞到成功
//其他值,阻塞的Tick个数,可以使用pdMS_TO_TICKS()来指定阻塞时间为若干ms
//返回值,pdTRUE表示成功
#define xSemaphoreTake( xSemaphore, xBlockTime )    xQueueSemaphoreTake( ( xSemaphore ), ( xBlockTime ) )

//在ISR中拿走信号量
//xSemaphore:信号量句柄,拿走哪个信号量
//pxHigherPriorityTaskWoken 如果获取信号量导致更高优先级的任务变为了就绪态,
则*pxHigherPriorityTaskWoken = pdTRUE
//返回值:返回pdTRTUE代表成功
#define xSemaphoreTakeFromISR( xSemaphore, pxHigherPriorityTaskWoken )    xQueueReceiveFromISR( ( QueueHandle_t ) ( xSemaphore ), NULL, ( pxHigherPriorityTaskWoken ) )

三、互斥量(互斥锁)

1、互斥量主要来解决两个问题:

        信号量导致的优先级继承:假设一个场景,有三个信号量,A,B,C.优先级顺序是C>B>A,A执行后,A获得锁后执行,B到达就绪态,B执行,C到达就绪态,C执行,C想要获得锁,但是由于A拿走了锁,导致C阻塞,从而B执行,但是由于C阻塞,A的优先级小于B,所以B会一直执行。

优先级继承指的是,当C到达就绪态后,C想要获得锁,但是锁在A那里,往后,A获得C的优先级,A执行完后,释放锁,C继续执行。

        谁拿走锁,谁释放锁。

        但是FreeRTOS的一般的互斥量并没有实现这一点,而要靠递归锁实现。

2、函数原型

互斥量不能在ISR中使用,互斥量是一种特殊的信号量,获得和释放基本和信号量一致。

//动态创建一个互斥量,返它的句柄
//函数内部会分配一个互斥量的结构体
//返回值:返回句柄,非NULL表示成功
#define xSemaphoreCreateMutex()    xQueueCreateMutex( queueQUEUE_TYPE_MUTEX )

//静态创建一个互斥量,返回它的句柄
//此函数无需动态分配内存,传入一个pxMutexBuffer 即可
//返回值:非NULL代表成功
#define xSemaphoreCreateMutexStatic( pxMutexBuffer )    xQueueCreateMutexStatic( queueQUEUE_TYPE_MUTEX, ( pxMutexBuffer ) )


//删除一个互斥量
//xSemaphore :信号量的句柄结构体

//#define vSemaphoreDelete( xSemaphore )                   vQueueDelete( ( QueueHandle_t ) ( xSemaphore )

//释放互斥量
//xSemaphore:信号量的句柄结构体
#define xSemaphoreGive( xSemaphore )    xQueueGenericSend( ( QueueHandle_t ) ( xSemaphore ), NULL, semGIVE_BLOCK_TIME, queueSEND_TO_BACK )

//获得互斥量
//xSemaphore:互斥量的句柄结构体
//xBlockTime :等待时间
#define xSemaphoreTake( xSemaphore, xBlockTime )    xQueueSemaphoreTake( ( xSemaphore ), ( xBlockTime ) )

3、递归锁

递归锁的函数和一般互斥量的函数名不一样,参数类型一样,列表如下

递归锁一般互斥量
创建xSemaphoreCreateRecursiveMutexxSemaphoreCreateMutex
获得xSemaphoreTakeRecursivexSemaphoreTake
释放xSemaphoreGiveRecursivexSemaphoreGive

函数原型

//创建一个递归锁
//返回值:返回句柄,非NULL代表成功
#define xSemaphoreCreateRecursiveMutex()    xQueueCreateMutex( queueQUEUE_TYPE_RECURSIVE_MUTEX )

//释放互斥锁
//xMutex:递归锁句柄
//返回值:成功返回pdTRUE
#define xSemaphoreGiveRecursive( xMutex )    xQueueGiveMutexRecursive( ( xMutex ) )

//获得互斥锁
//xMutex:互斥锁句柄结构体
//xBlockTime:等待时间
//返回值:成功返回pdTRUE,超过xBlockTime时间,返回pdFALSE
#define xSemaphoreTakeRecursive( xMutex, xBlockTime )    xQueueTakeMutexRecursive( ( xMutex ), ( xBlockTime ) )

四、事件组

1、特点

1)事件组可以认为是一个整数,每一位表示一个事件,每一位事件的含义由程序员决定,

2)一个或多个任务,ISR都可以去写这些位,一个或多个任务,ISR都可以去读这些位

3)可以等待某一位,某些位中的任意一个,也可以等待多位

4)能表示事件组的整数是多少位的,取决于系统是多少位的。

5)和队列和信号量的对比如下

队列、信号量事件组
事件发生时,只会唤醒一个任务时间发生时们可以唤醒所有符合条件的任务
是消耗型的资源,队列数据读走就没了,信号量获取后就减少了被唤醒的任务有两个选择,可以让事件保持不动,也可以清除事件

2、函数原型

//静态创建一个事件组
//成功返回它的句柄结构体
EventGroupHandle_t xEventGroupCreate( void );

//动态创建一个事件组
EventGroupHandle_t xEventGroupCreateStatic( StaticEventGroup_t * pxEventGroupBuffer );

//删除事件组
//xEventGroup:要删除事件组的句柄结构体
void vEventGroupDelete( EventGroupHandle_t xEventGroup );

//设置事件组中的位
//xEventGroup:事件组的句柄结构体
//uxBitsToSet:设置哪些位
//返回值:返回原来的事件值,意义不大
EventBits_t xEventGroupSetBits( EventGroupHandle_t xEventGroup,
                                const EventBits_t uxBitsToSet );

//在ISR中设置事件组的位
//pxHigherPriorityTaskWoken:有没有导致更高优先级的任务进入就绪态,pdTRUE-有,pdFALSE-没有
//返回值,paTRUE-成功,pdFALSE失败
BaseType_t xEventGroupSetBitsFromISR( EventGroupHandle_t xEventGroup,
                                          const EventBits_t uxBitsToSet,
                                          BaseType_t * pxHigherPriorityTaskWoken )
//等待事件
//uxBitsToWaitFor:等待哪些位,哪些位要被测试
//xClearOnExit:函数退出前是否要清除事件,pdTRUE,清除uxBitsToWaitFor的位,pdFALSE,不清除
//xClearOnExit:怎么测试,是AND还是OR,pdTRUE,等待的位全为1,pdFALSE,等待的位某一个为1
//xTicksToWait :等待多久
//返回值:返回的是事件,即如果期待的时间发生了,则返回发生的事件。如果超时退出,则返回超时时刻的事件值
EventBits_t xEventGroupWaitBits( EventGroupHandle_t xEventGroup,
                                 const EventBits_t uxBitsToWaitFor,
                                 const BaseType_t xClearOnExit,
                                 const BaseType_t xWaitForAllBits,
                                 TickType_t xTicksToWait )

3、同步点

假设有一个事情需要多个任务协同。

任务A:炒菜

任务B:买酒

任务C:摆台

ABC做好自己的任务后,还需等待别人做完才可以开饭

这个时候用同步点会更加方便

函数原型如下:

//xEventGroup:哪个事件组
//uxBitsToSet:自己完成了哪些事件
//uxBitsToWaitFor:要等待哪些事件
//xTicksToWait:等待多久
//返回值:如果期待的事件发生了,返回的是“非阻塞条件成立”时事件值
//如果超时退出,返回的是超时时刻的事件值
EventBits_t xEventGroupSync( EventGroupHandle_t xEventGroup,
                             const EventBits_t uxBitsToSet,
                             const EventBits_t uxBitsToWaitFor,
                             TickType_t xTicksToWait )

五、任务通知

1、任务通知的优点和缺点如下

优点缺点
效率更高不能发送数据给ISR
更节省内存,不需要创建额外的结构体数据只能给该任务独享
无法缓冲数据
无法广播给多个任务
如果发送受阻,发送方无法进入阻塞状态等待

任务通知中,值和状态保存在这里。

//configTASK_NOTIFICATION_ARRAY_ENTRIES 是1   
// ucNotifyState:taskNOT_WAITING_NOTIFICATION:任务没有在等待通知
//taskWAITING_NOTIFICATION:任务在等待通知
//taskNOTIFICATION_RECEIVED:任务接受到了通知
//通知值也有很多类型:计数值,位(类似事件组),任意数值
#if ( configUSE_TASK_NOTIFICATIONS == 1 )
        volatile uint32_t ulNotifiedValue[ configTASK_NOTIFICATION_ARRAY_ENTRIES ];
        volatile uint8_t ucNotifyState[ configTASK_NOTIFICATION_ARRAY_ENTRIES ];
    #endif

也就是说一个uint32_t类型,用来表示通知值,一个uint8_t 类型,用来表示状态。

2、函数原型

有两套,一套专业版,一套简化版

简化版:

使用xTaskNotifyGive()时:

使得通知值加1,并使得通知状态变为“pending”。

使用ulTaskNotifyTake()取出通知值时:

如果通知值等于0,则阻塞

当通知值大于0时。任务从阻塞态进入就绪态

在ulTaskNotifyTake()返回前,还可以做些清理工作,如把通知值减一,或者把通知值清零。


//xTaskToNotify:任务句柄,创建任务时得到,
//返回值:必定返回pdPASS 
#define xTaskNotifyGive( xTaskToNotify ) \
    xTaskGenericNotify( ( xTaskToNotify ), ( tskDEFAULT_INDEX_TO_NOTIFY ), ( 0 ), eIncrement, NULL )

#define vTaskNotifyGiveFromISR( xTaskToNotify, pxHigherPriorityTaskWoken ) \
    vTaskGenericNotifyGiveFromISR( ( xTaskToNotify ), ( tskDEFAULT_INDEX_TO_NOTIFY ), ( pxHigherPriorityTaskWoken ) );

//xClearCountOnExit:退出前是否清零,pdTRUE:把通知值清零,pdFALSE:不把通知值清零
//xTicksToWait:等待时间
//返回值:函数返回之前,在清零或减一之前的通知值
//如果xTicksToWait非零,则返回值有两种情况 大于0,:在超时前,通知值被增加了,等于0,一直没有其他任 
 //务增加通知值,最后超时返回。
#define ulTaskNotifyTake( xClearCountOnExit, xTicksToWait ) \
    ulTaskGenericNotifyTake( ( tskDEFAULT_INDEX_TO_NOTIFY ), ( xClearCountOnExit ), ( xTicksToWait ) )

专业版:

使用xTaskNotify(),可以设置参数,实现不同的功能。

1)让接收任务的通知值加1,:这时xTaskNotify()等同于xTaskNotifyGive()。

2)设置接收任务通知的某一位,某些位,这就是一个轻量级的更高效的事件组。

3)把一个新值写入接收任务的通知值:上一次被读走后写入才成功。这就是轻量级的,长度为1的队列。

4)用一个新值覆盖接收任务的通知值:无论上一次的通知值是否被读走,覆盖都成功。

xTaskNotifyWait(),

1)可以让任务等待(可以加上超时时间),等到任务状态为pending

2)可以在函数进入,退出时,清除通知值的指定位。

 函数原型如下:

BaseType_t xTaskNotify( TaskHandle_t xTaskToNotify, uint32_t ulValue,
eNotifyAction eAction );
BaseType_t xTaskNotifyFromISR( TaskHandle_t xTaskToNotify,
uint32_t ulValue,eNotifyAction eAction,
BaseType_t *pxHigherPriorityTaskWoken );
BaseType_t xTaskNotifyWait( uint32_t ulBitsToClearOnEntry,uint32_t ulBitsToClearOnExit,
uint32_t *pulNotificationValue,TickType_t xTicksToWait );

参数说明如下:

xTaskNotify()

xTaskToNotify任务句柄
ulValue如何使用,有eAction决定
eAction 见下表
返回值

paPASS:大部分都会成功

pdFAIL:只有一种情况会失败,当eAction为eSetValueWithoutOverwrite,并且通知状态为pending时,失败

eAction参数说明

eNoAction仅仅是更新通知状态为“pending”,未使用value,这个相当于轻量级、更高效的二进制信号量
eSetBits

通知值 = 原来的通知值 | ulValue,按位或

相当于轻量级的、更高效的事件组

eIncrement

通知值 = 原来的通知值 + 1,未使用ulValue

相当于轻量级的、更高效的二进制信号量、技术型信号量

eSetValueWithoutOverwrite不覆盖。
如果通知状态为"pending"(表示有数据未读),
则此次调用xTaskNotify不做任何事,返回pdFAIL。
如果通知状态不是"pending"(表示没有新数据),
则:通知值 = ulValue。
eSetValueWithOverwrite覆盖,无论如何,不管通知状态是否为“pending”,通知值为value

xTaskNotifyWait()

ulBitsToClearOnEntry

在xTaskNotifyWait()入口处,要清除通知值的哪些位?通知状态不是“pending”的情况下,才会清除。

它的本意是:我想等待某些事件的发生,所以先把“旧数据”的某些位清零

能清零的话,通知值 = 通知值 & ~(ulBitsToClearOnEntry)

比如传入0x01,表示清除通知值的bit0,传入0xffffffff,表示清除所有值

ulBitsToClearOnExit

在xTaskNotifyWait()出口处,如果不是因为超时退出,而是因为得到了数据退出:

通知值 = 通知值 & ~(ulBitsToClearOnExit)

在清除某些位之前,通知值先被赋给*pulNotificationValue

比如传入0x03,表示清除通知值的bit0,bit1

传入0xffffffff,表示清除所有位,即把值设置为0

pulNotificationValue

用来取出通知值。在函数退出时,使用ulBitsToClearOnExit清除之前,把通知值赋给*pulNotificationValue

如果不需要取出通知值,可设为NULL

xTicksToWait等待时间
返回值

pdPASS:表示xTaskNotifyWait()成功获得了通知,可能是在调用函数之前,通知状态变为了“pending”

也可能是在阻塞期间,通知状态变为了“pending”

pdFAIL:没有得到通知

任务通知和队列的区别如下:

任务通知队列
只有一个数据,数据是32位的可以容纳多个数据,数据大小可以指定
写队列时,不可以阻塞写队列时,可以阻塞
可以覆盖,也可以不覆盖如果队列长度是1,可以选择覆盖队列

任务通知不能真正意义上实现事件组,因为

它不能等待指定的事件

它不能等待若干个事件中的任意一个

一旦有事件,总会唤醒任务

总结

内核对象生产者消费者数据/状态说明
队列ALLALL

数据:若干个数据

谁都可以向队列里扔数据

谁都可以从队列中读取数据

用来传递数据

发送者、接受者无限制,一个数据只能唤醒一个接收者

信号量ALLALL

数量:0-n

谁都可以增加一个数量

谁都可以消耗一个数量

用来维持资源的个数,生产者、消费者无限制。1个资源只能唤醒1个接收者
互斥量只能A开锁A上锁

位:0、1

我上锁:1变为0

只能由我开锁:0变为1

就向一个空厕所,谁使用谁上锁,

也只能由他开锁

事件组ALLALL

多个位:或、与

谁都可以设置(生产)多个位,谁都可以等待某个位、若干个位

用来传递事件,

发送者接收者无限制

可以唤醒多个接收者

任务通知ALL只有我

数据、状态都可以传输

使用任务通知时,必须指定接收者

N对1的关系,发送者无限制

接受者只能是这个任务

以上参考了韦东山老师的FreeRTOS完全开发手册。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值