
做工业控制器这些年我越来越觉得现场最能体现产品功力的往往不是CPU主频多高、算法多花哨而是“数据到底怎么落”这件事。这期是该系列的第十二篇前面把整机方案、通讯链路、电源防护都过了一遍今天专门把存储层级拆开讲——为什么一个STM32FPGA的控制器里要同时装EEPROM、NOR Flash和SD卡而不是一块大Flash全搞定。简单说工业控制器的数据分好几类配置参数、标定数据、运行日志、批量采集数据、固件镜像。它们对容量、擦写次数、掉电可靠性、读写速度的要求完全不同。用同一类介质硬扛要么贵得离谱要么迟早翻车。三级存储本质是把“经常改的”“按块覆盖的”“大量搬运的”分开伺候。这篇就把每一层的选型逻辑、接线方式、踩坑细节全部交代清楚照着实操即可不用再翻几十份数据手册。1. 先搞清楚控制器里到底有哪几类数据再谈存储介质1.1 工业控制器里实际存在的五类数据在设计存储方案之前建议你先坐下来把产品定义里的数据一项项列出来。我做过好几个项目后总结下来绝大多数工业控制器逃不出这几类配置参数类。包括设备地址、通讯波特率、输入量程、报警阈值、校准系数等。体积一般就几十到几百字节写入频率极低可能设备出厂后一年都不改几次但掉电绝对不能丢而且要保证不被干扰改写。标定调试类。产线调试或现场整定时PID参数、零点增益、偏置值会反复试错修改可能一个下午就写上百次。这类数据对擦写寿命要求很高而且写错了要能快速回滚。运行日志与黑匣子类。设备开停记录、故障码、报警事件、关键变量曲线。写入频率从每秒几次到每分钟一次不等容量需求几MB到几十MB通常要求循环覆盖即最老的数据被最新数据顶掉。批量采集类。振动波形、工艺过程数据、能耗统计体积可能达到几百MB甚至几GB需要周期性搬走导出分析不要求实时覆盖但要求能稳定连续写入。固件镜像类。Bootloader加应用固件体积几十KB到几MB更新频率很低但一旦更新中断导致损坏设备要能自我恢复不能被锁死。这五类数据放到一张表里看矛盾非常明显配置数据要求写入寿命高、掉电可靠但容量需求极小采集数据要求容量大、写入连续但擦写次数可能没那么敏感固件数据要求绝对可靠但不追求频繁写入。1.2 介质选型逻辑EEPROM、NOR Flash、SD卡各管一段三类介质各自的脾气性格完全不同放在一起对比就很清楚了特性项EEPROMAT24C系列NOR FlashW25Q系列SD卡NAND Flash内核典型容量2Kbit~4Mbit4MB~128MB数百MB~数百GB擦写寿命约100万次约10万次约数千次MLC写入粒度字节页写加速按页256B擦除按扇区4KB按块管理FAT表频繁更新掉电保存极稳单字节可写比较稳需防扇区擦除中断依赖文件系统掉电易损FAT接口I2CSPISPI或SDIO典型成本低中按GB算单价最低如果你只选一种介质会遇到一个无解的矛盾用EEPROM装固件容量和成本都不现实用SD卡存配置参数寿命和可靠性都扛不住用NOR Flash两者兼顾但擦写次数又不如EEPROM而且掉电时扇区擦除中断会让管理变得麻烦。所以分级不是“炫技”是被需求逼出来的。EEPROM伺候高寿命小数据NOR Flash伺候中等容量加频繁改写SD卡伺候大数据搬运。这就像仓库里不能只有一个货架——螺丝钉、半成品和整机发货区注定要用不同的管理方式。1.3 整体存储架构与数据流走向我这边实际项目中的连接关系大致是这样STM32通过I2C独占一颗EEPROM存放配置参数和标定数据STM32通过SPI独占一颗NOR Flash存放固件镜像、运行日志和黑匣子数据STM32再通过SDIO或SPI接SD卡存放批量采集数据、报表导出和升级包暂存FPGA不直接操作任何存储介质它只把采集到的高速数据流通过并行总线或SPI送进STM32的DMA通道再由STM32按节奏落盘。这个架构里STM32是存储模块的唯一管理者FPGA是数据产生者。这样做的原因后面章节会专门展开但先记住结论存储管理权集中于MCU实时数据采集交给FPGA两边用明确的硬件接口交接。2. STM32 和 FPGA 的存储分工谁来管数据怎么交接不丢帧2.1 为什么不是STM32把存储全包FPGA专心跳采样很多第一次做双芯片方案的工程师会问FPGA不是也能接SD卡、也能操作Flash吗为什么不让它也直接写存储核心原因是并发管理问题。工业控制器里FPGA承担的是高速ADC采集、编码器计数、脉冲输出这类硬实时任务它的时间粒度是纳秒和微秒级的。如果让FPGA去写Flash或SD卡写卡过程中命令响应是毫秒级甚至几十毫秒级的而且中间还伴随着坏块管理、文件系统簇链更新这类“非确定性”操作。一旦FPGA被这些操作拖住高速采集流就会出现周期抖动甚至丢脉冲。STM32这边则不同它的优势在于有完整的外设中断、DMA和文件系统生态处理毫秒级存储操作不影响任何实时任务。打个比方FPGA是流水线上的高速机械臂STM32是仓库调度员。机械臂负责把每一个零件精准放到位调度员负责按批次把成品运到不同的库区。你不能让机械臂自己跑去做仓库的账目工作哪怕它也能干最后一定会拖慢产线。2.2 高速数据流交接的三种做法FPGA采集的数据要想交给STM32落盘硬件通道一般有三种选择。一是异步FIFO加并行总线。FPGA内部用双口RAM或FIFO缓存采集数据写入侧由FPGA时钟控制读出侧由STM32的FSMC或FMC接口以外部存储器访问方式读取。STM32只需要配置一个地址区间用DMA批量搬数即可吞吐可以到几十MB/s。适合振动波形、高速计数这类突发数据流。二是SPI加DMA。FPGA做SPI从机STM32做SPI主机配合DMA连续读取。数据速率取决于SPI时钟一般10Mbps到30Mbps之间适合中等速率传感器数据。这个方案接线少调试方便项目中后期改动也容易。三是GPIO模拟并口加FIFO水线信号。FPGA侧FIFO接近半满时拉高一个GPIOSTM32收到中断后启动DMA搬运一批。适合数据率不恒定、但偶发大批量数据的场景。关键在于FIFO深度要能覆盖从“水线触发”到“DMA启动”的最大延迟时间内的数据量这个需要结合时钟和中断响应算。我这边的经验是不管用哪种方式STM32侧接收模块务必用DMA而不是中断逐字节读。曾见过一个项目用中断读SPI数据率稍高一点CPU就全耗在进出中断上了日志永远慢半拍后来改成DMA加环形缓冲CPU占用从70%降到8%。2.3 存储仲裁规则为什么FPGA坚决不碰SD卡文件系统第三点也是很多人容易忽略的。SD卡的FAT文件系统在设计上默认是“单写者模型”一个时刻只允许一个设备修改FAT表。如果STM32在写日志FPGA也同时去写采集文件两个写入者互相不知道对方改了哪个扇区FAT表很快就会被写坏表现为卡上文件打不开、目录结构错乱。所以整个方案里SD卡只归STM32一个主人。FPGA要存的数据先进内部FIFO再由STM32统一搬到SD卡。如果你确实需要更高的存储并行度那应该考虑的是换双端口存储器做暂存而不是让FPGA直接操作SD卡。3. EEPROM这一层配置参数与标定数据的“保险柜”3.1 选型与容量估算EEPROM这一层99%的工业控制器选的是AT24C系列I2C接口价格便宜、资料多、替换料遍地都是。容量怎么定有个最简单的估算方法把你所有需要掉电保存的参数全部加起来再乘以二或三。为什么要翻倍因为可靠存储通常要有双备份或CRC校验区不是原始数据一版就完事的。举个例子设备有80个配置参数每个参数按4字节浮点数算加上参数ID和CRC总共约400字节。你选512字节或者1Kbit的EEPROM肯定不够用因为还要存状态标志、帧格式版本、设备序列号、出厂校准数据。我一般按需求量的三倍选型比如算出来600字节就直接上AT24C044Kbit512字节或AT24C088Kbit1KB。选型还有一个容易被忽视的点EEPROM的页大小。AT24C系列常见页大小为8字节或16字节WP引脚极性各厂家略有差异。页写时跨页会回卷覆盖这是新手最容易踩的坑。后面驱动设计里我会专门讲怎么避免。3.2 STM32侧I2C驱动与EEPROM读写要点EEPROM挂在STM32的I2C外设上驱动代码不复杂但这几个点做到了现场会少掉很多头发WP写保护引脚一定要设计好上电时序。AT24C系列的WP引脚为高时禁止写入。很多产品直接把WP接地图省事但这样EEPROM在任何时刻都处于可写状态一旦MCU上电时序异常或程序跑飞写错地址就可能破坏参数区。正确做法是把WP接到MCU的一个GPIO正常运行时置高保护只有确需写参数前才拉低。这里注意上电瞬间GPIO默认是输入浮空状态光靠GPIO驱动WP可能不可靠我习惯在WP引脚硬件上再并一个下拉或上拉电阻保证复位期间处于默认锁定状态。I2C写时序页写和数据轮询缺一不可。AT24C系列在收到一页数据后会进入内部写周期典型2到5毫秒期间不响应任何总线请求。驱动里写一个字节后不要立即发下一个命令要先发送“设备地址写零”进行ACK轮询直到器件应答为止。这个机制比固定延时好用得多既不会因晶振误差导致写不完整也不浪费等待时间。数据存储格式帧结构校验。不要裸存参数建议每条参数都是一个自描述的小帧包含参数ID、长度、数据、CRC16或累加和。读取时逐帧校验CRC错误则回退到备份区备份区也错则启用默认值并上报异常。实现上就是逻辑地址分两个区各存一份正常写请求先更新备份区再更新主区加上帧计数标志上电就能判断哪份是新哪份是旧。代码片段可以这样组织typedef struct { uint16_t param_id; uint16_t data_len; uint8_t data[64]; uint16_t crc; } ParamFrame; void EEPROM_WriteParam(uint16_t id, uint8_t *buf, uint16_t len) { ParamFrame frame; frame.param_id id; frame.data_len len; memcpy(frame.data, buf, len); frame.crc crc16((uint8_t *)frame, sizeof(frame) - 2); // 先写备份区 EEPROM_PageWrite(EEPROM_BACKUP_ADDR, (uint8_t *)frame, sizeof(frame)); // 再写主区 EEPROM_PageWrite(EEPROM_MAIN_ADDR, (uint8_t *)frame, sizeof(frame)); }3.3 防“参数莫名其妙丢”的三个关键细节做过的设备多了你会发现“EEPROM数据丢”这个故障报告大概率不是EEPROM本身坏了而是周边设计问题。第一个是I2C总线被拉死。SDA线被某个外部干扰拉低如果没有超时恢复机制整个I2C总线会一直卡在等待状态。我的习惯是在I2C驱动里加总线恢复函数检测到总线忙超过50毫秒就主动切换GPIO模式把SCL翻转九次让从机释放总线再重新初始化。第二个是WP极性搞反。替换料和原装料在WP有效电平上可能不同。批量生产时如果物料替换了写入保护可能完全失效。产线测试一定要覆盖“参数空写验证”即上电后只允许外部写入一次第二次写须先解锁防止程序异常时反复改写。第三个是地址跨页。AT24C页写时如果写入数据跨过了页边界地址会回卷到页首导致数据被写在错误的位置。解决方法是驱动层提前判断当前页剩余空间需要跨页时自动拆成两次或多次页写。这些逻辑在仿真里跑得很顺但在现场总线噪声较大的环境里可能偶发所以建议每次写完读回校验校验不过就再写并计数告警。4. NOR Flash这一层固件镜像与运行日志的“黑匣子”4.1 为什么固件和日志选SPI NOR而不是NANDNAND Flash容量大、单价低很多工程师第一反应是用它做存储。但工业控制器场景下SPI NOR有几个压倒性优势NOR Flash支持XIP也就是片上执行。虽然我们一般不会直接从Flash里跑代码但Bootloader设计上如果需要做在线升级回滚NOR的线性寻址让操作更简单。NOR Flash擦写模型简单。按4KB扇区擦除按256字节页写入坏块极少几乎不需要做复杂的坏块管理。相比之下NAND有坏块、有擦写干扰需要ECC和坏块表文件系统也要专门适配复杂度直接上一个数量级。NOR Flash掉电可靠性更好。NAND擦除时如果掉电容易出现“假坏块”后续访问越来越不可靠。NOR的扇区擦除过程中掉电最多是该扇区数据无效重新擦除即可且不会滚动损坏其它扇区。我项目里用的主流型号是W25Q64、W25Q128这些SPI接口四线制QSPI也能支持容量64Mbit或128Mbit。配套的库和工具链非常成熟测试仪器也好找。4.2 日志环形覆盖区的设计日志和黑匣子数据的特点是“永远写不满”因为满了就要顶掉最旧的。NOR Flash没法按字节擦除只能按扇区擦。所以环形覆盖区的基本单元就是扇区。我一般这样规划把一片NOR Flash按功能分区比如64Mbit容量1Mbit留给固件A/B镜像1Mbit留给参数备份剩余6Mbit全部作为日志区。日志区再划分成若干个4KB扇区使用两个固定地址记录日志区的写指针和擦除指针。写入流程是这样的当前扇区写完最后一页先把写指针和目标扇区标志写入专用状态区再擦除下一个扇区。上电恢复时根据状态区的记录判断是接着写还是先恢复。擦除扇区前把“准备擦除扇区N”的标志先写好擦除完成后再写“擦除完成”。这样即使掉电落在擦除中途上电后也能知道哪个扇区状态不完整直接重新擦除就行。另一个容易忽略的问题是跨扇区日志条目的处理。一条日志记录可能在扇区末尾被截断此时宁可在扇区末尾写一个“本条无效”的填充标记把完整日志从下一个扇区重新写也不要让日志跨扇区拼接。跨扇区拼接在掉电恢复时会带来灾难性的复杂度——你要同时恢复两个扇区的状态。4.3 固件A/B镜像与损坏恢复固件升级是工业控制器最怕出事的环节之一。方案很简单两块镜像区A区和B区Bootloader根据升级标志决定启动哪块。升级流程大致是应用程序收到升级包写到B区如果当前启动的是A区写完校验固件头、CRC和长度全部无误后置“待启动B区”标志然后复位。Bootloader上电检查标志发现待启动B区则校验B区完整后跳转。如果B区校验失败Bootloader自动回退到A区并清除标志。坑在于“标志放哪里”。如果放NOR Flash要保证标志的写入是原子的不能出现写了一半被擦除的情况。我的做法是设置四个状态字连续存放置位时先全部写0x55校验通过再写0xAA读取时按多数表决。这比单一标志位可靠得多至少不会出现“明明要启动新固件却起错了老固件”的灵异事件。调试阶段用STM32 ST-LINK Utility非常方便整片读取、逐扇区对比、擦除和编程都是图形界面操作。产线阶段则建议写一个基于串口YModem或网口的升级工具而不是依赖仿真器毕竟操作工不该去碰ST-LINK。4.4 产线批量烧录与整片校验NOR Flash的烧录可以在贴片前用编程器烧好也可以贴片后通过MCU的ISP或BOOT引脚烧录。前者适合大批量稳定产品后者适合小批量灵活生产。我这边更推荐贴片后烧录因为可以同时测试MCU与Flash的SPI链路。批量烧录有个经验不要只烧固件区要把整个Flash的布局一起烧进去。也就是扇区分配表、日志区初始状态、固件A/B镜像全部一次性写好。这样设备上电后固件和日志区状态是一致的不会出现“固件是新的但日志区标志还是上代状态”的错乱。烧录完成后一定要做整片读回校验不要只校验固件区。产线遇到过用编程器批量烧录固件区正常但日志区的某几个扇区初始值被编程器误判的情况如果不是整片比对后面现场出问题了才追回来代价就大了。5. SD卡这一层批量采集数据与远程运维文件的大仓库5.1 什么数据会走到SD卡这一层SD卡在工业控制器里不是一个“标配”只有当数据量超过一定规模时它才登场。我的判断标准很简单如果连续记录能力超过64MB或者需要把数据文件导出到电脑上分析就考虑SD卡。典型场景是振动波形采集。比如一台旋转机械的状态监测终端FPGA以20kHz采样率连续采集三轴振动一秒钟就是120KB一小时就是432MB。这个量级用NOR Flash存储既不经济也不现实必须落到SD卡。另一个场景是报表和升级包暂存。日报表、事件记录、参数导出文件以及远程下发的固件升级包都可以放在SD卡里。控制器通过以太网或串口接收文件先写SD卡校验后再执行。这样做的好处是升级包的传输和烧录解耦网络抖动不会直接影响固件烧录过程。5.2 SPI模式还是SDIO模式接口选择要实际SD卡有两种工作模式SPI模式和SDIO模式。SDIO四线并行带宽高但初始化时序复杂对引脚分配和PCB走线要求高。SPI模式接线少调试方便兼容性在所有卡上都很好缺点是带宽低。工业控制器里我倾向于优先考虑SPI模式除非你的采集数据率确实超过了SPI的吞吐。SPI模式时钟跑到20MHz时理论带宽约2.5MB/s实际加上协议开销约1.5到2MB/s。如果你连续记录数据率低于这个值SPI模式完全够用换来的是整个系统简单可靠。如果你决定用SDIO要注意SDIO的时序对信号完整性更敏感SD卡座离MCU的走线要短线间要加地隔离而且SDIO的4位模式对卡的质量也更挑剔。现场劣质SD卡在这个模式下会频繁出错排查起来比SPI模式麻烦得多。5.3 FATFS裁剪与掉电保护别让一次断电毁了整张卡SD卡加FATFS几乎是标准组合。代码用FatFs开源库裁剪时注意几个关键配置项_FS_FAT32开、_USE_LFN开否则中文文件名没法处理_FS_READONLY不要开因为你要写_FS_MINSSIZE根据扇区设置_USE_FNVAR和_FS_EXLOCK在单任务环境不用开。掉电保护是SD卡方案的核心问题。FATFS写文件时如果中途断电FAT表可能处于不一致状态轻则文件损坏重则整个目录结构错乱。我的处理策略有三层第一层物理层写SD卡前把数据先拼成整块比如512字节对齐减少单次写操作次数SD卡电源上并联一个大容量电容容值按“最大写入时长×电流”估算保证写入过程中断电也能撑到写完。第二层逻辑层用“临时文件重命名”策略。关键数据先写到一个隐藏临时文件写完并f_sync后再通过f_rename替换正式文件。这样即使写临时文件时掉电正式文件仍是上一版完整的。第三层系统层在SD卡上维护一个连续的区域作为“原始数据块”不经过FATFS定期把原始数据块封包导入FAT文件系统。这样高频采集数据根本不碰FAT表FAT表只在封包时刻更新一次损坏概率大幅降低。f_sync的调用位置也需要注意。每次写完一批数据后立即调用而不是等到f_close时才flush。工业现场最怕的就是数据写在缓冲里断电后缓冲丢了。别省那几百毫秒的写卡时间。5.4 量产镜像、序列号与“寄存器锁死”的排查思路热词里有人提到“sd卡内部寄存器锁死”这个现象多半不是寄存器物理锁死而是卡内控制器的状态机进入了无法恢复的分支。常见原因是多主机竞争例如在系统运行时调试工具或外部读卡器同时访问了SD卡命令线电平互相冲突导致卡内控制器进入异常状态。遇到这种问题先把SPI或SDIO的片选信号时序用示波器抓一遍确认是否出现两个主机同时拉低片选的情况。如果没有再查SD卡的初始化序列是否完整特别是CMD0、CMD8、ACMD41、CMD2、CMD3、CMD7这几条命令是否符合规范很多“认不出卡”的问题是初始化序列中ACMD41的参数错误。产线镜像这块我做过一个批量方案用一张基准SD卡制作整卡镜像再通过镜像工具批量写入。但必须强调SD卡里要预留一个序列号分区批量写入后每个设备单独改写序列号否则全世界的设备序列号都一样后面做运维管理必乱。另一个坑是镜像工具和文件系统兼容性。用某个镜像软件做出来的卡插到控制器上FATFS可能不认原因是镜像工具重建的分区表格式与FatFs的期望不一致。产线测试要包含“镜像后上机识别并读写”这一步不能只验证镜像软件里能看到文件。6. 常见故障速查表与独家避坑记录故障现象可能原因排查方向解决方案上电后配置参数全部变默认值WP上电时序不对EEPROM被意外改写示波器抓WP和I2C SCL/SDA上电波形WP引脚加默认下拉/上拉程序跑飞时保持锁定EEPROM写成功但读回不对页写跨页回卷查看AT24C数据手册页大小驱动层做跨页拆分写后读回校验FATFS挂载SD卡失败SD卡初始化序列不完整逻辑分析仪抓CMD序列严格按规范实现CMD0/CMD8/ACMD41确认卡响应日志写一段时间后卡死FAT表频繁更新导致扇区磨损查看文件系统错误代码用原始块写入定期封包策略减少FAT更新掉电后上一版固件也起不来固件标志位被写坏或擦除中断检查Bootloader的状态机四个状态字多数表决擦除前先写准备标志采集数据大量丢帧FPGA FIFO溢出或DMA响应不及时看水线时间与DMA启动延迟增大FIFO深度或改用FMC并行总线直接DMA同一批卡某几张不识别卡质量和SPI模式兼容性差异换不同品牌卡测试用SPI模式降低兼容性风险产线固定卡源最后一个补充的经验调试存储模块时不要只盯着芯片数据手册要习惯用代码里打印关键状态。我经常在STM32里加一个调试串口或者USB虚拟串口实时输出“哪个区在写、当前指针在哪、这次校验结果是什么”。这套调试口在联调阶段秒杀任何示波器因为你看到的是系统层面的状态流而不是一根根信号线上的毛刺。做过的控制器多了以后我的体会是存储设计最大的风险不在介质本身而在你对“掉电瞬间系统正在干什么”有没有把握。所以我给每个存储模块都留了一个状态记录区上电第一件事是自检恢复而不是直接继续正常业务。这个习惯救过我很多次也建议你把这个思路带进自己的设计里——存储这件事永远要假设下一秒就会断电来设计。