type: raw source: bilibili lecture: "15-1 FLASH闪存, 15-2 读写内部FLASH & 读取芯片ID" lecture_material: "15-1 FLASH闪存, 15-2 读写内部FLASH&读取芯片ID" project_code: "15-1 读写内部FLASH, 15-2 读取芯片ID"
上图:STM32 Flash 闪存模块组织(来源:STM32入门教程 PPT 第196页)
上图:STM32 Flash 内部结构(主存储器、系统存储器、选项字节)(来源:STM32入门教程 PPT 第197页)
STM32F103C8T6 的内部 Flash 组织结构(以 64KB 为例):
0x08000000 ┌──────────────────────┐
│ 第 0 页(1KB) │ 启动代码区的起始
│ 第 1 页(1KB) │
│ ... │
│ 第 62 页(1KB) │
│ 第 63 页(1KB) │
0x0800FFFF └──────────────────────┘ ← 64KB 主存储区
0x1FFFF000 ┌──────────────────────┐
│ 系统存储区(2KB) │ ← 出厂 Bootloader
0x1FFFF7FF └──────────────────────┘
0x1FFFF800 ┌──────────────────────┐
│ 选项字节(16B) │ ← 读保护、看门狗等配置
0x1FFFF80F └──────────────────────┘
教师讲解:Flash 按页(Page)管理,每页 1KB(大容量型号每页 2KB)。写 Flash 时必须以页为单位擦除,不能单独擦除某个字节。
| 区域 | 地址范围 | 大小 | 用途 |
|---|---|---|---|
| 主存储器 | 0x08000000 ~ 结束 | 64KB~1MB | 存储程序代码和数据 |
| 系统存储区 | 0x1FFFF000 ~ 0x1FFFF7FF | 2KB | ISP Bootloader |
| 选项字节 | 0x1FFFF800 ~ 0x1FFFF80F | 16B | 读保护/看门狗配置 |
不同型号 Flash 容量对应关系:
| 型号 | 容量 | 页数 | 每页大小 |
|---|---|---|---|
| STM32F103C4 | 16KB | 16 | 1KB |
| STM32F103C8 | 64KB | 64 | 1KB |
| STM32F103CB | 128KB | 128 | 1KB |
| STM32F103RC | 256KB | 256 | 1KB |
| STM32F103VE | 512KB | 512 | 2KB |
| STM32F103ZG | 1MB | 512 | 2KB |
Flash 读操作不需要解锁,直接用地址指针读取:
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 防止编译器优化掉指针访问教师强调:读 Flash 就像读普通内存变量一样简单——因为 Cortex-M3 直接从 Flash 总线读取指令和数据,ARM 已经把映射做好了。
Flash 控制器默认加锁,防止误写:
FLASH_Unlock(); // 写 KEY1=0x45670123 → KEY2=0xCDEF89AB 到 FLASH_KEYR
若还需写入选项字节:
FLASH_Unlock(); // 主解锁
FLASH_OB_Unlock(); // 选项字节解锁(写入 0x45670123, 0xCDEF89AB 到 FLASH_OPTKEYR)
教师解释:解锁序列就像输入"密码"——必须按照 ST 规定的密钥顺序写入。如果写错了顺序,Flash 控制器会视作非法操作并锁定。这是 Flash 写保护的基本机制。
写前必须擦除——Flash 只能将 1 写为 0,不能将 0 写回 1:
// 页擦除
FLASH_Unlock();
FLASH_ErasePage(PageAddress); // 一次擦除整页(1KB 或 2KB)
FLASH_Lock();
// 全擦除
FLASH_Unlock();
FLASH_EraseAllPages(); // 擦除所有用户区(包括程序自身!)
FLASH_Lock();
FLASH_EraseInitTypeDef EraseInitStruct = {0};
uint32_t PageError = 0;
HAL_FLASH_Unlock();
EraseInitStruct.TypeErase = FLASH_TYPEERASE_PAGES; // 页擦除
EraseInitStruct.PageAddress = 0x0800FC00; // 起始地址
EraseInitStruct.NbPages = 1; // 擦除 1 页
if (HAL_FLASHEx_Erase(&EraseInitStruct, &PageError) != HAL_OK)
{
// 擦除失败,通过 HAL_FLASH_GetError() 获取详细错误
uint32_t err = HAL_FLASH_GetError();
}
// 全擦除
EraseInitStruct.TypeErase = FLASH_TYPEERASE_MASSERASE;
HAL_FLASHEx_Erase(&EraseInitStruct, &PageError);
HAL_FLASH_Lock();
擦除后,Flash 全页为 0xFF,然后写入数据:
// 编程 32 位字
FLASH_Unlock();
FLASH_ProgramWord(Address, Data);
FLASH_Lock();
// 编程 16 位半字
FLASH_Unlock();
FLASH_ProgramHalfWord(Address, Data);
FLASH_Lock();
HAL_FLASH_Unlock();
// 按字节写入
HAL_FLASH_Program(FLASH_TYPEPROGRAM_BYTE, Address, Data8);
// 按半字写入
HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, Address, Data16);
// 按字写入
HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, Address, Data32);
HAL_FLASH_Lock();
⚠ 写入前必须保证该地址所在页已擦除(全
0xFF),否则写入不可靠。
void MyFLASH_WritePage(uint32_t PageAddr, uint16_t *data, uint16_t count)
{
FLASH_Unlock(); // 1. 解锁
FLASH_ErasePage(PageAddr); // 2. 擦除整页
for (uint16_t i = 0; i < count; i++)
{
FLASH_ProgramHalfWord(PageAddr + i * 2, data[i]); // 3. 逐半字写入
}
FLASH_Lock(); // 4. 加锁
}
FLASH_Status status = FLASH_WaitForLastOperation(Timeout); // 等待 BSY 位清零
// 可检查:FLASH_COMP_PG 编程错误 / FLASH_COMP_WRPRT 写保护错误
// MyFLASH.c — 封装标准库 Flash 操作
void MyFLASH_ErasePage(uint32_t PageAddress)
{
FLASH_Unlock();
FLASH_ErasePage(PageAddress);
FLASH_Lock();
}
void MyFLASH_ProgramHalfWord(uint32_t Address, uint16_t Data)
{
FLASH_Unlock();
FLASH_ProgramHalfWord(Address, Data);
FLASH_Lock();
}
// Store.c — 存储中间件
#define STORE_START_ADDRESS 0x0800FC00 // 最后 1KB 页(64KB 的最后一页)
#define STORE_COUNT 512 // 512 个 16 位数据
uint16_t Store_Data[STORE_COUNT]; // SRAM 缓冲数组
void Store_Init(void)
{
// 首次使用:擦除页 → 写标志位 0xA5A5 → 清空其余位置
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);
}
// 上电加载:Flash → SRAM
for (uint16_t i = 0; i < STORE_COUNT; i++)
Store_Data[i] = MyFLASH_ReadHalfWord(STORE_START_ADDRESS + i * 2);
}
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]);
}
void Store_Clear(void)
{
for (uint16_t i = 1; i < STORE_COUNT; i++)
Store_Data[i] = 0x0000;
Store_Save();
}
// Store.c — HAL 版,使用 HAL_FLASH 函数
#define STORE_START_ADDRESS 0x0800FC00
#define STORE_COUNT 512
uint16_t Store_Data[STORE_COUNT];
static void Store_ErasePage_HAL(void)
{
FLASH_EraseInitTypeDef erase = {0};
uint32_t PageError = 0;
erase.TypeErase = FLASH_TYPEERASE_PAGES;
erase.PageAddress = STORE_START_ADDRESS;
erase.NbPages = 1;
HAL_FLASHEx_Erase(&erase, &PageError);
}
void Store_Init_HAL(void)
{
HAL_FLASH_Unlock();
if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, STORE_START_ADDRESS, 0xA5A5) != HAL_OK)
{
// 第一次初始化:擦除后写标志
Store_ErasePage_HAL();
HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, STORE_START_ADDRESS, 0xA5A5);
for (uint16_t i = 1; i < STORE_COUNT; i++)
HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, STORE_START_ADDRESS + i * 2, 0x0000);
}
HAL_FLASH_Lock();
// 上电加载
for (uint16_t i = 0; i < STORE_COUNT; i++)
Store_Data[i] = *((__IO uint16_t *)(STORE_START_ADDRESS + i * 2));
}
void Store_Save_HAL(void)
{
HAL_FLASH_Unlock();
Store_ErasePage_HAL();
for (uint16_t i = 0; i < STORE_COUNT; i++)
HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, STORE_START_ADDRESS + i * 2, Store_Data[i]);
HAL_FLASH_Lock();
}
int main(void)
{
OLED_Init();
Key_Init();
Store_Init(); // 上电加载 Flash → SRAM
OLED_ShowString(1, 1, "Flag:");
OLED_ShowString(2, 1, "Data:");
while (1)
{
KeyNum = Key_GetNum();
if (KeyNum == 1) // 按键1:修改数据 → 保存
{
Store_Data[1] ++;
Store_Data[2] += 2;
Store_Data[3] += 3;
Store_Data[4] += 4;
Store_Save();
}
if (KeyNum == 2) // 按键2:全部清0
{
Store_Clear();
}
OLED_ShowHexNum(1, 6, Store_Data[0], 4); // 标志位
OLED_ShowHexNum(3, 1, Store_Data[1], 4); // 数据
// ...
}
}
教师总结设计要点:
- 选择最后 1KB 页作为存储区——不占用 Flash 前面的代码空间。
- 用 0xA5A5 标志位 判断是否首次上电——如果首次则格式化。
- 使用 SRAM 缓冲 + Flash 备份 策略——运行中只操作 SRAM,保存时才写 Flash(Flash 写入很慢且有擦写寿命限制)。
| 问题 | 解决方案 |
|---|---|
| 存储地址放在哪里? | 最后 1 页(0x0800FC00),不干扰代码区 |
| 如何判断首次使用? | 标志位 0xA5A5 存到第一个半字 |
| 写入前需要做什么? | 先页擦除 → 再逐一写入 |
| 如何提高写寿命? | SRAM 做缓冲,仅在有修改时才触发保存 |
| Flash 擦写寿命(~10k 次) | 每个保存操作成本 = 1 擦除 + 512 次写入 |
STM32 在系统存储区中有两个关键只读寄存器:
0x1FFFF7E0 都一个 16 位只读寄存器以 KB 为单位:C8 = 0x0040 = 64KB
uint16_t flash_size = *((__IO uint16_t *)(0x1FFFF7E0));
// C8T6 → 64 → 0x0040
0x1FFFF7E8,96 位(12 字节)int main(void)
{
OLED_Init();
// 显示闪存容量
OLED_ShowString(1, 1, "F_SIZE:");
OLED_ShowHexNum(1, 8, *((__IO uint16_t *)(0x1FFFF7E0)), 4);
// 显示 96-bit UID
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) {}
}
教师说明:唯一 ID 的典型应用场景——软件加密:写固件时绑定 UID,即使固件被读出也无法在其他芯片上运行。或作为网络节点的 MAC 地址。
// 软件加密验证
uint8_t CheckUID(void)
{
uint8_t uid[12];
for (int i = 0; i < 12; i++)
uid[i] = *((__IO uint8_t *)(0x1FFFF7E8 + i));
for (int i = 0; i < 12; i++)
if (uid[i] != EXPECTED_UID[i])
return 0; // 芯片不匹配,拒绝运行
return 1;
}
// 生成唯一 MAC 地址
void GenerateMAC(uint8_t *mac)
{
uint32_t uid0 = *((__IO uint32_t *)(0x1FFFF7E8));
uint32_t uid1 = *((__IO uint32_t *)(0x1FFFF7EC));
uint32_t uid2 = *((__IO uint32_t *)(0x1FFFF7F0));
mac[0] = 0x02; // 本地管理
mac[1] = 0x00;
mac[2] = (uid0 & 0xFF0000) >> 16;
mac[3] = (uid0 & 0x0000FF00) >> 8;
mac[4] = uid1 & 0xFF;
mac[5] = uid2 & 0xFF;
}
尚硅谷课程中 FLASH 部分使用外部 SPI Flash(W25Q32)而非内部 FLASH 进行多字节写入,因为内部 FLASH 必须以页擦除再写入,不适合频繁小量写入。
内部 FLASH 解锁密钥寄存器参考:
| 寄存器 | 密钥值 |
|---|---|
FLASH->KEYR |
0x45670123 → 0xCDEF89AB(依次写入) |
FLASH->OPTKEYR |
0x45670123 → 0xCDEF89AB(选项字节解锁) |
FLASH->KEYR = 0x45670123; // 密钥1
FLASH->KEYR = 0xCDEF89AB; // 密钥2 → 解锁
完整操作序列参考标准库或 HAL 库封装。
Flash 操作流程总结:
读 ──→ 直接指针访问,无需解锁
写 ──→ 解锁 → 擦除页 → 编程写入 → 加锁
芯片 ID ──→ 只读系统地址 0x1FFFF7E0 / 0x1FFFF7E8
| 场景 | 使用策略 |
|---|---|
| 系统参数掉电保存 | 取最后一页 Flash,上电加载到 SRAM,修改后保存 |
| 固件加密 | 读取 96-bit UID 做加密校验 |
| IAP 升级 | 使用扩展存储区(非当前运行页)存储新固件 |
| 数据日志 | 循环使用多页,磨损均衡(避免单页擦写死) |
FLASH_ACR 配置。