嵌入式内存基础:Flash、SRAM、寄存器映射与段的概念
在深入TFLite Micro的Map文件分析之前,必须先建立扎实的嵌入式内存模型。MCU(微控制器)的内存布局与PC系统有本质差异:没有虚拟内存、没有磁盘交换分区,所有代码和数据都必须在物理存储介质上精确定位。理解Flash、SRAM、寄存器映射以及代码段、数据段、BSS段、堆栈的划分,是解读链接脚本(Linker Script)和Map文件的先决条件。本章将系统梳理这些基础概念,并建立一套用于后续章节的内存分析思维框架。
2.1 存储介质与地址空间:Flash、SRAM与寄存器映射
MCU的内存系统由三种物理实体构成:Flash(非易失性闪存)、SRAM(静态随机存取存储器)和寄存器映射(Memory-Mapped Registers)。三者通过总线矩阵连接到CPU核心,但具有截然不同的电气特性和使用场景。
Flash存储器用于存放程序指令(.text段)和只读常量(.rodata段)。其特点是断电后数据不丢失,但写入速度慢(微秒级)、擦写寿命有限(通常10万次)。在嵌入式系统中,Flash的容量决定了程序可以有多大。例如,STM32F103C8T6拥有64KB Flash,而ESP32-S3则提供8MB Flash。TFLite Micro模型通常以C数组形式存储在Flash中,因此模型大小直接受Flash容量约束。
SRAM用于存放运行时数据:全局变量、静态变量、堆和栈。SRAM速度快(与CPU同频)、可无限次读写,但断电即失。SRAM容量通常远小于Flash(典型比例为1:4到1:8)。例如,STM32F103C8T6只有20KB SRAM,而TFLite Micro的Tensor Arena(张量分配区)就驻留于此。SRAM的紧张程度往往比Flash更严重,因为模型推理时的中间张量、激活值、临时缓冲区全部需要SRAM。
寄存器映射是MCU外设控制的核心机制。ARM Cortex-M系列将外设寄存器映射到固定的地址区间(如0x4000_0000~0x5FFF_FFFF),CPU通过普通内存读写指令(LDR/STR)即可操作外设。例如,控制GPIO引脚电平只需向特定地址写入数据:
// 以STM32F4为例,点亮LED(PA5)
#define GPIOA_BASE 0x40020000UL
#define GPIOA_ODR (GPIOA_BASE + 0x14UL)
*(volatile uint32_t*)GPIOA_ODR |= (1UL << 5);
这种设计使得外设操作与普通内存访问统一,但同时也意味着寄存器映射区域占据了地址空间的一部分,在阅读Map文件时不会出现这些地址,但理解它们有助于区分“内存占用”与“外设地址空间”。
2.2 段(Section)的概念:代码段、数据段、BSS段
编译器将源代码编译后,输出目标文件(.o),其中包含多个段(Section)。链接器将这些段按照链接脚本的规则合并到最终的可执行文件中。理解段的概念是阅读Map文件的基础,因为Map文件本质上就是各段在内存中的分布清单。
| 段名 | 存储位置 | 内容 | 生命周期 | 典型示例 |
|---|---|---|---|---|
| .text | Flash | 函数指令、中断向量表 | 程序运行期间始终存在 | main()、TFLite推理函数 |
| .rodata | Flash | 只读常量、字符串字面量 | 程序运行期间始终存在 | 模型权重数组、错误消息 |
| .data | 初始值在Flash,运行时复制到SRAM | 已初始化的全局/静态变量 | 启动时初始化,运行期存在 | int counter = 10; |
| .bss | SRAM(零初始化) | 未初始化或零初始化的全局/静态变量 | 启动时清零,运行期存在 | int buffer[256]; |
| 堆(Heap) | SRAM | 动态分配的内存 | malloc/free管理 | TFLite Micro的Tensor Arena |
| 栈(Stack) | SRAM | 函数调用帧、局部变量 | 函数调用期间 | 递归调用、局部数组 |
.text段是代码段,包含所有编译后的机器指令。在TFLite Micro场景中,.text段的大小取决于模型推理代码的复杂度。例如,包含完整算子库的TFLite Micro可能占用50-100KB Flash,而裁剪掉不需要的算子后可以缩减到20KB以下。
.rodata段存储只读数据。对于TFLite Micro,模型权重通常以const unsigned char[]数组形式存放在.rodata段中。一个1MB的模型文件,其权重数组就会占用约1MB的Flash空间。这也是为什么模型压缩(量化、剪枝)对嵌入式部署至关重要的原因。
.data段比较特殊:它的初始值存放在Flash中,但变量本身在SRAM中。系统启动时,C运行时库(crt0)会将Flash中的初始值复制到SRAM中的对应位置。例如:
// 全局变量,已初始化
int inference_count = 0; // 存储在.data段
// Flash中保存初始值0,启动时复制到SRAM
.bss段存放未初始化或显式零初始化的全局/静态变量。启动时,crt0会将整个.bss段清零。由于不需要在Flash中保存初始值,.bss段不占用Flash空间,只占用SRAM。例如:
// 全局缓冲区,未初始化
static int32_t tensor_arena[10000]; // 存储在.bss段,占用40KB SRAM
.data段的“双份占用”特性——它同时占用Flash(存放初始值)和SRAM(存放运行时变量)。Map文件中通常会分别列出load region(加载地址,即Flash中的位置)和execution region(执行地址,即SRAM中的位置)。如果忽略这一点,可能会低估Flash的实际占用。
2.3 堆与栈:动态内存与函数调用的基石
栈(Stack)是向下生长的内存区域,由编译器自动管理。每次函数调用时,参数、返回地址、局部变量被压入栈中;函数返回时,这些数据被弹出。栈的大小在链接时确定,通常由链接脚本指定。栈溢出是嵌入式系统最常见的崩溃原因之一,尤其是在递归调用或声明大型局部数组时。
堆(Heap)是向上生长的内存区域,由程序员通过malloc/free(C语言)或new/delete(C++)管理。TFLite Micro的一个重要设计决策是避免使用堆,而是使用静态分配的Tensor Arena。这是因为嵌入式环境中动态内存分配可能导致碎片化、不可预测的延迟和内存泄漏。Tensor Arena本质上是一个预分配的静态缓冲区,TFLite Micro在初始化时将其划分为多个张量区域。
// TFLite Micro典型内存分配方式
// 静态分配Tensor Arena,避免使用堆
static uint8_t tensor_arena[1024 * 100]; // 100KB静态缓冲区
void setup() {
static tflite::MicroMutableOpResolver<10> resolver;
static tflite::MicroInterpreter interpreter(
model, resolver, tensor_arena, sizeof(tensor_arena));
// 解释器从tensor_arena中分配张量内存
}
堆和栈共享SRAM空间,它们相向生长。如果堆向上增长到栈的底部,或者栈向下增长到堆的顶部,就会发生内存重叠,导致数据损坏。在Map文件中,堆和栈通常被定义在SRAM的特定区域,链接脚本会为它们预留空间。
kTfLiteError)。常见的做法是先设置一个较大的值(如芯片SRAM的70%),然后通过interpreter->arena_used_bytes()查询实际使用量,再调整到合适大小。
2.4 链接脚本与Map文件的关系
链接脚本(Linker Script,通常以.ld或.icf为后缀)定义了各段在内存中的布局规则。Map文件则是链接器根据链接脚本实际执行后的输出报告。理解两者的关系,是定位内存问题的关键。
一个典型的ARM GCC链接脚本片段如下:
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
SRAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}
SECTIONS
{
.text :
{
KEEP(*(.isr_vector))
*(.text*)
*(.rodata*)
_etext = .;
} > FLASH
.data :
{
_sdata = .;
*(.data*)
_edata = .;
} > SRAM AT> FLASH
.bss :
{
_sbss = .;
*(.bss*)
_ebss = .;
} > SRAM
}
这段脚本定义了:.text段放在Flash中;.data段的执行地址在SRAM,但加载地址在Flash(AT> FLASH);.bss段直接在SRAM中。链接器生成Map文件时,会详细列出每个符号(变量、函数)的地址和大小。
Map文件通常包含以下关键信息:
- 内存配置:Flash和SRAM的总容量、起始地址
- 段分布:每个段(.text、.data、.bss等)的起始地址、结束地址、总大小
- 符号表:每个全局变量、函数的名称、地址、大小
- 交叉引用:哪些目标文件(.o)贡献了哪些符号
region SRAM overflowed by 4096 bytes。打开Map文件,搜索.bss段,发现tensor_arena占用了80KB,而芯片SRAM只有128KB。进一步查看,发现tensor_arena被定义为static uint8_t tensor_arena[80 * 1024],而实际模型推理只需要50KB。解决方案:将数组缩小到60KB,重新编译后SRAM溢出消失。这个过程中,Map文件帮助你快速定位了内存占用的“大头”。
2.5 常见误区与总结复盘
在嵌入式内存基础的学习中,初学者常犯以下错误:
- 混淆Flash与SRAM的容量:误以为模型文件大小等于SRAM占用。实际上,模型权重在Flash中,而推理时的中间张量在SRAM中。
- 忽略.data段的双份占用:一个已初始化的全局数组,既占用Flash(存放初始值)又占用SRAM(运行时数据)。在计算总内存时,需要分别计入。
- 低估栈的需求:TFLite Micro的算子实现可能使用较大的局部数组,如果栈设置过小,会导致静默的数据损坏或HardFault。
- 将寄存器映射地址误认为内存占用:外设寄存器地址(如0x40000000)不属于Flash或SRAM,不应计入内存统计。
| 检查项 | 方法 | 预期结果 |
|---|---|---|
| Flash总占用 | Map文件中.text + .rodata + .data(加载地址部分) |
小于Flash容量的80% |
| SRAM总占用 | Map文件中.data(执行地址部分) + .bss + 堆 + 栈 |
小于SRAM容量的85% |
| Tensor Arena大小 | 代码中静态数组定义 | 大于arena_used_bytes()返回值 |
| 栈使用峰值 | 使用__stack_chk_guard或调试器查看 |
不超过栈区域 |
总结复盘:嵌入式内存管理的核心在于“静态规划”。与PC不同,MCU没有虚拟内存和交换空间,所有内存分配必须在编译期确定。TFLite Micro的设计哲学正是顺应这一约束——通过静态Tensor Arena避免动态分配,通过链接脚本精确控制段布局。掌握Flash、SRAM、寄存器映射和段的概念,是后续分析Map文件、定位内存瓶颈的基石。下一层次的学习中,我们将深入Map文件的具体格式,学习如何从符号级别追踪内存占用。