程序存储器(Flash/ROM)详解:程序烧录过程、代码存储原理、读取速度与功耗

程序存储器,说白了就是单片机里存代码的地方。你写的 C 语言、汇编指令,最终都变成二进制机器码,老老实实躺在这里。断电不丢,上电就能跑——这是它和 RAM 最大的区别。

我个人习惯把 Flash 和 ROM 放在一起讲,因为现在绝大多数单片机用的都是 Flash 工艺。传统掩膜 ROM 基本只在超大批量的玩具、遥控器里还能见到,咱们日常开发碰到的,99% 是 Flash。

程序烧录过程:从 .hex 到芯片内部

你点一下 IDE 里的“下载”按钮,背后发生了什么?我拆成几步说:

  1. 编译链接:IDE 把你的源代码变成 .hex 或 .bin 文件。这里面已经是纯粹的机器码和地址信息了。
  2. 通信握手:烧录器(比如 ST-Link、J-Link)通过 SWD 或 JTAG 接口,和芯片内部的 Bootloader 或调试模块建立连接。
  3. 擦除扇区:Flash 写入前必须先擦除。注意,擦除是按扇区(Sector)来的,不是按字节。一个扇区通常 1KB 到 4KB 不等。
  4. 逐页写入:擦干净后,烧录器把数据一页一页写进去。页(Page)比扇区小,一般是 256 字节或 512 字节。
  5. 校验回读:写完后,芯片会把刚写的内容读出来和原始数据比对。不一致就报错。
我的经验: 遇到过好几次烧录到一半断电,芯片变砖的情况。后来我习惯在烧录前先读一下芯片的选项字节,确认写保护没开。尤其是 STM32,RDP 等级设成 1 之后,不先解除保护是没法烧录的。

这里有个关键点:擦除和写入是两回事。擦除是把 Flash 单元全部置为 1(0xFF),写入是把某些位从 1 改成 0。所以你不能直接往一个非空地址写数据——必须先擦再写。

代码存储原理:Flash 内部到底长什么样?

Flash 的核心存储单元是浮栅晶体管。每个单元存 1 位数据,通过控制栅和浮栅之间的电荷来区分 0 和 1。

  • 写入(编程):在控制栅上加高电压,热电子穿过氧化层注入浮栅。浮栅带负电,阈值电压升高,管子关断——读出时为 0。
  • 擦除:在衬底上加高电压,把浮栅里的电子拉出来。阈值电压降低,管子导通——读出时为 1。
  • 读取:在控制栅上加一个中间电压,看管子是否导通。导通就是 1,不导通就是 0。

你想想看,为什么 Flash 有擦写寿命?因为每次注入和拉出电子,都会对氧化层造成不可逆的损伤。一般 NOR Flash 的擦写次数在 1 万到 10 万次之间。NAND Flash 更少,几千次就差不多了。

避坑指南: 我曾经在一个数据记录项目里,每 100ms 就往 Flash 写一次参数。结果三个月后 Flash 就坏了。后来改成只在参数变化时才写,并且用磨损均衡算法,把写操作分散到不同扇区。这个问题才解决。

读取速度与功耗:为什么程序不能跑在 Flash 上?

很多初学者会问:既然程序存在 Flash 里,CPU 直接从 Flash 取指令不行吗?行,但慢。

我列个对比表,你一看就明白:

存储器类型 读取速度 典型功耗(读) 特点
NOR Flash 20-50 ns 5-15 mA 可随机访问,指令可直接执行
NAND Flash 50-100 μs(首次) 10-20 mA 按页读取,不能直接执行
SRAM 5-15 ns 1-5 mA 速度快,但掉电丢失
Cache(SRAM) 2-5 ns 0.5-2 mA CPU 内部,极快

看到了吧?Flash 的读取速度比 SRAM 慢 3-5 倍。如果 CPU 主频很高(比如 100MHz 以上),直接从 Flash 取指令就会产生等待周期,拖慢整体性能。

那怎么办?现代单片机的做法是:

  • 预取缓冲(Prefetch Buffer):CPU 一次从 Flash 读 4 条或 8 条指令,存到内部缓冲里。这样大部分时候 CPU 直接从缓冲取指令,不用等 Flash。
  • 指令 Cache:把常用的指令段缓存到 SRAM 里。循环代码尤其受益。
  • ART 加速器:ST 在 STM32F4/F7 上用的技术,说白了就是 Flash 接口带了一个 64 位宽的读缓冲,配合自适应预取,能把 Flash 的等效读取速度拉到接近 0 等待。
注意: 低功耗模式下,Flash 的读取功耗会显著增加。因为 Flash 内部需要电荷泵来产生读取电压,电荷泵的效率在低电压下会下降。我做过一个电池供电的项目,发现休眠时 Flash 的漏电占了总功耗的 30%。后来把不用的 Flash 区域配置为掉电模式,才把功耗降下来。

Flash 读取时序:一个简单的例子

假设你用的单片机主频 72MHz,Flash 读取速度 30ns。一个时钟周期大约是 13.9ns。也就是说,读一次 Flash 需要 2-3 个时钟周期。

如果 CPU 没有预取缓冲,执行一条单周期指令(比如 NOP)需要:

取指令:3 个周期(等待 Flash)
译码:1 个周期
执行:1 个周期
总耗时:5 个周期

有了预取缓冲后,大部分取指令操作从缓冲里拿,只需要 1 个周期:

取指令:1 个周期(从缓冲)
译码:1 个周期
执行:1 个周期
总耗时:3 个周期

性能提升了 40%。这就是为什么现在的单片机都标配预取和 Cache。

SVG 图:Flash 读取与预取缓冲流程

Flash 读取与预取缓冲流程图 CPU 内核 预取缓冲 Flash 阵列 取指令(1 周期) 预取请求 返回 4 条指令 流程说明: 1. CPU 从预取缓冲取指令,只需 1 个时钟周期。 2. 预取缓冲为空时,向 Flash 发起预取请求,一次读入 4 条指令。 3. Flash 读取需要 2-3 个周期,但后续指令直接从缓冲拿,无需等待。 无预取缓冲 每条指令:3 周期(Flash 等待) 功耗:Flash 持续读取,约 10-15 mA 有预取缓冲 每条指令:1 周期(缓冲命中) 功耗:Flash 间歇读取,约 5-8 mA

功耗优化:Flash 读取的隐藏成本

很多人以为 Flash 读取功耗很低,其实不然。Flash 内部有电荷泵,需要把电压升到 4.5V 左右才能读取。电荷泵的效率通常在 50%-70% 之间,剩下的能量都变成热量散掉了。

我总结几个降低 Flash 读取功耗的方法:

  • 降低主频:频率越低,Flash 读取的等待周期越少,电荷泵的开关损耗也越小。
  • 使用睡眠模式:不执行代码时,把 Flash 切换到低功耗待机状态。很多单片机支持 Flash 掉电模式,功耗可以降到 μA 级。
  • 代码紧凑:把频繁调用的函数放在同一个 Flash 页里,减少跨页读取的额外开销。
  • 使用 RAM 执行:对时间敏感的部分代码,复制到 SRAM 里执行。Flash 可以进入低功耗模式。
我的习惯: 在低功耗项目中,我会把中断服务函数和关键循环放到 RAM 里。Flash 在大部分时间处于掉电状态,只有需要更新数据时才唤醒。这样整体功耗能降低 40% 以上。

嗯,关于程序存储器的内容就这些。Flash 的烧录、存储原理、读取速度和功耗,是嵌入式开发的基础。你理解了这些,后面遇到代码跑飞、烧录失败、功耗异常的问题,就知道从哪里入手了。