☰
工业控制器存储设计:EEPROM+NOR Flash+SD卡三级方案解析
2026/9/29 1:05:11 网站建设 项目流程

做工业控制器这些年,被存储问题坑过太多次。参数莫名丢失、断电后日志文件打不开、固件升级到一半直接变砖,这些故障的根子往往不在芯片本身,而是数据放错了地方。我现在的方案是STM32+FPGA组合,存储侧用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卡负责运行日志、报警记录、高速采样数据这类"海量可追溯"的数据。三级之间各有边界,互相不抢活。

这个分类思路也是整个方案的骨架。下面先逐个拆解每种介质的能力特点,再讲STM32+FPGA怎么配合,把数据送到它们各自的存储位置上。

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 分区表设计

以常见的W25Q128(16MB)为例,我一般规划成这样:

起始地址长度用途
0x000000512KBBootloader
0x0800004MB固件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设为4096,cache_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可以变成0,0不能直接变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 存储方案的"三板斧"思路

回看这套STM32+FPGA分级存储方案,最核心的经验就是一句话:先把数据分成配置参数、固件、日志三大类,每类匹配一种存储介质,然后再谈驱动和策略。这个思路几乎把所有工业控制器场景都覆盖了。

配置参数的数据量小、写次数多,放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;配置参数的可靠性还能通过"EEPROM+Flash冗余双写"再上一级保险。

就我个人经验而言,工业控制器的存储设计是最考验系统思维的部分。每级存储都不是孤立存在的,它们之间靠掉电保护、磨损均衡、文件系统和硬件握手串成一条完整的链路。如果你正处在方案选型阶段,建议先从"数据分类表"开始梳理,把每一类数据和对应的介质匹配清楚,再动手画原理图和写驱动——这样做出来的系统稳定性上限会高很多。

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

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

立即咨询