FreeRTOS - 任务管理

在学习FreeRTOS过程中,结合韦东山-FreeRTOS手册和视频、野火-FreeRTOS内核实现与应用开发、及网上查找的其他资源,整理了该篇文章。如有内容理解不正确之处,欢迎大家指出,共同进步。

参考:https://rtos.100ask.net/zh/FreeRTOS/DShanMCU-F103/chapter9.html#_9-5-%E7%A4%BA%E4%BE%8B5-%E4%BB%BB%E5%8A%A1%E6%9A%82%E5%81%9C

https://freertos.blog.****.net/article/details/50312443

1. 任务

对于整个单片机程序,我们称之为 application,应用程序。

使用FreeRTOS时,我们可以在application中创建多个任务(task),有些文档把任务也称为线程(thread)。

1.1 任务特性
  • 使用RTOS的实时应用程序可认为是一系列独立任务的集合。
  • 每个任务在自己的环境中运行,不依赖于系统中的其它任务或者RTOS调度器。
  • 在任何时刻,只有一个任务得到运行,RTOS调度器决定运行哪个任务。
  • 调度器会不断的启动、停止每一个任务,宏观看上去就像整个应用程序都在执行。
  • 作为任务,不需要对调度器的活动有所了解,在任务切入切出时保存上下文环境(寄存器值、堆栈内容)是调度器主要的职责。
  • 为了实现这点,每个任务都需要有自己的堆栈。
  • 当任务切出时,它的执行环境会被保存在该任务的堆栈中,这样当再次运行时,就能从堆栈中正确的恢复上次的运行环境。
1.2 任务状态

  • 运行:如果一个任务正在执行,那么说这个任务处于运行状态。此时它占用处理器。
  • 就绪:就绪的任务已经具备执行的能力(不同于阻塞和挂起),但是因为有一个同优先级或者更高优先级的任务处于运行状态而还没有真正执行。
  • 阻塞:如果任务当前正在等待某个时序或外部中断,我们就说这个任务处于阻塞状态。比如一个任务调用vTaskDelay()后会阻塞到延时周期到为止。任务也可能阻塞在队列或信号量事件上。进入阻塞状态的任务通常有一个“超时”周期,当事件超时后解除阻塞。
  • 挂起:处于挂起状态的任务同样对调度器无效。仅当明确的分别调用vTaskSuspend() 和xTaskResume() API函数后,任务才会进入或退出挂起状态。不可以指定超时周期事件(不可以通过设定超时事件而退出挂起状态)
  1. 阻塞状态

在阻塞状态的任务,它可以等待两种类型的事件:

  • 时间相关的事件
    • 可以等待一段时间:我等2分钟
    • 也可以一直等待,直到某个绝对时间:我等到下午3点
  • 同步事件:这事件由别的任务,或者是中断程序产生
    • 例子1:任务A等待任务B给它发送数据
    • 例子2:任务A等待用户按下按键
    • 同步事件的来源有很多(这些概念在后面会细讲):
      • 队列(queue)
      • 二进制信号量(binary semaphores)
      • 计数信号量(counting semaphores)
      • 互斥量(mutexes)
      • 递归互斥量、递归锁(recursive mutexes)
      • 事件组(event groups)
      • 任务通知(task notifications)

在等待一个同步事件时,可以加上超时时间。比如等待队里数据,超时时间设为10ms:

- 10ms之内有数据到来:成功返回
- 10ms到了,还是没有数据:超时返回
  1. 挂起状态

在日常生活的例子中,母亲正在电脑前跟同事沟通,母亲可以暂停:

  • 好烦啊,我暂停一会
  • 领导说:你暂停一下

FreeRTOS中的任务也可以进入暂停状态,唯一的方法是通过vTaskSuspend函数。函数原型如下:

void vTaskSuspend( TaskHandle_t xTaskToSuspend );

参数xTaskToSuspend表示要暂停的任务,如果为NULL,表示暂停自己。

要退出暂停状态,只能由别人来操作:

  • 别的任务调用:vTaskResume
  • 中断程序调用:xTaskResumeFromISR
1.3 空闲任务及其钩子函数
1.3.1 空闲任务

空闲任务(Idle任务)的作用之一:释放被删除的任务的内存。

除了上述目的之外,为什么必须要有空闲任务?一个良好的程序,它的任务都是事件驱动的:平时大部分时间处于阻塞状态。有可能我们自己创建的所有任务都无法执行,但是调度器必须能找到一个可以运行的任务:所以,我们要提供空闲任务。在使用vTaskStartScheduler()函数来创建、启动调度器时,这个函数内部会创建空闲任务。

#define tskIDLE_PRIORITY			( ( UBaseType_t ) 0U )

void vTaskStartScheduler( void )
{
    BaseType_t xReturn;
	/* The Idle task is being created using dynamically allocated RAM. */
	xReturn = xTaskCreate(	prvIdleTask,
							configIDLE_TASK_NAME,
							configMINIMAL_STACK_SIZE,
							( void * ) NULL,
							( tskIDLE_PRIORITY | portPRIVILEGE_BIT ),
							&xIdleTaskHandle ); /*lint !e961 MISRA exception, justified as it is not a redundant explicit cast to all supported compilers. */
}
  • 空闲任务优先级为0:它不能阻碍用户任务运行
  • 空闲任务要么处于就绪态,要么处于运行态,永远不会阻塞
  • 空闲任务的优先级为0,这意味着一旦某个用户的任务变为就绪态,那么空闲任务马上被切换出去,让这个用户任务运行。在这种情况下,我们说用户任务"抢占"(pre-empt)了空闲任务,这是由调度器实现的。
  • 要注意的是:如果使用vTaskDelete()来删除任务,那么你就要确保空闲任务有机会执行,否则就无法释放被删除任务的内存。

我们可以添加一个空闲任务的钩子函数(Idle Task Hook Functions),空闲任务的循环每执行一次,就会调用一次钩子函数。钩子函数的作用有这些:

  • 执行一些低优先级的、后台的、需要连续执行的函数
  • 测量系统的空闲时间:空闲任务能被执行就意味着所有的高优先级任务都停止了,所以测量空闲任务占据的时间,就可以算出处理器占用率。
void vApplicationIdleHook( void )
{
	int i;
    // 用于获取系统运行时间和任务运行时间的统计信息。
	vTaskGetRunTimeStats(pcWriteBuffer);
	for (i = 0; i < 16; i++)
		printf("-");
	printf("\n\r");
	printf("%s\n\r", pcWriteBuffer);
}
  • 让系统进入省电模式:空闲任务能被执行就意味着没有重要的事情要做,当然可以进入省电模式了。

空闲任务的钩子函数的限制:

  • 不能导致空闲任务进入阻塞状态、暂停状态
  • 如果你会使用vTaskDelete()来删除任务,那么钩子函数要非常高效地执行。如果空闲任务执行卡在钩子函数里的话,它就无法释放内存。
1.3.2 使用钩子函数的前提

在FreeRTOS\Source\tasks.c中,可以看到如下代码,所以前提就是:

  • 把这个宏定义为1:configUSE_IDLE_HOOK
  • 实现vApplicationIdleHook函数

2. 支持多优先级

2.1 任务优先级
  • 每个任务都要被指定一个优先级,取值范围:0~(configMAX_PRIORITIES - 1),configMAX_PRIORITIES定义在FreeRTOSConfig.h中。
  • FreeRTOS的调度器可以使用2种方法来快速找出优先级最高的、可以运行的任务。使用不同的方法时,configMAX_PRIORITIES 的取值有所不同。
    • 通用方法:
      • 使用C函数实现,对所有的架构都是同样的代码。对configMAX_PRIORITIES的取值没有限制。但是configMAX_PRIORITIES的取值还是尽量小,因为取值越大越浪费内存,也浪费时间。
      • configUSE_PORT_OPTIMISED_TASK_SELECTION被定义为0、或者未定义时,使用此方法。
    • 架构相关的优化的方法
      • 架构相关的汇编指令,可以从一个32位的数里快速地找出为1的最高位。使用这些指令,可以快速找出优先级最高的、可以运行的任务。
      • 使用这种方法时,configMAX_PRIORITIES的取值不能超过32。
      • configUSE_PORT_OPTIMISED_TASK_SELECTION被定义为1时,使用此方法。
  • 低优先级数值代表低优先级。
  • 空闲任务(idle task)的优先级为0(tskIDLE_PRIORITY)。
  • FreeRTOS调度器确保处于最高优先级的就绪或运行态任务获取处理器,换句话说,处于就绪状态的任务,只有其中的最高优先级任务才会运行。
  • 任何数量的任务可以共享同一个优先级。如果宏configUSE_TIME_SLICING定义为1,处于就绪态的多个相同优先级任务将会以时间片切换的方式共享处理器。
2.2 查找最高优先级的就绪任务

就绪列表 pxReadyTasksLists[ configMAX_PRIORITIES ]是一个数组,数组里面存的是就绪任务的 TCB(准确来说是 TCB 里面的 xStateListItem 节点),数组的下标对应任务的优先级,优先级越低对应的数组下标越小。

空闲任务的优先级最低,对应的是下标为 0 的链表。

任务在创建的时候,会根据任务的优先级将任务插入到就绪列表不同的位置。相同优先级的任务插入到就绪列表里面的同一条链表中。

pxCurrentTCB 是一个全局的 TCB 指针,用于指向优先级最高的就绪任务的 TCB,即当前正在运行的 TCB 。那么我们要想让任务支持优先级,即在任务切换(taskYIELD)的时候,让pxCurrenTCB 指向**最高优先级的就绪任务的 TCB **就可以。

查找最高优先级的就绪任务有两种方法,具体由configUSE_PORT_OPTIMISED_TASK_SELECTION</font> 这个宏控制,定义为 0 选择通用方法,定义为 1 选择根据处理器优化的方法,该宏默认在 portmacro.h 中定义为 1,即使用优化过的方法。

2.3 通用方法
/* 空闲任务优先级宏定义,在 task.h 中定义 */
#define tskIDLE_PRIORITY ( ( UBaseType_t ) 0U )

/* 定义 uxTopReadyPriority,在 task.c 中定义 */
static volatile UBaseType_t uxTopReadyPriority = tskIDLE_PRIORITY;
2.3.1 taskRECORD_READY_PRIORITY()

taskRECORD_READY_PRIORITY()用于更新 uxTopReadyPriority 的值。

uxTopReadyPriority 用于表示创建的任务的最高优先级,默认初始化为 0,即空闲任务的优先级。

2.3.2 taskSELECT_HIGHEST_PRIORITY_TASK()

taskSELECT_HIGHEST_PRIORITY_TASK()用于寻找优先级最高的就绪任务,实质就是更新 uxTopReadyPriority 和 pxCurrentTCB 的值。

在创建时已通过taskRECORD_READY_PRIORITY记录当前就绪任务的最高优先级uxTopReadyPriority,在这里再次更新uxTopReadyPriority,是因为:最高优先级的任务可能会被挂起或阻塞,那就会从其就绪链表中移除,若最高优先级的就绪链表中再没有其他就绪任务,则该优先级的就绪链表就为空。所以需要从高优先级到低优先级遍历就绪链表,找到第一个非空的就绪链表。

#if ( configUSE_PORT_OPTIMISED_TASK_SELECTION == 0 )
    #define taskRECORD_READY_PRIORITY( uxPriority )	\
	{	\
        /* uxTopReadyPriority 存的是就绪任务的最高优先级 */\
		if( ( uxPriority ) > uxTopReadyPriority )\
		{										\
			uxTopReadyPriority = ( uxPriority );\
		}										\
	} /* taskRECORD_READY_PRIORITY */


    #define taskSELECT_HIGHEST_PRIORITY_TASK()      \
	{										        \
    	UBaseType_t uxTopPriority = uxTopReadyPriority;	\
													\
		/* 寻找包含就绪任务的最高优先级的队列 */\
		while( listLIST_IS_EMPTY( &( pxReadyTasksLists[ uxTopPriority ] ) ) )\
		{																	\
			--uxTopPriority;				\
		}									\
		/* 获取优先级最高的就绪任务的 TCB,然后更新到 pxCurrentTCB */	\
		listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) );\
		/*更新 uxTopReadyPriority */\
        uxTopReadyPriority = uxTopPriority;	\
	} /* taskSELECT_HIGHEST_PRIORITY_TASK */
2.4 优化方法

这得益于 Cortex-M 内核有一个计算前导零的指令CLZ,所谓前导零就是计算一个变量(Cortex-M 内核单片机的变量为 32位)从高位开始第一次出现 1 的位的前面的零的个数。比如:一个 32 位的变量uxTopReadyPriority,其位 0、位 24 和 位 25 均 置 1 , 其 余 位 为 0 , 具 体 见 。 那 么 使 用 前 导 零 指 令 __CLZ (uxTopReadyPriority)可以很快的计算出 uxTopReadyPriority 的前导零的个数为 6。

如果 uxTopReadyPriority 的每个位号对应的是任务的优先级,任务就绪时,则将对应的位置 1,反之则清零。那么图 10-2 就表示优先级 0、优先级 24 和优先级 25 这三个任务就绪,其中优先级为 25的任务优先级最高。利用前导零计算指令可以很快计算出就绪任务中的最高优先级为:

( 31UL - ( uint32_t ) __clz( ( uxReadyPriorities ) ) ) = ( 31UL - ( uint32_t ) 6 ) = 25。

#define portGET_HIGHEST_PRIORITY( uxTopPriority, uxReadyPriorities ) \
        uxTopPriority = ( 31UL - ( uint32_t ) __clz( ( uxReadyPriorities ) ) )

#define portRECORD_READY_PRIORITY( uxPriority, uxReadyPriorities ) \
             ( uxReadyPriorities ) |= ( 1UL << ( uxPriority ) )

#define portRESET_READY_PRIORITY( uxPriority, uxReadyPriorities ) \
             ( uxReadyPriorities ) &= ~( 1UL << ( uxPriority ) )   
2.4.1 taskRECORD_READY_PRIORITY()

根据传进来的形参(通常形参就是任务的优先级)将变量 uxTopReadyPriority 的某个位置 1。

uxTopReadyPriority:它在优化方法中担任的是一个优先级位图表的角色,即该变量的每个位对应任务的优先级,如果任务就绪,则将对应的位置 1,反之清零。根据这个原理,只需要计算出 uxTopReadyPriority 的前导零个数就算找到了就绪任务的最高优先级。与taskRECORD_READY_PRIORITY() 作 用 相 反 的 是 taskRESET_READY_PRIORITY() 。

    #define taskRECORD_READY_PRIORITY( uxPriority )\ 
            portRECORD_READY_PRIORITY( uxPriority, uxTopReadyPriority )
2.4.2 taskRESET_READY_PRIORITY()

用于根据传进来的形参(通常形参就是任务的优先级)将变量 uxTopReadyPriority的某个位清零。

根据优先级调用 taskRESET_READY_PRIORITY()函数复位 uxTopReadyPriority 变量中对应的位时,要先确保就绪列表中对应该优先级下的链表没有任务才行, 如果 有则不清零。

假设当前实验中,任务 1 会调用 vTaskDelay(),会将自己挂起,只能是将任务 1 从就绪列表删除,不能将任务 1 在优先级位图表 uxTopReadyPriority 中对应的位清 0,因为该优先级下还有任务 2,否则任务 2 将得不到执行。

#define taskRESET_READY_PRIORITY( uxPriority )\
	{\
		if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ ( uxPriority ) ] ) ) == ( UBaseType_t ) 0 )\
		{\
			portRESET_READY_PRIORITY( ( uxPriority ), ( uxTopReadyPriority ) );	\
		}\
	}
2.4.3 taskSELECT_HIGHEST_PRIORITY_TASK()

用于寻找优先级最高的就绪任务,实质就是更新 uxTopReadyPriority 和 pxCurrentTCB 的值。

根据 uxTopReadyPriority 的值,找到最高优先级,然后更新到uxTopPriority 这个局部变量中。

根据 uxTopPriority 的值,从就绪列表中找到就绪的最高优先级的任务的 TCB,然后将 TCB 更新到 pxCurrentTCB。

/* 查找最高优先级的就绪任务:根据处理器架构优化后的方法 */
#define taskSELECT_HIGHEST_PRIORITY_TASK()\
 {\
     UBaseType_t uxTopPriority;	\
     portGET_HIGHEST_PRIORITY( uxTopPriority, uxTopReadyPriority );\
     listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) );\
 }  
2.4.3.1 listGET_OWNER_OF_NEXT_ENTRY
#define listGET_OWNER_OF_NEXT_ENTRY( pxTCB, pxList )\
{											\
List_t * const pxConstList = ( pxList );\
	/* 节点索引指向链表第一个节点调整节点索引指针,指向下一个节点, */\
	/* 如果当前链表有 N 个节点,当第 N 次调用该函数时,pxIndex 则指向第 N 个节点  */\
	( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext;\
    /* 当遍历完链表后,pxIndex 回指到根节点 */\
	if( ( void * ) ( pxConstList )->pxIndex == ( void * ) &( ( pxConstList )->xListEnd ) )\
	{\
		( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext;\
	}\
    /* 获取节点的 OWNER,即 TCB */\
	( pxTCB ) = ( pxConstList )->pxIndex->pvOwner;\
}

用于获取下一个节点的 OWNER 。

假设当前链表有 N 个节点,当第 N 次调用该函数时,pxIndex 则指向第 N个节点,即每调用一次,节点遍历指针 pxIndex 则会向后移动一次,用于指向下一个节点。

3. 创建任务

3.1 什么是任务?

从这个角度想:函数被暂停时,我们怎么保存它、保存什么?怎么恢复它、恢复什么?

  • 什么叫任务:

    • 运行中的函数、被暂停运行的函数
  • 任务是一个函数? 函数需要保存吗?

    • 函数保存在Flash上,不会被破坏,无需保存
    • Flash上的函数无需再次保存
    • 所以:任务不仅仅是函数
  • 任务是变量吗?

    • 单纯通过变量无法做事
    • 所以:任务不仅仅是变量
  • 任务是一个运行中的函数

    • 运行中:可以曾经运行,现在暂停了,但是未退出
    • 怎么描述一个运行中的函数
    • 假设在某一个瞬间时间停止,你怎么记录这个运行中的函数
  • 函数执行到哪里(运行的位置),需要保存吗?

    • 它是一个CPU寄存器,名为“PC”
    • 需要保存,
  • 函数用到了全局变量,全局变量需要保存吗?

    • 全局变量在内存上,不需要保存
  • 函数里用到了局部变量,局部变量需要保存吗?

    • 局部变量在栈里,也是在内存里,只要避免栈不被破坏即可,局部变量无需保存
  • 运算的中间值需要保存吗?中间值保存在哪里?

    • 在CPU寄存器里,另一个任务也要用到CPU寄存器,所以CPU寄存器需要保存
  • 汇总:

    • CPU寄存器需要保存!
    • 保存在任务的栈里
  • 什么叫现场?

    • 暂且认为现场就是:被打断瞬间,CPU中的R0~R15这16个寄存器的值。
  • 怎么保存?

    • 保存进内存里;
  • 保存到内存的哪里?–栈里

    • 把16个寄存器(现场)保存到栈里。

(1)提问:LR被覆盖了怎么办?(LR:用来保存返回地址)

  • 在C入口,划分出自己的栈;保存LR进栈里(在内存中);保存局部变量。

(2)提问:局部变量是在栈里面分配的,如何分配?

  • 局部变量有可能保存在寄存器中,不一定保存在内存里;
  • 或有 volatile,局部变量保存在内存中;
  • 或寄存器不够用时,保存在内存中。

(3)提问:为何每个RTOS任务都有自己的栈?

  • 每个任务都有自己的:{ 调用关系;局部变量;现场 },所以需要自己的栈。
  • 栈:从哪里分配,大小怎样确定?
    • 栈的大小:用估计的方法确定,
      • 局部变量的大小
      • 调用深度
    • 栈从哪里分配
      • 栈就是一块空闲的内存,
      • 从巨大的数组里去划分出一块内存给某个任务用作栈

任务切换时,怎样找到任务? -——通过链表

在freeRTOS中,任务就是一个函数,原型如下:

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

在嵌入式领域里面有很多RTOS:

  • FreeRTOS操作系统
    • 创建新任务是xTaskCreate
  • RT-Thread操作系统
    • 创建新任务是rt_thread_create

不同的操作系统创建任务的函数不一样,若想统一起来,则要在上面增加一个统一的接口层cmsis_os2.c,该文件会抽象出一个统一的接口,比如osThreadNew函数(348行)。我们写程序的人、写APP的人就可以直接调用该函数,会根据不同的操作系统调用 不同的创建新任务的函数。

画板

使用该原生代码<font style="color:#DF2A3F;">xTaskCreateStatic</font><font style="color:#DF2A3F;">xTaskCreate</font>来创建任务。

3.3 创建第1个多任务程序(9.2.2创建任务)

任务创建:

  • 动态创建
    • 任务控制块和栈的内存是创建任务时动态分配的,任务删除时,内存可以释放。
  • 静态创建
    • 任务控制块和栈的内存需要事先定义好,是静态的内存,任务删除时,内存不能释放。

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

  1. 动态分配内存
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 ); // 任务句柄, 以后使用它来操作这个任务
  • "02-first-rtos-app"程序

不同的任务,pvParameters不一样

/* 使用同一个函数创建不同的任务 */
  xTaskCreate(LcdPrintTask, "task1", 128, &g_Task1Info, osPriorityNormal, NULL);
  xTaskCreate(LcdPrintTask, "task2", 128, &g_Task2Info, osPriorityNormal, NULL);
  xTaskCreate(LcdPrintTask, "task3", 128, &g_Task3Info, osPriorityNormal, NULL);
3.4 创建任务流程
  1. TCB:
// 已删减

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. */

	ListItem_t			xStateListItem;	/*< (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;		/* 创建任务时设置的优先级 */
		UBaseType_t		uxMutexesHeld;
	#endif

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

} tskTCB;

/* The old tskTCB name is maintained above then typedefed to the new TCB_t name
below to enable the use of older kernel aware debuggers. */
typedef tskTCB TCB_t;
  1. 动态创建–函数调用关系如下:

3.4.1 创建新任务prvInitialiseNewTask
// 3.1 获取栈顶地址,并做向下做8字节对齐
    pxTopOfStack = pxNewTCB->pxStack + ( ulStackDepth - ( uint32_t ) 1 );
    pxTopOfStack = ( StackType_t * ) ( ( ( uint32_t ) pxTopOfStack ) & ( ~( ( uint32_t ) 0x0007 ) ) ); 
// 3.2 将任务的名字存在TCB中
    pxNewTCB->pcTaskName[ x ] = pcName[ x ];
// 3.3 初始化优先级
    pxNewTCB->uxPriority = uxPriority;
// 3.4 初始化链表
    vListInitialiseItem( &( pxNewTCB->xStateListItem ) );
    vListInitialiseItem( &( pxNewTCB->xEventListItem ) );
// 3.6 设置链表的拥有者
    listSET_LIST_ITEM_OWNER( &( pxNewTCB->xStateListItem ), pxNewTCB );
    listSET_LIST_ITEM_VALUE( &( pxNewTCB->xEventListItem ), ( TickType_t ) configMAX_PRIORITIES - ( TickType_t ) uxPriority );
    listSET_LIST_ITEM_OWNER( &( pxNewTCB->xEventListItem ), pxNewTCB );
// 3.5 初始化任务栈
    pxNewTCB->pxTopOfStack = pxPortInitialiseStack(...)
3.4.2 初始化任务栈pxPortInitialiseStack

3.4.3 放入就绪链表prvAddNewTaskToReadyList
prvAddNewTaskToReadyList(...)
    // 进入临界段
    taskENTER_CRITICAL();
    // 全局任务计时器加一操作
    uxCurrentNumberOfTasks++;
    // 如果 pxCurrentTCB 为空,则将 pxCurrentTCB 指向新创建的任务
    if( pxCurrentTCB == NULL )
    {
        pxCurrentTCB = pxNewTCB;
        // 如果是第一次创建任务,则需要初始化任务相关的列表
        if( uxCurrentNumberOfTasks == ( UBaseType_t ) 1 )
    	{
            prvInitialiseTaskLists();
        }
    }
    else
    {   // 如果 pxCurrentTCB 不为空,
        //则根据任务的优先级将 pxCurrentTCB 指向最高优先级任务的 TCB
            if( pxCurrentTCB->uxPriority <= pxNewTCB->uxPriority )
            {
                pxCurrentTCB = pxNewTCB;
            }
    }
prvAddTaskToReadyList( pxNewTCB );
taskEXIT_CRITICAL();    
            
3.4.3.1 就绪链表初始化prvInitialiseTaskLists
上一篇:【时时三省】(C语言基础)函数介绍strncmp


下一篇:Golang | Leetcode Golang题解之第480题滑动窗口中位数-题解: