文章

FreeRTOS任务基础知识

任务基础知识

1. 多任务系统

1.1 前后台系统

单片机裸机开发时,一般都是在 main 函数里面用 while(1) 做一个大循环来完成所有的处理,循环中调用相应的函数完成所需的处理。有时也需要在中断中完成一些处理。相对于多任务系统而言,这就是单人单任务系统也称作前后台系统,中断服务函数作为前台程序,while(1) 作为后台程序,如下图示

img

1.2 抢占式多任务系统

多任务系统会把一个大问题分而治之,把大问题划分成很多个小问题,逐步的把小问题解决掉,大问题也就随之解决了,这些小问题可以单独的作为一个小任务来处理。多个任务带来了一个新的问题,即究竟哪个任务先运行,哪个任务后执行?完成这个功能的东西在 RTOS 系统中叫做任务调度器。不同的系统其任务调度器的实现方法也不同,比如 FreeRTOS 是一个抢占式的实时多任务系统,那么其任务调度器也是抢占式的,其运行过程如下图示

img

从图中可以看出,高优先级的任务可以打断低优先级任务的运行而取得 CPU 的使用权。高优先级的任务执行完成以后重新把 CPU 的使用权归还给低优先级的任务,这就是抢占式任务系统的基本原理

2. FreeRTOS 任务

在使用 RTOS 的时候一个实时应用可以作为一个独立的任务。每个任务都有自己的运行环境,不依赖于系统中其他的任务或者 RTOS 调度器。任何一个时间点只能有一个任务运行,具体运行哪个任务是由 RTOS 调度器来决定的,RTOS 调度器因此就会重复的开启、关闭每个任务。任务不需要了解 RTOS 调度器的具体行为,RTOS 调度器的职责是确保当一个任务开始执行的时候其上下文环境 (寄存器值,堆栈内容等) 和任务上一次退出的时候相同。为了做到这一点,每个任务都必须有个堆栈,当任务切换的时候将上下文环境保存在堆栈中,这样当任务再次执行的时候就可以从堆栈中取出上下文环境,任务恢复运行

2.1 任务状态

FreeRTOS 中的任务有下面几种状态

  • 运行态:当任务正在运行时,处于运行态;处于运行态的任务就是当前正在使用处理器的任务
  • 就绪态:已经准备就绪,可以运行的任务;有同优先级或更高优先级的任务正在运行占用 CPU
  • 阻塞态:任务正在等待某个外部事件即处于阻塞态;阻塞态有超时时间,若超时会退出阻塞态
  • 挂起态:进入挂起态后也不能被调度器调用进入运行态;挂起态的任务没有超时时间

任务状态之间的转换如下图

img

2.2 任务优先级

每个任务都可以分配一个从 0 ~ (configMAX_PRIORITIES - 1)的优先级,configMAX_PRIORITIES 在文件 FreeRTOSConfig.h 中定义,可以通过 STM32CubeMX 中 FreeRTOS 下的 MAX_PRIORITIES 参数进行配置。MAX_PRIORITIES 可以为任意值,但是考虑到 RAM 的消耗,最好设置为一个满足应用的最小值

优先级数字越低表示任务的优先级越低,0 的优先级最低,configMAX_PRIORITIES - 1 的优先级最高。空闲任务的优先级最低,为 0

2.3 任务实现

在使用 FreeRTOS 的过程中,要使用 xTaskCreate()xTaskCreateStatic() 来创建任务,这两个函数的第一个参数 pxTaskCode,就是这个任务的任务函数。任务函数就是完成本任务工作的函数。FreeRTOS 官方给出的任务函数模板如下:

1
2
3
4
5
6
7
8
9
void vATaskFunction(void *pvParameters)		//根据实际情况定义任务名;返回类型一定要为void类型
{
	for(;;)		//代表一个循环
	{
		任务应用程序;	//任务代码
		vTaskDelay();	//延时函数,不一定会用到
	}
	vTaskDelete(NULL);	//任务函数一般不允许跳出循环,如果一定要跳出的话在跳出循环后一定要删除此任务
}

2.4 任务控制块

FreeRTOS 的每个任务都有一些属性需要存储,FreeRTOS 把这些属性集合到一起用一个结构体来表示,这个结构体叫做任务控制块:TCB_t,在使用函数 xTaskCreate() 创建任务的时候就会自动的给每个任务分配一个任务控制块。此结构体在文件 task.c 中有定义,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
typedef struct tskTaskControlBlock
{
	volatile StackType_t	*pxTopOfStack;	/*任务堆栈栈顶*/

	#if ( portUSING_MPU_WRAPPERS == 1 )
		xMPU_SETTINGS	xMPUSettings;		/*MPU相关设置*/
	#endif

	ListItem_t			xStateListItem;		/*状态列表项*/
	ListItem_t			xEventListItem;		/*事件列表项*/
	UBaseType_t			uxPriority;			/*任务优先级*/
	StackType_t			*pxStack;			/*任务堆栈起始地址*/
	char				pcTaskName[ configMAX_TASK_NAME_LEN ];	/*任务名字*/

	#if ( portSTACK_GROWTH > 0 )
		StackType_t		*pxEndOfStack;		/*任务堆栈栈底*/
	#endif

	#if ( portCRITICAL_NESTING_IN_TCB == 1 )
		UBaseType_t		uxCriticalNesting;	/*临界区嵌套深度*/
	#endif

	#if ( configUSE_TRACE_FACILITY == 1 )
		UBaseType_t		uxTCBNumber;		/*debug的时候用到*/
		UBaseType_t		uxTaskNumber;		/*trace的时候用到*/
	#endif

	#if ( configUSE_MUTEXES == 1 )
		UBaseType_t		uxBasePriority;		/*任务基础优先级,优先级反转时用到*/
		UBaseType_t		uxMutexesHeld;		/*任务获取到的互斥信号量个数*/
	#endif

	#if ( configUSE_APPLICATION_TASK_TAG == 1 )
		TaskHookFunction_t pxTaskTag;
	#endif

	#if( configNUM_THREAD_LOCAL_STORAGE_POINTERS > 0 )	//与本地存储有关
		void *pvThreadLocalStoragePointers[ configNUM_THREAD_LOCAL_STORAGE_POINTERS ];
	#endif

	#if( configGENERATE_RUN_TIME_STATS == 1 )
		uint32_t		ulRunTimeCounter;	/*用来记录任务运行总时间*/
	#endif

	#if ( configUSE_NEWLIB_REENTRANT == 1 )
		struct	_reent xNewLib_reent;		/*定义一个newlib结构体变量*/
	#endif

	#if( configUSE_TASK_NOTIFICATIONS == 1 )	/*任务通知相关变量*/
		volatile uint32_t ulNotifiedValue;		/*任务通知值*/
		volatile uint8_t ucNotifyState;			/*任务通知状态*/
	#endif

	/* 用来标记任务是动态创建还是静态创建*/
	#if( tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0 )
		uint8_t	ucStaticallyAllocated; 		/*静态创建此变量为pdTURE;动态创建此变量为pdFALSE*/
	#endif

	#if( INCLUDE_xTaskAbortDelay == 1 )
		uint8_t ucDelayAborted;
	#endif

} tskTCB;

/*新版本的任务控制块重命名为TCB_t,本质上还是tskTCB,主要考虑旧版本的兼容*/
typedef tskTCB TCB_t;

2.5 任务堆栈

FreeRTOS 之所以能正确的恢复一个任务的运行就是因为有任务堆栈在保驾护航,任务调度器在进行任务切换的时候会将当前任务的现场(CPU 寄存器值等)保存在此任务的任务堆栈中,等到此任务下次运行的时候就会先用堆栈中保存的值来恢复现场,恢复现场以后任务就会接着从上次中断的地方开始运行

创建任务的时候需要给任务指定堆栈,若使用 xTaskCreate() 动态创建任务,任务堆栈会由函数 xTaskCreate() 自动创建;若使用 xTaskCreateStatic() 静态创建任务,就需要自行定义任务堆栈,将堆栈首地址作为函数的参数 puxStackBuffer 传递给函数,如下:

1
2
3
4
5
6
7
TaskHandle_t xTaskCreateStatic(	TaskFunction_t pxTaskCode,
								const char * const pcName,
								const uint32_t ulStackDepth,
								void * const pvParameters,
								UBaseType_t uxPriority,
								StackType_t * const puxStackBuffer,		//需要自定义,并将堆栈首地址传递给此参数
								StaticTask_t * const pxTaskBuffer )

不论是使用动态还是静态方法创建任务,创建任务都需要指定任务堆栈大小。任务堆栈的数据类型为 StackType_t,其本质是 uint32_t,在 portmacro.h 文件中由定义,如下:

1
2
3
#define portSTACK_TYPE	uint32_t
....
typedef portSTACK_TYPE StackType_t;

可见 StackType_t 类型的变量为 4 个字节,因此任务的实际堆栈大小就是我们所定义的 4 倍

以上转载自博主“安迪西嵌入式”的FreeRTOS专栏,仅作学习记录,如有侵权,请联系删除。

本文由作者按照 CC BY 4.0 进行授权