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+ 超低功耗 MCU ARMv6-M 2级流水线,无MPU STM32F0, RP2040
M3 主流 MCU ARMv7-M 3级流水线,MPU可选 STM32F1, GD32F103
M4 DSP+FPU ARMv7E-M 单精度FPU + DSP指令 STM32F4, nRF52840
M7 高性能 MCU ARMv7E-M 6级流水线,ICache/DCache STM32H7, i.MX RT
M33 安全 MCU ARMv8-M TrustZone安全扩展 STM32L5, nRF5340
R5/R52 实时核 ARMv7-R 多核锁步,低延迟 TMS570(汽车级)
A53/A72 应用核 ARMv8-A MMU + 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 指令集

  • GE3: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 + 不涉及 FPU 32 字节 和 CM3 一致
CM4F + 涉及 FPU 100 字节 额外 68 字节的浮点寄存器
CM4F + Lazy Stacking 32~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+ M3 M4 M7
架构版本 ARMv6-M ARMv7-M ARMv7E-M ARMv7E-M
流水线 2级 3级 3级 6级
硬件除法
单周期乘法
DSP 指令
FPU 单精度 单/双精度
位带
MPU ✅(可选) ✅(可选)
Cache ✅(IC/DC)
中断延迟 15 cycles 12 cycles 12 cycles 12 cycles
代码密度 高(Thumb) 更高(T2) 更高(T2) 更高(T2)
代表芯片 STM32F0 STM32F1 STM32F4 STM32H7

七、常见面试追问

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 PSP MSP 给中断/内核,PSP 给任务------RTOS 栈隔离的基础
AAPCS R0-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 的调试方法、启动代码的执行流程就不再有黑盒。


相关推荐
Deven_Huang3 小时前
STM32 ADC 使用经验总结与常见踩坑点
stm32·adc
KaifuZeng3 小时前
通信与接口协议面试五、RS422
嵌入式硬件·面试·通信与接口
迷茫、Peanut5 小时前
蜂鸣器介绍
单片机
Topplyz5 小时前
如何减小DCDC的纹波?
单片机·嵌入式硬件·硬件工程·电源·layout·dcdc
sramdram6 小时前
灵动高性能32位MCU空调解决方案
单片机·嵌入式硬件·mcu·32位mcu
Hotchip_MEMS8 小时前
ASIC+MCU方案简化:MS2102AB-M00如何降低主控芯片的判别压力?
人工智能·单片机·嵌入式硬件·物联网·制造
sramdram8 小时前
应用在单片机上的MRAM芯片嵌入式存储方案
单片机·嵌入式硬件·mram·mram芯片
piaoyiren8 小时前
普中A6 STC8A 动态数码管显示讲解
嵌入式硬件·51单片机·c
阿拉斯攀登10 小时前
内核模块开发:module_init 与模块机制详解
linux·网络·嵌入式硬件·linux驱动