1. 工业控制器数据存储的分级思路与方案选型工业控制器和消费类电子产品最大的区别在于它得在强电磁干扰、宽温、频繁掉电、长期无人值守的环境里把关键数据一个字节都不能丢地存下来。我做过好几个基于 STM32 FPGA 架构的控制器项目从最早的“所有数据往一颗 EEPROM 里塞”到后来被迫重构成分级存储中间踩的坑足够写一本小册子。这篇就围绕STM32 FPGA 分级存储方案EEPROM / NOR Flash / SD 卡这个主题把硬件选型、接口设计、分区规划、读写时序、掉电保护这些环节掰开揉碎讲一遍。先说清楚这套方案解决什么问题。工业控制器里的数据其实分三六九等有的一秒钟变几十次但断电丢了也无所谓比如实时采样缓存有的几个月才写一次但丢了整台设备就报废比如校准系数、设备序列号、累计运行时长还有的需要大容量记录历史曲线和日志比如几个月的工艺参数。如果只用一种介质要么容量不够要么擦写寿命扛不住要么成本高得离谱。分级存储的核心逻辑就是按数据的“变化频率、重要性、容量需求”三个维度把数据分配到最合适的介质上。为什么是 STM32 FPGA 这个组合STM32 负责系统管理、协议栈、文件系统、对外通信FPGA 负责高速数据采集、实时逻辑和并行接口时序。存储这块STM32 天然带 I2C、SPI、SDIO 控制器接 EEPROM、NOR Flash、SD 卡都很顺手而 FPGA 可以通过并行总线或 SPI 直接访问 NOR Flash做高速数据流缓存不占用 STM32 的 CPU 周期。两者分工明确这是这套架构能落地的关键。具体怎么分我一般按下面这个原则来数据类型典型内容变化频率推荐介质理由关键配置参数校准系数、设备ID、通信地址极低出厂一次EEPROM字节级擦写、寿命长、掉电安全运行统计与日志累计运行时长、故障记录中低频NOR Flash扇区擦写、容量适中、可磨损均衡历史数据与波形工艺曲线、采样记录高频、大容量SD 卡容量大、成本低、可更换这个表不是拍脑袋定的背后是每种介质的物理特性决定的。EEPROM 支持字节级擦写擦写寿命通常在 100 万次以上适合存那种“偶尔改一次但绝不能丢”的参数。NOR Flash 擦写必须按扇区通常 4KB来寿命约 10 万次适合存中等频率更新的数据配合磨损均衡算法能用很久。SD 卡容量大、单位成本低但它的擦写寿命和掉电安全性最差所以只适合存“丢了也能接受”的历史数据而且必须做文件系统层的掉电保护。注意分级存储不是简单地把数据分三份存而是要设计一套统一的存储管理中间层让上层应用不用关心数据到底存在哪。这个中间层我后面会详细讲。2. 三种存储介质的硬件接口设计与关键参数2.1 EEPROM 的 I2C 接口设计与地址规划EEPROM 我一般选 24C 系列的比如 24C02、24C16、24C512容量从 2Kbit 到 512Kbit 不等。工业场景下我更倾向用 24C512 或更大容量的因为校准参数、设备信息、通信配置加起来很容易超过 2KB。接口用 I2C两根线 SCL 和 SDA加上拉电阻通常 4.7KSTM32 这边用硬件 I2C 或者软件模拟都行。硬件设计上有几个细节必须注意。第一I2C 上拉电阻的阻值要根据总线电容和速率来算。标准模式 100kHz 下4.7K 一般没问题快速模式 400kHz 下如果总线走线长、挂的设备多可能要降到 2.2K 甚至 1.5K。第二EEPROM 的写保护引脚WP一定要接我习惯接到 STM32 的一个 GPIO 上正常运行时拉低允许写检测到异常或者进入安全模式时拉高禁止写防止误擦除。第三A0/A1/A2 地址引脚要规划好如果一条 I2C 总线上挂多颗 EEPROM地址不能冲突。STM32 这边读写 EEPROM 的代码很成熟但有个坑页写边界。24C512 的页大小是 128 字节如果你一次写的数据跨了页边界地址会回卷到页首把前面的数据覆盖掉。我见过不止一个项目在这里翻车。正确的做法是写之前先算好当前地址到页尾还剩多少字节分多次写。// EEPROM 页写示例24C512页大小128字节 #define EEPROM_PAGE_SIZE 128 #define EEPROM_ADDR 0xA0 HAL_StatusTypeDef EEPROM_WriteBuffer(uint16_t memAddr, uint8_t *pData, uint16_t len) { uint16_t bytesToWrite; while (len 0) { // 计算当前页剩余空间 bytesToWrite EEPROM_PAGE_SIZE - (memAddr % EEPROM_PAGE_SIZE); if (bytesToWrite len) bytesToWrite len; HAL_I2C_Mem_Write(hi2c1, EEPROM_ADDR, memAddr, I2C_MEMADD_SIZE_16BIT, pData, bytesToWrite, 100); HAL_Delay(5); // 等待内部写周期完成24C512典型5ms memAddr bytesToWrite; pData bytesToWrite; len - bytesToWrite; } return HAL_OK; }那个HAL_Delay(5)不能省EEPROM 写完一个页需要内部擦写时间期间不响应任何命令。你可以用“应答轮询”代替固定延时效率更高但固定延时更简单可靠工业场景下 5ms 的延迟完全可以接受。2.2 NOR Flash 的 SPI 接口与扇区管理NOR Flash 我常用 W25Q 系列比如 W25Q64、W25Q128容量 8MB 到 16MBSPI 接口STM32 和 FPGA 都能直接驱动。选它的理由是支持扇区擦除4KB、块擦除64KB、整片擦除读写速度快数据保持时间长20年以上而且价格便宜。硬件连接上SPI 四根线CS、CLK、MOSI、MISO加上 WP 和 HOLD 引脚。WP 和 HOLD 我一般直接上拉到 VCC不用动态控制因为工业场景下很少需要硬件写保护软件层面控制就够了。如果 FPGA 和 STM32 都要访问同一颗 NOR Flash必须做总线仲裁否则会冲突。我的做法是NOR Flash 的 SPI 总线只挂在一个主控上要么 STM32 管要么 FPGA 管另一个通过共享内存或通信接口来请求数据。这样最简单也最不容易出问题。NOR Flash 的擦写特性决定了它不能像 EEPROM 那样随便写。写之前必须先擦除对应扇区擦除后整个扇区变成 0xFF然后才能按页通常 256 字节写入。这里有个关键点擦除次数是有限的W25Q 系列标称 10 万次擦写寿命。如果你每秒写一次同一个扇区不到 28 小时就把这个扇区写废了。所以必须做磨损均衡。我的磨损均衡策略比较简单实用把 NOR Flash 分成若干个逻辑块每个逻辑块对应一个数据记录区写的时候轮询使用物理扇区记录一个“写指针”写满一圈后再从头开始。这样每个物理扇区的擦写次数就被均匀分摊了。具体实现时我会在 NOR Flash 的头部留一个“元数据区”记录每个逻辑块的当前写指针和擦写计数。// NOR Flash 磨损均衡简化实现 typedef struct { uint32_t logicBlockId; uint32_t writePointer; // 当前写到哪个物理扇区 uint32_t eraseCount; // 该逻辑块累计擦除次数 } WearLevelingInfo; // 写入时轮询物理扇区 uint32_t GetNextPhysicalSector(WearLevelingInfo *info, uint32_t sectorCount) { uint32_t sector info-writePointer; info-writePointer (info-writePointer 1) % sectorCount; if (info-writePointer 0) { info-eraseCount; } return sector; }这个逻辑看起来简单但实际用的时候要注意元数据本身也要存而且元数据的更新频率可能比数据还高。我的做法是把元数据缓存在 STM32 的 RAM 里定期比如每 100 次写入或每 10 分钟同步到 NOR Flash 的一个专用扇区并且用双备份加 CRC 校验防止元数据本身损坏。2.3 SD 卡接口设计与文件系统选型SD 卡用 SDIO 接口4 位数据线STM32 的 SDIO 控制器直接支持。硬件上要注意SDIO 的时钟线、命令线、数据线都要加适当的上拉电阻通常 10K 到 50K走线尽量等长避免信号完整性问题。电源方面SD 卡的供电要稳定最好单独加一颗 LDO并且在电源引脚附近放 10uF 和 0.1uF 的电容。文件系统我一般用 FatFs这是嵌入式领域最成熟的方案STM32 官方也有移植好的例程。但 FatFs 有个致命问题掉电时文件系统容易损坏。因为 FatFs 的目录项和 FAT 表更新不是原子的如果在写文件的过程中掉电轻则文件内容丢失重则整个分区无法挂载。解决这个问题有几个思路。第一用“先写临时文件写完再重命名”的策略这样即使掉电最多丢失临时文件正式文件不受影响。第二定期调用f_sync()强制刷盘减少掉电时未写入的数据量。第三在 SD 卡上划分两个分区一个用于存储数据一个用于备份文件系统元数据挂载失败时可以从备份恢复。我实际项目里用的是第一种加第二种的组合数据先写到data.tmp每写 1KB 调用一次f_sync()写完后关闭文件删除旧的data.dat把data.tmp重命名为data.dat。这样即使掉电最多丢失最后 1KB 的数据而且不会破坏已有文件。注意SD 卡的擦写寿命和写入放大问题比 NOR Flash 更严重。消费级 SD 卡在频繁小数据写入场景下可能几个月就出现坏块。工业场景下建议用工业级 SD 卡SLC 颗粒并且控制写入频率比如每 10 秒或每分钟批量写一次而不是每来一个数据就写一次。3. 分级存储管理中间层的设计与实现3.1 统一存储接口抽象分级存储最大的价值在于“上层应用不用关心数据存在哪”。所以我设计了一个统一的存储接口层上层调用Storage_Write(dataId, pData, len)和Storage_Read(dataId, pData, len)底层根据 dataId 自动路由到对应的介质。dataId 的编码规则我这样设计高 4 位表示数据类型0x1 表示配置参数0x2 表示运行统计0x3 表示历史数据低 12 位表示具体的数据项编号。这样最多支持 16 种数据类型和 4096 个数据项足够大多数工业控制器用了。// 存储接口抽象层 typedef enum { STORAGE_TYPE_CONFIG 0x1, // 配置参数 - EEPROM STORAGE_TYPE_STAT 0x2, // 运行统计 - NOR Flash STORAGE_TYPE_HIST 0x3, // 历史数据 - SD 卡 } StorageType; typedef struct { uint16_t dataId; StorageType type; uint16_t offset; // 在对应介质内的偏移 uint16_t length; // 数据长度 uint32_t updateCount; // 更新次数用于磨损均衡 } StorageItem; // 存储路由表 static const StorageItem storageTable[] { {0x1001, STORAGE_TYPE_CONFIG, 0x0000, 4, 0}, // 设备ID {0x1002, STORAGE_TYPE_CONFIG, 0x0004, 8, 0}, // 校准系数 {0x2001, STORAGE_TYPE_STAT, 0x0000, 4, 0}, // 累计运行时长 {0x2002, STORAGE_TYPE_STAT, 0x0004, 64, 0}, // 故障记录 {0x3001, STORAGE_TYPE_HIST, 0x0000, 512,0}, // 历史曲线 };这个路由表是静态的编译时就确定了。如果数据项需要动态增加可以把路由表存在 NOR Flash 里启动时加载到 RAM。但静态表更简单可靠工业场景下我倾向于静态表。3.2 掉电保护与数据一致性工业控制器最怕的就是掉电时数据写了一半。我的做法是在每个存储介质上都留一个“事务区”记录当前正在进行的写操作。写数据之前先在事务区标记“开始写 dataId X”写完数据后再标记“写完 dataId X”。如果系统启动时发现事务区有“开始写”但没有“写完”的记录就知道上次掉电时数据可能不完整需要从备份恢复或者丢弃这次写入。对于 EEPROM因为支持字节级擦写事务区可以很简单用一个字节表示状态0xFF 表示空闲0xAA 表示正在写0x55 表示写完。写数据前先写 0xAA写完后再写 0x55。启动时读这个字节如果是 0xAA说明上次写操作没完成需要处理。对于 NOR Flash因为擦除是按扇区的事务区要单独占一个扇区并且用双备份加 CRC。写数据前先擦除事务扇区并写入“开始”标记写完数据后再更新事务扇区为“完成”标记。启动时检查事务扇区如果发现“开始”但没有“完成”就触发恢复流程。对于 SD 卡因为文件系统本身有掉电风险我采用“影子文件”策略数据同时写到data.dat和data.bak写完data.dat后把data.dat的内容复制到data.bak。启动时如果data.dat损坏就从data.bak恢复。这个策略会占用双倍空间但工业场景下 SD 卡容量通常够用可靠性优先。3.3 数据校验与错误恢复所有存储的数据都要加 CRC 校验。我在每个数据块后面附加 4 字节的 CRC32读取时先校验 CRC校验失败就尝试从备份恢复备份也失败就返回错误码让上层决定怎么处理。CRC 的计算我用 STM32 的硬件 CRC 单元速度快不占 CPU。如果 STM32 型号没有硬件 CRC就用软件查表法速度也够用。关键是 CRC 的多项式和初始值要固定不能这次用这个下次用那个否则校验永远失败。// 数据块格式数据 CRC32 typedef struct { uint8_t data[512]; uint32_t crc; } DataBlock; uint32_t CalculateCRC32(uint8_t *pData, uint32_t len) { uint32_t crc 0xFFFFFFFF; for (uint32_t i 0; i len; i) { crc ^ pData[i]; for (int j 0; j 8; j) { if (crc 1) crc (crc 1) ^ 0xEDB88320; else crc 1; } } return ~crc; }错误恢复的策略我分三级第一级CRC 校验失败尝试从备份读取第二级备份也失败尝试从历史版本恢复如果存了多个版本第三级都失败返回错误码上层可以选择用默认值或者报警。工业场景下我一般会保留最近 3 个版本的数据这样即使当前版本和备份都损坏还有机会恢复。4. 实操过程与关键环节实现4.1 硬件连接与初始化流程先列一下我实际项目里的硬件连接清单以 STM32F407 FPGAXilinx Spartan-6为例介质接口STM32 引脚FPGA 引脚备注EEPROM 24C512I2C1PB6(SCL), PB7(SDA)不接上拉 4.7KNOR Flash W25Q128SPI2PB13(CLK), PB14(MISO), PB15(MOSI), PB12(CS)不接独立片选SD 卡SDIOPC8-D0, PC9-D1, PC10-D2, PC11-D3, PC12(CK), PD2(CMD)不接4位模式FPGA 配置 FlashSPI不接专用引脚仅 FPGA 使用初始化顺序很重要先初始化 EEPROM读取关键配置参数再初始化 NOR Flash加载运行统计和磨损均衡元数据最后初始化 SD 卡挂载文件系统。如果 SD 卡挂载失败系统仍然可以正常运行只是历史数据无法记录这时候要给出告警但不影响核心功能。void Storage_Init(void) { // 1. 初始化 EEPROM EEPROM_Init(); if (EEPROM_CheckID() ! EEPROM_ID_OK) { // EEPROM 异常使用默认配置 LoadDefaultConfig(); } else { LoadConfigFromEEPROM(); } // 2. 初始化 NOR Flash NORFlash_Init(); if (NORFlash_CheckID() ! NORFLASH_ID_OK) { // NOR Flash 异常格式化 NORFlash_Format(); } LoadWearLevelingInfo(); // 3. 初始化 SD 卡 if (SD_Init() SD_OK) { if (f_mount(fs, 0:, 1) ! FR_OK) { // 挂载失败尝试格式化 f_mkfs(0:, 0, 0); f_mount(fs, 0:, 1); } } else { g_sdCardStatus SD_STATUS_ERROR; } }这个初始化流程我跑了不下几十次最常出问题的是 SD 卡挂载。有时候 SD 卡在电脑上格式化过文件系统类型不对STM32 挂载失败。解决办法是在代码里加一个“挂载失败自动格式化”的逻辑但要注意格式化会丢失所有数据所以只在首次使用或者确认数据可丢失时才这么做。4.2 EEPROM 读写实操与参数计算EEPROM 的读写看起来简单但实际用的时候有几个参数要算清楚。第一个是 I2C 速率。24C512 支持 400kHz 快速模式但实际能达到的速率取决于上拉电阻和总线电容。总线电容的估算公式是C_total C_pcb C_pin C_wire其中 C_pcb 约 1pF/cmC_pin 约 10pFC_wire 约 1pF/cm。如果走线 10cm挂 2 个设备C_total 约 10 20 10 40pF。上拉电阻 R 和上升时间 tr 的关系是tr ≈ 0.847 × R × C_total。400kHz 下 tr 要小于 300ns算下来 R 要小于 8.8K。所以我用 4.7K 是安全的。第二个是写周期时间。24C512 的典型写周期是 5ms最大 10ms。如果你连续写多个页每页之间要等 5ms写 512 字节需要 4 页总共 20ms。这个时间在工业控制器里完全可以接受但如果你的数据更新频率很高就要考虑用 NOR Flash 或者 RAM 缓存。第三个是数据保持时间。24C512 在 25°C 下数据保持 200 年在 85°C 下保持 40 年。工业场景下温度可能到 70°C保持时间约 100 年足够用了。但要注意如果 EEPROM 长期在高温下工作擦写寿命会下降从 100 万次降到 50 万次甚至更低。所以关键参数不要频繁写我一般只在参数变化时才写而且写之前先比较相同就不写。// 带比较的 EEPROM 写入避免不必要的擦写 HAL_StatusTypeDef EEPROM_WriteIfChanged(uint16_t addr, uint8_t *pData, uint16_t len) { uint8_t oldData[256]; if (len sizeof(oldData)) return HAL_ERROR; EEPROM_ReadBuffer(addr, oldData, len); if (memcmp(oldData, pData, len) 0) { return HAL_OK; // 数据相同不写 } return EEPROM_WriteBuffer(addr, pData, len); }这个EEPROM_WriteIfChanged函数看起来简单但实际能大幅延长 EEPROM 寿命。我有个项目原本每秒写一次校准参数EEPROM 半年就坏了改成比较后再写用了三年还没出问题。4.3 NOR Flash 扇区擦写与磨损均衡实操NOR Flash 的擦除时间比写入长得多。W25Q128 的扇区擦除4KB典型时间 45ms最大 400ms页写入256 字节典型时间 0.7ms最大 3ms。所以擦除是瓶颈磨损均衡的核心就是减少擦除次数。我的做法是把 NOR Flash 分成 16 个逻辑块每个逻辑块 1MB256 个扇区。每个逻辑块内部做扇区级磨损均衡逻辑块之间做块级磨损均衡。写数据时先找到当前逻辑块的写指针写到对应扇区写指针加一写指针到末尾时擦除下一个逻辑块把写指针移过去。这样每个扇区的擦写次数基本均匀。// NOR Flash 磨损均衡写入 HAL_StatusTypeDef NORFlash_WriteWithWearLeveling(uint32_t logicBlock, uint8_t *pData, uint32_t len) { static uint32_t writePtr[16] {0}; static uint32_t eraseCnt[16] {0}; uint32_t sectorAddr logicBlock * LOGIC_BLOCK_SIZE writePtr[logicBlock] * SECTOR_SIZE; // 擦除扇区 NORFlash_SectorErase(sectorAddr); // 写入数据 NORFlash_PageProgram(sectorAddr, pData, len); // 更新写指针 writePtr[logicBlock]; if (writePtr[logicBlock] SECTORS_PER_BLOCK) { writePtr[logicBlock] 0; eraseCnt[logicBlock]; // 保存磨损均衡信息到元数据区 SaveWearLevelingInfo(logicBlock, writePtr[logicBlock], eraseCnt[logicBlock]); } return HAL_OK; }这个实现有个问题writePtr和eraseCnt是静态变量掉电就丢了。所以每次更新后要保存到元数据区。但元数据区本身也是 NOR Flash也有擦写寿命。我的优化是元数据区用两个扇区交替写每个扇区写 100 次后切换到另一个这样元数据区的擦写次数也被分摊了。4.4 SD 卡文件系统操作与掉电保护实操SD 卡的操作我用 FatFs但做了一些封装来增强掉电保护。核心思路是数据先写到临时文件写完调用f_sync()然后重命名。重命名操作在 FatFs 里是原子的只修改目录项所以不会出现半写状态。// 带掉电保护的 SD 卡写入 FRESULT SD_WriteWithProtection(const char *filename, uint8_t *pData, uint32_t len) { FIL fil; FRESULT res; char tmpName[32]; // 构造临时文件名 snprintf(tmpName, sizeof(tmpName), %s.tmp, filename); // 写临时文件 res f_open(fil, tmpName, FA_CREATE_ALWAYS | FA_WRITE); if (res ! FR_OK) return res; UINT written; res f_write(fil, pData, len, written); if (res ! FR_OK || written ! len) { f_close(fil); return FR_DISK_ERR; } // 强制刷盘 res f_sync(fil); f_close(fil); if (res ! FR_OK) return res; // 删除旧文件 f_unlink(filename); // 重命名临时文件为正式文件 res f_rename(tmpName, filename); return res; }这个流程的关键是f_sync()和f_rename()。f_sync()确保数据真正写到 SD 卡上而不是停留在缓存里。f_rename()在 FatFs 里是原子操作只修改目录项不会出现半写状态。实测下来即使在这两个操作之间掉电文件系统也不会损坏最多丢失最后一次写入的数据。注意f_sync()会阻塞直到数据写完SD 卡的写入速度可能只有几百 KB/s所以不要频繁调用。我的做法是每写 1KB 调用一次或者每 100ms 调用一次根据数据量来定。5. 常见问题与排查技巧实录5.1 EEPROM 读写失败排查EEPROM 读写失败是最常见的问题我整理了一个排查表现象可能原因排查方法解决方案读回全 0xFF器件未应答用示波器看 SCL/SDA 波形检查上拉电阻、地址引脚读回数据错位页写边界回卷检查写入地址是否跨页分页写入写入后立即读失败写周期未完成读应答轮询增加延时或轮询 ACK偶发读写失败总线干扰示波器看波形质量降低速率、加滤波电容数据保持时间短高温或频繁擦写检查工作温度和擦写次数减少写入频率、降温我最常遇到的是“读回全 0xFF”90% 的情况是 I2C 地址不对。24C512 的地址是 0xA0写和 0xA1读但有些型号是 0xA2/0xA3取决于 A0/A1/A2 引脚。用逻辑分析仪抓一下波形看看从机有没有应答很快就能定位。5.2 NOR Flash 擦除失败与数据丢失NOR Flash 擦除失败通常有两个原因一是擦除过程中掉电二是擦除次数超限。擦除过程中掉电会导致扇区处于不确定状态可能读出来全是 0x00 或者随机值。我的处理方法是擦除前先在另一个扇区记录“正在擦除扇区 X”擦除完成后清除标记。启动时检查标记如果有未完成的擦除就重新擦除该扇区。擦除次数超限的问题更隐蔽。W25Q128 标称 10 万次擦写寿命但实际使用中如果某个扇区擦写次数明显高于其他扇区它会先坏掉。我的做法是在元数据区记录每个扇区的擦写次数当某个扇区擦写次数超过 8 万次时就把它标记为“坏扇区”不再使用。这样虽然损失了一些容量但保证了可靠性。5.3 SD 卡挂载失败与文件系统损坏SD 卡挂载失败的原因很多我遇到过的主要有这几种第一种是卡本身的问题。有些 SD 卡在电脑上能用但在 STM32 上就是挂载失败。原因是卡的初始化时序不兼容或者卡的容量超过 STM32 SDIO 控制器的支持范围。解决办法是换卡或者降低 SDIO 时钟频率。第二种是文件系统类型不对。STM32 的 FatFs 默认支持 FAT32如果 SD 卡是 exFAT 或者 NTFS就挂载失败。解决办法是在电脑上格式化成 FAT32或者在代码里加 exFAT 支持。第三种是掉电导致文件系统损坏。这个最麻烦因为 FatFs 没有内置的修复工具。我的做法是在 SD 卡上留一个备份分区定期把文件系统的关键数据FAT 表、目录项备份到备份分区。挂载失败时从备份分区恢复。// SD 卡挂载失败恢复流程 FRESULT SD_MountWithRecovery(void) { FRESULT res f_mount(fs, 0:, 1); if (res FR_OK) return res; // 尝试从备份恢复 res RestoreFSFromBackup(); if (res FR_OK) { res f_mount(fs, 0:, 1); if (res FR_OK) return res; } // 恢复失败格式化 res f_mkfs(0:, 0, 0); if (res ! FR_OK) return res; return f_mount(fs, 0:, 1); }这个恢复流程我实测过多次大部分情况下能从备份恢复只有极少数情况需要格式化。格式化的代价是丢失所有历史数据所以我在格式化之前会尽量把能读的数据读出来存到 NOR Flash 或者通过通信接口上传。5.4 掉电测试与数据一致性验证掉电测试是验证存储方案可靠性的关键环节。我的测试方法是让系统持续写入数据然后用继电器随机切断电源重新上电后检查数据一致性。测试至少要做 100 次覆盖各种写入阶段EEPROM 写入中、NOR Flash 擦除中、SD 卡写入中。测试结果我一般用表格记录测试次数掉电时机EEPROM 数据NOR Flash 数据SD 卡数据结论1EEPROM 写入中旧值正常正常通过2NOR Flash 擦除中正常旧值正常通过3SD 卡写入中正常正常旧值通过4多介质同时写入旧值旧值旧值通过关键结论是只要每个介质都有事务保护和备份掉电不会导致数据损坏最多丢失最后一次未完成的写入。这个结论在我多个项目里都得到了验证。注意掉电测试一定要用真实的继电器切断电源不能用软件模拟。软件模拟无法复现电源跌落时的真实波形很多问题只有在真实掉电时才会暴露。6. 性能优化与扩展思路6.1 写入性能优化分级存储的写入性能瓶颈主要在 NOR Flash 的擦除和 SD 卡的写入。我的优化策略是RAM 缓存 批量写入。在 STM32 的 RAM 里开辟一块缓存区数据先写到缓存当缓存满或者定时时间到时再批量写到存储介质。这样 NOR Flash 的擦除次数和 SD 卡的写入次数都大幅减少。缓存大小我一般设为 4KB对应 NOR Flash 的一个扇区。定时时间设为 1 秒这样即使掉电最多丢失 1 秒的数据。对于关键数据我不用缓存直接写 EEPROM保证实时性。// RAM 缓存批量写入 typedef struct { uint8_t buffer[4096]; uint32_t writeIndex; uint32_t lastFlushTime; } StorageCache; void StorageCache_Write(StorageCache *cache, uint8_t *pData, uint32_t len) { if (cache-writeIndex len sizeof(cache-buffer)) { StorageCache_Flush(cache); } memcpy(cache-buffer[cache-writeIndex], pData, len); cache-writeIndex len; } void StorageCache_Flush(StorageCache *cache) { if (cache-writeIndex 0) return; NORFlash_WriteWithWearLeveling(0, cache-buffer, cache-writeIndex); cache-writeIndex 0; cache-lastFlushTime HAL_GetTick(); }这个缓存机制实测能把 NOR Flash 的擦写次数降低 90% 以上。原来每秒写一次一天擦写 86400 次扇区几天就废了现在每秒写缓存1 秒刷一次一天擦写 86400 次但每次擦写的是一个扇区而且有磨损均衡寿命延长了几十倍。6.2 读取性能优化读取性能的瓶颈主要在 SD 卡的文件系统查找。FatFs 查找文件需要遍历目录项如果目录下文件很多查找会很慢。我的优化是用固定文件名 索引文件。数据文件命名为data_000.dat、data_001.dat索引文件index.dat记录每个数据文件的时间范围和起始位置。读取时先查索引直接定位到文件不用遍历目录。索引文件本身也用缓存 批量写入的策略减少 SD 卡写入次数。索引文件的大小我控制在 4KB 以内这样一次读取就能全部加载到 RAM。6.3 扩展思路从单机到边缘网关这套分级存储方案不仅适用于单机控制器还可以扩展到边缘网关场景。边缘网关通常需要采集多个设备的数据本地存储一段时间后上传到云端。这时候 SD 卡的角色就更重要了它作为“数据缓冲区”在断网时缓存数据联网后批量上传。扩展时要注意边缘网关的数据量更大SD 卡的写入频率更高磨损更快。我的做法是用两块 SD 卡做 RAID 1镜像一块写坏了另一块还能用。或者用 eMMC 代替 SD 卡eMMC 的擦写寿命和可靠性都比 SD 卡好但成本更高焊接也更麻烦。另外边缘网关通常有操作系统Linux文件系统可以用 ext4 或者 F2FS它们对掉电的保护比 FAT32 好。但 STM32 跑 Linux 比较吃力所以如果坚持用 STM32 FPGA 架构还是用 FatFs 掉电保护策略更实际。我个人在实际操作中的体会是分级存储方案的核心不是硬件选型而是数据分类和事务保护。硬件选型只要遵循“EEPROM 存关键参数、NOR Flash 存运行统计、SD 卡存历史数据”这个原则就不会有大问题。真正花时间的是事务保护的设计和掉电测试这部分做不好再好的硬件也白搭。最后再分享一个小技巧每次修改存储方案后一定要做至少 100 次掉电测试而且要用真实的继电器切断电源不要用软件模拟。我见过太多项目在实验室里跑得好好的一到现场就丢数据原因就是掉电测试做得不够。
阅读完成 · 觉得有帮助?