内部 Flash 详解:寄存器逐位拆解 + 工作流程顺序全讲
一、基础概念(回顾)
1.1 什么是内部 Flash
Flash(闪存) 是 STM32 芯片内部的非易失性存储器:
- 掉电不丢失:断电后数据依然保留;
- 片内集成:同一颗芯片内,总线直接访问;
- 可擦写:可反复擦除和编程(寿命约 1 万次)。
1.2 Flash vs SRAM vs EEPROM
对比项 内部 Flash SRAM EEPROM(外部) 易失性 非易失 易失 非易失 访问速度 快(可取指执行) 最快 慢(I2C/SPI) 擦写单位 页/扇区擦除 无需擦除 按字节 擦写寿命 ~10,000 次 无限 ~1,000,000 次 主要用途 存代码、常量、数据 变量、栈、堆 小量掉电数据 1.3 内部 Flash 能干什么
- 存代码(上电从 0x08000000 取指执行);
- 存常量(const 数组、字符串);
- 掉电保存数据(模拟 EEPROM);
- OTA 升级(Bootloader 写新固件);
- 配置芯片行为(选项字节)。
二、Flash 内部结构
2.1 整体布局(F103 为例)
cppSTM32F103 内部 Flash 总览 ┌─────────────────────────────────────────────┐ │ 主存储块(Main Memory) │ │ 0x0800_0000 ──► 页0 (1KB) │ │ 0x0800_0400 ──► 页1 (1KB) │ │ ... │ │ 0x0801_FC00 ──► 页127 (1KB) │ │ 共 128 页 × 1KB = 128KB │ ├─────────────────────────────────────────────┤ │ 系统存储器(System Memory) 2KB Bootloader │ │ 0x1FFF_F000 ~ 0x1FFF_F7FF │ ├─────────────────────────────────────────────┤ │ 选项字节(Option Bytes) 16B │ │ 0x1FFF_F800 ~ 0x1FFF_F80F │ ├─────────────────────────────────────────────┤ │ OTP 区域(一次性可编程) │ │ 0x1FFF_7800 ~ 0x1FFF_7A0F │ └─────────────────────────────────────────────┘2.2 F1 页式 vs F4 扇区式
系列 结构 擦除单位 编程单位 典型容量 F1(F103) 页式 1KB/页 半字(16bit) 64KB~512KB F4(F407) 扇区式 32KB/128KB/256KB 字(32bit)/双字 512KB~2MB F4 扇区:
Sector03 各 32KB、Sector4 128KB、Sector511 各 256KB(1MB 容量时用前 12 个扇区)。
三、寄存器详解(逐位拆解)
STM32F1 的 Flash 接口一共 8 个寄存器。本章是重点,逐个展开。
3.1 FLASH_ACR --- 访问控制寄存器(地址 0x4002_2000 + 0x00)
作用:配置 Flash 读取时序(等待状态、预取缓冲)。
位 名称 类型 复位值 作用 bit2:0 LATENCY RW 000 等待状态(见下表) bit3 PRFTBE RW 0 预取缓冲使能:1=开启,0=关闭 bit4 PRFTBS R 0 预取缓冲状态:1=缓冲已满 bit5 HLFCLK RW 0 半周期配置:1=半周期(仅 8MHz 以下且关闭预取时用) bit31:6 保留 --- --- --- LATENCY 等待状态表(F103,HCLK 频率决定):
HCLK 频率 电压 2.7V~3.6V 电压 2.4V~2.7V 0 < HCLK ≤ 24MHz 0 个等待 0 个等待 24MHz < HCLK ≤ 48MHz 1 个等待 1 个等待 48MHz < HCLK ≤ 72MHz 2 个等待 2 个等待 为什么需要等待状态:
Flash 读取速度是固定的(约 24MHz 一个周期),CPU 主频 72MHz 时 CPU 每 13.9ns 就要取一次指,Flash 跟不上,必须插入 2 个等待周期让数据稳定。
等待状态配错 = 程序随机跑飞。
3.2 FLASH_KEYR --- 密钥寄存器(偏移 0x04)
**作用:解锁 Flash 编程/擦除。**只写寄存器,读无意义。
写入值 作用 第一次写 0x45670123密钥序列第一部分 第二次写 0xCDEF89AB密钥序列第二部分,写对后解锁 解锁细节:
- 两把密钥必须按顺序连续写入,中间不能隔其他 Flash 操作;
- 写错任意一把:本次解锁失败,需要重新从第一把开始;
- 解锁成功后,FLASH_CR 的 LOCK 位自动清 0;
- 硬件复位或置 LOCK=1 后,Flash 重新上锁,需重新解锁。
3.3 FLASH_OPTKEYR --- 选项字节密钥寄存器(偏移 0x08)
作用:解锁选项字节的编程/擦除(写 OB 区域前必须先解锁)。
写入值 作用 第一次写 0x08192A3B选项字节密钥第一部分 第二次写 0x4C5D6E7F选项字节密钥第二部分 和 KEYR 同样的机制:
两把密钥、按序写、写错重来。
只有要修改选项字节(读保护/写保护/硬件狗配置)时才用这个寄存器。
3.4 FLASH_SR --- 状态寄存器(偏移 0x0C)
作用:查询 Flash 操作状态。这是编程时查得最多的寄存器。
位 名称 类型 复位值 作用 bit0 BSY R 0 忙标志:1=正在擦除/编程,任何新操作前必须等它=0 bit2 PGERR RW0 0 编程错误:1=编程操作失败(未擦除/地址未对齐/越界),写 1 清除 bit4 WRPRTERR RW0 0 写保护错误:1=试图写被保护的页,写 1 清除 bit5 EOP RW0 1 操作完成:1=擦除/编程成功结束,写 1 清除 bit1,3,6,7 保留 --- --- --- 各标志的使用场景:
cppwhile(FLASH->SR & FLASH_SR_BSY); // ① 操作前:等上一次忙完 // ... 发起擦除/编程 ... while(FLASH->SR & FLASH_SR_BSY); // ② 操作后:等本次完成 if(FLASH->SR & FLASH_SR_PGERR) { // ③ 查编程错误 // 处理:一般是没擦除就写 / 地址没对齐 } if(FLASH->SR & FLASH_SR_WRPRTERR) { // ④ 查写保护错误 // 处理:目标页被选项字节写保护 } FLASH->SR |= FLASH_SR_EOP; // ⑤ 清除完成标志(写 1 清 0)为什么 EOP 复位值是 1:
芯片设计时让"未发生过操作"也表现为完成,方便读取。
真正判断"本次操作完成"要看 BSY 从 1 变 0,EOP 只作为可选的完成确认。
3.5 FLASH_CR --- 控制寄存器(偏移 0x10)
作用:控制 Flash 的模式(编程/擦除/选项字节)和启动。这是最核心的寄存器。
位 名称 类型 复位值 作用 bit0 PG RW 0 编程使能:1=进入编程模式(置位后写地址才触发编程) bit1 PER RW 0 页擦除****使能:1=进入页擦除模式 bit2 MER RW 0 整片擦除****使能:1=进入全片擦除模式(危险!) bit3 OPTPG RW 0 选项字节编程使能 bit4 OPTER RW 0 选项字节擦除使能 bit5 STRT RW 0 启动擦除:写 1 启动当前模式的擦除(仅擦除需要,编程不需要) bit6 LOCK RW 1 **锁定:1=Flash 锁定;写 1 上锁,**只能由硬件复位或成功解锁清 0 bit7 OPTWRE RW 0 选项字节写使能:1=允许写选项字节(解锁 OPTKEYR 后自动置 1) CR 的关键设计:
- 模式位(PG/PER/MER)是"预设置"------先选好要干什么,再触发;
- STRT 只对擦除有效------擦除需要"按启动按钮",编程不需要(写数据动作本身触发);
- LOCK 只能写 1 不能写 0------上锁容易,解锁必须走密钥流程,防误操作。
3.6 FLASH_AR --- 地址寄存器(偏移 0x14)
作用:指定擦除的目标地址。
位 名称 类型 作用 bit31:0 FAR RW 页擦除时存放目标页地址 只有页擦除用到 AR;
整片擦除(MER)不需要地址。
编程也不通过 AR------编程地址就是你要写数据的那个地址本身(直接对 Flash 地址写)。
3.7 FLASH_OBR --- 选项字节状态寄存器(偏移 0x1C)
作用:只读,反映当前选项字节的配置状态。
位 名称 类型 作用 bit0 OPTERR R 选项字节错误:1=选项字节校验失败 bit1 RDPRT R 读保护级别:00=Level 0,01=Level 1,10/11=Level 2 bit2 IWDG_SW R 看门狗配置:1=软件看门狗,0=硬件看门狗 bit4:3 BOR_LEV R 掉电检测阈值等级 bit9:8 nBOOT1 R 启动模式配置位 读这个寄存器就能知道芯片当前处于什么保护/启动状态,不用回读选项字节本身。
3.8 FLASH_WRPR --- 写保护寄存器(偏移 0x20)
作用:标记哪些页被写保护(只读,实际配置要写选项字节区)。
位 名称 作用 bit127:0 WRP 每一位对应 4 页(F103 中每 bit 保护 4 个 1KB 页),1=该组被写保护 被写保护的页无法擦除和编程,但可以正常读取。
用于保护 Bootloader 等重要区域不被误写。
四、Flash 三大特性
4.1 特性一:只能写 0,不能写 1
Flash 位状态: 1(未编程)──编程──► 0(已编程) 0(已编程)──擦除──► 1(未编程)4.2 特性二:擦除是"按块"的
- 擦除把整页/整扇区恢复成全 1(0xFF);
- 不能按字节擦除;
- 修改数据三步曲:① 读出到 SRAM ② 擦整页 ③ 重写。
4.3 特性三:写前必须擦除,且要等 BSY
- 每次操作前必须查 FLASH_SR.BSY=0;
- 编程单位:F1 半字(16bit)、F4 字(32bit)/双字。
五、Flash 工作流程顺序(重点,逐步拆解)
核心总序:
解锁 → 等待空闲 → 配置模式 → 指定地址 → 触发操作 → 等待完成 → 清除标志 → 退出模式 → 加锁。
下面按"为什么是这个顺序"逐阶段讲。
5.0 工作流程总览
cpp阶段1 解锁 FLASH->KEYR 连写两把密钥 │ (必须先于一切操作,否则 CR 被锁,操作无效) ▼ 阶段2 等空闲 while(SR.BSY) ← 每次都查 │ ▼ 阶段3 配置模式 CR 置 PG / PER / MER(先选模式) │ ▼ 阶段4 指定地址 AR = 目标页(仅页擦除需要) │ ▼ 阶段5 触发操作 擦除:置 STRT=1 启动 │ 编程:直接向 Flash 地址写数据(写动作即触发) ▼ 阶段6 等完成 while(SR.BSY) ← 操作期间 CPU 停在这 │ ▼ 阶段7 清标志 SR 写 1 清 EOP(及错误标志) │ ▼ 阶段8 退出模式 CR 清 PG / PER(防止误触发) │ ▼ 阶段9 加锁 CR 置 LOCK=1(防跑飞误擦写)5.1 阶段一:解锁------为什么必须先做?
顺序原因:
FLASH_CR 的 LOCK 位复位值为 1,Flash 默认处于锁定状态。
锁定时,CR 的所有模式位(PG/PER/MER)写不进,一切操作无效。
FLASH->KEYR = 0x45670123; // 密钥 1 FLASH->KEYR = 0xCDEF89AB; // 密钥 2 → 硬件校验通过 → LOCK 清 0细节:
- 两把密钥必须连续、按序写入,中间不能插入其他 Flash 相关操作;
- 写错任意一把:解锁失败,CR 保持锁定,必须从密钥 1 重来;
- 解锁只解除"这一次"的锁------如果之后你置 LOCK=1 或发生复位,需要重新解锁。
为什么设计成两把密钥:
跑飞后 PC 随机写寄存器,连续写对两个 32 位特定值(0x45670123 和 0xCDEF89AB)的概率为 1/2^64,几乎不可能误解锁------防止程序失控后把自己的代码擦没。
5.2 阶段二:等待空闲------每次操作前后都要查
顺序原因:
Flash 是单接口设备,同一时间只能做一个操作。
上一次擦除/编程没结束(BSY=1)时,发起新操作会导致本次操作无效甚至错误。
while(FLASH->SR & FLASH_SR_BSY); // 忙则死等细节:
- BSY 由硬件在操作开始置 1、结束清 0;
- 解锁、加锁不需要等 BSY(它们不访问存储阵列);
- 擦除、编程前后都要查:前面等上一次完成,后面等本次完成。
5.3 阶段三:配置模式------先选模式再触发
顺序原因:
CR 是"预设置寄存器"。
必须先把模式位(PG/PER/MER)置 1,再触发,硬件才知道接下来要做什么。
反过来(先触发再设模式)无效。
cs// 页擦除模式 FLASH->CR |= FLASH_CR_PER; // 选页擦除 // 整片擦除模式 FLASH->CR |= FLASH_CR_MER; // 选全片擦除(危险) // 编程模式 FLASH->CR |= FLASH_CR_PG; // 选编程细节:
- PG、PER、MER 互斥使用------一次只能选一个模式,做完要清掉再换下一个;
- 选完模式后,硬件在下一次触发/写数据时按模式执行;
- 模式位不清除的话,后续任何写 Flash 地址的操作都会被当成编程/擦除触发------用完必须清。
5.4 阶段四:指定地址------只有页擦除需要 AR
顺序原因:擦除需要知道"擦哪一页",地址先给硬件,触发后才执行。
FLASH->AR = page_addr; // 页擦除目标地址(必须按 1KB 页对齐)细节:
- 页擦除:必须写 AR,且地址要页对齐(F1 按 1KB 对齐,即地址低 10 位为 0);
- 整片擦除(MER):不需要 AR;
- 编程:不需要 AR------编程目标地址就是你写数据的那个地址(
*(volatile uint16_t*)addr = data)。5.5 阶段五:触发操作------擦除与编程的触发方式不同
擦除需要"按启动按钮",编程不需要。这是最容易混的点:
cpp// 擦除触发:写 STRT=1 FLASH->CR |= FLASH_CR_STRT; // 编程触发:直接向 Flash 地址写数据,写动作本身即触发 *(volatile uint16_t*)addr = data;为什么编程不用 STRT:
编程的触发条件是"向 Flash 地址执行一次写总线操作"。
硬件看到 PG=1 且出现 Flash 写访问,就自动开始编程------不需要额外的启动位。
而擦除没有"写地址"这个动作(擦除是整页操作),必须用 STRT 手动启动。
5.6 阶段六:等待完成------操作期间 CPU 只能等
while(FLASH->SR & FLASH_SR_BSY);细节:
- 擦除 1KB 页:典型耗时 20~40ms;
- 编程 1 个半字:典型耗时 40~50µs;
- 这段时间内不要再发起任何 Flash 操作;
- 擦除期间(F1)不能从被擦除的 Flash 执行代码------因为 Flash 接口正忙,取指会失败。擦除/编程函数本身要放在 SRAM 里跑,或者保证代码不在被操作的页上。
5.7 阶段七:清除标志------写 1 清 0
cppFLASH->SR |= FLASH_SR_EOP; // 清除完成标志 FLASH->SR |= FLASH_SR_PGERR; // 若有编程错误,清除(也便于下次判断) FLASH->SR |= FLASH_SR_WRPRTERR; // 若有写保护错误,清除为什么必须清:
EOP、PGERR、WRPRTERR 是粘性标志(置 1 后保持)。
不清除的话,下次判断会误以为"出错"或"完成"。
写 1 清 0,写 0 无效。
5.8 阶段八:退出模式------用完必清
FLASH->CR &= ~FLASH_CR_PER; // 退出页擦除模式 FLASH->CR &= ~FLASH_CR_PG; // 退出编程模式为什么必须退:
模式位不清,之后任何一次误写 Flash 地址都会触发编程/擦除,可能毁掉数据。
用完即清,是 Flash 驱动的基本素养。
5.9 阶段九:加锁------最后一道保险
FLASH->CR |= FLASH_CR_LOCK;为什么最后加锁:
加锁后所有模式位写不进去,Flash 回到"只读"状态。
此时即使程序跑飞乱写寄存器,也无法触发擦除/编程------把"能破坏 Flash"的窗口缩到最小。
5.10 完整带错误处理的工作流程代码(F1 寄存器版)
六、HAL 库操作(对照)
6.1 F1 页擦除
cppFLASH_EraseInitTypeDef erase; uint32_t page_error = 0; HAL_FLASH_Unlock(); // ① 解锁(内部帮你写了 KEYR 两密钥) erase.TypeErase = FLASH_TYPEERASE_PAGES; erase.PageAddress = 0x08080000; erase.NbPages = 1; HAL_FLASHEx_Erase(&erase, &page_error); // ②③④⑤ 擦除(内部完成全部流程) HAL_FLASH_Lock(); // ⑥ 加锁6.2 F4 扇区擦除
cppFLASH_EraseInitTypeDef erase; uint32_t sector_error = 0; HAL_FLASH_Unlock(); erase.TypeErase = FLASH_TYPEERASE_SECTORS; erase.Sector = FLASH_SECTOR_6; erase.NbSectors = 1; erase.VoltageRange = FLASH_VOLTAGE_RANGE_3; HAL_FLASHEx_Erase(&erase, §or_error); HAL_FLASH_Lock();6.3 编程
cppHAL_FLASH_Unlock(); HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, 0x08080000, 0x1234); // F1 // HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, 0x08080000, 0x12345678); // F4 HAL_FLASH_Lock();HAL 库帮你包好了"等 BSY、清 EOP、模式切换"这些脏活,但内部流程和我们手写的完全一致。
七、选项字节(Option Bytes)
7.1 配置项总表
配置项 作用 RDP(读保护) 防调试器/程序读出 Flash WRP(写保护) 防误擦写指定页 IWDG_SW 软件看门狗 / 硬件看门狗 BOR 级别 掉电检测阈值 nBOOT1 启动模式配置 7.2 硬件看门狗(与 IWDG 联动)
cppIWDG_SW = 0:硬件看门狗 → 上电后 IWDG 自动启动,软件关不掉 IWDG_SW = 1:软件看门狗 → 需软件写 KR=0xCCCC 启动7.3 读保护级别
级别 含义 Level 0 无保护 Level 1 禁止调试口读 Flash(SWD/JTAG) Level 2 永久锁定,无法降级(变砖)
八、实战:掉电保存数据
cpp#define SAVE_ADDR 0x08080000 // 保存区(Flash 最后 1KB 页,避开程序) uint16_t Flash_ReadData(void) { return *(volatile uint16_t*)SAVE_ADDR; // 读(无需解锁,读不受锁影响) } void Flash_SaveData(uint16_t data) { // 完整 9 阶段流程(见 5.10 完整代码) Flash_WriteHalfWord(SAVE_ADDR, data); } int main(void) { uint16_t counter = Flash_ReadData(); counter++; // ... 业务 ... Flash_SaveData(counter); // 掉电前保存 }注意:保存区必须编译链接时排除(改分散加载/链接脚本),否则程序代码会写进这一页。
九、工程坑位总结
坑 原因 解决 写前不擦除 → 数据不对 Flash 只能 1→0 必须先擦整页 擦除时卡死 Flash 接口忙 + 不能执行 Flash 内代码 等 BSY;擦写函数放 SRAM 中断里操作 Flash 擦写时间长,中断打断 关中断或避开 WRPERR 目标区被 WRP 写保护 / 未解锁 检查选项字节 PGERR 未擦除 / 地址未对齐 先擦后写、按半字/字对齐 模式位没清 之后误写地址触发编程 用完即清 PG/PER Flash 越用越慢/坏 寿命 ~1 万次 模拟 EEPROM 磨损均衡 调试改了 Flash SWD 直接写 生产前设读保护
面试题(30 道,答案同页)
一、基础概念(原 20 道保留)
Q1. Flash 和 SRAM 的本质区别?
A:
Flash 非易失(掉电保留),存代码和长期数据;
SRAM 易失,存运行变量。
Flash 需擦除才能重写且寿命有限。
Q2. 为什么 Flash 写之前必须擦除?
A:
编程只能把位从 1 变成 0,不能 0→1。
只有擦除能把整页恢复成全 1,所以修改已为 0 的位必须先擦除。
Q3. F1 和 F4 的 Flash 擦除单位区别?
A:
F1 页式 1KB/页;F4 扇区式 32KB/128KB/256KB。
F4 改一个字节也得擦整个扇区。
Q4. 解锁 Flash 为什么要写两把密钥?
A:
防止程序跑飞后误擦 Flash。
连续写对 0x45670123 和 0xCDEF89AB 概率极低,几乎不可能误解锁。
Q5. FLASH_SR 有哪些标志位?
A:
BSY(忙)、PGERR(编程错误)、WRPRTERR(写保护错误)、EOP(完成)。操作前查 BSY,出错查 PGERR/WRPRTERR,完成后清 EOP。
Q6. 擦除和编程的操作流程?
A:
解锁→等 BSY=0→设 CR 模式(PER/PG)→设 AR→触发(擦除写 STRT,编程直接写地址)→等 BSY=0→清 EOP→退出模式→加锁。
Q7. 擦除/编程时为什么不能执行 Flash 内代码?
A:
操作期间 Flash 接口被占用,无法响应取指。
F1 从 Flash 取指会停顿卡死。
解决:擦写函数放 SRAM 执行。
Q8. 什么是选项字节?能配置什么?
A:
Flash 独立小区域,配置硬件行为:
RDP 读保护、WRP 写保护、IWDG_SW 硬件看门狗、BOR 阈值、启动模式。
上电自动读取。
Q9. 硬件看门狗选项是什么意思?
A:
IWDG_SW=0 时 IWDG 上电自动启动,无需软件写 0xCCCC,软件也关不掉。
保证代码没跑起来狗已在监控。
Q10. 读保护 RDP 的三个级别?
A:
Level 0 无保护;
Level 1 禁止调试口读 Flash;
Level 2 永久锁定无法降级(变砖)。
Q11. 模拟 EEPROM 是什么?为什么需要?
A:
用 Flash 模拟按字节写的 EEPROM。
因 Flash 擦除单位大、寿命短,通过"写满一页才擦"、磨损均衡、日志式写入延长寿命。
Q12. Flash 擦写寿命多少?
A:
典型 10,000 次。
频繁擦写区域要设计磨损均衡。
Q13. 掉电保存数据的一般步骤?
A:
① 规划程序用不到的页;
② 保存:读旧值→擦整页→写新值;
③ 上电从固定地址读回。
Q14. 为什么写 0xFF 等于"无操作"?
A:
0xFF 全位为 1,编程只能 1→0。
写 0xFF 不改变任何位,所以对全 1 区域写无效,必须先擦除。
Q15. 编程地址为什么要对齐?
A:
F1 按半字(16bit)对齐,F4 按字(32bit)。
未对齐触发 PGERR。
Q16. 预取缓冲和等待状态是什么?
A:
CPU 主频高时 Flash 读取跟不上,需插等待周期(WS)。
预取缓冲提前取指减少等待。
ACR 配置。
Q17. 程序能直接从 Flash 执行吗?
A:
能。
Flash 在统一编址空间(0x08000000),CPU 经 ICode 总线直接取指(XIP)。
比外部 SPI Flash 快。
Q18. OTP 区域是什么?
A:
一次性可编程区,只能写一次不可擦除。存序列号、MAC、密钥。
Q19. 手撕:解锁和加锁。
A:
cppvoid Flash_Unlock(void) { FLASH->KEYR = 0x45670123; FLASH->KEYR = 0xCDEF89AB; } void Flash_Lock(void) { FLASH->CR |= FLASH_CR_LOCK; }二、寄存器专题
Q21. FLASH_ACR 的 LATENCY 是干什么的?F103 在 72MHz 下应设多少?
A:
Flash 读取等待状态。
CPU 主频高过 Flash 速度时插入等待周期。
F103 在 72MHz(2.7~3.6V)下应设 2 个等待。
设错会导致取指错乱、程序跑飞。
Q22. FLASH_CR 的 PG、PER、MER 分别是什么?能同时置位吗?
A:
PG=编程模式、PER=页擦除模式、MER=整片擦除模式。
三个互斥,一次只能选一个,用完必须清除。
Q23. STRT 位是什么?为什么编程不需要它?
A:
STRT 是擦除启动位,写 1 启动当前模式的擦除。
编程不需要,因为编程的触发条件是"向 Flash 地址写数据"这个总线动作本身。
Q24. LOCK 位为什么只能写 1 不能写 0?
A:
上锁容易,解锁必须走 KEYR 两密钥流程。
防止程序跑飞后直接写 LOCK=0 解锁然后乱擦写。
Q25. FLASH_AR 什么时候用?什么时候不用?
A:
页擦除时用(指定目标页,需页对齐);
整片擦除和编程不用------整片擦除不需要地址,编程地址就是写数据的地址本身。
Q26. EOP 标志为什么复位值是 1?
A:
芯片设计让"未发生操作"也表现为完成。
真正判断操作完成要看 BSY 从 1 变 0,EOP 只作可选确认。
用完写 1 清除。
Q27. OPTKEYR 的密钥是什么?什么时候用?
A:
0x08192A3B 和 0x4C5D6E7F。
修改选项字节(读保护、写保护、硬件看门狗)前必须先解锁 OPTKEYR。
Q28. OBR 寄存器能读出什么?
A:
只读,反映当前选项字节状态:OPTERR 校验错误、RDPRT 读保护级别、IWDG_SW 看门狗模式、BOR_LEV 掉电阈值、nBOOT1 启动配置。
Q29. WRPR 写保护寄存器和选项字节 WRP 什么关系?
A:
WRPR 是只读反映位,真正的写保护配置在选项字节里。
WRPR 每一位对应 4 页(F1),1=该组被写保护,不可擦写但可读。
Q30. 手撕:说出 Flash 解锁失败后如何恢复。
A:
解锁失败(密钥写错)不会损坏 Flash,只是保持锁定。
只需重新按顺序写两把密钥即可,不需要复位。
若操作中产生 PGERR/WRPRTERR,先清标志再重新走流程。
完整实现EEPROM(F103 寄存器版)
头文件 eeprom_sim.h
cpp#ifndef __EEPROM_SIM_H #define __EEPROM_SIM_H #include <stdint.h> /* 参数配置 */ #define EEPROM_PAGE_SIZE 1024 /* F103 一页 1KB */ #define EEPROM_PAGE0 0x08080000 /* 页0(避开程序区,需链接排除) */ #define EEPROM_PAGE1 0x08080400 /* 页1 */ #define EEPROM_SLOT_SIZE 4 /* 每条记录 4 字节:数据(2B)+状态(2B) */ #define EEPROM_SLOTS (EEPROM_PAGE_SIZE / EEPROM_SLOT_SIZE) /* 每页 256 槽 */ /* 状态字节 */ #define STATUS_EMPTY 0xFFFF /* 空槽(擦除态) */ #define STATUS_VALID 0xAAAA /* 有效记录 */ #define STATUS_INVALID 0x0000 /* 无效记录 */ /* 对外接口 */ void EEPROM_Init(void); uint16_t EEPROM_Read(uint16_t default_val); void EEPROM_Write(uint16_t data); #endif底层 Flash 驱动
cpp/* Flash 底层:解锁、擦页、写半字 */ static void Flash_Unlock(void) { FLASH->KEYR = 0x45670123; FLASH->KEYR = 0xCDEF89AB; } static void Flash_Lock(void) { FLASH->CR |= FLASH_CR_LOCK; } /* 擦除一页 */ static void 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->SR |= FLASH_SR_EOP; FLASH->CR &= ~FLASH_CR_PER; /* 退出模式 */ } /* 编程一个半字 */ static void Flash_ProgramHalfWord(uint32_t addr, uint16_t data) { while(FLASH->SR & FLASH_SR_BSY); FLASH->CR |= FLASH_CR_PG; /* 编程模式 */ *(volatile uint16_t*)addr = data; /* 写数据即触发 */ while(FLASH->SR & FLASH_SR_BSY); FLASH->SR |= FLASH_SR_EOP; FLASH->CR &= ~FLASH_CR_PG; /* 退出模式 */ }模拟 EEPROM 核心实现 eeprom_sim.c
cpp#include "eeprom_sim.h" #include "stm32f10x.h" /* 按你的工程改头文件 */ static uint32_t g_cur_page; /* 当前写页 */ static uint16_t g_last_valid; /* 缓存最新值(加速读取 + 翻页用) */ /* ---------- 槽位访问辅助 ---------- */ static uint16_t slot_data(uint32_t page, uint16_t idx) { return *(volatile uint16_t*)(page + idx * EEPROM_SLOT_SIZE); } static uint16_t slot_status(uint32_t page, uint16_t idx) { return *(volatile uint16_t*)(page + idx * EEPROM_SLOT_SIZE + 2); } /* 写槽位数据(此时状态还是空,读不到) */ static void slot_write_data(uint32_t page, uint16_t idx, uint16_t data) { Flash_ProgramHalfWord(page + idx * EEPROM_SLOT_SIZE, data); } /* 数据写完后,把状态 0xFFFF → 0xAAAA(标记有效,1→0 无需擦除) */ static void slot_mark_valid(uint32_t page, uint16_t idx) { Flash_ProgramHalfWord(page + idx * EEPROM_SLOT_SIZE + 2, STATUS_VALID); } /* ---------- 核心查询:从页尾向前找最后一条有效记录 ---------- */ static int find_last_valid(uint32_t page, uint16_t *idx, uint16_t *data) { int i; for (i = EEPROM_SLOTS - 1; i >= 0; i--) { if (slot_status(page, i) == STATUS_VALID) { *idx = (uint16_t)i; *data = slot_data(page, i); return 1; } } return 0; /* 页里没有有效记录 */ } /* 找第一个空槽;页满返回 -1 */ static int find_first_empty(uint32_t page) { int i; for (i = 0; i < EEPROM_SLOTS; i++) { if (slot_status(page, i) == STATUS_EMPTY) return i; } return -1; } /* ---------- 初始化:扫描两页,确定当前写页和最新值 ---------- */ void EEPROM_Init(void) { uint16_t d0, d1, i0, i1; int f0 = find_last_valid(EEPROM_PAGE0, &i0, &d0); int f1 = find_last_valid(EEPROM_PAGE1, &i1, &d1); if (!f0 && !f1) { /* 全新芯片:两页都空,用页0 */ g_cur_page = EEPROM_PAGE0; g_last_valid = 0; } else if (f0 && !f1) { g_cur_page = EEPROM_PAGE0; g_last_valid = d0; } else if (!f0 && f1) { g_cur_page = EEPROM_PAGE1; g_last_valid = d1; } else { /* 两页都有有效记录:取"槽序号更大"的一页(更新的日志) */ if (i0 >= i1) { g_cur_page = EEPROM_PAGE0; g_last_valid = d0; } else { g_cur_page = EEPROM_PAGE1; g_last_valid = d1; } } } /* ---------- 读取:优先当前页,找不到再查另一页 ---------- */ uint16_t EEPROM_Read(uint16_t default_val) { uint16_t data, idx; if (find_last_valid(g_cur_page, &idx, &data)) return data; /* 当前页无有效记录(可能上次翻页途中掉电),查另一页 */ { uint32_t other = (g_cur_page == EEPROM_PAGE0) ? EEPROM_PAGE1 : EEPROM_PAGE0; if (find_last_valid(other, &idx, &data)) return data; } return default_val; /* 从未写过 */ } /* ---------- 写入 ---------- */ void EEPROM_Write(uint16_t data) { int idx = find_first_empty(g_cur_page); /* 找当前页空槽 */ if (idx < 0) { /* 页满 → 页翻转 */ uint32_t other = (g_cur_page == EEPROM_PAGE0) ? EEPROM_PAGE1 : EEPROM_PAGE0; /* ① 先擦备用页 */ Flash_Unlock(); Flash_ErasePage(other); Flash_Lock(); /* ② 把最新值写入新页槽0(先数据后状态,掉电安全) */ Flash_Unlock(); slot_write_data(other, 0, g_last_valid); slot_mark_valid(other, 0); Flash_Lock(); /* ③ 再擦旧页(此时最新值已在 other 页,旧页可安全擦除) */ Flash_Unlock(); Flash_ErasePage(g_cur_page); Flash_Lock(); /* ④ 切换当前写页,新页槽0已被占用,下一个空槽是槽1 */ g_cur_page = other; idx = 1; } /* 写入新记录:先写数据,再标有效 */ Flash_Unlock(); slot_write_data(g_cur_page, (uint16_t)idx, data); slot_mark_valid(g_cur_page, (uint16_t)idx); Flash_Lock(); g_last_valid = data; /* 更新缓存 */ }测试 main.c
cpp#include "eeprom_sim.h" #include <stdio.h> int main(void) { uint16_t boot_count; EEPROM_Init(); /* ① 初始化(扫描两页) */ boot_count = EEPROM_Read(0); /* ② 读回上次的计数值 */ boot_count++; /* ③ 本次开机 +1 */ printf("boot count: %d\n", boot_count); EEPROM_Write(boot_count); /* ④ 保存(写一个新槽) */ while(1); }
为什么 Flash 有半字、单字、双字写入
一句话答案:
"最小写入单位"由 Flash 存储阵列的物理结构决定------编程不是"写一个字节",而是"沿一行字线、给整行单元并行注入电荷"。
这行有多宽,最小编程单位就是多宽。
F1 的编程行是 16bit,所以只能半字写;
F4 的行是 32bit,所以是字写;F7/H7 的行更宽,支持 64bit 双字。
一、先分清三个粒度
操作 本质 F1 (F103) F4 (F407) 读取 译码寻址,读任意字节 按字节读 按字节读 编程(写) 向浮栅注入电荷 半字 16bit 字 32bit(大容量型号支持双字 64bit) 擦除 从浮栅抽取电荷 整页 1KB 整扇区 32KB+ 读可以按字节,写不行,擦除更是一整块------因为三者是完全不同的物理过程。
二、编程最小单位的物理本质
Flash 存储单元是浮栅晶体管,阵列组织是"字线 × 位线":
cppFlash 存储阵列(示意,每行 = 一次编程操作的单位) 位线0 位线1 位线2 ... 位线15 ← 一"行"共 16 条位线(F1 半字) │ │ │ │ 字线0 ──┼─[单元]─┼─[单元]─┼──[单元]──┼── ──► 编程时整行一起注入电荷 字线1 ──┼─[单元]─┼─[单元]─┼──[单元]──┼── │ │ │ │ │关键机制:
- 编程 = 在字线上加高压,同时把数据锁存到这一行的**编程缓冲器(write buffer/latch)**里;
- 缓冲器里是 1 的位不注入电荷(保持浮栅空),是 0 的位注入电荷(浮栅充电 → 阈值电压升高 → 读出为 0);
- 整行单元是一次性并行操作的------硬件只提供"一行"的电荷注入通路,这就是最小编程单位;
- 这一行连了多少条位线,最小单位就是多少 bit。
三、为什么不同系列不一样宽
因素 F1 (F103) F4 (F407) F7/H7 内核 Cortex-M3(32bit) Cortex-M4(32bit) Cortex-M7(双发射,取指 64bit) 制造工艺 较老(90nm 级) 较新(65nm 级) 更新(40nm 级) 编程最小单位 半字 16bit 字 32bit(低容量)/ 双字 64bit(高容量) 双字 64bit(部分 256bit) 擦除单位 页 1KB 扇区 32KB+ 扇区 32KB/64KB/128KB 具体原因:
- 总线与内核宽度:CPU 数据总线是 32bit,F1 时代 Flash 接口按 16bit 组织(半字),一次写操作对应一条物理编程行;F4 把编程行加宽到 32bit,与总线对齐;M7 内核取指是 64bit,Flash 接口随之加宽到 64bit------接口越宽,编程行越宽,最小单位越大。
- 工艺与面积成本:编程行加宽意味着同一时刻并行注入电荷的单元更多,需要更大的电荷泵(charge pump)输出电流、更粗的电源/地走线、更大的存储单元面积。老工艺做不了太宽的编程行,新工艺可以。
- 性能权衡:行越宽,一次编程写入的数据越多(编程时间几乎不变),写入吞吐率越高。F7 双字写入比 F1 半字写入快 4 倍。
四、为什么不能按字节写?(物理限制)
**你可能想问:**为什么 EEPROM 能按字节写,Flash 不行?
- 编程干扰(Program Disturb):给一行加高压时,同一字线上的其他单元也会受到应力,可能被误编程。行越窄,受影响的邻单元越少。Flash 追求密度,用"整行并行"来换取面积,代价就是最小单位大。
- 电荷泵能力:按字节写要单独给一个单元加高压,而 Flash 单元的写入电压要靠片内电荷泵从 1.8V/3.3V 升到 ~10V。一次驱动整行的电流预算固定,并行写整行比逐个写字节更高效(总时间更短)。
- 架构继承:NOR Flash 最初就是按"行(word line)"组织的,半导体厂的 Flash IP 行宽固定,MCU 只是把这一行宽暴露给用户:F1 暴露 16bit、F4 暴露 32bit。
外部 I2C EEPROM 按字节写,是因为它的存储密度低、单元大、工艺容忍逐字节操作------代价是容量小(Kbit 级)、速度慢。
内部 Flash 追求高密度大容量,就得接受"整行写、整块擦"的物理约束。
五、对开发者的实际影响
影响点 说明 地址对齐 F1 半字写必须 2 字节对齐;F4 字写必须 4 字节对齐;未对齐 → PGERR 模拟 EEPROM 槽位设计 槽位宽度必须 ≥ 编程单位:F1 槽位 4B(2B 数据+2B 状态)没问题;F4 上要按 4/8B 设计,状态字段最好用完整字 写数组/结构体 不能直接 memcpy到 Flash------必须按半字/字逐个编程跨行边界 一个"数据项"不能横跨两个编程行边界,否则要拆成两次操作
面试题
Q1. F1 和 F4 的 Flash 编程最小单位分别是什么?
A:
F1 半字 16bit(2 字节对齐),F4 字 32bit(4 字节对齐),大容量 F4 及 F7 支持双字 64bit。
Q2. 为什么编程单位是半字/字而不是字节?
A:
编程是物理上"整行并行注入电荷",行宽由存储阵列的位线数量决定。
Flash 以行组织追求密度,最小编程单位 = 行宽,无法像 EEPROM 那样逐字节操作。
Q3. 编程单位跟什么硬件有关?
A:
内核总线宽度(M3 32bit / M7 64bit)、制造工艺(新工艺能支撑更宽的编程行和更大的电荷泵)、Flash IP 的存储阵列组织。
Q4. 未对齐写会怎样?
A:
触发编程错误标志 PGERR,数据不写入。
F1 写 3 字节对齐地址、F4 写 2 字节对齐地址都会报错。
Q5. F4 上做模拟 EEPROM 要注意什么?
A:
槽位按 4/8 字节对齐设计;
扇区最小 32KB,擦除代价大,两页翻转要占 64KB;
编程单位是字,状态字段也要用完整字写入。
Q6. 为什么读取可以按字节而写入不行?
A:
读取是低电压译码寻址,不改变电荷,可以任意寻址任意字节;
写入需要升压、整行并行注入电荷,只能按行(编程单位)操作。
【STM32】 内部 Flash 详解:寄存器逐位拆解 + 工作流程顺序全讲 + EEPROM再实现
Brilliantwxx2026-10-31 18:29
相关推荐
木子单片机1 小时前
基于单片机的DS18B20温度传感器测温系统设计华清远见IT开放实验室1 小时前
实验室建设案例 | 安徽三联学院物联网实验室一只鹿鹿鹿2 小时前
智算算力、AI-芯片与产品解析(PPT文件)Zw-awa2 小时前
串口为什么会丢字节:从轮询到中断和环形缓冲区xwz小王子2 小时前
Science Advances 让机器人触觉在重压下继续分辨微小变化小小AK3 小时前
聚水潭售后单到金蝶退货单同步实战:从增量拉取到目标单据落库的全流程拆解深圳光特通信豆子3 小时前
TTL 光模块在工业控制与串口通信中的应用指南(10M BIDI 单模系列)桀人3 小时前
SPI概念引入离凌寒4 小时前
一、基于stm32+freertos下LD2410D和R60ADB1两款雷达的使用效果




