个人学习的笔记,希望帮助的和我一样阅读Cortex-M3权威指南Cn遇到困难的人。
强烈建议先阅读Cortex-M3权威指南Cn第三章在来观看笔记
在cortex m3 中,什么是MPU
在嵌入式处理器(尤其是Cortex-M3这类微控制器)中,MPU指的是内存保护单元 。
它是一种硬件模块,用于控制和保护对内存区域的访问 ,是增强系统鲁棒性、隔离不同任务、防止内存错误访问的关键组件。
你可以把它理解为一个内存的"交通管制员"或"保安"。它不负责将虚拟地址转换为物理地址(那是MMU内存管理单元 的主要工作,常见于高性能应用处理器),而是定义和管理内存区域的访问权限和属性。
MPU的核心功能
- 区域划分: 将系统的内存空间(如Flash、RAM、外设等)划分为数个独立的区域(例如,在Cortex-M3中通常支持8个区域)。
- 规则制定 : 为每个区域配置详细的"规则",包括:
- 起始地址和大小
- 访问权限: 如是否允许读、写、执行。
- 内存属性: 如是否可缓存、是否可缓冲。
- 特权要求: 规定该区域是只能在"特权模式"下访问,还是允许在"用户模式"下也访问。
- 实时监控与拦截 : 当处理器内核(或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架构过程调用标准的惯例:
- R0-R3 :常用于传递函数的前4个参数 ,以及保存函数的返回值(尤其是R0)。
- R4-R11 :通常用作局部变量寄存器 。在函数内部,如果它们的值被修改,调用者希望保存这些值,那么在函数入口需要将它们压入堆栈保存 ,在退出前弹出恢复 。因此,它们也被称为 "被调用者保存寄存器"。
- 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而言,它必须分解成多个触及物理硬件的步骤。编译器会把它翻译成类似这样的汇编指令:
LOAD R1, [address_of_b]:从内存 中b的地址处,把值加载到寄存器R1。(从冰箱拿出黄油)LOAD R2, [address_of_c]:从内存 中c的地址处,把值加载到寄存器R2。(从柜子拿出糖)ADD R0, R1, R2:在寄存器内部 ,将R1和R2的值相加,结果存入寄存器R0。(在碗里混合)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函数
}
编译后可能发生的情况:
main函数将x的值5放入 R0 ,将y的值10放入 R1(准备参数)。main调用add函数。add函数进入后,直接从 R0 拿到第一个参数a(5),从 R1 拿到第二个参数b(10)。add函数在内部进行加法运算,结果可能暂存在 R0。add函数返回,返回值就在 R0(15)中。main函数从 R0 拿到返回值15,存入为变量sum分配的内存或寄存器中(例如 R4)。
总结
理解通用寄存器的关键两点:
- 存在意义 :它们是CPU的高速数据缓存工作区,是程序能以极速运行的根本物理基础之一。
- 使用规则(调用约定):为了让程序模块(函数)能协同工作,业界制定了关于如何使用R0-R12的软件约定。这不是硬件的强制要求,但所有编译器都遵守它,从而保证了程序的正确链接和执行。
在cortex m3 中,R13为什么要有两个堆栈指针(MSP和PSP)
R13(堆栈指针SP)的基本作用
堆栈指针(Stack Pointer, SP) 就像一个图书管理员的手指,始终指向堆栈的"顶部"(最新位置)。堆栈是内存中的一个特殊区域,用于:
- 保存函数调用时的返回地址
- 保存局部变量
- 保存寄存器的值 (在函数调用或中断发生时,只保存8个寄存器(xPSR, PC, LR, R12, R3-R0)
- 传递函数参数 (在某些情况下)
堆栈遵循后进先出(LIFO)原则,就像一摞盘子:最后放上去的盘子(数据)最先被取走。
c
void functionA() {
int x = 10; // 局部变量x被压入堆栈
functionB(); // 返回地址被压入堆栈
// ... // functionB返回后,从堆栈弹出返回地址
} // 函数结束时,x从堆栈中弹出
在Cortex-M3处理器有两种操作模式:线程模式(Thread Mode) 和 处理模式(Handler Mode) 。
线程模式是执行普通代码的模式,处理模式是处理异常(包括中断)的模式。
同时,Cortex-M3支持两个堆栈指针:主堆栈指针(MSP)和进程堆栈指针(PSP)。
处理器在任一时刻只使用其中一个堆栈指针,具体使用哪个由处理器的当前模式和CONTROL寄存器的设置决定。
那么为什么要有两个堆栈指针?
主要是为了将操作系统内核(以及异常处理)的堆栈和用户应用程序的堆栈分开,这样可以在多任务环境中保护系统内核的堆栈不被用户程序破坏,同时也可以使得每个任务拥有自己的堆栈,便于任务切换和上下文保存。
详细说明:
- 主堆栈指针(MSP) :
- 这是系统复位后默认使用的堆栈指针。
- 当处理器处于处理模式(Handler Mode) 时,总是使用MSP。
- 操作系统内核以及异常处理程序(包括中断服务程序)通常使用MSP。这样,当发生异常时,处理器自动切换到MSP,从而确保异常处理程序有一个安全、可靠的堆栈(不会被用户程序破坏)。
- 进程堆栈指针(PSP) :
- 主要用于线程模式下的用户任务。
- 当处理器处于线程模式(Thread Mode),并且CONTROL寄存器的bit[1](SPSEL)设置为1时,使用PSP。
- 在多任务操作系统中,每个任务都有自己的堆栈空间,任务运行时使用PSP指向自己的堆栈。这样,每个任务的堆栈是独立的,一个任务的堆栈溢出不会直接影响其他任务,也不会影响内核。
注意:复位在cortex m3中也属于一种中断
注意:在cortex m3中中断又被称之为异常
- 如何切换 :
- 在异常进入时,处理器自动从线程模式切换到处理模式,并自动将当前使用的堆栈指针(可能是PSP,如果之前线程模式使用的是PSP)切换为MSP。
- 在异常返回时,处理器根据异常返回时加载的CONTROL寄存器值决定返回到线程模式后使用哪个堆栈指针。
- CONTROL寄存器 :
- CONTROL[1](SPSEL):在线程模式下,0表示使用MSP,1表示使用PSP。在处理模式下,此位为0(总是使用MSP)且写入非零值会被忽略。
- CONTROL[0](nPRIV):定义线程模式下的特权级别。0表示特权级,1表示用户级(非特权)。处理模式总是特权级。
- 双堆栈机制的好处 :
- 安全性:内核和异常处理使用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++)。
总结
- 向下生长 :堆栈向内存低地址方向扩展。这是 ARM Cortex-M 系列的固定设计。
- 满栈 :堆栈指针
SP永远指向最后一个被压入的有效数据 。这决定了PUSH是"先减后存",POP是"先取后加"。 - 统一硬件行为:这种模型由硬件固化,所有软件(编译器、操作系统)都必须遵循。它保证了中断处理、函数调用时上下文保存/恢复的可靠性和高效性。
什么是向下生长而不是向上生长?
因为这里"上下"指的是内存地址数值的变化方向,而不是物理空间位置。
想象计算机的内存是一栋从1楼开始向上编号的高楼:
- 低地址 = 低楼层 (如 1楼
0x20001000) - 高地址 = 高楼层 (如 10楼
0x20001024)
现在,堆栈 就像是这栋楼里的一个特殊书柜 。这个书柜有一个固定在天花板上的弹簧托盘。
- 初始状态 :托盘被弹簧顶在高楼层 (比如10楼)。这是书柜的栈底。
- "向下生长" :当你
PUSH放入一本书(数据)时,弹簧被压缩,托盘会带着书一起向低楼层移动 (比如降到9楼)。你继续放书,托盘就继续向更低楼层下降 (8楼、7楼...)。从楼层号来看,书柜(堆栈)使用的空间是向着越来越小的楼层号(低地址)方向扩展的,这就是"向下生长"。 - "向上生长" (对比理解):如果是一个普通的、放在地上的书柜。你从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号到N号。1号格在最左边(低地址),N号格在最右边(高地址)。
- 它有固定编号的格子 ,从
- 盘子 :代表数据。
- 服务员:代表 堆栈指针
SP**。- 你永远站在餐具柜前,用手指着某一个格子。
- 你手指的位置,就是SP的值。
核心规则(硬件逻辑)
餐厅规定,这个餐具柜的使用方式非常严格:
- 初始化(系统启动) :
- 你被要求一开始必须站在最右边的格子(高地址,比如第100号格)前 。这是栈底。
- 此时柜子是空的,但你的手"指向"这个准备接收第一个盘子的位置。
- 存盘子操作
PUSH(发生函数调用或中断) :- 当需要存一个脏盘子(数据入栈)时,你必须遵守以下步骤:
- 先向左移动一步 (向编号更小的格子移动,例如从100号走到99号)。这一步对应
SP = SP - 4(向下生长)。 - 然后把盘子放进你面前这个新格子 。这一步对应
*SP = data。
- 先向左移动一步 (向编号更小的格子移动,例如从100号走到99号)。这一步对应
- 现在,你手指着这个装有盘子的格子(99号) 。这就是"满栈"------指针永远指着最后一个有效物品。
- 当需要存一个脏盘子(数据入栈)时,你必须遵守以下步骤:
- 取盘子操作
POP(函数返回或恢复上下文) :- 当需要取回一个干净盘子(数据出栈)时,步骤相反:
- 从你当前手指的格子里拿出盘子 (例如从99号格)。这一步对应
data = *SP。 - 然后向右移动一步 (向编号更大的格子移动,例如从99号走回100号)。这一步对应
SP = SP + 4。
- 从你当前手指的格子里拿出盘子 (例如从99号格)。这一步对应
- 现在,你手指着下一个可用的"旧"位置(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 func 或 BLX func 指令。CPU执行这条指令时,会同时完成两件事:
- 跳转 :将程序计数器PC(R15)设置为函数
func的入口地址,开始执行函数。 - 链接(保存返回地址) :自动将当前指令的下一条指令地址(即返回地址)保存到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进入处理流程。此时,硬件的行为与函数调用有重要区别:
- 硬件自动将返回地址(及一些状态)保存到当前使用的堆栈中。
- 同时,硬件将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}),否则原始的返回地址会丢失。 |
我们可以使用比喻来理解:
想象你正在读一本很厚的书(这本书就是你的主程序)。
- 书签登场 :你有一个专用的、智能的书签 。这个书签就是 LR 。它只有一个任务:帮你记得"读到哪了"。
场景一:主动去查资料(这就像 函数调用)
你读着读着,看到一个词不懂,书里写着"详情见附录X "。你想去查一下。
这时,你的完整操作流程是:
- 放书签 :你很自然地把当前这页(比如第100页)折个角,或者把专用书签放在这里 。这样你就绝对不会忘记待会儿要回来接着读。
- 这就是硬件在执行
BL(跳转并链接)指令时的行为:它自动把"下一条指令的地址"(第100页)这个"返回地址"保存到 LR 里。
- 这就是硬件在执行
- 跳转 :你翻到附录X(这就是子函数)开始查阅。
- 查阅完毕:你看懂了,合上附录。
- 返回 :你凭借书签(LR)的记录,准确地翻回第100页 ,继续往下读。
- 这就是函数末尾执行
BX LR指令的行为:跳转到 LR 里保存的地址,完美返回。
这个场景的关键是 :你自己主动 去查资料,所以你记得放书签这个动作。流程很直观。
- 这就是函数末尾执行
场景二:被紧急电话打断(这就像 中断或异常)
你正在第100页读得入神,突然手机响了 (一个硬件中断产生了)!这个电话你必须接。
这时,情况就复杂了:
- 紧急打断:电话铃声是最高优先级,你必须立刻、马上接,来不及多想。
- 大脑的自动反应 :在接电话的瞬间 ,你的大脑(CPU硬件 )会下意识地、自动地 做一件事:它飞速地瞥一眼当前的书页(程序状态),然后把这个"快照"记在桌面的便签条上 。
- 这就是中断发生时,硬件自动将xPSR, PC, LR, R12, R3-R0等一系列寄存器(即"上下文")压入堆栈的过程。这个"桌面便签条"就是内存里的堆栈。
- 接电话 :然后你开始全神贯注地打电话(执行中断服务程序)。
- 赋予新使命的书签 :在开始打电话的瞬间,有人(硬件)递给你一个新的、特别的纸条 ,上面没写页码,而是写了一串像"#恢复模式# "这样的暗号。这个纸条就被放在了你手中的"书签"(LR)位置上。
- 这个"暗号纸条"就是
EXC_RETURN值。此时,LR 不再是一个简单的地址,而是一个告诉CPU如何恢复现场的"指令码"。
- 这个"暗号纸条"就是
- 电话打完:电话说完了,你该回去看书了。
- 神奇的返回 :你看了一眼手中书签位上的"暗号纸条"(
EXC_RETURN),说了一句:"按暗号恢复! "(执行BX LR)。- 此时,魔法发生了:你的大脑(硬件)看到这个暗号,就自动去桌面找到刚才那张便签条,按照上面的"快照"瞬间恢复了所有状态------你的视线焦点、书本的角度、读到哪一行......全部回到接电话前的那一刻。
- 你无缝衔接 地继续从第100页的那一行读下去,仿佛电话从未打断过你。
这个场景的关键是 :你是被动被打断 。保存现场(记便签条)和恢复现场(按便签条恢复)这两个最复杂、最不能出错的工作,由硬件自动完成 了。LR在这里的角色从一个"记录员"变成了一个"指挥员"(持有EXC_RETURN)。
两个场景拼起来
正在查资料(场景一,处于某个函数中),突然电话响了(场景二,中断发生)。
- 你查资料前,已经用书签(LR)标记了主程序的位置(比如第100页)。
- 在查资料(函数执行)中途,电话来了。
- 硬件自动把"当前状态"(包括你正在查资料的这个函数自己的返回地址,它当时正放在LR里!)都保存到"桌面便签条"(堆栈)上。
- 接完电话,硬件根据"暗号纸条"(
EXC_RETURN)自动恢复。你发现自己又回到了正在查资料的那个函数中间,LR里也神奇地恢复成了当初查资料时保存的"第100页"地址。 - 你查完资料,最后用书签(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最核心的三个特点:
- 指向性:它存的是一个地址(乐谱行号),不是普通数据。
- 实时性 :它指向的地址,就是CPU马上要执行的指令地址。
- 动态性:它在程序运行中时刻变化。
注意:在在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。
被动跳转:中断与异常
这是由硬件事件(如按键、定时器到点)触发的强制跳转,流程完全由硬件控制:
- 硬件自动 将关键的现场信息(包括当前的PC值,即被打断指令的下一条指令地址)压入堆栈。
- 硬件自动从一个叫"向量表"的固定位置,查找到中断处理函数的入口地址。
- 硬件自动将这个入口地址装入PC,CPU开始执行中断服务程序。
- 中断处理完,通过
BX LR(此时LR是特殊的EXC_RETURN值)触发硬件自动 从堆栈恢复PC和其他寄存器,程序从被打断处继续执行。
在这个过程中,PC的修改和保存是完全自动、不可抗拒的,这保证了系统能及时响应外部事件并正确返回。
注意:
- 读取PC的值 :当你用
MOV R0, PC这样的指令读取PC时,你读到的值不等于 当前这条指令的地址,而是下下条指令的地址 (通常是当前PC值 + 4)。这是因为CPU的流水线结构导致的,需要简单记住这个规则。 - 地址对齐 :由于Cortex-M3始终处于Thumb状态(指令至少2字节对齐),PC的最低位(
bit[0])在硬件上被固定为0 。给PC赋值时,即使试图写入一个奇数地址(如0x20000001),硬件也会强制对齐到0x20000000。BX和BLX指令利用PC的这个特性来判断指令集状态(在Cortex-M3上,此位必须为1,表示Thumb状态,但硬件会处理)。 - 写入PC即跳转 :这是理解PC最关键的一点。任何修改PC寄存器的操作,都会立刻导致程序执行流的改变 。除了专门的跳转指令(
B,BL,BX等),像MOV PC, LR、POP {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)。
- 使能/禁止某个特定中断(
总结
- 特殊性 :它们没有内存地址 ,必须用
MRS R0, PSR(读)和MSR CONTROL, R0(写)指令访问。 - 分工明确 :
- xPSR 负责 "看状态"。
- 中断屏蔽寄存器 负责 "管开关"。
- CONTROL 负责 "定模式"。
- NVIC 负责 "调中断"。
- 实际编程 :在实际嵌入式开发中(如使用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 寄存器"。这是一个"长"操作,需要多个时钟周期。
- 正常情况:CPU 默默地一个接一个搬,直到搬完 7 个。
- 中断打断 :就在搬到第 3 个数据(刚装完 R3)时,一个高优先级中断来了!
- 致命问题 :如果不做任何处理,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 是一个逻辑块。
- 正常情况 :如果 R0 > R1,CPU 会连续执行两条
MOVGT。 - 中断打断 :如果在执行完第一条
MOVGT之后,第二条MOVGT之前,中断来了! - 关键问题 :中断返回后,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寄存器的关系
两种执行模式
- Thread模式 :
- 执行普通应用程序代码
- CONTROL寄存器完全有效,决定特权级和堆栈指针
- Handler模式 :
- 执行异常/中断处理程序
- 总是特权模式 ,总是使用MSP
- CONTROL寄存器的设置被忽略
txt
Thread模式 (用户模式, 使用PSP)
↓ 中断发生
Handler模式 (特权模式, 使用MSP) ← 自动切换
↓ 异常返回
Thread模式 (恢复原来的nPRIV和SPSEL) ← 根据EXC_RETURN恢复
使用比喻来理解:
可以把CONTROL寄存器看作是处理器的 "身份卡和工作证":
- nPRIV位 :身份卡的颜色
- 红卡(特权模式):可以进入所有办公室,操作所有设备
- 蓝卡(用户模式):只能进入公共区域,操作有限设备
- SPSEL位 :工作证的类型
- MSP工作证:使用公司的主仓库(主堆栈)
- PSP工作证:使用个人储物柜(进程堆栈)
- 模式决定权限 :
- 上班时间(Thread模式):必须出示工作证,按身份卡权限工作
- 紧急会议(Handler模式) :所有人临时升级为红卡,统一使用主仓库
这种机制使得:
- 系统内核可以拥有完全控制权(红卡+MSP)
- 用户任务被限制在安全沙箱中(蓝卡+PSP)
- 中断处理可以无条件获得最高权限
实际中的一般设计
- 操作系统内核:使用特权模式 + MSP
- 用户任务:使用用户模式 + PSP(每个任务独立的PSP)
- 异常处理:总是特权模式 + MSP(硬件保证)
- 模式切换:总是通过异常机制(SVC调用)
- 修改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)提供,它会继续完成软件初始化:
- 初始化数据段 :将存储在Flash中的初始值复制到RAM中(初始化全局变量、静态变量)。
- 清零BSS段:将未初始化的全局变量区域(BSS段)清零。
- 初始化C库 :为调用
main()函数准备标准C语言环境。 - 跳转到main() :最终,
Reset_Handler调用你的C语言main()函数,你的应用程序正式开始运行。
注意:
- 向量表是起点 :地址
0x00000000开始的向量表是复位序列的"导航图"。芯片厂商的启动文件会预先定义好这个表。 - 固定流程:整个序列不可更改、不可中断。你的程序必须适应这个启动方式。
- 特权模式启动:所有代码初始时都在特权模式下运行,拥有最高权限。操作系统(如果有)可在之后切换到用户模式。
- 堆栈在先 :先初始化堆栈(MSP),再跳转执行,这个顺序至关重要。因为函数调用和中断立刻就需要堆栈。