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函数中进行实际的上下文切换?
notion image
  • 如图所示,在任务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)
notion image
 

2 任务切换函数具体实现

2.1 在OSCtxSw或OSIntCtxSw中触发PendSV异常

notion image
不知道这个是在干什么?

2.2 在PendSV实现上下文切换

关键代码分析
  • Cortex-M系列在进入中断时,硬件自动保存xPSR,PC,LR,R12,R0~R3。所以我们在PendSV中只需要手动入栈R4-R11
  • 为什么还需要手动入栈R14(LR),这里的LR是原任务的上下文的内容
notion image
notion image
 
notion image
在进入中断时,自动压栈保存寄存器结束后,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初始化时,根据优先级在优先级位图相应位置更新就绪状态。
  • 什么时候启动任务系统运行?
    • notion image
  • 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结构:
notion image
代码体现
 

3.3 OSStartHighRdy

问题一:psp初始化

notion image
RTOS 一般让中断使用 MSP,而线程/任务使用 PSP,以实现栈空间隔离。
CONTROL 寄存器的含义:
notion image
回顾2-3的笔记:

6.3 Cortex M4

内核寄存器:
notion image
💡
这里可以看到和Cortex-M4寄存器布局和EOS课上课程系统讲解的一致,但是这里有一个PSP和MSP来决定SP的取值。同时还要加上CONTROL寄存器来决定使用PSP还是MSP。除此之外CONTROL寄存器需要来决定处理器的工作模式
特殊寄存器(中断屏蔽寄存器)
notion image
notion image
notion image
  • 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)
    • notion image
    • 应用程序程序状态寄存器(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状态是否为活动。
    • notion image
    • 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):在此级别下,代码的访问权限受限,某些特定的寄存器和内存区域可能无法访问,尝试访问这些受限资源将导致故障。
notion image
  • 模式切换
    • 处理模式→线程模式
      • 自动恢复:中断服务程序(ISR)执行完毕后,通过BX LRPOP {PC}返回线程模式。
      • 特权级可配置:返回后线程模式的特权级由EXCRETURN值决定。
        • notion image
    • 线程模式→处理模式
      • 硬件自动切换:当发生异常(如中断、SVC指令)时,CPU自动进入处理模式(特权级)。
  • 权限切换
    • 特权级→非特权级(用户级)
      • 设置CONTROL[0]=1,进入非特权级
    • 非特权级(用户级)→特权级
      • 只能通过触发异常(如SVC)进入处理模式(特权级),在异常处理中修改CONTROL寄存器CONTROL[0]=0,进入特权级。
        • notion image
        • 注意:第一个异常处理后,便回到出现异常前的最后一个状态:非特权线程。是因为在这个异常处理中并没有修改CONTROL[0]。第二个异常中修改了CONTROL寄存器,然后使得处理完异常后进入特权线程。
        • 从处理模式到线程模式的异常返回期间,栈指针的选择可由EXC RETURN(异常返回)数值决定处理器硬件会自动更新第1位的数值。
        • SVC:SVC(Supervisor Call)是一种由软件触发的异常,通常用于实现系统调用。当用户程序需要请求操作系统的服务访问受保护的硬件资源时,会通过执行 SVC 指令来触发 SVC 异常。属于SVC的异常:系统服务调用、任务调度相关、硬件资源访问等
💡
MSP\PSP
  • MSPPSP 都是堆栈指针(Stack Pointer)的寄存器
  • MSP主堆栈指针。
    • 通常用于内核模式(特权模式)和异常处理。
    • 默认堆栈指针,所以系统启动的时候,通常使用MSP
  • PSP进程堆栈指针
    • 通常用于用户模式(非特权模式)。
    • 在多任务操作系统中,每个任务通常有自己的堆栈,而 PSP 指向该堆栈。
  • 通过 CONTROL 寄存器的 SPSEL 位来选择使用哪个堆栈指针。
    • 0:使用MSP
    • 1:使用PSP
notion image
  • 思考:以下操作能按预期执行吗?
  • 注意: 当修改某些系统控制寄存器(如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寄存器的地址
    • notion image
      notion image
notion image
notion image
  • 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] —— 保留或未使用(有的芯片厂商会不同)
      • 异常名
        异常号
        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)中,确实描述了系统优先级寄存器为:
      • SHPR1(System Handler Priority Register 1)\SHPR2\SHPR3
      • 而不是直接叫 SHP[0]~SHP[11]。其实这两者指的是同一组寄存器的不同表示方法,我们来详细解释一下这之间的关系:
      • SHPRx vs SHP[]:两种对同一片内存的不同访问方式
        • 在手册(ARM TRM)中使用:
          • 名称
            地址
            包含哪些异常的优先级
            SHPR1
            0xE000ED18
            MemManage, BusFault, UsageFault(异常 4-6)
            SHPR2
            0xE000ED1C
            SVCall(异常 11)
            SHPR3
            0xE000ED20
            PendSV(异常 14), SysTick(异常 15)
        • 在core_cm4码中使用:这12个优先级字段直接封装为:
          • 也就是说,SCB->SHP[] 数组是对 SHPR1~3 这三组寄存器按字节映射后的访问方式。
        • 对应关系:SHP[i] 映射到 SHPRx 的字节结构如下
          • 地址
            寄存器
            字节(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])
            notion image
            notion image
        • 为什么CMSIS这么设计?
          • SHPR1~3 三个寄存器拆成了12个8位数组项。优点:可以使用SCB->SHP[10] = 0xFF; 简洁地设置PendSV优先级;不用再移位处理32位的SHPR3与中断号(异常号 - 4)直接匹配,逻辑清晰。
        • 但还是采用裸地址+宏访问SHPR3寄存器
          • 使用方式
            是否推荐
            场景
            #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
    notion image
    • #define SCB_SHPR3 0xE000ED20
      • SHPR3System Handler Priority Register 3,共占用 4 字节(32 位),对应的是:
      • 字节偏移
        系统中断号
        中断类型
        +0 (byte 0)
        Reserved
        Reserved
        +1 (byte 1)
        Reserved
        Reserved
        +2 (byte 2)
        IRQ #14 (0xE)
        PendSV
        +3 (byte 3)
        IRQ #15 (0xF)
        SysTick
        💡
        关键理解:地址本身的单位就是“字节”
        • 地址 0x00 表示第 0 个字节,地址 0x01 表示第 1 个字节,地址 0x02 表示第 2 个字节...
        • SHPR3一个32位的寄存器,占4字节,那么地址为0xE000ED20~0xE000ED23,访问第16~23位,如图所示,就是偏移量为2个字节,也就是两个地址。
         
         
        设置pendsv为最低优先级,理由如下:
      • 因此STM32采取的策略是:触发PendSV异常实现延迟的上下文切换。
      • 知识点补充
      • 💡
        • SVC(Supervisor Call):是ARM架构中用于实现系统调用的关键机制,其核心作用是从用户模式切换到特权模式,使应用程序能够安全地请求操作系统服务。
        • PendSV(Pendable Service Call,可挂起的服务调用):操作系统内核通过触发PendSV异常(通常是通过软件指令触发,设置标志位)。
        notion image
        notion image
        • PendSV几个关键位:
          • Bit28:PENDSVSET(PendSV设置挂起位)
            • 写:0:无影响;1:将PendSV异常状态设置为挂起
            • 读:0:PendSV异常未处于挂起状态;1:PendSV异常为挂起状态
          • Bit27:PENDSVCLR(PendSV清除挂起位)
            • 只写
            • 写:0无影响,移除PendSV异常状态的挂起
          • Bit26:PENDSTSET(Systick设置挂起位)
        notion image
      • 流程分析:
        • 当前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中断驱动的,而不是由任务执行过程驱动的。
          • notion image

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

        notion image

    问题五:开始执行第一个任务

    和初始化时模拟压栈的顺序对应,进行出栈操作,这是一个纯软件过程,不涉及任何异常。
    背景回顾: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;
        • notion image
          代表返回线程模式,使用PSP。
      • 【软件手动压栈区】:额外保存的寄存器 R4~R11
        • 这些寄存器是调用约定中 被调用者(callee)保存的寄存器,在任务切换时由调度器保存/恢复(软件保存)。
      • 最后返回
        • 返回新栈顶(即:恢复上下文时的起始地址)
     
     

    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字节对齐的方式压栈和出栈
      • 举例子
        • 如果发生异常(比如 SysTickPendSV),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 运行逻辑示意

      1. uCOS 启动后,优先执行优先级较高的task1;
      1. task1先执行一次LED2_ON(),然后调用 OSTimeDly(1000)
          • 系统把task1挂起1000个tick,即延时1s,task1变为延时等待状态;
          • 调度器选择就绪的task0(此时唯一可运行);
          • 一个tick是1ms,也就是每次SysTick中断进来,会触发OSTimeTick(),负责tick减1。
      1. task0执行一次LED2_OFF(),然后OSTimeDly(5000)
          • 系统把task0挂起5000个tick,即延时5s,task0变为延时等待状态;
          • 此时,系统没有任何可运行任务(Idle task会运行)
      1. 1000 tick过去,task1苏醒,从delay/wait队列→就绪队列
          • task1的优先级高,触发上下文切换(在SysTick中断里,由OSIntExit()完成);
          • task1再执行一次LED2_ON(),然后调用OSTimeDly(1000),再次挂起自己;
          • 然后此时的t0还在wait队列里面,就继续执行idle任务和倒计时
      总体呈现:
      每隔1s,task1短暂执行一下,每5s,task0重新执行一次。

      ✅ 总结:任务切换的核心链条
       
      notion image
      2-3 ARM STM32 时间驱动机制2-7 Systemview与任务级调试
      Loading...
      🐟🐟
      🐟🐟
      在坚冰还盖着北海的时候,我看到了怒放的梅花
      最新发布
      2-6-1 uC/OSII 在STM32上的移植
      2025-6-2
      Chapter2:Kernel of ERTOS
      2025-5-31
      2-2 ARM STM32 事件驱动机制
      2025-5-31
      2-5 任务上下文切换
      2025-5-31
      2-7 Systemview与任务级调试
      2025-5-31
      2-3 ARM STM32 时间驱动机制
      2025-5-29
      公告
      🎉NotionNext 3.15已上线🎉
      -- 感谢您的支持 ---
      👏欢迎更新体验👏