STM32 Flash 扇区擦写与磨损均衡详解:从底层原理到工程实践
👁 2 阅读 · 2026-08-19 · STM32 存储
STM32 内置 Flash 是嵌入式系统中常用的非易失性存储,但其擦写寿命有限且操作有严格约束。本文深入剖析 STM32 Flash 的扇区结构、擦写时序与硬件限制,重点讲解磨损均衡算法的设计思路与实现方法,并给出基于标准库的完整代码示例,帮助开发者安全、高效地管理片上存储,延长设备使用寿命。
## 一、STM32 Flash 硬件特性与擦写原理
STM32 系列(如 F1、F4、H7)内置 Flash 采用 NOR 型工艺,具有以下关键特性:
- **最小擦除单位**:扇区(Sector)或块(Block),不同型号大小不同(如 F103 的 1KB/扇区,F407 的 16KB/扇区)。
- **写入单位**:半字(16位)或字(32位),需先擦除后写入。
- **擦除操作**:将扇区所有位写为 0xFF,耗时约 20-40ms(取决于型号)。
- **编程操作**:将某些位从 1 改为 0,不可逆,需按字/半字写入。
- **寿命限制**:典型擦写次数 10,000 次(工业级可达 100,000 次),超过后可能产生坏块。
**关键时序**:擦除和编程操作需要解锁 Flash(写入 KEY1/KEY2 到 FLASH_KEYR),操作完成后重新锁定。若违反时序(如未等待 BSY 位清零),会导致总线错误或数据损坏。
## 二、磨损均衡的必要性与设计思路
若固定使用同一扇区存储参数,写入 10,000 次后扇区即失效。磨损均衡(Wear Leveling)的核心思想是**将写入操作分散到多个扇区**,避免热点扇区过早老化。
### 常见策略
- **静态均衡**:将数据均匀分配到所有扇区,每次写入选择下一个扇区(轮询)。
- **动态均衡**:记录每个扇区的擦写次数,优先选择次数最少的扇区。
- **日志结构**:将数据以日志形式追加到当前活动扇区,写满后擦除最旧扇区。
### 设计要点
- 扇区数量至少 2 个,推荐 4-8 个以降低擦写频率。
- 需要元数据(如扇区状态、序号)来管理有效性。
- 掉电安全:需考虑写入中断时的恢复机制(如双备份)。
## 三、基于 STM32F103 的磨损均衡实现
以下示例使用 STM32F103 的 4 个 1KB 扇区(如扇区 0-3,地址 0x08000000 起),实现简单的轮询磨损均衡。
### 1. 硬件配置
- 使用标准外设库(SPL)或 HAL 库,本文以 SPL 为例。
- 定义扇区基地址和数量。
```c
#define FLASH_BASE_ADDR 0x08000000
#define SECTOR_SIZE 1024
#define SECTOR_COUNT 4
#define DATA_ADDR (FLASH_BASE_ADDR + 0x10000) // 避开程序区,假设程序占用 <64KB
```
### 2. Flash 底层操作函数
```c
// 擦除指定扇区(按地址)
void Flash_EraseSector(uint32_t addr) {
FLASH_Unlock();
while (FLASH_GetFlagStatus(FLASH_FLAG_BSY) == SET);
if (FLASH_ErasePage(addr) != FLASH_COMPLETE) {
// 错误处理
}
FLASH_Lock();
}
// 写入一个字(32位)
void Flash_WriteWord(uint32_t addr, uint32_t data) {
FLASH_Unlock();
while (FLASH_GetFlagStatus(FLASH_FLAG_BSY) == SET);
if (FLASH_ProgramWord(addr, data) != FLASH_COMPLETE) {
// 错误处理
}
FLASH_Lock();
}
// 读取一个字
uint32_t Flash_ReadWord(uint32_t addr) {
return *(volatile uint32_t*)addr;
}
```
### 3. 磨损均衡管理结构
使用扇区头部存储元数据:状态(有效/无效)、序号(递增)。
```c
#define SECTOR_STATUS_VALID 0xA5A5A5A5
#define SECTOR_STATUS_INVALID 0x00000000
typedef struct {
uint32_t status;
uint32_t seq; // 写入序号,越大越新
uint32_t data[250]; // 实际数据,1KB-8字节头部
} SectorHeader;
```
### 4. 核心函数实现
```c
// 查找当前活动扇区(序号最大的有效扇区)
uint32_t FindActiveSector(void) {
uint32_t max_seq = 0;
uint32_t active_addr = 0;
for (int i = 0; i < SECTOR_COUNT; i++) {
uint32_t addr = DATA_ADDR + i * SECTOR_SIZE;
SectorHeader *hdr = (SectorHeader*)addr;
if (hdr->status == SECTOR_STATUS_VALID && hdr->seq >= max_seq) {
max_seq = hdr->seq;
active_addr = addr;
}
}
return active_addr;
}
// 写入数据(执行磨损均衡)
void Flash_WriteData(uint32_t *data, uint32_t len) {
uint32_t active = FindActiveSector();
uint32_t new_seq = 0;
if (active != 0) {
SectorHeader *hdr = (SectorHeader*)active;
new_seq = hdr->seq + 1;
}
// 选择下一个扇区(轮询)
int idx = (active == 0) ? 0 : ((active - DATA_ADDR) / SECTOR_SIZE + 1) % SECTOR_COUNT;
uint32_t new_addr = DATA_ADDR + idx * SECTOR_SIZE;
// 擦除新扇区
Flash_EraseSector(new_addr);
// 写入头部和数据
SectorHeader new_hdr;
new_hdr.status = SECTOR_STATUS_VALID;
new_hdr.seq = new_seq;
Flash_WriteWord(new_addr, new_hdr.status);
Flash_WriteWord(new_addr + 4, new_hdr.seq);
for (uint32_t i = 0; i < len; i++) {
Flash_WriteWord(new_addr + 8 + i*4, data[i]);
}
// 将旧扇区标记为无效(擦除后写入状态字)
if (active != 0) {
Flash_WriteWord(active, SECTOR_STATUS_INVALID);
}
}
// 读取最新数据
void Flash_ReadData(uint32_t *buf, uint32_t len) {
uint32_t active = FindActiveSector();
if (active == 0) return; // 无数据
SectorHeader *hdr = (SectorHeader*)active;
for (uint32_t i = 0; i < len; i++) {
buf[i] = hdr->data[i];
}
}
```
### 5. 使用示例
```c
int main(void) {
uint32_t write_data[4] = {0x12345678, 0x9ABCDEF0, 0xDEADBEEF, 0xCAFEBABE};
uint32_t read_data[4] = {0};
// 写入数据(每次调用会切换扇区)
Flash_WriteData(write_data, 4);
// 读取数据
Flash_ReadData(read_data, 4);
while (1);
}
```
## 四、注意事项与优化建议
- **掉电保护**:在写入新扇区前,先标记旧扇区无效,再擦写新扇区,可避免数据丢失。但若在擦除新扇区时掉电,旧数据仍可恢复。更可靠的做法是使用双缓冲(A/B区)。
- **擦写次数统计**:可在每个扇区头部记录擦写次数,动态选择最少使用的扇区,但会增加管理开销。
- **对齐与缓存**:写入时尽量按字对齐,避免跨扇区操作。
- **中断处理**:Flash 操作期间应关闭中断或确保中断不访问 Flash,否则可能引发硬件错误。
- **使用 HAL 库**:若使用 HAL,函数名类似 `HAL_FLASH_Program`,但底层原理相同。
- **性能考量**:磨损均衡会增加写入延迟(擦除+写入),对于高频写入场景,可考虑外部 EEPROM 或 SPI Flash。
## 五、总结
STM32 内置 Flash 的扇区擦写与磨损均衡是嵌入式存储管理的基础。通过合理设计扇区管理策略,可以显著延长 Flash 寿命,保证数据可靠性。本文提供的轮询算法简单有效,适用于大多数参数存储场景。对于更复杂的系统,可结合文件系统(如 LittleFS)或使用外部存储。理解底层原理,才能写出健壮的存储代码。