【STM32】最后一刷-江科大Flash闪存-学习笔记

发布于:2025-04-01 ⋅ 阅读:(22) ⋅ 点赞:(0)

FLASH简介

  1. STM32F1系列的FLASH包含程序存储器、系统存储器和选项字节三个部分,通过闪存存储器接口(外设)可以对程序存储器和选项字节进行擦除和编程,(系统存储器用于存储原厂写入的BootLoader程序,用于串口下载,不允许我们修改)
  2. 读写FLASH的用途:     
  • 利用程序存储器的剩余空间来保存掉电不丢失的用户数据     
  • 通过在程序中编程(IAP),实现程序的自我更新

在线编程(In-Circuit Programming – ICP)用于更新程序存储器的全部内容,它通过JTAG、SWD协议或系统加载程序(Bootloader)下载程序

在程序中编程(In-Application Programming – IAP)可以使用微控制器支持的任一种通信接口下载程序

 存储器映像参考这篇:【江协科技STM32】DMA直接存储器存储-学习笔记_江科 stm32 dma-CSDN博客

 闪存模块组织

对应主存储器,进行了分页,分页是为了更好地管理闪存,擦除和写保护都是以页为单位的,这一点和之前的W25Q64的闪存一样,写入前必须擦除等等。具体参考Flash操作注意事项:【STM32】SPI通信协议&W25Q64Flash存储器芯片(学习笔记)_spi存储芯片-CSDN博客

 FLASH基本结构

 FLASH解锁(解除闪存锁)

 FPEC共有三个键值:     

  • RDPRT键 = 0x000000A5(解除读保护密钥)
  • KEY1 = 0x45670123     
  • KEY2 = 0xCDEF89AB
  • 解锁:     
  • 复位后,FPEC被保护,不能写入FLASH_CR     
  • 在FLASH_KEYR先写入KEY1,再写入KEY2,解锁     
  • 错误的操作序列会在下次复位前锁死FPEC和FLASH_CR

加锁:     

  • 置FLASH_CR中的LOCK位锁住FPEC和FLASH_CR

 

 使用指针访问存储器

uint16_t Data = *((__IO uint16_t *)(0x08000000)); 

这行代码是在 C 语言中用于从特定内存地址读取数据,并将其赋值给变量Data。解释如下:

  1. uint16_t是一种无符号 16 位整数类型,定义在<stdint.h>头文件中,确保了数据类型的宽度为 16 位,可表示的范围是 0 到 65535。
  2. Data是定义的一个uint16_t类型的变量,用于存储从特定内存地址读取的数据。
  3. (__IO uint16_t *)(0x08000000):这部分是一个强制类型转换。(__IO uint16_t *)0x08000000这个地址值转换为指向__IO uint16_t类型的指针。其中__IO通常是由编译器定义的宏,可能表示该内存地址是可读写(volatile)的,防止编译器对该地址的访问进行优化,确保每次访问都是真实地从该内存地址读写数据。0x08000000是一个十六进制表示的内存地址。
  4. *((__IO uint16_t *)(0x08000000)):这部分通过指针解引用操作,从0x08000000这个内存地址读取一个uint16_t类型的数据。
  5. 最后,将从指定内存地址读取的数据赋值给Data变量。

 *((__IO uint16_t *)(0x08000000)) = 0x1234;

  1. 指针类型强制转换
    (__IO uint16_t *)(0x08000000) 这部分代码将地址值 0x08000000 强制转换为一个指向 __IO uint16_t 类型的指针。其中 __IO 可能是一个特定的修饰符,通常用于表示该内存位置具有特殊的读写属性(例如可能是与外设寄存器相关,允许读写操作),uint16_t 表示无符号 16 位整数类型。通过这种强制类型转换,告诉编译器把 0x08000000 这个地址当作是一个 __IO uint16_t 类型数据的起始地址。
  2. 赋值操作
    在完成指针类型强制转换后,使用 * 运算符对这个指针进行解引用,然后将值 0x1234 赋给该指针所指向的内存位置。也就是将 0x1234 这个 16 位无符号整数值写入到了内存地址 0x08000000 开始的两个字节(因为 uint16_t 是 16 位,占两个字节)。

 实例

 指定地址下读:

/**
  * 函    数:FLASH读取一个32位的字
  * 参    数:Address 要读取数据的字地址
  * 返 回 值:指定地址下的数据
  */
uint32_t HerFlash_ReadWord(uint32_t Address)
{
	return *((__IO uint32_t *)(Address)); //使用指针访问指定地址下的数据并返回
}

/**
  * 函    数:FLASH读取一个16位的半字
  * 参    数:Address 要读取数据的半字地址
  * 返 回 值:指定地址下的数据
  */
uint16_t HerFlash_ReadHalfWord(uint32_t Address)
{
	return *((__IO uint16_t *)(Address)); //使用指针访问指定地址下的数据并返回
}

/**
  * 函    数:FLASH读取一个8位的字节
  * 参    数:Address 要读取数据的字节地址
  * 返 回 值:指定地址下的数据
  */
uint8_t HerFlash_ReadByte(uint32_t Address)
{
	return *((__IO uint8_t *)(Address));//使用指针访问指定地址下的数据并返回
}

 

 程序存储器全擦除

  1. 第一步:读 FLASH_CR 的 LOCK 位

    • 理由:LOCK 位用于锁定闪存控制寄存器(FLASH_CR),在对闪存进行擦除等操作前,需要先了解其锁定状态。如果 LOCK 位 = 1,表示闪存处于锁定状态,不能直接进行后续的擦除操作,需要先执行解锁过程;若 LOCK 位 = 0,则可跳过解锁过程直接进行擦除设置。
    • 操作及结果:读取该位,得到 LOCK 位 = 1,说明闪存处于锁定状态。
  2. 第二步:执行解锁过程

    • 理由:因为第一步检测到 LOCK 位为 1,闪存锁定,所以必须执行解锁过程才能对闪存控制寄存器进行操作,以实现擦除等功能。
    • 操作及结果:执行解锁过程后,将 LOCK 位置为 0,此时闪存解锁,可以对相关寄存器进行设置。
  3. 第三步:置 FLASH_CR 的 MER = 1

    • 理由:MER(Mass Erase)位用于选择是否进行全擦除操作。将 MER 位置 1,表示要进行闪存全擦除操作。
    • 操作及结果:将 MER 位置 1,准备执行全擦除。
  4. 第四步:置 FLASH_CR 的 STRT = 1

    • 理由:STRT(Start)位用于启动闪存擦除操作。当 MER 位已置 1 准备好全擦除,再将 STRT 位置 1,就可以正式启动全擦除过程。
    • 操作及结果:将 STRT 位置 1,闪存全擦除操作开始执行。
  5. 第五步:关注 FLASH_SR 的 BSY 位

    • 理由:BSY(Busy)位用于指示闪存操作是否正在进行。在全擦除操作启动后,需要监测该位来判断擦除操作是否完成。当 BSY = 1 时,表示闪存操作正在进行中;当 BSY = 0 时,表示闪存操作已完成。
    • 操作及结果:在全擦除操作执行过程中,BSY 位会变为 1,表示操作正在进行。等待一段时间后,当 BSY 位变为 0,说明全擦除操作完成。
  6. 第六步:读出并验证所有页的数据

    • 理由:全擦除操作完成后,需要验证是否所有页的数据都已被正确擦除。通过读出所有页的数据,并与预期的擦除后数据(一般为全 1 或特定的擦除后状态)进行比较,来验证擦除操作的正确性。
    • 操作及结果:读出所有页的数据,与预期的擦除后数据进行对比,若完全一致,则说明全擦除操作成功;若有不一致的地方,则说明擦除操作可能存在问题。

实例 

/**
  * 函    数:FLASH全擦除
  * 参    数:无
  * 返 回 值:无
  * 说    明:调用此函数后,FLASH的所有页都会被擦除,包括程序文件本身,擦除后,程序将不复存在
  */
void HerFlash_EraseAllPages(void)
{
	FLASH_Unlock();			//解锁
	FLASH_EraseAllPages();	//全擦除
	FLASH_Lock();			//加锁
}

void FLASH_Unlock(void)//解锁FLASH程序擦除控制器(解锁) 

 void FLASH_Lock(void)//锁定FLASH程序擦除控制器

 FLASH_Status FLASH_EraseAllPages(void)//擦除所有FLASH页面

 注意:以上功能可用于所有STM32F10x器件  

  程序存储器页擦除

  1. 读取锁定状态:首先读取闪存控制寄存器(FLASH_CR)的LOCK位,判断闪存是否处于锁定状态。
  2. 解锁操作(若需):若LOCK位为1(锁定状态),执行解锁过程;若为0,跳过解锁。
  3. 配置擦除参数
    • FLASH_CRPER(Page Erase,页擦除使能)位为1
    • FLASH_AR(地址寄存器)中选择要擦除的闪存页;
    • FLASH_CRSTRT(Start,启动)位为1,启动页擦除操作。
  4. 等待擦除完成:监测闪存状态寄存器(FLASH_SR)的BSY(Busy,忙)位。若BSY=1,表示擦除仍在进行,需持续等待;若BSY=0,表示擦除完成。
  5. 验证擦除结果:擦除完成后,读出并验证被擦除页的数据,确保擦除操作成功。

实例

/**
  * 函    数:FLASH页擦除
  * 参    数:PageAddress 要擦除页的页地址
  * 返 回 值:无
  */
void HerFlash_ErasePage(uint32_t PageAddress)
{
	FLASH_Unlock();
	FLASH_ErasePage(PageAddress);
	FLASH_Lock();
}

 FLASH_Status FLASH_ErasePage(uint32_t Page_Address)//擦除指定的FLASH页面 

 注意:此功能可用于所有STM32F10x器件  

 程序存储器编程

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

编程过程讲解:

  1. 读取锁定状态:首先读取闪存控制寄存器(FLASH_CR)的LOCK位,判断闪存是否处于锁定状态。
  2. 解锁操作(若需):若LOCK位为1(锁定状态),执行解锁序列;若为0,直接进入下一步。
  3. 使能编程模式:将FLASH_CR寄存器的PG位(Program,编程使能)置为1,开启编程功能。
  4. 写入数据:在指定的闪存地址中写入半字(16 位)数据。用到这句代码 *((__IO uint16_t *)(0x08000000)) = 0x1234;
  5. 等待操作完成:监测闪存状态寄存器(FLASH_SR)的BSY(Busy,忙)位。若BSY=1,表示编程操作仍在进行,需持续等待;若BSY=0,表示编程操作完成。
  6. 验证数据:读取编程地址中的数据,检查写入的数据是否正确,确保编程操作成功。

 实例

/**
  * 函    数:FLASH编程字
  * 参    数:Address 要写入数据的字地址
  * 参    数:Data 要写入的32位数据
  * 返 回 值:无
  */
void HerFLASH_ProgramWord(uint32_t Address, uint32_t Data)
{
	FLASH_Unlock();
	FLASH_ProgramWord(Address, Data);
	FLASH_Lock();
}

/**
  * 函    数:FLASH编程字
  * 参    数:Address 要写入数据的半字地址
  * 参    数:Data 要写入的16位数据
  * 返 回 值:无
  */
void HerFLASH_ProgramHalfWord(uint32_t Address, uint16_t Data)
{
	FLASH_Unlock();
	FLASH_ProgramHalfWord(Address, Data);
	FLASH_Lock();
}

 FLASH_Status FLASH_ProgramWord(uint32_t Address, uint32_t Data)//在指定地址编程一个字

 注意:以上功能可用于所有STM32F10x器件   

参数 说明
Address 指定要编程的地址
Data 指定要编程的数据

FLASH_Status FLASH_ProgramHalfWord(uint32_t Address, uint16_t Data)//在指定地址上编程半个字 

参数 说明
Address 指定要编程的地址
Data 指定要编程的数据

  注意:以上功能可用于所有STM32F10x器件   

 选择字节说明

  • RDP:写入RDPRT键(0x000000A5)后解除读保护
  • USER:配置硬件看门狗和进入停机/待机模式是否产生复位
  • Data0/1:用户可自定义使用
  • WRP0/1/2/3:配置写保护,每一个位对应保护4个存储页(中容量)

选项字节擦除 

  • 检查FLASH_SR的BSY位,以确认没有其他正在进行的闪存操作
  • 解锁FLASH_CR的OPTWRE位
  • 设置FLASH_CR的OPTER位为1
  • 设置FLASH_CR的STRT位为1
  • 等待BSY位变为0
  • 读出被擦除的选择字节并做验证 

选项字节编程 

  • 检查FLASH_SR的BSY位,以确认没有其他正在进行的编程操作
  • 解锁FLASH_CR的OPTWRE位
  • 设置FLASH_CR的OPTPG位为1
  • 写入要编程的半字到指定的地址
  • 等待BSY位变为0
  • 读出写入的地址并验证数据 

读取内部FLASH闪存

要实现数据掉电不丢失的存储,那就要基于底层代码,再建一个模块Store,在Store模块我们要用SRAM缓存数组来管理FLASH闪存的最后一页,实现参数的任意读写和保存。因为闪存每次都是擦除,再写入,擦除之后,还容易丢失数据,所以要想灵活管理数据,还是得靠SRAM数组,需要备份的时候,我们再统一转到闪存里。所以在Store模块里要先定义一个Store_数组,用于存放备份数据。

 参数存储模块初始化

#define  STORE_START_ADDRESS  	0x0800FC00
#define  STORE_DATA  			512

uint16_t Store_Data[STORE_DATA];	//定义SRAM数字,512个数据,每个数据16位,2字节,刚好对应闪存一页1024字节


void Store_Init(void)
{
	/*判断是不是第一次使用*/
	if(HerFlash_ReadHalfWord(STORE_START_ADDRESS) != 0xA8A8)
	{
		HerFlash_ErasePage(STORE_START_ADDRESS);
		HerFLASH_ProgramHalfWord(STORE_START_ADDRESS, 0xA8A8);//在第一个半字写入自己规定的标志位,用于判断是不是第一次使用
		                                                      
		for(uint16_t i =1; i < STORE_DATA; i ++)	//循环STORE_COUNT次,除了第一个标志位
		{
			HerFLASH_ProgramHalfWord(STORE_START_ADDRESS + i * 2,0x0000);//除了标志位的有效数据全部清0
		}
	}
	/*上电时,将闪存数据加载回SRAM数组,实现SRAM数组的掉电不丢失*/
	for(uint16_t i =0;i < STORE_DATA; i ++)
	{
		Store_Data[i] = HerFlash_ReadHalfWord(STORE_START_ADDRESS + i * 2);//将闪存的数据加载回SRAM数组
	}
}

 参数存储模块保存数据到闪存

/**
  * 函    数:参数存储模块保存数据到闪存
  * 参    数:无
  * 返 回 值:无
  */
void Store_Save(void)
{
	HerFlash_ErasePage(STORE_START_ADDRESS);
	for(uint16_t i = 0;i < STORE_DATA;i ++)	//循环STORE_COUNT次,包括第一个标志位
	{
		HerFLASH_ProgramHalfWord(STORE_START_ADDRESS +i*2, Store_Data[i]);//将SRAM数组的数据备份保存到闪存
	}
}

 参数存储模块将所有有效数据清0

/**
  * 函    数:参数存储模块将所有有效数据清0
  * 参    数:无
  * 返 回 值:无
  */
void Store_Clear(void)
{
	for(uint16_t i = 1;i < STORE_DATA;i ++)
	{
		Store_Data[i] = 0x0000;	//SRAM数组有效数据清0
	}                           
	Store_Save();               //保存数据到闪存
}

 最终梳理思路:梳理思路

其实就是,在主函数对SRAM数组Store_Data进行修改,然后在放到闪存,防止SRAM掉电丢失,然后在上电初始化的时候在把闪存的数据再读取到SRAMStore_Data数组,实现SRAM掉电不丢失 

main函数 

#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] = 0x1234;		//变换测试数据,断电丢失
			Store_Data[2] = 0xABCD;
			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);
	}
}

结果 

 

注意一个问题:程序文件大小和用户数据大小的冲突 

程序占用空间大小

 

器件电子签名&读取芯片ID

电子签名存放在闪存存储器模块的系统存储区域,包含的芯片识别信息在出厂时编写,不可更改,使用指针读指定地址下的存储器可获取电子签名

闪存容量寄存器:     

  • 基地址:0x1FFF F7E0     
  • 大小:16位

产品唯一身份标识寄存器:     

  • 基地址: 0x1FFF F7E8     
  • 大小:96位 

int main(void)
{
	OLED_Init();						
	
	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)
	{
		
	}
}

结果 

最后记得看数据手册,这东西是真的能看懂 学会 学到知识!!!! 


网站公告

今日签到

点亮在社区的每一天
去签到