【嵌入式】STM32内部NOR Flash磨损平衡与掉电保护总结

news/2025/2/27 1:33:37

在这里插入图片描述

1. NOR Flash与NAND Flash

先deepseek看结论:

特性Nor FlashNAND Flash
读取速度快(支持随机访问,直接执行代码)较慢(需按页顺序读取)
写入/擦除速度慢(擦除需5秒,写入需逐字节操作)快(擦除4ms,按块操作)
存储密度低(1MB-1GB,适合小容量)高(8GB-1TB+,适合大容量)
擦写寿命约10万次约100万次
成本
坏块管理无需坏块管理,可靠性高需ECC纠错和坏块管理
代码执行直接运行代码需将代码加载到RAM运行
  • 本次只针对STM32内部Flash,即Nor Flash进行总结,不考虑坏块问题。
  • 本次设计结合实际项目,存储固定个数的文件进行说明,文件存储时包含文件索引+文件内容。

2. 掉电保护原理

2.1 Flash擦除过程掉电

参考链接nor flash之擦除与写入,擦除的步骤有三步:

  1. 先设置全0,此过程掉电,FLASH前面部分数据会变成0,后面部分数据不变;
  2. 擦除,此过程掉电,FLASH全部数据可能是错乱的数值;
  3. 维持全1,此过程掉电,FLASH中大部分数据读出是0xFF,某些部分读出的数据可能为其他数值;

通过设置magic num,且存放在扇区头部,可以解决前两种问题,但第三种无法解决,这种问题只能通过扫描是否全是0xFF来检查并重新擦除!

2.2 Flash写入过程掉电

掉电后,前面写入的数据为正常数据,后面未写入,仍未0xFF。
比如写一个结构体,可能会出现写一半的情况,可通过一个状态字段来进行识别,后面进行详细说明。

2.3 磨损平衡

考虑到FLASH的寿命,写入数据时是增量写入的,比如修改一个变量或者结构,会重新写入一个新的,同时将旧的标记为已删除,这个标记可以和状态字段整合到一起。
还需要考虑扇区写满后数据转移的问题,本次项目中采用主扇区和备份扇区交替写入的机制来控制磨损平衡,需添加扇区头结构,对扇区进行标识,要做到既可以在启动时选择到正确的扇区,又可以识别出破坏的扇区。

2.4 断电数据恢复

当写扇区头或者写文件时,如果掉电,那么本次的数据需在启动时识别出来,并可以恢复到增量写入时上个文件完整的数据。本次项目未考虑数据恢复的问题,只识别出了是否发生了损坏,损坏会全部擦除,上位机重新下发文件。

3. 扇区头部结构

考虑到自己项目中文件较小,且写入频次较低,项目设计并没采用类似easyFlash库那样对所有扇区进行平衡写入。为了加快搜索,项目中对文件索引单独存储一个扇区,同时结合另一个相同大小的扇区进行写满后换扇区写入。同样,文件数据也采用两个扇区进行交替写入。
不管哪个扇区,考虑到擦除中断电导致扇区被破坏,每个扇区的头部添加如下结构:

typedef struct tagSectorHeader{
    uint32_t u32Magic; //魔术字,0x12345678
    uint32_t u32SectorStatus; //扇区的状态 
}TSectorHeader;
//扇区的状态
#define SECTOR_STATUS_INIT 0xFFFFFFFF   //未使用
#define SECTOR_STATUS_USED 0x00FFFFFF   //已使用,写完数据后设置该装填
#define SECTOR_STATUS_ERASE 0x0000FFFF  //擦除中,先搬运数据,搬运完毕后擦除扇区,变为未使用

这里使用了Flash中的一个小技巧,对同一个地址,写4字节数据时,可以多次写入,但必须按照先写0x00FFFFFF再写0x0000FFFF这种方式进行写入,只要之前是FF位置的数据就可以写入。

判定扇区是否损坏:

uint32_t isBadSector(TSectorHeader *pHdr)
{
    // 魔术字无效且不是初始值 或 魔术字是初始值但扇区状态不是初始值
    if (pHdr->u32Magic != SECTOR_MAGIC_NUM && pHdr->u32Magic != 0xFFFFFFFF 
        || pHdr->u32Magic == 0xFFFFFFFF && pHdr->u32SectorStatus != SECTOR_STATUS_INIT)
    {
        return 1; // 扇区损坏
    }

    return 0;
}

第一次写扇区头:

int32_t writeNewSectorHdr(uint32_t u32SectorAddr)
{
    // 标记新扇区 - 使用状态
    if (writeSectorStatus(u32SectorAddr + offsetof(TSectorHeader, u32SectorStatus), SECTOR_STATUS_USED) != 0)
    {
        return -1;
    }
    // 标记新扇区 - 魔术字
    uint32_t u32Magic = SECTOR_MAGIC_NUM;
    if (AflFlashWriteWords(u32SectorAddr + offsetof(TSectorHeader, u32Magic), &u32Magic, 1) != 0)
    {
        return -1;
    }

    return 0;
}

更改扇区状态:

int32_t writeSectorStatus(uint32_t u32StatusStartAddr, SECTOR_STATUS_DEFINE statusNow)
{
    int32_t iRet = 0;
    FILEINDEX_STATUS_DEFINE statusLast; // 注意状态写入必须按状态定义的顺序进行写入!
    AflFlashReadWords(u32StatusStartAddr, &statusLast, 1);
    if (statusNow == SECTOR_STATUS_USED)
    {
        if (statusLast != SECTOR_STATUS_INIT)
        {
            ASSERT(1, "last sector status err!");  
            return -1;
        }
    }
    else if (statusNow == SECTOR_STATUS_ERASE)
    {
         if (statusLast != SECTOR_STATUS_USED)
        {
            ASSERT(1, "last sector status err!");
            return -1;
        }
    }
    else
    {
    }

    iRet = AflFlashWriteWords(u32StatusStartAddr, &statusNow, 1);

    return iRet;
}

4. 文件索引结构

//文件索引的操作状态
#define FILEINDEX_STATUS_INIT 0xFFFFFFFF         //未使用
#define FILEINDEX_STATUS_PRE_WRITE 0x00FFFFFF    //准备写入
#define FILEINDEX_STATUS_FIN_WRITE 0x0000FFFF    //已写入 写入索引结构数据即更新,文件数据靠CRC校验
#define FILEINDEX_STATUS_FIN_DEL 0x000000FF      //已删除
//#define FILEINDEX_STATUS_PRE_DEL 0x000000FF    //准备删除 该操作可以用来恢复上次的数据,本次设计无需恢复数据
//文件索引信息
typedef struct tagFileIndex
{
    uint32_t u32DataStatus;      // 操作状态
    uint32_t u32IndexAddr;       // 该索引在扇区中的起始地址
    uint32_t u32DataAddr;        // 该文件在Flash中的起始地址
    uint32_t u32DataLen;         // 数据块的长度
    uint16_t u16FileCRC;         // 数据校验值
    uint16_t u16AlignReserved;   // 对齐保留
    // 其他数据
}TFileIndex;

写索引时,先写状态为准备写入,然后写结构体字段,写完后再修改状态为写入完成。
比如写一个文件,会先写索引结构,再写文件内容,索引结构中添加状态字段来进行控制,文件内容没写完掉电,可通过检查索引结构中的文件长度、CRC来对文件内容进行校验。

写索引数据:

// 写索引数据,状态无需传入,内部控制
int32_t writeFileIndexToFlash(uint32_t u32CopyToAddr, TFileIndex *ptfileIndex)
{
    int32_t iRet = 0;
    do
    {
        // 先写状态为【准备写入】
        iRet = writeFileIndexStatus(u32CopyToAddr + offsetof(TFileIndex, u32DataStatus), FILEINDEX_STATUS_PRE_WRITE);
        if (iRet != 0)
            break;

        //写索引地址
        iRet = AflFlashWriteWords(u32CopyToAddr + offsetof(TFileIndex, u32IndexAddr), &u32CopyToAddr, 1);
        if (iRet != 0)
            break;

        // 写剩余的其他所有数据
        iRet = AflFlashWriteWords(u32CopyToAddr + offsetof(TFileIndex, u32DataAddr), 
                (uint32_t *)&ptfileIndex->u32DataAddr, (sizeof(TFileIndex) - offsetof(TFileIndex, u32DataAddr))/4);
        if (iRet != 0)
            break;

        // 最后,写状态为【已写入】
        iRet = writeFileIndexStatus(u32CopyToAddr + offsetof(TFileIndex, u32DataStatus), FILEINDEX_STATUS_FIN_WRITE);
        if (iRet != 0)
            break;

        ptfileIndex->u32DataStatus = FILEINDEX_STATUS_FIN_WRITE;
        ptfileIndex->u32IndexAddr = u32CopyToAddr;

    } while (0);

    if (iRet != 0)
    {
        DEBUG(DEBUG_ERROR, ("writeFileIndexToFlash Error!!"));
    }

    return iRet;
}

写文件数据(略),过程就是先写索引,再写数据,最后检查写入的文件CRC是否正确。

5. 总结

以上是状态控制的核心代码,其他和写文件数据内容相关、扇区切换相关略去了,以后有机会可以写个通用的、小型的模块。
数据恢复的思路如下,本次检查到非法数据,直接擦了:

当写入新的结构时(注意这个结构必须具备唯一标识符),需将旧的结构标记为准备删除,然后写新的结构,当新结构数据写完后,会将旧的结构数据再标记为已删除。这样初始化时扫描所有数据,如果同一个标识符的结构,一个状态为准备删除,一个为准备写入,此时认为数据未拷贝完,可以将准备写入改为已删除,废掉这个数据,重新拷贝一个新的,此时即使再次掉电也没问题。如果同一个标识符的结构,一个状态为准备删除,一个为写入完成,此时认为数据已经拷贝完,将准备删除改为已删除即可。

6. 调试中问题

  • 使用ST-Link单步调试读写flash代码,可能会出现写失败,甚至数据被篡改,改为J-Link解决该问题。

http://www.niftyadmin.cn/n/5869352.html

相关文章

Android 8.0 (API 26) 对广播机制做了哪些变化

大部分隐式广播无法通过静态注册接收,除了以下白名单广播: ACTION_BOOT_COMPLETED ACTION_TIMEZONE_CHANGED ACTION_LOCALE_CHANGED ACTION_MY_PACKAGE_REPLACED ACTION_PACKAGE_ADDED ACTION_PACKAGE_REMOVED 需要以动态注册方案替换: cl…

【AI+智造】基于DeepSeek的船舶海工设备多维度数据分析技术方案——以南通船舶制造企业为例

作者:Odoo技术开发/资深信息化负责人 日期:2025年2月25日 以下技术方案基于南通市制造业特点,结合船舶海工行业实际应用场景,针对设备数据量化分析需求展开论述。全文以技术可行性、行业适配性及实施路径为核心,深度整…

H3C商场无线零售解决方案技术资料汇总集

互联网各领域资料分享专区(不定期更新): Sheet 前言 由于内容较多,且不便于排版,为避免资源失效,请用手机点击链接进行保存,若链接生效请及时反馈,谢谢~ 正文 链接如下(为避免资源失效&#x…

RK3399 Android10双WiFi功能实现

在Android9开始,就支持WiFi并发功能,在官方链接(WLAN STA/AP 并发 | Android Open Source Project)有如下描述: Android 9 引入了可让设备同时在 STA 和 AP 模式下运行的功能。对于支持双频并发 (DBS) 的设备,此功能让一些新功能得以实现,例如在用户想要启用热点 (sof…

[回顾]从原型链视角解读Vue底层实现Vue VueCompoent VM VC关系

从原型链视角解读VueComponent与Vue关系 原型链 根据,原型链涉及三个关键属性:__proto__是所有对象的私有属性,指向原型链的第一个元素;prototype是函数的属性,实例对象不拥有它;constructor指向构造函数。提到原型链是JS中实现继承的机制,通过属性链式查找属性,直到…

计算机网络————(三)

前文二 前文一 Websocket协议 是一种存在TCP协议之上的协议 当客户端需要了解服务器是否更新就需要不断给客户端发送请求询问是否更新,这行会造成服务端压力很大 而Websocket相当于服务器一旦更新了就会给客户端发送消息表明自己更新了,类似客户端订阅…

模型蒸馏:让人工智能更智能、更小、更高效的艺术

你有没有想过,我们如何才能让一个需要巨大计算能力的庞大人工智能模型变得更精简、更快速、更强大?答案在于模型蒸馏,这是一种允许知识从大型、计算成本高昂的人工智能系统转移到较小、更高效的系统的技术,而不会牺牲智能。 什么是模型蒸馏 模型蒸馏是一种技术,其…

神经网络 - 神经元

人工神经元(Artificial Neuron),简称神经元(Neuron),是构成神经网络的基本单元,其主要是模拟生物神经元的结构和特性,接收一组输入信号并产生输出。 生物学家在 20 世纪初就发现了生物神经元的结构。一个生物神经元通常具有多个树…