目录
堆栈是一种数据结构。堆栈都是一种数据项按序排列的数据结构,只能在一端(称为栈顶(top))对数据项进行插入和删除,相应地,另一端为成为栈底(bottom),不含元素的空表称为空栈。
其实堆栈是由栈(Stack)和堆(Heap)组成的,汇编中应用的 PUSH 和 POP 就是对 栈(Stack)的操作,其按照后进先出(LIFO-Last In First Out)的原理运作。
笼统地讲,堆栈操作就是对内存的读写操作,但是其地址由 SP 给出。
1. SRAM
STM32的内存(存储器)的地址空间大小为4G(0x0000 0000 ~ 0xFFFF FFFF),被分为8个block(block0~block7),每个block为512Mbyte,如下图所示。
SRAM 被分配到 block 1,内有SARM1(112KB,0x2000 00000 ~ 0x2001 BFFF)和SRAM2(16KB, 0x2001 C000 ~ 0x2001 FFFF)两块连续的SRAM,可供所有的AHB 主控总线访问。
- 不同系类单片机的 SRAM 是不一样的,但它们的起始地址都是0x2000 0000。
- 为什么是两块SRAM?这是因为主总线支持并发SRAM访问,提高执行效率。例如当 CPU 对 112 KB SRAM 进行读/写操作时,以太网MAC 可以同时对 16 KB SRAM 进行读/写操作;或者CPU和DMA可以同时访问不同的SRAM。
2. 堆栈的作用
栈(Stack):由编译器自动分配和释放。栈存放局部变量,函数调用时的返回地址和现场保护,函数的参数(形参)等。
堆(Heap):有程序员主动分配和释放。主要用于动态内存分配,malloc 申请,free 释放。
在函数中定义的局变量占用栈空间,当数据较多,栈空间使用完,则会占用堆空间,甚至其他全局变量空间,造成程序崩溃或数据错误(内存溢出错误)。
在不断申请、释放的过程中,容易产生碎片化内存;当然如果不是在短时间内频繁的使用 malloc 申请和 free 释放内存,系统是有足够的时间来回收碎片内存。
堆栈指针的最低两位永远是 0,这意味着堆栈总是 4 字节对齐的。
3. 堆栈的设置
这里我们以 STM32F407 的程序代码为例进行说明,找到启动文件 startup_stm32f407xx.s,设置栈大小为 2048(0x800),堆大小为 1024(0x400)。
编译代码后,在 Build output 窗口中显示编译结果信息,如下图所示:
这里只是针对当前测试实例程序进行说明,但万变不离其宗,一样可以对照理解。
- Code:存储到Flash(ROM)中的程序代码,显示占用了 12372 个字节。
- RO-data:只读数据(Read Only data),被存储到ROM中的数据,也就是常量等不能被程序修改的数据;例如 const 修饰的变量。
- RW-data:可读写数据(Read Write data),在程序编写时已经初始化初始值。
- ZI-data:(Zero Initialized data),没有初始化的变量,被编译器初始化为0;(使用了RTOS)
Total RO Size (Code + RO-data) = 12876 ( 12.57kB)
Total RW Size (RW-data + ZI-data) = 24888 ( 24.30kB)
Total ROM Size (Code + RO-data + RW-data) = 12984 ( 12.68kB)
编译成功后会在工程文件下生成一个 xxxx.map 文件,这个文件的详细讲解网上很多,请百度一下即可;如图红框所示,STACK 和 HEAP 的大小和我们所设置的一样,
不知道大家注意到 HEAP 和 STACK 的起始地址分别 0x2000 5538 和 0x2000 5938,这个其实是有编译器决定的,是不固定的;起始地址后面紧跟的就是HEAP 和 STACK 的大小(size)。
若程序中完全没有使用 malloc 动态申请堆(HEAP)空间,编译器会优化,不把堆空间计算在内。
最后,这里再看下上面实例的MSP(R13,主堆栈指针)和PC(R15,程序计数器)初始化流程图:
4. 堆栈的实现
STM32使用的是“向下生长的满栈”模型,堆栈指针 SP 指向最后一个被压入堆栈的 32位数值。
堆栈指针指向最后压入的堆栈的有效数据项,称为满栈;堆栈指针指向下一个要放入的空位置,称为空栈。
为什么说“向上生长(向高地址方向生长)”和“向下生长(向低地址方向生长)”呢?那是因为,一般画堆栈示意图都是把低地址画在下面,高地址画在上面。如下图。
压栈(PUSH): SP 先自减 4,再存入新的数值。
出栈(POP):先从 SP 指针处读取上一次被压入的值,再把 SP 指针自增 4。
虽然 POP 后被压入的数值还保存在栈中,但它已经无效了,因为为下次的 PUSH 将覆盖它的值!
5. 双堆栈机制
堆栈分为主堆栈(MSP)和进程堆栈(PSP),选择当前使用哪个堆栈指针由CONTROL寄存器(特殊功能寄存器)决定。
当处理器处于线程模式时,控制寄存器控制所使用的堆栈和软件执行的特权级别,并指示 FPU 状态是否为活动的。
位(bits) | 功能(Function) |
31:3 | Reserved |
2 | FPCA:指示当前浮点上下文是否活动: 0:不激活浮点上下文 1:表示浮点上下文活动。 Cortex-M4在处理异常时使用该位来决定是否保留浮点状态。 |
1 | SPSEL:激活选择的堆栈指针。选择当前堆栈: 0:MSP是当前的堆栈指针 1:PSP是当前的堆栈指针。 在 Handle 模式下,该位读为零并忽略写。在异常返回时,Cortex-M4会自动更新此位。 |
0 | nPRIV:线程模式权限级别。定义线程模式权限级别。 0:特权 1:无特权的。 |
在 Cortex‐M4 的 handler 模式中, CONTROL[1](CONTROL寄存器的bit[1])总是 0(Handle 模式下总是使用 MSP)。
当 CONTROL[1]=0 时,只使用 MSP,此时用户程序和异常 handler 共享同一个堆栈。这也是复位后的缺省使用方式。
当 CONTROL[1]=1 时,线程模式将不再使用 PSP,而改用 MSP( handler 模式永远使用 MSP)。
注意,在这种情况下,进入异常时的自动压栈使用的是进程堆栈,进入异常 handler 后才自动改为 MSP,退出异常时切换回 PSP,并且从进程堆栈上弹出数据。
在特权级下,可以直接对 MSP 和 PSP 执行读/写操作,而不会混淆你所引用的R13,示例代码如下:
MRS R0, MSP ; 读取主堆栈指针到 R0
MSR MSP, R0 ; 写入 R0 的值到主堆栈中
MRS R0, PSP ; 读取进程堆栈指针到 R0
MSR PSP, R0 ; 写入 R0 的值到进程堆栈中
通过MRS指令读取 PSP 的值,OS(操作系统) 可以读取用户应用程序堆积的数据(比如在系统服务调用前的寄存器内容,SVC)。此外,OS 可以改变 PSP 指针的值,例如,在多任务系统的上下文切换。
参考资料
- STM32F407数据手册 —— https://www.st.com/resource/en/datasheet/stm32f405rg.pdf
- STM32F407参考手册
- Cortex‐M3 权威指南(中文)