4、静态内存分配:全局变量与静态变量的布局、常量区优化、链接脚本定制

说到内存优化,很多人第一反应是「动态内存省着点用」。但说实话,在嵌入式触摸屏项目里,真正吃内存的大户往往是那些你平时不太注意的静态数据。全局变量、静态变量、常量字符串……它们安安静静地躺在那里,一躺就是整个生命周期。

我早年做一款 64KB RAM 的彩屏菜单时,就吃过这个亏。功能都调通了,一算内存,还剩 3KB。后来仔细一查,光是一张中文字库和几个全局结构体数组,就占了将近 40KB。嗯,从那以后,我对静态内存的布局就格外上心。

4.1 全局变量与静态变量的内存布局

先看一张图,帮你快速建立全局变量和静态变量在内存中的分布概念。

静态内存布局示意图 静态内存布局(RAM + Flash) RAM 运行时数据 .bss 段 未初始化全局变量 未初始化静态变量 (启动时清零) .data 段 已初始化全局变量 已初始化静态变量 (初值从 Flash 拷贝) Flash 只读数据 .rodata 段 const 常量 字符串字面量 (直接寻址,不占 RAM) .data 初值镜像 (启动时拷贝到 RAM) 启动时拷贝 关键点 .bss 不占 Flash 空间 .rodata 不占 RAM 空间

这张图里有两个关键区域:RAM 和 Flash。全局变量和静态变量在 RAM 中分为 .bss 和 .data 两段。你想想看,.bss 段存放的是未初始化的变量,启动时由启动代码统一清零。.data 段则存放已初始化的变量,它们的初值保存在 Flash 里,上电后拷贝到 RAM。

我个人习惯把大块缓冲区放在 .bss 段。为什么?因为不占 Flash 空间。比如一个 8KB 的帧缓冲区,如果初始化成全零,放在 .bss 里只占 RAM,不占 Flash。要是你手欠给它赋了个初值,哪怕全是 0,编译器也会把它扔进 .data,Flash 里就得多存一份 8KB 的镜像。

经验法则:能放 .bss 就别放 .data。大数组、缓冲池、状态标志,统统声明时不初始化,让链接器帮你省 Flash。

4.2 常量区优化:把不变的数据留在 Flash 里

触摸屏项目里,常量数据特别多:菜单字符串、图标位图、字体点阵、配置参数……这些数据一旦确定就不会变。如果处理不当,它们会偷偷占用两份空间——一份在 Flash,一份在 RAM。

为什么会这样?因为编译器默认把 const 变量放在 .rodata 段,但有些情况下,它会被拷贝到 RAM。比如你定义了一个局部 const 数组,编译器可能把它放在栈上。再比如你用了某些库函数,内部会做内存拷贝。

我遇到过最典型的一个案例:一个同事把 16x16 的中文字库定义成 const uint8_t font[4096][32],结果编译出来 RAM 占用多了 128KB。查了半天,发现他用的编译器版本默认把大 const 数组放到了 .data 段。解决方案很简单——加一个 __attribute__((section(".rodata"))),强制指定段。

小技巧:在链接脚本里把 .rodata 段紧跟在 .text 段后面,放在 Flash 的高地址区域。这样既能利用 Flash 的连续读取特性,又能避免碎片化。

常量区优化的核心思路就一句话:让不变的数据只占 Flash,不占 RAM。具体做法有这几条:

  • 全局常量加 const 和 static:限制作用域,防止外部引用导致拷贝。
  • 字符串用指针指向字面量:比如 const char *s = "Hello"; 比 char s[] = "Hello"; 省 RAM。前者只存指针(4 字节),后者要存整个数组(6 字节)。
  • 大常量表用 __attribute__((section(".rodata"))):强制指定段,避免编译器自作主张。
  • 查表代替计算:比如正弦表、CRC 表,提前算好放 Flash,运行时直接读。

来看一个对比示例:

// ❌ 坏写法:字符串存在 RAM 里
char menu_item[] = "Settings";

// ✅ 好写法:字符串存在 Flash 里
const char *menu_item = "Settings";

// ❌ 坏写法:大常量表可能被拷贝
const uint8_t sin_table[256] = {0, 1, 2, ...};

// ✅ 好写法:强制指定 .rodata 段
const uint8_t sin_table[256] __attribute__((section(".rodata"))) = {0, 1, 2, ...};

注意:强制指定段后,如果代码试图修改这个数组,会触发硬件异常(写 Flash 失败或 HardFault)。所以一定要确保数据是真正只读的。

4.3 链接脚本定制:把内存控制权握在自己手里

链接脚本(Linker Script)是嵌入式开发的「最后一道防线」。默认的链接脚本通常由芯片厂商提供,能满足大部分通用需求。但到了内存优化阶段,你就得自己动手了。

我记得第一次改链接脚本时,心里还挺没底的。生怕改错了导致程序跑飞。后来发现,其实只需要关注几个关键段的位置和大小就够了。

下面是一个典型的链接脚本片段(以 ARM Cortex-M 为例):

MEMORY
{
    FLASH (rx)  : ORIGIN = 0x08000000, LENGTH = 512K
    RAM   (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}

SECTIONS
{
    .text : {
        *(.text*)
        *(.rodata*)
    } > FLASH

    .data : {
        _sdata = .;
        *(.data*)
        _edata = .;
    } > RAM AT> FLASH

    .bss : {
        _sbss = .;
        *(.bss*)
        _ebss = .;
    } > RAM
}

这个脚本做了三件事:

  • .text 段(代码 + 只读数据)放在 Flash。
  • .data 段(已初始化变量)放在 RAM,但初值镜像存在 Flash(AT> FLASH)。
  • .bss 段(未初始化变量)放在 RAM,启动时清零。

优化时,你可以做这些调整:

优化目标 链接脚本修改 效果
减少 RAM 占用 把大常量段放到 Flash 节省 RAM,增加 Flash 占用
减少 Flash 占用 把 .bss 段尽量扩大 不占 Flash,只占 RAM
提高访问速度 把频繁访问的变量放到特定 RAM 区域 利用 TCM 或紧耦合内存
调试方便 为每个模块单独分配段 快速定位内存溢出

我曾经在一个项目里,把触摸屏的帧缓冲区单独放在一个段里,然后映射到外部 SDRAM。链接脚本里这样写:

/* 在 MEMORY 区域定义外部 SDRAM */
SDRAM (rw) : ORIGIN = 0x60000000, LENGTH = 8M

/* 在 SECTIONS 里单独分配帧缓冲区 */
.framebuffer : {
    *(.framebuffer*)
} > SDRAM

然后在代码里声明:

uint8_t frame_buffer[320*240*2] __attribute__((section(".framebuffer")));

这样一来,内部 RAM 省下了 150KB,触摸屏的刷新速度反而更快了——因为 SDRAM 的带宽足够大。

调试技巧:在链接脚本里给每个段加一个符号(比如 _sdata、_edata),然后在代码里打印这些符号的值,就能实时监控内存使用情况。我习惯在启动日志里输出各段的起始地址和大小,方便排查问题。

4.4 实战:一个触摸屏菜单的静态内存优化案例

光说不练假把式。我们来看一个具体的优化案例。

假设你有一个触摸屏菜单系统,包含以下数据:

  • 10 个菜单页面,每个页面有 5 个菜单项
  • 每个菜单项有一个名称字符串(平均 10 个字符)和一个回调函数指针
  • 一个 320x240 的 16 位色帧缓冲区
  • 一个 256 字节的触摸校准参数

优化前的代码可能是这样的:

// 菜单项结构体
typedef struct {
    char name[16];      // 菜单名称
    void (*handler)(void); // 回调函数
} MenuItem;

// 全局菜单数组(已初始化)
MenuItem menu[10][5] = {
    {{"Main Menu", main_handler}, {"Settings", settings_handler}, ...},
    ...
};

// 帧缓冲区(已初始化全零)
uint8_t fb[320*240*2] = {0};

// 校准参数(已初始化)
uint8_t calib[256] = {0xAA, 0xBB, ...};

优化后:

// 菜单名称改为指针,指向 Flash 中的字符串常量
typedef struct {
    const char *name;   // 指向 Flash 中的字符串
    void (*handler)(void);
} MenuItem;

// 菜单数组放在 .bss 段,运行时再填充
MenuItem menu[10][5] __attribute__((section(".bss")));

// 帧缓冲区不初始化,放在 .bss 段
uint8_t fb[320*240*2] __attribute__((section(".bss")));

// 校准参数放在 .rodata 段
const uint8_t calib[256] __attribute__((section(".rodata"))) = {0xAA, 0xBB, ...};

优化前后的内存对比:

项目 优化前 RAM 优化后 RAM 优化前 Flash 优化后 Flash
菜单数组 800 字节 400 字节 800 字节 0 字节
帧缓冲区 153.6 KB 153.6 KB 153.6 KB 0 字节
校准参数 256 字节 0 字节 256 字节 256 字节
总计 154.6 KB 154 KB 154.6 KB 256 字节

你看,RAM 节省不多(因为帧缓冲区本身就必须在 RAM 里),但 Flash 节省了 154KB!对于 Flash 只有 512KB 的芯片来说,这相当于多出了 30% 的空间。

核心思想:静态内存优化的本质是「把不变的数据留在 Flash,把可变的数据留在 RAM」。链接脚本是你实现这个目标的工具,全局变量和静态变量的布局是你需要精心设计的战场。

好了,这一章的内容就到这里。静态内存分配看起来基础,但真正用好的人不多。你想想看,一个链接脚本、几个 __attribute__、一点布局调整,就能省出几十 KB 的空间——这笔买卖,划算。