目录
- FreeRTOS 任务切换
- PendSV 异常
- PendSV 中断服务函数
- FreeRTOS 确定下一个要运行的任务
- 函数vTaskSwitchContext()
- 函数taskSELECT_HIGHEST_PRIORITY_TASK()
- PendSV 异常何时触发
- FreeRTOS 时间片调度实验
- 功能设计
- 软件设计
- 下载验证
- FreeRTOS 内核控制函数
- FreeRTOS 内核控制函数预览
- FreeRTOS 内核控制函数详解
- FreeRTOS 其他任务API 函数
- FreeRTOS 任务相关API 函数
- FreeRTOS 任务相关API 函数预览
- FreeRTOS 任务相关API 函数详解
FreeRTOS 任务切换
RTOS 的核心时任务管理,而任务管理的重中之重任务切换,系统中任务切换的过程决定
了操作系统的运行效率和稳定性,尤其是对于实时操作系统。而对于深入了解和学习FreeRTOS,
FreeRTOS 的任务切换是必须要掌握的一个知识点。本章就来学习FreeRTOS 的任务切换。
本章分为如下几部分:
9.1 PendSV 异常
9.2 PendSV 中断服务函数
9.3 FreeRTOS 确定下一个要运行的任务
9.4 PendSV 异常何时触发
9.5 FreeRTOS 时间片调度实验
PendSV 异常
PendSV(Pended Service Call,可挂起服务调用),是一个对RTOS 非常重要的异常。PendSV
的中断优先级是可以编程的,用户可以根据实际的需求,对其进行配置。PendSV 的中断由将中
断控制状态寄存器(ICSR)中PENDSVSET 为置一触发(中断控制状态寄存器的有关内容,请
查看4.1.5 小节《中断控制状态寄存器》)。PendSV 与SVC 不同,PendSV 的中断是非实时的,
即PendSV 的中断可以在更高优先级的中断中触发,但是在更高优先级中断结束后才执行。
利用PendSV 的这个可挂起特性,在设计RTOS 时,可以将PendSV 的中断优先级设置为
最低的中断优先级(FreeRTOS 就是这么做的,更详细的内容,请查看4.3.1 小节《PendSV 和
SysTick 中断优先级》),这么一来,PendSV 的中断服务函数就会在其他所有中断处理完成后才
执行。任务切换时,就需要用到PendSV 的这个特性。
首先,来看一下任务切换的一些基本概念,在典型的RTOS 中,任务的处理时间被分为多
个时间片,OS 内核的执行可以有两种触发方式,一种是通过在应用任务中通过SVC 指令触发,
例如在应用任务在等待某个时间发生而需要停止的时候,那么就可以通过SVC 指令来触发OS
内核的执行,以切换到其他任务;第二种方式是,SysTick 周期性的中断,来触发OS 内核的执
行。下图演示了只有两个任务的RTOS 中,两个任务交替执行的过程:
在操作系统中,任务调度器决定是否切换任务。图9.1.1 中的任务及切换都是在SysTick 中
断中完成的,SysTick 的每一次中断都会切换到其他任务。
如果一个中断请求(IRQ)在SysTick 中断产生之前产生,那么SysTick 就可能抢占该中断
请求,这就会导致该中断请求被延迟处理,这在实时操作系统中是不允许的,因为这将会影响
到实时操作系统的实时性,如下图所示:
并且,当SysTick 完成任务的上下文切换,准备返回任务中运行时,由于存在中断请求,
ARM Cortex-M 不允许返回线程模式,因此,将会产生用法错误异常(Usage Fault)。
在一些RTOS 的设计中,会通过判断是否存在中断请求,来决定是否进行任务切换。虽然
可以通过检查xPSR 或NVIC 中的中断活跃寄存器来判断是否存在中断请求,但是这样可能会
影响系统的性能,甚至可能出现中断源在SysTick 中断前后不断产生中断请求,导致系统无法
进行任务切换的情况。
PendSV 通过延迟执行任务切换,直到处理完所有的中断请求,以解决上述问题。为了达到
这样的效果,必须将PendSV 的中断优先级设置为最低的中断优先等级。如果操作系统决定切
换任务,那么就将PendSV 设置为挂起状态,并在PendSV 的中断服务函数中执行任务切换,如
下图所示:
- 任务一触发SVC 中断以进行任务切换(例如,任务一正等待某个事件发生)。
- 系统内核接收到任务切换请求,开始准备任务切换,并挂起PendSV 异常。
- 当退出SVC 中断的时候,立刻进入PendSV 异常处理,完成任务切换。
- 当PendSV 异常处理完成,返回线程模式,开始执行任务二。
- 中断产生,并进入中断处理函数。
- 当运行中断处理函数的时候,SysTick 异常(用于内核时钟节拍)产生。
- 操作系统执行必要的操作,然后挂起PendSV 异常,准备进行任务切换。
- 当SysTick 中断处理完成,返回继续处理中断。
- 当中断处理完成,立马进入PendSV 异常处理,完成任务切换。
- 当PendSV 异常处理完成,返回线程模式,继续执行任务一。
PendSV 在RTOS 的任务切换中,起着至关重要的作用,FreeRTOS 的任务切换就是在PendSV
中完成的。
PendSV 中断服务函数
FreeRTOS 在PendSV 的中断中,完成任务切换,PendSV 的中断服务函数由FreeRTOS 编
写,将PendSV 的中断服务函数定义成函数xPortPendSVHandler()。
针对ARM Cortex-M3 和针对ARM Cortex-M4 和ARM Cortex-M7 内核的函数
xPortPendSVHandler()稍有不同,其主要原因在于ARM Cortex-M4 和ARM Cortex-M7 内核具有
浮点单元,因此在进行任务切换的时候,还需考虑是否保护和恢复浮点寄存器的值。
针对ARM Cortex-M3 内核的函数xPortPendSVHandler(),具体的代码如下所示:
__asm void xPortPendSVHandler(void) {
/* 导入全局变量及函数*/
extern uxCriticalNesting;
extern pxCurrentTCB;
extern vTaskSwitchContext;
/* 8字节对齐*/
PRESERVE8
/* R0为PSP,即当前运行任务的任务栈指针*/
mrs r0, psp
isb
/* R3为pxCurrentTCB的地址值,即指向当前运行任务控制块的指针*/
/* R2为pxCurrentTCB的值,即当前运行任务控制块的首地址*/
ldr r3, = pxCurrentTCB
ldr r2, [r3]
/* 将R4~R11入栈到当前运行任务的任务栈中*/
stmdb r0!, {
r4 - r11
}
/* R2指向的地址为此时的任务栈指针*/
str r0, [r2]
/* 将R3、R14入栈到MSP指向的栈中*/
stmdb sp!, {
r3, r14
}
/* 屏蔽受FreeRTOS管理的所有中断*/
mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
msr basepri, r0
dsb
isb
/* 跳转到函数vTaskSeitchContext
* 主要用于更新pxCurrentTCB,
* 使其指向最高优先级的就绪态任务
*/
bl vTaskSwitchContext
/* 使能所有中断*/
mov r0, #0
msr basepri, r0
/* 将R3、R14重新从MSP指向的栈中出栈*/
ldmia sp !, { r3, r14 }
/* 注意:R3为pxCurrentTCB的地址值,
* pxCurrentTCB已经在函数vTaskSwitchContext中更新为最高优先级的就绪态任务
* 因此R1为pxCurrentTCB的值,即当前最高优先级就绪态任务控制块的首地址*/
ldr r1, [ r3 ]
/* R0为最高优先级就绪态任务的任务栈指针*/
ldr r0, [ r1 ]
/* 从最高优先级就绪态任务的任务栈中出栈R4~R11 */
ldmia r0 !, { r4 - r11 }
/* 更新PSP为任务切换后的任务栈指针*/
msr psp, r0
isb
/* 跳转到切换后的任务运行
* 执行此指令,CPU会自动从PSP指向的任务栈中,
* 出栈R0、R1、R2、R3、R12、LR、PC、xPSR寄存器,
* 接着CPU就跳转到PC指向的代码位置运行,
* 也就是任务上次切换时运行到的位置
*/
bx r14
nop
}
针对ARM Cortex-M4 内核的函数xPortPendSVHandler(),具体的代码如下所示(针对ARM Cortex-M7 内核的函数xPortPendSVHandler()与之类似):
__asm void xPortPendSVHandler(void) {
/* 导入全局变量及函数*/
extern uxCriticalNesting;
extern pxCurrentTCB;
extern vTaskSwitchContext;
/* 8字节对齐*/
PRESERVE8
/* R0为PSP,即当前运行任务的任务栈指针*/
mrs r0, psp
isb
/* R3为pxCurrentTCB的地址值,即指向当前运行任务控制块的指针*/
/* R2为pxCurrentTCB的值,即当前运行任务控制块的首地址*/
ldr r3, = pxCurrentTCB
ldr r2, [r3]
/* 获取R14寄存器的值,因为处于中断,此时R14为EXC_RETURN
* 通过判断EXC_RETURN的bit4是否为0,
* 判断在进入PendSV中断前运行的任务是否使用的浮点单元,
* 若使用了浮点单元,需要在切换任务时,保存浮点寄存器的值
*/
tst r14, #0x10
it eq
vstmdbeq r0!, {s16-s31}
/* 将R4~R11和R14寄存器入栈到当前运行任务的任务栈中
* 注意:此时的R14为EXC_RETURN,主要用于指示任务是否使用了浮点单元
*/
stmdb r0!, {r4-r11, r14}
/* R2指向的地址为此时的任务栈指针*/
str r0, [ r2 ]
/* 将R0、R3入栈到MSP指向的栈中*/
stmdb sp!, {r0, r3}
/* 屏蔽受FreeRTOS管理的所有中断*/
mov r0, # configMAX_SYSCALL_INTERRUPT_PRIORITY
msr basepri, r0
dsb
isb
/* 跳转到函数vTaskSeitchContext
* 主要用于更新pxCurrentTCB,
* 使其指向最高优先级的就绪态任务
*/
bl vTaskSwitchContext
/* 使能所有中断*/
mov r0, #0
msr basepri, r0
/* 将R0、R3重新从MSP指向的栈中出栈*/
ldmia sp!, {r0, r3}
/* 注意:R3为pxCurrentTCB的地址值,
* pxCurrentTCB已经在函数vTaskSwitchContext中更新为最高优先级的就绪态任务
* 因此R1为pxCurrentTCB的值,即当前最高优先级就绪态任务控制块的首地址*/
ldr r1, [ r3 ]
/* R0为最高优先级就绪态任务的任务栈指针*/
ldr r0, [ r1 ]
/* 从最高优先级就绪态任务的任务栈中出栈R4~R11和R14
* 注意:这里出栈的R14为EXC_RETURN,其保存了任务是否使用浮点单元的信息
*/
ldmia r0!, {r4-r11, r14}
/* 此时R14为EXC_RETURN,通过判断EXC_RETURN的bit4是否为0,
* 判断任务是否使用的浮点单元,
* 若使用了浮点单元,则需要从任务的任务栈中恢复出浮点寄存器的值
*/
tst r14, # 0x10
it eq
vldmiaeq r0!, {
s16 - s31
}
/* 更新PSP为任务切换后的任务栈指针*/
msr psp, r0
isb
/* 用于修改XMC4000的BUG,不用理会*/
# ifdef WORKAROUND_PMU_CM001#
if WORKAROUND_PMU_CM001 == 1
push {
r14
}
pop {
pc
}
nop# endif# endif
/* 跳转到切换后的任务运行
* 执行此指令,CPU会自动从PSP指向的任务栈中,
* 出栈R0、R1、R2、R3、R12、LR、PC、xPSR寄存器,
* 接着CPU就跳转到PC指向的代码位置运行,
* 也就是任务上次切换时运行到的位置
*/
bx r14
}
从上面的代码可以看出,FreeRTOS 在进行任务切换的时候,会将CPU 的运行状态,在当
前任务在进行任务切换前,进行保存,保存到任务的任务栈中,然后从切换后运行任务的任务
栈中恢复切换后运行任务在上一次被切换时保存的CPU 信息。
但是从PendSV 的中断回调函数代码中,只看到程序保存和恢复的CPU 信息中的部分寄存
器信息(R4 寄存器~R11 寄存器),这是因为硬件会自动出栈和入栈其他CPU 寄存器的信息。
在任务运行的时候,CPU 使用PSP 作为栈空间使用,也就是使用运行任务的任务栈。当
SysTick 中断(SysTick 的中断服务函数会判断是否需要进行任务切换,相关内容在后续章节会
进行讲解)发生时,在跳转到SysTick 中断服务函数运行前,硬件会自动将除R4~R11 寄存器的
其他CPU 寄存器入栈,因此就将任务切换前CPU 的部分信息保存到对应任务的任务栈中。当
退出PendSV 时,会自动从栈空间中恢复这部分CPU 信息,以共任务正常运行。
因此在PendSV 中断服务函数中,主要要做的事情就是,保存硬件不会自动入栈的CPU 信
息,已经确定写一个要运行的任务,并将pxCurrentTCB 指向该任务的任务控制块,然后更新
PSP 指针为该任务的任务堆栈指针。
FreeRTOS 确定下一个要运行的任务
从上一小节中可以看到,在PendSV 的中断服务函数中,调用了函数vTaskSwitchContext()
来确定写一个要运行的任务。
函数vTaskSwitchContext()
函数vTaskSwitchContext()在task.c 文件中有定义,具体的代码如下所示:
void vTaskSwitchContext(void) {
/* 判断任务调度器是否运行*/
if (uxSchedulerSuspended != (UBaseType_t) pdFALSE) {
/* 此全局变量用于在系统运行的任意时刻标记需要进行任务切换
* 会在SysTick的中断服务函数中统一处理
* 任务任务调度器没有运行,不允许任务切换,
* 因此将xYieldPending设置为pdTRUE
* 那么系统会在SysTick的中断服务函数中持续发起任务切换
* 直到任务调度器运行
*/
xYieldPending = pdTRUE;
} else {
/* 可以执行任务切换,因此将xYieldPending设置为pdFALSE */
xYieldPending = pdFALSE;
/* 用于调试,不用理会*/
traceTASK_SWITCHED_OUT();
/* 此宏用于使能任务运行时间统计功能,不用理会*/
#
if (configGENERATE_RUN_TIME_STATS == 1) {#
ifdef portALT_GET_RUN_TIME_COUNTER_VALUE
portALT_GET_RUN_TIME_COUNTER_VALUE(ulTotalRunTime);#
else
ulTotalRunTime = portGET_RUN_TIME_COUNTER_VALUE();#
endif
if (ulTotalRunTime > ulTaskSwitchedInTime) {
pxCurrentTCB - > ulRunTimeCounter +=
(ulTotalRunTime - ulTaskSwitchedInTime);
} else {
mtCOVERAGE_TEST_MARKER();
}
ulTaskSwitchedInTime = ulTotalRunTime;
}#
endif
/* 检查任务栈是否溢出,
* 未定义,不用理会
*/
taskCHECK_FOR_STACK_OVERFLOW();
/* 此宏为POSIX相关配置,不用理会*/
#
if (configUSE_POSIX_ERRNO == 1) {
pxCurrentTCB - > iTaskErrno = FreeRTOS_errno;
}#
endif
/* 将pxCurrentTCB指向优先级最高的就绪态任务
* 有两种方法,由FreeRTOSConfig.h文件配置决定
*/
taskSELECT_HIGHEST_PRIORITY_TASK();
/* 用于调试,不用理会*/
traceTASK_SWITCHED_IN();
/* 此宏为POSIX相关配置,不用理会*/
#
if (configUSE_POSIX_ERRNO == 1) {
FreeRTOS_errno = pxCurrentTCB - > iTaskErrno;
}#
endif
/* 此宏为Newlib相关配置,不用理会*/
#
if (configUSE_NEWLIB_REENTRANT == 1) {
_impure_ptr = & (pxCurrentTCB - > xNewLib_reent);
}#
endif
}
}
函数vTaskSwitchContext()调用了函数taskSELECT_HIGHEST_PRIORITY_TASK(),来将
pxCurrentTCB 设置为指向优先级最高的就绪态任务。
函数taskSELECT_HIGHEST_PRIORITY_TASK()
函数taskSELECT_HIGHEST_PRIORITY_TASK()用于将pcCurrentTCB 设置为优先级最高
的就绪态任务,因此该函数会使用位图的方式在任务优先级记录中查找优先级最高任务优先等
级,然后根据这个优先等级,到对应的就绪态任务列表在中取任务。
FreeRTOS 提供了两种从任务优先级记录中查找优先级最高任务优先等级的方式,一种是
由纯C 代码实现的,这种方式适用于所有运行FreeRTOS 的MCU;另外一种方式则是使用了硬
件计算前导零的指令,因此这种方式并不适用于所有运行FreeRTOS 的MCU,而仅适用于具有
有相应硬件指令的MCU。正点原子所有板卡所使用的STM32 MCU 都支持以上两种方式。具
体使用哪种方式,用户可以在FreeRTOSConfig.h 文件中进行配置,配置方法,请查看第三章
《FreeRTOS 系统配置》的相关章节。
软件方式实现的函数taskSELECT_HIGHEST_PRIORITY_TASK()是一个宏定义,在task.c
文件中由定义,具体的代码如下所示:
#
define taskSELECT_HIGHEST_PRIORITY_TASK()\ {\
/* 全局变量uxTopReadyPriority以位图方式记录了系统中存在任务的优先级*/
\
/* 将遍历的起始优先级设置为这个全局变量,*/
\
/* 而无需从系统支持优先级的最大值开始遍历,*/
\
/* 可以节约一定的遍历时间*/
\
UBaseType_t uxTopPriority = uxTopReadyPriority;\\
/* Find the highest priority queue that contains ready tasks. */
\
/* 按照优先级从高到低,判断对应的就绪态任务列表中是否由任务,*/
\
/* 找到存在任务的最高优先级就绪态任务列表后,退出遍历*/
\
while (listLIST_IS_EMPTY( & (pxReadyTasksLists[uxTopPriority])))\ {\
configASSERT(uxTopPriority);\
--uxTopPriority;\
}\\
/* 从找到了就绪态任务列表中取下一个任务,*/
\
/* 让pxCurrentTCB指向这个任务的任务控制块*/
\
listGET_OWNER_OF_NEXT_ENTRY(pxCurrentTCB, \ & (pxReadyTasksLists[uxTopPriority]));\
/* 更新任务优先级记录*/
\
uxTopReadyPriority = uxTopPriority;\
}
依靠特定硬件指令实现的函数taskSELECT_HIGHEST_PRIORITY_TASK()是一个宏定义,
在task.c 文件中有定义,具体的代码如下所示:
#define taskSELECT_HIGHEST_PRIORITY_TASK() \
{ \
UBaseType_t uxTopPriority; \
\
/* 使用硬件方式从任务优先级记录中获取最高的任务优先等级*/ \
portGET_HIGHEST_PRIORITY( uxTopPriority, uxTopReadyPriority ); \
configASSERT( listCURRENT_LIST_LENGTH( \
&( pxReadyTasksLists[ uxTopPriority ] ) ) > \
0 ); \
/* 从获取的任务优先级对应的就绪态任务列表中取下一个任务*/ \
/* 让pxCurrentTCB指向这个任务的任务控制块*/ \
listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, \
&( pxReadyTasksLists[ uxTopPriority ] ) ); \
}
在使用硬件方式实现的函数taskSELECT_HIGHEST_PRIORITY_TASK()中调用了函数
portGET_HIGHEST_PRIORITY() 来计算任务优先级记录中的最高任务优先级,函数
portGET_HIGHEST_PRIORITY()实际上是一个宏定义,在portmacro.h 文件中有定义,具体的代
码如下所示:
#define portGET_HIGHEST_PRIORITY( uxTopPriority, uxReadyPriorities ) \
uxTopPriority = \
( 31UL - ( uint32_t ) __clz( ( uxReadyPriorities ) ) )
可以看到,宏portGET_HIGHEST_PRIORITY() 使用了__clz 这个硬件指定来计算
uxReadyPriorities 的前导零,然后使用31(变量uxReadyPriorities 的最大比特位)减去得到的前
导零,那么就得到了变量uxReadyPriorities 中,最高位1 的比特位。使用此方法就限制了系统
最大的优先级数量不能超多32,即最高优先等级位31,不过对于绝大多数的应用场合,32 个
任务优先级等级已经足够使用了。
PendSV 异常何时触发
PendSV 异常用于进行任务切换,当需要进行任务切换的时候,FreeRTOS 就会触发PendSV
异常,以进行任务切换。
FreeRTOS 提供了多个用于触发任务切换的宏,如下所示:
#
if (configUSE_PREEMPTION == 0)# define taskYIELD_IF_USING_PREEMPTION()#
else# define taskYIELD_IF_USING_PREEMPTION() portYIELD_WITHIN_API()# endif#
if (configUSE_PREEMPTION == 0)# define queueYIELD_IF_USING_PREEMPTION()#
else# define queueYIELD_IF_USING_PREEMPTION() portYIELD_WITHIN_API()# endif# define portEND_SWITCHING_ISR(xSwitchRequired)\
do\ {\
if (xSwitchRequired != pdFALSE)\
portYIELD();\
}\
while (0)# define portYIELD_FROM_ISR(x) portEND_SWITCHING_ISR(x)# define taskYIELD() portYIELD()# define portYIELD_WITHIN_API portYIELD
从上面的代码中可以看到,这些后实际上最终都是调用了函数portYIELD(),函数实际上是
一个宏定义,在portmacro.h 文件中有定于,具体的代码如下所示:
#define portYIELD() \
{ \
/* 设置中断控制状态寄存器,以触发PendSV异常*/ \
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; \
\
__dsb( portSY_FULL_READ_WRITE ); \
__isb( portSY_FULL_READ_WRITE ); \
}
上面代码中宏portNVIC_INT_CTRL_REG 和宏portNVIC_PENDSVSET_BIT 在portmacro.h
文件中有定义,具体的代码如下所示:
#define portNVIC_INT_CTRL_REG ( *( ( volatile uint32_t * ) 0xe000ed04 ) )
#define portNVIC_PENDSVSET_BIT ( 1UL << 28UL )
中断控制状态寄存器的有关内容,请参考第4.1.5 小节《中断控制状态寄存器》。
FreeRTOS 时间片调度实验
功能设计
- 例程功能
本实验主要用于学习FreeRTOS 的时间片调度,了解FreeRTOS 任务切换的结果,时间片调
度的相关内容,请参考第5.4.2 小节《时间片调度》。本实验设计了三个任务,这三个任务的功
能如下表所示:
该实验的实验工程,请参考《FreeRTOS 实验例程9 FreeRTOS 时间片调度实验》。
软件设计
- 程序流程图
本实验的程序流程图,如下图所示:
- FreeRTOS 函数解析
(1) 函数taskENTER_CRITICAL()
此函数是一个宏定义,此宏的具体解析,请参考4.3.3 小节《FreeRTOS 进出临界区》。
(2) 函数taskEXIT_CRITICAL()
此函数是一个宏定义,此宏的具体解析,请参考4.3.3 小节《FreeRTOS 进出临界区》。 - 程序解析
整体的代码结构,请参考2.1.6 小节,本小节着重讲解本实验相关的部分。
(1) start_task 任务
start_task 任务的入口函数代码如下所示:
/**
* @brief start_task
* @param pvParameters : 传入参数(未用到)
* @retval 无
*/
void start_task(void * pvParameters) {
taskENTER_CRITICAL(); /* 进入临界区*/
/* 创建任务1 */
xTaskCreate((TaskFunction_t) task1, (const char * )
"task1", (uint16_t) TASK1_STK_SIZE, (void * ) NULL, (UBaseType_t) TASK1_PRIO, (TaskHandle_t * ) & Task1Task_Handler);
/* 创建任务2 */
xTaskCreate((TaskFunction_t) task2, (const char * )
"task2", (uint16_t) TASK2_STK_SIZE, (void * ) NULL, (UBaseType_t) TASK2_PRIO, (TaskHandle_t * ) & Task2Task_Handler);
vTaskDelete(StartTask_Handler); /* 删除开始任务*/
taskEXIT_CRITICAL(); /* 退出临界区*/
}
start_task 任务主要用于创建task1 和task2 任务,这里要注意的是,由于本实验要展示的是
FreeRTOS 的时间片调度,时间片调度是对于任务优先级相同的多个任务而言的,因此创建用于
测试FreeRTOS 时间片调度的task1 和task2 任务的任务优先级必须相同。
(2) task1 和task2 任务
/**
* @brief task1
* @param pvParameters : 传入参数(未用到)
* @retval 无
*/
void task1(void * pvParameters) {
uint32_t task1_num = 0;
while (1) {
taskENTER_CRITICAL();
printf("任务1运行次数: %d\r\n", ++task1_num);
taskEXIT_CRITICAL();
}
}
/**
* @brief task2
* @param pvParameters : 传入参数(未用到)
* @retval 无
*/
void task2(void * pvParameters) {
uint32_t task2_num = 0;
while (1) {
taskENTER_CRITICAL();
printf("任务2运行次数: %d\r\n", ++task2_num);
taskEXIT_CRITICAL();
}
}
可以看到,task1 和task2 任务都是循环打印任务运行的次数,并没有进行任务延时,因此
task1 和task2 任务会由于时间片调度,在任务调度器的调度下轮询运行。值得一提的是,在打
印任务运行次数的时候,需要使用到串口硬件,为了避免多个任务“同时”使用同一个硬件,
因此在使用串口硬件打印任务运行次数之前,进入临界区,在使用串口硬件打印任务运行次数
之后,再退出临界区。
由于task1 和task2 的任务优先级相同,因此可以猜测,在时间片的调度下,task1 和task2
任务应该轮询打印各自的任务运行次数。
下载验证
编译并下载代码,复位后可以看到LCD 屏幕上显示了本次实验的相关信息,如下图所示:
同时,通过串口调试助手就能看到本次实验的结果,如下图所示:
从上图可以看到,task1 和task2 任务的运行情况,与猜测的相同,task1 和task2 任务交替
轮询运行,符合时间片调度下的运行情况。
FreeRTOS 内核控制函数
FreeRTOS 提供了一些用于控制内核的API 函数,这些API 函数主要包含了进出临界区、
开关中断、启停任务调度器等一系列用于控制内核的API 函数。本章就来学习FreeRTOS 的内
核控制函数。
FreeRTOS 内核控制函数预览
在FreeRTOS 官方在线文档的网页页面中,可以看到官方列出的FreeRTOS 内核控制函数,
如下图所示:
以上FreeRTOS 内核的控制函数描述,如下表所示:
FreeRTOS 内核控制函数详解
- 函数taskYIELD()
此函数用于请求切换任务,调用后会触发PendSV 中断,请参考第9.4 小节《PendSV 异常
何时触发》。 - 函数taskENTER_CRITICAL()
此函数用于在任务中进入临界区,请参考第4.3.3 小节《FreeRTOS 进出临界区》。 - 函数taskEXIT_CRITICAL()
此函数用于在任务中退出临界区,请参考第4.3.3 小节《FreeRTOS 进出临界区》。 - 函数taskENTER_CRITICAL_FROM_ISR()
此函数用于在中断服务函数中进入临界区,请参考第4.3.3 小节《FreeRTOS 进出临界区》。 - 函数taskEXIT_CRITICAL_FROM_ISR()
此函数用于在中断服务函数中退出临界区,请参考第4.3.3 小节《FreeRTOS 进出临界区》。 - 函数taskDISABLE_INTERRUPTS()
此函数用于关闭受FreeRTOS 管理的中断,请参考第4.3.2 小节《FreeRTOS 开关中断》。 - 函数taskENABLE_INTERRUPTS()
此函数用于开启所有中断,请参考第4.3.2 小节《FreeRTOS 开关中断》。 - 函数vTaskStartScheduler()
此函数用于开启任务调度器,请参考第8.1.1 小节《函数vTaskStartScheduler()》。 - 函数vTaskEndScheduler()
此函数用于关闭任务调度器,要注意的是,此函数只适用于X86 架构的PC 端。对于STM32
平台,调用此函数会关闭受FreeRTOS 管理的中断,并强制触发断言。代码如下所示:
void vTaskEndScheduler(void) {
/* 关闭受FreeRTOS管理的中断*/
portDISABLE_INTERRUPTS();
/* 标记任务调度器未运行*/
xSchedulerRunning = pdFALSE;
vPortEndScheduler();
}
void vPortEndScheduler(void) {
/* 强制断言*/
configASSERT(uxCriticalNesting == 1000 UL);
}
- 函数vTaskSuspendAll()
此函数用于挂起任务调度器,当任务调度器被挂起后,就不能进行任务切换,直到任务调
度器恢复运行。此函数的代码如下所示:
void vTaskSuspendAll( void )
{
/* 未定义,不用理会*/
portSOFTWARE_BARRIER();
/* 任务调度器挂起计数器加1 */
++uxSchedulerSuspended;
/* 未定义,不用理会*/
portMEMORY_BARRIER();
}
从上面的代码可以看出,函数vTaskSuspendAll()挂起任务调度器的操作是可以递归的,也
就是说,可以重复多次挂起任务调度器,只要后续调用相同次数的函数xTaskResumeAll()来恢
复任务调度器运行即可。函数vTaskSuspendAll()挂起任务调度器的操作就是将任务调度器挂起
计数器(uxSchedulerSuspended)的值加1。在FreeRTOS 的源码中会通过任务调度器挂起计数
器的值是否为0,来判断任务调度器时候被挂起,如果任务调度器被挂起,FreeRTOS 就不会进
行任务切换等操作,如函数vTaskSwitchContext() ,请参考第9.3.1 小节《函数
vTaskSwitchContext()》。
11. 函数xTaskResumeAll()
此函数用于恢复任务调度器运行,要注意的是,任务调度器的挂起是可递归的,因此需要
使用此函数恢复任务调度器与任务调度器被挂起相同的次数,才能恢复任务调度器运行。此函
数的代码如下所示:
BaseType_t xTaskResumeAll(void) {
TCB_t * pxTCB = NULL;
BaseType_t xAlreadyYielded = pdFALSE;
/* 不会恢复没有被挂起的任务调度器
* 当uxSchedulerSuspended为0时,
* 表示任务调度器没有被挂起
*/
configASSERT(uxSchedulerSuspended);
/* 进入临界区*/
taskENTER_CRITICAL(); {
/* 任务调度器挂起计数器减1 */
--uxSchedulerSuspended;
/* 如果任务调度器挂起计数器减到0
* 说明任务调度器可以恢复运行了
*/
if (uxSchedulerSuspended == (UBaseType_t) pdFALSE) {
/* 任务数量计数器大于0
* 说明系统中有任务,
* 因此需要作向相应地处理
*/
if (uxCurrentNumberOfTasks > (UBaseType_t) 0 U) {
/* 将所有挂起态任务添加到就绪态任务列表中
* 同时,如果被恢复的挂起态任务的优先级比当前运行任务的优先级高,
* 则标记需要进行任务切换
*/
while (listLIST_IS_EMPTY( & xPendingReadyList) == pdFALSE) {
pxTCB =
listGET_OWNER_OF_HEAD_ENTRY(( & xPendingReadyList));
listREMOVE_ITEM( & (pxTCB - > xEventListItem));
portMEMORY_BARRIER();
listREMOVE_ITEM( & (pxTCB - > xStateListItem));
prvAddTaskToReadyList(pxTCB);
if (pxTCB - > uxPriority >= pxCurrentTCB - > uxPriority) {
xYieldPending = pdTRUE;
} else {
mtCOVERAGE_TEST_MARKER();
}
}
/* 如果pxTCB非空,
* 则表示在任务调度器挂起期间,
* 有阻塞任务超时,
* 因此需要重新计算下一个任务阻塞超时的时间
*/
if (pxTCB != NULL) {
/* 重新计算下一个任务的阻塞超时时间*/
prvResetNextTaskUnblockTime();
}
/* 处理在任务调度器挂起期间,未处理的系统使用节拍
* 这样可以保证正确地计算阻塞任务的阻塞超时时间
* 处理方式就是调用相同次数的函数xTaskIncrementTick()
*/
{
TickType_t xPendedCounts = xPendedTicks;
if (xPendedCounts > (TickType_t) 0 U) {
do {
/* 调用函数xTaskIncrementTick() */
if (xTaskIncrementTick() != pdFALSE) {
xYieldPending = pdTRUE;
} else {
mtCOVERAGE_TEST_MARKER();
}
--xPendedCounts;
} while (xPendedCounts > (TickType_t) 0 U);
xPendedTicks = 0;
} else {
mtCOVERAGE_TEST_MARKER();
}
}
/* 根据需要进行任务切换*/
if (xYieldPending != pdFALSE) {#
if (configUSE_PREEMPTION != 0) {
xAlreadyYielded = pdTRUE;
}#
endif
taskYIELD_IF_USING_PREEMPTION();
} else {
mtCOVERAGE_TEST_MARKER();
}
}
} else {
mtCOVERAGE_TEST_MARKER();
}
}
/* 退出临界区*/
taskEXIT_CRITICAL();
return xAlreadyYielded;
}
- 函数vTaskStepTick()
此函数用于设置系统时钟节拍计数器的值,可以设置系统时钟节拍计数器的值为当前值加
上指定值,不过要注意的值,更新后系统时钟节拍计数器的值,不能超过下一个任务阻塞超时
时间。具体的代码如下所示:
void vTaskStepTick( const TickType_t xTicksToJump )
{
/* 系统使用节拍计数器更新后的值
* 不能超过下一个任务阻塞超时时间
*/
configASSERT( ( xTickCount + xTicksToJump ) <= xNextTaskUnblockTime );
/* 更新系统时钟节拍计数器*/
xTickCount += xTicksToJump;
/* 用于调试,不用理会*/
traceINCREASE_TICK_COUNT( xTicksToJump );
}
- 函数xTaskCatchUpTicks()
此函数用于修正中断后的系统时钟节拍,主要是用过更新全局变量xPendedTicks 实现的,
全局变量xPendedTicks 用于计数系统使用节拍在任务调度器挂起时被忽略处理的次数。具体的
代码如下所示:
BaseType_t xTaskCatchUpTicks(TickType_t xTicksToCatchUp) {
BaseType_t xYieldOccurred;
/* 该函数不能在任务调度器被挂起期间被调用*/
configASSERT(uxSchedulerSuspended == 0);
/* 挂起任务调度器*/
vTaskSuspendAll();
/* 更新xPendedTicks */
xPendedTicks += xTicksToCatchUp;
/* 恢复任务调度器运行*/
xYieldOccurred = xTaskResumeAll();
return xYieldOccurred;
}
FreeRTOS 其他任务API 函数
通过前面几章的学习,了解了FreeRTOS 任务管理的相关内容,但仅涉及了任务创建、删
除、挂起和恢复等几个任务相关的API 函数。除此之外,FreeRTOS 还提供了很多与任务相关的
API 函数,通过这些函数,用户可以更加灵活地使用FreeRTOS。本章就来学习FreeRTOS 中一
些其他的任务API 函数。
FreeRTOS 任务相关API 函数
FreeRTOS 任务相关API 函数预览
在FreeRTOS 官方在线文档的网页页面中,通过查看API 参考,可以看到官方列出的
FreeRTOS 任务相关的API 函数,如下图所示:
以上部分FreeRTOS 任务相关的API 函数描述,如下表所示:
FreeRTOS 任务相关API 函数详解
-
函数uxTaskPriorityGet()
此函数用于获取指定任务的任务优先级,若使用此函数,需在FreeRTOSConfig.h 文件中设
置配置项INCLUDE_uxTaskPriorityGet 为1,此函数的函数原型如下所示:
UBaseType_t uxTaskPriorityGet(const TaskHandle_t xTask);
-
函数vTaskPrioritySet()
此函数用于设置指定任务的优先级,若使用此函数,需在FreeRTOSConfig.h 文件中设置配
置项INCLUDE_vTaskPrioritySet 为1,此函数的函数原型如下所示:
void vTaskPrioritySet(
TaskHandle_t xTask,
UBaseType_t uxNewPriority);
函数vTaskPrioritySet()的形参描述,如下表所示:
函数vTaskPrioritySet()无返回值。
3. 函数uxTaskGetSystemState()
此函数用于获取所有任务的状态信息,若使用此函数,需在FreeRTOSConfig.h 文件中设置
配置项configUSE_TRACE_FACILITY 为1,此函数的函数原型如下所示:
UBaseType_t uxTaskGetSystemState(
TaskStatus_t * const pxTaskStatusArray,
const UBaseType_t uxArraySize,
configRUN_TIME_COUNTER_TYPE * const pulTotalRunTime);
函数uxTaskGetSystemState()的形参描述,如下表所示:
函数uxTaskGetSystemState()的返回值,如下表所示:
函数uxTaskGetSystemState()的形参pxTaskStatusArray 指向变量类型为TaskStatus_t 的变量
的首地址,可以是一个数组,用来存放多个TaskStatus_t 类型的变量,函数uxTaskGetSystemState()
使用将任务的状态信息,写入到该数组中,形参uxArraySize 指示该数组的大小,其中变量类型
TaskStatus_t 的定义如下所示:
typedef struct xTASK_STATUS {
TaskHandle_t xHandle; /* 任务句柄*/
const char * pcTaskName; /* 任务名*/
UBaseType_t xTaskNumber; /* 任务编号*/
eTaskState eCurrentState; /* 任务状态*/
UBaseType_t uxCurrentPriority; /* 任务优先级*/
UBaseType_t uxBasePriority; /* 任务原始优先级*/
configRUN_TIME_COUNTER_TYPE ulRunTimeCounter; /* 任务被分配的运行时间*/
StackType_t * pxStackBase; /* 任务栈的基地址*/
configSTACK_DEPTH_TYPE usStackHighWaterMark; /* 任务栈历史剩余最小值*/
}
TaskStatus_t;
该结构体变量就包含了任务的一些状态信息,获取到的每个任务都有与之对应的
TaskStatus_t 结构体来保存该任务的状态信息。
4. 函数vTaskGetInfo()
此函数用于获取指定任务的任务信息,若使用此函数,需在FreeRTOSConfig.h 文件中设置
配置项configUSE_TRACE_FACILITY 为1,此函数的函数原型如下所示:
void vTaskGetInfo(
TaskHandle_t xTask,
TaskStatus_t * pxTaskStatus,
BaseType_t xGetFreeStackSpace,
eTaskState eState);
函数vTaskGetInfo()的形参描述,如下表所示: