写在前面
本文主要是对于 FreeRTOS 中调度器函数实现的详细解释,代码大部分参考了野火 FreeRTOS 教程配套源码,作了一小部分修改。
一、MSP 和 PSP
Cortex-M有两种栈空间,主堆栈和进程堆栈。
- MSP 用于系统级别和中断处理的堆栈
- MSP 用于保存中断发生时的堆栈状态以及在特殊操作(例如任务切换)期间的堆栈状态。MSP 在启动时会被设置为合适的内存地址,并在系统级代码运行期间始终保持不变。
- PSP 用于任务级别的堆栈
- 用于保存任务执行期间的局部变量、函数调用、参数等。在任务切换时,任务的 PSP 被保存,并加载下一个任务的 PSP。每个任务有自己独立的堆栈空间,并且在任务切换时,PSP 的值会发生变化。
FreeRTOS中:中断用MSP,中断以外用PSP。
二、调度器函数逻辑
三、调度器函数详解
1.vTaskStartScheduler()
- 本函数为调度器的启动函数
- pxCurrentTCB 是一个在 task.c 定义的全局指针,用于指向当前正在运行或者即将要运行的任务的任务控制块
- 目前没有使用优先级,所以手动指定第一个运行的任务
- 调用
xPortStartScheduler()
启动调度器
c
void vTaskStartScheduler( void )
{
/* 手动指定第一个运行的任务 */
pxCurrentTCB = &Task1TCB;
/* 启动调度器 */
if( xPortStartScheduler() != pdFALSE )
{
/* 调度器启动成功,则不会返回,即不会来到这里 */
}
}
2.xPortStartScheduler()
- 配置PendSV 和 SysTick 的中断优先级为最低
- 调用函数 prvStartFirstTask()启动第一个任务
c
BaseType_t xPortStartScheduler( void )
{
/*
PendSV是一个用于低优先级任务切换的软件中断。
通过触发PendSV中断,可以请求处理器在合适的时
间切换到更高优先级的任务。PendSV中断具有最低
的中断优先级,因此可以在其他中断处理完成后立
即执行。*/
/* 配置PendSV 和 SysTick 的中断优先级为最低 */
portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;
/* 启动第一个任务,不再返回 */
prvStartFirstTask();
/* 不应该运行到这里 */
return 0;
}
3.prvStartFirstTask()
-
用于初始化启动第一个任务的环境,主要是重新设置MSP指针,并使能全局中断
-
被调度器启动函数
xPortStartScheduler( void )
调用: -
prvStartFirstTask
函数:
- PRESERVE8 指令保留 8 字节栈对齐
- 取出向量表起始地址对应的内容
- 使用向量表起始地址对应的内容设置主堆栈指针msp的值
- 使能全局中断
- 使用 dsb 和 isb 指令确保数据和指令同步
- 调用SVC去启动第一个任务
c
/*
* 参考资料《STM32F10xxx Cortex-M3 programming manual》4.4.3,百度搜索"PM0056"即可找到这个文档
* 在Cortex-M中,内核外设SCB的地址范围为:0xE000ED00-0xE000ED3F
* 0xE000ED008为SCB外设中SCB_VTOR这个寄存器的地址,里面存放的是向量表的起始地址,即MSP的地址
*/
__asm void prvStartFirstTask( void )
{
/*使用 PRESERVE8 指令保留 8 字节栈对齐*/
PRESERVE8
/* 在Cortex-M中,0xE000ED08是SCB_VTOR这个寄存器的地址,
里面存放的是向量表的起始地址,即MSP的地址 */
// 向量表通常是从内部 FLASH 的起始地址开
// 始存放,那么可知 memory:0x00000000 处存放的就是 MSP 的值。
ldr r0, =0xE000ED08
ldr r0, [r0] //把 0xE000ED08 处向量表起始地址取出
ldr r0, [r0] //取出向量表起始地址对应的内容
/* 设置主堆栈指针msp的值 */
msr msp, r0
/* 使能全局中断 */
cpsie i //开中断 PRIMASK=0
cpsie f //开异常 FAULTMASK=0
/*使用 dsb 和 isb 指令确保数据和指令同步*/
//1. dsb 指令:dsb 指令用于确保数据的同步。它会强制在 dsb 指令之
// 前的所有数据访问和加载操作完成,然后再继续执行 dsb 指令后面
// 的指令。这样可以确保所有数据操作在 dsb 指令之前都已经完成,
// 避免数据争用和不一致性的问题。
//2. isb 指令:isb 指令用于确保指令的同步。它会刷新处理器流水线中
//的指令,并确保在 isb 指令之前的所有指令都已经执行完毕,然后再继
//续执行 isb 指令后面的指令。这样可以确保流水线中的指令执行顺序与
//程序中的顺序一致,避免指令重排和乱序执行带来的问题。
dsb
isb
/* 调用SVC去启动第一个任务 */
// "Supervisor Call"(超级用户调用),
// 用于从用户模式(通常是应用程序运行的模式)
// 切换到特权模式(通常是操作系统内核运行的模式)
// 执行一段特权代码,以执行一些需要特权级别权限的操作或服务
svc 0 //服务号 0表示 SVC 中断,接下来将会执行 SVC 中断服务函数
nop
nop
}
- 关于Cortex-M中三个中断屏蔽寄存器
4.vPortSVCHandler()
- 本函数为 SVC 的中断服务函数
- 加载 TCB 到 r0,以 r0 为基地址,将栈里面的内容加载到 r4~r11 寄存器
- 开启所有中断
- 设置 r14 寄存器,以使用 PSP 出栈,进入线程模式,返回 Thumb 状态
- 如果异常返回,则
bx r14
进入 Thumb 状态,并且栈中的剩下内容将会自动加载到CPU寄存器
c
//SVC中断函数
__asm void vPortSVCHandler( void )
{
extern pxCurrentTCB; //1. 加载要运行的 TCB 的指针
PRESERVE8
ldr r3, =pxCurrentTCB //2. 加载要运行的 TCB 的指针的地址到 r3
ldr r1, [r3] //3. 加载要运行的 TCB 的指针到 r1
ldr r0, [r1] //4. 加载 TCB 到 r0,目前 r0 的值等于第一个任务堆栈的栈顶
ldmia r0!, {r4-r11} //5. 以 r0 为基地址,将栈里面的内容加载到 r4~r11 寄存器,同时r0会递增
msr psp, r0 //6. 将r0的值,即任务的栈指针更新到 psp
isb //7. 等待指令同步
mov r0, #0
msr basepri, r0 //8. 设置basepri寄存器的值为0,即所有的中断都没有被屏蔽
orr r14, #0xd //9. 当从SVC中断服务退出前,通过向r14寄存器最后4位按位或上0x0D,
// 使得硬件在退出时使用进程堆栈指针PSP完成出栈操作并返回后进入线程模式、返回Thumb状态
bx r14 //10. 异常返回,这个时候栈中的剩下内容将会自动加载到CPU寄存器:
// xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0(任务的形参)
// 同时PSP的值也将更新,即指向任务栈的栈顶
}
执行成功后,PSP 的指向(图片来自野火):
ARM 状态和 Thumb 状态详解
在 ARM 架构中,ARM 状态和 Thumb 状态是指处理器运行的不同工作模式。这些模式决定了处理器执行代码的指令集。
- ARM 状态:
- 在 ARM 状态下,处理器执行 ARM 指令集。这些指令集是 32 位宽度的。
- ARM 状态提供了更高的代码密度和更强大的功能,可以执行更复杂的指令。
- ARM 状态下的指令集包括了更多的寄存器和更多的数据处理指令。
- ARM 指令使用的是 32 位的寄存器。
- 进入 ARM 状态可以使用跳转指令 bx。
- Thumb 状态:
- 在 Thumb 状态下,处理器执行 Thumb 指令集。这些指令集是 16 位宽度的,它们可以通过压缩来提供更好的代码密度。
- Thumb 状态下的指令集相对于 ARM 状态来说更为紧凑,但功能上略有限制。
- Thumb 指令使用的是 16 位的寄存器,这些寄存器只能存放 16 位的数据。
- 进入 Thumb 状态可以使用跳转指令 bx。
后记
如果您觉得本文写得不错,可以点个赞激励一下作者!
如果您发现本文的问题,欢迎在评论区或者私信共同探讨!
共勉!