STM32G030内部Flash读写-模拟EEPROM-磨损均衡完整代码

本文主要记录如何使用STM32片内的flash模拟EEPROM的操作。传统场景下,一般使用片外的EEPROM进行关键数据的掉电存储。我之前也写过这方面的笔记,例如:STM8S_库函数_IIC接口_IO软件模拟_硬件实现,但某些特殊场景下,可以使用单片机内部的flash实现同样的操作。

片外flash的寿命是很大的,例如:STM32CUBEMX_SPI接口操作片外FLASH-W25Q128

但是有个问题,STM32片内的flash一次只能擦除一页,而一页的读写次数通常在一万次左右。因此,如果读写过于频繁,那么flash的寿命很快就用完了。

所以要考虑如何最大化的发挥flash的使用寿命,核心就是专业上常说的磨损均衡(Wear Leveling)机制。

原理也很简单,定义一个特定结构的数组,包含数据的有效标记、序号(写入的次数)、实际数据、校验数据等。使用flash的时候,擦一次,写入多次。

例如使用的flash,一页有2048字节,根据存入的数据定义一个64字节的数组。

2048/64=32

第一次使用前64个字节,第二次使用第65~128字节,第三次使用…,以此类推。所以一页flash可以写32次,假设一页可以擦除一万次,那么通过磨损均衡的机制,可以使用32万次。这个次数,绝大多数场景都够用了吧。

因此,设计时,需要考虑几个因素:

接下来看代码,首先是存入数据的结构,我这里使用结构体的方式定义:

  1. 根据要存储的数据量,定义相应的数组,尽量让一页flash的字节数能被数组元素个数整除;
  2. 如何找到最新写入的一组数据,通过数组中的序号判断;
  3. 找到下一个能写入的地址,并写入数据;
  4. 一页写满后,记得擦除。
typedef struct {
    uint16_t magic;         // 数据有效性标记
    uint16_t index;         // 数据索引
    uint8_t  data[60];      // 实际数据(60字节)
} FlashEmuBlock;

我这个结构体由三部分组成,数据有效性标记、数据索引和实际的数据。

有效标记使用固定的0x55aa或者0xaa55,表明这是一组数据开始的地方。

数据索引,也就是个序号。例如第一次写入的时候索引是0x01,第二次写入的时候,索引是0x02。这样的话,通过索引值,就能知道哪一组数据是最新的。

最后是实际数据,这个就简单了,不解释。

我这个结构体里面没有数据校验的部分,其实不太稳妥。可以把这一部分放在实际数据里,或者修改结构体,在实际数据后面增加这部分。

第二部分是查找最后一个有效数据块的地址,通过有效性标记和结构体的大小来实现,具体函数如下:

/**
  * @brief  查找最后一个有效数据块的地址
  * @retval 有效数据块地址,0表示未找到
  */
 uint32_t FindLastValidBlock(void)
 {
     uint32_t addr = FLASH_START_ADDR + FLASH_EMU_PAGE * FLASH_PAGE_SIZE;
     uint32_t end_addr = addr + FLASH_PAGE_SIZE;
     FlashEmuBlock block;
     
     uint32_t last_valid_addr = 0;
     
     while(addr + sizeof(FlashEmuBlock) <= end_addr)
     {
         ST_Flash_Read(addr, (uint32_t*)&block, sizeof(FlashEmuBlock)/4);
         
         if(block.magic == FLASH_EMU_MAGIC)
         {
             last_valid_addr = addr;
         }
         else
         {
             break;
         }
         addr += FLASH_EMU_BLOCK_SIZE;
     }
     return last_valid_addr;
 }

第三部分是找到下一个能写入的地址,并写入数据。

其实第二部分找到了最新的数据起始地址,在此基础上加一个结构体的长度就是下一个能写入的地址,然后进行写操作即可。

/**
  * @brief  保存数据到Flash(固定60字节)
  * @param  *data 要保存的数据(必须60字节)
  * @retval 0成功,其他失败
  */
uint8_t SaveDataToFlash(uint8_t *data)
{
    static uint16_t index = 0;
    uint32_t next_addr;
    FlashEmuBlock block;
    // 获取最后一个有效块地址
    uint32_t last_addr = FindLastValidBlock();
    // 计算下一个写入地址
    if(last_addr == 0)
    {
        next_addr = FLASH_START_ADDR + FLASH_EMU_PAGE * FLASH_PAGE_SIZE;
        index = 0;
    }
    else
    {
        next_addr = last_addr + FLASH_EMU_BLOCK_SIZE;
        ST_Flash_Read(last_addr, (uint32_t*)&block, sizeof(FlashEmuBlock)/4);
        index = block.index + 1;
    }
    // 检查是否需要擦除
    if(next_addr + FLASH_EMU_BLOCK_SIZE > FLASH_END_ADDR)
    {
        Erase_Flash(FLASH_EMU_PAGE, FLASH_EMU_PAGE);
        next_addr = FLASH_START_ADDR + FLASH_EMU_PAGE * FLASH_PAGE_SIZE;
    }
    // 准备数据块
    block.magic = FLASH_EMU_MAGIC;
    block.index = index;
    memcpy(block.data, data, 60); // 固定拷贝60字节
    
    // 写入Flash
    Write_Flash(FLASH_EMU_PAGE, next_addr,(uint64_t*)&block, sizeof(FlashEmuBlock)/8);
    
    return 0;
}

这其中也包含了整页写满后,擦出的操作。

第四部分,这里就比较简单了,从flash中读出最新的一组数据,代码如下:

/** 
  * @brief       从Flash读取多个数据
  * @param       start_addr  起始地址
  * @param       *buf        读取数据缓冲区
  * @param       count       要读取的数据个数(32位字)
  */
void ST_Flash_Read(uint32_t start_addr, uint32_t *buf, uint16_t count)
{
    uint16_t i;
    
    // 检查地址范围是否有效
    if(start_addr < FLASH_START_ADDR || (start_addr + count*4) > FLASH_END_ADDR)
    {
        return; // 地址不在范围内
    }
    
    for(i = 0; i < count; i++)
    {
        buf[i] = *(volatile uint32_t *)start_addr;
        start_addr += 4; // 每次读取4字节(32位字)
    }
}

/**
  * @brief  从Flash读取最后保存的数据(固定读取60字节)
  * @param  *data 数据缓冲区(需保证至少60字节空间)
  * @param  *length 返回的数据长度(固定返回60)
  * @retval 0成功,其他失败
  */
 uint8_t ReadLastDataFromFlash(uint8_t *data, uint16_t *length)
 {
     uint32_t last_addr = FindLastValidBlock();
     FlashEmuBlock block;
     if(last_addr == 0)
     {
         return 1; // 没有有效数据
     }
     // 读取整个块结构体(假设FlashEmuBlock能容纳60字节数据)
     ST_Flash_Read(last_addr, (uint32_t*)&block, sizeof(FlashEmuBlock)/4);
     
     // 强制拷贝60字节,不考虑数据内容
     memcpy(data, block.data, 60);
     // 固定返回长度60
     *length = 60;
     return 0;
 }

接下来可以进行上电测试,我提供的例程中自带一个测试函数,把它添加到程序开始位置即可。

程序优化等级选O1,编译后下载,重新给板子上下电,让程序运行一下。然后用STM32 ST-LINK Utility可以看到flash中的数据。

完整例程我放到了公众号后台,需要的同学回复关键词:032 或者“STM32磨损均衡”,即可获取下载链接。

我是单片机爱好者-MCU起航,打完收工!

发表评论

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理