尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

STM32 片上 Flash 读写实战:把参数存进芯片,掉电不丢

STM32 片上 Flash 读写实战:把参数存进芯片,掉电不丢 我早年做个温控器用户拿面板按键设个目标温度 26 度再调两下 PID 的 P、I 参数。我图省事这些全塞在一个全局结构体里RAM 里跑得挺欢。结果客户打电话来“我家地暖昨天设的 28 度今早变回出厂默认 20 度了大冬天给我冻醒。”我一拍脑门——对哦断电重启全局变量全清零谁给你记得住。更坑的是 ADC 同步采样、低功耗唤醒这类场景的校准偏移值每次上电重新算一遍又慢又漂好不容易标定的零点偏移说没就没。后来才明白STM32 片内那块 Flash本来就是拿来存固件的你代码只占前面一小段后面大把没用到的页就是一块断电不丢、随你当 EEPROM 用的存储区。把它用起来参数掉电就保留了。一、片上 Flash 到底是个啥脾气和 RAM 最大的区别在三个字它嫌烦。只能 1 写 0不能 0 写 1。Flash 里每个 bit 出厂全是 10xFF。你写数据是把某些 bit 清成 0。想把一个已经为 0 的 bit 改回 1做不到除非整页擦除。写之前必须整页/整扇区擦除。擦除的操作就是把一整页恢复成全 10xFFFF…。不擦直接写原来 0 的位置永远改不回来数据就错了。有写保护得先解锁。Flash 寄存器默认锁着写之前往密钥寄存器塞两个魔数解锁写完好锁回去防止程序跑飞误改固件。读随便写很贵。读它跟读 RAM 差不多随意写要按固定粒度半字/字/双字对齐编程一页擦除动辄几十毫秒而且写的时候这块 Flash 总线是锁死的。低功耗节点把采得省解决了这一篇解决采完的配置存得住——校准偏移、用户设定掉电不丢下次上电直接读。二、CubeMX 怎么配Flash 在 CubeMX 里没有专门的外设开关它跟着 AHB 时钟走默认就开着。你要确认的是三件事1. 系统时钟 → FLASH 等待周期LATENCYCPU 主频越高读 Flash 取指要等的周期越多。CubeMX 在SystemClock_Config()里会按你设的频率自动配FLASH_LATENCY比如 F103 跑 72MHz 要 2 个 wait state。你自己改时钟后得回头确认这个值对——配小了取指错程序跑飞。2. 确认参数页地址避开你的代码最稳的办法是看编译出来的.map文件找到代码实际占到的末尾地址把参数页放在 Flash最末尾且留足余量。F103C8T6 是 64KB Flash、每页 1KB最后一页就是0x0800FC00F407 是 1MB、扇区 11 在0x080E0000128KB。3. 链接脚本留区域进阶可选不想手算地址的在链接脚本里加一段_flash_param区域让链接器保证你的代码不会长到那去。初学阶段直接用固定常数地址也行只要留够余量。三、最少的代码读很简单Flash 随意读拿指针解引用就行。写要按解锁→擦页→编程→锁四步来。typedefstruct{uint16_tversion;// 版本号, 区分不同固件结构的参数floattarget_temp;// 目标温度, 用户设的int16_tadc_offset;// ADC 校准偏移, 同步采样场景用得上uint16_tcrc;// 校验和, 防读坏}Param_t;#definePARAM_VERSION1#definePARAM_FLASH_ADDR((uint32_t)0x0800FC00)// F103 最后一页(1KB)Param_t g_param;// ---- 读: Flash 随意读, 直接指针解引用, 但必须校验 ----staticuint16_tcalc_crc(Param_t*p){uint16_tsum0;uint8_t*b(uint8_t*)p;for(inti0;isizeof(Param_t)-2;i)sumb[i];returnsum;}voidparam_load(void){Param_t*p(Param_t*)PARAM_FLASH_ADDR;// 版本不对 或 校验不过 - 说明没写过/写坏了, 用默认, 别拿垃圾当真值if(p-versionPARAM_VERSIONcalc_crc(p)p-crc){g_param*p;}else{g_param.versionPARAM_VERSION;g_param.target_temp20.0f;// 出厂默认g_param.adc_offset0;}}// ---- 写: 解锁 - 擦页 - 编程 - 锁 ----HAL_StatusTypeDefparam_save(void){g_param.versionPARAM_VERSION;g_param.crccalc_crc(g_param);HAL_FLASH_Unlock();// 1. 解锁FLASH_EraseInitTypeDef ei{0};// 2. 擦整页(页内全变0xFF)ei.TypeEraseFLASH_TYPEERASE_PAGES;ei.PageAddressPARAM_FLASH_ADDR;ei.NbPages1;uint32_terr0;HAL_FLASHEx_Erase(ei,err);// 3. 按半字(16位)编程, F1 系列特点; 地址必须 2 对齐uint16_t*src(uint16_t*)g_param;for(inti0;isizeof(Param_t)/2;i){HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD,PARAM_FLASH_ADDRi*2,src[i]);}HAL_FLASH_Lock();// 4. 锁回returnHAL_OK;}这段在干嘛上电param_load从末尾那页指读取结构体版本号和校验和都过了才用不然回落到出厂默认——这步救过我好几次参数页写一半掉电变砖的命。存的时候param_save先解锁把整页擦干净再一个半字一个半字把结构体写进去最后锁回去。F1 系列是半字16 位编程你要是 F4/F7用的是字32 位或双字64 位编程见后面坑 3。★ 关键认知Flash 不是能改写的 RAM是断电不丢、但写起来很贵的 ROM。读它随便写它先擦、对齐、解锁、关中断。你把它当 EEPROM 随手param_save()迟早吃瘪。四、真正会坑你的几个点1. 写之前没擦除。最高频的坑。Flash 只能 1 写 0你跳过擦除直接HAL_FLASH_Program原来为 0 的 bit 永远回不了 1写进去的数据跟你想的完全两样。记住擦除是写的前置动作页内全变 0xFF 之后才能编程。2. 擦除/编程前没解锁。HAL_FLASH_Unlock()不调Flash 寄存器是锁的所有写操作直接返回HAL_ERROR你以为存进去了其实啥也没动。忘记解锁是静默失败的典型——函数返回错参数页还是旧的。3. 编程粒度/地址对齐错。F1 只能半字16 位编程地址必须 2 字节对齐F4/L4 是字32 位或双字64 位对齐H7 支持 256 位线但编程单位是双字。你用uint32_t指针按字节往里怼、或地址没对齐直接 HardFault 或写失败。照着你芯片手册的编程并行度来选FLASH_TYPEPROGRAM_xxx。4. 地址算错擦到自己的代码。★ 最惨的坑。参数页地址没算准落在你的固件身上一调HAL_FLASHEx_Erase把自己的程序擦没了——板子变砖得拿下载器重刷。务必看.map确认代码实际大小参数页放 Flash 最末尾留足余量或者链接脚本里划一块专属区域让链接器保证代码长不过去。5. 写 Flash 时来了个中断直接 HardFault。★ 第二惨的坑。CPU 取指、中断向量表、常量全在 Flash 里。擦写 Flash 时那块 Bank 的总线是锁死的期间任何读 Flash的操作会被 stall 甚至 HardFault。你正在param_save擦页几十毫秒突然来个定时器中断CPU 去 Flash 取中断向量和 ISR 代码——取不到HardFault 原地爆炸。写 Flash 期间要么__disable_irq()关中断要么把关键代码/向量搬 RAM要么接受这段时间里别跑别的 Flash 代码。6. FLASH_LATENCY 没跟上频率。你把 CPU 超到 72MHz 但 LATENCY 还配成 0 wait state读 Flash 取指时序不对程序跑飞、进 HardFault。HAL 的SystemClock_Config一般自动配好但你手动改时钟后一定回头核对等待周期。7. 在中断里调 param_save整机卡死。页擦除动辄二三十毫秒在中断服务程序里调param_save别的实时任务全被卡住而且还会撞上坑 5 的 HardFault。写 Flash 放主循环、关中断或搬 RAM 里做别在 ISR 里干。8. 页大小因型号天差地别。F103C8 是 1KB/页F407 是扇区16/64/128KB 不等F0 有 1KB/2KB 页L4 又是 2KB 页。你照抄别人擦第 63 页的代码型号一换页大小变了地址全错轻则写不进、重则擦到代码见坑 4。每次换芯片先翻 reference manual 的 Flash 组织表。9. 频繁写寿命耗尽。Flash 擦写寿命大约 1 万次部分型号 10 万。你每采一笔、每动一下按键就param_save一天几万次几小时写废。做法只在参数真正变化时才存或上磨损均衡——用两页乒乓或在一页里顺序追加写轮流用把磨损摊开见下图。10. 不校验就读读出垃圾当真值。Flash 擦写中途掉电参数页写一半半残。读出来 version 不对、crc 不过必须 fallback 默认并标记无效不然读到一堆乱七八糟的数去控温、去标定偏移出的是实打实的事故。我那个温控器一开始没做校验偶尔冷启动会读到离谱的 target_temp差点烧了加热棒。五、完整例程骨架把读校验、写四步、磨损均衡串起来上电读校验参数变了才存用两页乒乓把擦写寿命摊开。#definePARAM_PAGE_A0x0800FC00#definePARAM_PAGE_B0x0800F800// 上一页, 乒乓用voidparam_load(void){Param_t*p(Param_t*)PARAM_PAGE_A;if(p-versionPARAM_VERSIONcalc_crc(p)p-crc){g_param*p;return;}p(Param_t*)PARAM_PAGE_B;// A 坏了试 Bif(p-versionPARAM_VERSIONcalc_crc(p)p-crc){g_param*p;return;}g_paramdefault_param();// 都坏, 用出厂默认}HAL_StatusTypeDefparam_save(void){g_param.versionPARAM_VERSION;g_param.crccalc_crc(g_param);HAL_FLASH_Unlock();// 轮流擦 A / 擦 B, 把磨损摊开staticinttoggle0;uint32_taddrtoggle?PARAM_PAGE_B:PARAM_PAGE_A;FLASH_EraseInitTypeDef ei{0};ei.TypeEraseFLASH_TYPEERASE_PAGES;ei.PageAddressaddr;ei.NbPages1;uint32_terr0;HAL_FLASHEx_Erase(ei,err);uint16_t*src(uint16_t*)g_param;for(inti0;isizeof(Param_t)/2;i)HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD,addri*2,src[i]);HAL_FLASH_Lock();returnHAL_OK;}磨损均衡的本质别总怼同一页两页轮流擦写寿命翻倍要求更高就在一页里顺序追加多条记录、写满再换页。平均到每页的擦写次数降下来十年也写不废。Flash 读写这件事难的不是那几行Unlock/Erase/Program/Lock是搞清楚它不能像 RAM 那样随便改——读随便写先擦、对齐、解锁、关中断别把它当 EEPROM 随手怼。把参数页放对位置、写好校验、加上磨损均衡它就是一块稳稳断电不丢的小仓库。适合需要掉电保留参数的任何场景用户设定、校准偏移、配置标志、运行计数正好给低功耗节点做配置持久化补完。不适合高频连续记录Flash 慢且寿命有限那种该上外部 SPI Flash 或 FRAM。铁律四条写前必擦除、写前必解锁、写期间必关中断或搬 RAM防 HardFault、参数必带版本号加校验防读坏参数页地址务必核对芯片手册的页/扇区大小和代码实际占用千万别擦到自己的固件。
返回列表