STM32的内部flash读写操作(含结构体的保存)

cathy的头像

不同的STM32单片机的flash大小不同,这个需要查阅芯片手册或者查看STM32CubeMX软件。

STM32的flash地址起始于0x0800 0000,结束地址是0x0800 0000加上芯片实际的flash大小,要操作flash时注意不要超出此范围。

Flash中的内容一般用来存储代码和一些定义为const的数据,和一些用户自定义的保存数据,它断电不丢失。

不同型号的单片机对flash的操作方式略有不同。下面我以自己用到的STML4R9VIT6为例贴上代码。

STM32的内部 FLASH 包含主存储器、系统存储器、 OTP 区域以及选项字节区域,具体分部查看响应的数据手册。

flash的写机制,只能将存储区域的1改为0,从0改为1只能选择擦除(页擦除或者扇区擦除),一次性擦除多个字节。

flash的读机制,只需要从flash的地址中读取数据就行了,可以字节读取,也可以按字读取。具体操作看代码。

首先需要说明的是,STM32内部flash空间包含多个用途,在保存自定义的数据时,千万不要去操作正在使用的区域。

其次,STM32是小端模式,数据的高字节保存在内存的高地址中, 而数据的低字节保存在内存的低地址中, 这种存储模式将地址的高低和数据位权有效地结合起来, 高地址部分权值高,低地址部分权值低。

//针对STM32L4R9VIT6单片机,通过地址获取所在页的函数如下
uint32_t getPage(uint32_t Address)											//获取地址所在的
{
    uint32_t page = 0;
    if (Address < (FLASH_BASE + FLASH_BANK_SIZE))
        page = (Address - FLASH_BASE) / FLASH_PAGE_SIZE;
    else
        page = (Address - (FLASH_BASE + FLASH_BANK_SIZE)) / FLASH_PAGE_SIZE;
  return page;
}
//这是获取bank的代码
uint32_t GetBank(uint32_t Addr)
{
  uint32_t bank = 0;
  
  if (READ_BIT(SYSCFG->MEMRMP, SYSCFG_MEMRMP_FB_MODE) == 0)
  {
    if (Addr < (FLASH_BASE + FLASH_BANK_SIZE))/* No Bank swap */
      bank = FLASH_BANK_1;
    else
      bank = FLASH_BANK_2;
  }
  else
  {
    if (Addr < (FLASH_BASE + FLASH_BANK_SIZE))/* Bank swap */
      bank = FLASH_BANK_2;
    else
      bank = FLASH_BANK_1;
  }
  return bank;
}
//这是擦除函数,stm32在写数据之前,都需要先把地址中的数据擦除成0xFF,然后才能写数据
FLASH_EraseInitTypeDef EraseInitStruct;
uint8_t eraseFlash(uint32_t start_addr,uint32_t end_addr)		//擦除flash
{
	uint32_t BANK;
	uint32_t FirstPages = 0,LastPages = 0, NbPages = 0;
	uint32_t pageError = 0;
	HAL_FLASH_Unlock();                                         //首先解锁flash
	FirstPages = getPage(start_addr);		            //获取要擦除的第一个页
	LastPages = getPage(end_addr);
	NbPages = LastPages - FirstPages + 1;	                //获取擦除的页数量
	BANK = GetBank(start_addr);																//判断地址的Banks是1还是2
	/* Fill EraseInit structure*/
	EraseInitStruct.TypeErase = FLASH_TYPEERASE_PAGES;	//页擦除
	EraseInitStruct.Banks = BANK;
	EraseInitStruct.Page = FirstPages;
	EraseInitStruct.NbPages = NbPages;
	if(HAL_FLASHEx_Erase(&EraseInitStruct, &pageError) != HAL_OK)
	{ 
        HAL_FLASH_Lock();                                       //上锁
		return 2;												//擦除有错误,返回2
	}
    //下面这些是清除标志位
	__HAL_FLASH_DATA_CACHE_DISABLE();
	__HAL_FLASH_INSTRUCTION_CACHE_DISABLE();
 
	__HAL_FLASH_DATA_CACHE_RESET();
	__HAL_FLASH_INSTRUCTION_CACHE_RESET();
 
	__HAL_FLASH_INSTRUCTION_CACHE_ENABLE();
	__HAL_FLASH_DATA_CACHE_ENABLE();
 
	HAL_FLASH_Lock();                                           //上锁
	return 0;
}

其中有个重要的东西是FLASH_EraseInitTypeDef结构体

typedef struct
{
  uint32_t TypeErase;   //页擦除和块擦除
  uint32_t Banks;       //选择擦除的bank区域
  uint32_t Page;        //擦除的起始页
  uint32_t NbPages;     //要擦除的页数
} FLASH_EraseInitTypeDef;

然后下面是写flash操作

__HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_OPTVERR);//清除flash标志位
eraseFlash(ADDR_FLASH_START,ADDR_FLASH_END);//擦除要写入的flash地址的数据
HAL_FLASH_Unlock();//解锁flash
HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, ADDR_FLASH_START, (uint64_t)data);//写数据
HAL_FLASH_Lock();//上锁

这里详细解说一下HAL_FLASH_Program()函数,函数原型如下

HAL_StatusTypeDef HAL_FLASH_Program(uint32_t TypeProgram, uint32_t Address, uint64_t Data);
/*
返回值类型如下
typedef enum
{
  HAL_OK       = 0x00,
  HAL_ERROR    = 0x01,
  HAL_BUSY     = 0x02,
  HAL_TIMEOUT  = 0x03
} HAL_StatusTypeDef;
可以用返回值是否等于HAL_OK来判断是否写入成功.
参数TypeProgram是按什么类型写入数据,不同的stm32支持的数据类型不尽相同,需要自己查看程序定义,
支持的数据类型有按字节/半字/字/双字.
参数Address是要写的flash地址,这里需要注意的是,如果要写入两次数据,第二次写入时地址需要做偏移.一个地址对应一个字节.见如下实例.
*/
 
//如果是按字节写入(STM32L4R9VIT6不支持这种方式)
uint8_t a=1;
uint8_t b=2;
HAL_FLASH_Program(FLASH_TYPEPROGRAMDATA_BYTE, addr, a);//第一次写数据
HAL_FLASH_Program(FLASH_TYPEPROGRAMDATA_BYTE, addr+1, b);//第二次写数据,需要地址偏1
 
//如果是按双字写入(STM32L4R9VIT6支持)
uint64_t a=0x12345678;
uint64_t b=0x87654321;
HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, addr, a);//第一次写数据
HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, addr+8, b);//第二次写数据,需要地址偏8
 
//这里不同的是,按字节写入,占用一个地址的数据,所以偏移1,按双字写入,占用8个地址的数据,所以偏移8

读数据就很简单了

//读取操作中不需要加锁和解锁,直接一句代码即可完成一次数据的读取操作
//按字节读取
uint8_t a = 0;
a = *(__IO uint8_t*)(start_addr);
 
//按双字读取
uint64_t a = 0;
a = *(__IO uint64_t*)(start_addr);
 
//这里依然要注意读取多个数据时地址偏移量的问题,字节偏移1,半字偏移2,字偏移4,双字偏移8

另外,一个比较方便的操作,是读写结构体。这种操作可以很容易的获取和保存多个数据,从而不需要数组赋值

比如我们要保存如下结构体

typedef struct _userinfo//保存的信息
{
	uint8_t id;
	uint8_t ip[16];			//ip
	uint16_t port;			//端口
}USERINFO;
USERINFO User_Info = {1,"192.168.0.1",8808};
__HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_OPTVERR);
eraseFlash(ADDR_FLASH_USER_START,ADDR_FLASH_USER_END);
HAL_FLASH_Unlock();
for(uint8_t i = 0;i < sizeof(USERINFO);i+=8)
{
	if(HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, ADDR_FLASH_USER_START+i,*((__IO uint64_t*)(&userinfo->id+i)))!= HAL_OK)//这里的数据由于是结构体,需要从里面取值,一定要以第一个成员的地址开始偏移,不能使用结构体自身的地址来偏移,否则数据会出错
	{
		HAL_FLASH_Lock();
		return 1;
	}
}
HAL_FLASH_Lock();
//为了避免数据出错,读操作一定要和写操作对应,写是按双字写,读就要按双字读,否则就需要解决大小端的问题
for(uint8_t i = 0;iid+i)) = *(__IO uint64_t*)(ADDR_FLASH_USER_START + i);//注意赋值的左边,必须要用结构体第一个成员的地址来偏移,双字偏移量是8
}
//这样获取的结构体内容,可以直接通过结构体变量或者结构体指针来访问了.

版权声明:本文为CSDN博主「瑟寒凌风」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/sehanlingfeng/article/details/106226223
免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。