type
slug
status
summary
icon
category
date
tags
password
1 任务切换函数讲解
在uC/0S-II中,任务的上下文切换发生在如下情况中:
- 外部中断导致更高优先级就绪,触发上下文切换
- Systick使更高优先级的延时任务的延时到期,触发上下文切换
- 任务主动调用如OSTimeDly等挂起自己的函数,触发上下文切换。
其中,前两种由OSIntExit调用OSIntCtxSw实现上下文切换,而第三种则调用OSCtxSw实现上下文切换。
与S3C2440基于ARM9内核不同的是,我们使用的开发板STM32F401RE是基于ARM Cortex-M4内核。
具体来说,S3C2440的上下文切换是直接在0SCtxSw或者0SIntCtxSw中完成的。而Cortex-M系列有一个特有的异常:PendSV异常,我们实际的上下文切换是在PendSV_Handler中完成的。
为什么要在PendSV_Handler函数中进行实际的上下文切换?

- 如图所示,在任务A执行时收到了一个中断请求,在中断服务程序执行时又发生了Systick中断,在SysTick_Handler中会进行任务的切换,因此SysTick_Handler完成后并没有回到之前的中断服务程序,而是转而执行任务B,这样会导致fault。(????这里的理解是有点问题的,应该是)
- 一些我的疑问的解决:
- SysTick_Handler 中会直接调用
OSCtxSw
吗? - uC/OS-II 中的 SysTick_Handler → 调用
OSTimeTick()
→OSIntExit()
→ 触发 PendSV 中断,任务切换真正发生在 PendSV_Handler 中。 - SysTick 不直接切任务,它只是“发起切换请求”。
- SysTick 的优先级确实比任务高吗?
- 在 ARM Cortex-M 中,任务的运行级别是 线程模式,中断(包括 SysTick)运行在 处理器模式,所以任何中断优先级都比任务高(除非你改了CONTROL寄存器,非典型)
- 中断之间也有优先级:SysTick 通常优先级高于外设 IRQ(如 USART、EXTI), PendSV是最低优先级(0xFF)

2 任务切换函数具体实现
2.1 在OSCtxSw或OSIntCtxSw中触发PendSV异常

不知道这个是在干什么?
2.2 在PendSV实现上下文切换
关键代码分析
- Cortex-M系列在进入中断时,硬件自动保存xPSR,PC,LR,R12,R0~R3。所以我们在PendSV中只需要手动入栈R4-R11
- 为什么还需要手动入栈R14(LR),这里的LR是原任务的上下文的内容



在进入中断时,自动压栈保存寄存器结束后,LR会被自动更新为exc_return。具体更新的值由进入中断之前的状态决定。
在退出中断时,LR中的值被加载到PC,硬件会根据exc_return的值来决定使用哪个堆栈指针(MSP或PSP)以及返回后的模式(线程模式或处理模式):
举个例子:
设想一个情况:Task1需要切换到Task2,而进入PendSV Handler时,LR已经根据Task1运行时的状态被更新为了exc_return。在之后我们需要切换回Task1时,我们需要回到之前Task1运行的状态。这就是我们为什么要手动入栈保存LR,并在任务恢复时将其从栈中恢复的原因。还有一点需要注意:在进入中断前,XPSR、PC、LR、R12、R0-R3会被自动保存到进程堆栈。因此我们在返回时,一定要确保在进程堆栈中出栈,否则就会出错。
3 OSStart
3.1 为什么需要OSStart?它与上下文切换有什么关系?
简单回顾:
- OSTaskCreate()
- 任务创建函数中初始化了栈和TCB,但这些创建的任务只是在TCB初始化时,根据优先级在优先级位图相应位置更新就绪状态。
- 什么时候启动任务系统运行?

- OS_Sched()函数的执行有一个if判定条件
if(OSRunning== OS_TRUE)
- OSRunning是一个全局标志变量,用于指示RTOS系统是否正在运行。所以仅在OS运行的时候才触发任务调度。
- 为什么要有这个判断?OS启动之前不调度?任务创建完后就立刻调度执行可以吗?
- 不可以。
- 事务逻辑上:无效的任务切换、破坏启动流程
- 在系统启动前,通常会有多个任务通过 0STaskCreate()被创建。此时,我们希望任务的初始化是静态的,不需要动态调度,在多任务创建后可以一次性切换到最高优先级任务。
- 程序逻辑上:PSP错误
- 虽然OS_Sched()调用的OSCtxSw()/pendSV异常 也进行了保护现场与恢复现场,但是并不适用OS启动之前。 第一次任务切换:空->任务(特殊的硬件上下文加载) 第n(n>1)次任务切换:任务->任务
PendSV_Handler中直接使用了任务的进程堆栈指针psp,但并未交代psp是在哪里赋的初值PSP 是可能指向非法内存或随机地址的。
为了解决以上两个事务和程序上的问题,OSStart应运而生。
OSStart相当于一个分水岭,在它之前完成0S的一切配置,包括任务的创建;0SStart一旦执行,OS就正式启动了,系统也就由裸机转换成了RTOS。
3.2 OSStart是怎么解决问题的?
- 问题一:psp赋初值
- 问题二:标识OS已经启动
- 问题三:pendsv优先级设置
- 问题四:找到第一个任务(优先级位图法,就绪队列中优先级最高的任务)
- 问题五:开始执行第一个任务
OSStart结构:

代码体现
3.3 OSStartHighRdy
问题一:psp初始化

RTOS 一般让中断使用 MSP,而线程/任务使用 PSP,以实现栈空间隔离。
CONTROL 寄存器的含义:

回顾2-3的笔记:
6.3 Cortex M4
内核寄存器:

这里可以看到和Cortex-M4寄存器布局和EOS课上课程系统讲解的一致,但是这里有一个PSP和MSP来决定SP的取值。同时还要加上CONTROL寄存器来决定使用PSP还是MSP。除此之外CONTROL寄存器需要来决定处理器的工作模式。
特殊寄存器(中断屏蔽寄存器)



- primask
- The PRIMASK register prevents activation of all exceptions with configurable priority。
- 在它被置1后,就关掉所有可屏蔽的异常,只剩下Reset(-3)、NMI(-2)和Hardfault(-1) 可以响应。它的缺省值是0,表示没有屏蔽。
- 当前优先级改为0(最高可编程优先级)
- faultmask
- The FAULTMASK register prevents activation of all exceptions except for Non-Maskable Interrupt (NMI).
- 当它置1时,只有NMI才能响应,它的缺省值也是 0,表示没有屏蔽。
- 处理器在退出任何异常处理程序(除了NMI处理程序)时将FAULTMASK位清除为0。
- The processor clears the FAULTMASK bit to 0 on exit from any exception handler except the NMI handler.
- basepri(Base priority mask register)
- 根据前面NVIC优先级分组使用位数,只有[7:4]位可用。
- 定义了被屏蔽优先级的阈值。当它被设成某个值后,所有优先级号大于等于此值的中断都被关(优先级号越大,优先级越低,即优先级比这个阙值更低的优先级将被屏蔽)。但若被设成0,则不关闭任何中断,0也是缺省值。
- XPSR(Program status register)
- 分类:APSR/IPSR/EPSR(Application/Interrrupt/Execution)
- 应用程序程序状态寄存器(APSR)、中断程序状态寄存器(IPSR)和执行程序状态寄存器(EPSR)。三个逻辑子寄存器共享同一个物理寄存器(即xPSR),通过访问 xPSR 的不同位段来获取各自的信息。
- 在代码中对前面三个寄存器的操作,本质上是在对CPU里面的PSR一个寄存器进行更改。
- 功能分工明确
- APSR:主要用于记录算术和逻辑运算的结果状态,如N、Z、C、V、Q标志位。
- IPSR:专门用于记录当前正在执行的中断服务程序编号(ISR_NUMBER)。
- EPSR:涉及Thumb状态(T位)和IT指令执行状态(ICI/IT位)等,用于控制指令集的切换和特定指令的执行流程。
- 硬件设计优化
- 独立更新:将PSR划分为多个子寄存器处理器在执行指令时,只需更新相关的子寄存器。例如,算术运算只影响APSR,而不会涉及 EPSR 或IPSR。这减少了不必要的硬件操作,提高了性能。
- 并行处理:不同的子寄存器可以由不同的硬件单元并行处理,进一步提升处理器的效率。
- 增强系统安全性
- 受限访问:如果直接操作PSR,可能会不小心修改到不需要改变的部分,导致意外的程序行为。而通过分别操作APSR、IPSR和EPSR,可以更精确地控制修改范围,减少对其他部分的干扰。
- 兼容性和扩展性
- 这种设计使得 ARM 架构在不同版本(如 Cortex-M0、M3、M4、M7)上可以保持一致的状态管理,而不需要每次修改整个 PSR 结构。
- 可以使用寄存器名称作为MSR或MRS指令的参数,单独访问这些寄存器或组合访问任意两个或所有三个寄存器。例如:
- 使用MRS指令使用PSR,读所有寄存器
- 使用MSR指令向APSRN的Z、C、V和Q位写入数据。

为什么要再次划分三个寄存器?
- CONTROL寄存器
- Cortex-M4有两种工作模式:Thread模式(线程模式)和Handler(处理模式)
- 异常和中断会进入Handler模式(执行没有限制),其他情况会进入Thread模式。
- 特权级别
- Privileged和Unpriviledged
- 但其实看说明书上,Thread模式下可以选择Privileged和Unprivileged。Handler模式一直是Privileged。
- Unpriviledged(非特权级别)只能通过MSR和MRS有限访问,不能使用CPS指令访问系统定时器,NVIC或者系统控制模块等。UCOSII执行期间,只会处于Handler模式下和Thread模式下的特权级别。
- 当处理器处于线程模式时,CONTROL寄存器控制软件执行所使用的堆栈和特权级别,并指示FPU状态是否为活动。
- nPRIV(第0位):控制进入特权模式或用户模式(非特权模式)
- 0:特权模式(默认),可以执行所有指令,访问系统控制寄存器等。
- 1:用户模式,此时不能执行所有的指令,某些系统控制寄存器不可访问。
- SPSEL(第1位):该位决定了堆栈指针的选择。
- 线程模式
- 0:使用主堆栈指针(MSP)。
- 1:使用进程堆栈指针(PSP)。
- 处理模式:
- 始终为0,即一直使用MSP。进行写操作会被忽略。

也就是说Handler模式下一直使用的是MSP作为SP,在Thread模式下可以选择使用MSP/PSP。Bit0决定Thread是否在特权级里面,Bit1决定Thread模式使用的 是MSP还是PSP。在UCOSII中我们使用Thread模式下PSP作为任务栈SP。
6.4 工作模式
- 操作模式
- 线程模式(Thread Mode):用于执行主程序代码,即普通的应用程序代码。
- 处理模式(Handler Mode):用于处理异常和中断服务程序。当发生中断或异常时,处理器会切换到处理者模式来执行相应的处理程序。
- 特权级别
- 特权级(Privileged):在此级别下,代码可以访问所有的内存区域和外设寄存器,并能执行所有指令。
- 用户级(Unprivieged):在此级别下,代码的访问权限受限,某些特定的寄存器和内存区域可能无法访问,尝试访问这些受限资源将导致故障。

- 模式切换
- 处理模式→线程模式
- 自动恢复:中断服务程序(ISR)执行完毕后,通过
BX LR
或POP {PC}
返回线程模式。 - 特权级可配置:返回后线程模式的特权级由
EXCRETURN
值决定。 - 线程模式→处理模式
- 硬件自动切换:当发生异常(如中断、SVC指令)时,CPU自动进入处理模式(特权级)。

- 权限切换
- 特权级→非特权级(用户级)
- 设置CONTROL[0]=1,进入非特权级
- 非特权级(用户级)→特权级
- 只能通过触发异常(如SVC)进入处理模式(特权级),在异常处理中修改CONTROL寄存器CONTROL[0]=0,进入特权级。
- 注意:第一个异常处理后,便回到出现异常前的最后一个状态:非特权线程。是因为在这个异常处理中并没有修改CONTROL[0]。第二个异常中修改了CONTROL寄存器,然后使得处理完异常后进入特权线程。
- 从处理模式到线程模式的异常返回期间,栈指针的选择可由EXC RETURN(异常返回)数值决定处理器硬件会自动更新第1位的数值。
- SVC:SVC(Supervisor Call)是一种由软件触发的异常,通常用于实现系统调用。当用户程序需要请求操作系统的服务或访问受保护的硬件资源时,会通过执行 SVC 指令来触发 SVC 异常。属于SVC的异常:系统服务调用、任务调度相关、硬件资源访问等

MSP\PSP
- MSP和PSP 都是堆栈指针(Stack Pointer)的寄存器
MSP
是主堆栈指针。- 通常用于内核模式(特权模式)和异常处理。
- 默认堆栈指针,所以系统启动的时候,通常使用MSP
PSP
是进程堆栈指针。- 通常用于用户模式(非特权模式)。
- 在多任务操作系统中,每个任务通常有自己的堆栈,而
PSP
指向该堆栈。
- 通过
CONTROL
寄存器的 SPSEL 位来选择使用哪个堆栈指针。 - 0:使用MSP
- 1:使用PSP

- 思考:以下操作能按预期执行吗?
- 注意: 当修改某些系统控制寄存器(如CONTROL、PRIMASK)时,CPU的流水线和指令预取机制可能导致后续指令仍然使用旧的寄存器值,而不是刚刚写入的新值。
- ISB:Instruction Synchronization Barrier,保证前面设置立即生效。
- 清空处理器流水线(Pipeline Flush),丢弃已预取但未执行的指令。
- 强制CPU重新从内存取指令,确保后续指令基于最新的寄存器状态执行。
- 现在 CPU 已准备好:它将从任务自己的 PSP 中读写栈帧。
问题二:标识OS已经启动
问题三:pendsv优先级设置
回顾:pendsv的作用
我们实际的上下文切换是在PendSV_Handler中完成的。
- EQU相当于C语言的#define,因为是直接使用汇编语言,所以是直接使用地址来写值
NVIC_INT_CTRL EQU 0xE000ED04
这个是ICSR寄存器的地址




NVIC_SYSPRI14 EQU 0xE000ED22
- 系统优先级寄存器14的地址,对应于PendSV的优先级设置。
- 在ARM Cortex-M中,系统异常(如 PendSV、SysTick)也可以设置优先级,而不是只能NVIC外设中断才有优先级。
- 系统异常的优先级(如 SVC、PendSV、SysTick)被存储在System Handler Priority Registers(SHP)中。
SCB->SHP[12]; // SHP 是一个 uint8_t 类型的数组,共 12 个元素
SHP[0] ~ SHP[7]
—— 保留或未使用(有的芯片厂商会不同)- SHPR1(System Handler Priority Register 1)\SHPR2\SHPR3
- SHPRx vs SHP[]:两种对同一片内存的不同访问方式
- 在手册(ARM TRM)中使用:
- 在core_cm4码中使用:这12个优先级字段直接封装为:
- 对应关系:SHP[i] 映射到 SHPRx 的字节结构如下
- 为什么CMSIS这么设计?
- 但还是采用裸地址+宏访问SHPR3寄存器
异常名 | 异常号 | SCB->SHP[n] | 地址 |
SVC | 11 | SHP[8] | 0xE000ED20 |
DebugMon | 12 | SHP[9] | 0xE000ED21 |
PendSV | 14 | SHP[10] | 0xE000ED22 |
SysTick | 15 | SHP[11] | 0xE000ED23 |
NVIC_SYSPRI14 EQU 0xE000ED22
在ARM Cortex-M3/M4 的技术参考手册(TRM)中,确实描述了系统优先级寄存器为:
而不是直接叫
SHP[0]~SHP[11]
。其实这两者指的是同一组寄存器的不同表示方法,我们来详细解释一下这之间的关系:名称 | 地址 | 包含哪些异常的优先级 |
SHPR1 | 0xE000ED18 | MemManage, BusFault, UsageFault(异常 4-6) |
SHPR2 | 0xE000ED1C | SVCall(异常 11) |
SHPR3 | 0xE000ED20 | PendSV(异常 14), SysTick(异常 15) |
也就是说,
SCB->SHP[]
数组是对 SHPR1~3
这三组寄存器按字节映射后的访问方式。地址 | 寄存器 | 字节(SHP下标) | 说明 |
0xE000ED18 | SHPR1 | SHP[0]~SHP[3] | 异常 4~7(一般未用) |
0xE000ED1C | SHPR2 | SHP[4]~SHP[7] | 异常 8~11(SVC 是 SHP[8]) |
0xE000ED20 | SHPR3 | SHP[8]~SHP[11] | 异常 12~15(PendSV是 SHP[10],Systick是SHP[11]) |


把
SHPR1~3
三个寄存器拆成了12个8位数组项。优点:可以使用SCB->SHP[10] = 0xFF;
简洁地设置PendSV优先级;不用再移位处理32位的SHPR3
;与中断号(异常号 - 4)直接匹配,逻辑清晰。使用方式 | 是否推荐 | 场景 |
#define SHPR3 + 裸地址 | ✅ 推荐 | 内核移植、裸机开发、强控寄存器的低级代码(如uC/OS移植) |
SCB->SHP[11] | ✅✅ 强烈推荐 | 高级开发、HAL/RTOS 环境、易读性好、配合 IDE 调试更方便 |
所以设计Systick的优先级
SHP[11]
就是 SysTick Handler 的优先级控制位,对应 SHPR3
的最高 8 位。- 注意这个宏定义
- (volatile INT32U *)0xE000ED20uL
- 将地址0xE000ED20强制转化成一个32位无符号整型指针(即
uint32_t *
) volatile
的意思是: 编译器不要优化访问,每次读取/写入都必须真的去内存/寄存器中操作volatile uint32_t * const p_shpr3 = (volatile uint32_t *)0xE000ED20;
- 再加上*,即*((volatile INT32U *)0xE000ED20uL)
- 读取这个地址处的值(即 SHPR3 寄存器 32 位内容)
- u和ul的后缀是为了指定整数常量的类型是无符号整型(
unsigned
),避免类型提升错误、符号扩展、警告等潜在问题,特别是在位操作和寄存器地址处理时。 - 注意:常量一旦没有后缀,默认是 signed int。
后缀 | 含义 | 示例 |
u | unsigned int | 100u |
ul | unsigned long | 100ul |
l | long (可能是有符号) | 100l |
llu | unsigned long long | 1000llu |
- pendsv优先级设置
NVIC_SYSPRI14 EQU 0xE000ED22

#define SCB_SHPR3 0xE000ED20
SHPR3
是 System Handler Priority Register 3,共占用 4 字节(32 位),对应的是:- 地址 0x00 表示第 0 个字节,地址 0x01 表示第 1 个字节,地址 0x02 表示第 2 个字节...
- SHPR3一个32位的寄存器,占4字节,那么地址为
0xE000ED20
~0xE000ED23
,访问第16~23位,如图所示,就是偏移量为2个字节,也就是两个地址。 - 因此STM32采取的策略是:触发PendSV异常实现延迟的上下文切换。
- 知识点补充
- SVC(Supervisor Call):是ARM架构中用于实现系统调用的关键机制,其核心作用是从用户模式切换到特权模式,使应用程序能够安全地请求操作系统服务。
- PendSV(Pendable Service Call,可挂起的服务调用):操作系统内核通过触发PendSV异常(通常是通过软件指令触发,设置标志位)。
- PendSV几个关键位:
- Bit28:PENDSVSET(PendSV设置挂起位)
- 写:0:无影响;1:将PendSV异常状态设置为挂起
- 读:0:PendSV异常未处于挂起状态;1:PendSV异常为挂起状态
- Bit27:PENDSVCLR(PendSV清除挂起位)
- 只写
- 写:0无影响,移除PendSV异常状态的挂起
- Bit26:PENDSTSET(Systick设置挂起位)
- 流程分析:
- 当前A任务正在运行。如果此时发生中断(一般普通中断不会触发PenSV)或任务主动调用OS服务(如通过SVC指令),可能会导致需要进行任务切换。
- 外部中断本质上是与具体硬件外设(如定时器、串口)绑定的,它们被 NVIC 控制。它们触发时,不会自动引发PendSV。
- 但是在中断服务函数(ISR)里,如果你调用了某些操作系统 API,比如
OSSemPost()
,导致了任务状态的变化(比如高优先级任务变成就绪态),操作系统内核会判断是否需要任务切换,如果需要,它就会主动触发 PendSV。(总结一句话就是外部中断不会触发PendSV,是否触发PendSV取决于OS的调度判断逻辑) - 所以正常的流程是:外设中断发生,CPU进入ISR,ISR中调用操作系统服务(如唤醒一个更高优先级的任务),操作系统判断需要任务切换,内核代码中执行
SCB->ICSR |= SCB_ICSR_PENDSVSET_Msk
来触发PendSV异常。 - PendSV被触发后,当前任务的上下文被保存。操作系统选择下一个任务并恢复它的上下文。(PendSV_Handler中做任务上下文切换)
- 在执行任务B的时候中断产生,跳转执行ISR。这个时候恰好碰到Systick定时触发,然后执行优先级更高的定时器中断(在OSIntExit检查到了更高优先级的就绪任务A,OS设置PendSV挂起标志位ICSR的PENDSVSET=1,挂起PendSV).
- 然后继续执行ISR(因为PendSV被设置为最低优先级,所以应该是先执行完ISR之后在去PendSV中执行上下文切换,这样就避免了在中断执行时出现任务切换的error了)。当ISR完成时,在PendSV_Handler中进行任务上下文切换。然后切换到任务A执行。
- 操作系统的“时钟节拍”由SysTick定时器中断提供,SysTick定时器是一个24位的倒数定时器,通常设置为每1ms触发一次中断。每次中断会进入
SysTick_Handler
,调用这三个函数。 - OSTimeTick():会遍历所有延时状态的任务,将其延时计数器-1。
- OSIntExit()与PendSV:在Systick中断退出前,
OSIntExit()
会检查是否有更高优先级的任务就绪;如果有优先级更高的任务就绪(任务切换需求),OS会触发 PendSV中断(通过设置ICSR寄存器中的PENDSVSET位);PendSV_Handler
会完成任务上下文的保存、调度器选择、以及恢复新任务的上下文。 - 注意:时间片不是在任务中减少的,同时任务也不是跑满一个时间片后再触发Systick,而是:SysTick固定周期打断系统,然后统一减时间片、减延时、调度任务;所以时间片的计数是由Systick中断驱动的,而不是由任务执行过程驱动的。
字节偏移 | 系统中断号 | 中断类型 |
+0 (byte 0) | Reserved | Reserved |
+1 (byte 1) | Reserved | Reserved |
+2 (byte 2) | IRQ #14 (0xE) | PendSV ✅ |
+3 (byte 3) | IRQ #15 (0xF) | SysTick |
关键理解:地址本身的单位就是“字节”
设置pendsv为最低优先级,理由如下:




问题四:找到第一个任务(不在OSStartHighRdy里面解决)

问题五:开始执行第一个任务
和初始化时模拟压栈的顺序对应,进行出栈操作,这是一个纯软件过程,不涉及任何异常。
背景回顾:pendsv切换任务时,硬件会自动压栈寄存器(地址从高到低为)xPSR,PC,LR,R12,R3,R2,R1,R0。uCOS等为了保存上下文,还需要软件手动压栈R4~R11。最后一句的BX R1,也就是任务的PC,继续运行
这里我们也贴一段PendSV的压栈代码:
根据pendsv可知PSP栈内从低到高的内容为,R4~R11,LR,R0~R3,R12,LR,PC,XPSR
所以恢复上下文的顺序是这个顺序。LDMFD,应该是说弹出两个字节到R1和R2,所以在那个位置弹出的内容是PC和XPSR,分别存到R1和R2中。
“任务堆栈初始化”,
这段代码是 RTOS(比如 uC/OS-II)在创建任务时调用的函数,用于伪造一个“异常返回栈帧”,让新任务在切换上来时能“像异常返回一样开始运行”,从而跳转到对应的任务函数。
这是模拟压栈的顺序
代码详细分析
- 函数名和参数
参数 | 含义 |
task | 新任务函数的入口地址 |
p_arg | 启动任务时传给任务函数的参数 |
ptos | 栈顶地址(高地址) |
opt | 启动选项(未使用) |
返回值 | 初始化后任务的堆栈指针(从哪里开始恢复) |
(void)opt;
- 这行是为了消除未使用变量
opt
的编译警告。书上写的是 opt = opt这样来消除警告
p_stk = ptos + 1u;
- 这里就很值得注意的一点:由于是
—p_stk
,前缀为先-再填入。所以要填入第一个元素之前,需要将指针上移一个地址单位。
p_stk = (OS_STK *)((OS_STK)(p_stk) & 0xFFFFFFF8u);
- 保证栈地址8字节对齐(ARM EABI 规定:栈在异常进入时必须是8字节对齐),以避免硬件抛出“unaligned access”。
- 开始构造伪造堆栈帧(模拟异常进入的压栈)
- 【硬件自动压栈区】:异常自动压栈的内容(共8项)
(--p_stk) = 0x01000000uL; // xPSR
- xPSR 设置为 Thumb 状态(必须 bit[24] = 1)
(--p_stk) = (OS_STK)task; // PC = task
- task:新任务函数的入口地址
(--p_stk) = (OS_STK)OS_TaskReturn; // LR
- 如果任务函数退出,就跳转到这个函数
- 设置初始通用寄存器,尤其R0为任务的参数(会被传入任务函数)
- 伪造 EXC_RETURN(异常返回值)
(--p_stk) = 0xFFFFFFEDuL;
- 【软件手动压栈区】:额外保存的寄存器 R4~R11
- 这些寄存器是调用约定中 被调用者(callee)保存的寄存器,在任务切换时由调度器保存/恢复(软件保存)。
- 最后返回
注意后面的数据的值:

代表返回线程模式,使用PSP。
返回新栈顶(即:恢复上下文时的起始地址)
4 异常栈(Exception Stack)
“异常栈”是系统在异常(如中断、PendSV、HardFault)中使用的特殊堆栈。
Cortex-M的两个栈指针
栈名 | 说明 |
MSP(Main Stack Pointer) | 上电默认使用,用于异常处理、启动阶段 |
PSP(Process Stack Pointer) | 多任务时每个任务用自己的 PSP 作为堆栈 |
当异常发生时,CPU会自动切换到 MSP(主堆栈)进行处理:所以需要一段安全、干净的栈空间给异常使用——这就是:
异常栈(Exception Stack) = OS_CPU_ExceptStk[]
总的来说:OSInitHookBegin():负责初始化异常栈,异常栈基地址指针,BASEPRI的优先级边界设置,这个函数在
OSInit()
中被调用。贴一个OSInit()的代码
4.1 初始化异常栈
4.1.1 #ifdef OS_CPU_GLOBALS
条件编译详解
第一步是先理解清除这几个文件的关系:
在os_cpu.h中
在os_cpu.c中
在其他文件(如main.c)中
🔁 这样避免了重复定义、符号冲突的问题,是C语言中管理全局变量的一种经典写法。
4.1.2 设置异常栈大小的宏定义
- 提供默认值256u(unsigned int)(单位:OS_STK = 栈元素类型,通常是
uint32_t
);
- 即:异常栈空间大小为
256 * 4 = 1024 字节
(OS_STK 是 uint32_t)
4.1.3 异常栈的变量声明与作用
- OS_CPU_ExceptStk[OS_CPU_EXCEPT_STK_SIZE]
- 是一个数组,表示异常时MSP使用的堆栈空间;
- 具体内容在
OSInitHookBegin()
中清零,栈空间就在这个数组中分配。
- OS_CPU_ExceptStkBase
- ARM Cortex-M 使用的是向下增长的栈结构。表示异常栈的栈顶指针(即高地址)
- 这个值被写入到MSP,供PendSV、SysTick、HardFault等异常处理使用;
4.1.4 清空异常栈
- *pstk++ = (OS_STK)0;
- 首先是类型转换,将0转化成OS_STK类型,因为每个栈单元(数组单元都是OS_STK的元素,即32位usigned int);然后解引用和赋值,*pstk =(OS_STK) 0;
- 最后pstk++,执行后置递增操作,在解引用和赋值之后,pstk指向下一个元素。
4.1.5 初始化异常栈
- 关于这个对齐8字节的知识点
- 为什么要对齐?
- 因为异常进入/返回、FPU使用等操作时,硬件会按8字节对齐的方式压栈和出栈。
- 举例子
- 如果发生异常(比如
SysTick
、PendSV
),CPU自动压栈的寄存器(R0-R3, R12, LR, PC, xPSR)总共8个×4字节=32字节;Cortex-M内核要求MSP和PSP必须是8字节对齐,否则会HardFault。
"The stack pointer (SP) must be 8-byte aligned at all times. If the SP is not aligned, the processor will generate a HardFault exception.”(ARM官方文档)
& 0xFFFFFFF8
的作用:清除低3位,使地址变成8的倍数- 因为从第四位开始,2^n就可以变成8*2^(n-3).结果一定是8的倍数,即一定是8字节对齐。
- 如果 MSP/PSP不是8字节对齐,硬件直接抛出
HardFault
异常;很难调试,问题隐蔽而致命。所以:即使你的链接脚本大概率对齐了,手动& 0xFFFFFFF8
是必要的保险操作。
4.1.6 OSStartHighRdy实际调用
下面是这几个文件的实际调用关系图示
4.2 BASEPRI的优先级边界设置
app_cfg.h
os_cpu.c
- BASEPRI是什么?
- BASEPRI是ARM Cortex-M 特有的中断优先级屏蔽寄存器。它控制哪些中断能被响应,哪些不能。Cortex-M 的优先级是 数值越小优先级越高,数值越大越低。
- 当你设置
BASEPRI = x
,意味着:
所有 “数值 ≥ x”的中断优先级都不能打断当前代码(即被屏蔽);所有 “数值 < x”的中断优先级可以打断当前代码(即不可屏蔽)
表达式 | 含义 |
CPU_CFG_KA_IPL_BOUNDARY | 是一个逻辑优先级值,代表“任务级别以下的中断会被屏蔽” |
CPU_CFG_NVIC_PRIO_BITS | 表示芯片实际实现的优先级位数(如 4、3、2) |
8 - CPU_CFG_NVIC_PRIO_BITS | 是把用户定义的“逻辑优先级”转换成“寄存器中使用的数值” |
<< 左移操作 | 是因为 NVIC 的中断优先级值 存储在高位(MSB),低位未使用 |
这意味着:
- 设置
BASEPRI = 0x40
,屏蔽逻辑优先级≥4的中断
- 优先级为 0~3的中断(包括 SysTick)不被屏蔽
- 修改systick的优先级
5 整体流程分析
5.1 启动阶段(main 函数)
一旦
OSStart()
被调用:- uCOS 会调用
OSStartHighRdy()
;
OSStartHighRdy()
会调用OSCtxSw()
;
OSCtxSw()
(初次切换时)会触发OS_CPU_PendSVHandler()
,触发一次PendSV异常,进入第一次任务上下文切换。
5.2 任务调度时机:由 SysTick 控制节拍中断
SysTick_Init
函数中配置了:- 每当1ms到达,
SysTick_Handler()
被调用。
5.3 SysTick_Handler 中干了什么?
你提供的
SysTick_Handler
:5.3.1 OSTimeTick()
的作用
- 维护系统节拍计数器
OSTime
- 检查所有任务的
OSTCB->OSTCBDly
(任务延时计数器);
- 判断是否有任务延时结束,需要就绪;
- 如果有更高优先级的任务就绪,将设置任务切换标志。
5.3.2 OSIntExit()
的作用
- 判断中断是否嵌套完毕;
- 判断是否需要任务切换;
- 若需要,通过
OSIntCtxSw()
触发PendSV中断,也就是设置 SCB 寄存器,让 PendSV 异常挂起。
5.4. PendSV异常真正执行任务切换
OS_CPU_PendSVHandler
(你用的是STM32F4 + uC/OS-II,在os_cpu_a.asm
汇编中):- 保存当前任务的CPU上下文(寄存器R4~R11等)到当前任务的栈;
- 调用
OSPrioHighRdy
切换OSTCBHighRdy
;
- 恢复新任务的上下文(从其任务栈中恢复寄存器);
- 最终执行
bx lr
(返回到任务),完成任务切换。
5.5 你的两个任务在切换中是怎么配合的?
任务 | 功能 | 延时 | 优先级 |
task0 | LED2_OFF | OSTimeDly(5000) | 16 |
task1 | LED2_ON | OSTimeDly(1000) | 13 |
5.5.1 基础知识回顾
OSTimeDly(tick)
会让当前任务进入延时状态,进入等待队列;
SysTick_Handler()
每1ms触发一次,会调用OSTimeTick()
来处理延时计数;
OSIntExit()
是在中断(比如 SysTick)中退出时用于判断是否需要任务切换的关键函数。其中会调用OSIntCtxSw,挂起PendSV,进行任务切换。
5.5.2 运行逻辑示意
- uCOS 启动后,优先执行优先级较高的task1;
- task1先执行一次
LED2_ON()
,然后调用OSTimeDly(1000)
; - 系统把task1挂起1000个tick,即延时1s,task1变为延时等待状态;
- 调度器选择就绪的task0(此时唯一可运行);
- 一个tick是1ms,也就是每次SysTick中断进来,会触发
OSTimeTick()
,负责tick减1。
- task0执行一次
LED2_OFF()
,然后OSTimeDly(5000)
; - 系统把task0挂起5000个tick,即延时5s,task0变为延时等待状态;
- 此时,系统没有任何可运行任务(Idle task会运行)
- 1000 tick过去,task1苏醒,从delay/wait队列→就绪队列
- task1的优先级高,触发上下文切换(在SysTick中断里,由
OSIntExit()
完成); - task1再执行一次
LED2_ON()
,然后调用OSTimeDly(1000)
,再次挂起自己; - 然后此时的t0还在wait队列里面,就继续执行idle任务和倒计时
总体呈现:
每隔1s,task1短暂执行一下,每5s,task0重新执行一次。
✅ 总结:任务切换的核心链条

- 作者:🐟🐟
- 链接:https://www.imyuyu.top//article/Quadcopter2-5
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。