【学习日记】【FreeRTOS】调度器函数实现详解

写在前面

本文主要是对于 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()

  1. 配置PendSV 和 SysTick 的中断优先级为最低
  2. 调用函数 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函数:

  1. PRESERVE8 指令保留 8 字节栈对齐
  2. 取出向量表起始地址对应的内容
  3. 使用向量表起始地址对应的内容设置主堆栈指针msp的值
  4. 使能全局中断
  5. 使用 dsb 和 isb 指令确保数据和指令同步
  6. 调用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 的中断服务函数
  1. 加载 TCB 到 r0,以 r0 为基地址,将栈里面的内容加载到 r4~r11 寄存器
  2. 开启所有中断
  3. 设置 r14 寄存器,以使用 PSP 出栈,进入线程模式,返回 Thumb 状态
  4. 如果异常返回,则 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 状态:
  1. 在 ARM 状态下,处理器执行 ARM 指令集。这些指令集是 32 位宽度的。
  2. ARM 状态提供了更高的代码密度和更强大的功能,可以执行更复杂的指令。
  3. ARM 状态下的指令集包括了更多的寄存器和更多的数据处理指令。
  4. ARM 指令使用的是 32 位的寄存器。
  5. 进入 ARM 状态可以使用跳转指令 bx。
  • Thumb 状态:
  1. 在 Thumb 状态下,处理器执行 Thumb 指令集。这些指令集是 16 位宽度的,它们可以通过压缩来提供更好的代码密度。
  2. Thumb 状态下的指令集相对于 ARM 状态来说更为紧凑,但功能上略有限制。
  3. Thumb 指令使用的是 16 位的寄存器,这些寄存器只能存放 16 位的数据。
  4. 进入 Thumb 状态可以使用跳转指令 bx。

后记

如果您觉得本文写得不错,可以点个赞激励一下作者!

如果您发现本文的问题,欢迎在评论区或者私信共同探讨!

共勉!

相关推荐
dengqingrui1235 小时前
【树形DP】AT_dp_p Independent Set 题解
c++·学习·算法·深度优先·图论·dp
我的心永远是冰冰哒5 小时前
ad.concat()学习
学习
ZZZ_O^O5 小时前
二分查找算法——寻找旋转排序数组中的最小值&点名
数据结构·c++·学习·算法·二叉树
slomay7 小时前
关于对比学习(简单整理
经验分享·深度学习·学习·机器学习
hengzhepa7 小时前
ElasticSearch备考 -- Async search
大数据·学习·elasticsearch·搜索引擎·es
小小洋洋9 小时前
BLE MESH学习1-基于沁恒CH582学习
学习
Ace'10 小时前
每日一题&&学习笔记
笔记·学习
IM_DALLA10 小时前
【Verilog学习日常】—牛客网刷题—Verilog进阶挑战—VL25
学习·fpga开发·verilog学习
丶Darling.10 小时前
LeetCode Hot100 | Day1 | 二叉树:二叉树的直径
数据结构·c++·学习·算法·leetcode·二叉树
z樾12 小时前
Github界面学习
学习