工业控制器这类设备有个很现实的问题:它不像消费电子那样可以随便"重启大法"糊弄过去,一旦上了产线,参数丢了、日志断了、配方没了,轻则停机排查,重则整批产品报废。所以存储这块,从一开始就得当成系统可靠性的一部分来设计,而不是"随便挂个Flash存一下"。
我这些年做过的控制器项目里,存储方案基本都绕不开三类介质:EEPROM、NOR Flash、SD卡。它们各自解决的是完全不同层级的问题——EEPROM管"关键到不能丢"的小数据,NOR Flash管"要频繁改、还要掉电安全"的配置和日志,SD卡管"量大、可导出、可替换"的历史数据。STM32负责逻辑调度和文件系统,FPGA负责高速数据流的搬运和时序控制,两者分工明确。
这篇就围绕"STM32+FPGA分级存储"这个方案,把选型逻辑、硬件连接、驱动分层、掉电保护、实测踩坑这些事一次讲透。不管你是刚接触存储设计的新手,还是做过几个项目但总在数据一致性上翻车的同行,应该都能从里面找到能直接抄作业的部分。
1. 为什么工业控制器必须做分级存储,而不是一颗大Flash走天下
很多人第一反应是:既然都是存数据,那我直接上一颗大容量NOR Flash或者eMMC不就行了?这个想法在消费类产品里没问题,但在工业控制器上会踩三个硬坑。
1.1 三类数据的寿命和更新频率完全不同
工业控制器里的数据,按更新频率和重要性可以粗暴分成三档:
- 出厂标定与设备身份数据:设备序列号、校准系数、硬件版本、出厂日期。这类数据可能整个生命周期只写一次,但绝对不能丢,丢了设备就没法追溯。
- 运行配置与实时参数:PID参数、通信地址、报警阈值、累计运行时间。这类数据可能每天改几次,也可能几个月不动,但每次修改都必须掉电安全。
- 过程数据与历史记录:采样曲线、报警日志、批次记录。这类数据量大、写入频繁,但允许一定程度的丢失,且需要能导出。
如果你把这三类数据全塞进同一颗NOR Flash,会面临一个尴尬:频繁写的日志会把Flash的擦写寿命快速消耗掉,而标定数据又可能因为日志区写满触发整片擦除而受影响。分级存储的本质,是把不同寿命、不同重要性、不同访问模式的数据物理隔离,让它们互不干扰。
1.2 擦写寿命决定了介质不能混用
来看一组实际参数对比,这是选型的核心依据:
| 介质类型 | 典型擦写寿命 | 写入粒度 | 容量范围 | 掉电安全性 | 单位成本 |
|---|---|---|---|---|---|
| EEPROM | 100万~400万次 | 字节级 | 几KB~几MB | 极高 | 高 |
| NOR Flash | 1万~10万次 | 扇区级(4KB) | 几MB~几百MB | 高 | 中 |
| SD卡/NAND | 3000~1万次 | 块级(512B~4KB) | GB级 | 中(需管理) | 低 |
EEPROM能按字节改,改一个字节不用擦整个扇区,所以适合存那种"偶尔改一个参数"的场景。NOR Flash必须按扇区擦除,但支持随机读取和XIP,适合存代码和结构化配置。SD卡容量大、便宜,但内部有FTL(闪存转换层),掉电时正在写的块可能损坏,必须配合文件系统和日志机制。
提示:不要用SD卡存标定数据。SD卡的文件系统在掉电瞬间可能损坏整个FAT表,导致所有数据一起丢。标定数据放EEPROM,这是底线。
1.3 STM32+FPGA的分工逻辑
为什么不是STM32一个人干完?因为工业控制器里往往有高速数据流,比如多路ADC同步采样、编码器计数、PWM波形捕获。这些数据如果全让STM32中断去接,CPU会被打满,根本顾不上存储调度。
FPGA在这里的角色是数据搬运工和时序控制器:它把高速采集的数据先缓存到内部FIFO或外挂SRAM,然后按STM32的节奏通过并行总线或SPI把数据块推给STM32。STM32则负责决定"这批数据该写EEPROM还是NOR还是SD卡",以及维护文件系统和掉电保护逻辑。
这种分工的好处是:FPGA保证数据不丢帧,STM32保证数据存得对。两者通过一个简单的握手协议通信,比让STM32硬扛高速采集要稳得多。
2. 三块存储介质的硬件连接与选型细节
硬件设计这块,很多人是照着开发板抄的,但工业现场和实验室环境差别很大。下面把三块介质的连接方式和选型要点拆开讲。
2.1 EEPROM:I2C总线上最省心的那颗
EEPROM我一般选24系列,比如AT24C512(64KB)或CAT24M01(128KB)。接口用I2C,两根线搞定,STM32这边随便找个硬件I2C外设就能驱动。
连接上要注意几个点:
- 地址引脚A0/A1/A2:如果总线上挂多颗EEPROM,靠这三个引脚区分地址。单颗的话全接地即可。
- 写保护引脚WP:建议接到STM32的一个GPIO上,正常工作时拉低允许写,标定完成后拉高防止误写。这个细节很多项目会忽略,但在现场误操作导致标定数据被覆盖的事故并不少见。
- 上拉电阻:I2C的SDA和SCL都需要上拉,典型值4.7kΩ。如果总线走线长,可以降到2.2kΩ提高上升沿速度。
I2C的时钟频率,工业环境建议不要跑满400kHz,100kHz~200kHz更稳。因为工业现场电磁干扰大,高速I2C容易出现位错误。我实测过在变频器旁边跑400kHz,误码率明显上升,降到100kHz后连续运行几个月没出过问题。
2.2 NOR Flash:SPI接口是主流,但要注意扇区结构
NOR Flash我常用W25Q系列(华邦)或S25FL系列(赛普拉斯/英飞凌)。接口用SPI,STM32的硬件SPI或者FPGA的SPI主控都能驱动。
选型时重点看三个参数:
- 扇区大小:W25Q系列典型是4KB扇区、64KB块。擦除最小单位是4KB,所以你的配置数据结构要按4KB对齐设计,不然改一个参数要擦4KB,浪费寿命。
- 页大小:通常是256字节。写入时不能跨页,跨页要分两次写。这个坑我在早期项目里踩过,一次写300字节直接导致后44字节丢失。
- 状态寄存器:BUSY位、WEL位、BP保护位。写之前要查BUSY,写之后要等BUSY清零,不能写完立刻发下一条命令。
SPI时钟频率,W25Q系列标称能到104MHz,但工业环境建议跑20~50MHz。走线长的话还要降。我一般用STM32的SPI2跑18MHz,配合DMA搬运,实测写入速度能到1.5MB/s左右,够用了。
2.3 SD卡:SPI模式还是SDIO模式,这是个问题
SD卡有两种驱动方式:SPI模式和SDIO模式。
- SPI模式:只用4根线(CS、CLK、MOSI、MISO),STM32的SPI外设就能驱动,FPGA也能轻松实现。缺点是速度慢,实测写入速度大概200KB/s~500KB/s。
- SDIO模式:4位数据线并行,速度快,STM32的SDIO外设能跑到10MB/s以上。缺点是引脚多,时序要求高,FPGA实现起来复杂。
工业控制器里,如果只是存日志和导出数据,SPI模式完全够用,而且更稳。SDIO模式在长走线、干扰大的环境下容易出现CRC错误,排查起来很头疼。我现在的做法是:STM32用SDIO跑高速采集数据的存储,FPGA用SPI做备份通道,两条路互为冗余。
SD卡的硬件设计有几个必须注意的点:
- 电源去耦:SD卡写入瞬间电流能到100mA以上,电源引脚旁边必须放10μF+0.1μF的组合电容,否则写入时电压跌落会导致卡复位。
- 上拉电阻:SPI模式下,CS、MOSI、MISO都要上拉,典型值10kΩ~50kΩ。SDIO模式下,4根数据线都要上拉。
- 卡座选型:工业级卡座要选带金属屏蔽罩的,防止插拔时静电打坏芯片。推拉式比弹出式更可靠,弹出式在振动环境下容易接触不良。
注意:SD卡的热插拔在工业控制器里要谨慎。如果系统正在写卡,突然拔卡会导致文件系统损坏。建议在软件里做检测,检测到卡移除时立即停止写入并卸载文件系统。
3. STM32侧的驱动分层与数据调度设计
硬件连好只是第一步,真正决定系统稳不稳的是软件架构。STM32这边的存储驱动,我习惯分成三层:硬件抽象层、介质驱动层、存储管理层。
3.1 硬件抽象层:把I2C、SPI、SDIO统一成一套接口
这一层的目的是让上层不关心底层用的是哪个外设。定义一个统一的接口结构体:
typedef struct { int (*init)(void); int (*read)(uint32_t addr, uint8_t *buf, uint32_t len); int (*write)(uint32_t addr, const uint8_t *buf, uint32_t len); int (*erase)(uint32_t addr, uint32_t len); uint32_t sector_size; uint32_t total_size; } storage_dev_t;EEPROM的驱动实现里,read/write直接映射到I2C读写;NOR Flash的erase映射到扇区擦除命令;SD卡的read/write映射到文件系统的f_read/f_write。这样上层存储管理层只需要操作storage_dev_t指针,换介质不用改上层代码。
这一层还有个关键点:所有操作都要带超时和重试。I2C读写要等ACK,SPI要等BUSY,SD卡要等命令响应。工业环境下这些等待都可能超时,必须有超时退出机制,不能死等。
3.2 介质驱动层:EEPROM的页写与NOR Flash的扇区管理
EEPROM驱动里,最容易出错的是页写边界。24系列EEPROM的页大小通常是64字节或128字节,一次写不能跨页。比如AT24C512页大小128字节,你从地址0x7F开始写10字节,会写到0x7F~0x88,跨了页边界,后面的数据会回卷到页首覆盖。正确做法是:
int eeprom_write(uint32_t addr, const uint8_t *buf, uint32_t len) { while (len > 0) { uint32_t page_remain = EEPROM_PAGE_SIZE - (addr % EEPROM_PAGE_SIZE); uint32_t write_len = (len < page_remain) ? len : page_remain; if (i2c_eeprom_write_page(addr, buf, write_len) != 0) return -1; // 等待内部写周期完成,典型5ms delay_ms(6); addr += write_len; buf += write_len; len -= write_len; } return 0; }注意那个delay_ms(6),EEPROM写完一页后需要等待内部写周期,典型5ms,最大10ms。这段时间I2C不响应任何命令。如果你不等,下一次写会失败。很多新手在这里翻车,表现为"偶尔写不进去"。
NOR Flash驱动里,核心是读-改-写流程。因为NOR不能覆盖写,改一个字节也要先把整个扇区读到RAM,改完再擦除扇区,再写回去。这个流程必须保证原子性,否则擦除后掉电,整个扇区数据就没了。
我的做法是在RAM里维护一个扇区的影子副本,所有修改先改影子,然后标记"脏",在合适的时机(比如系统空闲或掉电中断里)统一刷入Flash。刷入时先擦后写,写完校验。
3.3 存储管理层:数据分类与写入策略
这一层决定"什么数据写到哪块介质"。我一般定义一个配置表:
| 数据类型 | 存储介质 | 写入时机 | 保护机制 |
|---|---|---|---|
| 设备序列号/标定系数 | EEPROM | 出厂一次 | WP引脚保护+CRC校验 |
| 运行配置/PID参数 | NOR Flash | 修改后延迟写 | 双备份+版本号 |
| 累计运行时间 | EEPROM | 每小时更新 | 磨损均衡 |
| 报警日志 | NOR Flash | 实时写 | 环形缓冲区 |
| 采样曲线 | SD卡 | 批量写 | 文件系统+定期flush |
累计运行时间这个数据特别典型:它需要频繁更新(比如每小时加1),但EEPROM寿命有限。如果每小时写一次,一年8760次,EEPROM的100万次寿命能用100多年,没问题。但如果每秒写一次,几天就写废了。所以更新频率必须和介质寿命匹配,这是分级存储的核心计算。
4. FPGA侧的数据搬运与握手协议
FPGA在这套方案里不是可有可无的配角。当采样率上去之后,STM32的中断响应时间(微秒级)根本跟不上数据产生速度(纳秒级),必须有FPGA做缓冲。
4.1 FPGA内部FIFO的深度计算
假设前端有8路ADC,每路采样率100kHz,16位精度。数据产生速率是:
8路 × 100kHz × 2字节 = 1.6MB/s
STM32通过FSMC或SPI来取数据,假设每10ms取一次,那么两次取数之间FPGA要缓存:
1.6MB/s × 10ms = 16KB
考虑余量和突发,FIFO深度至少要做到32KB。如果FPGA内部BRAM不够,就外挂一颗SRAM做缓冲。
FIFO的位宽一般设成16位或32位,和ADC数据对齐。写时钟用ADC的采样时钟,读时钟用STM32的读取时钟,跨时钟域处理用异步FIFO,格雷码指针同步,这是标准做法。
4.2 FPGA与STM32的握手协议设计
两者之间的通信,我用得最多的是SPI从机模式或者FSMC并行总线。
SPI从机模式接线简单,但速度受限于STM32的SPI时钟。FSMC并行总线速度快,但占用引脚多。工业控制器里如果数据量大,建议用FSMC;如果只是传控制命令和少量数据,SPI够了。
握手协议我一般设计成简单的"请求-应答":
- FPGA检测到FIFO里数据超过阈值(比如半满),拉高DATA_READY信号。
- STM32检测到DATA_READY,发起读取,先读长度,再读数据。
- STM32读完一批,拉高ACK信号。
- FPGA收到ACK,清除DATA_READY,继续缓存下一批。
这个协议的关键是ACK必须可靠。如果STM32读了一半死机了,FPGA不能一直等,要有超时机制,超时后丢弃当前批次,重新开始缓存。否则FIFO满了之后新数据全丢。
4.3 FPGA直接写SD卡的可行性分析
有人会问:能不能让FPGA直接写SD卡,绕过STM32?技术上可行,FPGA实现SPI模式的SD卡控制器并不复杂,网上也有开源核。但我不推荐这么做,原因有两个:
第一,SD卡的文件系统(FAT32/exFAT)维护很复杂,FPGA实现完整的文件系统会消耗大量逻辑资源,而且调试困难。第二,工业控制器需要记录"谁在什么时候写了什么",这个元数据管理放在STM32侧用软件做更灵活。
所以我的方案是:FPGA只管搬运原始数据,STM32负责组织成文件写入SD卡。FPGA把数据推给STM32,STM32按时间戳和批次号打包,通过FatFs写入SD卡。这样职责清晰,出问题也好定位。
5. 掉电保护:这套方案里最容易翻车的地方
工业现场断电是常态,不是异常。掉电保护做不好,前面所有设计都白搭。
5.1 掉电检测电路与中断响应时间
掉电检测一般用比较器+分压电阻实现。电源正常时,分压点电压高于比较器阈值;电源跌落时,分压点电压低于阈值,比较器输出翻转,触发STM32的外部中断。
关键参数是保持时间。从检测到掉电到电源完全跌落到芯片工作电压以下,这段时间就是你能用来保存数据的时间。它由电源上的储能电容决定:
t = C × (V1 - V2) / I
假设电源上有个1000μF电容,系统工作电流200mA,从12V跌落到5V(STM32最低工作电压按3.3V算,但稳压器需要压差,所以按5V算):
t = 1000μF × (12V - 5V) / 200mA = 35ms
35ms足够STM32把关键数据写进EEPROM(EEPROM写一页5ms)或者NOR Flash(扇区擦除+写入大概几十ms,可能不够)。所以掉电时优先保存EEPROM里的关键数据,NOR Flash的数据靠双备份和日志机制保证。
5.2 双备份与版本号机制
NOR Flash里的配置数据,我用双备份+版本号的方式保证掉电安全。具体做法:
- 把配置区分成A、B两个扇区。
- 每个扇区头部有个结构:魔数、版本号、数据长度、CRC。
- 写入时,先写版本号小的那个扇区,写完校验CRC,通过后更新版本号。
- 读取时,读两个扇区,选版本号大且CRC正确的那个。
这样即使写入过程中掉电,另一个扇区还是完好的。下次上电读取时,发现一个扇区CRC错误,就用另一个。这个机制我在多个项目里用过,实测在反复断电测试中没丢过配置。
5.3 SD卡的文件系统一致性
SD卡上的文件系统,掉电时最容易损坏的是FAT表。FatFs提供了f_sync函数,可以把缓存的数据刷到卡上。但f_sync本身也要时间,掉电时不一定来得及。
我的做法是:
- 采样数据先写到NOR Flash的环形缓冲区,缓冲区满了再批量写入SD卡。
- 写SD卡时,每写一个文件就调用
f_sync,保证文件系统元数据及时更新。 - 掉电时,NOR Flash里的缓冲区数据还在,下次上电可以恢复。
这样即使SD卡文件系统损坏,最坏情况是丢最后一个文件,不会丢全部数据。而且SD卡可以拔下来在电脑上修复,NOR Flash里的数据是最后的保险。
提示:工业控制器建议用工业级SD卡,带掉电保护功能的更好。普通消费级SD卡在反复掉电环境下,寿命和可靠性都撑不住。
6. 实测踩坑记录与排查思路
这部分是我这些年实际踩过的坑,每个都花了时间排查,写出来供参考。
6.1 EEPROM偶发写失败:I2C上拉电阻选大了
现象:EEPROM写入成功率大概99%,偶尔失败,重试就好。
排查过程:用示波器看I2C波形,发现SCL上升沿很缓,从0.3Vcc到0.7Vcc用了将近1μs。正常应该几百ns。原因是上拉电阻用了10kΩ,总线电容又比较大(走线长+多个器件),RC时间常数太大。
解决:上拉电阻换成2.2kΩ,上升沿降到300ns以内,写失败率降到零。
经验:I2C上拉电阻不是随便选的,要根据总线电容算。经验公式是R_max = 300ns / C_bus。总线电容1pF对应1kΩ左右,走线长的话要实测。
6.2 NOR Flash写入后读出来是0xFF
现象:往W25Q128写数据,写完读出来全是0xFF。
排查过程:先查SPI波形,发现写命令发出去了,但数据没进去。再查状态寄存器,发现WEL位没置起来。原因是写使能命令(0x06)发出后,没有等待,直接发了页写命令(0x02),导致写使能没生效。
解决:写使能命令发出后,读状态寄存器确认WEL=1,再发页写命令。写完后再读状态寄存器等BUSY=0。
经验:NOR Flash的每条命令都要确认状态,不能想当然。特别是写使能、写禁止、擦除这些命令,必须查状态寄存器。
6.3 SD卡写入速度突然变慢
现象:SD卡刚开始写入速度500KB/s,写了几分钟后降到50KB/s。
排查过程:用逻辑分析仪抓SPI波形,发现写入过程中出现了大量等待。查SD卡手册,发现是卡内部垃圾回收导致的。SD卡写满一部分后,内部FTL需要搬移数据,速度会下降。
解决:在软件里做预分配,创建文件时一次性分配足够大的空间,避免频繁扩展文件。另外,定期删除旧文件,给卡留出足够的空闲块。
经验:SD卡的写入速度不是恒定的,会随使用情况波动。设计时要按最慢速度算,留足余量。如果数据量大,建议用高耐久度的工业级卡。
6.4 掉电后配置丢失:中断优先级配置错误
现象:掉电测试时,配置偶尔丢失。
排查过程:查代码发现,掉电中断的优先级设得比较低,被其他中断抢占了。掉电时系统正在处理一个高优先级中断,等处理完,电源已经跌落到无法写Flash了。
解决:把掉电中断设成最高优先级(抢占优先级0),并且在中断里只做最关键的保存操作,其他事情交给主循环。
经验:掉电中断是"救命中断",优先级必须最高。而且中断服务程序要尽量短,只做保存,不做其他逻辑。
7. 方案落地时的几个实用建议
最后分享几个我在实际项目中总结的建议,都是踩过坑之后才明白的。
第一,存储介质的寿命要按最坏情况算。比如EEPROM标称100万次,但实际使用中温度、电压波动都会影响寿命,按10万次设计比较稳妥。如果某个数据每天写100次,一年36500次,三年就超过10万次了,这时候就要考虑换介质或者做磨损均衡。
第二,所有存储操作都要有校验。EEPROM写完读回来比对,NOR Flash写完算CRC,SD卡写完读文件头确认。校验不通过就重试,重试三次还失败就报警。工业设备不能"假装写成功了"。
第三,日志要记录存储操作本身。什么时候写了什么数据、写了多少字节、校验结果如何,这些都要记下来。出了问题可以追溯,也方便分析存储介质的健康状态。
第四,预留扩展空间。配置区不要写满,留20%的余量。以后加参数、改结构,不用重新规划整个存储布局。
第五,测试要覆盖掉电场景。用可编程电源做反复上下电测试,至少跑1000次。每次上电检查数据完整性。这个测试能暴露大部分掉电保护的问题。
这套STM32+FPGA分级存储方案,我在几个工业控制器项目里都用过,从简单的参数存储到高速采样记录都覆盖了。核心思路就是:让合适的介质干合适的事,让STM32管逻辑,让FPGA管速度,让掉电保护兜底。把这几点做到位,存储这块基本就不会成为系统的短板了。