FreeRTOS任务(深入到源码进行分析)

FreeRTOS出现的意义:

疑问:我们之前在学stm32入门课程的时候,也没听说过什么FreeRTOS啊,不也照样可以写代码,然后开发出我们的程序,做出我们的项目,实现项目的功能啊?要想解决这个疑问,我们先来了解一下单片机程序设计模式。

裸机程序设计模式

        裸机程序的设计模式可以分为:轮询、前后台、定时器驱动、基于状态机。前面三种方法都无法解决一个问题::假设有 A、B 两个都很耗时的函数,无法降低它们相互之间的影响。第四种方法可以解决这个问题,但是实践起来有难度。

        假设一位职场妈妈需要同时解决 2 个问题:给小孩喂饭、回复工作信息,场景如图所示:

 

我们会开始演示各类模式下如何写程序。

轮询模式

示例代码如下:

        在 main 函数中是一个 while 循环,里面依次调用 2 个函数,这两个函数相互之间有影响,如果说喂一口饭太花时间,就会导致迟迟无法回一个信息;如果回一个信息太花时间,就会导致迟迟无法喂下一口饭。

        使用轮询模式编写程序看起来很简单,但是要求 while 循环里调用到的函数要执行得非常快,在复杂的场景反而增加了编程难度。

前后台

        所谓“前后台”就是使用中断程序。假设收到同事发来的信息时,电脑会发出“滴”的一声,这时候妈妈才需要去回复信息。示例程序如下:
 

--main 函数里 while 循环里的代码是后台程序,平时都是 while 循环在运行;

--当同事发来信息,电脑发出“滴”的一声,触发了中断。妈妈暂停喂饭,去执行“滴_ 中断”,给同事回复信息。

        在这个场景里,给同事回复信息非常及时:即使正在喂饭也会暂停下来去回复信息。“喂一口饭” 无法影响到“回一个信息”。但是,如果“回一个信息”太花时间,就会导致“喂一口饭”迟迟无法执行。

        继续改进,假设小孩吞下饭菜后会发出“啊”的一声,妈妈听到后才会喂下一口饭。喂饭、回复信息都是使用中断函数来处理。示例程序如下:

        main 函数中的 while 循环是空的,程序的运行靠中断来驱使。如果电脑声音“滴”、小孩声音“啊”不会同时、相近发出,那么“回一个信息”、“喂一口饭”相互之间没有影响。在不能满足这个前提的情况下,比如“滴”、“啊”同时响起,先“回一个信息”时就会耽误“喂一口饭”,这种场景下程序遭遇到了轮询模式的缺点:函数相互之间有影响。

  定时器驱动

定时器驱动模式,是前后台模式的一种,可以按照不用的频率执行各种函数。比如需要每 2 分钟给小孩喂一口饭,需要每 5 分钟给同事回复信息。那么就可以启动一个定时器,让它每 1 分钟产生一次中断,让中断函数在合适的时间调用对应函数。示例代码如下:

 这种模式适合调用周期性的函数,并且每一个函数执行的时间不能超过一个定时器周期。如果“喂一口饭”很花时间,比如长达 10 分钟,那么就会耽误“回一个信息”;反过来也是一样的,如果“回一个信息”很花时间也会影响到“喂一口饭”;这种场景下程序遭遇到了轮询模式的缺点:函数相互之间有影响。

基于状态机

当“喂一口饭”、“回一个信息”都需要花很长的时间,无论使用前面的哪种设计模式,都会退化轮询模式的缺点:函数相互之间有影响。可以使用状态机来解决这个缺点,示例代码如下:

-- 在 main 函数里,还是使用轮询模式依次调用 2 个函数。

--关键在于这 2 个函数的内部实现:使用状态机,每次只执行一个状态的代码,减少每次执行的时间,代码如下:
以“喂一口饭”为例,函数内部拆分为 4 个状态:舀饭、喂饭、舀菜、喂菜。每次执行“喂一口饭”函数时,都只会执行其中的某一状态对应的代码。以前执行一次“喂一口饭” 函数可能需要 4 秒钟,现在可能只需要 1 秒钟,就降低了对后面“回一个信息”的影响。同样的,“回一个信息”函数内部也被拆分为 3 个状态:查看信息、打字、发送。每次 执行这个函数时,都只是执行其中一小部分代码,降低了对“喂一口饭”的影响。使用状态机模式,可以解决裸机程序的难题:假设有 A、B 两个都很耗时的函数,怎样降低它们相互之间的影响。但是很多场景里,函数 A、B 并不容易拆分为多个状态,并且这些状态执行的时间并不好控制。所以这并不是最优的解决方法,需要使用多任务系统。

多任务系统

多任务模式

对于裸机程序,无论使用哪种模式进行精心的设计,在最差的情况下都无法解决这个问腿:假如有A、B两个都很耗时的函数,无法降低它们相互之间的影响。使用状态机模式时,如果函数拆分的不好,也会导致这个问题。本质原因是:函数是轮流执行的。假设“喂一口饭”需要 t1~t5 这 5 段时间,“回一个信息需要”ta~te 这 5 段时间,轮流执行时:先执行完 t1~t5,再执行 ta~te,如下图所示:

 对于职场妈妈,她怎么解决这个问题呢?她是一个眼明手快的人,可以一心多用,她可以这样做:

--  左手拿勺子,给小孩喂饭
--  右手敲键盘,回复同事
-- 两不耽误,小孩“以为”妈妈在专心喂饭,同事“以为”她在专心聊天
--  但是脑子只有一个啊,虽然说“一心多用”,但是谁能同时思考两件事?
--  只是她反应快,上一秒钟在考虑夹哪个菜给小孩,下一秒钟考虑给同事回复什么信息
--  本质是:交叉执行,t1~t5 和 ta~te 交叉执行,如下图所示:

 基于多任务系统编写程序时,示例代码如下:

第 21、22 行,创建 2 个任务;
第 25 行,启动调度器
之后,这 2 个任务就会交叉执行了;
基于多任务系统编写程序时,反而更简单了:
1) 上面第 2~8 行是“喂饭任务”的代码;
2) 第 10~16 行是“回信息任务”的代码,编写它们时甚至都不需要考虑它和其他函数的相互影响。这是因为每个任务都有不同的堆栈,就好像有 2 个单板:一个只运行“喂饭任务”这个函数、另一个只运行“回信息任务”这个函数。
多任务系统会依次给这些任务分配时间:你执行一会,我执行一会,如此循环。只要切换的间隔足够短,用户会“感觉这些任务在同时运行”。如下图所示:

这就是核心的时间片轮转。其实就是通过系统时钟(tick中断),不断的去切换任务,我们可以去改变tick中断触发的事件,来改变任务切换的频率。

当然,FreeRTOS可不只是这么简单,它还可以处理复杂的情况,不单单只是我前面提到的所谓的“轮流执行”,还会存在高优先级的任务优先执行任务唤醒任务暂停优先级反转等一系列功能,这就是FreeRTOS学习的核心点了,后续会逐步带大家去了解。

基本概念

        使用FreeRTOS时,我们可以在application中创建多个任务(task),也有其他的说法,把任务叫做线程(thread)。
        这里用韦东山老师最喜欢的喂饭和回信息例子来说明。 以日常生活为例,比如这个母亲要同时做两件事:喂饭:这是一个任务,回信息:这是另一个任务。这可以引入很多概念:

任务状态

当前正在喂饭,它是 running 状态;另一个 " 回信息 " 的任务就是 not running状态。
--"not running"状态还可以细分:
        --ready:就绪,随时可以运行
        --blocked:阻塞,卡住了,母亲在等待同事回信息
        --suspended:挂起,同事废话太多,不管他了

优先级

--我工作生活兼顾:喂饭、回信息优先级一样,轮流做(优先级一样)

--我忙里偷闲:还有空闲任务,休息一下(空闲任务优先级最低)

--厨房着火了,什么都别说了,先灭火:优先级更高(高优先级抢占低优先级)

(Stack)

--喂小孩时,我要记得上一口喂了米饭,这口要喂青菜了

--回信息时,我要记得刚才聊的是啥

--做不同的任务,这些细节不一样

--对于人来说,当然是记在脑子里

--对于人来说,当然是记在脑子里

--每个任务有自己的栈

事件驱动 

--孩子吃饭太慢:先休息一会,等他咽下去了、等他提醒我了,再喂下一口

协助式调度(Co-operative Scheduling)

--你在给同事回信息

        --同事说:好了,你先去给小孩喂一口饭吧,你才能离开

        --同事不放你走,即使孩子哭了你也不能走

--你在给孩子喂饭

        --孩子说:好了,妈妈你去处理一下工作吧,你才能离开

        --孩子不放你走,即使同事连发信息你也不能走

这涉及很多概念,后续会详细分析。

任务创建与删除

什么是任务

在 FreeRTOS 中,任务就是一个函数,原型如下:
void ATaskFunction(void *pvParameters);

要注意的是:
-- 这个函数不能返回(这个函数不能结束,要一直死循环)
--同一个函数,可以用来创建多个任务;换句话说,多个任务可以运行同一个函数(不同栈)
--函数内部,尽量使用局部变量:
      --每个任务都有自己的栈
      --每个任务运行这个函数时
      --任务 A 的局部变量放在任务 A 的栈里、任务 B 的局部变量放在任务 B 的栈里
      --不同任务的局部变量,有自己的副本
--函数使用全局变量、静态变量的话
      --只有一个副本:多个任务使用的是同一个副本
      --要防止冲突(后续会讲)
下面是一个示例:
void ATaskFunction( void *pvParameters )
{
/* 对于不同的任务,局部变量放在任务的栈里,有各自的副本 */
int32_t lVariableExample = 0;
 /* 任务函数通常实现为一个无限循环 */
for( ;; )
{
/* 任务的代码 */
}
 /* 如果程序从循环中退出,一定要使用vTaskDelete删除自己
 * NULL表示删除的是自己
 */
vTaskDelete( NULL );
 
 /* 程序不会执行到这里, 如果执行到这里就出错了 */
}

创建任务

创建任务时可以使用 2 个函数:动态分配内存、静态分配内存。

动态分配

使用动态分配内存的函数如下:
BaseType_t xTaskCreate( 
TaskFunction_t pxTaskCode, // 函数指针, 任务函数
 const char * const pcName, // 任务的名字
 const configSTACK_DEPTH_TYPE usStackDepth, // 栈大小,单位为word,10表示40字节
 void * const pvParameters, // 调用任务函数时传入的参数
 UBaseType_t uxPriority, // 优先级
 TaskHandle_t * const pxCreatedTask ); // 任务句柄, 以后使用它来操作这个任务
}

参数说明:

静态分配

使用静态分配内存的函数如下:
TaskHandle_t xTaskCreateStatic ( 
 TaskFunction_t pxTaskCode, // 函数指针, 任务函数
 const char * const pcName, // 任务的名字
 const uint32_t ulStackDepth, // 栈大小,单位为word,10表示40字节
 void * const pvParameters, // 调用任务函数时传入的参数
 UBaseType_t uxPriority, // 优先级
 StackType_t * const puxStackBuffer, // 静态分配的栈,就是一个buffer
 StaticTask_t * const pxTaskBuffer // 静态分配的任务结构体的指针,用它来操作这个任务
);
相比于使用动态分配内存创建任务的函数,最后2个参数不一样:

示例 1: 创建任务
使用动态、静态分配内存的方式,分别创建多个任务:分别在逻辑分析仪上打印不同的数字。

1、先创建三个任务函数,可以把任务理解为运行中的函数
void Task1Function(void  * argument);
void Task2Function(void  * argument);
void Task3Function(void  * argument);

这里有个问题,需要把osDelay(1)换为Hal_Delay(1)。 

2、创建任务的句柄,后续可以通过句柄来找到对应的任务进行对应操作

3、创建任务函数的返回值,通过返回值可以知道是否创建成功

static BaseType_t Task1_ret;
static BaseType_t Task2_ret;
static BaseType_t Task3_ret;

4、调用创建函数

参数1:任务函数。

参数2:任务的名字(实际上没用,负责调试)。

参数3:给任务分配栈空间的大小。

参数4:传人任务函数的参数(这里不需要,传入NULL)。

参数5:指向该任务的句柄,后续可用这个对对应任务进行操作。

返回值:告诉你任务是否创建成功(一般不需要理)。

5、编译、烧录、查看现象

这样,就成功创建出了任务,静态创建和动态创建的区别就是,动态创建的栈是由内核自动分配,而静态创建的栈是由程序手动分配,我们只需要自定义一个数组作为任务的栈,把数组的地址传入静态创建任务里面,其他参数不变,这样,也就创建完成一个新的静态任务。

 删除任务

删除任务时使用的函数如下:
void vTaskDelete( TaskHandle_t xTaskToDelete );

 怎么删除任务?

--自杀:vTaskDelete(NULL)。

--被杀:别的任务执行 vTaskDelete(pvTaskCode)pvTaskCode 是自己的句柄。

--杀人:执行 vTaskDelete(pvTaskCode)pvTaskCode 是别的任务的句柄。

前面创建任务时候的任务句柄这里就派上用场了。

任务创建的内部机制

ARM架构

在单片机中,CPU(中央处理器)是负责执行指令和控制数据流的主要组件。它负责从存储器中读取指令并执行这些指令,以及处理输入/输出操作。

ROM(只读存储器)用于存储固定的程序或数据,其中的内容在生产时被固化,用户无法对其进行修改。通常用于存储引导程序、固件或其他需要保持不变的数据。

Flash存储器通常用作可编程存储器,可以被用户编程和擦除。它通常用于存储程序代码、配置数据和其他需要在运行时进行修改的内容。

外设是指与CPU相连但不是直接用于处理数据的设备,例如输入/输出设备、通信设备、定时器、中断控制器等。外设通过与CPU进行交互来完成特定的任务,扩展了单片机的功能和应用范围。 

堆栈

定义

要先理解任务的内部机制,我们首先要去知道堆栈。

注意:我们经常 " 堆栈 " 混合着说,其实它们不是同一个东西:
--  堆, heap ,就是一块空闲的内存,需要提供管理函数
        --malloc:从堆里划出一块空间给程序使用
        --free:用完后,再把它标记为 " 空闲 " 的,可以再次使用
-- 栈, stack ,函数调用时局部变量保存在栈中,当前程序的环境也是保存在栈中
        --可以从堆中分配一块空间用作栈
        --为了好叫,我们直接把栈叫为堆栈

 在单片机程序中的作用

在FreeRTOS中,堆栈(stack)是用来存储任务执行时的临时数据和函数调用信息的内存区域。堆栈在任务执行过程中起着非常重要的作用,其细节如下:

  1. 存储局部变量和函数参数:当一个函数被调用时,函数的参数、局部变量以及函数调用所需的临时数据都会存储在堆栈中。这些数据被称为栈帧(stack frame),每次函数调用时都会分配一个新的栈帧。

  2. 保存函数调用信息:在函数调用过程中,当前函数的执行状态(比如局部变量值、返回地址等)会被压入堆栈中,以便在函数返回时能够正确地恢复到调用该函数之前的状态。

  3. 支持函数的递归调用:通过堆栈,程序能够支持函数的递归调用。每次递归调用都会分配一个新的栈帧,使得函数调用能够按照正确的顺序执行。

  4. 上下文切换:在多任务系统中,堆栈还用于保存任务的上下文信息,包括任务的寄存器值、程序计数器等。当任务切换时,当前任务的堆栈信息会被保存,而下一个任务的堆栈信息会被加载,从而实现任务之间的切换。

总的来说,堆栈在FreeRTOS中扮演着至关重要的角色,它不仅用于支持函数调用和递归,还用于保存任务的执行状态,实现任务之间的切换,确保多任务系统能够正确、高效地运行,所以每个任务都会有一个自己独立的栈。

堆栈大小的估算

要粗略估算 FreeRTOS 任务的栈大小,可以考虑以下几个方面:

1. 函数调用深度:估计任务执行过程中函数调用的最大嵌套深度。每次函数调用都会占用一定的栈空间。

2. 局部变量和临时变量:估计任务中使用的局部变量和临时变量所需的栈空间。

3. 中断处理:如果任务会与中断处理共享栈空间,还需要考虑中断处理过程中所需的栈空间。这个我们不需要考虑。

4. 递归调用:如果任务中存在递归调用,需要预留足够的栈空间来支持递归深度。

通常,可以通过观察任务中函数调用的嵌套深度、局部变量的内存占用以及可能的递归调用情况来进行估算。在实际使用中,可以先分配一个较大的栈空间,然后通过监控栈使用情况来进行调整和优化。

这里我教大家一个粗略的方法:

1、cpu中有不少寄存器,他们负责程序运算,在切换任务的时候,我们会直接全部把他们压入栈中,在ARM架构的处理器中,通用寄存器(General Purpose Registers)的编号通常是从R0到R15。其中:

  • R0 到 R12 是通用寄存器,用于存储数据和进行计算。
  • R13 是栈指针寄存器(Stack Pointer Register),用于指向栈顶。
  • R14 是链接寄存器(Link Register),用于存储调用子程序时的返回地址。
  • R15 是程序计数器(Program Counter),用于存储当前执行指令的地址。

 这样一算,就有64个字节了,因为一个寄存器占据四个字节。

2、把任务的局部变量占据所有字节大小加起来,不同类型的变量大小不一样。

3、任务中可能会有函数的嵌套,找到你认为函数中嵌套最深的,假如调用了函数a,函数a又调用了函数b,函数b又调用了函数c,你需要把函数a、函数b、函数c占据栈空间的大小考虑进去。

4、把他们全部加在一起,就可以大概算出任务所需栈的大小,当然,这个方法不准确,所以,FreeRTOS提供了另一种方法给我们精确的算出栈空间的大小,等大小深入的学习之后,我会告诉大家。

源码分析

这里需要读者有链表基础,可以去看完写的一篇链表介绍链表基础

xTaskCreat();

BaseType_t xTaskCreate(	TaskFunction_t pxTaskCode,
							const char * const pcName,		/*lint !e971 Unqualified char types are allowed for strings and single characters only. */
							const configSTACK_DEPTH_TYPE usStackDepth,
							void * const pvParameters,
							UBaseType_t uxPriority,
							TaskHandle_t * const pxCreatedTask )
	{
	TCB_t *pxNewTCB;
	BaseType_t xReturn;

		/* If the stack grows down then allocate the stack then the TCB so the stack
		does not grow into the TCB.  Likewise if the stack grows up then allocate
		the TCB then the stack. */
		#if( portSTACK_GROWTH > 0 )
		{
			/* Allocate space for the TCB.  Where the memory comes from depends on
			the implementation of the port malloc function and whether or not static
			allocation is being used. */
			pxNewTCB = ( TCB_t * ) pvPortMalloc( sizeof( TCB_t ) );

			if( pxNewTCB != NULL )
			{
				/* Allocate space for the stack used by the task being created.
				The base of the stack memory stored in the TCB so the task can
				be deleted later if required. */
				pxNewTCB->pxStack = ( StackType_t * ) pvPortMalloc( ( ( ( size_t ) usStackDepth ) * sizeof( StackType_t ) ) ); /*lint !e961 MISRA exception as the casts are only redundant for some ports. */

				if( pxNewTCB->pxStack == NULL )
				{
					/* Could not allocate the stack.  Delete the allocated TCB. */
					vPortFree( pxNewTCB );
					pxNewTCB = NULL;
				}
			}
		}
		#else /* portSTACK_GROWTH */
		{
		StackType_t *pxStack;

			/* Allocate space for the stack used by the task being created. */
			pxStack = ( StackType_t * ) pvPortMalloc( ( ( ( size_t ) usStackDepth ) * sizeof( StackType_t ) ) ); /*lint !e961 MISRA exception as the casts are only redundant for some ports. */

			if( pxStack != NULL )
			{
				/* Allocate space for the TCB. */
				pxNewTCB = ( TCB_t * ) pvPortMalloc( sizeof( TCB_t ) ); /*lint !e961 MISRA exception as the casts are only redundant for some paths. */

				if( pxNewTCB != NULL )
				{
					/* Store the stack location in the TCB. */
					pxNewTCB->pxStack = pxStack;
				}
				else
				{
					/* The stack cannot be used as the TCB was not created.  Free
					it again. */
					vPortFree( pxStack );
				}
			}
			else
			{
				pxNewTCB = NULL;
			}
		}
		#endif /* portSTACK_GROWTH */

		if( pxNewTCB != NULL )
		{
			#if( tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0 ) /*lint !e731 Macro has been consolidated for readability reasons. */
			{
				/* Tasks can be created statically or dynamically, so note this
				task was created dynamically in case it is later deleted. */
				pxNewTCB->ucStaticallyAllocated = tskDYNAMICALLY_ALLOCATED_STACK_AND_TCB;
			}
			#endif /* configSUPPORT_STATIC_ALLOCATION */

			prvInitialiseNewTask( pxTaskCode, pcName, ( uint32_t ) usStackDepth, pvParameters, uxPriority, pxCreatedTask, pxNewTCB, NULL );
			prvAddNewTaskToReadyList( pxNewTCB );
			xReturn = pdPASS;
		}
		else
		{
			xReturn = errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY;
		}

		return xReturn;
	}

分析:

在这段代码中,还涉及了一些条件编译的逻辑,例如对静态和动态分配的支持以及一些MISRA规范的例外处理,这些不是我们的重点,于是我把他简化(只是把主要思路简化,实际代码不能换)。

BaseType_t xTaskCreate(TaskFunction_t pxTaskCode,
                       const char * const pcName,
                       const configSTACK_DEPTH_TYPE usStackDepth,
                       void * const pvParameters,
                       UBaseType_t uxPriority,
                       TaskHandle_t * const pxCreatedTask)
{
    TCB_t *pxNewTCB;
    BaseType_t xReturn;

    /* 创建一个新的任务控制块 */
    pxNewTCB = prvAllocateTCBAndStack(usStackDepth);

    if (pxNewTCB != NULL)
    {
        /* 初始化任务控制块的成员 */
        prvInitialiseNewTask(pxTaskCode, pcName, usStackDepth, pvParameters, pxNewTCB);

        /* 将任务添加到就绪队列中 */
        xReturn = xTaskGenericCreate(pxNewTCB, pxCreatedTask);

        if (xReturn == pdPASS)
        {
            /* 如果创建成功,则将任务添加到就绪列表中 */
            vTaskPlaceOnEventList(pxNewTCB);
        }
        else
        {
            /* 如果创建失败,则释放任务控制块和堆栈 */
            vPortFree(pxNewTCB->pxStack);
            vPortFree(pxNewTCB);
        }
    }
    else
    {
        /* 如果无法分配任务控制块和堆栈,则返回错误 */
        xReturn = errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY;
    }

    return xReturn;
}

上述代码是简化的示例,仅包含了主要的步骤。下面是对上述代码的解释:

  1. 首先,prvAllocateTCBAndStack 函数分配一个新的任务控制块(TCB)和堆栈空间,并返回指向 TCB 的指针。
  2. 如果成功分配了 TCB 和堆栈空间,则调用 prvInitialiseNewTask 函数初始化任务控制块的成员,包括任务函数、名称、堆栈等。
  3. 接下来,调用 xTaskGenericCreate 函数将任务添加到就绪队列中,并返回创建任务是否成功的状态。
  4. 如果成功创建任务,则调用 vTaskPlaceOnEventList 函数将任务添加到就绪列表中,使其可以开始运行。
  5. 如果任务创建失败,则释放之前分配的任务控制块和堆栈空间。
  6. 最后,根据创建任务的结果返回相应的状态。

这只是一个简单的示例,实际的实现可能因具体的 FreeRTOS 版本和目标平台而有所不同。但是,基本的思想是相似的:分配任务控制块和堆栈,初始化任务控制块的成员,然后将任务添加到就绪队列中。

内存的分配和释放

   pvPortMalloc();vPortFree();

可以看到,在任务创建的时候,多次调用了 pvPortMalloc(); 这个函数其实就是分配一块内存给任务,我们在这里埋下一个伏笔,我们现在只需要知道,调用完这个函数之后,单片机会为我们任务分配好一个合适的堆栈,在之后的深入学习,我们再去对他进行讲解。

任务控制块(TCB)

在FreeRTOS中,TCB是任务控制块(Task Control Block)的缩写,它是用于管理任务的数据结构,具有以下几个重要的作用:

1. **任务状态的维护**:TCB中包含了任务的状态信息,比如就绪、阻塞、延时等状态。通过TCB,FreeRTOS能够准确地跟踪每个任务的当前状态,并根据状态进行任务调度和管理。

2. **任务上下文的保存**:TCB中存储了任务的上下文信息,包括任务的寄存器值、堆栈指针、堆栈大小等。这些信息能够帮助系统在任务切换时保存和恢复任务的执行环境,实现任务的切换和并发执行。

3. **优先级的管理**:TCB中存储了任务的优先级信息,这使得系统能够根据任务的优先级来进行任务调度,确保高优先级任务能够及时得到执行而不会被低优先级任务长时间占用处理器。

4. **任务列表的维护**:TCB通过指针等方式连接成任务链表,从而支持对任务的快速查找、遍历和管理。

5. **任务延时和超时的管理**:TCB中存储了任务的延时信息,这使得系统能够追踪任务的延时状态并在适当的时候唤醒任务。

总的来说,TCB在FreeRTOS中扮演着非常重要的角色,它是用于管理任务的关键数据结构,包括任务的状态、上下文、优先级等信息,通过TCB,FreeRTOS能够有效地管理和调度多个任务,支持多任务的并发执行。

源码:

typedef struct tskTaskControlBlock
{
	volatile StackType_t	*pxTopOfStack;	/*< Points to the location of the last item placed on the tasks stack.  THIS MUST BE THE FIRST MEMBER OF THE TCB STRUCT. */

	#if ( portUSING_MPU_WRAPPERS == 1 )
		xMPU_SETTINGS	xMPUSettings;		/*< The MPU settings are defined as part of the port layer.  THIS MUST BE THE SECOND MEMBER OF THE TCB STRUCT. */
	#endif

	ListItem_t			xStateListItem;	/*< The list that the state list item of a task is reference from denotes the state of that task (Ready, Blocked, Suspended ). */
	ListItem_t			xEventListItem;		/*< Used to reference a task from an event list. */
	UBaseType_t			uxPriority;			/*< The priority of the task.  0 is the lowest priority. */
	StackType_t			*pxStack;			/*< Points to the start of the stack. */
	char				pcTaskName[ configMAX_TASK_NAME_LEN ];/*< Descriptive name given to the task when created.  Facilitates debugging only. */ /*lint !e971 Unqualified char types are allowed for strings and single characters only. */

	#if ( ( portSTACK_GROWTH > 0 ) || ( configRECORD_STACK_HIGH_ADDRESS == 1 ) )
		StackType_t		*pxEndOfStack;		/*< Points to the highest valid address for the stack. */
	#endif

	#if ( portCRITICAL_NESTING_IN_TCB == 1 )
		UBaseType_t		uxCriticalNesting;	/*< Holds the critical section nesting depth for ports that do not maintain their own count in the port layer. */
	#endif

	#if ( configUSE_TRACE_FACILITY == 1 )
		UBaseType_t		uxTCBNumber;		/*< Stores a number that increments each time a TCB is created.  It allows debuggers to determine when a task has been deleted and then recreated. */
		UBaseType_t		uxTaskNumber;		/*< Stores a number specifically for use by third party trace code. */
	#endif

	#if ( configUSE_MUTEXES == 1 )
		UBaseType_t		uxBasePriority;		/*< The priority last assigned to the task - used by the priority inheritance mechanism. */
		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;	/*< Stores the amount of time the task has spent in the Running state. */
	#endif

	#if ( configUSE_NEWLIB_REENTRANT == 1 )
		/* Allocate a Newlib reent structure that is specific to this task.
		Note Newlib support has been included by popular demand, but is not
		used by the FreeRTOS maintainers themselves.  FreeRTOS is not
		responsible for resulting newlib operation.  User must be familiar with
		newlib and must provide system-wide implementations of the necessary
		stubs. Be warned that (at the time of writing) the current newlib design
		implements a system-wide malloc() that must be provided with locks. */
		struct	_reent xNewLib_reent;
	#endif

	#if( configUSE_TASK_NOTIFICATIONS == 1 )
		volatile uint32_t ulNotifiedValue;
		volatile uint8_t ucNotifyState;
	#endif

	/* See the comments above the definition of
	tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE. */
	#if( tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0 ) /*lint !e731 Macro has been consolidated for readability reasons. */
		uint8_t	ucStaticallyAllocated; 		/*< Set to pdTRUE if the task is a statically allocated to ensure no attempt is made to free the memory. */
	#endif

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

} tskTCB;

分析:

先说几个重要的成员:

1. `pxTopOfStack`: 指向任务栈顶的指针,用于保存任务栈最后一个放置的数据项。

2. `xMPUSettings`: 如果使用MPU包装器,则包含MPU设置信息。

3. `xStateListItem`: 用于将任务的状态列表项链接到任务的状态列表中,表示任务的状态(就绪、阻塞、挂起)。

4. `xEventListItem`: 用于将任务链接到事件列表中。

5. `uxPriority`: 任务的优先级,值越小优先级越高。

6. `pxStack`: 指向任务堆栈的起始地址。

7. `pcTaskName`: 任务的描述性名称,用于调试目的。

8. `pxEndOfStack`: 指向堆栈的最高有效地址,用于确定堆栈的结束地址。

9. `uxCriticalNesting`: 用于保存任务的关键部分嵌套深度。

10. `uxTCBNumber` 和 `uxTaskNumber`: 用于存储任务的编号,用于跟踪任务的创建和删除。

11. `uxBasePriority`: 任务的基本优先级,用于优先级继承机制。

12. `uxMutexesHeld`: 持有的互斥量数量。

13. `pxTaskTag`: 任务钩子函数指针,用于任务标记。

14. `pvThreadLocalStoragePointers[]`: 线程本地存储指针数组。

15. `ulRunTimeCounter`: 存储任务在运行状态下花费的时间。

16. `xNewLib_reent`: 如果使用Newlib,并启用了reentrant模式,则分配一个特定于任务的Newlib reent结构。

17. `ulNotifiedValue` 和 `ucNotifyState`: 任务通知的值和状态。

18. `ucStaticallyAllocated`: 如果任务是静态分配的,则设置为pdTRUE。

19. `ucDelayAborted`: 如果任务延时被中止,则设置为1。

这些成员变量共同构成了任务控制块(TCB),用于管理任务的状态、优先级、堆栈、名称等信息。每个成员变量的具体作用在注释中有详细说明,有助于理解任务在FreeRTOS中的管理和调度过程。

就绪链表

FreeRTOS 中的就绪链表(Ready List)是用于存储所有处于就绪状态的任务的数据结构。就绪链表的作用主要有以下几个方面:

  1. 高效管理就绪任务:就绪链表用于高效地管理系统中处于就绪状态的任务。当一个任务变为就绪状态时,它会被添加到就绪链表中;当任务被阻塞或者进入延时状态时,它会从就绪链表中移除。

  2. 支持任务调度:任务调度器会根据就绪链表中任务的优先级顺序来选择下一个要执行的任务。通过维护就绪链表,任务调度器能够快速地找到优先级最高的就绪任务,并将其交给处理器来执行。

  3. 提高系统的响应速度:由于就绪链表中的任务已经准备好可以被立即执行,因此任务调度器能够快速地选择下一个要执行的任务,从而提高系统对外部事件的响应速度。

  4. 支持多任务并发执行:通过就绪链表,FreeRTOS 能够支持多任务的并发执行。多个任务在就绪链表中排队等待执行,任务调度器会按照任务的优先级来选择下一个要执行的任务,从而实现多任务的并发执行。

总的来说,就绪链表在 FreeRTOS 中扮演着非常重要的角色,它是任务调度器进行任务选择的依据,能够有效地管理处于就绪状态的任务,提高系统的响应速度,并支持多任务的并发执行。

补充:

我们在源码分析的时候,只提到了就绪链表,实际上,FreeRTOS中,有着许多许多的链表,每一个链表都有它自己的作用,一次举例出所有的链表,是不利于我们学习的,我会一步步带大家,遇到一个问题解决一个,实现应用和底层的全能王者。

  • 31
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
FreeRTOS 是一个开的实时操作系统 (RTOS),被广泛用于嵌入式系统开发。它提供了多任务管理、内存管理、任务通信和同步等功能,适用于多种处理器架构和开发板。 如果你想进行 FreeRTOS 分析,以下是一些常见的步骤和建议: 1. 下载码:你可以从 FreeRTOS 官方网站或 GitHub 上获取 FreeRTOS码。确保下载适合你的目标硬件平台和编译器的版本。 2. 阅读文档:FreeRTOS 官方网站提供了详细的文档和用户指南,包括任务管理、内存管理、同步机制等方面的说明。阅读文档可以帮助你更好地理解系统的设计和使用方法。 3. 理解核心结构:FreeRTOS 的核心结构包括任务控制块 (TCB)、调度器、时间片和内核对象等。深入理解这些结构的作用和相互关系,可以帮助你分析系统的运行原理。 4. 调试和跟踪:使用适合你的开发环境和硬件平台的调试工具,可以对 FreeRTOS 进行调试和跟踪。你可以设置断点、监视任务状态、查看任务堆栈和中断处理等信息,以便更好地理解系统的运行过程。 5. 逐步分析代码:从 FreeRTOS任务入口点开始,逐步分析代码的执行流程。重点关注任务的创建、调度、挂起和恢复等关键操作。通过阅读代码和调试,你可以更深入地了解 FreeRTOS 的实现细节。 6. 查阅社区资FreeRTOS 社区有许多活跃的开发者和用户,他们在论坛上分享了大量的问题解答、示例代码和优化技巧。查阅社区资可以帮助你更好地理解 FreeRTOS 的使用和调试技巧。 请注意,由于 FreeRTOS 是一个相对复杂的系统,分析可能需要一定的时间和经验。建议你先从简单的示例程序开始,逐步深入研究码。另外,参考官方文档和社区资也是非常有帮助的。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值