最近在调试一个模块的时候,先是IAR配置的环境,操作很正常,在keil简单移植操作以后老是在函数调用的时候,导致log实时数据出问题,找了好多天,修改了好几个版本,最后面终于确认在局部变量的内存溢出,具体细节是函数体内部分配一个2K+2byte的局部变量,超过启动文件分配的栈空间的大小导致了内存溢出,程序有时候会进入hardfault有时候正差运行但都伴随log数据失败。
溢出的原因我们找到了,但是为什么2k的buffer分配内存就会溢出呢,要知道手里用的MCU的FLASH有128k,RAM有16K大小,理论上使用是足够的
这要从程序编译成代码的原理进行分析,程序的几个预编译、编译、汇编、链接几个步骤就不多做赘述,不了解的大家可以自行搜索,言归正传,程序在转为二进制的代码时候不仅在逻辑上语言类型上进行转换,在一些变量上也通过不同的修饰符来进行分配其在代码中或者在程序运行的时候所处的位置,这个时候出现了ROM区以及RAM区的变量。先用一个例子进行展示编译器如何识别定位变量的位置:
int *p1,*p2; int val_b = 2; //全局变量 栈(程序自动分配 自动释放) int val_e ; // 未初始化的全局变量 栈 char s[]="abc"; //全局变量 栈 char *p3= "1234567"; //全局变量 在文字常量区ROM(FALSH) const int val_c = 3 ; // const只读数据段 全局变量 ROM(FALSH) static int val_a=0 ; //全局变量 初始化的静态初始化区 栈 static int val_d ; // 未初始化的静态变量 栈 p1= (char *)malloc(10); //全局变量 堆区(由程序员分配) int main() //此处按照普通函数进行解释 { static int val_f = 5; // 初始化的局部静态变量 栈 static int val_g; //未初始化局部静态变量 栈 int val_h = 6; //初始化局部变量 栈 int val_i; //未初始化局部变量 栈 const int val_j = 7; //const局部变量 栈 p2= (char *)malloc(10); //局部变量堆区(由程序员分配) while(1) { } free(p2); return 0; } free(p1);
除去ROM区变量剩下的都在RAM区,其余变量在芯片上电之后在RAM执行。
①static无论是全局变量还是局部变量都存储在全局/静态区域,在编译期就为其分配内存,在程序结束时释放,例如:val_a、val_d、val_f、val_g。
②const全局变量存储在只读数据段,编译期最初将其保存在符号表中,第一次使用时为其分配内存,在程序结束时释放,例如:val_c;const局部变量存储在栈中,代码块结束时释放,例如:val_j。
③全局变量存储在全局/静态区域,在编译期为其分配内存,在程序结束时释放,例如:val_b、val_e。
④局部变量存储在栈中,代码块结束时释放,例如:val_h、val_i。
注:当全局变量和静态局部变量未赋初值时,系统自动置为0
编译之后内存的情况
(1)栈区(stack):由编译器自动分配和释放,存放函数的参数值、局部变量的值等,其操作方式类似于数据结构中的栈。
(2)堆区(heap):一般由程序员分配和释放,若程序员不释放,程序结束时可能由操作系统回收。分配方式类似于数据结构中的链表。
(3)全局区(静态区)(static):全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后由系统自动释放。
(4)文字常量区:常量字符串就是存放在这里的。
(5)程序代码区:存放函数体的二进制代码。
单片机的堆和栈是分配在RAM里的,有可能是内部也有可能是外部,可以读写;栈:存函数的临时变量,即局部变量,函数返回时随时有可能被其他函数栈用。所以栈是一种分时轮流使用的存储区,编译器里定义的Stack_Size,是为了限定函数的局部数据活动的范围,超过这个范围,程序就会跑飞,也就是栈溢出;
Stack_Size不影响Hex,更不影响Hex怎么运行的,只是在Debug调试时会提示错。栈溢出也有是超过了边界进行所以单片机代码在函数里定义一个大数组 int buf[2048],栈要是小于2K= 2048=0x400会导致内存溢出,导致很多不可知的bug,例如溢出到其他外设的寄存器进行操作。
堆:存的是全局变量,这变量理论上是所有函数都可以访问的,全局变量有的有初始值,但这个值不是存在RAM里的,是存在Hex里,下载到Flash里,上电由代码(编译器生成的汇编代码)搬过去的。上电的时候就分配一块很大内存大的RAM(Heap_Size),作为(malloc_init),别人用只能通过 (malloc)去分配,用完还得(free),否则一不小心内存都使用光了。所以一旦有需要分配很大的内存是编译器里必须定义Heap_Size,否则malloc 也没有实际效果。
从这里可以想到函数体内部分配临时变量超过限制,可能需要调整 STACK SIZE。
而ST系列的堆栈一般都在在头文件或者在IDE进行配置(IAR),本人习惯在在头文件去看堆栈的大小,一般在在STM32Fxxxxx.s文件中(对应的头文件)
修改Stack_Size EQU 0x00000200 (keil 默认1k)
修改Heap_Size EQU 0x00000400 (keil 默认2k)
在IDE开发的时候我们有一处可以很方便的看到我们程序使用内存空间的大小
就是在下方的编译窗口信息。
eg:
keil
IAR
eclipse
其中Code 或者 .text为程序代码部分
RO-data 表示 程序定义的常量 const temp;
RW-data 或者 .data表示 已初始化的全局变量
ZI-data 或者 .bss表示 未初始化的全局变量
Total RO Size (Code + RO Data)
Total RW Size (RW Data + ZI Data)
Total ROM Size (Code + RO Data + RW Data)
**这块map文件详解以及数据分配地方的详解推荐大家看哈[strongerhuang的CSDN或者公众号]**讲的很详细。
也可以关注公众号嵌入式Linux,这些大佬都写的很详细。
前情提要就是这些帮助大家理解问题
我也是通过看文章以及和几位大佬聊天测试之后发现了问题:
通过以上了解我用了三种方法来解决我的问题。
1. 通过const 修饰,将分配的内存置于FLASH空间执行
2. 扩大堆区分配最小值实现
3. 扩大栈区分配最小值实现
注释:前两种都是在RAM分配实现,后一种是在FLASH空间实现(本次解决问题的测试环境都在keil下进行)
初始化程序什么都没有操作的情况下 程序容量大小
第一种方法: 我还是依旧在函数内部定义一个buffer,首先判断buffer被分配到哪个区(在ROM 区)
发现RO Data明显增加2K,所以确认是在ROM区。放置于ROM(FLASH)区一般要需要程序读写要求不高的数据,因为成本的问题,一般MCU 的ROM(FLASH)区远大于RAM,放置于FLASH基本不需要担心溢出等的问题。由于我的数据是固定的,所以我用const也可以进行成功操作。
但是切记放于ROM代码只读不可写,如果你的变量需要读写操作,此种操作是不适合的,可以看接下来的其他两种实现方法。
设置完也可以在调试call+stack或者watch窗口看数据的地址:
ST一般0x08000000这样0x8打头的地址为FASH区(ROM),0x20000000 这样0x2打头的地址为RAM区,不同的芯片请看对应的数据手册
第二种方法: 通过在堆区进行设置大小,从而使空间足够用(RAM区)
发现其中ZI-data明显变大置于RAM 区的空间
记得前面提到过,在stm32的启动文件中有堆栈大小的分配,但是默认比较小一个1k一个2k,当我们分配2K大小的一个buffer的时候空间是不够的,所以我们要去把堆的空间变大。
启动文件的堆栈默认值:
此次需要malloc一个2k 的内存,属于heap区,而初始化默认的堆区的分配大小只有1K,所以需要需要进行安排需要修改,此处堆区所需的内存大家可以设置稍微大些,不要刚刚好,也容易出现问题。
第三种方法:通过在栈区进行设置大小,从而使空间足够用(RAM区)
同样的char定义一个buffer之后可以看到RAM空间变大,而定义的变量属于栈区所以我们需要增加栈的空间分配。
此次需要一个2k 的buffer,属于stack区,而初始化默认的堆区的分配大小只有2K,所以需要需要进行安排需要修改,此处栈区所需的内存大家也可以设置稍微大些
按照空间大小修改之后情况:
程序成功运行,希望对大家有用,谢谢。