存储技术

本页目录

保存一个配置值只需几个字节,但底层可能要擦除几 KiB,写入期间可能阻塞取指,掉电还可能留下半条记录。选择存储不能只比较容量和读写速度,要先问:读取粒度、编程与擦除粒度是什么,何时算持久化成功,最坏写入延迟和寿命由谁管理?

数据用途首先考虑不应省略的条件
运行缓冲SRAM、外部 RAMDMA 可达性、时序、易失性
程序与资源内部或外部 FlashXIP、擦写期间访问、启动路径
少量配置EEPROM、日志式 Flash、键值存储校验、版本、更新原子性
高频小记录F-RAM 或适当日志方案总线吞吐、有限耐久性、多字节一致性
大容量记录/导出SD、eMMC 等受管理存储长尾延迟、控制器行为与文件系统同步

介质、总线、控制器与文件系统分层看

SRAM/DRAM 描述存储单元类型,SPI/QSPI 描述接口,SD/eMMC 是包含协议与管理功能的存储产品形态,FAT/littlefs 则组织数据。不同层的名称不能直接放在同一张“谁更快”排名里;读取延迟、持续带宽、编程时间和擦除时间也不是同一个速度指标。

介质或形态典型组织方式主机需要关心什么
SRAM / DRAM易失随机访问;DRAM 需刷新供电、控制器、时序与总线访问
NOR Flash可随机读,按规定单位编程和分块擦除1→0 编程、擦写限制、XIP 支持
原始 NAND页读写、块擦除ECC、坏块、读取扰动、磨损管理
EEPROM字节/页写,内部完成相关擦写页边界、忙时间、耐久与保持条件
F-RAM无 Flash 式块擦除的非易失读写接口速度、寿命、掉电写入窗口
SD / eMMC控制器呈现逻辑块接口缓存/同步、长尾延迟和断电恢复能力

XIP 还需要处理器的存储映射接口、启动配置和可能的缓存支持,不是任何 SPI Flash 接上就能直接执行。某些平台也会先把代码复制到 RAM。片内 Flash 擦写时能否同时读另一区域,则取决于 bank、总线和芯片限制。

NOR 编程与擦除为什么要分开

典型 NOR 擦除后为全 1,编程将选定位变为 0;将 0 恢复为 1 需要擦除所属块。因此向空白记录区追加新数据,可以先编程而不立即擦除;原地改变任意字节则可能触发擦除和数据迁移。具体器件还有最小编程单位、页边界、对齐、内部 ECC 与重复编程限制,不能仅按位逻辑推断允许操作。

示例 4 KiB 擦除块:16 个 256 B 编程页页 0256 B页 1256 B页 2256 B页 3256 B页 4256 B页 5256 B页 6256 B页 7256 B后续页 8 … 15;具体尺寸以器件规格为准NOR:编程页与擦除块是两种不同粒度已擦除 1111 → 编程 1010;若要恢复为 1111,需擦除所属块不能把更新一个字节等同于必须立即擦块;也不能忽略页边界、最小编程单位和重复编程限制。

假设器件编程页为 256 B,从页内偏移 250 写 20 B,应按驱动要求拆为本页 6 B、下一页 14 B。越界一次写入在某些器件上会页内回绕,不能假定自动顺延。典型命令流程还包含写使能、发出编程/擦除、轮询忙状态与错误处理;SPI 传输结束不等于内部操作已经完成。

AT25SF041 是 4 Mbit SPI Flash;器件名中的 bit 与 Byte 也要分清。Renesas AT25SF041 数据表

EEPROM 与 F-RAM:快写不等于事务自动原子

以 24LC256 为例,它是 256 Kbit,即 32 KiB EEPROM,有 64 B 页写缓冲。页写需要遵守边界,内部写周期期间的就绪检测与 ACK 语义按数据表实现,不能把总线 ACK 当成数据已永久写完。Microchip 24LC256

F-RAM 不需要 NOR 式块擦除,适合某些频繁小写入任务,但耐久性仍是有限且附条件的规格。芯片内部写入快,也不能让 I²C/SPI 串行传输瞬间完成;多个字节组成的一条业务记录在中途掉电时仍可能只更新一部分。FM25V02A 是 256 Kbit、32 KiB,不是 2 Mbit。Infineon FM25V02A

寿命按实际擦写放大计算

若同一个擦除块每 10 s 擦除一次,额定 100000 次擦除对应约 11.57 天的计数预算。关键条件是“每次保存都擦同一个块”,不是“每 10 s 有一次业务写入”。如果在空白区追加多条记录后再擦除,擦除频率可以明显下降。

假设每块 4096 B、每条完整记录 64 B,理想一块容纳 64 条;16 块均匀轮转,每写 1024 条记录才让每块各经历一次擦除。按 100000 次可得约 1.024 亿条记录的理想吞吐预算,但元数据、回收搬运、预留空间、坏块及具体编程限制都会减少它。这不是产品使用年限保证,保持时间、温度和其他器件寿命仍可能先到边界。

动态磨损均衡让参与写入的块分散磨损,静态均衡还可能迁移长期不变的数据以利用更多块。算法也会产生写放大;需要测量实际物理操作次数,不能仅看文件系统声称“带磨损均衡”就认定所有地址承受相同磨损。

双区记录如何在掉电后选择有效版本

准备A:旧记录,有效B:空白 / 可安全擦除写入A:仍保留B:正文 + 校验,未提交提交A:可稍后回收B:完整提交,序号更新启动扫描两个区域:格式、长度、提交标记、校验都有效后才比较序号双区恢复:新记录完整提交前,旧记录仍是恢复依据原子性依赖介质写入/掉电行为与驱动顺序;两份数据放在同一擦除块内并不构成独立双区。

两个区域应能独立擦除。准备新记录时保留旧记录,先写格式版本、序号、长度、正文及校验,确认顺序和完成条件后,再按介质允许的独立编程粒度写提交标记。启动时拒绝未提交、越界长度、错误格式或校验失败的记录,在有效记录中选择较新序号;新记录可靠提交后才回收旧区。

掉电位置恢复所见应采取的结果
新区擦除或正文写入中旧区有效,新区不完整保留旧版本
校验写完但未提交新区仍不是已提交记录保留旧版本
提交完成、旧区未擦两区均有效选新序号
回收旧区中新区有效使用新版本

提交写本身也可能被中断,必须定义其有效编码和校验规则;CRC 能检测大量常见错误,但不是数学上的任意损坏绝对保证,也不能提供恶意篡改认证。还要处理序号回绕、格式升级,以及设备掉电时是否可能影响目标区域之外的数据。简单双区模型不能直接覆盖 SD 控制器内部所有失效方式。

下面枚举新记录按字节顺序写入的每个断点,验证不选择半条新记录。模型约定旧区独立且未损坏、写入只形成指定前缀、序号不回绕;真实 Flash 的最小编程单位和撕裂写行为仍须另行验证。

import struct
import zlib

def encode(seq, payload):
    body = struct.pack('<IH', seq, len(payload)) + payload
    return body + struct.pack('<I', zlib.crc32(body)) + b'DONE'

def decode(blob):
    if len(blob) < 14:
        return None
    seq, size = struct.unpack('<IH', blob[:6])
    end = 6 + size
    if end+8 > len(blob) or blob[end+4:end+8] != b'DONE':
        return None
    if zlib.crc32(blob[:end]) != struct.unpack('<I', blob[end:end+4])[0]:
        return None
    return seq, blob[6:end]

old = encode(7, b'old configuration')
new = encode(8, b'new configuration')
for cut in range(len(new)+1):
    target = new[:cut] + bytes([255])*(len(new)-cut)
    valid = [x for x in (decode(old), decode(target)) if x is not None]
    recovered = max(valid, key=lambda x: x[0])
    assert recovered[0] == (8 if cut == len(new) else 7)
assert decode(b'bad') is None
assert decode(old)[1] == b'old configuration'
print('所有前缀掉电点:提交前恢复旧记录,提交完成后选择新记录')

掉电落在哪个字节,决定选哪个版本?

把断点从11移到12,区别只有最后的提交字节,却改变了恢复选择。再制造数据损坏,即使提交标记存在也会拒绝新记录。取消保留旧区可以直接看到提前回收的风险:未提交的新记录无法替代已经丢掉的旧记录。

正在呈现知识画面
掉电落在哪个字节,决定选哪个版本?

新记录写完校验也不等于提交;保留独立旧区才能在未提交或校验失败时恢复。

SD / eMMC:逻辑块背后还有控制器

受管理存储把 NAND 的许多细节封装起来,主机面对逻辑块读写,但内部仍有缓存、映射、垃圾回收和磨损管理。同样大小的写入可能出现不同忙时间;持续采集的缓冲要覆盖可接受的最长停顿,并定义超时和丢样策略。

例如 160 kB/s 数据流遇到 200 ms 写入停顿,需要至少 32000 B 额外缓冲,尚未计调度与其他余量。只看卡的峰值 MB/s 不能保证零丢样。SD 存储总线和 SDIO 扩展功能也不应混为同一个术语;是否用 SPI 或原生总线,按卡与主控的支持和初始化要求实现,不依靠固定“多发几个时钟”补丁。

块设备可以存放自定义原始格式,并非必须使用文件系统。若要和 PC 交换文件,可以选支持目标格式的实现;电压、上拉及去耦按相应接口与硬件资料设计,不应对所有信号线统一加上拉。

文件系统一致性不等于业务提交完成

FAT 提供广泛的文件交换能力,但多处元数据更新与掉电语义需要应用认真处理。littlefs 面向受限设备和 Flash 的掉电恢复,使用相应元数据与写时复制设计;它仍要求底层读、编程、擦除和同步契约正确。littlefs 设计

例如写入新配置文件后,应按 API 同步/关闭并使用适合的原子替换流程;两个文件的分别成功更新不自动组成一次跨文件事务。littlefs 文档明确要求有缓存的存储驱动在 sync 时落实同步,否则无法保证数据完整性。挂载成功也不意味着最后一条尚未同步的记录一定保留。littlefs 接口与同步说明

掉电保持电容可以成为设计的一部分,但要先检测电压下降、停止新事务,并给最坏待完成操作留能量。恒流简化下 C ≥ I·Δt/ΔV,例如 50 mA、10 ms、允许下降 0.3 V 得到约 1667 μF;还需考虑 ESR、容差、转换效率和写入最低电压。未知长尾写时间不能靠随意加一颗电容获得保证。

复算粒度与预算

from math import isclose
assert 256*1024//8 == 32768
page, offset, count = 256, 250, 20
first = min(count, page-offset%page)
assert (first, count-first) == (6, 14)
assert isclose(100000*10/86400, 11.574074074074074)
records = (4096//64)*16*100000
assert records == 102400000
assert 160000*0.2 == 32000
assert isclose(0.05*0.010/0.3*1e6, 1666.6666666666667)
print('容量单位、跨页拆分、擦除预算、缓冲与保持电容计算通过')

本篇验证模型与断点恢复逻辑,没有擦写实际存储器,也没有进行真实断电测试。

继续查阅