
工业控制器最容易被轻视的往往是数据存储。以前接手过一套设备调试时一切正常一旦跑现场就出现参数丢失、日志残缺最后排查下来不是协议写错而是从一开始就把所有数据都塞进了一块NOR Flash。后来换了STM32FPGA这套架构我认真把存储需求拆开看才意识到“分级存储”不是玄学而是工业设备的生存刚需。这一篇是硬件篇的第12篇就把我实际做过的EEPROM / NOR Flash / SD卡三级存储方案完整梳理出来。从需求拆解、器件选型、电路设计到写代码时容易踩的坑一并讲清楚。适合正在做STM32FPGA平台、或者准备给老产品升级存储架构的工程师参考。1. 分级存储需求拆解一块Flash搞不定的原因1.1 工业控制器里到底有哪些数据要存很多人一上来就谈存储器型号我建议先聊数据。工业控制器里的数据按访问频率、数据量、可靠性要求可以分成四类。第一类是运行参数。比如PID系数、校准零点、设备地址、报警阈值这类数据通常只有几KB到几十KB改动次数不算频繁但绝不能丢而且往往需要单字节修改。设备断电后参数要保留十年以上。第二类是程序与逻辑配置。STM32的固件、FPGA的bitstream、上电初始化脚本这些文件往往是MB级别一年升级几次。第三类是运行日志和采样快照。故障时刻的电压波形、通信报文、传感器原始数据数据量可能每天几GB需要循环覆盖。第四类是临时缓冲。FPGA采集的高速数据流要先放进一个缓冲区再交给STM32处理或转发。这里最核心的矛盾在于一种存储介质无法同时满足“快速随机改写”“大容量连续写入”“掉电绝对可靠”“成本可控”四个条件。EEPROM可以单字节擦写且寿命高但容量太小NOR Flash可以随机读、XIP但擦除粒度大SD卡容量大但写入延迟不确定。把参数塞进SD卡每次上电都初始化文件系统既慢又容易损坏把日志写进NOR Flash那点容量几天就写穿。所以必须分级。1.2 为什么STM32FPGA架构让存储设计更复杂STM32FPGA的典型分工是STM32跑通信协议、任务调度、参数维护FPGA干高速采集、并行信号处理、实时控制。这个架构的存储挑战在于两边都要读写存储介质而且对时序的容忍度完全不一样。我在实际项目里遇到过两种情况。一种是FPGA直接控制存储芯片比如把采样数据实时写入NOR Flash或者SDRAM缓存这时候存储时序必须和FPGA逻辑时钟严格对齐稍微有点毛刺就会导致数据错位。另一种是FPGA先把数据放入双口RAM或FIFOSTM32再从缓冲区取出数据写入SD卡。这种方案更灵活但需要处理好跨时钟域和握手信号不然在高速采集下极易丢数据。另外STM32自身也有片内Flash和RAM有些工程师就想把参数存进STM32内部Flash省掉外部EEPROM。这件事不是不能做但要注意两个问题一是STM32内部Flash擦写次数通常只有一万次左右工业设备每天频繁修改参数几个月就可能磨掉一个扇区二是固件升级时如果误擦Flash程序就没了。所以即便是STM32内部Flash可用我仍然会在设计上给它安排一个专用的外部EEPROM用于最关键的参数保存。2. EEPROM小参数与大寿命需求的答案2.1 选型和容量考量EEPROM在整个存储体系里承担的是“最后一道防线”的角色。选型我一般看三个指标接口、容量、擦写寿命。接口方面I2C接口的24C系列用得最多两根线就能挂多个设备STM32硬件I2C或者模拟I2C都可以。如果对速度有要求SPI接口的EEPROM如25AA系列会更快但工业场景下参数操作频率其实不高I2C的400kbps完全够用而且I2C的电气连接更简单抗干扰能力也不差。容量上我习惯按“设备地址表校准参数工作参数日志索引”四个区块估算。平均一个参数用8字节一台控制器实际用的参数往往不到1000个所以64Kbit8KB是性价比最高的起步档位也就是24C64。如果设备组网复杂需要存多台从机配置可以直接选256Kbit的24C256没必要在容量上抠门EEPROM的成本差异很小。寿命这一项工业级EEPROM的常规标称是100万次擦写。请注意这指的是“每个字节或每页”的擦写次数不是整芯片。如果软件不做损耗管理每次都往同一个地址写寿命再长也会提前报废。所以EEPROM的核心设计不是选芯片而是怎么写。2.2 读写设计与避坑我在EEPROM读写上踩过不少坑最典型的三个问题地址越界导致丢数据、页写溢出导致写错地址、掉电瞬间写入导致数据不完整。逐一说明。I2C EEPROM的地址是线性连续的但写入有页边界限制。比如24C64的页大小是32字节写入时如果跨页芯片不会自动换页而是回卷到当前页开头把之前的数据覆盖。刚入门时我写过一段写入函数没做页边界判断结果每写满32字节就从页内偏移0处开始覆盖最后调试发现数据错乱表情直接凝固。正确做法是每次写入前计算“目标地址到当前页末尾的距离”如果剩余长度超过这个距离就拆成多次写操作。这个逻辑可以用一个while循环完成但注意每次写页间隔要有5ms左右的写入周期延时不然连续写会失败。第二个坑是掉电保护。EEPROM写入过程中如果掉电可能产生两种情况写入的数据损坏或者因为内部状态机混乱导致芯片暂时不可用。我的方案是把关键参数做成双备份分别存到两个地址区域。启动时先读主区校验CRC通过就用主区校验失败再读备份区如果备份区正确就恢复主区同时产生一条预警记录。这样即使掉电发生在两个区域之间的瞬间也至少有一份是完整的。第三个坑是日志索引频繁更新。如果我每天记录一千条事件每条事件都要在EEPROM里更新头指针那么这个地址就要承受很高的擦写次数。解决办法是不要用固定地址存储指针而是采用循环递增方式指针写到一个地址下一个事件写到下一个地址满了就从头开始同时用一个额外的“魔数”标识有效块。这样所有块均匀磨损寿命可以提升接近容量/块数倍。下面给一段用STM32 HAL库模拟I2C读取EEPROM的示例框架重点在页写判断部分。#define EEPROM_I2C hi2c1 #define EE_PAGE_SIZE 32 HAL_StatusTypeDef EEPROM_WriteBytes(uint16_t addr, uint8_t *buf, uint16_t len) { uint16_t localAddr addr; uint16_t remain len; uint8_t *p buf; while (remain 0) { uint16_t pageOffset localAddr % EE_PAGE_SIZE; uint16_t chunk EE_PAGE_SIZE - pageOffset; if (chunk remain) chunk remain; // I2C开始设备地址目标地址数据 // 这里省略细节实际需要用HAL_I2C_Mem_Write HAL_StatusTypeDef status HAL_I2C_Mem_Write(EEPROM_I2C, EEPROM_ADDR, localAddr, I2C_MEMSIZE_16BIT, p, chunk, 100); if (status ! HAL_OK) return status; HAL_Delay(5); // 等待EEPROM内部写周期 localAddr chunk; p chunk; remain - chunk; } return HAL_OK; }这里我特意用localAddr % EE_PAGE_SIZE计算页偏移而不是固定按32字节分块这样即使基地址不是页对齐也能保证每一小片写入都不跨页。第一次写的时候觉得这个是小事后来看到生产线上连续几台设备参数乱掉才明白这种边界判断对批量生产的稳定性有多重要。3. NOR Flash固件存储与FPGA配置的“中间层”3.1 为什么用NOR Flash而不是NANDNOR Flash在工业控制器里扮演的角色很特殊它既存放STM32的固件也是FPGA的配置存储。相比NAND FlashNOR Flash的优点是支持随机读、不需要坏块管理、可以XIP直接执行代码这对启动加载非常友好。STM32内部Flash放Bootloader应用固件则需要一个大一点的介质来做升级备份。外部NOR Flash可以做到掉电不丢失读速度也满足XIP要求。FPGA这边更直接大部分FPGA芯片支持从SPI NOR Flash自动加载逻辑比如赛灵思的SPI x1/x2/x4模式或者自家厂家通用的配置方式。所以一块NOR Flash可以同时划分两个区域一个放STM32应用代码一个放FPGA的bitstream。具体如何分配取决于硬件板卡上NOR Flash的容量。我常用的型号是W25Q648MB和W25Q12816MB接口是标准SPI或QSPI引脚少布局容易。但要记住一个关键区别NOR Flash不能像EEPROM那样按字节改写。它必须先擦除把整块变为0xFF然后按页编程而擦除的最小单位通常是4KB扇区。这意味着更新一个参数可能要先把整个4KB扇区读出来在内存里修改再整扇区擦除最后写回去。所以NOR Flash适合存放“整体更新频率低、但读取频繁”的数据不适合做单点参数的频繁改写。3.2 存储布局与掉电保护我给NOR Flash做分区时会分成四个区Boot程序区、STM32应用程序区、FPGA配置区、配置文件区。布局表大约长这样分区起始地址大小内容更新方式Boot区0x000000256KBSTM32 Bootloader出厂固化App区0x0400001MBSTM32用户固件通过Bootloader升级FPGA区0x1400001MBFPGA bitstream升级时整体替换Config区0x2400001MB程序配置文件、升级备份按扇区读写把启动代码放在起始地址是硬件要求STM32从0x08000000启动后如果要跳到外部NOR执行一般需要配置外部内存映射功能。但大多数情况我们是把NOR里的数据通过Bootloader复制到STM32内部RAM或内部Flash后执行不直接XIP。这样简单很多也避免外部Flash在极寒环境下读取时序变慢导致的随机死机。配置文件区我用两套结构类似乒乓存储。同时保存当前版本和上一版本每次修改配置文件时先写入上一版本区域校验通过后更新当前版本标志。如果写入中途掉电启动时发现当前版本校验失败可以直接回退到上一版本。这个方案的代价是容量翻倍但换来的是固件升级失败也能恢复非常划算。另一个容易被忽视的问题是擦除时间。SPI NOR Flash擦除一个4KB扇区通常耗时几十到几百毫秒具体取决于芯片型号。如果代码在擦除过程中被系统调度抢占可能有风险。我的处理方式是擦除操作放在低优先级任务里或者用DMA传输命令不让CPU在那里傻等。同时对写命令加超时和状态轮询避免卡死。FPGA的bitstream加载也需要注意。FPGA上电时从NOR Flash的固定偏移读取配置数据。如果这个区域不小心被擦掉或者写坏FPGA就起不来。我在设计上给这个区域加了写保护引脚比如WP拉高禁止写入升级时再用IO控制解除。这样可以防止意外擦写。4. SD卡从“记录数据”到“数据回传”4.1 接口选型SPI模式还是SDIO模式到了SD卡这个层级目标就一个字大。工业现场的日志数据、波形记录、远程回传前的缓存没有几百MB根本扛不住。SD卡的接口有两种SPI模式和SDIO模式。SPI模式兼容性好几乎所有的MCU都支持接线就是CS、SCLK、MOSI、MISO四根线初始化逻辑也简单。缺点是速度慢即便在20MHz的SPI时钟下实测写速也很难超过1MB/s而且命令格式有特殊性部分SD卡在SPI模式下初始化不成功。SDIO模式速度快得多4位数据线可以跑几十MB/s但STM32的SDIO外设配置麻烦还要处理好DMA。如果只是记录几十Hz的传感器日志SPI足够如果要记录音频、高速波形必须SDIO。我自己做的时候优先考虑SDIO即使数据量不大也选SDIO原因是SPI模式下SD卡的年轮磨损、命令重试、以及卡兼容性问题更多。工业现场的SD卡很多是普通消费级抗恶劣环境能力差SPI模式出错后排查很费劲。使用SDIO加上CRC校验至少能快点发现问题。4.2 文件系统与可靠性设计SD卡不能直接按扇区管理要不然日志没法交给上位机分析。必须上文件系统。在STM32环境下最经典的方案是FatFS。这个库免费、源码清晰、支持FAT12/16/32和exFAT社区资料也多。文件系统不是随便挂上就完事我在项目里踩过最惨的坑是“格式化后正常写跑几天后文件打不开”。最后定位到两个原因一是日志文件始终开着长时间追加写导致FAT表频繁更新个别SD卡更新不及时会掉链子二是在写文件时突然断电FAT表和目录项损坏。为此我做了几个改动效果显著。第一个改动是分区写日志。不再让一个文件无限增长而是按天生成一个新文件文件名带日期。写满一定大小比如100MB就关闭当前文件新建下一个。这样单个文件系统的元数据操作次数大大减少。第二个改动是写入缓冲。FPGA采集数据先放在STM32内存里攒够512字节的整数倍SD卡一个扇区是512字节再一次写入文件。不能每条数据都调用f_write否则文件系统开销极大也容易打断底层写卡时序。计算一下如果1秒100条日志每条100字节那就是10KB/s。我用2KB的缓冲攒够4次再写一次每秒只调用5次写文件压力小很多。第三个改动是掉电处理的“脏区”管理。SD卡本身有写保护失效的可能尤其是在写FAT表时掉电。我采用的方法是在写日志前先把本次要写的扇区数据在NOR Flash的配置区写一份镜像写完确认后再写SD卡。如果SD卡写入失败或掉电重启后可以基于镜像重放。当然这个方案对NOR Flash容量有要求我只镜像最近几个扇区也就是最后一笔未完成写操作的局部数据代价很小。SD卡的另一个常被忽视的点是温度范围。普通SD卡在高温下容易写失败工业级SD卡价格高不少。如果产品批量出货没有预算买工业级至少要在固件里做好读写错误重试和坏扇区重映射。SD卡内部其实有磨损均衡和坏块管理但那是自带的我们无法控制。能做的就是每次写完后读回校验失败就重新初始化卡并把数据写到另一个扇区。这能挽救不少故障卡。5. 常见故障与排查实录5.1 系统性排查思路这套三级存储架构在调试阶段最烦人的是“不知道数据到底丢在哪一环”。我习惯按这条路径排查现象归类 - 介质选择判断 - 接口时序 - 电源和复位。看到“参数全部丢失”先不要怀疑EEPROM芯片坏了。先想是不是代码在上电初始化时把默认参数写回去了我遇到过有人把默认参数放在EEPROM的低地址区每次上电都执行“恢复出厂设置”结果用户改的参数一掉电就消失排查了一下午看着配置代码一脸苦笑。所以现象先说清楚是“上电丢”还是“运行一段时间丢”还是“断电后丢”对应的问题区域完全不一样。看到“日志文件损坏”先检查文件系统挂载是否正常再测SD卡的写入时序。我见过最诡异的情况STM32的SPI时钟频率设为18MHz示波器看波形完全正常但换一张卡就报错。最后发现是SPI模式命令线拉不够低把时钟降到12MHz就好了。SD卡的SPI模式协议规定命令帧的CS低电平时间是有最小值的有些卡在高速下不配合。电源是更大的隐形杀手。SD卡在写入时电流尖峰可以到100mA以上如果板卡上的3.3V LDO余量不足电压跌落会直接导致写卡失败。同样的道理适用于NOR Flash擦除瞬间。我处理过一块系统的故障表现为“FPGA偶尔加载失败”最后用示波器抓到NOR Flash擦除时电源线上有400mV的跌落。加固去耦电容后问题彻底消失。5.2 典型问题速查表异常现象可能原因排查动作解决措施上电后参数全部丢失EEPROM初始化代码覆盖了保存区打断点检查初始化顺序把参数加载与恢复出厂逻辑分离EEPROM写一会就不响应页写跨边界读返回值计算页偏移按页拆分写入增加延时NOR Flash擦写后重启代码损坏代码写入期间掉电检查电源添加备份区增加写保护、双备份回退FPGA加载失败概率出现Flash的配置区被意外擦写检查WP引脚、升级流程启用硬件写保护SD卡日志文件打不开文件系统未正常卸载读卡器查看目录项增加干净卸载流程或用镜像日志SD卡写速度骤降卡内碎片化或温度高检查连续擦写性能定时格式重整或换高速卡STM32与FPGA共享数据丢失跨时钟域未同步查看仿真波形使用异步FIFO或握手信号5.3 经验总结与建议把三级存储玩明白之后我对存储架构的理解越来越直接不要试图用一块芯片解决所有问题也不要觉得“存储容量大就好”。EEPROM、NOR Flash、SD卡在工业控制器里各有各的位置合理分级各司其职才是可靠的基础。我个人的建议是在做原理图之前先花半天时间把整机的数据流图画一遍标出每条数据从产生到消费的路径写入频率、大小、实时性要求。这一步做完存储方案基本就定了。另外每级存储都要设计“失效自动降级”机制EEPROM写失败系统要能报警而不是静默丢参数SD卡满了要自动删最老的日志或者切换到一个新的日志文件NOR Flash擦写失败要有重试机制和状态上报。最后还有个容易被忽略的细节调试工具。多花一点时间做一个简单的存储调试命令——比如串口输入命令读取任意地址的数据、强制擦除某个扇区、格式化SD卡并导出文件列表。这个调试命令在量产现场排查问题时比逻辑分析仪派上用场得多。我在不同项目里反复写这组命令虽然代码量不大但每次遇到现场故障都能快速定位层级省下不少熬夜时间。