
做工业控制器这些年被存储问题坑过太多次。参数莫名丢失、断电后日志文件打不开、固件升级到一半直接变砖这些故障的根子往往不在芯片本身而是数据放错了地方。我现在的方案是STM32FPGA组合存储侧用EEPROM、NOR Flash、SD卡三级搭配跑过几个版本后总算把这块理顺了。这一篇就展开聊聊这套分级存储方案每种介质负责什么数据、STM32和FPGA之间怎么配合、底层驱动和写策略怎么设计以及我在实测中踩过的一串坑。1. 给数据分门别类存储分层的前提是先回答存什么很多人一上来就挑芯片其实存储方案设计的起点不是器件选型而是先把手里的数据分类。工业控制器里的数据特征差异极大如果混用同一种介质要么浪费容量要么磨损过快要么掉电丢数据最后表现就是现场各种诡异故障。1.1 四类数据的不同脾气我平时习惯把控制器里的数据分成四类。第一类是配置参数与校准系数。这类数据量很小通常几十到几百字节写入频率不高但读得频繁而且掉电必须保持。典型的如设备地址、量程上下限、PID系数、传感器标定值。这类数据丢失的后果很直接——设备参数清零现场重新调试。第二类是固件镜像与启动代码。工业控制器的代码量通常从几百KB到几MB不等更新频率极低但要求启动时能快速可靠加载最好还支持现场固件升级和升级失败回退。这个需求就决定了存储介质必须支持随机读、片内执行或者快速映射且写操作要足够可靠。第三类是运行日志、报警记录、趋势数据。这类数据单条不大但会持续追加数据量从几十MB到几GB都有可能。掉电不能全丢但允许少量最近记录丢失同时要求接口规范方便上位机直接提取分析。第四类是高速采样原始数据比如AD采集的波形、视觉或振动信号。这类数据带宽非常大每秒几十兆字节都很正常写入必须用DMA或乒乓缓冲搬运对存储介质的顺序写性能和容量的要求是最高的。这四类数据的访问模式、容量需求、掉电要求完全不同。存储方案的价值不在于某一颗芯片多强而是让每一类数据都落到合适的介质上。1.2 一种介质解决所有问题贪心方案往往两头不讨好第一版方案我只用了一片SPI NOR Flash把配置参数、固件、日志全部塞进去。结果出现了几个问题配置参数每次被频繁重写地质磨损很快写几个月后参数区就读不出正确值了日志持续追加的写入又让剩余设计非常被动因为NOR Flash是按扇区擦除的小数据频繁改写带来的写放大特别严重。换成的三级方案是这样分工的EEPROM负责配置参数、校准系数这类小而金贵的数据NOR Flash负责固件镜像、启动代码和一小部分配置存储SD卡负责运行日志、报警记录、高速采样数据这类海量可追溯的数据。三级之间各有边界互相不抢活。这个分类思路也是整个方案的骨架。下面先逐个拆解每种介质的能力特点再讲STM32FPGA怎么配合把数据送到它们各自的存储位置上。2. EEPROM、NOR Flash、SD 卡的能力边界谁适合存什么我认为做存储方案必须建立对应介质脾气的认知。三颗器件虽然都能存数据但内部结构、读写特性和寿命模型完全不同选错了轻则寿命短重则现场数据直接废掉。2.1 三张介质能力对比特性EEPROMNOR FlashSD 卡典型容量2KB~1MB1MB~128MB256MB~512GB最小访问单位字节扇区擦除4KB常见块/页512B/4KB常见读速度慢I2C/SPI快SPI 50MHz普通中到快SDIO 4-bit写速度慢一字节ms级中扇区编程几十ms快顺序写MB/s级擦写寿命约100万次1万次~10万次卡内主控管理通常可承受大量顺序写掉电保持很好好好但文件系统层有风险典型接口I2C/SPISPI/QSPISDIO/SPI复杂文件系统不适用可用littlefs等FATFS/exFAT等你看这张表就能发现一个规律容量越大的介质数据组织越偏块器件本身越不关心单个字节的可靠性。EEPROM是唯一支持按字节改写的存储器件所以适合放配置参数NOR Flash读快写慢、支持XIP适合放固件SD卡扩容方便、顺序写性能好适合放日志和批量采样数据。2.2 工业环境下的额外考量选型不能只看常温指标。工业控制器经常面对宽温、强震动、频繁掉电的环境。EEPROM在宽温下数据保持能力较好温度漂移对I2C通信影响有限NOR Flash的SPI信号在长走线时容易受干扰所以布局要靠近主控信号线上加串联电阻和必要的上拉SD卡是插接件震动环境必须考虑锁卡机构和批量供应问题。另外还有一点EEPROM和NOR Flash都是焊接在板上的数据稳定性主要取决于器件本身和驱动代码SD卡是可拆卸的可能被用户拿去读卡也可能被误格式化这在设计上要有容忍度。比如关键数据不只存在SD卡还要在NOR Flash留一份副本防止卡丢失或损坏后整个历史记录归零。2.3 接口差异决定了驱动复杂度EEPROM以I2C为主接线只有两根但协议有ACK/NAK、写周期等概念NOR Flash以SPI为主需要正确执行读命令、写使能、状态轮询、扇区擦除等指令SD卡从SPI模式到SDIO 4-bit模式初始化和命令交互复杂度又上一台阶。越往下级介质驱动工作量和调试成本越高。这给方案设计带来的直接结论是EEPROM和NOR Flash驱动必须自己牢牢把控因为它们是系统运行的基本盘SD卡则建议引入文件系统层FATFS把块读写和文件管理分开代码结构清晰很多。3. STM32 和 FPGA 的分工协战计算归 FPGA存储归 STM32这套系统里FPGA负责高速数据采集和预处理STM32负责存储管理和整机逻辑。很多人会问FPGA不是也能挂SD卡吗为什么一定要让STM32来管存储这个分工考虑其实挺现实的。3.1 为什么不让 FPGA 直接管理存储FPGA擅长的是并行计算、高速接口、时序控制写一个SPI控制器去读写NOR Flash或SD卡并不难但难的是上层管理。文件系统本身就是个复杂的状态机要在FPGA里实现FATFS、冗余校验、文件分配表管理、掉电恢复逻辑资源开销非常大而且后期维护难度极高。相比之下STM32生态实在太成熟了。标准库或HAL库自带SPI、I2C、SDIO外设驱动FatFS的移植教程遍地都是配置参数的存储方式也灵活。与其让FPGA去硬啃文件系统不如让它把数据搬到STM32能够快速接收的位置剩下的事情交给MCU的软件栈。这其实是典型的让擅长的人做擅长的事。FPGA负责大量数据的高速搬运和整形STM32负责存储协议、任务调度和故障恢复。3.2 块传输协议的设计要点FPGA把数据采集打包后通过并行总线或SPI送给STM32不能靠简单的中断信号一帧帧传那样STM32很容易被中断风暴淹没。我实际用的是一个块传输协议。协议格式大致为帧头固定标识0xAA55、数据长度、数据体、CRC校验。FPGA内置一个发送FIFO当FIFO半满时向STM32发出DMA请求STM32通过DMA接收整块数据收完后在内存里做校验再转移到存储任务。FIFO深度我在项目中选的是1KB到4KB之间。选太深FPGA内部资源占用多选太浅STM32还没来及响应DMA请求就可能溢出。1KB加上双缓冲配合STM32的DMA循环模式实测下来比较稳妥。3.3 FIFO 深度、流控与超时重传流控是必须的。如果SD卡写入慢STM32来不及消费FIFO里的数据FPGA侧就需要暂停发送。我在FPGA逻辑里有一个FIFO剩余深度信号输出STM32在SD卡繁忙时拉低准备好信号FPGA就暂停发送等FIFO低于阈值再恢复。这套握手不需要太复杂但能解决大部分背压问题。超时重传也要设计。如果STM32端DMA接收超时比如发现帧头不对或CRC出错会向FPGA发一个重传命令FPGA把同一块数据重新发一遍。这套重传机制不追求高吞吐但用于工业日志场景下保证数据完整度非常有效。4. EEPROM 这一级的实现细节小参数的大讲究EEPROM虽然是三级存储里最不起眼的一级但恰恰是故障高发区。因为配置参数一旦错了整个控制器都可能跑飞。这块我的实现经验主要有三条写入状态机、双备份校验、槽位轮转。4.1 I2C 写入状态机与 tWR 等待I2C写EEPROM的时序并不复杂但阻塞式写法在工业代码里问题很大。因为写入一个字节后器件需要几毫秒的写周期tWR如果写多个参数就循环等待主控被卡住的时间会吃掉太多实时任务。我改用状态机方式将一次写入流程拆成启动、发送地址、发送数据、停止、等待tWR结束这几个步骤放到周期任务里调度。这样主循环不用死等EEPROM写的同时MCU还能干别的事。// EEPROM 写入状态机简化示例 typedef enum { EP_IDLE, EP_START, EP_WAIT_ACK, EP_SEND_ADDR, EP_SEND_DATA, EP_STOP, EP_WAIT_BUSY, EP_DONE, EP_ERROR } eeprom_state_t; void eeprom_periodic_task(void) { switch (ep_state) { case EP_START: i2c_start(); i2c_send_byte(0xA0 | (slave_addr 1)); // 器件地址写方向 ep_state EP_WAIT_ACK; break; case EP_WAIT_ACK: if (i2c_get_ack()) { i2c_send_byte(ep_mem_addr 8); // 存储地址高字节 ep_state EP_SEND_ADDR; } else { ep_state EP_ERROR; } break; case EP_SEND_ADDR: i2c_send_byte(ep_mem_addr 0xFF); // 存储地址低字节 ep_state EP_SEND_DATA; break; case EP_SEND_DATA: i2c_send_byte(*ep_data_ptr); ep_state EP_STOP; break; case EP_STOP: i2c_stop(); ep_wait_ticks 5; // 等待 tWR ep_state EP_WAIT_BUSY; break; case EP_WAIT_BUSY: if (--ep_wait_ticks 0) { ep_state EP_DONE; } break; default: ep_state EP_IDLE; break; } }如果你用的是HAL库I2C状态机可以基于HAL_I2C_Mem_Write_IT回调实现思路一样。关键是不要在每个参数上阻塞调用否则实时性很难看。4.2 双备份与CRC校验EEPROM数据最怕的不是写入失败而是写入过程中掉电。设想一下正在写第3个字节系统掉电第3个字节只写了一半整个参数区就处于不可信状态。解决这个问题的通用做法就是双备份。我把EEPROM的配置区划分为两个槽位每个槽位都保存完整的参数结构体CRC校验值。写入流程是先在上一次使用的对侧槽位写入新数据校验写入成功后再更新当前槽位的有效标志。读取时先检查主槽位CRC如果正确就使用如果主槽位CRC异常就回退读备份槽位并触发一次自动恢复主槽位的修复任务。这个做法从原理上讲有点类似Flash的垃圾回收思想代价只是EEPROM容量多花一倍但换来的是掉电瞬间数据依然可信。4.3 槽位轮转式磨损均衡EEPROM的寿命虽然高到100万次但长期频繁写入同一个地址照样会出问题尤其是日志型计数器或运行时长累计这类数据。我处理的方式是把这类高频写入数据放到一组槽位里轮转写。例如预定义8个槽位每次写入使用新槽位并在槽位头部写一个递增序列号。读取时扫描所有槽位取序列号最大的槽位作为最新数据序列号差距过大的场景做异常处理。这样每个槽位的写入次数被平均摊开寿命就变成原来的8倍。代价是搜索时间变长但对配置参数这种小数据来说几百字节的扫描能接受。5. NOR Flash 存储规划分区、文件系统和双镜像NOR Flash这级既承担固件存储又承担部分配置和日志缓存。一个好的分区规划比驱动代码本身更影响整个系统的稳定性。5.1 分区表设计以常见的W25Q12816MB为例我一般规划成这样起始地址长度用途0x000000512KBBootloader0x0800004MB固件A区0x4800004MB固件B区0x8800001MBlittlefs文件系统区配置、维护日志0x980000剩余暂存/备份区Bootloader放在最低地址上电先由它决定加载A区还是B区。A/B两个固件区容量一样升级时写入非当前区全部写完后更新启动标志。如果更新过程中掉电或写入CRC不对Bootloader自动切回旧固件区设备不至于变砖。5.2 littlefs 还是裸操作在NOR Flash上做配置存储有两种路线裸地址管理或文件系统。我最终选择了littlefs原因是它天生支持掉电安全和磨损均衡又不像FATFS那样需要为NOR Flash做很多适配工作。littlefs的关键参数有三个block_size对应Flash扇区大小、block_count、cache_size。W25Q128的扇区是4096字节所以把block_size设为4096cache_size也设为4096。另外要合理设置--block_cycles让littlefs在擦写达到阈值后自动做磨损迁移。实测下来littlefs对SPI NOR Flash的适配非常友好掉电模拟测试几百次后文件系统仍然完好。使用裸操作的话你必须自己管理磨损均衡、坏块标记、掉电恢复工程量比想象中大得多。除非你的场景极其简单比如固定地址写一行日志否则我建议直接上littlefs。5.3 A/B 固件镜像为什么是刚需工业控制器现场固件升级最怕的就是升级失败变砖。A/B镜像的成本只是Flash容量翻倍换来的是极低的运维风险。具体流程是升级工具先把新固件写入处于非激活状态的B区写完后对B区做CRC校验校验通过后修改Bootloader侧的启动标志指定下次启动加载B区系统重启后如果Bootloader发现B区校验失败或用户主动回退就自动切回A区。这个机制在整机代码里实现起来不复杂但现场收益非常大。另外Bootloader本身要非常克制除了Flash驱动、启动跳转、A/B判断之外不要在Bootloader里做太多功能否则它自己也需要升级等于引入了新的变砖风险。6. SD 卡日志记录从 FIFO 缓冲到批量落盘SD卡是三级存储里容量最大的一级也是最容易出问题的一级。问题往往不在卡本身而在写入策略如果在SD卡上一条一条地写日志不仅效率低而且掉电时很容易损坏FAT表。6.1 SDIO FATFS 的工程配置SD卡接口我走的是SDIO 4-bit模式配FATFS文件系统。工程配置上要特别注意DMA和中断的协调SDIO数据传输页尽量用DMA避免主控在数据拷贝上耗费时间DMA中断服务里只做标志位和回调真正的文件操作放到低优先级任务里。FATFS的FF_FS_LOCK建议设为1以上避免多线程同时操作文件系统导致临界区竞争。读写SD卡时任务调度上要加一个互斥锁防止日志任务和配置读取任务同时操作文件系统。6.2 批量写入策略与掉电安全直接来一条日志就f_write一次的做法速度慢且危险。我采用的方案是内存缓冲批量落盘。数据采集任务先把日志条目写入一个环形缓冲区例如32KB当缓冲区的数据量达到一个完整扇区倍数比如16KB或者距离上次写入超过1秒就唤醒写盘任务一次性把整个缓冲写入文件。这种批量写的顺序性能比单条写高很多也减少了FAT表频繁更新的风险。掉电安全靠两个机制兜底。第一是文件写入后立即f_sync确保FAT表和目录项实际落盘虽然频繁sync会牺牲一些速度但工业场景宁可慢一点不能丢数据。第二是文件头写一个干净关闭标志系统启动时如果发现这个标志不完整就说明上次掉电时文件没有正常关闭自动进入日志扫描修复流程。f_write和f_sync之间的时序要对齐不能在DMA传输没有完成时就认为数据已经写进卡里。我遇到过FATFS返回OK但DMA实际还在飞的情况所以必须等SDIO的传输完成回调。6.3 FPGA 大数据连续记录的搬运链路当FPGA以高速率采集数据时SD卡写入就不能靠任务循环慢慢搬了必须把DMA和文件系统结合起来。我的链路是这样的FPGA把采集数据通过并行FIFO送到STM32的外部内存区域STM32开两个DMA缓冲区乒乓切换DMA写完一个缓冲区就触发回调写盘任务把这块缓冲区的内容顺序追加到当日日志文件。因为SD卡顺序写性能通常比采集速率高所以留出的时间余量足够。如果采集速率偶尔超过SD卡的写入能力整个链路需要接受FPGA的流控信号暂停采集或降低速率避免数据在内存里溢出。这个策略我在储系统联调时反复压制测过可靠性和速度之间能达到很好的平衡。7. 实测中踩过的四个坑完整排查链路记录方案设计再完善也要经过现场数据验证。这一节写我实际遇到过的问题和排查思路每一条都花了很长时间定位。7.1 坑一EEPROM 参数随机错乱现象是控制器上电后偶发出现个别配置参数变成0xFF或随机值。起初怀疑是EEPROM体质问题换了芯片依然复现。排查链路先在示波器上并联抓取I2C信号结果发现写入周期tWR还没有结束下次写操作已经开始了。原来我用的EEPROM虽然标称5ms写周期但在接近极限温度或供电波动时实际周期会拉长状态机里等待时间不够就会导致上一笔没写完、下一笔覆盖。解决方案很简单把tWR等待时间从固定5ms改为查询模式即写完一个字节后发起一个虚拟读命令通过ACK/NAK判断器件是否忙。另外还在I2C总线上增加了通信重试机制连续失败3次就重启EEPROM写入流程并记录错误标志。改完后几百次掉电实验中再没出现参数错乱。7.2 坑二NOR Flash 写入后读出数据不对现象非常典型往W25Q128写入一段数据后读回来前几个字节对后面的数据全错。我开始以为是SPI速率太快导致误码把时钟频率从50MHz降到10MHz问题依旧。后来看了Flash命令手册才意识到W25Q系列扇区编程前必须先擦除扇区而我直接在有旧数据的扇区上执行了写命令。NOR Flash的特性是1可以变成00不能直接变1要写入新数据必须先把整个扇区擦成0xFF。修复方法是把写入流程改为读取旧数据到缓冲区→修改目标字节→擦除整个扇区→写回缓冲区。这个流程虽然多了几步但在Flash操作里是基本功。7.3 坑三SD 卡断电后日志文件打不开这个坑特别隐蔽。设备没断电时跑得好好的一旦现场停电再上电后当天的日志文件打开就提示文件系统损坏甚至整个目录都读不出来。排查时先看了FATFS的错误返回值发现大多在FR_INT_ERR。后来分析FAT表更新机制才明白写文件时FAT表和文件目录项是在缓存里的掉电一刹那缓存未能全部落盘写一半的FAT表就破坏了整个文件簇链。修复方式是在每次写完若干条日志后主动f_sync并且把日志分成小时级小文件每个文件最多几MB掉电时最多丢最近一个文件另外在文件头增加标记上电启动时检查并自动裁掉未正常关闭的文件尾部。从那次以后日志系统的掉电损坏率基本降到零。7.4 坑四FPGA 与 STM32 传输丢块现象是FPGA明明发了连续两帧数据STM32收到的只有一帧另一帧丢了。起初怀疑DMA配置问题反复检查缓冲区和指针都没有发现异常。后来单步跟踪中断发现问题出在FIFO深度信号的同步上。STM32的DMA接收完成中断里我直接根据FPGA提供的FIFO阈值标志判断是否还有下一块数据但标志信号是跨时钟域的没有做同步处理导致STM32采到了不稳定的标志电平偶尔跳过了数据块。修复方式是在FPGA内部把FIFO状态信号打两拍做跨时钟域同步同时增加块序号字段STM32每收到一块就检查块序号是否连续不连续则主动请求重传。这个改动让传输链路的丢块率从原先的偶发性降为零。8. 经验沉淀这套方案在工业产品里的几个补充建议8.1 存储方案的三板斧思路回看这套STM32FPGA分级存储方案最核心的经验就是一句话先把数据分成配置参数、固件、日志三大类每类匹配一种存储介质然后再谈驱动和策略。这个思路几乎把所有工业控制器场景都覆盖了。配置参数的数据量小、写次数多放EEPROM用双备份和槽位轮转保证可靠性固件代码容量中等、读多写少放NOR Flash用分区和A/B镜像保证升级安全日志和原始数据海量且顺序追加放SD卡用批量写和掉电恢复兜底。如果产品里没有FPGA只有一颗MCU这个框架同样成立只需要把高速采集逻辑交给MCU的DMA即可。8.2 掉电检测与系统级收尾我在这套方案里还加了一路掉电检测电路。主电源跌落时上拉一个电压比较器触发外部中断给MCU留出几毫秒到几十毫秒的黄金时间。这个时间内MCU只做一件事停止日常任务保存关键系统状态到EEPROM并且在NOR Flash文件系统里写入系统断电标记然后在SD卡日志里追加一条正常关机记录。不要小看这几毫秒窗口期的价值。它能让系统从未知断电变成受控断电存储数据完整性提高一个数量级。8.3 后续可扩展的方向如果项目进一步扩大数据记录需求可以考虑把SD卡换成eMMC。eMMC的寿命、抗震动、读写稳定性都优于普通SD卡代价是成本和布局复杂度上升NOR Flash区域如果日志容量不够可以扩展一颗更大容量的SPI NOR配置参数的可靠性还能通过EEPROMFlash冗余双写再上一级保险。就我个人经验而言工业控制器的存储设计是最考验系统思维的部分。每级存储都不是孤立存在的它们之间靠掉电保护、磨损均衡、文件系统和硬件握手串成一条完整的链路。如果你正处在方案选型阶段建议先从数据分类表开始梳理把每一类数据和对应的介质匹配清楚再动手画原理图和写驱动——这样做出来的系统稳定性上限会高很多。