从零入门 FreeRTOS 操作系统之创建任务流程

从零入门 FreeRTOS 操作系统之创建任务

1 启动方式

main() 函数中将硬件和 RTOS 系统先初始化好,在主函数内部创建一个启动任务后就启动调度器,然后在启动任务里面创建各种应用任务,当所有任务都创建成功后,启动任务把自己删除。

由于 AppTaskCreate 任务执行一次之后就进行删除,并不影响系统的运行,所以,只执行一次的任务在执行完毕要记得及时删除。

2 创建静态内存任务

创建 SRAM 静态内存任务的要点是:configSUPPORT_STATIC_ALLOCATION 这个宏定义必须为 1(在 FreeRTOSConfig.h 文件中)。并且任务使用的栈和任务控制块都使用 静态内存,即预先定义好的全局变量,这些预先定义好的全局变量都存在内部的 SRAM 中。

代码编写流程1

打开 FreeRTOSConfig.h 文件,将宏定义 configSUPPORT_STATIC_ALLOCATION 的值改为 1,代码如下所示:

//支持静态内存
#define configSUPPORT_STATIC_ALLOCATION			1

2.1 定义任务函数

任务实际上就是一个无限循环且不带返回值的 C 函数。

注意事项1: 任务必须是一个死循环,否则任务将通过 LR 返回,如果 LR 指向了非法的内存就会产生 HardFault_Handler,而 FreeRTOS 指向一个死循环,那么任务返回之后就在死循环中执行,这样子的任务是不安全的,所以避免这种情况,任务一般都是死循环并且无返回值的。

注意事项2: 任务里面的延时函数必须使用 FreeRTOS 里面提供的延时函数,并不能使用我们裸机编程中的那种延时。

这两种的延时的区别是 FreeRTOS 里面的延时是阻塞延时,即调用 vTaskDelay() 函数的时候,当前任务会被挂起,调度器会切换到其它就绪的任务,从而实现多任务。如果还是使用裸机编程中的那种延时,那么整个任务就成为了一个死循环,如果恰好该任务的优先级是最高的,那么系统永远都是在这个任务中运行,比它优先级更低的任务无法运行,根本无法实现多任务。

代码编写流程2

根据启动方式,我们需要创建一个启动任务,让它专门负责创建各种应用任务,因此需要同时创建启动任务、应用任务。

首先,我们在主函数的后面定义应用任务函数,如下代码所示:

/**********************************************************************
  * @ 函数名  : LED_Task
  * @ 功能说明: LED_Task任务主体
  * @ 参数    :   
  * @ 返回值  : 无
  ********************************************************************/
static void LED_Task(void* parameter)
{	
    while (1)
    {
        LED1_ON;
        vTaskDelay(500);   /* 延时500个tick */
        printf("LED_Task Running,LED1_ON\r\n");
        
        LED1_OFF;     
        vTaskDelay(500);   /* 延时500个tick */		 		
        printf("LED_Task Running,LED1_OFF\r\n");
    }
}

通过上述代码,可以看出此应用任务的功能是让 LED 间隔 1 秒闪烁一次,通过串口输出文字的方式查看当前任务的运行情况。

之后,我们同样在主函数的后面定义 启动任务函数,代码如下所示:

/***********************************************************************
  * @ 函数名  : AppTaskCreate
  * @ 功能说明: 为了方便管理,所有的任务创建函数都放在这个函数里面
  * @ 参数    : 无  
  * @ 返回值  : 无
  **********************************************************************/
static void AppTaskCreate(void)
{
	taskENTER_CRITICAL();           		//进入临界区

	/* 创建LED_Task任务 */
	LED_Task_Handle = xTaskCreateStatic((TaskFunction_t	)LED_Task,			//任务函数
										(const char* 	)"LED_Task",		//任务名称
										(uint32_t 		)128,				//任务堆栈大小
										(void* 		  	)NULL,				//传递给任务函数的参数
										(UBaseType_t 	)4, 				//任务优先级
										(StackType_t*   )LED_Task_Stack,	//任务堆栈
										(StaticTask_t*  )&LED_Task_TCB);	//任务控制块   
	
	if(NULL != LED_Task_Handle)				/* 创建成功 */
		printf("LED_Task任务创建成功!\n");
	else
		printf("LED_Task任务创建失败!\n");
	
	vTaskDelete(AppTaskCreate_Handle); 		//删除AppTaskCreate任务
  
	taskEXIT_CRITICAL();            		//退出临界区
}

此启动任务的函数的功能就是创建应用任务,里面的代码下面会着重讲,在这只需要知道其创建应用任务即可。

同时通过上面这个函数我们还可以看到,当创建应用任务完毕之后,启动任务自行删除:vTaskDelete(AppTaskCreate_Handle);

为了代码的规范性,还需将 应用任务函数启动任务的功能函数 在主函数的前面进行声明。

2.2 空闲任务与定时器任务堆栈函数

用户设定空闲(Idle)任务与定时器(Timer)任务的堆栈大小分别由 vApplicationGetIdleTaskMemory()vApplicationGetTimerTaskMemory() 两个函数实现,且必须由用户自己分配,而不能是动态分配。

代码编写流程3

在主函数的前面创建如下代码所示的 空闲、定时器任务堆栈及空闲、定时器任务控制块 代码:

// 声明空闲任务、定时器任务堆栈及控制块
/* 空闲任务任务堆栈 */
static StackType_t Idle_Task_Stack[configMINIMAL_STACK_SIZE];
/* 定时器任务堆栈 */
static StackType_t Timer_Task_Stack[configTIMER_TASK_STACK_DEPTH];

/* 空闲任务控制块 */
static StaticTask_t Idle_Task_TCB;	
/* 定时器任务控制块 */
static StaticTask_t Timer_Task_TCB;

之后需要在主函数的后面,创建获取空闲、定时器任务堆栈及任务控制块的代码,如下所示:

// 获取空闲任务、定时器任务堆栈及控制块内存
/**
  **********************************************************************
  * @brief  获取空闲任务的任务堆栈和任务控制块内存
	*					ppxTimerTaskTCBBuffer	:		任务控制块内存
	*					ppxTimerTaskStackBuffer	:	任务堆栈内存
	*					pulTimerTaskStackSize	:		任务堆栈大小
  * @author  fire
  * @version V1.0
  * @date    2018-xx-xx
  **********************************************************************
  */ 
void vApplicationGetIdleTaskMemory(StaticTask_t **ppxIdleTaskTCBBuffer, 
								   StackType_t **ppxIdleTaskStackBuffer, 
								   uint32_t *pulIdleTaskStackSize)
{
	*ppxIdleTaskTCBBuffer=&Idle_Task_TCB;/* 任务控制块内存 */
	*ppxIdleTaskStackBuffer=Idle_Task_Stack;/* 任务堆栈内存 */
	*pulIdleTaskStackSize=configMINIMAL_STACK_SIZE;/* 任务堆栈大小 */
}

/**
  *********************************************************************
  * @brief  获取定时器任务的任务堆栈和任务控制块内存
	*					ppxTimerTaskTCBBuffer	:		任务控制块内存
	*					ppxTimerTaskStackBuffer	:	任务堆栈内存
	*					pulTimerTaskStackSize	:		任务堆栈大小
  * @author  fire
  * @version V1.0
  * @date    2018-xx-xx
  **********************************************************************
  */ 
void vApplicationGetTimerTaskMemory(StaticTask_t **ppxTimerTaskTCBBuffer, 
									StackType_t **ppxTimerTaskStackBuffer, 
									uint32_t *pulTimerTaskStackSize)
{
	*ppxTimerTaskTCBBuffer=&Timer_Task_TCB;/* 任务控制块内存 */
	*ppxTimerTaskStackBuffer=Timer_Task_Stack;/* 任务堆栈内存 */
	*pulTimerTaskStackSize=configTIMER_TASK_STACK_DEPTH;/* 任务堆栈大小 */
}

2.3 定义任务栈

目前我们只创建了一个任务,当任务进入延时的时候,因为没有另外就绪的用户任务,那么系统就会进入空闲任务,空闲任务是 FreeRTOS 系统自己启动的一个任务,优先级最低。当整个系统都没有就绪任务的时候,系统必须保证有一个任务在运行,空闲任务就是为这个设计的。当用户任务延时到期,又会从空闲任务切换回用户任务。

FreeRTOS 系统中,每一个任务都是独立的,他们的运行环境都单独的保存在他们的栈空间当中。那么在定义好任务函数之后,我们还要为任务定义一个栈,目前我们使用的是静态内存,所以任务栈是一个独立的全局变量。

任务的栈占用的是 MCU 内部的 RAM,当任务越多的时候,需要使用的栈空间就越大,即需要使用的RAM 空间就越多。一个 MCU 能够支持多少任务,就得看你的 RAM 空间有多少。

代码编写流程4

从上文可以知道,我们的 FreeRTOS 操作系统的启动方式是:通过创建一个启动任务,然后启动此任务,启动完毕之后,再创建应用任务,创建完毕自行删除。

因此,我们需要将启动任务、应用任务所占用的栈空间定义出来。

接下来在主函数的前面定义 启动任务 AppTaskCreate 任务堆栈及应用任务 LED 任务堆栈,代码如下所示:

/* AppTaskCreate 任务任务堆栈 */
static StackType_t AppTaskCreate_Stack[128];
/* LED 任务堆栈 */
static StackType_t LED_Task_Stack[128];

在大多数系统中需要做栈空间地址对齐,在 FreeRTOS 中是以 8 字节大小对齐,并且会检查堆栈是否已经对齐,其中 portBYTE_ALIGNMENT 是在 portmacro.h 里面定义的一个宏,其值为 8,就是配置为按 8 字节对齐,当然用户可以选择按 1、2、4、8、16、32 等字节对齐,目前默认为 8。如果有需要,可以打开 portmacro.h 文件,找到 portBYTE_ALIGNMENT 宏定义,并更改其值。

在文件中声明 portBYTE_ALIGNMENT 宏可以应有的值,如下所示:

#if portBYTE_ALIGNMENT == 32
	#define portBYTE_ALIGNMENT_MASK ( 0x001f )
#endif

#if portBYTE_ALIGNMENT == 16
	#define portBYTE_ALIGNMENT_MASK ( 0x000f )
#endif

#if portBYTE_ALIGNMENT == 8
	#define portBYTE_ALIGNMENT_MASK ( 0x0007 )
#endif

#if portBYTE_ALIGNMENT == 4
	#define portBYTE_ALIGNMENT_MASK	( 0x0003 )
#endif

#if portBYTE_ALIGNMENT == 2
	#define portBYTE_ALIGNMENT_MASK	( 0x0001 )
#endif

#if portBYTE_ALIGNMENT == 1
	#define portBYTE_ALIGNMENT_MASK	( 0x0000 )
#endif

2.4 定义任务控制块

定义好任务函数和任务栈之后,我们还需要为任务定义一个任务控制块,通常我们称这个任务控制块为任务的身份证。在 C 代码上,任务控制块就是一个结构体,里面有非常多的成员,这些成员共同描述了任务的全部信息。

代码编写流程5

在主函数的前面定义如下代码所示的 AppTaskCreate 、LED_Task 任务控制块。

/* AppTaskCreate 任务控制块 */
static StaticTask_t AppTaskCreate_TCB;
/* LED_Task 任务控制块 */
static StaticTask_t LED_Task_TCB;

2.5 静态创建任务

一个任务的三要素是 任务主体函数、任务栈、任务控制块FreeRTOS 里面有一个叫静态任务创建函数 xTaskCreateStatic(),它将任务主体函数、任务栈(静态的)和任务控制块(静态的)这三者联系在一起,让任务可以随时被系统启动。

代码编写流程6

首先在主函数的前面定义启动任务的任务句柄,是一个指针,用于指向一个任务,当任务创建好之后,它就具有了一个任务句柄,以后我们要想操作这个任务都需要通过这个任务句柄。

 /* 创建任务句柄 */
static TaskHandle_t AppTaskCreate_Handle;

之后,在主函数内部创建如下代码所示的启动任务。

/* 创建 AppTaskCreate 任务 */
AppTaskCreate_Handle = xTaskCreateStatic((TaskFunction_t)AppTaskCreate,			//任务函数
										 (const char* 	)"AppTaskCreate",		//任务名称
										 (uint32_t 		)128,					//任务堆栈大小
										 (void* 		)NULL,					//传递给任务函数的参数
										 (UBaseType_t 	)3, 					//任务优先级
										 (StackType_t*  )AppTaskCreate_Stack,	//任务堆栈
										 (StaticTask_t* )&AppTaskCreate_TCB);	//任务控制块

if(NULL != AppTaskCreate_Handle)/* 创建成功 */
    vTaskStartScheduler();   /* 启动任务,开启调度 */	

其中:

  • 任务入口函数,即任务函数的名称,需要我们自己定义并且实现。
  • 任务名字,字符串形式,最大长度由 FreeRTOSConfig.h 中定义的 configMAX_TASK_NAME_LEN 宏指定,多余部分会被自动截掉,这里任务名字最好要与任务函数入口名字一致,方便进行调试。
  • 任务堆栈大小,单位为字,在32 位的处理器下(STM32),一个字等于 4 个字节,那么任务大小就为 128 * 4 字节。
  • 任务入口函数形参,不用的时候配置为 0 或者 NULL 即可。
  • 任务的优先级的范围根据 FreeRTOSConfig.h 中的宏
    configMAX_PRIORITIES 决定,如果使能 configUSE_PORT_OPTIMISED_TASK_SELECTION 这个宏定义,则最多支持 32 个优先级;如果不用特殊方法查找下一个运行的任务,那么则不强制要求限制最大可用优先级数目。在 FreeRTOS 中,数值越大优先级越高,0 代表最低优先级。
  • 任务栈起始地址,只有在使用静态内存的时候才需要提供,在使用动态内存的时候会根据提供的任务栈大小自动创建。
  • 任务控制块指针,在使用静态内存的时候,需要给任务初始化函数 xTaskCreateStatic() 传递预先定义好的任务控制块的指针。在使用动态内存的时候,任务创建函数 xTaskCreate() 会返回一个指针指向任务控制块,该任务控制块是 xTaskCreate() 函数里面动态分配的一块内存。

上面所述代码为创建启动任务,根据启动方式,我们需要在启动任务中创建更多的应用任务,代码在定义任务函数部分已列出来。

至此,我们的配置部分已经完毕。

2.6 启动任务

当任务创建好后,任务就处于就绪状态(Ready),就绪态的任务可以参与操作系统的调度。但是此时任务仅仅是创建了,还未开启任务调度器,也没创建空闲任务与定时器任务(如果使能了 configUSE_TIMERS 这个宏定义),而这两个任务的实现就是在启动任务调度器中完成的。每个操作系统,任务调度器只启动一次,之后就不会再次执行了,FreeRTOS 中启动任务调度器的函数是 vTaskStartScheduler(),并且启动任务调度器的时候就不会返回,从此任务管理都由 FreeRTOS 管理,此时才是真正进入实时操作系统中的第一步。

因此,需要在主函数最后添加如下代码来开启调度器。

if(NULL != AppTaskCreate_Handle)	/* 创建成功 */
	vTaskStartScheduler();   		/* 启动任务,开启调度 */

3 创建动态内存任务

动态内存,及堆,也属于 SRAM

任务使用的栈和任务控制块是在创建任务的时候 FreeRTOS 动态分配的,并不是预先定义好的全局变量。

在创建 SRAM 静态内存任务的时候,任务控制块和任务栈的内存空间都是从内部的 SRAM 里面分配的,具体分配到哪个地址由编译器决定。那么创建 SRAM 动态内存任务的时候,FreeRTOS 做法是在 SRAM 里面定义一个大数组,也就是堆内存,供 FreeRTOS 的动态内存分配函数使用,在第一次使用的时候,系统会将定义的堆内存进行初始化,在 FreeRTOS 提供的内存管理方案中实现(heap_1.c、heap_2.c、heap_4.c 等)。

首先,在 FreeRTOSConfig.h 文件中,定义有堆内存大小的宏为 configTOTAL_HEAP_SIZE,同时定义有宏 configSUPPORT_DYNAMIC_ALLOCATION,但是要注意这个宏
定义在使用 FreeRTOS 操作系统的时候必须开启。

//支持动态内存申请
#define configSUPPORT_DYNAMIC_ALLOCATION        1
//系统所有总的堆大小
#define configTOTAL_HEAP_SIZE					((size_t)(36*1024)) 

由于我们使用 heap_4.c,因此在此文件中有如下代码,从内部 SRAMM 里面定义一个静态数组 ucHeap,大小由 configTOTAL_HEAP_SIZE 这个宏决定,目前定义为 36KB。要注意定义的堆大小不能超过内部 SRAM 的总大小。

static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];

同时在 heap_4.c 文件中,有如下代码用来将堆进行初始化(如果这是第一次调用 malloc),以设置空闲块列表,方便以后分配内存,初始化完成之后会取得堆的结束地址。

/* If this is the first call to malloc then the heap will require
   initialisation to setup the list of free blocks. */
if( pxEnd == NULL )
{
	prvHeapInit();
}
else
{
	mtCOVERAGE_TEST_MARKER();
}

3.1 定义任务函数

使用动态内存的时候,任务的主体函数与使用静态内存时是一样的,代码如下所示:

/**********************************************************************
  * @ 函数名  : LED_Task
  * @ 功能说明: LED_Task任务主体
  * @ 参数    :   
  * @ 返回值  : 无
  ********************************************************************/
static void LED_Task(void* parameter)
{	
    while (1)
    {
        LED1_ON;
        vTaskDelay(500);   /* 延时500个tick */
        printf("LED_Task Running,LED1_ON\r\n");
        
        LED1_OFF;     
        vTaskDelay(500);   /* 延时500个tick */		 		
        printf("LED_Task Running,LED1_OFF\r\n");
    }
}

同时,要注意代码的规范性,记得将函数在主函数前面进行声明。

3.2 定义任务栈

使用动态内存的时候,任务栈在任务创建的时候创建,不用跟使用静态内存那样要预先定义好一个全局的静态的栈空间,动态内存就是按需分配内存,随用随取。

3.3 定义任务控制块指针

使用动态内存时候,不用跟使用静态内存那样要预先定义好一个全局的静态的任务控制块空间。任务控制块是在任务创建的时候分配内存空间创建,任务创建函数会返回一个指针,用于指向任务控制块,所以要预先为任务栈定义一个任务控制块指针,也是我们常说的任务句柄。

在主函数的前面,预先声明任务句柄,代码如下所示:

/**************************** 任务句柄 ********************************/
/* 
 * 任务句柄是一个指针,用于指向一个任务,当任务创建好之后,它就具有了一个任务句柄
 * 以后我们要想操作这个任务都需要通过这个任务句柄,如果是自身的任务操作自己,那么
 * 这个句柄可以为NULL。
 */
/* 创建任务句柄 */
static TaskHandle_t AppTaskCreate_Handle = NULL;
/* LED任务句柄 */
static TaskHandle_t LED_Task_Handle = NULL;

3.4 动态创建任务

使用静态内存时,使用 xTaskCreateStatic() 函数来创建一个任务,而使用动态内存的时,则使用 xTaskCreate() 函数来创建一个任务,两者的函数名不一样,具体的形参也有区别。

由于启动方式还是通过创建一个启动任务,之后由启动任务来创建应用任务,因此需要在主函数的内部创建启动任务,代码如下所示:

xReturn = xTaskCreate((TaskFunction_t )AppTaskCreate,  			/* 任务入口函数 */
                      (const char*    )"AppTaskCreate",			/* 任务名字 */
                      (uint16_t       )512,  		   			/* 任务栈大小 */
                      (void*          )NULL,           			/* 任务入口函数参数 */
                      (UBaseType_t    )1,              			/* 任务的优先级 */
                      (TaskHandle_t*  )&AppTaskCreate_Handle);	/* 任务控制块指针 */

代码详解:

  • 任务入口函数:即任务函数的名称,需要我们自己定义并且实现。
  • 任务名字:字符串形式,最大长度由 FreeRTOSConfig.h 中定义的 configMAX_TASK_NAME_LEN 宏指定,多余部分会被自动截掉,这里任务名字最好要与任务函数入口名字一致,方便进行调试。
  • 任务堆栈大小:单位为字,在 32 位的处理器下(STM32),一个字等于 4 个字节,那么任务大小就为 128 * 4 字节。
  • 任务入口函数形参:不用的时候配置为 0 或者 NULL 即可。
  • 任务的优先级:优先级范围根据 FreeRTOSConfig.h 中的宏
    configMAX_PRIORITIES 决定,如果使能 configUSE_PORT_OPTIMISED_TASK_SELECTION 这个宏定义,则最多支持 32 个优先级;如果不用特殊方法查找下一个运行的任务,那么则不强制要求限制最大可用优先级数目。在 FreeRTOS 中,数值越大优先级越高,0 代表最低优先级。
  • 任务控制块指针:在使用内存的时候,需要给任务初始化函数 xTaskCreateStatic() 传递预先定义好的任务控制块的指针。在使用动态内存的时候,任务创建函数 xTaskCreate() 会返回一个指针指向任务控制块,该任务控制块是 xTaskCreate() 函数里面动态分配的一块内存。

3.5 启动任务

当任务创建好后,任务处于就绪状态(Ready),在就绪态的任务可以参与操作系统的调度。但是此时任务仅仅是创建了,还未开启任务调度器,也没创建空闲任务与定时器任务(如果使能了 configUSE_TIMERS 这个宏定义),那这两个任务就是在启动任务调度器中实现。每个操作系统,任务调度器只启动一次,之后就不会再次执行了,FreeRTOS 中启动任务调度器的函数是 vTaskStartScheduler(),并且启动任务调度器的时候就不会返回,从此任务管理都由 FreeRTOS 管理,此时才是真正进入实时操作系统中的第一步。

因此,需要在上面创建启动任务之后开启调度器,具体代码如下所示:

/* 启动任务调度 */           
if(pdPASS == xReturn)
	vTaskStartScheduler();   /* 启动任务,开启调度 */
else
	return -1;

你可能感兴趣的:(CONTROL,stm32)