Flash(闪)在单片机指Flash Memory闪存,是一种非易失性存储器(NVM),所谓的非易失性存储器指断电后仍然保持数据的存储器。常见的NVM有Flash,EEPROM(电可擦除可编程只读存储器),ROM(只读存储器)等等。

  在单片机中,Flash常用来存储代码和固件,用于存储启动程序(Bootloader)以及主应用程序代码。

  以STM32F103C8T6为例,其FLASH大小为64K。从地址0x08000000开始,到0x0800FFFF总共64K的空间用来存放Bootloader和用户代码。

用STM32CubeProgram可以查看Flash具体的所写内容。

0x08000000开始,可以看到我的程序写到了0x8008CF0就结束了,后面都是未写的空间。

本期我们将使用STM32F103C8T6介绍如何读写Flash的剩余空间内容来存放一些数据使其断电不会丢失。

  不过需要注意的是,这种方法可能会在擦写数据的时候(例如全片擦写)将数据擦写。真正的保险方式应该是采用外部EEPROM或者外部FLASH实现数据的存放,本文仅作参考。

STM32 的闪存并不像普通的内存那样可以单字节、单字或双字写入或擦除。闪存的写入和擦除是按照“页”进行的,每个页包含一定数量的字节

  具体的一页可能是1KB,可能是2KB具体需要查看STM32的参考手册才能知道。

 STM32F103的页大小为1KB(0x400U),总共分成了64页。因此我们每次写入擦写的时候都需要先寻找到页的起始地址。然后计算总共需要写入的页数再进行擦写和写入。

我们利用CubeMX创建一个空的工程,因为内部FLASH的读写不需要涉及到任何的外设。因此不需要其他的设置。

 在默认情况下Flash读写是被锁定的,因此我们要先对Flash解锁。


HAL_FLASH_Unlock();
//解锁Flash
HAL_FLASH_Lock();
//上锁Flash

  在写入之前,需要对我们写入的部分先进行擦写,这需要我们确定起始地址和结束地址。


void
 
Erase_Flash
(
uint32_t
 startAddress, 
uint32_t
 endAddress)
{
    FLASH_EraseInitTypeDef eraseInit;
    
uint32_t
 pageError;
    HAL_FLASH_Unlock();
    
// 设置擦除操作:按照页擦除
    eraseInit.TypeErase = FLASH_TYPEERASE_PAGES;
    eraseInit.PageAddress = startAddress;
    eraseInit.NbPages = (endAddress - startAddress) / FLASH_PAGE_SIZE + 
1
;  
    
// 计算需要擦除的页数
    
// 执行擦除操作
    
if
 (HAL_FLASHEx_Erase(&eraseInit, &pageError) != HAL_OK) {
       
        Error_Handler();
    }
    HAL_FLASH_Lock();
}

擦写函数,根据起始地址和结束地址计算需要擦写的页。

我们先用STlink在Flash中写入一些数据,之后调用擦写函数。

可以看到,这一页的内容被成功的擦除了。之后我们再定义一个写入内容的函数。


void
 
Write_Flash
(
uint32_t
 address, 
uint32_t
 data)
 
{
    HAL_FLASH_Unlock();
    
// 确保地址对齐到 4 字节边界
    
if(address % 4!= 0) {
        
// 错误处理:地址不对齐
        Error_Handler();
    }

    
// 写入数据到 Flash
    
if
 (HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, address, data) != HAL_OK) {
        Error_Handler();
    }
    HAL_FLASH_Lock();
}

单字写入,这里需要注意的是,Flash寻址的时候需要四字节对其,例如0x800FFF0而不能是0x800FFF1这样子。

可以看到成功的读取了Flash地址800FFF0的值。这里用了volatile关键字,目的是防止目标地址的值被优化(不加也不会出什么大问题)。

  接着在这个的基础之上,我们来实现一个写入字符串的函数。


void
 
Write_Spring_Flash(uint32_t address, uint8_t * str)
 {
    uint32_t data = 0;
//每32位数据缓存
    uint32_t i = 0;
//位置索引
    HAL_FLASH_Unlock();
  
    
// 确保地址对齐到 4 字节边界
    if (address % 4 != ) {
        Error_Handler();
    }
    
while(*(str+i)!='\0')
//不是结尾符号
    {
      data = data | (((uint32_t)*(str+i))<<(8*(i%4)));      i++;      if(i%4 == 0)
//4*8 = 32位 之后写入一次
      {
        HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, address+(i-4), data);data = 0;
      }
    }
    i++;
    data = data | (((uint32_t)*(str+i))<<(8*(i%4)));
    
//最后截止符号'\0'也写入
    HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, address+((i-4)/4+1)*4, data);
    HAL_FLASH_Lock();
}

  这个函数的主要流程是这样子的:

 可以看到我们的数据成功的写入了进去,其实这里面还涉及到了单片机的大端小端存储方式。这里就不过多赘述了。

  需要读字符串的时候,也较为简单,大家只要对指针的理解较为深入,就明白。一个uint8_t*的指针是可以用来存放字符串的。


  
uint8_t* strss;  (uint32_t*)strss = (uint32_t*)0x800F000;

  我们只需要定义一个字符串指针,在让他指向存放我们字符串的Flash地址,就可以获取字符串了。

  内存空间和值是不变的,不同类型表达的输出方式不同。这也就是为什么我们要多走一步,把字符串结束符号\0也写入Flash的目的,这样子才会让uint8_t*截到完整的字符串。

  不过大家在使用的过程中一定要小心谨慎。中间出错容易造成内存溢出的情况。

之所以采用字符串,是因为我们可以用sprintf函数和sscanf函数来快速获取我们需要的值。例如这里我们写入Temp:32.9来假设我们保存了一个温度数据。

这样子就完成了字符串的读取并且提取中我们需要的数据啦。


      再次声明,内部Flash可能会遇到很多情况,例如被创建的数组覆盖,被程序代码覆盖,过多的擦写导致Flash失效等等情况。非必要情况下建议使用外置Flash使用。


嘉立创PCB

还没有评论,抢个沙发!