Lazy loaded image
🪜 实时操作系统
原理篇03-实现上下文处理关键-异常处理
Words 5353Read Time 14 min
2025-5-6
2025-5-6
type
date
slug
category
icon
password
参考资料
  1. 用于ARM架构的C编译器基于AAPCS
  1. Cortex-M 的异常和中断编程模型 【参考原理篇01-从Cortex-M处理器架构谈起 | Felix’s Micro Space
    1. 操作模式和状态
    2. 异常和中断关系
    3. 中断控制用的NVIC寄存器
    4. 特殊寄存器-用于异常和中断屏蔽
    5. 特殊寄存器-CONTROL 寄存器
    6. 存储系统-栈存储
 
嵌入式OS中任务调度就是通过处理器异常机制实现,比如systick中断、SVC异常和PendSV异常。对于Cortex-M处理器,可将异常处理或中断服务程序(ISR)实现为普通的C程序/函数。我们按如下步骤逐步分析异常处理的机制。
  1. 首先普通C函数在ARM架构上如何工作的 - ATPCS 规则
  1. 接着,深入了解上下文切换关键-中断异常处理机制
  1. 最后,拓展说明异常压栈出栈流程
 
首先,我们来看一下C函数在ARM架构上是如何工作的?

一、ARM-Thumb过程调用标准

在 arm 中有个 ATPCS 规则(ARM-THUMB procedure call standard(ARM-Thumb过程调用标准)。 约定 R0-R15 寄存器的用途:
notion image
  • R0-R3 传参
    • 调用者和被调用者之间传参数
  • R0 返回值
  • R4-R11
    • 函数可能被使用,所以在函数的入口保存它们(寄存器保存到栈中),在函数的出口恢复它们(栈中数据还原到寄存器)。
  • R12-临时寄存器
  • R13-SP
    • 主要功能:指向当前栈顶位置
    • 工作方式:栈操作(push/pop)时动态调整,通常在函数调用时用于保存局部变量、返回地址和寄存器状态
    • 重要性:管理函数调用栈,确保函数正确执行和返回
    • 注意事项:栈溢出是嵌入式系统中常见的问题,正确管理SP至关重要
  • R14-LR(Link Register)
    • 主要功能:存储函数调用的返回地址
    • 工作方式:当执行函数调用指令时,当前PC值(即下一条指令地址)会被保存到LR
    • 重要性:使函数能够正确返回到调用点继续执行
    • 特殊情况:在嵌套函数调用中,之前的LR值需要保存到栈中以防被覆盖
  • R15-PC(Program Counter)
    • 主要功能:存储当前执行指令的内存地址
    • 工作方式:CPU执行一条指令后,PC通常会自动递增,指向下一条指令的地址
    • 重要性:控制程序执行流程,是CPU取指令过程的核心
    • 应用场景:程序分支、跳转和函数调用时,PC会被修改以改变执行流程
  • R16-PSR
三者协同工作示例
在典型的函数调用过程中:
  1. 当前PC值(返回地址)存入LR
  1. PC修改为被调用函数的起始地址
  1. SP调整,为局部变量分配空间
  1. 函数执行完毕后,SP恢复,PC从LR加载返回地址

1.1 ATPCS 规则

notion image
若Cortex-M处理器具有浮点单元,则浮点单元中的寄存器也有类似的需求:
  • S0 ~ S15 为“调用者保存寄存器”。
  • S16 ~ S31 、FPSCR为“被调用者保存寄存器”。
1. 参数传递 / 返回值
寄存器被拆分成2部分:调用者保存的寄存器(R0-R3,R12,LR,PSR)被调用者保存的寄存器(R4-R11)
比如函数 A 调用函数 B,函数A为调用者,B为被调用者。
规则1.1:R0-R3 是用来传参数给被调用者;
规则1.2:R0 用来返回数据。
2. 寄存器保护
规则2.1:调用函数需要保存 R0-R3、LR、PSR;被调用函数可以肆意修改 R0-R3。
规则2.2:被调用函数入口处保存R4-R11,函数返回前恢复;
3. 双字节对齐
数据地址必须是偶数,确保数据更高效访问。对于异常处理,特别是函数调用时,处理器需要将当前上下文(寄存器值等)保存到栈中,然后恢复上一个函数的上下文。如果栈不是双字节对齐的,这个过程可能会变得复杂,并且会增加异常处理的时间开销。
notion image
在使能双字栈对齐功能,且栈指针未对齐到双字边界。栈中会插入空位,强制对齐到双字地址。xPSR 的第 9 位被置1,表明插入一段区域。在异常退出时,可以指示是否需要调整SP的数值。
对于带浮点单元的处理器,栈帧分布和双字对齐情况可以参考下图:
notion image
对于M3/M4 处理器,双字对齐特性默认情况如下:
  • Cortex-M4 处理器中默认使能。
  • Cortex-M3r2p0 及之后版本默认使能。
  • Cortex-M3rlp0 和 rlp1 默认禁止。
  • Cortex-M3r0p0 中不可用。
可以通过配置SCB块种CCR寄存器栈对齐控制位来使能该特性
💡
规则2.1 主要带来如下优点:
  1. 允许被调用的函数(callee)自由使用这些寄存器来传递参数和返回值,而不需要在每次函数调用后恢复它们的原始值。这简化了函数的编写;
  1. 提高了代码的效率,因为不需要在每次函数调用后都保存和恢复这些寄存器,特别是在嵌入式系统中,这些操作可能会导致额外的时间和空间开销;
  1. 通过允许函数自由改变这些寄存器的值,ATPCS 支持更好的函数封装和独立性。
  1. 调用者负责保存和恢复它需要保留的值,而被调用者只需关心如何使用传入的参数执行其功能。这样的责任分配有助于减少错误和提高代码的可读性和可维护性。

1.2 示例程序1:基础函数调用和参数传递的示例

1.2.1 反汇编分析

说明1:L14-L17,通过将使用的 r0,r1 寄存器变量保存在栈中。因为变量定义为 volatile 类型,防止编译器优化
说明2:L18-L19,通过r0,r1 寄存器传递参数 &a、&b,规则1.1。
说明3:L21, 通过栈来保存局部变量 c
说明4:链接寄存器保存返回的指令地址,在函数调用结束,跳转到PC执行

1.2.2 函数调用示意图(寄存器和栈)

程序运行过程:取指令 -> 从栈中读数据 -> 计算 -> 向栈中写数据

1.3 示例程序2:故意使用额外R4寄存器

反汇编分析

C函数用于异常处理需要两个处理特殊处理,第一是硬件自动保存调用者保存寄存器,第二是返回值特殊处理。

二、上下文切换关键-异常处理机制

2.1 背景:FreeRTOS 任务初始化和上下文切换流程

FreeRTOS 任务初始化,需利用SVC异常进入处理模式,然后创建进程栈中的栈帧,且触发使用PSP的异常返回。当加载栈帧时,应用任务就会启动。

SVC 异常

  1. SVC 需在 SVC 指令后执行
  1. 通过 SVC 可以通过服务编号和OS 服务所需参数获得OS服务,而无需知道OS服务函数地址
  1. 调用服务指令 SVC #0x02
  1. 如何获取 SVC 指令中的立即数?
    1. 根据 LR 寄存器,获取当前使用堆栈
    2. 获取 PC-2 地址中存储编号
在OS设计中,需要在不同任务间切换,这一般被称作上下文切换,其通常在PendSV异常处理中执行,该异常可由SysTick异常触发。在上下文切换操作中需要:
  • 将寄存器的当前状态保存到当前栈中
  • 保存当前PSP数值
  • 将PSP设置为下一个任务的上一次SP数值
  • 恢复下一个任务的上一次的数值
  • 利用异常返回切换任务
注意:需修改PendSV中断为最低优先级。

PendSV 异常

  1. 周期性的SysTick异常触发Pensv异常,然后完成上下文切换
  1. 中断请求发生在 SysTick异常前,为了防止 IRQ 处理被 SysTick打断延迟。有两种解决方案
    1. 方案1:检查压栈栈帧中 xPSR 或 NVIC中断活跃状态寄存器,有中断服务时不执行上下文切换
    2. 方案2:PendSV 设置为最低优先级,在IRQ处理完成后进行上下文切换
  1. PendSV中执行任务切换流程
    1. A 任务调用 SVC 进行任务切换(例如,等待一些工作完成)。
    2. OS收到请求,准备进行上下文切换,且挂起 PendSV 异常。
    3. 当 CPU 退出 SVC 时,会立即进入PendSV 且进行上下文切换。
    4. 当 PendSV 完成并返回线程等级时,OS 会执行B任务。
    5. 中断产生且进入中断处理。
    6. 在运行中断处理程序时,SYSTICK 异常(用于OS节拍)会产生。
    7. OS 执行重要操作,然后挂起 PendSV 异常并准备进行上下文切换。
    8. 当 SYSTICK 异常退出时,会返回到中断服务程序。
  1. PendSV用于不存在OS的环境中,拆分处理时间长的中断服务(其他中断服务响应慢)为两部分:
    1. 第一部分对时间要求比较高,需要快速执行,且优先级较高。它位于普通的 ISR 内,在 ISR 结束时,设置 PendSV 的挂起状态。
    2. 第二部分包括中断服务所需的剩余的处理工作,它位于 PendSV 处理内且具有较低的异常优先级。

2.2 硬件控制寄存器保存和恢复

正常函数调用,寄存器 R0-R3、R12、LR 和 PSR根据约定,调用者会保存。但异常处理(操作系统任务初始化和上下文切换的核心)时如何实现寄存器和栈管理?
中断处理函数本质上也是 C 函数,由于中断和异常异步发生,无法确定其发生位置。程序可能在任何位置被打断,为了能恢复中断发生前现场,需保存被中断任务的寄存器状态,包括程序计数器(PC)、状态寄存器和工作寄存器(如 R0-R3)。这部分在Cortex-M处理器中,由硬件控制保存寄存器 R0-R3、R12、LR、PSR到栈中(MSP/PSP)。
同时C 函数可以用作异常处理,还有一处不同:
  • 与普通的C函数调用不同,返回地址(PC)没有存储在 LR 中,而是存储在返回地址。这样异常处理期间保存的寄存器共有 8 个(增加了一个返回地址)。
notion image
总的流程如下:
  1. 保存现场(保存所有寄存器)
      • 硬件保存寄存器 R0-R3、R12、LR、PSR到栈中(MSP/PSP)
      • C 函数调用规则保证保证 R4 - R11不被破坏(同样入栈)
  1. 中断处理
  1. 恢复现场
      • 硬件恢复所有的寄存器
⚠️
注意:不同的芯片,不同的架构,在这方面的处理稍有差别:
  • 保存/恢复现场:cortex M3/M4是硬件实现的,cortex A7是软件实现的
  • CPU 中止当前执行,跳转去执行处理异常的代码:也有差异
    • cortex M3/M4在向量表上放置的是函数地址
    • cortex A7在向量表上放置的是跳转指令

2.3 异常返回机制 - EXC_RETURN

异常异步发生,怎么确定是从异常中返回,触发恢复现场。异常返回后是使用 MSP 还是 PSP?
通过LR添加特殊值,确定返回后使用栈类型,返回模式,是否使用FPU模式等,防止中断嵌套时直接返回线程堆栈中运行。
  • 软件函数调用, LR=下句指令的地址;
  • 硬件中断调用,恢复现场怎么确定返回地址;
    • LR 修改为 EXC_RETURN
    • 当利用BXPOP指令或存储器加载(LDR或LDM)指令,加载到程序寄存器中时,该数值用于触发异常返回机制
EXC_RETURN 中的一些位提供异常处理额外信息。EXC_RETURN 各位定义如下表所示:
描述
数值
31:28
EXC_RETURN 指示
0xF
27:5
保留全为0
0xEFFFFF (23位全为1)
4
栈帧类型
1 - 浮点单元不可用时总是为 1,在进入异常处理时,会被置为 CONTROL 寄存器的 FPCA
3
返回模式
1(返回线程)或 0(返回处理)
2
返回栈
1(返回线程栈)或 0(返回主栈)
1
保留
0
0
保留
1
EXCRETURN的合法值则如下表所示。
浮点单元在中断前使用(FPCA=1)
浮点单元未在中断前使用(FPCA=0)
返回处理模式
0xFFFFFFE1
0xFFFFFFF1
返回线程模式,返回后使用主栈
0xFFFFFFE9
0xFFFFFFF9
返回线程模式,返回后使用线程栈
0xFFFFFFED
0xFFFFFFFD
由于EXC_RETURN的编码格式,在地址区域0xF0000000~0xFFFFFFFF中是无法执行中断返回的。不过,由于系统空间中的地址区域已经被架构定义为不可执行的,因此这样不会带来什么问题。

2.4 示例程序分析

轮流执行task_a、task_b,打印字符。
notion image

2.4.1 反汇编分析

当程序在汇编所指位置发生systick中断后,为了实现任务切换,主要做了如下工作:
  1. 硬件保存寄存器
  1. systick中断,保存R4-R11,恢复task_b现场

2.4.2 任务栈示意图

notion image

三、拓展内容-异常流程详解

3.1 异常进入和压栈

  1. 哈佛总线架构,AHB Lite 流水线设计。使得在压栈同时,处理器还可以使用I-CODE总线取向量和取指(即RAM和Flash同时访问)。这样可以降低中断等待时间
  1. 压栈期间栈访问顺序和栈帧中寄存器顺序不同。栈优先将PC和xPSP压栈(原本位于N+24,N+28),再从低地址依次压栈,这样可以尽快更新PC。
    1. notion image
      Cortex-M3处理器的AHB Lite总线上的压栈流程
压栈操作中用的栈可以为主栈(使用主栈指针,MSP)或进程栈(使用进程栈,PSP),由CONTROL寄存器第二位决定。
压栈示意图
 
使用主栈的线程模式的异常栈帧
使用主栈的线程模式的异常栈帧
使用进程栈的线程模式的异常栈帧,以及使用主栈的嵌套中断压栈(嵌套只使用msp)
使用进程栈的线程模式的异常栈帧,以及使用主栈的嵌套中断压栈(嵌套只使用msp)

3.2 异常返回和出栈

在异常处理结束时,异常入口处生成的EXC RETURN数值的第2位用于确定提取栈帧所用的栈指针。若第2位为0,则处理器会知道之前压栈时使用的是主栈,则出栈也会使用主栈,如下图所示。
出栈示意图
线程模式使用主栈
线程模式使用主栈
线程模式使用进程栈
线程模式使用进程栈
在每次出栈操作结束时,处理器还会检查出栈xPSR数值的第9位,并且若压栈时插入了额外的空间则会将其去除。
上一篇
原理篇02-再说Cortex-M处理器对OS支持特性
下一篇
ROS 官网教程01-基础概念和操作

Comments
Loading...