做嵌入式项目,几乎所有电池设备、工控设备、传感器终端,都需要一个核心功能:参数掉电保存。
设备地址、校准偏移、报警阈值、波特率参数、用户配置......这些数据不能每次上电都恢复默认。
很多人为了省成本、省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、量产标准架构:独立参数页 + 结构体打包 + 数据校验 + 变更写保护 + 中断时序保护。
能用、稳定、抗异常,才是量产代码的核心标准。