FreeRTOS_第20章_事件

简介:

学习[野火®]《FreeRTOS 内核实现与应用开发实战—基于STM32》 这本书第20章_事件的补充。

FreeRTOS 提供的事件具有如下特点

  1. 事件只与任务相关联,事件相互独立,一个 32 位的事件集合(EventBits_t 类型的变量,实际可用与表示事件的只有 24 位),用于标识该任务发生的事件类型,其中每一位表示一种事件类型(0表示该事件类型未发生、1 表示该事件类型已经发生),一共 24 种事件类型。

  2. 事件仅用于同步,不提供数据传输功能。

  3. 事件无排队性,即多次向任务设置同一事件(如果任务还未来得及读走),等效于

  4. 只设置一次。

  5. 允许多个任务对同一事件进行读写操作。

  6. 支持事件等待超时机制。

事件变量和结构体

事件结构体和一些事件现场的宏

//TCB事件列表的辅助值可以用作其他用途。
//但是下面宏位置1时,说明该辅助值用作事件
#if( configUSE_16_BIT_TICKS == 1 )
	#define taskEVENT_LIST_ITEM_VALUE_IN_USE	0x8000U
#else
	#define taskEVENT_LIST_ITEM_VALUE_IN_USE	0x80000000UL
#endif

/* The following bit fields convey control information in a task's event list
item value.  It is important they don't clash with the
taskEVENT_LIST_ITEM_VALUE_IN_USE definition. */
//事件函数配置的现场保存,这些宏是对应参数
//不可以和taskEVENT_LIST_ITEM_VALUE_IN_USE相同,taskEVENT_LIST_ITEM_VALUE_IN_USE是最高位,
//用作表明TCB事件列表的辅助值用作事件
#if configUSE_16_BIT_TICKS == 1
	//匹配成功后退出清除事件标志位
	#define eventCLEAR_EVENTS_ON_EXIT_BIT	0x0100U
	//被事件唤醒,如果这位不为1就是超时
	#define eventUNBLOCKED_DUE_TO_BIT_SET	0x0200U
	//匹配所有事件才算成功
	#define eventWAIT_FOR_ALL_BITS			0x0400U
	//用于分离TCB辅助值中的控制部分和事件部分
	#define eventEVENT_BITS_CONTROL_BYTES	0xff00U
#else
	#define eventCLEAR_EVENTS_ON_EXIT_BIT	0x01000000UL
	#define eventUNBLOCKED_DUE_TO_BIT_SET	0x02000000UL
	#define eventWAIT_FOR_ALL_BITS			0x04000000UL
	#define eventEVENT_BITS_CONTROL_BYTES	0xff000000UL
#endif

//S型信号量格式 		{型号量,任务等待队列}
//定义事件格式		{事件量(+一些现场保存数据),任务等待队列}
typedef struct xEventGroupDefinition
{
	//事件其中高8位用作其他标志位,如果是16位,实际事件是8位
	EventBits_t uxEventBits; 			
	List_t xTasksWaitingForBits;		/*< List of tasks waiting for a bit to be set. */

	#if( configUSE_TRACE_FACILITY == 1 )
		UBaseType_t uxEventGroupNumber;
	#endif

	#if( ( configSUPPORT_STATIC_ALLOCATION == 1 ) && ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) )
		uint8_t ucStaticallyAllocated; /*< Set to pdTRUE if the event group is statically allocated to ensure no attempt is made to free the memory. */
	#endif
} EventGroup_t;

函数详解

​ 事件函数主要存放在 event_groups.c 这个C文件中。

事件组创建 xEventGroupCreate( )

​ 函数流程如下:

​ 1)申请事件组空间。

​ 2)初始化事件组变量。

/**************************************************************************************************
* 函数:                                   xEventGroupCreate
* 说明:   动态申请的方式创建一个事件组
* 输入:  
*		   
* 关联变量: 
*			宏 configSUPPORT_DYNAMIC_ALLOCATION  内存动态申请
* 返回: 		  
*		事件组变量指针
* 注释作者:  日南方  12/3/2020
**************************************************************************************************/
#if( configSUPPORT_DYNAMIC_ALLOCATION == 1 )
	EventGroupHandle_t xEventGroupCreate( void )
	{
		//申请事件组指针
		EventGroup_t *pxEventBits;
		//申请事件组空间,并链接指针
		/* Allocate the event group. */
		pxEventBits = ( EventGroup_t * ) pvPortMalloc( sizeof( EventGroup_t ) );

		//事件空间申请成功
		if( pxEventBits != NULL )
		{
			//初始化
			//没有任何事件
			pxEventBits->uxEventBits = 0;
			任务事件等待的连接初始化
			vListInitialise( &( pxEventBits->xTasksWaitingForBits ) );

			#if( configSUPPORT_STATIC_ALLOCATION == 1 )
			{
				/* Both static and dynamic allocation can be used, so note this
				event group was allocated statically in case the event group is
				later deleted. */
				pxEventBits->ucStaticallyAllocated = pdFALSE;
			}
			#endif /* configSUPPORT_STATIC_ALLOCATION */

			traceEVENT_GROUP_CREATE( pxEventBits );
		}
		else
		{
			traceEVENT_GROUP_CREATE_FAILED();
		}
		return ( EventGroupHandle_t ) pxEventBits;
	}
#endif /* configSUPPORT_DYNAMIC_ALLOCATION */

事件删除函数 vEventGroupDelete()

​ vEventGroupDelete()用于删除由函数 xEventGroupCreate()创建的事件组,只有被创建成功的事件才能被删除,但是需要注意的是该函数不允许在中断里面使用。当事件组被删除之后,阻塞在该事件组上的任务都会被解锁,并向等待事件的任务返回事件组的值为 0。

/**************************************************************************************************
* 函数:                                   vEventGroupDelete
* 说明:   事件组删除
* 输入:  
*		   EventGroupHandle_t xEventGroup 事件组句柄		
* 关联变量: 
* 返回:  
* 注释作者:  日南方  12/3/2020
**************************************************************************************************/
void vEventGroupDelete( EventGroupHandle_t xEventGroup )
{
EventGroup_t *pxEventBits = ( EventGroup_t * ) xEventGroup;
const List_t *pxTasksWaitingForBits = &( pxEventBits->xTasksWaitingForBits );

	//挂起调度器
	vTaskSuspendAll();
	{
		traceEVENT_GROUP_DELETE( xEventGroup );

		//如果有任务阻塞在这个事件组上
		while( listCURRENT_LIST_LENGTH( pxTasksWaitingForBits ) > ( UBaseType_t ) 0 )
		{
			/* Unblock the task, returning 0 as the event list is being deleted
			and	cannot therefore have any bits set. */
			//把事件从等待事件列表中移除
			//调用 xTaskRemoveFromUnorderedEventList()函数将任务从等待事件列表中移除,然后添加到就绪列表中,参与任务调度,
			//当然,因为挂起了调度器,所以在这段时间里,即使是优先级更高的任务被添加到就绪列表,系统也不会进行任务调度,
			//所以也就不会影响当前任务删除事件的操作,这也是为什么需要挂起调度器的原因。
			//但是,使用事件删除函数 vEventGroupDelete()的时候需要注意,尽量在没有任务阻塞在这个事件的时候进行删除,
			//否则任务无法等到正确的事件,因为删除之后,所有被恢复的任务都只能获得事件的值为 
			configASSERT( pxTasksWaitingForBits->xListEnd.pxNext != ( ListItem_t * ) &( pxTasksWaitingForBits->xListEnd ) );
			( void ) xTaskRemoveFromUnorderedEventList( pxTasksWaitingForBits->xListEnd.pxNext, eventUNBLOCKED_DUE_TO_BIT_SET );
		}

		#if( ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) && ( configSUPPORT_STATIC_ALLOCATION == 0 ) )
		{
			/* The event group can only have been allocated dynamically - free
			it again. */
			//释放事件的内存
			vPortFree( pxEventBits );
		}
		#elif( ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) && ( configSUPPORT_STATIC_ALLOCATION == 1 ) )
		{
			/* The event group could have been allocated statically or
			dynamically, so check before attempting to free the memory. */
			if( pxEventBits->ucStaticallyAllocated == ( uint8_t ) pdFALSE )
			{
				vPortFree( pxEventBits );
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}
		}
		#endif /* configSUPPORT_DYNAMIC_ALLOCATION */
	}
	//恢复调度器
	( void ) xTaskResumeAll();
}
/*-----------------------------------------------------------*/

事件组置位函数,寻找相应等待事件的任务并唤醒 xEventGroupSetBits()

/**************************************************************************************************
* 函数:                                   xEventGroupSetBits
* 说明:   事件组置位函数,兼唤醒功能
* 输入:  
*		   EventGroupHandle_t 	xEventGroup		事件组
*		const EventBits_t 		uxBitsToSet		事件宏		
* 关联变量: 
* 返回:  
* 注释作者:  日南方  12/3/2020
**************************************************************************************************/
EventBits_t xEventGroupSetBits( EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToSet )
{
	//事件当前节点,事件下一个节点
	ListItem_t *pxListItem, *pxNext;
	ListItem_t const *pxListEnd;
	//事件组等待列表
	List_t *pxList;
	//需要清除的事件标志位,等待事件标志位,事件控制标志位
	EventBits_t uxBitsToClear = 0, uxBitsWaitedFor, uxControlBits;
	//事件组 = 事件标志 + 事件组等待列表
	EventGroup_t *pxEventBits = ( EventGroup_t * ) xEventGroup;
	BaseType_t xMatchFound = pdFALSE;

	/* Check the user is not attempting to set the bits used by the kernel
	itself. */
	configASSERT( xEventGroup );
	configASSERT( ( uxBitsToSet & eventEVENT_BITS_CONTROL_BYTES ) == 0 );

	//事件组任务等待列表指针(事件组任务等待列表 = 基础信息(节点的个数) + 任务最后节)
	pxList = &( pxEventBits->xTasksWaitingForBits );
	//事件组任务等待列表中最后一个节点的指针
	pxListEnd = listGET_END_MARKER( pxList ); /*lint !e826 !e740 The mini list structure is used as the list end to save RAM.  This is checked and valid. */

	//挂起任务调度器
	vTaskSuspendAll();
	{
		traceEVENT_GROUP_SET_BITS( xEventGroup, uxBitsToSet );

		//事件组任务等待列表中第一个任务
		pxListItem = listGET_HEAD_ENTRY( pxList );

		/* Set the bits. */
		//事件标志位赋值
		pxEventBits->uxEventBits |= uxBitsToSet;

		/* See if the new bit value should unblock any tasks. */
		//遍历的终止条件
		while( pxListItem != pxListEnd )
		{
			//遍历下一个节点,这边要提前准备下一个节点,
			//万一当前节点就是和事件匹配的任务,把任务节点剥离加入到就绪列表,那么原本的顺序就发生了变化
			pxNext = listGET_NEXT( pxListItem );
			
			//遍历当前节点任务节点的辅助值(一般用于存放滴答计数用来定时),这里放进来就变成了等待事件的宏+控制宏
			uxBitsWaitedFor = listGET_LIST_ITEM_VALUE( pxListItem );
			//事件匹配标志初始化
			xMatchFound = pdFALSE;

			/* Split the bits waited for from the control bits. */
			//控制和事件分离
			uxControlBits = uxBitsWaitedFor & eventEVENT_BITS_CONTROL_BYTES;
			uxBitsWaitedFor &= ~eventEVENT_BITS_CONTROL_BYTES;

			//只要匹配一个事件
			if( ( uxControlBits & eventWAIT_FOR_ALL_BITS ) == ( EventBits_t ) 0 )
			{
				/* Just looking for single bit being set. */
				//任务等待事件 在 事件组中的事件 中有
				if( ( uxBitsWaitedFor & pxEventBits->uxEventBits ) != ( EventBits_t ) 0 )
				{
					//匹配成功
					xMatchFound = pdTRUE;
				}
				else
				{
					mtCOVERAGE_TEST_MARKER();
				}
			}
			//匹配所有事件
			else if( ( uxBitsWaitedFor & pxEventBits->uxEventBits ) == uxBitsWaitedFor )
			{
				/* All bits are set. */
				xMatchFound = pdTRUE;
			}
			else
			{
				/* Need all bits to be set, but not all the bits were set. */
			}

			//匹配标志位 是 匹配成功
			if( xMatchFound != pdFALSE )
			{
				/* The bits match.  Should the bits be cleared on exit? */
				//匹配成功,然后看下是否需要清除标志位
				//如果需要,就记录下需要清除的标志位,等遍历完队列之后统一处理 *
				if( ( uxControlBits & eventCLEAR_EVENTS_ON_EXIT_BIT ) != ( EventBits_t ) 0 )
				{
					//运用或运算,标记一下要清除的事件标志位是哪些。
					uxBitsToClear |= uxBitsWaitedFor;
				}
				else
				{
					mtCOVERAGE_TEST_MARKER();
				}


				/* Store the actual event flag value in the task's event list
				item before removing the task from the event list.  The
				eventUNBLOCKED_DUE_TO_BIT_SET bit is set so the task knows
				that is was unblocked due to its required bits matching, rather
				than because it timed out. */
				//将满足事件条件的任务从等待列表中移除,并且添加到就绪列表中
				( void ) xTaskRemoveFromUnorderedEventList( pxListItem, pxEventBits->uxEventBits | eventUNBLOCKED_DUE_TO_BIT_SET );
			}

			/* Move onto the next list item.  Note pxListItem->pxNext is not
			used here as the list item may have been removed from the event list
			and inserted into the ready/pending reading list. */
			//遍历节点,将提早储存好的下一个节点更新
			pxListItem = pxNext;
		}

		//正式清除事件
		/* Clear any bits that matched when the eventCLEAR_EVENTS_ON_EXIT_BIT
		bit was set in the control word. */
		pxEventBits->uxEventBits &= ~uxBitsToClear;
	}
	//恢复调度器
	( void ) xTaskResumeAll();

	//返回剩余事件详情
	return pxEventBits->uxEventBits;
}
/*-----------------------------------------------------------*/

将任务移除事件等待列表添加到就绪列表xTaskRemoveFromUnorderedEventList()


/**************************************************************************************************
* 函数:                                   xTaskRemoveFromUnorderedEventList
* 说明:   将任务移除事件等待列表添加到就绪列表
* 输入:  
*		   ListItem_t * 		pxEventListItem			任务TCB
*			const TickType_t 	xItemValue		
* 关联变量: 
*								pxCurrentTCB			当前运行任务TCB
*								xYieldPending			下次是否任务调度
* 返回:  
*								xReturn					是否需要任务调度
* 注释作者:  日南方  12/3/2020
**************************************************************************************************/
BaseType_t xTaskRemoveFromUnorderedEventList( ListItem_t * pxEventListItem, const TickType_t xItemValue )
{
	TCB_t *pxUnblockedTCB;
	BaseType_t xReturn;

	//这个函数必须在任务调度器被挂起的情况下使用
	/* THIS FUNCTION MUST BE CALLED WITH THE SCHEDULER SUSPENDED.  It is used by
	the event flags implementation. */
	configASSERT( uxSchedulerSuspended != pdFALSE );

	//储存任务的TCB辅助值,这个辅助值是TCB事件节点上面的辅助值
	/* Store the new item value in the event list. */
	//最高位是1,表示这个辅助值是表示事件相关
	listSET_LIST_ITEM_VALUE( pxEventListItem, xItemValue | taskEVENT_LIST_ITEM_VALUE_IN_USE );

	/* Remove the event list form the event flag.  Interrupts do not access
	event flags. */
	//根据节点指到对应任务的TCB
	pxUnblockedTCB = ( TCB_t * ) listGET_LIST_ITEM_OWNER( pxEventListItem );
	configASSERT( pxUnblockedTCB );
	//将任务TCB节点剥离
	( void ) uxListRemove( pxEventListItem );

	/* Remove the task from the delayed list and add it to the ready list.  The
	scheduler is suspended so interrupts will not be accessing the ready
	lists. */
	//移除TCB在(就绪/阻塞/悬挂)大表中的任务节点,接到就绪队列中
	( void ) uxListRemove( &( pxUnblockedTCB->xStateListItem ) );
	prvAddTaskToReadyList( pxUnblockedTCB );

	if( pxUnblockedTCB->uxPriority > pxCurrentTCB->uxPriority )
	{
		/* Return true if the task removed from the event list has
		a higher priority than the calling task.  This allows
		the calling task to know if it should force a context
		switch now. */
		//任务调度的标志位
		xReturn = pdTRUE;

		/* Mark that a yield is pending in case the user is not using the
		"xHigherPriorityTaskWoken" parameter to an ISR safe FreeRTOS function. */
		//下一次进行任务调度
		xYieldPending = pdTRUE;
	}
	else
	{
		xReturn = pdFALSE;
	}

	return xReturn;
}
/*-----------------------------------------------------------*/

等待事件函数xEventGroupWaitBits()

​ 事件等待有个重要的地方,就是设置了超时时间的话,恢复调度的时候,任务已经被阻塞了。再次唤醒两种情况,一种是被事件唤醒,另一种是阻塞时间到被唤醒。

代码详解

/**************************************************************************************************
* 函数:                                   xEventGroupWaitBits
* 说明:  
* 输入:  
*		EventGroupHandle_t 	xEventGroup				事件的主体变量
*		const EventBits_t 	uxBitsToWaitFor			我们等待什么事件的变量
*		const BaseType_t 	xClearOnExit			是否读取后清除发生事件
*		const BaseType_t 	xWaitForAllBits			等待事件发生是全部都发生(逻辑与)还是只发生一件(逻辑或)
*		TickType_t 			xTicksToWait			等待事件的超时时间
* 关联变量: 
* 返回:  
* 注释作者:  日南方  11/26/2020
**************************************************************************************************/
EventBits_t xEventGroupWaitBits( EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToWaitFor, 
										const BaseType_t xClearOnExit, const BaseType_t xWaitForAllBits, 
										TickType_t xTicksToWait )
{
	EventGroup_t *pxEventBits = ( EventGroup_t * ) xEventGroup;
	//申请当前事件标志位返回变量,申请一个用作超时等待保存现场的变量
	EventBits_t uxReturn, uxControlBits = 0;
	//申请一个等待条件满足的变量,
	BaseType_t xWaitConditionMet, xAlreadyYielded;
	BaseType_t xTimeoutOccurred = pdFALSE;

	/* Check the user is not attempting to wait on the bits used by the kernel
	itself, and that at least one bit is being requested. */
	configASSERT( xEventGroup );
	configASSERT( ( uxBitsToWaitFor & eventEVENT_BITS_CONTROL_BYTES ) == 0 );
	configASSERT( uxBitsToWaitFor != 0 );
	#if ( ( INCLUDE_xTaskGetSchedulerState == 1 ) || ( configUSE_TIMERS == 1 ) )
	{
		configASSERT( !( ( xTaskGetSchedulerState() == taskSCHEDULER_SUSPENDED ) && ( xTicksToWait != 0 ) ) );
	}
	#endif

	//悬挂调度器
	vTaskSuspendAll();
	{
		const EventBits_t uxCurrentEventBits = pxEventBits->uxEventBits;

		/* Check to see if the wait condition is already met or not. */
		//查看当前事件是否已经满足事件条件,输入参数有(当前拥有的事件,任务等待的事件,是否所有任务满足)
		xWaitConditionMet = prvTestWaitCondition( uxCurrentEventBits, uxBitsToWaitFor, xWaitForAllBits );

		//等待条件满足
		if( xWaitConditionMet != pdFALSE )
		{
			/* The wait condition has already been met so there is no need to
			block. */
			uxReturn = uxCurrentEventBits;
			//超时计数清零
			xTicksToWait = ( TickType_t ) 0;

			/* Clear the wait bits if requested to do so. */
			//退出清除
			if( xClearOnExit != pdFALSE )
			{
				//清除事件发生标志
				pxEventBits->uxEventBits &= ~uxBitsToWaitFor;
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}
		}
		//等待条件不满足,但是超时为0
		else if( xTicksToWait == ( TickType_t ) 0 )
		{
			/* The wait condition has not been met, but no block time was
			specified, so just return the current value. */
			//返回变量设置为当前事件
			uxReturn = uxCurrentEventBits;
		}
		//等待条件不满足,还设置超时时间
		else
		{
			/* The task is going to block to wait for its required bits to be
			set.  uxControlBits are used to remember the specified behaviour of
			this call to xEventGroupWaitBits() - for use when the event bits
			unblock the task. */
			//现场的参数保存
			//读取清除?
			if( xClearOnExit != pdFALSE )
			{
				uxControlBits |= eventCLEAR_EVENTS_ON_EXIT_BIT;
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}
			//等待所有事件满足?
			if( xWaitForAllBits != pdFALSE )
			{
				uxControlBits |= eventWAIT_FOR_ALL_BITS;
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}

			/* Store the bits that the calling task is waiting for in the
			task's event list item so the kernel knows when a match is
			found.  Then enter the blocked state. */
			//将当前任务从就绪列表中摘下来,加入到延时阻塞列表中
			//并且这个列表项的辅助值是用于保存任务等待事件需求的信息标记,
			//以便在事件标志位置位的时候对等待事件的任务进行相应的操作。
			vTaskPlaceOnUnorderedEventList( &( pxEventBits->xTasksWaitingForBits ), ( uxBitsToWaitFor | uxControlBits ), xTicksToWait );

			/* This is obsolete as it will get set after the task unblocks, but
			some compilers mistakenly generate a warning about the variable
			being returned without being set if it is not done. */
			uxReturn = 0;

			//调试函数
			traceEVENT_GROUP_WAIT_BITS_BLOCK( xEventGroup, uxBitsToWaitFor );
		}
	}
	//恢复调度器
	xAlreadyYielded = xTaskResumeAll();

	//有设置超时参数
	if( xTicksToWait != ( TickType_t ) 0 )
	{
		//如果是抢占式内核,就进行任务调度
		if( xAlreadyYielded == pdFALSE )
		{
			//任务调度
			portYIELD_WITHIN_API();
		}
		else
		{
			mtCOVERAGE_TEST_MARKER();
		}

		/* The task blocked to wait for its required bits to be set - at this
		point either the required bits were set or the block time expired.  If
		the required bits were set they will have been stored in the task's
		event list item, and they should now be retrieved then cleared. */
		/* 进入到这里说明当前的任务已经被重新调度了 */
		//前面设置超时参数的时候,已经加到阻塞列表中。
		//运行到这里,是被事件唤醒了,或者是阻塞时间到了
		uxReturn = uxTaskResetEventItemValue();

		//如果不是被事件唤醒的
		if( ( uxReturn & eventUNBLOCKED_DUE_TO_BIT_SET ) == ( EventBits_t ) 0 )
		{
			taskENTER_CRITICAL();
			{
				/* The task timed out, just return the current event bit value. */
				/* 超时返回时,直接返回当前事件的所有标志位 */
				uxReturn = pxEventBits->uxEventBits;

				/* It is possible that the event bits were updated between this
				task leaving the Blocked state and running again. */
				/* 再判断一次是否发生了事件 */
				if( prvTestWaitCondition( uxReturn, uxBitsToWaitFor, xWaitForAllBits ) != pdFALSE )
				{
					/* 如果发生了,那就清除事件标志位并且返回 */
					if( xClearOnExit != pdFALSE )
					{
						pxEventBits->uxEventBits &= ~uxBitsToWaitFor;
					}
					else
					{
						mtCOVERAGE_TEST_MARKER();
					}
				}
				else
				{
					mtCOVERAGE_TEST_MARKER();
				}
			}
			taskEXIT_CRITICAL();
			
			/* Prevent compiler warnings when trace macros are not used. */
			xTimeoutOccurred = pdFALSE;
		}
		else
		{
			/* The task unblocked because the bits were set. */
		}

		/* The task blocked so control bits may have been set. */
		//删除控制信息
		uxReturn &= ~eventEVENT_BITS_CONTROL_BYTES;
	}
	traceEVENT_GROUP_WAIT_BITS_END( xEventGroup, uxBitsToWaitFor, xTimeoutOccurred );

	return uxReturn;
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值