1. 工业控制器里那点数据,到底该怎么安家
做工业控制器的朋友大概率都遇到过这个场景:板子跑起来了,电机转起来了,传感器数据也采上来了,结果老板一句“把参数存一下,断电不能丢”,你就得开始琢磨存储方案。更麻烦的是,工业现场的数据还不是一种脾气——有的一小时写一次,有的一毫秒就得记一笔,有的丢了整台设备就废了,有的丢几帧无所谓。你要是拿一种存储器硬扛所有需求,要么成本爆炸,要么寿命堪忧,要么数据完整性出问题。
这篇内容就是围绕这个真实痛点展开的:在STM32加FPGA的工业控制器架构下,怎么用EEPROM、NOR Flash和SD卡搭一套分级存储方案。STM32负责系统管理、参数配置和低速数据记录,FPGA负责高速采集和实时缓存,三种存储介质各司其职。如果你正在做数据采集板、运动控制器、边缘网关或者任何需要“断电不丢数据”的工业设备,这套思路可以直接参考。
先给不熟悉的朋友补一下背景。STM32是大家熟悉的ARM Cortex-M系列微控制器,外设丰富、生态成熟,适合做控制逻辑和通信管理。FPGA则是现场可编程门阵列,并行处理能力强,适合做高速ADC采集、多路PWM、编码器解码这类任务。两者配合的架构在工业控制器里非常常见:FPGA管“快”的活,STM32管“杂”的活。而存储这块,恰恰是两者都要参与的系统工程。
为什么非要分级?因为工业数据天然就是分层的。我习惯把它分成三类:关键参数(设备ID、校准系数、通信配置)、运行日志(报警记录、操作记录、状态快照)、高速采样数据(振动波形、电流波形、瞬态过程)。这三类数据的写入频率、数据量、可靠性要求完全不同,用同一种介质去存,要么大材小用,要么力不从心。
下面这张表是我在实际项目中总结的对照关系,先给大家一个整体印象:
| 数据类型 | 典型大小 | 写入频率 | 可靠性要求 | 推荐介质 |
|---|---|---|---|---|
| 关键参数 | 几十字节到几KB | 极低(配置时写) | 极高 | EEPROM |
| 运行日志 | 几KB到几MB | 中低(事件触发) | 高 | NOR Flash |
| 高速采样 | 几十MB到几GB | 极高(连续写) | 中 | SD卡 |
这张表不是拍脑袋来的,后面我会逐层拆解每种介质为什么适合这类数据,以及STM32和FPGA各自该承担什么角色。
2. EEPROM:那些“写一次就不能丢”的关键参数
2.1 为什么关键参数非EEPROM不可
关键参数的特点是:数据量小、写入次数少、但绝对不能丢。设备ID、传感器校准系数、PID参数、通信地址这些东西,出厂写一次,之后可能几个月都不动,但一旦丢了,设备要么无法工作,要么输出错误。这种场景对存储器的要求是字节级可寻址、写入可靠、寿命足够。
NOR Flash和SD卡能不能存?能,但不合适。NOR Flash虽然也支持随机读取,但擦除必须按扇区来,改一个字节要先擦整个扇区再写回,操作复杂且容易出错。SD卡更不用说,文件系统层叠加上去,小数据写入效率极低,还有掉电损坏文件系统的风险。EEPROM则是天生的字节级读写,支持单字节修改,写入寿命通常在100万次以上,正好匹配关键参数的使用模式。
我见过不少项目为了省一颗EEPROM,把参数塞进STM32的内部Flash。短期看没问题,但内部Flash的擦写寿命通常只有1万次左右,而且擦除粒度大。如果设备需要频繁保存运行统计、累计时长这类“半关键”数据,内部Flash很快就会到寿命。所以我的建议是:真正关键且可能频繁更新的参数,老老实实外挂一颗EEPROM。
2.2 STM32通过I2C读写EEPROM的实操细节
工业控制器里最常用的EEPROM是I2C接口的24系列,比如24C02、24C16、24C256,容量从2Kbit到256Kbit不等。STM32的硬件I2C外设可以直接驱动,也可以用GPIO模拟。我个人的经验是:低速场景用硬件I2C,遇到总线锁死问题时用软件模拟更省心。
硬件I2C的配置不复杂,但有几个坑必须注意。第一,上拉电阻的选择。I2C总线需要上拉,典型值是4.7kΩ,但如果总线电容大或者速率高,要适当减小。我遇到过一块板子用10kΩ上拉,100kHz下勉强能跑,400kHz就频繁NACK,换成2.2kΩ后稳定。第二,EEPROM的写周期。24系列EEPROM一次写操作后需要5ms左右的内部写周期,这期间不响应任何命令。如果你连续写多个字节,必须每写一页就等待写周期结束,否则数据会丢。
下面是一段我常用的EEPROM页写函数,基于STM32 HAL库:
#define EEPROM_ADDR 0xA0 #define PAGE_SIZE 8 HAL_StatusTypeDef EEPROM_WritePage(uint16_t memAddr, uint8_t *data, uint16_t len) { HAL_StatusTypeDef status; status = HAL_I2C_Mem_Write(&hi2c1, EEPROM_ADDR, memAddr, I2C_MEMADD_SIZE_16BIT, data, len, 100); if (status != HAL_OK) return status; HAL_Delay(6); // 等待内部写周期,留1ms余量 return HAL_OK; }这段代码里HAL_Delay(6)是关键。很多新手会忽略这个等待,结果写进去的数据时对时错,排查半天以为是I2C时序问题。实际上就是EEPROM还在忙内部写周期,你发的新命令它根本没理。
2.3 参数存储的可靠性设计:双备份与校验
EEPROM本身可靠,但工业现场的电磁干扰、电源波动、程序跑飞都可能导致写入异常。我在实际项目中会做两层保护:双备份加CRC校验。
具体做法是:把参数区分成A、B两个区块,每个区块包含参数数据加一个CRC32校验值。写入时先写A区,校验通过后再写B区;读取时先读A区校验,失败则读B区,再失败则加载默认参数并报警。这样即使某次写入过程中掉电,至少还有一个区块是完整的。
typedef struct { uint32_t magic; // 0x55AA55AA,标识有效 uint32_t crc; // 数据区CRC32 uint8_t data[PARAM_SIZE]; } ParamBlock; ParamBlock blockA, blockB; uint8_t Param_Load(void) { EEPROM_Read(ADDR_A, (uint8_t*)&blockA, sizeof(blockA)); if (blockA.magic == 0x55AA55AA && CRC32_Check(&blockA)) { memcpy(&g_params, blockA.data, PARAM_SIZE); return 0; } EEPROM_Read(ADDR_B, (uint8_t*)&blockB, sizeof(blockB)); if (blockB.magic == 0x55AA55AA && CRC32_Check(&blockB)) { memcpy(&g_params, blockB.data, PARAM_SIZE); return 0; } Param_LoadDefault(); return 1; // 返回1表示使用了默认参数,需要报警 }这套逻辑看起来简单,但在现场救过我好几次。有一次客户设备在雷击后参数全乱,就是靠B区备份恢复的。所以别嫌麻烦,关键参数的双备份是工业设备的基本素养。
3. NOR Flash:运行日志和配置文件的可靠仓库
3.1 NOR Flash在工业控制器中的定位
EEPROM容量小,通常只有几KB到几十KB,存关键参数够用,但存运行日志就不够了。运行日志包括报警记录、操作记录、状态快照,可能积累到几MB甚至几十MB。这时候就需要NOR Flash出场。
NOR Flash的特点是支持随机读取、按扇区擦除、按页写入。读取速度很快,可以直接执行代码(XIP),写入前必须先擦除。常见容量从几MB到几百MB,SPI接口的W25Q系列在工业控制器里用得最多。它的擦写寿命通常在10万次左右,比EEPROM低,但比SD卡的文件系统写入要可靠得多。
为什么不用NAND Flash?NAND容量大、成本低,但有坏块管理、ECC校验这些复杂问题,对于中小容量的日志存储来说,NOR的简单可靠更合适。而且NOR支持字节级读取,STM32可以直接寻址读取数据,不需要复杂的驱动。
3.2 日志存储的环形缓冲区设计
运行日志的特点是持续产生、旧记录可以覆盖。比如报警记录,你只需要保留最近1000条,更早的可以丢弃。这种场景最适合用环形缓冲区。
我的做法是在NOR Flash上划分一个日志区,比如从地址0x10000开始,大小1MB。把这块区域分成若干个固定大小的槽位,每个槽位存一条日志记录。维护一个写指针,写到末尾就回到开头覆盖最旧的记录。每条记录包含时间戳、事件类型、数据内容和CRC校验。
#define LOG_SLOT_SIZE 64 #define LOG_SLOT_COUNT 16384 // 1MB / 64B #define LOG_BASE_ADDR 0x10000 typedef struct { uint32_t timestamp; uint16_t event_id; uint8_t data[54]; uint16_t crc; } LogRecord; uint32_t log_write_ptr = 0; void Log_Write(uint16_t event_id, uint8_t *data, uint8_t len) { LogRecord rec; rec.timestamp = Get_RTC_Time(); rec.event_id = event_id; memcpy(rec.data, data, len); rec.crc = CRC16_Calc((uint8_t*)&rec, sizeof(rec) - 2); uint32_t addr = LOG_BASE_ADDR + log_write_ptr * LOG_SLOT_SIZE; NOR_EraseSector(addr & 0xFFFFF000); // 擦除所在扇区 NOR_WritePage(addr, (uint8_t*)&rec, sizeof(rec)); log_write_ptr = (log_write_ptr + 1) % LOG_SLOT_COUNT; }这里有个关键点:NOR Flash擦除是按扇区的,通常4KB一个扇区。如果你每条记录都擦一次扇区,寿命消耗太快。所以实际项目中我会做一个缓存,攒够一个扇区的数据再统一擦写。或者用“日志区轮转”的方式,写满一个扇区再擦下一个,避免频繁擦除同一区域。
3.3 FPGA直接访问NOR Flash的可行性
在STM32加FPGA的架构里,NOR Flash通常挂在STM32的SPI总线上。但有些高速场景,比如FPGA采集完一批数据需要快速转存到NOR Flash,走STM32中转就慢了。这时候可以让FPGA也具备SPI Master能力,直接访问NOR Flash。
FPGA实现SPI Master并不复杂,核心是一个状态机加移位寄存器。我一般会实现一个简单的SPI控制器,支持模式0(CPOL=0,CPHA=0),时钟分频可配。FPGA把数据写入NOR Flash后,再通过中断通知STM32更新日志索引。这样分工明确:FPGA管高速写入,STM32管索引管理和读取展示。
不过要注意总线仲裁问题。如果STM32和FPGA都能访问NOR Flash,必须有一个仲裁机制,否则同时发起访问会冲突。我的做法是用一根GPIO做总线请求信号,FPGA要访问时先拉低请求,STM32释放SPI总线后拉低应答,FPGA再开始操作。简单可靠,不需要复杂的协议。
4. SD卡:高速采样数据的最终归宿
4.1 为什么高速采样数据适合SD卡
FPGA采集的振动波形、电流波形、瞬态过程数据,特点是数据量大、写入连续、对单次写入可靠性要求没那么高。比如一个1kHz采样率、16位精度的振动通道,一小时就是7.2MB数据。如果用NOR Flash存,容量和成本都吃不消。SD卡容量大、成本低、读写速度快,正好匹配这类需求。
但SD卡也有明显的短板:文件系统复杂、掉电容易损坏、写入寿命有限。所以我的原则是:SD卡只存“丢了可以重新采集”的数据,关键参数和日志绝不放在SD卡上。而且SD卡要配合文件系统使用,通常是FatFs,方便数据导出到电脑分析。
4.2 STM32挂载SD卡并跑通FatFs的关键步骤
STM32通过SDIO接口或者SPI接口访问SD卡。SDIO速度快,但引脚多、配置复杂;SPI简单,但速度慢。工业控制器里如果数据量不是特别大,SPI模式够用;如果要连续写高速数据,建议用SDIO。
跑通FatFs的步骤大致是:初始化SD卡、挂载文件系统、打开文件、写入数据、关闭文件。听起来简单,但实际调试时问题不少。我整理了一个排查清单:
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 挂载失败 | 卡未格式化或格式不对 | 用电脑格式化为FAT32 |
| 写入速度慢 | SPI时钟太低 | 提高到20MHz以上 |
| 数据丢失 | 未调用f_sync | 定期f_sync或关闭文件 |
| 卡识别不了 | 上拉电阻缺失 | 命令线加10kΩ上拉 |
| 长时间写入后卡死 | 卡内部垃圾回收 | 定期关闭文件重新打开 |
其中定期f_sync是最容易被忽略的。FatFs为了效率会缓存数据,如果你不主动同步,掉电时缓存里的数据就丢了。我的做法是每写1MB数据调用一次f_sync,兼顾效率和安全性。
4.3 FPGA高速数据流如何高效写入SD卡
FPGA采集的数据要写入SD卡,通常有两条路:一是FPGA把数据传给STM32,STM32写SD卡;二是FPGA直接控制SD卡。第一种方式简单,但STM32要同时处理通信、控制和存储,负担重。第二种方式FPGA直接写SD卡,速度快,但FPGA要实现SD卡协议和文件系统,复杂度高。
我的折中方案是:FPGA做数据缓存和打包,STM32做文件系统管理。FPGA内部用Block RAM或外挂SRAM做FIFO,采集到的数据先存入FIFO。STM32通过FSMC或SPI接口从FIFO读取数据,再写入SD卡。这样FPGA只管采集和缓存,STM32只管存储,各司其职。
如果数据率特别高,比如几十MB/s,STM32来不及搬运,那就需要FPGA直接写SD卡。这时候FPGA需要实现SD卡的SPI模式协议,按扇区写入。文件系统可以简化,比如用裸扇区写入,数据导出时再用上位机软件解析。这种方式牺牲了文件系统的便利性,换来了速度。
5. 三级存储的协同调度:谁先写、谁后写、冲突怎么办
5.1 数据分级路由策略
三种存储介质就位后,下一个问题是谁来决定数据往哪写。我的做法是在STM32里做一个存储路由层,根据数据类型自动分发。
关键参数走EEPROM,这个在参数修改时直接触发,不需要路由。运行日志走NOR Flash,由事件触发,比如报警产生、状态变化。高速采样数据走SD卡,由FPGA的采集状态触发。路由层维护一个状态机,确保同一时间只有一种介质在写,避免总线冲突。
具体实现上,我会定义一个存储任务队列。STM32的主循环里轮询队列,取出任务后根据类型调用对应的存储驱动。FPGA的高速数据通过中断通知STM32,STM32把“转存SD卡”任务加入队列。这样即使多个存储请求同时到来,也能有序处理。
5.2 掉电保护与数据完整性
工业现场掉电是常态,存储方案必须考虑掉电保护。我的经验是分介质处理:
EEPROM写入时掉电,靠双备份和CRC恢复。NOR Flash写入时掉电,靠日志记录的CRC校验识别不完整记录,丢弃即可。SD卡写入时掉电最麻烦,可能导致文件系统损坏。所以SD卡上的数据我建议按文件分段存储,比如每采集10分钟存一个文件,掉电最多损失当前文件,之前的文件不受影响。
另外,STM32要监测电源电压,在电压下降到阈值时触发紧急保存。具体做法是用ADC监测24V或12V输入,当电压低于比如10V时,进入掉电中断,把关键数据写入EEPROM,然后安全关闭SD卡文件。这个时间窗口通常有几十毫秒,足够完成关键操作。
5.3 实际项目中的存储寿命估算
存储介质的寿命是有限的,设计时要算一笔账。EEPROM按100万次擦写算,如果每天写100次,可以用27年,完全够用。NOR Flash按10万次擦写算,如果每天擦写1000次,只能用100天,所以必须做磨损均衡。SD卡按3000次擦写算,如果每天写满一次,只能用8年,但实际工业场景不会每天写满。
我在一个振动监测项目里的实际数据是:EEPROM每天写约50次,NOR Flash每天擦写约200次,SD卡每天写入约500MB。按这个用量,EEPROM和NOR Flash的寿命都超过10年,SD卡大约3到5年需要更换。所以我在设备维护手册里明确写了SD卡的建议更换周期。
6. 调试过程中踩过的坑和验证方法
6.1 EEPROM读写失败的排查链路
有一次调试一块新板子,EEPROM死活读不出数据。排查过程是这样的:先用示波器看I2C波形,发现SCL和SDA都有信号,但EEPROM不响应ACK。检查地址,发现原理图上A0、A1、A2都接地,地址应该是0xA0,但代码里写的是0xA1。改过来后能读写了,但数据偶尔出错。再查,发现上拉电阻是10kΩ,换成4.7kΩ后稳定。最后发现写周期等待时间设成了3ms,改成6ms后彻底没问题。
这个排查链路告诉我:I2C问题先看波形,再看地址,再看上拉,最后看时序参数。按这个顺序,大部分问题都能定位。
6.2 NOR Flash数据丢失的根因分析
另一个项目里,NOR Flash的日志偶尔会丢几条。一开始怀疑是擦除没完成就写入,加了等待后还是有。后来用逻辑分析仪抓SPI波形,发现STM32在擦除期间被高优先级中断打断,中断服务程序里又访问了SPI总线,导致擦除命令被干扰。解决办法是在擦除和写入期间关中断,或者用DMA传输加完成中断。
这个坑的教训是:NOR Flash的擦除和写入是原子操作,期间不能被打断。如果系统中断频繁,要么关中断,要么用硬件保护机制。
6.3 SD卡在工业现场的稳定性加固
SD卡在实验室用得好好的,到了现场就出问题。常见的是电磁干扰导致SD卡掉线,或者振动导致接触不良。我的加固措施有三个:一是选用工业级SD卡,工作温度范围宽;二是SD卡座加固定胶,防止振动松脱;三是在软件里加超时重试机制,SD卡操作失败后重试三次,再失败就报错并尝试重新挂载。
还有一个细节:SD卡的写保护开关。有些卡座没有检测这个开关,如果卡被写保护了,程序会一直写失败。所以代码里要检测写保护引脚,或者至少在写入失败时给出明确的错误提示。
7. 写在最后:一些个人经验和小技巧
这套分级存储方案我在三个项目里实际用过,从振动监测到运动控制都有。最大的体会是:存储设计不是选最贵的,而是选最合适的。EEPROM、NOR Flash、SD卡各有各的脾气,顺着它们的特性来设计,系统就稳;硬要一种介质扛所有需求,迟早出问题。
再分享几个小技巧。第一,EEPROM的I2C地址要留跳线,方便一块板子用多颗EEPROM时改地址。第二,NOR Flash的日志区建议预留一个“元数据扇区”,记录写指针、擦除次数这些信息,方便磨损均衡。第三,SD卡的文件名用时间戳命名,比如data_20250101_120000.bin,导出时一目了然。
最后说一个容易被忽略的点:存储介质的初始化顺序。上电后先初始化EEPROM,加载关键参数;再初始化NOR Flash,恢复日志索引;最后初始化SD卡,挂载文件系统。这个顺序不能乱,因为后面的初始化可能依赖前面的参数。我见过有人先初始化SD卡,结果因为参数没加载导致文件系统配置错误,卡挂载失败。
这套方案不是唯一的,但经过实际验证,在中小型工业控制器里足够可靠。如果你正在做类似的项目,希望这些经验能帮你少走点弯路。