第三章 ARM Cortex-M体系结构:寄存器组、异常与中断向量表、存储器映射、MPU内存保护单元
各位同学,大家好。今天我们来聊聊Cortex-M内核的“骨架”——它的体系结构。说实话,这部分内容看起来有点枯燥,但它是你写BSP和驱动的基础。我当年刚入行时,觉得寄存器嘛,查手册就行了。直到有一次调试一个诡异的系统崩溃,折腾了三天,最后发现是中断向量表配置错了。嗯,从那以后,我再也不敢小看这些基础了。
3.1 寄存器组:你的“手边工具箱”
Cortex-M的寄存器,说白了就是你写代码时最常用的那几样工具。我个人习惯把它们分成两类:通用寄存器和特殊寄存器。
3.1.1 通用寄存器组(R0-R15)
R0-R12是通用寄存器,随便用。但R13、R14、R15这三个,你得小心伺候着。
- R13(SP):栈指针。分MSP(主栈指针)和PSP(进程栈指针)。我建议你在裸机开发中只用MSP,省心。
- R14(LR):链接寄存器。保存函数返回地址。有一次我手欠改了LR的值,程序直接飞了……
- R15(PC):程序计数器。指向当前执行的指令。别想着直接写它,除非你想玩跳转。
重点:异常发生时,硬件会自动压栈R0-R3、R12、LR、PC和xPSR。你写中断服务函数时,不用操心保存现场,硬件帮你干了。
3.1.2 特殊寄存器
这几个寄存器,你写BSP时几乎天天打交道。
| 寄存器 | 全称 | 我的用法 |
|---|---|---|
| xPSR | 程序状态寄存器 | 查溢出、零标志。调试时我经常看它。 |
| PRIMASK | 优先级屏蔽寄存器 | 全局关中断。写临界区代码时必用。 |
| FAULTMASK | 故障屏蔽寄存器 | 连HardFault都屏蔽。慎用!我只有调试时才碰它。 |
| BASEPRI | 基础优先级寄存器 | 屏蔽低于某优先级的中断。比PRIMASK更精细。 |
小技巧:用BASEPRI代替PRIMASK做临界区保护,可以保留高优先级中断的响应。我在医疗设备中常用,因为心电采集的中断不能等。
3.2 异常与中断向量表:系统的“紧急通讯录”
异常和中断,本质上是一回事——CPU被“打断”了。区别在于:异常是内部事件(比如除零),中断是外部事件(比如按键按下)。
3.2.1 向量表的结构
向量表放在Flash的最开头(地址0x00000000)。第一个字是MSP的初始值,第二个字是Reset_Handler的地址。后面依次排列各种异常和中断的入口地址。
; 典型的向量表(汇编)
__Vectors DCD __initial_sp ; 栈顶地址
DCD Reset_Handler ; 复位向量
DCD NMI_Handler ; NMI
DCD HardFault_Handler ; 硬故障
DCD MemManage_Handler ; MPU故障
DCD BusFault_Handler ; 总线故障
DCD UsageFault_Handler ; 用法故障
; ... 后面是中断向量
你想想看,如果这里写错了,CPU上电后直接跑飞。我曾经见过一个同事,把Reset_Handler的地址写成了偶数(Cortex-M要求地址最低位为1表示Thumb状态),结果芯片死活不启动。
3.2.2 中断优先级与嵌套
Cortex-M支持最多256级优先级(实际芯片通常只实现8-16级)。优先级数值越小,优先级越高。这个和有些RTOS的习惯相反,注意别搞混了。
避坑指南:我曾经在STM32上配置中断优先级分组时,把抢占优先级和子优先级搞反了。结果两个中断互相抢占,系统卡死。记住:先查芯片手册的优先级位数,再配置NVIC。
3.3 存储器映射:你的“城市地图”
Cortex-M的4GB地址空间被划分成几个区域。每个区域有固定的用途。你写驱动时,外设寄存器就在特定区域里。
| 地址范围 | 区域名称 | 用途 |
|---|---|---|
| 0x00000000 - 0x1FFFFFFF | Code区 | 存放程序代码。可配置为Flash或SRAM。 |
| 0x20000000 - 0x3FFFFFFF | SRAM区 | 存放变量、堆栈。我习惯把关键数据放这里。 |
| 0x40000000 - 0x5FFFFFFF | 外设区 | 所有外设寄存器都在这里。比如GPIO、UART。 |
| 0xE0000000 - 0xE00FFFFF | 系统区 | NVIC、MPU、SysTick等内核外设。 |
为什么这么设计?说白了就是为了统一。不管哪个芯片厂,外设寄存器都在0x40000000附近。你换芯片时,至少地址空间的大框架不用重新学。
3.4 MPU内存保护单元:系统的“保安队长”
MPU是Cortex-M3/4/7的可选功能。它让你给内存区域设置访问权限。比如:某段内存只能读不能写,或者只能特权模式访问。
3.4.1 MPU的配置
配置MPU需要设置区域号、基地址、大小和属性。我一般这么配:
void MPU_Config(void)
{
// 禁用MPU
MPU->CTRL = 0;
// 配置区域0:Flash区域,只读,不可执行
MPU->RNR = 0; // 区域号0
MPU->RBAR = 0x08000000; // 基地址
MPU->RASR = (0x1 << 28) | // 使能
(0x3 << 24) | // 全访问权限
(0x1 << 18) | // 不可执行
(0x1F << 1); // 大小:1MB
// 配置区域1:SRAM区域,可读写
MPU->RNR = 1;
MPU->RBAR = 0x20000000;
MPU->RASR = (0x1 << 28) |
(0x3 << 24) |
(0x0 << 18) | // 可执行
(0x13 << 1); // 大小:8KB
// 使能MPU
MPU->CTRL = 0x01;
}
关键点:MPU的区域大小必须是2的幂次,且基地址要对齐。比如8KB的区域,基地址必须是8KB的整数倍。这个坑我踩过,配错了MPU直接触发MemManage Fault。
3.4.2 在医疗设备中的应用
医疗设备对安全性要求极高。我做过一个输液泵的项目,用MPU做了三件事:
- 把关键参数(比如输液速度)放在受保护的内存区域,普通代码不能写。
- 把用户代码区设为不可执行,防止缓冲区溢出攻击。
- 把外设寄存器区设为只特权模式可访问,用户程序不能直接操作硬件。
我的经验:MPU不是万能的。它只能保护内存访问,不能保护外设本身。比如DMA直接写内存,MPU管不了。你需要结合DMA的传输控制一起用。
3.5 实战:系统启动时的初始化顺序
写BSP时,系统上电后的初始化顺序很重要。我个人习惯这么来:
- 设置向量表偏移(如果有Bootloader)。
- 初始化堆栈(C库的__main会做,但裸机要自己来)。
- 配置MPU(如果使用)。
- 初始化时钟和PLL。
- 初始化外设(GPIO、UART等)。
- 使能全局中断。
你想想看,如果先开了中断再配MPU,中断一来直接触发异常。这个顺序不能乱。
避坑指南:我曾经在某个项目里,把MPU配置放在了时钟初始化之后。结果MPU配置时访问了未初始化的外设寄存器,触发了BusFault。后来我把MPU配置提前到最前面,问题解决。
好了,这一章的内容就到这里。寄存器组是你的工具箱,向量表是你的通讯录,存储器映射是你的地图,MPU是你的保安。把这四样东西搞明白,Cortex-M的体系结构你就拿下了。下一章我们讲时钟和复位,那是系统的“心跳”。