Program Size 参数全解析
本文深入解析嵌入式开发中编译后输出的Program Size参数(Code、RO-data、RW-data、ZI-data)及其对存储资源的影响。这些参数反映了程序在Flash和RAM中的占用情况:Code和RO-data存储在Flash中,RW-data需同时占用Flash和RAM空间,ZI-data仅占用RAM。文章详细解释了各参数的定义、存储机制及实际应用价值,包括硬件选型验证、内存溢出问题
嵌入式开发必备:Program Size 参数全解析
前言
使用MDK 进行·程序编译之后会看到如下的信息,刚接触的时候我就在想,这是啥?有啥用?,工作了一段时间之后也明白了,这些信息代表是啥,今天这篇文章算是一个总结,写给当年懵懵懂懂的自己,随有此文。
嵌入式开发中,编译完成后编译器通常会输出类似 Program Size: Code=8904 RO-data=2700 RW-data=40 ZI-data=821312 的信息。这些数值并非简单的数字,而是反映了程序在存储系统中的关键信息,直接影响程序的正常运行和资源利用率。本文将深入解析这些参数的具体含义、存储原理及其实际应用价值。
一、程序的内存分段:为什么需要这些参数?
嵌入式系统的存储资源通常由两种介质组成:Flash(或ROM) 和 RAM。Flash 是非易失性存储,用于长期保存程序和数据;RAM 是易失性存储,用于程序运行时的数据处理。由于这两种存储的物理特性不同(容量、速度、读写特性),编译器会将程序自动划分为不同的"段(Section)",以便高效利用存储资源。
程序的内存分段本质上是基于数据的"读写属性"和"生命周期"进行的分类管理:
- 有些数据需要长期保存且只读(如程序代码)
- 有些数据需要长期保存且可修改(如初始化后的变量)
- 有些数据仅在运行时存在且可修改(如临时变量)
这就是 Program Size 中四个核心参数(Code、RO-data、RW-data、ZI-data)的由来。
二、四大核心参数详解
1. Code(代码段)
- 定义:存放程序的可执行指令,包括函数体、汇编指令、跳转表等所有CPU可直接执行的二进制代码。
- 特性:只读(CPU仅读取执行,不修改),需要长期保存(断电不丢失)。
- 示例:
void led_blink() { // 这里的所有指令都会被编译到Code段 GPIO_SetBits(GPIOA, GPIO_Pin_5); delay_ms(500); GPIO_ResetBits(GPIOA, GPIO_Pin_5); delay_ms(500); } - 存储位置:必存于Flash(或ROM)中。
- 大小意义:反映程序功能的复杂度,代码越复杂(函数越多、逻辑越复杂),Code段越大。
2. RO-data(只读数据段)
- 定义:存放程序中所有只读常量,即不会被修改的数据。
- 特性:只读(程序运行中不会被修改),需要长期保存。
- 示例:
const int system_freq = 72000000; // 常量,存放于RO-data const char* welcome_msg = "Hello, Embedded!"; // 字符串常量,存放于RO-data static const uint8_t lookup_table[16] = {0x01, 0x02, ...}; // 只读数组,存放于RO-data - 存储位置:必存于Flash(或ROM)中,与Code段通常连续存放。
- 大小意义:反映程序中常量数据的多少,过多的大常量(如字库、固定参数表)会导致RO-data增大。
3. RW-data(读写数据段)
- 定义:存放已初始化且需要修改的全局变量和静态变量。
- 特性:可读写(程序运行中会被修改),但需要保存初始值(断电后需恢复)。
- 示例:
int global_counter = 100; // 已初始化的全局变量,存放于RW-data static float temperature = 25.5f; // 已初始化的静态变量,存放于RW-data - 存储机制:
- 加载时:初始值存放在Flash中(与Code、RO-data一起)
- 运行时:系统启动后,会将初始值从Flash复制到RAM中,程序运行时直接操作RAM中的数据
- 大小意义:反映程序中需要初始值的变量数量,RW-data越大,启动时从Flash复制到RAM的时间越长。
4. ZI-data(零初始化数据段)
- 定义:存放未初始化或明确初始化为0的全局变量和静态变量。
- 特性:可读写,无需保存初始值(默认初始化为0)。
- 示例:
int buffer[1024]; // 未初始化的全局数组,存放于ZI-data static uint32_t error_count = 0; // 初始化为0的静态变量,存放于ZI-data - 存储机制:
- 不占用Flash空间:无需保存初始值(默认0)
- 运行时:系统启动后,会在RAM中分配一块连续空间并初始化为0
- 大小意义:反映程序中动态数据的规模,ZI-data是RAM占用的主要部分,过大可能导致内存不足。
三、存储占用计算:Flash与RAM需求
Program Size 的四个参数直接决定了系统对Flash和RAM的实际需求,这是硬件选型和程序优化的关键依据。
1. Flash 总占用
Flash需要存储所有"需要持久化"的内容,计算公式:
Flash总占用 = Code + RO-data + RW-data
以示例 Code=8904 RO-data=2700 RW-data=40 计算:
Flash总占用 = 8904 + 2700 + 40 = 11644字节 ≈ 11.4KB
这意味着:
- 单片机的Flash容量必须大于11.4KB才能存放程序
- 实际下载时,编程器会将这三部分数据写入Flash的指定地址
2. RAM 总占用
RAM需要存储所有"运行时可修改"的内容,计算公式:
RAM总占用 = RW-data + ZI-data
以示例 RW-data=40 ZI-data=821312 计算:
RAM总占用 = 40 + 821312 = 821352字节 ≈ 802KB
这意味着:
- 单片机的RAM容量必须大于802KB才能保证程序正常运行
- 程序运行中动态分配的内存(如
malloc)也需要从这部分空间中扣除
四、启动过程中的段处理
理解程序段在系统启动时的处理流程,有助于深入掌握这些参数的实际意义:
- 上电复位:CPU从复位向量开始执行,首先运行启动代码(通常是汇编编写)
- 复制RW-data:启动代码将Flash中的RW-data初始值复制到RAM的指定地址
- 初始化ZI-data:启动代码在RAM中分配ZI-data空间,并将其全部清零
- 初始化栈指针:设置栈(Stack)的起始地址(通常位于RAM高位)
- 跳转到main函数:完成上述初始化后,程序进入用户代码
这个过程解释了为什么RW-data需要同时占用Flash和RAM空间(Flash存初始值,RAM存运行值),以及为什么ZI-data不占用Flash空间(仅在RAM中动态创建)。
五、实际开发中的应用与优化
Program Size 参数是嵌入式开发中的"检查表",直接反映程序的存储使用状况,在以下场景中尤为重要:
1. 硬件选型验证
在项目初期,需根据程序的存储需求选择合适的单片机:
- 若Flash总占用为11.4KB,需选择Flash容量≥16KB的型号(留有余量)
- 若RAM总占用为802KB,需选择RAM容量≥1MB的型号(避免内存溢出)
例如,若上述示例程序运行在仅有512KB RAM的单片机上,必然会因内存不足导致程序崩溃(表现为硬fault、复位或异常行为)。
2. 内存溢出问题定位
程序运行中出现的很多异常都与内存占用相关:
- 频繁的硬fault可能是RAM溢出(ZI-data+RW-data超过实际RAM容量)
- 变量值莫名被修改可能是栈溢出(栈空间与ZI-data重叠)
通过对比Program Size中的RAM总占用与单片机实际RAM容量,可快速判断是否存在内存不足问题。
3. 程序优化方向
根据各段的大小特点,可针对性优化存储占用:
-
Code段过大:
- 优化算法,减少冗余代码
- 使用编译器优化选项(如
-Os优化空间) - 将部分常量逻辑用查表法替代(用RO-data换Code)
-
RO-data过大:
- 检查是否有不必要的
const大数组 - 对于字库等超大常量,考虑存放在外部Flash
- 检查是否有不必要的
-
RW-data过大:
- 减少已初始化的全局变量,尽量使用局部变量
- 将可延迟初始化的变量改为在
main中初始化(转为栈变量)
-
ZI-data过大:
- 减少大数组定义,改用动态内存分配(
malloc) - 将临时缓冲区定义为函数内的局部变量(使用栈空间)
- 检查是否有未使用的全局变量(冗余定义)
- 减少大数组定义,改用动态内存分配(
4. 链接脚本配置
可通过修改链接脚本(如Keil的.sct文件、GCC的.ld文件)自定义各段的存储地址
参考nxp的xip实现分散加载文件:
#!armclang --target=arm-arm-none-eabi -mcpu=cortex-m7 -E -x c
/*
** ###################################################################
** Processors: MIMXRT1011CAE4A
** MIMXRT1011DAE5A
**
** Compiler: Keil ARM C/C++ Compiler
** Reference manual: IMXRT1010RM Rev.0, 09/2019
** Version: rev. 1.0, 2019-08-01
** Build: b191120
**
** Abstract:
** Linker file for the Keil ARM C/C++ Compiler
**
** Copyright 2016 Freescale Semiconductor, Inc.
** Copyright 2016-2019 NXP
** All rights reserved.
**
** SPDX-License-Identifier: BSD-3-Clause
**
** http: www.nxp.com
** mail: support@nxp.com
**
** ###################################################################
*/
#if (defined(__ram_vector_table__))
#define __ram_vector_table_size__ 0x00000400
#else
#define __ram_vector_table_size__ 0x00000000
#endif
#define m_flash_config_start 0x60000400
#define m_flash_config_size 0x00000C00
#define m_ivt_start 0x60001000
#define m_ivt_size 0x00001000
#define m_interrupts_start 0x60002000
#define m_interrupts_size 0x00000400
#define m_text_start 0x60002400
#define m_text_size 0x00FFDC00
#define m_interrupts_ram_start 0x20000000
#define m_interrupts_ram_size __ram_vector_table_size__
#define m_data_start (m_interrupts_ram_start + m_interrupts_ram_size)
#define m_data_size (0x00008000 - m_interrupts_ram_size)
#define m_data2_start 0x20200000
#define m_data2_size 0x00010000
#define m_psram_start 0x61000000 //PSRAM 8M
#define m_psram_size 0x00800000
/* Sizes */
#if (defined(__stack_size__))
#define Stack_Size __stack_size__
#else
#define Stack_Size 0x0400
#endif
#if (defined(__heap_size__))
#define Heap_Size __heap_size__
#else
#define Heap_Size 0x0400
#endif
#if defined(XIP_BOOT_HEADER_ENABLE) && (XIP_BOOT_HEADER_ENABLE == 1)
LR_m_text m_flash_config_start m_text_start+m_text_size-m_flash_config_start { ; load region size_region
RW_m_config_text m_flash_config_start FIXED m_flash_config_size { ; load address = execution address
* (.boot_hdr.conf, +FIRST)
}
RW_m_ivt_text m_ivt_start FIXED m_ivt_size { ; load address = execution address
* (.boot_hdr.ivt, +FIRST)
* (.boot_hdr.boot_data)
* (.boot_hdr.dcd_data)
}
#else
LR_m_text m_interrupts_start m_text_start+m_text_size-m_interrupts_start { ; load region size_region
#endif
VECTOR_ROM m_interrupts_start FIXED m_interrupts_size { ; load address = execution address
* (.isr_vector,+FIRST)
}
ER_m_text m_text_start FIXED m_text_size { ; load address = execution address
* (InRoot$$Sections)
.ANY (+RO)
}
#if (defined(__ram_vector_table__))
VECTOR_RAM m_interrupts_ram_start EMPTY m_interrupts_ram_size {
}
#else
VECTOR_RAM m_interrupts_start EMPTY 0 {
}
#endif
RW_m_data m_data_start m_data_size-Stack_Size-Heap_Size { ; RW data
.ANY (+RW +ZI)
* (RamFunction)
* (NonCacheable.init)
* (*NonCacheable)
flexspi_psram.o (+RO +RW +ZI)
fsl_flexspi.o (+RO +RW +ZI)
}
ARM_LIB_HEAP +0 EMPTY Heap_Size { ; Heap region growing up
}
ARM_LIB_STACK m_data_start+m_data_size EMPTY -Stack_Size { ; Stack region growing down
}
RW_m_ncache m_data2_start EMPTY 0 {
}
RW_m_ncache_unused +0 EMPTY m_data2_size-ImageLength(RW_m_ncache) { ; Empty region added for MPU configuration
}
}
通过合理配置链接脚本,可实现:
- 将高频访问的代码放到高速Flash区域
- 将大数组分配到外部如PSRAM
六、常见问题解答
1. 为什么ZI-data不占用Flash空间?
ZI-data的初始值为0,系统启动时会自动将其清零,无需在Flash中存储初始值,因此不占用Flash空间。
2. 局部变量算在哪一段?
函数内的局部变量(非static)不包含在上述四个段中,它们存放在栈(Stack)中,栈空间是从RAM总容量中分配的,因此需要确保RAM总容量 > RW-data + ZI-data + 栈大小 + 堆大小。
3. 动态分配的内存(malloc)算在哪一段?
动态内存从堆(Heap)中分配,堆也位于RAM中,同样需要从RAM总容量中扣除,因此实际可用内存为:RAM总容量 - (RW-data + ZI-data + 栈大小 + 堆大小)。
4. 如何查看更详细的段信息?
编译器通常会生成映射文件(Map File),包含各文件、各函数占用的具体大小:
- Keil:在工程设置中勾选"Generate Map File",生成
.map文件 - GCC:通过
-Wl,-Map=output.map选项生成映射文件 - IAR:在链接器设置中勾选"Generate linker map file"
映射文件可帮助定位哪些函数或变量占用了过多空间,是优化的重要依据。
七、总结
程序存储空间中的 Code、RO-data、RW-data 和 ZI-data 四个指标,看似基础实则完整反映了程序在存储系统中的分布情况。声明:部分资料来源ai。
更多推荐



所有评论(0)