第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的架构就算入门了。下一章,咱们聊聊时钟系统和复位机制,这可是嵌入式系统的脉搏。