4.4 双堆栈机制
讲解双堆栈机制之前,大家要先对CM3/CM4对堆栈的处理方式有一个基本的了解。在CM3/CM4中,除了可以使用PUSH和POP指令来处理堆栈外,内核还会在异常处理的始末自动地执行PUSH与POP操作。
4.4.1 堆栈的基本操作
笼统地讲,堆栈是一种存储器的使用模型。它由一块连续的内存和一个栈顶指针组成,用于实现“后进先出”的缓冲区(堆栈是LIFO的模式,和咱们前面讲的按键FIFO和串口FIFO不一样,别搞混了)。其最典型的应用,就是在数据处理前先保存寄存器的值,再在处理任务完成后从中恢复先前保护的这些值。堆栈操作就是对内存的读写操作,但是访问地址由SP给出。寄存器的数据通过PUSH操作存入堆栈,以后用POP操作从堆栈中取回。在PUSH与POP的操作中,SP的值会按堆栈的使用法则自动调整,以保证后续的PUSH不会破坏先前PUSH进去的内容。下面的图片摘自Cortex-M4权威指南,看起来更形象。简单演示了PUSH操作和POP操作过程,堆栈地址由下往上表示从低地址到高地址,也就是所谓的“向下生长的满栈”模型。
Cortex-M3/M4使用的就是“向下生长的满栈”模型(这个要谨记,后面讲RTOS任务切换的时候要用到)。堆栈指针SP指向最后一个被压入堆栈的32位数值。在下一次压栈时,SP先自减4,再存入新的数值。
堆栈的功能就是把寄存器的数据临时备份在内存中,以便将来能恢复之——在一个任务或一段子程序执行完毕后恢复。正常情况下,PUSH与POP必须成对使用,而且参与的寄存器,不论是身份还是先后顺序都必须完全一致。当PUSH/POP指令执行时,SP指针的值也根着自减/自增。
主程序 子程序
; R0=X, R1=Y,R2=Z
BL Fx1 ------------------------------->
Fx1
PUSH {R0 } ;把R0存入栈 & 调整SP
PUSH {R1} ;把R1存入栈 & 调整SP
PUSH {R2} ;把R2存入栈 & 调整SP
… ;执行Fx1的功能,中途可以改变R0-R2的值
POP {R2} ;恢复R2早先的值 & 再次调整SP
POP {R1} ;恢复R1早先的值 & 再次调整SP
POP {R0} ;恢复R0早先的值 & 再次调整SP
BX LR ;返回
<------------------------------------------------
;回到主程序
;R0=X, R1=Y,R2=Z (调用Fx1的前后R0-R2的值完好无损,从寄存器上下文来看,就好像什么都没发生过一样)。
PUSH/POP指令支持一次操作多个寄存器。在前面1.2.2小节有说明。
4.4.2 堆栈空间的分配
为了让大家对堆栈有一个深入的了解,这里以STM32-V5开发板提高篇的第一个例子F4-001_按键检测和LED控制例程(V1.1)跟大家讲解下。在启动文件中定义了堆栈的大小:
Stack_Size EQU 0x00004000
Heap_Size EQU 0x00000400
在main函数中添加malloc函数的调用,要不无法看到heap的使用情况。
uint32_t *p;
p = (uint32_t*)malloc(sizeof(uint32_t)*100); //申请400个字节
首先用MDK全编译工程,打开.map文件:
工程文件生成的map文件信息量很大,涉及到的知识也很多,这里就先说下我们关系的堆栈空间的分配,看之前简易初学者再看看第6章STM32F4XX启动过程详解。
Image Symbol Table
Local Symbols
Symbol Name Value OvType Size Object(Section)
HEAP 0x200021c0 Section 1024 startup_stm32f4xx.o(HEAP)
STACK 0x200025c0 Section 16384 startup_stm32f4xx.o(STACK)
Global Symbols
Symbol Name Value OvType Size Object(Section)
__heap_base 0x200021c0 Data 0 startup_stm32f4xx.o(HEAP)
__heap_limit 0x200025c0 Data 0 startup_stm32f4xx.o(HEAP)
__initial_sp 0x200065c0 Data 0 startup_stm32f4xx.o(STACK)
Memory Map of the image
Execution Region RW_IRAM1 (Base: 0x20000000,Size: 0x000065c0, Max: 0x00020000, ABSOLUTE)
Base Addr Size Type Attr Idx E Section Name Object
0x20000000 0x0000000d Data RW 322 .data bsp_timer.o
0x2000000d 0x00000001 Data RW 423 .data bsp_uart_fifo.o
0x2000000e 0x00000010 Data RW 694 .data stm32f4xx_rcc.o
0x2000001e 0x00000002 PAD
0x20000020 0x00000014 Data RW 1293 .data system_stm32f4xx.o
0x20000034 0x00000004 Data RW 1621 .data mc_w.l(stdout.o)
0x20000038 0x00000004 Data RW 1630 .data mc_w.l(mvars.o)
0x2000003c 0x00000004 Data RW 1631 .data mc_w.l(mvars.o)
0x20000040 0x000000ad Zero RW 174 .bss bsp_key.o
0x200000ed 0x00000003 PAD
0x200000f0 0x00000030 Zero RW 320 .bss bsp_timer.o
0x20000120 0x000020a0 Zero RW 422 .bss bsp_uart_fifo.o
0x200021c0 0x00000400 Zero RW 643 HEAP startup_stm32f4xx.o
0x200025c0 0x00004000 Zero RW 642 STACK startup_stm32f4xx.o
上面的这些信息还不够直接,这里借助Cortex-M3权威指南中的一个截图进行说明。
先解释下这个截图的含义:在离开复位状态后,CM3做的第一件事就是读取下列两个32位整数的值
l 从地址0x0000,0000处取出MSP的初始值(也就是上面的0x20008000)。
l 从地址0x0000,0004处取出PC的初始值——这个值是复位向量,LSB必须是1。然后从这个值所对应的地址处取指。
请注意,这与传统的ARM架构不同——其实也和绝大多数的其它单片机不同。传统的ARM架构总是从0地址开始执行第一条指令。它们的0地址处总是一条跳转指令。在CM3/CM4中,在0地址处提供MSP的初始值,然后紧跟着就是向量表(向量表在以后还可以被移至其它位置) 。向量表中的数值是32位的地址,而不是跳转指令。向量表的第一个条目指向复位后应执行的第一条指令。
因为CM3/CM4使用的是向下生长的满栈,所以MSP的初始值必须是堆栈内存的末地址加1。举例来说,如果你的堆栈区域在0x20007C00-0x20007FFF之间,那么MSP的初始值就必须是0x20008000。
向量表跟随在MSP的初始值之后——也就是第2个表目。要注意因为CM3/CM4是在Thumb态下执行,所以向量表中的每个数值都必须把LSB置1(也就是奇数)。正是因为这个原因,上图中使用0x101来表达地址0x100。当0x100处的指令得到执行后,就正式开始了程序的执行。在此之前初始化MSP是必需的,因为可能第1条指令还没来得及执行,就发生了NMI或是其它fault。MSP初始化好后就已经为它们的服务例程准备好了堆栈。
对于不同的开发工具,需要使用不同的格式来设置MSP初值和复位向量。有些则由开发工具自行计算并生成。现在接着上面的.map文件进行说明,这个文件里面有一项是:
__initial_sp 0x200065c0
其实__initial_sp就是MSP的初始值,下面这个是Debug状态下的截图,MSP初始值和__initial_sp是一样的,也就是MDK帮我们计算好了MSP初始值。
这里就跟大家讲这么多,后面的教程中继续的完善。
4.4.3 双堆栈机制的实现
我们已经知道了CM3/CM4的堆栈是分为两个:主堆栈和进程堆栈,CONTROL[1]决定如何选择。当CONTROL[1]=0时,只使用MSP,此时用户程序和异常handler共享同一个堆栈。这也是复位后的缺省使用方式。下图是CONTROL[1]=0时,堆栈的使用情况(线程模式和handler都是使用的MSP):
当CONTROL[1]=1时,线程模式将不再使用MSP,而改用PSP(handler模式永远使用MSP)。 这样做的好处在哪里?在使用OS的环境下,只要OS内核仅在handler模式下执行,用户应用程序仅在用户模式下执行,这种双堆栈机制派上了用场——防止用户程序的堆栈错误破坏OS使用的堆栈。(特别注意:此时,进入异常时的自动压栈使用的是进程堆栈,进入异常handler后才自动改为MSP,退出异常时切换回PSP,并且从进程堆栈上弹出数据)。下图是线程模式使用的PSP,handler模式使用的MSP。
在特权级下,可以指定具体的堆栈指针,而不受当前使用堆栈的限制,示例代码如下:
MRS R0, MSP ; 读取主堆栈指针到R0
MSR MSP, R0 ; 写R0的值到主堆栈中
MRS R0, PSP ; 读取进程堆栈指针到R0
MSR PSP, R0 ; 写R0的值到进程堆栈中
通过读取PSP的值,OS就能够获取用户应用程序使用的堆栈,进一步地就知道了在发生异常时,被压入寄存器的内容,而且还可以把其它寄存器进一步压栈(使用STMDB和LDMIA的书写形式)。OS还可以修改PSP,用于实现多任务中的任务上下文切换。
这里我们再从Cortex-M3权威指南上获取两个关于双堆栈,操作模式以及特权模式的图片,第一个截图是线程模式使用主堆栈(注意观察中断嵌套后,堆栈和模式的变化):
第二个截图是线程模式使用线程堆栈(注意观察中断嵌套后,堆栈和模式的变化,在嵌套时,更深层ISR所看到的LR总是0xFFFF_FFF1):
4.5 内核相关的API函数
要访问上面提到的寄存器,可以通过ARM官方提供的CMSIS软件包,里面有个文件core_cmdFunc.h,相关API函数就在里面,提供了三个版本的函数支持:MDK,IAR,GCC。这里将MDK版本的函数摘录出来,方便大家查看。
/** \\brief Get Control Register
This function returns the content of theControl Register.
\\return Control Register value
*/
__STATIC_INLINEuint32_t __get_CONTROL(void)
{
register uint32_t __regControl __ASM("control");
return(__regControl);
}
/** \\brief Set Control Register
This function writes the given value to theControl Register.
\\param [in] control Control Register value to set
*/
__STATIC_INLINEvoid __set_CONTROL(uint32_t control)
{
register uint32_t __regControl __ASM("control");
__regControl = control;
}
/** \\brief Get IPSR Register
This function returns the content of theIPSR Register.
\\return IPSR Register value
*/
__STATIC_INLINEuint32_t __get_IPSR(void)
{
register uint32_t __regIPSR __ASM("ipsr");
return(__regIPSR);
}
/** \\brief Get APSR Register
This function returns the content of theAPSR Register.
\\return APSR Register value
*/
__STATIC_INLINEuint32_t __get_APSR(void)
{
register uint32_t __regAPSR __ASM("apsr");
return(__regAPSR);
}
/** \\brief Get xPSR Register
This function returns the content of thexPSR Register.
\\return xPSR Register value
*/
__STATIC_INLINEuint32_t __get_xPSR(void)
{
register uint32_t __regXPSR __ASM("xpsr");
return(__regXPSR);
}
/** \\brief Get Process Stack Pointer
This function returns the current value ofthe Process Stack Pointer (PSP).
\\return PSP Register value
*/
__STATIC_INLINEuint32_t __get_PSP(void)
{
register uint32_t__regProcessStackPointer __ASM("psp");
return(__regProcessStackPointer);
}
/** \\brief Set Process Stack Pointer
This function assigns the given value tothe Process Stack Pointer (PSP).
\\param [in] topOfProcStack Process Stack Pointer value to set
*/
__STATIC_INLINEvoid __set_PSP(uint32_t topOfProcStack)
{
register uint32_t__regProcessStackPointer __ASM("psp");
__regProcessStackPointer = topOfProcStack;
}
/** \\brief Get Main Stack Pointer
This function returns the current value ofthe Main Stack Pointer (MSP).
\\return MSP Register value
*/
__STATIC_INLINEuint32_t __get_MSP(void)
{
register uint32_t __regMainStackPointer __ASM("msp");
return(__regMainStackPointer);
}
/** \\brief Set Main Stack Pointer
This function assigns the given value tothe Main Stack Pointer (MSP).
\\param [in] topOfMainStack Main Stack Pointer value to set
*/
__STATIC_INLINEvoid __set_MSP(uint32_t topOfMainStack)
{
register uint32_t __regMainStackPointer __ASM("msp");
__regMainStackPointer = topOfMainStack;
}
/** \\brief Get Priority Mask
This function returns the current state ofthe priority mask bit from the Priority Mask Register.
\\return Priority Mask value
*/
__STATIC_INLINEuint32_t __get_PRIMASK(void)
{
register uint32_t __regPriMask __ASM("primask");
return(__regPriMask);
}
/** \\brief Set Priority Mask
This function assigns the given value tothe Priority Mask Register.
\\param [in] priMask Priority Mask
*/
__STATIC_INLINEvoid __set_PRIMASK(uint32_t priMask)
{
register uint32_t __regPriMask __ASM("primask");
__regPriMask = (priMask);
}
#if (__CORTEX_M >= 0x03)
/** \\brief Enable FIQ
This function enables FIQ interrupts byclearing the F-bit in the CPSR.
Canonly be executed in Privileged modes.
*/
#define__enable_fault_irq __enable_fiq
/** \\brief Disable FIQ
This function disables FIQ interrupts bysetting the F-bit in the CPSR.
Can only be executed in Privileged modes.
*/
#define__disable_fault_irq __disable_fiq
/** \\brief Get Base Priority
This function returns the current value ofthe Base Priority register.
\\return Base Priority register value
*/
__STATIC_INLINEuint32_t __get_BASEPRI(void)
{
register uint32_t __regBasePri __ASM("basepri");
return(__regBasePri);
}
/** \\brief Set Base Priority
This function assigns the given value tothe Base Priority register.
\\param [in] basePri Base Priority value to set
*/
__STATIC_INLINEvoid __set_BASEPRI(uint32_t basePri)
{
register uint32_t __regBasePri __ASM("basepri");
__regBasePri = (basePri & 0xff);
}
/** \\brief Get Fault Mask
This function returns the current value ofthe Fault Mask register.
\\return Fault Mask register value
*/
__STATIC_INLINEuint32_t __get_FAULTMASK(void)
{
register uint32_t __regFaultMask __ASM("faultmask");
return(__regFaultMask);
}
/** \\brief Set Fault Mask
This function assigns the given value tothe Fault Mask register.
\\param [in] faultMask Fault Mask value to set
*/
__STATIC_INLINEvoid __set_FAULTMASK(uint32_t faultMask)
{
register uint32_t __regFaultMask __ASM("faultmask");
__regFaultMask = (faultMask &(uint32_t)1);
}
#endif /*(__CORTEX_M >= 0x03) */
#if (__CORTEX_M == 0x04)
/** \\brief Get FPSCR
This function returns the current value ofthe Floating Point Status/Control register.
\\return Floating Point Status/Controlregister value
*/
__STATIC_INLINEuint32_t __get_FPSCR(void)
{
#if(__FPU_PRESENT == 1) && (__FPU_USED == 1)
register uint32_t __regfpscr __ASM("fpscr");
return(__regfpscr);
#else
return(0);
#endif
}
/** \\brief Set FPSCR
This function assigns the given value tothe Floating Point Status/Control register.
\\param [in] fpscr Floating Point Status/Control value to set
*/
__STATIC_INLINEvoid __set_FPSCR(uint32_t fpscr)
{
#if(__FPU_PRESENT == 1) && (__FPU_USED == 1)
register uint32_t __regfpscr __ASM("fpscr");
__regfpscr = (fpscr);
#endif
}
4.6 总结
对于初学者一下子掌握这么多的东西比较的困难,这里总结一下主要的几点,一般应用记住这几点即可,需要的时候再查阅这个知识点相关的内容:
l 系统复位后默认使用的是MSP,复位后的状态是特权级线程状态,在这个状态下是允许修改寄存器的。进入到用户特权以后就不能修改这些寄存器了。
l 用户特权的情况(也就是用户建立的非中断服务程序)下可以使用MSP或PSP,特权模式(中断服务程序)只能使用MSP。
l 还有很重要的一条就是。假如在用户模式下使用的是PSP,那么寄存器的数值被保存到任务堆栈的空间,进入中断程序后就开始使用MSP,如果还有一个高优先级的中断难么就继续的使用MSP,在程序推出最后一级中断的时候就用用户堆栈恢复寄存器。
l Cortex-M4的堆栈模型是向下生长的满栈。 下期教程会做两个相关的例子帮大家深入的理解CM4内核方面的知识。 参考资料: 1. Patterns fortime-triggered embedded systems英文版和中文版 2. Cortex-M3权威指南中文版 3. TheDefinitive Guide to Arm Cortex-M3 and Cortex-M4 Processors(M4权威指南) |