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(避免覆盖程序)
  • 重要数据使用校验和验证
  • 高频写入必须使用磨损均衡
相关推荐
ytttr8732 小时前
四线制步进电机驱动器设计详解
单片机·嵌入式硬件
独小乐2 小时前
013.定时器之系统Tick实现|千篇笔记实现嵌入式全栈/裸机篇
linux·笔记·单片机·嵌入式硬件·arm
三佛科技-134163842122 小时前
无线遥控器开关方案开发 ,无线遥控器开关MCU控制方案设计-基于国产单片机
单片机·嵌入式硬件·物联网·智能家居·pcb工艺
北城笑笑2 小时前
FPGA 与 市场主流芯片分类详解:SoC/CPU/GPU/DPU 等芯片核心特性与工程应用
前端·单片机·fpga开发·fpga
Heartache boy2 小时前
DWT基础应用与获取程序运行时间Debug练习(上)
笔记·stm32·单片机
我要成为嵌入式大佬3 小时前
正点原子MP157--问题详解--五(beep编写报错端口繁忙)
stm32·嵌入式硬件·学习
振浩微433射频芯片3 小时前
433MHz在智能家居中的应用大全(一):智能窗帘篇——为什么稳定比花哨更重要?
网络·单片机·嵌入式硬件·物联网·智能家居
咸鱼嵌入式3 小时前
【AutoSAR】详解CANIF模块
单片机·mcu·车载系统·autosar
小小的代码里面挖呀挖呀挖3 小时前
恒玄BES蓝牙耳机开发--IIC接口应用
笔记·单片机·物联网·学习·iot