FreeRTOS学习笔记


										FreeRTOS学习历程

总结:
	关键词:动态创建 静态创建 任务级 中断级
	一个任务至少有一个	do
   					 {	
						vtaskDelay(10);
   					 }while(1);	//不然程序容易卡死在一个任务中出不来,让CPU有运行空闲任务的机会。
0.0FreeRTOSv9.0移植

0.1任务相关API函数(*打开相应的宏)
	*uxTaskPriorityGet("任务句柄");		//获取任务优先级
	*vTaskPrioritySet("任务句柄","任务优先级");	//改变某一个任务的优先级
	*uxTaskGetSystemState("任务状态","任务状态数组大小","系统中的运行时间");//获取任务中所有任务的状态
	*uxTaskGetNumberOfTasks();//获取任务个数
	*xTaskGetCurrentTaskHandle();//获取当前任务句柄
	*xTaskGetHandle("任务名字");//根据任务名字获取任务句柄
	*uxTaskGetStackHighWaterMark("任务句柄");//任务堆栈历史剩余最小值
	eTaskGetState("任务句柄");//任务的运行状态(0运行 1就绪 ....)
	xTaskGetTickCount();//获取滴答定时器时钟节拍值
	相对重要API函数:
	vTaskList("定义缓冲区"); //获取所有运行任务的名字 状态(B阻塞 R就绪 S挂起 D删除) 任务优先级 任务堆栈剩余大小 任务运行编号
	
	vTaskGetRunTimeStats:
		打开相应的宏:
		configGENERATE_RUN_TIME_STATS			1
		configUSE_STATS_FORMATTING_FUNCTIONS	1
		之后需要自实现两个宏定义:
		#define portCONFIGURE_TIMER_FOR_RUN_TIME_STATS()  	ConfigureTimeForRunTimeStats()//定时器3提供时间统计的时基
		
		#define portGET_RUN_TIME_COUNTER_VALUE()			FreeRTOSRunTimeTicks	//获取时间统计时间值 
		vTaskGetRunTimeStats("定义缓冲区");//获取所有运行任务的名字 运行时间 占运行总时间的百分比
1.任务的动态创建:
	遇到错误:(	char i=0;//必须要赋值初始值不然为随机数 )
	动态创建任务API:
	//任务创建配置参数
	#define START_TASK_PRIO 	1 //优先级
	#define START_STK_SIZE		120  //任务堆栈大小
	void start_tast( void * pvParameters );	//任务函数
	TaskHandle_t StartTask_Handler;//任务句柄

	xTaskCreate((TaskFunction_t ) start_tast,  		//任务
				 (char *					) "start_tast", 	//名字
				 (uint16_t				) START_STK_SIZE, //任务堆栈
				 (void *					) NULL,   				//参数
				 (UBaseType_t			) START_TASK_PRIO,//任务优先级
				 (TaskHandle_t *	) &StartTask_Handler);//	任务句柄
	
	vTaskStartScheduler();          //开启任务调度
	vTaskDelete(StartTask_Handler);			//删除自身也可以食用NULL或者是任务句柄
2.任务的静态创建:
	遇到错误:()
	重要宏和函数:
	configSUPPORT_STATIC_ALLOCATION	1//静态内存
	//为空闲任务和定时器任务的任务堆栈和任务控制块分配内存
	//空闲任务
	static StaticTask_t IdleTaskTCB;//任务控制块
	static StackType_t IdleTaskStack[configMINIMAL_STACK_SIZE];//任务堆栈  堆栈大小
	//定时任务
	static StaticTask_t TimerTaskTCB;//任务控制块
	static StackType_t TimerTaskStack[configTIMER_TASK_STACK_DEPTH];//任务堆栈  
	
	//空闲任务所需内存
	void vApplicationGetIdleTaskMemory(StaticTask_t **ppxIdleTaskTCBBuffer, 
									   StackType_t **ppxIdleTaskStackBuffer, 
									   uint32_t *pulIdleTaskStackSize)
	//定时器任务所需内存
	void vApplicationGetTimerTaskMemory(StaticTask_t **ppxTimerTaskTCBBuffer, 
										StackType_t **ppxTimerTaskStackBuffer, 
										uint32_t *pulTimerTaskStackSize)
	
	//静态创建API	
	//任务创建配置参数
	#define START_STK_SIZE 			128
	#define START_TASK_PRIO 		1
	StackType_t StartTaskStack[START_STK_SIZE];
	StaticTask_t StartTaskTCB;
	TaskHandle_t StartTask_Handler;
	void start_task( void * pvParameters );
	
		StartTask_Handler=xTaskCreateStatic((	TaskFunction_t) start_task,
	(char *					) "start_task",	
	(uint32_t				) START_STK_SIZE,
	(void *					) NULL,
	(UBaseType_t		) START_TASK_PRIO,
	(StackType_t *	) StartTaskStack,
	(StaticTask_t *	) &StartTaskTCB );
	
	vTaskStartScheduler();          				//开启任务调度
	vTaskDelete(StartTask_Handler);			//删除自身也可以食用NULL或者是任务句柄
3.任务的挂起和恢复:
	遇到错误:()
	临界区:(临界区代码一定要精简,临界区会关闭中断,导致优先级低于configMAX_SYSCALL_INTERRUPT_PRIORITY的中断得不到及时响应)
	任务级临界代码保护:
	taskENTER_CRITICAL();	//进入临界区
	taskEXIT_CRITICAL();            //退出临界区
	任务:
	vTaskSuspend("任务句柄");			//挂起任务
	vTaskResume("任务句柄");			//恢复任务
4.开关中断:
	遇到错误:(任务中如果使用到延时函数要使用delay_xms();因为其他延时中带有中断,关闭中断之后可能一调用延时立马就把中断给打开。所以看不到效果 )
	需要设置FreeRTOS的PendSV  SysTick中断优先级
	临界区:(临界区代码一定要精简,临界区会关闭中断,导致优先级低于configMAX_SYSCALL_INTERRUPT_PRIORITY的中断得不到及时响应)
	中断级临界代码保护:
	x = taskENTER_CRITICAL_FROM_ISR()//进入
	taskEXIT_CRITICAL_FROM_ISR( x )//退出
	中断:
	portDISABLE_INTERRUPTS();			//关闭中断
	portENABLE_INTERRUPTS();			//打开中断
5.列表和列表项:
	遇到错误:(在末尾添加列表项,遍历没搞完全清楚)
	重要宏和函数:
	configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES设置为1开启列表完整性检查
	//创建结构体列表和列表项
	List_t ListText; 
	ListItem_t ListItem1;	
	//重要函数
	vListInitialise(&ListText);//创建列表ListText
	vListInitialiseItem(&ListItem1);//创建列表项ListItem1
	vListInsert(&ListText,&ListItem1);//插入列表项ListItem1
	uxListRemove(&ListItem1);//删除列表ListItem1
6.时间管理:
	相对延时:
			vTaskDelay("ms");	//延时ms级
	
	绝对延时(相对于相对延时来说):
	定义变量:
			const TickType_t TimeIncrement = pdMS_TO_TICKS(1000);	//把时间转化为时钟节拍
	
	
	
	
	
			PreviousWakeTime = xTaskGetTickCount();	//获取当前的时钟节拍
	实现:
			vTaskDelayUntil(&PreviousWakeTime,TimeIncrement);
7.消息队列:(全局变量尽量不要随意使用)
	全局变量的弊端:(可以使用记录当前状态,最忌讳用来传递参数)
	0.增加程序之间的耦合性,应用层也要关注驱动层的事情,导致分层不合理与模块化编程相违背
	2.阅读比较吃力,可维护性极差,需要找到全局的出处和所有使用的过程,除了原创作者其他人很难维护
	3.全局变量任何地方都被修改,使系统变得不可预测,软件安全性得不到保证,导致产品最终走向死亡
	4.后期维护非常困难,对于全局满天飞的系统
	5.程序初始化就诞生,程序结束才消亡,始终占用数据段上的空间一定程度上造成内存浪费
	6.不让维护人员戳你的脊梁骨养成一个良好的编程习惯
	全局变量应对之道:
	0.全局变量超过3个使用结构体包起来
	1.static到需要使用的文件中
	2.(返回值)return出去这样属性就是只读,使用起来就不用担心使用时修改值
	3.(传参)非要修改值的话可以使用函数传参方式
	4.非要非要extern的话要严格控制在自己.h中,不要放到公共的.h中丢人现眼

	遇到错误:
	创建:
	//动态创建
	QueueHandle_t Key_Queue;
	
	#define Key_Q_Temp		1
	Key_Queue = xQueueCreate(Key_Q_Temp,sizeof(u8)); 	//队列的长度  队列中每个消息长度 return:队列句柄
	
	//静态创建
	QueueHandle_t Key_Queue;
	
	#define Key_Q_Temp	1
	u8 buff[Key_Queue+1];
	StaticQueue_t QueueBuff;
	Key_Queue = xQueueCreateStatic(Key_Q_Temp,sizeof(u8),buff,QueueBuff); //项目数 字节 缓存区 保存队列 return:队列句柄
	
	任务级:
	//发送	
	err = xQueueSend(Key_Queue,&key,10);//同	xQueueSendToBack 完全一样
	err = xQueueSendToBack(Key_Queue,&key,10);	//发送消息到队头	//队列 值 时钟节拍	return:判断值
	err = xQueueSendToFront(Key_Queue,&key,10);	//发送消息到队尾
	err = xQueueOverwrite(Key_Queue,&key);		//带覆盖功能,队列满之后自动覆盖旧消息
	
	//接收
	err = xQueueReceive(Key_Queue,&key,portMAX_DELAY);	//读取之后删除队列项  //队列 值 接收等待时间 	return:判断值
	err = xQueuePeek(Key_Queue,&key,portMAX_DELAY);	//读取之后不删除 	//队列 值 接收等待时间 	return:判断值
	
	中断级:
	//发送:
	err = xQueueSendFromISR(Message_Queue,USART_RX_BUF,&HigherPriorityTaskWoken);/	/同下完全一样
	err = xQueueSendToBackFromISR(Message_Queue,USART_RX_BUF,&HigherPriorityTaskWoken);//发送消息到队尾
	err = xQueueSendToFrontFromISR(Message_Queue,USART_RX_BUF,&HigherPriorityTaskWoken);//发送消息到队头
	err = xQueueOverwriteFromISR(Message_Queue,USART_RX_BUF,&HigherPriorityTaskWoken);//队列满自动覆盖旧值
	//接收:
	err = xQueueReceiveFromISR(Message_Queue,Message_Buff,&TaskWoken);	//读取之后删除队列项
	err = xQueuePeekFromISR(Message_Queue,Message_Buff);			//读取之后不删除
	
	
	锁:
	上锁:
			prvLockQueue();
	
	解锁:
	prvUnlockQueue();

8.信号量:(#include "semphr.h")
	遇到错误:被按键队列的接收死等给卡死了
	优先级翻转:低优先级任务占据信号量时间较长,高优先级任务无法使用此信号量导致高优先级任务阻塞,
	使不使用此信号量的比中等优先级任务比高优先级任务早得到CPU的处理
	二值信号量创建:(没有优先级继承,适用于同步。优先级翻转问题)
	动态创建:
	SemaphoreHandle_t BinarySemaphore;
	BinarySemaphore = xSemaphoreCreateBinary();	//创建成功之后立马先释放(xSemaphoreGive)一次,不然不会运行,
	
	静态创建:
	StaticSemaphore_t SemaphoreBuff;
	BinarySemaphore = xSemaphoreCreateBinaryStatic(SemaphoreBuff);
	
	计数型信号量创建:
	动态创建:
	SemaphoreHandle_t CountingSemaphore;//计数信号量
	CountingSemaphore = xSemaphoreCreateCounting("MAX","InitValue");//最大值 初始值
	
	CountingValue = uxSemaphoreGetCount(CountingSemaphore); //获取信号量当前剩余值
	静态创建:
	SemaphoreHandle_t CountingSemaphore;
	StaticSemaphore_t SemaphoreBuffer;
	CountingSemaphore = xSemaphoreCreateCountingStatic( "MAX","InitValue",&SemaphoreBuffer );
	
	
	互斥信号量创建:(有优先级继承,适用于简单的互斥访问。只能用于任务,不能用于中断。优化优先级翻转问题)
	动态创建:
	SemaphoreHandle_t MutexSemaphore;//计数信号量
	MutexSemaphore = xSemaphoreCreateMutex();
	
	静态创建:
	SemaphoreHandle_t MutexSemaphore;//计数信号量
	StaticSemaphore_t MutexBuffer;
	MutexSemaphore = xSemaphoreCreateMutexStatic(&MutexBuffer);
	
	递归互斥信号量创建:(与之前三种信号量的释放和获取不同。只能用于任务,不能用于中断。)
	#define configUSE_RECURSIVE_MUTEXES		1;
	动态创建:
	SemaphoreHandle_t RecursiveMutexSemaphore;
	RecursiveMutexSemaphore = xSemaphoreCreateRecursiveMutex();
	
	静态创建:
	SemaphoreHandle_t RecursiveMutexSemaphore;
	StaticSemaphore_t RecursiveMutexBuffer;
	RecursiveMutexSemaphore = xSemaphoreCreateRecursiveMutexStatic(&RecursiveMutexBuffer);
	
	任务级私用信号量释放和获取:(only)
	BaseType_t err;
	信号量释放:
	err = xSemaphoreGiveRecursive(RecursiveMutexSemaphore);
	信号量获取:
	err = xSemaphoreTakeRecursive(RecursiveMutexSemaphore,"portTICK_PERIOD_MS");
	
	信号量释放和获取:
	任务级:
	信号量释放:
	xSemaphoreGive(BinarySemaphore);
	信号量获取:
	xSemaphoreTake(BinarySemaphore,portMAX_DELAY);	//获取信号量句柄	阻塞时间
	中断级:
		BaseType_t HigherPriorityTaskWoken;
	
		BaseType_t err;
	信号量释放:
	err = xSemaphoreGiveFromISR(BinarySemaphore,&HigherPriorityTaskWoken);//信号量句柄 标记是否进行任务切换	retuen:Y/N	
								(当HigherPriorityTaskWoken为psTRUE退出中断服务函数之前一定要进行一次任务切换)
	portYIELD_FROM_ISR(HigherPriorityTaskWoken);		//任务切换
	信号量获取:
	err = xSemaphoreTakeFromISR(BinarySemaphore,&HigherPriorityTaskWoken);
	
	在保存变量(HigherPriorityTaskWoken)为pdTRUE时退出中断服务函数之前一定要进行一次任务切换

9.软件定时器:(#include "timers.h")
	遇到错误:
	创建:
	TimerHandle_t TimerHandle;
	void CallbackFunction( TimerHandle_t xTimer );//回调函数
	StaticTimer_t TimerBuffer;
	
	动态创建:
	
	单次/周期定时器:
	TimerHandle = xTimerCreate((const char *			) "CallbackFunction",
								(TickType_t				) 1000,
								(UBaseType_t				) pdFALSE,//pdFALSE单次	pdTRUE周期
								(void *					) 1,
								(TimerCallbackFunction_t	) CallbackFunction );
	静态创建:
	单次/周期定时器:
		PeriodTimer = xTimerCreateStatic(const char *			) "CallbackFunction",
									(TickType_t			) 1000,
									(UBaseType_t			) pdFALSE,//pdFALSE单次	pdTRUE周期
									(void *				) 2,
									(TimerCallbackFunction_t) CallbackFunction,
									(StaticTimer_t	*		) TimerBuffer );	//保护成员变量
	复位软件定时器:.
	BaseType_t err;
	BaseType_t HigherPriorityTaskWoken;
	任务级:
	err = xTimerReset(TimerHandle,0);//句柄 阻塞时间
	中断级:
	err = xTimerResetFromISR(TimerHandle,&HigherPriorityTaskWoken);//句柄  时钟节拍
	开启软件定时器:
	BaseType_t err;
	BaseType_t HigherPriorityTaskWoken;
	任务级:
	err = xTimerStart(TimerHandle,0);//句柄 阻塞时间
	中断级:
	err = xTimerStartFromISR(TimerHandle,&HigherPriorityTask);//句柄  时钟节拍
	关闭软件定时器:
	任务级:
	err = xTimerStop(TimerHandle,0);//句柄 阻塞时间
	中断级:
	err = xTimerStopFromISR(TimerHandle,&HigherPriorityTask);//句柄  时钟节拍

10.事件标志组:(#include "event_groups.h")
	遇到错误:
	悉知:事件标志组的高八位不能用,1616-8)位只能表示8个事件,32位能表示2432-8)个事件
	创建:
	EventGroupHandle_t EventGroupHandle;//创建函数返回的句柄
	EventGroupHandle_t EventGroupBuffer; //静态创建保存数据的数组
	动态创建:
	EventGroupHandle = xEventGroupCreate();
	静态创建:
	EventGroupHandle = xEventGroupCreateStatic(&EventGroupBuffer); //保存数据所需数组
	设置事件位:
	#define BIT_0		(1 << 0)
	#define BIT_1		(1 << 1)
	
	
	
	#define BIT_ALL 	(BIT_0|BIT_1)0:
	任务级:
	EventBits_t EventGroupValue;
	EventGroupValue = xEventGroupClearBits(EventGroupHandle,BIT_0);
	中断级:
	BaseType_t EventGroupValue;
	EventGroupValue = xEventGroupClearBitsFromISR(EventGroupHandle,BIT_0);1:
	任务级:
	EventBits_t EventGroupValue;
	EventGroupValue = xEventGroupSetBits(EventGroupHandle,BIT_0);
	中断级:
	BaseType_t EventGroupValue;
	BaseType_t HigherPriorityTaskWoken;
	EventGroupValue = xEventGroupSetBitsFromISR(EventGroupHandle,BIT_0,&HigherPriorityTaskWoken);
	
	在保存变量(HigherPriorityTaskWoken)为pdTRUE时退出中断服务函数之前一定要进行一次任务切换
	
	获取事件标志组值:
	EventBits_t err;
	任务级:
	err = xEventGroupGetBits(EventGroupHandle);
	中断级:
	err = xEventGroupGetBitsFromISR(EventGroupHandle);
	等待指定的事件位:
	EventBits_t EventValue;
	EventValue = xEventGroupWaitBits((EventGroupHandle_t	) EventGroupHandle,
									(EventBits_t			) BIT_ALL,//操作的标志位
									(BaseType_t			) pdFALSE,//pdTRUE标志位被清除	pdFALSE不会被清除
									(BaseType_t			) pdFALSE, //pdTRUE 满足所有位 pdFALSE 满足其中一位
									(TickType_t			) portMAX_DELAY)//阻塞时间	

11.任务通知:(#include "tasks.h")
	遇到错误:
	悉知:需要打开宏才能使用 #define configUSE_TASK_NOTIFICATIONS		1 
	
	//枚举
	typedef enum
	{
		eNoAction = 0,				/* 通知任务而不更新其通知值. */
		eSetBits,						/* 更新指定bit. */
		eIncrement,					/* 通知值加1. */
		eSetValueWithOverwrite,		/*  覆写的方式更新通知值. */
		eSetValueWithoutOverwrite		/* 不覆写通知值. */
	} eNotifyAction;
	
	发送任务通知:
	任务级:
			xTaskNotifyGive(Task2Task_Handler);	//任务句柄				
			err = xTaskNotify(Task2Task_Handler,Value,"枚举");//任务句柄 任务通知值 任务通知更新的办法
			err = xTaskNotifyAndQuery(Task2Task_Handler,Value,"枚举",&PreviousNotifyValue);
	//任务句柄 任务通知值 任务通知更新办法 保存更新前的任务通知值
	中断级:
			vTaskNotifyGiveFromISR(Task2Task_Handler,&HigherPriorityTaskWoken);//任务句柄 保存变量
			err = xTaskNotifyFromISR(Task2Task_Handler,value,"枚举",&HigherPriorityTaskWoken);	
	//任务句柄  任务通知值  任务通知更新的办法 保存变量
	
			err = xTaskNotifyAndQueryFromISR(Task2Task_Handler,value,"枚举",&PreviousNotifyValue,&HigherPriorityTaskWoken);
	//任务句柄 任务通知值 任务通知更新办法 保存更新前的任务通知值 保存变量
	
	在保存变量(HigherPriorityTaskWoken)为pdTRUE时退出中断服务函数之前一定要进行一次任务切换
	
	获取任务通知:
	vlaue = ulTaskNotifyTake(pdTRUE,portMAX_DELAY);//推荐模拟二值信号量 计数型信号量使用
	//pdFALSE 任务通知值加1(类似于计数型信号量) pdTRUE任务通知值清0(类似于二值信号量)return:任务通知值减少清零的值
	
	err = xTaskNotifyWait(0,0xffffffff,&NotificationValue,portMAX_DELAY);//可以获取任何模拟的任务通知 消息邮箱 事件标志组等
	//不理解,不理解,保存任务通知值 ,阻塞时间  return:pdTRUE获取任务通知成功 pdFALSE获取失败

12.低功耗Tickless:
	打开宏:#define configUSE_TICKLESS_IDLE		1                     //1启用低功耗tickless模式
	
	自定义两个函数对接官方给的宏:
	//低功耗模式Tickless
	#define configPRE_SLEEP_PROCESSING	PreSleepProcessing
	#define configPOST_SLEEP_PROCESSING	PostSleepProcessing
	函数继承声明:
	extern void PreSleepProcessing(uint32_t ulExpectedIdleTime);
	extern void PostSleepProcessing(uint32_t ulExpectedIdleTime);
	主函数进行函数实现:
	//进入低功耗模式前需要处理的事情
	//ulExpectedIdleTime:低功耗模式运行时间
	void PreSleepProcessing(uint32_t ulExpectedIdleTime)
	{
		//关闭某些低功耗模式下不使用的外设时钟,
		RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, DISABLE);
		RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOC, DISABLE);	
		RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOD, DISABLE);	
		RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOE, DISABLE);	
		RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOF, DISABLE);	
		RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOG, DISABLE);	
		RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOH, DISABLE);	  
	}
	
	//退出低功耗模式以后需要处理的事情
	//ulExpectedIdleTime:低功耗模式运行时间
	void PostSleepProcessing(uint32_t ulExpectedIdleTime)
	{
		//退出低功耗模式以后打开那些被关闭的外设时钟
		RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);
		RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOC, ENABLE);	
		RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOD, ENABLE);	
		RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOE, ENABLE);	
		RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOF, ENABLE);	
		RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOG, ENABLE);	
		RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOH, ENABLE);	              
	}

13.空闲任务钩子函数:
	打开宏#define configUSE_IDLE_HOOK		1                     //1,使用空闲钩子;0,不使用
	实现钩子函数	void vApplicationIdleHook(void)
	void vApplicationIdleHook(void)
	{
		__disable_irq();
		__dsb( portSY_FULL_READ_WRITE );
		__isb( portSY_FULL_READ_WRITE );	
		
		BeforeEnterSleep(void);		//进入睡眠之前关闭不使用的外设时钟,函数自己实现(模拟低功耗函数实现)
		__wfi();					//进入睡眠模式
		AfterExitSleep(void);		//退出低功耗模式之后打开关闭的外设时钟,函数自己实现(如上)
		
		__dsb( portSY_FULL_READ_WRITE );	
		__isb( portSY_FULL_READ_WRITE );
		__enable_irq();
	}
	
	14.内存管理:
	u8 *buffer;
	size_t freemem;
	内存申请:
	buffer=pvPortMalloc(30);
	
	内存释放:
	vPortFree(buffer);
	buffer=NULL;//释放完内存之后必须要要对内存进行清空,不然会造成假清空
	
	获取剩余内存大小:
	freemem = xPortGetFreeHeapSize();
	
	

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

maosql

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

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

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

打赏作者

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

抵扣说明:

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

余额充值