4、静态内存分配:全局变量与静态变量的布局、常量区优化、链接脚本定制
说到内存优化,很多人第一反应是「动态内存省着点用」。但说实话,在嵌入式触摸屏项目里,真正吃内存的大户往往是那些你平时不太注意的静态数据。全局变量、静态变量、常量字符串……它们安安静静地躺在那里,一躺就是整个生命周期。
我早年做一款 64KB RAM 的彩屏菜单时,就吃过这个亏。功能都调通了,一算内存,还剩 3KB。后来仔细一查,光是一张中文字库和几个全局结构体数组,就占了将近 40KB。嗯,从那以后,我对静态内存的布局就格外上心。
4.1 全局变量与静态变量的内存布局
先看一张图,帮你快速建立全局变量和静态变量在内存中的分布概念。
这张图里有两个关键区域: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 的空间——这笔买卖,划算。