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、量产标准架构:独立参数页 + 结构体打包 + 数据校验 + 变更写保护 + 中断时序保护。

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

相关推荐
沉淀的.晴天8 分钟前
can总线
stm32
桀人24 分钟前
认识AT24C02
stm32·单片机·嵌入式硬件
aaa最北边33 分钟前
Linux 上的交叉编译
linux·运维·服务器
垆边人似月.1 小时前
Python中如何使用get函数实现频次查询
服务器·前端·python
小张成长计划..1 小时前
【Linux】27:日志
linux·运维·服务器
moshuying1 小时前
DeepSeek开源生产级推理框架DeepInfer:单集群一天处理155万亿token,这意味着什么?
前端
别动我齐刘海1 小时前
机器人 C++ / 系统联调补强2——异常处理、Protobuf
linux·c++·人工智能·学习·算法·机器学习·机器人
Lsetea1 小时前
curl -I检查HTTPS返回405:HEAD、GET与证书验收分层排查
linux·http·https·ssl证书·curl
一直C2 小时前
嵌入式 Linux 驱动开发学习笔记:内核定时器、platform 总线与 I²C 驱动
linux·驱动开发·嵌入式硬件·学习·imx6ull
承渊政道2 小时前
Linux网络学习【传输层协议UDP】
linux·网络·网络协议·学习·ubuntu·udp