news 2026/11/6 3:05:48

工业控制器分级存储设计:EEPROM、NOR Flash与SD卡选型及STM32/FPGA协同

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
工业控制器分级存储设计:EEPROM、NOR Flash与SD卡选型及STM32/FPGA协同

1. 工业控制器存储架构的整体设计思路

工业控制器和消费类电子产品最大的区别在于:它可能要在无人值守的配电柜里连续跑十年,断电重启是家常便饭,现场电磁环境恶劣,而且一旦参数丢失或日志断档,排查成本极高。所以存储方案的设计目标从来不是"能存就行",而是分级可靠、各司其职、掉电不丢关键数据。

STM32 加 FPGA 这个组合在工业控制器里非常常见:STM32 负责业务逻辑、通信协议栈、人机交互;FPGA 负责高速采集、多路并行时序、实时信号处理。两者各自有存储需求,又需要共享一部分数据,这就决定了存储不能只挂一颗芯片了事,而要按"访问频率、数据量、掉电敏感性、写入寿命"四个维度做分级。

我一般把工业控制器里的数据分成四类,对应四种存储介质:

数据类别典型内容数据量写入频率掉电要求推荐介质
配置参数校准系数、设备ID、通信地址、量程几十字节~几KB极低(出厂/维护时)绝对不能丢EEPROM
关键日志故障记录、动作事件、运行统计几十KB~几MB中低频不能丢NOR Flash
大批量数据波形采样、图像帧、历史曲线几十MB~GB高频可容忍少量丢失SD 卡
运行缓存实时变量、DMA 缓冲与 RAM 同量级极高无需掉电保持SRAM/DDR

这个分级逻辑背后的核心考量是写入寿命和访问粒度的匹配。EEPROM 可以按字节擦写,寿命通常在百万次级别,适合存那种"一年改不了几次但一次都不能错"的参数;NOR Flash 按扇区擦除,寿命十万次量级,支持 XIP 和随机读,适合存结构化的日志和固件;SD 卡容量大、单位成本低,但擦写寿命和掉电一致性都比较脆弱,只适合存"丢了也能接受"的大数据。

注意:很多新手会把所有数据一股脑塞进 SD 卡,觉得容量大省事。工业现场一次意外断电就可能让 FAT 文件系统直接损坏,整卡数据全废。这个坑我见过太多次了。

STM32 和 FPGA 的分工也要在存储设计里体现出来。STM32 侧通常通过 I2C 挂 EEPROM、通过 SPI 挂 NOR Flash、通过 SDIO 挂 SD 卡,这三条总线在 STM32 上都有成熟的 HAL 库支持。FPGA 侧如果要独立存数据,一般用 SPI 访问 NOR Flash,或者通过并行总线直接读写 SRAM,再或者把数据通过 FMC/FSMC 交给 STM32 统一落盘。具体选哪种,取决于数据产生的速率和实时性要求。

2. 三种存储介质的核心原理与选型依据

2.1 EEPROM:为什么参数区非它不可

EEPROM 的全称是电可擦除可编程只读存储器,它的存储单元是浮栅 MOS 管。写数据时通过隧穿效应把电子注入浮栅,擦除时把电子抽走,靠浮栅上电荷的有无来表示 0 和 1。这个物理机制决定了它两个关键特性:按字节寻址、可反复擦写。

工业上常用的 EEPROM 型号有 AT24C 系列(I2C 接口)、93C 系列(Microwire 接口)、25 系列(SPI 接口)。AT24C02 是 2Kbit(256 字节),AT24C256 是 256Kbit(32KB),基本覆盖了参数存储的需求。I2C 接口只需要两根线,STM32 的硬件 I2C 或者软件模拟都能驱动,布线简单,这是它被广泛采用的原因。

选 EEPROM 存参数,核心看三点:

  • 字节级擦写:改一个参数不用擦整个扇区,不会影响旁边的数据。
  • 寿命足够:100 万次擦写寿命,按每天改 10 次算能用 270 年,实际项目根本用不完。
  • 掉电保持:数据保存 40 年以上,工业设备的生命周期完全覆盖。

但 EEPROM 也有明显的短板:容量小、写入速度慢(I2C 100kHz 下写一页要几毫秒)、单位成本高。所以它只适合存参数,绝不能拿来存日志或大数据。

2.2 NOR Flash:日志和固件的可靠载体

NOR Flash 的存储单元结构和 EEPROM 类似,也是浮栅,但它的读取方式和 EEPROM 完全不同——支持随机访问,可以像读 RAM 一样按地址读任意字节,这就是所谓的 XIP(eXecute In Place)能力。很多 STM32 项目把程序直接放在外部 NOR Flash 里跑,就是利用这个特性。

NOR Flash 的写入规则和 EEPROM 差别很大,这是新手最容易踩坑的地方:

  • 必须先擦后写:写入只能把 1 变成 0,要把 0 变回 1 必须擦除。
  • 擦除按扇区:最小擦除单位通常是 4KB 或 64KB,不能按字节擦。
  • 擦除寿命有限:一般 10 万次,比 EEPROM 低一个数量级。

工业控制器里用 NOR Flash 存日志,通常的做法是环形日志 + 扇区轮转。把 Flash 分成若干个扇区,每个扇区写满后擦除最老的扇区继续写,这样能把擦写次数均匀分摊到所有扇区上,避免某个扇区被写穿。W25Q 系列(W25Q64、W25Q128)是工业项目里最常见的 SPI NOR Flash,容量从 8Mbit 到 128Mbit 都有,价格便宜,驱动成熟。

2.3 SD 卡:大容量数据的性价比之选

SD 卡内部其实是 NAND Flash 加一个控制器,控制器负责坏块管理、磨损均衡、ECC 纠错。对主控来说,SD 卡就是一个块设备,通过 SDIO 或 SPI 接口按扇区(512 字节)读写。

SD 卡的优势很直接:容量大(几 GB 到几十 GB)、单位容量成本极低、可插拔更换。但它的坑也最多:

  • 掉电一致性差:写操作进行到一半掉电,FAT 表可能损坏,整卡数据读不出来。
  • 写入寿命不透明:消费级 SD 卡用的是 TLC 甚至 QLC 颗粒,擦写寿命可能只有几百次,工业级 SLC 卡能到几万次,价格差好几倍。
  • 控制器行为不可控:卡内部什么时候做垃圾回收、什么时候搬数据,主控完全不知道,可能导致写入延迟突然飙高。

所以 SD 卡在工业控制器里的定位很明确:存那些丢了不影响设备运行的大数据,比如历史曲线、波形记录、图像帧。关键参数和故障日志绝对不能只存 SD 卡。

实操心得:如果项目必须用 SD 卡存关键数据,一定要做双备份——SD 卡存一份,NOR Flash 再存一份精简版。我做过一个项目,现场电网波动频繁,SD 卡平均三个月坏一张,后来加了 NOR Flash 备份才稳住。

3. STM32 侧存储驱动的实操实现

3.1 I2C 读写 EEPROM 的完整流程

STM32 驱动 AT24C 系列 EEPROM,硬件上 SCL、SDA 各接一个 4.7k 上拉电阻到 3.3V,A0/A1/A2 决定器件地址。以 AT24C02 为例,7 位地址是 1010 加 A2A1A0,如果三个脚都接地,地址就是 0x50。

写一个字节的时序是:起始条件 → 发送设备地址+写位 → 等待 ACK → 发送字地址 → 等待 ACK → 发送数据 → 等待 ACK → 停止条件。写完一个字节后 EEPROM 需要 5ms 左右的内部门写周期,这期间它不会响应任何命令,所以连续写多个字节时必须等这个周期结束。

用 HAL 库写的话,核心代码大概是这样:

#define EEPROM_ADDR 0xA0 // 8位地址形式,HAL库左移后使用 HAL_StatusTypeDef EEPROM_WriteByte(uint16_t memAddr, uint8_t data) { uint8_t buf[3]; buf[0] = (uint8_t)(memAddr >> 8); // 高地址(大容量型号需要) buf[1] = (uint8_t)(memAddr & 0xFF); buf[2] = data; // AT24C02 地址是单字节,这里按单字节处理 return HAL_I2C_Mem_Write(&hi2c1, EEPROM_ADDR, memAddr, I2C_MEMADD_SIZE_8BIT, &data, 1, 100); }

页写能显著提升效率。AT24C02 的页大小是 8 字节,AT24C256 是 64 字节。页写时要注意不能跨页,如果起始地址在页尾,写到页边界就必须停下来,否则会回卷覆盖页首数据。这个坑我在早期项目里踩过,写 16 个字节的参数,起始地址是 0x0A,结果后 6 个字节把 0x08 开始的数据覆盖了,排查了半天。

读操作相对简单,支持当前地址读、随机读、顺序读。顺序读可以一次把整个参数区读出来,效率最高。

3.2 SPI 驱动 NOR Flash 的关键细节

W25Q 系列 NOR Flash 的 SPI 命令集是标准化的,常用命令有:

命令编码功能
Write Enable0x06写使能,每次写/擦前必须发
Read Status0x05读状态寄存器,判断忙闲
Read Data0x03读数据
Page Program0x02页编程,一页 256 字节
Sector Erase0x20擦除 4KB 扇区
Block Erase0xD8擦除 64KB 块
Chip Erase0xC7全片擦除

写数据的标准流程是:发写使能 → 发页编程命令+地址+数据 → 轮询状态寄存器等待 BUSY 位清零。擦除流程类似,只是把页编程换成扇区擦除。

这里有个容易被忽略的点:页编程不能跨页。一页 256 字节,如果起始地址是 0xF0,最多只能写 16 字节,写到 0x100 就会回卷到页首。所以写大块数据时,驱动层必须做地址对齐和分页处理。

void NOR_WriteBuffer(uint32_t addr, uint8_t *buf, uint32_t len) { while (len > 0) { uint32_t pageRemain = 256 - (addr % 256); uint32_t writeLen = (len < pageRemain) ? len : pageRemain; NOR_WriteEnable(); NOR_PageProgram(addr, buf, writeLen); NOR_WaitBusy(); addr += writeLen; buf += writeLen; len -= writeLen; } }

擦除操作要特别注意:擦除前必须确认该扇区的数据已经不需要了。NOR Flash 擦除是按扇区的,一旦擦掉整个 4KB 都没了。做日志系统时,我通常会在每个扇区头部放一个魔数和序号,擦除前先校验,避免误擦有效数据。

3.3 SD 卡在 STM32 上的挂载与文件操作

STM32 挂 SD 卡有两条路:SDIO 接口和 SPI 接口。SDIO 速度快(4 位模式下能到 20MB/s 以上),但引脚多、时序要求高;SPI 慢(通常 1~2MB/s),但引脚少、兼容性好。工业控制器如果只是偶尔存点日志,SPI 模式完全够用;如果要存波形或图像,必须上 SDIO。

用 FatFs 文件系统的话,STM32CubeMX 可以直接勾选 FatFs 中间件,生成初始化代码。挂载流程是:初始化 SDIO/SPI → 初始化 FatFs → f_mount 挂载 → 打开文件读写。

FATFS fs; FIL fil; FRESULT res; UINT bw; res = f_mount(&fs, "0:", 1); if (res != FR_OK) { /* 挂载失败处理 */ } res = f_open(&fil, "0:/log.txt", FA_OPEN_ALWAYS | FA_WRITE); if (res == FR_OK) { f_lseek(&fil, f_size(&fil)); // 定位到文件末尾追加 f_write(&fil, "event\r\n", 7, &bw); f_close(&fil); }

SD 卡最大的风险是掉电导致文件系统损坏。缓解办法有几个:一是定期 f_sync,把缓存刷到卡上;二是用追加模式写日志,避免频繁修改 FAT 表;三是关键数据双写,SD 卡和 NOR Flash 各存一份。

注意:SD 卡热插拔时一定要先 f_mount(NULL, "0:", 0) 卸载,再断电拔卡。直接拔卡可能导致卡内数据损坏,尤其是正在写的时候。

4. FPGA 侧存储访问与双芯片协同

4.1 FPGA 访问 NOR Flash 的 Verilog 实现

FPGA 访问 SPI NOR Flash,本质是用 Verilog 实现一个 SPI 主机状态机。核心模块包括时钟分频、状态控制、移位寄存器、片选控制。以读数据为例,状态机流程是:拉低 CS → 发送 0x03 → 发送 24 位地址 → 连续接收数据 → 拉高 CS。

// SPI 读 NOR Flash 状态机核心片段 localparam IDLE = 3'd0, CMD = 3'd1, ADDR = 3'd2, DATA = 3'd3, DONE = 3'd4; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= IDLE; cs_n <= 1'b1; end else begin case (state) IDLE: if (start) begin cs_n <= 1'b0; shift <= {8'h03, addr}; // 命令+地址共32位 state <= CMD; end CMD: begin if (bit_cnt == 5'd31) begin state <= DATA; bit_cnt <= 0; end else begin sck <= ~sck; if (sck) shift <= {shift[30:0], 1'b0}; bit_cnt <= bit_cnt + 1; end end DATA: begin // 接收数据位,每8位拼成一个字节写入FIFO end endcase end end

FPGA 侧做存储访问,最大的优势是并行和确定性。STM32 读 Flash 要经过指令周期、总线仲裁,延迟不确定;FPGA 可以用状态机精确控制每个时钟沿,采集数据的同时把数据流直接写进 Flash,中间不经过 CPU,实时性极好。

4.2 STM32 与 FPGA 的数据交换与落盘分工

STM32 和 FPGA 之间通常用 FSMC/FMC 并行总线、SPI、或者双口 RAM 交换数据。存储分工上,我一般这样设计:

  • FPGA 负责采集和预处理:高速 ADC 采到的数据先在 FPGA 内部做滤波、抽取、打包,降低数据率。
  • STM32 负责存储管理:FPGA 把打包好的数据通过 FMC 或 SPI 传给 STM32,STM32 决定存哪里——关键事件写 NOR Flash,大批量数据写 SD 卡。
  • 共享参数区:校准系数、量程这些参数存在 EEPROM 里,STM32 启动时读出来,通过寄存器或双口 RAM 同步给 FPGA。

这样分工的好处是各用所长:FPGA 做它擅长的实时并行处理,STM32 做它擅长的文件系统和协议管理。如果让 FPGA 直接管 SD 卡文件系统,那简直是自找麻烦——FAT 文件系统的复杂度用 Verilog 实现,调试成本高到离谱。

4.3 掉电保护与数据一致性设计

工业现场掉电是常态,存储设计必须考虑掉电瞬间的数据完整性。我的做法是三层保护:

第一层:硬件掉电检测。用一个比较器监测电源电压,电压跌到阈值以下时产生中断,STM32 在中断里紧急保存关键数据。这需要一个大电容维持几百毫秒的供电,让保存操作能完成。

第二层:软件双备份。关键参数在 EEPROM 里存两份,一份是当前值,一份是备份值,每份带 CRC 校验。读取时两份都校验,哪份对用哪份,两份都对取当前值,两份都错用默认值。

第三层:日志原子写。NOR Flash 写日志时,先在扇区头部写"写入中"标志,数据写完后再改成"写入完成"。上电时扫描所有扇区,发现"写入中"标志就说明上次掉电了,该条日志作废,从下一条继续。

typedef struct { uint32_t magic; // 0x55AA55AA 表示有效 uint32_t seq; // 序号,用于判断新旧 uint16_t len; // 数据长度 uint16_t crc; // 数据CRC uint8_t data[64]; } LogEntry; // 写入流程 void Log_Write(LogEntry *entry) { entry->magic = 0x00000000; // 先清magic,表示写入中 NOR_Write(addr, (uint8_t*)entry, sizeof(LogEntry)); entry->magic = 0x55AA55AA; // 再写magic,表示完成 NOR_Write(addr, (uint8_t*)&entry->magic, 4); }

这个"先清标志再写数据最后置标志"的顺序很关键。如果反过来先置标志再写数据,掉电后标志是有效的但数据是残缺的,上电校验 CRC 才发现问题,但已经浪费了一次判断。先清标志的话,上电看到标志无效直接跳过,逻辑更干净。

5. 常见问题排查与避坑经验实录

5.1 存储相关故障速查表

现象可能原因排查方法解决措施
EEPROM 读全 0xFF器件未应答/地址错用逻辑分析仪抓 I2C 波形检查 A0-A2 地址脚和上拉电阻
EEPROM 写后读不一致未等写周期结束读状态轮询或延时 5ms每次写后加延时或 ACK 轮询
NOR Flash 读 ID 错误SPI 模式不对/CS 时序读 JEDEC ID(0x9F)验证检查 CPOL/CPHA 和片选建立时间
NOR Flash 写不进忘记写使能读状态寄存器看 WEL 位每次写擦前发 0x06
SD 卡挂载失败卡未初始化/格式不对用 f_mount 返回值判断格式化为 FAT32,重新初始化
SD 卡写文件损坏掉电时正在写 FAT检查文件系统错误定期 f_sync,关键数据双写
数据 CRC 校验失败掉电写入不完整检查日志标志位用原子写+双备份机制

5.2 几个我踩过的坑

坑一:I2C 上拉电阻选错。有次用 10k 上拉,100kHz 下波形上升沿太缓,EEPROM 偶尔不应答。换成 4.7k 后稳定。上拉电阻的计算要考虑总线电容,线长超过 20cm 时建议用 2.2k。

坑二:NOR Flash 扇区边界没对齐。日志系统按固定长度写,结果某条日志跨了两个扇区,擦除时只擦了一个扇区,数据就乱了。后来改成每条日志固定占一个扇区,浪费点空间但逻辑简单可靠。

坑三:SD 卡在低温下不工作。工业现场冬天零下 20 度,消费级 SD 卡直接罢工。换成工业级宽温卡(-40~85 度)才解决。这个坑让我明白,工业项目的物料选型不能只看参数表,工作温度范围必须留余量。

坑四:FPGA 和 STM32 同时访问 NOR Flash 冲突。早期设计里两者共用一条 SPI 总线,没有仲裁机制,偶尔同时发起访问导致数据错乱。后来加了片选仲裁逻辑,同一时刻只允许一方访问,问题消失。

5.3 存储寿命的估算方法

选型时经常要估算存储寿命够不够用。以 NOR Flash 日志为例:

假设每天产生 1000 条日志,每条 64 字节,一天写入 64KB。Flash 容量 8MB,分成 2048 个 4KB 扇区。环形写入时,每个扇区大约 2048 天被擦一次,一年擦 0.18 次。10 万次擦写寿命,理论上能用 55 万年。实际受温度、电压影响会打折,但余量巨大。

SD 卡就没这么乐观了。假设每天写 1GB,卡容量 32GB,每天全盘擦写 1/32 次,一年约 11 次。消费级 TLC 卡 500 次寿命,理论上能用 45 年。但这是理想情况,实际卡内磨损均衡算法、写入放大都会让寿命大幅缩短,所以工业项目里 SD 卡一般按 3~5 年更换来设计。

实操心得:估算寿命时,写入放大系数一定要考虑。NOR Flash 的写入放大接近 1,SD 卡可能到 3~5。也就是说实际写入量是逻辑写入量的好几倍,寿命要按这个打折。

6. 分级存储方案的落地建议

把 EEPROM、NOR Flash、SD 卡三级存储用起来,硬件上其实不复杂:STM32 的 I2C1 挂 EEPROM,SPI1 挂 NOR Flash,SDIO 挂 SD 卡,三条总线互不干扰。PCB 布局时注意 I2C 和 SPI 的走线尽量短,SDIO 的时钟线要包地处理,避免高速信号串扰。

软件架构上,我建议做一个统一的存储抽象层,把三种介质的读写接口统一成storage_read、storage_write、storage_erase三个函数,上层业务代码不关心数据存在哪里,由抽象层根据数据类型路由到对应介质。这样后期换料或者调整分级策略时,只需要改抽象层,业务代码不动。

typedef enum { STORAGE_PARAM, // 参数 -> EEPROM STORAGE_LOG, // 日志 -> NOR Flash STORAGE_BULK // 大数据 -> SD 卡 } StorageType; int storage_write(StorageType type, uint32_t offset, uint8_t *buf, uint32_t len) { switch (type) { case STORAGE_PARAM: return eeprom_write(offset, buf, len); case STORAGE_LOG: return nor_write(offset, buf, len); case STORAGE_BULK: return sdcard_write(offset, buf, len); default: return -1; } }

这个抽象层还有个好处:方便做数据迁移。比如产品升级后 NOR Flash 容量不够了,可以把日志迁到 SD 卡上,抽象层改一下路由规则就行,上层完全无感。

最后再分享一个小技巧:调试存储问题时,逻辑分析仪比示波器好用。I2C、SPI 的协议解码功能能直接把波形翻译成命令和数据,一眼就能看出是地址发错了还是时序不对。我调试 EEPROM 和 NOR Flash 问题时,逻辑分析仪基本是标配,能省掉大量猜测时间。SD 卡的问题则更多靠软件日志,在 FatFs 的 diskio 层加打印,能看到每次读写的扇区号和返回值,定位问题很快。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/6 3:05:26

欧姆龙PLC通信协议全解析:FINS/TCP、地址映射与踩坑实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/11/6 3:04:23

FOC电流环设计与实现:从原理到代码的工程实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/11/6 3:03:10

AUTOSAR诊断事件管理DEM:DTC从诞生到老化的一生

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/11/6 3:02:04

欧姆龙PLC通信协议深度解析:HostLink与FINS实战排坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/11/6 3:00:43

I2C多主机仲裁与时钟延展:开漏输出下的精妙设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/11/6 3:00:41

车规MCU芯驰E3118 XIP模式下FEE开发实战与避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华