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

STM32内部FLASH(STM32标准库学习笔记)

在上一章《STM32标准库学习笔记》中,我们学习了 WDG 看门狗,掌握了如何通过独立看门狗和窗口看门狗来监控程序运行、在系统跑飞时及时复位,从而提升程序的可靠性。本章我们把目光转向另一块同样重要的片上资源——STM32 的内部 FLASH。

FLASH 是 STM32 存放程序代码的地方,但它并不只是“只读”的代码仓库:利用程序存储器中剩余的空间,我们可以保存掉电不丢失的用户数据,例如系统配置参数、校准系数、运行计数等;更进一步,还可以通过 IAP(在程序中编程)实现程序的自我更新,为 Bootloader 和在线升级打下基础。本章将从 FLASH 的内部结构讲起,依次介绍闪存模块组织、FLASH 解锁与加锁、使用指针访问存储器、程序存储器的编程与擦除、选项字节以及器件电子签名,最后通过“读写内部 FLASH”和“读取芯片 ID”两个实例,把标准库函数真正用起来。

阅读本章建议结合 STM32F1 参考手册中的闪存章节一起看,重点关注解锁序列、擦除与编程的先后顺序,以及擦写次数和存储区规划这两个容易踩坑的地方。下面正式开始。

如果觉得有收获可以为我的文章点点小赞,这将激励我创作更好的内容!谢谢!

一、STM32内部FLASH                                       

1.1 FLASH简介

STM32F1系列的FLASH包含程序存储器、系统存储器和选项字节三个部分,通过闪存存储器接口(外设)可以对程序存储器和选项字节进行擦除和编程

读写FLASH的用途:

利用程序存储器的剩余空间来保存掉电不丢失的用户数据

通过在程序中编程(IAP),实现程序的自我更新

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

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

1.2 闪存模块组织

1.3 FLASH基本结构

1.4 FLASH解锁

FPEC共有三个键值:

RDPRT键 = 0x000000A5

KEY1 = 0x45670123

KEY2 = 0xCDEF89AB

解锁:

复位后,FPEC被保护,不能写入FLASH_CR

在FLASH_KEYR先写入KEY1,再写入KEY2,解锁

错误的操作序列会在下次复位前锁死FPEC和FLASH_CR

加锁:

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

1.5 使用指针访问存储器

使用指针读指定地址下的存储器:

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

使用指针写指定地址下的存储器:

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

其中:

#define __IO volatile

1.6 程序存储器编程

1.7 程序存储器页擦除

1.8 程序存储器全擦除

1.9 选项字节

RDP:写入RDPRT键(0x000000A5)后解除读保护

USER:配置硬件看门狗和进入停机/待机模式是否产生复位

Data0/1:用户可自定义使用

WRP0/1/2/3:配置写保护,每一个位对应保护4个存储页(中容量)

1.10 选项字节编程

1.检查FLASH_SR的BSY位,以确认没有其他正在进行的编程操作

2.解锁FLASH_CR的OPTWRE位

3.设置FLASH_CR的OPTPG位为1

4.写入要编程的半字到指定的地址

5.等待BSY位变为0

6.读出写入的地址并验证数据

1.11 选项字节擦除

1.检查FLASH_SR的BSY位,以确认没有其他正在进行的闪存操作

2.解锁FLASH_CR的OPTWRE位

3.设置FLASH_CR的OPTER位为1

4.设置FLASH_CR的STRT位为1

5.等待BSY位变为0

6.读出被擦除的选择字节并做验证

1.12 器件电子签名

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

闪存容量寄存器:

基地址:0x1FFF F7E0

大小:16位

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

基地址: 0x1FFF F7E8

大小:96位

1.13 实例一 15.1.1 读写内部FLASH

读写内部FLASH的程序结构

第一步,建立一个底层模块,最底层是MyFLASH,要实现闪存最基本的3个功能:读取、擦除和编程;

第二步,在MyFLASH代码模块的基础上,再建一个Store模块,主要实现参数数据的读写和存储管理;

最后,就是main.c里面的应用层部分,想要保存参数,就写到Store层的数组,再调用保存函数,备份到闪存。

读写内部闪存的基本流程:

首先,闪存并不需要初始化,直接操作即可;

第一步,读取数据;

第二步,解锁,解除FLASH写保护,擦除闪存数据,选择全擦除或是页擦除,然后加锁,恢复写保护;

第三步,编程。

可能用到的FLASH库函数:

//解锁内部FLASH

void FLASH_Unlock(void);

//加锁内部FLASH

void FLASH_Lock(void);

//擦除内部闪存的某一页

FLASH_Status FLASH_ErasePage(uint32_t Page_Address);

//擦除全部的内部闪存

FLASH_Status FLASH_EraseAllPages(void);

//擦除选项字节

FLASH_Status FLASH_EraseOptionBytes(void);

//在指定地址写入字、在指定地址写入半字

FLASH_Status FLASH_ProgramWord(uint32_t Address, uint32_t Data);

FLASH_Status FLASH_ProgramHalfWord(uint32_t Address, uint16_t Data);

//写入选项字节的自定义Data0与Data1

FLASH_Status FLASH_ProgramOptionByteData(uint32_t Address, uint8_t Data);

//写入选项字节的写保护

FLASH_Status FLASH_EnableWriteProtection(uint32_t FLASH_Pages);

//写入选项字节的读保护

FLASH_Status FLASH_ReadOutProtection(FunctionalState NewState);

//写入选项字节的用户选项的三个配置位

FLASH_Status FLASH_UserOptionByteConfig(uint16_t OB_IWDG, uint16_t OB_STOP, uint16_t OB_STDBY);

//获取选项字节当前的状态——获取用户选项的三个配置位

uint32_t FLASH_GetUserOptionByte(void);

//获取选项字节当前的状态——获取写保护状态

uint32_t FLASH_GetWriteProtectionOptionByte(void);

//获取预取缓冲区状态

FlagStatus FLASH_GetPrefetchBufferStatus(void);

//中断使能

void FLASH_ITConfig(uint32_t FLASH_IT, FunctionalState NewState);

//获取标志位、清除标志位

FlagStatus FLASH_GetFlagStatus(uint32_t FLASH_FLAG);

void FLASH_ClearFlag(uint32_t FLASH_FLAG);

//获取状态

FLASH_Status FLASH_GetStatus(void);

//等待上一次操作/等待忙

FLASH_Status FLASH_WaitForLastOperation(uint32_t Timeout);

STM32内部闪存的数据是以小端模式存储的,也就是低位字节存储在低位地址,例如地址0x0800 0000存有一个字的数据0x2000 0660,实际上数据“0x2000 0660”在存储器的地址由低到高是以0660 0002的顺序存放的

可以在Keil软件设置写入程序使用的ROM和RAM的起始地址和程序可使用的存储大小

MyFLASH.h↓

—————————–

#ifndef __MYFLASH_H
#define __MYFLASH_H
uint32_t MyFLASH_ReadWord(uint32_t Address);
uint16_t MyFLASH_ReadHalfWord(uint32_t Address);
uint8_t MyFLASH_ReadByte(uint32_t Address);
void MyFLASH_EraseAllPages(void);
void MyFLASH_ErasePage(uint32_t PageAddress);
void MyFLASH_ProgramWord(uint32_t Address,uint32_t Data);
void MyFLASH_ProgramHalfWord(uint32_t Address,uint16_t Data);
#endif

—————————–

MyFLASH.c↓

—————————–

#include "stm32f10x.h" // Device header
/**
函 数:FLASH读取一个32位的字
参 数:Address 要读取数据的字地址
返 回 值:指定地址下的数据
*/
uint32_t MyFLASH_ReadWord(uint32_t Address)
{
return *((__IO uint32_t *)(Address));//使用指针访问指定地址下的数据并返回
}
/**
函 数:FLASH读取一个16位的半字
参 数:Address 要读取数据的半字地址
返 回 值:指定地址下的数据
*/
uint16_t MyFLASH_ReadHalfWord(uint32_t Address)
{
return *((__IO uint16_t *)(Address));//使用指针访问指定地址下的数据并返回
}
/**
函 数:FLASH读取一个8位的字节
参 数:Address 要读取数据的字节地址
返 回 值:指定地址下的数据
*/
uint8_t MyFLASH_ReadByte(uint32_t Address)
{
return *((__IO uint8_t *)(Address));//使用指针访问指定地址下的数据并返回
}
/**
函 数:FLASH全擦除
参 数:无
返 回 值:无
说 明:调用此函数后,FLASH的所有页都会被擦除,包括程序文件本身,擦除后,程序将不复存在
*/
void MyFLASH_EraseAllPages(void)
{
FLASH_Unlock();//解锁
FLASH_EraseAllPages();//全擦除
FLASH_Lock();//加锁
}
/**
函 数:FLASH页擦除
参 数:PageAddress 要擦除页的页地址
返 回 值:无
*/
void MyFLASH_ErasePage(uint32_t PageAddress)
{
FLASH_Unlock();//解锁
FLASH_ErasePage(PageAddress);//页擦除
FLASH_Lock();//加锁
}
/**
函 数:FLASH编程字
参 数:Address 要写入数据的字地址
参 数:Data 要写入的32位数据
返 回 值:无
*/
void MyFLASH_ProgramWord(uint32_t Address,uint32_t Data)
{
FLASH_Unlock();//解锁
FLASH_ProgramWord(Address,Data);//编程字
FLASH_Lock();//加锁
}
/**
函 数:FLASH编程半字
参 数:Address 要写入数据的半字地址
参 数:Data 要写入的16位数据
返 回 值:无
*/
void MyFLASH_ProgramHalfWord(uint32_t Address,uint16_t Data)
{
FLASH_Unlock();//解锁
FLASH_ProgramHalfWord(Address,Data);//编程半字
FLASH_Lock();//加锁
}

—————————–

Store.h↓

—————————–

#ifndef __STORE_H
#define __STORE_H
extern uint16_t Store_Data[];
void Store_Init(void);
void Store_Save(void);
void Store_Clear(void);
#endif

—————————–

Store.c↓

—————————–

#include "stm32f10x.h" // Device header
#include "MyFLASH.h"
#define STORE_START_ADDRESS 0x0800FC00//存储的起始地址
#define STORE_COUNT 512//存储数据的个数
uint16_t Store_Data[STORE_COUNT];//定义SRAM数组
/**
函 数:参数存储模块初始化
参 数:无
返 回 值:无
/
void Store_Init(void)
{
/判断是不是第一次使用/
if( MyFLASH_ReadHalfWord(STORE_START_ADDRESS) != 0xA5A5 )
{
MyFLASH_ErasePage(STORE_START_ADDRESS);//擦除指定页
MyFLASH_ProgramHalfWord(STORE_START_ADDRESS,0xA5A5);//在第一个半字写入自己规定的标志位,用于判断是不是第一次使用
for (uint16_t i=1;i<STORE_COUNT;i++)//循环STORE_COUNT次,除了第一个标志位
{
MyFLASH_ProgramHalfWord(STORE_START_ADDRESS+i2,0x0000);//除了标志位的有效数据全部清0
}
}
/上电时,将闪存数据加载回SRAM数组,实现SRAM数组的掉电不丢失/
for (uint16_t i=0;i<STORE_COUNT;i++)//循环STORE_COUNT次,包括第一个标志位
{
Store_Data[i] = MyFLASH_ReadHalfWord(STORE_START_ADDRESS+i*2);//将闪存的数据加载回SRAM数组
}
}
/**
函 数:参数存储模块保存数据到闪存
参 数:无
返 回 值:无
/
void Store_Save(void)
{
MyFLASH_ErasePage(STORE_START_ADDRESS);//擦除指定页
for (uint16_t i=0;i<STORE_COUNT;i++)//循环STORE_COUNT次,包括第一个标志位
{
MyFLASH_ProgramHalfWord(STORE_START_ADDRESS+i2,Store_Data[i]);//将SRAM数组的数据备份保存到闪存
}
}
/**
函 数:参数存储模块将所有有效数据清0
参 数:无
返 回 值:无
*/
void Store_Clear(void)
{
for (uint16_t i=1;i<STORE_COUNT;i++)//循环STORE_COUNT次,除了第一个标志位
{
Store_Data[i] = 0x0000;//SRAM数组有效数据清0
}
Store_Save();//保存数据到闪存
}

—————————–

main.c↓

—————————–

#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "Store.h"
#include "Key.h"
uint8_t KeyNum;
int main(void)
{
OLED_Init();
Key_Init();
Store_Init();
OLED_ShowString(1,1,"Flag:");
OLED_ShowString(2,1,"Data:");
while(1)
{
KeyNum = Key_GetNum();
if (KeyNum == 1)
{
Store_Data[1]++;
Store_Data[2]+=2;
Store_Data[3]+=3;
Store_Data[4]+=4;
Store_Save();

}
if (KeyNum == 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(4,6,Store_Data[4],4);
}
}

—————————–

页擦除

全擦除

向闪存的最后一页的第一个半字写入标志位A5A5

使用Store代码模块向闪存的最后一页写入数据

1.14 实例二 15.2.1 读取芯片ID

main.c↓

—————————–

#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
int main(void)
{
OLED_Init();//OLED初始化
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)
{
}
}

—————————–

二、总结

本文围绕 STM32F1 系列内部 FLASH 展开,从硬件结构到代码实现完整梳理了一遍,核心内容可以归纳为以下几点:

  • 结构认知:STM32F1 的 FLASH 由程序存储器、系统存储器和选项字节三部分组成,通过闪存存储器接口(FPEC)对程序存储器和选项字节进行擦除与编程;系统存储区则存放出厂固化的 Bootloader 和器件电子签名,不可更改。
  • 解锁与加锁:复位后 FPEC 处于保护状态,必须先向 FLASH_KEYR 依次写入 KEY1(0x45670123)和 KEY2(0xCDEF89AB)才能解锁;操作完成后应设置 FLASH_CR 的 LOCK 位重新加锁。错误的键值序列会在下次复位前锁死 FPEC,这一点在调试时要特别注意。
  • 读、擦、写三步走:读取最简单,直接用指针访问指定地址即可,配合 __IO(volatile)保证编译器不优化掉访问;擦除分为页擦除和全擦除,全擦除会连同程序本身一起擦掉,实际使用中应优先选择页擦除;编程以半字(16 位)为基本单位,写入前必须确保目标区域已被擦除。
  • 选项字节:包括读保护(RDP)、用户选项(USER)、自定义数据(Data0/1)和写保护(WRP),其编程与擦除流程和普通程序存储器不同,需要额外解锁 FLASH_CR 的 OPTWRE 位,并通过 OPTPG、OPTER、STRT 等控制位完成操作。
  • 工程实践:实例中采用分层设计,底层 MyFLASH 模块封装读取、擦除、编程三个基本功能,中间 Store 模块负责参数数据的读写与存储管理,应用层只需操作 SRAM 数组并调用保存函数即可把数据备份到闪存,从而实现参数的掉电不丢失。读取芯片 ID 的实例则演示了如何通过指针访问系统存储区的闪存容量寄存器和 96 位唯一身份标识寄存器。

需要强调的是,内部 FLASH 的擦写次数有限(典型约 1 万次),且擦除操作会阻塞 CPU 执行,因此在实际项目中应避免频繁写入,可结合数据变化判断、写入间隔控制等策略延长寿命;同时务必把存储区规划在程序占用空间之外,防止擦写时破坏程序代码。

掌握内部 FLASH 的读写,不仅能解决参数保存这类常见需求,也是理解 IAP 在线升级、Bootloader 设计等进阶内容的基础。建议读者先跑通文中的两个实例,再尝试把 Store 模块改造成支持多类型数据(如结构体、浮点数)的通用参数管理组件,进一步加深理解。

赞(0)
未经允许不得转载:网硕互联帮助中心 » STM32内部FLASH(STM32标准库学习笔记)
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!