目录一开启任务调度器1中断配置检测 port.c 340~3942PendSV SysTick 优先级3配置 SysTick 定时器4 临界区计数器清零 port.c 4055使能 FPU6prvStartFirstTask() → 触发 SVC → 跳转到第一个任务总结二任务切换的底层原理MSP和PSP介绍1阶段一我是裸机2阶段二准备搬家3阶段三开调度器4阶段四第一次进入任务SVC5阶段五多任务并发的假象一张总图三FreeRTOS 调度器启动与任务切换 — 完整流程1、开启调度器vTaskStartScheduler2、启动第一个任务SVC 一次性电梯1关键概念2流程3为什么任务能被启动3、任务切换PendSV 无休止循环1触发链2PendSV 完整流程 — port.c:457-5153、分阶段的数据流4、你实际要碰的 vs 不用碰的四PendSV 和SVC 介绍1介绍pendsv 和 svc2prvStartFirstTask触发SVC3SVC中断做了什么4SysTick 触发 PendSV5pendsv中断里做了什么6SVC vs PendSV 对比一开启任务调度器vTaskStartScheduler() ← FreeRTOS/Source/tasks.c通用层所有芯片一样 ├── xReturn xTaskCreate(prvIdleTask, ...) → 创建空闲任务 ├── #if configUSE_TIMERS → xTimerCreateTimerTask() → 创建定时器任务 ├── portDISABLE_INTERRUPTS() → 关中断 ├── xNextTaskUnblockTime portMAX_DELAY ┐ ├── xTickCount 0 ├→ 初始化全局变量 ├── uxCurrentNumberOfTasks 0 ┘ ├── xPortStartScheduler() ← port.c硬件移植层M4专属 ├── configASSERT(优先级配置) → ① ├── SHPR2 ← PendSV/SysTick 0xF0 → ② ├── SYSTICK_LOAD 179999 → ③ ├── uxCriticalNesting 0 → ④ ├── prvEnableVFP() FPCCR → ⑤ └── prvStartFirstTask() → SVC → 第一个任务 两层分工很清晰tasks.c 管调度器要干什么port.c 管Cortex-M4 怎么干。上层创建任务、设好全局状态底层把 PendSV/SysTick/FPU 配好最后一脚 svc 0 踹进第一个任务。1中断配置检测 port.c 340~394configASSERT( configMAX_SYSCALL_INTERRUPT_PRIORITY ); // 不能为0 configASSERT( portCPUID ! portCORTEX_M7_r0p1_ID ); // 不是有bug的M7版本 configASSERT( portCPUID ! portCORTEX_M7_r0p0_ID ); // ... 下面还有一段 写NVIC优先级寄存器 → 读回 → 看多少bit卡住了 // 用这个验证 configKERNEL_INTERRUPT_PRIORITY 和实际硬件优先级位数一致2PendSV SysTick 优先级[port.c:109 123124397~398]#define portNVIC_SYSPRI2_REG ( * ( ( volatile uint32_t * ) 0xe000ed20 ) ) //109 #define portNVIC_PENDSV_PRI ( ( ( uint32_t ) configKERNEL_INTERRUPT_PRIORITY ) 16UL ) //123 #define portNVIC_SYSTICK_PRI ( ( ( uint32_t ) configKERNEL_INTERRUPT_PRIORITY ) 24UL ) //124 // 使用行 397-398 portNVIC_SYSPRI2_REG | portNVIC_PENDSV_PRI; // PendSV 0xF0 最低 portNVIC_SYSPRI2_REG | portNVIC_SYSTICK_PRI; // SysTick 0xF0 最低 ​ configKERNEL_INTERRUPT_PRIORITY 在Freertos.config.H 里 宏定义3配置 SysTick 定时器SysTick 的预分频和重装载值分散在两个文件串成一条链① FreeRTOSConfig.h:52 — 心跳频率 ​ ​ #define configTICK_RATE_HZ ((TickType_t)1000) // 1000Hz 每1ms一次SysTick中断 ② FreeRTOSConfig.h:49 — CPU 频率 ​ ​ #define configCPU_CLOCK_HZ (SystemCoreClock) // 180MHz ③ port.c:86-87 — SysTick 时钟源 ​ ​ #ifndef configSYSTICK_CLOCK_HZ #define configSYSTICK_CLOCK_HZ configCPU_CLOCK_HZ // 默认CPU时钟 180MHz #endif ④ port.c:713 — 计算重装载值 ​ ​ portNVIC_SYSTICK_LOAD_REG (configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ) - 1UL; // 180,000,000 / 1000 - 1 179,999 一句话总结Cortex-M4 的 SysTick 没有单独的预分频器不像普通定时器有 PSC就直接用 HCLK180MHz 作为时钟源每条指令减 1。重装载值 180M / 1000 - 1 179999所以每 180,000 个周期1ms溢出一次。要改心跳周期改 FreeRTOSConfig.h 里 configTICK_RATE_HZ 就行。4 临界区计数器清零 port.c 405uxCriticalNesting 0; // 一行就完了确保进第一个任务时不在临界区里 ​5使能 FPUFPUFloat Point Unit浮点运算单元。Cortex-M4F 里的 F 就是指带了 FPU。没 FPU 的话float a 3.14 * 2.0这种运算全靠软件模拟慢几十倍。M4 的 FPU 支持单精度float不支持双精度double。你笔记里prvEnableVFP()那个VFPVector Floating Point是 ARM 对 FPU 的叫法——同一个东西ARM 官方文档叫 VFPFreeRTOS 代码里也沿用了这个名字。① prvEnableVFP() 汇编实现 — port.c:317-330 ​ ​ __asm void prvEnableVFP( void ) { PRESERVE8 ​ ldr.w r0, 0xE000ED88 ; CPACR 寄存器地址 ldr r1, [r0] ; 读当前值 orr r1, r1, #(0xf 20); 把 bit20-23 全置 1使能 CP10 CP11 str r1, [r0] ; 写回 CPACR bx r14 ; 返回 } ② 调用链 — port.c:396-414 ​ ​ // xPortStartScheduler() 里的顺序 portNVIC_SYSPRI2_REG | portNVIC_PENDSV_PRI; // ① PendSV 设最低优先级 portNVIC_SYSPRI2_REG | portNVIC_SYSTICK_PRI; // ② SysTick 设最低优先级 vPortSetupTimerInterrupt(); // ③ 配 SysTick 重装载 启动 uxCriticalNesting 0; // ④ 临界区计数器清零 prvEnableVFP(); // ⑤ 使能 FPU *(portFPCCR) | portASPEN_AND_LSPEN_BITS; // ⑥ 开 lazy stacking prvStartFirstTask(); // ⑦ 启动第一个任务 ③ FPU 懒保存 — port.c:411 ​ ​ *( portFPCCR ) | portASPEN_AND_LSPEN_BITS; // FPCCR: 自动保存 懒保存 ​ portFPCCR地址 0xE000EF34的两个高位 ​ 位 名字 作用 bit 31 ASPEN 进出异常时硬件自动压栈/出栈 FPU 寄存器S0~S31 FPSCR bit 30 LSPEN 懒保存——进异常时硬件只做标记不真的压 FPU 寄存器等 ISR 里第一次用 FPU 指令时再压 两个配合的精妙之处 ​ ​ 进 PendSV 中断 ──→ ASPEN1但 LSPEN1懒保存 ↓ CPU 只标记FPU 被占用了不实际压 S0~S31省时间 ↓ PendSV 里如果没有浮点运算 → 一次都不压直接跳过 PendSV 里如果有浮点运算 → 第一句 FPU 指令触发 lazy save才真正压栈 ↓ 退出中断时 → ASPEN 自动恢复 S0~S31如果有保存的话 你之前笔记里 PendSV 只手动压了 R4-R11FPU 的 S0~S31 全靠 ASPEN LSPEN 这套硬件机制处理——不用自己写汇编硬件帮你搞定。这也是为什么 prvEnableVFP() 写 CPACR 开 FPU 之后紧接着就要设这两位的道理。6prvStartFirstTask() → 触发 SVC → 跳转到第一个任务第一跳prvStartFirstTask() — port.c:295-314 ​ ​ ldr r0, 0xE000ED08 ; 读 VTOR向量表偏移寄存器 ldr r0, [r0] ; 读向量表基地址 ldr r0, [r0] ; 读向量表第一项 → 初始 MSP 值 msr msp, r0 ; 把 MSP 复位到栈顶从头开始 cpsie i ; 开全局中断IRQ cpsie f ; 开 Fault 中断 dsb / isb ; 内存屏障 svc 0 ; ★ 触发 SVC 异常CPU 切到特权模式进 ISR 这一步只做两件事复位 MSP 发软中断。不碰任何任务上下文。 ​ 第二跳vPortSVCHandler() — port.c:277-292 ​ ​ ldr r3, pxCurrentTCB ; 拿 TCB 指针的指针 ldr r1, [r3] ; 拿 pxCurrentTCB指向当前任务的 TCB ldr r0, [r1] ; 拿 TCB 第一个字段 → 任务栈顶指针 pxTopOfStack ldmia r0!, {r4-r11, r14} ; ★ 从任务栈弹出 R4-R11、R14软件保存的寄存器 msr psp, r0 ; PSP 弹出后的新栈顶 isb mov r0, #0 msr basepri, r0 ; BASEPRI 0开所有中断 bx r14 ; ★ 返回 → CPU 硬件自动弹 R0-R3,R12,LR,PC,xPSR → PC任务函数 SVC ISR 干的事从 TCB 恢复第一个任务的寄存器设 PSP开中断bx r14 后 CPU 自动跳到任务入口函数执行。总结一张图串起来 ​ xPortStartScheduler() └── prvStartFirstTask() [port.c:295] ├── MSP 复位到向量表栈顶 ├── 开全局中断 └── svc 0 ─────────────────────┐ ▼ vPortSVCHandler() [port.c:277] ├── 读 pxCurrentTCB → 拿到任务栈顶 ├── 弹出 R4-R11, R14 → PSP ├── BASEPRI 0开中断 └── bx r14 ─────────────┐ ▼ CPU 自动弹 R0-R3,R12,LR,PC,xPSR PC 第一个任务函数入口 ✅ 这就是笔记里说的 SVC 只用来启动第一次之后不用——之后任务切换走 PendSVSVC 这扇门只推一次。二任务切换的底层原理MSP和PSP介绍MSP 和 PSP 都是指针寄存器里面存的是一个地址指向当前用到的栈顶。 RAM 内存 MSP 寄存器 ┌──────────┐ 高地址 ┌──────────┐ │ │ │ 0x20003000│ ───→ │ 内核数据 │ ← MSP 指向这里主栈栈顶 内核栈 └──────────┘ │ ... │ ├──────────┤ │ │ PSP 寄存器 │ 任务A数据 │ ← PSP 指向这里任务A的栈顶 ┌──────────┐ │ │ │ 0x20002000│ ───→ │ │ └──────────┘ ├──────────┤ │ 任务B数据 │ ← 切到任务B时 PSP 改指这里 │ │ ├──────────┤ │ │ └──────────┘ 低地址 MSP 和 PSP 本质上就是两个指针变量里面存的值就是哪个位置是当前栈顶。 唯一不同的是 MSP PSP 存的是 内核栈的栈顶地址 当前任务栈的栈顶地址 什么时候用 中断、OS 内核代码 普通任务代码 CPU 怎么选 进异常自动切 退异常看 EXC_RETURN 切 prvStartFirstTask 里那句 msr msp, r0 就是改写 MSP 这个指针的值把它复位到内存最高处。后面 msr psp, r0 就是让 PSP 指向任务 A 的栈。 两个都是指针区别在于指向谁的栈。就这么简单。1阶段一我是裸机上电 → 复位向量 → startup_stm32f429xx.s ├── 设 MSP 栈顶 ├── SystemInit关 FPU但后面会重开 └── __main → main()此时只有MSPCPU 在特权模式跟裸机一模一样。2阶段二准备搬家main() 里你做三件事HAL_Init(); // HAL 结构体初始化 SystemClock_Config(); // 180MHz 各种 _Init(); // LED、Key、USART...然后创建任务xTaskCreate(LedTask, ...) → 在任务A的栈里伪造好正在跑的存档 任务A的栈 ────────────── R4~R11 初始值一般是0 R0~R3 初始值 R14(LR) prvTaskExitError (防止裸返) R15(PC) LedTask 函数指针 ★ 最关键 xPSR 0x01000000Thumb状态 ─────────────────────────此时任务 A 还没跑CPU 还在用 MSP 执行 main。3阶段三开调度器vTaskStartScheduler() // tasks.c通用层 ├── 创建空闲任务 ├── 关中断 ├── 设全局变量xTickCount0, uxCurrentNumberOfTasks 等 └── xPortStartScheduler() // port.c硬件层 ├── ① 查中断配置没毛病 ├── ② PendSV SysTick 最低优先级0xF0 ├── ③ SysTick 启动1ms 一次心跳 ├── ④ 临界区计数归零 ├── ⑤ 开 FPU 懒保存 └── ⑥ prvStartFirstTask() ← 准备跳了4阶段四第一次进入任务SVCprvStartFirstTask() MSP 复位 → 清掉之前的历史 开全局中断 svc 0 ───────────────────┐ ▼CPU 自动切 MSP、R14EXC_RETURN vPortSVCHandler() ← SVC 中断 读 pxCurrentTCB → 拿到任务 A 的栈顶 手动弹 R4~R11, R14 ← 灌进 CPU PSP 任务 A 栈顶 ← 钥匙插进房间 BASEPRI 0 ← 开门允许中断 bx r14 ────────────┐ ▼CPU 按 EXC_RETURN切线程模式PSP 硬件自动弹 R0~R3, R12, LR, PC, xPSR PC LedTask 函数地址 任务A开始跑了此时 CPU 从 MSP 切到 PSP第一次在用任务的栈跑业务代码。5阶段五多任务并发的假象任务 A 跑着跑着1ms 到了SysTick 中断心跳 ├── 当前是任务 A 在跑PSP A的栈 ├── 进中断 → CPU 自动切 MSP硬件压 R0~R3,R12,LR,PC,xPSR 到 A 的栈 ├── xTaskIncrementTick() → 发现该切了 ├── 触发 PendSV不在这里切先退出 SysTick └── 退出 SysTick PendSV 中断优先级最低等所有中断完 ├── 当前用 MSP ├── 读 PSP → 知道任务 A 跑到哪了 ├── 手动压 R4~R11,R14 → 任务 A 的栈存档完整了 ├── vTaskSwitchContext() → 选任务 BpxCurrentTCB B ├── 读 B 的栈顶 → 手动弹 R4~R11,R14 ├── PSP B 的栈顶 └── bx r14 ──→ 硬件弹剩余寄存器 → PC B 的函数 任务B开始跑了之后就是循环心跳 → 决定切 → PendSV 执行 A存档→选B→B恢复 → 下一个心跳...一张总图上电(MSP) ──→ main(MSP) ──→ vTaskStartScheduler(MSP) │ ▼ xPortStartScheduler(MSP) 配 PendSV/SysTick/FPU │ ▼ prvStartFirstTask(MSP) svc 0 ──────────────┐ ▼ vPortSVCHandler(MSP) 从TCB恢复任务A PSP A栈 bx r14 │ ┌──────────────────┘ ▼ ┌─────────────────────┐ │ 任务 A 在跑 (PSP) │ │ 任务 B 在跑 (PSP) │ ← 之后永远在 PSP 下 │ 任务 C 在跑 (PSP) │ └─────────────────────┘ ↑ ↑ │ │ SysTick心跳 PendSV切换 (切MSP短暂) (切MSP短暂)一句话裸机时代 CPU 只用 MSP创建任务时往每个任务的栈里塞好假存档SVC 第一次把 CPU 从 MSP 踹进 PSP 跑第一个任务之后每 1ms SysTick 心跳检查要不要切要切就 PendSV 出手——A 存盘 → 选 B → B 读盘循环往复看起来就像所有任务在同时跑。三FreeRTOS 调度器启动与任务切换 — 完整流程1、开启调度器vTaskStartSchedulervTaskStartScheduler() ← tasks.c通用层所有芯片一样 ├── 创建空闲任务 (prvIdleTask) ├── 创建定时器任务configUSE_TIMERS1 时 ├── 关中断 ├── 初始化全局变量xTickCount0, xNextTaskUnblockTime... └── xPortStartScheduler() ← port.c硬件移植层M4 专属xPortStartScheduler() 六个步骤步骤源码位置干了什么① 中断检测port.c:340-3943 个 configASSERT 验证优先级配置正确② PendSV/SysTick 优先级port.c:397-398写 SHPR2(0xE000ED20)设 0xF0 最低优先级③ SysTick 定时器port.c:713-714LOAD 180M/1000-1 1799991ms 一次心跳④ 临界区归零port.c:405uxCriticalNesting 0⑤ 使能 FPUport.c:408,411写 CPACR 开 FPU 写 FPCCR 开懒保存⑥ 启动第一个任务port.c:414prvStartFirstTask()2、启动第一个任务SVC 一次性电梯1关键概念MSP主栈PSP进程栈谁用OS 内核 中断 ISR普通任务怎么切进异常自动切 MSP退异常按 EXC_RETURN 切回 PSP有几个1 个每个任务 1 个2流程prvStartFirstTask() — port.c:295 ├── 读向量表 0xE000ED08 → 取初始 MSP ├── msr msp, r0 → MSP 复位到栈顶 ├── cpsie i / cpsie f → 开全局中断 └── svc 0 → 触发 SVC只用这一次 │ ▼CPU 自动切 MSPR14EXC_RETURN vPortSVCHandler() — port.c:277 ├── ldr r3,pxCurrentTCB → 拿当前任务 TCB ├── ldr r0,[r1] → 拿任务栈顶指针 ├── ldmia r0!,{r4-r11,r14} → 手动弹核心寄存器 ├── msr psp, r0 → PSP 任务栈顶 ├── mov r0,#0 → msr basepri,r0 → 开中断 └── bx r14 → 返回 │ ▼CPU 按 EXC_RETURN切线程模式PSP → 硬件弹 R0~R3,PC,xPSR PC 第一个任务函数入口任务开始跑3为什么任务能被启动创建任务时FreeRTOS 在任务栈里提前放好了伪造的寄存器值任务栈 ┌────────────┐ │ R4~R11 │ ← 手动弹出 │ R14(LR) │ ├────────────┤ │ R0~R3 │ ← bx r14 后硬件自动弹出 │ R12 │ │ LR │ │ PC ★ │ → 任务函数地址 │ xPSR │ └────────────┘SVC 恢复这个栈 → CPU 以为任务一直在跑无缝衔接。3、任务切换PendSV 无休止循环1触发链SysTick 心跳 (1ms) └── xTaskIncrementTick() └── 发现该切了 → 触发 PendSV不在这里切 │ ▼ PendSV 中断优先级最低等所有中断完事再切2PendSV 完整流程 — port.c:457-515阶段一保存任务 A mrs r0, psp → 读 PSP指向 A 的栈 tst r14, #0x10 → A 用过 FPU 吗 → 用过vstmdbeq r0!,{s16-s31} 压浮点寄存器 → 没用直接跳过零开销 ldr r3,pxCurrentTCB / ldr r2,[r3] → 拿当前 TCB stmdb r0!,{r4-r11,r14} → 手动压核心寄存器 str r0,[r2] → 栈顶存回 TCB ✅ 任务 A 存档完成 阶段二选新任务 stmdb sp!,{r3} → 压 r3 到 MSP mov r0,#0x50 → msr basepri,r0 → 屏蔽低优先级中断 bl vTaskSwitchContext() → C 函数前导零找最高优先级 → pxCurrentTCBB mov r0,#0 → msr basepri,r0 → 放开中断 ldmia sp!,{r3} → 恢复 r3pxCurrentTCB 现在指向 B 阶段三恢复任务 B ldr r1,[r3] / ldr r0,[r1] → 读 B 的栈顶 ldmia r0!,{r4-r11,r14} → 手动弹核心寄存器 tst r14,#0x10 → B 用过 FPU 吗 → 用过vldmiaeq r0!,{s16-s31} 恢复浮点寄存器 msr psp,r0 → PSP B 的栈顶 bx r14 → 返回 → 硬件弹 R0~R3,PC 任务 B 开始跑3、分阶段的数据流vTaskStartScheduler → xPortStartScheduler → prvStartFirstTask ┌──────────┐ ┌────────────────┐ ┌──────────────┐ ┌────────────┐ │ 裸机时代 │ → │ 配 PendSV/ │ → │ MSP 复位 │ → │ 任务A │ │ 只有 MSP │ │ SysTick/FPU │ │ svc 0 ⚡ │ │ 在 PSP 下 │ └──────────┘ └────────────────┘ └──────┬───────┘ │ 活过来了 │ │ └─────┬──────┘ ▼ │ vPortSVCHandler │ 恢复任务A的假存档 │ 切到 PSP │ │ 每 1ms 心跳 ↓ ┌──────────────────────────┐ │ SysTick → PendSV │ │ 保存 A → 选 B → 恢复 B │ │ 保存 B → 选 C → 恢复 C │ │ ...无限循环... │ └──────────────────────────┘4、你实际要碰的 vs 不用碰的永远不改FreeRTOS 写死每个项目要改port.cSVC / PendSV / SysTick / FPU 汇编FreeRTOSConfig.h心跳频率、堆大小、优先级级数tasks.cvTaskStartSchedulermain.cxTaskCreate、vTaskSuspend、vTaskDelay懂了就行出 bug 有用SVC 只用一次PendSV 循环用HardFault 时知道 PSP 指向哪个任务的栈硬件压 R0R3手动压 R4R11栈溢出知道从哪个寄存器查MSP 内核地盘PSP 任务地盘中断里调 xQueueSendFromISR 知道为什么不能阻塞一句话裸机时代 CPU 只用 MSP。创建任务时在每个任务栈里埋好假存档。vTaskStartScheduler配好硬件PendSV/SysTick/FPUprvStartFirstTask发 SVC 把 CPU 从 MSP 踹进 PSP 跑第一个任务。之后每 1ms SysTick 心跳检查要不要切要切就 PendSV 出手——A 存档 → 选 B → B 读档循环往复看起来所有任务在同时跑。四PendSV 和SVC 介绍1介绍pendsv 和 svcPendSVPendableService Call可挂起系统调用SVCSupervicorCall管理程序调用名字就透露了分工SVCPendSV全称Supervisor CallPendable Service Call触发方式软件svc 0指令置位 ICSR 寄存器等所有高优先级中断处理完才执行优先级不着急立刻执行Pendable 可挂起优先级最低排队等FreeRTOS 里一次性电梯启动第一个任务循环班车所有任务切换都靠它为什么选它开中断后立刻进 SVC不用等不会被 SysTick 抢不会被外设中断抢切任务时安全Pend 就是挂起、排队的意思——PendSV 被触发后不立刻执行等 CPU 忙完更重要的事再处理正好适合不急但必须安全的任务切换。2prvStartFirstTask触发SVC__asm void prvStartFirstTask( void ) { PRESERVE8 ldr r0, 0xE000ED08 ; 读向量表 ldr r0, [r0] ldr r0, [r0] msr msp, r0 ; 复位 MSP cpsie i ; 开 IRQ cpsie f ; 开 Fault dsb isb svc 0 ; ★ 就这一句触发 SVC 异常 nop nop }前面 6 步MSP 复位 开中断都是准备工作最后svc 0一脚踹下去CPU 立刻切特权模式跳进vPortSVCHandler。后面两个nop理论上永远不会执行到只是占位防流水线。3SVC中断做了什么__asm void vPortSVCHandler( void ) { PRESERVE8 ldr r3, pxCurrentTCB ; ① 加载 pxCurrentTCB 变量的地址到 r3 ldr r1, [r3] ; ② 读 pxCurrentTCB 的值 → r1 当前 TCB 指针 ldr r0, [r1] ; ③ 读 TCB 第一个字段 → r0 任务栈顶指针 ldmia r0!, {r4-r11, r14} ; ④ 从任务栈弹出 R4~R11, R14手动保存的寄存器 msr psp, r0 ; ⑤ PSP 弹出后的新栈顶 isb mov r0, #0 msr basepri, r0 ; ⑥ BASEPRI 0开所有中断 bx r14 ; ⑦ 异常返回 } SVC ISR 就是个存档加载器——顺着 TCB 找到任务栈把压栈的寄存器一口气灌回 CPUPSP 指过去BASEPRI 开门bx r14 放行。这扇门只用一次4SysTick 触发 PendSVSysTick 中断服务函数 — port.c:519-537void xPortSysTickHandler( void ) { vPortRaiseBASEPRI(); // ① 屏蔽低优先级中断BASEPRI0x50 { if( xTaskIncrementTick() ! pdFALSE ) // ② 心跳计数 检查要不要切 { portNVIC_INT_CTRL_REG portNVIC_PENDSVSET_BIT; // ③ ★ 触发 PendSV } } vPortClearBASEPRIFromISR(); // ④ 放开 BASEPRI }触发 PendSV 的那一句展开// portmacro.h:137-138 #define portNVIC_INT_CTRL_REG (*(volatile uint32_t *)0xE000ED04) // ICSR 寄存器 #define portNVIC_PENDSVSET_BIT (1UL 28UL) // bit28 // 等效于往 0xE000ED04 的 bit28 写 1 (*(uint32_t *)0xE000ED04) | (1 28); // PendSV 挂起时序图每 1ms ──→ SysTick 中断优先级0xF0最低 │ ├── ① 抬 BASEPRI屏蔽同级中断 ├── ② xTaskIncrementTick() │ ├── xTickCount │ ├── 检查延时任务是否到期 → 到期就移到就绪列表 │ └── 返值pdTRUE该切了 / pdFALSE不用切 ├── ③ 如果该切 │ 写 ICSR bit28 1 → 给 PendSV 挂个号 │ ← 但是 PendSV 不立刻跑等 SysTick 退出再说 └── ④ 放 BASEPRI 退出 SysTick ──→ 现在没有更高优先级中断了... └──→ PendSV 开始执行 ✅为什么不在 SysTick 里直接切因为 SysTick 里可能外设中断正排队等执行直接切任务会导致外设中断得不到及时处理。PendSV 优先级最低等所有中断都处理完了再安心切。SysTick中断 (优先级 0xF0) ├── xTaskIncrementTick() → 该切了 ├── 写 ICSR bit28 1 → PendSV 挂号排队 └── 退出 │ ▼ 外设中断 (优先级高, 0x00~0xE0) ← 如果有先处理 ├── UART 收到数据 └── 退出 │ ▼ DMA中断 (优先级别中等) ← 继续处理 └── 退出 │ ▼ 所有中断都处理完了只剩 PendSV 还在排队 │ PendSV中断 (优先级 0xF0最低) ← 终于轮到我了 ├── 保存任务 A ├── vTaskSwitchContext() → 找到最高优先级的就绪任务 ├── 恢复任务 B └── bx r14 → 任务 B 在跑关键就是这个排队不插队——SysTick 只是往小本子上记一笔等会要切自己不干然后 PendSV 排到最后等所有大哥外设中断走了才动手。这样任务切换永远不会卡外设中断。5pendsv中断里做了什么完整源码 — port.c:457-515__asm void xPortPendSVHandler( void ) { PRESERVE8 ; 阶段一保存旧任务 mrs r0, psp ; ① 读 PSP → r0当前任务 A 的栈顶 isb tst r14, #0x10 ; ② 检查 EXC_RETURN任务用过 FPU 吗 it eq vstmdbeq r0!, {s16-s31} ; 用过 → 压 S16~S31没用直接跳过 ldr r3, pxCurrentTCB ; ③ r3 pxCurrentTCB 变量本身的地址 ldr r2, [r3] ; r2 当前 TCB 指针 stmdb r0!, {r4-r11, r14} ; ④ ★ 手动把 R4~R11, R14 压进 A 的栈 str r0, [r2] ; ⑤ 更新 TCB → pxTopOfStack 新栈顶 ; ✅ 任务 A 存档完成 ; 阶段二选新任务 stmdb sp!, {r3} ; ⑥ 把 r3 压到 MSP保护起来 mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY ; ⑦ BASEPRI 0x50 msr basepri, r0 ; 屏蔽选任务期间不被打断 dsb / isb bl vTaskSwitchContext() ; ⑧ ★ C 函数找最高优先级 → pxCurrentTCB B mov r0, #0 ; ⑨ BASEPRI 0放开中断 msr basepri, r0 ldmia sp!, {r3} ; ⑩ 恢复 r3现在指向新的 pxCurrentTCB ; 阶段三恢复新任务 ldr r1, [r3] ; ⑪ r1 新 TCB 指针 ldr r0, [r1] ; ⑫ r0 B 的任务栈顶 ldmia r0!, {r4-r11, r14} ; ⑬ ★ 从 B 的栈手动弹出 R4~R11, R14 tst r14, #0x10 ; ⑭ B 用过 FPU 吗 it eq vldmiaeq r0!, {s16-s31} ; 用过 → 恢复 S16~S31 msr psp, r0 ; ⑮ PSP B 的新栈顶 isb bx r14 ; ⑯ ★ 异常返回 → 硬件弹 R0~R3,PC → 跳到 B }┌─────────────────────┐ │ 保存任务 A │ │ PSP → 栈 │ │ 压 S16-S31 (有FPU) │ │ 压 R4-R11, R14 │ │ 栈顶 → TCB │ └─────────┬───────────┘ ▼ ┌─────────────────────┐ │ 选新任务 │ │ BASEPRI 0x50 │ ← 防打断 │ vTaskSwitchContext() │ ← 核心前导零找最高优先级 │ pxCurrentTCB B │ │ BASEPRI 0 │ └─────────┬───────────┘ ▼ ┌─────────────────────┐ │ 恢复任务 B │ │ TCB → 栈顶 │ │ 弹 R4-R11, R14 │ │ 弹 S16-S31 (有FPU) │ │ PSP B 的栈顶 │ │ bx r14 │ └─────────┬───────────┘ ▼ 任务 B 在跑了6SVC vs PendSV 对比SVCPendSV什么时候调用prvStartFirstTask里svc 0SysTick 写 ICSR bit28 触发调用次数一辈子 1 次每秒钟几百上千次干了什么只恢复第一个任务没有保存旧任务保存 A → 选新任务 → 恢复 B有保存步骤吗❌ 不用之前没有任务在跑✅ 先存后还FPU 处理❌ 第一个任务肯定没用过 FPU✅ 检查 懒保存一句话SysTick 是报时钟每 1ms 响一次看看该不该切PendSV 是搬家公司负责把旧任务搬走、新任务搬进 CPU。报时钟只发指令搬家公司干活。两家都设最低优先级谁也不抢谁。