保存一个配置值只需几个字节,但底层可能要擦除几 KiB,写入期间可能阻塞取指,掉电还可能留下半条记录。选择存储不能只比较容量和读写速度,要先问:读取粒度、编程与擦除粒度是什么,何时算持久化成功,最坏写入延迟和寿命由谁管理?
| 数据用途 | 首先考虑 | 不应省略的条件 |
| 运行缓冲 | SRAM、外部 RAM | DMA 可达性、时序、易失性 |
| 程序与资源 | 内部或外部 Flash | XIP、擦写期间访问、启动路径 |
| 少量配置 | 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 与重复编程限制,不能仅按位逻辑推断允许操作。
假设器件编程页为 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 亿条记录的理想吞吐预算,但元数据、回收搬运、预留空间、坏块及具体编程限制都会减少它。这不是产品使用年限保证,保持时间、温度和其他器件寿命仍可能先到边界。
动态磨损均衡让参与写入的块分散磨损,静态均衡还可能迁移长期不变的数据以利用更多块。算法也会产生写放大;需要测量实际物理操作次数,不能仅看文件系统声称“带磨损均衡”就认定所有地址承受相同磨损。
双区记录如何在掉电后选择有效版本
两个区域应能独立擦除。准备新记录时保留旧记录,先写格式版本、序号、长度、正文及校验,确认顺序和完成条件后,再按介质允许的独立编程粒度写提交标记。启动时拒绝未提交、越界长度、错误格式或校验失败的记录,在有效记录中选择较新序号;新记录可靠提交后才回收旧区。
| 掉电位置 | 恢复所见 | 应采取的结果 |
| 新区擦除或正文写入中 | 旧区有效,新区不完整 | 保留旧版本 |
| 校验写完但未提交 | 新区仍不是已提交记录 | 保留旧版本 |
| 提交完成、旧区未擦 | 两区均有效 | 选新序号 |
| 回收旧区中 | 新区有效 | 使用新版本 |
提交写本身也可能被中断,必须定义其有效编码和校验规则;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,区别只有最后的提交字节,却改变了恢复选择。再制造数据损坏,即使提交标记存在也会拒绝新记录。取消保留旧区可以直接看到提前回收的风险:未提交的新记录无法替代已经丢掉的旧记录。
正在呈现知识画面
掉电落在哪个字节,决定选哪个版本?新记录写完校验也不等于提交;保留独立旧区才能在未提交或校验失败时恢复。
{"id":"flash-prefix-commit","title":"掉电落在哪个字节,决定选哪个版本?","summary":"新记录写完校验也不等于提交;保留独立旧区才能在未提交或校验失败时恢复。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"control\"><label for=\"prefix\" data-i18n=\"prefix\"></label><output for=\"prefix\" id=\"prefix-value\"></output><input id=\"prefix\" type=\"range\" min=\"0\" max=\"12\" value=\"7\" step=\"1\"></div><label class=\"check\"><input type=\"checkbox\" id=\"old\" checked><span data-i18n=\"old\"></span></label><label class=\"check\"><input type=\"checkbox\" id=\"corrupt\"><span data-i18n=\"corrupt\"></span></label><div class=\"track-row\"><span data-i18n=\"bytes\"></span><div id=\"bytes\" class=\"track\"></div></div><div class=\"metrics\"><div><span data-i18n=\"commit\"></span><strong id=\"commit\"></strong></div><div><span data-i18n=\"crc\"></span><strong id=\"crc\"></strong></div><div><span data-i18n=\"chosen\"></span><strong id=\"chosen\"></strong></div></div><p id=\"eq\" class=\"formula\"></p><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:550;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:550 20px ui-monospace,monospace}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:ui-monospace,monospace;font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px ui-monospace,monospace;background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n.panel{border:1px solid var(--rule);border-radius:9px;padding:12px;margin:14px 0;min-width:0}.panel h3{font-size:13px;margin:0 0 10px;color:var(--muted)}.source{font:13px/1.8 ui-monospace,monospace;white-space:pre-wrap;overflow-wrap:anywhere;margin:12px 0}.source mark{background:var(--accent-soft);color:var(--ink);border-bottom:2px solid var(--accent)}.source .fault{background:var(--second-soft);border-color:var(--second)}.diagram{display:block;width:100%;height:auto;max-height:320px;margin:14px 0;overflow:visible}.diagram text{fill:var(--ink);font:12px ui-monospace,monospace}.diagram .edge{stroke:var(--muted);stroke-width:1.5;fill:none}.diagram .edge.on{stroke:var(--accent);stroke-width:3}.diagram .node{fill:var(--surface);stroke:var(--rule);stroke-width:1.5}.diagram .node.on{fill:var(--accent-soft);stroke:var(--accent)}.diagram .node.gold{fill:var(--second-soft);stroke:var(--second)}.log{font:12px/1.8 ui-monospace,monospace;white-space:pre-line;overflow-wrap:anywhere;min-height:7.2em}.table{display:grid;gap:5px;margin:14px 0}.table .tr{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px}.table .tr>*{padding:8px 5px;background:var(--surface);border-radius:4px;min-width:0;overflow-wrap:anywhere;font-size:12px}.table .tr .on{background:var(--accent-soft)}.table .tr .gold{background:var(--second-soft)}.choices button{min-height:44px}.metrics strong{min-height:1.6em}.code-line{display:block}.code-line.on{background:var(--accent-soft);border-left:2px solid var(--accent);padding-left:6px}.status-badge{font-size:12px;color:var(--accent)}.tile-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}.tile-grid .cell{min-height:40px}.actions button{min-height:48px}.form-field{display:grid;gap:8px;margin:14px 0;font-size:13px}.form-field input{width:100%;font-family:ui-monospace,monospace}.metrics.three{grid-template-columns:repeat(3,minmax(0,1fr))}.formula{min-height:3.2em}.presets{flex-wrap:wrap}.presets button{flex-basis:25%;min-height:44px}\n\n.budget{display:flex;height:28px;margin:16px 0;background:var(--surface);border-radius:5px;overflow:hidden}.budget i{display:block;background:var(--accent);transition:width .2s}.budget i.gold{background:var(--second)}.intervals{margin:16px 0}.interval{position:relative;height:36px;background:var(--surface);margin:6px 0;border-radius:4px;overflow:hidden}.interval i{position:absolute;height:100%;background:var(--accent-soft);border-left:2px solid var(--accent)}.interval i.gold{background:var(--second-soft);border-color:var(--second)}.interval span{position:relative;z-index:1;font:12px/36px ui-monospace,monospace;padding-left:7px}.table .tr>*{min-height:4.8em}.formula{white-space:pre-line;min-height:7.2em}.actions:has(>button:only-child){grid-template-columns:1fr}.metrics strong{min-height:3.2em}.rangegrid{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:3px;margin:14px 0}.rangegrid .cell{min-height:30px;padding:3px 1px;font-size:10px}.message{min-height:7.2em;white-space:pre-line;font:13px/1.8 ui-monospace,monospace;overflow-wrap:anywhere}\n/* Allow CJK hanging punctuation without overflowing a metric's border box. */\n.metrics>div{padding-right:4px}.metrics span{line-break:anywhere}\n\n.chart-wrap{margin:18px 0}.chart-label{font-size:12px;color:var(--muted);min-height:3.2em}.curve{display:block;width:100%;height:auto;max-height:320px;overflow:visible}.curve text{font:11px ui-monospace,monospace;fill:var(--muted)}.gridline{stroke:var(--rule);stroke-width:1}.metrics strong{font-size:20px;min-height:3.2em}.formula{font:12px/1.8 ui-monospace,monospace;min-height:9em}.readout{min-height:8em}.presets button{flex-basis:30%}.scope p{overflow-wrap:anywhere} .unit-note{font-size:12px;color:var(--muted);margin:10px 0}\n\n.metrics span{word-break:break-all;line-break:strict}.metrics>div{padding-right:6px}\n\n#bytes{display:grid;grid-template-columns:repeat(6,1fr)}#chosen{min-height:5em}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}const esc=s=>String(s).replace(/[&<>\"']/g,c=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));const fmt=s=>'{'+[...s].sort().join(', ')+'}';function graph(id,nodes,edges){const el=$('#'+id);el.setAttribute('viewBox','0 0 360 200');el.innerHTML='<defs><marker id=\"arrow-'+id+'\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\" markerWidth=\"5\" markerHeight=\"5\" orient=\"auto-start-reverse\"><path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"var(--muted)\"/></marker></defs>'+edges.map(e=>{const a=nodes[e[0]],b=nodes[e[1]],dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1;if(edges.some(r=>r[0]===e[1]&&r[1]===e[0])){const nx=-dy/d,ny=dx/d;return '<path class=\"edge '+(e[2]?'on':'')+'\" d=\"M '+(a.x+dx/d*22+nx*12)+' '+(a.y+dy/d*22+ny*12)+' Q '+((a.x+b.x)/2+nx*38)+' '+((a.y+b.y)/2+ny*38)+' '+(b.x-dx/d*25+nx*12)+' '+(b.y-dy/d*25+ny*12)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}return '<line class=\"edge '+(e[2]?'on':'')+'\" x1=\"'+(a.x+dx/d*25)+'\" y1=\"'+(a.y+dy/d*25)+'\" x2=\"'+(b.x-dx/d*28)+'\" y2=\"'+(b.y-dy/d*28)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}).join('')+nodes.map(n=>'<g><circle class=\"node '+(n.cls||'')+'\" cx=\"'+n.x+'\" cy=\"'+n.y+'\" r=\"25\"/><text x=\"'+n.x+'\" y=\"'+(n.y+4)+'\" text-anchor=\"middle\">'+esc(n.name)+'</text>'+(n.sub?'<text x=\"'+n.x+'\" y=\"'+(n.y+43)+'\" text-anchor=\"middle\">'+esc(n.sub)+'</text>':'')+'</g>').join('');}function table(id,rows){$('#'+id).classList.add('table');$('#'+id).replaceChildren(...rows.map(r=>{const row=document.createElement('div');row.className='tr';row.style.gridTemplateColumns='repeat('+r.length+',minmax(0,1fr))';r.forEach(x=>{const c=document.createElement('div');if(typeof x==='object'){c.textContent=x.text;c.className=x.cls||'';}else c.textContent=x;row.append(c)});return row;}));}function band(id,parts,total){$('#'+id).replaceChildren(...parts.map((p,i)=>{const e=document.createElement('i');e.style.width=(100*p.value/total)+'%';e.className=i%2?'gold':'on';e.title=p.name+': '+p.value;return e}));}function intervals(id,items,total){$('#'+id).replaceChildren(...items.map(p=>{const row=document.createElement('div');row.className='interval';const bar=document.createElement('i');bar.style.left=(100*p.start/total)+'%';bar.style.width=(100*(p.end-p.start)/total)+'%';bar.className=p.cls||'';const label=document.createElement('span');label.textContent=p.label;row.append(bar,label);return row;}));}function plot(id,series,xr,yr,opts={}){const e=$('#'+id),X=x=>42+(x-xr[0])/(xr[1]-xr[0])*298,Y=y=>175-(y-yr[0])/(yr[1]-yr[0])*148;let out='<defs><clipPath id=\"clip-'+id+'\"><rect x=\"42\" y=\"27\" width=\"298\" height=\"148\"/></clipPath></defs>';for(let i=0;i<3;i++){let x=xr[0]+i*(xr[1]-xr[0])/2,y=yr[0]+i*(yr[1]-yr[0])/2;out+='<path class=\"gridline\" d=\"M '+X(x)+' 27V175M42 '+Y(y)+'H340\"/><text x=\"'+X(x)+'\" y=\"194\" text-anchor=\"middle\">'+esc(opts.xfmt?opts.xfmt(x):Number(x.toFixed(2)))+'</text><text x=\"36\" y=\"'+(Y(y)+4)+'\" text-anchor=\"end\">'+esc(opts.yfmt?opts.yfmt(y):Number(y.toFixed(2)))+'</text>';}out+='<g clip-path=\"url(#clip-'+id+')\">';for(const s of series){out+='<path fill=\"none\" stroke=\"'+(s.color||'var(--accent)')+'\" stroke-width=\"2.5\" '+(s.dash?'stroke-dasharray=\"5 4\"':'')+' d=\"'+s.pts.map((p,i)=>(i?'L':'M')+X(p[0]).toFixed(2)+','+Y(p[1]).toFixed(2)).join(' ')+'\"/>';}for(const p of opts.points||[])out+='<circle cx=\"'+X(p[0])+'\" cy=\"'+Y(p[1])+'\" r=\"4\" fill=\"var(--second)\" stroke=\"var(--paper)\" stroke-width=\"1.5\"/>';if(opts.cursor!==undefined)out+='<path d=\"M'+X(opts.cursor)+' 27V175\" stroke=\"var(--muted)\" stroke-dasharray=\"3 3\"/>';e.innerHTML=out+'</g>';e.dataset.curves=JSON.stringify(series.map(s=>s.pts));}const curve=(fn,lo,hi,n=120)=>Array.from({length:n+1},(_,i)=>{const x=lo+(hi-lo)*i/n;return[x,fn(x)]});function inputs(draw){document.querySelectorAll('input,select').forEach(e=>{e.addEventListener('input',draw);e.addEventListener('change',draw)});draw()}const n=id=>+$('#'+id).value;const show=(id,v,d=3)=>set(id,Number(v.toFixed(d)));function crc32(bytes){let c=0xffffffff;for(const b of bytes){c^=b;for(let i=0;i<8;i++)c=(c>>>1)^((c&1)?0xedb88320:0);}return(c^0xffffffff)>>>0}function draw(){let p=n('prefix'),head=[1,8,4,8,16,32,64],crc=crc32(head),record=[...head,...[0,8,16,24].map(s=>(crc>>>s)&255),165],mem=record.map((v,i)=>i<p?v:255);if($('#corrupt').checked&&p>3)mem[3]^=1;const stored=(mem[7]|mem[8]<<8|mem[9]<<16|mem[10]<<24)>>>0,ok=crc32(mem.slice(0,7))===stored,commit=mem[11]===165,valid=commit&&ok&&mem[0]===1&&mem[2]===4,chosen=valid?'new':$('#old').checked?'previous':'none';show('prefix-value',p);cells('bytes',mem.map((v,i)=>({text:v.toString(16).padStart(2,'0').toUpperCase(),cls:i>=p?'empty':i===11?'gold':'on'})));set('commit',+commit);set('crc',+ok);set('chosen',t(chosen));$('#chosen').dataset.state=chosen;set('eq','CRC32 = 0x'+crc.toString(16).padStart(8,'0')+'\\nvalid = format ∧ length ∧ CRC ∧ commit\\nsequence(new) = 8; sequence(old) = 7');}inputs(draw);","audio":false,"strings":{"bytes":"新区12字节(十六进制)","chosen":"恢复选择","commit":"提交标记有效","corrupt":"破坏新记录的一个数据位","crc":"CRC符合","heading":"掉电落在哪个字节,决定选哪个版本?","intro":"直接选择掉电断点,检查提交标记与CRC,再观察旧区是否仍在。","new":"新版本8","none":"无有效记录","old":"保留旧区有效记录","prefix":"掉电前写完字节数","previous":"旧版本7","reset":"重置","scope":"12字节教学记录:版本、序号、长度、4字节数据、CRC32、提交A5;逐字节前缀写入,旧区独立有效,无序号回绕,不模拟真实撕裂范围。","scopeLabel":"模型范围与约定"}}
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('容量单位、跨页拆分、擦除预算、缓冲与保持电容计算通过')
本篇验证模型与断点恢复逻辑,没有擦写实际存储器,也没有进行真实断电测试。
继续查阅