前言

编程手册下载地址:STM32F10xxx闪存编程手册中文:基于 STM32F10xxx 的闪存编程中文手册项目 - AtomGit

闪存存储器即Flash,在 STM32 微控制器中,Flash 存储器承担着以下核心作用:

  1. 存储程序代码:保存编译后的固件,确保每次复位后程序能够正常运行。
  2. 保存配置数据:用于持久化存储系统参数。
  3. 支持固件更新:用于空中固件更新(FOTA)以及存储引导加载程序(Bootloader)。

手册里也有提到STM32F10xxx内嵌的闪存存储器可以用于在线编程(ICP)或在程序中编程(IAP)烧写。当然,对于容量不大的芯片,通常使用外部存储器来实现IAP升级功能。

从底层物理结构来看,STM32 的 Flash 模块通常基于 NOR Flash 技术,这种结构支持快速的随机读取操作,非常适合程序的直接执行。然而,它的写入机制比较特殊:每次向 Flash 写入数据时,只能将数据位从 1 变为 0。如果希望将某位恢复为 1,就必须先执行擦除操作,且擦除操作必须以“页(Page)”或“扇区(Sector)”为单位进行。

这种按扇区擦除的机制决定了 Flash 的读写粒度。例如,STM32F1 系列通常以 1KB 或 2KB 的页面为单位,而 STM32F4 系列则以 16KB、64KB 甚至 128KB 的扇区为单位。此外,Flash 存储器的擦写寿命是有限的(通常在 10万次左右),过度擦写会导致扇区失效。

闪存模块

判断mcu类型

以F1为例,按照不同容量,存储器组织成32个1K字节/页(小容量)、128个1K字节/页(中容量)、128个2K字 节/页(互联型)、256个2K字节/页(大容量)的主存储器块和一个信息块。

以STM32F103RCT6为例,根据上述图片,其为引脚为64,256K闪存,48KRAM的大容量产品。

启动方式

手册中提到:

每一个 STM32F10xxx 微 控制器的闪存模块都有一个特定的启始地址,有关的启始地址请参考 STM32F10xxx 参考手册

这句话怎么理解呢?我们结合以下图片。stm32的启动模式取决于boot0和boot1的引脚电平,不管你是哪种启动模式,STM32 上电或复位后,CPU 的 PC(程序计数器)指针永远是从 0x00000000 开始取指令的。这是 ARM Cortex-M 内核的硬件规定。

既然 CPU 总是从 0x00000000 开始跑,那这个地址里装的是什么代码呢?这就取决于你板子上 BOOT0 和 BOOT1 引脚的电平状态(即启动模式)了。STM32 会在复位瞬间锁存这两个引脚的电平,进行内存重映射(Memory Remap):

  • 模式一:主闪存存储器启动(Main Flash)

    • BOOT0 = 0
    • 映射机制:芯片将主闪存(物理地址 0x08000000)映射到 0x00000000
    • 结果:这就是我们平时最常用的模式。CPU 从 0x00000000 取指令,实际上读取的是 0x08000000 里的用户代码。
  • 模式二:系统存储器启动(System Memory)

    • BOOT0 = 1,BOOT1 = 0
    • 映射机制:芯片将系统存储器(物理地址 0x1FFFF000)映射到 0x00000000
    • 结果:这块区域是 ST 官方在出厂时固化在芯片里的 Bootloader(ISP 程序),出厂后不可修改。此时 CPU 从 0x00000000 取指令,实际上运行的是官方的 Bootloader,这就是为什么我们可以通过串口下载程序的原因。
  • 模式三:内置 SRAM 启动

    • BOOT0 = 1,BOOT1 = 1
    • 映射机制:芯片将内部 SRAM(物理地址 0x20000000)映射到 0x00000000
    • 结果:SRAM 是易失性存储器,掉电数据就没了。这种模式一般用于快速调试代码,不需要反复擦写 Flash。

读操作

从系统架构图可以看到Flash接口通过ICode总线和DCode总线与MCU相连,而Flash只与Flash接口交换数据,这样做有什么好处呢?CPU 的运行速度(比如 72MHz)远快于 Flash 的物理读取速度。如果 CPU 直接去 Flash 拿数据,每次都要等很久(插入等待周期)。这个接口充当了 CPU 和 Flash 之间的“缓冲翻译官”,负责发出底层物理控制信号,并管理预取缓冲器。

ICode总线负责取指令,CPU 运行程序时,读控制器会提前把接下来要执行的指令块“预取”到缓存里,这样 CPU 就不需要停下来等 Flash,从而实现流水线的高效执行。DCode总线负责数据,当你的代码里定义了 const 常量(如 const int arr[] = {...};),或者在调试时读取 Flash 里的数据,都会走这条总线。

并且Flash 接口内置了仲裁机制,数据访问(D-Code)的优先级高于指令预取(I-Code)。

取指令

预取缓冲器

预取缓冲器包含两个数据块,每个数据块8个字节,STM32F103 的内部 Flash 物理位宽是 64 位(即 8 个字节),因此读取预取指令块可以在一个周期内完成。

CPU 正在执行当前这 8 字节里的指令时,Flash 控制器并没有闲着,它已经提前把接下来的 8 字节(下一个数据块)从 Flash 里搬到了缓冲器里。当 CPU 执行完当前指令,需要下一条时,直接从缓冲器里拿,不需要等待 Flash 的物理读取时间。这就完美掩盖了 Flash 读取慢的缺陷。

预取控制器

单片机每次复位后,预取缓冲器处于开启状态。预取控制器根据预取缓冲器中可用的空间决定是否访问闪存,预取缓冲器中有至少一块的空余 空间时,预取控制器则启动一次读操作。

AHB 时钟的预分频系数不为 ’1’ 时,必须打开预取缓冲器 (FLASH_ACR[4]=1)

DCode接口

D-Code 接口包含 CPU 端简单的 AHB 接口和对闪存访问控制器的仲裁器提出访问请求的逻辑电
路。
D-Code总线专门负责将Cortex-M内核的数据总线与闪存的数据接口相连接。当CPU在编程过程中需要读取某些常量数据,或者调试器需要对闪存进行调试访问时,都是通过D-Code接口来完成的。

闪存访问控制器

这个模块就是在 I-Code 上的指令预取请求和 D-Code 接口上读请求的仲裁器。

闪存编程和擦除控制器(FPEC)

FPEC 模块处理闪存的编程和擦除操作,它包括 7 32 位的寄存器:
  1.  FPEC键寄存器(FLASH_KEYR)
  2.  选项字节键寄存器(FLASH_OPTKEYR)
  3.  闪存控制寄存器(FLASH_CR)
  4.  闪存状态寄存器(FLASH_SR)
  5.  闪存地址寄存器(FLASH_AR)
  6.  选项字节寄存器(FLASH_OBR)
  7.  写保护寄存器(FLASH_WRPR)
正在进行闪存编程或擦除时,读闪存的操作将被暂停CPU的运行,直到编程或擦除操作结束

解除闪存锁

需要注意以下三点:

  1. 默认上锁,两步解锁:复位后闪存是锁死的。必须严格按照顺序,先向密钥寄存器(FLASH_KEYR)写入KEY1,再写入KEY2,才能成功解锁。
  2. 写错直接“死机”:如果解锁时没按顺序写,或者写错了密钥,系统会立刻报总线错误,并且在下次重启前彻底锁死,再也无法写入。
  3. 支持手动上锁:解锁后,程序可以主动把闪存重新锁上(设置LOCK位)。如果后续还需要写入,只要再次输入正确的“KEY1+KEY2”就能重新解锁。

三个键值分别为:RDPRT键 = 0x000000A5, KEY1 = 0x45670123, KEY2 = 0xCDEF89AB

以HAL库为例,首先判断一下是否上锁,如果是就按顺序写入KEY1和KEY2,如果不是直接返回HAL_OK。写完键值之后再判断一下是否还是上锁状态,如果是的话就返回HAL_ERROR。

主闪存编程

标准的闪存编程顺序如下:
  1.  检查FLASH_SR寄存器的BSY位,以确认没有其他正在进行的编程操作;
  2.  设置FLASH_CR寄存器的PG位为’1’
  3.  在指定的地址写入要编程的半字;
  4.  等待BSY位变为’0’
  5.  读出写入的地址并验证数据。

注意:这种编程模式必须以半字形式写入,否则会报总线错误;FPEC先读出指定地址的内容并检查它是否被擦除,如未被擦除则不执行编程并在FLASH_SR寄存器的PGERR位提出警告(唯一的例外是当要烧写的数值是0x0000时,0x0000可被正确烧入且 PGERR位不被置位);如果指定的地址在FLASH_WRPR中设定为写保护,则不执行编程并在 FLASH_SR寄存器的WRPRTERR位置’1’提出警告。FLASH_SR寄存器的EOP’1’时表示编程结束。

FLASH_SR寄存器的BSY位为’1’时,不能对任何寄存器执行写操作。

闪存擦除

闪存可以按页擦除,也可以整片擦除。
参考HAL库代码HAL_FLASHEx_Erase函数,应用层通常使用此函数

页擦除

闪存的任何一页都可以通过 FPEC 的页擦除功能擦除;擦除一页应遵守下述过程:
  1. 检查FLASH_SR寄存器的BSY位,以确认没有其他正在进行的闪存操作;
  2.  设置FLASH_CR寄存器的PER位为’1’
  3.  FLASH_AR寄存器选择要擦除的页;
  4.  设置FLASH_CR寄存器的STRT位为’1’
  5.  等待BSY位变为’0’
  6.  读出被擦除的页并做验证。

HAL库代码如下:

整片擦除

可以用整片擦除功能擦除所有用户区的闪存,信息块不受此操作影响。建议使用下述过程:
  1.  检查FLASH_SR寄存器的BSY位,以确认没有其他正在进行的闪存操作;
  2.  设置FLASH_CR寄存器的MER位为’1’
  3.  设置FLASH_CR寄存器的STRT位为’1’
  4.  等待BSY位变为’0’
  5.  读出所有页并做验证。

HAL库代码如下:

选项字节编程

之后补充

保护

闪存中的用户代码区可以防止非法的读出;同样可以对闪存区的页加以保护,防止在程序跑飞
的情况下不被意外地改变,写保护的基本单位是:
  1.  对于小容量和中容量的产品为4页;
  2.  对于大容量和互联型的产品为2页。

HAL库中没有针对保护的特定库函数,可以参考:

49. 设置FLASH的读写保护及解除 — [野火]STM32 HAL库开发实战指南——基于野火F4系列开发板 文档

[STM32 - 野火] - - - 固件库学习笔记 - - - 十五.设置FLASH的读写保护及解除 - 技术栈

读保护

这项保护是通过设置 RDP 选项字节,然后在系统重新复位加载了新的 RDP 选项字节后启动的。如果是在连接调试器时开启的读保护,需要插拔电源完成复位。

读保护只防“外人”,不防“自己”。芯片正常上电,从内部 Flash 启动运行你的程序时,你的程序可以正常读取 Flash,也可以通过 IAP(在应用编程)去擦写其他区域的 Flash。

为了防止恶意代码修改中断向量表跳到 RAM 里执行,Flash 的第 0~3 页会被硬件强制加上写保护。这意味着哪怕是你自己的程序,也无法擦写这几页(通常用来放 Bootloader)。

一旦开启读保护,外部调试器(JTAG/SWD)就彻底变成了“瞎子”。

  • 调试器不能读 Flash,不能写 Flash,不能擦除 Flash。
  • 黑客常用的手段——把破解代码下载到 RAM 里,然后让 CPU 从 RAM 启动去读 Flash,这条路也被堵死了
  • 甚至连 DMA(直接内存访问)也被限制了,防止有人用 DMA 绕过 CPU 偷偷把 Flash 里的数据搬运出来。

虽然调试器不能读 Flash 了,但 STM32 还是留了一个后门:你依然可以通过调试器把代码下载到 RAM 里并运行。如果你以后想更新程序或关闭读保护,你可以写一段只有几行的极简代码(里面只包含解锁选项字节、关闭读保护的指令),下载到 RAM 里运行。

为了防止有人用这种方法窃取数据,STM32 规定:只要你把读保护级别降回去(比如从 Level 1 降到 Level 0),芯片会立刻自动执行“全片擦除”。也就是说,你原来的程序和数据会瞬间灰飞烟灭,必须重新烧录。

写保护

不同容量的芯片,写保护打包的规则不同:

  • 小/中容量产品:以 4页 为一组。比如你开启了第0页的写保护,那么第0、1、2、3页会同时被锁死。
  • 大容量产品:前62页以 2页 为一组(比如第0、1页绑定,第2、3页绑定);但第62页到第255页这最后的一大块区域,是绑定在一起的,只要开启保护,这最后的一大片就全部被锁死。
  • 互联型产品:前62页同样以 2页 为一组;第62页到第127页是绑定在一起的。

如果试图在一个受保护的页面进行编程或擦除操作,在闪存状态寄存器(FLASH_SR)中会返回一个保护错误标志。

写保护(WRP)是存储在“选项字节(Option Bytes)”里的。你在代码里修改了 WRP 寄存器的值,写保护并不会立刻生效。你必须让芯片执行一次系统复位(Reset),芯片在启动时才会去读取新的选项字节,把写保护规则真正应用到硬件上。

选项字节写保护

后续补充

选项字节说明

后续补充

寄存器

暂不额外说明

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐