1. 项目概述
在嵌入式产品开发中,数据存储是个永恒的话题。最近我在开发一款计量产品时,遇到了一个典型的存储难题:需要每20分钟记录两个32位数据(共8字节),但产品对成本极其敏感,无法使用外置EEPROM或FRAM。这让我把目光投向了STM32内置的Flash存储器。
Flash存储器有个致命弱点——擦写寿命有限(通常10万次)。按20分钟一次的频率计算,不到3年就会耗尽寿命。但经过巧妙设计,我实现了将擦除次数降低到原来的1/64,使理论寿命延长到200年以上。这个方案的核心就是"扇区滚动存储"算法。
2. 技术原理分析
2.1 Flash存储特性
STM32的Flash存储器有几个关键特性需要特别注意:
- 最小擦除单位:必须以扇区(Page)为单位擦除,STM32F030的扇区大小为1KB(实际项目中我使用512字节的子扇区)
- 写入限制:只能将1变为0,不能将0变为1(必须擦除才能变回1)
- 寿命限制:每个扇区约10万次擦写周期
- 对齐要求:写入必须按32位对齐
2.2 滚动存储算法设计
传统Flash存储方式是擦除-写入-擦除的循环,而滚动存储的核心思想是:
- 将一个扇区划分为多个记录槽(本方案中512字节/8字节=64个槽)
- 每次写入时找到第一个空白槽写入
- 当所有槽写满后才执行擦除操作
- 这样实际擦除次数降为原来的1/64
注意:记录大小必须是4的倍数,因为STM32 Flash编程最小单位是32位字
3. 具体实现方案
3.1 硬件配置
本项目使用的硬件平台:
- MCU: STM32F030C8T6(Cortex-M0内核)
- Flash容量: 64KB
- 扇区大小: 1KB(实际使用前半部分512字节)
3.2 关键参数定义
c复制#define FLASH_SECTOR_BASE 0x0800FC00 // 使用最后一个扇区的前512字节
#define FLASH_SECTOR_SIZE 512 // 实际使用扇区大小
#define RECORD_SIZE 8 // 每条记录8字节(2个uint32)
#define RECORD_NUM (FLASH_SECTOR_SIZE/RECORD_SIZE) // 64条记录
3.3 核心函数实现
3.3.1 记录空白检查
c复制static BOOL Flash_IsRecordEmpty(uint32_t addr)
{
for (uint32_t i = 0; i < (RECORD_SIZE/4); ++i) {
if (*((uint32_t*)(addr + i*4)) != 0xFFFFFFFF)
return FALSE;
}
return TRUE;
}
这个函数检查指定地址的记录是否全为0xFF(擦除状态)。Flash擦除后所有位变为1,所以未写入的记录应该是全0xFFFFFFFF。
3.3.2 滚动写入函数
c复制int Flash_RollingWrite(const uint8_t *data, uint16_t len)
{
if (len > RECORD_SIZE) return -1;
FLASH_Unlock();
__disable_irq(); // 写Flash期间禁止中断
// 查找第一个空白记录
uint32_t writeAddr = 0;
int writeIdx = -1;
for (int i = 0; i < RECORD_NUM; ++i) {
uint32_t addr = FLASH_SECTOR_BASE + i * RECORD_SIZE;
if (Flash_IsRecordEmpty(addr)) {
writeAddr = addr;
writeIdx = i;
break;
}
}
// 扇区已满,需要擦除
if (writeIdx == -1) {
FLASH_ErasePage(FLASH_SECTOR_BASE);
writeAddr = FLASH_SECTOR_BASE;
writeIdx = 0;
}
// 写入数据(按32位对齐)
for (uint16_t i = 0; i < RECORD_SIZE; i += 4) {
uint32_t value = 0xFFFFFFFF;
if (i < len) {
// 将数据拷贝到32位变量
for (uint8_t j = 0; j < 4 && (i + j) < len; ++j)
((uint8_t*)&value)[j] = data[i + j];
}
FLASH_ProgramWord(writeAddr + i, value);
}
__enable_irq();
FLASH_Lock();
return writeIdx;
}
3.3.3 读取最新记录
c复制int Flash_RollingReadLatest(uint8_t *outBuf)
{
for (int i = RECORD_NUM - 1; i >= 0; --i) {
uint32_t addr = FLASH_SECTOR_BASE + i * RECORD_SIZE;
if (!Flash_IsRecordEmpty(addr)) {
memcpy(outBuf, (void*)addr, RECORD_SIZE);
return RECORD_SIZE;
}
}
return -1; // 没有有效数据
}
这个函数从后向前查找,返回最后写入的有效记录,实现了"最新数据优先"的读取策略。
4. 实际应用中的优化技巧
4.1 扇区选择策略
- 避开程序存储区:选择靠后的扇区,避免影响程序运行
- 双扇区备份:对于关键数据,可以使用两个扇区交替存储
- 磨损均衡:多个扇区轮换使用,延长整体寿命
4.2 数据完整性保障
- CRC校验:每条记录可以增加2字节CRC校验
- 序列号:每条记录添加递增序号,便于恢复时排序
- 数据头标记:使用特定模式标记有效数据头
改进后的记录结构示例:
c复制#pragma pack(push, 1)
typedef struct {
uint16_t crc;
uint16_t seq;
uint32_t data1;
uint32_t data2;
} FlashRecord;
#pragma pack(pop)
4.3 低功耗优化
- 批量写入:积累多条记录后一次性写入
- 写入前检查:避免重复写入相同数据
- 延迟擦除:非必要时推迟擦除操作
5. 常见问题与解决方案
5.1 写入失败排查
- 地址未对齐:确保写入地址是4的倍数
- 未解锁Flash:写入前必须调用FLASH_Unlock()
- 写保护:检查选项字节(Option Bytes)设置
5.2 数据丢失问题
- 电源稳定性:写入期间掉电会导致数据损坏
- 解决方案:增加大电容,检测电压后再写入
- 中断干扰:在写操作期间发生中断
- 解决方案:写入前关闭中断(__disable_irq())
5.3 跨平台适配
对于不同STM32型号需要注意:
- 扇区大小:F0系列通常1KB/2KB,F4系列可能有不同大小
- 编程粒度:有些型号支持字节编程,有些必须字编程
- 库函数差异:HAL库与标准外设库接口不同
6. 性能评估与优化
6.1 寿命计算
原始方案:
- 20分钟写入一次 → 每天72次 → 每年26,280次
- 10万次寿命 → 约3.8年
滚动存储方案(64次/擦除):
- 实际擦除次数降为1/64 → 理论寿命64×3.8≈243年
6.2 写入速度测试
在STM32F030@48MHz下的实测结果:
- 单次写入(8字节):约1.2ms
- 整页擦除(1KB):约20ms
6.3 内存占用分析
- 代码增加:约1.5KB Flash
- RAM占用:仅需少量栈空间
7. 进阶应用场景
7.1 日志系统实现
基于滚动存储可以构建简易日志系统:
c复制void Log_Write(const char* msg)
{
uint32_t timestamp = HAL_GetTick();
uint8_t buf[RECORD_SIZE];
memcpy(buf, ×tamp, 4);
strncpy(buf+4, msg, RECORD_SIZE-4);
Flash_RollingWrite(buf, RECORD_SIZE);
}
7.2 配置参数存储
存储设备配置参数,支持版本兼容:
c复制typedef struct {
uint8_t ver; // 数据结构版本
uint8_t reserved[3];
uint32_t param1;
uint32_t param2;
} DeviceConfig;
7.3 大数据块存储
对于大于单条记录的数据,可以采用分块存储:
- 添加块头标记(Block Header)
- 使用连续多条记录存储
- 添加块CRC校验
8. 替代方案对比
8.1 外置EEPROM
优点:
- 擦写寿命高(100万次以上)
- 字节可寻址,使用简单
缺点:
- 增加BOM成本
- 占用I2C/SPI接口
- 速度较慢
8.2 FRAM铁电存储器
优点:
- 几乎无限次擦写
- 高速读写
- 字节可寻址
缺点:
- 成本最高
- 供货可能不稳定
8.3 内置Flash方案
优点:
- 零成本
- 读写速度快
- 不占用额外接口
缺点:
- 需要复杂的管理算法
- 存在寿命限制
- 写入期间CPU停顿
在实际项目中,我最终选择了内置Flash方案,因为它完美满足了我们对成本的苛刻要求,而通过滚动存储算法有效克服了Flash的寿命限制。这套方案已经稳定运行超过1年,经历了各种严苛环境测试,证明其可靠性完全满足工业级应用需求。