1. Cortex-M 架构概览:寄存器模型、工作模式与存储器映射
好,咱们直接开始。第一章我打算先带你把 Cortex-M 的「骨架」搭起来。说白了,就是搞清楚三件事:CPU 手里有哪些寄存器、它能在哪几种状态下干活、以及它怎么找到内存里的代码和数据。
这三件事搞明白了,后面写外设驱动、配中断,你心里就有底了。我记得刚转做 ARM 固件那会儿,上来就啃中断向量表,结果模式切换时栈指针飞了,查了两天才发现是没搞懂寄存器组……嗯,咱们别走那个弯路。
1.1 寄存器模型:程序员手里的牌
Cortex-M 的寄存器组,比经典 ARM 精简了不少。你打开内核手册,第一张表就是它。我习惯把这组寄存器分成三类:
| 类别 | 寄存器 | 我的理解 |
|---|---|---|
| 通用寄存器 | R0 – R12 | 干活的主力,加减乘除、传参、暂存都靠它们 |
| 特殊功能寄存器 | R13 (SP), R14 (LR), R15 (PC) | 栈指针、链接寄存器、程序计数器——一个都不能错 |
| 内核控制寄存器 | xPSR, PRIMASK, BASEPRI, FAULTMASK, CONTROL | 管中断开关、优先级、线程/Handler 模式切换 |
关键点:Cortex-M 的 R13(SP)有两个实体——MSP 和 PSP。你在 Handler 模式(比如中断服务函数)里永远用 MSP,在线程模式里可以用 MSP 或 PSP。我建议裸机项目统一用 MSP,省心;跑 RTOS 时再切 PSP,把任务栈和内核栈隔开。
再说说 xPSR。它其实是三个状态寄存器的合体:
- APSR:算术标志位,N(负)、Z(零)、C(进位)、V(溢出)——条件跳转全靠它
- IPSR:当前中断号,你在 ISR 里读它就知道是谁触发的
- EPSR:Thumb 状态位和 IF-THEN 块状态——Cortex-M 只跑 Thumb-2 指令集,这个位永远为 1
你写汇编时可能会直接操作 xPSR,但 C 代码里一般用内联函数或 intrinsic。比如关全局中断:
// 关中断:设置 PRIMASK
__disable_irq();
// 开中断:清除 PRIMASK
__enable_irq();
// 读当前中断号
uint32_t irq_num = __get_IPSR();
我的习惯:在项目里定义一个 cpu_irq_disable() 和 cpu_irq_enable(),底层就包这两个 intrinsic。这样移植到不同 Cortex-M 型号时,只需要改这一个地方。
1.2 工作模式与特权级:谁说了算
Cortex-M 只有两种工作模式,比 Linux 的用户态/内核态简单得多:
- 线程模式(Thread Mode):复位后默认的模式,跑 main 函数的地方
- Handler 模式(Handler Mode):发生异常或中断时自动进入,处理完返回线程模式
但光有模式还不够,ARM 又加了一层「特权级」控制:
| 组合 | 典型场景 | 能干啥 |
|---|---|---|
| 线程模式 + 特权级 | 裸机 main 函数 | 访问所有寄存器、所有内存、开关中断 |
| 线程模式 + 非特权级 | RTOS 的用户任务 | 不能操作内核控制寄存器,不能开关中断 |
| Handler 模式 | 中断服务函数 | 永远是特权级,可以干任何事 |
为什么会设计非特权级?我在一个物联网项目里吃过亏——某个传感器驱动库在中断里误操作了 PRIMASK,导致系统死锁。后来我把第三方驱动都放在非特权级线程里跑,就算它崩了,也影响不到内核调度。
注意:非特权级线程不能直接写 CONTROL 寄存器来切回特权级。必须通过 SVC(系统服务调用)异常,让 Handler 模式帮你提权。RTOS 的任务创建和切换,底层就是靠这个机制。
1.3 存储器映射:地址空间长什么样
Cortex-M 的 4GB 地址空间被 ARM 划分成了几个固定区域。你写链接脚本、配外设基地址时,必须按这个地图来:
0x00000000 – 0x1FFFFFFF 代码区(Code)
0x20000000 – 0x3FFFFFFF SRAM 区
0x40000000 – 0x5FFFFFFF 外设区(Peripheral)
0x60000000 – 0x9FFFFFFF 外部 RAM / 设备
0xE0000000 – 0xE00FFFFF 系统控制块(SCB, NVIC, MPU 等)
0xE0100000 – 0xFFFFFFFF 保留 / 厂商自定义
你想想看,为什么 ARM 要把外设区固定放在 0x40000000?我个人的理解是:这样编译器、链接器、调试器都能用统一的地址映射,不用每个芯片厂商自己定义一套。你换芯片时,只要改外设基地址宏定义就行,内核寄存器地址完全不变。
举个例子,STM32F4 的 GPIOA 基地址是 0x40020000,而 NXP LPC1768 的 GPIO 基地址是 0x2009C000——它们都在外设区范围内,但具体偏移不同。这就是为什么 CMSIS 头文件里会有一堆 #define。
避坑指南:我曾经在项目里把一个大数组定义到了 0x08000000(Flash 地址),结果写数据时直接 HardFault。后来才意识到——Flash 只能通过编程接口写,不能像 SRAM 那样直接寻址写入。所以,变量一定要放在 0x20000000 开始的 SRAM 区。
1.4 一张图看懂 Cortex-M 架构
下面这张 SVG 把寄存器、模式、存储器映射串在了一起。你可以把它当作本章的知识地图:
1.5 本章小结:你该带走什么
第一章的内容,说白了就是三个数字:
- 16 个通用寄存器 + 双栈指针 MSP/PSP
- 2 种工作模式 × 2 级特权 = 4 种运行态组合
- 4 个主要地址区域:Code、SRAM、Peripheral、System Control
我建议你打开手头的开发板手册,找到存储器映射那一页,对照着 0x40000000 附近的外设地址看一遍。然后写个简单的 main 函数,用 __get_CONTROL() 和 __get_xPSR() 打印出当前模式——亲眼看到寄存器值的变化,比背十遍手册都管用。
课后动手:在 main 函数里读一下 SCB->VTOR 寄存器(向量表偏移地址),看看你的芯片把中断向量表放在了 Flash 还是 SRAM。这个值在你要做 IAP 在线升级时特别重要。