STM32——Flash闪存
目录
一、Flash简介
二、闪存模块组织
三、Flash基本结构
如何操作控制器FPEC对程序存储器或者选项字节进行擦除和编程呢?
下文就是编程步骤:
四、Flash解锁
五、使用指针访问存储寄存器
六、程序存储器编程(写入)
七、程序存储器页擦除
八、程序存储器全擦除
九、选项字节
9.1选项字节编程
9.2选项字节擦除
十、器件地址签名
十一、读写内部Flash
10.1.程序模块规划
10.2.Flash相关库函数
10.3.底层模块Flash编程
10.4.底层模块三个功能的实现编程
10.5.模块Store代码编程
10.6.模块Store实现数据掉电不丢失
10.7.编程中出现的问题:
十二、读取芯片ID
一、Flash简介
STM32F1系列的FLASH包含程序存储器、系统存储器和选项字节三个部分,通过闪存存储器接口(外设)可以对程序存储器和选项字节进行擦除和编程。之前学习的W25Q64就为一种闪存存储芯片。(之前学习的DMA中,有学习到存储器映像STM32——DMA_code memory-CSDN博客)
Flash存储器用于存储用户的应用程序代码、常量数据以及用户自定义的数据(如参数、配置信息)。它与RAM(运行内存)不同:Flash在断电后数据不会丢失,但写入速度较慢,且需要先擦除(变为0xFF)才能写入。
可以把它想象成电脑的硬盘,而RAM则是内存条。
读写FLASH的用途:
- 利用程序存储器的剩余空间来保存掉电不丢失的用户数据
- 通过在程序中编程(IAP/OTA),实现程序的自我更新
在线编程(In-Circuit Programming – ICP)用于更新程序存储器的全部内容,它通过JTAG、SWD协议(仿真器下载程序)或系统加载程序(Bootloader)下载程序(串口下载)
在程序中编程(In-Application Programming – IAP)可以使用微控制器支持的任一种通信接口下载程序
二、闪存模块组织
不同系列的STM32其Flash的组织结构略有不同,但核心思想一致。在闪存编程参考手册内:
-
主存储器/程序存储器 (Main Memory): 存放代码和数据的地方,容量最大的一块。它被划分为多个页 (Page).
(之前也有学习过相关知识STM32——SPI通信+W25Q64_w25q 模拟spi,仅2线-CSDN博客)
-
小容量产品: 每页1KB。32页
-
中容量产品: 每页1KB(F3C8为中容量产品128页)。页的起始地址末位000/400/800/c00
-
大容量产品: 每页2KB。256页
-
注意: 对于F4/F7/H7等系列,这个概念变成了扇区 (Sector),且扇区大小各不相同(有16KB, 32KB, 128KB, 256KB等),擦除操作是以扇区为单位进行的。
-
信息块 (Information Block):
-
系统存储器/启动程序代码: 存放ST出厂预置的Bootloader程序,用于通过串口、USB等接口下载程序。用户不可修改。
-
选项字节 (Option Bytes)/用户选择字节: 用于配置芯片的硬件特性(如读写保护、看门狗、复位电平等)。我们后面会详细讲。
-
-
闪存存储器接口寄存器(闪存管理员): 用于控制Flash的编程、擦除、上锁等操作。所有对Flash的写/擦除操作,都必须通过操作这些寄存器来完成。
简单来说: 你的程序存放在主存储器的各个页中,对程序的修改(如IAP升级)或存储数据,就需要对指定的页进行擦除和写入操作。
三、Flash基本结构
要操作Flash,主要涉及以下几个关键寄存器:
-
FLASH_ACR: 闪存访问控制寄存器。用于配置等待周期(与CPU时钟速度相关)、使能预取缓冲区等。
-
FLASH_KEYR: 闪存密钥寄存器。用于输入解锁序列。
-
FLASH_OPTKEYR: 选项字节密钥寄存器。用于解锁选项字节。
-
FLASH_SR: 闪存状态寄存器。用于查看当前操作状态,如是否忙(BSY位)、操作是否完成(EOP位)、是否有写保护错误(WRPRTERR位)等。
-
FLASH_CR: 闪存控制寄存器。这是最核心的寄存器!
-
PG (Programming): 置1启动编程(写入)操作。
-
PER (Page Erase): 置1选择页擦除模式。
-
MER (Mass Erase): 置1选择全擦除模式。
-
STRT (Start): 在页擦除模式下,置1开始擦除操作。
-
LOCK: 置1表示Flash被锁定,无法写入/擦除;写0则解锁。
-
OPTWRE: 选项字节编程使能位。
-
所有对Flash的写操作(无论是程序区还是选项字节)都必须遵循严格的解锁 -> 操作 -> 上锁流程。
如何操作控制器FPEC对程序存储器或者选项字节进行擦除和编程呢?
下文就是编程步骤:
四、Flash解锁
为了防止程序“跑飞”意外修改Flash内容,STM32在上电或复位后,Flash控制寄存器(FLASH_CR)是锁定(LOCK位=1) 的。你必须先输入正确的“密钥”序列才能解锁。
FPEC共有三个键值:
RDPRT键 = 0x000000A5(解除读保护)
KEY1 = 0x45670123
KEY2 = 0xCDEF89AB
解锁:
复位后,FPEC被保护,不能写入FLASH_CR(Flash锁)
在FLASH_KEYR先写入KEY1,再写入KEY2,解锁
错误的操作序列会在下次复位前锁死FPEC和FLASH_CR
加锁:
设置FLASH_CR中的LOCK位锁住FPEC和FLASH_CR
重要: 如果密钥写错,会触发一个总线错误,导致芯片锁定,直到下次复位后才能再次尝试解锁。
五、使用指针访问存储寄存器
在C语言中,我们可以通过指针直接访问Flash的任意地址进行读取。写入则需要通过前面提到的寄存器控制。
使用指针读指定地址下的存储器:uint16_t Data = *((__IO uint16_t *)(0x08000000));使用指针写指定地址下的存储器:*((__IO uint16_t *)(0x08000000)) = 0x1234;其中:#define __IO volatile//防止编译器优化
读取示例:#define FLASH_START_ADDR 0x08000000 // Flash起始地址// 读取Flash地址0x08001000处的半字(16位数据)
uint16_t read_data;
uint16_t *flash_ptr = (uint16_t*)(FLASH_START_ADDR + 0x1000);
read_data = *flash_ptr; // 直接解引用指针读取// 读取字(32位数据)
uint32_t *flash_ptr32 = (uint32_t*)(FLASH_START_ADDR + 0x1000);
uint32_t read_data32 = *flash_ptr32;写入/擦除: 绝不能使用 *flash_ptr = 0x1234; 这样的方式直接写!必须通过配置FLASH_CR寄存器来启动官方规定的编程过程。
六、程序存储器编程(写入)
写入(编程)操作只能将Flash中的位从 ‘1’ 变为 ‘0’ 。
如果想把‘0’变回‘1’,必须进行擦除操作(将整个页/扇区恢复为0xFF)。
标准库编程步骤(以半字16位写入为例):
解锁 Flash。
等待 FLASH_SR中的BSY(忙)位为0,确保当前没有其他Flash操作。
在FLASH_CR中设置PG位为1,启用编程模式。
向目标地址写入一个半字(16位数据,使用指针写指定地址的数据)。
等待 BSY位变为0,表示写入完成。
检查 EOP(操作完成)标志位,如果置起则表示成功(可选,但建议)。
清除 EOP标志位(通过写1清零)。
清除PG位,退出编程模式。
重新上锁 Flash(可选,但为安全起见建议上锁)。
七、程序存储器页擦除
擦除是以页(或扇区) 为最小单位进行的,擦除后整个页的内容变为0xFFFFFFFF。
标准库页擦除步骤:
解锁 Flash。
等待 BSY位为0。
在FLASH_CR中设置PER位为1,选择页擦除模式。
在FLASH_AR寄存器中写入要擦除的页的起始地址
设置FLASH_CR中的STRT位为1,启动擦除。
等待 BSY位变为0。
检查 EOP标志,并清除它。
清除PER位。
上锁 Flash。
八、程序存储器全擦除
全擦除会擦除整个主存储区的所有内容,选项字节区不受影响。请谨慎使用!
步骤:
读取LOCK位,若=1,解锁Flash,先执行KEY1再执行KEY2。在库函数中,都是直接解锁Flash,无读取步骤.
等待BSY=0。
设置FLASH_CR中的MER位为1。
设置STRT位为1(触发条件)。
等待BSY=0。
清除MER位。
上锁。
九、选项字节
有些存储器前+n,意思是:在xxx写入数据时,要同时在nxxx写入数据的反码,此存储数据有效
RDP(Read Protection):写入RDPRT键(0x000000A5)后解除读保护
USER:用户配置,配置硬件看门狗和进入停机/待机模式是否产生复位
Data0/1:用户可自定义使用的俩个字节,通常用于存储产品序列号
WRP0/1/2/3(Write Protection):配置写保护,每一个位对应保护4个存储页(中容量)
9.1选项字节编程
操作选项字节也需要先解锁(使用不同的密钥),并且操作会立即生效,有时会引发系统复位。
- 检查FLASH_SR的BSY位,以确认没有其他正在进行的编程操作
- 解锁FLASH_CR的OPTWRE位
- 设置FLASH_CR的OPTPG位为1
- 写入要编程的半字到指定的地址
- 等待BSY位变为0
- 读出写入的地址并验证数据
9.2选项字节擦除
操作选项字节也需要先解锁(使用不同的密钥),并且操作会立即生效,有时会引发系统复位。
- 检查FLASH_SR的BSY位,以确认没有其他正在进行的闪存操作
- 解锁FLASH_CR的OPTWRE位(解锁选项字节)
- 设置FLASH_CR的OPTER位为1(擦除选项字节)
- 设置FLASH_CR的STRT位为1(触发芯片)
- 等待BSY位变为0
- 读出被擦除的选择字节并做验证
十、器件地址签名
电子签名存放在闪存存储器模块的系统存储区域,包含的芯片识别信息在出厂时编写,不可更改,使用指针读指定地址下的存储器可获取电子签名
闪存容量寄存器:
基地址:0x1FFF F7E0
大小:16位
产品唯一身份标识寄存器:
基地址: 0x1FFF F7E8
大小:96位
十一、读写内部Flash
10.1.程序模块规划
三个程序设计-代码整体规划:
1.最底层建立模块MyFlash:实现闪存最基本的3个功能:读取、擦除、编程
2.在MyFlash上建一个模块Store:
实现参数数据的读写和存储管理:
定义SRAM数组,将掉电不丢失的数据存入;
后调用保存函数,SRAM数组自动备份到闪存中;
上电后,Store初始化,会自动把闪存里数据读回到SRAM数组
此为闪存管理策略
3.在应用层main:实现任意读写参数,并且这些参数是掉电不丢失
想保存参数,就写到Store层数组,再调用保存函数,备份到闪存
10.2.Flash相关库函数
1.解锁Flash
void FLASH_Unlock(void);
2.加锁Flash
void FLASH_Lock(void);
3.闪存擦除某一页,参数给起始地址,有返回执行状态
FLASH_Status FLASH_ErasePage(uint32_t Page_Address);
4.闪存全擦除
FLASH_Status FLASH_EraseAllPages(void);
5.擦除选项字节
FLASH_Status FLASH_EraseOptionBytes(void);
6.写入全字
FLASH_Status FLASH_ProgramWord(uint32_t Address, uint32_t Data);
7.写入半字
FLASH_Status FLASH_ProgramHalfWord(uint32_t Address, uint16_t Data);
8.选项字节的写入
FLASH_Status FLASH_ProgramOptionByteData(uint32_t Address, uint8_t Data);
9.选项字节的读保护
FLASH_Status FLASH_EnableWriteProtection(uint32_t FLASH_Pages);
10.选项字节的写保护
FLASH_Status FLASH_ReadOutProtection(FunctionalState NewState);
11.选项字节的用户选项的三个配置位
FLASH_Status FLASH_UserOptionByteConfig(uint16_t OB_IWDG, uint16_t OB_STOP, uint16_t OB_STDBY);
获取选项字节的当前状态
12.获取用户选项的三个配置位
uint32_t FLASH_GetUserOptionByte(void);
13.获取写保护状态
uint32_t FLASH_GetWriteProtectionOptionByte(void);
14.获取读保护状态
FlagStatus FLASH_GetReadOutProtectionStatus(void);
15.中断使能
void FLASH_ITConfig(uint32_t FLASH_IT, FunctionalState NewState);
16.获取标志位
FlagStatus FLASH_GetFlagStatus(uint32_t FLASH_FLAG);
17.清除标志位
void FLASH_ClearFlag(uint32_t FLASH_FLAG);
18.获取状态
FLASH_Status FLASH_GetStatus(void);
19.等待上一次操作(等待忙 BSY=0)
FLASH_Status FLASH_WaitForLastOperation(uint32_t Timeout);
10.3.底层模块Flash编程
#include "stm32f10x.h" // Device header
/*
在模块内实现擦除,读取,编程三个功能
Flash不需要初始化,直接写读取部分
1.读取32位字,在STM32中,所有地址都是hi32位
2.读取16位半字
3.读取8位字节其中,地址必须为32位,与指针的类型是32位16位还是8位无关
因为地址是门牌号,比如flash的起始地址08000000,只有32位的变量才能够存得下这么大的门牌号
*(__IO uint16_t*)只是将Address强制转换为一个指针, 该指针指向的数据对象为 uint16_t,即规定该地址开始的16位“数据存储空间”,并非Address转成16位
指针的类型是用来对所指向地址的解释,比如取指向开始的32位、16位等。另外在对指针加减时, 指针的类型也会影响结果
指针变量存放的是首地址,如果你读取是byte,巧了,一个32位的地址下就是一个byte,但你要一个word,就是要4个byte, 首地址下的数据自然达不到你要求,那个传过去的首地址会自动向下加1,直到找到连续的四个地址,那存放的正好为一个word。这也就是 char* short* int*存在的意义,指引CPU找几个地址。
*/uint32_t MyFlash_ReadWord(uint32_t Address)
{return *((__IO uint32_t *)(Address));
}uint32_t MyFlash_ReadHalfWord(uint32_t Address)
{return *((__IO uint16_t *)(Address));
}uint32_t MyFlash_ReadByte(uint32_t Address)
{return *((__IO uint8_t *)(Address));
}
/*
擦除功能
1.全擦除:解锁Flash;调用库函数;锁Flash
2.页擦除:给定指定页地址,解锁Flash;调用库函数;锁Flash
*/void MyFlash_EraseAllPage(void)
{FLASH_Unlock();FLASH_EraseAllPages();FLASH_Lock();
}void MyFlash_EraseSomeOnePage(uint32_t PageAddress)
{FLASH_Unlock();FLASH_ErasePage(PageAddress);FLASH_Lock();
}/*
编程功能
1.写入一个字:解锁Flash,32位数据和地址,锁Flash
2.写入一个半字:解锁Flash,32位地址和16位数据,锁Flash
*/void MyFlash_WriteProgramWord(uint32_t Address,uint32_t Data)
{FLASH_Unlock();FLASH_ProgramWord(Address,Data);FLASH_Lock();}void MyFlash_WriteProgramHalfWord(uint32_t Address,uint16_t Data)
{FLASH_Unlock();FLASH_ProgramHalfWord(Address,Data);FLASH_Lock();}
10.4.底层模块三个功能的实现编程
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "MyFlash.h"
#include "key.h"uint8_t key;
int main(void)
{ OLED_Init();key_scan();OLED_ShowHexNum(1,1,MyFlash_ReadWord(0x08000000),8);OLED_ShowHexNum(2,1,MyFlash_ReadHalfWord(0x08000000),4);OLED_ShowHexNum(3,1,MyFlash_ReadByte(0x08000000),2);/*在最后一页0x0800 FC00进行编程操作写入之前一定要擦除:调用页擦除在ST—Link utility可在相关页看到写入的数据*/MyFlash_EraseSomeOnePage(0x0800FC00);MyFlash_WriteProgramWord(0x0800FC00,0x12345678);MyFlash_WriteProgramHalfWord(0x0800FC00,0x1234);while(1){key=key_init();if(key==1){MyFlash_EraseAllPage();}if(key==2){MyFlash_EraseSomeOnePage(0x08000000);}//在按下按键前,按下复位键键,OLED显示屏刷新//按下按键,再次按下复位键OLED显示屏不刷新。//可知按下按键,程序文件已经不复存在。而OLED显示的数值并没有消失,是因为OLED里面有显存,可以保存最后一次显示的内容。//此时可以选择断电重新上电,可发现OLED显示屏没有任何内容,这是闪存全部擦除的现象。//重新编程,按下擦除某一页的按键,发现重新按下复位键键,OLED显示屏也没有刷新,说明现在程序是损坏的,没法运行。//但是通过ST-Link utility,可查询到第一页是被擦除的,但是后面一页的内容还是存在。}
}
10.5.模块Store代码编程
#include "stm32f10x.h" // Device header
#include "MyFlash.h"
/*
实现参数掉电不丢失的存储
在store模块用sram缓存数组来管理flash的最后一页,实现参数的任意读写和保存。
由于闪存每次都是擦除再写入,擦除之后还容易丢失数据。
因此想要灵活管理数据还是要靠sram数组,需要备份的时候再统一转到闪存里。
*///定义SRAM数组,大容量产品 每页2KB,共256页,数组大小给512,确保不会溢出
uint16_t Store_Data[512];
//①将闪存初始化,若第1次使用代码闪存默认全是ff,而参数和sram一般都默认0。因此第1次使用要给闪存的各个参数都置0。
/*
如何判断是不是第1次使用?
定义一个标志位把闪存最后一页的第一个半字当作标志位判断
若是第1次使用先擦除页,在起始位置写入规定的标志位,后将其余的位置为默认值0。
注意一个半字占用两个地址。
*/
void Store_Init(void)
{if(MyFlash_ReadHalfWord(0x0800FC00)!=0xA5A5){MyFlash_EraseSomeOnePage(0x0800FC00);MyFlash_WriteProgramHalfWord(0x0800FC00,0xA5A5);for(uint16_t i=1;i<512;i++){MyFlash_WriteProgramHalfWord(0x0800FC00+i*2,0x0000);}}//②上电时,将闪存数据全部转存到sram数组。转存数组的过程,是上电时恢复数据,实现数据掉电不丢失。
/*
之后想存储掉电不丢失的参数时,先任意更改数组,除了标志位的其他数据,更改好之后,将数组整体备份到闪存的最后一页。
*/for(uint16_t i=1;i<512;i++){Store_Data[i]=MyFlash_ReadHalfWord(0x0800FC00+i*2);}
}
//③备份保存。擦除最后一页。将数组完全备份保存到闪存最后一页中。void Store_Save(void)
{MyFlash_EraseSomeOnePage(0x0800FC00);for(uint16_t i=1;i<512;i++){MyFlash_WriteProgramHalfWord(0x0800FC00+i*2,Store_Data[i]);}
}
//④清除数据:数据掉电不丢失,不方便数据全部清0void Store_Clear(void)
{for(uint16_t i=1;i<512;i++){Store_Data[i]=0x0000;}Store_Save();//每次更改玩数组,都要保存到Flash里
}
/*
参数保存整体思路梳理:
在闪存最后一页,直接对它读写,不方便效率低且容易丢失数据
在sram中定义一个数组,为闪存的分身,再读写任何数据,直接对sram操作。
但是sram掉电丢失,因此需要闪存的配合。
SRAM数组每次更改后,都把数组整体备份到闪存里;
在上电的时候再把闪存里的数据,初始化加载,回到SRAM里。
SRAM数组相当于掉电不丢失
为了判断闪存是不是之前保存过数据,需要一个标志位来配合。
如果标志位是A5A5,说明闪存已经保存过数据,上电就直接加载回备份的数据。
如果标志位不是A5A5,说明闪存是第1次使用,先初始化再加载数据.
*/
10.6.模块Store实现数据掉电不丢失
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"#include "key.h"
#include "Store.h"uint8_t key;
int main(void)
{ OLED_Init();key_scan();Store_Init();//第一次使用初始化闪存,后把闪存备份的数据加载会SRAM数组,实现数组掉电不丢失OLED_ShowString(1,1,"Flag:");OLED_ShowString(2,1,"Data:");while(1){key=key_init();if(key==1){Store_Data[1]++;Store_Data[2]+=2;Store_Data[3]+=3;Store_Data[4]+=4;Store_Save();}if(key==2){Store_Clear();}OLED_ShowHexNum(1,6,Store_Data[0],4);OLED_ShowHexNum(3,1,Store_Data[1],4);OLED_ShowHexNum(3,6,Store_Data[2],4);OLED_ShowHexNum(4,1,Store_Data[3],4);OLED_ShowHexNum(5,6,Store_Data[4],4);}
}
10.7.编程中出现的问题:
目前的闪存前面一部分存储的是程序文件,最后一页存储的是用户数据。
目前的假设是程序文件比较小,最后一页肯定没有用到。
若程序文件比较大,触及到了最后一页,那么程序和用户数据存储的位置就冲突了。
如何解决这个问题?
我们给程序文件限定一个存储范围,不让他分配到后面我们用户数据的空间来。
工程选项→编译器内为各个数据分配的空间地址和范围→程序空间的size改小0xFC00
如何得知自己的程序文件大小?
若想知道自己的程序文件大小,给文件编程一下。在program size中前三个数相加得到的是程序占用闪存的大小,后两个数相加得到的是占用sram的大小。也可以双击Target1,打开.map文件,在最后,程序的大小已经算出。倒数第2行是SRAM的大小, 最后一行是闪存的大小。
十二、读取芯片ID
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
int main(void)
{ OLED_Init();OLED_ShowString(1,1,"F_SIZE:");OLED_ShowHexNum(1,8,*((__IO uint16_t *)(0x1FFFF7E0)),4);//闪存容量寄存器OLED_ShowString(1,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 uint16_t *)(0x1FFFF7E8+0x04)),4);//基地址偏移OLED_ShowHexNum(4,1,*((__IO uint32_t *)(0x1FFFF7E8+0x08)),4);//基地址偏移while(1){}
}