目录
[1 FLASH 闪存简介](#1 FLASH 闪存简介)
[2 STM32 FLASH 存储结构](#2 STM32 FLASH 存储结构)
[2.1 STM32 容量分类](#2.1 STM32 容量分类)
[2.2 FLASH 的构成与分类](#2.2 FLASH 的构成与分类)
[2.2.1 主存储器](#2.2.1 主存储器)
[2.2.2 启动程序代码](#2.2.2 启动程序代码)
[2.2.3 用户选择字节](#2.2.3 用户选择字节)
[2.3 闪存存储器接口(FPEC)第二版本](#2.3 闪存存储器接口(FPEC)第二版本)
[2.3.1 FPEC 映射](#2.3.1 FPEC 映射)
[2.3.2 FPEC主要寄存器](#2.3.2 FPEC主要寄存器)
[2.3.2 FLASH锁定机制](#2.3.2 FLASH锁定机制)
[3 FLASH 的基本操作](#3 FLASH 的基本操作)
[3.1 读取操作](#3.1 读取操作)
[3.2 解锁与加锁](#3.2 解锁与加锁)
[3.3 页擦除](#3.3 页擦除)
[3.4 全擦除](#3.4 全擦除)
[3.5 编程(写入)](#3.5 编程(写入))
[3.6 选项字节操作](#3.6 选项字节操作)
[3.6.1 选项字节擦除](#3.6.1 选项字节擦除)
[3.6.2 选项字节编程](#3.6.2 选项字节编程)
[4 STM32 ST-LINK Utility](#4 STM32 ST-LINK Utility)
[4.1 工具概述](#4.1 工具概述)
[4.2 查看 FLASH 内容](#4.2 查看 FLASH 内容)
[4.3 直接修改 FLASH 数据](#4.3 直接修改 FLASH 数据)
[4.4 配置选项字节](#4.4 配置选项字节)
[4.5 芯片锁死的恢复方法](#4.5 芯片锁死的恢复方法)
[5 本章节实验](#5 本章节实验)
[5.1 读写内部 FLASH](#5.1 读写内部 FLASH)
[5.1.1 实验目标](#5.1.1 实验目标)
[5.1.2 硬件连接](#5.1.2 硬件连接)
[5.1.3 软件设计](#5.1.3 软件设计)
[5.1.3.1 底层 MyFLASH 模块](#5.1.3.1 底层 MyFLASH 模块)
[5.1.3.2 上层 Store 模块](#5.1.3.2 上层 Store 模块)
[5.1.3.3 主程序逻辑](#5.1.3.3 主程序逻辑)
[5.1.4 实验现象](#5.1.4 实验现象)
[5.2 读取芯片 ID](#5.2 读取芯片 ID)
[5.2.1 实验目标](#5.2.1 实验目标)
[5.2.2 硬件设计](#5.2.2 硬件设计)
[5.2.3 软件设计](#5.2.3 软件设计)
[5.2.3.1 闪存容量寄存器(F_SIZE)的读取](#5.2.3.1 闪存容量寄存器(F_SIZE)的读取)
[5.2.3.2 产品唯一身份标识寄存器(UID)的读取](#5.2.3.2 产品唯一身份标识寄存器(UID)的读取)
[5.2.3.3 主程序逻辑](#5.2.3.3 主程序逻辑)
[5.2.4 实验现象](#5.2.4 实验现象)
[6 注意事项与避坑指南](#6 注意事项与避坑指南)
[6.1 程序空间与用户数据空间的冲突](#6.1 程序空间与用户数据空间的冲突)
[6.2 查看程序占用空间](#6.2 查看程序占用空间)
[6.3 限制程序存储范围](#6.3 限制程序存储范围)
[6.4 下载配置中的擦除方式选择](#6.4 下载配置中的擦除方式选择)
[6.5 FLASH 操作期间 CPU 暂停的问题](#6.5 FLASH 操作期间 CPU 暂停的问题)
[6.6 其他操作注意事项](#6.6 其他操作注意事项)
[6.7 选项字节配置的风险](#6.7 选项字节配置的风险)
[7 总结](#7 总结)
1 FLASH 闪存简介
**FLASH(Flash Memory)**是一种非易失性存储器,其特点是在芯片断电后仍能够保持数据内容不丢失,重新上电后可直接读取。因此,FLASH成为嵌入式系统中存储程序代码和持久化配置数据的核心载体。
与之相对,**SRAM(Static Random Access Memory)**属于易失性存储器。SRAM具有访问速度快、读写效率高的特点,适合存放程序运行过程中的变量、缓冲区以及临时数据,但其内容在断电后会立即丢失。
因此,在STM32系统中,FLASH主要负责长期数据存储,而SRAM则承担程序运行期间的数据交换与处理任务,两者共同构成完整的存储体系,如图1-1所示。
图1-1 STM32系统构架图
对于典型的 STM32F103C8T6 芯片,其程序存储器总容量为 64KB。在实际项目中,一个规模适中的应用程序通常只占用几 KB 到十几 KB,这意味着程序存储器中存在大片剩余空间。如果能够合理利用这些剩余空间来存储用户自定义的配置参数------例如设备校准数据、运行状态记录、通信地址表等------则无需在电路板上额外增加外部 EEPROM 或 FLASH 芯片,既简化了硬件设计,也降低了物料成本。
这正是本文的核心学习目标:掌握 STM32 内部 FLASH 的读写操作方法,利用程序存储器的剩余空间保存掉电不丢失的用户数据。
为此,本文将围绕STM32内部FLASH展开介绍,理解 FLASH 的存储组织结构(页的划分与地址分布)、闪存存储器接口(FPEC)的工作机制、FLASH 操作的基本流程(解锁、擦除、编程、加锁)以及相关的库函数调用方式。对应本章节实验一《读写内部 FLASH》。
此外,还将介绍利用指针直接访问存储器地址的方法,并演示如何读取芯片出厂时固化的唯一身份标识(UID)和闪存容量信息。对应本章节实验二《读取芯片 ID》。
2 STM32 FLASH 存储结构
2.1 STM32 容量分类
STM32F1 系列芯片根据内部主存储器容量的大小分为以下三类产品。虽然不同容量等级下 FLASH 的总页数和页划分方式存在差异,但均以"页"(Page)作为硬件擦除操作的最小物理单位:
- **小容量产品(16KB~32KB):**划分为 32 页,每页 1KB;
- **中容量产品(64KB~128KB):**划分为 128 页,每页 1KB;
- **大容量产品(256KB~512KB):**划分为 256 页,每页 2KB;
STM32 内部 FLASH 的具体容量类型可通过芯片型号的命名后缀进行识别,具体规则可参考下图:
图2-1 STM32芯片的命名规则
由图2-1可知,STM32F103C8T6 的内部 FLASH 容量为 64KB,属于中容量产品。本文后续均以该型号为例进行说明。
提示:关于FLASH的参考资料不在《STM32参考手册》中,需查阅《STM32F10x闪存编程参考手册》
2.2 FLASH 的构成与分类
从存储组织结构来看,STM32F1系列的闪存(Flash)模块划分为以下3个功能区域(闪存存储器接口寄存器不属于闪存模块,而是一个普通的外设,作为闪存的控制器,其存储介质是SRAM,后面会详细解释):
- 主存储器
- 启动程序代码
- 用户选择字节
对于中容量产品,其FLASH模块的组织结构如下表2-1所示:
表2-1 STM32中容量产品内部FLASH的构成
从图中可以看到,系统存储器和选项字节统称为信息块(Information Block)。STM32内部FLASH虽然采用相同的物理存储介质,但根据用途不同被划分为多个独立区域,各区域拥有不同的访问权限和功能定位。
2.2.1 主存储器
主存储器又称程序存储器,是 FLASH 中容量最大的核心区域。通常所说的芯片 FLASH 容量,如 64KB、256KB,即单指该区域的大小。它主要用于存储用户的应用程序代码以及常量数据(如 const 类型数据)。
对于容量为 64KB 的 STM32F103C8T6,其主存储器共划分了 64 页(页 0 ~ 页 63),每页大小为 1KB。该区域的起始地址为 0x0800 0000,后续每一页的起始地址以 0x400(十进制 1024 字节)为步长呈线性递增,直至第 63 页的起始地址 0x0800 FC00。如下所示:
cpp
页0起始地址: 0x0800 0000
页1起始地址: 0x0800 0400
页2起始地址: 0x0800 0800
页3起始地址: 0x0800 0C00
...
页63起始地址: 0x0800 FC00
用户通过调试器或串口下载的二进制目标代码即被固化于此区域。
注意:通常所说的芯片FLASH容量(如64KB、128KB、256KB)仅指主存储器的大小,系统存储器和选项字节的容量不计入其中。
2.2.2 启动程序代码
启动程序代码也称系统存储器,地址范围为:0x1FFF F000 ~ 0x1FFF F7FF,容量固定为 2KB。
该区域在芯片出厂时已被预先写入了 Bootloader 启动程序代码。该 Bootloader 支持通过 USART 串口接收固件数据并将其编程至主存储器中,是实现串口在线下载(ISP)功能的底层基础。
同时,系统存储器在出厂时已被硬件永久锁定,用户程序无法对其执行任何擦除或编程操作。
2.2.3 用户选择字节
用户选择字节(简称选项字节),地址范围为:0x1FFF F800 ~ 0x1FFF F80F,总容量为 16 字节。
该区域专门用于配置微控制器的底层硬件行为参数,包括 FLASH 的读写保护级别、独立看门狗的硬件/软件模式选择、以及停机/待机模式下的复位触发条件等。
为了保证配置数据的安全性与可靠性,选项字节在信息块中采用了"原码+反码"的校验机制格式进行组织。每个32位字被划分为4个字节,并且每个有效配置字节都带有一个反码字节,用于校验数据完整性。其组织格式如下:
表2-2 选项字节格式
选项字节的具体组织方式如下表所示:
表2-3 选项字节详细说明
在选项字节的配置项中,各字段的具体定义如下(详细说明请参考表2-5):
- RDP:写入 RDPRT 键值(0x000000A5)后可解除读保护,写入其他值则启用读保护。
- USER:配置硬件/软件看门狗,以及进入停机/待机模式时是否产生系统复位。
- Data0 / Data1:两个可自由使用的用户数据字节。
- WRP0~WRP3:写保护配置位,每个位对应保护一组连续页。对于中容量产品,每个位保护4个页(共4KB)。
表2-4 用户选项字节说明
每次系统复位后,硬件装载器自动读取信息块中的选项字节数据,并利用其反码进行数据完整性验证。
- 若校验通过,配置参数将被锁存在内部控制寄存器中,并立即生效;
- 若选项位与其反码位不一致(将置位选项字节错误标志FLASH_OBR0:OPTERR),或者检测到该区域为全 0xFF(即擦除后的默认状态),验证功能将被关闭,对应的选项字节将被硬件强制置为安全的默认值(0xFF)。
2.3 闪存存储器接口(FPEC)第二版本
前面介绍的主存储器、系统存储器以及选项字节都属于 FLASH 存储区域,它们本身只负责保存程序代码和数据,并不具备自行完成擦除和编程操作的能力。
这是因为 FLASH 的擦除和编程过程与 SRAM 的读写不同。FLASH 在写入数据时需要专门的时序控制和内部高压电路支持,因此 STM32 在内部集成了一个专门用于管理 FLASH 的硬件控制模块------FPEC(Flash Program and Erase Controller,闪存编程与擦除控制器) 。FPEC对外提供的寄存器接口如表2-5所示,手册中简称为闪存存储器接口寄存器(Flash Interface Registers)。
表2-5 闪存存储器接口寄存器
CPU 通过访问这些寄存器来控制 FPEC,从而完成对主存储器和信息块的以下工作:
- 控制 FLASH 的擦除与编程过程;
- 产生编程和擦除所需的内部高压;
- 检测 FLASH 操作状态;
- 管理 FLASH 的读保护、写保护及选项字节配置。
因此,主存储器、系统存储器和选项字节是真正存放数据的 FLASH 区域,而 FPEC 是负责管理这些 FLASH 区域的控制模块。用户对 FLASH 的擦除和编程操作,本质上都是通过配置 FPEC 的寄存器来完成的。
2.3.1 FPEC 映射
与 GPIO、USART、TIM 等外设一样,FPEC 也拥有独立的寄存器组,并被映射到 STM32 的外设地址空间中。其基地址为:0x4002 2000。参考下图2-2 STM32F103 的存储器映射图可知,该地址位于Block2外设地址空间(Peripheral)内,而不是 FLASH 存储区域。
图2-2 STM32F103X存储器映射图
从图中可以看到,地址范围 0x4002 2000 ~ 0x4002 23FF 对应的正是 Flash Interface(闪存接口)区域。
这说明 FPEC 本质上属于一个片上外设,而非 FLASH 本身。CPU 对 FLASH 的擦除、编程和状态查询操作,实际上都是通过访问这些闪存存储器接口寄存器实现的。
2.3.2 FPEC主要寄存器
FPEC 提供了一组专门用于控制 FLASH 操作的寄存器,其主要功能如下表2-6所示:
| 寄存器 | 地址范围 | 主要功能 |
|---|---|---|
| FLASH_ACR 访问控制寄存器 | 0x4002 2000 - 0x4002 2003 | 配置 FLASH 访问延迟等参数 |
| FLASH_KEYR 密钥寄存器 | 0x4002 2004 - 0x4002 2007 | 用于接收解锁键值,执行解锁序列 |
| FLASH_OPTKEYR 选项字节密钥寄存器 | 0x4002 2008 - 0x4002 200B | 用于解除选项字节的独立写保护锁 |
| FLASH_SR 状态寄存器 | 0x4002 200C - 0x4002 200F | 反映当前 FLASH 操作的状态,其中 BSY 位指示控制器是否正处于忙状态 |
| FLASH_CR 控制寄存器 | 0x4002 2010 - 0x4002 2013 | 控制 FLASH 操作的启动与模式选择,包含 LOCK(加锁)、STRT(启动擦除)、PER(页擦除使能)、MER(全擦除使能)、PG(编程使能)、OPTER(选项字节擦除使能)、OPTPG(选项字节编程使能)等控制位 |
| FLASH_AR 地址寄存器 | 0x4002 2014 - 0x4002 2017 | 在执行页擦除操作时,用于指定目标页的起始地址 |
| 保留 | 0x4002 2018 - 0x4002 201B | ------ |
| FLASH_OBR 选项字节寄存器 | 0x4002 201C - 0x4002 201F | 反映选项字节的当前配置状态 |
| FLASH_WRPR 写保护寄存器 | 0x4002 2020 - 0x4002 2023 | 反映当前的写保护配置 |
| [表2-6 FPEC主要寄存器说明] |
其中最常用的是以下几个寄存器:
- FLASH_KEYR:用于解除 FLASH 写保护;
- FLASH_SR:用于查询 FLASH 当前工作状态;
- FLASH_CR:用于配置页擦除、全擦除和编程等操作模式;
- FLASH_AR:用于指定待擦除页的起始地址。
后续所有 FLASH 的擦除和编程操作,本质上都是围绕这几个寄存器展开的。
2.3.2 FLASH锁定机制
为了防止程序异常跑飞或误操作导致 FLASH 数据被破坏,STM32 在系统复位后默认会将 FPEC 置于锁定状态。此时 FLASH_CR 寄存器中的 LOCK 位被置为 1,任何针对主存储器或选项字节的擦除、编程操作都会被硬件禁止。
因此,在执行 FLASH 写操作之前,必须先完成解锁过程。解锁时需要依次向 FLASH_KEYR 寄存器写入两个固定键值:
cpp
KEY1 = 0x45670123
KEY2 = 0xCDEF89AB
只有按照规定顺序写入上述两个键值后,LOCK 位才会被硬件自动清零,FLASH 进入可操作状态。
如果键值错误或写入顺序不正确,则本次复位周期内 FPEC 将保持锁定状态,只能通过系统复位重新开始解锁流程。
完成擦除或编程操作后,通常会重新将 FLASH_CR 寄存器中的 LOCK 位置 1,使 FLASH 恢复保护状态,从而提高系统运行的安全性和可靠性。
**注意:**需要特别说明的是,这种锁定机制仅针对 FLASH 的擦除和编程操作。对于 FLASH 的读取操作,CPU 无需经过 FPEC,也无需执行解锁流程,而是可以像访问普通存储器一样,通过地址直接读取数据。因此在下一章中将会看到,FLASH 的读取实际上只需使用指针访问对应地址即可完成。
3 FLASH 的基本操作
对 STM32 内部 FLASH 的操作可分为三类:读取、擦除和编程(写入)。读取操作直接通过系统总线访问目标地址即可完成,无需经过闪存存储器接口。擦除和编程操作则必须通过闪存存储器接口进行,且在操作前需要先解除 FLASH 的锁定状态,操作完成后应及时重新锁定。
3.1 读取操作
读取 FLASH 中指定地址的数据,本质上是通过 C 语言的指针取内容操作直接访问存储器地址。STM32 内部的程序存储器、系统存储器和选项字节均挂载在系统总线上,属于统一编址的存储空间,因此读取操作不需要调用任何库函数,也不需要提前解锁。
读取操作的 C 语言表达式为:
cpp
uint32_t data = *((__IO uint32_t *)(Address));
该表达式由三个部分构成:
(1)给定目标地址 Address,该地址为一个 32 位无符号整型数值
(2)将 Address 强制转换为指针类型,指针所指向的数据类型决定了单次读取的数据宽度:
- uint32_t *:一次读取 32 位(一个字)
- uint16_t *:一次读取 16 位(一个半字)
- uint8_t *:一次读取 8 位(一个字节)
(3)使用星号 * 对指针进行取内容操作,获得该地址下存储的实际数据
将以上述表达式封装为函数,可得到三个读取函数:
cpp
uint32_t MyFLASH_ReadWord(uint32_t Address) // 以 32 位宽度读取
uint16_t MyFLASH_ReadHalfWord(uint32_t Address) // 以 16 位宽度读取
uint8_t MyFLASH_ReadByte(uint32_t Address) // 以 8 位宽度读取
三者的实现方式完全相同,仅在返回类型和指针强制转换的目标类型上有所区别。
**注意:**表达式中的 __IO 是 STM32 库函数中定义的宏,对应 C 语言关键字 volatile。volatile 的作用是告知编译器:该地址下的数据可能在程序流之外被改变,每次访问都必须从原始内存地址重新读取,禁止使用缓存副本或进行优化省略。
3.2 解锁与加锁
芯片复位后,闪存存储器接口默认处于锁定状态,FLASH_CR 寄存器中的 LOCK 位为 1。在锁定状态下,任何试图对程序存储器或选项字节执行擦除和编程的操作都会被硬件禁止。因此,在进行 FLASH 修改操作之前,必须首先解除闪存存储器接口的锁定。
解锁操作由库函数 FLASH_Unlock() 完成。该函数通过向 FLASH_KEYR 寄存器依次写入两个固定键值,完成解锁操作:
cpp
第一个键值(KEY1):0x45670123
第二个键值(KEY2):0xCDEF89AB
这两个键值由 ST 官方在硬件中预先设定,其作用类似于访问密码,用于降低因程序异常运行或误操作导致 FLASH 被意外修改的风险。
解锁序列必须严格按照 KEY1 → KEY2 的顺序执行。如果写入顺序错误,或者写入的数据与规定键值不匹配,闪存存储器接口将在当前复位周期内进入锁死状态,后续无法再次执行解锁操作,只能通过复位恢复。
FLASH_Unlock() 函数的内部实现如下:
cpp
void FLASH_Unlock(void)
{
/* Authorize the FPEC of Bank1 Access */
FLASH->KEYR = FLASH_KEY1;
FLASH->KEYR = FLASH_KEY2;
#ifdef STM32F10X_XL
/* Authorize the FPEC of Bank2 Access */
FLASH->KEYR2 = FLASH_KEY1;
FLASH->KEYR2 = FLASH_KEY2;
#endif
}
解锁成功后,FLASH_CR 寄存器中的 LOCK 位会被硬件自动清零,表示闪存存储器接口已经进入可操作状态。此时即可执行后续的 FLASH 擦除或编程操作。
当所有 FLASH 修改操作完成后,应及时重新锁定闪存存储器接口,避免后续程序异常执行时误修改 FLASH 内容。
加锁操作由库函数 FLASH_Lock() 完成,其内部实现是将 FLASH_CR 寄存器的 LOCK 位置 1,使闪存存储器接口重新进入保护状态,禁止后续擦除和编程操作。
FLASH_Lock() 函数实现如下:
cpp
void FLASH_Lock(void)
{
/* Set the Lock Bit to lock the FPEC and the CR of Bank1 */
FLASH->CR |= CR_LOCK_Set;
#ifdef STM32F10X_XL
/* Set the Lock Bit to lock the FPEC and the CR of Bank2 */
FLASH->CR2 |= CR_LOCK_Set;
#endif
}
因此,STM32 内部 FLASH 的典型操作流程为:FLASH_Unlock() → 执行擦除或编程操作 → FLASH_Lock()。
其中,读取 FLASH 数据不涉及存储内容修改,因此无需执行解锁和加锁操作。
3.3 页擦除
FLASH 的存储特性决定了其写入方式不同于 SRAM。对于已经存储数据的 FLASH 区域,如果需要写入新的内容,通常必须先执行擦除操作,将原有数据清除后才能重新编程。
擦除操作的本质是将 FLASH 存储单元恢复到默认状态,使所有数据位变为逻辑 1,因此擦除后的存储内容通常表现为 0xFF。由于 FLASH 的物理结构限制,STM32F1 系列并不支持按字节或按半字进行擦除,而是以 **页(Page)**作为最小擦除单位。
对于 STM32F103C8T6 这类中容量产品,每页大小为 1KB(1024 字节)。执行页擦除时,目标页内的全部数据都会被清除,无法只擦除其中某几个字节。
下图展示了页擦除的完整流程。
图3-1 闪存页擦除过程
页擦除操作由库函数 FLASH_ErasePage(uint32_t PageAddress) 完成。其中,参数 PageAddress 表示需要擦除页的起始地址。调用页擦除函数前,必须先通过 FLASH_Unlock() 解除闪存接口锁定,否则擦除操作会被硬件禁止。擦除完成后,应调用 FLASH_Lock() 重新锁定 FLASH,避免程序异常运行时误修改存储内容。
库函数 FLASH_ErasePage() 内部主要完成以下操作:
- **等待上一次 FLASH 操作完成:**首先通过检查 FLASH_SR 寄存器中的 BSY 位,判断当前 FLASH 是否正在执行其他操作。BSY = 1:表示 FLASH 忙,仍在执行擦除或编程操作;BSY = 0:表示 FLASH 空闲,可以开始新的操作。
- **开启页擦除模式:**将 FLASH_CR 寄存器中的 PER 位 置 1,使 FPEC 进入页擦除模式。
- **指定目标页地址:**将待擦除页的起始地址写入 FLASH_AR 地址寄存器,用于告诉 FPEC 需要操作的目标页。
- **启动擦除操作:**将 FLASH_CR 寄存器中的 STRT 位 置 1,触发硬件开始执行页擦除。
- **等待擦除完成:**再次检测 FLASH_SR 中的 BSY 位,等待其恢复为 0,表示擦除操作结束。
- **关闭页擦除模式:**清除 FLASH_CR 中的 PER 位,使 FLASH 接口恢复默认状态。
对应的库函数核心代码如下:
cpp
FLASH_Status FLASH_ErasePage(uint32_t Page_Address)
{
FLASH_Status status = FLASH_COMPLETE;
/* 等待上一次FLASH操作完成 */
status = FLASH_WaitForLastOperation(EraseTimeout);
if(status == FLASH_COMPLETE)
{
/* 开启页擦除模式 */
FLASH->CR |= CR_PER_Set;
/* 设置目标页地址 */
FLASH->AR = Page_Address;
/* 启动擦除 */
FLASH->CR |= CR_STRT_Set;
/* 等待擦除完成 */
status = FLASH_WaitForLastOperation(EraseTimeout);
/* 关闭页擦除模式 */
FLASH->CR &= CR_PER_Reset;
}
return status;
}
页擦除完成后,目标页中的全部存储单元均恢复为擦除状态,即每个字节均为 0xFF。库函数返回值可用于判断擦除结果:
| 返回值 | 含义 |
|---|---|
| FLASH_COMPLETE | 操作成功完成 |
| FLASH_BUSY | FLASH 长时间处于忙状态,操作超时 |
| FLASH_ERROR_PG | 编程错误 |
| FLASH_ERROR_WRP | 写保护错误 |
在实际应用中,页擦除通常不会直接暴露给上层程序,而是封装为更加简单的接口。例如:
cpp
void MyFLASH_ErasePage(uint32_t PageAddress)
{
FLASH_Unlock(); //解锁
FLASH_ErasePage(PageAddress); //页擦除
FLASH_Lock(); //加锁
}
通过将解锁 → 擦除 → 加锁这一固定流程封装起来,可以避免每次操作时遗漏必要步骤,同时降低误操作 FLASH 的风险。
3.4 全擦除
与页擦除不同,全擦除(Mass Erase)并不是针对某一个指定页进行操作,而是一次性擦除整个主存储器(Main Memory)区域中的所有内容。
以 STM32F103C8T6 为例,其主存储器容量为 64KB,共划分为 64 页(页 0 ~ 页 63)。执行全擦除后,这 64 页中的所有数据均会被清除,所有存储单元恢复为擦除状态,即数据全部变为 0xFF。
因此,全擦除会直接删除用户下载到芯片中的应用程序代码。擦除完成后,主存储器中不再存在有效程序,芯片复位后无法正常执行用户应用程序。因此,该操作通常用于调试阶段清空 FLASH、重新烧录程序,或者在设备出厂前清除已有用户数据等场景。
需要注意的是,全擦除只作用于主存储器区域,不会影响信息块(Information Block)中的内容,包括:
- 系统存储器(System Memory),即芯片内部固化的 Bootloader 程序;
- 选项字节(Option Bytes),即用于配置读保护、写保护等硬件参数的区域。
下图展示了全擦除的完整流程。
图3-2 闪存全擦除过程
全擦除由库函数 FLASH_EraseAllPages(void) 完成。与页擦除相同,调用该函数前必须先通过 FLASH_Unlock() 解除 FLASH 接口锁定,操作完成后再调用 FLASH_Lock() 重新锁定。
全擦除操作的执行过程如下:
- **等待上一次 FLASH 操作完成:**首先检查 FLASH_SR 寄存器中的 BSY 位,确认当前 FLASH 控制器处于空闲状态。
- **开启全擦除模式:**将 FLASH_CR 寄存器中的 MER 位 置 1,使 FPEC 进入主存储器全擦除模式。
- **启动擦除操作:**将 FLASH_CR 寄存器中的 STRT 位 置 1,触发硬件开始执行全擦除。
- **等待擦除完成:**持续检测 FLASH_SR 中的 BSY 位,直到其恢复为 0,表示整个擦除过程结束。
- **关闭全擦除模式:**清除 FLASH_CR 中的 MER 位,使 FLASH 接口恢复默认状态。
对应的库函数核心代码如下:
cpp
FLASH_Status FLASH_EraseAllPages(void)
{
FLASH_Status status = FLASH_COMPLETE;
/* 等待上一次FLASH操作完成 */
status = FLASH_WaitForLastOperation(EraseTimeout);
if(status == FLASH_COMPLETE)
{
/* 开启全擦除模式 */
FLASH->CR |= CR_MER_Set;
/* 启动全擦除 */
FLASH->CR |= CR_STRT_Set;
/* 等待擦除完成 */
status = FLASH_WaitForLastOperation(EraseTimeout);
/* 关闭全擦除模式 */
FLASH->CR &= CR_MER_Reset;
}
return status;
}
与页擦除相比,全擦除最大的区别在于:
| 操作类型 | 擦除范围 | 是否需要指定地址 | 对应控制位 |
|---|---|---|---|
| 页擦除 | 单个 FLASH 页(1KB) | 需要,通过 FLASH_AR 指定 | PER |
| 全擦除 | 整个主存储器区域 | 不需要 | MER |
页擦除时,FPEC 需要知道具体擦除哪一页,因此必须通过 FLASH_AR 地址寄存器提供目标地址;而全擦除的范围固定为整个主存储器,因此无需设置地址寄存器,只需开启 MER 位并启动操作即可。
在实际开发中,由于全擦除会清除整个程序存储区域,因此相比页擦除具有更大的破坏范围。除非明确需要清空整个 FLASH,否则通常优先采用页擦除方式,仅操作需要更新的数据存储区域。
3.5 编程(写入)
在完成擦除之后,目标存储区域的所有数据位均为 1,此时可向其中写入新的数据。这一写入过程在 FLASH 操作术语中称为 编程(Program)。
下图展示了 FLASH 编程的完整流程。
图3-3 闪存编程过程
FLASH 的编程操作具有一个重要的约束:数据写入只能将存储单元中的特定位从 1 变为 0,无法将 0 变为 1。将 0 恢复为 1 的唯一手段是擦除。因此,若向一个尚未擦除的地址写入数据(即该地址当前数据并非全为 0xFF),STM32 的闪存控制器会拒绝执行写入操作,并提出编程错误标志。这一规则存在一个例外:若写入的数据为全 0(即 0x0000 或 0x00000000),则无论目标地址当前内容如何,控制器均允许执行。
另一个重要约束是编程的数据宽度。STM32F1 系列的 FLASH 编程仅支持以 16 位(半字)为单位进行,任何非半字的写入操作(如 8 位字节写入或非对齐的 32 位写入)均会导致总线错误。
库函数中提供了两个编程函数:
cpp
// 在指定地址写入一个 32 位字
FLASH_Status FLASH_ProgramWord(uint32_t Address, uint32_t Data);
// 在指定地址写入一个 16 位半字
FLASH_Status FLASH_ProgramHalfWord(uint32_t Address, uint16_t Data)
{
FLASH_Status status = FLASH_COMPLETE;
/* Check the parameters */
assert_param(IS_FLASH_ADDRESS(Address));
/* Wait for last operation to be completed */
status = FLASH_WaitForLastOperation(ProgramTimeout);
if(status == FLASH_COMPLETE)
{
/* if the previous operation is completed, proceed to program the new data */
FLASH->CR |= CR_PG_Set;
*(__IO uint16_t*)Address = Data;
/* Wait for last operation to be completed */
status = FLASH_WaitForLastOperation(ProgramTimeout);
/* Disable the PG Bit */
FLASH->CR &= CR_PG_Reset;
}
/* Return the Program Status */
return status;
}
return status;
}
FLASH_ProgramWord() 实际上是对 FLASH_ProgramHalfWord() 的两次调用:先将 32 位数据的低 16 位写入 Address,再将高 16 位写入 Address + 2。
FLASH_ProgramHalfWord() 内部的执行流程如下:
- 读取 FLASH_CR 的 LOCK 位:判断是否需要执行解锁过程
- 检查 FLASH_SR 的 BSY 位:等待上一次操作完成
- 置 FLASH_CR 的 PG 位为 1:表示即将执行编程操作
- 在指定的地址写入半字(16 位):使用指针取内容操作,将 Data 写入 Address 所指向的存储单元
- 等待 FLASH_SR 的 BSY 位变为 0:表示编程完成
- 读编程地址并检查写入的数据:验证写入是否成功
与擦除操作不同的是,编程操作不需要手动设置 STRT 位。向目标地址写入数据的指针操作本身即为启动信号,硬件在检测到写入动作后自动进入编程流程。
如果需要单独写入一个 8 位字节并保留同地址另一个字节的原有数据,情况会变得复杂。由于 FLASH 不支持字节级写入,且编程前必须整页擦除(擦除会导致整页数据丢失),正确的做法是:先将目标页的全部数据读入 SRAM 中的缓冲区,在 SRAM 中修改需要变更的字节,然后擦除目标页,最后将 SRAM 缓冲区的整页数据逐一半字编程回 FLASH。这种"读---改---写"策略是实现单字节级数据管理的标准方法。
在本文的实验代码中,Store 模块正是基于这一思想设计:在 SRAM 中维护一个与 FLASH 页等大的数组作为缓存,所有数据修改在 SRAM 中完成,仅在需要持久化时执行一次"擦除+整页编程"操作,从而兼顾了数据管理的灵活性和 FLASH 的操作约束。
3.6 选项字节操作
选项字节(Option Bytes)的擦除和编程操作与程序存储器类似,但需要额外解锁选项字节的写保护机制,因此在操作流程上多了专用的解锁步骤。
在进行任何选项字节操作之前,首先需要确认 FLASH 当前是否处于空闲状态,即检查 FLASH_SR 的 BSY 位,避免与正在进行的闪存操作冲突。
3.6.1 选项字节擦除
选项字节擦除函数如下:
cpp
FLASH_Status FLASH_EraseOptionBytes(void)
{
uint16_t rdptmp = RDP_Key;
FLASH_Status status = FLASH_COMPLETE;
/* 获取当前读保护状态 */
if(FLASH_GetReadOutProtectionStatus() != RESET)
{
rdptmp = 0x00;
}
/* 等待上一次操作完成 */
status = FLASH_WaitForLastOperation(EraseTimeout);
if(status == FLASH_COMPLETE)
{
/* 解锁选项字节区域 */
FLASH->OPTKEYR = FLASH_KEY1;
FLASH->OPTKEYR = FLASH_KEY2;
/* 启动擦除操作 */
FLASH->CR |= CR_OPTER_Set;
FLASH->CR |= CR_STRT_Set;
/* 等待擦除完成 */
status = FLASH_WaitForLastOperation(EraseTimeout);
if(status == FLASH_COMPLETE)
{
/* 关闭擦除控制位 */
FLASH->CR &= CR_OPTER_Reset;
/* 进入编程选项字节 */
FLASH->CR |= CR_OPTPG_Set;
/* 恢复读保护配置 */
OB->RDP = (uint16_t)rdptmp;
/* 等待编程完成 */
status = FLASH_WaitForLastOperation(ProgramTimeout);
if(status != FLASH_TIMEOUT)
{
FLASH->CR &= CR_OPTPG_Reset;
}
}
}
return status;
}
选项字节擦除的核心流程如下:
- 检查 FLASH_SR 的 BSY 位,确认没有正在进行的闪存操作
- 解锁选项字节写保护(通过 OPTKEYR 写入特定键值)
- 置 FLASH_CR 的 OPTER 位
- 置 FLASH_CR 的 STRT 位,启动擦除
- 等待 BSY 位变为 0,表示擦除完成
- 重新进行选项字节编程并恢复关键配置数据
3.6.2 选项字节编程
选项字节编程函数如下:
cpp
FLASH_Status FLASH_ProgramOptionByteData(uint32_t Address, uint8_t Data)
{
FLASH_Status status = FLASH_COMPLETE;
/* 参数检查 */
assert_param(IS_OB_DATA_ADDRESS(Address));
/* 等待上一次操作完成 */
status = FLASH_WaitForLastOperation(ProgramTimeout);
if(status == FLASH_COMPLETE)
{
/* 解锁选项字节区域 */
FLASH->OPTKEYR = FLASH_KEY1;
FLASH->OPTKEYR = FLASH_KEY2;
/* 使能选项字节编程 */
FLASH->CR |= CR_OPTPG_Set;
/* 写入数据 */
*(__IO uint16_t*)Address = Data;
/* 等待编程完成 */
status = FLASH_WaitForLastOperation(ProgramTimeout);
if(status != FLASH_TIMEOUT)
{
FLASH->CR &= CR_OPTPG_Reset;
}
}
return status;
}
选项字节编程的主要流程如下:
- 检查 FLASH_SR 的 BSY 位
- 解锁选项字节写保护
- 置 FLASH_CR 的 OPTPG 位
- 向指定地址写入数据(触发编程)
- 等待 BSY 位清零
- 关闭 OPTPG 位并完成验证
与程序存储器操作类似,选项字节的擦除与编程同样依赖 FLASH 控制寄存器的状态控制位(OPTER / OPTPG)以及 BSY 状态位进行流程同步。
但由于选项字节涉及芯片的读写保护、启动配置等关键安全参数,一旦配置错误可能导致芯片锁死或访问受限,因此在实际开发中应谨慎操作。
在工程实践中,通常优先使用 ST-LINK Utility 等图形化工具进行配置,以降低误操作风险,这一部分将在后续章节中进一步说明。
4 STM32 ST-LINK Utility
在掌握了 FLASH 的基本操作原理后,我们需要一个工具来验证代码的正确性。STM32 ST-LINK Utility 正是这样一个强大的调试助手,它能够让我们直观地查看和修改 FLASH 中的数据,从而独立验证读取、擦除和编程等各项操作的正确性。
4.1 工具概述
STM32 ST-LINK Utility 是意法半导体(STMicroelectronics)官方提供的一款免费上位机软件,用于通过 ST-Link 调试器与 STM32 芯片建立通信,实现对芯片内部 FLASH 的可视化读写管理。
在使用 ST-LINK Utility 之前,需要先将 ST-Link 调试器通过 USB 接口连接至计算机,并通过 SWD 接口连接至目标 STM32 芯片。打开软件后,点击工具栏中的连接按钮 Connect(或通过菜单 Target → Connect),软件即可识别芯片型号并建立通信。连接成功后,主界面下方的数据窗口将显示当前 FLASH 中的内容。如下图4-1所示:
图4- 1 ST-LINK Utility连接STM32示意图
需要特别注意,ST-LINK Utility 与 Keil 等 IDE 共用 ST-Link 硬件资源,两者不可同时占用该设备。在 Keil 中执行下载操作前,必须先在软件 ST-LINK Utility 中断开连接(Target → Disconnect),否则下载将因设备被占用而失败。
4.2 查看 FLASH 内容
连接成功后,软件主界面的数据窗口以十六进制形式显示 FLASH 中的原始数据。用户可在窗口上方的地址栏中指定查看的起始地址,默认起始地址为 0x0800 0000(即主存储器的起始位置)。地址栏右侧的 Size 输入框用于设定从起始地址开始总共查看的字节数,例如输入 0x10000(十进制 65536)即可查看 64KB 的完整程序存储器内容。如下图所示:
图4-2 ST-LINK Utility主界面
如图4-2所示,数据窗口支持三种显示宽度:8 位(字节)、16 位(半字)和 32 位(字),可通过工具栏按钮或菜单进行切换。这一功能与代码中使用不同宽度指针读取 FLASH 的操作相互对应。
例如,在代码中调用 MyFLASH_ReadWord(0x08000000) 读取到的 32 位数值,可以直接与 ST-LINK Utility 中以字宽度显示的第一个数据单元进行比对,验证读取函数是否正确。
此外,在查看芯片的唯一身份标识时,可直接将起始地址设置为 UID 寄存器的基地址 0x1FFF F7E8,软件即可显示 96 位 UID 数据。如下图所示:
图4-3 ST-LINK Utility 查看芯片 UID
同时,结合数据宽度的切换,可以验证代码中分 16 位和 32 位多次读取的正确性,以及理解小端模式(低位字节存放在低地址)在 STM32 中的具体表现。
4.3 直接修改 FLASH 数据
ST-LINK Utility 允许直接在数据窗口中对 FLASH 的任意地址进行数据修改。操作方式为:在数据窗口中找到目标地址对应的数据单元,单击选中后直接输入新的十六进制数值,再Enter确认后软件会将新数据写入芯片。这一操作的下层同样执行了擦除和编程的完整流程,但对用户而言是透明的。例如,修改地址0x0800 0030下前8位数据为66,如下图所示:
图4-4 ST-LINK Utility中直接修改 FLASH 数据
利用这一功能,开发者可以在不编写任何代码的情况下预先向 FLASH 中写入测试数据,然后通过运行程序读取该数据以验证读取函数;也可以在程序执行擦除或编程操作后,通过刷新数据窗口检查目标地址的内容是否符合预期,从而独立判定擦除或编程函数是否存在问题。
对于外挂 FLASH 芯片(如 W25Q64),由于芯片内部数据无法通过上位机直接可视化,验证写入结果只能依赖"写入后再读出"的闭环测试------若读出数据与写入数据不一致,难以定位是写入错误还是读取错误。而内部 FLASH 借助 ST-LINK Utility 的可视化能力,可以绕过读取代码,直接从外部确认 FLASH 中的真实内容,显著提升调试效率。
4.4 配置选项字节
ST-LINK Utility 集成了选项字节的图形化配置界面,通过菜单 Target → Option Bytes 打开。在该界面中,可以直观地配置以下参数:
图4-5 ST-LINK Utility选项字节配置界面
如上图所示,选项字节的组织结构包含以下关键配置项:
- **读保护(Read Out Protection,RDP):**用于控制用户程序的读保护状态,防止通过调试器或编程工具读取芯片内部 FLASH 内容。对于 STM32F1 系列,RDP 只有两种状态,Disabled(关闭读保护,允许正常读取和调试)和Enabled(开启读保护,禁止通过调试接口读取 FLASH 内容)。
- 用户配置字节(USER):用于配置部分系统级功能,包括:独立看门狗工作模式(硬件模式或软件模式)、进入 STOP、STANDBY 等低功耗模式时是否产生复位,以及复位引脚(NRST)的相关功能配置等。
- 用户数据(Data0/Data1):提供两个可供用户自由使用的数据字节,可用于存储简单的标志位、版本号或其他少量配置信息。
- 写保护(Write Protection,WRP0/1/2/3):用于配置主存储器的写保护区域。被保护的 FLASH 页将禁止擦除和编程操作,从而防止程序或关键数据被意外修改。对于中容量产品,选项字节中写保护域的每一位WRPx对应四页FLASH,因此,勾选其中的某一Page x时,相关的四页也同时被勾选,即同时使能该四页的写保护。
配置完成后,点击 Apply 按钮,ST-LINK Utility 将自动执行选项字节的擦除与编程流程,并将新的配置写入选项字节区域。与直接调用选项字节编程函数相比,使用图形化界面进行配置更加直观、安全,也更便于观察各配置项对芯片行为的影响,因此在开发调试阶段被广泛采用。
4.5 芯片锁死的恢复方法
在开发过程中,如果错误地配置了选项字节,可能导致芯片无法正常下载、擦除或调试。例如:
- 对部分 FLASH 页面启用了写保护,导致对应区域无法重新擦除或编程;
- 开启了读保护(Read Out Protection),调试器无法读取主存储器内容;
- 用户程序修改了调试接口相关配置,导致 ST-LINK 无法正常连接目标芯片。
对于上述情况,通常可以借助 ST-LINK Utility 的 Option Bytes 配置功能进行恢复。
首先连接目标芯片,然后打开 Target → Option Bytes 配置界面。若芯片开启了读保护,可将 Read Out Protection 设置为 Disabled;若配置了写保护,则取消对应页面的保护选项,最后点击 Apply 按钮使新的配置生效。
图4-6 ST-LINK Utility 的 Option Bytes 配置功能
需要注意的是,对于 STM32F1 系列,当读保护状态由 Enabled 修改为 Disabled 时,芯片会自动执行一次主存储器全擦除(Mass Erase)操作,以防止受保护程序被绕过读出。因此,解除读保护后,原有用户程序和存储在主存储器中的数据将全部被清除,这是正常现象。
写保护的解除则不会触发额外的全片擦除,仅会修改对应的选项字节配置。配置完成后,芯片恢复正常的擦除、编程和调试能力,用户即可重新下载程序。
从原理上讲,选项字节属于独立于主存储器的 FLASH 区域,其擦除和编程由 FPEC 单独管理。因此,即使主存储器已经启用了读保护或写保护,只要调试接口仍能够建立连接,开发者仍然可以通过 ST-LINK Utility 重新配置选项字节,从而恢复芯片的正常使用状态。
因此,在进行 FLASH 实验或选项字节配置时,掌握 ST-LINK Utility 的恢复方法十分重要。当芯片因保护配置不当而无法正常下载或调试时,该功能往往是最直接、最有效的解决手段。
**注意:**这里还需要说明的是,不同 STM32 系列对于读保护机制的实现方式并不完全相同。本文所使用的 STM32F103 属于 STM32F1 系列,在 ST-LINK Utility 中通常仅提供 Enabled 和 Disabled 两种读保护状态。而在部分较新的 STM32 系列中,读保护可能被划分为多个等级(如 RDP Level 0、Level 1、Level 2 等),其保护能力、解除方式以及恢复条件均有所不同。由于不同系列之间存在差异,本文不再展开讨论,读者在实际使用时应以对应型号的数据手册和参考手册为准。
5 本章节实验
5.1 读写内部 FLASH
5.1.1 实验目标
本实验的目的是利用 STM32F103C8T6 内部 FLASH 中主存储器的剩余空间,存储一组用户自定义的参数数据Store_Data ,并确保这些数据在芯片断电或复位后仍能保持不丢失。
具体实现方式为:将主存储器的最后一页(第 63 页,地址范围:0x0800FC00~0x0800 FFFF,大小1KB)预留为数据存储区,在 SRAM 中建立一个与该页等大的缓存数组Store_Data 应用程序对参数的所有读写操作均针对这个 SRAM 数组进行,仅在需要持久化时通过一次 "擦除--->编程" 操作将整个数组备份至 FLASH。上电初始化时,系统自动将 FLASH 中的数据加载回 SRAM 数组,从而实现对上层应用透明的掉电不丢失存储功能。
通过两个按键的交互,可以在运行时修改存储的参数值并保存,也可以将所有参数清零。整个过程中,数据是否真正实现了掉电不丢失,可通过断电再上电和按复位键两种方式验证。
5.1.2 硬件连接

5.1.3 软件设计
软件架构按照功能抽象层级划分为三层:
- **底层 MyFLASH 模块:**封装对 FLASH 的直接操作(读取、擦除、编程),隐藏解锁/加锁等底层细节。
- **上层 Store 模块:**基于 MyFLASH 提供面向应用的参数存储管理机制,通过 SRAM 数组实现数据灵活存取,并通过整页备份策略保证掉电不丢失。
- **应用层主程序:**负责初始化各模块、处理按键输入并驱动 OLED 显示。
5.1.3.1 底层 MyFLASH 模块
MyFLASH模块位于工程目录的 MyFLASH.c / MyFLASH.h 文件中,提供以下七个函数接口:
cpp
// 以32位宽度读取指定地址的数据
uint32_t MyFLASH_ReadWord(uint32_t Address);
// 以16位宽度读取指定地址的数据
uint16_t MyFLASH_ReadHalfWord(uint32_t Address);
// 以8位宽度读取指定地址的数据
uint8_t MyFLASH_ReadByte(uint32_t Address);
// 全擦除:擦除整个程序存储器(解锁 → 全擦除 → 加锁)
void MyFLASH_EraseAllPages(void);
// 页擦除:擦除指定页(解锁 → 页擦除 → 加锁)
void MyFLASH_ErasePage(uint32_t PageAddress);
// 在指定地址写入一个32位字(解锁 → 编程字 → 加锁)
void MyFLASH_ProgramWord(uint32_t Address, uint32_t Data);
// 在指定地址写入一个16位半字(解锁 → 编程半字 → 加锁)
void MyFLASH_ProgramHalfWord(uint32_t Address, uint16_t Data);
所有擦除和编程函数均包含解锁和加锁步骤,上层模块调用时无需再关心解锁逻辑。读取函数不涉及解锁,直接通过指针返回数据。
5.1.3.2 上层 Store 模块
Store 模块(Store.c / Store.h)是参数存储管理的核心,其设计思路为:将 FLASH 的最后一页视为掉电不丢失的后备存储,同时在 SRAM 中定义一个与之等大的数组 Store_Data 作为日常数据操作的中间缓冲区。对参数的读取、修改全部在 SRAM 数组中完成,仅在需要持久化时调用 Store_Save() 将整个数组写入 FLASH。上电初始化时调用 Store_Init() ,自动根据 FLASH 中的标志位判断是否需要初始化 FLASH 数据,并将 FLASH 内容加载至 SRAM 数组。
宏定义配置:
cpp
#define STORE_START_ADDRESS 0x0800FC00 // 存储的起始地址(也是最后一页的起始地址)
#define STORE_COUNT 512 // 存储数据的个数(半字个数,对应1024字节)
STORE_COUNT 设为 512,是因为主存储器一页大小为 1Kb(即,1024 字节)。若以 16 位半字uint16_t 为单位存储,每个半字为2字节,且2 * 512 = 1024 字节,则填满整页恰好需要 512 个半字。
SRAM 数组定义:
cpp
uint16_t Store_Data[STORE_COUNT];
数组的每个元素对应 FLASH 页中一个半字的数据。通过索引可以直接访问和修改参数。
标志位机制:
为判断FLASH数据区是否已被本程序初始化过,使用数组的第一个半字 Store_Data0 作为标志位,存储固定值 0xA5A5 。
- 若FLASH最后一页的第一个半字读取值为 0xA5A5 ,则认为该页已被本程序写入过有效数据,上电时直接加载数据至 SRAM 数组;
- 若读取值非 0xA5A5(如全新芯片或之前被擦除),则认为 FLASH 数据区尚未初始化,需要执行首次初始化流程:擦除该页、写入标志位、将其余位置清0。
Store_Init() 函数:
cpp
/**
* 函 数:参数存储模块初始化
* 参 数:无
* 返 回 值:无
*/
void Store_Init(void)
{
/*判断是不是第一次使用*/
if (MyFLASH_ReadHalfWord(STORE_START_ADDRESS) != 0xA5A5) //读取第一个半字的标志位,if成立,则执行第一次使用的初始化
{
MyFLASH_ErasePage(STORE_START_ADDRESS); //擦除指定页
MyFLASH_ProgramHalfWord(STORE_START_ADDRESS, 0xA5A5); //在第一个半字写入自己规定的标志位,用于判断是不是第一次使用
for (uint16_t i = 1; i < STORE_COUNT; i ++) //循环STORE_COUNT次,除了第一个标志位
{
MyFLASH_ProgramHalfWord(STORE_START_ADDRESS + i * 2, 0x0000); //除了标志位的有效数据全部清0
}
}
/*上电时,将闪存数据加载回SRAM数组,实现SRAM数组的掉电不丢失*/
for (uint16_t i = 0; i < STORE_COUNT; i ++) //循环STORE_COUNT次,包括第一个标志位
{
Store_Data[i] = MyFLASH_ReadHalfWord(STORE_START_ADDRESS + i * 2); //将闪存的数据加载回SRAM数组
}
}
函数执行逻辑分为两个阶段:
- **第一阶段:**通过读取标志位判断 FLASH 状态,若标志位不匹配,则执行完整的初始化流程------擦除整页并依次写入标志位和零值。
- **第二阶段:**不论是否经过初始化,都将 FLASH 中当前整页数据逐一读入 SRAM 数组 Store_Data,使 SRAM 数组与 FLASH 内容同步。
补充说明:地址偏移量 i * 2 的含义
在表达式:STORE_START_ADDRESS + i * 2 中,i * 2 用于计算第 i 个数据在 FLASH 中的地址偏移量。这是因为 STM32 存储器是按字节编址,地址是以字节为单位递增的。而本实验存储的数据类型为 uint16_t,每个元素占用 2 个字节。因此,相邻两个数据在 FLASH 中的起始地址相差 2 个字节,如下表所示:
数组元素 FLASH地址 Store_Data0 0x0800FC00 Store_Data1 0x0800FC02 Store_Data2 0x0800FC04 ... ... Store_Datai 0x0800FC00 + i × 2 因此,通过 STORE_START_ADDRESS + i * 2 就可以定位到第 i 个半字数据对应的 FLASH 地址。
同理,若存储的数据类型为 uint32_t,由于每个元素占用 4 个字节,则地址偏移量应改为 i * 4。
Store_Save() 函数:
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]); // 写入整页
}
}
保存操作采用"全擦全写"策略:先将目标页Page 63整页擦除,然后将 SRAM 数组Store_Data 中全部 512 个半字依次写入到内部 Flash 的Page 63页中。这种方式虽然每次保存都需要擦除和重写整页(包括未变更的数据),但实现简单、逻辑清晰,且由于本实验保存频率低(仅按键触发),擦写耗时在可接受范围内。
Store_Clear() 函数:
cpp
void Store_Clear(void)
{
for (uint16_t i = 1; i < STORE_COUNT; i++)
{
Store_Data[i] = 0x0000; // SRAM有效数据清零(保留标志位)
}
Store_Save(); // 将更改保存到FLASH
}
清零操作仅针对 Store_Data1 至 Store_DataSTORE_COUNT-1(即标志位以外的所有用户数据),在SRAM中(即,数组Store_Data )完成清零后,调用 Store_Save() 将全 0 数据写回 FLASH。标志位保持不变,确保下次上电时仍能识别为已初始化状态。
5.1.3.3 主程序逻辑
主程序(main.c)完成各模块初始化后进入主循环,周期性检测按键状态并更新 OLED 显示。
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(); //OLED初始化
Key_Init(); //按键初始化
Store_Init(); //参数存储模块初始化,在上电的时候将闪存的数据加载回Store_Data,实现掉电不丢失
/*显示静态字符串*/
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(); //将Store_Data的数据备份保存到闪存,实现掉电不丢失
}
if (KeyNum == 2) //按键2按下
{
Store_Clear(); //将Store_Data的数据全部清0
}
OLED_ShowHexNum(1, 6, Store_Data[0], 4); //显示Store_Data的第一位标志位
OLED_ShowHexNum(3, 1, Store_Data[1], 4); //显示Store_Data的有效存储数据
OLED_ShowHexNum(3, 6, Store_Data[2], 4);
OLED_ShowHexNum(4, 1, Store_Data[3], 4);
OLED_ShowHexNum(4, 6, Store_Data[4], 4);
}
}
初始化阶段依次执行OLED_Init()、Key_Init() 和 Store_Init()。Store_Init()在首次上电或 FLASH 数据区未被初始化时自动完成初始化,并加载数据至 SRAM 数组。
主循环中,通过 Key_GetNum() 获取按键键码:
- **若 KeyNum == 1:**表示 K1 按下,则将 Store_Data1 ~ Store_Data4 分别进行递增,然后调用 Store_Save() 将修改后的数组数据更新保存至 FLASH。
- **若 KeyNum == 2:**表示 K2 按下,则调用 Store_Clear() 清零全部有效数据并保存。
OLED 显示屏上固定显示两行标签:
- 第一行显示 Flag:,及标志位 Store_Data0 的值(应为 0xA5A5)
- 第二行显示 Data:
第三行和第四行分别显示四个用户数据字段 Store_Data1 至 Store_Data4 的十六进制值。
本实验其它关键代码文件如下:
MyFLASH.c文件:
cpp
#include "stm32f10x.h" // Device header
/**
* 函 数:FLASH读取一个32位的字
* 参 数:Address 要读取数据的字地址
* 返 回 值:指定地址下的数据
*/
uint32_t MyFLASH_ReadWord(uint32_t Address)
{
return *((__IO uint32_t *)(Address)); //使用指针访问指定地址下的数据并返回
}
/**
* 函 数:FLASH读取一个16位的半字
* 参 数:Address 要读取数据的半字地址
* 返 回 值:指定地址下的数据
*/
uint16_t MyFLASH_ReadHalfWord(uint32_t Address)
{
return *((__IO uint16_t *)(Address)); //使用指针访问指定地址下的数据并返回
}
/**
* 函 数:FLASH读取一个8位的字节
* 参 数:Address 要读取数据的字节地址
* 返 回 值:指定地址下的数据
*/
uint8_t MyFLASH_ReadByte(uint32_t Address)
{
return *((__IO uint8_t *)(Address)); //使用指针访问指定地址下的数据并返回
}
/**
* 函 数:FLASH全擦除
* 参 数:无
* 返 回 值:无
* 说 明:调用此函数后,FLASH的所有页都会被擦除,包括程序文件本身,擦除后,程序将不复存在
*/
void MyFLASH_EraseAllPages(void)
{
FLASH_Unlock(); //解锁
FLASH_EraseAllPages(); //全擦除
FLASH_Lock(); //加锁
}
/**
* 函 数:FLASH页擦除
* 参 数:PageAddress 要擦除页的页地址
* 返 回 值:无
*/
void MyFLASH_ErasePage(uint32_t PageAddress)
{
FLASH_Unlock(); //解锁
FLASH_ErasePage(PageAddress); //页擦除
FLASH_Lock(); //加锁
}
/**
* 函 数:FLASH编程字
* 参 数:Address 要写入数据的字地址
* 参 数:Data 要写入的32位数据
* 返 回 值:无
*/
void MyFLASH_ProgramWord(uint32_t Address, uint32_t Data)
{
FLASH_Unlock(); //解锁
FLASH_ProgramWord(Address, Data); //编程字
FLASH_Lock(); //加锁
}
/**
* 函 数:FLASH编程半字
* 参 数:Address 要写入数据的半字地址
* 参 数:Data 要写入的16位数据
* 返 回 值:无
*/
void MyFLASH_ProgramHalfWord(uint32_t Address, uint16_t Data)
{
FLASH_Unlock(); //解锁
FLASH_ProgramHalfWord(Address, Data); //编程半字
FLASH_Lock(); //加锁
}
Store.c文件:
cpp
#include "stm32f10x.h" // Device header
#include "MyFLASH.h"
#define STORE_START_ADDRESS 0x0800FC00 //存储的起始地址
#define STORE_COUNT 512 //存储数据的个数
uint16_t Store_Data[STORE_COUNT]; //定义SRAM数组
/**
* 函 数:参数存储模块初始化
* 参 数:无
* 返 回 值:无
*/
void Store_Init(void)
{
/*判断是不是第一次使用*/
if (MyFLASH_ReadHalfWord(STORE_START_ADDRESS) != 0xA5A5) //读取第一个半字的标志位,if成立,则执行第一次使用的初始化
{
MyFLASH_ErasePage(STORE_START_ADDRESS); //擦除指定页
MyFLASH_ProgramHalfWord(STORE_START_ADDRESS, 0xA5A5); //在第一个半字写入自己规定的标志位,用于判断是不是第一次使用
for (uint16_t i = 1; i < STORE_COUNT; i ++) //循环STORE_COUNT次,除了第一个标志位
{
MyFLASH_ProgramHalfWord(STORE_START_ADDRESS + i * 2, 0x0000); //除了标志位的有效数据全部清0
}
}
/*上电时,将闪存数据加载回SRAM数组,实现SRAM数组的掉电不丢失*/
for (uint16_t i = 0; i < STORE_COUNT; i ++) //循环STORE_COUNT次,包括第一个标志位
{
Store_Data[i] = MyFLASH_ReadHalfWord(STORE_START_ADDRESS + i * 2); //将闪存的数据加载回SRAM数组
}
}
/**
* 函 数:参数存储模块保存数据到闪存
* 参 数:无
* 返 回 值:无
*/
void Store_Save(void)
{
MyFLASH_ErasePage(STORE_START_ADDRESS); //擦除指定页
for (uint16_t i = 0; i < STORE_COUNT; i ++) //循环STORE_COUNT次,包括第一个标志位
{
MyFLASH_ProgramHalfWord(STORE_START_ADDRESS + i * 2, Store_Data[i]); //将SRAM数组的数据备份保存到闪存
}
}
/**
* 函 数:参数存储模块将所有有效数据清0
* 参 数:无
* 返 回 值:无
*/
void Store_Clear(void)
{
for (uint16_t i = 1; i < STORE_COUNT; i ++) //循环STORE_COUNT次,除了第一个标志位
{
Store_Data[i] = 0x0000; //SRAM数组有效数据清0
}
Store_Save(); //保存数据到闪存
}
5.1.4 实验现象
程序烧录至芯片后,上电或复位时 OLED 屏幕显示如下内容:
Flag 一行显示 A5A5,表示数据区已初始化(我的这个OLED模块的前面几行显示晶体管损坏了,所以看着像AbAb),Data 一行下显示四个参数的具体数值。若为首次运行,FLASH 数据区尚未被初始化,初始化流程自动执行,四个数据字段将被置为 0x0000并由 Store_Init() 加载显示。

按下 K1 键,四个数据分别按照 +1、+2、+3、+4 的步进值累加,例如从 0000 0000 0000 0000 变为 0001 0002 0003 0004,再次按下变为 0002 0004 0006 0008,以此类推。每次按下后,数据立即更新显示,同时 Store_Save() 将新数据写入 FLASH。

此时将芯片完全断电(断开 USB 供电或外部电源),稍后重新上电。OLED 屏幕将恢复显示断电前最后保存的数据,证明数据在掉电后未丢失。按下复位键(不切断电源),同样可以看到数据保持,说明复位不会清除 FLASH 中的用户数据。
按下 K2 键,程序调用 Store_Clear(),四个数据字段全部变为 0x0000,标志位保持 0xA5A5 不变。再次断电重启,显示数据为全零,符合预期。
利用 STM32 ST-LINK Utility 可进一步验证 FLASH 中的物理数据。连接芯片后,将起始地址设置为 0x0800FC00,数据宽度选择 16 位,可观察到:第一个半字为 A5A5,后续半字依次为对应的数据值。

按下 K1 修改数据后,在 ST-LINK Utility 中刷新(需要断开再重连,或因 Keil 占用需切换),可观察到数据同步变化。

按下 K2 清零后,后续半字全部变为 0000。这些现象与 OLED 显示完全一致,验证了底层 FLASH 读写操作的正确性。

5.2 读取芯片 ID
在完成了 FLASH 读写实验后,我们再通过一个简单的实验来巩固对 FLASH 读取操作的理解。本实验将演示如何读取芯片出厂时固化的标识信息。
5.2.1 实验目标
每一枚 STM32F103C8T6 芯片在出厂时,其内部 FLASH 的系统存储器区域已被预先写入一组不可更改的标识信息,包括闪存容量和 96 位的唯一身份标识。本实验的目标是通过软件直接读取这两个标识信息,并将其显示在 OLED 屏幕上,以验证通过指针访问系统存储器读取芯片电子签名的可行性。
器件电子签名存放在闪存存储器模块的系统存储区域(即,信息块中的启动程序代码区域),包含的芯片识别信息在出厂时编写,不可更改。使用指针读指定地址下的存储器可获取电子签名。具体包括:
- 闪存容量寄存器:基地址 0x1FFFF7E0,大小 16 位,存储的是芯片内部程序存储器(主存储器)的容量值,单位为 KB。对于 C8T6 芯片,该值为 64(十六进制 0x0040)
- 产品唯一身份标识寄存器:基地址 0x1FFFF7E8,大小 96 位,是一个 96 位的二进制序列,每一枚芯片的该序列均不相同,可用作设备身份认证、序列号管理、软件加密绑定等用途
读取这些信息无需任何外设初始化或 FLASH 解锁操作,仅需通过 C 语言指针直接访问指定的系统存储器地址。
5.2.2 硬件设计

5.2.3 软件设计
本实验的硬件连接仅需一个 OLED 显示屏,用于输出读取到的数据,无需额外按键或存储器。软件实现全部在 main.c 中完成,不涉及额外的底层模块封装。
5.2.3.1 闪存容量寄存器(F_SIZE)的读取
闪存容量寄存器的基地址为 0x1FFFF7E0,该寄存器位于系统存储器地址空间内。寄存器宽度为 16 位,存储的数值以 KB 为单位表示程序存储器的总容量。
读取操作使用 16 位宽度的指针取内容表达式:
cpp
*((__IO uint16_t *)(0x1FFFF7E0))
该表达式的执行过程为:将地址 0x1FFFF7E0 强制转换为指向 16 位无符号整型(uint16_t)的指针,然后通过取内容操作符 * 读出该地址处的 16 位数据。__IO 宏(即 volatile)确保读取操作不会被编译器优化省略,直接从硬件地址获取最新值。
读取到的数据通过 OLED 显示函数输出到屏幕的第一行,显示格式为 4 位十六进制数。对于 C8T6 芯片,预期显示值为 0040(十进制 64)。
5.2.3.2 产品唯一身份标识寄存器(UID)的读取
产品唯一身份标识寄存器的基地址为 0x1FFFF7E8,总宽度为 96 位(12 字节)。在 STM32 的存储器映射中,这 96 位数据以连续的 12 个字节形式存储,可按照不同的数据宽度组合进行读取。
本实验采用混合宽度读取方式,将 96 位数据分为四个部分依次读出:
- 第一部分:地址 0x1FFFF7E8,以 16 位宽度读取,获取 UID 的低 16 位
- 第二部分:地址 0x1FFFF7E8 + 0x02,以 16 位宽度读取,获取 UID 的次低 16 位
- 第三部分:地址 0x1FFFF7E8 + 0x04,以 32 位宽度读取,获取 UID 的中间 32 位
- 第四部分:地址 0x1FFFF7E8 + 0x08,以 32 位宽度读取,获取 UID 的高 32 位
对应的 C 语言表达式分别为:
cpp
*((__IO uint16_t *)(0x1FFFF7E8))
*((__IO uint16_t *)(0x1FFFF7E8 + 0x02))
*((__IO uint32_t *)(0x1FFFF7E8 + 0x04))
*((__IO uint32_t *)(0x1FFFF7E8 + 0x08))
地址表达式中的加法必须在括号内部完成,再强制类型转换为对应宽度的指针,否则可能因运算符优先级问题导致计算错误。四个部分的数据通过 OLED 显示函数依次输出到屏幕的第二、三、四行,显示宽度分别为 4 位、4 位、8 位、8 位十六进制数。
5.2.3.3 主程序逻辑
cpp
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
int main(void)
{
OLED_Init(); //OLED初始化
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)
{
}
}
程序初始化 OLED 后,一次性读取并显示所有 ID 信息,随后进入无限循环保持显示。整个代码不需要调用任何 FLASH 库函数,因为读取系统存储器属于标准的存储器访问操作,通过总线直接完成。
5.2.4 实验现象
将程序编译并下载到 STM32 后,OLED 将显示芯片的 FLASH 容量和唯一身份标识(UID)信息,如下图所示:
其中:
- **F_SIZE:0040:**0040 为 FLASH 容量寄存器的内容;十六进制 0x0040 转换为十进制后为 64,表示该芯片具有 64KB 主存储器容量,与 C8T6 芯片标称的 64KB 容量完全一致。
- **U_ID:**UID 由三个 32 位数据组成(U_ID:0670FF48 49517850 87083138),总长度为 96 位(12 字节);每颗 STM32 芯片的 UID 均由工厂唯一分配,因此显示结果不会完全相同。
为了验证程序读取结果是否正确,可以使用 STM32 ST-LINK Utility 直接查看芯片内部存储器内容。
将 ST-LINK Utility 连接至目标芯片后,在地址栏输入:0x1FFFF7E8,并将数据显示宽度设置为 32 bits,即可看到 UID 所在区域的数据,如图所示。

从图中可以看到,地址 0x1FFFF7E8 开始的三个 32 位数据分别为:
cpp
0670FF48
49517850
87083138
与 OLED 显示结果完全一致,说明程序已经正确读取了芯片的 UID 信息。
需要说明的是,STM32 采用 小端存储模式(Little Endian)。在这种存储方式下,一个多字节数据的低字节存放在低地址,高字节存放在高地址。例如:
cpp
32位数据:0x0670FF48
内存地址:
0x1FFFF7E8 → 0x48
0x1FFFF7E9 → 0xFF
0x1FFFF7EA → 0x70
0x1FFFF7EB → 0x06
不过 ST-LINK Utility 在 32 bits 显示模式下已经按照一个完整的 32 位整数进行解析,因此界面中直接显示为 0670FF48,与程序读取得到的结果保持一致,无需再手动进行字节重组。
对于 FLASH 容量信息,可将起始地址设置为:0x1FFFF7E0,并选择 16 bits 显示宽度,此时可看到数据值为 0040:

对应芯片的 FLASH 容量为 64KB,与 OLED 显示结果一致。
芯片唯一身份标识(UID)的实际应用价值在于,它为每颗 STM32 提供了一个全球唯一且出厂固化的硬件标识。开发者可以利用 UID 实现设备序列号管理、产品追溯、软件授权绑定以及设备身份认证等功能。结合加密算法后,还能够有效提高嵌入式系统的软件安全性和防复制能力。
6 注意事项与避坑指南
在实际应用中使用内部 FLASH 存储用户数据时,需要注意一些关键问题,以避免常见的错误和潜在的风险。
6.1 程序空间与用户数据空间的冲突
在使用内部 FLASH 存储用户数据时,最核心的风险是用户数据区域与程序代码区域发生重叠。若程序代码本身占用了用户数据所规划的存储页,在执行擦除或编程操作时会将自身代码破坏,导致程序运行异常或彻底崩溃。因此,在选定数据存储区域之前,必须先确认程序代码的占用范围,确保数据存储地址完全位于程序代码覆盖区域之外。
选择数据存储区域的一般原则是:优先使用程序存储器末尾的若干页。对于一个规模较小的应用程序(例如代码体积在几 KB 至十几 KB 以内),程序代码主要集中在低地址区域,末尾页处于空闲状态,被程序本身占用的概率极低,可安全用于数据存储。
6.2 查看程序占用空间
在 Keil MDK 开发环境中,工程编译完成后,Build Output 窗口会输出程序的尺寸信息,例如:

其中:
- Code:程序代码段,占用 FLASH;
- RO-data:只读数据段(如 const 常量、字符串常量等),占用 FLASH;
- RW-data:已初始化的全局变量和静态变量,其初始值存储在 FLASH 中,上电后由启动代码复制到 SRAM,因此同时占用 FLASH 和 SRAM;
- ZI-data:未初始化的全局变量和静态变量,仅占用 SRAM,不占用 FLASH。
因此,程序实际占用的 FLASH 空间为:
cpp
Code + RO-data + RW-data
= 2792 + 1788 + 4
= 4584 Bytes
= 0x11E8 Bytes
程序实际占用的 SRAM 空间为:
cpp
RW-data + ZI-data
= 4 + 2660
= 2664 Bytes
另一种更精确的方法是查看链接器生成的 .map文件。
在工程编译完成后,可打开工程目录中位于 Listings 文件夹或工程根目录下的 Project.map 文件,或直接在软件 Keil 中双击Target 1打开对应的 .map 文件。如下图所示:

.map 文件是链接器生成的详细内存分配报告,在文件末尾可以看到:
cpp
Total ROM Size (Code + RO Data + RW Data) 4584 (4.48kB)
该数值即为程序在 FLASH 中实际占用的总空间,与 Build Output 框中统计结果完全一致。
以 STM32F103C8T6 为例,其主存储器容量为 64KB(65536 Bytes,0x10000 Bytes),若当前程序占用 0x11E8 Bytes,则剩余 FLASH 空间为:0x10000 - 0x11E8 = 0xEE18 Bytes ≈ 59.5KB。
因此,将数据存储区规划在最后一页 Page63:0x0800FC00 ~ 0x0800FFFF,与程序代码区域之间仍有大量空闲空间,两者不会发生重叠,可安全用于参数存储。
除了查看编译统计信息之外,还可以借助 STM32 ST-LINK Utility 直接观察 FLASH 中程序的实际分布情况。
连接芯片后,在 ST-LINK Utility 中从主存储器起始地址:0x08000000 开始查看数据内容。已被程序占用的区域会显示为各种十六进制数据,而尚未使用或已经擦除的区域则全部显示为:FFFFFFFF。从编译结果可知:Total ROM Size = 4584 Bytes = 0x11E8,而 STM32 的程序默认从0x08000000 开始存放,因此程序占用范围理论上为 0x08000000 + 0x11E8 = 0x080011E8。这意味着 0x080011E8 已经是程序空间的结束边界地址,程序实际占用的最后一个字节地址为0x080011E7,如下图所示:
图7-2 ST-LINK Utility 截图
从图中可以看到,在地址 0x080011E8 附近开始出现连续的 FFFFFFFF 数据,说明程序已经结束,后续 FLASH 空间处于空闲状态。
因此,ST-LINK Utility 观察到的程序结束位置与 .map 文件中统计得到的:Total ROM Size = 4584 Bytes 完全吻合,两种方法相互验证,证明当前程序仅占用了约 4.48KB 的 FLASH 空间,其余空间均可作为后续数据存储区域使用。
6.3 限制程序存储范围
当程序体积较大,接近甚至可能触及末尾数据存储页时,即使当前编译结果尚未发生冲突,后续添加代码后也存在覆盖数据区的风险。为了避免这种潜在的地址冲突,可以在工程配置中显式地限制程序代码的存储范围,将末尾页从编译器的可分配空间中剔除。
具体操作方法为:在 Keil MDK 中打开工程选项(Project → Options for Target),进入 Target 选项卡,找到 Read/Only Memory Areas 区域的 IROM1 配置项。默认配置下,起始地址(Start)为 0x08000000,大小(Size)为 0x10000(即全部 64KB 均分配给程序代码使用)。如下图所示:

若希望保留最后一页(起始地址 0x0800FC00,大小 1KB),则应将 Size 修改为 0xFC00,即缩减 1KB 的地址空间。此时编译器将只在前 0xFC00 字节(63KB)范围内为程序代码和只读数据分配存储地址,最后一页被排除在可分配区域之外,无论程序体积如何增长,都不会触及该页。
若 Size 设置的数值过小,以致于程序本身所需的存储空间超出了限定范围,编译阶段链接器将报错,提示 FLASH 空间不足。此时需要在优化代码体积或扩大限定范围之间做出取舍。
6.4 下载配置中的擦除方式选择
在 Keil MDK 的下载配置中,FLASH Download 选项卡提供了三种擦除方式:
- 全擦除(Erase Full Chip)
- 页擦除(Erase Sectors)
- 不擦除(Do not Erase)

这三种方式对用户数据存储区域的影响存在显著差异。
-
全擦除:每次下载程序前,将整个程序存储器全部擦除(信息块除外)。这意味着存储在 FLASH 末尾页的用户数据也会在下载时一并被清除。在开发调试阶段,频繁的全擦除会导致每次下载后都需要重新初始化数据存储区,虽然可由程序的初始化逻辑自动完成(标志位不匹配时触发),但之前在芯片上积累的测试数据会全部丢失
-
页擦除:仅擦除程序代码实际占用的那些页,其余未被本次下载涉及的页(包括数据存储页)保持原有内容不变。选择该方式可以在反复下载调试程序的同时,保留末尾页中的用户数据,避免每次下载后重新初始化
-
不擦除:下载前不执行任何擦除操作。该选项仅在极特殊的调试场景下使用,一般不推荐
因此,在涉及 FLASH 用户数据存储的开发过程中,建议将下载配置的擦除方式设置为页擦除,以保护已存储的数据不被无意清除。若需要彻底清理 FLASH(包括用户数据),可临时切换为全擦除并下载一次,或使用 ST-LINK Utility 手动执行全擦除。
6.5 FLASH 操作期间 CPU 暂停的问题
在 FLASH 执行擦除或编程操作期间,闪存存储器接口会处于忙状态,FLASH_SR 寄存器的 BSY 位被置 1。在此状态下,CPU 对 FLASH 的任何读写访问都将被暂停,直到本次 FLASH 操作完成。由于程序的指令执行需要不断地从 FLASH 中读取代码,FLASH 的忙状态将直接导致 CPU 暂停运行,整个程序的执行被冻结。
这种暂停机制在实际应用中可能引发一个严重问题:若系统中存在对时间要求严格的中断服务函数(如定时器中断、串口接收中断、显示屏扫描刷新中断等),FLASH 操作期间这些中断将无法得到及时响应。中断服务函数本身存储在 FLASH 中,当 FLASH 处于忙状态时,CPU 无法从中断向量表获取中断服务函数的入口地址,也无法读取中断服务函数的指令代码,中断响应被硬件阻塞。虽然中断标志位在此期间仍然会被硬件置位,但 CPU 必须等到 FLASH 操作完成后才能转而执行中断服务函数,这可能导致中断响应出现不可接受的延迟。
举例而言,在一个需要定时器中断以恒定频率刷新 LED 点阵屏的项目中,若同时使用内部 FLASH 存储配置参数,每次执行 Store_Save() 触发擦除和编程操作时,点阵屏的扫描刷新中断可能被阻塞数毫秒,表现为屏幕出现可察觉的闪烁。擦除一页的时间在数十毫秒量级,对于刷新率要求较高的显示应用而言,这一延迟足以严重影响用户体验。
因此,在对实时性要求较高的应用中,使用内部 FLASH 存储数据需要充分评估这一风险。若程序包含以下特征,应慎用或避免使用内部 FLASH 存储用户数据:
- 存在高频中断(如 kHz 级别的定时器中断),且中断服务函数对执行时机有严格的时间偏差容忍度限制
- 存在通信类中断(如 USART、SPI、CAN 等),若中断响应延迟可能导致数据接收溢出或通信协议超时
- 存在需要持续刷新的人机交互外设(如 LED 点阵屏、TFT 液晶屏等),刷新中断的任何停顿都会在视觉上产生可感知的异常
对于此类场景,可考虑以下替代方案:将掉电不丢失的数据存储任务交由外部独立的 FLASH 或 EEPROM 芯片完成(通过 SPI 或 I2C 接口通信),这些外部芯片的操作不会阻塞 STM32 内部总线的指令读取,因此不影响中断响应;或者在系统处于空闲状态(无中断活跃、无实时任务执行)时才执行 FLASH 的擦除和编程操作,将对实时任务的干扰降至最低。
6.6 其他操作注意事项
除上述核心问题外,以下几点在编程实践中同样值得关注。
- 先擦除后写入原则:FLASH 编程必须遵循"先擦除后写入"的原则。向一个未经过擦除的地址写入非全 0 数据时,闪存控制器会拒绝执行并报出编程错误。虽然库函数内部会检查写入状态,但上层程序若未对返回值进行判断,可能无法察觉写入失败。在调试阶段可借助 ST-LINK Utility 直接检查目标地址的写入结果,确保操作正确执行。
- 编程数据宽度限制:FLASH 的编程仅支持 16 位半字写入,任何 8 位字节写入或非对齐的 32 位写入均会引发总线异常。若需要实现字节级的数据管理,应先在 SRAM 中建立整页的缓冲区,修改字节后在 SRAM 层面完成重组,再通过整页擦除和半字编程统一写回。
- 全擦除的风险:全擦除操作(FLASH_EraseAllPages())会清除整个程序存储器,包括正在运行的程序本身。除非有意为之(如实现安全销毁功能),否则在正常运行的程序中不应调用此函数。实验环节中对全擦除函数的测试也应以观察现象和原理验证为目的,测试后需要重新下载程序。
- 及时加锁:解锁后应及时加锁。解锁状态下若程序发生异常跳转并错误执行了擦除或编程相关代码,FLASH 内容可能被意外破坏。将擦除/编程操作与解锁/加锁封装在同一函数中,可以有效降低这一风险。
6.7 选项字节配置的风险
选项字节的配置如果使用代码实现,存在将芯片锁定的风险。例如,若代码中配置了某几页的写保护,但在后续升级或调试时未预留解除写保护的代码逻辑,芯片将陷入写保护状态,无法再通过正常编程流程擦除和重新写入。类似地,若使能了读保护(RDP 设为非 0xA5A5 的值),调试器将无法读取 FLASH 内容,程序下载和调试功能均被阻断。
由于选项字节在代码中配置的 "自锁" 特性,对于一般的配置需求(如读写保护设置),建议优先使用 STM32 ST-LINK Utility 的图形化配置界面完成。该工具通过调试器直接访问选项字节区域,不受程序存储器保护状态的影响。
若芯片因保护配置错误而无法下载程序,ST-LINK Utility 可打开 Option Bytes 界面,将读保护设为 Level 0、将写保护位全部清除,然后 Apply 完成恢复。仅当芯片被设置为 Level 2 不可逆保护时,芯片将永久锁定,无法通过任何方式恢复。因此,在开发和实验阶段应避免使用 Level 2 保护级别。
7 总结
通过本文的学习,我们系统地掌握了 STM32 内部 FLASH 的存储结构、操作原理和实际应用方法。从基本概念到硬件结构,从底层操作到辅助工具,再到完整的实验案例,整个知识体系循序渐进、环环相扣。
利用内部 FLASH 存储用户数据,不仅能够简化硬件设计、降低成本,还能为嵌入式系统提供可靠的掉电不丢失存储能力。在实际应用中,只要充分理解 FLASH 的操作约束,合理规划存储空间,注意避免常见的陷阱,就能够充分发挥这一特性的价值,为项目开发带来实实在在的便利。