目录

获取 ARM 编译后的 ROM 及 RAM 大小方法及原理

前言

● 我们常用的 RT-Thread、Free OS 这类的操作系统都有动态内存管理,这段内存是编译后剩余的空间。那么是如何获知 Keil 编译器编译后剩余 RAM 的大小呢?
● 前些年我做了 IAP 在线升级功能,其中需要程序能获知自己大小。那么又是如何通过特殊语法获取的呢?

知识

1、RO段、RW段、ZI段 的组成:
名称 组成
RO段 是程序中的指令(Code)和常量(RO-Data)
RW段 是程序中已初始化的变量(RW-Data)
ZI段 是程序中未初始化的变量(ZI-Data)(默认初始化为0)
2、RO段、RW段、ZI段 的分布:

../img/20210318_21.jpg

方法

1、获取 RO段、RW段、ZI段 地址及大小的方法:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/*--------------IROM1--------------*/
extern int Image$$ER_IROM1$$RO$$Base;
extern int Image$$ER_IROM1$$RO$$Limit;
extern int Image$$ER_IROM1$$RO$$Length;
#define LINKER_VAR_RO_START ((void *)&Image$$ER_IROM1$$RO$$Base)   //RO段起始地址
#define LINKER_VAR_RO_LIMIT ((void *)&Image$$ER_IROM1$$RO$$Limit)  //RO段结尾地址的后一个地址
#define LINKER_VAR_RO_SIZE  ((void *)&Image$$ER_IROM1$$RO$$Length) //RO段正使用空间的大小(即 _LIMIT - _START)

/*--------------IRAM1--------------*/
extern int Image$$RW_IRAM1$$RW$$Base;
extern int Image$$RW_IRAM1$$RW$$Limit;
extern int Image$$RW_IRAM1$$RW$$Length;
#define LINKER_VAR_RW_START ((void *)&Image$$RW_IRAM1$$RW$$Base)   //RW段起始地址
#define LINKER_VAR_RW_LIMIT ((void *)&Image$$RW_IRAM1$$RW$$Limit)  //RW段结尾地址的后一个地址
#define LINKER_VAR_RW_SIZE  ((void *)&Image$$RW_IRAM1$$RW$$Length) //RW段正使用空间的大小(即 _LIMIT - _START)

extern int Image$$RW_IRAM1$$ZI$$Base;
extern int Image$$RW_IRAM1$$ZI$$Limit;
extern int Image$$RW_IRAM1$$ZI$$Length;
#define LINKER_VAR_ZI_START ((void *)&Image$$RW_IRAM1$$ZI$$Base)   //ZI段起始地址
#define LINKER_VAR_ZI_LIMIT ((void *)&Image$$RW_IRAM1$$ZI$$Limit)  //ZI段结尾地址的后一个地址
#define LINKER_VAR_ZI_SIZE  ((void *)&Image$$RW_IRAM1$$ZI$$Length) //ZI段正使用空间的大小(即 _LIMIT - _START)

/*------------应用例子-------------*/
ro_segment_size = (unsigned int)LINKER_VAR_RO_SIZE; //RO段大小
rw_segment_size = (unsigned int)LINKER_VAR_RW_SIZE; //RW段大小
zi_segment_size = (unsigned int)LINKER_VAR_ZI_SIZE; //ZI段大小
zi_segment_size = (unsigned int)LINKER_VAR_ZI_LIMIT - (unsigned int)LINKER_VAR_ZI_START; //ZI段大小
p_zi       = LINKER_VAR_ZI_START;               //ZI段起始地址
p_free_ram = LINKER_VAR_ZI_LIMIT;               //空闲RAM起始地址
rom_size   = ro_segment_size + rw_segment_size; //编译生成bin文件的大小
2、实测 rom_size 与实际 bin 文件大小不同原因:

那是因为编译器做了优化压缩,个人猜想是将RW段中初始化为 0 的变量优化掉,将它移到ZI段。大小差异如下图:

../img/20210318_31.jpg

扩展

1、各种编译器的识别宏
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
/////////////////////////////////////////////////
//用于 ARM 处理器程序识别不同编译器,这些宏用于隐
//藏可由多个编译器编译的代码中与编译器相关的内容:
/////////////////////////////////////////////////

// 1、MDK-ARM 使用编译器的宏名称(ARM RealView)
#if   defined(__CC_ARM) || defined(__CLANG_ARM)

// 2、IAR-ARM 使用编译器的宏名称(IAR EWARM)
#elif defined(__ICCARM__)

// 3、GNU-gcc 使用编译器的宏名称(GNU Compiler Collection)
#elif defined(__GNUC__)

#endif
2、gcc 环境下获取 ROM、RAM 编译大小的方法
类型
说明
text 代码(Code)和常量(RO-Data)的大小(ROM)
data 已初始化的全局变量(global)和静态变量(static)的大小(RAM/ROM)
bss 未初始化的全局变量(global)和静态变量(static)的大小(RAM)。 其值一般默认为零!从 STM32F103 官方的 .ld 链接文件生成的 .map 文件查到,其包括:heap(堆)和 stack (栈)的大小!
dec text + data + bss 的总和值(十进制表示)
hex text + data + bss 的总和值(十六进制表示)
补充 1、程序固件大小(ROM):text + data
2、程序已用内存(RAM):data + bss(包括:heap 和 stack 的大小)
1
2
3
4
5
6
7
8
// 在 C 语言中,通过如下方式获取某个分段
// 的起始与结束地址,再由计算可得出大小。
// 具体变量名称在链接脚本中找出!!!!!
extern int _sbss;
extern int _ebss;
#define LINKER_VAR_ZI_START ((void *)&_sbss)
#define LINKER_VAR_ZI_LIMIT ((void *)&_ebss)
#define LINKER_VAR_ZI_SIZE  (((void *)&_ebss) - ((void *)&_sbss))

关于 text、data、bss、heap、stack 的分布更多知识,请查阅《linux-STM32F开发-makefile 构建与使用》→【提升】→【关于 text、data、bss、heap、stack 的分布】!

3、MCU 的堆栈及静态区
RAM 区域
地址分布
说明
(1)静态区(static) 在低地址 存储包括未初化、已初始化的全局变量和静态变量的一块区域!
(2)堆区(heap) 在中地址 一般由程序员分配和释放,若程序员不释放,程序结束时可能由操作系统回收。分配方式类似于数据结构中的链表。通过malloc函数申请,通过free函数释放!堆:向高地址扩展!
(3)栈区(stack) 在高地址 由编译器自动分配和释放,存放函数的参数值、局部变量的值等,其操作方式类似于数据结构中的栈。函数调用及函数退出时自动处理!栈:向低地址扩展!
4、scatter 分散加载文件

keil 默认使用自定义链接文件 link.sct(scatter 分散加载文件。扩展:对应 gcc 编译器就是链接脚本,文件扩展名一般为 .ld,链接参数 -T,用法如:-Txxx.ld),简单的 IAP 则建议不用它,使用 keil 图形化配置即可(下图)。更多 scatter 分散加载文件知识以后再深入了解,可参考下面几篇网文:
网文 1:《如何在C代码中获取编译后的BIN文件的大小
网文 2:《keil如何生成scatter文件
网文 3:《试图搞懂MDK程序下载到flash(二)–分散加载文件scatter》《搞懂MDK的分散加载文件

../img/20210318_41.jpg