1.开启任务调度器
vTaskStartScheduler()
作用:用于启动任务调度器,任务调度器启动后, FreeRTOS 便会开始进行任务调度【动态创建任务为例】
- 创建空闲任务
- 如果使能软件定时器,则创建定时器任务
- 关闭中断,防止调度器开启之前或过程中,受中断干扰,会在运行第一个任务时打开中断
- 初始化全局变量,并将任务调度器的运行标志设置为已运行
- 初始化任务运行时间统计功能的时基定时器 【可选】
- 调用函数 xPortStartScheduler()
xPortStartScheduler()
作用:该函数用于完成启动任务调度器中与硬件架构相关的配置部分,以及启动第一个任务
- 检测用户在 FreeRTOSConfig.h 文件中对中断的相关配置是否有误
- 配置 PendSV 和 SysTick 的中断优先级为最低优先级
- 调用函数 vPortSetupTimerInterrupt()配置 SysTick(清空计数值、配置节拍频率、重装载值、启动计数与中断)
- 初始化临界区嵌套计数器为 0
- 调用函数 prvEnableVFP()使能 FPU
- 将FPCCR寄存器的[31:30]置l,这样在进出异常时,FPU的相关寄存器就会自动地保存和恢复(M4/M7)
- 调用函数prvStartFirstTask() 启动第一个任务
2.启动第一个任务
prvStartFirstTask()
__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
}
执行过程为:
- 获取MSP的初始值(栈顶地址)
- 将MSP重新赋值为栈底指针(让MSP回到原点,启动任务一去不复返)
- 使能全局中断
- 使用SVC指令,传入系统调用信号,出发SVC中断vPortSVCHandler ()
- 关于MSP指针
程序在运行过程中需要一定的栈空间来保存局部变量等一些信息。当有信息保存到栈中时,MCU 会自动更新 SP 指针,ARM Cortex-M 内核提供了两个栈空间
主堆栈指针(MSP)它由 OS 内核、异常服务例程以及所有需要特权访问的应用程序代码来使用。
进程堆栈指针(PSP)用于常规的应用程序代码(不处于异常服务例程中时)。
在裸机中,程序全部使用MSP,在FreeRTOS中,中断使用MSP(主堆栈),中断以外使用PSP(进程堆栈)- 关于0xE000ED08
0xE000ED08是VTOR(中断向量表)的地址,向量表的第一个是 MSP 指针,取 MSP 的初始值的思路是先根据向量表的位置寄存器 VTOR (0xE000ED08) 来获取向量表存储的地址;在根据向量表存储的地址,来访问第一个元素,也就是初始的 MSP。
vPortSVCHandler ()
__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 }
运行过程为:
- 获取优先级最高的就绪任务的TCB,并取其栈顶元素pxTopOfStack
- 模拟出栈,将寄存器值出栈至CPU寄存器,并设置PSP指针
- 开启中断
- 线与0xd,将r14设置为线程模式并使用PSP
- 跳转到任务的任务函数中运行,CPU自动出栈R0-xPSR等寄存器(M4:若EXC_RETURN使用FPU,则恢复浮点单元)
M4的vPortSVCHandler () ,除了手动出栈r4-r11外,还有r14,这是因为M4等系列支持FPU,需要该变量进行判别
M4的vPortSVCHandler () ,不需要线与0xd,因为在初始化时,已经对EXC_RETURN进行赋值了,不需要再线与
一般情况下,使用动态创建任务,第一个启动的任务是软件定时器任务
注意:SVC中断只在启动第一次任务时会调用一次,以后均不调用
开启任务调度器及启动第一个任务总结
3.任务切换
任务切换的本质:就是CPU寄存器的切换(又称上下文切换),在PendSV中断服务函数中完成 主要分为两步:
- 需暂停任务A的执行,并将此时任务A的寄存器保存到任务堆栈,这个过程叫做保存现场
- 将任务B的各个寄存器值(被存于任务堆栈中)恢复到CPU寄存器中,这个过程叫做恢复现场
触发PendSV中断方式
- 滴答定时器中断调用
- 执行FreeRTOS提供的相关API函数:portYIELD()
- 本质:通过向中断控制和状态寄存器 ICSR 的bit28 写入 1 挂起 PendSV 来启动 PendSV 中断
PendSV中断服务函数xPortPendSVHandler()
- 进入中断,使用PSP自动压栈
- 当前的psp是正在运行的任务的栈指针,读取当前PSP进程指针,存入r0(M4还要考虑FPU压栈)
- 手动压栈,并将最终结果封存至pxTopOfStack,方便下次恢复
- 屏蔽中断
- 调用vTaskSeitchContext(),获取当前最高优先级任务的任务控制块
- 使能中断
- 从最高优先级的TCB中获取pxTopOfStack,并手动出栈
- 更新切换后的任务的的栈指针给PSP
- PSP负责自动出栈
- bx r14 执行新任务函数
查找最优先级任务vTaskSwitchContext( )
通过这个函数完成:taskSELECT_HIGHEST_PRIORITY_TASK( )
- 使用硬件方式(本文使用)
- 使用软件方式
#define taskSELECT_HIGHEST_PRIORITY_TASK()
{
UBaseType_t uxTopPriority;
portGET_HIGHEST_PRIORITY( uxTopPriority, uxTopReadyPriority );
configASSERT( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ uxTopPriority ] ) ) > 0);
listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) );
}
前导置零指令
所谓的前导置零指令,大家可以简单理解为计算一个 32位数,头部 0 的个数。通过前导置零指令获得最高优先级
#define portGET_HIGHEST_PRIORITY( uxTopPriority, uxReadyPriorities ) uxTopPriority = ( 31UL - ( uint32_t ) __clz( ( uxReadyPriorities ) ) )
获取最高优先级任务的任务控制块
通过该函数获取当前最高优先级任务的任务控制块
#define listGET_OWNER_OF_NEXT_ENTRY( pxTCB, pxList )
{
List_t * const pxConstList = ( pxList );
( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext;
if( ( void * ) ( pxConstList )->pxIndex == ( void * ) &( ( pxConstList )->xListEnd ) ){
(pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext;
}
( pxTCB ) = ( pxConstList )->pxIndex->pvOwner;
}