工业控制器这行做久了,你会发现存储选型是个特别容易被低估的环节。早几年我见过一个同行产品,所有东西都塞一块 NAND Flash,参数、固件、日志不分家,结果客户现场频繁上下电,几个月后报“参数全部丢光”,最后排查发现日志高频写入把扇区擦写寿命提前耗尽,参数区和日志区物理上靠太近,一坏全坏。另一个极端是有人直接把 SD 卡当唯一存储介质,跑着跑着出现“单条记录神秘消失”,查了半天是掉电瞬间缓存没落地。
那工业控制器的数据到底应该怎么存?针对 STM32+FPGA 这种双芯控制架构,我今天把一套成熟的思路展开聊聊,核心就是分级存储:用 EEPROM 存关键参数、NOR Flash 存固件和 FPGA 配置、SD 卡存运行日志。会讲清楚为什么这样分,以及每类介质在工程落地时那些文档不会写、只有通电跑现场才知道的细节。
1. 存储分级不是拍脑袋:按数据的三个维度做切分
我判断一个数据该放哪,从来不只看容量,而是问三个问题:掉电丢了会怎样?每天写多少次?最大能长到多大?这三个答案基本决定了它该交给哪种介质。
1.1 工业控制器的四类典型数据
一套完整的工业控制器,存储对象少说也有四种:
- 运行参数和标定值:PID 系数、传感器零点、通信地址、设备序列号。体量通常在几 KB 以内,写入频率很低,但最要命的是掉电瞬间不能写坏。
- 固件和 FPGA 配置:STM32 的应用程序、FPGA 的 bit/rbf 文件。体量几十 KB 到几 MB,平时只读,只有升级时才写。
- 运行日志和事件记录:温度曲线、报警记录、位置追踪。体量可以到几十 MB 甚至几十 GB,写入频繁,但单条丢失的容忍度相对高。
- 掉电现场数据:伺服位置、工艺步骤、当前配方。体量小,但要求掉电瞬间必须保存成功,时效性极强。
这四类数据如果混用一个介质,几乎等于埋雷。日志高频擦写会拖垮整个物理介质的寿命,固件升级时一个误操作可能把参数区连锅端,掉电缓存机制和文件系统缓存搅在一起时,数据一致性根本没法证明。
1.2 三种存储介质的技术边界
| 介质 | 典型容量 | 最小写单位 | 擦写寿命 | 特点 | 适合什么 |
|---|---|---|---|---|---|
| EEPROM(I2C) | 2KB–1Mb | 字节 | 100 万次/字节 | 字节级改写,无需先擦除 | 参数、标定值、序列号 |
| SPI NOR Flash | 1MB–256MB | 扇区(常见 4KB) | 10 万次/扇区 | 读快、随机访问好,但写前必须先擦 | 固件、FPGA 配置、分区参数 |
| SD 卡 | 128MB–512GB | 块(512B 起) | 取决于主控和磨损均衡 | 大容量、方便导出,但掉电一致性弱 | 日志、历史曲线、导出文件 |
这里有个常见误解:EEPROM 一定是慢的。实际上 I2C 400kHz 下写一个字节也就几毫秒,配合页写缓冲,批量写 128 字节参数块完全可以在几十毫秒内完成,足够应对断电保存窗口。另外 NOR Flash 最大的价值不是容量,是它支持内存映射读,上电后把固件区像读数组一样取出来,启动过程确定性很高。SD 卡则正好相反,容量大但时序黑盒,掉电行为受内部主控影响,是最难保证可靠性的一个。
2. STM32 和 FPGA 到底谁来管存储
双芯架构里最容易犯的错是让 FPGA 直接操作存储介质。FPGA 实现 I2C/SPI/SD 控制器完全可行,Verilog 代码网上也一大堆,但从系统设计角度,这不是最优解。
2.1 实时信号链归 FPGA,存储管理归 STM32
我通常的划法是:FPGA 只碰和时序强相关的数据搬运,比如高速 ADC 采样进 FIFO,或者把位置计数器的值在掉电瞬间锁存并触发中断;真正解析参数、组织日志帧、管理文件系统、执行固件升级这些生命周期行为,全部放在 STM32 侧。
理由很直接:STM32 有现成的 I2C、QSPI、SDMMC 外设和 FatFS、校验算法等中间件,开发效率高,调试手段也多。FPGA 去实现文件系统或者完整 SD 卡协议栈,逻辑资源占用大,而且一旦存储部分出现 bug,排查范围会被迫同时覆盖 HDL 和 C 代码,双倍痛苦。让 FPGA 专注实时信号,STM32 当“存储管家”,是我见过性价比最高的分工。
2.2 总线拓扑的几点硬性要求
物理连接上,我给项目定的规则是:
- EEPROM 挂 STM32 的 I2C1,只有 STM32 读写,FPGA 不碰。
- SPI NOR Flash 挂 STM32 的 QSPI,优先开内存映射模式;如果用的 MCU 不支持 QSPI,普通 SPI 也可以,但读取速度会差不少。
- SD 卡挂 SDMMC 的四线 SD 模式,不要为了省引脚走 SPI 模式。SPI 模式兼容性差、速度上限低,工业日志量稍大就顶不住。
- FPGA 与 STM32 的数据交换用独立通道,FPGA 做 SPI 从机接收配置,另用一组并行 FIFO 接口上传高速采集数据。
有个教训值得单独提:不同可靠性层级的介质不要共用同一条总线。我见过有人为了省片选,把 EEPROM 和 NOR Flash 挂同一 SPI 总线,结果 NOR 擦除期间总线上的延迟毛刺被 EEPROM 误当成起始条件,把参数区写花。总线尽量按介质的可靠性等级隔离,片选引脚在工业产品里不值钱,值钱的是现场不半夜响电话。
2.3 FPGA 侧存储相关 Verilog 的注意点
如果你是负责 FPGA 的工程师,即使主控分工明确,FPGA 上也很可能会有自己的 I2C/SPI 控制模块,比如上电后从 EEPROM 读校准参数。这时候有几个 HDL 层面的坑需要提前规避。
I2C 状态机不要用简单的延时模拟起止时序,要用状态机严格跟踪 SCL 跳变沿和 SDA 建立保持窗口。器件地址和寄存器地址位宽必须参数化,AT24 系列是 8 位字地址,有些型号支持 16 位字地址,换型号不改参数就是数据错位。最关键的是必须有超时保护:I2C 从机拉低时钟是合法扩展,但如果从机挂了,状态机会死等,必须设计一个超时退出模式,否则整板存储链路都会被拖死。
3. EEPROM 参数存储的关键操作与防篡改设计
EEPROM 是整套存储方案里数据最金贵、但代码最容易写错的一环。我在 AT24C256 上吃过不少亏,下面这些细节都是真金白银换来的。
3.1 页写边界是 EEPROM 的第一大坑
选型时别只盯容量,页大小同样关键。AT24C256 是 64 字节页,AT24C02 只有 8 字节页。批量写一个 128 字节的参数块,用 64 字节页两轮搞定,用 8 字节页要拆 16 次,而且每次都要做页内地址对齐。工业设备的参数保存往往发生在断电前的最后几十毫秒,写入时间越短,掉电窗口越小,可靠性越高。
代码实现上,驱动函数必须处理“起始地址 + 长度跨越页边界”的情况。比如从地址 0x3F 开始写 10 字节,页大小 64,第一页只剩 1 字节空间,必须拆成两笔页写,否则芯片会从页内回卷,把不该覆盖的地址冲掉。这个 bug 很难立刻暴露,但总有一天会让你在现场抓狂。
3.2 写周期轮询和重复起始条件
EEPROM 每笔页写之后有 tWR 内部写周期,普遍在 3~5ms,期间芯片不响应任何 I2C 命令。稳妥做法是写完后用“器件地址 ACK 轮询”等待写周期结束。参考实现:
uint8_t eeprom_wait_ready(uint8_t dev_addr) { for (uint8_t i = 0; i < 100; i++) { i2c_start(); if (i2c_send_byte(dev_addr << 1) == I2C_ACK) { i2c_stop(); return 1; } i2c_stop(); delay_ms(1); } return 0; }注意读操作部分,随机读必须执行“写寄存器地址 + 重复起始条件 + 读字节”的完整序列。很多人漏掉重复起始条件,直接在器件地址后读,第一次能读到,第二次就是从错误地址取的数据了。
3.3 写保护和双区备份
参数被莫名其妙篡改,往往是 MCU 复位瞬间 GPIO 高阻态导致 I2C 总线出现伪起始条件,EEPROM 收到不完整的写命令。我在硬件上会做两重防御:I2C 引脚加 2.2kΩ 串联电阻,同时用 GPIO 控制 EEPROM 的 WP 引脚,平时拉高锁死写保护,只在真正要写参数时拉低释放。软件上再做双字备份:EEPROM 里划分 A/B 两个参数区,每次先写 A 再写 B,启动时先校验 A,失败就回退 B。一页容量的成本换掉电时半字写坏参数的容错能力,这笔账非常划算。
4. NOR Flash 分区规划与升级不掉电的工程方案
SPI NOR Flash 我常用 W25Q64 / W25Q128。它和 SD 卡的最大区别是只能 1→0 写入,必须先整扇区擦除。这意味着分区规划必须前置,量产之后再改分区表就是灾难。
4.1 一颗 16MB NOR 的分区方案
| 分区 | 地址范围(近似) | 内容 | 说明 |
|---|---|---|---|
| Bootloader | 0x000000–0x00FFFF | 引导代码 | 独立划区,应用升级不能触碰 |
| 固件 A | 0x010000–0x3FFFFF | 当前版本固件 | QSPI 内存映射读取 |
| 固件 B | 0x400000–0x7FFFFF | 待升级/回退版本 | 与 A 等大小 |
| FPGA 配置 | 0x800000–0xBFFFFF | bit/rbf 文件 | 启动时由 STM32 读出并下发 |
| 系统参数 | 0xC00000–0xCFFFFF | 网络配置等 | 按扇区读写 |
| 预留/暂存 | 0xD00000–0xFFFFFF | 临时数据 | 后期扩展 |
分区规约里有一条容易被忽略:A/B 切换标志要放到系统参数区,不要放在固件区末尾。否则固件升级时校验长度写错,就会把状态标志一起冲掉,升级失败后无法回退。
4.2 擦写流程和 QSPI 内存映射的切换陷阱
NOR Flash 驱动的基本流程是写使能 → 扇区擦除 → 轮询 BUSY → 页写 → 回读校验。不同品牌的寄存器位定义不同,W25Q 的 BUSY 是状态寄存器 bit0,WEL 是 bit1,换了其他厂商必须先看数据手册,不能照搬驱动。
使用 QSPI 内存映射时有个特别隐蔽的坑:映射模式下 Flash 对主控表现为只读内存,此时发写命令基本无效。升级流程必须先退出内存映射,切回普通 SPI 模式完成擦写,再重新映射。如果只是简单操作外部 Flash API 而忘了重新配置 QUADSPI 的地址映射,升级时数据看上去写了,实际读回的全是 0xFF,产品就成为一台“变砖”的废机。
4.3 我为什么坚持给固件做双区备份
NOR 寿命 10 万次,理论上擦写不容易坏。但工业现场高温、电压跌落、静电干扰都会让擦写过程不可控。我在产品里坚持固件 A/B 双备份,系统参数区再存三份,启动时依次回退。对固件而言,最坏情况是可以进 recovery 模式从 SD 卡恢复;对参数而言,最坏情况是恢复出厂设置。这些兜底机制平时不触发,但触发一次就能省掉一块板子甚至一次现场出差。
5. SD 卡日志存储的实用经验
日志存储是整套方案里最能体现工程功力的一部分。很多人以为把 FatFS 跑起来就完事了,直到发现设备正常运行,日志却在关键时刻缺了最重要的几行。
5.1 先想清楚要不要文件系统
如果日志量只有几 MB,我强烈建议考虑裸扇区方案:固定块号顺序写,块头带 magic 和序列号。好处是完全避开 FatFS 的目录项和 FAT 表更新,掉电后重启扫描一遍块头就能恢复位置,逻辑极其简单可靠。
但如果要把日志导出到 PC 分析、按天管理、限制总容量,就得上 FatFS。我的选择是当前维护分支的版本,开启FF_FS_MINIMIZE裁剪掉不用的长文件名和时间戳,RAM 占用能压到很低。注意 FatFS 不是事务型文件系统,它不做掉电保护,任何一次掉电落在 FAT 表或目录项更新窗口内,文件都可能损坏——这是设计日志方案时心里的底线。
5.2 掉电丢日志的真相和缓解措施
丢日志的真正原因不是卡坏了,而是“数据落盘顺序”问题。SD 卡内部有写缓存,发完写命令不意味着数据已经进了 NAND;FatFS 写完数据区后还要更新 FAT 表,数据区写好了但 FAT 表没更新,掉电后文件系统就认为这些簇仍空闲,数据等于白写。
我在项目里用的缓解手段是:
- 扇区大小设为 4096,减少每个文件写入时需要更新的 FAT 项数量。
- 每条日志帧写完立即
f_sync,不攒批。攒批虽然吞吐高,但掉电丢失窗口会线性变大。 - 日志文件固定容量循环覆盖,记录写入位置,超过上限自动回卷。这比无限追加更容易控制寿命。
- 检测到掉电信号时,在掉电处理例程里执行一次
f_sync和f_close,再等电容放完电。不能保证 100% 不丢,但能把窗口压到毫秒级。
磨损均衡这块我想泼点冷水:别指望消费级 SD 卡主控能帮你公平磨损。日志是纯顺序写,磨损集中在固定区域。要么定期整盘格式化重写文件系统,要么直接用工业级高耐用卡。这个成本不该省。
5.3 STM32+FPGA 协作下的日志数据链路
实际项目中,日志数据流是:FPGA 采样 → FIFO 暂存 → 并行总线或 SPI 交给 STM32 → DMA 进内存池 → 打包日志帧 → FatFS + SDMMC 写入 SD 卡 → 握手通知 FPGA。
这条链路最考验的是背压处理。SD 卡写入速度会波动,内部磨损均衡、温控降速都会让写入变慢。此时 STM32 的内存池会被打满,如果 FPGA 还以原速率往上传,最终会丢数据甚至干扰实时控制。我一般会在 STM32 侧做水位告警:内存池超过 70% 时拉低背压信号,FPGA 降低采样缓冲速率或丢掉低优先级日志帧并递增丢弃计数。处理好背压,比一味换快卡更有效。
6. 实测中那些坑:从现象到根因的排查链路
存储问题有个共性:很难稳定复现,往往客户现场跑几个月才突然出现。所以排查思路必须结构清晰,先判断问题类别,再针对性抓现场。
6.1 先分清数据错乱和数据丢失
如果参数全部变成 0x00 或 0xFF,优先怀疑写保护失效和复位时序,用示波器抓 I2C/SPI 引脚看 MCU 复位瞬间有没有异常起始条件。如果是一部分新一部分旧,通常和掉电写入顺序、A/B 切换逻辑有关。如果设备正常跑但数据永远停留在某个时间点,先查初始化流程:SD 卡识别失败、FatFS 挂载失败、EEPROM ACK 轮询超时,这类流程错误常常被上层吞掉。我的一个重要建议是在产品里做一个存储健康监视器,每次读写异常都累积计数,直到现场告警面板显示。这功能越早加越好,我就是当年没加,排查起来全靠猜。
6.2 三种介质的典型故障排查步骤
EEPROM 写坏且 ACK 轮询超时:先确认上电时序,EEPROM 的 VCC 必须比 I2C 总线早稳定至少 10ms;再确认 WP 引脚有没有被外部干扰意外拉低。
NOR 擦除卡在 BUSY=1:示波器量 VCC 电平和 SPI CLK 波形,排除低压擦除异常;软件加上擦除失败重试和软件复位序列(0xAB 命令),若仍卡死就要考虑换料。
SD 卡运行中掉卡:优先查 SDMMC 时钟是否被高优先级中断长期打断,以及 FatFS 多任务访问有没有加互斥锁。初始化时先用 400kHz 慢速完成 CMD0/CMD8/ACMD41,再切到高速模式,能规避大量换卡不兼容问题。
6.3 分级存储的取舍,本质是给可靠性做预算
把这三块拼在一起后你会发现,分级存储不是一个固定公式,而是根据设备的可靠性预算做的平衡。设备只保存几十个参数,SD 卡根本没有出场必要;日志量很小,NOR 划两个扇区就够了;像飞行记录仪那种高价值场景,SD 卡可能还得升级成 eMMC 或者带超级电容的掉电保护方案。每次做选型,我都会反复问自己:这份数据在什么极端场景下不能丢,在什么场景下丢了只是损失一点分析素材?把这个边界划清楚,介质选型就不纠结了。
我自己的体会是,硬件存储方案做得好的产品,往往是那些把“概率性故障”提前用结构手段堵住的产品。分级存储表面上是在选芯片、写驱动,实际上是在为整个产品生命周期铺路:产线烧录、现场升级、远程维护、售后故障分析,每一步都在消耗或者享用这套存储结构带来的红利。如果你正在设计类似的工业控制器,不妨先把数据分类清单拉出来,再决定介质,别让 SD 卡去管它不该管的参数,也别指望 EEPROM 能装下它装不下的日志。