嵌入式实时操作系统笔记3:FreeRTOS移植(STM32F407)_编写简单的FreeRTOS任务例程

上文讲到UC/OS III系统的移植,那篇文章是失败了的,网络上的资料真是层次不清,多有遗漏步骤,导致单片机连操作系统的初始化都卡在那,这次换个赛道,学FreeRTOS吧......

今日任务如标题所示:FreeRTOS移植(STM32F407)_编写简单的FreeRTOS任务例程

文章提供测试代码讲解、完整工程下载、测试效果图

这次总算是成功了!也是成功触碰到操作系统的领域了

目录

FreeRTOS移植准备:

准备STM32F407VET6空项目:

下载FreeRTOS V202212.00源码:

提取FreeRTOS V202212.00源码:

工程添加FreeRTOS源码:

添加文件到FreeRTOS/port:

添加文件到FreeRTOS/src:

添加FreeRTOSConfig.h:

所有工程源码添加情况如下所示:

 魔棒添加各个头文件路径:

 FreeRTOS/includes:

工程修改一些文件:

修改 FreeRTOSConfig.h :

修改stm32f4xx_it.h:

修改系统时钟初始化函数:

修改systick中断服务函数:

主函数:

问题解决:

编译报错:FCARM - Output Name not specified, please check 'Options for Target - Utilities'

编译报错:钩子函数未定义:

最终实现0报错:

编写简单的FreeRTOS任务例程:

宏定义各个任务:

主函数创建开始任务:

编写START_TASK 开始任务函数:

编写其余任务函数:

测试效果图:

测试遇到的问题:

1、只有task3一个最后初始化的任务能执行:

2、串口经常断开与PC的连接,且LED亮度闪烁不定:

测试工程说明:

测试工程下载:

测试工程文件注释勘误:

网上查阅资料贴出:


FreeRTOS移植准备:

准备STM32F407VET6空项目:

这里我使用了自己封装的裸机开发空项目文件,它的介绍如下,下载网址也同步贴出:

https://download.csdn.net/download/qq_64257614/89338792?spm=1001.2014.3001.5503

下载FreeRTOS V202212.00源码:

官方网址贴出:

FreeRTOS - Market leading RTOS (Real Time Operating System) for embedded systems with Internet of Things extensions

官网提供的github链接太卡了,建议去代码托管网站下载:

FreeRTOS Real Time Kernel (RTOS) - Browse /FreeRTOS at SourceForge.net

我这里就直接下载目前的最新版本了:

 点这个zip文件下载就行了:

提取FreeRTOS V202212.00源码:

目录   FreeRTOSv202212.00\FreeRTOS\Source  下的就是源码:

在STM32空项目文件夹目录新建文件夹FreeRTOS:并移动文件:

然后在FreeRTOSv202212.00\FreeRTOS\Source\portable 目录将这俩文件夹移动到PORT

最后直接将 目录 FreeRTOSv202212.00\FreeRTOS\Source 下的include文件夹移动到在STM32空项目文件夹目录新建文件夹FreeRTOS

最后复制FreeRTOSv202212.00\FreeRTOS\Demo\CORTEX_M4F_STM32F407ZG-SK 中的 FreeRTOSConfig.h 到文件夹至此移动结束:

工程添加FreeRTOS源码:

先新建俩个分组:

FreeRTOS/src
FreeRTOS/port

添加文件到FreeRTOS/port

点击---FreeRTOS/src----AddFiles

添加以下目录中所有文件 :\FreeRTOS\PORT\RVDS\ARM_CM4F

如果查找不到文件,别忘记将文件类型改为 ALL Files

添加完后应如是:

方才添加时发现,有俩个ARM_M4相关的文件夹供选择,那它们有何区别呢:

  1. ARM_CM4F:这通常表示针对ARM Cortex-M4F内核的源代码。STM32F407VET6是一个基于ARM Cortex-M4F的处理器,它包含了浮点单元(FPU),因此如果你打算使用到浮点运算,那么选择ARM_CM4F的文件可能更为合适。
  2. ARM_CM4_MPU:这通常与ARM Cortex-M4处理器中的内存保护单元(MPU)相关。MPU用于提供硬件级别的内存访问控制,以防止一个任务或进程访问到另一个任务或进程的内存区域。如果你的项目需要这种级别的内存保护,那么你可能需要选择ARM_CM4_MPU的文件。
  3. 至此我们知道了STM32F407VET6 需要添加的是ARM_CM4F 文件夹中的内容!

然后再添加添加以下目录中的文件 :\FreeRTOS\FreeRTOS\PORT\MemMang

这里我们就添加heap4.c即可,(这个是选择一个添加的,具体介绍下面会提到)

添加heap4.c
  • heap_1.c这是一个非常简单的内存管理实现,它使用一个静态分配的数组作为堆。适用于那些内存需求相对固定且不需要复杂的内存管理的应用。
  • heap_2.c这个实现提供了比heap_1更复杂的内存管理,包括内存碎片合并等特性。但它仍然基于一个静态分配的数组。
  • heap_3.cheap_4.c这些实现提供了更复杂的内存管理算法,但它们可能并不总是适合嵌入式系统,因为它们可能需要更多的RAM或处理时间。
  • heap_5.c(可能称为heap_5x.c):这是一个基于最佳适应(best-fit)算法的堆实现,它提供了较好的内存碎片管理,但可能需要更复杂的实现和更多的处理时间。
  • heap_x.c这是一个用户定义的内存管理实现,允许你根据自己的需求编写自定义的内存管理代码。

添加文件到FreeRTOS/src

点击---FreeRTOS/src----AddFiles

添加以下目录中所有文件 :\FreeRTOS\SRC

如果查找不到文件,别忘记将文件类型改为 ALL Files

添加文件到 FreeRTOS/src

添加FreeRTOSConfig.h:

由之前移动提取源码的操作可知,这个.h文件目录在截图所示文件夹内:

FreeRTOSConfig.h目录

这个头文件随便添加在哪都行,我是放置在了startup 组内了:

所有工程源码添加情况如下所示:

 魔棒添加各个头文件路径:

 FreeRTOS/includes:

这我额外新增一个组FreeRTOS/includes并将FreeRTOS\include文件夹中的内容都放进了:

工程修改一些文件:

修改 FreeRTOSConfig.h :

宏定义配置功能介绍,这部分可以先跳过或者快速浏览,以后用到在仔细看:

/*设置为1,表示启用抢占式调度。这意味着当一个更高优先级的任务准备好运行时,它会立即抢占当前运行的任务的CPU。*/
#define configUSE_PREEMPTION			1
/*设置为1,表示启用空闲钩子函数。空闲钩子函数是一个可以在系统处于空闲状态时调用的函数。*/
#define configUSE_IDLE_HOOK				1
/*设置为1,表示启用滴答定时器钩子函数。这个函数在每个滴答定时器中断时都会被调用。*/
#define configUSE_TICK_HOOK				1
/*定义CPU的时钟频率。这里使用了SystemCoreClock宏,它可能是在其他地方定义的。*/
#define configCPU_CLOCK_HZ				( SystemCoreClock )
/*定义了FreeRTOS滴答定时器的频率。这里设置为1000Hz,意味着每秒有1000个滴答。*/
#define configTICK_RATE_HZ				( ( TickType_t ) 1000 )
/*定义了FreeRTOS中可以使用的最大任务优先级数量。这里设置为5。*/
#define configMAX_PRIORITIES			( 5 )
/*定义了每个任务的最小栈大小(以字节为单位)。这里设置为130字节。*/
#define configMINIMAL_STACK_SIZE		( ( unsigned short ) 130 )
/*定义了FreeRTOS可用的总堆大小(以字节为单位)。这里设置为75KB。*/	
#define configTOTAL_HEAP_SIZE			( ( size_t ) ( 75 * 1024 ) )
/*定义了任务名称的最大长度。这里设置为10个字符。*/
#define configMAX_TASK_NAME_LEN			( 10 )
/*设置为1,表示启用跟踪功能。这允许你收集有关任务执行、队列操作和其他RTOS活动的信息。*/
#define configUSE_TRACE_FACILITY		1
/*设置为0,表示使用32位滴答计数器。如果设置为1,则使用16位滴答计数器。*/
#define configUSE_16_BIT_TICKS			0
/*设置为1,表示在空闲任务中应该进行任务切换(如果有其他任务准备好运行)。*/
#define configIDLE_SHOULD_YIELD			1
/*设置为1,表示启用互斥量(mutex)功能。*/
#define configUSE_MUTEXES				1
/*定义了队列注册表的项数。这允许你跟踪队列的使用情况,并帮助调试。*/
#define configQUEUE_REGISTRY_SIZE		8
/*定义了栈溢出检查的类型。这里设置为2,可能是某种特定类型的检查(具体的检查方式可能因FreeRTOS版本而异)。*/
#define configCHECK_FOR_STACK_OVERFLOW	2
/*设置为1,表示启用递归互斥量。递归互斥量允许同一个任务多次获取同一个互斥量。*/
#define configUSE_RECURSIVE_MUTEXES		1
/*设置为1,表示启用内存分配失败钩子函数。当FreeRTOS的堆内存耗尽时,这个函数会被调用。*/
#define configUSE_MALLOC_FAILED_HOOK	1
/*设置为0,表示不启用应用程序任务标签功能。*/
#define configUSE_APPLICATION_TASK_TAG	0
/*设置为1,表示启用计数信号量。*/
#define configUSE_COUNTING_SEMAPHORES	1
/*设置为0,表示不生成运行时统计信息。如果设置为1,FreeRTOS会收集关于任务运行时间的信息。*/
#define configGENERATE_RUN_TIME_STATS	0


/* Software timer definitions. */
/*设置为1,表示启用FreeRTOS的软件定时器功能。软件定时器允许你在没有硬件定时器可用或想要更复杂的定时行为时使用软件实现定时器。*/
#define configUSE_TIMERS				1
/*定义了软件定时器任务的优先级。在这里,它被设置为2,意味着软件定时器任务的优先级为2。*/
#define configTIMER_TASK_PRIORITY		( 2 )
/*定义了软件定时器队列的长度。当定时器到期时,它们会被添加到这个队列中以供软件定时器任务处理。队列长度为10表示最多可以有10个定时器等待处理。*/
#define configTIMER_QUEUE_LENGTH		10
/*定义了软件定时器任务的栈深度。这里它被设置为configMINIMAL_STACK_SIZE * 2,意味着软件定时器任务的栈大小是最小栈大小的两倍。*/
#define configTIMER_TASK_STACK_DEPTH	( configMINIMAL_STACK_SIZE * 2 )

/* Set the following definitions to 1 to include the API function, or zero
to exclude the API function. */
/*设置为1,表示包含vTaskPrioritySet函数。这个函数用于动态地改变任务的优先级。*/
#define INCLUDE_vTaskPrioritySet		1
/*设置为1,表示包含uxTaskPriorityGet函数。这个函数用于获取任务的当前优先级。*/
#define INCLUDE_uxTaskPriorityGet		1
/*设置为1,表示包含vTaskDelete函数。这个函数用于删除一个任务,释放其占用的资源。*/
#define INCLUDE_vTaskDelete				1
/*设置为1,表示包含vTaskCleanUpResources函数。这个函数用于清理FreeRTOS内核在不再需要时占用的资源。通常,在应用程序的最后一个任务结束时调用。*/
#define INCLUDE_vTaskCleanUpResources	1
/*设置为1,表示包含vTaskSuspend函数。这个函数用于挂起一个任务,使其不再运行,直到它被恢复。*/
#define INCLUDE_vTaskSuspend			1
/*设置为1,表示包含vTaskDelayUntil函数。这个函数用于使当前任务延迟到指定的绝对时间。*/
#define INCLUDE_vTaskDelayUntil			1
/*设置为1,表示包含vTaskDelay函数。这个函数用于使当前任务延迟指定的时间片数(滴答数)。*/
#define INCLUDE_vTaskDelay				1

 在FreeRTOSConfig.h添加以下头文件:

#include "stm32f4xx.h"//

修改条件编译:

改为:

#if defined (__ICCARM__) || defined (__CC_ARM)  ||  defined (__GNUC__) 
	#include <stdint.h>
	extern uint32_t SystemCoreClock;
#endif

屏蔽FreeRTOSConfig.h掉底部的#define xPortSysTickHandler SysTick_Handler

修改stm32f4xx_it.h:

注释掉几个中断服务函数 PendSV_Handler(),SVC_Handler(),SysTick_Handler()

因为已经在FreeRTOS的相关文件中需要用到,并提供定义了:

修改系统时钟初始化函数:

这部分Systick系统时钟的初始化目的是要将系统时钟的频率  与FreeRTOS的宏定义configTICK_RATE_HZ 进行联系,之后是可以通过修改 configTICK_RATE_HZ来反过来直接修改Systick系统时钟的频率的

			   
//初始化延迟函数
//SYSTICK的时钟固定为AHB时钟,基础例程里面SYSTICK时钟频率为AHB/8
//这里为了兼容FreeRTOS,所以将SYSTICK的时钟频率改为AHB的频率!
//SYSCLK:系统时钟频率
void delay_init(u8 SYSCLK)
{
	u32 reload;
 	SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK); 
	fac_us=SYSCLK;							//不论是否使用OS,fac_us都需要使用
	reload=SYSCLK;							//每秒钟的计数次数 单位为M	   
	reload*=1000000/configTICK_RATE_HZ;		//根据configTICK_RATE_HZ设定溢出时间
											//reload为24位寄存器,最大值:16777216,在168M下,约合0.0998s左右	
	fac_ms=1000/configTICK_RATE_HZ;			//代表OS可以延时的最少单位	   
	SysTick->CTRL|=SysTick_CTRL_TICKINT_Msk;//开启SYSTICK中断
	SysTick->LOAD=reload; 					//每1/configTICK_RATE_HZ断一次	
	SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk; //开启SYSTICK     
}								    

修改systick中断服务函数:

这个函数我添加在了#include "delay.h":

// 声明SysTick中断处理函数  
void SysTick_Handler(void)
{
	if(xTaskGetSchedulerState()!=taskSCHEDULER_NOT_STARTED)
	{
		xPortSysTickHandler();
	}
}

主函数:

先添加头文件,然后删去while(1)循环之类的:

/*****************FREE RTOS头文件***************/
#include "FreeRTOS.h"
#include "task.h"

问题解决:

编译报错:FCARM - Output Name not specified, please check 'Options for Target - Utilities'

这是因为某个导入文件的格式选错了:这是个说明文件,不小心暴力导入了,直接鼠标右键Remove即可

编译报错:钩子函数未定义:

FreeRTOSConfig.h中关闭这些钩子函数,他们都是宏定义决定,这里将  configUSE_IDLE_HOOK

configUSE_TICK_HOOK

configUSE_MALLOC_FAILED_HOOK

configUSE_FOR_STACK_OVERFLOW

或者叫configCHECK_FOR_STACK_OVERFLOW

定义为0

最终实现0报错:

编写简单的FreeRTOS任务例程:

各个TASK的任务如下描述:

//TASK_1 串口报告自己执行的次数 (0.5s) 并打开LED

//TASK_2 串口报告自己执行的次数 (1.2s) 并关闭LED

//TASK_3 会每隔 5S 打印

宏定义各个任务:

任务优先级 \任务堆栈大小   \任务句柄 \任务函数
/*******开始任务的宏定义*********/
#define START_TASK_PRIO		1         //任务优先级
#define START_STK_SIZE 		128       //任务堆栈大小	
TaskHandle_t StartTask_Handler;     //任务句柄
void start_task(void *pvParameters);//任务函数

/*******TASK1任务的宏定义*********/
#define TASK1_TASK_PRIO		2         //任务优先级
#define TASK1_STK_SIZE 		50        //任务堆栈大小	
TaskHandle_t Task1_Handler;         //任务句柄
void task1_task(void *pvParameters);//任务函数

/*******TASK2任务的宏定义*********/
#define TASK2_TASK_PRIO		3         //任务优先级
#define TASK2_STK_SIZE 		50        //任务堆栈大小	
TaskHandle_t Task2_Handler;         //任务句柄
void task2_task(void *pvParameters);//任务函数

/*******TASK3任务的宏定义*********/
#define TASK3_TASK_PRIO		3         //任务优先级
#define TASK3_STK_SIZE 		50        //任务堆栈大小	
TaskHandle_t Task3_Handler;         //任务句柄
void task3_task(void *pvParameters);//任务函数

主函数创建开始任务:

int main(void)
{	
  system_init_all();       //开机初始化系统所有模块
  //开启 FreeRTOS 操作系统,创建开始任务!
	//创建开始任务
    xTaskCreate((TaskFunction_t )start_task,            //任务函数
                (const char*    )"start_task",          //任务名称
                (uint16_t       )START_STK_SIZE,        //任务堆栈大小
                (void*          )NULL,                  //传递给任务函数的参数
                (UBaseType_t    )START_TASK_PRIO,       //任务优先级
                (TaskHandle_t*  )&StartTask_Handler);   //任务句柄              
    UsartPrintf(USART1," FreeRTOS : Hello World!\r\n");	//开机打印测试字符串 
		vTaskStartScheduler();          //开启任务调度	
}

编写START_TASK 开始任务函数:

 START_TASK 开始任务函数 在创建完其余任务后必须删除 START_TASK 任务

不然他也会出现在时间片轮询,造成不必要的占用!

//开始任务任务函数
void start_task(void *pvParameters)
{

	//创建TASK1任务

    xTaskCreate((TaskFunction_t )task1_task,     	
                (const char*    )"task1_task",   	
                (uint16_t       )TASK1_STK_SIZE, 
                (void*          )NULL,				
                (UBaseType_t    )TASK1_TASK_PRIO,	
                (TaskHandle_t*  )&Task1_Handler);   

	
	//创建TASK2任务
    xTaskCreate((TaskFunction_t )task2_task,     	
                (const char*    )"task2_task",   	
                (uint16_t       )TASK2_STK_SIZE, 
                (void*          )NULL,				
                (UBaseType_t    )TASK2_TASK_PRIO,	
                (TaskHandle_t*  )&Task2_Handler);  
								
	//创建TASK3任务
    xTaskCreate((TaskFunction_t )task3_task,     	
                (const char*    )"task3_task",   	
                (uint16_t       )TASK3_STK_SIZE, 
                (void*          )NULL,				
                (UBaseType_t    )TASK3_TASK_PRIO,	
                (TaskHandle_t*  )&Task3_Handler);  
								
	//删除开始任务							
		vTaskDelete(StartTask_Handler); 
}

编写其余任务函数:

在开始任务的基础上,其他任务只需要在此基础上添加三个地方:
①声明;      ②函数编写;     ③开始任务函数调用


//TASK_1 串口报告自己执行的次数 (0.5s)
void task1_task(void *pvParameters)
{
	int TASK1_num=0;  //记录任务TASK_1执行次数 
	
	
	while(1)
	{
		TASK1_num++;	//任务TASK_1执行次数加1
		PBout(2)=1;
	  UsartPrintf(USART1,"TASK_1 has Carred out %d times! \r\n",TASK1_num);	//打印测试字符串(并报告TASK_1执行次数)
		vTaskDelay(500);//使当前任务延迟指定的时间(2s):  (让当前任务放弃CPU一段时间,CPU让给其余任务)
	}
}
 
//TASK_2 串口报告自己执行的次数 (1.2s)
void task2_task(void *pvParameters)
{
	int TASK2_num=0;  //记录任务TASK_2执行次数
		
	while(1)
	{
		TASK2_num++;
		PBout(2)=0;
	    UsartPrintf(USART1,"TASK_2 has Carred out %d times! \r\n",TASK2_num);	//打印测试字符串(并报告TASK_2执行次数)
		vTaskDelay(1200);//使当前任务延迟指定的时间(1s):  (让当前任务放弃CPU一段时间,CPU让给其余任务)
	}
}
 
//TASK_3 会每隔 5S 打印
void task3_task(void *pvParameters)
{
	int TASK3_num=0;  //记录任务TASK_3执行次数								 
	while(1)
	{
		  TASK3_num++;
	    UsartPrintf(USART1,"TASK_3 has Carred out %d times! \r\n",TASK3_num);	//打印测试字符串(并报告TASK_3执行次数)							 
		  vTaskDelay(5000); //使当前任务延迟指定的时间(17s):  (让当前任务放弃CPU一段时间,CPU让给其余任务)	
	}
}

测试效果图:

在串口测试可以看出三个任务各自的延时关系,以及不阻塞延时就是将延时的时间空闲留给其余任务执行:

框图关系可以看出以下几点:

1、三个任务被创建后立马执行一次

2、任务三 Task3 执行一次到下次的间隔有 5S,任务1 Task1 执行了9次(约4.5s),任务2 Task2 执行了4次(约4.8s)

3、任务二 Task2执行一次到下次的间隔有 1.2S,任务1 Task1 执行了2次(约1s)

然后还有LED会被 Task1 Task2 周期性亮灭

测试遇到的问题:

1、只有task3一个最后初始化的任务能执行:

(测试时尝试删去过task3,发现最后初始化的任务task2仍然是这个效果,也能执行,但仅仅是最后初始化的任务能执行)

解决:我是屏蔽删去了定时器的初始化,就能让三个任务都能执行了,后面应该会学到任务屏蔽块函数,能够屏蔽这些中断,防止任务被打断得连初始化进入都进不来

2、串口经常断开与PC的连接,且LED亮度闪烁不定:

这明显是供电不足导致的,我额外插个USB稳定的电源解决了

测试工程说明:

这里说明一下,因为我在初学时到处查阅资料无法理解Systick系统时钟的初始化与中断服务函数是如何与FreeRTOS的宏定义configTICK_RATE_HZ进行联系的,因此移植了正点原子的delay.h文件(初始化系统时钟的,可选OS系统),原工程的board.h被移除了 

/*
                              NULL指向我 { FreeRTOS } 项目封装说明:
                                                                    
    1、所有头文件都引用在headfire.h中,新建.h都只需 #include "headfire.h" 即可,也方便查阅修改
       (1) headfire.h新增 位带操作,实现51类似的GPIO控制功能
       
    2、Timer_common_init();可以初始化任意 TIMx 为通用定时器(带溢出中断),但必须在调用前添加开启相应定时器时钟的语句    
        
    3、函数 system_init_all(void); 初始化STM32所有外设模块 移动到了 headfire.h
    
    4、 自主配置修改了 SystemTick 的配置 内核定时器SystemTick  
       (1) 工程文件 #include "delay.h" 内配置了 SystemTick 内核定时器
       (2) SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK); 函数初始化了SystemTick 内核定时器 的时钟源是168M
       (3) 增加了 SystemTick 定时中断功能
       (4) 声明SysTick中断处理函数  void SysTick_Handler(void) 必须要有,否则中断无法跳出,程序会卡在 初始化 1ms (这个时间是根据说明项5得出的) 后
       
       
    5、 SysTick新增中断,频率 为1000Hz, 即1ms  
       (1) 如想更改 内核定时器 溢出频率 更改   #define configTICK_RATE_HZ (1000) 括号内的值即可:  (在FreeRTOSConfig.h中)        
                uint32_t reload = SystemCoreClock / 1000 - 1; // 1ms中断频率 (1000Hz)
                         reload = configTICK_RATE_HZ
                Tips: 1、SystemCoreClock =168 000 000
                       2、SysTick_LOAD = (SystemCoreClock / TickRate) - 1
                       3、SysTick_LOAD = reload;
       (2) 内核定时器 未配置 溢出中断的NVIC优先级 (如果需要配置,我觉得应该要比其余中断低 甚至于最低组4)
       (3)
          
          
    6、 #include <board.h> 新增位带操作的宏定义 ,可以实现51类似的GPIO控制功能
        
    7、 UsartPrintf (USART_TypeDef *USARTx, char *fmt,...)函数 能够做到使用任意串口打印,并且用法与Printf相同(需要多传第一个参数 USARTx)
        (1) 该函数定义在 : #include "Uart.h"
        (2) 如需别处复制使用 需包含头文件 #include "stdarg.h"
    
*/

测试工程下载:

https://download.csdn.net/download/qq_64257614/89342421

测试工程文件注释勘误:

测试工程在学习的过程中经过多次修改,有部分注释与代码实际有出入不慎对应,这里勘误一下:

这里的延时是500ms,但注释写的是2S,注释错误

这里的延时是1200ms,但注释写的是1S,注释错误

这里的延时是5s,但注释写的是17S,注释错误

网上查阅资料贴出:

2、STM32F407移植FreeRTOS步骤_systemcoreclock 未定义-CSDN博客

FreeRTOS运行一次后卡死_freertos任务执行一次-CSDN博客

01 FreeRTOS任务实例_freertos例程-CSDN博客

  • 44
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

NULL指向我

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

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

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

打赏作者

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

抵扣说明:

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

余额充值