操作系统开发:(10) 线程创建与调度的底层原理:从硬件行为解释线程
RTOS任务切换机制的核心在于通过异常处理来保存和恢复任务现场。首次任务启动时,通过SVC异常处理函数伪造任务栈帧,设置PSP指向新任务栈,异常返回时加载伪造的寄存器值实现任务跳转。任务切换则通过PendSV异常完成:1)保存当前任务的R4-R11到PSP栈;2)调用调度器选择新任务;3)从新任务栈恢复R4-R11;4)设置PSP指向新任务栈,异常返回时自动加载剩余寄存器。两种方式本质相同,区别在
核心:无论是执行第一个新任务的伪造现场,还是旧任务切换到新任务时的切换现场,核心都是在异常处理时设置PSP为新任务的栈,然后从这个栈中弹出值到寄存器中恢复新任务的现场,只不过执行第一个新任务时栈中的值是伪造的,切换到新任务时栈中的值是在PendSV异常中保存的。
1. 通过 SVC 异常执行第一个任务
1.1 源码
// 57. 初始化任务栈函数定义,头文件 64
// 在创建任务时,手动模拟一个中断返回时的栈帧结构,以便任务第一次被调度运行时,能像从中断返回一样正确地跳转到任务入口函数
StackType_t * port_pu32InitStack( StackType_t * pxTopOfStack,
TaskFunc_t pxCode,
void * pvParameters ){
pxTopOfStack--;
*pxTopOfStack = portINITIAL_XPSR; /* xPSR */
pxTopOfStack--;
*pxTopOfStack = ( ( StackType_t ) pxCode ) & portSTART_ADDRESS_MASK; /* PC */
pxTopOfStack--;
*pxTopOfStack = ( StackType_t ) portTASK_RETURN_ADDRESS; /* LR */
pxTopOfStack -= 5; /* R12, R3, R2 and R1. */
*pxTopOfStack = ( StackType_t ) pvParameters; /* R0 */
pxTopOfStack--;
*pxTopOfStack = portINITIAL_EXC_RETURN;
pxTopOfStack -= 8; /* R11, R10, R9, R8, R7, R6, R5 and R4. */
return pxTopOfStack;
}
// 59. SVC异常处理函数定义,把系统从“启动模式”切换到“任务运行模式”,让第一个任务开始干活
// msr psp, r0 把栈指针切换到任务专用的栈
void port_vSVCHandler( void )
{
__asm volatile (
" ldr r3, pxCurTCBConst2 \n"
" ldr r1, [r3] \n"
" ldr r0, [r1] \n"
" ldmia r0!, {r4-r11, r14} \n"
" msr psp, r0 \n"
" isb \n"
" mov r0, #0 \n"
" msr basepri, r0 \n"
" bx r14 \n"
" \n"
" .align 4 \n"
"pxCurTCBConst2: .word g_pxCurTCB \n"
);
}
// 60. 启动第一个任务函数定义,完成最后的系统初始化,然后按下 SVC 0 按钮,触发之前的 port_vSVCHandler,让第一个任务正式开始运行
static void port_prv_vStartFirstTask( void )
{
__asm volatile (
" ldr r0, =0xE000ED08 \n"
" ldr r0, [r0] \n"
" ldr r0, [r0] \n"
" msr msp, r0 \n"
" mov r0, #0 \n"
" msr control, r0 \n"
" cpsie i \n"
" cpsie f \n"
" dsb \n"
" isb \n"
" svc 0 \n"
" nop \n"
" .ltorg \n"
);
}
FLASH (0x08000000 – 0x0801FFFF, 128KB)
┌──────────────────────────────────────┐
│ .isr_vector (中断向量表) │ ← 0x08000000
├──────────────────────────────────────┤
│ .text (代码) │
├──────────────────────────────────────┤
│ .rodata (只读数据) │
├──────────────────────────────────────┤← _sidata
│ .data 副本 (初始化数据镜像) │
└──────────────────────────────────────┘
RAM (0x20000000 – 0x20007FFF)
┌──────────────────────────────┐
│ .data (运行时初始化数据) │ ← 0x20000000 (_sdata)
├──────────────────────────────┤← _sbss
│ .bss (未初始化数据) │
├──────────────────────────────┤← _ebss
│ 未使用区域(安全缓冲) │
├──────────────────────────────┤
│ 栈 (Stack) 向下/低地址增长 |
└──────────────────────────────┘ ← 0x20008000 (_estack)
注意:压入栈时向低地址增长,但是异常返回时从低地址向高地址读取数据!
1.2 流程
第一个任务开始执行时:
上电
↓
CPU 读 0x0 / 0x4 → 设置 SP 和 PC
↓
执行 Reset_Handler → 调用 main()
↓
main() 中创建任务 → 分配栈 + 调用 port_pu32InitStack 伪造栈帧
↓
启动调度器
↓
触发 svc 异常,硬件自动执行:压入现场(当前8个寄存器的值)到MSP栈中,并设置r14(控制异常从PSP返回而不是MSP)
↓
svc 异常处理:设置栈指针 PSP 为新任务的栈,
↓
异常返回到 PSP:自动从 PSP 指向的新任务的栈顶依次弹出8个值并加载到寄存器(加载新任务的现场)
↓
加载新任务伪造的现场的值到PC:程序从PC开始执行,即新任务的任务函数
↓
新任务开始执行!
当任务被创建时,它从未运行过,那么异常返回就需要返回到这个从未运行过的任务。
Cortex-M 处理器在进入异常时,会自动将 8 个寄存器的值压入当前栈,自动保存当前现场,然后异常返回时
如果不伪造,则异常返回加载8个值时,就与异常返回应该获取的8个寄存器值对应不上,导致 HardFault.
1.3 预处理:伪造栈帧 port_pu32InitStack
// 57. 初始化任务栈函数定义,头文件 64
// 在创建任务时,手动模拟一个中断返回时的栈帧结构,以便任务第一次被调度运行时,能像从中断返回一样正确地跳转到任务入口函数
StackType_t * port_pu32InitStack( StackType_t * pxTopOfStack,
TaskFunc_t pxCode,
void * pvParameters ){
pxTopOfStack--;
*pxTopOfStack = portINITIAL_XPSR; /* xPSR */
pxTopOfStack--;
*pxTopOfStack = ( ( StackType_t ) pxCode ) & portSTART_ADDRESS_MASK; /* PC */
pxTopOfStack--;
*pxTopOfStack = ( StackType_t ) portTASK_RETURN_ADDRESS; /* LR */
pxTopOfStack -= 5; /* R12, R3, R2 and R1. */
*pxTopOfStack = ( StackType_t ) pvParameters; /* R0 */
pxTopOfStack--;
*pxTopOfStack = portINITIAL_EXC_RETURN;
pxTopOfStack -= 8; /* R11, R10, R9, R8, R7, R6, R5 and R4. */
return pxTopOfStack;
}
|
步骤 |
代码 |
作用详解 |
寄存器与说明 |
|---|---|---|---|
|
1 |
pxTopOfStack--; |
栈顶指针下移 1(4 字节),为压入 xPSR 腾出空间。 |
栈是向下增长的(满递减栈)。初始 pxTopOfStack 指向分配内存的最高地址+1,需先减再写。 |
|
2 |
*pxTopOfStack = portINITIAL_XPSR; |
写入初始 xPSR(程序状态寄存器) |
将 xPSR 写入当前地址 |
|
3 |
pxTopOfStack--; |
指针下移,准备写入 PC(任务入口地址)。 |
— |
|
4 |
*pxTopOfStack = ((StackType_t) pxCode) & portSTART_ADDRESS_MASK; |
写入任务函数地址 |
异常恢复后会把这个值(任务函数的地址)加载到 PC |
|
5 |
pxTopOfStack--; |
指针下移,准备写入 LR。 |
— |
|
6 |
*pxTopOfStack = (StackType_t) portTASK_RETURN_ADDRESS; |
写入“伪返回地址”。 |
异常恢复后会把这个值加载到 LR,若任务意外返回,会跳转到此地址 → 触发 HardFault,便于调试。 |
|
7 |
pxTopOfStack -= 5; |
一次性下移 5 个单位(20 字节),跳过 R12, R3, R2, R1, R0 的位置(稍后单独初始化 R0)。 |
R12, R3, R2, R1 这些寄存器在 AAPCS 中属于 caller-saved,任务启动时可为任意值 |
|
8 |
*pxTopOfStack = (StackType_t) pvParameters; |
在 R0 位置写入任务参数 pvParameters |
符合 AAPCS:函数第一个参数通过 R0 传递。任务函数将收到此参数。 |
|
9 |
pxTopOfStack--; *pxTopOfStack = portINITIAL_EXC_RETURN; |
指针下移,写入 EXC_RETURN 值 |
|
|
10 |
pxTopOfStack -= 8; |
下移 8 个单位(32 字节),跳过 R4–R11 的位置,R4在高地址。 |
这些是 callee-saved 寄存器,任务启动时设为 0(未显式写入,但栈内存通常已清零)。 |
|
11 |
return pxTopOfStack; |
返回更新后的栈顶指针 |
供调度器恢复上下文时使用。 |
完整构造了一个符合异常返回的任务栈帧,这样后续异常返回到重新设置的 PSP 后读取这里的栈帧会认为正确返回了。
1.4 第一步:触发svc异常 port_prv_vStartFirstTask
// 60. 启动第一个任务函数定义,完成最后的系统初始化,然后按下 SVC 0 按钮,触发之前的 port_vSVCHandler,让第一个任务正式开始运行
static void port_prv_vStartFirstTask( void )
{
__asm volatile (
" ldr r0, =0xE000ED08 \n"
" ldr r0, [r0] \n"
" ldr r0, [r0] \n"
" msr msp, r0 \n"
" mov r0, #0 \n"
" msr control, r0 \n"
" cpsie i \n"
" cpsie f \n"
" dsb \n"
" isb \n"
" svc 0 \n"
" nop \n"
" .ltorg \n"
);
}
|
步骤 |
指令 |
操作说明 |
|---|---|---|
|
0 |
函数调用前 |
port_lStartScheduler() 调用此函数,进入启动第一个任务的准备阶段 |
|
1 |
ldr r0, =0xE000ED08 | 获取向量表基地址寄存器的地址 |
|
2 |
ldr r0, [r0] |
从寄存器中获取中断向量表在内存中的实际位置 |
|
3 |
ldr r0, [r0] |
读取向量表第 0 项 _estack |
|
4 |
msr msp, r0 |
将 r0 的值即 _estack 写入到 msp |
|
5 |
mov r0, #0 |
CONTROL 寄存器清零,强制系统进入“特权级 + 使用主栈指针(MSP)”的状态,并禁用浮点单元(FPU)相关特性 |
|
6 |
cpsie i |
使能全局中断 |
|
7 |
cpsie f |
使能浮点异常(若支持) |
|
8 |
dsb |
数据同步屏障,等待所有读写操作完成 |
|
9 |
isb |
指令同步屏障,清空处理器流水线,并丢弃预取的指令,确保后续指令从内存中重新获取 |
|
10 |
svc 0 |
触发 SVC 异常 |
|
11 |
nop |
占位符:防止编译器优化,实际永不执行 |
|
12 |
SVC 处理开始 |
硬件跳转到 port_vSVCHandler |
- 恢复主栈:从向量表读取 MSP 初始值,确保中断有合法栈空间
- 强制特权:清零 CONTROL 寄存器,保证后续操作在特权模式下执行
- 触发异常:svc 0 是唯一能安全启动任务的途径,利用硬件异常机制完成上下文切换
1.5 第二步:在异常中启动第一个任务 port_vSVCHandler
// 59. SVC异常处理函数定义,把系统从“启动模式”切换到“任务运行模式”,让第一个任务开始干活
// msr psp, r0 把栈指针切换到任务专用的栈
void port_vSVCHandler( void )
{
__asm volatile (
" ldr r3, pxCurTCBConst2 \n"
" ldr r1, [r3] \n"
" ldr r0, [r1] \n"
" ldmia r0!, {r4-r11, r14} \n"
" msr psp, r0 \n"
" isb \n"
" mov r0, #0 \n"
" msr basepri, r0 \n"
" bx r14 \n"
" \n"
" .align 4 \n"
"pxCurTCBConst2: .word g_pxCurTCB \n"
);
}
|
步骤 |
指令 |
作用详解 |
涉及寄存器 |
|---|---|---|---|
|
1 |
ldr r3, pxCurTCBConst2 |
将符号 pxCurTCBConst2 的值即 g_pxCurTCB 的地址加载到 r3。 |
r3:临时指针寄存器,用于存放 g_pxCurTCB 的地址。 |
|
2 |
ldr r1, [r3] |
从 r3 指向的内存(即 &g_pxCurTCB)中读取值 → 得到当前任务 TCB 的指针值,存入 r1。 |
r1:保存 &g_pxCurTCB |
|
3 |
ldr r0, [r1] |
从 TCB 结构体首字段(即 pxTopOfStack)读取任务的栈顶指针(已初始化好的上下文栈),存入 r0。 |
r0:保存任务专用栈的栈顶地址 pxTopOfStack |
|
4 |
ldmia r0!, {r4-r11, r14} |
从任务栈中弹出寄存器: |
r4–r11:官方规定的被调用者保存寄存器,任务上下文的一部分。 ldmia : 从 r0 开始,连续加载多个寄存器,加载完后 r0 自动增加(指向下一个未加载的位置),加载完成后,r0 会被更新为 r0 + 总字节数 |
|
5 |
msr psp, r0 |
将更新后的 r0(即任务栈指针)写入 PSP 寄存器。 |
PSP:进程栈指针,任务运行时使用的栈。即 PSP 被设为异常返回栈帧起始地址。 |
|
6 |
isb |
指令同步屏障 |
|
|
7 |
mov r0, #0 |
将立即数 0 加载到 r0,为清除 BASEPRI 做准备。 |
|
|
8 |
msr basepri, r0 |
清除 BASEPRI 寄存器 → 允许所有优先级中断 |
BASEPRI:可屏蔽中断的阈值寄存器。写 0 表示“不屏蔽任何中断” |
|
9 |
bx r14 |
异常返回:跳转到 r14(即 EXC_RETURN 值)。 |
r14 (LR):在异常处理中,LR 被硬件自动设为 EXC_RETURN 码。 |
|
10 |
.align 4 |
确保 pxCurTCBConst2 地址按 4 字节对齐 |
|
|
11 |
pxCurTCBConst2: .word g_pxCurTCB |
定义一个 32 位字,其值为全局变量 g_pxCurTCB 的地址(链接时确定)。 |
供第 1 行 LDR 使用 |
- 硬件压栈:svc #0 触发后,硬件自动保存现场 (8 个寄存器的值)到 MSP 栈
- 软件恢复:从任务栈恢复 R4-R11 + EXC_RETURN (0xFFFFFFFD) 到 r14
- 硬件弹栈:bx r14 触发异常返回,硬件自动从 PSP 弹出 8 个寄存器并切换模式/栈
2. 通过 PendSV 异常切换任务
2.1 源码
// 65. PendSV异常处理函数定义
void port_vPendSVHandler( void )
{
__asm volatile
(
" mrs r0, psp \n"
" isb \n"
" \n"
" ldr r3, pxCurTCBConst \n"
" ldr r2, [r3] \n"
" \n"
" tst r14, #0x10 \n"
" it eq \n"
" vstmdbeq r0!, {s16-s31} \n"
" \n"
" stmdb r0!, {r4-r11, r14} \n"
" str r0, [r2] \n"
" \n"
" stmdb sp!, {r0, r3} \n"
" mov r0, %0 \n"
" msr basepri, r0 \n"
" dsb \n"
" isb \n"
" bl task_vSwitchContext \n"
" mov r0, #0 \n"
" msr basepri, r0 \n"
" ldmia sp!, {r0, r3} \n"
" \n"
" ldr r1, [r3] \n"
" ldr r0, [r1] \n"
" \n"
" ldmia r0!, {r4-r11, r14} \n"
" \n"
" tst r14, #0x10 \n"
" it eq \n"
" vldmiaeq r0!, {s16-s31} \n"
" \n"
" msr psp, r0 \n"
" isb \n"
" \n"
" \n"
" bx r14 \n"
" \n"
" .align 4 \n"
"pxCurTCBConst: .word g_pxCurTCB \n"
::"i" ( cfgMAX_SYSCALL_INTERRUPT_PRIORITY )
);
}
2.2 流程
任务运行中(Thread Mode + PSP)
↓
触发任务切换(如 taskYIELD / task_vDelay / SysTick 唤醒高优任务)
↓
写 ICSR.PENDSVSET 位 → 挂起 PendSV 异常(portYIELD)
↓
(等待安全时机:所有高优先级中断执行完毕)
↓
PendSV 异常触发,CPU 切换到 Handler Mode
↓
硬件自动将当前任务的 8 个寄存器(R0-R3/R12/LR/PC/xPSR)压入 MSP 栈
↓
进入自定义的 PendSV 异常处理函数(port_vPendSVHandler)
↓
读取当前任务的 PSP(mrs r0, psp)→ 获取当前任务栈顶
↓
手动保存 R4-R11 和 EXC_RETURN(0xfffffffd)到当前任务的栈(stmdb r0!, {r4-r11, r14})
↓
更新当前任务 TCB 的 pu32TopOfStack 字段(str r0, [g_pxCurTCB])
↓
调用 task_vSwitchContext():从就绪队列选择最高优先级任务作为新任务
↓
从新任务 TCB 中读取其 pu32TopOfStack → 加载新任务栈顶到 r0
↓
从新任务栈中恢复 R4-R11 和 EXC_RETURN(ldmia r0!, {r4-r11, r14})
↓
设置 PSP 为新任务的栈指针(msr psp, r0)
↓
异常返回(bx r14),因 r14 = 0xfffffffd,硬件从 PSP 弹出 8 个寄存器(R0-R3/PC/xPSR)
↓
加载新任务上次被切换出去时保存的现场值到 PC 等寄存器
↓
新任务继续执行!
可以看出,这里旧任务的现场(即8个寄存器值)在 PendSV 异常处理中被保存在了旧任务的PSP栈中,那么假如在新任务切换到旧任务时,重新读取的PSP栈就不是一开始伪造的,而是在 PendSV 异常处理中手动压入的原始现场。
所以可以得知,无论是执行第一个新任务的伪造现场,还是旧任务切换到新任务时的切换现场,核心都是在异常处理时设置PSP为新任务的栈,然后从这个栈中弹出值到寄存器中恢复新任务的现场,只不过执行第一个新任务时栈中的值是伪造的,切换到新任务时栈中的值是在PendSV异常中保存的。
2.3 详细步骤
|
步骤 |
指令 |
作用详解 |
|---|---|---|
|
1 |
mrs r0, psp |
读取当前任务的 进程栈指针(PSP) 到 r0,用于保存/恢复上下文 |
|
2 |
isb |
指令同步屏障 |
|
3 |
ldr r3, pxCurTCBConst |
加载 g_pxCurTCB 的地址到 r3。 |
|
4 |
ldr r2, [r3] |
从 r3 读取 g_pxCurTCB 的值(当前任务 TCB 指针)到 r2。 |
|
5 |
tst r14, #0x10 |
测试 r14/LR(即 EXC_RETURN 值)的 bit4 是否为 0。 |
|
6 |
it eq |
IT(If-Then)指令:若上一条结果为相等(即 bit4=0),则下一条指令条件执行。 |
|
7 |
vstmdbeq r0!, {s16-s31} |
若使用 FPU,则将高 16 个浮点寄存器(S16-S31)压栈(满递减,写后更新)。 |
|
8 |
stmdb r0!, {r4-r11, r14} |
将 R4-R11 和 R14 压入当前任务栈。 |
|
9 |
str r0, [r2] |
当前任务上下文保存:将更新后的栈顶指针(r0)写回当前任务的 TCB(pxTopOfStack 字段)。 |
|
10 |
stmdb sp!, {r0, r3} |
将 r0(新栈顶)、r3(g_pxCurTCB 地址)临时压入 MSP 栈(Handler 模式栈)。 |
|
11 |
mov r0, %0 |
将立即数 %0(即 cfgMAX_SYSCALL_INTERRUPT_PRIORITY)加载到 |
|
12 |
msr basepri, r0 |
进入临界区:设置 BASEPRI,屏蔽优先级数值 ≥ 此值的中断(即允许更高优先级中断,禁止低优先级)。 |
|
13–14 |
dsb / isb |
数据/指令同步屏障,确保 BASEPRI 生效。 |
|
15 |
bl task_vSwitchContext |
调用 C 函数 |
|
16 |
mov r0, #0 |
清零 r0 |
|
17 |
msr basepri, r0 |
退出临界区:清除 BASEPRI(写 0),重新使能所有中断。 |
|
18 |
ldmia sp!, {r0, r3} |
从 MSP 栈弹出之前保存的 r0 和 r3。 |
|
19 |
ldr r1, [r3] |
重新加载 g_pxCurTCB(现在指向新任务)到 r1。 |
|
20 |
ldr r0, [r1] |
从新任务 TCB 读取其 栈顶指针(pxTopOfStack) 到 r0。 |
|
21 |
ldmia r0!, {r4-r11, r14} |
从新任务栈中弹出 R4-R11 和 R14 并更新 r0 |
|
22 |
tst r14, #0x10 |
再次测试 EXC_RETURN 的 bit4(判断新任务是否使用 FPU)。 |
|
23 |
it eq |
条件执行准备。 |
|
24 |
vldmiaeq r0!, {s16-s31} |
若新任务使用 FPU,则从栈中恢复 S16-S31。 |
|
25 |
msr psp, r0 |
将更新后的 r0(指向新任务的异常帧起始位置)写入 PSP。 |
|
26 |
isb |
指令同步,确保 PSP 写入完成。 |
|
27 |
bx r14 |
异常返回:跳转到 r14(即 EXC_RETURN 值,如 0xFFFFFFFD),触发硬件自动从 PSP 弹出 xPSR/PC/LR/R12/R3-R0,切换到线程模式 + PSP,新任务开始运行! |
|
28 |
.align 4 |
字节对齐 |
|
29 |
pxCurTCBConst: .word g_pxCurTCB |
定义文字池项,值为 g_pxCurTCB 的地址,供第 3 行 LDR 使用。 |
流程图:

3. 总结

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐


所有评论(0)