FreeRTOS任务切换
FreeRTOS任务切换
- 前言
- 一、任务切换的核心原理
- 1. 本质定义:任务切换 = CPU 寄存器切换
- 2. 两步走流程(以任务 A → 任务 B 为例)
- ① 保存现场(任务 A 的寄存器 → 任务 A 堆栈)
- ② 恢复现场(任务 B 堆栈 → CPU 寄存器)
- 3. 任务切换的完整过程
- (1) 核心前提:任务的 “独立运行环境” 靠堆栈和 TCB 维护
- (2) 任务切换的完整流程(分 6 步,结合硬件 + 软件逻辑 )
- 步骤 1:触发任务切换请求
- 步骤 2:进入 PendSV 异常(硬件接管,准备切换 )
- 步骤 3:保存任务 A 的现场(寄存器 → 任务 A 堆栈 )
- 步骤 4:更新当前任务指针(调度器选任务 B )
- 步骤 5:恢复任务 B 的现场(任务 B 堆栈 → CPU 寄存器 )
- 步骤 6:退出 PendSV 异常,运行任务 B
- 4. 关键细节:为什么要分 “自动压栈” 和 “手动压栈”?
- 5. 任务切换涉及的寄存器
- 6. 总结
- (1) 任务切换的本质是 “堆栈 + 寄存器的替换”
- (2) 涉及的其他知识点
- 二、PendSV和任务切换
- 1. PendSV 基础定义
- 2. PendSV 在上下文切换中的核心价值
- 3. PendSV中断触发逻辑
- 4. PendSV中断触发代码逻辑分析
- (1) vTaskDelay()函数触发PendSV
- (2) SysTick中断触发进一步触发PendSV
- 三、PendCV中断处理函数
- 1. 进入PendSV中断处理函数之后PSP指向哪里?
- 2. r2寄存器获取的是什么内容?
- 3. 为什么r2寄存器要获取这个地址?
- 4. 对r14寄存器的判断
- 5. 手动压栈r14,r4-r11
- 6. r0寄存器内容写入r2寄存器对应的内存里面
- 补充本文涉及的其余知识点
- 1. `xTaskIncrementTick()`函数简介
- 1. 核心作用:驱动系统时钟与任务调度
- 2. 具体功能拆解
- (1)更新系统时间
- (2)唤醒超时的任务
- (3)决定是否需要切换任务
- 3. 与其他组件的协作
- 4. `xTaskIncrementTick()`的重要性
- 5. 总结:一句话概括
- 2. PendSV 如何解决“上下文切换被中断阻塞”的问题
- 流程图解举例
前言
本文详细解释了任务切换的机制和原理,清晰详细地解释了PendSV中断处理函数的内部代码逻辑,全网很少有人做到这样,讲的如此清晰。先不要觉得我吹牛,看过之后,也许你会竖大拇指,哈哈哈,欢迎点赞、收藏、转发!
一、任务切换的核心原理
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 内核特性 ):
-
自动压栈(部分寄存器 ):
CPU 会把当前任务 A 的xPSR
、PC
、LR
、R12
、R0 - R3
寄存器,自动压入任务 A 的堆栈(硬件自动完成,不需要代码干预 )。
这一步对应 “保存现场” 的前半部分。 -
手动压栈(剩余寄存器 ):
剩下的寄存器(如R4 - R11
),需要在 PendSV 异常处理函数中,手动用汇编代码压栈。
例如 FreeRTOS 的xPortPendSVHandler
中,会有类似:STMDB SP!, {R4 - R11} ; 把 R4 - R11 压入当前任务堆栈
这一步完成 “保存现场” 的全部操作——任务 A 的所有寄存器,都存在自己的堆栈里了。
步骤 4:更新当前任务指针(调度器选任务 B )
保存完任务 A 的现场后,软件(调度器 )会做:
-
找到下一个任务(任务 B ):
调度器遍历 “就绪任务列表”,选最高优先级的就绪任务(假设是任务 B )。 -
更新 TCB 指针:
把全局变量pxCurrentTCB
(指向当前运行任务的 TCB ),从任务 A 的 TCB 换成任务 B 的 TCB。
步骤 5:恢复任务 B 的现场(任务 B 堆栈 → CPU 寄存器 )
-
手动出栈(剩余寄存器 ):
从任务 B 的堆栈中,手动恢复R4 - R11
寄存器(和保存时对称 )。
例如:LDMIA SP!, {R4 - R11} ; 从任务 B 堆栈恢复 R4 - R11
-
自动出栈(部分寄存器 ):
硬件自动从任务 B 的堆栈中,恢复xPSR
、PC
、LR
、R12
、R0 - R3
寄存器(和步骤 3 对称 )。这一步完成 “恢复现场”——任务 B 的寄存器值,全部回到 CPU 中,CPU 认为 “自己一直在运行任务 B”。
步骤 6:退出 PendSV 异常,运行任务 B
恢复完任务 B 的现场后,PendSV 异常处理函数执行 BX LR
(汇编指令 ),退出异常。
此时,CPU 回到 “线程模式”,开始从任务 B 上次暂停的地方(PC
寄存器记录的地址 )继续执行——任务切换完成!
4. 关键细节:为什么要分 “自动压栈” 和 “手动压栈”?
Cortex - M 内核设计时,为了 “加速中断响应”,规定:
- 异常进入时,硬件会自动压栈
xPSR
、PC
、LR
、R12
、R0 - R3
(这些是调用函数时最常用的寄存器,优先保存 )。 - 剩下的寄存器(R4 - R11 ) 属于 “调用者保存寄存器”,需要软件手动压栈/出栈(否则会被破坏 )。
因此,在 PendSV 异常处理中,必须用汇编代码手动处理 R4 - R11,保证任务现场完整。
5. 任务切换涉及的寄存器
下图来自《FreeRTOS任务调度器的启动流程和第一个任务被调用的全过程》如有疑惑,请参考这篇文章。
6. 总结
(1) 任务切换的本质是 “堆栈 + 寄存器的替换”
整个过程,本质就是:
- 把任务 A 的寄存器(现场 )完整保存到它的堆栈;
- 把任务 B 的寄存器(现场 )从它的堆栈恢复到 CPU;
- 通过更新
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;PRESERVE8mrs r0, pspisb/* Get the location of the current TCB. */ldr r3, =pxCurrentTCBldr r2, [r3]/* Is the task using the FPU context? If so, push high vfp registers. */tst r14, #0x10it eqvstmdbeq 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_PRIORITYmsr basepri, r0dsbisbbl vTaskSwitchContextmov r0, #0msr basepri, r0ldmia 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 registerstoo. */tst r14, #0x10it eqvldmiaeq r0!, {s16-s31}msr psp, r0isb#ifdef WORKAROUND_PMU_CM001 /* XMC4000 specific errata */#if WORKAROUND_PMU_CM001 == 1push { r14 }pop { pc }nop#endif#endifbx r14
}
下面进行讲解代码:
图片上解释了前几句的代码,但还需要进一步补充。
上图中一处错误:
修改:图中对于r2和r3寄存器的内容写的不对,这里特别容易搞混,所以要特别细心地分析。我也是耗费了点脑细胞,好不容易理清楚了。整理了下面的图片。
注:下图包含了我的思路和心血,如果您要用在别处,希望一定一定要注明出处,谢谢!保护版权,互相尊重!
1. 进入PendSV中断处理函数之后PSP指向哪里?
mrs r0, psp
:在进入中断后,硬件会自动保存一部分寄存器到任务栈。具体操作是PSP堆栈寄存器会将下图中的寄存器压栈到当前的任务栈中。之后指向了下图中R0的位置。下图截取自《FreeRTOS任务调度器的启动流程和第一个任务被调用的全过程》。
2. r2寄存器获取的是什么内容?
ldr r3, =pxCurrentTCBldr 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, #0x10it eqvstmdbeq r0!, {s16-s31}
第一句,判断r14
寄存器的bit4
是不是1
?如果是1,那么就不使用浮点运算单元FPU
。如果是0
,那就是使用FPU
,就要压栈寄存器s16-s31
。因为FPU
有32
个寄存器,包括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指向哪里?
标题上面,我绘制的图片,一看就懂了,现在截取到这里,省的大家上下翻动界面。
已经晚上十一点半了,挨不住了,未完待续……
补充本文涉及的其余知识点
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 的“时间管家”和“任务闹钟”,它负责:
- 推进系统时间(滴答计数);
- 叫醒“睡够了”的任务(超时任务);
- 告诉调度器“该换人干活了”(需要切换任务)。
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 都处理完了,才执行切换”,既避免了故障,又保障了实时性。
流程图解举例
-
PendSV 的核心价值:
通过 “延迟切换请求” 到 “所有中断处理完毕后” 执行,解决图 中的问题。具体流程:- 当 OS 收到 “切换请求”(比如任务阻塞、调用
taskYIELD()
),不立刻切换,而是 挂起 PendSV 异常(设置 PendSV 的 pending 状态 )。 - PendSV 被配置为 最低优先级异常,因此会等到所有高优先级中断(如 IRQ、SysTick )处理完,才会被响应。
- 当 OS 收到 “切换请求”(比如任务阻塞、调用
-
分步理解:
- 请求阶段:任务调用 SVC 触发切换请求 → OS 准备切换,并 “挂起 PendSV”(先不执行切换 )。
- 延迟执行:CPU 退出 SVC 后,因为 PendSV 优先级最低,会等所有 IRQ、SysTick 等中断处理完,才进入 PendSV 异常。
- 安全切换:在 PendSV 异常处理函数中,执行真正的上下文切换(保存 Task A 寄存器 → 加载 Task B 寄存器 )。
- 后续中断处理:若切换后又发生 SysTick 中断,OS 会先处理必要操作(如更新任务超时、调度器心跳 ),再挂起 PendSV → 等中断处理完,再次触发切换(比如切回 Task A )。
-
设计思想:
利用 PendSV “低优先级、可延迟触发” 的特性,确保 上下文切换发生在 “无活跃中断” 的安全时机,既不破坏中断实时性,又能正确完成多任务调度。