1. 工业现场的数据分层逻辑:为什么一颗芯片扛不住所有存储需求
工业控制器和消费类电子产品最大的区别,在于它面对的数据类型极其杂乱。一台典型的运动控制器或者边缘网关,运行时同时存在几类完全不同性质的数据:PLC 下发的配方参数、编码器零位校准值、设备序列号和出厂日期这类掉电绝不能丢、但写入频率极低的关键数据;运行过程中累积的报警记录、产量统计、工艺曲线这类写入频繁、容量需求中等、允许一定延迟的日志数据;还有视觉检测的原始图像、长时间波形采样这类容量巨大、单次写入、事后导出的批量数据。
这三类数据如果都往一颗芯片里塞,结果一定是灾难。我见过不少项目图省事,把配方参数和运行日志全写进 STM32 内部的 Flash,跑几个月就出现参数区被日志写坏的情况——STM32 内部 Flash 的擦写寿命通常标称 1 万次左右,日志一天写几百次,一年下来就逼近极限了。更麻烦的是,内部 Flash 擦除时整个扇区一起擦,你改一个字节可能要把旁边存着校准参数的扇区一起动,风险极高。
所以工业控制器里做分级存储不是炫技,是被现场逼出来的工程选择。核心思路很简单:按数据的"重要性 × 写入频率 × 容量"三个维度分层,让每种介质干它最擅长的事。
- EEPROM:字节级可改、擦写寿命百万次级别,适合存参数、校准值、序列号这类"小、精、贵"的数据。
- NOR Flash:容量比 EEPROM 大得多(常见 16MB~256MB),支持随机读取、可按扇区擦除,适合存日志、配置、字库、小文件系统。
- SD 卡:容量以 GB 计,成本极低,适合存图像、大批量采样数据、可插拔导出的历史记录。
而 STM32 和 FPGA 的分工,则是这套方案里另一个关键决策点。STM32 擅长协议栈、文件系统、任务调度这类"软件逻辑密集"的活;FPGA 擅长高速并行采集、时序严格的数据搬运、多路 ADC 同步这类"硬件时序密集"的活。把两者拼起来,STM32 当"大脑"管存储策略,FPGA 当"搬运工"管高速数据流,各司其职。
这篇就把这套分级存储方案从硬件选型、接口设计、分区规划到实际踩坑,完整拆一遍。不管你是做毕业设计的学生,还是正在搭边缘网关的工程师,这套思路都能直接抄。
2. 三种存储介质的物理特性决定了它们各自的岗位
选型之前必须搞清楚每种介质的底层脾气,否则分区规划就是拍脑袋。这一节把 EEPROM、NOR Flash、SD 卡的关键特性摊开讲,重点说清楚"为什么它适合干这个、不适合干那个"。
2.1 EEPROM 的字节级改写能力是它不可替代的根本
EEPROM(电可擦可编程只读存储器)最核心的特性是支持字节级读写和擦除。你往地址 0x10 写一个字节,不需要动 0x0F 和 0x11,这在参数存储场景里太重要了。工业设备经常要在线修改某个校准系数,如果每次改一个参数都要擦一整块,那旁边的数据就有丢失风险。
它的擦写寿命通常在100 万次量级,读次数几乎无限。容量一般很小,I2C 接口的常见型号从 2Kbit 到 512Kbit(也就是 256 字节到 64KB),SPI 接口的能到 1Mbit 甚至 2Mbit。对工业控制器来说,几 KB 到几十 KB 的参数区完全够用。
代价是写入速度慢。I2C 接口的 EEPROM 单字节写入后需要 5ms 左右的内部写周期,这期间总线不响应。如果你要连续写 100 个字节,用页写模式(通常 32 或 64 字节一页)能快不少,但页写有跨页回卷的坑,后面细说。
注意:EEPROM 的"字节可改"是相对 NOR Flash 的"扇区擦除"而言的。它内部其实也是按页组织,只是硬件帮你处理了读-改-写的细节,对软件呈现为字节级接口。
2.2 NOR Flash 的随机读取和扇区擦除是它的定位
NOR Flash 和 NAND Flash 经常被混淆。工业控制器里用的大多是SPI NOR Flash,比如 W25Q 系列。它的特点是:
- 支持随机读取,可以像内存一样按地址读任意字节,执行代码(XIP)都行,这是 NAND 做不到的。
- 擦除必须按扇区,最小擦除单位通常是 4KB,有些型号支持 32KB、64KB 块擦除。
- 擦写寿命约 10 万次,比 EEPROM 低一个数量级,但比 NAND 高。
- 写入前必须先擦除,且只能把 1 写成 0,要把 0 变回 1 必须擦除整扇区。
这就决定了 NOR Flash 的用法:适合"整块更新、读取频繁、写入不极端频繁"的场景。日志记录如果做成"攒够一个扇区再擦写一次"的环形缓冲,寿命完全够用。存字库、网页资源、配置文件这些只读或极少改的数据,更是它的主场。
容量上,SPI NOR Flash 从 1MB 到 512MB 都有,工业上 16MB 和 32MB 最常见,价格便宜到几块钱。
2.3 SD 卡的容量优势背后是复杂的内部管理
SD 卡本质是 NAND Flash 加一颗控制器。那颗控制器负责坏块管理、磨损均衡、ECC 纠错,对外呈现为一个块设备。它的优势是容量大、单位成本极低,32GB 的卡十几块钱,存图像和长时间采样数据毫无压力。
但它的坑也最多:
- 写入有延迟抖动,内部垃圾回收时单次写入可能卡顿几百毫秒,实时性要求高的数据不能直接写。
- 掉电容易损坏文件系统,尤其是 FAT32,写一半断电可能整个分区都挂掉。
- 寿命取决于卡的质量,工业级卡和消费级卡差距巨大,消费卡在频繁小写入场景下可能几个月就坏。
- 热插拔和接触问题,工业现场振动环境下卡座接触不良是常见故障。
所以 SD 卡在工业控制器里的定位是**"大容量、可导出、非关键"**的数据仓库,绝不能拿它存参数或实时控制数据。
| 介质 | 擦写寿命 | 最小写入单位 | 典型容量 | 接口 | 工业定位 |
|---|---|---|---|---|---|
| EEPROM | 100 万次 | 字节 | 2KB~64KB | I2C/SPI | 参数、校准、序列号 |
| NOR Flash | 10 万次 | 扇区(4KB) | 1MB~512MB | SPI/QSPI | 日志、配置、字库 |
| SD 卡 | 取决于卡 | 扇区(512B) | 4GB~128GB | SDIO/SPI | 图像、批量采样、导出 |
3. STM32 与 FPGA 的分工边界:谁管策略,谁管搬运
分级存储不只是"选三种介质"这么简单,真正的难点在于数据怎么在三种介质之间流动、谁来调度。这就涉及到 STM32 和 FPGA 的职责划分。
3.1 STM32 负责存储策略和文件系统
STM32 在这套方案里扮演"存储管理员"的角色,具体承担:
- 协议栈和文件系统:跑 FatFs 管理 SD 卡,跑自己的 Flash 驱动管理 NOR,跑 I2C/SPI 驱动管理 EEPROM。
- 分区表和磨损均衡逻辑:决定哪块数据写哪个扇区、日志环形缓冲的读写指针怎么走。
- 掉电保护:检测到电源跌落时,把关键数据紧急落盘。
- 对外通信:通过串口、以太网、USB 把存储的数据导出去。
STM32 的强项是软件生态成熟,HAL 库、FatFs、各种中间件拿来就用,开发效率高。但它的短板是实时性和并行能力有限,多路高速 ADC 同步采集、严格时序的数据流,靠 STM32 的 CPU 轮询或者 DMA 很难做到又稳又快。
3.2 FPGA 负责高速采集和时序严格的数据搬运
FPGA 在这套方案里的价值体现在三个地方:
- 多路并行采集:比如 8 路 ADC 同时采样,FPGA 可以给每路独立的时序控制,采样时刻精确对齐,STM32 做不到这种确定性。
- 高速数据流缓冲:采集到的数据先写进 FPGA 内部的 Block RAM 或者外挂的 SRAM,攒够一批再通过并行总线或 SPI 传给 STM32,避免 STM32 被高频中断拖垮。
- 时序严格的外设控制:比如驱动 NOR Flash 的 QSPI 高速读写、SD 卡的 SDIO 时序,FPGA 可以用状态机精确控制每个时钟沿。
一个典型的配合方式是:FPGA 采集 → 内部 FIFO 缓冲 → 通过 FSMC/SPI 传给 STM32 → STM32 决定存哪一级介质。FPGA 只管"把数据搬进来",STM32 只管"把数据放对地方",职责清晰。
3.3 两者之间的数据通道怎么选
STM32 和 FPGA 之间的通信接口选择,直接影响整套方案的吞吐能力。常见方案对比:
| 接口 | 带宽 | 引脚数 | 适用场景 |
|---|---|---|---|
| SPI | 10~50Mbps | 4 | 低速控制、参数传递 |
| FSMC/FMC | 可达 100MB/s | 20+ | 高速数据流、并行总线 |
| 并口+中断 | 中等 | 16+ | 中等速率、简单可靠 |
| 以太网 | 100Mbps~1Gbps | 视 PHY | 远距离、大数据量 |
我的经验是:控制命令和状态走 SPI,高速数据流走 FSMC。SPI 接线少、抗干扰好,适合传参数和握手信号;FSMC 带宽高,适合把 FPGA 缓冲的图像或采样数据批量搬给 STM32。如果项目对成本敏感、数据量不大,纯 SPI 也能凑合,但要注意 SPI 时钟拉高后信号完整性问题。
提示:FSMC 接 FPGA 时,地址线和数据线的时序要仔细约束,尤其是建立时间和保持时间。FPGA 侧用同步 FIFO 对接,能大幅降低时序调试难度。
4. 分区规划:把三种介质当成一个逻辑存储空间来设计
硬件选好了、分工定了,接下来是最考验工程经验的部分——分区规划。分区做得好,后面写代码顺风顺水;分区做得烂,运行半年就出各种诡异问题。
4.1 按数据生命周期划分存储区域
我习惯按数据的"生命周期"来分区,而不是按介质来分。具体做法是先列出所有需要存储的数据类型,标注它们的写入频率、容量、掉电敏感度,然后映射到介质上。
一个实际项目的分区表长这样:
| 数据类别 | 介质 | 容量 | 写入频率 | 掉电要求 |
|---|---|---|---|---|
| 设备参数/校准值 | EEPROM | 4KB | 极低 | 绝不能丢 |
| 出厂信息/序列号 | EEPROM | 1KB | 一次性 | 绝不能丢 |
| 运行日志 | NOR Flash | 8MB | 中 | 可丢最近几条 |
| 配置文件 | NOR Flash | 1MB | 低 | 不能丢 |
| 字库/资源 | NOR Flash | 4MB | 只读 | 不涉及 |
| 图像/采样数据 | SD 卡 | 剩余 | 高 | 可丢 |
| 历史记录导出 | SD 卡 | 剩余 | 中 | 可丢 |
这张表的关键在于把"绝不能丢"和"可丢"的数据物理隔离。参数在 EEPROM 里,日志在 NOR 里,就算 NOR 写坏了也不影响设备启动。
4.2 EEPROM 分区要留冗余和校验
EEPROM 容量小,但每一字节都金贵。我的分区习惯是:
- 前 256 字节:设备信息区,存序列号、型号、出厂日期、硬件版本。这部分出厂写一次,之后只读。
- 中间区域:参数区,每个参数占固定长度,带 CRC 校验。
- 末尾区域:参数备份区,主参数区校验失败时从备份恢复。
关键技巧是双备份 + CRC。每个参数存两份,写入时先写备份区再写主区,读取时两份都校验,哪份对用哪份。这样即使写入过程中掉电,也总有一份是完整的。
typedef struct { uint16_t magic; // 0x5A5A 标识有效 uint16_t param_id; int32_t value; uint16_t crc; // 前面所有字节的 CRC16 } eeprom_param_t;写入流程:擦除备份槽 → 写备份槽 → 校验 → 擦除主槽 → 写主槽 → 校验。读取流程:读主槽校验,失败读备份槽,再失败用默认值。
4.3 NOR Flash 的日志环形缓冲设计
NOR Flash 存日志最忌讳"来一条写一条",那样扇区擦除次数会爆炸。正确做法是环形缓冲 + 批量落盘。
具体设计:把日志区划分为 N 个扇区(比如 8MB 分成 2048 个 4KB 扇区),维护一个写指针。日志先写进 RAM 缓冲,攒够一个扇区或者超时(比如 5 秒)再擦除下一个扇区并整块写入。写指针循环前进,写满一圈就覆盖最老的日志。
这样做的收益:擦除次数从"每条日志一次"降到"每扇区一次",寿命提升几百倍。代价是最新的日志可能还在 RAM 里没落盘,掉电会丢最后几秒——对日志来说完全可以接受。
注意:NOR Flash 擦除一个 4KB 扇区通常需要 50~200ms,这期间不能对该扇区做任何操作。如果你的日志写入很频繁,要确保缓冲够大,别让擦除成为瓶颈。
4.4 SD 卡的文件系统选择和掉电保护
SD 卡上跑 FatFs 是最常见的选择,但 FatFs 在掉电时容易损坏 FAT 表。几个实用对策:
- 定期调用
f_sync():把缓存刷到卡上,减少掉电丢失量。 - 用两个文件交替写:A 文件写满切换到 B,B 写满切回 A,避免单文件损坏导致全部丢失。
- 关键数据先写 NOR 再转存 SD:SD 卡只做"仓库",不做"唯一副本"。
- 选用工业级 SD 卡:带掉电保护电容的卡能显著降低损坏概率。
如果项目对可靠性要求极高,可以考虑在 SD 卡上跑 LittleFS 或者 SPIFFS 这类掉电安全的文件系统,代价是兼容性差一些,PC 上不能直接读。
5. 接口电路与时序:硬件设计里最容易翻车的地方
方案设计得再漂亮,硬件画错一样白搭。这一节讲三个介质在硬件设计上的关键点,都是实际项目里踩过的坑。
5.1 I2C EEPROM 的上拉电阻和总线电容
I2C 总线是开漏输出,必须接上拉电阻。电阻选多大有讲究:
- 阻值太小(如 1K):上升沿快,但静态电流大,低电平时灌电流可能超过器件极限。
- 阻值太大(如 10K):省电,但上升沿变慢,高速通信时波形爬不上去。
经验值:3.3V 系统、100kHz 速率用 4.7K,400kHz 速率用 2.2K。如果总线上挂了很多器件,总线电容增大,上拉电阻要相应减小。
另一个坑是总线电容上限。I2C 规范规定总线电容不超过 400pF,超了波形就会畸变。走线长、器件多的时候要算一下,必要时用 I2C 缓冲器或者分总线。
5.2 SPI NOR Flash 的片选和时钟走线
SPI NOR Flash 的硬件设计有几个要点:
- 片选信号(CS)要单独走线,不要和其他信号并行走太长,否则容易串扰导致误选中。
- 时钟线(CLK)尽量短,高速时(50MHz 以上)要考虑阻抗匹配,必要时串一个 22~33 欧姆的电阻。
- 电源去耦电容要靠近 Flash 的 VCC 引脚,0.1uF 加 1uF 组合,抑制擦写时的大电流冲击。
- WP 和 HOLD 引脚如果不用,要上拉到 VCC,别悬空。
我遇到过一次诡异故障:Flash 偶尔读不出数据,查了半天发现是 CLK 走线太长,和旁边的高速信号串扰,导致时钟边沿抖动。缩短走线加串阻后问题消失。
5.3 SD 卡的电源和热插拔保护
SD 卡座是机械部件,工业现场振动环境下最容易出问题。硬件设计要注意:
- 电源加 TVS 和滤波:SD 卡插拔瞬间会有浪涌,加 TVS 管保护。
- 数据线加串阻:22 欧姆左右的串阻能抑制反射,提高信号质量。
- CD(卡检测)引脚要接,软件能感知卡是否插入。
- 电源开关:用 MOS 管控制 SD 卡供电,不使用时断电,既省电又避免热插拔冲击。
提示:SD 卡在 SPI 模式下比 SDIO 模式兼容性好,但速度慢。如果只是存数据、对速度要求不高,SPI 模式更省心,引脚也少。
6. 软件实现:从驱动到存储管理的完整链路
硬件搭好之后,软件是重头戏。这一节按"底层驱动 → 中间层管理 → 上层应用"的顺序,把关键代码逻辑讲清楚。
6.1 EEPROM 的页写和跨页回卷处理
EEPROM 的页写模式能大幅提升写入速度,但有个经典坑:页写不能跨页。比如页大小 32 字节,你从地址 30 开始写 10 个字节,写到地址 32 时会回卷到本页开头(地址 0),把前面的数据覆盖掉。
正确做法是写之前计算本次能写多少字节到页边界:
#define EEPROM_PAGE_SIZE 32 int eeprom_write(uint16_t addr, const uint8_t *data, uint16_t len) { while (len > 0) { uint16_t page_remain = EEPROM_PAGE_SIZE - (addr % EEPROM_PAGE_SIZE); uint16_t chunk = (len < page_remain) ? len : page_remain; if (eeprom_write_page(addr, data, chunk) != 0) return -1; addr += chunk; data += chunk; len -= chunk; delay_ms(6); // 等待内部写周期 } return 0; }每次写完一页要等 5~6ms 的内部写周期,这期间 EEPROM 不响应任何命令。如果不等就发下一条,数据会丢。
6.2 NOR Flash 的擦写均衡和坏块管理
NOR Flash 虽然比 NAND 可靠,但 10 万次寿命也不是无限的。日志区用环形缓冲天然实现了均衡——每个扇区轮流被擦写,磨损均匀。但如果某个扇区因为制造缺陷提前坏了,要有检测机制。
简单做法:每个扇区头部存一个写入计数器和 CRC。写入前先读计数器,如果发现某个扇区写入次数远超平均值,或者 CRC 反复校验失败,就标记为坏块,跳过它。
typedef struct { uint32_t write_count; // 该扇区被擦写的次数 uint32_t data_len; // 有效数据长度 uint16_t crc; // 头部 CRC } nor_sector_header_t;读取时先校验头部,头部坏了说明这个扇区写入过程中掉电,数据不可信,跳过。
6.3 SD 卡 FatFs 的挂载、写入和同步策略
FatFs 的典型使用流程:
FATFS fs; FIL fil; UINT bw; f_mount(&fs, "0:", 1); // 挂载 f_open(&fil, "0:/log.txt", FA_OPEN_APPEND | FA_WRITE); f_write(&fil, buf, len, &bw); f_sync(&fil); // 关键:定期同步 f_close(&fil);f_sync()是掉电保护的关键,它把 FatFs 的缓存和 FAT 表刷到卡上。但调用太频繁会拖慢速度,我的策略是每写 64KB 或者每 5 秒同步一次,在可靠性和性能之间取平衡。
如果检测到卡被拔出(CD 引脚变化),要立即停止写入并关闭文件,否则 FatFs 内部状态会错乱。
6.4 掉电检测与紧急落盘
工业现场掉电是常态,必须做掉电保护。硬件上用一个比较器监测电源电压,低于阈值时触发 STM32 的外部中断。中断里要尽快把 RAM 里的关键数据写进 EEPROM 或 NOR。
这里有个时间预算问题:STM32 从检测到掉电到完全断电,靠电源上的大电容通常能撑 10~50ms。这段时间够写几十字节到 EEPROM,或者写一个 NOR 扇区。所以紧急落盘的数据量要提前规划好,别指望掉电时能写几 MB。
void EXTI_PowerFail_Handler(void) { // 最高优先级,关中断,直接操作寄存器 eeprom_write_emergency(critical_params, sizeof(critical_params)); nor_flash_write_sector(emergency_sector, ram_buffer, buf_len); while(1); // 等待断电 }注意:掉电中断里不要调用 FatFs 或任何带锁的函数,直接用底层驱动写,避免死锁。
7. 实测中暴露的问题和排查思路
方案跑起来不代表没问题,这一节记录几个实际项目中遇到的故障和排查过程,都是花钱买来的经验。
7.1 EEPROM 偶发读写出错:上拉电阻和总线电容的锅
某项目 EEPROM 在实验室一切正常,装到现场后偶发读写失败。排查过程:
- 先用示波器看 I2C 波形,发现 SDA 上升沿明显变缓,高电平只有 2.8V(3.3V 系统)。
- 算总线电容:走线约 20cm,加上几个器件,估算超过 300pF。
- 上拉电阻是 10K,太大。换成 2.2K 后波形改善,但静态电流上升。
- 最终方案:缩短走线 + 换 3.3K 上拉 + 降低速率到 100kHz,问题解决。
教训:I2C 上拉电阻要根据实际总线电容算,不能照抄参考设计。
7.2 NOR Flash 日志丢失:擦除期间掉电导致扇区损坏
现场反馈设备重启后最近一段日志丢失。分析发现是日志写入时正好掉电,扇区擦了一半,头部 CRC 校验失败,整个扇区被跳过。
改进方案:双扇区交替写。日志写扇区 A 的同时,扇区 B 保留上一批数据。A 写坏了还能从 B 恢复。代价是日志容量减半,但可靠性大幅提升。
7.3 SD 卡文件系统损坏:写入过程中拔卡
操作员在设备运行时直接拔 SD 卡,导致 FAT 表损坏,卡插回电脑提示需要格式化。这个问题无解,只能靠流程规避:
- 软件检测到 CD 引脚变化立即停止写入。
- 面板上标注"运行中勿拔卡"。
- 关键数据同时存 NOR,SD 卡只做导出副本。
7.4 STM32 与 FPGA 通信丢数据:FSMC 时序不匹配
FSMC 接 FPGA 时,STM32 读到的数据偶尔错位。用逻辑分析仪抓时序发现,FPGA 侧的输出建立时间不够,STM32 在数据稳定前就采样了。
解决:在 FPGA 侧加一级寄存器打拍,延长数据保持时间;同时调整 STM32 FSMC 的DataSetupTime和AddressSetupTime参数,留足余量。改完后连续跑 72 小时无错。
| 故障现象 | 根因 | 解决方案 |
|---|---|---|
| EEPROM 偶发读写失败 | 上拉电阻过大、总线电容超标 | 减小上拉、缩短走线、降速 |
| NOR 日志丢失 | 擦除期间掉电 | 双扇区交替写 |
| SD 卡文件系统损坏 | 运行中拔卡 | CD 检测 + 流程规范 |
| FSMC 通信错位 | 时序不匹配 | FPGA 打拍 + 调整 FSMC 参数 |
8. 几个能直接抄的工程习惯
最后分享几个我在多个项目里固化下来的习惯,都是踩坑之后总结的,能帮你少走弯路。
第一,所有存储数据都带 CRC。不管是 EEPROM 里的参数、NOR 里的日志、还是 SD 卡里的文件,写入时算 CRC,读取时校验。多花几个字节,换来的是数据可信度。我见过太多项目因为没校验,把损坏的数据当成正常值用,导致设备行为异常。
第二,关键参数永远双备份。EEPROM 里主备两份,NOR 里关键配置也存两份。写入时先写备份再写主,读取时主坏了读备份。这个习惯救过我好几次。
第三,存储操作全部异步化。别在主循环里同步等 EEPROM 写周期或者 NOR 擦除,用状态机或者 RTOS 任务处理,主循环该干嘛干嘛。否则一个 200ms 的擦除能把整个控制周期拖垮。
第四,上电自检存储介质。设备启动时读一遍 EEPROM 参数、校验 NOR 头部、挂载 SD 卡,任何一项失败都要有明确的降级策略。比如 SD 卡挂了就只存 NOR,NOR 挂了就只存 EEPROM 并报警。
第五,留一个"恢复出厂"的物理或软件入口。存储数据损坏时,操作员能一键恢复到默认参数,比现场调试快得多。
这套 STM32 + FPGA 的分级存储方案,核心思想就是让合适的介质干合适的事,让合适的芯片管合适的活。EEPROM 管参数、NOR 管日志、SD 卡管大数据,STM32 管策略、FPGA 管搬运。把这套逻辑理清楚,剩下的就是按部就班地实现和调试。实际做下来,最花时间的往往不是写代码,而是硬件时序调试和掉电场景的验证——这两块建议预留充足的时间。