Cortex-M3权威指南Cn第三章——笔记

个人学习的笔记,希望帮助的和我一样阅读Cortex-M3权威指南Cn遇到困难的人。
强烈建议先阅读Cortex-M3权威指南Cn第三章在来观看笔记



在cortex m3 中,什么是MPU

在嵌入式处理器(尤其是Cortex-M3这类微控制器)中,MPU指的是内存保护单元

它是一种硬件模块,用于控制和保护对内存区域的访问 ,是增强系统鲁棒性、隔离不同任务、防止内存错误访问的关键组件。

你可以把它理解为一个内存的"交通管制员"或"保安"。它不负责将虚拟地址转换为物理地址(那是MMU内存管理单元 的主要工作,常见于高性能应用处理器),而是定义和管理内存区域的访问权限和属性

MPU的核心功能

  1. 区域划分: 将系统的内存空间(如Flash、RAM、外设等)划分为数个独立的区域(例如,在Cortex-M3中通常支持8个区域)。
  2. 规则制定 : 为每个区域配置详细的"规则",包括:
    • 起始地址和大小
    • 访问权限: 如是否允许读、写、执行。
    • 内存属性: 如是否可缓存、是否可缓冲。
    • 特权要求: 规定该区域是只能在"特权模式"下访问,还是允许在"用户模式"下也访问。
  3. 实时监控与拦截 : 当处理器内核(或DMA等总线主设备)尝试访问内存时,MPU会实时检查这次访问是否符合该区域的规则。如果违规(例如用户程序试图写入一个只读区域),MPU会立即触发一个MemManage Fault(内存管理错误)异常,阻止非法访问,从而保护关键数据。

在Cortex-M3中的MPU特点

  • 集成数量: Cortex-M3可选配一个MPU模块,具体是否实现取决于芯片厂商。
  • 区域数量: 通常支持8个独立的可编程区域。每个区域可以独立启用或禁用,并且可以灵活地重叠配置。
  • 背景区域 : 当MPU启用但某个内存地址不属于任何已定义的使能区域时,对于特权模式 的访问,可以通过启用"背景区域"(使用默认的映射规则)来允许访问;而对于用户模式的访问,则一律会触发错误。


在cortex m3 中,msr、mrs是什么指令

下面的表格清晰地展示了它们各自的功能和用法:

指令助记符 功能描述 典型汇编语法示例
MRS 将特殊功能寄存器的值读取到通用寄存器中。 MRS R0, PSR (读取程序状态到R0)
MSR 将通用寄存器的值写入到特殊功能寄存器中。 MSR CONTROL, R0 (将R0的值写入控制寄存器)


在cortex m3 中,R0 - R12通用寄存器

在Cortex-M3中,R0-R12这13个(注意是13个,而非12个)寄存器是"最通用目的"的,这意味着它们在软件编程中功能完全通用,没有强制的特殊用途,编译器或程序员可以自由地使用它们来保存任意数据或地址。 它们每一个都是32位 宽。

为了更清晰地理解所有寄存器的分工,下表列出了Cortex-M3的所有核心寄存器及其核心功能:

寄存器组 寄存器名 位数 主要用途与特性
通用寄存器 R0 - R12 32位 真正的通用目的。用于算术运算、数据搬运、地址暂存等所有常规操作,无任何硬件强制规则。
特殊功能寄存器 R13 (SP) 32位 堆栈指针。Cortex-M3有两个独立的SP:主堆栈指针(MSP,用于Handler模式)和进程堆栈指针(PSP,用于线程模式)。
R14 (LR) 32位 链接寄存器。在调用子程序或发生异常时,硬件自动将返回地址存入LR。
R15 (PC) 32位 程序计数器。存储下一条要执行的指令地址。对它写入会导致程序跳转。
程序状态寄存器 xPSR 32位 程序状态寄存器组,包含应用程序状态(APSR)、中断号(IPSR)和执行状态(EPSR)等信息。

尽管通用,但在编译器和汇编编程中,通常会遵循ARM架构过程调用标准的惯例:

  1. R0-R3 :常用于传递函数的前4个参数 ,以及保存函数的返回值(尤其是R0)。
  2. R4-R11 :通常用作局部变量寄存器 。在函数内部,如果它们的值被修改,调用者希望保存这些值,那么在函数入口需要将它们压入堆栈保存 ,在退出前弹出恢复 。因此,它们也被称为 "被调用者保存寄存器"
  3. R12 (IP) :有时被编译器用作内部临时寄存器 ,特别是在函数调用和跳转的胶合代码中。
    注:在 Thumb-2指令集 下(Cortex-M3只支持此指令集),有一条重要特性:许多常用的16位Thumb指令只能访问R0-R7(即低位寄存器)。而访问R8-R12可能需要使用32位的指令。这对追求极致代码密度的场合有细微影响,但对一般C语言编程是透明的。

注:与早期ARM架构(如ARM7)的16个通用寄存器(R0-R15,其中R13-R15已专用)相比,Cortex-M3的这种定义(R0-R12通用,R13-R15完全专用)体现了其作为microcontroller 内核的简化与优化设计,更强调确定性和中断响应。


为什么要有通用寄存器,在c语言中如何理解他们。

用一个更深入的比喻和类比来解释:

厨房做饭

想象你要做一道菜(执行一个程序)。

  • C语言变量 :就像你写在菜谱 上的那行字:"将黄油(100克)和糖(50克)混合搅拌"。
    • 菜谱本身只是指令和抽象描述
  • 内存 :就像你的冰箱和储藏柜。里面存放着"黄油"、"糖"等所有原材料(数据)。
  • 通用寄存器 :就像你面前的碗、锅和操作台 。你必须把黄油从冰箱(内存)里拿出来,放到碗里(寄存器),把糖从柜子里拿出来,也放到碗里(寄存器),然后才能在碗里进行"混合"这个实际操作。
    关键点:你无法直接操作还在冰箱里的黄油。你必须把它放到一个容器(寄存器)里,才能搅拌、加热或混合它。

C代码的机器指令的翻译:

c 复制代码
a = b + c;

对你而言,这是一行指令。但对CPU而言,它必须分解成多个触及物理硬件的步骤。编译器会把它翻译成类似这样的汇编指令:

  1. LOAD R1, [address_of_b]从内存b的地址处,把值加载到寄存器R1。(从冰箱拿出黄油)
  2. LOAD R2, [address_of_c]从内存c的地址处,把值加载到寄存器R2。(从柜子拿出糖)
  3. ADD R0, R1, R2在寄存器内部 ,将R1和R2的值相加,结果存入寄存器R0。(在碗里混合)
  4. STORE R0, [address_of_a]:将寄存器R0的值写回到内存a的地址处。(把混合好的成品放回某个冰箱格子)
    可以看到:没有寄存器(R0, R1, R2),CPU根本没有地方来进行最核心的加法运算!

为什么不能只用内存,取消寄存器?

理论上可以设计一个所有操作都直接对内存进行的CPU(有些古老的架构接近这样),但这样的CPU会极其缓慢 ,因为每一条指令都要等待缓慢的内存读写,CPU的高速优势将荡然无存。这就好比让厨师每切一刀都要跑去冰箱拿一次食材,做一道菜需要一整天。
寄存器的存在,是计算机体系结构遵循"访问速度越快,容量越小"分级存储思想的根本体现,是为了匹配CPU与内存之间巨大的速度差距而必须设计的"高速缓存区"。


一个C语言实例

c 复制代码
// C 代码
int add(int a, int b) {
    int c = a + b;
    return c;
}

void main() {
    int x = 5;
    int y = 10;
    int sum = add(x, y); // 调用add函数
}

编译后可能发生的情况:

  1. main函数将 x 的值 5 放入 R0 ,将 y 的值 10 放入 R1(准备参数)。
  2. main调用add函数。
  3. add函数进入后,直接从 R0 拿到第一个参数a(5),从 R1 拿到第二个参数b(10)。
  4. add函数在内部进行加法运算,结果可能暂存在 R0
  5. add函数返回,返回值就在 R0(15)中。
  6. main函数从 R0 拿到返回值 15,存入为变量sum分配的内存或寄存器中(例如 R4)。

总结

理解通用寄存器的关键两点:

  1. 存在意义 :它们是CPU的高速数据缓存工作区,是程序能以极速运行的根本物理基础之一。
  2. 使用规则(调用约定):为了让程序模块(函数)能协同工作,业界制定了关于如何使用R0-R12的软件约定。这不是硬件的强制要求,但所有编译器都遵守它,从而保证了程序的正确链接和执行。


在cortex m3 中,R13为什么要有两个堆栈指针(MSP和PSP)

R13(堆栈指针SP)的基本作用
堆栈指针(Stack Pointer, SP) 就像一个图书管理员的手指,始终指向堆栈的"顶部"(最新位置)。堆栈是内存中的一个特殊区域,用于:

  1. 保存函数调用时的返回地址
  2. 保存局部变量
  3. 保存寄存器的值 (在函数调用或中断发生时,只保存8个寄存器(xPSR, PC, LR, R12, R3-R0)
  4. 传递函数参数 (在某些情况下)
    堆栈遵循后进先出(LIFO)原则,就像一摞盘子:最后放上去的盘子(数据)最先被取走。
c 复制代码
void functionA() {
    int x = 10;          // 局部变量x被压入堆栈
    functionB();         // 返回地址被压入堆栈
    // ...               // functionB返回后,从堆栈弹出返回地址
}                         // 函数结束时,x从堆栈中弹出

在Cortex-M3处理器有两种操作模式:线程模式(Thread Mode)处理模式(Handler Mode)

线程模式是执行普通代码的模式,处理模式是处理异常(包括中断)的模式。

同时,Cortex-M3支持两个堆栈指针:主堆栈指针(MSP)和进程堆栈指针(PSP)。

处理器在任一时刻只使用其中一个堆栈指针,具体使用哪个由处理器的当前模式和CONTROL寄存器的设置决定。

那么为什么要有两个堆栈指针?

主要是为了将操作系统内核(以及异常处理)的堆栈和用户应用程序的堆栈分开,这样可以在多任务环境中保护系统内核的堆栈不被用户程序破坏,同时也可以使得每个任务拥有自己的堆栈,便于任务切换和上下文保存。

详细说明:

  1. 主堆栈指针(MSP)
    • 这是系统复位后默认使用的堆栈指针。
    • 当处理器处于处理模式(Handler Mode) 时,总是使用MSP。
    • 操作系统内核以及异常处理程序(包括中断服务程序)通常使用MSP。这样,当发生异常时,处理器自动切换到MSP,从而确保异常处理程序有一个安全、可靠的堆栈(不会被用户程序破坏)。
  2. 进程堆栈指针(PSP)
    • 主要用于线程模式下的用户任务。
    • 当处理器处于线程模式(Thread Mode),并且CONTROL寄存器的bit[1](SPSEL)设置为1时,使用PSP。
    • 在多任务操作系统中,每个任务都有自己的堆栈空间,任务运行时使用PSP指向自己的堆栈。这样,每个任务的堆栈是独立的,一个任务的堆栈溢出不会直接影响其他任务,也不会影响内核。
      注意:复位在cortex m3中也属于一种中断
      注意:在cortex m3中中断又被称之为异常
  3. 如何切换
    • 在异常进入时,处理器自动从线程模式切换到处理模式,并自动将当前使用的堆栈指针(可能是PSP,如果之前线程模式使用的是PSP)切换为MSP。
    • 在异常返回时,处理器根据异常返回时加载的CONTROL寄存器值决定返回到线程模式后使用哪个堆栈指针。
  4. CONTROL寄存器
    • CONTROL[1](SPSEL):在线程模式下,0表示使用MSP,1表示使用PSP。在处理模式下,此位为0(总是使用MSP)且写入非零值会被忽略。
    • CONTROL[0](nPRIV):定义线程模式下的特权级别。0表示特权级,1表示用户级(非特权)。处理模式总是特权级。
  5. 双堆栈机制的好处
    • 安全性:内核和异常处理使用MSP,用户任务使用PSP,这样用户任务的堆栈错误不会破坏内核堆栈。
    • 多任务支持:每个任务可以拥有独立的堆栈,任务切换时只需切换PSP的值即可。

场景演化:

c 复制代码
// 伪代码示意,非实际代码
void main() {
    // 1. 系统启动,使用MSP(主堆栈)
    // 内核初始化...
    
    // 2. 创建任务A和任务B,为每个任务分配独立的堆栈空间
    uint32_t taskA_stack[512];  // 任务A的堆栈
    uint32_t taskB_stack[512];  // 任务B的堆栈
    
    // 3. 启动任务调度器
    while(1) {
        // 调度器决定运行任务A
        switch_to_task_A();  // 切换到PSP指向taskA_stack
        // 任务A运行一段时间...
        
        // 发生中断(比如定时器中断)
        // 硬件自动:1) 保存任务A的上下文到当前活动的堆栈(可能是MSP或PSP),然后切换到MSP(通过PSP)
        //           2) 切换到MSP(处理中断)
        //           3) 执行中断服务程序
        // 发生中断更准确的描述:
		// 1) 硬件使用当前活动的堆栈指针(如果是PSP,就使用PSP)保存部分上下文
		// 2) 切换到Handler模式,使用MSP
		// 3) 执行中断服务程序
        
        // 中断服务程序中,调度器决定切换到任务B
        // 保存任务A的完整状态到它的堆栈(通过PSP)
        // 将PSP设置为指向taskB_stack的顶部
        // 从taskB_stack恢复任务B的上下文
        
        // 中断返回,恢复任务B的执行(使用PSP),硬件只恢复那8个寄存器,R4-R11需要软件手动保存/恢复
    }
}

时间线图示:

c 复制代码
时间轴:       任务A运行    →   中断发生    →   任务B运行
堆栈指针:     PSP(A)          →    MSP      →    PSP(B)
                 ↑                  ↑               ↑
             任务A的堆栈        内核堆栈       任务B的堆栈
              [局部变量]       [中断上下文]     [局部变量]
              [返回地址]       [调度数据]      [返回地址]

精确的时间线图示:

c 复制代码
时间轴:     任务A运行     →     中断进入     →     中断处理     →     中断返回     →     任务B运行
-----------------------------------------------------------------------------------------------
处理器模式:   Thread        →    自动切换       →     Handler      →    自动切换       →     Thread
             (用户任务)       ↓   (硬件完成)     ↓   (内核代码)     ↓   (硬件完成)     ↓   (用户任务)
-----------------------------------------------------------------------------------------------
使用堆栈指针: PSP(A)         →    保存上下文      →     MSP         →    恢复上下文      →     PSP(B)
                            ↓    到PSP(A)      ↓                ↓    从PSP(B)      ↓
-----------------------------------------------------------------------------------------------
关键操作:                [硬件自动]          [软件执行]         [软件调度]        [硬件自动]
                      - 压入xPSR,PC等     - 可选的保存       - 保存任务A状态  - 弹出xPSR,PC等
                      - 设置EXC_RETURN    - 处理中断         - 恢复任务B状态  - 根据EXC_RETURN
                      - 切换到Handler     - 可能任务切换     - 修改EXC_RETURN  切换堆栈指针

实际的任务切换代码示例(FreeRTOS风格)

c 复制代码
// PendSV_Handler - 实际的任务切换异常处理程序
__attribute__((naked)) void PendSV_Handler(void) {
    __asm volatile (
        // 1. 保存当前任务的上下文到它的堆栈(手动保存R4-R11)
        "mrs r0, psp                 \n"  // 获取当前任务的PSP到R0
        "stmdb r0!, {r4-r11}         \n"  // 将R4-R11保存到任务堆栈
        
        // 2. 保存当前任务的堆栈指针到它的TCB
        "ldr r1, =pxCurrentTCB       \n"  // 获取当前TCB指针的地址
        "ldr r1, [r1]                \n"  // 获取当前TCB指针
        "str r0, [r1]                \n"  // 将更新后的PSP保存到TCB
        
        // 3. 切换到下一个任务
        "bl vTaskSwitchContext       \n"  // 调用C函数选择下一个任务
        
        // 4. 从下一个任务的TCB加载堆栈指针
        "ldr r1, =pxCurrentTCB       \n"
        "ldr r1, [r1]                \n"
        "ldr r0, [r1]                \n"  // 从TCB获取新的PSP
        
        // 5. 从新任务的堆栈恢复寄存器
        "ldmia r0!, {r4-r11}         \n"  // 从堆栈恢复R4-R11
        
        // 6. 更新PSP
        "msr psp, r0                 \n"
        
        // 7. 异常返回
        "bx lr                       \n"
    );
}


在cortex m3 中,什么是向下生长的满栈

"向下生长的满栈"是描述 Cortex-M3 堆栈操作行为的两个关键硬件特性。我们把它拆成"向下生长"和"满栈"两部分,并用一个比喻来理解。

想象你的内存空间是一张从低地址到高地址(比如从1楼到10楼)的桌子。堆栈就是桌上的一摞书。

  • 栈底 :这摞书最初放在桌子的较高楼层(高地址)
  • 栈顶:书的最上面一本。
  • 堆栈指针 :一个始终指向最上面那本书的箭头。

"向下生长"

这指的是堆栈在内存中扩展的方向

  • 当你 PUSH 一本书(存入一个数据)时,你需要在现有这摞书的下方(更低楼层)放一本新书 。因此,书堆(堆栈)向低地址方向增长。
  • 当你 POP 一本书(取出一个数据)时,你是从当前书堆的最上面取走一本,书堆向高地址方向收缩。
  • 对应硬件行为 :执行 PUSH {R0} 时,堆栈指针 SP (R13)先自减4 (指向更低地址),然后再存入数据。这就是你例子中 *(--R13)=R0 的含义。
    "满栈"
    这指的是堆栈指针 SP 所指向的位置状态
  • "满栈"意味着堆栈指针 SP 始终指向最后一个被压入的有效数据(也就是那摞书最上面那本书的位置)。
  • 与之相对的是"空栈",即 SP 指向下一个空闲的、可用的位置 (书堆上方第一个空位)。Cortex-M3 不使用 这种模式。
  • 对应硬件行为 :因为 SP 指向的是有效数据,所以 PUSH 时必须先移动指针腾出空位(先--SP),再把数据放入这个新位置。POP 时则是先取出 SP 指向的数据,再移动指针指向上一个数据(SP++)。

总结

  1. 向下生长 :堆栈向内存低地址方向扩展。这是 ARM Cortex-M 系列的固定设计。
  2. 满栈 :堆栈指针 SP 永远指向最后一个被压入的有效数据 。这决定了 PUSH 是"先减后存",POP 是"先取后加"。
  3. 统一硬件行为:这种模型由硬件固化,所有软件(编译器、操作系统)都必须遵循。它保证了中断处理、函数调用时上下文保存/恢复的可靠性和高效性。

什么是向下生长而不是向上生长?
因为这里"上下"指的是内存地址数值的变化方向,而不是物理空间位置

想象计算机的内存是一栋从1楼开始向上编号的高楼

  • 低地址 = 低楼层 (如 1楼 0x20001000)
  • 高地址 = 高楼层 (如 10楼 0x20001024)
    现在,堆栈 就像是这栋楼里的一个特殊书柜 。这个书柜有一个固定在天花板上的弹簧托盘
  1. 初始状态 :托盘被弹簧顶在高楼层 (比如10楼)。这是书柜的栈底
  2. "向下生长" :当你 PUSH 放入一本书(数据)时,弹簧被压缩,托盘会带着书一起向低楼层移动 (比如降到9楼)。你继续放书,托盘就继续向更低楼层下降 (8楼、7楼...)。从楼层号来看,书柜(堆栈)使用的空间是向着越来越小的楼层号(低地址)方向扩展的,这就是"向下生长"。
  3. "向上生长" (对比理解):如果是一个普通的、放在地上的书柜。你从1楼开始往上摞书,书堆会向着越来越高的楼层号(高地址)方向扩展 ,这就是"向上生长"。但Cortex-M3不采用这种方式。

CPU和程序员视角看:

特性 向下生长 (Cortex-M3采用) 向上生长 (某些架构采用)
堆栈扩展方向 向内存低地址方向 扩展。 向内存高地址方向 扩展。
PUSH 操作 先减小栈指针(SP) ,再存数据。 SP = SP - 4; *SP = data; 先存数据 ,再增大栈指针(SP)。 *SP = data; SP = SP + 4;
POP 操作 先取数据 ,再增大栈指针(SP)。 data = *SP; SP = SP + 4; 先减小栈指针(SP) ,再取数据。 SP = SP - 4; data = *SP;
栈指针指向 最后一个有效数据(满栈)。 下一个空闲位置(空栈)。
直观感受 栈底在高地址,栈顶向低地址"生长",像从天而降。 栈底在低地址,栈顶向高地址"生长",像从地而起。

Cortex-M3的 PUSH {R0} 对应的 *(--R13)=R0 正是"向下生长的满栈"的完美体现:先减(向下),后存(满栈)。


注意:

我们通常的习惯是:

  • 看书、写字:从页面上方(低) 开始,向下(高) 进行。
  • 建房子:从地基(低) 开始,向上(高) 搭建。
    而计算机内存地图的绘制习惯是:
  • 低地址画在上方高地址画在下方
  • 在这种图示里,"向下生长"的堆栈,看起来就是从上往下画 ,这反而和我们的阅读方向(从上到下)一致了,但和建筑方向(从下到上)相反。
    我们需要翻转一下思维:在内存地图上,"向下"意味着地址数值在变小。

当然如果觉得托盘弹簧与书架比喻难以理解还可以换一种比喻:

餐厅餐具柜与服务员

想象一个高级餐厅的管理员,负责管理一个超长餐具柜。这个柜子用来临时存放客人用过的盘子(数据)。

场景设定

  1. 餐具柜 :代表内存
    • 它有固定编号的格子 ,从 1 号到 N 号。1号格在最左边(低地址),N号格在最右边(高地址)。
  2. 盘子 :代表数据
  3. 服务员:代表 堆栈指针 SP**。
    • 你永远站在餐具柜前,用手指着某一个格子。
    • 你手指的位置,就是SP的值。
      核心规则(硬件逻辑)
      餐厅规定,这个餐具柜的使用方式非常严格:
  4. 初始化(系统启动)
    • 你被要求一开始必须站在最右边的格子(高地址,比如第100号格)前 。这是栈底
    • 此时柜子是空的,但你的手"指向"这个准备接收第一个盘子的位置。
  5. 存盘子操作 PUSH (发生函数调用或中断)
    • 当需要存一个脏盘子(数据入栈)时,你必须遵守以下步骤:
      1. 先向左移动一步 (向编号更小的格子移动,例如从100号走到99号)。这一步对应 SP = SP - 4(向下生长)
      2. 然后把盘子放进你面前这个新格子这一步对应 *SP = data
    • 现在,你手指着这个装有盘子的格子(99号) 。这就是"满栈"------指针永远指着最后一个有效物品。
  6. 取盘子操作 POP (函数返回或恢复上下文)
    • 当需要取回一个干净盘子(数据出栈)时,步骤相反:
      1. 从你当前手指的格子里拿出盘子 (例如从99号格)。这一步对应 data = *SP
      2. 然后向右移动一步 (向编号更大的格子移动,例如从99号走回100号)。这一步对应 SP = SP + 4
    • 现在,你手指着下一个可用的"旧"位置(100号格)。

补充说明:

对应的Thumb-2指令集

assembly 复制代码
; Cortex-M3中常见的堆栈操作指令
PUSH {R0-R3, LR}    ; 将多个寄存器压栈
POP {R0-R3, PC}     ; 从堆栈恢复寄存器,并直接返回

; 这些指令在硬件层面自动执行正确的递减/递增操作

实际代码中的体现:

c 复制代码
// 在C代码中如何观察堆栈行为
void stack_demo(void) {
   int local_var = 42;  // 这个局部变量会被放在堆栈上
   
   // 在反汇编中,你会看到类似:
   // SUB sp, sp, #8      ; 为局部变量分配栈空间(向下生长!)
   // STR r0, [sp, #4]    ; 将值存储到栈中
   
   // 函数返回时:
   // ADD sp, sp, #8      ; 释放栈空间(向上收缩)
}


在cortex m3 中,什么是连接寄存器 R14

连接寄存器 R14,也叫链接寄存器(Link Register, LR),它的核心作用是由硬件自动保存函数或异常发生时的"返回地址",是实现程序正确跳转和返回的关键硬件机制。

可以把它理解为CPU的 "专用书签" :每当CPU跳转去执行一个子程序(函数)或处理中断时,硬件会自动在这个"书签"(R14)上记录下当前的位置 。等子程序或中断处理完,CPU就能根据这个书签准确地翻回原来的那页继续执行。

在函数调用时(BL指令)

当你在C语言中调用一个函数 func() 时,编译器会将其编译为一条 BL funcBLX func 指令。CPU执行这条指令时,会同时完成两件事

  1. 跳转 :将程序计数器PC(R15)设置为函数 func 的入口地址,开始执行函数。
  2. 链接(保存返回地址)自动将当前指令的下一条指令地址(即返回地址)保存到R14(LR)中
    函数执行完毕后,需要返回。在汇编层面,函数末尾通常是一条 BX LR 指令。这条指令让CPU跳转到LR中保存的地址,完美地返回到调用者下一条指令继续执行。

C语言代码

c 复制代码
// C 代码
int main() {
    func();    // 调用函数
    a = a + 1; // 这是函数调用后需要返回的地址
}

void func() {
    // 做一些事情
    return;    // 返回
}

汇编逻辑(非实际编译结果,为示意)

assembly 复制代码
main:
    ...
    BL func      ; 1. 跳转到func,同时硬件自动将下一条指令地址存入LR
    ADD a, a, #1 ; 3. 函数返回后,从这里继续执行

func:
    ...          ; 2. 执行函数体
    BX LR        ;   通过LR中保存的地址返回

当发生中断(如定时器触发)或异常(如系统调用)时,CPU进入处理流程。此时,硬件的行为与函数调用有重要区别

  1. 硬件自动将返回地址(及一些状态)保存到当前使用的堆栈中
  2. 同时,硬件将R14(LR)的值更新为一个特殊的、有含义的代码(EXC_RETURN) ,而不是简单的返回地址。
    这个特殊的 EXC_RETURN 值(如 0xFFFFFFF9, 0xFFFFFFFD 等)非常重要,它告诉CPU:
  • 返回时应该使用主堆栈指针(MSP) 还是进程堆栈指针(PSP)
  • 返回后应回到Thread模式 还是Handler模式
  • 返回后应使用Thumb指令集 (Cortex-M全系使用Thumb)。
    因此,在中断服务程序结束时,你通常执行一条 BX LR 指令。此时CPU读取到的LR是 EXC_RETURN,这个特殊值会触发CPU硬件自动执行从堆栈恢复上下文并返回 的完整流程。这就是为什么中断服务程序不能像普通函数那样直接用 BX LR 返回,因为在进入中断时,硬件已经自动处理了保存。

总结

场景 R14 (LR) 的角色 关键点
函数调用 (BL) 返回地址的暂存器 硬件自动保存返回地址。函数通过 BX LR 返回。
中断/异常进入 接收 EXC_RETURN 值 硬件自动更新LR为一个特殊值,用于指导返回流程。
中断/异常返回 提供 EXC_RETURN 执行 BX LR 时,该特殊值触发硬件自动恢复现场并返回。
嵌套调用 需要手动保存 如果一个函数(或中断)内部需要再调用其他函数,它必须先将当前的LR值压入堆栈(PUSH {LR}),否则原始的返回地址会丢失。

我们可以使用比喻来理解:

想象你正在读一本很厚的书(这本书就是你的主程序)。

  1. 书签登场 :你有一个专用的、智能的书签 。这个书签就是 LR 。它只有一个任务:帮你记得"读到哪了"。

场景一:主动去查资料(这就像 函数调用)

你读着读着,看到一个词不懂,书里写着"详情见附录X "。你想去查一下。

这时,你的完整操作流程是:

  1. 放书签 :你很自然地把当前这页(比如第100页)折个角,或者把专用书签放在这里 。这样你就绝对不会忘记待会儿要回来接着读。
    • 这就是硬件在执行 BL(跳转并链接)指令时的行为:它自动把"下一条指令的地址"(第100页)这个"返回地址"保存到 LR 里。
  2. 跳转 :你翻到附录X(这就是子函数)开始查阅。
  3. 查阅完毕:你看懂了,合上附录。
  4. 返回 :你凭借书签(LR)的记录,准确地翻回第100页 ,继续往下读。
    • 这就是函数末尾执行 BX LR 指令的行为:跳转到 LR 里保存的地址,完美返回。
      这个场景的关键是你自己主动 去查资料,所以你记得放书签这个动作。流程很直观。

场景二:被紧急电话打断(这就像 中断或异常)

你正在第100页读得入神,突然手机响了 (一个硬件中断产生了)!这个电话你必须接。

这时,情况就复杂了:

  1. 紧急打断:电话铃声是最高优先级,你必须立刻、马上接,来不及多想。
  2. 大脑的自动反应 :在接电话的瞬间 ,你的大脑(CPU硬件 )会下意识地、自动地 做一件事:它飞速地瞥一眼当前的书页(程序状态),然后把这个"快照"记在桌面的便签条上
    • 这就是中断发生时,硬件自动将xPSR, PC, LR, R12, R3-R0等一系列寄存器(即"上下文")压入堆栈的过程。这个"桌面便签条"就是内存里的堆栈。
  3. 接电话 :然后你开始全神贯注地打电话(执行中断服务程序)。
  4. 赋予新使命的书签 :在开始打电话的瞬间,有人(硬件)递给你一个新的、特别的纸条 ,上面没写页码,而是写了一串像"#恢复模式# "这样的暗号。这个纸条就被放在了你手中的"书签"(LR)位置上。
    • 这个"暗号纸条"就是 EXC_RETURN 值。此时,LR 不再是一个简单的地址,而是一个告诉CPU如何恢复现场的"指令码"。
  5. 电话打完:电话说完了,你该回去看书了。
  6. 神奇的返回 :你看了一眼手中书签位上的"暗号纸条"(EXC_RETURN),说了一句:"按暗号恢复! "(执行 BX LR)。
    • 此时,魔法发生了:你的大脑(硬件)看到这个暗号,就自动去桌面找到刚才那张便签条,按照上面的"快照"瞬间恢复了所有状态------你的视线焦点、书本的角度、读到哪一行......全部回到接电话前的那一刻。
    • 无缝衔接 地继续从第100页的那一行读下去,仿佛电话从未打断过你。
      这个场景的关键是你是被动被打断 。保存现场(记便签条)和恢复现场(按便签条恢复)这两个最复杂、最不能出错的工作,由硬件自动完成 了。LR在这里的角色从一个"记录员"变成了一个"指挥员"(持有EXC_RETURN)。

两个场景拼起来

正在查资料(场景一,处于某个函数中),突然电话响了(场景二,中断发生)。

  1. 你查资料前,已经用书签(LR)标记了主程序的位置(比如第100页)。
  2. 在查资料(函数执行)中途,电话来了。
  3. 硬件自动把"当前状态"(包括你正在查资料的这个函数自己的返回地址,它当时正放在LR里!)都保存到"桌面便签条"(堆栈)上。
  4. 接完电话,硬件根据"暗号纸条"(EXC_RETURN)自动恢复。你发现自己又回到了正在查资料的那个函数中间,LR里也神奇地恢复成了当初查资料时保存的"第100页"地址。
  5. 你查完资料,最后用书签(LR)准确地翻回第100页
    如果没有硬件自动保存/恢复,电话一响,你就会把"第100页"这个书签弄丢,永远回不到主程序了。

示意代码:

assembly 复制代码
MyFunction:
    PUSH {R4, R5, LR}  ; 1. 准备查资料/处理事情。太重要了,先把"第100页"书签(LR)
                       ;    和其他重要笔记(R4,R5)一起,夹到桌面的文件堆(堆栈)里备份!
                       ;    这样即使被电话打断也不怕丢。

    ...                 ; 2. 在这里安全地查资料(执行函数),或处理事情。
                       ;    此时甚至可以再调用其他函数(相当于在附录里又看到一个词,去查另一个附录)。

    POP {R4, R5, PC}   ; 3. 事情做完了。从桌面文件堆里,把当初备份的书签和笔记拿回来。
                       ;    注意:这里直接把"书签"(LR)弹出给了"现在要读哪页"(PC)!
                       ;    所以,弹回书签的同时,也就完成了"翻到那一页"的动作。完美返回。

总结
链接寄存器LR,就是CPU的"智能书签":

  • 主动跳转 (函数调用)时,它是一个记录员,记下回来的路。
  • 被动打断 (中断)时,它是一个指挥官 ,手持恢复现场的"魔法指令"(EXC_RETURN)。
  • 正因为有硬件用这个书签玩出的自动保存/恢复魔法,我们的程序才能在函数调用和中断打断的复杂交织中,永远能找到回家的路,正确无误地运行下去。


在cortex m3 中,什么是程序计数器 R15

我们使用一个比喻来理解:
指挥家的乐谱指针

假设你是一个乐队的指挥,面前有一本很厚的乐谱(这就是你存储在内存里的整个程序)。

  • 乐谱(内存) :每一行都写着一个音符指令(一条机器指令)。每行都有固定的行号(内存地址)。
  • 你的手指(PC/R15) :你用手指着乐谱的某一行手指指到哪一行,乐队(CPU)就立刻演奏(执行)哪一行的音符。
  • 自动翻页(硬件自动递增) :默认情况下,每演奏完当前这一行音符,你的手指就会自动往下移动一行 ,指向下一个要演奏的音符。这就是程序顺序执行的根本原理。
    这涵盖了PC最核心的三个特点:
  1. 指向性:它存的是一个地址(乐谱行号),不是普通数据。
  2. 实时性 :它指向的地址,就是CPU马上要执行的指令地址。
  3. 动态性:它在程序运行中时刻变化。

注意:在在cortex m3 中,程序计数器R15又被称之为PC指针

工作原理

普通情况:顺序执行

这是最常见的情形。CPU像个听话的学生,一行一行地读书。

assembly 复制代码
0x20000000 MOV R0, #1    ; PC 开始指向这里,执行这条指令
0x20000004 ADD R0, R0, #2 ; 上一条执行完,PC自动+4,指向这里并执行
0x20000008 MOV R1, R0    ; PC再+4,指向这里并执行

关键 :在Cortex-M3(使用Thumb-2指令集)中,指令可能是2字节或4字节长。但PC的移动总是以字(4字节)为最小单位 。因此,在顺序执行时,PC的值每次增加 4
主动跳转:函数调用、循环、分支

当程序需要"翻到乐谱的另一页"时,就必须直接修改PC的值。所有跳转指令的本质,都是给PC赋一个新值。

  • 直接跳转
assembly 复制代码
B   Label       ; 无条件跳转。执行这句时,硬件会把Label的地址装入PC。

这就好比指挥说:"停!我们从第50小节(Label)重新开始!" 然后手指直接跳到乐谱的第50小节。

  • 函数调用(与LR完美配合)
assembly 复制代码
BL  MyFunc      ; 这是最经典的操作:
                ; 1. 先把"下一行指令的地址"(PC+4)保存到LR(放书签),
                ; 2. 然后把MyFunc的地址装入PC(跳转)。

在函数内部返回时:

assembly 复制代码
BX  LR          ; 将LR中保存的"返回地址"赋给PC,实现返回。

这是PC与LR协作的典范BL指令在修改PC前,先把PC的"未来值"备份到LR;返回时,再用LR的值恢复PC。
被动跳转:中断与异常

这是由硬件事件(如按键、定时器到点)触发的强制跳转,流程完全由硬件控制:

  1. 硬件自动 将关键的现场信息(包括当前的PC值,即被打断指令的下一条指令地址)压入堆栈。
  2. 硬件自动从一个叫"向量表"的固定位置,查找到中断处理函数的入口地址。
  3. 硬件自动将这个入口地址装入PC,CPU开始执行中断服务程序。
  4. 中断处理完,通过 BX LR(此时LR是特殊的EXC_RETURN值)触发硬件自动 从堆栈恢复PC和其他寄存器,程序从被打断处继续执行。
    在这个过程中,PC的修改和保存是完全自动、不可抗拒的,这保证了系统能及时响应外部事件并正确返回。

注意:

  1. 读取PC的值 :当你用 MOV R0, PC 这样的指令读取PC时,你读到的值不等于 当前这条指令的地址,而是下下条指令的地址 (通常是当前PC值 + 4)。这是因为CPU的流水线结构导致的,需要简单记住这个规则。
  2. 地址对齐 :由于Cortex-M3始终处于Thumb状态(指令至少2字节对齐),PC的最低位(bit[0])在硬件上被固定为0 。给PC赋值时,即使试图写入一个奇数地址(如 0x20000001),硬件也会强制对齐到 0x20000000BXBLX指令利用PC的这个特性来判断指令集状态(在Cortex-M3上,此位必须为1,表示Thumb状态,但硬件会处理)。
  3. 写入PC即跳转 :这是理解PC最关键的一点。任何修改PC寄存器的操作,都会立刻导致程序执行流的改变 。除了专门的跳转指令(B, BL, BX等),像 MOV PC, LRPOP {PC} 这样的操作,同样能实现跳转或返回

总结:
程序计数器PC(R15)是CPU执行指令的"指挥棒"

  • 它指哪,CPU就打哪。它的值就是下一条指令的内存地址。
  • 它的自动递增实现了顺序执行。
  • 对它的直接赋值实现了跳转、调用和返回。
  • 它是所有程序流程控制(顺序、分支、循环、函数、中断)在硬件层面最终的实现者。


在cortex m3 中,特殊功能寄存器

寄存器组 寄存器名称 主要功能与描述
程序状态寄存器 xPSR 记录处理器核心的当前状态 。它是以下三个寄存器的组合视图: • APSR :记录算术运算结果标志(N负, Z零, C进位, V溢出)。 • IPSR :记录当前正在处理的异常/中断编号 。 • EPSR :记录执行状态(如Thumb状态,以及是否在IT指令块内)。
中断屏蔽寄存器 PRIMASK 总中断开关(局部) 。置1后,屏蔽所有可屏蔽异常(仅剩NMI和硬Fault不可屏蔽)。用于保护临界区代码。
FAULTMASK 故障总开关。置1后,屏蔽所有异常(仅剩NMI)。优先级升至-1,用于在严重故障处理中临时屏蔽其他故障。
BASEPRI 优先级阈值开关 。写入一个优先级值(如0x05)后,所有优先级大于等于此值的中断被屏蔽。更精细的中断控制。
控制寄存器 CONTROL 控制处理器的工作模式堆栈选择 。 • CONTROL[0] :0=特权模式,1=用户模式。 • CONTROL[1]:0=使用主堆栈指针MSP,1=在线程模式下使用进程堆栈指针PSP。
中断控制器 NVIC 相关寄存器 嵌套向量中断控制器 的寄存器组,用于使能/禁止中断、设置优先级、查询悬起状态等。它们有内存映射地址,可通过普通存储指令访问。

功能详解

程序状态寄存器 (xPSR)

  • APSR :你的C语言 if (a > b) 这样的条件判断,底层就是通过检查这里的 N、Z、C、V 标志位来实现的。
  • IPSR :在中断服务程序中,你可以读取它来判断是哪个中断源被触发
  • EPSR :通常由编译器管理,用于支持Thumb-2指令集中的 IT(条件执行)指令块。
    中断屏蔽寄存器组 (PRIMASK, FAULTMASK, BASEPRI)
    典型场景
c 复制代码
// 进入临界区前,关闭中断
__disable_irq(); // 内部使用 MSR 指令设置 PRIMASK
critical_code(); // 执行不能被中断的代码
__enable_irq(); // 清除 PRIMASK

三者的区别与选择(关键):

寄存器 屏蔽对象 典型应用场景
PRIMASK 所有可屏蔽中断 保护极短的硬件操作临界区。
FAULTMASK 所有中断(除NMI) 在系统严重错误(Fault)处理中,临时阻止其他Fault,方便调试。
BASEPRI 优先级低于等于设定值的中断 实现更优雅的临界区,允许高优先级紧急中断仍能响应。
控制寄存器 (CONTROL)
这是处理器的"身份和装备切换开关"。
  • 模式切换 (CONTROL[0]):操作系统内核运行在特权模式 (可以访问所有资源),而用户应用程序运行在受限制的用户模式。通过修改此位,RTOS可以实现任务隔离。
  • 堆栈指针选择 (CONTROL[1]):这是实现双堆栈机制 的关键。操作系统内核和异常处理使用主堆栈MSP ,而各个用户任务使用自己独立的进程堆栈PSP 。任务切换时,只需更新PSP的值。
    • MSP 在复位后默认使用,服务于系统和异常。
    • PSP 用于普通的用户线程,由RTOS管理。
      NVIC寄存器组
      这是处理器的"中断调度中心 "。虽然它位于系统内存地址空间(如 0xE000E100 开始),但功能上属于核心中断管理。你可以通过C语言指针直接访问它们来配置中断。
  • 主要功能
    • 使能/禁止某个特定中断(NVIC_ISER, NVIC_ICER)。
    • 设置中断的优先级(NVIC_IPRx)。
    • 查询中断是否处于悬起状态(NVIC_ISPR, NVIC_ICPR)。

总结

  1. 特殊性 :它们没有内存地址 ,必须用 MRS R0, PSR(读)和 MSR CONTROL, R0(写)指令访问。
  2. 分工明确
    • xPSR 负责 "看状态"
    • 中断屏蔽寄存器 负责 "管开关"
    • CONTROL 负责 "定模式"
    • NVIC 负责 "调中断"
  3. 实际编程 :在实际嵌入式开发中(如使用STM32的HAL库或ARM的CMSIS),通常会使用封装好的函数来操作它们,例如 __get_PRIMASK()__set_CONTROL(0)等,这比直接写汇编更安全、可读性更好。


在cortex m3 中,什么是xPSR

可以把它想象成处理器核心的 "综合仪表盘""飞行黑匣子" ,它实时记录并反映了 CPU 在任意时刻的 健康状况、工作模式和执行现场

xPSR 的全称是 "组合程序状态寄存器" 。关键在于,它不是单一寄存器,而是将三个具有不同功能的子寄存器(APSR, IPSR, EPSR )在逻辑上合并为一个32位的视图,方便同时查看。但它们也可以通过 MRS/MSR 指令单独访问。

APSR (应用程序状态寄存器) - "条件标志仪":

这是你编程时最常间接接触 的部分。当你写 if (a > b)while (i != 0) 时,编译器生成的比较和测试指令会设置APSR的标志位,条件跳转指令则读取这些位来决定是否跳转。

  • N (负标志,第31位) : 上次运算结果为负时置1。 (result[31] == 1)
  • Z (零标志,第30位): 上次运算结果为零时置1。
  • C (进位/借位标志,第29位): 无符号数运算产生进位(加法)或借位(减法)时置1。也用于移位操作。
  • V (溢出标志,第28位) : 有符号数运算发生溢出时置1。
    示例场景:
c 复制代码
int a = 10, b = 20;
if (a < b) { // 编译器会生成 CMP 指令比较 a 和 b
    // 因为 a - b 产生负结果且无溢出,硬件会设置 N=1, V=0
    // 对于有符号数小于判断,条件满足当且仅当 N != V
    // 因此这里会跳转进入此代码块
}

IPSR (中断程序状态寄存器) - "中断源识别码":

这个寄存器告诉你处理器当前正在服务谁 。它存放的是一个异常编号

  • 关键编号举例 :
    • 0 : 表示处理器处于线程模式(执行普通程序)。
    • 1-15 : 系统异常(如 1: 复位, 2: NMI, 3: 硬Fault, 4: 内存管理Fault 等)。
    • 16及以上: 外部中断(IRQ)。例如,编号16对应IRQ0。
  • 应用价值: 在异常或中断服务程序中,你可以读取IPSR来确认自己因何被调用。在复杂的Fault处理函数中,这常用于诊断错误来源。

EPSR (执行程序状态寄存器) - "执行现场记录仪":

这个寄存器由硬件自动维护,记录指令执行的上下文状态。绝大多数位是只读的,编程时通常不直接写入。

  • T位 (第24位) : 必须为 1 。表示处理器处于 Thumb状态。Cortex-M3只支持Thumb指令集,如果此位被意外清除,将触发异常。
  • ICI/IT位 (第26-25位,第15-10位) : 这是支持 Thumb-2条件执行(IT指令)中断继续执行 的关键。
    • 当使用 IT (If-Then) 指令块时,IT位会记录后续指令的条件执行状态。
    • 当一个中断打断了正在执行的 IT 块或多寄存器加载/存储指令时,ICI 位会保存"现场",以便中断返回后能从中断点恢复执行,而不是重新开始。
  • 异常返回标志 : EPSR与前面提到的特殊返回码 EXC_RETURN 紧密协作,确保从异常正确返回。

这里的EPSR (执行程序状态寄存器)可能比较抽象,我们使用一个比喻来理解

EPSR 是处理器的 "即时存档/读档"系统

想象你正在玩一个不能随时存档的老式游戏(比如《超级马里奥》),但这个游戏机有一个神奇功能 :当你被强制打断时(比如接电话) ,游戏机会自动、精确地为你创建一个"即时存档" ,记下的不是关卡开始,而是你被打断那一帧的所有状态 :马里奥是正在空中跳跃的第几帧、乌龟在什么位置、金币是否已吃...
这个"自动即时存档系统",就是 EPSR 的核心功能。

场景一:存档长指令------我正在搬一大堆东西,别让我重来!

想象你有一条指令 LDMIA R0!, {R1-R7},它的意思是"从 R0 指向的地址开始,连续装载 7 个数据到 R1 到 R7 寄存器"。这是一个"长"操作,需要多个时钟周期。

  1. 正常情况:CPU 默默地一个接一个搬,直到搬完 7 个。
  2. 中断打断 :就在搬到第 3 个数据(刚装完 R3)时,一个高优先级中断来了!
  3. 致命问题 :如果不做任何处理,CPU 会立刻跳去处理中断。等中断返回后,CPU 会从这条 LDMIA 指令重新开始执行 。结果就是:R1, R2, R3 会被重新装载一遍 ,数据完全错误!
    EPSR 的 ICI(中断可继续指令)位 出场了:
  • 在中断发生的瞬间,硬件不仅自动保存了 R0-R3, R12, LR, PC, xPSR 到堆栈,还自动将当前搬到的"进度"("下一个该搬 R4 了")记录在了 EPSR 的 ICI 位域中
  • 同时,硬件把被打断的 LDMIA 指令本身,替换成一条等效的、可以在中断返回后接着执行的"继续指令"
  • 中断处理完毕返回后,硬件根据堆栈里保存的 xPSR(包含 EPSR 的 ICI 状态)自动恢复现场 ,并看到那条"继续指令"。于是 CPU 不是从头开始,而是精准地从装载 R4 开始,完成剩下的工作。
  • 对你来说,整个过程完全透明 ,就像中断从未发生过一样,长指令原子性地完成了。
    这就是 EPSR 的 ICI 功能:它让那些需要多个周期的多寄存器加载/存储指令可以被中断,且能正确恢复,保证了指令的原子性。

场景二:存档条件块------我正在执行一个带条件的连环动作!

Thumb-2 指令集有一个强大功能:IT (If-Then) 指令块。它让后续最多 4 条指令可以条件执行。

assembly 复制代码
CMP R0, R1      ; 比较 R0 和 R1
ITT GT          ; If Then Then (如果大于,则...则...)
MOVGT R2, #1    ; 条件指令1:如果大于,R2=1
MOVGT R3, #2    ; 条件指令2:如果大于,R3=2

这个 IT GT 和后面两条 MOVGT 是一个逻辑块

  1. 正常情况 :如果 R0 > R1,CPU 会连续执行两条 MOVGT
  2. 中断打断 :如果在执行完第一条 MOVGT 之后,第二条 MOVGT 之前,中断来了!
  3. 关键问题 :中断返回后,CPU 应该执行第二条 MOVGT,还是它之后的指令?
    EPSR 的 IT(IT 块状态)位 出场了:
  • 在中断发生时,硬件同样在 EPSR 的 IT 位域中,自动记下了"我们正处在一个 IT 块内,并且已经执行了其中的第几条指令"这个状态
  • 中断返回后,硬件恢复 xPSR,CPU 通过查看 EPSR 的 IT 位,就能精确知道自己应该回到 IT 块的哪个位置继续执行 ,而不会错误地跳出块去执行无关指令。
    这就是 EPSR 的 IT 功能:它保证了条件执行指令块的完整性,即使被中断打断,逻辑也不会错乱。

EPSR总结

功能部件 作用 解决的痛点
T 位 Thumb 状态标志。必须为1。这是 Cortex-M 系列的基础。 确保处理器永远执行正确的指令集。
ICI 位域 中断可继续指令状态 。用于 LDM/STM 等多周期指令。 防止长指令被中断打断后,恢复执行时发生重复操作或数据错误 ,保证指令的原子性
IT 位域 IT 条件执行块状态 。用于 IT 指令块。 防止条件指令块被中断打断后,恢复执行时逻辑错位 ,保证条件执行的完整性
其他位 用于表示处理器是否处于可恢复的异常状态等。 协助硬件进行更精细的异常管理和恢复。
简单来说,EPSR 是处理器的 "现场冻结器"
  • 意外 (中断)打断 一个过程性操作 (长指令或条件块)时,EPSR 负责冻结"进度条" (ICI 位)或冻结"步骤编号"(IT 位)。
  • 当中断结束,它负责精确解冻 ,让被冻结的操作无缝衔接,仿佛时间从未流逝。

另一个比喻:

如果把 CPU 执行指令比作你看一本书,那么:

  • PC 是你正在读的那一行字
  • LR 是你主动夹的书签(准备返回哪一页)。
  • EPSR 则是当你被迫合上书时,大脑自动记住的 :"我正看到这一行的第几个词,这句话的语法结构是怎样的",以便再次打开时能一字不差地接上

c语言中的xPSR交互:

  • 在C代码中: 你通常不直接操作它,但你的所有条件逻辑都依赖它。编译器帮你处理了一切。
  • 在汇编代码中: 你可以显式地检查标志位。
assembly 复制代码
CMP R0, R1      ; 比较 R0 和 R1,结果影响 APSR (N, Z, C, V)
BEQ Label       ; 如果 Z==1(相等),则跳转到 Label
IT GT           ; If-Then 块:如果 N==V 且 Z==0(大于),则...
ADDGT R2, R2, #1; ...条件执行此加法指令
  • 通过MRS/MSR指令访问:
assembly 复制代码
MRS R0, APSR    ; 只读取 APSR 部分到 R0
MRS R0, IPSR    ; 只读取 IPSR 部分到 R0
MRS R0, EPSR    ; 尝试读取 EPSR(但某些位可能读为0)
MRS R0, PSR     ; 读取完整的组合 xPSR
MSR APSR_nzcvq, R0 ; 将 R0 的值写入 APSR 的标志位(用于手动修改标志)

调试中的xPSR:

当程序崩溃或行为异常时,调试器会捕获并显示此刻的 xPSR 值,这提供了第一现场线索

  • 查看 APSR 可以知道崩溃前最后一次运算的结果状态。
  • 查看 IPSR 可以立刻知道是什么异常导致了崩溃(例如,是4号"内存管理Fault"还是6号"用法Fault")。
  • 查看 EPSR 可以知道崩溃时是否处于 IT 块中,或者指令是否对齐。

总结:

部分 核心职能 对程序员的意义 是否可写
APSR 条件判断 实现 if, for, while 等流程控制。 (可写标志位)
IPSR 异常识别 判断当前正在处理哪个中断或异常。 (由硬件自动更新)
EPSR 执行现场保持 支持条件执行(IT)和中断后恢复现场。 几乎只读

简单来说,xPSR是CPU的"状态灵魂"

  • APSR 是它的 "决策依据"(根据条件做判断)。
  • IPSR 是它的 "工作证"(标明当前身份)。
  • EPSR 是它的 "记忆锚点"(保证工作不被意外打断而丢失进度)。


在cortex m3 中,什么是中断屏蔽寄存器组(PRIMASK, FAULTMASK,以及 BASEPRI)

Cortex-M3 中断屏蔽寄存器组,包括 PRIMASK、FAULTMASK 和 BASEPRI。它们的作用是控制处理器的中断屏蔽(或称为异常屏蔽)行为,从而影响中断和异常的处理。

关键概念

  • 屏蔽 = 阻止中断/异常的处理
  • 优先级数字越小,优先级越高(如优先级 0 最高,255 最低)
  • NMI(不可屏蔽中断)永远不能被屏蔽
寄存器 宽度 作用域 可屏蔽的内容 典型用途
PRIMASK 1位 屏蔽所有可屏蔽的异常 除 NMI 和 HardFault 外的所有异常 短临界区保护
FAULTMASK 1位 屏蔽几乎所有异常 除 NMI 外的所有异常(包括 HardFault) Fault 处理、极短关键操作
BASEPRI 8位 基于优先级屏蔽 优先级低于指定值的所有异常 可配置的灵活屏蔽

详细说明:

  • PRIMASK(优先级屏蔽寄存器)
    • 这是一个1位的寄存器(但占用一个完整的32位寄存器空间,只有最低位有效)。
    • 当设置为1时,它会屏蔽所有可配置优先级的中断(即除了NMI和硬Fault以外的所有异常)。换句话说,它把当前优先级提升到0(可编程的最高优先级)。
    • 通常用于临界区保护,防止被中断打断。
  • 典型使用场景:
c 复制代码
// 进入临界区(保护极短的关键操作)
__disable_irq();        // 汇编:CPSID I(设置 PRIMASK=1)
// 这里不会被普通中断打断
critical_operation();
__enable_irq();         // 汇编:CPSIE I(清除 PRIMASK=0)
  • FAULTMASK(故障屏蔽寄存器)
    • 同样是一个1位的寄存器。
    • 当设置为1时,它会屏蔽所有优先级小于等于硬Fault的中断,即屏蔽所有异常(包括NMI和硬Fault以外的异常,但硬Fault除外)。实际上,它会把当前优先级提升到-1(即硬Fault的优先级)。
    • 通常用于故障处理代码中,防止其他异常干扰,尤其是在从硬Fault中恢复时。
  • 重要注意事项
    • 特权级限制:FAULTMASK 只能在 Handler 模式(异常处理程序中)使用
    • 自动清除:从异常返回时,硬件自动清除 FAULTMASK
    • 慎用:屏蔽 HardFault 可能导致系统无法响应严重错误
  • 典型使用场景:
c 复制代码
// 在 Fault 处理程序中临时屏蔽更低优先级的 Fault
void HardFault_Handler(void) {
    // 临时屏蔽其他 Fault,专心处理当前的 HardFault
    __set_FAULTMASK(1);
    
    // 诊断和恢复操作...
    
    // 恢复(通常在返回前自动清除)
    __set_FAULTMASK(0);
}
  • BASEPRI(基本优先级屏蔽寄存器)
    • 这是一个8位的寄存器,用于根据优先级来屏蔽中断。它指定了一个优先级阈值,所有优先级号(注意:优先级号越大,优先级越低)大于等于这个值的中断都会被屏蔽。
    • 例如,如果 BASEPRI 设置为 0x40,那么所有优先级值在 0x40 到 0xFF(最低优先级)的中断都会被屏蔽,而优先级值更高(即优先级号更小,如 0x30)的中断则不会被屏蔽。
    • 当 BASEPRI 设置为0时,则不屏蔽任何中断(相当于不起作用)。
  • 屏蔽规则
BASEPRI 值 屏蔽的优先级范围 说明
0 不屏蔽任何中断
0x40 0x40-0xFF 屏蔽优先级 64-255(较低优先级)
0x80 0x80-0xFF 屏蔽优先级 128-255(低优先级)
0xC0 0xC0-0xFF 屏蔽优先级 192-255(最低优先级)
0xF0 0xF0-0xFF 仅屏蔽优先级 240-255
  • 典型使用场景:
c 复制代码
// 保护中优先级临界区,允许高优先级中断
#define CRITICAL_PRIORITY 0x60

// 进入临界区:屏蔽优先级 >= 0x60 的中断
uint32_t old_basepri = __get_BASEPRI();
__set_BASEPRI(CRITICAL_PRIORITY << (8 - __NVIC_PRIO_BITS));

// 这里不会被优先级低于 0x60 的中断打断
// 但优先级高于 0x60 的中断仍可响应
critical_section();

// 恢复原来的屏蔽设置
__set_BASEPRI(old_basepri);

总结一下,这三个寄存器都是用来屏蔽中断(异常)的,但屏蔽的力度和条件不同:

  • PRIMASK:屏蔽除NMI和硬Fault外的所有中断。
  • FAULTMASK:屏蔽除NMI外的所有中断(包括硬Fault,但注意:FAULTMASK本身是在硬Fault中使用的,所以它不会屏蔽当前的硬Fault,但会屏蔽其他异常)。
  • BASEPRI:根据优先级阈值屏蔽,只有优先级号大于等于设定值的中断才会被屏蔽。
  • FAULTMASK > PRIMASK > BASEPRI
特性 PRIMASK FAULTMASK BASEPRI
屏蔽强度 中等 最强 可配置
屏蔽对象 除 NMI、HardFault 外的所有异常 除 NMI 外的所有异常 基于优先级屏蔽
使用模式 Thread/Handler 模式 主要在 Handler 模式 Thread/Handler 模式
恢复方式 手动清除 异常返回时自动清除 手动设置/清除
优先级效果 临时提升到 0 临时提升到 -1 临时提升到设定值

注意:在编程中,我们通常使用特定的汇编指令来操作这些寄存器,但Cortex-M3也提供了CMSIS-Core函数 ,可以更方便地操作。

例如,在CMSIS中:

  • 设置PRIMASK:void __set_PRIMASK(uint32_t priMask)
  • 读取PRIMASK:uint32_t __get_PRIMASK(void)
  • 类似地,也有__set_FAULTMASK__get_FAULTMASK__set_BASEPRI__get_BASEPRI

与嵌套向量中断控制器(NVIC)的关系

这些屏蔽寄存器是 NVIC 的一部分 ,但它们不同于 NVIC 中的中断使能/除能寄存器:

  • NVIC 中断使能/除能寄存器 :控制单个中断源的使能
  • PRIMASK/FAULTMASK/BASEPRI全局性地屏蔽多个中断,基于优先级

如果觉得抽象我们可以使用比喻来来理解:
比喻:医院的"分诊屏蔽系统"

想象一个医院急诊室,病人(中断/异常)不断涌来,医护人员(CPU)需要处理。医院有三种"分诊屏蔽"策略:

BASEPRI - "病情阈值分诊员"

  • 角色:一个拿着标尺的分诊护士。
  • 工作:她设置一个"病情严重程度阈值"(比如"只接收5级以上危重病人")。
  • 效果
    • 病情等级高于5级 (优先级数字更小)的病人→ 立即送抢救室(中断被响应)。
    • 病情等级5级及以下 (优先级数字≥阈值)的病人→ 请在等候区等待(中断被屏蔽)。
  • 特点灵活可调。可以根据急诊室忙碌程度,随时调整这个阈值。

PRIMASK - "暂时停诊牌"

  • 角色:急诊室主任。
  • 工作 :当需要进行一台极度精细、不能被打扰的手术(临界区代码)时,他在门口挂出 "暂时停诊,仅处理最危重病例" 的牌子。
  • 效果
    • 普通病人 (所有可屏蔽中断)→ 一律等待
    • 心脏骤停病人(NMI)院内重大医疗事故(HardFault)仍然可以破门而入
  • 特点 :简单粗暴,全面屏蔽,但为最极端情况留了后门。

FAULTMASK - "灾难应急模式"

  • 角色:医院院长。
  • 工作 :当医院本身发生火灾、地震等灾难性事件(CPU发生严重Fault)时,他启动全院灾难应急模式。
  • 效果
    • 所有普通诊疗暂停,包括处理医疗事故的团队(HardFault也被屏蔽)!
    • 全院资源只服务于一个目标:应对眼前的灾难(当前Fault处理)。
    • 只有 "核爆警报"(NMI)才能超越这个模式。
  • 特点最高强度屏蔽,是系统最后的"安全屋",只在自身陷入危机时使用。


在cortex m3 中,控制寄存器(CONTROL)

CONTROL寄存器 是Cortex-M3中一个关键的系统控制寄存器 ,它决定了处理器在Thread模式 下的特权级堆栈指针的选择
核心功能:

名称 功能 默认值 备注
0 nPRIV 特权级别控制 0 0=特权模式,1=用户模式
1 SPSEL 堆栈指针选择 0 0=MSP,1=PSP
2 FPCA 浮点上下文激活 0 (Cortex-M4F特有,M3中保留)
31:3 - 保留 0 必须写0
详细解释
nPRIV 位(位0):特权级别控制
这个位决定了处理器在Thread模式下的访问权限。
nPRIV值 模式 访问权限
0 特权模式 可以访问所有系统资源和特殊寄存器
1 用户模式 访问受限,不能直接访问某些特殊寄存器

关键点

  • Handler模式 (异常处理中)总是特权模式,无视nPRIV位
  • 用户模式切换到特权模式的唯一方法是触发异常(如SVC调用)
  • 特权模式切换到用户模式是单向操作(除了异常进入)
c 复制代码
// C代码示例:切换到用户模式
void enter_user_mode(void) {
    // 必须在特权模式下执行
    __set_CONTROL(__get_CONTROL() | 0x01);  // 设置nPRIV=1
    
    // 必须执行ISB屏障指令,确保上下文同步
    __ISB();
    
    // 现在处于用户模式
    // 不能再直接访问某些特殊寄存器
}

SPSEL 位(位1):堆栈指针选择

这个位决定了在Thread模式下使用哪个堆栈指针。

SPSEL值 使用的堆栈指针 说明
0 主堆栈指针(MSP) 默认值,系统和异常处理使用
1 进程堆栈指针(PSP) 应用程序任务使用
关键点
  • Handler模式 (异常处理中)总是使用MSP,无视SPSEL位
  • 这种双堆栈设计是RTOS实现任务隔离的基础
assembly 复制代码
; 切换到使用PSP
MRS R0, CONTROL
ORR R0, R0, #0x02    ; 设置SPSEL=1
MSR CONTROL, R0
ISB                  ; 指令同步屏障

处理器模式与CONTROL寄存器的关系
两种执行模式

  1. Thread模式
    • 执行普通应用程序代码
    • CONTROL寄存器完全有效,决定特权级和堆栈指针
  2. Handler模式
    • 执行异常/中断处理程序
    • 总是特权模式 ,总是使用MSP
    • CONTROL寄存器的设置被忽略
txt 复制代码
Thread模式 (用户模式, 使用PSP)
    ↓ 中断发生
Handler模式 (特权模式, 使用MSP) ← 自动切换
    ↓ 异常返回
Thread模式 (恢复原来的nPRIV和SPSEL) ← 根据EXC_RETURN恢复

使用比喻来理解:

可以把CONTROL寄存器看作是处理器的 "身份卡和工作证"

  1. nPRIV位 :身份卡的颜色
    • 红卡(特权模式):可以进入所有办公室,操作所有设备
    • 蓝卡(用户模式):只能进入公共区域,操作有限设备
  2. SPSEL位 :工作证的类型
    • MSP工作证:使用公司的主仓库(主堆栈)
    • PSP工作证:使用个人储物柜(进程堆栈)
  3. 模式决定权限
    • 上班时间(Thread模式):必须出示工作证,按身份卡权限工作
    • 紧急会议(Handler模式) :所有人临时升级为红卡,统一使用主仓库
      这种机制使得:
  • 系统内核可以拥有完全控制权(红卡+MSP)
  • 用户任务被限制在安全沙箱中(蓝卡+PSP)
  • 中断处理可以无条件获得最高权限

实际中的一般设计

  1. 操作系统内核:使用特权模式 + MSP
  2. 用户任务:使用用户模式 + PSP(每个任务独立的PSP)
  3. 异常处理:总是特权模式 + MSP(硬件保证)
  4. 模式切换:总是通过异常机制(SVC调用)
  5. 修改CONTROL后:总是执行ISB指令


在cortex m3 中,什么是复位序列

复位序列 是Cortex-M3处理器上电或复位后,硬件自动执行的一系列强制性、不可中断的初始化操作。这是芯片从"无意识"的物理状态,转变为可执行你编写的程序的"智能"状态的关键启动过程。

你可以把它理解为计算机的 "开机自检+引导程序" 阶段。就像电脑开机时,BIOS会检查硬件、加载操作系统一样,Cortex-M3的复位序列为运行C语言main()函数做好了所有底层准备。

详细步骤(7步流程):
第一步:获取主堆栈指针初始值

  • 动作 :硬件从内存地址 0x00000000(向量表的第一个表项)读取一个32位的值。
  • 作用 :将这个值作为主堆栈指针(MSP) 的初始值。堆栈是函数调用和中断响应的基础,这是C语言运行时环境能够工作的首要前提
    第二步:获取程序计数器初始值
  • 动作 :硬件从内存地址 0x00000004(向量表的第二个表项)读取一个32位的值。
  • 作用 :将这个值作为程序计数器(PC) 的初始值,即处理器要执行的第一条指令的地址。这通常指向 Reset_Handler 函数
    第三步:初始化核心寄存器
  • 动作 :硬件将通用寄存器 R0-R12、链接寄存器 LR (R14)、程序状态寄存器 xPSR 设置为确定的初始值(通常为0)。这一步确保了软件从一个干净、一致的寄存器环境 开始运行。
    第四步:配置系统寄存器
  • 动作 :硬件将控制寄存器 CONTROL 初始化为0。这意味着:
    • CONTROL[0] = 0:处理器启动后处于特权模式(可以访问所有资源)。
    • CONTROL[1] = 0:默认使用主堆栈指针MSP
  • 动作 :同时,将配置与故障状态寄存器 CFSR 等清空。
    第五步:设置链接寄存器
  • 动作 :硬件将链接寄存器 LR (R14) 设置为一个特殊的初始值 0xFFFFFFFF。这个值不是有效的返回地址 ,它的作用是:如果程序意外试图通过 BX LR 从复位序列返回,会触发一个故障,便于调试发现错误。
    第六步:使能必要的系统异常
  • 动作 :硬件自动使能 一些关键的系统异常(如 NMI(不可屏蔽中断)HardFault(硬件故障) ),以确保系统在遇到严重错误时有基本的处理能力。
    第七步:跳转执行
  • 动作 :硬件完成上述所有设置后,将PC的值(来自第二步)写入程序计数器,处理器正式跳转到复位处理程序(Reset_Handler)开始执行第一条用户代码

软件视角:

硬件序列结束后,控制权交给了 Reset_Handler。这个函数通常由启动文件(如 startup_stm32f103xe.s)提供,它会继续完成软件初始化:

  1. 初始化数据段 :将存储在Flash中的初始值复制到RAM中(初始化全局变量、静态变量)。
  2. 清零BSS段:将未初始化的全局变量区域(BSS段)清零。
  3. 初始化C库 :为调用 main() 函数准备标准C语言环境。
  4. 跳转到main() :最终,Reset_Handler 调用你的C语言 main() 函数,你的应用程序正式开始运行。

注意:

  • 向量表是起点 :地址 0x00000000 开始的向量表是复位序列的"导航图"。芯片厂商的启动文件会预先定义好这个表。
  • 固定流程:整个序列不可更改、不可中断。你的程序必须适应这个启动方式。
  • 特权模式启动:所有代码初始时都在特权模式下运行,拥有最高权限。操作系统(如果有)可在之后切换到用户模式。
  • 堆栈在先先初始化堆栈(MSP),再跳转执行,这个顺序至关重要。因为函数调用和中断立刻就需要堆栈。
相关推荐
方安乐2 小时前
react笔记之useCallback
前端·笔记·react.js
三伏5222 小时前
stm32f103系列手册IIC笔记2
笔记·stm32·嵌入式硬件
鄭郑2 小时前
【Playwright学习笔记 06】用户视觉定位的方法
笔记·学习
jrlong3 小时前
DataWhale大模型基础与量化微调task4学习笔记(第 1章:参数高效微调_LoRA 方法详解)
笔记·学习
方安乐3 小时前
react笔记之useMemo
前端·笔记·react.js
卡布叻_星星3 小时前
后端笔记之Maven配置以及解决Maven中央仓库没有的依赖
笔记
Jackchenyj3 小时前
基于艾宾浩斯记忆曲线的AI工具实战:ShiflowAI助力高效知识沉淀
人工智能·笔记·信息可视化·智能体
傻小胖3 小时前
5.BTC-实现-北大肖臻老师客堂笔记
笔记·区块链
鄭郑3 小时前
【Playwright学习笔记 07】其它用户视觉定位的方法
笔记·学习