STM32H7无RTOS应用堆栈机制与检测

摘要:单片机堆栈溢出会引发不可预知的错误。本文探讨了基于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%

根据监测结果调整堆栈空间大小,保持足够的余量即可。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值