工业控制器这东西,我在产线上碰过不少,也在售后电话里听过不少惨案:一台设备跑着跑着参数全部丢失,伺服上电就乱撞;日志写不进SD卡,故障原因无从追溯;固件升级到一半断电,控制器直接变砖。这些问题十有八九都出在数据存储设计上。这篇硬件篇·12,就把STM32+FPGA架构下最常用的分级存储方案——EEPROM、NOR Flash、SD卡——彻底讲清楚:为什么三种介质要同时用、各自负责什么数据、硬件上怎么接、驱动和写入策略怎么做,以及调试现场踩过的坑。内容偏向运动控制、数据采集终端、小型PLC这类设备,基本覆盖了工业控制器存储的主流需求。
1. 存储需求拆解:分级方案的出发点是数据属性
1.1 工业控制器里到底要存什么数据
很多朋友上来就问“用EEPROM还是NOR Flash”,我一般会反问一句:你的数据是什么属性?因为工业控制器里要存的东西,属性差异非常大,粗暴地统一用一种介质,不是容量不够就是可靠性出问题。
以我做过的一台多轴运动控制器为例,需要存储的数据可以分成三大类。第一类是系统参数,包括PID增益、限位位置、编码器方向、设备序列号、通信地址、校准系数等。这类数据量很小,几十字节到几KB,但价值极高。它们变化频率不高,只在调试、校准或现场调整时改写,可绝对不能丢。参数一旦丢失,设备轻则报警停机,重则机械结构直接撞坏。
第二类是固件与配置镜像,包括STM32的应用程序镜像、FPGA的bitstream、字体库或配置文件。量级在几百KB到十几MB,更新频率很低,一年几次,但读取频率很高,因为设备每次上电都要加载它们。这类数据要求能够快速、可靠地整块读取,更新时又要能整体替换。
第三类是运行日志,包括故障码、报警记录、带时间戳的温度曲线、实际运行统计等。这部分数据量很大,一台设备一天产生几十MB日志很常见。写入频率极高,每次采样或每秒钟都会追加记录。日志的价值在于追溯问题,丢失最后几秒甚至几分钟可以接受,但整个文件系统不能损坏。
三类数据的特性完全不同:参数数据量最小但可靠性要求最高,固件要求读取性能和可更新性,日志要求容量和持续写入能力。把它们混在一种介质里,必然顾此失彼。
1.2 为什么不能“一块大Flash全搞定”
有人说,我直接挂一块大容量NOR Flash甚至NAND,分层分区,不也能存所有数据吗?理论上是,实际上坑很致命。
关键在于写入粒度。EEPROM可以按字节改写,简直是“随时改一小点”的理想介质;NOR Flash虽然可以随机读,但写入前必须先擦除整个扇区,4KB的扇区擦除一次要上百毫秒。你为了修改一个字节的PID参数,得先把整个扇区读出来、改好、整扇区擦掉、再全写回去。要是这个过程中断电,整个扇区的数据全部报销。NAND Flash更复杂,它按页读写,物理坏块多,管理算法复杂,在工业控制器里用来存关键参数完全是自找麻烦。
所以标准工业方案就是把三种介质各司其职:关键参数进EEPROM,代码和FPGA镜像进NOR Flash,日志大数据进SD卡。这不是拍脑袋,而是在可靠性、容量、寿命、成本、开发周期之间反复权衡后的结果。
2. 存储介质选型:从底层原理到接口选型
2.1 EEPROM:几十字节参数的核心保险柜
EEPROM为什么能按字节写?因为它每个存储单元除了浮栅管之外,还多做了一个用于选通控制的晶体管,可以独立擦除和编程每一个字节。代价是单位面积大,容量做不了太大,市面上主流也就几Mb,但这对存参数来说完全够了。
我项目里最常用的器件是AT24C256,容量256Kbit,也就是32KB,I2C接口,支持64字节页写,工业级工作温度-40~85℃,擦写寿命标称100万次,数据保持能力在40年以上。这些参数对参数存储来说,每一项都够用又留有余量。
接线时要注意两个点。第一,I2C总线上拉电阻我习惯用4.7kΩ,不要用10kΩ太大,线电容一大波形就变差。第二,AT24C256有WP写保护引脚,我会把它直接接高电平,硬件上禁止意外写入。这个设计在实战中救过我一次,后面调试实录里细说。
选型还有一个容易被忽略的点:同样是“256”后缀,AT24C256和AT24C02的页大小完全不同,AT24C02一页只有8字节,如果按64字节去写,数据会回绕覆盖到当前页起始位置,属于经典的“看着能写其实是错的”。所以一定先看数据手册确认页大小。
2.2 NOR Flash:固件和比特流的可靠仓库
NOR Flash和EEPROM本质上是同一种浮栅工艺家族,但结构上每个单元只有一个晶体管,省掉了字节级选通管,密度上去了,代价是只能按扇区擦除。它最大的优势是随机读取极快且支持XIP(就地执行),STM32可以直接从NOR Flash映射地址运行代码,不过大多数项目里还是会把固件拷贝到RAM跑。
我常用的NOR Flash芯片是W25Q128,128Mbit也就是16MB,SPI接口,4KB扇区,256字节页编程,擦写寿命约10万次。20MHz SPI时钟下实测读速度能到2MB/s以上,做固件加载绰绰有余。选16MB容量是因为它同时要装STM32固件镜像和FPGA的bitstream,还要留出版本回滚区。
NOR Flash有个特性要记住:它不像EEPROM那样能字节擦除,擦除最小单位是4KB扇区,而且擦除操作一旦开始不能中断,大概需要150ms。如果恰好在这个窗口断电,这个扇区可能处于“半擦半写”的诡异状态。所以稍微像样点的工业设计,都会在NOR Flash里做双镜像备份,或者至少有一个“启动头+校验和”结构,确保启动加载时能判断镜像是否完整。
2.3 SD卡:日志大数据仓库,同时也是最脆弱的环节
SD卡从物理结构上看就是一个微型控制器加NAND Flash,对外通过CMD/ACMD指令操作。好处是容量大、即插即用、可更换,坏处是内部FTL、磨损均衡、坏块管理全都是黑盒。你在应用层写一个文件,它内部怎么搬运数据、怎么更新FAT表,你完全看不见。
这种黑盒在工业场景里非常头疼。掉电瞬间,SD卡控制器内部SRAM里可能还有没落盘的缓存数据,FAT表条目和文件数据更新顺序也没法保证,轻则丢最后一段日志,重则文件系统整个挂掉。工业级SD卡会做掉电检测和更强的缓存管理,但价格是消费卡的好几倍,而且同样存在坏卡风险。
我的实际策略是:日志数据用普通SD卡,但在软件层面做好缓冲、定期同步、日志文件大小滚动,并且明确告知用户“这张卡是消耗品”。关键参数绝不落SD卡,这是铁律。
2.4 三种介质关键参数对比
| 介质 | 典型容量 | 写入粒度 | 擦写寿命 | 常用接口 | 掉电可靠性 | 典型型号 |
|---|---|---|---|---|---|---|
| EEPROM | 2Kb~2Mb | 字节 | 约100万次 | I2C/SPI | 高 | AT24C256 |
| NOR Flash | 8Mb~256Mb | 扇区4KB | 约10万次 | SPI/QSPI | 中,需防擦除断电 | W25Q128 |
| SD卡 | 128MB~数GB | 扇区512B | 依赖内部FTL | SDIO/SPI | 低 | 各类工业卡 |
| 适用数据 | 参数/配置 | 固件/bitstream | 日志/大数据 | - | - | - |
3. STM32+FPGA硬件架构:谁管存储,怎么接
3.1 数据流划分:FPGA专注采集,STM32统一管存储
在STM32+FPGA的架构里,我最推荐的做法是让STM32当存储的总管家,所有EEPROM、NOR Flash、SD卡全部挂在STM32的I2C、SPI和SDIO总线上。FPGA专心做它擅长的事:高速信号采集、编码器计数、ADC采样、滤波算法、相控阵相位控制这类实时任务,然后把处理结果或原始数据通过内部FIFO缓存,用SPI或并行总线交给STM32。
这个分工不是随便拍的。FPGA里写SD卡协议栈和FAT文件系统,资源开销很大,调试起来极痛苦,而且FPGA每改一版逻辑还要重新验证存储时序,工作量大到难以承受。反过来,STM32生态里有成熟的HAL库、CubeMX、FatFS移植,挂个SD卡半天搞定,所以这种“FPGA采集+STM32存储”的架构开发效率和稳定性都最好。
有一次做10Msps、16位的ADC采集项目,原始数据率是20MB/s,STM32的SDIO实测写速虽然能到20MB/s,但STM32同时还要跑通信协议和运动控制,CPU占用极高。最后我改成在FPGA里先做降采样和特征提取,把10Msps原始数据抽成每秒几千个特征点,STM32再落盘,压力瞬间消失。这就是FPGA存在的意义之一:不是所有数据都需要原样进存储。
3.2 具体接口分配:以STM32F4为例
以我常用的STM32F407为例,接口分配可以这样规划:
- EEPROM(AT24C256)挂I2C1,SCL在PB6、SDA在PB7,两颗4.7kΩ上拉电阻到3.3V。A0/A1/A2地址引脚都接地,WP引脚接高电平禁止误写。
- NOR Flash(W25Q128)挂SPI2,SCK在PB13、MISO在PB14、MOSI在PB15,片选CS用PB12。WP#和HOLD#引脚直接接3.3V,防止调试时被意外拉低导致写保护或挂起。
- SD卡用SDIO接口,4位数据模式。CLK在PC12、CMD在PD2、D0-D3在PC8-PC11。高速读写需要20MB/s以上时,别用SPI模式驱动SD卡,SDIO才是正道。
- 掉电检测用STM32内置PVD(可编程电压检测器),通过库函数配置在2.9V触发中断,并设为最高优先级。当主电源掉电时,MCU的3.3V还靠大电容撑着,能争取几十毫秒黄金时间。
硬件连好之后,还有一个容易被忽略的点:SDIO的CLK线上如果布线过长,最好加33Ω串联电阻,否则高速读写时序容易出错,而且这种错误很随机,找起来特别费劲。
3.3 FPGA的bitstream到底放哪里
FPGA本身是SRAM型器件,掉电就丢配置,所以它需要一份外部配置源。最传统的方法是用专用配置芯片EPCS,FPGA上电自己加载,不依赖外部MCU。但EPCS容量固定、采购渠道窄、远程升级不方便,所以我在多数工业控制器项目里改用“NOR Flash+STM32被动配置”方案。
具体做法是:FPGA的bitstream文件预先通过STM32写入W25Q128的固定区域,上电后STM32先从NOR Flash把bitstream读出来,再按FPGA要求的被动串行或selectMAP时序,把数据逐字节送进FPGA配置引脚。这样做的最大好处是远程升级非常灵活:STM32可以从SD卡或网络拿到新bitstream,先写进NOR Flash,再复位FPGA重新加载,几秒钟完成现场升级,不用拆机不用编程器。
代价是上电后FPGA要等STM32先把程序跑起来才能配置,启动延迟比EPCS长几十毫秒。对绝大多数工业设备来说,这点延迟完全无所谓,但如果有“上电立即要输出”的严格要求,就得换回主动配置方案。
3.4 掉电检测与供电保持电路
工业现场电源毛刺多、断电不讲道理,所以掉电保护是存储设计里最不能省的一环。我的标准做法分三层:一是PVD检测到电压跌落后,STM32立即进中断,把还在RAM里的关键状态和未落盘的参数以最快速度写进EEPROM;二是在EEPROM里做一个事务标志,比如“写入开始前先把事务ID标记为未完成,全部写完后改成完成”,下次上电一看标志就知道上次写入是否完整;三是用一个大容量电解电容或超级电容,让MCU在断电后能多撑几十毫秒,覆盖最关键的一次参数落盘。
电源保持电路的具体容量,可以用电流乘时间来估算。假设系统掉电后还需要3.3V/200mA工作20ms,那么需要提供的能量是0.2A×0.02s=0.004C,也就是至少4000uF电容在允许压降范围内释放这么多电荷。实际工程里我会留两倍余量,用1000uF到2200uF的电容效果也不错,具体看你系统功耗。
4. 驱动实现与写入策略:代码背后的设计思路
4.1 EEPROM驱动:I2C页写和神奇的5ms等待
STM32CubeMX生成I2C初始化之后,向AT24C256写数据其实就一行核心调用,但要理解背后的时序。AT24C256的7位从机地址是0x50,因为A0/A1/A2接地,转换成8位写地址就是0xA0,读地址0xA1。
// 向EEPROM地址0x0000写入32字节 uint8_t buf[32] = {0}; HAL_I2C_Mem_Write(&hi2c1, 0xA0, 0x0000, I2C_MEMSIZE_8BIT, buf, 32, 100);写完这行之后,AT24C256内部要进入写周期,典型时间5ms。在这5ms内,EEPROM对I2C总线上的任何命令都不响应,表现为从机不拉低ACK。如果你继续发命令,只会得到NACK。最稳妥的等待方式是不断发一个零长度的写命令,直到EEPROM回ACK,表示内部写周期结束。这是I2C设备手册里明确推荐的做法,比固定delay更可靠,因为不同批次、不同温度下写周期时间有差异。
页写回绕是另一个常见的坑。AT24C256的页大小是64字节,也就是说一次最多连续写64字节,而且这64字节必须在同一个页内。如果起始地址在页的尾部,比如地址0x003F,你写4字节,它会写到0x0040吗?不会,它会回绕到0x0000去!所以跨页写必须由软件拆分。写参数表时,我习惯要么把参数表长度控制在64字节以内,要么在函数里做页边界判断,先写本页剩余空间,再写下一页。
写入之后回读校验也很有必要。I2C时序边缘情况下,数据可能无声无息地写错,回读一遍整表并比较CRC,比任何理论论证都让人放心。
4.2 NOR Flash驱动:SPI命令序列和固件双镜像升级
W25Q128的SPI操作很直接,但要严格按命令时序来。先发0x9F读制造商和设备ID,确认芯片在;然后发0x06写使能;接下来可以页编程0x02或者扇区擦除0x20;最后轮询状态寄存器0x05,等待WIP位(bit0)清零。
uint32_t nor_read_id(void) { uint8_t tx[4] = {0x9F, 0x00, 0x00, 0x00}; uint8_t rx[4] = {0}; HAL_GPIO_WritePin(FLASH_CS_PORT, FLASH_CS_PIN, GPIO_PIN_RESET); HAL_SPI_TransmitReceive(&hspi2, tx, rx, 4, 100); HAL_GPIO_WritePin(FLASH_CS_PORT, FLASH_CS_PIN, GPIO_PIN_SET); return (rx[1] << 16) | (rx[2] << 8) | rx[3]; // 高字节是厂商ID }真正的难点在固件升级流程。直接擦旧写新,一旦断电就是砖头。我在多个项目里反复验证过的双镜像流程是这样的:
- Bootloader位于NOR Flash固定起始区域,上电先读启动头,里面保存“当前有效版本号”和版本对应的起始地址、长度、CRC。
- 新固件通过SD卡或串口上传后,先写入另一块空闲区域,写完后整体回读校验CRC。
- 校验通过后,更新启动头里的“有效版本”指针,指向新镜像;更新完这个指针瞬间,才算是升级完成。
- 下次上电Bootloader按指针加载新镜像。如果指针没更新成功,继续加载旧镜像。
这套流程写起来比直接擦写复杂,但它保证升级过程中任何一步断电,设备都能回到一个可用版本。工业设备远程升级,没有这种保护机制就等于在赌命。
还要注意NOR Flash改一个字节也要整扇区重写。如果只是更新配置结构体里的某个字段,正确做法是:把整个4KB扇区读进RAM,修改目标字节,擦除扇区,整扇区写回。这一步如果忘了,数据就是错的,而且错得很隐蔽。
4.3 SD卡驱动:FatFS移植和日志落盘姿势
SD卡部分我强烈建议直接用FatFS文件系统库,别自己造轮子。用STM32CubeMX把SDIO初始化好,再下载FatFS源码,配置ffconf.h里的几个开关:FF_USE_LFN设为1支持长文件名,FF_FS_EXFAT可以关掉,工业控制器用FAT32足够,兼容性更好。FF_USE_FIND设为1方便以后做日志目录遍历。
日志文件的操作模式要固定。我的标准写法是:
f_open(&file, "0:/log/data_2025.log", FA_OPEN_ALWAYS | FA_WRITE); f_lseek(&file, f_size(&file)); // 定位到文件末尾,追加写 f_write(&file, log_buffer, len, &bw); f_sync(&file); // 关键记录立即落盘 f_close(&file);f_sync的作用是把FatFS内部的缓存数据真正刷到SD卡硬件,保证文件大小和目录项是同步的。如果只f_write不f_sync,数据可能还在缓冲池里,掉电直接蒸发。但每次f_sync都有性能损耗,所以我的策略是分两级:关键故障记录、报警记录,必须立即sync;普通连续采样数据,攒够512字节(SD卡一个扇区)再写一次,既不浪费吞吐,也控制掉电影响范围。
日志文件不能无限大,否则FAT表越改越频繁,SD卡寿命掉得飞快。我给每个日志文件设上限,比如64MB,写满了自动切换新文件,保留最近N个文件,旧文件允许被覆盖。这样既控制容量,又减少SD卡FAT区擦写频率。
4.4 写入限频与磨损均衡:让“100万次”真的够用
很多工程师看到EEPROM标称100万次擦写寿命,就觉得随便写。我算一笔账:如果程序bug导致每秒写一次EEPROM,100万次大约11.5天就耗尽。这还没算页擦写放大。所以EEPROM绝对不能按固定周期傻写,正确姿势是只在参数值发生变化时写入,而且要连续两次写入之间加最小间隔,比如100ms防抖。
参数区我还会做成“双份+校验”结构。一份主区一份备份区,写入时先写备份区并标记校验,再写主区,上电读主区校验,不对就回退备份区。这样即使某个字节写坏了,系统也能自动恢复,而不是参数全丢。
NOR Flash的磨损均衡我在W25Q128上做得很简单但有效:日志或配置分区块循环使用,一个循环计数器指示当前写到哪一块,每次优先写擦写次数最少的块。对于只有10万次擦写寿命的NOR Flash,配合固件低频更新,寿命绰绰有余。SD卡内部的磨损均衡不是你能控制的,只能靠减少写入次数和降低低频写频率,前面说的“攒扇区再写”就是最重要的手段。
// 配置区循环扇区写入示意 uint8_t current_sector = read_config_sector_index(); current_sector = (current_sector + 1) % CONFIG_SECTOR_COUNT; erase_sector(current_sector); write_sector(current_sector, config_data); write_sector_index(current_sector); // 更新索引5. 调试实录:那些折腾到半夜的坑
5.1 EEPROM一上电就被清空,凶手是WP悬空
有一台控制器调试了很久,EEPROM写入读回都正常,但每次整机断电重启后参数全部归零。我一度怀疑是芯片质量问题,换了三片都一样。后来用示波器抓掉电瞬间的I2C波形,发现掉电时SDA线上有一串异常毛刺,顺着毛刺查下去,发现EEPROM的WP引脚是浮空的,没有接任何电平。掉电瞬间I2C线上的耦合噪声让WP引脚电平波动,EEPROM进入写保护解除状态,同时I2C线上还有残余信号,正好被当成写命令执行,把数据冲掉了。
修复方式非常简单:WP引脚直接接3.3V,硬件上永远写保护;再把I2C上拉电阻从10kΩ降到4.7kΩ增强抗干扰。从此再没出现过参数被清空。这个案例之后,我做任何存储电路的第一件事永远是检查WP和HOLD引脚,必须接死电平,绝不能浮空。
5.2 NOR Flash擦除卡死,SPI时钟背锅
W25Q128页擦除时,有一阵子总在产线上随机出现WIP位永远为1的卡死现象。排查发现SPI时钟配置到了42MHz,虽然W25Q128的读命令能承受这个速度,但在擦除和页编程等操作命令阶段,内部状态机对时序边沿有更严格的要求,时钟太快导致命令字节变形,芯片进入未知状态。把SPI时钟降到20MHz,擦除命令前先确保CS拉低时SCK是高电平空闲状态,卡死问题彻底消失。之后我在所有项目里,NOR Flash的SPI时钟都控制在20MHz以下,只追求稳定,不追求极限速度。
还有一个看似搞笑实则危险的坑:擦除过程中调试器复位了MCU。如果你在调试时单步或者复位,代码正好卡在擦除流程中间,NOR Flash的擦除已经开始,它不管主控是否复位都会继续擦完。但你的代码不知道这个状态,复位后如果直接发下一个擦除命令,可能干扰内部状态机。所以调试NOR Flash代码时,要么不要随意复位,要么在代码初始化里加“读状态寄存器直到WIP清0”的等待。
5.3 SD卡初始化失败和“寄存器锁死”
热词里有人提到“sd卡内部寄存器锁死”,这个我太有共鸣了。现象是:设备在写SD卡日志时突然断电,重新上电后SD卡要么返回错误,要么完全无响应。本质是SD卡控制器掉电时正处于某种内部写状态,重新上电后上电时序异常,导致寄存器状态卡死。普通消费级卡尤其明显,工业级稍好但也会中招。
我的处理流程是:上电后做完整的初始化序列CMD0→CMD8→ACMD41,ACMD41轮询最多给1秒超时,超时后强制进行一次电源完全断开再重新上电(硬件上用MOS管控制SD卡供电),再做一次初始化。实测下来绝大多数卡能救回来。如果还不行,只能备份数据后格式化,格式化前优先尝试用STM32的USB虚拟串口把卡里还能读的部分拷出来,能救多少救多少。
要更根本地降低这类问题,还得回到软件策略:关键日志f_sync勤快一些,让数据在断电前尽可能落盘;日志文件滚动写,不要长期挂在同一个文件上,减少FAT表目录项热区。做全套下来,SD卡损坏概率至少降一半。
5.4 FPGA配置加载失败,GPIO顺序搞鬼
用STM32被动配置FPGA时,遇到过大概十分之一的概率FPGA加载失败,DONE引脚始终拉不高。查了半天发现是初始化顺序问题:必须在配置前先把控制引脚设为正确状态,尤其要用GPIO输出模式拉低FPGA的PROGRAM_B,并等待足够长的时间(规格书一般要求至少2us),再释放。我原本在CubeMX初始化后立刻开始发数据,GPIO模式还是默认浮空输入,导致FPGA误判配置时序没有开始。
正确顺序是:先初始化所有配置相关GPIO,PROGRAM_B先拉低再拉高完成一次配置复位,然后检测DONE为低表示FPGA已经等待配置,最后才开始往SPI里送bitstream数据。每次配置前最好再读一次NOR Flash ID,确保存储芯片正常,避免在“Flash没就绪”的情况下读出一堆0xFF喂给FPGA。按这个流程改完,加载失败率从十分之一降到几乎为零。
5.5 存储故障通用排查顺序
最后分享一套我处理存储问题的通用排查顺序,几乎适用所有外设,按这个顺序能少走很多弯路。第一步量硬件电平:供电电压、上拉电阻、WP/HOLD引脚电平是否正确,EMMC/SD卡供电是不是在标准范围内。第二步看波形,用示波器抓I2C/SPI/SDIO波形,确认地址和命令字节,波形乱就查布线、上拉、时钟频率。第三步读ID:EEPROM读特定地址,NOR读0x9F返回的ID,SD卡回CMD0响应;这些硬件ID读不对,后面全是白费功夫。第四步检查代码层面,等待时间够不够、状态寄存器有没有轮询、命令序列有没有按手册来。最后才是查文件系统逻辑,FATFS挂载返回什么错误码、是不是空间满了、日志文件长度是否异常。
按照这个顺序,九成的存储类故障都能在半小时内定位到根因,剩下的那一成,大概率是硬件批次问题和偶然的电气干扰,需要用老化测试去暴露。
我做工业控制器多年,最大的体会是存储方案不是“芯片能存就算完事”,而是一整套可靠性设计。选什么介质、接什么电平、用什么写入策略、怎么处理掉电和磨损,环环相扣。你在这上面多花的心思,最终都会变成设备更低的返修率和现场更少的半夜电话。这篇存储篇先聊到这里,硬件篇·13我准备写通信接口隔离和RS-485总线的那些玄学故障,到时候继续。