很多情况下,在STM32中写入一些数据,在某些不可控因素下其数据无法保存。因此,解决此问题就要用到FLASH.
什么是内部 Flash?
Flash 是一种非易失性存储器,STM32 的程序和常量数据就存在 Flash 中。它的关键特点是:
特性 | 说明 |
---|---|
地址映射 | Flash 地址从 0x08000000 开始 |
掉电不丢 | 电源断了之后数据还在 |
读操作 | 和 RAM 一样,可以直接读 |
写操作 | 必须擦除后写入(写之前必须全是 1) |
写入单位 | 至少为半字(16位) |
擦除单位 | 以页为单位(STM32F103 是 1KB) |
Flash 是如何读数据的?
STM32 的 Flash 是memory-mapped(内存映射)的,即:
Flash 被挂在总线上,和 SRAM 一样的访问方式,读取数据只需要访问地址。
底层硬件会自动完成数据译码、电荷检测、字线控制等,所以我们可以像访问 RAM 一样访问 Flash。
STM32 读取和写入内部 Flash 的本质区
操作 | 是否需要解锁 | 是否需要擦除 | 最小单位 | 是否会改变 Flash 数据 |
---|---|---|---|---|
读取 | 否 | 否 | 1字节/2字节/4字节 | 否 (只读) |
写入 | 是 | 是(必须) | 半字(16位) | 是 (只能1→0) |
为什么不需要解锁就能读取?
因为 读取 Flash 是只读操作,不会修改 Flash 结构,不存在写保护一说,也不需要擦除。Flash 读取只需要:
cpp
uint16_t data = *(volatile uint16_t *)0x0800FC00;
STM32 会自动从 Flash 控制器中读取该地址的数据。
读取代码详解
cpp
uint32_t MyFLASH_ReadWord(uint32_t Address)
{
return *((__IO uint32_t *)(Address));
}
uint16_t MyFLASH_ReadHalfWord(uint32_t Address)
{
return *((__IO uint16_t *)(Address));
}
uint8_t MyFLASH_ReadByte(uint32_t Address)
{
return *((__IO uint8_t *)(Address));
}
通用结构分析
*((__IO 类型 *)(Address)) 是什么意思?
组成部分 | 含义 |
---|---|
(类型 *) |
把地址强制转换成指针类型 |
__IO |
volatile ,防止编译器优化(告诉编译器:值可能随时变化) |
*(指针) |
取出地址中的值(解引用) |
举个例子:
cpp
*((__IO uint16_t *)0x0800FC00) → 读取地址0x0800FC00的2字节数据
注意事项
不要读取未对齐地址(比如读取 uint16_t 不能从奇数地址读取)
不要在 Flash 正在写入时读取 Flash
写入 Flash 的完整流程
写入 Flash 是一个有顺序、受保护的敏感操作,必须严格遵守 STM32 的流程!
要求 | 原因 |
---|---|
写入前必须解锁 | 防止误写程序或数据 |
写入前必须擦除 | 因为 Flash 只能从 1 → 0 ,不能反向 |
写入地址必须对齐 | 每次只能写一个 "半字" |
写入时 Flash 忙,不能再操作 Flash | 必须等待写完成(BSY = 0) |
cpp
┌────────────┐
│ Flash_Unlock│ ← 解锁 Flash 写保护
└─────┬──────┘
↓
┌──────────────┐
│Flash_ErasePage│ ← 擦除所在页(将所有位置1)
└─────┬────────┘
↓
┌──────────────────────┐
│Flash_ProgramHalfWord │ ← 写入一个16位值(地址必须对齐)
└────────────┬─────────┘
↓
┌────────────┐
│ Flash_Lock │ ← 锁回 Flash,防止误写
└────────────┘
cpp
void MyFLASH_EraseAllPages(void)
{
FLASH_Unlock();
FLASH_EraseAllPages();
FLASH_Lock();
}
void MyFLASH_ErasePage(uint32_t PageAddress)
{
FLASH_Unlock();
FLASH_ErasePage(PageAddress);
FLASH_Lock();
}
void MyFLASH_ProgramWord(uint32_t Address, uint32_t Data)
{
FLASH_Unlock();
MyFLASH_ErasePage(Address);
FLASH_ProgramWord(Address, Data);
FLASH_Lock();
}
void MyFLASH_ProgramHalfWord(uint32_t Address, uint16_t Data)
{
FLASH_Unlock();
MyFLASH_ErasePage(Address);
FLASH_ProgramHalfWord(Address, Data);
FLASH_Lock();
}
把 STM32 的内部 Flash 最后一页 0x0800FC00
用作数据存储区
函数名 | 功能说明 |
---|---|
Store_Init() |
开机时检查是否初始化过,若无则初始化数据,再加载到 RAM 数组 |
Store_Save() |
把当前 RAM 中的数据整体写入 Flash(覆盖) |
Store_Clear() |
清除 RAM 中的数据,再写入 Flash(相当于清空) |
Store_Init() 函数解释
cpp
#define STORE_START_ADDRESS 0x0800FC00
#define STORE_COUNT 512
uint16_t Store_Data[STORE_COUNT];
void Store_Init(void)
{
if (MyFLASH_ReadHalfWord(STORE_START_ADDRESS) != 0xA5A5)
{
MyFLASH_ErasePage(STORE_START_ADDRESS);
MyFLASH_ProgramHalfWord(STORE_START_ADDRESS, 0xA5A5);
for (uint16_t i = 1; i < STORE_COUNT; i ++)
{
MyFLASH_ProgramHalfWord(STORE_START_ADDRESS + i * 2, 0x0000);
}
}
for (uint16_t i = 0; i < STORE_COUNT; i ++)
{
Store_Data[i] = MyFLASH_ReadHalfWord(STORE_START_ADDRESS + i * 2);
}
}

空间计算(为什么是 512?)
每个 uint16_t 是 2 字节(16 位),所以:
cpp
512 × 2 字节 = 1024 字节 = 1KB
正好使用 Flash 的最后一页(1KB 大小)来存储 512 个 uint16_t。
这是一种 对齐页大小的设计,防止写入跨页、浪费空间或出错。
步骤 | 说明 |
---|---|
读取魔数 | 检查 Flash 中是否已经初始化过(首地址是否为 0xA5A5) |
若未初始化 | 则擦除该页,写入魔数 + 写入 511 个 0x0000 |
读取数据 | 把 Flash 中的所有数据读入 RAM 数组 Store_Data[] |
为什么用魔数 0xA5A5?
Flash 出厂是全 0xFFFF
,你写入 0xA5A5
后,可以作为初始化标志,下次重启时避免重复擦除。
Store_Save() 函数解释(将 RAM 数据写回 Flash)
cpp
void Store_Save(void)
{
MyFLASH_ErasePage(STORE_START_ADDRESS);
for (uint16_t i = 0; i < STORE_COUNT; i ++)
{
MyFLASH_ProgramHalfWord(STORE_START_ADDRESS + i * 2, Store_Data[i]);
}
}
步骤 | 说明 |
---|---|
先擦除页 | Flash 写之前必须擦除 |
顺序写入 | 把 Store_Data[] 数组中 512 个半字写入 Flash |
为什么每个数据偏移 i * 2?
因为你每个数据是 uint16_t,占用 2 字节,要避免覆盖前一个数据。
Store_Clear() 函数解释(清除并保存)
cpp
void Store_Clear(void)
{
for (uint16_t i = 1; i < STORE_COUNT; i ++)
{
Store_Data[i] = 0x0000;
}
Store_Save();
}
功能分析:
从第 1 个数据开始清 0(保留 Store_Data[0] 为魔数)
调用 Store_Save() 整页重写
main函数
cpp
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "Store.h"
#include "Key.h"
uint8_t KeyNum;
int main(void)
{
OLED_Init();
Key_Init();
Store_Init();
OLED_ShowString(1, 1, "Flag:");
OLED_ShowString(2, 1, "Data:");
while (1)
{
KeyNum = Key_GetNum();
if (KeyNum == 1)
{
Store_Data[1] ++;
Store_Data[2] += 2;
Store_Data[3] += 3;
Store_Data[4] += 4;
Store_Save();
}
if (KeyNum == 2)
{
Store_Clear();
}
OLED_ShowHexNum(1, 6, Store_Data[0], 4);
OLED_ShowHexNum(3, 1, Store_Data[1], 4);
OLED_ShowHexNum(3, 6, Store_Data[2], 4);
OLED_ShowHexNum(4, 1, Store_Data[3], 4);
OLED_ShowHexNum(4, 6, Store_Data[4], 4);
}
}
FLASH读取芯片ID
cpp
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
int main(void)
{
OLED_Init();
OLED_ShowString(1, 1, "F_SIZE:");
OLED_ShowHexNum(1, 8, *((__IO uint16_t *)(0x1FFFF7E0)), 4);
OLED_ShowString(2, 1, "U_ID:");
OLED_ShowHexNum(2, 6, *((__IO uint16_t *)(0x1FFFF7E8)), 4);
OLED_ShowHexNum(2, 11, *((__IO uint16_t *)(0x1FFFF7E8 + 0x02)), 4);
OLED_ShowHexNum(3, 1, *((__IO uint32_t *)(0x1FFFF7E8 + 0x04)), 8);
OLED_ShowHexNum(4, 1, *((__IO uint32_t *)(0x1FFFF7E8 + 0x08)), 8);
while (1)
{
}
}
名称 | 地址 | 说明 |
---|---|---|
Flash 大小 | 0x1FFFF7E0 |
单位:KB(例如 64 → 64KB Flash) |
UID[0] | 0x1FFFF7E8 |
UID 第 0~15 位(低 16 位) |
UID[1] | 0x1FFFF7EA |
UID 第 16~31 位(高 16 位) |
UID[2] | 0x1FFFF7EC |
UID 第 32~63 位(32 位) |
UID[3] | 0x1FFFF7F0 |
UID 第 64~95 位(32 位) |
应用场景
防伪 / 唯一性绑定:
使用 UID 作为设备编号上传服务器或产品注册
Flash 容量检测:
某些型号可能 Flash 容量不一致,可动态检测
License 系统:
生成授权码绑定到 UID,防止盗版
设备信息显示:
工程生产中可直接通过 OLED 显示设备 ID 进行追溯