ARM Cortex-M 体系结构深度解析——寄存器模型、处理器模式、AAPCS 与异常模型

📅 2026/7/6 15:01:20 👁️ 阅读次数 📝 编程学习
ARM Cortex-M 体系结构深度解析——寄存器模型、处理器模式、AAPCS 与异常模型

一、引言

ARM Cortex-M 系列是当前嵌入式 MCU 市场占有率最高的处理器架构。从 STM32F103 的 Cortex-M3 到 STM32F407 的 Cortex-M4F,再到 GD32、nRF52、RP2040,这些芯片的内核架构高度统一

理解 Cortex-M 体系结构,就是掌握嵌入式的"内功"。本文从以下维度深度解析:

  • ARM 公司商业模式与产品线

  • RISC vs CISC:为什么 ARM 是嵌入式首选?

  • Cortex-M 寄存器模型(16 个关键寄存器)

  • MSP 与 PSP 双栈指针机制

  • AAPCS 调用约定与栈帧分析

  • 异常模型与中断处理

  • 位带操作与存储器映射

  • Cortex-M3 vs M4 vs M0 核心差异


二、ARM 体系结构基础

2.1 ARM 公司的商业模式

ARM 不造芯片,只卖设计方案(IP 授权)。这种模式决定了其生态的丰富性:

ARM 设计内核 → 授权给芯片厂商 → 厂商添加外设 → 生产芯片 ↑ ↑ ARM Cortex-M3 STM32F1 / GD32F103 / APM32 ARM Cortex-M4 STM32F4 / nRF52840 / AT32F4 ARM Cortex-M0 STM32F0 / RP2040 / MM32

2.2 RISC vs CISC 对比

ARM 是典型的 RISC(精简指令集计算机)架构。

特性RISC(ARM)CISC(x86)
指令数量少(精选常用指令)多(几百条复杂指令)
指令长度固定(32位 ARM / 16位 Thumb);Thumb-2 为 16/32 位混合可变(1~15字节)
执行周期大多数 1 个时钟周期复杂指令需多个周期
寻址方式少(Load/Store 架构多(内存可直接运算)
寄存器数量(16~31 个)少(8 个通用寄存器)
功耗
编译器依赖(靠编译器优化)低(硬件完成复杂操作)

面试必问:ARM 是 Load/Store 架构——所有运算只在寄存器之间进行,数据必须先从内存 Load 到寄存器,运算完再 Store 回内存。不能像 x86 那样直接对内存地址做加法。

2.3 Cortex-M/A/R 产品线定位

系列定位架构版本关键特征典型产品
M0/M0+超低功耗 MCUARMv6-M2级流水线,无MPUSTM32F0, RP2040
M3主流 MCUARMv7-M3级流水线,MPU可选STM32F1, GD32F103
M4DSP+FPUARMv7E-M单精度FPU + DSP指令STM32F4, nRF52840
M7高性能 MCUARMv7E-M6级流水线,ICache/DCacheSTM32H7, i.MX RT
M33安全 MCUARMv8-MTrustZone安全扩展STM32L5, nRF5340
R5/R52实时核ARMv7-R多核锁步,低延迟TMS570(汽车级)
A53/A72应用核ARMv8-AMMU + 64位支持RK3588, i.MX8

三、Cortex-M 寄存器模型

3.1 16 个核心寄存器一览

低寄存器(Caller-saved,被调函数不保护) R0 ──── a1 ──── 函数参数/返回值 R1 ──── a2 ──── 函数参数 R2 ──── a3 ──── 函数参数 R3 ──── a4 ──── 函数参数 ​ 高寄存器(Callee-saved,被调函数必须保存) R4 ──── v1 ──── 通用 R5 ──── v2 ──── 通用 R6 ──── v3 ──── 通用 R7 ──── v4 ──── 通用 R8 ──── v5 ──── 通用 R9 ──── v6 ──── 通用 R10 ─── v7 ──── 通用 R11 ─── v8 ──── 通用 +--- 以上 8 个是 PendSV 手动保存的寄存器 ​ 特殊寄存器 R12 ─── IP ──── 临时(链接器使用) R13 ─── SP ──── 栈指针(MSP + PSP 两个!) R14 ─── LR ──── 链接寄存器(存返回地址 / EXC_RETURN) R15 ─── PC ──── 程序计数器(PC = 当前地址 + 4,流水线效应)

3.2 程序状态寄存器 xPSR

xPSR 实际上由 3 个独立的寄存器组合而成:

xPSR(32位,由 APSR + EPSR + IPSR 组合而成): 31 30 29 28 27 26:25 24 23:20 19:16 15:10 9 8 7:0 ┌────┬────┬────┬────┬──┬───────┬──┬────────┬───────┬───────┬────┬──┬──────────┐ │ N │ Z │ C │ V │ Q │ ICI/IT│ T │ 保留 │ GE │ ICI/IT│ IT │ Exception │ │负数│零 │进位│溢出│饱│中断续 │Thumb│(RAZ) │大于等 │中断续 │IF- │ Number │ │ │标志│标志│标志│和│续指令 │位(1)│ │于标志 │续指令 │Then│(IPSR,低9位)│ └────┴────┴────┴────┴──┴───────┴──┴────────┴───────┴───────┴────┴──┴──────────┘
  • NZCV(bit 31~28):条件标志位,由比较和算术指令设置

  • Q(bit 27):DSP 饱和标志(M4/M7 使用,M3 保留)

  • ICI/IT(bit 26~25 和 bit 15~10):中断可继续指令 / If-Then 指令状态位(EPSR)

  • T(bit 24):必须为 1——Cortex-M 只支持 Thumb/Thumb-2 指令集

  • GE[3:0](bit 19~16):大于等于标志(SIMD 指令使用,M4/M7)

  • IPSR(bit 8~0):当前异常号(Exception Number),只读

// 读取 xPSR 中的异常号(IPSR 位于低 9 位) uint32_t ulActiveIRQ = (__get_xPSR() & 0x1FF); // 0 = 线程模式 // 2 = NMI // 3 = HardFault // 11 = SVC(系统服务调用) // 14 = PendSV(可挂起系统调用) // 15 = SysTick(系统滴答定时器) // 16+= 外设中断(IRQ0 ~ IRQn)

3.3 MSP 与 PSP:为什么要两个栈指针?

特性MSP(主栈指针)PSP(进程栈指针)
用途中断处理 + OS 内核用户任务
复位默认✅ 使用 MSP需要 OS 手动切换到 PSP
中断中始终使用 MSP不使用
CONTROL.SPSEL=0使用 MSP
CONTROL.SPSEL=1使用 PSP
裸机程序只用 MSP不使用
RTOS 场景中断/内核用 MSP每个任务各有自己的 PSP
裸机程序: Thread 模式 ──→ 使用 MSP 中断发生 ──→ 仍使用 MSP(在中断中) RTOS 程序: 任务 A ──→ 使用 PSP_A(独立的栈空间) 任务 B ──→ 使用 PSP_B(独立的栈空间) 触发中断 ──→ 切换回 MSP(内核栈) ★ 这就是 RTOS 的"栈隔离"——无论哪个任务触发中断, 压栈都在 MSP 所在的主栈上进行,不占用任务栈空间!

3.4 函数调用时的寄存器使用(AAPCS)

AAPCS(ARM Architecture Procedure Call Standard)定义了 ARM 架构的函数调用规则:

void func(int a, int b, int c, int d, int e) R0 R1 R2 R3 [栈] 调用 func(1, 2, 3, 4, 5): ① R0=1, R1=2, R2=3, R3=4 ② 第5个参数 5 推到栈上(SP -= 4, *SP = 5) ③ 执行 BL func → LR = 返回地址,PC = func 入口 被调函数 func: ④ 如果用到 R4-R11 → 先 PUSH {R4-R11, LR} 保存 ⑤ 函数体 ... ⑥ POP {R4-R11, PC} 恢复 + 返回

为什么面试常考 AAPCS?因为理解调用约定才能:

  1. 写正确的汇编/内联汇编

  2. 分析 HardFault 时的栈回溯(Stack Unwinding)

  3. 理解 RTOS 任务切换时的上下文保存范围


四、异常模型与中断处理

4.1 Cortex-M 异常系统

Cortex-M3/M4 有15 个系统异常 + 最多 240 个外部中断

异常号 优先级 名称 用途 ────── ────── ──── ──── 1 -3(最高) Reset 复位 2 -2 NMI 不可屏蔽中断 3 -1 HardFault 硬错误(默认错误处理) 4 可配置 MemManage 内存管理错误 5 可配置 BusFault 总线错误 6 可配置 UsageFault 用法错误 7~10 — 保留 11 可配置 SVC **系统服务调用** 12 可配置 DebugMon 调试监视 13 — 保留 14 可配置 PendSV **可挂起系统调用** 15 可配置 SysTick **系统滴答定时器** 16+ 可配置 外设中断 GPIO/TIM/ADC/USART...

4.2 中断处理的硬件自动压栈

Cortex-M 最强大的特性之一:硬件自动压栈

中断发生时,CPU 自动将 8 个寄存器推入当前栈: ​ SP 指向 → ┌──── xPSR ────┐ ← 高地址(先压) │ PC │ ← 返回地址 │ LR │ ← EXC_RETURN │ R12 │ │ R3 │ │ R2 │ │ R1 │ │ R0 │ ← 低地址(后压) └──────────────┘ SP 指向这里(压完 8 个寄存器后) ​ MSP vs PSP 的选择由 EXC_RETURN 决定: 0xFFFFFFF9 → 中断前使用 MSP(裸机场景) 0xFFFFFFFD → 中断前使用 PSP(RTOS 场景)

F103(Cortex-M3)vs F407(Cortex-M4F)压栈差异:

场景压栈大小说明
CM3 任何情况32 字节(恒定)无 FPU,简单确定
CM4F + 不涉及 FPU32 字节和 CM3 一致
CM4F + 涉及 FPU100 字节额外 68 字节的浮点寄存器
CM4F + Lazy Stacking32~100 字节按需保存,但抖动更大

4.3 PendSV 与任务切换

RTOS 的任务切换依赖 PendSV 异常(优先级最低): ┌─────────────┐ │ SysTick 中断 │ ← 周期性触发(默认 1ms) └──────┬──────┘ │ ▼ xTaskIncrementTick() → 是否要切换任务? │ 是 ←───┴───→ 否 │ │ ▼ ▼ 触发 PendSV 继续当前任务 │ ▼ PendSV_Handler(汇编实现): ① MRS R0, PSP ← 读当前任务的 PSP ② STMDB R0!, {R4-R11} ← 保存 R4~R11(手动压栈) ③ STR R0, [TCB] ← 更新 pxTopOfStack ④ vTaskSwitchContext() ← 选择下一个任务 ⑤ LDR R0, [新TCB] ← 读取新任务的堆栈指针 ⑥ LDMIA R0!, {R4-R11} ← 恢复新任务的 R4~R11 ⑦ MSR PSP, R0 ← 更新 PSP ⑧ BX LR ← LR=0xFFFFFFFD → 自动出栈 R0~R3/R12/LR/PC/xPSR 为什么只手动保存 R4~R11? → R0~R3/R12/LR/PC/xPSR 由**硬件自动压栈**(中断进入时已完成) → R4~R11 是 Callee-saved,软件必须手动保存

五、存储器映射与位带操作

5.1 Cortex-M 4GB 地址空间

Cortex-M3/M4 的 4GB 地址空间被预定义为几个大区: 0xE0000000 ~ 0xFFFFFFFF ─── 系统外设(SCB/NVIC/MPU) ─── 512MB 0x60000000 ~ 0x9FFFFFFF ─── 外部 RAM / 外设 ─── 1GB 0x40000000 ~ 0x5FFFFFFF ─── 片上外设寄存器 ─── 512MB 0x20000000 ~ 0x3FFFFFFF ─── SRAM ─── 512MB 0x00000000 ~ 0x1FFFFFFF ─── Code(Flash + Boot 区) ─── 512MB

5.2 位带操作(Bit-banding)

Cortex-M3/M4 支持位带操作——将1 位映射到1 个字(32 位)的地址空间,通过写一个字来原子性地操作一个 bit。

位带区域(SRAM): 位带别名区(地址映射): 0x20000000.0 ─────→ 0x22000000 (可读写 bit 0) 0x20000000.1 ─────→ 0x22000004 (可读写 bit 1) 0x20000000.2 ─────→ 0x22000008 (可读写 bit 2) ... 0x200FFFFF.31 ─────→ 0x23FFFFFC (最后一个 bit) 计算公式: 别名地址 = 0x22000000 + (A - 0x20000000) × 32 + B × 4 其中 A 是 SRAM 地址,B 是位号(0~31)

STM32F103 寄存器位带操作实战:

/* GPIOA->ODR 的第 5 位的位带地址 */ // GPIOA->ODR 地址 = 0x4001080C #define GPIOA_ODR_ADDR 0x4001080CUL #define BITBAND_PERIPH(addr, bit) ((volatile uint32_t *)(0x42000000 + \ ((uint32_t)(addr) - 0x40000000) * 32 + (bit) * 4)) #define PA5_ODR (*BITBAND_PERIPH(GPIOA_ODR_ADDR, 5)) // 使用位带操作 PA5(原子操作!比读-改-写快且安全) PA5_ODR = 1; // PA5 输出高 PA5_ODR = 0; // PA5 输出低 // 类比:不使用位带的读-改-写方式 // GPIOA->ODR |= (1 << 5); // 非原子!可能被中断打断

位带优势:不需要关中断就能原子性地修改单个 bit。FreeRTOS 的某些底层操作可以利用位带来保护标志位。


六、Cortex-M3 vs M4 vs M0 关键差异

特性M0+M3M4M7
架构版本ARMv6-MARMv7-MARMv7E-MARMv7E-M
流水线2级3级3级6级
硬件除法
单周期乘法
DSP 指令
FPU单精度单/双精度
位带
MPU✅(可选)✅(可选)
Cache✅(IC/DC)
中断延迟15 cycles12 cycles12 cycles12 cycles
代码密度高(Thumb)更高(T2)更高(T2)更高(T2)
代表芯片STM32F0STM32F1STM32F4STM32H7

七、常见面试追问

Q: Cortex-M3 和 Cortex-M4 的区别是什么?

M4 在 M3 基础上增加了:

  1. DSP 指令(SMUAD、SMLAD——单指令乘加/饱和运算)

  2. 单精度 FPU(硬件浮点运算,不依赖软件模拟)

  3. 16 位 SIMD 指令(一次处理 2 个 16 位数据)

其他(NVIC、MPU、位带、存储器映射)完全一致。

Q: 为什么 Cortex-M 不能跑 Linux?

因为 Cortex-M 系列没有 MMU(只有可选的 MPU)。

  • MMU 提供虚拟地址到物理地址的映射,是标准 Linux 进程隔离的基础

  • MPU 只能做内存区域权限保护,不能做地址翻译

  • 跑标准 Linux 至少需要 Cortex-A 系列(或有 MMU 的 R 系列)

  • 无 MMU 的 Linux 变体(如 uClinux)理论上可在部分 Cortex-M 上运行,但应用范围极窄

Q: RISC-V 和 ARM 比怎么样?

RISC-V 是开源指令集架构,ARM 是商业 IP

  • RISC-V 优势:开源免费、模块化(可自定义指令扩展)、无授权费

  • RISC-V 劣势:生态不如 ARM(编译器/调试器/RTOS 支持较少)

  • 目前 RISC-V 在 IoT 领域快速崛起(ESP32-C3、GD32V),但 ARM 在主流嵌入式占据绝对优势


八、总结

知识点核心要点
寄存器模型16 个核心寄存器,R0-R3 传参,R4-R11 被调保存,R13(SP) 双栈指针
MSP vs PSPMSP 给中断/内核,PSP 给任务——RTOS 栈隔离的基础
AAPCSR0-R3 传参 → 第5个参数上栈 → LR 存返回地址
硬件压栈中断自动压 8 个寄存器(32 字节),PendSV 手动压 R4-R11
位带操作1 bit 映射到 1 word,原子修改——不用关中断
异常号1~15 系统异常,16+ 外设中断,PendSV(14) 和 SysTick(15) 是 RTOS 基础
M0/M3/M4 差异M0 最简无除法,M3 主力带硬件除法,M4 加 DSP+FPU

理解 ARM Cortex-M 体系结构是嵌入式工程师从"能用"到"懂"的分水岭。掌握了寄存器模型和异常机制,RTOS 的任务切换原理、HardFault 的调试方法、启动代码的执行流程就不再有黑盒。