深入解析:单片机裸机和RTOS中断任务切换那些事~
目录
5.PendSV Handler(FreeRTOS 内核关键函数)
0、前置知识点(关键)
1.核心寄存器概览
寄存器 | 别名 | 主要用途 |
---|---|---|
R0 | - | 通用寄存器/参数/结果 |
R1 | - | 通用寄存器/参数 |
R2 | - | 通用寄存器/参数 |
R3 | - | 通用寄存器/参数 |
R4 | - | 通用寄存器(必须保护) |
R5 | - | 通用寄存器(必须保护) |
R6 | - | 通用寄存器(必须保护) |
R7 | - | 通用寄存器(帧指针,在某些模式下) |
R8 | - | 通用寄存器(必须保护) |
R9 | - | 通用寄存器(必须保护) |
R10 | - | 通用寄存器(必须保护) |
R11 | - | 通用寄存器(必须保护) |
R12 | - | 通用寄存器(临时寄存器,用于函数调用) |
R13 | SP | 堆栈指针 |
R14 | LR | 链接寄存器(存放函数返回地址) |
R15 | PC | 程序计数器(指向当前执行的指令) |
2.详解R4-R11
(1)根本性质:通用寄存器
R4-R11的核心作用是临时存储数据和地址通过。它们能够被CPU的绝大多数指令(如加减乘除、逻辑运算、资料加载/存储)自由使用。程序员和编译器会用它们来存放计算中的中间变量、循环计数器、数组下标等。
(2)关键特性:被调用者保存寄存器
这是理解R4-R11行为的关键。在函数调用过程中,为了确保一个函数(调用者)不会因为调用另一个函数(被调用者)而丢失自己正在使用的资料,有一套严格的寄存器采用约定。
R0-R3, R12:被称为调用者保存寄存器。
如果调用者(Caller)函数希望在这些寄存器中保留值,它必须在自己调用其他函数之前,手动将这些寄存器的值保存到堆栈上。
被调用者(Callee)可以随意修改这些寄存器。
R4-R11:被称为被调用者保存寄存器。
如果一个函数(被调用者)打算启用R4-R11中的任何一个寄存器,它必须在函数的开头将这些寄存器的值压入堆栈(PUSH)进行保存,并在函数结束返回前,再从堆栈中恢复(POP)这些值。
“受保护的”,可以放心地认为即使调用了子函数,这些寄存器里的值也不会改变。就是对于调用者来说,R4-R11的值就像
一、单片机中断上下文切换
实际执行流程(以 Cortex-M 为例):
1.硬件自动压栈(进入中断瞬间)
CPU 自动把 R0-R3、R12、LR、PC、xPSR 压到当前栈(MSP/PSP)。
2.软件手动压栈(ISR 开始)
ISR 开头编译器会生成 PUSH {R4-R11}
,保存高寄存器。
ISR_Handler:
PUSH {R4-R11} ; 软件手动保存现场(ISR一开始)
; -------------------
; 中断处理逻辑
; -------------------
POP {R4-R11} ; 软件恢复现场(ISR退出前)
BX LR ; 硬件自动恢复现场并返回
3.中断处理逻辑
ISR 内部自由使用寄存器(不怕覆盖主程序的值)。
4.软件手动出栈(ISR 结束)
ISR 退出前 POP {R4-R11}
,恢复保存的寄存器值。
5.硬件自动出栈(执行 BX LR
)
CPU 自动从栈里弹出 R0-R3、R12、LR、PC、xPSR,恢复到中断前的执行点。
二、RTOS中断上下文切换
1.任务A运行中
PSP 指向任务A的栈。
2.中断发生
硬件自动压栈:R0-R3, R12, LR, PC, xPSR → PSP
编译器压栈:R4-R11 → MSP(当前使用的栈,此时已切换到中断),压入栈的是任务A的寄存器原值,此时中断写R4-R11不会影响任务A的现场。
3.执行ISR
如果调用了 FreeRTOS 的
...FromISR()
API,并且解锁了一个高优先级任务 → 标记需要任务切换。
4.中断退出前触发 PendSV
FreeRTOS 在 ISR 里设置 PendSV 异常挂起。
ISR 结束后,CPU 自动跳到 PendSV Handler,执行这个函数的时候就会把R4-R11恢复到任务A的PSP中。
5.PendSV Handler(FreeRTOS 内核关键函数)
保存任务A的栈指针(PSP)到任务控制块(TCB)。
从调度器里选出下一个任务(任务B)。
加载任务B的栈指针(PSP)。
从任务B的栈恢复寄存器(POP R4-R11 + 硬件自动恢复 R0-R3,PC 等)。
6.任务B开始执行
“从中断直接跳到任务B”,但实际上是就是好像PendSV 完成了上下文切换。
三、RTOS中断错误切换
下面给你看三个最常见、也是最致命的污染路径(逐条按时间线解释)。
1. 寄存器污染(R4-R11 被提前改写)
时间线:
任务A在 PSP 上运行,寄存器(含 R4-R11)里有它自己的值。
中断发生:硬件把
R0-R3,R12,LR,PC,xPSR
压到 PSP,R4-R11 还在寄存器里(还没保存)。处理器切到 Handler 模式,改用 MSP。在 ISR 里你错误地调用了任务 API(非 FromISR),这是一个“普通 C 函数调用”,编译器会很自然地用到 R4-R11(callee-saved)。
注意:此时 R4-R11 依然是“任务A的寄存器值”,但你在 ISR 里把它们当成可用临时寄存器用了,也会被函数序言/内部逻辑修改。
ISR 结束前,FreeRTOS 可能因为你该 API 的动作而触发了 PendSV做任务切换。
PendSV(naked) 入口的第一件事是:
MRS r0, psp
取到任务A的 PSP,然后STMDB r0!, {r4-r11}
把 当前寄存器里的 R4-R11 压到 任务A 的 PSP,并写回到 TCB 的pxTopOfStack
。
但这些 R4-R11 已经不是“任务A被打断时的原始值”了(被你在 ISR 里误用的任务 API 改过)。
等将来切回任务A时,“被污染后的 R4-R11”就是恢复出来的——任务A用这些错误的寄存器继续跑,各种诡异挑战就来了:
局部变量/静态链表指针错乱、返回地址拼不起来、最终HardFault/跑飞。
精髓:PendSV 期望在保存 R4-R11 之前,没人动过它们。ISR 里调用“普通任务 API”正好打破了这个前提。
2. 栈指针错配(把 MSP 当 PSP 用)
时间线:
中断发生后,被打断任务的硬件堆栈帧在 PSP,此时 ISR 在 MSP 上运行。
你在 ISR 里调用了任务 API。这类 API 在设计上默认自己运行在任务上下文(PSP),很多路径会**基于“当前 SP 就是任务栈”**去做事(保存/恢复局部上下文、计算可用栈、甚至有的端口会采样 SP 进
pxTopOfStack
等)。但现在 SP=MSP(因为在 Handler 模式),导致这些内部操作基于错误的栈:
可能把 MSP 的值(中断栈指针)当成任务栈指针去保存到 TCB;
可能在 MSP上做出“以为自己在 PSP 上做”的入栈/出栈;
结果是 TCB 里的
pxTopOfStack
与实际 PSP 脱节,或直接把中断栈内容写进任务栈,两边都乱套。
后果:下一次上下文切换或异常返回时,PSP/TCB 指针对不上,取回来的栈顶不是它自己 →恢复失败/崩溃。
精髓:任务 API 假设 SP=PSP,但在 ISR 中 SP=MSP。错用导致“把 A 的事写到了 B 的地方”。
3. 错误的阻塞/让出(在 ISR 里企图阻塞)
时间线:
某些任务 API 允许阻塞(如
xQueueSend()
带portMAX_DELAY
),内部会走“加入事件队列、调用portYIELD_WITHIN_API()
或触发调度”的路径。ISR 不能阻塞:它正处于 异常嵌套的“半保存状态”(被中断的任务的硬件堆栈帧还在 PSP 上、尚未出栈)。
要是你在 ISR 里走到“阻塞/切换”的路径:
内核会试图 切换到别的任务,但当前上下文不是任务,而是异常上下文;
异常返回序列(依据 LR=EXC_RETURN 自动出栈 PSP 的硬件帧)被你“半路换车”:TCB/PSP 可能已被改到另一个任务上;
于是 异常返回恢复不了(EXC_RETURN 指向旧帧,PSP 指到新任务),直接HardFault / UsageFault。
精髓:异常返回是一套严格的硬件流程;ISR 里发起“像任务那样的阻塞/切换”会把这套流程半途打断,硬件和内核对“不该在此时发生的切换”没有共同语义,必炸。