9.任务调度

一、开启任务调度器

在这里插入图片描述
在这里插入图片描述

1.函数 vTaskStartScheduler()

在这里插入图片描述
函数 vTaskStartScheduler()用于启动任务调度器,任务调度器启动后,FreeRTOS 便会开始
进行任务调度,除非调用函数 xTaskEndScheduler()停止任务调度器,否则不会再返回。函数
vTaskStartScheduler()的代码如下所示:

void vTaskStartScheduler( void )
{
 BaseType_t xReturn;
 
 /* 如果启用静态内存管理,则优先使用静态方式创建空闲任务 */
#if ( configSUPPORT_STATIC_ALLOCATION == 1 )
{
 StaticTask_t * pxIdleTaskTCBBuffer = NULL;
 StackType_t * pxIdleTaskStackBuffer = NULL;
 uint32_t ulIdleTaskStackSize;
 
 vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer,
 &pxIdleTaskStackBuffer,
 &ulIdleTaskStackSize);
 xIdleTaskHandle = xTaskCreateStatic( prvIdleTask,
 configIDLE_TASK_NAME,
 ulIdleTaskStackSize,
 ( void * ) NULL,
 portPRIVILEGE_BIT,
 pxIdleTaskStackBuffer,
 pxIdleTaskTCBBuffer);
 
 if( xIdleTaskHandle != NULL )
 {
 xReturn = pdPASS;
 }
 else
 {
 xReturn = pdFAIL;
 }
}
#else
 /* 未启用静态内存管理,则使用动态方式创建空闲任务 */
{
 xReturn = xTaskCreate( prvIdleTask,
 configIDLE_TASK_NAME,
 configMINIMAL_STACK_SIZE,
 ( void * ) NULL,
 portPRIVILEGE_BIT,
 &xIdleTaskHandle);
 }
#endif
 
 /* 如果启用软件定时器,则需要创建定时器服务任务 */
#if ( configUSE_TIMERS == 1 )
{
 if( xReturn == pdPASS )
 {
 xReturn = xTimerCreateTimerTask();
 }
 else
 {
 mtCOVERAGE_TEST_MARKER();
 }
}
#endif
 
 if( xReturn == pdPASS )
 {
#ifdef FREERTOS_TASKS_C_ADDITIONS_INIT
{
 /* 此函数用于添加一些附加初始化,不用理会 */
 freertos_tasks_c_additions_init();
}
#endif
 
 /* FreeRTOS 关闭中断,
 * 以保证在开启任务任务调度器之前或过程中,SysTick 不会产生中断,
 * 在第一个任务开始运行时,会重新打开中断。
 */
 portDISABLE_INTERRUPTS();
 
#if ( configUSE_NEWLIB_REENTRANT == 1 )
{
 /* Newlib 相关 */
 _impure_ptr = &( pxCurrentTCB->xNewLib_reent );
}
#endif
 /* 初始化一些全局变量
 * xNextTaskUnblockTime: 下一个距离取消任务阻塞的时间,初始化为最大值
 * xSchedulerRunning: 任务调度器运行标志,设为已运行
 * xTickCount: 系统使用节拍计数器,宏 configINITIAL_TICK_COUNT 默认为 0
 * */
 xNextTaskUnblockTime = portMAX_DELAY;
 xSchedulerRunning = pdTRUE;
 xTickCount = ( TickType_t ) configINITIAL_TICK_COUNT;
 
 /* 为任务运行时间统计功能初始化功能时基定时器
 * 是否启用该功能,可在 FreeRTOSConfig.h 文件中进行配置
 */
 portCONFIGURE_TIMER_FOR_RUN_TIME_STATS();
 /* 调试使用,不用理会 */
 traceTASK_SWITCHED_IN();
 
 /* 设置用于系统时钟节拍的硬件定时器(SysTick)
 * 会在这个函数中进入第一个任务,并开始任务调度
 * 任务调度开启后,便不会再返回
 */
 if( xPortStartScheduler() != pdFALSE )
 {
 }
 else
 {
 
 }
 }
 else
 {
 /* 动态方式创建空闲任务和定时器服务任务(如果有)时,因分配给 FreeRTOS 的堆空间
 * 不足,导致任务无法成功创建 */
 configASSERT( xReturn != errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY );
 }
 
 /* 防止编译器警告,不用理会 */
 ( void ) xIdleTaskHandle;
 
 /* 调试使用,不用理会 */
 ( void ) uxTopUsedPriority;
}

从上面的代码可以看出,函数 vTaskStartScheduler()主要做了六件事情。

  1. 创建空闲任务,根据是否支持静态内存管理,使用静态方式或动态方式创建空闲任务。
  2. 创建定时器服务任务,创建定时器服务任务需要配置启用软件定时器,创建定时器服务
    任务,同样是根据是否配置支持静态内存管理,使用静态或动态方式创建定时器服务任务。
  3. 关闭中断,使用 portDISABLE_INTERRUPT()关闭中断,这种方式只关闭受 FreeRTOS 管
    理的中断。关闭中断主要是为了防止 SysTick 中断在任务调度器开启之前或过程中,产生中断。
    FreeRTOS 会在开始运行第一个任务时,重新打开中断。
  4. 初始化一些全局变量,并将任务调度器的运行标志设置为已运行。
  5. 初始化任务运行时间统计功能的时基定时器,任务运行时间统计功能需要一个硬件定时
    器提供高精度的计数,这个硬件定时器就在这里进行配置,如果配置不启用任务运行时间统计
    功能的,就无需进行这项硬件定时器的配置。
  6. 最后就是调用函数 xPortStartScheduler()。

2.函数xPortStartScheduler()

在这里插入图片描述
函数 xPortStartScheduler()完成启动任务调度器中与硬件架构相关的配置部分,以及启动第
一个任务,具体的代码如下所示:

BaseType_t xPortStartScheduler( void )
{
#if ( configASSERT_DEFINED == 1 )
{
 /* 检测用户在 FreeRTOSConfig.h 文件中对中断相关部分的配置是否有误,代码省略 */
}
#endif
 
 /* 设置 PendSV 和 SysTick 的中断优先级为最低优先级 */
 portNVIC_SHPR3_REG |= portNVIC_PENDSV_PRI;
 portNVIC_SHPR3_REG |= portNVIC_SYSTICK_PRI;
 
 /* 配置 SysTick
 * 清空 SysTick 的计数值
 * 根据 configTICK_RATE_HZ 配置 SysTick 的重装载值
 * 开启 SysTick 计数和中断
 */
 vPortSetupTimerInterrupt();
 
 /* 初始化临界区嵌套次数计数器为 0 */
 uxCriticalNesting = 0;
 
 /* 使能 FPU
 * 仅 ARM Cortex-M4/M7 内核 MCU 才有此行代码
 * ARM Cortex-M3 内核 MCU 无 FPU
 */
 prvEnableVFP();
 
 /* 在进出异常时,自动保存和恢复 FPU 相关寄存器
 * 仅 ARM Cortex-M4/M7 内核 MCU 才有此行代码
 * ARM Cortex-M3 内核 MCU 无 FPU
 */
 *( portFPCCR ) |= portASPEN_AND_LSPEN_BITS;
 
 /* 启动第一个任务 */
 prvStartFirstTask();
 
 /* 不会返回这里 */
 return 0;
}

函数 xPortStartScheduler()的解析如下所示:

  1. 在启用断言的情况下,函数 xPortStartScheduler()会检测用户在 FreeRTOSConfig.h 文件
    中对中断的相关配置是否有误,感兴趣的读者请自行查看这部分的相关代码。
  2. 配置 PendSV 和 SysTick 的中断优先级为最低优先级,请参考 4.3.1 小节。
  3. 调用函数 vPortSetupTimerInterrupt()配置 SysTick,函数 vPortSetupTimerInterrupt()首先会
    将 SysTick 当 前 计 数 值 清 空 , 并 根 据 FreeRTOSConfig.h 文件中配置的
    configSYSTICK_CLOCK_HZ(SysTick 时钟源频率)和 configTICK_RATE_HZ(系统时钟节拍
    频率)计算并设置 SysTick 的重装载值,然后启动 SysTick 计数和中断。
  4. 初始化临界区嵌套计数器为 0。
  5. 调用函数 prvEnableVFP()使能 FPU,因为 ARM Cortex-M3 内核 MCU 无 FPU,此函数
    仅在 ARM Cortex-M4/M7 内核 MCU 平台上被调用,执行改函数后 FPU 被开启。
  6. 接下来将 FPCCR 寄存器的[31:30]置 1,这样在进出异常时,FPU 的相关寄存器就会自
    动地保存和恢复,同样地,因为 ARM Cortex-M3 内核 MCU 无 FPU,此当代码仅在 ARM Cortex-M4/M7 内核 MCU 平台上被调用。
  7. 调用函数 prvStartFirstTask()启动第一个任务。

二、FreeRTOS 启动第一个任务

在这里插入图片描述
在这里插入图片描述

1.函数 prvStartFirstTask()

函数 prvStartFirstTask()用于初始化启动第一个任务前的环境,主要是重新设置 MSP 指针,
并使能全局中断,具体的代码如下所示:

__asm void prvStartFirstTask( void )
{
 /* 8 字节对齐 */
 PRESERVE8
 
 ldr r0, =0xE000ED08 /* 0xE000ED08 为 VTOR 地址 */
 ldr r0, [ r0 ] /* 获取 VTOR 的值 */
 ldr r0, [ r0 ] /* 获取 MSP 的初始值 */
 
 /* 初始化 MSP */
 msr msp, r0
 /* 使能全局中断 */
 cpsie i
 cpsie f
 dsb
 isb
 
 /* 调用 SVC 启动第一个任务 */
 svc 0
 nop
 nop
}

从上面的代码可以看出,函数 prvStartFirstTask()是一段汇编代码,解析如下所示:

  1. 首先是使用了 PRESERVE8,进行 8 字节对齐,这是因为,栈在任何时候都是需要 4 字
    节对齐的,而在调用入口得 8 字节对齐,在进行 C 编程的时候,编译器会自动完成的对齐的操
    作,而对于汇编,就需要开发者手动进行对齐。

  2. 接下来的三行代码是为了获得 MSP 指针的初始值,那么这里就能够引出两个问题:
    (1) 什么是 MSP 指针?
    程序在运行过程中需要一定的栈空间来保存局部变量等一些信息。当有信息保存到栈中时,
    MCU 会自动更新 SP 指针,使 SP 指针指向最后一个入栈的元素,那么程序就可以根据 SP 指针
    来从栈中存取信息。对于正点原子的 STM32F1、STM32F4、STM32F7 和 STM32H7 开发板上使
    用的 ARM Cortex-M 的 MCU 内核来说,ARM Cortex-M 提供了两个栈空间,这两个栈空间的堆
    栈指针分别是 MSP(主堆栈指针)和 PSP(进程堆栈指针)。在 FreeRTOS 中 MSP 是给系统栈
    空间使用的,而 PSP 是给任务栈使用的,也就是说,FreeRTOS 任务的栈空间是通过 PSP 指向
    的,而在进入中断服务函数时,则是使用 MSP 指针。当使用不同的堆栈指针时,SP 会等于当
    前使用的堆栈指针。
    在这里插入图片描述

(2) 为什么是 0xE00ED08?
0xE00ED08 是 VTOR(向量表偏移寄存器)的地址,VTOR 中保存了向量表的偏移地址。
一般来说向量表是从其实地址 0x00000000 开始的,但是在有情况下,可能需要修改或重定向向
量表的首地址,因此 ARM Corten-M 提供了 VTOR 对向量表进行从定向。而向量表是用来保存
中断异常的入口函数地址,即栈顶地址的,并且向量表中的第一个字保存的就是栈底的地址,
在 start_stm32xxxxxx.s 文件中有如下定义:

__Vectors DCD __initial_sp ; 栈底指针
 DCD Reset_Handler ; Reset Handler
 DCD NMI_Handler ; NMI Handler
 DCD HardFault_Handler ; Hard Fault Handler
 DCD MemManage_Handler ; MPU Fault Handler

以上就是向量表(只列出前几个)的部分内容,可以看到向量表的第一个元素就是栈指针
的初始值,也就是栈底指针。

在了解了这两个问题之后,接下来再来看看代码。首先是获取 VTOR 的地址,接着获取
VTOR 的值,也就是获取向量表的首地址,最后获取向量表中第一个字的数据,也就是栈底指
针了。
在这里插入图片描述

  1. 在获取了栈顶指针后,将 MSP 指针重新赋值为栈底指针。这个操作相当于丢弃了程序
    之前保存在栈中的数据,因为FreeRTOS从开启任务调度器到启动第一个任务都是不会返回的,
    是一条不归路,因此将栈中的数据丢弃,也不会有影响。
  2. 重新赋值 MSP 后,接下来就重新使能全局中断,因为之前在函数 vTaskStartScheduler()
    中关闭了受 FreeRTOS 的中断。
  3. 最后使用 SVC 指令,并传入系统调用号 0,触发 SVC 中断。

2.函数 vPortSVCHandler()

在这里插入图片描述
在这里插入图片描述

当使能了全局中断,并且手动触发 SVC 中断后,就会进入到 SVC 的中断服务函数中。SVC
的中断服务函数为 vPortSVCHandler(),该函数在 port.c 文件中有定义,具体的代码如下所示
:

__asm void vPortSVCHandler( void )
{
 /* 8 字节对齐 */
 PRESERVE8
 
 /* 获取任务栈地址 */
 ldr r3, = pxCurrentTCB /* r3 指向优先级最高的就绪态任务的任务控制块 */
 ldr r1, [ r3 ] /* r1 为任务控制块地址 */
 ldr r0, [ r1 ] /* r0 为任务控制块的第一个元素(栈顶) */
 
 /* 模拟出栈,并设置 PSP */
 ldmia r0 !, { r4 - r11 } /* 任务栈弹出到 CPU 寄存器 */
 msr psp, r0 /* 设置 PSP 为任务栈指针 */
 isb
 
 /* 使能所有中断 */
 mov r0, # 0
 msr basepri,
 
 /* 使用 PSP 指针,并跳转到任务函数 */
 orr r14, # 0xd
 bx r14
}

从上面代码中可以看出,函数 vPortSVCHandler()就是用来跳转到第一个任务函数中去的,
该函数的具体解析如下:

  1. 首先通过 pxCurrentTCB 获取优先级最高的就绪态任务的任务栈地址,优先级最高的就
    绪态任务就是系统将要运行的任务。pxCurrentTCB 是一个全局变量,用于指向系统中优先级最
    高的就绪态任务的任务控制块,在前面创建 start_task 任务、空闲任务、定时器处理任务时自动
    根据任务的优先级高低进行赋值的,具体的赋值过程在后续分析任务创建函数时,会具体分析。
    这里举个例子,在《FreeRTOS 移植实验》中,start_task 任务、空闲任务、定时器处理任务
    的优先级如下表所示:
    在这里插入图片描述
    在这里插入图片描述
    从上表可以看出,在《FreeRTOS 移植实验》中,定时器处理任务的任务优先级为 31,是系
    统中优先级最高的任务,因此当进入 SVC 中断时,pxCurrentTCB 就是指向了定时器处理任务
    的任务控制块。

接着通过获取任务控制块中的第一个元素,得到该任务的栈顶指针,任务控制块的相关内
容,请查看第 5.5 小节《FreeRTOS 任务控制块》。

  1. 接下来通过任务的栈顶指针,将任务栈中的内容出栈到 CPU 寄存器中,任务栈中的内
    容在调用任务创建函数的时候,已经初始化了。然后再设置 PSP 指针,那么,这么一来,任务
    的运行环境就准备好了。
  2. 通过往 BASEPRI 寄存器中写 0,允许中断。
  3. 最后通过两条汇编指令,使 CPU 跳转到任务的函数中去执行,代码如下所示:
orr r14, # 0xd
 bx r14

要弄清楚这两条汇编代码,首先要清楚 r14 寄存器是干什么用的。通常情况下,r14 为链接
寄存器(LR),用于保存函数的返回地址。但是在异常或中断处理函数中,r14 为 EXC_RETURN
(关于 r14 寄存器的相关内容,感兴趣的读者请自行查阅相关资料),EXC_RETURN 各比特位
的描述如下表所示:
在这里插入图片描述
因为此时是在 SVC 的中断服务函数中,因此此时的 r14 应为 EXC_RETURN,将 r14 与 0xd
作或操作,然后将值写入 r14,那么就是将 r14 的值设置为了 0xFFFFFFED 或 0xFFFFFFED(具
体看是否使用了浮点单元),即返回后进入线程模式,并使用 PSP。这里要注意的是,SVC 中断
服务函数的前面,将 PSP 指向了任务栈。

说了这么多,FreeRTOS 对于进入中断后 r14 为 EXC_RETURN 的具体应用就是,通过判断
EXC_RETURN 的 bit4 是否为 0,来判断任务是否使用了浮点单元。

最后通过 bx r14 指令,跳转到任务的任务函数中执行,执行此指令,CPU 会自动从 PSP 指
向的栈中出栈 R0、R1、R2、R3、R12、LR、PC、xPSR 寄存器,并且如果 EXC_RETURN 的
bit4 为 0(使用了浮点单元),那么 CPU 还会自动恢复浮点寄存器。

三、任务切换

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

1.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 的中断服务函数中执行任务切换,如
下图所示:
在这里插入图片描述

  1. 任务一触发 SVC 中断以进行任务切换(例如,任务一正等待某个事件发生)。
  2. 系统内核接收到任务切换请求,开始准备任务切换,并挂起 PendSV 异常。
  3. 当退出 SVC 中断的时候,立刻进入 PendSV 异常处理,完成任务切换。
  4. 当 PendSV 异常处理完成,返回线程模式,开始执行任务二。
  5. 中断产生,并进入中断处理函数。
  6. 当运行中断处理函数的时候,SysTick 异常(用于内核时钟节拍)产生。
  7. 操作系统执行必要的操作,然后挂起 PendSV 异常,准备进行任务切换。
  8. 当 SysTick 中断处理完成,返回继续处理中断。
  9. 当中断处理完成,立马进入 PendSV 异常处理,完成任务切换。
  10. 当 PendSV 异常处理完成,返回线程模式,继续执行任务一。
    PendSV在RTOS的任务切换中,起着至关重要的作用,FreeRTOS的任务切换就是在PendSV
    中完成的。

2.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 指针为该任务的任务堆栈指针。

3.函数 vTaskSwitchContext()

在这里插入图片描述
在这里插入图片描述

在 PendSV 的中断服务函数中,调用了函数 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 设置为指向优先级最高的就绪态任务。

4.函数 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 个
任务优先级等级已经足够使用了。

  • 15
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值