STM32读写内部FLASH

注: 本文是学习野火的指南针开发板过程的学习笔记,可能有误,详细请看B站野火官方配套视频教程(这个教程真的讲的很详细,请给官方三连吧)

01 STM32的内部FLASH简介

在STM32芯片内部有一个FLASH存储器,它主要用于存储代码,我们在电脑上编写好应用程序后,使用下载器把编译后的代码文件烧录到该内部FLASH中,由于FLASH存储器的内容在掉电后不会丢失,芯片重新上电复位后,内核可从内部FLASH中加载代码并运行:

除了使用外部的工具(如下载器)读写内部FLASH外,STM32芯片在运行的时候,也能对自身的内部FLASH进行读写,因此,若内部FLASH存储了应用程序后还有剩余的空间,我们可以把它像外部SPI-FLASH那样利用起来,存储一些程序运行时产生的需要掉电保存的数据。

由于访问内部FLASH的速度要比外部的SPI-FLASH快得多,所以在紧急状态下常常会使用内部FLASH存储关键记录;为了防止应用程序被抄袭,有的应用会禁止读写内部FLASH中的内容,或者在第一次运行时计算加密信息并记录到某些区域,然后删除自身的部分加密代码,这些应用都涉及到内部FLASH的操作。

STM32的内部FLASH包含主存储器、系统存储器以及选项字节区域,它们的地址分布及大小如下:

1. 内部FLASH的构成

FLASH的各个存储区域的说明如下:

  • 主存储器
    一般我们说STM32内部FLASH的时候,都是指这个主存储器区域,它是存储用户应用程序的空间,芯片型号说明中的256K FLASH、512K FLASH都是指这个区域的大小。
    主存储器分为256页,每页大小为2KB,共512KB。这个分页的概念,实质就是FLASH存储器的扇区,与其它FLASH一样,在写入数据前,要先按页(扇区)擦除。

注意上表中的主存储器是本实验板使用的STM32ZET6型号芯片的参数,即STM32F1大容量产品。若使用超大容量、中容量或小容量产品,它们主存储器的页数量、页大小均有不同,使用的时候要注意区分。

CSDN关于STM32内部FLASH的容量类型可根据它的型号名获知: CSDN

  • 系统存储区
    系统存储区是用户不能访问的区域,它在芯片出厂时已经固化了启动代码,它负责实现串口、USB以及CAN等ISP烧录功能。
  • 选项字节
    选项字节用于配置FLASH的读写保护、待机/停机复位、软件/硬件看门狗等功能,这部分共16字节。可以通过修改FLASH的选项控制寄存器修改。

02 对内部FLASH的写入过程

1. 解锁

由于内部FLASH空间主要存储的是应用程序,是非常关键的数据,为了防止误操作修改了这些内容,芯片复位后默认会结FLASH上锁,这个时候不允许设置FLASH的控制寄存器,并且不能对修改FLASH中的内容。

所以对FLASH写入数据前,需要先给它解锁。解锁的操作步骤如下: 往Flash 密钥寄存器 FLASH_KEYR中写入 KEY1 = 0x45670123 再往Flash 密钥寄存器 FLASH_KEYR中写入 KEY2 = 0xCDEF89AB

2.擦除扇区

在写入新的数据前,需要先擦除存储区域,STM32提供了扇区擦除指令和整个FLASH擦除(批量擦除)的指令,批量擦除指令仅针对主存储区。 扇区擦除的过程如下:

  • 检查 FLASH_SR 寄存器中的"忙碌寄存器位 BSY",以确认当前未执行任何 Flash 操作;
  • 在 FLASH_CR 寄存器中,将"激活页擦除寄存器位PER "置 1, 用FLASH_AR寄存器选择要擦除的页;
  • 将 FLASH_CR 寄存器中的"开始擦除寄存器位 STRT "置 1,开始擦除;
  • 等待 BSY 位被清零时,表示擦除完成

3.写入数据

擦除完毕后即可写入数据,写入数据的过程并不是仅仅使用指针向地址赋值,赋值前还还需要配置一系列的寄存器,步骤如下:

  • 检查 FLASH_SR 中的 BSY 位,以确认当前未执行任何其它的内部 Flash 操作;
  • 将 FLASH_CR 寄存器中的 "激活编程寄存器位PG" 置 1;
  • 向指定的FLASH存储器地址执行数据写入操作,每次只能以16位的方式写入;
  • 等待 BSY 位被清零时,表示写入完成

03 查看工程的空间分布

打开map文件后,查看文件最后部分的区域,可以看到一段以"Memory Map of the image"开头的记录:

这一段是某工程的ROM存储器分布映像,在STM32芯片中,ROM区域的内容就是指存储到内部FLASH的代码。

1.程序ROM的加载与执行空间

上述说明中有两段分别以"Load Region LR_ROM1"及"Execution Region ER_IROM1"开头的内容,它们分别描述程序的加载及执行空间。在芯片刚上电运行时,会加载程序及数据,例如它会从程序的存储区域加载到程序的执行区域,还把一些已初始化的全局变量从ROM复制到RAM空间,以便程序运行时可以修改变量的内容。加载完成后,程序开始从执行区域开始执行

在上面map文件的描述中,我们了解到加载及执行空间的基地址(Base)都是0x08000000,它正好是STM32内部FLASH的首地址,即STM32的程序存储空间就直接是执行空间;它们的大小(Size)分别为0x000017a8及0x0000177c,执行空间的ROM比较小的原因就是因为部分RW-data类型的变量被拷贝到RAM空间了;它们的最大空间(Max)均为0x00080000,即512K字节,它指的是内部FLASH的最大空间。 计算程序占用的空间时,需要使用加载区域的大小进行计算,本例子中应用程序使用的内部FLASH是从0x08000000至(0x08000000+0x000017a8)地址的空间区域。

2. ROM空间分布表

在加载及执行空间总体描述之后,紧接着一个ROM详细地址分布表,它列出了工程中的各个段(如函数、常量数据)所在的地址Base Addr及占用的空间Size,列表中的Type说明了该段的类型,CODE表示代码,DATA表示数据,而PAD表示段之间的填充区域,它是无效的内容,PAD区域往往是为了解决地址对齐的问题。

观察表中的最后一项,它的基地址是0x0800175c,大小为0x00000020,可知它占用的最高的地址空间为0x0800177c,跟执行区域的最高地址0x0000177c一样,但它们比加载区域说明中的最高地址0x80017a8要小,所以我们以加载区域的大小为准。对比内部FLASH页地址分布表,可知仅使用页0至页2就可以完全存储本应用程序,所以从页3(地址0x08001800)后的存储空间都可以作其它用途,使用这些存储空间时不会篡改应用程序空间的数据。

04 操作内部FLASH的库函数介绍

为简化编程,STM32标准库提供了一些库函数,它们封装了对内部FLASH写入数据操作寄存器的过程。

操作内部FLASH的库函数

1. FLASH解锁、上锁函数

cs 复制代码
1 #define FLASH_KEY1 ((uint32_t)0x45670123)
2 #define FLASH_KEY2 ((uint32_t)0xCDEF89AB)
3 /**
4 * @brief 对 FLASH 控制寄存器解锁,使能访问
5 * @param None
6 * @retval None
7 */
8 void FLASH_Unlock(void)
9 {
10 if ((FLASH->CR & FLASH_CR_LOCK) != RESET) {
11 /* 写入确认验证码 */
12 FLASH->KEYR = FLASH_KEY1;
13 FLASH->KEYR = FLASH_KEY2;
14 }
15 }
16
17 /**
18 * @brief 对 FLASH 控制寄存器上锁,禁止访问
19 * @param None
20 * @retval None
21 */
22 void FLASH_Lock(void)
23 {
24 /* 设置 FLASH 寄存器的 LOCK 位 */
25 FLASH->CR |= FLASH_CR_LOCK;
26 }

解锁的时候,它对 FLASH_KEYR 寄存器写入两个解锁参数,上锁的时候,对 FLASH_CR 寄存器的 FLASH_CR_LOCK 位置 1 。

2.设置操作位数及擦除扇区

cs 复制代码
1 /**
2 * @brief 擦除指定的页
3 * @param Page_Address: 要擦除的页地址.
4 * @retval FLASH Status:
5 可能的返回值: FLASH_BUSY, FLASH_ERROR_PG,
6 * FLASH_ERROR_WRP, FLASH_COMPLETE or FLASH_TIMEOUT.
7 */
8 FLASH_Status FLASH_ErasePage(uint32_t Page_Address)
9 {
10 FLASH_Status status = FLASH_COMPLETE;
11 /* 检查参数 */
12 assert_param(IS_FLASH_ADDRESS(Page_Address));
13 /*... 此处省略 XL 超大容量芯片的控制部分 */
14 /* 等待上一次操作完成 */
15 status = FLASH_WaitForLastOperation(EraseTimeout);
16
17 if (status == FLASH_COMPLETE) {
18 /* 若上次操作完成,则开始页擦除 */
19 FLASH->CR|= CR_PER_Set;
20 FLASH->AR = Page_Address;
21 FLASH->CR|= CR_STRT_Set;
22
23 /* 等待操作完成 */
24 status = FLASH_WaitForLastOperation(EraseTimeout);
25
26 /* 复位 PER 位 */
27 FLASH->CR &= CR_PER_Reset;
28 }
29
30 /* 返回擦除结果 */
31 return status;
32 }

该函数包含以Page_Address输入参数获得要擦除的地址。内部根据该参数配置FLASH_AR地址,然后擦除扇区,擦除扇区的时候需要等待一段时间,它使用FLASH_WaitForLastOperation等待,擦除完成的时候才会退出FLASH_EraseSector函数。

3.写入数据

对内部FLASH写入数据不像对SDRAM操作那样直接指针操作就完成了,还要设置一系列的寄存器,利用FLASH_ProgramWord和FLASH_ProgramHalfWord函数可按字、半字节单位写入数据:

cs 复制代码
1 /**
2 * @brief 向指定的地址写入一个字的数据(32 位)
3 * @param Address: 要写入的地址
4 * @param Data: 要写入的数据
5 * @retval FLASH Status:
6 可能的返回值: FLASH_ERROR_PG,
7 * FLASH_ERROR_WRP, FLASH_COMPLETE or FLASH_TIMEOUT.
8 */
9 FLASH_Status FLASH_ProgramWord(uint32_t Address, uint32_t Data)
10 {
11 FLASH_Status status = FLASH_COMPLETE;
12 __IO uint32_t tmp = 0;
13
14 /* 检查参数 */
15 assert_param(IS_FLASH_ADDRESS(Address));
16 /*... 此处省略 XL 超大容量芯片的控制部分 */
17 /* Wait for last operation to be completed */
18 status = FLASH_WaitForLastOperation(ProgramTimeout);
19
20 if (status == FLASH_COMPLETE) {
21 /* 若上次操作完成,则开始页入低 16 位的数据(输入参数的第 1 部分) */
22 FLASH->CR |= CR_PG_Set;
23
24 *(__IO uint16_t*)Address = (uint16_t)Data;
25 /* 等待上一次操作完成 */
26 status = FLASH_WaitForLastOperation(ProgramTimeout);
27
28 if (status == FLASH_COMPLETE) {
29 /* 若上次操作完成,则开始页入高 16 位的数据(输入参数的第 2 部分) */
30 tmp = Address + 2;
31
32 *(__IO uint16_t*) tmp = Data >> 16;
33
34 /* 等待操作完成 */
35 status = FLASH_WaitForLastOperation(ProgramTimeout);
36
37 /* 复位 PG 位 */
38 FLASH->CR &= CR_PG_Reset;
39 } else {
40 /* 复位 PG 位 */
41 FLASH->CR &= CR_PG_Reset;
42 }
43 }
44
45 /* 返回写入结果 */
46 return status;
47 }

从函数代码可了解到,使用指针进行赋值操作前设置了PG寄存器位,在赋值操作后,调用了FLASH_WaitForLastOperation函数等待写操作完毕。HalfWord和Byte操作宽度的函数执行过程类似。

05 实验:读写内部****FLASH

bsp_internal_Flash.h

cs 复制代码
#ifndef __INTERNAL_FLASH_H
#define	__INTERNAL_FLASH_H

#include "stm32f10x.h"

/* STM32大容量产品每页大小2KByte,中、小容量产品每页大小1KByte */
//先根据芯片类型定义了宏 FLASH_PAGE_SIZE
#if defined (STM32F10X_HD) || defined (STM32F10X_HD_VL) || defined (STM32F10X_CL) || defined (STM32F10X_XL)
  #define FLASH_PAGE_SIZE    ((uint16_t)0x800)	//2048
#else
  #define FLASH_PAGE_SIZE    ((uint16_t)0x400)	//1024
#endif

//写入的起始地址与结束地址
/*WRITE_START_ADDR 和 WRITE_END_ADDR 定义了后面本工程测试读写内部 FLASH 的
起始地址与结束地址,这部分区域与 map 文件指示的程序本身占用的空间不重合,所以在后面
修改这些地址的内容时,它不会把自身的程序修改掉*/
#define WRITE_START_ADDR  ((uint32_t)0x08008000)
#define WRITE_END_ADDR    ((uint32_t)0x0800C000)



typedef enum 
{
	FAILED = 0, 
  PASSED = !FAILED
} TestStatus;


int InternalFlash_Test(void);



#endif /* __INTERNAL_FLASH_H */

一切准备就绪,可以开始对内部 FLASH 进行擦写,这个过程不需要初始化任何外设,只要按解
锁、擦除及写入的流程走就可以

(bsp_internal_Flash.c

cs 复制代码
/**
  ******************************************************************************
  * @file    bsp_internalFlash.c
  * @author  fire
  * @version V1.0
  * @date    2015-xx-xx
  * @brief   内部FLASH读写测试范例
  ******************************************************************************
  * @attention
  *
  * 实验平台:野火  STM32 霸道 开发板  
  * 论坛    :http://www.firebbs.cn
  * 淘宝    :https://fire-stm32.taobao.com
  *
  ******************************************************************************
  */
  
#include "./internal_flash/bsp_internal_flash.h"   
#include "./usart/bsp_usart.h"

/**
  * @brief  InternalFlash_Test,对内部FLASH进行读写测试
  * @param  None
  * @retval None
  */
int InternalFlash_Test(void)
{
	uint32_t EraseCounter = 0x00; 	//记录要擦除多少页
	uint32_t Address = 0x00;				//记录写入的地址
	uint32_t Data = 0x3210ABCD;			//记录写入的数据
	uint32_t NbrOfPage = 0x00;			//记录写入多少页
	
	FLASH_Status FLASHStatus = FLASH_COMPLETE; //记录每次擦除的结果	
	TestStatus MemoryProgramStatus = PASSED;//记录整个测试结果
	

  /* 解锁 */
  FLASH_Unlock();

  /* 计算要擦除多少页 */
  NbrOfPage = (WRITE_END_ADDR - WRITE_START_ADDR) / FLASH_PAGE_SIZE;

  /* 清空所有标志位 */
  FLASH_ClearFlag(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR);	

  /* 按页擦除*/
  for(EraseCounter = 0; (EraseCounter < NbrOfPage) && (FLASHStatus == FLASH_COMPLETE); EraseCounter++)
  {
    FLASHStatus = FLASH_ErasePage(WRITE_START_ADDR + (FLASH_PAGE_SIZE * EraseCounter));
  
	}
  
  /* 向内部FLASH写入数据 */
  Address = WRITE_START_ADDR;

  while((Address < WRITE_END_ADDR) && (FLASHStatus == FLASH_COMPLETE))
  {
    FLASHStatus = FLASH_ProgramWord(Address, Data);
    Address = Address + 4;
  }

  FLASH_Lock();
  
  /* 检查写入的数据是否正确 */
  Address = WRITE_START_ADDR;

  while((Address < WRITE_END_ADDR) && (MemoryProgramStatus != FAILED))
  {
    if((*(__IO uint32_t*) Address) != Data)
    {
      MemoryProgramStatus = FAILED;
    }
    Address += 4;
  }
	return MemoryProgramStatus;
}

main.c

cs 复制代码
int main(void)
{ 	
	/*初始化USART,配置模式为 115200 8-N-1*/
    USART_Config();
	LED_GPIO_Config();
 
	LED_BLUE;
	printf("\r\n 欢迎使用野火  STM32  开发板。\r\n");	
	printf("正在进行读写内部FLASH实验,请耐心等待\r\n");
	
	if(InternalFlash_Test()== PASSED)
	{
		LED_GREEN;
		printf("读写内部FLASH测试成功\r\n");

	}
	else
	{
		printf("读写内部FLASH测试失败\r\n");
		LED_RED;
	}
	
	
  while(1)
	{	} 
}

06 实际应用

MyFLASH.c

cs 复制代码
#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();							//加锁
}

MyFLASH.h

cs 复制代码
#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
cs 复制代码
#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)	//读取第一个半字的标志位,if成立,则执行第一次使用的初始化
	{
		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 + i * 2, 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 + i * 2, 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();										//保存数据到闪存
}

Store.c

cs 复制代码
#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.h


cs 复制代码
#include "Store.h"//C8T6自带闪存
int main(void)
{
    Store_Init( );				//参数存储模块初始化,在上电的时候将闪存的数据加载回Store_Data,实现掉电不丢失
/*读取*/
   	KP_R=Store_Data[1]  ;
	KI_R=Store_Data[2]  ;
	KD_R=Store_Data[3]  ;

}
/*存入*/
Store_Data[1] =KP_R ;
Store_Data[2] =KI_R ;
Store_Data[3] =KD_R ;
Store_Save();			//将Store_Data的数据备份保存到闪存,实现掉电不丢失

main.c

相关推荐
嵌入式科普2 小时前
嵌入式科普(24)从SPI和CAN通信重新理解“全双工”
c语言·stm32·can·spi·全双工·ra6m5
重生之我是数学王子2 小时前
点亮核心板小灯 STM32U575
stm32·单片机·嵌入式硬件
end_SJ2 小时前
初学stm32 --- 定时器中断
stm32·单片机·嵌入式硬件
南城花随雪。2 小时前
单片机:实现数码管动态显示(0~99999999)74hc138驱动(附带源码)
单片机·嵌入式硬件
南城花随雪。5 小时前
单片机:实现信号发生器(附带源码)
单片机·嵌入式硬件
灵槐梦6 小时前
【速成51单片机】2.点亮LED
c语言·开发语言·经验分享·笔记·单片机·51单片机
三月七(爱看动漫的程序员)7 小时前
HiQA: A Hierarchical Contextual Augmentation RAG for Multi-Documents QA---附录
人工智能·单片机·嵌入式硬件·物联网·机器学习·语言模型·自然语言处理
新晨单片机设计8 小时前
【087】基于51单片机智能宠物喂食器【Proteus仿真+Keil程序+报告+原理图】
嵌入式硬件·51单片机·proteus·宠物·ad原理图
大风起兮128 小时前
STM32HAL库中RTC闹钟设置时分秒,年月日
stm32·嵌入式硬件
超能力MAX9 小时前
IIC驱动EEPROM
单片机·嵌入式硬件·fpga开发