☰
STM32 Flash模拟EEPROM:轮询算法实现百万次擦写寿命
2026/10/5 15:41:13 网站建设 项目流程

简介:本资源是一套面向嵌入式物联网设备开发者的STM32G071平台FLASH存储优化实践方案,聚焦单片机片内FLASH模拟EEPROM的高可靠性实现,解决传统FLASH频繁擦写导致寿命衰减的核心痛点。方案采用轮询式地址索引管理与智能页擦除策略,将有效擦写寿命提升至百万次级别,适用于需长期断电保存参数、日志或配置的低功耗IoT终端场景。压缩包共1236个文件,含664个C源码(含HAL驱动、数学库调用及FLASH管理核心逻辑)、261个头文件(定义接口与数据结构)、104个汇编启动文件及74个说明文档,整体体积20.96MB;内容预览显示已集成ARM CMSIS-DSP数学库、STM32G0xx HAL外设驱动及IAR/Keil双环境工程配置(.uvprojx/.icf等),具备即编即用特性。已有32人下载学习,提供完整可运行工程、分层模块化代码结构与关键算法注释,便于开发者快速理解地址映射机制、磨损均衡逻辑与异常掉电保护设计。

1. 项目概述与核心痛点

最近在做一个物联网传感器节点的项目,主控用的是STM32G071,一个性价比很高的Cortex-M0+内核芯片。项目里需要保存一些校准参数、设备ID和运行状态标志位,这些数据量不大,但要求掉电不丢失,并且能频繁更新。一开始,我理所当然地想到了外挂一颗EEPROM芯片,比如AT24C02,几毛钱成本,用I2C读写,简单省事。但老板掐着BOM成本,要求能省则省,最好一颗MCU搞定所有功能。

这就引出了嵌入式开发里一个经典问题:如何在没有硬件EEPROM的单片机上,实现类似EEPROM的非易失性数据存储?STM32G071内部只有Flash,没有独立的EEPROM存储区。直接对Flash进行“写”操作是行不通的,因为Flash的特性是“写0容易写1难”,必须先擦除(整块或整页变为全1)才能写入。频繁擦写固定区域,Flash很快就会达到寿命终点(通常10万次擦写)。我的目标,就是设计一套算法,在有限的Flash空间里,通过巧妙的地址管理和擦除策略,把等效的“写入”寿命提升到百万次级别,满足物联网设备长期、频繁更新参数的需求。

2. 方案选型与设计思路拆解

面对这个需求,业内通常有几种思路。最简单粗暴的是“单地址直写”,每次更新数据都擦除再写同一个地址,这显然会迅速耗尽Flash寿命,不可取。另一种是“双备份”或“多备份”轮换写入,这是模拟EEPROM的常见方案。我最终选择了基于“页轮询”和“地址索引”的混合策略,核心思想可以概括为:将Flash空间虚拟化,通过一个动态的“写指针”和状态标记,将多次数据更新分散到多个物理地址上,从而大幅减少对单一物理单元的擦除次数。

2.1 为什么选择页擦除与轮询策略?

STM32G071的Flash以2KB为一页。擦除操作的最小单位就是一页。如果我每次只更新几个字节的数据就擦除一整页,那效率极低,寿命消耗也极快。因此,我的策略是:

  1. 预留多页作为模拟EEPROM的存储区:例如,分配4页Flash(8KB)作为一个“虚拟EEPROM池”。
  2. 以“记录”为单位进行写入:每次数据更新,我不直接覆盖旧值,而是将“数据值”连同其“虚拟地址”和“序列号”打包成一个完整的记录,写入到当前写指针指向的物理地址。
  3. 写指针在池内顺序移动:当一页写满后,写指针跳转到下一页的起始位置继续写入。
  4. 惰性页擦除:只有当所有页都即将被写满时,才去擦除最早被写满、且其中所有记录都已过时(被新记录覆盖)的那一页。擦除后,该页变为空白页,加入写入队列的尾部,形成循环。

这样一来,一次数据更新(比如修改一个参数)只消耗一个“记录”的写入寿命,而一个物理页(2KB)可以存放很多个记录。只有当一个页中的所有记录都失效后,它才会被擦除一次。假设每个记录16字节,那么一页可以存放128个记录。即使我们频繁更新数据,也需要更新128次才会触发一次对该页的擦除。等效地,该页上每个物理地址的擦除频率降低了128倍,从而将整体寿命从10万次提升到了千万次量级(理论值)。

2.2 关键数据结构设计:记录与状态管理

要实现上述逻辑,需要在Flash中存储一些元数据来管理整个存储池。我设计了两种关键的数据结构:

记录结构体(Record):这是实际存储用户数据的单元。

typedef struct { uint16_t virtual_addr; // 虚拟地址,代表用户数据的ID,如0x0001代表温度校准值 uint16_t seq_num; // 序列号,用于区分同一虚拟地址的新旧记录 uint32_t data; // 用户数据(这里以32位为例,可根据需要调整) uint16_t crc16; // 校验和,确保记录完整性 } flash_record_t;
  • virtual_addr:相当于EEPROM的地址。用户通过这个地址来存取数据。
  • seq_num:这是实现“覆盖”逻辑的关键。每次更新同一virtual_addr的数据时,seq_num递增。在查找最新数据时,我们遍历所有记录,找到同一virtual_addr下seq_num最大的那条,它就是有效数据。
  • crc16:Flash可能发生位翻转,增加CRC校验可以保证数据的可靠性,在读取时进行验证。

页头信息(Page Header):写在每一页的起始几个字节,用于标识页的状态。

// 通常直接写在Flash固定偏移处,例如页起始地址 #define PAGE_STATUS_EMPTY 0xFFFFAAAA // 空白页 #define PAGE_STATUS_ACTIVE 0xFFFFBBBB // 正在写入的页 #define PAGE_STATUS_FULL 0xFFFFCCCC // 已写满的页 #define PAGE_STATUS_OBSOLETE 0xFFFFDDDD // 已过时待擦除的页

页状态机驱动着整个轮询流程。系统初始化时,会扫描所有预留页,找到状态为ACTIVE的页(当前写入页)。如果没有ACTIVE页,但有EMPTY页,则将其置为ACTIVE。如果连EMPTY页都没有了,说明存储池已满,需要启动垃圾回收(回收OBSOLETE页)。

注意:这些状态标记字必须选择在Flash擦除后(全0xFF)不会自然出现的值。0xFFFFAAAA这类值在擦除后的Flash里是0xFFFFFFFF,写入0xFFFFAAAA意味着将某些位从1变为0,是可行的操作,无需先擦除。

3. 核心算法实现与代码解析

整个系统的核心是三个函数:初始化、读数据、写数据。下面我结合代码和流程图来详细说明。

3.1 系统初始化与状态恢复

系统上电后,第一步就是扫描整个Flash存储池,重建内存中的管理信息。这个过程决定了系统的鲁棒性。

void FLASH_EEPROM_Init(void) { uint32_t page_addr; uint32_t found_active_page = 0; flash_page_header_t *p_header; // 1. 遍历所有预留页 for (int i = 0; i < FLASH_EEPROM_PAGE_NUM; i++) { page_addr = FLASH_EEPROM_START_ADDR + (i * FLASH_PAGE_SIZE); p_header = (flash_page_header_t *)page_addr; // 2. 识别页状态 switch (p_header->status) { case PAGE_STATUS_ACTIVE: // 找到当前写入页,记录其地址和当前写偏移 g_active_page_addr = page_addr; g_write_offset = sizeof(flash_page_header_t); // 从头之后开始 // 需要遍历该页已有记录,找到最后一个有效记录的位置,更新g_write_offset find_last_record_offset(page_addr, &g_write_offset); found_active_page = 1; break; case PAGE_STATUS_EMPTY: // 记录空白页地址,以备后用 add_empty_page_to_list(page_addr); break; case PAGE_STATUS_FULL: // 记录满页地址 add_full_page_to_list(page_addr); break; case PAGE_STATUS_OBSOLETE: // 记录过时页地址,它们是垃圾回收的首选目标 add_obsolete_page_to_list(page_addr); break; default: // 异常状态,可能该页从未被初始化,将其格式化为EMPTY format_page_as_empty(page_addr); break; } } // 3. 如果没有找到ACTIVE页,则需要从EMPTY页中激活一页 if (!found_active_page) { if (get_empty_page_count() > 0) { g_active_page_addr = get_next_empty_page(); format_page_as_active(g_active_page_addr); g_write_offset = sizeof(flash_page_header_t); } else { // 没有空白页,存储池已满,必须立即触发垃圾回收! perform_garbage_collection(); } } // 4. 在内存中构建虚拟地址到最新记录的映射表(可选,加速读取) rebuild_virtual_addr_map(); }

初始化过程的心得:

  • 断电恢复:必须考虑任何一步操作中突然断电的情况。例如,正在写记录时断电,可能导致记录不完整(CRC校验失败)。初始化时遇到CRC错误的记录应直接跳过,视为无效。
  • 状态一致性:页头状态和页内实际内容必须一致。我的做法是,只有在成功写完一个记录后,才去更新内存中的写指针。修改页状态(如从ACTIVE改为FULL)是一个关键操作,必须在确保页内数据都稳定后再进行。

3.2 数据写入流程:追加记录与页切换

写入数据是核心操作,它不仅仅是“写Flash”,更包含了状态判断和空间管理。

bool FLASH_EEPROM_Write(uint16_t virtual_addr, uint32_t data) { flash_record_t new_record; uint32_t record_size = sizeof(flash_record_t); uint32_t next_seq_num; // 1. 为该虚拟地址分配下一个序列号 next_seq_num = get_next_sequence_number(virtual_addr); // 2. 封装新记录 new_record.virtual_addr = virtual_addr; new_record.seq_num = next_seq_num; new_record.data = data; new_record.crc16 = calculate_crc16((uint8_t*)&new_record, record_size - 2); // 3. 检查当前ACTIVE页剩余空间是否足够 if (g_write_offset + record_size > FLASH_PAGE_SIZE) { // 当前页已满,需要切换页 if (!switch_to_next_page()) { return false; // 切换失败(如无可用页且回收失败) } } // 4. 执行Flash写入 HAL_FLASH_Unlock(); // 解锁Flash写保护 if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, g_active_page_addr + g_write_offset, *(uint64_t*)&new_record) != HAL_OK) { HAL_FLASH_Lock(); return false; } HAL_FLASH_Lock(); // 5. 更新内存中的管理信息 g_write_offset += record_size; update_virtual_addr_map(virtual_addr, next_seq_num, g_active_page_addr + g_write_offset - record_size); return true; }

页切换函数switch_to_next_page的要点:

  1. 将当前ACTIVE页的状态标记为FULL。
  2. 检查是否有EMPTY页。如果有,将其状态标记为ACTIVE,并更新g_active_page_addr和g_write_offset。
  3. 如果没有EMPTY页,则触发垃圾回收(Garbage Collection)。

3.3 垃圾回收机制:提升寿命的关键

垃圾回收是算法中最精妙的部分,目标是回收那些已经“过时”(里面所有记录都有更新版本在其他页)的页,将其擦除变为EMPTY页。

static bool perform_garbage_collection(void) { uint32_t page_to_erase; // 1. 选择要回收的页。策略:选择最早被标记为FULL或OBSOLETE的页。 page_to_erase = select_page_for_erasure(); if (page_to_erase == 0) { return false; // 没有合适的页可回收(理论上不应该发生,除非所有数据都是最新的) } // 2. 关键步骤:搬迁有效数据。 // 遍历待擦除页中的所有记录,对于每个记录,检查其是否为该虚拟地址的最新记录。 // 如果不是,则忽略(因为它已过时)。如果是,则必须将其写入到当前的ACTIVE页! // 这一步确保了有效数据不会丢失。 if (!relocate_valid_records(page_to_erase)) { return false; } // 3. 数据搬迁完成后,将该页标记为OBSOLETE(可选,也可直接擦除) mark_page_as_obsolete(page_to_erase); // 4. 执行页擦除 HAL_FLASH_Unlock(); if (HAL_FLASHEx_Erase(&pEraseInit, &PageError) != HAL_OK) { HAL_FLASH_Lock(); return false; } HAL_FLASH_Lock(); // 5. 将擦除后的页状态标记为EMPTY,并加入空白页列表 format_page_as_empty(page_to_erase); return true; }

垃圾回收的注意事项:

  • 搬迁风暴:在最坏情况下,一个页里可能所有记录都是最新的(比如设备刚初始化后第一次写满一页)。此时垃圾回收需要搬迁大量数据,会阻塞系统较长时间。对于实时性要求高的系统,需要将搬迁操作分步进行,或者选择有效数据最少的页进行回收。
  • 断电保护:在垃圾回收的“标记-搬迁-擦除”过程中,任何一步断电都可能导致数据不一致。一种增强策略是引入“事务日志”或使用两套页状态标记,但会增大复杂性和Flash开销。对于百万次寿命的场景,发生断电时正好在进行垃圾回收的概率极低,通常可以接受基础方案的风险。

3.4 数据读取流程:查找最新记录

读取操作相对简单,但效率需要考虑。最直接的方法是每次读取都遍历所有页的所有记录,找到对应virtual_addr且seq_num最大的记录。这在记录数不多时可以接受。

bool FLASH_EEPROM_Read(uint16_t virtual_addr, uint32_t *data) { flash_record_t *p_record; uint16_t max_seq_num = 0; uint32_t found_addr = 0; bool found = false; // 遍历所有页(可以从后向前遍历,因为新数据通常在更后面的地址) for (int page_idx = 0; page_idx < FLASH_EEPROM_PAGE_NUM; page_idx++) { uint32_t page_start = FLASH_EEPROM_START_ADDR + page_idx * FLASH_PAGE_SIZE; uint32_t offset = sizeof(flash_page_header_t); while (offset < FLASH_PAGE_SIZE) { p_record = (flash_record_t *)(page_start + offset); // 检查记录头魔数或CRC,判断是否为一个有效记录 if (!is_valid_record(p_record)) { offset += sizeof(flash_record_t); continue; } if (p_record->virtual_addr == virtual_addr) { if (p_record->seq_num > max_seq_num) { max_seq_num = p_record->seq_num; *data = p_record->data; found = true; } } offset += sizeof(flash_record_t); } } return found; }

读取性能优化: 在初始化时rebuild_virtual_addr_map(),就是在内存(RAM)中建立一个哈希表或数组,将virtual_addr映射到其最新记录的物理地址和seq_num。这样,读取操作就变成了O(1)复杂度的内存查找,速度极快。代价是消耗一些RAM,并在每次写入后需要更新这个映射表。对于STM32G071(16KB RAM)和几十个参数的情况,这点开销完全可以接受。

4. 关键参数配置与寿命估算

设计完成后,我们需要定量评估这个方案是否能达到“百万次”级别的寿命。这取决于几个关键参数:

  1. Flash物理擦写次数(Endurance):以STM32G071为例,其数据手册标称的Flash擦写寿命通常是10万次(100k cycles)。这是每个物理扇区/页的保证值。
  2. 模拟EEPROM总大小(Total_Size):我们分配的Flash总字节数。
  3. 页大小(Page_Size):STM32G071是2KB。
  4. 记录大小(Record_Size):包含头信息和数据的完整记录长度,假设为16字节。
  5. 用户数据更新频率:假设最频繁更新的一个参数,每秒更新1次。

寿命估算过程:

  • 每页可存储记录数:Records_Per_Page = Page_Size / Record_Size = 2048 / 16 = 128个。
  • 写放大系数(Write Amplification):由于垃圾回收时需要搬迁有效数据,实际物理写入量会大于逻辑写入量。在最坏情况下(每次垃圾回收都搬迁整页数据),写放大系数接近2。在平均情况下,假设为1.5。
  • 等效擦除一次页面对应的逻辑写入次数:一次页擦除对应着该页被逻辑写满。因此,Logical_Writes_Per_Erase = Records_Per_Page = 128次。
  • 系统总逻辑写入次数:Total_Logical_Writes = Flash_Endurance * Logical_Writes_Per_Erase * Number_Of_Pages / Write_Amplification。
    • 假设我们使用4页(8KB):Total_Logical_Writes = 100,000 * 128 * 4 / 1.5 ≈ 34,133,333次。
  • 对单一地址的写入寿命:系统总逻辑写入次数需要分配到各个虚拟地址上。如果只有1个地址频繁写入,那么它就能独占这约3400万次写入。如果频繁写入的地址有10个,那么每个地址的寿命约为340万次。如果频繁写入的地址有100个,每个地址的寿命约为34万次。

结论:通过分配4页Flash,在最坏写放大1.5倍的情况下,即使有10个参数需要每秒更新1次,也能连续工作约340万秒 ≈ 39天才耗尽寿命。而实际物联网设备参数更新频率远低于此(可能几分钟甚至几小时一次),因此实现“百万次级别”的寿命绰绰有余,甚至可以达到千万次。

重要提示:上述估算是理论值。实际寿命受以下因素影响:

  1. 温度:高温会显著降低Flash寿命。
  2. 数据模式:全0xFF擦除后写入特定模式,其磨损程度可能不同。
  3. 均衡磨损:上述基础算法并未主动实现全池的均衡磨损。select_page_for_erasure()函数如果总是选择最早的满页,会导致前面几页磨损更快。更高级的算法可以记录每页的擦除计数,优先选择擦除次数最少的页进行回收。

5. 实战调试与问题排查实录

在STM32G071上实现并调试这套系统时,我遇到了几个典型问题,这里分享出来供大家避坑。

5.1 Flash编程对齐与数据类型问题

问题现象:调用HAL_FLASH_Program写入数据后,读取出来的值不正确,或者直接导致HardFault。排查过程:

  1. 检查地址对齐。STM32G071的Flash编程要求64位(双字)对齐。我的记录结构体是16字节,本来是自然对齐的,但在计算写入地址g_active_page_addr + g_write_offset时,必须确保这个地址是8的倍数。
  2. 检查解锁序列。必须在编程前调用HAL_FLASH_Unlock(),编程后调用HAL_FLASH_Lock()。且不能嵌套。
  3. 检查编程函数的数据类型。HAL_FLASH_Program的第一个参数是编程类型(FLASH_TYPEPROGRAM_DOUBLEWORD),第二个参数是地址,第三个参数是uint64_t类型的数据。我需要将我的flash_record_t指针强制转换为uint64_t*,并一次写入8个字节。由于记录是16字节,需要分两次调用HAL_FLASH_Program。

解决方案:

// 正确的写入方式 uint64_t *p_data = (uint64_t*)&new_record; HAL_StatusTypeDef status; status = HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, target_addr, p_data[0]); if (status == HAL_OK) { status = HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, target_addr+8, p_data[1]); } if (status != HAL_OK) { // 错误处理 }

同时,在定义存储池起始地址FLASH_EEPROM_START_ADDR时,必须确保其是8字节对齐的,通常可以指定到某个页的起始地址,这本身就是对齐的。

5.2 中断与擦写时序冲突

问题现象:在频繁写入Flash时,系统偶尔会死机或数据错乱。排查过程:Flash擦写操作期间,CPU会暂停执行指令(因为Flash总线被占用)。如果此时发生了中断,而中断向量表也存放在Flash中,CPU就无法取指响应中断,导致程序跑飞。解决方案:

  • 在执行Flash擦写操作(HAL_FLASHEx_Erase)和编程操作(HAL_FLASH_Program)的关键段内,必须关闭全局中断。
  • HAL库的擦写函数内部可能已经处理了部分中断屏蔽,但为了绝对安全,我通常在调用前后加上:
    __disable_irq(); // ... Flash操作 ... __enable_irq();
  • 注意,关闭中断的时间应尽可能短。一次双字编程很快,但一次页擦除可能需要几十毫秒。长时间关闭中断会影响系统实时性。因此,垃圾回收这类耗时操作最好放在低优先级任务或主循环的空闲时段进行。

5.3 电源跌落导致数据损坏

问题现象:设备意外断电重启后,发现某些参数恢复了旧值,或者整个模拟EEPROM区域无法初始化。排查过程:这通常发生在写入记录或更改页状态的过程中断电。例如,刚写完记录数据,还没来得及更新页头的状态指针,或者正在搬迁数据时断电。解决方案:

  1. 增强记录完整性校验:除了CRC16,可以在记录头添加一个固定的“魔数”(Magic Number),如0x55AA。初始化时,只有同时具备正确魔数和CRC的记录才被认可。
  2. 状态变更原子化:页状态的变更应尽可能一步完成。例如,将页状态从ACTIVE改为FULL,只需要写入一个双字(64位)。STM32的Flash编程可以保证这个64位写入是原子的(要么全成功,要么全失败)。
  3. 引入预写日志(WAL):对于更苛刻的应用,可以在另一个Flash页开辟一个小区域作为日志。在修改主要数据区之前,先把要做的操作(如“将地址0x0001的值改为123”)写到日志页。操作完成后再清除日志。上电恢复时,检查日志页,完成未完成的操作。这会增加复杂性和写放大,但安全性最高。

5.4 常见问题速查表

问题现象可能原因排查步骤与解决方案
写入后读回数据错误1. 地址未8字节对齐
2. 未正确解锁Flash
3. 数据长度非8字节倍数,尾部处理错误
1. 检查并确保写入地址(addr & 0x7) == 0
2. 确认调用了HAL_FLASH_Unlock()且返回成功
3. 对于非8倍数字节的数据,填充0xFF并分次写入
系统在擦写Flash时死机1. 擦写期间未关闭中断
2. 擦写函数超时
1. 在擦写操作前后调用__disable_irq()和__enable_irq()
2. 检查擦除参数是否正确,页地址是否有效
初始化时卡住或进入错误状态1. Flash内容被意外修改,状态字异常
2. CRC校验大量失败,遍历逻辑陷入死循环
1. 在初始化代码中增加默认状态处理,遇到非法状态时格式化该页
2. 在遍历记录循环中增加超时或最大记录数限制
频繁写参数后,参数丢失1. 垃圾回收失败,导致无空白页可用
2. 序列号溢出处理不当
1. 检查垃圾回收函数perform_garbage_collection的返回值,并增加日志
2. 为seq_num设置合理的位宽(如16位),溢出后从0开始,但需确保能区分新旧记录(结合页新旧判断)
读取速度慢每次读取都全盘遍历在RAM中建立“虚拟地址->最新记录物理地址”的查找表,在初始化rebuild_virtual_addr_map和每次写入后更新该表

6. 进阶优化与扩展思路

实现基础功能后,还可以从以下几个方向进行优化,以适应更复杂或更极端的场景。

6.1 磨损均衡算法优化

基础算法中,页的回收顺序是FIFO(先写满的先回收),这可能导致前面几页的磨损远高于后面几页。为了实现更均匀的磨损,可以引入“擦除计数”。

  • 在每页的页头信息中,增加一个erase_count字段。
  • 每次页被擦除后,将该计数加1(注意,写入erase_count本身也是一次Flash操作,需要小心设计以避免无限递归)。
  • 在select_page_for_erasure()函数中,优先选择erase_count最小的满页进行回收。
  • 这样,擦除次数少的页会被优先使用,从而让所有页的磨损程度趋于一致,最大化整体Flash寿命。

6.2 掉电保护增强策略

对于数据安全性要求极高的场景,可以采用“双状态位”或“预写日志”策略。

  • 双状态位:每个页的状态用两个不相关的Flash字来表示,例如status_a和status_b。只有在两个状态字都成功写入新值后,状态变更才被认为生效。上电恢复时,如果两个状态字不一致,则通过预定义的规则(如选择序号大的)来确定最终状态,或者执行恢复操作。
  • 预写日志:如前所述,在另一个小区域记录操作日志。这相当于一个简易的文件系统日志,安全性最高,但实现也最复杂。

6.3 适配不同容量与频率需求

这套算法框架是通用的,可以通过宏定义轻松适配不同需求:

  • 小数据量、低频更新:可以减少预留页数(如2页),减小记录头大小(比如只用8位序列号),以节省Flash空间。
  • 大数据量、高频更新:可以增加预留页数(如8页或更多),减少垃圾回收的频率。同时,可以考虑将记录头与数据分离,将频繁更新的元数据(如序列号)放在RAM中,仅定期备份到Flash,但这会牺牲掉电保存性。
  • 多数据类型:当前的data字段是uint32_t。可以将其定义为一个联合体(union),以支持存储浮点数、数组或结构体。
    typedef union { uint32_t u32; float f32; uint8_t bytes[4]; } data_field_t;
    在记录结构体中,可以再增加一个data_type字段来标识数据类型。

6.4 与文件系统或键值数据库结合

对于更复杂的应用,需要管理大量且结构多样的非易失性数据,可以在此模拟EEPROM的基础上,构建一个轻量级的键值存储(Key-Value Store)。

  • 将virtual_addr扩展为更灵活的“键”(Key),可以是一个字符串哈希值。
  • 值(Value)可以是任意长度的二进制数据,通过多个连续的记录来存储。
  • 实现基本的put(key, value)、get(key, &value)、delete(key)接口。
  • 这相当于实现了一个极简的、针对Flash特性优化的嵌入式数据库,非常适合物联网设备的配置管理。

经过在STM32G071上的实际测试,这套轮询式Flash模拟EEPROM的方案完全满足项目需求。在预留4页Flash(8KB)、每秒写入1次最频繁参数的极限测试下,连续运行了两个月未出现任何数据错误或Flash失效。对于一般的物联网设备参数存储(几分钟到几小时更新一次),其寿命足以支撑设备全生命周期的使用。整个方案的核心价值在于,它用软件算法的复杂性,换取了硬件成本的节约和系统可靠性的提升,这种权衡在成本敏感的嵌入式产品中是非常经典的思路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询