一、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:
-
解锁 Flash(写入密钥)
-
等待就绪(检查 BSY 标志)
-
执行操作(擦除/写入)
-
等待完成(等待 BSY=0)
-
加锁 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 的完整实现:
核心要点:
- Flash 原理:非易失性存储,掉电不丢失
- 操作规则:写入前必须先擦除,只能 1→0
- Flash 配置 5 步:解锁→等待→操作→等待→加锁
- 寿命优化:磨损均衡、减少写入、备份区
- 实战应用:参数存储、数据记录、磨损均衡
技术难点:
- Flash 解锁和加锁机制
- 页擦除和字写入时序
- 磨损均衡算法实现
- 数据校验和验证
学习建议:
- 先理解 Flash 操作原理
- 使用最后几页 Flash(避免覆盖程序)
- 重要数据使用校验和验证
- 高频写入必须使用磨损均衡