> 文章列表 > FreeRTOS学习笔记(一)——初识FreeRTOS

FreeRTOS学习笔记(一)——初识FreeRTOS

FreeRTOS学习笔记(一)——初识FreeRTOS

  • FreeRTOS官网:FreeRTOS - 适用于具有物联网扩展功能的嵌入式系统的市场领先 RTOS(实时操作系统)

  • FreeRTOS源码下载:FreeRTOS Real Time Kernel (RTOS) - Browse /FreeRTOS at SourceForge.net

目录

0x01 FreeRTOS编程风格

一、数据类型

二、变量名、函数名、宏的规定

0x02 FreeRTOS内核原理(链表)

一、裸机与操作系统的区别

二、多任务系统

三、数据结构

四、FreeRTOS中链表的表现

0x03 使用CubeMX将RTOS部署到STM32L151

0x04 创建任务TASK与Queue

0x05 STM32L151移植FreeRTOS时需要port.c文件依据

0x06 函数接口

osThreadDef

0x07 任务

xTaskCreateStatic()

prvInitialiseNewTask()

pxPortInitialiseStack()

0x08 列表

prvInitialiseTaskLists()

0x09 任务调度

vTaskStartScheduler()

xPortStartScheduler()

prvStartFirstTask()

vPortSVCHandler()

0x0A 任务切换

taskYIELD()

xPortPendSVHandler()

vTaskSwitchContext()


 

0x01 FreeRTOS编程风格

一、数据类型

在 FreeRTOS 中,使用的数据类型虽然都是标准 C 里面的数据类型,但是针对不同的 处理器,对标准 C 的数据类型又进行了重定义,给它们取了一个新的名字,比如 char 重新定义了一个名字 portCHAR,这里面的 port 表示接口的意思,就是 FreeRTOS 要移植到这些处理器上需要这些接口文件来把它们连接在一起。

其主要的规定位于文件portmacro.h

#define portCHAR		char
#define portFLOAT		float
#define portDOUBLE		double
#define portLONG		long
#define portSHORT		short
#define portSTACK_TYPE	uint32_t
#define portBASE_TYPE	longtypedef portSTACK_TYPE StackType_t;
typedef long BaseType_t;
typedef unsigned long UBaseType_t;
#if( configUSE_16_BIT_TICKS == 1 ) (1) 
typedef uint16_t TickType_t;
#define portMAX_DELAY ( TickType_t ) 0xffff
#else
typedef uint32_t TickType_t; 
#define portMAX_DELAY ( TickType_t ) 0xffffffffUL
#endif

在 FreeRTOS 中,int 型从 不使用,只使用 short 和 long 型。在 Cortex-M 内核的 MCU 中,short 为 16 位,long 为 32 位。

在 FreeRTOS 中,我们都需要明确的指定变量 char 是有符号的还是无符号的。设定时可以进入keil中进行配置:

二、变量名、函数名、宏的规定

在 FreeRTOS 中,定义变量的时候往往会把变量的类型当作前缀加在变量上,这样的好处是让用户一看到这个变量就知道该变量的类型。

变量规定:

  • char 型变量的前缀是 c,short 型变量的前缀是 s,long 型变量的前缀是 l, portBASE_TYPE 类型变量的前缀是 x。

  • 其他 的数据类型,比如数据结构,任务句柄,队列句柄等定义的变量名的前缀也是 x。

  • 无符号型的会有一个前缀 u。

  • 指针变量会有一个前缀 p。

函数名规定:

  • 函数名包含了函数返回值类型、函数所在的文件名和函数的功能,如果是私有的函数则会加一个 prv(private)的前缀。

  1. vTaskPrioritySet()函数的返回值为 void 型,在task.c 这个文件中定义。

  2. xQueueReceive()函数的返回值为 portBASE_TYPE 型,在 queue.c 这个文件中定义。

  3. vSemaphoreCreateBinary()函数的返回值为 void 型,在 semphr.h 这个文件中定义。

宏的规定:

  • 宏均是由大写字母表示,并配有小写字母的前缀,前缀用于表示该宏在哪个头文件定义。

  • 信号量的函数都是一个宏定义,但是它的函数的命名方法是遵循函数的命名方法而不是宏定义的方法。

  • 通用宏,表示0和1的宏:

 

0x02 FreeRTOS内核原理(链表)

一、裸机与操作系统的区别

裸机系统通常分成轮询系统前后台系统

  • 轮询系统即是在裸机编程的时候,先初始化好相关的硬件,然后让主程序在一个死循环里面不断循环,顺序地做各种事情。轮询系统只适合顺序执行的功能代码,当有外部事件驱动时,实时性会降低。

  • 相比轮询系统,前后台系统是在轮询系统的基础上加入了中断。外部事件的响应在中断里面完成,事件的处理还是回到轮询系统中完成,中断在这里我们称为前台,main 函数里面的无限循环我们称为后台。

在顺序执行后台程序的时候,如果有中断来临,那么中断会打断后台程序的正常执行流,转而去执行中断服务程序,在中断服务程序里面标记事件,如果事件要处理的事情很简短,则可在中断服务程序里面处理,如果事件要处理的事情比较多,则返回到后台程序里面处理。虽然事件的响应和处理是分开了,但是事件的处理还是在后台里面顺序执行的,但相比轮询系统,前后台系统确保了事件不会丢失,再加上中断具有可嵌套的功能,这可以大大的提高程序的实时响应能力。在大多数的中小型项目中,前后台系统运用的好,堪称有操作系统的效果。

二、多任务系统

相比前后台系统中后台顺序执行的程序主体,在多任务系统中,根据程序的功能,我 们把这个程序主体分割成一个个独立的,无限循环且不能返回的小程序,这个小程序我们称之为任务。每个任务都是独立的,互不干扰的,且具备自身的优先级,它由操作系统调度管理。加入操作系统后,我们在编程的时候不需要精心地去设计程序的执行流,不用担 心每个功能模块之间是否存在干扰。整个系统随之带来的额外开销就是操作系统占据的那一丁点的 FLASH 和 RAM。

三、数据结构

单链表

单链表:该链表中共有 n个节点,前一个节点都有一个箭头指向后一个节点,首尾相连,组成一个圈。

 节点都是一个自定义类型的数据结构,在这个数据结构里面可以有单个的数据、数组、指针数据和自定义的结构体数据类型等等信息:

struct node
{struct node *next; /* 指向链表的下一个节点 */char data1; /* 单个的数据 */unsigned char array[]; /* 数组 */unsigned long *prt /* 指针数据 */struct userstruct data2; /* 自定义结构体类型数据 *//* ...... */
}

通常的做法是节点里面只包含一个用于指向下一个节点的指针。要通过链表存储的数据内嵌一个节点即可,这些要存储的数据通过这个内嵌的节点即可挂接到链表中:

/* 节点定义 */
struct node
{struct node *next; /* 指向链表的下一个节点 */
}struct userstruct
{/* 在结构体中,内嵌一个节点指针,通过这个节点将数据挂接到链表 */struct node *next;/* 各种各样......,要存储的数据 */
}

 

链表的操作

链表常规的操作就是节点的插入和删除,为了顺利的插入,通常一条链表我们会人为地规定一个根节点,这个根节点称为生产者。通常根节点还会有一个节点计数器,用于统计整条链表的节点个数。

双向链表

双向链表与单向链表的区别就是节点中有两个节点指针,分别指向前后两个节点,其它完全一样。

 

四、FreeRTOS中链表的表现

FreeRTOS 中与链表相关的操作均在 list.hlist.c 这两个文件中实现。

定义链表节点的数据结构

struct xLIST_ITEM
{TickType_t xItemValue; 		/* 辅助值,用于帮助节点做顺序排列 */ struct xLIST_ITEM * pxNext; /* 指向链表下一个节点 */ struct xLIST_ITEM * pxPrevious; /* 指向链表前一个节点 */ void * pvOwner; 			/* 指向拥有该节点的内核对象,通常是 TCB */void * pvContainer; 		/* 指向该节点所在的链表 */ 
};
typedef struct xLIST_ITEM ListItem_t; /* 节点数据类型重定义 */ 

 

链表节点初始化

初始化函数主要位于list.c中:

void vListInitialiseItem( ListItem_t * const pxItem )
{/* 初始化该节点所在的链表为空,表示节点还没有插入任何链表 */pxItem->pvContainer = NULL;
}

链表节点 ListItem_t 总共有 5 个成员,但是初始化的时候只需将pvContainer 初始化为空即可,表示该节点还没有插入到任何链表。

实现链表根节点

根节点数据结构位于list.h

 typedef struct xLIST
{UBaseType_t uxNumberOfItems; 	/* 链表节点计数器 */ ListItem_t * pxIndex; 			/* 链表节点索引指针 */ MiniListItem_t xListEnd; 		/* 链表最后一个节点 */ 
} List_t;

 

 该根节点为生产者,链表为首尾相连的结构,该生产者的数据类型是一个精简节点,于list.h

struct xMINI_LIST_ITEM
{TickType_t xItemValue; /* 辅助值,用于帮助节点做升序排列 */struct xLIST_ITEM * pxNext; /* 指向链表下一个节点 */struct xLIST_ITEM * pxPrevious; /* 指向链表前一个节点 */
};
typedef struct xMINI_LIST_ITEM MiniListItem_t; /* 精简节点数据类型重定义 */

在FreeRTOS中链表的操作

  • 根节点初始化函数void vListInitialise( List_t * const pxList )

  • 将节点插入到链表尾部void vListInsertEnd( List_t * const pxList, ListItem_t * const pxNewListItem )

  • 将节点按照升序排列插入到链表void vListInsert( List_t * const pxList, ListItem_t * const pxNewListItem )

  • 将节点从链表中删除UBaseType_t uxListRemove( ListItem_t * const pxItemToRemove )

0x03 使用CubeMX将RTOS部署到STM32L151

首先是新建工程,之后选择MCU以及对应的封装,接下来就是配置时钟了:

 配置调试模式:

 需要注意的是对sys的配置中,需要选择除了SysTick之外的时钟作为HAL库的时基:

 

  • 在裸机运行时,可以通过systick或TIMx定时器来维护SYS Timebase Source,也就是HAL库中的uwTick,这是HAL库中维护的一个全局变量,在裸机的情况下,我们可以选择SysTick方式即可,直接放在SysTick_Handler()中断服务函数中来维护。

  • 带OS的运行时,SYS Timebase Source是STM32的HAL库中新增的部分,主要是实现于HAL_Delay()以及各种作为timeout的时钟基准。在使用了OS(操作系统)之后,OS的运行也需要一个时钟基准(简称“时基”),来对任务和时间等进行管理。而OS的这个 时基 一般也都是通过 SysTick(滴答定时器) 来维护的,这时就需要考虑 “HAL的时基” 和 “OS的时基” 是否要共用 SysTick(滴答定时器) 了。

使用RTOS时,最好也不要共用SysTick,据哟潜在的风险,并且最后生成代码的时候会有这样的提示:

在最后程序生成后,可以看到我们设置的SYS使用的时基是什么:

main()->HAL_Init()->HAL_InitTick(TICK_INT_PRIORITY);其中TICK_INT_PRIORITY的值为0,此时tim2的中断优先级是最高的。在tim2的中断函数中,我们可以看到,HAL库使用tim2的更新中断作为了时钟源:

 接下来是FreeRTOS的参数配置:

 在 Middleware 中选择 FREERTOS 设置,并选择 CMSIS_V1 接口版本,对于CMSIS_V1CMSIS_V2的区别:

CMSIS(Cortex Microcontroller Software Interface Standard)是ARM公司推出的一种软件开发框架,旨在为Cortex-M系列微控制器提供统一的软件接口。CMSIS定义了一套通用的API,包括处理器、外设和软件组件,使得开发人员可以更加方便地编写可移植的嵌入式软件。

CMSIS_V1和CMSIS_V2是CMSIS的两个版本,它们之间的区别如下:

  1. 支持的处理器架构不同:CMSIS_V1主要支持ARM Cortex-M3处理器架构,而CMSIS_V2则支持ARM Cortex-M0、M0+、M3、M4和M7等处理器架构。

  2. API接口不同:CMSIS_V2相对于CMSIS_V1增加了一些新的API接口,如DSP库、内存保护等。

  3. 支持的编译器不同:CMSIS_V1支持Keil MDK和IAR Embedded Workbench编译器,而CMSIS_V2则还支持GCC编译器。

  4. 支持的操作系统不同:CMSIS_V2支持更多的操作系统,如FreeRTOS、CMSIS-RTOS等。

  5. 支持的开发工具不同:CMSIS_V2支持更多的开发工具,如STM32CubeMX、Atmel Studio等。

总的来说,CMSIS_V2相对于CMSIS_V1提供了更加完善的API接口和更广泛的处理器支持,使得开发人员可以更加方便地编写可移植的嵌入式软件。

接下来是各种参数的具体配置:

Kernel settings:

  • USE_PREEMPTION: Enabled:RTOS使用抢占式调度器;Disabled:RTOS使用协作式调度器(时间片)。

  • TICK_RATE_HZ: 值设置为1000,即周期就是1ms。RTOS系统节拍中断的频率,单位为HZ。

  • MAX_PRIORITIES: 可使用的最大优先级数量。设置好以后任务就可以使用从0到(MAX_PRIORITIES - 1)的优先级,其中0位最低优先级,(MAX_PRIORITIES - 1)为最高优先级。

  • MINIMAL_STACK_SIZE: 设置空闲任务的最小任务堆栈大小,以字为单位,而不是字节。如该值设置为128 Words,那么真正的堆栈大小就是 128*4 = 512 Byte。

  • MAX_TASK_NAME_LEN: 设置任务名最大长度。

  • IDLE_SHOULD_YIELD: Enabled 空闲任务放弃CPU使用权给其他同优先级的用户任务。

  • USE_MUTEXES: 为1时使用互斥信号量,相关的API函数会被编译。

  • USE_RECURSIVE_MUTEXES: 为1时使用递归互斥信号量,相关的API函数会被编译。

  • USE_COUNTING_SEMAPHORES: 为1时启用计数型信号量, 相关的API函数会被编译。

  • QUEUE_REGISTRY_SIZE: 设置可以注册的队列和信号量的最大数量,在使用内核调试器查看信号量和队列的时候需要设置此宏,而且要先将消息队列和信号量进行注册,只有注册了的队列和信号量才会在内核调试器中看到,如果不使用内核调试器的话次宏设置为0即可。

  • USE_APPLICATION_TASK_TAG: 为1时可以使用vTaskSetApplicationTaskTag函数。

  • ENABLE_BACKWARD_COMPATIBILITY: 为1时可以使V8.0.0之前的FreeRTOS用户代码直接升级到V8.0.0之后,而不需要做任何修改。

  • USE_PORT_OPTIMISED_TASK_SELECTION: FreeRTOS有两种方法来选择下一个要运行的任务,一个是通用的方法,另外一个是特殊的方法,也就是硬件方法,使用MCU自带的硬件指令来实现。STM32有计算前导零指令吗,所以这里强制置1。

  • USE_TICKLESS_IDLE: 置1:使能低功耗tickless模式;置0:保持系统节拍(tick)中断一直运行。假设开启低功耗的话可能会导致下载出现问题,因为程序在睡眠中,可用ISP下载办法解决。

  • USE_TASK_NOTIFICATIONS: 为1时使用任务通知功能,相关的API函数会被编译。开启了此功能,每个任务会多消耗8个字节。

  • RECORD_STACK_HIGH_ADDRESS: 为1时栈开始地址会被保存到每个任务的TCB中(假如栈是向下生长的)。

Memory management settings:

  • Memory Allocation: Dynamic/Static 支持动态/静态内存申请

  • TOTAL_HEAP_SIZE: 设置堆大小,如果使用了动态内存管理,FreeRTOS在创建 task, queue, mutex, software timer or semaphore的时候就会使用heap_x.c(x为1~5)中的内存申请函数来申请内存。这些内存就是从堆ucHeap[configTOTAL_HEAP_SIZE]中申请的。

  • Memory Management scheme: 内存管理策略 heap_4。

Hook function related definitions:

  • USE_IDLE_HOOK: 置1:使用空闲钩子(Idle Hook类似于回调函数);置0:忽略空闲钩子。

  • USE_TICK_HOOK: 置1:使用时间片钩子(Tick Hook);置0:忽略时间片钩子。

  • USE_MALLOC_FAILED_HOOK: 使用内存申请失败钩子函数。

  • CHECK_FOR_STACK_OVERFLOW: 大于0时启用堆栈溢出检测功能,如果使用此功能用户必须提供一个栈溢出钩子函数,如果使用的话此值可以为1或者2,因为有两种栈溢出检测方法。

Run time and task stats gathering related definitions:

  • GENERATE_RUN_TIME_STATS: 启用运行时间统计功能。

  • USE_TRACE_FACILITY: 启用可视化跟踪调试。

  • USE_STATS_FORMATTING_FUNCTIONS: 与宏configUSE_TRACE_FACILITY同时为1时会编译下面3个函数prvWriteNameToBuffer()、vTaskList()、vTaskGetRunTimeStats()。

Co-routine related definitions:

  • USE_CO_ROUTINES: 启用协程。

  • MAX_CO_ROUTINE_PRIORITIES: 协程的有效优先级数目。

Software timer definitions:

  • USE_TIMERS: 启用软件定时器。

Interrupt nesting behaviour configuration:

  • LIBRARY_LOWEST_INTERRUPT_PRIORITY: 中断最低优先级。

  • LIBRARY_LOWEST_INTERRUPT_PRIORITY: 系统可管理的最高中断优先级。

参考链接:STM32CubeMX学习笔记(29)——FreeRTOS实时操作系统使用(消息队列)_freertos cmsis 消息队列_Leung_ManWah的博客-CSDN博客

0x04 创建任务TASK与Queue

 同样也是使用STM32CubeMX进行任务的创建,具体操作如下:

  • Task Name: 任务名称

  • Priority: 优先级,在 FreeRTOS 中,数值越大优先级越高,0 代表最低优先级 Stack Size (Words): 堆栈大小,单位为字,在32位处理器(STM32),一个字等于4字节,如果传入512那么任务大小为512*4字节

  • Entry Function: 入口函数

  • Code Generation Option: 代码生成选项

  • Parameter: 任务入口函数形参,不用的时候配置为0或NULL即可

  • Allocation: 分配方式:Dynamic 动态内存创建

  • Buffer Name: 缓冲区名称

  • Conrol Block Name: 控制块名称

最后生成代码即可。在MX_FREERTOS_Init();中可以看到线程的创建:

 之后可以在这里实现其函数的实现:

 创建Queue:

  • Queue Name: 队列名称

  • Queue Size: 队列能够存储的最大单元数目,即队列深度,即我们设置的最高等级。

  • Item Size: 队列中数据单元的长度,以字节为单位

  • Allocation: 分配方式:Dynamic 动态内存创建

  • Buffer Name: 缓冲区名称

  • Buffer Size: 缓冲区大小

  • Conrol Block Name: 控制块名称

最后可以在串口上实现对应的打印:

 

0x05 STM32L151移植FreeRTOS时需要port.c文件依据

在将FreeRTOS移植到STM32L151上时,需要根据芯片的具体情况编写port.c文件。port.c文件是FreeRTOS的一个核心文件,包含了与处理器体系结构相关的代码,如任务切换、中断处理等。

以下是在STM32L151上移植FreeRTOS时需要关注的port.c文件的编写细节:

  1. 确认处理器体系结构:在编写port.c文件之前,需要确认处理器体系结构,即ARM Cortex-M3/M4。这可以通过查看芯片手册或者开发板资料得到确认。

  2. 编写中断处理函数:在port.c文件中需要编写处理器的中断处理函数。这包括处理器的系统中断和外设中断。在编写中断处理函数时,需要注意中断向量表的地址和中断处理函数的命名规则。

  3. 编写任务切换函数:在port.c文件中需要编写任务切换函数。这个函数负责将当前的任务切换到下一个任务。在任务切换函数中,需要保存当前任务的上下文,然后加载下一个任务的上下文。

  4. 设置堆栈指针:在port.c文件中需要设置堆栈指针。这个指针指向任务的堆栈顶部。在任务切换时,需要使用堆栈指针保存任务的上下文。

  5. 编写空闲任务函数:在port.c文件中需要编写空闲任务函数。这个函数是一个特殊的任务,当没有任务需要执行时,系统会执行空闲任务函数。

  6. 调整系统时钟:在port.c文件中需要调整系统时钟,以确保FreeRTOS能够正确地进行任务调度。在调整系统时钟时,需要注意时钟频率和分频器的设置。

以上是在STM32L151上移植FreeRTOS时需要关注的port.c文件的编写细节。在编写port.c文件时,需要仔细阅读FreeRTOS的官方文档,并参考其他已经移植成功的例子。

0x06 函数接口

osThreadDef

osThreadDef(name, thread, priority, instances, stacksz);

其中,每个参数的含义如下:

  1. name:线程的名称,必须是一个字符数组。

  2. thread:线程的入口函数,必须是一个函数指针。

  3. priority:线程的优先级,必须是一个整数值。优先级的范围一般是0~255,数字越大,优先级越高。

  4. instances:线程的实例数,必须是一个整数值。如果线程只有一个实例,则此值为1,如果有多个实例,则此值为实例数。

  5. stacksz:线程的堆栈大小,必须是一个整数值。堆栈大小的单位一般是字节,可以根据实际情况设置。

osThreadDef宏定义了一个线程描述符,可以通过调用osThreadCreate函数来创建线程。例如:

osThreadId myThreadHandle;
osThreadDef(myThread, myThreadEntry, osPriorityNormal, 1, 128);
myThreadHandle = osThreadCreate(osThread(myThread), NULL);

上述代码定义了一个名为myThread的线程,优先级为osPriorityNormal,堆栈大小为128字节,并创建了一个myThreadHandle句柄用于后续操作。其中,myThreadEntry是线程的入口函数,可以根据实际情况进行修改。对于优先级枚举:

FreeRTOS定义了多个任务优先级的枚举类型,包括以下几种:

  1. ePriority:这是最基本的任务优先级枚举类型,包括了0~configMAX_PRIORITIES-1个优先级。其中,configMAX_PRIORITIES是FreeRTOS配置文件中定义的最大优先级数。

  2. osPriority:这是CMSIS-RTOS API中定义的任务优先级枚举类型,包括了以下几种优先级:

  • osPriorityIdle:空闲任务的优先级。

  • osPriorityLow:低优先级。

  • osPriorityBelowNormal:低于正常优先级。

  • osPriorityNormal:正常优先级。

  • osPriorityAboveNormal:高于正常优先级。

  • osPriorityHigh:高优先级。

  • osPriorityRealtime:实时优先级。

  1. UBaseType_t:这是FreeRTOS中定义的另一个任务优先级类型,一般用于比较任务优先级。其取值范围是0~configMAX_PRIORITIES-1。

在使用FreeRTOS时,可以根据需要选择不同的任务优先级枚举类型。其中,ePriority是最基本的枚举类型,osPriority是CMSIS-RTOS API中定义的枚举类型,UBaseType_t是FreeRTOS中定义的另一个任务优先级类型。

0x07 任务

在裸机系统中,系统的主体就是 main 函数里面顺序执行的无限循环,这个无限循环里面 CPU 按照顺序完成各种事情。在多任务系统中,我们根据功能的不同,把整个系统分割成一个个独立的且无法返回的函数,这个函数我们称为任务。

  • 编写任务函数

    void task_entry (void *parg)
    {/* 任务主体,无限循环且不能返回 */for (;;) {/* 任务主体代码 */}
    }
  • 创建任务栈

在操作系统中,我们需要知道全局变量、局部变量、函数返回地址等存放位置,在裸机中,一半存放于单片机的RAM中,也就是栈,具体位置我们不关心,但是在RTOS中,要为每个任务都分配独立的栈空间,这个栈空间通常是一个预先定义好的全局数组,也可以是动态分配的一段内存空间,相同点是他们都存在于RAM中。

对于我们使用函数osThreadDef创建一个线程时,我们会将他的输入存放于结构体:

typedef struct os_thread_def  {char                   *name;        ///< Thread name os_pthread             pthread;      ///< start address of thread functionosPriority             tpriority;    ///< initial thread priorityuint32_t               instances;    ///< maximum number of instances of that thread functionuint32_t               stacksize;    ///< stack size requirements in bytes; 0 is default stack size
#if( configSUPPORT_STATIC_ALLOCATION == 1 )uint32_t               *buffer;      ///< stack buffer for static allocation; NULL for dynamic allocationosStaticThreadDef_t    *controlblock;     ///< control block to hold thread's data for static allocation; NULL for dynamic allocation
#endif
} osThreadDef_t;

FreeRTOS 推荐的最小的任务栈:128字。

  • 设定任务控制块

在裸机系统中,程序的主体是 CPU 按照顺序执行的。而在多任务系统中,任务的执行是由系统调度的。系统为了顺利的调度任务,为每个任务都额外定义了一个任务控制块,这个任务控制块就相当于任务的身份证,里面存有任务的所有信息,比如任务的栈指针,任务名称,任务的形参等。

typedef struct tskTaskControlBlock
{volatile StackType_t	*pxTopOfStack;	//栈顶ListItem_t			xStateListItem;		//任务节点ListItem_t			xEventListItem;		//用于从事件列表中引用任务UBaseType_t			uxPriority;			//优先级StackType_t			*pxStack;			//任务栈的起始地址char				pcTaskName[ configMAX_TASK_NAME_LEN ];		//栈的名称} tskTCB;
typedef tskTCB TCB_t;

任务的栈,任务的函数实体,任务的控制块最终需要联系起来才能由系统进行统一调度。那么这个联系的工作就由任务创建函数 xTaskCreateStatic()来实现:

xTaskCreateStatic()

FreeRTOS 中,任务的创建有两种方法,一种是使用动态创建,一种是使用静态创建。动态创建时,任务控制块和栈的内存是创建任务时动态分配的,任务删除时,内存可以释放。静态创建时,任务控制块和栈的内存需要事先定义好,是静态的内存 , 任务删除时 , 内存不能释放 。具体设置可以由宏来设定:if( configSUPPORT_STATIC_ALLOCATION == 1 ):

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 )	// 任务控制块指针
{TCB_t *pxNewTCB;TaskHandle_t xReturn;			// 定义一个任务句柄 xReturn,任务句柄用于指向任务的 TCB。configASSERT( puxStackBuffer != NULL );configASSERT( pxTaskBuffer != NULL );#if( configASSERT_DEFINED == 1 ){/* Sanity check that the size of the structure used to declare avariable of type StaticTask_t equals the size of the real taskstructure. */volatile size_t xSize = sizeof( StaticTask_t );configASSERT( xSize == sizeof( TCB_t ) );}#endif /* configASSERT_DEFINED */if( ( pxTaskBuffer != NULL ) && ( puxStackBuffer != NULL ) ){/* The memory used for the task's TCB and stack are passed into thisfunction - use them. */pxNewTCB = ( TCB_t * ) pxTaskBuffer; /*lint !e740 Unusual cast is ok as the structures are designed to have the same alignment, and the size is checked by an assert. */pxNewTCB->pxStack = ( StackType_t * ) puxStackBuffer;#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 thistask was created statically in case the task is later deleted. */pxNewTCB->ucStaticallyAllocated = tskSTATICALLY_ALLOCATED_STACK_AND_TCB;}#endif /* configSUPPORT_DYNAMIC_ALLOCATION */// 创建新任务prvInitialiseNewTask( pxTaskCode, 		//任务入口pcName, 			//任务名称,字符串形式ulStackDepth, 		//任务栈大小,单位为字pvParameters, 		//任务形参uxPriority, 		//任务优先级&xReturn, 			//任务句柄pxNewTCB, 			//任务栈起始地址NULL );prvAddNewTaskToReadyList( pxNewTCB );}else{xReturn = NULL;}// 返回任务句柄,如果任务创建成功,此时 xReturn 应该指向任务控制块return xReturn;
}

对于任务句柄TaskHandle_t:typedef void * TaskHandle_t;,其实就是一个空指针。

prvInitialiseNewTask()

对于函数xTaskCreateStatic()所调用的函数prvInitialiseNewTask(),可以在task.c中找到:

static void prvInitialiseNewTask(TaskFunction_t pxTaskCode, 		//任务入口const char * const pcName, 			//任务名称const uint32_t ulStackDepth, 		//任务栈大小,单位为字void * const pvParameters, 			//任务形参TaskHandle_t * const pxCreatedTask,	//任务句柄TCB_t *pxNewTCB ) 					//任务控制块地址
{StackType_t *pxTopOfStack;UBaseType_t x;/* 获取栈顶地址 */ pxTopOfStack = pxNewTCB->pxStack + ( ulStackDepth - ( uint32_t ) 1 );/* 向下做 8 字节对齐 */ pxTopOfStack = ( StackType_t * ) \\( ( ( uint32_t ) pxTopOfStack ) & ( ~( ( uint32_t ) 0x0007 ) ) );/* 将任务的名字存储在 TCB 中 */ for ( x = ( UBaseType_t ) 0; x < ( UBaseType_t ) configMAX_TASK_NAME_LEN; x++ ){pxNewTCB->pcTaskName[ x ] = pcName[ x ];if ( pcName[ x ] == 0x00 ){break;}}/* 任务名字的长度不能超过 configMAX_TASK_NAME_LEN */ pxNewTCB->pcTaskName[ configMAX_TASK_NAME_LEN - 1 ] = '\\0';/* 初始化 TCB 中的 xStateListItem 节点 */ vListInitialiseItem( &( pxNewTCB->xStateListItem ) );/* 设置 xStateListItem 节点的拥有者 */ listSET_LIST_ITEM_OWNER( &( pxNewTCB->xStateListItem ), pxNewTCB );/* 初始化任务栈 ,更新栈顶指针,任务第一次运行的环境参数就存在任务栈中。*/ // 让任务句柄指向任务控制块。pxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack,pxTaskCode,pvParameters );/* 让任务句柄指向任务控制块 */ if ( ( void * ) pxCreatedTask != NULL ){*pxCreatedTask = ( TaskHandle_t ) pxNewTCB;}
}

 

  • 对于栈顶指针向下做八字节对其:在Cortex-M3内核的单片机中,因为总线宽度是32位的,通常只要栈保持4字节对齐就行,但是浮点运算需要八个字节对齐。

pxPortInitialiseStack()

该函数位于port.c中:

StackType_t *pxPortInitialiseStack( StackType_t *pxTopOfStack, TaskFunction_t pxCode, void *pvParameters )
{//异常发生时自动加载到CPU寄存器的内容pxTopOfStack--; // 增加的偏移量,以考虑MCU在中断进入/退出时使用堆栈的方式。*pxTopOfStack = portINITIAL_XPSR;	/* xPSR */pxTopOfStack--;*pxTopOfStack = ( ( StackType_t ) pxCode ) & portSTART_ADDRESS_MASK;	/* PC */pxTopOfStack--;*pxTopOfStack = ( StackType_t ) prvTaskExitError;	/* LR */pxTopOfStack -= 5;	/* R12, R3, R2 and R1. */*pxTopOfStack = ( StackType_t ) pvParameters;	/* R0 *///异常发生时,手动加载到CPU寄存器的内容pxTopOfStack -= 8;	/* R11, R10, R9, R8, R7, R6, R5 and R4. *///返回栈顶指针,此时pxTopOfStack指向空闲栈return pxTopOfStack;
}

 

  • 异常发生时,CPU 自动从栈中加载到 CPU 寄存器的内容。包括 8个寄存器,分别为 R0、R1、R2、R3、R12、R14、R15 和 xPSR 的位 24,且顺序不能变。

  • xPSR 的 bit24 必须置 1,即 0x01000000。

  • 任务的入口地址。

  • 任务的返回地址,通常任务是不会返回的,如果返回了就跳转到prvTaskExitError,该函数是一个无限循环。

  • R12, R3, R2 and R1 默认初始化为 0。

  • 异常发生时,需要手动加载到 CPU 寄存器的内容,总共有 8 个,分别为 R4、R5、R6、R7、R8、R9、R10和 R11,默认初始化为 0。

  • 返回栈顶指针,此时 pxTopOfStack 指向见图,

  • 就是从这个栈指针开始手动加载 8 个字的内容到 CPU 寄存器:R4、R5、R6、R7、 R8、R9、R10 和 R11,当退出异常时,栈中剩下的 8 个字的内容会自动加载到 CPU 寄存器:R0、R1、R2、R3、R12、R14、R15 和 xPSR 的位 24。此时 PC 指针就指向了任务入口地址,从而成功跳转到第一个任务。

0x08 列表

以上我们就可以创建好一个任务了,接下来是要把任务添加到就绪列表里,表示任务已经就绪,系统随时可以调度。

 List_t pxReadyTasksLists[configMAX_PRIORITIES];

就绪列表实际上就是一个 List_t 类型的数组,数组的大小由决定最大任务优先级的宏 configMAX_PRIORITIES 决定 ,configMAX_PRIORITIES 在FreeRTOSConfig.h 中默认定义为 5,最大支持 256 个优先级

其数组的下标也就定义了任务的优先级同一优先级的任务插入到就绪列表的同一条链表中。

 

prvInitialiseTaskLists()

就绪列表初始化函数,也就是上面刚刚说的链表初始化,初始化该节点所在的链表为空,表示节点还没有插入任何链表。

 初始化后的链表都是为空的链表节点:

 

初始化完列表后,我们要使用添加节点的方式将任务添加到列表中,使用函数vListInsertEnd根据优先级来进行插入。在HAL库以及新版的RTOS中,函数prvInitialiseNewTask()已经加入了这个功能,也就是这个函数已经实现了创建线程并且加入到列表中。添加到列表中后,接下来我们要来实现任务调度了。

0x09 任务调度

调度器是操作系统的核心,其主要功能就是实现任务的切换,即从就绪列表里面找到优先级最高的任务,然后去执行该任务。从代码上来看,调度器无非也就是由几个全局变量和一些可以实现任务切换的函数组成,全部都在 task.c 文件中实现。

vTaskStartScheduler()

该函数用于调度器的启动。使用CubeMX生成的代码,在FreeRTOS中初始化后可以看到这个东西:

 其中调用的就是这个函数:

 在这个函数中调用的如下函数进行任务调度:

调用函数 xPortStartScheduler()启动调度器,调度器启动成功,则不会返回。该函数在 port.c 中实现。

xPortStartScheduler()

在Cortex-M中,内核外设SCB中SHPR3寄存器用于设置SysTick何PendSV的异常优先级。

对于这个函数需要关注一下:

System handler priority register 3 (SCB_SHPR3) SCB_SHPR3:0xE000 ED20
Bits 31:24 PRI_15[7:0]: Priority of system handler 15, SysTick exception
Bits 23:16 PRI_14[7:0]: Priority of system handler 14, PendSV

 其函数中的主要内容如下:

其主要作用有:

  • 配置 PendSV 和 SysTick 的中断优先级为最低SysTickPendSV 都会涉及到系统调度,系统调度的优先级要低于系统的其它硬件中断优先级,即优先相应系统中的外部硬件中断,所以 SysTick 和 PendSV 的中断优先级配置为最低。

  • 调用函数 prvStartFirstTask()启动第一个任务,启动成功后,则不再返回,该函数由汇编编写,在 port.c 实现。

prvStartFirstTask()

__asm void prvStartFirstTask( void )
{/* 当前栈需按照 8 字节对齐,如果都是 32 位的操作则 4 个字节对齐即可。在 Cortex-M 中浮点运算是 8字节的。*/PRESERVE8/* 在 Cortex-M 中,0xE000ED08 是 SCB_VTOR 这个寄存器的地址,里面存放的是向量表的起始地址,即 MSP 的地址 */ldr r0, =0xE000ED08ldr r0, [r0]ldr r0, [r0]/* 设置主堆栈指针 msp 的值 */msr msp, r0/*  使能全局中断 */cpsie icpsie fdsbisb/* 调用 SVC 去启动第一个任务 */svc 0nopnop
}

prvStartFirstTask()函数用于开始第一个任务,主要做了两个动作,一个是更新 MSP 的值,二是产生 SVC 系统调用,然后去到 SVC 的中断服务函数里面真正切换到第一个任务。

在Cortex-M中,内核外设SCB的地址范围在0xE000ED00-0xE000ED3F范围内, 0xE000ED008为SCB外设中SCB_VTOR这个寄存器的地址,里面存放着向量表的起始地址,即MSP地址。向量表通常是从内部 FLASH 的起始地址开始存放,那么可知 memory:0x00000000 处存放的就是 MSP 的值。这个可以通过仿真时查看内存的值证实。

对于上述函数做了如下操作 :

  • 将 0xE000ED08 这个立即数加载到寄存器 R0。

  • 将 0xE000ED08 这个地址指向的内容加载到寄存器 R0,此时 R0等于 SCB_VTOR 寄存器的值,等于 0x00000000,即 memory 的起始地址。

  • 将 0x00000000 这个地址指向的内容加载到 R0,此时 R0 等于0x200008DB。

  • 将 R0 的值存储到 MSP,此时 MSP 等于 0x200008DB,这是主堆栈的栈顶指针。起始这一步操作有点多余,因为当系统启动的时候,执行完 Reset_Handler的时候,向量表已经初始化完毕,MSP 的值就已经更新为向量表的起始值,即指向主堆栈的栈顶指针。

  • 使用 CPS 指令把全局中断打开。为了快速地开关中断, Cortex-M内核 专门设置了一条 CPS 指令,有 4 种用法:

CPSID I ;PRIMASK=1 ;关中断
CPSIE I ;PRIMASK=0 ;开中断
CPSID F ;FAULTMASK=1 ;关异常
CPSIE F ;FAULTMASK=0 ;开异常/*PRIMASK 和 FAULTMAST 是 Cortex-M内核 里面三个中断屏蔽寄存器中的两个,还有一个是 BASEPRI*/

内核中断屏蔽寄存器组的描述:

  • 产生系统调用SVC,接下来会执行SVC中断服务函数。

vPortSVCHandler()

SVC 中断要想被成功响应,其函数名必须与向量表注册的名称一致,在启动文件的向量表中,SVC 的中断服务函数注册的名称是 SVC_Handler,所以 SVC 中断服务函数的名称我们应该写成 SVC_Handler,但是在 FreeRTOS 中,官方版本写的是vPortSVCHandler(),为了能够顺利的响应 SVC 中断,需要改 FreeRTOS 中 SVC 的中断服务名称。需要在FreeRTOSconfig.h中修改对应的宏如下:

#define vPortSVCHandler    SVC_Handler
#define xPortPendSVHandler PendSV_Handler
#define xPortSysTickHandler SysTick_Handler

那么在函数vPortSVCHandler启动第一个任务:

__asm void vPortSVCHandler( void )
{extern pxCurrentTCB;	/* 声明外部变量 pxCurrentTCB,pxCurrentTCB 是一个在 task.c 中定义的全局指针,用于指向当前正在运行或者即将要运行的任务的任务控制块。*/PRESERVE8ldr	r3, =pxCurrentTCB	/* 加载 pxCurrentTCB 的地址到 r3 */ldr r1, [r3]			/* 加载 pxCurrentTCB 到 r3 */ldr r0, [r1]			/* 加载 pxCurrentTCB 指向的任务控制块到 r0,任务控制块的第一个成员就是栈顶指针,所以此时 r0 等于栈顶指针。 */ldmia r0!, {r4-r11}		/* 以 r0 为基地址,将栈中向上增长的 8 个字的内容加载到 CPU 寄存器 r4~r11,同时 r0 也会跟着自增。 */msr psp, r0				/* 将新的栈顶指针 r0 更新到 psp,任务执行的时候使用的堆栈指针是psp*/isbmov r0, #0				/*将寄存器 r0清 0*/msr	basepri, r0			/*设置 basepri 寄存器的值为 0,即打开所有中断。basepri 是一个中断屏蔽寄存器,大于等于此寄存器值的中断都将被屏蔽*/orr r14, #0xd		bx r14
}

后两句的解释如下:

  • 当从 SVC 中断服务退出前,通过向 r14 寄存器最后 4 位按位或上0x0D,使得硬件在退出时使用进程堆栈指针 PSP 完成出栈操作并返回后进入任务模式、返回 Thumb 状态。在 SVC 中断服务里面,使用的是 MSP 堆栈指针,是处在 ARM 状态。当 r14 为 0xFFFFFFFX,执行是中断返回指令,cortext-m3 的做法,X 的 bit0 为 1 表示返回 thumb 状态,bit1 和 bit2 分别表示返回后 sp 用 msp 还是 psp、以及返回到特权模式还是用户模式

  • 异常返回,这个时候出栈使用的是 PSP 指针,自动将栈中的剩下内容加载到 CPU 寄存器: xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0(任务的形参)同时 PSP 的值也将更新,即指向任务栈的栈顶。

0x0A 任务切换

任务切换就是在就绪列表中寻找优先级最高的就绪任务,然后去执行该任务。

taskYIELD()

#define taskYIELD()					portYIELD()#define portYIELD()																\\
{																				\\/* 触发 PendSV,产生上下文切换 */								\\portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;								\\\\/* Barriers are normally not required but do ensure the code is completely	\\within the specified behaviour for the architecture. */						\\__dsb( portSY_FULL_READ_WRITE );											\\__isb( portSY_FULL_READ_WRITE );											\\
}

实际就是将 PendSV 的悬起位置 1,当没有其它中断运行的时候响应 PendSV 中断,去执行我们写好的 PendSV中断服务函数,在里面实现任务切换。

xPortPendSVHandler()

PendSV 中断服务函数是真正实现任务切换的地方。

__asm void xPortPendSVHandler( void )
{extern uxCriticalNesting;extern pxCurrentTCB;		/* 声明外部变量 pxCurrentTCB,pxCurrentTCB 是一个在 task.c 中定义的全局指针,用于指向当前正在运行或者即将要运行的任务的任务控制块*/extern vTaskSwitchContext;PRESERVE8					/*当前栈需按照 8 字节对齐,如果都是 32 位的操作则 4 个字节对齐即可。在 Cortex-M 中浮点运算是 8字节的。*/mrs r0, psp					/*将 PSP 的值存储到 r0。当进入 PendSVC Handler 时,上一个任务运行的环境即: xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0(任务的形参)这些 CPU 寄存器的值会自动存储到任务的栈中,剩下的 r4~r11 需要手动保存,同时PSP 会自动更新(在更新之前 PSP 指向任务栈的栈顶)*/isbldr	r3, =pxCurrentTCB		/*加载 pxCurrentTCB 的地址到 r3*/ldr	r2, [r3]				/*加载 r3 指向的内容到 r2,即 r2 等于 pxCurrentTCB*/stmdb r0!, {r4-r11}			/* 以 r0 作为基址(指针先递减,再操作,STMDB 的 DB 表示Decrease Befor),将 CPU 寄存器 r4~r11 的值存储到任务栈,同时更新 r0的值 */str r0, [r2]				/* 将 r0 的值存储到 r2 指向的内容,r2 等于 pxCurrentTCB。具体为将r0 的值存储到上一个任务的栈顶指针 pxTopOfStack *//*以上实现了上下文切换中的上文保存*/stmdb sp!, {r3, r14}mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY  /*将 configMAX_SYSCALL_INTERRUPT_PRIORITY 的值存储到r0*/msr basepri, r0					/*关中断,进入临界段,因为接下来要更新全局指针 pxCurrentTCB的值*/dsbisbbl vTaskSwitchContext			/*调用函数 vTaskSwitchContext,选择优先级最高的任务*/mov r0, #0						/*退出临界段,开中断,直接往 BASEPRI 写 0*/msr basepri, r0ldmia sp!, {r3, r14}			/*从主堆栈中恢复寄存器 r3 和 r14 的值,此时的 sp 使用的是 MSP*/ldr r1, [r3]					/*加载 r3 指向的内容到 r1。r3 存放的是 pxCurrentTCB 的地址,即让 r1 等于 pxCurrentTCB。pxCurrentTCB 在上面的 vTaskSwitchContext 函数中被更新,指向了下一个将要运行的任务的 TCB*/ldr r0, [r1]					/* 加载 r1 指向的内容到 r0,即下一个要运行的任务的栈顶指针 */ldmia r0!, {r4-r11}				/* 以 r0 作为基地址(先取值,再递增指针,LDMIA 的 IA 表示Increase After),将下一个要运行的任务的任务栈的内容加载到 CPU 寄存器 r4~r11。 */msr psp, r0						/*更新 psp 的值,等下异常退出时,会以 psp 作为基地址,将任务栈中剩下的内容自动加载到 CPU 寄存器。*/isbbx r14nop
}
  • stmdb sp!, {r3, r14}:将 R3 和 R14 临时压入堆栈(在整个系统中,中断使用的是主堆栈,栈指针使用的是 MSP),因为接下来要调用函数 vTaskSwitchContext,调用函数时,返回地址自动保存到 R14 中,所以一旦调用发生,R14 的值会被覆盖(PendSV 中断服务函数执行完毕后,返回的时候需要根据 R14 的值来决定返回处理器模式还是任务模式,出栈时使用的是 PSP 还是 MSP),因此需要入栈保护。R3 保存的是当前正在运行的任务(准确来说是上文,因为接下来即将要切换到新的任务)的 TCB 指针(pxCurrentTCB)地址,函数调用后 pxCurrentTCB 的值会被更新,后面我们还需要通过 R3 来操作 pxCurrentTCB,但是运行函数vTaskSwitchContext 时不确定会不会使用 R3 寄存器作为中间变量,所以为了保险起见,R3 也入栈保护起来。

  • mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY:用来配置中断屏蔽寄存器 BASEPRI 的值,高四位有效。目前配置为 191,因为是高四位有效,所以实际值等于 11,即优先级高于或者等于11 的中断都将被屏蔽。在关中断方面,FreeRTOS 与其它的 RTOS 关中断不同,而是操作BASEPRI 寄存器来预留一部分中断,并不像 μC/OS 或者 RT-Thread 那样直接操作PRIMASK 把所有中断都关闭掉(除了硬 FAULT)。

  • 异常发生时,R14 中保存异常返回标志,包括返回后进入任务模式还是处理器模式、使用 PSP 堆栈指针还是 MSP 堆栈指针。此时的 r14 等于 0xfffffffd,最表示异常返回后进入任务模式,SP 以 PSP 作为堆栈指针出栈,出栈完毕后 PSP 指向任务栈的栈顶。当调用 bx r14 指令后,系统以 PSP 作为 SP 指针出栈,把接下来要运行的新任务的任务栈中剩下的内容加载到 CPU 寄存器:R0(任务形参)、R1、R2、R3、R12、R14(LR)、R15(PC)和 xPSR,从而切换到新的任务。

需要注意的是:

  • 在Cortex-M处理器中有两个栈指针,一个是主栈指针(Main Stack Pointer,即MSP),它可用于线程模式,在中断模式下只能用MSP;另一个是进程堆栈指针(Processor Stack Pointer,即PSP),PSP总是用于线程模式。在任何时刻只能使用到其中一个。

  • 复位后处于线程模式特权级,默认使用MSP。在FreeRTOS中,MSP用于OS内核和异常处理,PSP用于应用任务

  • 通过设置CONTROL寄存器的bit[1]选择使用哪个堆栈指针。CONTROL[1]=0选择主堆栈指针;CONTROL[1]=1选择进程堆栈指针。

vTaskSwitchContext()

检查任务堆栈使用是否溢出,和查找下一个优先级高的任务,如果使能运行时间统计功能,会计算任务运行时间。该函数会更新当前任务运行时间,检查任务堆栈使用是否溢出,然后调用宏 taskSELECT_HIGHEST_PRIORITY_TASK()获取更高优先级的任务。

void vTaskSwitchContext( void )
{/* 如果任务调度器已经挂起 */if( uxSchedulerSuspended != ( UBaseType_t ) pdFALSE ){xYieldPending = pdTRUE;		/* 标记任务调度器挂起,不允许任务切换 */}else{xYieldPending = pdFALSE;	/* 标记任务调度器没有挂起 */traceTASK_SWITCHED_OUT();/* * 如果启用运行时间统计功能,设置configGENERATE_RUN_TIME_STATS为1* 如果使用了该功能,要提供以下两个宏:* portCONFIGURE_TIMER_FOR_RUN_TIME_STATS()* portGET_RUN_TIME_COUNTER_VALUE()*/#if ( configGENERATE_RUN_TIME_STATS == 1 ){#ifdef portALT_GET_RUN_TIME_COUNTER_VALUEportALT_GET_RUN_TIME_COUNTER_VALUE( ulTotalRunTime );#elseulTotalRunTime = portGET_RUN_TIME_COUNTER_VALUE();#endif/*  * ulTotalRunTime记录系统的总运行时间,ulTaskSwitchedInTime记录任务切换的时间* 如果系统节拍周期为1ms,则ulTotalRunTime要497天后才会溢出* ulTotalRunTime &lt; ulTaskSwitchedInTime表示可能溢出*/if( ulTotalRunTime &gt; ulTaskSwitchedInTime ){/* 记录当前任务的运行时间 */pxCurrentTCB-&gt;ulRunTimeCounter += ( ulTotalRunTime - ulTaskSwitchedInTime );}else{mtCOVERAGE_TEST_MARKER();}/* 更新ulTaskSwitchedInTime,下个任务时间从这个值开始 */ulTaskSwitchedInTime = ulTotalRunTime;	}#endif /* configGENERATE_RUN_TIME_STATS *//* 核查堆栈是否溢出 */taskCHECK_FOR_STACK_OVERFLOW();/* 寻找更高优先级的任务 */taskSELECT_HIGHEST_PRIORITY_TASK();traceTASK_SWITCHED_IN();/* 如果使用Newlib运行库,你的操作系统资源不够,而不得不选择newlib,就必须打开该宏 */#if ( configUSE_NEWLIB_REENTRANT == 1 ){_impure_ptr = &amp;( pxCurrentTCB-&gt;xNewLib_reent );}#endif /* configUSE_NEWLIB_REENTRANT */
}