第三章 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时,系统上电后的初始化顺序很重要。我个人习惯这么来:

  1. 设置向量表偏移(如果有Bootloader)。
  2. 初始化堆栈(C库的__main会做,但裸机要自己来)。
  3. 配置MPU(如果使用)。
  4. 初始化时钟和PLL。
  5. 初始化外设(GPIO、UART等)。
  6. 使能全局中断。

你想想看,如果先开了中断再配MPU,中断一来直接触发异常。这个顺序不能乱。

避坑指南:我曾经在某个项目里,把MPU配置放在了时钟初始化之后。结果MPU配置时访问了未初始化的外设寄存器,触发了BusFault。后来我把MPU配置提前到最前面,问题解决。

好了,这一章的内容就到这里。寄存器组是你的工具箱,向量表是你的通讯录,存储器映射是你的地图,MPU是你的保安。把这四样东西搞明白,Cortex-M的体系结构你就拿下了。下一章我们讲时钟和复位,那是系统的“心跳”。