文章目录
- 前言
- [1. 问题背景](#1. 问题背景)
-
- [1. 怀疑是不是进入BOOT了](#1. 怀疑是不是进入BOOT了)
- [2. 进行DEBUG调试](#2. 进行DEBUG调试)
- [3. 使用keil5打断点的时候,一运行就变成了问号了](#3. 使用keil5打断点的时候,一运行就变成了问号了)
- [4. DEBUG能够正常运行,但是直接烧录重启运行不了](#4. DEBUG能够正常运行,但是直接烧录重启运行不了)
- [5. 发现单步调试时,按下了RUN之后经常停留在 BKPT(断点)处](#5. 发现单步调试时,按下了RUN之后经常停留在 BKPT(断点)处)
- [6. 确认程序是不是一起来就挂掉了](#6. 确认程序是不是一起来就挂掉了)
- [7. 确认挂掉的原因](#7. 确认挂掉的原因)
- [2. 堆栈空间如何分配](#2. 堆栈空间如何分配)
-
- [2.1 区分"Flash"和"SRAM"占用](#2.1 区分“Flash”和“SRAM”占用)
- [2.2 理解堆和栈的核心区别](#2.2 理解堆和栈的核心区别)
- [2.3 结合8K SRAM的实际分配方案(分场景)](#2.3 结合8K SRAM的实际分配方案(分场景))
- [2.4 STM32实操修改(启动文件中配置堆和栈)](#2.4 STM32实操修改(启动文件中配置堆和栈))
前言
最近在做一个小的转接板,这个转接板是用那种资源非常小的STM32做的(RAM-8K FLASH-32K)。
程序整体上来说也比较简单,很快就写完了,但是真正去调试的时候发现竟然起不来,但是DEBUG调试时又能正常运行。
于是一点点的排查下来后发现是栈空间不足导致的一启动就崩溃了。最后通过修改栈空间以及使用Microl Libc进行解决了,但是在解决的过程中发现自己对于如何配置栈和堆的空间这部分的知识有点模糊了(实际上之前也碰到过)可能没有系统的去了解或者记录过,时间长了就忘掉了。
所以这次想着把它给整理记录下,方便后续自身回顾也方便大家碰到此类问题时能有个参考。
1. 问题背景
整个问题回过头来看比较简单,但是排查过程中还是碰到了很多坎的,包括被各种现象误导之类的。这里顺便给大家介绍下吧。
拿到板子后,我把编译完成的程序烧录到板子上,但是发现程序跑不起来,连初始化时点亮的LED灯都不亮,串口也没有日志输出。
因为灯也不亮串口也不输出,所以起初我认为可能是硬件有问题。
1. 怀疑是不是进入BOOT了
之前碰到这种程序烧录进去后起都起不来的,第一时间我想到的是不是进入到了BOOT。对于STM32来说BOOT如果是悬浮的,很有可能导致芯片一起来就进入BOOT。
这个时候有一种很好的排查方法,就是通过在KEIL5上进行DEBUG查看起始运行地址能够确认芯片是从BOOT开始起来的还是FLASH开始起来的。
如果是从0x1xxxxx开头的就是进入到BOOT了,我们要先保证程序是从0x8xxxx开始运行的。

2. 进行DEBUG调试
当下程序起都起不来,最好的调试手段就是通过KEIL5进行DEBUG,通过DEBUG我们确认了程序是从0x8xxxxx开始运行的,同时我找了个只能点亮LED的DEMO程序烧录进去后发现能够正常运行,说明硬件基本上应该没啥问题。
然后我们要进行DEBUG调试了,调试的目的是希望确认程序是在哪一步挂掉的。但是在调试时我又碰到了个问题,那就是打不了断点。
3. 使用keil5打断点的时候,一运行就变成了问号了
然后通过查询一些资料,获取到如下信息

最终确认是有个选项没有勾选

4. DEBUG能够正常运行,但是直接烧录重启运行不了
确认了是从0x8xxxx开始运行的了之后,也能够进行了单步调试之后,发现经常调试着就崩溃了。
于是我开始一点点的屏蔽代码查看哪些原因导致的,慢慢的就发现如果我使用了printf打印东西的话那么肯定是跑不起来的,但是如果使用DEBUG进行调试一步步引导就能跑起来。
所以我就在思考为啥单步调试能跑起来,但是直接跑跑不起来,AI给出的提示时单步调试时会对程序进行引导。于是我在找引导和非引导情况下的差异。
整体上来说就是DEBUG单步调试的时候,调试器会帮我们做什么额外的工作,然后我就怀疑是不是我时钟之类的哪里错误了。

5. 发现单步调试时,按下了RUN之后经常停留在 BKPT(断点)处
排查了很久时钟之类的问题后也没有找到具体原因。于是我就继续从单步调试开始入手,看看有没有什么猫腻。然后我就发现我每次点了RUN之后,它还允许我继续点RUN,同时此时停留在了一个BKPT的断点处。需要我点击多次RUN之后才能运行到while(1)处。

最初我怀疑是不是我断点没有清理干净,可后来发现断点全部清理后依然这样,于是继续寻找答案,最终从下方的提示中获取到了灵感。

6. 确认程序是不是一起来就挂掉了
结合AI给出的一些提示怀疑是程序挂掉了,但是在单步调试的时候相当于帮忙辅助运行了一下把挂掉的这一步给走出来了。所以之前没有想到这里,没想到挂掉了的程序单步调试还能跳过去
7. 确认挂掉的原因
机缘巧合之下我弄了个最小的DEMO程序能够点亮LED灯的,然后在其基础上增加我的程序后发现一使用printf就会出现起不来(通过上面确认了是崩溃的问题),之前还在想到底是咋了。
于是查找资料发现可能是printf占据了比较多的栈空间。我们应该使用micro libc

于是更换为使用micro libc后发现可以了。基本上确认了是栈空间不足。然后增加栈空间后修复了。
2. 堆栈空间如何分配
2.1 区分"Flash"和"SRAM"占用
首先要明确:STM32的程序运行涉及两种存储介质,只有部分参数会占用你关注的8K SRAM(静态内存占用),堆和栈是在剩余SRAM中分配的,先拆解我们的Program Size:
Program Size: Code=13176 RO-data=536 RW-data=16 ZI-data=2816
各参数对应存储介质和含义如下:
| 参数 | 存储介质 | 含义 | 是否占用SRAM |
|---|---|---|---|
| Code | Flash | 程序代码(指令、常量函数等),运行时不加载到SRAM | 否 |
| RO-data(Read Only) | Flash | 只读常量(如const char buf[] = "hello"),运行时不加载到SRAM |
否 |
| RW-data(Read Write) | Flash→SRAM | 初始化非0的全局/静态变量(如int a = 10),运行时从Flash拷贝到SRAM并保持 |
是(占用固定SRAM) |
| ZI-data(Zero Initialized) | SRAM | 初始化为0的全局/静态变量、未初始化的全局/静态变量(如int b; static int c),运行时在SRAM中清零并占用空间 |
是(占用固定SRAM) |
计算已占用的静态SRAM大小
堆和栈是动态使用的SRAM,必须从"总SRAM - 静态SRAM占用"中分配,先计算静态占用:
静态SRAM占用 = RW-data + ZI-data = 16 + 2816 = 2832 字节(Byte)
我们的STM32总SRAM为8K,先换算单位:
总SRAM = 8 * 1024 = 8192 字节(Byte)
因此,可用于分配堆和栈的剩余SRAM总量(理论上限):
剩余SRAM = 总SRAM - 静态SRAM占用 = 8192 - 2832 = 5360 字节(约5.23K)
⚠️ 注意:这是理论上限,实际分配时必须预留5%~10%的安全余量 (避免堆/栈生长溢出、硬件外设占用少量SRAM等),实际可分配的堆+栈总和建议不超过5360 * 0.9 ≈ 4824 字节。
2.2 理解堆和栈的核心区别
堆和栈都是动态SRAM,但功能、生长方向、管理方式完全不同,这是分配的核心依据,先理清两者的差异:
| 对比维度 | 栈(Stack) | 堆(Heap) |
|---|---|---|
| 核心用途 | 1. 局部变量(函数内定义的非static变量);2. 函数调用的返回地址、参数传递;3. 中断现场保护(STM32中断嵌套时关键);4. 临时数据存储 | 1. 动态内存分配(malloc()、free()、new/delete);2. 运行时按需创建的缓冲区、数据结构(如链表、动态数组) |
| 管理方式 | 硬件自动分配/释放,无需程序员干预,遵循"先进后出(LIFO)" | 程序员手动申请/释放,若操作不当易造成内存碎片、内存泄漏 |
| 生长方向 | 从高地址向低地址生长(STM32默认栈顶在SRAM高地址) | 从低地址向高地址生长(STM32默认堆底在RW-data/ZI-data之后) |
| 大小特性 | 需求相对固定,波动小(除非有深层函数递归、大局部数组、多中断嵌套) | 需求波动大,随动态内存申请/释放变化,易产生碎片 |
| 溢出后果 | 栈溢出(Stack Overflow):程序跑飞、HardFault中断、数据篡改(后果严重,难以排查) | 堆溢出(Heap Overflow):malloc()返回NULL、内存碎片堆积、程序异常退出 |
核心分配原则
- 栈优先分配,且必须留足余量:栈的溢出后果更严重,且涉及中断、函数调用等基础运行机制,优先保证栈的需求,不建议压缩栈的大小来换取更大堆;
- 堆按需分配,不使用动态内存则设为0 :如果我们的程序中不调用
malloc()/free(),堆可以直接设为0,节省全部剩余SRAM给栈或安全余量; - 堆+栈总和不超过剩余SRAM的90%:预留安全余量,避免两者生长时重叠(栈从高地址向下、堆从低地址向上,中间重叠会直接导致程序崩溃);
- 避免大局部变量放在栈上:若需使用大缓冲区(如1K字节的接收缓存),建议改为全局变量(占用ZI-data)或动态分配(堆上),避免挤占栈空间。
2.3 结合8K SRAM的实际分配方案(分场景)
基于我们的剩余SRAM(约5.23K,安全上限约4.8K),分两种常见场景给出具体分配方案,可直接落地到STM32的启动文件中。
-
场景1:
程序不使用动态内存(无malloc()/free(),推荐嵌入式开发优先选择)
嵌入式开发中,为了稳定性,通常尽量避免动态内存(减少内存泄漏、碎片风险),此时堆可设为0,全部剩余空间优先分配给栈(预留安全余量)。虽然说堆在不使用malloc等动态分配函数时,设置为0都行(亲测确实也行),但是建议还是不要设置为0,因为我们不容易把控自己不使用malloc,但是其它的一些别的组件会不会使用malloc。
堆空间不足导致的malloc失败并不会像栈空间不足那样快速的触发HardFault,而是malloc失败产生的副作用,间接的影响后续某部分业务导致崩溃或者说功能不正常,整体上来说malloc失败带来的后果具有一定的滞后性不容易排查,所以内存空间充足的情况下最好还是预留一些。具体分配
- 堆大小(Heap_Size):
0x200(512字节) - 栈大小(Stack_Size):
0x1000(约4K)
- 堆大小(Heap_Size):
-
场景2 :
程序需要使用动态内存(有
malloc()/free(),如动态创建链表、可变长度缓冲区)此时需要兼顾堆和栈,优先保证栈的基础需求,再分配堆的大小,同时预留安全余量。
具体分配(两种子方案,按需选择)
子方案A:栈优先(中断/函数调用较多,堆需求较小)
- 栈大小(Stack_Size):
0x0C00(3072字节,3K) - 堆大小(Heap_Size):
0x0600(1536字节,1.5K) - 验证:堆+栈=3072+1536=4608字节 ≤ 安全上限4824,预留216字节安全余量。
子方案B:堆适中(堆需求较大,中断/函数调用较少)
- 栈大小(Stack_Size):
0x0800(2048字节,2K) - 堆大小(Heap_Size):
0x1000(4096字节,4K) - 验证:堆+栈=2048+4096=6144字节(⚠️ 超过安全上限4824,不可直接使用),需调整:
优化:堆大小改为0x0E00(3584字节),堆+栈=2048+3584=5632字节(仍超),最终调整为:- 栈大小:
0x0800(2048字节) - 堆大小:
0x0700(1792字节) - 验证:2048+1792=3840字节 ≤ 4824,预留984字节安全余量,兼顾堆需求和安全性。
- 栈大小:
适用场景与注意事项
- 适用:需要动态创建数据结构(如ROS节点动态订阅话题的缓冲区)、可变长度的数据采集;
- 注意1:堆的大小不宜超过2K(在我们的8K SRAM中),避免内存碎片堆积;
- 注意2:中断服务函数中禁止使用
malloc()/free(),会导致内存碎片和中断响应延迟; - 注意3:每次
malloc()后必须检查返回值是否为NULL,避免堆溢出导致程序崩溃。
2.4 STM32实操修改(启动文件中配置堆和栈)
堆和栈的大小在STM32的**启动文件(如startup_stm32f103xb.s、startup_stm32g030f6.s)**中配置,找到以下代码段,修改Stack_Size和Heap_Size即可:
asm
; ************************* 栈配置 *************************
Stack_Size EQU 0x1200 ; 对应场景1的栈大小4608字节,可修改为其他值
AREA STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem SPACE Stack_Size
__initial_sp ; 栈顶地址,由编译器自动计算
; ************************* 堆配置 *************************
Heap_Size EQU 0x0000 ; 对应场景1的堆大小0字节,场景2可修改为0x0600等
AREA HEAP, NOINIT, READWRITE, ALIGN=3
__heap_base
Heap_Mem SPACE Heap_Size
__heap_limit
。