
做工业控制器最头疼的问题之一就是数据存储。STM32FPGA 这套组合在电控、仪器、边缘计算设备里太常见了但“数据怎么存”一直没人系统讲清楚。EEPROM、NOR Flash、SD 卡三种介质各有脾气用错了轻则数据丢重则现场返工。这篇硬件篇第 12 篇我把实际项目里的分级存储方案完整拆开什么数据放 EEPROM什么数据必须进 NOR Flash什么数据才配用 SD 卡以及这三者在 STM32FPGA 架构下怎么分工、驱动怎么调、坑在哪里一次性讲透。这套方案适合正在做工业控制器、数据采集终端、或者打算用 STM32 当主控、FPGA 做前端采集的工程师参考。如果你只在单片机里写过几个变量存 Flash没系统想过存储架构这篇能帮你把整个数据存储体系理顺。1. 为什么工业控制器要搞分级存储——先想清楚再动手1.1 三种存储介质的本质差异先说个很多人容易忽略的前提EEPROM、NOR Flash、SD 卡不是“谁替代谁”的关系它们的工作原理完全不同适合干的活也完全不同。EEPROM比如 24C02、24C256是字节级擦写按字节写入写一个字节大概要 5ms 到 10ms寿命在 100 万次擦写左右容量从几 Kbit 到几 Mbit。它的核心价值是“按字节改、改完就稳”特别适合存那些改动频率低、但绝对不能丢的参数。我常用一个类比EEPROM 像个随手记事本你在上面改一个字其他字都不受影响。NOR Flash比如 W25Q64、W25Q128是按扇区擦除、按页编程。以 W25Q64 为例一个扇区 4KB擦除一次要 400ms 左右页编程一次最多 256 字节寿命在 10 万次擦写左右。它跟 EEPROM 的本质区别是NOR Flash 不能直接改单个字节必须先把整个扇区擦掉再重写。这就像储物柜你要换柜子里的一张纸得先把整个柜子清空再重新摆放。SD 卡则是块设备内部有主控做坏块管理和磨损均衡对外提供扇区读写接口容量可以做到几十 GB单次写一个扇区512 字节在 SDIO 高速模式下只需几百微秒。它的本质是一个完整的“小型文件系统载体”适合大量数据按文件组织存储。我把它类比成仓库东西多、种类杂但管理靠仓库管理员SD 卡主控你只管往里送大件。介质写入基本单位典型写入耗时擦写寿命典型容量适合数据EEPROM字节5~10ms/字节约100万次Kbit~Mbit参数、校准系数NOR Flash扇区擦除页编程擦除400ms、写页1ms约10万次4~128MB固件、配置、日志SD 卡扇区512B数百μs~ms取决于主控GB~TB海量采集数据1.2 分级存储的分工逻辑搞清楚了介质差异分级存储的设计逻辑就自然浮现了不是“哪个快用哪个”而是“数据的性质决定它该放哪”。我把工业控制器的数据分成三层。第一层是设备参数类比如设备序列号、通信地址、PID 参数、传感器校准系数这些数据一年可能才写几次但一旦丢失设备就废了必须放 EEPROM因为它字节级写入最可靠而且抗掉电。第二层是固件和运行配置类比如固件升级包、用户配置块、最近一小时的操作日志这些数据需要频繁擦写、单次数据量几百字节到几 MB适合放 NOR Flash因为它的随机读速度快、支持 XIP 原地执行而且坏块管理逻辑比 SD 卡简单可靠得多。第三层是海量采集数据比如振动波形、温度曲线、高速 ADC 采样流这些数据量大、以“文件”为单位管理、掉电后允许割尾必须用 SD 卡。这个分层逻辑的核心思想是“可靠性与成本匹配”。工业现场最怕的是数据全丢但全丢的根源往往不是存储介质选了差的而是把参数和日志混在一个存储区里频繁擦写把关键参数区搞坏了。分级存储从一开始就把不同性质的数据物理隔离让每次写入的代价和它保护的数据价值相匹配。2. 硬件架构STM32 和 FPGA 各自管什么存储2.1 存储拓扑方案工业控制器里 STM32FPGA 的分工通常是STM32 跑通信协议栈、控制逻辑、人机交互FPGA 做高速采集、信号处理、实时控制。那么存储应该挂在哪边这个问题我在项目里试过两种拓扑差距很大。方案一是 STM32 统一管理三种存储FPGA 通过并行总线FSMC/FMC和 STM32 交换数据。STM32 的 I2C 接 EEPROMSPI 接 NOR FlashSDIO 接 SD 卡FPGA 采集完的高速数据先打进 STM32 的 DMA再由 STM32 决定写哪儿。优点是软件栈集中存储调度逻辑简单缺点是高速采集数据过 STM32 会有瓶颈如果 ADC 采样率超过几 MspsSTM32 的中断和 DMA 压力会非常大数据容易丢。方案二是 FPGA 直接接管 SD 卡写入STM32 只管 EEPROM 和 NOR Flash。FPGA 内部用 FIFO 缓存高速采样流自己控制 SDIO 或 SPI 接口写 SD 卡写满一帧就切换缓冲区完全不占 STM32 的 CPU。STM32 只负责参数存储、固件管理和文件系统的元数据协调。这个方案在高速数据采集场景下非常稳但 FPGA 里写 SD 卡驱动尤其是 SDIO 带 FATFS工作量明显更大。我的实际选择是混合方案如果采样率在 1Msps 以下用方案一开发快、调试方便如果采样率在几 Msps 以上用方案二让 STM32 专注业务FPGA 专注吞吐。需要说明的是SD 卡的 FATFS 文件系统我依然放在 STM32 侧管理FPGA 只做裸扇区写入这样避免在 FPGA 里折腾文件系统的复杂度。2.2 电路设计要点无论选哪种拓扑有几个电路细节我必须单独拎出来这几处翻了车驱动写再漂亮也白搭。第一是上拉电阻。I2C 的 SCL、SDA 必须接上拉电阻典型值 4.7kΩ如果总线上挂的设备多、线长要降到 2.2kΩ。SPI 的 MISO 线在高速模式超过 10MHz下建议串联 22Ω 电阻减少振铃。SDIO 的数据线、时钟线同样要串联电阻我实测在 25MHz SDIO 模式下不加串联电阻CLK 线上的过冲能超过 3.3V直接导致读卡不稳定。第二是 SD 卡的电源和上电时序。SD 卡必须用独立的 3.3V LDO 供电不能直接从 STM32 的 VCC 拉因为 SD 卡写入时电流能冲到 100mA 以上会和主控产生串扰。上电时序上先给 SD 卡供电等电源稳定 10ms 之后再去初始化 SDIO否则会偶发初始化失败。很多新手直接在主控初始化代码里操作 SD 卡结果低温环境下一半概率找不到卡。第三是写保护引脚。NOR Flash 的 WP写保护引脚不要直接接地要接上拉电阻再连 STM32 的 GPIO这样可以在固件里动态控制写保护防止误擦除。SD 卡的 WP 引脚卡座上的写保护检测不是标准的很多卡座根本没引出一般的做法是直接把卡的 Data3/CD 脚复用为卡检测在初始化时读一下电平状态。第四是 FPGA 的 IO 电平匹配。现在的 STM32 大多是 3.3V IOFPGA 如果用 2.5V bank和 STM32 互联时要做电平转换。我的惯例是 FPGA 和 STM32 的所有存储相关信号统一走 3.3V bank并且 PCB 上让这些信号线在 FPGA 端加 33Ω 串联电阻防止 FPGA 内部 IO 驱动能力过强导致信号边沿过陡。3. 驱动层实操三块存储的读写实现3.1 EEPROMI2C 读写与掉电保护EEPROM 的驱动本身不复杂但有几个点必须处理到位。以 24C256 为例I2C 地址是 0xA07 位地址 0x50页大小是 64 字节。写多个字节时不能跨页写如果数据跨页必须拆分写入否则会回卷覆盖同页其他数据。代码上我会这样处理uint8_t eeprom_write_bytes(uint16_t addr, uint8_t *buf, uint16_t len) { uint16_t offset 0; while (offset len) { uint16_t page_remain 64 - (addr % 64); // 当前页剩余空间 uint16_t chunk (len - offset page_remain) ? page_remain : (len - offset); // 按 16 位地址 数据块的方式写入 i2c_start(); i2c_send_byte(0xA0); // 器件地址写模式 i2c_send_byte((addr 8) 0xFF); i2c_send_byte(addr 0xFF); for (uint16_t i 0; i chunk; i) { i2c_send_byte(buf[offset i]); } i2c_stop(); delay_ms(10); // 等待内部写周期完成 addr chunk; offset chunk; } return 0; }注意那个 delay_ms(10) 不能省。EEPROM 每次页写最多 64 字节时内部写周期约 5ms在 5ms 完成前发新的写命令总线会无响应这是 I2C 最常见的坑。我见过不少人想当然把延时缩到 1ms 靠轮询 ACK 来提速结果在工业现场温度偏高时大面积丢数据。EEPROM 写慢就慢点可靠优先。掉电保护更关键。我在这块踩过坑设备突然断电EEPROM 数据变成全 0xFF 或全 0x00。后来排查发现是写入过程中掉电EEPROM 内部电荷泵没完成编程。解决方案是双备份参数区在 EEPROM 里放两份主区放在前一半备份区放后一半每次写入先写备份区并校验再写主区并校验上电时比较两个区如果主区校验失败且备份区校验通过就用备份区恢复主区。这个策略简单但极其有效。3.2 NOR FlashSPI 接口与擦写策略NOR Flash 驱动核心是三件事读状态寄存器等待空闲、扇区擦除、页编程。以 W25Q64 为例写过一次的都知道擦除前必须先写使能0x06否则擦除命令无效。扇区擦除0x20后必须轮询状态寄存器的 BUSY 位bit0等它变 0 才能写下一页。uint8_t nor_flash_sector_erase(uint32_t sector_addr) { // 等待上次操作完成 while (nor_read_status() 0x01); nor_write_enable(); CS_LOW(); spi_send_byte(0x20); // Sector Erase spi_send_byte((sector_addr 16) 0xFF); spi_send_byte((sector_addr 8) 0xFF); spi_send_byte(sector_addr 0xFF); CS_HIGH(); // 轮询 BUSY直到擦除完成 while (nor_read_status() 0x01); return 0; }这里有两个容易忽视的地方。第一擦除一个 4KB 扇区要 400ms这段时间内系统不能断电否则扇区数据会处于未定义状态。所以要有掉电检测中断检测到主电源掉电瞬间先用超级电容或大电容的能量把正在擦写的扇区标记为“擦除未完成”上电后如果发现这个标记就把整个扇区重擦。第二页编程最多 256 字节超了必须分页否则数据会回卷。磨损均衡在 NOR Flash 上常被忽略但日志类的循环写入必须做。我的做法是划分一个日志区比如 1MB 的空间分成 256 个扇区写日志时按顺序写扇区写满一个扇区就移到下一个整个区域写满后再从第一个扇区开始擦除覆盖。这样每个扇区的擦写次数均匀不会出现集中在某几个扇区导致提前坏块。实测在 10 万次擦写寿命下1MB 日志区按 4KB 扇区分可以支撑接近 2560 万次日志写入工业设备完全够用。3.3 SD 卡SDIO 模式与文件系统接入SD 卡是三种介质里驱动最重的。我建议直接用 STM32 的 SDIO 外设别用 SPI 模式。SPI 模式虽然简单但写入速度只有几百 KB/s工业上要存高速采样数据完全不够。SDIO 四线模式跑到 25MHz 可以获得 10MB/s 左右的写速度对于大部分工业采集场景足够。接口上我直接在 STM32 上挂 FatFS完整流程是SDIO 底层驱动初始化 → 卡识别ACMD41、读取 CSD → 设置块长度 → 挂载 FatFS → f_open 写文件。关键在写入策略千万别一个字节一个字节写要攒缓冲区按扇区对齐写。我常用 2KB 的缓冲区攒满一次写 4 个扇区实测比单扇区写快 3 倍以上而且对 SD 卡寿命友好。// 批量写一帧数据到文件 FRESULT write_log_frame(uint32_t frame_id, uint8_t *buf, uint32_t len) { FATFS fs; FIL file; UINT written; f_mount(fs, , 1); f_open(file, 0:/data/log.bin, FA_OPEN_APPEND | FA_WRITE); f_write(file, frame_id, 4, written); // 帧头 f_write(file, buf, len, written); // 数据 f_sync(file); // 刷到物理层 f_close(file); f_mount(NULL, , 0); return FR_OK; }注意 f_sync 的调用频率不用每帧都调否则性能崩。正确的做法是每写满 4KB 或者积累到一定数据量再 sync 一次掉电时最多丢失最近未 sync 的 4KB。这里有个行业常见误区以为用了 FatFS 就安全了其实 FatFS 为了追求性能f_write 只是把数据写入缓冲并没有真正落盘只有 f_sync 才把数据写到 SD 卡扇区所以掉电丢失的本质是 “缓冲区没落盘”。如果你的设备允许掉电丢最后一小段数据那就攒到 4KB 再 sync这是性能和数据安全的平衡点。4. 数据分层策略什么数据放哪里4.1 第一级设备参数与校准系数EEPROMEEPROM 区我存五类数据设备序列号8 字节、通信地址和波特率4 字节、PID 参数12 字节、传感器校准系数16 字节、错误标志位2 字节。这些数据的共同特征是写入次数极低、掉电绝对不许丢。实操上有两个技巧。第一是参数区要建一个结构体统一序列化后写入。千万别把参数散着存否则 EEPROM 空间碎片化后面想加一个参数还要迁移数据。我习惯这样定义一个参数结构体放在一个公共头文件里typedef struct { uint32_t magic; // 魔数用于校验 0x5A5A0101 uint32_t device_sn; uint8_t comm_addr; uint8_t comm_baud_idx; float pid_kp; float pid_ki; float pid_kd; float sensor_gain; float sensor_offset; uint16_t error_flags; uint16_t crc16; // 结构体整体 CRC } DevParams;写之前先填充结构体算 CRC16再整体写进 EEPROM。读出来时先检查 magic 和 CRC16任何一项不对就认为参数区损坏启用备份区恢复。第二是写入策略写参数前先写“正在更新”标志位全部写完后写“更新完成”标志位上电后如果发现“正在更新”标志存在而“更新完成”标志不存在说明写入中途掉电直接用备份区覆盖主区。这套双标志加双备份的机制我在十几个项目里验证过再也没出现过参数区神秘丢失的问题。4.2 第二级固件、配置与近期日志NOR FlashNOR Flash 区我存三类固件备份区用于 IAP 升级、系统配置文件、近期运行日志环形覆盖。固件备份区用 A/B 双分区方案。当前固件在 A 区运行新固件先下载到 B 区下载完成后校验 CRC校验通过才把启动标志指向 B 区然后软复位。如果新固件启动失败Bootloader 检测到启动标志异常自动回滚到 A 区。这个方案的本质是利用 NOR Flash 的可随机读特性做原地校验比从 SD 卡升级更可靠。系统配置文件比如用户设置的操作参数也放这个区因为它的数据量比 EEPROM 大几十 KB而且希望支持批量更新。这里要注意配置文件不能放在固件备份区附近否则升级固件时容易误删。我把一个 8MB 的 W25Q64 按 4MB 2MB 2MB 划分A/B 固件各 4MB配置区 2MB日志区 2MB彼此物理隔离。日志区用环形覆盖的算法我上文提过再补充一个细节日志文件在开机时要检查头部的“当前写位置”指针如果发现指针异常比如上次掉电停留在半扇区就把那个扇区标记为无效并跳过从下一个扇区开始写。这样日志总是最新的且可解析的不会因为一个坏扇区让整个日志区读不出来。4.3 第三级海量采集数据SD 卡SD 卡上跑的是真正的大数据振动波形每通道 16 位 50kHz、温度趋势每秒一条、事件记录每次告警前后的原始数据。这些数据按天分文件文件名规则是“设备号日期”例如 DEV01_20250601_A.bin一天一个文件。这里重点说一下 FPGA 高速数据怎么落到 SD 卡。我的架构是FPGA 内部开两个 8KB 的 FIFO交替缓存当一个 FIFO 满时通过中断通知 STM32STM32 把 FIFO 数据搬到一个 4KB 的 DMA 缓冲区攒满两个 DMA 缓冲区8KB后对齐写 SD 卡。这样 STM32 的 CPU 占用率控制在 5% 以内数据吞吐能达到 4MB/s 左右。如果你用方案二FPGA 直接写 SD 卡FPGA 侧就是裸扇区写入不经过 FatFS写完一个 4KB 块就往 FAT 表里写一次更新性能更极限但调试难度翻倍。数据落盘之后还少不了一个“文件收割”机制。SD 卡不是无限大要定期清理老文件但工业设备不能等 SD 卡满了再处理所以我会在系统里维护一个“剩余空间阈值”检查低于 20% 时自动删除 30 天前的文件并记录一条删除日志到 NOR Flash。这样即使现场长期无人维护设备也能自己滚动留存数据。5. 常见问题与排查实录5.1 EEPROM 写入丢失与 I2C 总线卡死症状设备持续运行一段时间后参数偶尔变 0xFF或者 I2C 总线上 SDA 一直为低主控无法再通信。排查思路先把 SCL 和 SDA 的波形抓出来看如果 SDA 被拉死几乎可以断定是从设备把 SDA 拉住了。最常见的原因是 EEPROM 在写周期内收到了新的写命令此时它不应答且可能锁住总线。解决有两个方向I2C 时序严格遵守写周期延时另外写后立即读 ACK如果没有 ACK 就发 STOP 条件并复位 I2C 外设。硬件上我建议在 I2C 上加上拉电阻到 VCC且阻值不要大于 4.7kΩ否则噪声会把 SDA 拉低到误触发。关键教训不要用 I2C 中断驱动去做 EEPROM 写入除非你非常清楚时序否则老老实实用阻塞式写入加延时稳定压倒一切。EEPROM 的价值是可靠性不是速度。5.2 NOR Flash 擦写变慢与坏块处理症状设备用了一两年后NOR Flash 擦写明显变慢写入日志偶尔失败。原因多半不是芯片坏了而是你的日志算法没有磨损均衡导致某一个扇区被反复擦写先达到了寿命上限。NOR Flash 的擦写次数到了之后不是“立即失效”而是擦除时间变长、偶尔数据校验失败。排查方法读取每个扇区的擦写计数。如果你没有在驱动里存储每个扇区的擦写计数很可能根本看不出来所以我建议在扇区的末尾预留 4 个字节存计数每擦一次自增一次。日志算法上不要用“从区头写到区尾”的线性模式改成“随机起始扇区 顺序循环”的方式让磨损分布更均匀。如果一个扇区出现两次写入校验失败就把它标记为坏块跳过它继续用后面的扇区。NOR Flash 坏块管理是自己做的没有 SD 卡主控替你兜底。5.3 SD 卡掉电文件损坏与读写性能症状设备突然断电后SD 卡上的最近一个文件打不开或者 f_open 直接返回错误需要格式化才能恢复。先说结论FatFS 本身在掉电时很容易产生文件损坏因为 FAT 表和数据区的写入不是原子的。靠硬件上电时序做不了完美保护但可以大幅降低损坏率。我的经验是重要数据不要以“持续追加单个文件”的形式写而是采用“固定大小分块文件”策略。具体做法是每写满 8MB 就关闭当前文件创建一个新文件这样损坏最多影响一个 8MB 文件其余的完好。其次写入过程开启 FatFS 的 f_sync 周期同时保证掉电检测中断触发时第一时间把 DMA 缓冲区的数据写完并且 f_sync 一次。性能问题往往来自 FATFS 的默认配置。STM32 的 FatFS 默认是每写一个扇区都要读取 FAT 表在小文件连续写入时非常慢。我调过一版配置开启 FA_SEEKEND、打开 f_fsync 批量刷、提高内部缓冲区到 4KB实测 16KB 连续块的写入速度从 300KB/s 提升到了 2MB/s 以上空间占用还更均匀。5.4 排查速查表现象大概率原因解决方向EEPROM 数据偶尔丢失写入中掉电、跨页写回卷双备份双标志、拆分页写、写后校验I2C 总线卡死 SDA 低写周期内重复发命令/上拉过弱延长写周期等待、4.7k 上拉、I2C 外设复位NOR Flash 擦写慢无磨损均衡、扇区寿命耗尽擦写计数、循环扇区、坏块跳过日志区读取乱码擦除时掉电、半扇区残留扇区头写状态标志、上电扫描恢复SD 卡文件损坏掉电 FAT 表不一致分块文件、f_sync 批量刷、掉电中断处理SD 卡写入速度低FatFS 配置不当/SPI 模式开 SDIO 四线、加大缓冲区、扇区对齐写6. 一点扩展心得写到这后面再做存储方案我的习惯是先画一张“数据流表”把设备里所有需要存的数据列出来标上写入频率、数据量、掉电是否允许丢、是否需要掉电保持然后再去看它该进哪种介质。别一上来就选芯片、写驱动先把“什么数据放哪”这个逻辑想清楚后面能省大半调试时间。另外分享一个我踩过的比较深的坑早期我以为 FATFS 挂在 SD 卡上就万事大吉某次在客户现场设备连续运行 72 小时后SD 卡目录结构整个崩溃。后来查出来是 FatFS 长时间运行后打开文件句柄泄漏尤其是在频繁开关文件时容易出现。解决的土办法是每半小时做一次 f_mount 全部卸载再挂载虽然这个操作耗掉几十毫秒但能彻底清除文件系统内部状态。如果后续有精力我会把“存储状态机”和“功耗管理”也单独写一篇存储在其他项目里拿来直接用。最后提醒一句工业存储永远不要赌运气。该做备份做备份该加校验加校验哪怕多占一点容量也要保证现场哪怕掉电、干扰、异常复位数据都在。这套分级方案在多个项目里跑了几十万小时的运行记录希望它也能让你少走几天弯路。