摘要:单片机堆栈溢出会引发不可预知的错误。本文探讨了基于STM32CubeIDE设置STM32H7xx堆栈在无RTOS时的使用与检测方法。
一、堆栈的设置
STM32CubeIDE对工程设置堆栈很简单,在CubeMX中设置最小size如下图
堆(Heap)为0x400,等于1024字节,栈(Stack)为0x800,等于2048字节。堆用于用于程序运行中动态申请内存分配和释放,例如用malloc()和calloc()函数申请内存,用完free()函数释放。栈由编译器自动分配,用于函数调用形参、函数局部变量等临时数据存放,栈的使用量和工程程序的复杂程度密切相关,如果连续调用子函数,或者使用大量局部变量就要留足栈空间。
生成工程后在链接脚本文件(*.ld)中就可以看到,在第7,8行
/* Entry Point */
ENTRY(Reset_Handler)
/* Highest address of the user mode stack */
_estack = ORIGIN(RAM_D1) + LENGTH(RAM_D1); /* end of RAM */
/* Generate a link error if heap and stack don't fit into RAM */
_Min_Heap_Size = 0x400; /* required amount of heap */
_Min_Stack_Size = 0x800; /* required amount of stack */
/* Specify the memory areas */
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 12K /* 512K=12+1+499 */
FLASH2 (rx) : ORIGIN = 0x08100000, LENGTH = 512K
DTCMRAM (xrw) : ORIGIN = 0x20000000, LENGTH = 128K
RAM_D1 (xrw) : ORIGIN = 0x24000000, LENGTH = 512K
RAM_D2 (xrw) : ORIGIN = 0x30000000, LENGTH = 288K
RAM_D3 (xrw) : ORIGIN = 0x38000000, LENGTH = 64K
ITCMRAM (xrw) : ORIGIN = 0x00000000, LENGTH = 64K
FLASH_VER (rx) : ORIGIN = 0x08003000, LENGTH = 1K
FLASH_REM (rx) : ORIGIN = 0x08003400, LENGTH = 499K
}
STM32H7的堆栈属于SRAM,就是上面的RAM_D1域,起始地址0x24000000,长度512K
二、堆栈的分配
在启动文件(*.s)中定义了一些内存地址全局变量,在上面的链接脚本文件中也引用了,比如_estack、_Min_Heap_Size、_Min_Stack_Size,在工程的sysmem.c文件中使用了这些全局变量。文件中主要是定义了_sbrk()函数,这个函数是申请动态内存,比如malloc()执行后会被调用,返回内存地址指针,调整堆顶地址的功能。
/* Includes */
#include <errno.h>
#include <stdint.h>
/**
* Pointer to the current high watermark of the heap usage
*/
//static
uint8_t *__sbrk_heap_end = NULL;
/**
* @brief _sbrk() allocates memory to the newlib heap and is used by malloc
* and others from the C library
*
* @verbatim
* ############################################################################
* # .data # .bss # newlib heap # MSP stack #
* # # # # Reserved by _Min_Stack_Size #
* ############################################################################
* ^-- RAM start ^-- _end _estack, RAM end --^
* @endverbatim
*
* This implementation starts allocating at the '_end' linker symbol
* The '_Min_Stack_Size' linker symbol reserves a memory for the MSP stack
* The implementation considers '_estack' linker symbol to be RAM end
* NOTE: If the MSP stack, at any point during execution, grows larger than the
* reserved size, please increase the '_Min_Stack_Size'.
*
* @param incr Memory size
* @return Pointer to allocated memory
*/
__attribute__((weak)) void *_sbrk(ptrdiff_t incr)
{
extern uint8_t _end; /* Symbol defined in the linker script */
extern uint8_t _estack; /* Symbol defined in the linker script */
extern uint32_t _Min_Stack_Size; /* Symbol defined in the linker script */
const uint32_t stack_limit = (uint32_t)&_estack - (uint32_t)&_Min_Stack_Size;
const uint8_t *max_heap = (uint8_t *)stack_limit;
uint8_t *prev_heap_end;
/* Initialize heap end at first call */
if (NULL == __sbrk_heap_end)
{
__sbrk_heap_end = &_end;
}
/* Protect heap from growing into the reserved MSP stack */
if (__sbrk_heap_end + incr > max_heap)
{
errno = ENOMEM;
return (void *)-1;
}
prev_heap_end = __sbrk_heap_end;
__sbrk_heap_end += incr;
return (void *)prev_heap_end;
}
上面代码进行部分修改,原代码具体可以查看自己的文件。其中返回值prev_heap_end是本次申请内存的起始地址指针,而__sbrk_heap_end则是当前堆顶地址的指针。
下图是RAM_D1域的各区域分布情况
上图及sysmem.c文件中涉及的(全局)变量如下
- _sdata:SRAM_D1空间起始地址,即0x24000000
- _end:存放堆底地址的全局变量(由编译器给出,在ld文件),也是整个堆栈区的最小地址,_sdata~_end范围是全局变量和静态变量
- __sbrk_heap_end:动态指向堆顶的指针,初始值等于_end的地址
- _estack:存放栈顶地址的全局变量(在ld文件),大于_end与工程设置的堆与栈最小值之和,也是RAM_D1空间的上限,即0x24080000
- _Min_Stack_Size:存放设置的栈最小空间大小(0x800)的全局变量
- _Min_Heap_Size:存放设置的堆最小空间大小(0x400)的全局变量
- stack_limit:计算的结果就是栈底地址
- max_heap:stack_limit这个地址常量的指针,即栈底指针,也是理论上的堆顶指针上限
- prev_heap_end:改变前的堆顶指针
_sdata到_end之间是静态存储区,由编译器存放全局变量、静态局部变量等永久使用的数据。_end到_estack之间是堆栈区域,其中栈区由_estack到减去_Min_Stack_Size长度的地址,其他剩余的属于堆区。因此,工程设置的栈最小长度是确定值,而堆最小长度_Min_Heap_Size一般就是参考值。堆从堆底(_end)开始向上生长,栈从栈顶(_estack)开始向下生长。
三、堆栈的检测
为了实现对堆栈使用情况的监测,修改了sysmen.c文件
#include "stm32h7xx_hal.h"
/* Includes */
#include <errno.h>
#include <stdint.h>
extern uint8_t _estack; /* 链接脚本定义的栈顶地址 */
extern uint8_t _end; /* 链接脚本定义的堆底地址 */
extern uint8_t _sdata; /* 链接脚本定义的静态数据区起始地址 */
extern uint32_t _Min_Stack_Size; /* 链接脚本定义的栈长度 */
extern uint32_t _Min_Heap_Size; /* 链接脚本定义的堆长度 */
extern uint8_t *__sbrk_heap_end; /* 堆指针,当前堆顶地址 */
static uint8_t *__sbrk_heap_max; /* 堆指针,历史最高地址 */
/**
* @brief _sbrk() allocates memory to the newlib heap and is used by malloc
* and others from the C library
*
* @verbatim
* ############################################################################
* # .data # .bss # newlib heap # MSP stack #
* # # # # Reserved by _Min_Stack_Size #
* ############################################################################
* ^-- RAM start ^-- _end _estack, RAM end --^
* @endverbatim
*
* This implementation starts allocating at the '_end' linker symbol
* The '_Min_Stack_Size' linker symbol reserves a memory for the MSP stack
* The implementation considers '_estack' linker symbol to be RAM end
* NOTE: If the MSP stack, at any point during execution, grows larger than the
* reserved size, please increase the '_Min_Stack_Size'.
*
* @param incr Memory size
* @return Pointer to allocated memory
*/
void *_sbrk(ptrdiff_t incr)
{
const uint32_t stack_limit = (uint32_t)&_estack - (uint32_t)&_Min_Stack_Size;
const uint8_t *max_heap = (uint8_t *)stack_limit;
uint8_t *prev_heap_end;
/* Initialize heap end at first call */
if (NULL == __sbrk_heap_end)
{
__sbrk_heap_end = &_end;
__sbrk_heap_max = &_end;
}
/* Protect heap from growing into the reserved MSP stack */
if (__sbrk_heap_end + incr > max_heap)
{
errno = ENOMEM;
return (void *)-1;
}
prev_heap_end = __sbrk_heap_end;
__sbrk_heap_end += incr;
if ((uint32_t)__sbrk_heap_end > (uint32_t)__sbrk_heap_max)
{
__sbrk_heap_max = __sbrk_heap_end;
}
return (void *)prev_heap_end;
}
/* 栈开始地址(最高地址) */
void *BSP_MEM_StackTop(void)
{
return (void *)&_estack;
}
/* 栈结束地址(最低地址),堆结束地址(最高地址) */
void *BSP_MEM_StackHeap(void)
{
const uint32_t stack_limit = (uint32_t)&_estack - (uint32_t)&_Min_Stack_Size;
const uint8_t *max_heap = (uint8_t *)stack_limit;
return (void *)max_heap;
}
/* 堆开始地址(最低地址),静态数据结束地址 */
void *BSP_MEM_HeapBottom(void)
{
return (void *)&_end;
}
/* 堆当前地址 */
void *BSP_MEM_HeapPos(void)
{
return (void *)__sbrk_heap_end;
}
/* 堆历史最高地址 */
void *BSP_MEM_HeapMax(void)
{
return (void *)__sbrk_heap_max;
}
/* 栈当前地址 */
void *BSP_MEM_StackPos(void)
{
return (void *)__get_MSP();
}
/* 静态数据开始地址 */
void *BSP_MEM_StaticData(void)
{
return (void *)&_sdata;
}
/* 栈设置最小长度 */
void *BSP_MEM_StackSize(void)
{
return (void *)&_Min_Stack_Size;
}
/* 堆设置最小长度 */
void *BSP_MEM_HeapSize(void)
{
return (void *)&_Min_Heap_Size;
}
增加了几个函数,用于读取堆或者栈的地址。
对于堆最大使用空间的计算比较简单。每次调用_sbrk()函数后,比较保留最大地址的指针__sbrk_heap_max就可以,调用函数BSP_MEM_HeapMax()就行。
对于栈最大使用空间的计算比较困难,原因是无法捕捉到进入子函数后的情况。一个不可靠的方法是用定时中断(尽可能小,但太频繁中断会影响程序运行。记得监测后删掉代码),用函数__get_MSP()读取栈地址后再判断,基本上不是很靠谱了。
#ifdef DEBUG
/* 获取栈指针极限值 */
uint32_t stack_msp = __get_MSP();
if (stack_msp < stack_limit)
{
stack_limit = stack_msp;
}
#endif
主程序定义全局变量
/* 堆栈 */
__attribute__((unused)) uint32_t stack_limit = 0;
打印堆栈信息如下
#ifdef DEBUG
/* 堆栈测试 */
const uint32_t stack_top = (uint32_t)BSP_MEM_StackTop();
const uint32_t stack_heap = (uint32_t)BSP_MEM_StackHeap();
const uint32_t heap_bottom = (uint32_t)BSP_MEM_HeapBottom();
const uint32_t stack_size_min = (uint32_t)BSP_MEM_StackSize();
const uint32_t heap_size_min = (uint32_t)BSP_MEM_HeapSize();
const uint32_t stack_size = stack_top - stack_heap;
const uint32_t heap_size = stack_heap - heap_bottom;
uint32_t heap_usage = 0;
uint32_t stack_usage = 0;
uint32_t heap_usage_max = 0;
uint32_t stack_usage_max = 0;
stack_limit = stack_top;
LOG_DBG("# stack top: 0x%lX\r\n", stack_top);
LOG_DBG("# stack pos: 0x%lX\r\n", (uint32_t)BSP_MEM_StackPos());
LOG_DBG("# stack/heap: 0x%lX\r\n", stack_heap);
LOG_DBG("# heap pos: 0x%lX\r\n", (uint32_t)BSP_MEM_HeapPos());
LOG_DBG("# heap end: 0x%lX\r\n", heap_bottom);
LOG_DBG("# static data: 0x%lX\r\n", (uint32_t)BSP_MEM_StaticData());
LOG_DBG("# stack size: 0x%lX(min 0x%lX)\r\n", stack_size, stack_size_min);
LOG_DBG("# heap size: 0x%lX(min 0x%lX)\r\n", heap_size, heap_size_min);
#endif
动态监测如下
#ifdef DEBUG
/* 堆栈检测 */
heap_usage = ((uint32_t)BSP_MEM_HeapMax() - heap_bottom) * 100 / heap_size;
stack_usage = (stack_top - stack_limit) * 100 / stack_size;
if (heap_usage > heap_usage_max)
{
heap_usage_max = heap_usage;
LOG_DBG("# heap usage: %ld%%\r\n", heap_usage_max);
}
if (stack_usage > stack_usage_max)
{
stack_usage_max = stack_usage;
LOG_DBG("# stack usage: %ld%%\r\n", stack_usage_max);
}
#endif
使用效果
APP init...
stack top: 0x24080000
stack pos: 0x2407FF90
stack/heap: 0x2407F800
heap pos: 0x24006908
heap end: 0x240064E0
static data: 0x24000000
stack size: 0x800(min 0x800)
heap size: 0x79320(min 0x400)
APP loop...
heap usage: 44%
stack usage: 21%
stack usage: 30%
根据监测结果调整堆栈空间大小,保持足够的余量即可。