当前位置:首页 > 技术学院 > 技术前线
[导读]在STM32嵌入式开发中,RAM(随机存取存储器)是程序运行时存储动态数据、堆栈、全局变量的核心资源,直接决定了程序能实现的功能复杂度。很多开发者都遇到过莫名的程序崩溃、硬件异常,追根溯源往往是RAM分配错误、占用溢出:堆栈溢出覆盖了全局变量,全局变量过多超出芯片RAM容量,动态内存分配碎片化导致分配失败……这些问题排查难度大,往往需要对STM32的RAM分配规则有清晰的理解才能快速解决。

STM32嵌入式开发中,RAM(随机存取存储器)是程序运行时存储动态数据、堆栈、全局变量的核心资源,直接决定了程序能实现的功能复杂度。很多开发者都遇到过莫名的程序崩溃、硬件异常,追根溯源往往是RAM分配错误、占用溢出:堆栈溢出覆盖了全局变量,全局变量过多超出芯片RAM容量,动态内存分配碎片化导致分配失败……这些问题排查难度大,往往需要对STM32的RAM分配规则有清晰的理解才能快速解决。

本文从STM32的RAM硬件结构出发,梳理链接脚本中RAM的分区规则、不同类型数据的RAM占用规律,结合实战经验分析RAM优化和溢出排查技巧,帮助开发者理清STM32的RAM分配逻辑,避开常见的内存陷阱。

一、STM32 RAM的硬件基础与地址空间划分

STM32系列MCU的RAM都是片上集成的SRAM(静态随机存取存储器),不同系列型号的RAM容量差异很大:从STM32F103C8T6的20KB,到STM32H7系列的高达1MB,开发者选型时需要根据项目需求选择合适容量。

从地址空间来看,STM32采用32位地址空间,片上SRAM有固定的地址区间,以最常用的STM32F103系列为例:

片上SRAM基地址是0x20000000,总容量20KB(C8T6)的话,地址范围是0x20000000 ~ 0x20004FFF,刚好覆盖20KB空间。

部分大容量型号比如STM32F4、STM32H7还有多块SRAM:比如STM32F407有192KB片上SRAM,分为0x20000000开头的112KB(主SRAM)和0x2001C000开头的64KB(CCM SRAM),还有部分型号外接SDRAM,地址空间分配到0xC0000000开头的外部存储区域。

需要注意的是,CCM SRAM(内核耦合存储器)只能被CPU访问,不能被DMA访问,如果把DMA需要的缓冲区放到CCM里,DMA会读取到错误数据,这是新手容易踩的硬件级坑。而外部SDRAM容量大,但访问速度比片上SRAM慢,适合存大缓存、图片数据,不适合放对实时性要求高的栈和全局变量。

二、链接脚本定义的RAM分区逻辑

STM32的RAM分配规则是由链接脚本(.ld文件,STM32CubeIDE、GCC编译器使用)或者分散加载文件(.sct,MDK使用)定义的,编译器根据这个文件把不同类型的变量放到不同的RAM区域,我们以最常用的GCC链接脚本为例,拆解STM32的RAM分区:

一个典型的STM32 RAM区域定义如下:

MEMORY

{

RAM(xrw) : ORIGIN = 0x20000000, LENGTH = 20K /* 片上SRAM总大小 */

FLASH(rx) : ORIGIN = 0x08000000, LENGTH = 128K

}

/* 段定义 */

SECTIONS

{

.vectors : ALIGN(4) { *(.vectors) } > FLASH

.text : ALIGN(4) { *(.text) } > FLASH /* 代码段放FLASH */

.rodata : ALIGN(4) { *(.rodata) } > FLASH /* 只读常量放FLASH */

/* RAM中的各个段 */

.data ALIGN(4) : {

_sdata = .;

*(.data)

*(.data*)

. = ALIGN(4);

_edata = .;

} > RAM

.bss ALIGN(4) {

_sbss = .;

*(.bss)

*(.bss*)

*(COMMON)

. = ALIGN(4);

_ebss = .;

} > RAM

_end = .;

__bss_end__ = _ebss;

.heap ALIGN(4) : {

__heap_start__ = .;

KEEP(*(.heap))

__heap_end__ = .;

__heap_end__ = ALIGN(8) - __stack_size__;

} > RAM

.stack ALIGN(4) : {

__stack_start__ = .;

KEEP(*(.stack))

__stack_end__ = .;

} > RAM

}

从这个链接脚本可以看出,STM32的RAM从低地址到高地址依次分为五个部分:初始化的全局/静态变量(.data段)、未初始化/初始化为0的全局/静态变量(.bss段)、堆(heap,动态内存分配)、栈(stack,函数调用、局部变量存储),我们逐个分析每个区域的作用和占用规律:

1. .data段:已初始化的全局变量和静态变量

.data段存放的是程序启动时已经有初始值,且初始值不为0的全局变量和静态变量,比如:

// 全局变量,非0初始值,放在.data段

uint32_t global_buffer = {1, 2, 3, 4};

// 静态全局变量,放在.data段

static uint16_t static_var = 0x1234;

void func() {

// 静态局部变量,不管放哪里,只要非0初始化,都放在.data段

static uint32_t local_static = 0x5678;

}

.data段的内容需要掉电保存,因此它的初始值会存在FLASH中,程序启动的时候,由启动文件(startup_stm32xxx.s)把初始值从FLASH拷贝到RAM的.data段,这样程序运行的时候就能拿到正确的初始值。因此.data段会同时占用FLASH(存初始值)和RAM(运行时存储)两部分空间,这点要注意。

2. .bss段:未初始化和零初始化的全局静态变量

.bss段存放的是未初始化,或者初始化为0的全局变量和静态变量,比如:

// 未初始化,放在.bss段

uint32_t uninit_global;

// 初始化为0,放在.bss段

uint32_t zero_global = 0;

void func() {

static uint32_t uninit_static; // 放在.bss段

}

和.data段不同,.bss段不需要存储初始值,程序启动的时候,启动文件只需要把整个.bss段清零就可以,因此.bss段只占用RAM空间,不占用FLASH空间,这也是为什么把初始为0的变量放到这里能节省FLASH的原因。

这里有一个常见误区:很多人认为未初始化的全局变量不占用RAM,其实不对,STM32的链接规则中,所有全局静态变量都会分配RAM空间,不管你有没有初始化,只是初始为0的放在.bss,不需要占FLASH而已。

3. 堆(Heap):动态内存分配区域

堆是用来做动态内存分配的,也就是我们调用malloc()、free()或者RTOS的rt_malloc()的时候,内存管理器从堆划分空间给程序使用。堆的位置在.bss段之后,大小一般由链接脚本定义,从.bss结束地址开始,到栈起始地址之前,都是堆的可用空间。

堆是从低地址往高地址增长的,每次调用malloc就从堆里拿出一块空闲空间分配给用户,free之后释放回堆管理器。动态分配的好处是用的时候才分配,不用可以释放,提高内存利用率,但如果频繁分配释放小块内存,容易产生内存碎片,导致明明总空闲内存足够,却分配不出连续的大块内存。

4. 栈(Stack):局部变量、函数调用上下文存储

栈是STM32 RAM中非常重要的区域,用来存放:

函数内部的局部变量(非静态的局部变量,静态局部变量存在.data/.bss);

函数调用的时候的返回地址,保存CPU寄存器上下文;

中断发生的时候,保存当前任务的CPU寄存器上下文;

函数传参的时候的参数传递,超过4个参数的参数会放到栈上。

栈和堆的增长方向相反,栈是从RAM高地址往低地址增长的,STM32的栈起始地址默认是RAM的最高地址,启动的时候栈指针(SP)初始化为RAM的末尾地址,每次入栈SP减小,出栈SP增大,符合STM32的栈增长规则。

栈的大小是开发者在链接脚本或者启动文件里配置的,默认情况下STM32CubeMX生成的工程默认栈大小是1KB或者2KB,对于很多程序来说,如果有递归函数、大局部数组,很容易出现栈溢出。

我们以STM32F103C8T6的20KB RAM为例,算一个典型的RAM分配:

.data段占用1KB,.bss段占用3KB,合计4KB;

栈配置为2KB,从RAM末尾往下,占用2KB;

剩下的14KB都分给堆,用于动态分配,符合一般程序的分配规律。

三、不同类型变量的RAM占用规律总结

我们把STM32中不同类型变量的RAM占用情况做一个总结,方便开发者快速计算RAM占用:

变量类型存储位置是否占用RAM是否占用FLASH

非0初始化全局变量.data段是是(存初始值)

0初始化/未初始化全局变量.bss段是否

非0初始化静态全局/局部变量.data段是是

0初始化/未初始化静态全局/局部变量.bss段是否

函数局部变量(非静态)栈是(运行时占用)否

动态分配内存(malloc)堆是(分配后占用)否

常量、字符串常量.rodata(FLASH)否是

程序代码.text(FLASH)否是

这里有几个容易混淆的点:

大数组放在哪里? 很多开发者习惯在函数里面定义一个大数组uint8_t buffer,这个数组是放在栈上的,如果数组大小超过栈剩余空间,就会直接导致栈溢出,程序崩溃。因此超过几百字节的大数组,最好定义为全局变量,或者动态分配,不要放在栈上。

字符串常量存在哪里? 比如char *str = "hello world";,这个字符串"hello world"是存在FLASH的.rodata段,只占用FLASH,不占用RAM,指针str本身如果是局部变量就占栈空间,如果是全局变量就占.data/.bss空间。

const常量放在哪里? const uint32_t const_val = 123;,如果是全局const常量,会放到FLASH的.rodata段,不占用RAM,如果是局部const常量,运行时还是放在栈上,占用RAM。

四、查看RAM占用的常用方法

开发过程中,我们需要随时查看程序的RAM占用情况,不同开发环境有不同的查看方法:

1. MDK-ARM环境:编译完成直接输出占用

MDK编译完成后,会在编译日志中直接输出RAM和FLASH的占用情况,格式如下:

Program Size: Code=12345 RO-data=5678 RW-data=1234 ZI-data=4567

其中:

Code是代码段,占FLASH;

RO-data(只读数据)占FLASH;

RW-data就是.data段大小,同时占用FLASH和RAM;

ZI-data就是.bss段大小,只占用RAM;

所以总RAM占用就是 RW-data + ZI-data,剩下的RAM空间是给堆和栈用的,这个数字可以直接从编译日志拿到,非常方便。

2. STM32CubeIDE/GCC环境:查看链接map文件

GCC编译完成后,会生成一个.map后缀的map文件,里面详细记录了每个段的地址、大小,以及每个函数、变量的存储位置和大小,我们可以在map文件中搜索_sdata、_edata、_sbss、_ebss,就能算出.data和.bss的大小,总RAM占用等于_ebss - _sdata,堆和栈可用空间等于RAM总大小 - (_ebss - RAM起始地址)。

也可以编译完成后在IDE的“Memory Usage”视图中看到直观的RAM占用饼图,和MDK一样会输出总占用。

3. 运行时查看堆剩余空间

如果使用动态内存分配,想要知道运行时堆还剩多少空间,可以调用对应API:

标准库:可以用mallinfo()函数,里面的fordblks字段就是堆空闲大小;

RT-Thread:调用rt_memory_info()可以得到堆的总大小和空闲大小;

FreeRTOS:调用xPortGetFreeHeapSize()可以得到堆空闲大小。

五、RAM溢出与常见问题排查

RAM溢出是STM32开发中最常见的问题,不同区域溢出的表现不同,排查方法也不一样:

1. 栈溢出:最常见的崩溃原因

栈溢出一般是因为:栈配置太小、函数嵌套太深(递归)、函数内定义了太大的局部数组,溢出后会覆盖堆或者.bss段的数据,表现为:程序随机崩溃、全局变量被莫名修改、跑飞进入HardFault异常。

排查栈溢出的技巧:

先看编译后栈配置的大小,默认的1~2KB如果不够就改大,比如改到4KB,看看问题是否解决;

检查有没有大局部数组,把超过100字节的数组改成全局或者动态分配;

检查有没有递归函数,递归深度不要太大,递归深度乘以每个函数的栈占用不要超过栈总大小;

出现HardFault后,查栈指针的位置,看SP是否跑到了RAM地址范围外,就能确认是不是栈溢出。

一个常用的技巧:编译完成后,总RAM减去.data+.bss再减去堆的大小,剩下的就是栈大小,如果这个值是负数,说明链接的时候就已经超出RAM了,编译会提示警告,一定要注意看编译输出。

2. 堆溢出:动态分配用完内存

堆溢出就是malloc分配的时候,没有足够的空闲内存,返回NULL,如果程序没有判断返回值,直接使用空指针,就会导致程序崩溃。如果频繁分配释放,还会出现内存碎片化:总空闲内存足够,但没有连续的大块内存,导致分配失败。

解决堆溢出的方法:

合理调整堆和栈的大小,如果程序用很多动态分配,就把堆改大,栈不需要太大就缩小,把RAM空间让给堆;

减少不必要的动态分配,固定大小的缓冲区尽量用全局静态分配;

使用内存池管理固定大小的块,避免碎片化;

释放内存后把指针置空,避免野指针,重复释放导致堆管理器损坏。

3. 总RAM超出:链接阶段就能发现

如果全局变量加起来的大小已经超过了RAM总容量,链接的时候编译器就会报错,提示“region RAM overflowed with stack”或者类似错误,这时候需要优化RAM占用,去掉不必要的全局变量,缩小缓冲区大小,或者换更大RAM的型号。

六、RAM占用优化技巧

当RAM不够用的时候,可以用这些技巧优化RAM占用:

把大只读数据放到FLASH:比如字库、配置表这些只读不修改的数据,加上const关键字,放到FLASH的.rodata段,不占用RAM,这是最常用的优化技巧,很多开发者忘记加const,把大表放到RAM浪费空间。

复用缓冲区:不同功能模块复用同一个大缓冲区,比如串口接收、SPI接收不用各自开缓冲区,分时复用同一个,节省空间。

动态分配随用随取:不需要一直占用的内存,使用完及时释放,不要一直占用。

用合适的数据类型:不要都用int,能使用uint8_t的就不用uint32_t,节省空间,比如标志位可以用位域或者uint8_t,不用32位整型。

使用片上其他SRAM或者外部SDRAM:比如STM32F4的CCM SRAM可以放不需要DMA访问的数据,把主SRAM空间让出来,大的数据比如图片可以放到外部SDRAM。

裁剪不必要的功能:关闭RTOS不需要的功能,比如不用的调试功能、空闲任务栈不要配太大,每个线程的栈大小根据实际需求配置,不要都配成最大。

总结

STM32的RAM分配规则本质是链接脚本定义的分段存储,不同类型变量放在不同区域,核心逻辑是:从低地址到高地址依次是.data、.bss、堆、栈,分别存放不同类型的运行时数据,只要理清每个区域的作用,就能快速计算RAM占用,排查溢出问题。开发过程中养成编译后看RAM占用的习惯,提前规划堆和栈的大小,把大数组、只读数据放到合适的位置,就能避开大多数RAM相关的问题,让程序稳定运行。

对于开发者来说,理解RAM分配不仅能帮我们解决崩溃问题,还能在有限的RAM空间里实现更多功能,尤其对于小RAM型号比如STM32F103C8T6的20KB RAM,合理的分配和优化能让它放下比预期多很多的功能,充分发挥芯片的潜力。 以上是根据你的要求生成的内容,如需修改可继续提出。

本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除( 邮箱:macysun@21ic.com )。
换一批
延伸阅读

在STM32嵌入式开发中,精确延时是非常基础但又极其关键的功能。无论是驱动单总线传感器(比如DS18B20)、控制LCD屏幕时序、还是生成精确的脉冲信号,都需要用到微秒级甚至纳秒级精度的延时。很多新手刚开始使用STM32...

关键字: STM32 嵌入式

在嵌入式系统开发中,RAM(随机存取存储器)是决定系统性能、功能上限的核心资源。和通用PC不同,嵌入式系统的RAM资源往往非常紧张:从低端8位单片机的几百字节,到中高端MCU的几十KB几百KB,哪怕是高端嵌入式SOC也不...

关键字: RAM SRAM

在嵌入式STM32开发中,程序崩溃是开发者最常遇到也最头疼的问题之一:程序运行中突然跑飞、进入HardFault中断,开发者往往只能靠加打印猜位置,排查一个bug可能需要几天时间。这时候,Backtrace功能就像是嵌入...

关键字: 嵌入式STM32 STM32

当一个项目需要在STM32上运行FreeRTOS时,摆在工程师面前的不止一条路。STM32CubeMX图形化配置工具的出现,让RTOS的集成从“手工作坊”变成了“流水线作业”。但这是否意味着传统的手写移植已经过时?答案并...

关键字: STM32 FreeRTOS

一块STM32G431、三颗IRFS7530 MOSFET、两个1mΩ采样电阻——这就是一台20A无人机电调的全部硬件。但从上电到电机平稳空转,中间隔着寄存器配置、ADC同步触发、Park变换、PI调参、SVPWM生成五...

关键字: STM32 无人机 FOC电调

该项目是一个基于 RT-Spark STM32 开发板的实时、裸机硬件接口。它充当了一个交互式的控制面板,将物理世界与数字世界连接起来。通过读取来自一个 5 个方向操纵杆的输入,该系统会立即触发数字逻辑来控制外部独立的...

关键字: 液晶显示屏 FSMC 开发板 STM32

在工业现场,Modbus凭借其简单性成为事实标准。在STM32上实现Modbus,核心难点在于RTU帧同步与TCP粘包处理。本文将基于FreeModbus库,详解STM32上Modbus RTU与TCP的完整实现,并提供...

关键字: Modbus RTU TCP协议 STM32

在STM32开发中,HAL库(Hardware Abstraction Layer)与LL库(Low-layer)的选择常引发争议。HAL库开发快但体积大,LL库性能强但更底层。本文通过实测数据对比两者差异,并提供工程级...

关键字: STM32 HAL库 LL库

在基于STM32的嵌入式开发中,RAM(随机存取存储器)是影响系统性能和稳定性的核心资源之一。与PC端动辄数GB的内存不同,STM32系列微控制器的RAM容量通常在几KB到几百KB之间,部分高端型号可达数MB。有限的资源...

关键字: RAM 微控制器

在嵌入式开发的世界里,MCU的RAM资源如同沙漠中的绿洲,珍贵且有限。当项目推进到后期,功能不断叠加,代码量持续增长,RAM空间告急的警报往往会突然响起。这不仅可能导致系统崩溃、功能异常,甚至可能让整个项目陷入停滞。

关键字: MCU RAM
关闭