第2章:ARM Cortex-M架构入门:寄存器模型、中断向量表、启动文件分析、堆栈设置

好,咱们开始聊Cortex-M。说实话,我第一次接触ARM Cortex-M的时候,心里还挺没底的。那时候刚从8位单片机转过来,看到那一堆寄存器,第一反应是——这玩意儿能搞定吗?后来做多了才发现,Cortex-M其实比想象中要友好得多。它专门为嵌入式设计,没有那些复杂的内存管理单元,也没有流水线冒险的烦恼。说白了,它就是一颗为裸机程序量身定制的大脑。

2.1 寄存器模型:你手里的工具

Cortex-M的寄存器模型,我习惯把它分成两类:通用寄存器和特殊寄存器。通用寄存器就是R0到R12,这些是干活的主力。R13是堆栈指针SP,R14是链接寄存器LR,R15是程序计数器PC。嗯,这里要注意,SP在Cortex-M里有两个——MSP和PSP,分别对应主堆栈和进程堆栈。我刚开始做项目时,一直只用MSP,后来做RTOS才意识到PSP的妙处。

核心寄存器一览:

寄存器名称我的理解
R0-R12通用寄存器干活的主力,随便用
R13 (SP)堆栈指针分MSP和PSP,别搞混
R14 (LR)链接寄存器函数返回时用,中断里要小心
R15 (PC)程序计数器指向当前指令,别乱改
xPSR程序状态寄存器标志位都在这里

特殊寄存器里,我最常用的是PRIMASK和FAULTMASK。PRIMASK置1就能关掉所有可屏蔽中断,这在临界区保护时特别好用。我曾经在一个电机控制项目里,因为没处理好中断嵌套,导致PWM波形出现了毛刺。后来用PRIMASK包了一下关键代码,问题就解决了。你想想看,有时候一个简单的寄存器操作,就能省下好几天的调试时间。

2.2 中断向量表:系统的紧急联络簿

中断向量表,说白了就是一张表。表里存的是各种异常和中断的入口地址。Cortex-M强制要求向量表从0地址开始,第一个字是MSP的初始值,第二个字是复位向量。我记得第一次写启动文件时,把向量表地址写错了,结果芯片上电就跑飞了。后来养成了习惯,每次都会用调试器看一眼向量表的前几个字对不对。

我的小技巧:在调试阶段,我会在向量表里故意放一个错误的中断处理函数。如果某个中断意外触发,程序就会跳到我预设的陷阱里,这样能快速定位问题。

中断向量表的布局是这样的:

  • 偏移0x00:MSP初始值
  • 偏移0x04:复位向量
  • 偏移0x08:NMI(不可屏蔽中断)
  • 偏移0x0C:硬错误(HardFault)
  • 偏移0x10-0x3C:系统异常
  • 偏移0x40以后:外部中断

我曾经遇到过一个很隐蔽的问题:某个外设的中断号是16,但我在向量表里写成了15。结果中断来了之后,程序跳到了错误的地方,系统直接卡死。排查了整整一天才发现是偏移量算错了。所以,我建议你在写向量表时,一定要对照芯片手册仔细核对每个中断的编号。

2.3 启动文件分析:从零到一的旅程

启动文件,就是芯片上电后执行的第一段代码。它负责三件事:设置堆栈、初始化数据段、跳转到main函数。我刚开始学的时候,觉得启动文件是抄来抄去的模板,没什么好研究的。直到有一次我需要移植一个项目到新芯片上,才发现启动文件里的门道真不少。

一个典型的启动文件包含以下部分:

; 向量表定义
__Vectors       DCD     __initial_sp      ; 栈顶地址
                DCD     Reset_Handler     ; 复位向量
                DCD     NMI_Handler       ; NMI
                DCD     HardFault_Handler ; 硬错误
                ; ... 其他异常向量

; 复位处理函数
Reset_Handler   PROC
                EXPORT  Reset_Handler
                ; 初始化.data段
                LDR     R0, =_sidata
                LDR     R1, =_sdata
                LDR     R2, =_edata
                BL      copy_data
                ; 清零.bss段
                LDR     R0, =_sbss
                LDR     R1, =_ebss
                BL      zero_bss
                ; 跳转到main
                BL      main
                B       .
                ENDP

注意:启动文件里的符号名称必须和链接脚本里的一致。我见过有人把_sidata写成了_sidat,结果数据段没初始化,全局变量全是随机值。这种bug特别难查,因为看起来代码逻辑都对,但就是跑不出预期结果。

我个人习惯在启动文件里加一个看门狗喂狗的操作。有些芯片上电后看门狗默认是开启的,如果不及时喂狗,芯片会在初始化过程中复位。我曾经在一个项目中,因为没注意到这个细节,芯片一直在复位循环里打转,连调试器都连不上。后来在启动文件最前面加了几条喂狗指令,问题就解决了。

2.4 堆栈设置:别让程序饿死

堆栈设置,说白了就是给程序分配一块内存,用来存局部变量、函数参数和返回地址。Cortex-M使用满递减堆栈,也就是SP从高地址向低地址增长。我刚开始做项目时,堆栈大小都是随便设的,够用就行。直到有一次程序跑着跑着就死机了,排查了好久才发现是堆栈溢出了。

堆栈大小的设置,我建议遵循以下原则:

  • 先估算一下最大嵌套深度。比如你的中断嵌套了3层,每层用100字节,那至少需要300字节。
  • 加上main函数和各个任务的栈需求。RTOS里每个任务都有自己的栈,这个要单独算。
  • 留出20%的余量。别算得太死,否则后期加功能时容易出问题。

我的经验:在调试阶段,我会在堆栈的底部和顶部填充特定的模式(比如0xDEADBEEF)。程序跑一段时间后,检查这些模式有没有被覆盖。如果发现被覆盖了,就说明堆栈溢出了。这个方法虽然土,但特别管用。

堆栈设置通常在链接脚本里完成:

_estack = ORIGIN(RAM) + LENGTH(RAM);  /* 栈顶在RAM末尾 */
_Min_Heap_Size = 0x200;               /* 堆大小512字节 */
_Min_Stack_Size = 0x400;              /* 栈大小1KB */

这里要注意,堆和栈是两回事。堆用于动态内存分配(malloc),栈用于函数调用。在嵌入式系统里,我建议尽量少用堆,因为动态分配容易产生碎片。我见过一个项目,因为频繁malloc/free,最后堆空间碎得连一个100字节的块都分配不出来。后来改成静态分配,问题就解决了。

嗯,关于堆栈,还有一个容易忽略的点:中断处理函数会使用当前任务的栈。如果你的中断嵌套很深,或者中断处理函数里用了很多局部变量,那栈空间一定要留够。我曾经在一个项目中,中断处理函数里定义了一个512字节的局部数组,结果栈直接爆了。后来改成静态全局数组,才解决了问题。

好了,这一章的内容就到这里。寄存器模型是你手里的工具,中断向量表是系统的紧急联络簿,启动文件是从零到一的旅程,堆栈设置是程序的生命线。把这四个点吃透了,Cortex-M的架构就算入门了。下一章,咱们聊聊时钟系统和复位机制,这可是嵌入式系统的脉搏。