云计算百科
云计算领域专业知识百科平台

【STM32】 内部 Flash 详解:寄存器逐位拆解 + 工作流程顺序全讲 + EEPROM再实现


  内部 Flash 详解:寄存器逐位拆解 + 工作流程顺序全讲  

一、基础概念(回顾)

1.1 什么是内部 Flash

Flash(闪存) 是 STM32 芯片内部的非易失性存储器:

  • 掉电不丢失:断电后数据依然保留;
  • 片内集成:同一颗芯片内,总线直接访问;
  • 可擦写:可反复擦除和编程(寿命约 1 万次)。

1.2 Flash vs SRAM vs EEPROM

对比项内部 FlashSRAMEEPROM(外部)
易失性 非易失 易失 非易失
访问速度 快(可取指执行) 最快 慢(I2C/SPI)
擦写单位 页/扇区擦除 无需擦除 按字节
擦写寿命 ~10,000 次 无限 ~1,000,000 次
主要用途 存代码、常量、数据 变量、栈、堆 小量掉电数据

1.3 内部 Flash 能干什么

  • 存代码(上电从 0x08000000 取指执行);
  • 存常量(const 数组、字符串);
  • 掉电保存数据(模拟 EEPROM);
  • OTA 升级(Bootloader 写新固件);
  • 配置芯片行为(选项字节)。

  • 二、Flash 内部结构

    2.1 整体布局(F103 为例)

    STM32F103 内部 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 读取时序(等待状态、预取缓冲)。

    位名称类型复位值作用
    bit[2: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 保留 — — —

    各标志的使用场景:

    while(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)

    作用:指定擦除的目标地址。

    位名称类型作用
    bit[31: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 掉电检测阈值等级
    bit[9:8] nBOOT1 R 启动模式配置位

    读这个寄存器就能知道芯片当前处于什么保护/启动状态,不用回读选项字节本身。

    3.8 FLASH_WRPR — 写保护寄存器(偏移 0x20)

    作用:标记哪些页被写保护(只读,实际配置要写选项字节区)。

    位名称作用
    bit[127: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 工作流程总览

    阶段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,再触发,硬件才知道接下来要做什么。

            反过来(先触发再设模式)无效。

    // 页擦除模式
    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 阶段五:触发操作——擦除与编程的触发方式不同

    擦除需要"按启动按钮",编程不需要。这是最容易混的点:

    // 擦除触发:写 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

    FLASH->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 页擦除

    FLASH_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 扇区擦除

    FLASH_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, &sector_error);
    HAL_FLASH_Lock();

    6.3 编程

    HAL_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 联动)

    IWDG_SW = 0:硬件看门狗 → 上电后 IWDG 自动启动,软件关不掉
    IWDG_SW = 1:软件看门狗 → 需软件写 KR=0xCCCC 启动

    7.3 读保护级别

    级别含义
    Level 0 无保护
    Level 1 禁止调试口读 Flash(SWD/JTAG)
    Level 2 永久锁定,无法降级(变砖)

    八、实战:掉电保存数据

    #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:

    void 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

    #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 驱动

    /* 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

    #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

    #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 存储单元是浮栅晶体管,阵列组织是"字线 × 位线":

    Flash 存储阵列(示意,每行 = 一次编程操作的单位)

    位线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:

            读取是低电压译码寻址,不改变电荷,可以任意寻址任意字节;

            写入需要升压、整行并行注入电荷,只能按行(编程单位)操作。


    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 【STM32】 内部 Flash 详解:寄存器逐位拆解 + 工作流程顺序全讲 + EEPROM再实现
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!