天天看点

嵌入式 - 代码看加载【1】IDE 生成的代码大小引言生成数据区分参考资料

更新历史

20220318

  1. 首次编辑,简述 keil 工程生成的代码大小,以及存放的空间位置;

文章目录

  • 引言
  • 生成数据区分
    • 各数据含义
      • 为什么 RW 数据有两份?
    • 如何缩减空间
      • 闪存空间
      • RAM 空间
        • FreeRTOS 堆空间简述
  • 参考资料

引言

这篇文章介绍了 keil 工程生成代码的各中 data 的介绍,以及在地址空间中的存放差异。

依旧是【固件防复制系列】的衍射知识总结,在真正讨论程序加载以及堆栈的分配及使用问题前,先以 keil 为实践 IDE 对象来简单描述下代码生成后,各种数据的放置空间,以及若存在空间不足情况下,代码的简单体积缩减。

生成数据区分

各数据含义

嵌入式 - 代码看加载【1】IDE 生成的代码大小引言生成数据区分参考资料

图1 keil 编译信息截图

这里我们可以看到:

这里的数值,单位均为字节 Byte。

先分别来说下,各部分的含义:

  1. Code: 代码大小

    这个很容易理解,也是我们程序最主要的部分;90512 字节

  2. RO-data: Read Only data, 只读性质的数据

    如字符串,const 数据;4184 字节

  3. RW-data: Read Write data, 可读可写性质的数据

    已初始化的全局变量;820 字节

  4. ZI-data: Zero Initialized, 初始值为 0 的数据

    未初始化的全局变量;44140 字节

关于其放置位置,也是根据 MCU 存储介质的性质以及本身的操作类型来决定的,我们知道 MCU 的闪存或 Flash 空间其属于 ROM 存储器,适合存放只读或初始化值非 0 的数据,而 RAM 则适合存放反复修改的数据类型。

(RW-data 会先分配到闪存,但是由于其 RW 的性质,会在 RAM 中进行拷贝。)

则会有下面的分配:

Flash_occupied = Code + RO-data + RW-data = 95516 Bytes = 93 MB
RAM_occupied = RW-data + ZI-data = 44960 Bytes = 43 MB
           

为什么 RW 数据有两份?

RW 我们知道是初始化且非 0 的全局变量,也就是说相当于我们看书,看到了某个进度,需要下次再看,这里就需要记录一下进度。

由于 RAM 是掉电即失,则不会也不适合将这个进度的记录放到 RAM 中(程序的放置或者映射位置是编译器工具链进行设定的,而数据本身的读写性质是工程师进行编码确定的,映射放置时只看本身的读写性质),而又由于程序运行中,会对其进行读写(产品电源周期内的运行态),所以需要一块掉电不易失的存储空间来存储,这就会将其放置到了闪存及 Flash 空间。

相应的, ZI-data 放在了 RAM 区,因为其是未初始化或者初始化值为 0 的变量,则不用占用闪存空间(相当于,知道是新书,也不用记录这次看到哪了,拿起来看就完了),运行中进行默认初始化为 0 .

注:关于加载的知识,详细请见《程序员的自我修养》,具体版本请见参考资料章节。

如何缩减空间

在嵌入式开发领域中,嵌入式资源是宝贵的,不免会遇到代码体积过大或需要进行优化的情况,单单从代码体积来看,可以如何缩小占用空间。

想要有的放矢的缩减尺寸,需要了解哪些数据属于 RO, RW 和 ZI。

闪存空间

  1. 优化代码语法的使用;如替换 while(1) 为 for( ; ; ),这样汇编语句会减少,且不占用寄存器,没有判断和跳转,空间和时间上都具有优势:
while (1);

// while(1) 编译后
mov eax,1
test eax,eax
je foo+23h
jmp foo+18h


for(;;);
// 编译后
jmp foo+23h
           
  1. 结构体成员之间的排列问题;
  2. 不需要的字符串或 const 常量的删减;(RO-data区)
  3. 。。。

RAM 空间

这里的 RAM 一定程度上可以理解为运行时用的空间,涉及到动态建立、修改和访问的数据及数据结构都会放置于这里,比如一些任务控制块、堆栈等。

简化这部分的使用大小:

  1. 避免栈空间的大量使用,会导致内存溢出;
  2. 简化函数调用;
  3. 避免一些不用但是依旧声明的数组等;
  4. 。。。

如我在代码中注释掉了僵尸代码,一个在源文件中申明的全局数组(外部源文件没有引用):

我们可以看到,这个数据为 64 长的 32 字节元素长度的数组,计算下来就需要 256 字节长度的 RAM 空间,这里注释掉后,再次编译:

嵌入式 - 代码看加载【1】IDE 生成的代码大小引言生成数据区分参考资料

图2 keil 编译信息截图(删除冗余代码后)

这里我们对比图1,发现 ZI-data 的大小减少了 44140-43884=256 Byte,也就是我们刚才注释掉的数组大小。

注:这里只是演示了一个很简单的策略,当生成代码大小与预期不一致,有些许出入也是很正常的。

FreeRTOS 堆空间简述

这里简单提一下,如果包含了操作系统的话,比我们产品中使用的是 FreeRTOS,其是在 RAM 区申请了一个较大空间的数组,也就是连续存储空间以作为其本身资源的消耗及管理,这部分是计算在 ZI-data 空间内的,具体大小查看宏定义,比如 v10.0.1 版本的 FreeRTOS 系统堆空间为 15M:

嵌入式 - 代码看加载【1】IDE 生成的代码大小引言生成数据区分参考资料

图3 FreeRTOS 堆空间大小宏定义

嵌入式 - 代码看加载【1】IDE 生成的代码大小引言生成数据区分参考资料

图4 FreeRTOS 不同内存管理策略下的空间申请

结合图 3、4 我们可知,不同的内存管理策略,都是在 15MB 堆空间大小的前提下,至于不同的内存管理策略具体有什么不同,请参考 FreeRTOS 官网手册: Memory Management; 根据自身开发需求来选定策略。

参考资料

  1. stackoverflow - ROM and RAM in ARM;
  2. Arm Cortex-M4 Processor Technical Reference Manual;
  3. FreeRTOS Developer Docs;
  4. 《程序员的自我修养 - 链接、转载与库》,俞甲子 石凡 潘爱民 著,中国工信出版社;