STM32F103片内Flash 存储器操作(FLASH页划分)

一、Flash 基础

1.1 什么是 Flash?

Flash Memory: 闪存存储器

定义: 一种非易失性存储器,掉电后数据不丢失

核心特性:

  • 非易失性: 掉电后数据不丢失
  • 可擦写: 可以多次擦除和写入
  • 块操作: 擦除以页为单位,写入以字为单位
  • 寿命有限: 约 1 万次擦写循环

STM32F103 Flash 特性:

  • 容量:64KB~512KB(根据型号)
  • 页大小:1KB/页
  • 擦写寿命:1 万次
  • 数据保持:10 年(@25°C)
  • 工作电压:2.0V~3.6V

1.2 STM32F103 Flash 结构

容量分布:

型号 Flash 容量 页大小 页数 起始地址
STM32F103C8T6 64KB 1KB 64 页 0x08000000
STM32F103R8T6 64KB 1KB 64 页 0x08000000
STM32F103V8T6 64KB 1KB 64 页 0x08000000
STM32F103ZET6 512KB 2KB 256 页 0x08000000

地址映射(以 64KB 为例):

0x0800 0000 - 0x0800 FFFF : Flash 存储区(64KB)

第 0 页:0x0800 0000 - 0x0800 03FF(1KB)

第 1 页:0x0800 0400 - 0x0800 07FF(1KB)

第 2 页:0x0800 0800 - 0x0800 0BFF(1KB)

...下面就是我们将要使用存储数据的位置

第 62 页:0x0800 F800 - 0x0800 FBFF(1KB)← 参数存储

第 63 页:0x0800 FC00 - 0x0800 FFFF(1KB)← 参数存储

存储区划分建议:

0x0800 0000 - 0x0800 EFFF : 程序代码区

0x0800 F000 - 0x0800 F7FF : 数据记录区(8 页)

0x0800 F800 - 0x0800 FBFF : 参数存储区(2 页)

0x0800 FC00 - 0x0800 FFFF : 备份参数区(2 页)

1.3 Flash 操作原理

三种基本操作:

|----|----------------------|--------|---------|
| 操作 | 说明 | 时间 | 单位 |
| 读 | 直接读取,无需特殊操作 | 零等待 | 字节/半字/字 |
| 擦除 | 将数据全部置 1(0xFFFF) | 约 40ms | 页(1KB) |
| 写 | 将 1 改为 0(不能将 0 改为 1) | 约 40μs | 字(32 位) |

重要规则:

⚠️ 规则 1:写入前必须先擦除

擦除后:0xFFFF FFFF FFFF FFFF

写入后:0x1234 5678 ABCD EF01(只能将 1 改为 0)

⚠️ 规则 2:只能将 1 写为 0,不能将 0 写为 1

✅ 可以:1 → 0

❌ 不行:0 → 1(需要先擦除)

⚠️ 规则 3:擦写次数有限(约 1 万次)

解决方案:磨损均衡技术

操作时序:

擦除流程:

解锁 → 页擦除使能 → 设置地址 → 开始擦除 → 等待完成 → 验证

写入流程:

解锁 → 编程使能 → 写入数据 → 等待完成 → 验证 → 加锁

1.4 Flash 寿命与优化

擦写寿命:

操作类型 寿命次数 说明
擦除 1 万次/页 每页最多擦除 1 万次
写入 1 万次/页 写入前必须擦除
读取 无限次 读取不影响寿命

寿命优化技巧:

技巧 1:磨损均衡

使用多页循环写入,分散擦写次数

例如:4 页循环,寿命提升 4 倍

技巧 2:减少写入频率

只在数据变化时写入

使用 RAM 缓存,批量写入

技巧 3:使用备份区

主区损坏时使用备份区

提高数据可靠性

二、Flash 寄存器详解

2.1 Flash 寄存器总览

Flash 寄存器(基地址:0x40022000):

寄存器 名称 地址偏移 作用
FLASH_ACR 访问控制寄存器 0x00 等待周期、预取缓冲
FLASH_KEYR 密钥寄存器 0x04 解锁 Flash
FLASH_OPTKEYR 选项字节密钥 0x08 解锁选项字节
FLASH_SR 状态寄存器 0x0C 操作状态
FLASH_CR 控制寄存器 0x10 操作控制
FLASH_AR 地址寄存器 0x14 擦除地址
FLASH_OBR 选项字节寄存器 0x1C 选项字节状态
FLASH_WRPR 写保护寄存器 0x20 写保护设置

2.2 FLASH_KEYR 密钥寄存器

作用: 解锁 Flash 编程/擦除功能

解锁密钥:

FLASH->KEYR = 0x45670123; // 密钥 1

FLASH->KEYR = 0xCDEF89AB; // 密钥 2

解锁后:
  • FLASH_CR 寄存器可写
  • 可以执行擦除/写入操作
加锁:

FLASH->CR |= FLASH_CR_LOCK; // 加锁

2.3 FLASH_SR 状态寄存器

关键位:
名称 说明
0 BSY 忙标志(操作进行中)
2 PGERR 编程错误
4 WRPRTERR 写保护错误
5 EOP 操作结束

使用示例:

cpp 复制代码
// 等待操作完成
while (FLASH->SR & FLASH_SR_BSY);

// 检查错误
if (FLASH->SR & FLASH_SR_PGERR) {
    // 编程错误
}

// 清除 EOP 标志
FLASH->SR |= FLASH_SR_EOP;

2.4 FLASH_CR 控制寄存器

关键位:
名称 说明
0 PG 编程使能(写入)
1 PER 页擦除使能
2 MER 全片擦除使能
6 START 开始操作
7 LOCK 锁定
使用示例:
cpp 复制代码
// 页擦除
FLASH->CR |= FLASH_CR_PER;     // 页擦除使能
FLASH->AR = page_addr;         // 设置地址
FLASH->CR |= FLASH_CR_STRT;    // 开始擦除

// 写入
FLASH->CR |= FLASH_CR_PG;      // 编程使能
*(volatile uint16_t *)addr = data;

三、Flash 配置实战

3.1 完整配置流程

5 步操作 Flash:

  1. 解锁 Flash(写入密钥)

  2. 等待就绪(检查 BSY 标志)

  3. 执行操作(擦除/写入)

  4. 等待完成(等待 BSY=0)

  5. 加锁 Flash(可选,安全)

3.2 寄存器版本

完整代码:

cpp 复制代码
#include "stm32f10x.h"

// Flash 解锁
void FLASH_Unlock(void)
{
    FLASH->KEYR = 0x45670123;
    FLASH->KEYR = 0xCDEF89AB;
}

// Flash 加锁
void FLASH_Lock(void)
{
    FLASH->CR |= FLASH_CR_LOCK;
}

// 页擦除(1KB)
uint8_t FLASH_ErasePage(uint32_t page_addr)
{
    // 等待就绪
    while (FLASH->SR & FLASH_SR_BSY);
    
    // 页擦除使能
    FLASH->CR |= FLASH_CR_PER;
    
    // 设置地址
    FLASH->AR = page_addr;
    
    // 开始擦除
    FLASH->CR |= FLASH_CR_STRT;
    
    // 等待擦除完成
    while (FLASH->SR & FLASH_SR_BSY);
    
    // 关闭页擦除
    FLASH->CR &= ~FLASH_CR_PER;
    
    // 验证擦除结果
    if (*(volatileuint16_t *)page_addr != 0xFFFF) {
        return0;  // 擦除失败
    }
    
    return1;  // 擦除成功
}

// 写入半字(16 位)
uint8_t FLASH_WriteHalfWord(uint32_t addr, uint16_t data)
{
    // 等待就绪
    while (FLASH->SR & FLASH_SR_BSY);
    
    // 编程使能
    FLASH->CR |= FLASH_CR_PG;
    
    // 写入数据
    *(volatileuint16_t *)addr = data;
    
    // 等待写入完成
    while (FLASH->SR & FLASH_SR_BSY);
    
    // 关闭编程
    FLASH->CR &= ~FLASH_CR_PG;
    
    // 验证写入结果
    if (*(volatileuint16_t *)addr != data) {
        return0;  // 写入失败
    }
    
    return1;  // 写入成功
}

// 写入字(32 位)
uint8_t FLASH_WriteWord(uint32_t addr, uint32_t data)
{
    // 等待就绪
    while (FLASH->SR & FLASH_SR_BSY);
    
    // 编程使能
    FLASH->CR |= FLASH_CR_PG;
    
    // 写入低 16 位
    *(volatileuint16_t *)addr = data & 0xFFFF;
    while (FLASH->SR & FLASH_SR_BSY);
    
    // 写入高 16 位
    *(volatileuint16_t *)(addr + 2) = (data >> 16) & 0xFFFF;
    while (FLASH->SR & FLASH_SR_BSY);
    
    // 关闭编程
    FLASH->CR &= ~FLASH_CR_PG;
    
    // 验证写入结果
    if (*(volatileuint32_t *)addr != data) {
        return0;
    }
    
    return1;
}

// 读取字(32 位)
uint32_t FLASH_ReadWord(uint32_t addr)
{
    return *(volatileuint32_t *)addr;
}

// 批量写入
uint8_t FLASH_WriteBuffer(uint32_t addr, uint32_t *buffer, uint16_t len)
{
    FLASH_Unlock();
    
    for (uint16_t i = 0; i < len; i++) {
        if (!FLASH_WriteWord(addr + i * 4, buffer[i])) {
            FLASH_Lock();
            return0;
        }
    }
    
    FLASH_Lock();
    return1;
}

3.3 HAL 库版本

完整代码:

cpp 复制代码
#include "stm32f1xx_hal.h"

FLASH_EraseInitTypeDef EraseInitStruct;
uint32_t SectorError;

// Flash 解锁
void FLASH_Unlock(void)
{
    HAL_FLASH_Unlock();
}

// Flash 加锁
void FLASH_Lock(void)
{
    HAL_FLASH_Lock();
}

// 页擦除
uint8_t FLASH_ErasePage(uint32_t page_addr)
{
    EraseInitStruct.TypeErase = FLASH_TYPEERASE_PAGES;
    EraseInitStruct.PageAddress = page_addr;
    EraseInitStruct.NbPages = 1;
    
    if (HAL_FLASHEx_Erase(&EraseInitStruct, &SectorError) != HAL_OK) {
        return0;
    }
    
    return1;
}

// 写入字
uint8_t FLASH_WriteWord(uint32_t addr, uint32_t data)
{
    if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, addr, data) != HAL_OK) {
        return0;
    }
    
    // 验证
    if (*(volatileuint32_t *)addr != data) {
        return0;
    }
    
    return1;
}

// 读取字
uint32_t FLASH_ReadWord(uint32_t addr)
{
    return *(volatileuint32_t *)addr;
}

3.4 寄存器 vs HAL 库对比

特性 寄存器 HAL 库
代码量 较多 较少
执行效率 略低
可读性
移植性
错误处理 手动 自动
底层理解 深入 浅层

四、实战项目

4.1 项目一:参数存储

需求: 保存设备配置到 Flash,掉电不丢失

完整代码:

cpp 复制代码
#include "stm32f10x.h"
#include <stdio.h>
#include <string.h>

// 参数结构
typedefstruct {
    uint32_t magic;       // 魔数(验证数据有效性)
    uint8_t device_id;    // 设备 ID
    uint8_t baudrate;     // 波特率
    uint8_t mode;         // 工作模式
    uint8_t reserved;     // 保留
    uint32_t checksum;    // 校验和
} DeviceConfig;

// 使用最后 2 页 Flash(避免与程序冲突)
#define FLASH_PARAM_ADDR  0x0800F800  // 第 62 页

DeviceConfig default_config = {
    .magic = 0x12345678,
    .device_id = 1,
    .baudrate = 115200,
    .mode = 0,
    .reserved = 0,
    .checksum = 0
};

// Flash 基础函数(见上面)
void FLASH_Unlock(void);
void FLASH_Lock(void);
uint8_t FLASH_ErasePage(uint32_t page_addr);
uint8_t FLASH_WriteWord(uint32_t addr, uint32_t data);
uint32_t FLASH_ReadWord(uint32_t addr);

// 计算校验和
uint32_t CalcChecksum(DeviceConfig *cfg)
{
    uint8_t *p = (uint8_t *)cfg;
    uint32_t sum = 0;
    for (int i = 0; i < 12; i++) {
        sum += p[i];
    }
    return sum;
}

// 保存配置
uint8_t Config_Save(DeviceConfig *cfg)
{
    cfg->checksum = CalcChecksum(cfg);
    
    FLASH_Unlock();
    
    // 擦除页
    if (!FLASH_ErasePage(FLASH_PARAM_ADDR)) {
        FLASH_Lock();
        return0;
    }
    
    // 写入数据
    uint32_t *src = (uint32_t *)cfg;
    uint32_t addr = FLASH_PARAM_ADDR;
    
    for (int i = 0; i < sizeof(DeviceConfig) / 4; i++) {
        if (!FLASH_WriteWord(addr, src[i])) {
            FLASH_Lock();
            return0;
        }
        addr += 4;
    }
    
    FLASH_Lock();
    return1;
}

// 读取配置
uint8_t Config_Load(DeviceConfig *cfg)
{
    DeviceConfig *stored = (DeviceConfig *)FLASH_PARAM_ADDR;
    
    // 检查魔数
    if (stored->magic != 0x12345678) {
        *cfg = default_config;
        return0;  // 首次使用
    }
    
    // 复制数据
    *cfg = *stored;
    
    // 验证校验和
    if (cfg->checksum != CalcChecksum(cfg)) {
        *cfg = default_config;
        return0;  // 数据损坏
    }
    
    return1;  // 成功
}

int main(void)
{
    DeviceConfig config;
    
    if (Config_Load(&config)) {
        printf("配置加载成功\r\n");
        printf("Device ID: %d\r\n", config.device_id);
        printf("Baudrate: %d\r\n", config.baudrate);
    } else {
        printf("首次使用,保存默认配置\r\n");
        Config_Save(&default_config);
    }
    
    while (1) {
        // 运行
    }
}

应用: 设备配置、校准数据、用户设置

4.2 项目二:数据记录器

需求: 循环记录运行数据

完整代码:

cpp 复制代码
#include "stm32f10x.h"

#define LOG_ENTRY_SIZE  64
#define LOG_ENTRY_COUNT 16

typedefstruct {
    uint32_t timestamp;
    int16_t temperature;
    int16_t humidity;
    uint8_t status;
    uint8_t reserved[3];
} LogEntry;

#define FLASH_LOG_ADDR  0x0800F000  // 第 60 页

LogEntry logs[LOG_ENTRY_COUNT];
uint8_t current_index = 0;

// Flash 基础函数(见上面)
void FLASH_Unlock(void);
void FLASH_Lock(void);
uint8_t FLASH_ErasePage(uint32_t page_addr);
uint8_t FLASH_WriteWord(uint32_t addr, uint32_t data);

// 初始化
void Log_Init(void)
{
    // 从 Flash 加载当前索引
    uint8_t *p = (uint8_t *)FLASH_LOG_ADDR;
    if (p[0] == 0xAA) {
        current_index = p[1];
    } else {
        current_index = 0;
    }
}

// 保存日志
void Log_Save(LogEntry *entry)
{
    FLASH_Unlock();
    
    // 如果是第一条记录,擦除页
    if (current_index == 0) {
        FLASH_ErasePage(FLASH_LOG_ADDR);
        
        // 写入索引标志
        FLASH_WriteWord(FLASH_LOG_ADDR, 0xAA000000 | current_index);
    }
    
    // 计算写入地址(跳过索引区)
    uint32_t addr = FLASH_LOG_ADDR + 4 + (current_index * LOG_ENTRY_SIZE);
    
    // 写入数据
    uint32_t *src = (uint32_t *)entry;
    for (int i = 0; i < LOG_ENTRY_SIZE / 4; i++) {
        FLASH_WriteWord(addr, src[i]);
        addr += 4;
    }
    
    // 更新索引
    current_index++;
    if (current_index >= LOG_ENTRY_COUNT) {
        current_index = 0;
    }
    
    // 更新索引标志
    FLASH_ErasePage(FLASH_LOG_ADDR);
    FLASH_WriteWord(FLASH_LOG_ADDR, 0xAA000000 | current_index);
    
    FLASH_Lock();
}

// 加载所有日志
void Log_LoadAll(void)
{
    LogEntry *stored = (LogEntry *)(FLASH_LOG_ADDR + 4);
    
    for (int i = 0; i < LOG_ENTRY_COUNT; i++) {
        logs[i] = stored[i];
    }
}

// 模拟传感器读取
int16_t ReadTemperature(void)
{
    return250;  // 25.0°C
}

int16_t ReadHumidity(void)
{
    return600;  // 60.0%
}

uint8_t GetSystemStatus(void)
{
    return0;
}

int main(void)
{
    Log_Init();
    
    LogEntry entry;
    
    while (1) {
        entry.timestamp = millis();
        entry.temperature = ReadTemperature();
        entry.humidity = ReadHumidity();
        entry.status = GetSystemStatus();
        
        Log_Save(&entry);
        
        delay_ms(60000);  // 每分钟记录一次
    }
}

应用: 运行日志、故障记录、黑匣子

4.3 项目三:磨损均衡

问题: Flash 擦写次数有限(约 1 万次)

解决方案: 磨损均衡

完整代码:

cpp 复制代码
#include "stm32f10x.h"

#define WEAR_LEVEL_PAGES  4
#define WEAR_LEVEL_SIZE   (WEAR_LEVEL_PAGES * 1024)

typedefstruct {
    uint32_t sequence;      // 序列号(越大越新)
    uint32_t data[254];     // 数据区
} WearLevelBlock;

#define FLASH_WL_ADDR  0x0800E000  // 最后 4 页

uint32_t current_sequence = 0;

// Flash 基础函数(见上面)
void FLASH_Unlock(void);
void FLASH_Lock(void);
uint8_t FLASH_ErasePage(uint32_t page_addr);
uint8_t FLASH_WriteWord(uint32_t addr, uint32_t data);

// 找到最新的块
int FindLatestBlock(void)
{
    int latest = -1;
    uint32_t max_seq = 0;
    
    for (int i = 0; i < WEAR_LEVEL_PAGES; i++) {
        uint32_t addr = FLASH_WL_ADDR + (i * 1024);
        uint32_t seq = *(volatileuint32_t *)addr;
        
        if (seq != 0xFFFFFFFF && seq > max_seq) {
            max_seq = seq;
            latest = i;
        }
    }
    
    current_sequence = max_seq;
    return latest;
}

// 写入数据(磨损均衡)
void WearLevel_Write(uint32_t *data, int len)
{
    int latest = FindLatestBlock();
    int next = (latest + 1) % WEAR_LEVEL_PAGES;
    
    FLASH_Unlock();
    
    // 擦除下一页
    FLASH_ErasePage(FLASH_WL_ADDR + (next * 1024));
    
    // 写入序列号
    current_sequence++;
    FLASH_WriteWord(FLASH_WL_ADDR + (next * 1024), current_sequence);
    
    // 写入数据
    uint32_t addr = FLASH_WL_ADDR + (next * 1024) + 4;
    for (int i = 0; i < len; i++) {
        FLASH_WriteWord(addr, data[i]);
        addr += 4;
    }
    
    FLASH_Lock();
}

// 读取最新数据
void WearLevel_Read(uint32_t *data, int len)
{
    int latest = FindLatestBlock();
    
    if (latest >= 0) {
        uint32_t *src = (uint32_t *)(FLASH_WL_ADDR + (latest * 1024) + 4);
        for (int i = 0; i < len; i++) {
            data[i] = src[i];
        }
    }
}

int main(void)
{
    uint32_t data[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    uint32_t read_data[10];
    
    // 写入数据 1000 次
    for (int i = 0; i < 1000; i++) {
        data[0] = i;
        WearLevel_Write(data, 10);
    }
    
    // 读取数据
    WearLevel_Read(read_data, 10);
    
    while (1) {
        // 运行
    }
}

寿命提升: 4 页循环,寿命提升 4 倍(4 万次)

应用: 高频写入、数据记录、计数器

五、常见问题排查

5.1 Flash 写入失败

现象: 写入后读取数据不正确

检查清单:

1. 是否先擦除?

cpp 复制代码
// ✅ 写入前必须先擦除
FLASH_ErasePage(addr);
FLASH_WriteWord(addr, data);

2. Flash 是否解锁?

cpp 复制代码
// ✅ 解锁 Flash
FLASH_Unlock();
// 操作...
FLASH_Lock();

3. 地址是否对齐?

cpp 复制代码
// ✅ 字写入必须 4 字节对齐
// ✅ 半字写入必须 2 字节对齐

5.2 Flash 擦除失败

现象: 擦除后数据不是 0xFFFF

检查清单:

1. 地址是否在 Flash 范围内?

cpp 复制代码
// ✅ 检查地址范围
// STM32F103C8T6: 0x08000000 - 0x0800FFFF

2. 是否等待完成?

cpp 复制代码
// ✅ 等待擦除完成
while (FLASH->SR & FLASH_SR_BSY);

3. 写保护是否禁用?

cpp 复制代码
// ✅ 检查 FLASH_WRPR 寄存器
// 确保页没有被写保护

5.3 Flash 寿命耗尽

现象: 写入后数据很快损坏

检查清单:

1. 是否使用磨损均衡?

cpp 复制代码
// ✅ 高频写入使用磨损均衡
// 4 页循环,寿命提升 4 倍

2. 是否减少写入频率?

cpp 复制代码
// ✅ 只在数据变化时写入
// ✅ 使用 RAM 缓存,批量写入

3. 是否使用备份区?

cpp 复制代码
// ✅ 主区损坏时使用备份区
// 提高数据可靠性

总结

本文深入分析了 STM32 Flash 的完整实现:

核心要点:

  1. Flash 原理:非易失性存储,掉电不丢失
  2. 操作规则:写入前必须先擦除,只能 1→0
  3. Flash 配置 5 步:解锁→等待→操作→等待→加锁
  4. 寿命优化:磨损均衡、减少写入、备份区
  5. 实战应用:参数存储、数据记录、磨损均衡

技术难点:

  • Flash 解锁和加锁机制
  • 页擦除和字写入时序
  • 磨损均衡算法实现
  • 数据校验和验证

学习建议:

  • 先理解 Flash 操作原理
  • 使用最后几页 Flash(避免覆盖程序)
  • 重要数据使用校验和验证
  • 高频写入必须使用磨损均衡
相关推荐
归零鸟5 小时前
WD Elements移动硬盘能识别出盘但不能出盘的修复记录
stm32·单片机·嵌入式硬件
追兮兮6 小时前
MCUQuickStart v1.1.0发布,一键生成Keil工程+RTOS模板
stm32·单片机·嵌入式硬件·freertos·gd32·keil5
国科安芯7 小时前
ASP7A84AS与主流架构兼容替代及系统级电源完整性解决方案的深度研究
单片机·嵌入式硬件·架构
kaikaile19957 小时前
STC8单片机实现简单花样DMX512控制器
单片机·嵌入式硬件
rit84324997 小时前
STM32移植NES模拟器指南
stm32·单片机·嵌入式硬件
都在酒里7 小时前
STM32 I2C通信协议详解——标准库函数实现(通讯协议总结一)
stm32·嵌入式硬件·i2c
fengfuyao9857 小时前
STM32 HAL库实现串口DMA接收不定长数据
stm32·单片机·嵌入式硬件
yuan199977 小时前
STM32直流无刷电机六拍方波控制器程序
stm32·单片机·嵌入式硬件
番茄灭世神8 小时前
PN学堂GD32教程第21篇——WiFiIOT
c语言·stm32·单片机·嵌入式·gd32