嵌入式开发必备: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)也需要从这部分空间中扣除

四、启动过程中的段处理

理解程序段在系统启动时的处理流程,有助于深入掌握这些参数的实际意义:

  1. 上电复位:CPU从复位向量开始执行,首先运行启动代码(通常是汇编编写)
  2. 复制RW-data:启动代码将Flash中的RW-data初始值复制到RAM的指定地址
  3. 初始化ZI-data:启动代码在RAM中分配ZI-data空间,并将其全部清零
  4. 初始化栈指针:设置栈(Stack)的起始地址(通常位于RAM高位)
  5. 跳转到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。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐