2. 嵌入式内存基础

TFLite Micro 的模型权重、算子参数和中间张量最终都要落到嵌入式设备的物理存储介质上。很多开发者初次接触模型部署时,往往只关注“内存够不够”,却忽略了 Flash 与 SRAM 在访问速度、寿命、容量和寻址方式上的本质差异。这种认知缺失会导致两个典型问题:要么把大块权重放在 SRAM 中导致启动即崩溃,要么频繁改写 Flash 造成芯片提前报废。本章从物理介质出发,厘清 Flash、SRAM 与编译链接后生成的 RO、RW、ZI 段之间的对应关系,为后续理解 TFLite Micro 的常量区优化打下基础。

Flash(非易失) 容量大(512KB~16MB) 读速慢(~20-50ns) 写需擦除,寿命~10万次 掉电保留 SRAM(易失) 容量小(16KB~1MB) 读速快(~2-10ns) 可任意读写,无寿命限制 掉电丢失 编译链接产物 RO(只读段) const权重、代码、只读常量 RW(读写段) 已初始化全局变量、静态变量 ZI(零初始化段) 未初始化全局变量、bss 链接脚本决定加载域与运行域 (Load Region / Exec Region) 物理映射 RO → Flash 代码段、const权重 RW → 启动时从Flash拷贝 到SRAM运行 ZI → 仅占用SRAM 启动时清零,不占Flash TFLite Micro权重应放RO段 避免RW/ZI造成SRAM膨胀

上图展示了嵌入式系统中物理存储介质与编译产物段之间的核心映射关系。Flash 和 SRAM 是硬件层面的存储介质,而 RO、RW、ZI 是编译链接工具链生成的逻辑段。理解这张图的关键在于:RO 段默认被链接到 Flash 地址空间,RW 段在启动时由 Flash 拷贝到 SRAM,ZI 段则只占用 SRAM 空间。TFLite Micro 的权重若被声明为 const,就会进入 RO 段,直接放在 Flash 中,不消耗宝贵的 SRAM。

核心原理:物理介质决定访问模型

Flash 和 SRAM 的本质差异可以从存储单元结构、访问时序和系统总线设计三个层面理解。

  • Flash 存储单元:基于浮栅晶体管(Floating Gate Transistor),通过电荷注入实现数据保持。读取时需要对字线施加电压,检测位线电流变化,这个过程比 SRAM 的触发器翻转慢一个数量级。写入则必须先将整个扇区(Sector)擦除为全 1,再编程写入,因此写操作耗时在毫秒级。
  • SRAM 存储单元:由 6 个晶体管组成锁存器结构,只要供电就能保持状态。访问时直接通过字线选通、位线读出,无需擦除操作,读写速度接近 CPU 主频。但每个 bit 需要 6 个晶体管,导致同等容量下 SRAM 面积是 Flash 的 10 倍以上,成本高昂。
  • 总线架构影响:多数 MCU 采用哈佛架构或改进型哈佛架构,Flash 和 SRAM 挂载在不同的总线桥上。CPU 取指可以从 Flash 直接读取,但数据访问若命中 SRAM 则无需等待;若访问 Flash 中的数据,可能需要插入等待周期(Wait State)。例如 STM32F4 系列在 168MHz 主频下访问 Flash 需要 4 个等待周期,而 SRAM 零等待。

从系统角度,Flash 适合存放“只读且长期不变”的内容,SRAM 适合存放“频繁读写”的临时数据。TFLite Micro 的模型权重在推理过程中只被读取、从不修改,天然适合放在 Flash 中。

判断方法:从编译产物到物理地址

实际开发中,可以通过以下步骤确认各段在物理内存中的分布情况。

  1. 查看链接脚本:打开 MCU 厂商提供的 .ld 文件(GCC)或 .sct 文件(Keil/ARMCC),找到 FLASH 和 RAM 的起始地址与大小定义。
  2. 编译生成 map 文件:在编译选项中添加 -Wl,-Map=output.map(GCC)或勾选 “Generate Map File”(Keil)。map 文件中会列出每个段的起始地址、大小和所属目标文件。
  3. 分析符号地址:在 map 文件中搜索模型权重数组的符号名,查看其地址落在 Flash 区间还是 RAM 区间。例如 STM32F407 的 Flash 地址范围是 0x08000000-0x080FFFFF,SRAM 是 0x20000000-0x2001FFFF。
  4. 使用 readelf 或 objdump:在 Linux 环境下执行 arm-none-eabi-readelf -S firmware.elf 查看各段属性,arm-none-eabi-nm -n firmware.elf 查看符号地址。

关键判断标准:如果模型权重数组的地址落在 Flash 区间,说明它被正确放置在 RO 段;如果落在 SRAM 区间,说明它被错误地放入了 RW 或 ZI 段,这会直接导致 SRAM 占用飙升。TFLite Micro 的 tflite::GetModel() 函数要求传入的模型数据指针指向 Flash 中的常量区,否则在资源受限的 MCU 上很容易因 SRAM 不足而无法完成初始化。

案例拆解:一个 300KB 模型的 SRAM 灾难

某 IoT 设备使用 STM32F746(1MB Flash,320KB SRAM)部署 TFLite Micro 图像分类模型。模型文件大小为 280KB,开发者最初将模型数据声明为全局数组:

// 错误示例:模型数据被放入RW段
uint8_t model_data[] = {
  #include "model_data.h"
};

编译后查看 map 文件,发现 model_data 被分配到 RW 段,起始地址 0x20000000。这意味着 280KB 的模型数据需要从 Flash 拷贝到 SRAM,而 SRAM 总共只有 320KB,剩余可用空间不足 40KB,无法容纳 TFLite Micro 的算子工作区(通常需要 50-100KB)。程序在启动时直接进入 HardFault。

正确的做法是添加 const 限定符:

// 正确示例:模型数据放入RO段,直接驻留Flash
const uint8_t model_data[] = {
  #include "model_data.h"
};

修改后,model_data 被链接到 Flash 地址 0x08000000 区域,SRAM 仅需分配推理过程中的张量缓冲区。最终 SRAM 占用从 280KB 降至约 80KB,系统稳定运行。这个案例说明:一个 const 关键字的遗漏,可能导致整个部署方案不可行。

对比项错误方案(RW段)正确方案(RO段)
模型数据存放位置SRAM(0x20000000)Flash(0x08000000)
SRAM 占用280KB0KB(仅指针)
启动行为从Flash拷贝280KB到SRAM无需拷贝,直接引用
可用SRAM剩余约40KB(不足)约240KB(充足)
系统状态HardFault正常运行

常见误区与总结复盘

在嵌入式内存管理中,开发者容易陷入以下几个误区。

误区一:认为 Flash 和 SRAM 可以随意互相访问。实际上,Flash 的随机读取速度远低于 SRAM,且频繁写入会损耗存储单元。TFLite Micro 推理过程中不应有任何对 Flash 的写操作,否则不仅性能急剧下降,还会缩短芯片寿命。

误区二:混淆“只读段”与“只读属性”。RO 段是链接脚本中的逻辑分区,而 const 只是编译器的类型修饰。如果 const 变量被声明为局部变量,它可能被分配到栈上;只有全局 const 或 static const 才会进入 RO 段。TFLite Micro 的模型数据必须是全局 const 或 static const。

误区三:忽略 ZI 段对 SRAM 的占用。ZI 段虽然不占 Flash 空间,但启动时需要在 SRAM 中清零。如果模型推理需要的大缓冲区被声明为未初始化的全局数组,它们会进入 ZI 段,同样消耗 SRAM。建议使用 tflite::TensorArena 动态管理临时缓冲区,避免静态分配过大数组。