硬核解析FreeRTOS任务切换:从寄存器底层到PendSV机制,一文吃透核心原理

硬核解析FreeRTOS任务切换:从寄存器底层到PendSV机制,一文吃透核心原理

序言

你是否曾困惑于嵌入式系统中多任务是如何“无缝切换”的?当CPU在不同任务间快速跳转时,背后隐藏着怎样的底层逻辑?本文将以FreeRTOS为例,带您穿透代码表象,直击任务切换的核心本质——CPU寄存器与堆栈的精妙配合,以及PendSV异常如何确保切换过程安全无干扰。

亮点抢先看

  • 底层原理解构:用“保存现场→恢复现场”两步法,拆解任务切换的本质是“寄存器状态替换”,结合Cortex-M内核特性,揭秘硬件自动压栈与手动压栈的设计巧思。
  • 全流程实战分析:从触发切换请求到PendSV异常处理,通过6步完整流程,结合FreeRTOS汇编代码与TCB机制,还原任务A到任务B切换的每一个寄存器操作。
  • 关键机制深度解析:为什么PendSV能解决中断干扰问题?通过优先级配置与延迟执行特性,图解“低优先级异常”如何保证切换过程的原子性。
  • 代码级联动:结合vTaskDelay()SysTick中断等触发场景,分析C语言与汇编代码的协同逻辑,包括portYIELD宏如何操作ICSR寄存器触发PendSV。

无论是嵌入式开发新手想理解多任务调度底层,还是进阶工程师深究FreeRTOS内核机制,本文都将通过“原理+代码+图解”的三维解析,帮您彻底掌握任务切换这一实时操作系统的核心能力。觉得干货满满的话,别忘了点赞收藏转发,让更多开发者一起突破技术瓶颈!

一、任务切换的核心原理

1. 本质定义:任务切换 = CPU 寄存器切换

一句话概括:任务切换的本质,是把 CPU 内部寄存器的值,从“当前任务的状态”换成“下一个任务的状态”
因为 CPU 执行任务时,指令、数据、运行状态都靠寄存器承载。切换任务,本质就是让 CPU “忘记” 上一个任务的寄存器状态,“记住” 下一个任务的寄存器状态。

2. 两步走流程(以任务 A → 任务 B 为例)

① 保存现场(任务 A 的寄存器 → 任务 A 堆栈)

  • 动作:暂停任务 A 执行,把任务 A 当前的 CPU 寄存器(比如 R0 - R15、PSP 等 ),保存到任务 A 自己的堆栈里。
  • 目的:记录任务 A “暂停时的状态”,保证下次切回来时,能从暂停的地方继续执行。

② 恢复现场(任务 B 堆栈 → CPU 寄存器)

  • 动作:从任务 B 的堆栈里,把之前保存的寄存器值(任务 B 上次暂停时的状态 ),恢复到 CPU 寄存器中。
  • 目的:让 CPU “认为自己一直在执行任务 B”,接着从任务 B 暂停的地方继续运行。

3. 任务切换的完整过程

以下从 硬件底层(以 Cortex - M 内核为例 )、FreeRTOS 代码逻辑 两个维度,详细拆解 任务切换的完整过程,结合 “保存现场 → 恢复现场” 的本质,让你彻底理解每一步到底发生了什么:

(1) 核心前提:任务的 “独立运行环境” 靠堆栈和 TCB 维护

每个任务都有:

  • 独立堆栈(Stack ):存 CPU 寄存器、局部变量等,任务切换时,现场(寄存器值 )存在这里。
  • 任务控制块(TCB ):记录任务状态(优先级、堆栈指针、任务函数等 ),调度器靠 TCB 找下一个要运行的任务。

(2) 任务切换的完整流程(分 6 步,结合硬件 + 软件逻辑 )

任务 A → 任务 B 切换 为例,假设当前任务 A 正在运行,调度器决定切换到任务 B,完整过程如下:

步骤 1:触发任务切换请求

任务切换由 “调度器决策” 或 “任务主动请求” 触发,常见场景:

  • 场景 1:任务 A 调用 vTaskDelay()(主动阻塞 ),告诉调度器 “我要等一会儿,先切其他任务”。
  • 场景 2:SysTick 中断触发(时间片到了 ),调度器检查发现有更高优先级任务(或时间片轮换 ),决定切换。
  • 场景 3:任务 A 调用 taskYIELD()(主动让出 CPU ),请求立刻切换。

此时,调度器会标记 “需要切换任务”,并通过 触发 PendSV 异常(Cortex - M 内核 ),把 “切换动作” 延迟到 “安全时机”(所有中断处理完 )执行。

步骤 2:进入 PendSV 异常(硬件接管,准备切换 )

PendSV 是 Cortex - M 内核专门为 RTOS 设计的 “低优先级异常”,作用是 保证切换时不被其他中断干扰

  • 当调度器决定切换任务,会通过写 NVIC 的 ICSR 寄存器,挂起 PendSV 异常(设置 PENDSVSET 位 )。
  • 因为 PendSV 优先级最低,会等到所有高优先级中断(如 IRQ、SysTick )处理完,才会进入 PendSV 异常处理函数。
步骤 3:保存任务 A 的现场(寄存器 → 任务 A 堆栈 )

进入 PendSV 异常处理函数后,硬件自动做以下动作(Cortex - M 内核特性 ):

  1. 自动压栈(部分寄存器 )
    CPU 会把当前任务 A 的 xPSRPCLRR12R0 - R3 寄存器,自动压入任务 A 的堆栈(硬件自动完成,不需要代码干预 )。
    这一步对应 “保存现场” 的前半部分。

  2. 手动压栈(剩余寄存器 )
    剩下的寄存器(如 R4 - R11 ),需要在 PendSV 异常处理函数中,手动用汇编代码压栈
    例如 FreeRTOS 的 xPortPendSVHandler 中,会有类似:

    STMDB   SP!, {R4 - R11}   ; 把 R4 - R11 压入当前任务堆栈
    

    这一步完成 “保存现场” 的全部操作——任务 A 的所有寄存器,都存在自己的堆栈里了。

步骤 4:更新当前任务指针(调度器选任务 B )

保存完任务 A 的现场后,软件(调度器 )会做:

  1. 找到下一个任务(任务 B )
    调度器遍历 “就绪任务列表”,选最高优先级的就绪任务(假设是任务 B )。

  2. 更新 TCB 指针
    把全局变量 pxCurrentTCB(指向当前运行任务的 TCB ),从任务 A 的 TCB 换成任务 B 的 TCB

步骤 5:恢复任务 B 的现场(任务 B 堆栈 → CPU 寄存器 )
  1. 手动出栈(剩余寄存器 )
    从任务 B 的堆栈中,手动恢复 R4 - R11 寄存器(和保存时对称 )。
    例如:

    LDMIA   SP!, {R4 - R11}   ; 从任务 B 堆栈恢复 R4 - R11
    
  2. 自动出栈(部分寄存器 )
    硬件自动从任务 B 的堆栈中,恢复 xPSRPCLRR12R0 - R3 寄存器(和步骤 3 对称 )。

    这一步完成 “恢复现场”——任务 B 的寄存器值,全部回到 CPU 中,CPU 认为 “自己一直在运行任务 B”。

步骤 6:退出 PendSV 异常,运行任务 B

恢复完任务 B 的现场后,PendSV 异常处理函数执行 BX LR(汇编指令 ),退出异常
此时,CPU 回到 “线程模式”,开始从任务 B 上次暂停的地方(PC 寄存器记录的地址 )继续执行——任务切换完成!

4. 关键细节:为什么要分 “自动压栈” 和 “手动压栈”?

Cortex - M 内核设计时,为了 “加速中断响应”,规定:

  • 异常进入时,硬件会自动压栈 xPSRPCLRR12R0 - R3(这些是调用函数时最常用的寄存器,优先保存 )。
  • 剩下的寄存器(R4 - R11 ) 属于 “调用者保存寄存器”,需要软件手动压栈/出栈(否则会被破坏 )。

因此,在 PendSV 异常处理中,必须用汇编代码手动处理 R4 - R11,保证任务现场完整。

5. 任务切换涉及的寄存器

下图来自《FreeRTOS任务调度器的启动流程和第一个任务被调用的全过程》如有疑惑,请参考这篇文章。
在这里插入图片描述

6. 总结

(1) 任务切换的本质是 “堆栈 + 寄存器的替换”

整个过程,本质就是:

  1. 把任务 A 的寄存器(现场 )完整保存到它的堆栈
  2. 把任务 B 的寄存器(现场 )从它的堆栈恢复到 CPU
  3. 通过更新 pxCurrentTCB,让调度器 “认为” 任务 B 是当前运行任务。

而 PendSV 异常的作用,是 保证切换过程不被其他中断干扰,让 “保存/恢复现场” 操作安全、完整地执行。

理解这 6 步,你就彻底掌握了 FreeRTOS 任务切换的底层逻辑——不管是 Cortex - M 内核,还是其他架构,本质都是围绕 “保存/恢复 CPU 寄存器” 实现的!

(2) 涉及的其他知识点

  • 堆栈的作用:每个任务都有独立堆栈,用来存自己的寄存器现场。FreeRTOS 中,任务控制块(TCB )会记录堆栈指针(PSP )。
  • 触发时机:任务切换由调度器触发,常见场景:任务阻塞(vTaskDelay )、任务主动让出 CPU(taskYIELD )、SysTick 中断(时间片到了 )等。
  • 硬件依赖:在 Cortex - M 内核中,PendSV 异常专门用来实现上下文切换,保证切换时不被中断干扰(。

二、PendSV和任务切换

  • FreeRTOS实现多任务,本质是“上下文切换”——保存当前任务寄存器、加载下一个任务寄存器。为避免切换时被其他中断打断(导致寄存器混乱),让PendSV在“所有高优先级中断都处理完”后执行,是安全切换的核心设计。
  • FreeRTOS中,任务切换往往通过触发PendSV实现,利用的就是它“可延迟执行、优先级最低”的特点,确保切换过程不受干扰。

1. PendSV 基础定义

除了SVC异常(参见文章《FreeRTOS任务调度器的启动流程和第一个任务被调用的全过程》),PendSV挂起的服务调用)是另一种对支持操作系统(OS)操作至关重要的异常类型 。它是编号为14的异常,且优先级可配置 。通过向中断控制和状态寄存器ICSR)写入操作设置其挂起状态,即可触发PendSV异常。
如下图,将Bit28置位(即PENDSVSET=1),即可触发PendSV异常。将Bit27置位,即可清除PendSV中断标志位。
在这里插入图片描述
SVC系统服务调用)异常不同,它不要求“精准触发” 。因此,可在高优先级异常处理程序中设置它的挂起状态,待高优先级处理程序执行完毕后,再执行PendSV异常处理。

关键知识

  • 在ARM Cortex - M内核中,异常有编号与优先级,PendSV编号固定为14,常被FreeRTOS用于实现任务上下文切换 。
  • “不精准”意味着它的触发时机更灵活,可被高优先级中断“打断”,等高优先级中断处理完,再执行自身逻辑,这对上下文切换非常关键——保证“切换动作”在无更高优先级任务干扰时执行。

2. PendSV 在上下文切换中的核心价值

利用这一特性,只要将PendSV的异常优先级设为最低,就能安排它的处理程序在所有其他中断处理任务完成后执行 。这对“上下文切换”操作极为有用,而上下文切换是各类操作系统设计中的关键操作。

3. PendSV中断触发逻辑

请回头看一下一、任务切换的核心原理-> 3. 任务切换的完整过程->步骤 1:触发任务切换请求,可以看到PendSV中断触发的原因。
其实,上面的的场景1和场景3本质是一样的,都是调用portYIELD()宏函数。

4. PendSV中断触发代码逻辑分析

(1) vTaskDelay()函数触发PendSV

在这里插入图片描述
在这里插入图片描述
这里拿出宏portYIELD_WITHIN_API的定义来拆解。

#define portYIELD_WITHIN_API portYIELD

下面是重点!portYIELD()宏函数的内容如下:

#define portYIELD()																\
{																				\
	/* Set a PendSV to request a context switch. */								\
	portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;								\
																				\
	/* Barriers are normally not required but do ensure the code is completely	\
	within the specified behaviour for the architecture. */						\
	__dsb( portSY_FULL_READ_WRITE );											\
	__isb( portSY_FULL_READ_WRITE );											\
}

首先,portNVIC_INT_CTRL_REG portNVIC_PENDSVSET_BIT代表的含义如下:

#define portNVIC_INT_CTRL_REG		( * ( ( volatile uint32_t * ) 0xe000ed04 ) )
#define portNVIC_PENDSVSET_BIT		( 1UL << 28UL )

现在我们回顾上面 二、PendCV和任务切换->1. PendSV 基础定义章节里面讲过的PendSV中断触发的时候操作的寄存器。对比一下,你就瞬间明白了,0xe000ed04 这个地址,就是SCB->ICSR寄存器的地址,而( 1UL << 28UL )正是操作的PENDSVSET位。写1进去,就触发PendSV异常!
在这里插入图片描述

(2) SysTick中断触发进一步触发PendSV

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

上图有函数xTaskIncrementTick(),下面进行简单介绍:

  • xTaskIncrementTick()核心功能
    • 更新系统时钟:递增 xTickCount,处理溢出情况。
    • 唤醒超时任务:检查是否有任务的阻塞时间到期,将其从阻塞列表移至就绪列表。
    • 触发上下文切换:如果唤醒的任务优先级更高,或时间片轮转条件满足,则标记需要切换任务。
  • 上下文切换的触发条件(函数xTaskIncrementTick()最终返回 xSwitchRequired,其值为 pdTRUE 的条件)
    • 高优先级任务唤醒:被唤醒的任务优先级 ≥ 当前任务。
    • 时间片轮转:当前优先级有多个任务,且配置允许时间片轮转。
    • 手动标记xYieldPending 被置位(通常由 taskYIELD() 触发)。

由于上面代码xTaskIncrementTick()返回值不等于pdFALSE则会触发PendSV中断,所以由以上三种原因实现任务切换。
而里面操作逻辑:portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;跟上面的一样,就不再说了。


三、PendCV中断处理函数

在FreeRTOSConfig.h文件中有如下定义:

#define xPortPendSVHandler PendSV_Handler

在port.c文件中有函数定义:

__asm void xPortPendSVHandler( void )
{
	extern uxCriticalNesting;
	extern pxCurrentTCB;
	extern vTaskSwitchContext;

	PRESERVE8

	mrs r0, psp
	isb
	/* Get the location of the current TCB. */
	ldr	r3, =pxCurrentTCB
	ldr	r2, [r3]

	/* Is the task using the FPU context?  If so, push high vfp registers. */
	tst r14, #0x10
	it eq
	vstmdbeq r0!, {s16-s31}

	/* Save the core registers. */
	stmdb r0!, {r4-r11, r14}

	/* Save the new top of stack into the first member of the TCB. */
	str r0, [r2]

	stmdb sp!, {r3}
	mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
	msr basepri, r0
	dsb
	isb
	bl vTaskSwitchContext
	mov r0, #0
	msr basepri, r0
	ldmia sp!, {r3}

	/* The first item in pxCurrentTCB is the task top of stack. */
	ldr r1, [r3]
	ldr r0, [r1]

	/* Pop the core registers. */
	ldmia r0!, {r4-r11, r14}

	/* Is the task using the FPU context?  If so, pop the high vfp registers
	too. */
	tst r14, #0x10
	it eq
	vldmiaeq r0!, {s16-s31}

	msr psp, r0
	isb
	#ifdef WORKAROUND_PMU_CM001 /* XMC4000 specific errata */
		#if WORKAROUND_PMU_CM001 == 1
			push { r14 }
			pop { pc }
			nop
		#endif
	#endif

	bx r14
}

下面进行讲解代码:
在这里插入图片描述
图片上解释了前几句的代码,但还需要进一步补充。
上图中一处错误:
修改:图中对于r2和r3寄存器的内容写的不对,这里特别容易搞混,所以要特别细心地分析。我也是耗费了点脑细胞,好不容易理清楚了。整理了下面的图片。

注:下图包含了我的思路和心血,如果您要用在别处,希望一定一定要注明出处,谢谢!保护版权,互相尊重!
在这里插入图片描述
在校对过程中,发现上图还是存在一丢丢问题,为了不误导大家,在此解释一下:str r0, [r2]这一句代码是在讲把r0的内容写进r2寄存器所代表的内存地址中。有点拗口,r2里面存放着栈顶指针的地址,而加上方括号“[]”,就是在相当于对指针进行取指针“*”操作。不过图片上内存的示意图没问题。
这幅图片看明白了,下面的内容就容易理解了。

1. 进入PendSV中断处理函数之后PSP指向哪里?

mrs r0, psp:在进入中断后,硬件会自动保存一部分寄存器到任务栈。具体操作是PSP堆栈寄存器会将下图中的寄存器压栈到当前的任务栈中。之后指向了下图中R0的位置。下图截取自《FreeRTOS任务调度器的启动流程和第一个任务被调用的全过程》。
在这里插入图片描述

2. r2寄存器获取的是什么内容?

	ldr	r3, =pxCurrentTCB
	ldr	r2, [r3]

上面两行代码r2寄存器获取了当前任务的TCB的地址,也是首成员栈顶指针的地址。下面回顾一下TCB结构体的内容。如果对TCB存疑,请参见文章《FreeRTOS硬核解析:从任务调度到TCB核心机制,这篇文章让你避开90%的开发陷阱!》。

typedef struct tskTaskControlBlock {
    volatile StackType_t *pxTopOfStack;  // 栈顶指针,指向当前栈顶位置
    ListItem_t xStateListItem;           // 状态列表项,用于就绪/阻塞队列
    ListItem_t xEventListItem;           // 事件列表项,用于等待特定事件
    UBaseType_t uxPriority;              // 任务优先级
    StackType_t *pxStack;                // 栈底指针,指向任务栈的起始地址
    char pcTaskName[ configMAX_TASK_NAME_LEN ]; // 任务名称
    // ... 其他成员(如事件标志、栈溢出检测等)
} tskTCB;

3. 为什么r2寄存器要获取这个地址?

从图上看,从PSP当前指向的位置,向下压栈,一直到r4寄存器的位置。这部分的空间是需要我们手动压栈寄存器的位置。而当我们要出栈的时候,就可以从这个栈顶指针的位置开始,一步步向上出栈,手动恢复寄存器的值。这些手动保存和手动恢复的寄存器是相同的。而上面自动压栈的寄存器,也会在出栈的时候自动恢复现场。

当压栈完成的时候,r0寄存器会指向下图中的位置,这个时候,我们会将这个地址赋值给栈顶指针,将来可以从这个地址开始向上出栈。
在这里插入图片描述

4. 对r14寄存器的判断

	tst r14, #0x10
	it eq
	vstmdbeq r0!, {s16-s31}

第一句,判断r14寄存器的bit4是不是1?如果是1,那么就使用浮点运算单元FPU。如果是0,那就是使用FPU,就要压栈寄存器s16-s31。因为FPU32个寄存器,包括s0-s31,其中s0-s15是自动保存和恢复的,s16-s31是需要手动保存和恢复的。
当前我们是不使用FPU的,所以不需要压栈这些寄存器。
在这里插入图片描述

5. 手动压栈r14,r4-r11

	/* Save the core registers. */
	stmdb r0!, {r4-r11, r14}

压栈过程中,每压栈一个寄存器,r0寄存器减4,一直到下一次出栈的首地址。

6. r0寄存器内容写入r2寄存器对应的内存里面

r2寄存器存放着当前任务的TCB的地址,也是首成员栈顶指针的地址,通过这个地址可以找到栈顶指针,也就可以写内容到栈顶指针当中。写什么内容呢?就写r0寄存器的值。

	/* Save the new top of stack into the first member of the TCB. */
	str r0, [r2]

而r2里面存放的是栈顶指针的值。请回头看下在1. 进入PendSV中断处理函数之后PSP指向哪里?标题上面,我绘制的图片,一看就懂了。

7. 压栈r3寄存器

此处采用的是MSP堆栈指针,将r3寄存器压栈,等会儿还要弹出。这里存放着的是变量pxCurrentTCB的地址。

stmdb sp!, {r3}

8. 关闭中断

此处关闭的是优先级数值在5-15的中断。

	mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
	msr basepri, r0

上面汇编涉及的宏:

#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY	5
#define configMAX_SYSCALL_INTERRUPT_PRIORITY 	( configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )
#define configPRIO_BITS       		4        /* 15 priority levels */

将宏configMAX_SYSCALL_INTERRUPT_PRIORITY代表的值是多少?(5<<4)得到的结果是:二进制数0101 0000b。最终将这个值写进了basepri寄存器。至于为啥左移4位?请看下图。

在这里插入图片描述

9. isb和dsb指令详解

	dsb
	isb

在ARM Cortex-M处理器架构中,ISB(Instruction Synchronization Barrier)和DSB(Data Synchronization Barrier)是两条关键的内存屏障指令,用于确保指令和数据操作的顺序性与可见性。它们在FreeRTOS等实时操作系统中尤为重要,特别是在任务切换、中断处理和临界区保护中。

1. 详细功能解析

(1)isb指令
  • 作用
    • 刷新处理器的指令流水线,确保后续指令从缓存或内存中重新获取。
    • 用于保证指令执行顺序的可见性,例如在修改异常向量表、配置MMU后。
  • 典型场景
    • 在动态修改中断向量表后,使用ISB确保新向量表被正确加载
    • 在启用 / 禁用 MMU 或缓存后,强制刷新指令流。
(2)dsb指令
  • 作用
    • 确保屏障前的所有数据访问(加载/存储)操作完成后,才执行屏障后的指令。
    • 防止内存操作的乱序执行,保证数据一致性。
  • 典型场景
    • 在访问共享资源(如外设寄存器)前后。
    • 在临界区操作(如任务切换)中保护内存访问顺序。

2. 在FreeRTOS中的应用

在FreeRTOS的上下文切换和中断处理中,isbdsb常用于:

(1)任务切换同步
; FreeRTOS任务切换中的伪代码
PendSV_Handler:
    ; 保存当前任务上下文
    PUSH {r4-r11}
    ; 更新任务控制块指针
    STR r0, [CurrentTCB]
    
    ; 数据同步屏障:确保上下文保存完成
    DSB
    ISB
    
    ; 恢复下一个任务的上下文
    LDR r0, [NextTCB]
    POP {r4-r11}
    BX LR                ; 异常返回,跳转到新任务
(2)临界区保护
; FreeRTOS任务切换中的伪代码
; 进入临界区(禁用中断);
CPSID I                  ; 禁用中断
DSB                      ; 确保中断禁用完成
ISB

; 临界区代码...

; 退出临界区(启用中断)
CPSIE I                  ; 启用中断
DSB                      ; 确保中断启用完成
ISB

10. 找到优先级最高的任务

执行下面的汇编代码,实则跳转到函数中执行。

	bl vTaskSwitchContext

在这里插入图片描述
在这里插入图片描述
下面就这两个宏展开叙述。

(1) portGET_HIGHEST_PRIORITY

#define portGET_HIGHEST_PRIORITY( uxTopPriority, uxReadyPriorities ) uxTopPriority = ( 31UL - ( uint32_t ) __clz( ( uxReadyPriorities ) ) )

这个宏得到的值是uxTopPriority ,含义是从高到低,不为空的就绪列表的编号。
__clz是前导置零指令,也即是说,uxReadyPriorities变量,转化成二进制数之后,从高到低,最前面有几个0
变量uxReadyPriorities是传递进来的,它的本质是uxTopReadyPriority

PRIVILEGED_DATA static volatile UBaseType_t uxTopReadyPriority 		= tskIDLE_PRIORITY;
typedef unsigned long UBaseType_t;
#define tskIDLE_PRIORITY			( ( UBaseType_t ) 0U )

于是得出结论,这个变量是无符号长整型,长度是32位,初始值是0,用来表征各个就绪列表里面是否有任务的

a. 关键点

就绪列表一共有32个,从0-31,优先级是31最高,0最低。但每一个列表里面是否有任务,怎么知道呢?采用变量uxTopReadyPriority的每一个位来表征!它不也是32个位吗?从高到低,最高位bit31代表优先级为31的就绪列表里面有没有任务。如果有任务,这一位等于1,否则等于0。

b. 举例说明

如果uxTopReadyPriority等于0010 0000 0001 0000 0000 0000 0000 0001b那么我们可以使用__clz指令得到2,也就是说,最高位有2个0。最后计算:31-2 = 29,于是我们知道优先级为29的就绪列表里面有任务等待执行

(2) listGET_OWNER_OF_NEXT_ENTRY

a. 含义

接续上面的举例,已经知道了:优先级为29的就绪列表里面有任务等待执行。那么这一步就是从这个优先级的就绪列表中取出待执行的任务的TCB地址。

listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) );

所有的就绪列表构成一个长度是32的数组pxReadyTasksLists,所以可以根据数组下标获取数组的元素。而数组的下标就是我们上一步获取的优先级编号,本例中是29。

b. 具体实现流程

在这里插入图片描述通过上面的操作,就可以将当前优先级最高,且不为空的就绪列表当中取出一个任务,将此任务的TCB地址赋值到pxCurrentTCB变量。(这部分的知识涉及了列表和列表项的内容,欢迎大家阅读《FreeRTOS列表和列表项》一文回顾知识点)
注意:这里拿到的是地址!

11. 开中断

	mov r0, #0
	msr basepri, r0

12. 恢复r3寄存器

ldmia sp!, {r3}

注意,这里使用的是sp,默认使用的是MSP。还记得r3寄存器存放的是什么吗?回去看看!

13. 获取新任务的栈顶指针

	/* The first item in pxCurrentTCB is the task top of stack. */
	ldr r1, [r3]
	ldr r0, [r1]

由于r3寄存器存放着的是变量的地址,所以r1得到的是变量的值,也就是新任务的TCB地址。紧接着,r0寄存器获取的是TCB结构体当中首元素的值,就是新任务的栈顶指针的值。它指向了新任务的任务栈,下一步我们从其中恢复寄存器。

14. 恢复现场

	/* Pop the core registers. */
	ldmia r0!, {r4-r11, r14}

每恢复一个寄存器,r0都加4,最后就到了下面图示的位置。
在这里插入图片描述

15. 判断新任务有没有使用FPU

	/* Is the task using the FPU context?  If so, pop the high vfp registers
	too. */
	tst r14, #0x10
	it eq
	vldmiaeq r0!, {s16-s31}

如果使用了FPU,r14寄存器的bit4会等于1。
在这里插入图片描述
如果使用了FPU,需要我们手动恢复FPU的寄存器s16-s31,因为s0-s15是硬件自动恢复的!这里我们没有使用。

16. 将r0寄存器的内容存放到psp寄存器方便硬件从这里开始恢复剩余的寄存器

	msr psp, r0

17. bx r14——跳转到新任务的任务函数中执行

	bx r14

下面对bx r14指令进行讲解。此指令用于任务上下文切换时跳转到任务的执行函数,这涉及ARM Cortex-M处理器的异常返回机制和FreeRTOS的任务调度原理。

1. bx r14的作用

bx r14是ARM汇编中的分支跳转指令,其中:

  • bx:Branch and Exchange,跳转并切换指令集(ARM/Thumb)。
  • r14:即LR(Link Register),存储返回地址或异常返回信息。

在FreeRTOS的上下文切换中,bx r14的核心作用是触发异常返回,使处理器从PendSV或SVC异常处理中返回到任务代码执行。

2. 异常返回机制

在ARM Cortex-M处理器中,异常返回时:

  • LR的值决定返回行为:LR的最低4位(EXC_RETURN值)编码了返回模式、栈指针选择等信息。
  • 自动恢复寄存器:异常返回时,处理器会自动从栈中恢复xPSR、PC、LR、R12及R3-R0,然后跳转到PC指向的地址执行。
  • 当执行bx r14时:
    • 处理器读取LR中的EXC_RETURN值,识别为异常返回。
    • 从任务栈中自动恢复xPSR、PC、LR、R12、R3-R0。
    • 处理器从任务栈中恢复PC值(即任务函数地址),跳转到恢复的PC值(即任务函数的入口地址)执行。

因此,bx r14并非直接跳转到任务函数,而是通过异常返回机制,让处理器自动恢复并执行任务函数。

四、补充本文涉及的其余知识点

1. xTaskIncrementTick()函数简介

xTaskIncrementTick() 是 FreeRTOS 操作系统中最核心的函数之一,它由系统滴答定时器(SysTick)中断周期性调用(通常为 1ms 一次),主要负责推进系统时间、管理任务超时和触发任务调度。以下是其核心功能的通俗解释:

1. 核心作用:驱动系统时钟与任务调度

这个函数就像 FreeRTOS 的“心脏”,每产生一次滴答中断(例如 1ms),它就会被调用一次,主要做两件事:

  • 更新系统时间:累加系统滴答计数器(xTickCount),记录系统运行的总时间。
  • 检查任务状态:查看是否有任务的等待时间到期(例如 vTaskDelay() 超时),如果到期则将其唤醒。

2. 具体功能拆解

(1)更新系统时间
xTickCount = xTickCount + 1;  // 系统时间递增
  • 每次调用该函数时,xTickCount 加 1,表示系统运行时间增加了一个滴答周期(通常是 1ms)。
  • 如果 xTickCount 溢出(例如从最大值回滚到 0),会触发特殊处理(切换延迟列表),确保任务调度不受影响。
(2)唤醒超时的任务

FreeRTOS 中,任务可能因等待事件(如 vTaskDelay()xQueueReceive())而进入阻塞状态,并被放入“延迟列表”。该函数会检查这些任务:

if( xTickCount >= xNextTaskUnblockTime ) {
    // 从延迟列表中取出到期的任务
    // 将任务从阻塞状态移除,加入就绪列表
}
  • 关键逻辑:遍历延迟列表,将等待时间已到的任务移回“就绪列表”,使其有机会被调度执行。
(3)决定是否需要切换任务

如果唤醒的任务优先级比当前运行的任务更高,或者配置了时间片轮转(相同优先级任务轮流执行),则触发任务切换:

if( 新唤醒任务的优先级 >= 当前任务优先级 ) {
    xSwitchRequired = pdTRUE;  // 标记需要切换任务
}
  • 最终通过返回值 xSwitchRequired 通知调度器是否需要立即切换到更高优先级的任务。

3. 与其他组件的协作

  • 与 SysTick 中断的关系:该函数在 SysTick 中断服务函数中被调用,因此它的执行频率由 configTICK_RATE_HZ 配置(例如 1000Hz 表示每秒调用 1000 次)。
  • 与调度器的关系:如果函数返回 pdTRUE,调度器会在适当的时候(如中断返回前)触发上下文切换,切换到更高优先级的任务。

4. xTaskIncrementTick()的重要性

  • 实时性保障:通过精确管理任务超时,确保高优先级任务能在指定时间内被唤醒和执行。
  • 多任务调度基础:它是实现任务抢占、时间片轮转的关键驱动力,没有它,FreeRTOS 的多任务功能将无法正常工作。

5. 总结:一句话概括

xTaskIncrementTick() 是 FreeRTOS 的“时间管家”和“任务闹钟”,它负责:

  1. 推进系统时间(滴答计数);
  2. 叫醒“睡够了”的任务(超时任务);
  3. 告诉调度器“该换人干活了”(需要切换任务)。

2. PendSV 如何解决“上下文切换被中断阻塞”的问题

  • PendSV 的核心价值:PendSV(挂起的服务调用)是 Cortex - M 内核专门为 RTOS 设计的“延迟执行型异常” 。它的解决思路是:把“上下文切换请求”延迟到“所有其他 IRQ 处理程序都执行完毕后”再执行
  • 实现关键:要达到这个效果,需将 PendSV 配置为系统中优先级最低的异常 。这样,只要有其他 IRQ 在处理(不管是高优先级还是低优先级 ),PendSV 就会“排队等待”;只有所有 IRQ 处理完,PendSV 才会被响应。
  • 执行流程:当 OS 决策“需要上下文切换”时,并不会立刻执行切换,而是设置 PendSV 的“挂起状态”(通过写 NVIC 的 ICSR 寄存器 )。之后,当系统进入 PendSV 异常处理函数时,在 PendSV 里执行真正的上下文切换(保存当前任务寄存器、加载下一个任务寄存器 )。

关联知识

  • FreeRTOS 中,任务切换的“触发”与“执行”是分离的:
    • 调用 taskYIELD() 或任务阻塞(如 vTaskDelay() )时,会触发“请求上下文切换”(本质是设置 PendSV 挂起 );
    • 真正的切换动作,在 PendSV 异常处理函数中完成(如 xPortPendSVHandler )。
  • 这样设计的好处是:保证上下文切换时,没有任何 IRQ 干扰 。因为 PendSV 优先级最低,能确保“所有 IRQ 都处理完了,才执行切换”,既避免了故障,又保障了实时性。

1. 流程图解举例

在这里插入图片描述
下面是中文图解,更方便大家理解。
在这里插入图片描述

  • PendSV 的核心价值
    通过 “延迟切换请求” 到 “所有中断处理完毕后” 执行,解决图 中的问题。具体流程:

    • 当 OS 收到 “切换请求”(比如任务阻塞、调用 taskYIELD() ),不立刻切换,而是 挂起 PendSV 异常(设置 PendSV 的 pending 状态 )。
    • PendSV 被配置为 最低优先级异常,因此会等到所有高优先级中断(如 IRQ、SysTick )处理完,才会被响应。
  • 分步理解
    这是 FreeRTOS 等实时操作系统(RTOS)中 基于 SVC(系统调用)和 PendSV( Pendable Service Call,可悬起服务调用 )实现任务调度与上下文切换 的典型流程,体现了 RTOS 如何在中断、异常嵌套场景下,安全、有序地完成任务切换,核心围绕 “通过 PendSV 保证切换时机的安全性(避开高优先级中断干扰)” 设计。

1. 流程本质:用 SVC 发起请求 + PendSV 执行切换,实现安全调度

RTOS 中,直接在高优先级中断/异常里执行任务切换,容易因打断关键操作引发问题。所以设计 “两步走” 机制

  • SVC 负责 “发起切换请求”(类似 “挂号”),告诉 OS “需要切换任务了”;
  • PendSV 负责 “执行切换操作”(类似 “看病”),但会 延迟到所有高优先级中断/异常处理完后 执行,保证切换过程不被干扰。
2. 分步解析
1. 无中断嵌套场景(步骤1-4解析)
  1. 任务 A 调用 SVC 请求切换

    • 场景:任务 A 因等待资源(如信号量、延时)无法继续执行,需要 OS 调度其他任务(如任务 B)运行。
  2. OS 准备切换,挂起 PendSV 异常

    • OS 收到 SVC 请求后,不会立刻切换(避免打断当前关键流程 ),而是通过写 NVIC 寄存器(如 ICSRPENDSVSET 位 ),“挂起” 一个 PendSV 异常
    • 关键设计:PendSV 被配置为 系统中优先级最低的异常,确保它会 “排队等待”,等所有高优先级中断/异常处理完,才会执行。
  3. 退出 SVC,进入 PendSV 执行切换

    • CPU 处理完 SVC 异常后,会检查 PendSV 是否挂起。由于 PendSV 优先级最低(假设无更高优先级中断 ),会立即进入 PendSV 异常处理函数。
    • 核心操作:在 PendSV 处理函数中,OS 执行 上下文切换——保存任务 A 的寄存器到堆栈,恢复任务 B 的寄存器到 CPU,更新 pxCurrentTCB(当前任务控制块指针 ),完成任务 A → 任务 B 的切换。
  4. PendSV 结束,回到任务 B(线程模式)

    • PendSV 异常处理完毕后,CPU 回到 “线程模式”(非中断/异常模式 ),继续从任务 B 上次暂停的位置执行,任务切换完成。
2. 中断嵌套场景:PendSV 如何保障切换安全性(步骤 5 - 10 解析)

这部分体现 “PendSV 延迟执行,避开高优先级中断干扰” 的核心设计:
5. 中断触发,进入 ISR(中断服务程序)

  • 系统运行中发生外部中断(如定时器 ),CPU 进入中断模式,执行对应的 ISR。
  1. SysTick 异常抢占 ISR

    • SysTick 是 RTOS 的 “心跳”(通常用于任务调度计时 ),优先级高于普通中断。若 SysTick 触发时,ISR 正在执行,SysTick 会 抢占 ISR(因为优先级更高 ),进入 SysTick 异常处理。
  2. OS 挂起 PendSV,准备切换

    • SysTick 处理中,OS 发现需要切换任务(如任务 B 时间片到期,或有更高优先级任务就绪 ),同样通过 “挂起 PendSV” 的方式,标记 “需要切换任务”,但不立即执行
  3. 退出 SysTick,回到被抢占的 ISR

    • SysTick 异常处理完毕后,CPU 回到被抢占的 ISR,继续执行未完成的中断逻辑——体现 “中断嵌套” 与 “异常优先级” 的规则:高优先级异常(SysTick)处理完,返回低优先级中断(ISR)。
  4. ISR 结束,执行 PendSV 切换

    • ISR 执行完毕退出后,CPU 检查到 PendSV 挂起(且无更高优先级中断 ),进入 PendSV 异常处理函数,执行上下文切换(比如从任务 B 切回任务 A )。
  5. PendSV 结束,回到任务 A(线程模式)

    • PendSV 处理完切换逻辑后,CPU 回到线程模式,任务 A 从上次暂停处继续执行——完成一次 “中断嵌套 + 任务切换” 的完整流程。
3. 核心设计思想总结
  • SVC 做 “请求”:灵活触发任务切换诉求,让应用层任务能主动/被动通知 OS 调度;
  • PendSV 做 “执行”:利用 “低优先级、可延迟” 特性,确保切换操作在 所有高优先级中断/异常处理完毕后 执行,避免切换过程被打断,保障系统稳定性;
  • 中断嵌套兼容:通过异常优先级管理(SysTick > 普通 ISR > PendSV ),既保证实时性(高优先级中断及时响应 ),又保证切换安全性(PendSV 最后执行 )。
  • 设计思想
    利用 PendSV “低优先级、可延迟触发” 的特性,确保 上下文切换发生在 “无活跃中断” 的安全时机,既不破坏中断实时性,又能正确完成多任务调度。

这套机制是 RTOS 实现 “多任务并发 + 中断实时响应” 的基石,理解它就能掌握 FreeRTOS 等系统调度的核心逻辑,应对任务切换、中断优先级配置等开发难题。

2. 错误实例图解

在这里插入图片描述
这里简单介绍一下,上图是错误示例,大家了解一下就行了。当OS在滴答定时器中断之前,有一个中断IRQ进来了。所以滴答定时器的中断打断了这个IRQ的执行。在滴答定时器中,OS进行了任务切换,所以IRQ被搁置在一边,CPU先执行了任务B,之后又是滴答定时器中断,最后IRQ才有机会继续执行。
应该不用我说,大家也知道问题出在哪里了,对吧?中断本来需要实时响应的,在这里被强行分成两部分,而且在这种情况下,OS进行了任务切换,这是不合理的。在异常活跃的时候,OS不允许切换到“线程模式”(即任务切换,并且让任务B优先执行),否则会引发Usage Fault


本文结束,欢迎大家关注、点赞、收藏、转发!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值