STM32片内Flash读写深度详解|掉电参数保存、底层原理

做嵌入式项目,几乎所有电池设备、工控设备、传感器终端,都需要一个核心功能:参数掉电保存。

设备地址、校准偏移、报警阈值、波特率参数、用户配置......这些数据不能每次上电都恢复默认。

很多人为了省成本、省PCB空间,不愿意外挂AT24C02 EEPROM,直接用STM32片内Flash模拟掉电存储。

但90%开发者的Flash代码,只能"勉强跑通",完全不具备量产能力:

随机丢参数、上电读取乱码、频繁擦写导致芯片报废、擦写中途断电数据崩坏、升级固件覆盖参数区......

这些问题根本不是代码写错,而是完全不懂Flash硬件底层特性。


很多人把Flash当成"慢速RAM"使用,这是所有Bug的根源。

Flash:读写规则极度苛刻,物理特性决定操作逻辑。

✅写入编程:只能把 1 → 0

重点硬核结论:

比如某地址已经是 0x11,你想改成 0x22,直接写无效,必须先整页擦除,再重新写入。


1、擦除最小单位是「页」,不是字节

哪怕你只修改1个字节,也必须把整页2KB数据全部擦除。

2、Flash有擦写寿命,绝对不能高频保存

如果你的代码放在while循环、1秒保存一次,几天就能把芯片Flash写废,彻底损坏、无法修复。

Flash擦写过程中,内核无法正常读取Flash指令。


新手写法:随便找末尾地址直接读写。

1、独立分区,杜绝固件覆盖

后续代码量增加、固件升级,直接覆盖参数区,设备批量丢配置。

2、结构体统一管理参数

3、增加校验码,防止数据崩坏

带上校验码,上电自动校验,异常自动恢复默认参数,设备不会跑飞。

四、可直接量产的极简代码(HAL库、无冗余)

1、参数结构体与宏定义

// 专属参数页地址(根据自己芯片Flash大小合理分配)

#define FLASH_PARAM_PAGE_ADDR 0x08008000

// 设备全部可掉电保存参数

typedef struct

{

uint8_t dev_addr; // 设备通信地址

uint16_t temp_limit; // 温度报警阈值

uint16_t humi_limit; // 湿度报警阈值

uint32_t baud_rate; // 串口波特率

uint32_t check_sum; // 数据校验码

}DeviceParam_t;

2、校验码计算(保证数据完整性)

static uint32_t Param_CheckSum(DeviceParam_t *param)

{

uint32_t sum = 0;

uint8_t *buf = (uint8_t *)param;

for(int i = 0; i < sizeof(DeviceParam_t)-4; i++)

{

sum += bufi;

}

return sum;

}

3、Flash读取参数(上电调用)

uint8_t Flash_Read_DeviceParam(DeviceParam_t *param)

{

// 直接读取指定Flash地址

*param = *(DeviceParam_t *)FLASH_PARAM_PAGE_ADDR;

复制代码
// 校验失败说明数据损坏/首次上电
if(param->check_sum != Param_CheckSum(param))
{
    return 0;
}
return 1;

}

4、Flash写入参数(变更才保存)

uint8_t Flash_Write_DeviceParam(DeviceParam_t *param)

{

FLASH_EraseInitTypeDef erase_init;

uint32_t err_page = 0;

复制代码
// 更新校验码
param->check_sum = Param_CheckSum(param);

HAL_FLASH_Unlock();  // 解锁Flash
__HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPERR);

// 整页擦除
erase_init.TypeErase = FLASH_TYPEERASE_PAGES;
erase_init.PageAddress = FLASH_PARAM_PAGE_ADDR;
erase_init.NbPages = 1;
if(HAL_FLASHEx_Erase(&erase_init, &err_page) != HAL_OK)
{
    HAL_FLASH_Lock();
    return 0;
}

// 逐字写入结构体数据
uint32_t *p_buf = (uint32_t *)param;
for(int i = 0; i < sizeof(DeviceParam_t)/4; i++)
{
    HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, FLASH_PARAM_PAGE_ADDR + i*4, p_buf[i]);
}

HAL_FLASH_Lock(); // 锁定保护
return 1;

}

5、初始化调用逻辑(工程标准写法)

DeviceParam_t g_dev_param;

void Device_Param_Init(void)

{

// 读取失败:首次上电/数据损坏,加载默认参数并保存

if(Flash_Read_DeviceParam(&g_dev_param) == 0)

{

g_dev_param.dev_addr = 1;

g_dev_param.temp_limit = 35;

g_dev_param.humi_limit = 80;

g_dev_param.baud_rate = 9600;

复制代码
    Flash_Write_DeviceParam(&g_dev_param);
}

}

五、量产项目必避的6大致命坑

参数没变化就不要写Flash!

2、擦写期间必须屏蔽中断

3、绝对不要在中断里写Flash

4、必须做数据校验,不能裸读

5、禁止随意选Flash地址

6、不要超大结构体跨页存储


上面的单页方案,足够满足95%普通项目。

擦写中途断电,整页数据直接报废,无法恢复。

A页存当前参数,B页做备份。写数据先写备份区,成功后再更新主区。


✅ 适合片内Flash存储

❌ 不适合Flash


1、STM32 Flash的核心特性:写只能1变0,擦除才能0变1,必须按页操作。

3、量产标准架构:独立参数页 + 结构体打包 + 数据校验 + 变更写保护 + 中断时序保护。

能用、稳定、抗异常,才是量产代码的核心标准。

相关推荐
shimly1234561 小时前
比 find + grep 更好用的 nm + addr2line
linux
迷途之人不知返1 小时前
【进程控制】-3-进程等待
linux
data analyse 4561 小时前
埋点工具的私有化部署成本高吗?
前端·数据分析·github
_upupup2 小时前
包管理器和vim
linux·服务器
一颗小树x2 小时前
NVIDIA 驱动故障排查实录:内核升级导致 nvidia-smi 失效
linux·服务器·nvidia驱动·启动失败·内核升级
_smart_boy__2 小时前
I.MX6U开发板Uboot无法ping Ubuntu问题解决方案(二)
linux·运维·ubuntu
data analyse 4562 小时前
埋点工具的埋点查询语言难学吗?
前端·数据分析
DianSan_ERP2 小时前
多平台订单自动下载与回传的技术实现:从消息推送到状态闭环引言
java·linux·服务器·前端·网络·架构·自动化
恋猫de小郭2 小时前
Shopify 回应为什么从 RN 回到原生,为什么不用 KMP ?
android·前端·flutter