Flash 缓存没刷新读旧值——写了缓存未回写,写后 invalidate/回写
文章标签:#嵌入式开发 #单片机 #Flash #OTA升级 #STM32 #扇区 #擦写 #缓存 #回写
一、开篇:那个"写进去了却读不出"的缓存
做 STM32F7/H7(带 DCache/ICache/ART 预取)的设备,往 Flash 写完参数后立刻读回校验,结果读到的还是修改前的旧值。排查半天才发现:CPU 读 Flash 走的是数据缓存(DCache),我写的是 Flash 物理介质,但缓存里那份旧的没失效——读命中缓存旧副本,当然"看不见"新写的内容。复现率 100%(带缓存系列),是"写后读"经典坑。
如果你在带缓存的 MCU 上"写 Flash 后马上读",这一讲能帮你搞清楚缓存一致性。我会从缓存原理讲起,给你 invalidate/回写代码,并附实测对比。
全文导航:入门认知(为什么有缓存会读到旧值)→ 进阶原理(写直达 vs 写回 + 缓存行)→ 高级应用(错误写法复现 + 失效代码 + 实测对比)。
二、入门认知:CPU 看的是"缓存",不是 Flash 本体
带缓存的 MCU(F7/H7 有 L1 DCache/ICache,F4 有 ART 指令预取加速器),CPU 访问 Flash 时先查缓存。缓存里存着"Flash 某段的副本"。当你绕过缓存直接写 Flash 物理地址(Flash 编程控制器就是这么干的),缓存不知道你改了,里面还是旧副本。下次 CPU 读这段,命中缓存旧值——“写了像没写”。
类比:图书馆有书架(Flash)和阅览桌上的复印件(缓存)。你趁闭馆把书架上的书换了(写 Flash),但读者桌上的复印件没换(缓存),读者看复印件还以为书没变。
如图 1 展示"写 Flash 后缓存未失效"导致的读旧值:
图 1:写 Flash 后 DCache 未失效,读命中旧缓存
适用边界:仅带缓存/预取的系列(F4 ART、F7/H7 D/I Cache)需处理;F1/F0 无缓存,无此问题。SRAM 的 DMA 与 CPU 共享也有类似一致性问题。
三、进阶原理:为什么"写完要 invalidate"
Flash 编程属于"非缓存参与的写"(程序直接操作 Flash 控制器寄存器),缓存对此无感知。两种修复:
- Invalidate(失效):丢弃缓存里这段旧副本,下次读强制回 Flash 取新值——用于"我刚写了,缓存旧了"。
- Clean/Write-back(回写):把缓存里的新值刷回物理介质——用于"CPU 改了缓存,要落盘"。
这里场景是"Flash 物理已新、缓存旧",所以用Invalidate。
易混淆点对比:
| 操作 | 用途 | 本场景 |
|---|---|---|
| Invalidate(失效) | 缓存旧,物理新 | ✓ 写 Flash 后读前 |
| Clean(回写) | 缓存新,物理旧 | ✗ 不适用 |
| Clean+Invalidate | 都处理 | 通用但多余 |
[坑] 高频翻车:Invalidate 的地址必须按缓存行对齐(F7/H7 缓存行 32 字节),且长度要覆盖整行。若只失效 4 字节但落在某 32 字节行里,需把整行失效,否则相邻数据也被误失效(虽无害但需对齐计算)。用SCB_InvalidateDCache_by_Addr会自动按行处理。
四、高级应用:复现与根治
错误写法演示(F7/H7)
voidsave_bad(uint32_taddr,uint32_t*data,intn){flash_erase_and_program(addr,data,n);uint32_trd;flash_read(addr,&rd,4);if(rd!=data[0]){/* 必触发!读到的是 DCache 旧副本 */}}正确完整代码(写后 Invalidate 缓存)
#include"stm32f7xx_hal.h"// 或 stm32h7xx_hal.h#include<string.h>voidsave_good(uint32_taddr,uint32_t*data,intn){/* 1. 写 Flash 前,若 data 在可缓存 RAM,先清缓存保证源是对的(本例 data 已就绪可省) */flash_erase_and_program(addr,data,n);/* 2. 写后让 DCache 失效,迫使 CPU 回 Flash 取新值 */SCB_InvalidateDCache_by_Addr((void*)addr,n*4);/* 3. 现在读到的就是物理新值 */uint32_trd;flash_read(addr,&rd,4);// rd == data[0] ✓}/* 若启用 D-Cache 且 Flash 运行代码也会被改(OTA 写 App 区),还要失效 I-Cache */voidafter_ota_write(uint32_taddr,intlen){SCB_InvalidateDCache_by_Addr((void*)addr,len);SCB_InvalidateICache();// App 区代码变了,指令缓存也要清}F4 系列用 ART 预取加速器,写 Flash 后一般无需手动失效(ART 不缓存数据),但若开了数据缓存相关特性,按手册处理。F1/F0 无缓存,跳过本讲。
改前 vs 改后实测对比
| 指标 | 写后不失效 | 写后 Invalidate |
|---|---|---|
| 写后读回值 | 旧值(错) | 新值(对) |
| 校验通过率 | 0% | 100% |
| 受影响系列 | F4+/F7/H7 | 同上,修复后正常 |
[提示] 读 Flash 频繁且数据不变时,保留缓存能加速;只有"刚写过这块"才需失效。切忌每次读都全量SCB_InvalidateDCache()(清全部缓存性能差),用by_Addr精准失效。
五、收尾:要点复盘与最佳实践
核心要点:① 带缓存 MCU,写 Flash 后缓存旧副本导致读旧值;② 写后必须SCB_InvalidateDCache_by_Addr;③ 改 App 代码区还要失效 I-Cache;④ 失效按行对齐。
最佳实践清单(可收藏):
- F7/H7 写 Flash 后,读前必
SCB_InvalidateDCache_by_Addr(地址, 长度)。 - OTA 写 App 区后,额外
SCB_InvalidateICache()。 - 用
by_Addr精准失效,别每次全量清缓存。 - 确认芯片是否带缓存(F1/F0 无,无需处理)。
- DMA 与 CPU 共享 SRAM 同样有缓存一致性,按需 Clean/Invalidate。
进阶延伸:若把 Flash 映射到可缓存属性(MPU 配置),务必设成"写透(WT)"或"不可缓存"避免一致性麻烦;RT 系列/带 Cache 的 M7 务必仔细配 MPU。
互动:你在 F7/H7 上被"缓存读了旧值"坑过吗?排查了多久?评论区见。