一、开启任务调度器
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()主要做了六件事情。
- 创建空闲任务,根据是否支持静态内存管理,使用静态方式或动态方式创建空闲任务。
- 创建定时器服务任务,创建定时器服务任务需要配置启用软件定时器,创建定时器服务
任务,同样是根据是否配置支持静态内存管理,使用静态或动态方式创建定时器服务任务。 - 关闭中断,使用 portDISABLE_INTERRUPT()关闭中断,这种方式只关闭受 FreeRTOS 管
理的中断。关闭中断主要是为了防止 SysTick 中断在任务调度器开启之前或过程中,产生中断。
FreeRTOS 会在开始运行第一个任务时,重新打开中断。 - 初始化一些全局变量,并将任务调度器的运行标志设置为已运行。
- 初始化任务运行时间统计功能的时基定时器,任务运行时间统计功能需要一个硬件定时
器提供高精度的计数,这个硬件定时器就在这里进行配置,如果配置不启用任务运行时间统计
功能的,就无需进行这项硬件定时器的配置。 - 最后就是调用函数 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()的解析如下所示:
- 在启用断言的情况下,函数 xPortStartScheduler()会检测用户在 FreeRTOSConfig.h 文件
中对中断的相关配置是否有误,感兴趣的读者请自行查看这部分的相关代码。 - 配置 PendSV 和 SysTick 的中断优先级为最低优先级,请参考 4.3.1 小节。
- 调用函数 vPortSetupTimerInterrupt()配置 SysTick,函数 vPortSetupTimerInterrupt()首先会
将 SysTick 当 前 计 数 值 清 空 , 并 根 据 FreeRTOSConfig.h 文件中配置的
configSYSTICK_CLOCK_HZ(SysTick 时钟源频率)和 configTICK_RATE_HZ(系统时钟节拍
频率)计算并设置 SysTick 的重装载值,然后启动 SysTick 计数和中断。 - 初始化临界区嵌套计数器为 0。
- 调用函数 prvEnableVFP()使能 FPU,因为 ARM Cortex-M3 内核 MCU 无 FPU,此函数
仅在 ARM Cortex-M4/M7 内核 MCU 平台上被调用,执行改函数后 FPU 被开启。 - 接下来将 FPCCR 寄存器的[31:30]置 1,这样在进出异常时,FPU 的相关寄存器就会自
动地保存和恢复,同样地,因为 ARM Cortex-M3 内核 MCU 无 FPU,此当代码仅在 ARM Cortex-M4/M7 内核 MCU 平台上被调用。 - 调用函数 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()是一段汇编代码,解析如下所示:
-
首先是使用了 PRESERVE8,进行 8 字节对齐,这是因为,栈在任何时候都是需要 4 字
节对齐的,而在调用入口得 8 字节对齐,在进行 C 编程的时候,编译器会自动完成的对齐的操
作,而对于汇编,就需要开发者手动进行对齐。 -
接下来的三行代码是为了获得 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 的值,也就是获取向量表的首地址,最后获取向量表中第一个字的数据,也就是栈底指
针了。
- 在获取了栈顶指针后,将 MSP 指针重新赋值为栈底指针。这个操作相当于丢弃了程序
之前保存在栈中的数据,因为FreeRTOS从开启任务调度器到启动第一个任务都是不会返回的,
是一条不归路,因此将栈中的数据丢弃,也不会有影响。 - 重新赋值 MSP 后,接下来就重新使能全局中断,因为之前在函数 vTaskStartScheduler()
中关闭了受 FreeRTOS 的中断。 - 最后使用 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()就是用来跳转到第一个任务函数中去的,
该函数的具体解析如下:
- 首先通过 pxCurrentTCB 获取优先级最高的就绪态任务的任务栈地址,优先级最高的就
绪态任务就是系统将要运行的任务。pxCurrentTCB 是一个全局变量,用于指向系统中优先级最
高的就绪态任务的任务控制块,在前面创建 start_task 任务、空闲任务、定时器处理任务时自动
根据任务的优先级高低进行赋值的,具体的赋值过程在后续分析任务创建函数时,会具体分析。
这里举个例子,在《FreeRTOS 移植实验》中,start_task 任务、空闲任务、定时器处理任务
的优先级如下表所示:
从上表可以看出,在《FreeRTOS 移植实验》中,定时器处理任务的任务优先级为 31,是系
统中优先级最高的任务,因此当进入 SVC 中断时,pxCurrentTCB 就是指向了定时器处理任务
的任务控制块。
接着通过获取任务控制块中的第一个元素,得到该任务的栈顶指针,任务控制块的相关内
容,请查看第 5.5 小节《FreeRTOS 任务控制块》。
- 接下来通过任务的栈顶指针,将任务栈中的内容出栈到 CPU 寄存器中,任务栈中的内
容在调用任务创建函数的时候,已经初始化了。然后再设置 PSP 指针,那么,这么一来,任务
的运行环境就准备好了。 - 通过往 BASEPRI 寄存器中写 0,允许中断。
- 最后通过两条汇编指令,使 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 还会自动恢复浮点寄存器。