ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

STM32+FPGA工业控制器分级存储设计:EEPROM/NOR Flash/SD卡实战

STM32+FPGA工业控制器分级存储设计:EEPROM/NOR Flash/SD卡实战 做工业控制器这一行数据存储永远是个绕不开的“脏活累活”。最近把手里一套基于 STM32FPGA 架构的控制器存储方案重新梳理了一遍从 EEPROM 到 NOR Flash 再到 SD 卡做了完整的分级落地。这一篇是“硬件篇”的第 12 期专门聊聊这套分级存储是怎么设计、怎么选型、怎么把每个环节调稳的。先交代一下背景这套系统里STM32 跑应用逻辑和通信协议栈FPGA 负责高速采集和实时控制。FPGA 侧产生的高频数据比如编码器计数、ADC 采样、IO 状态快照不能全丢给 STM32 去存STM32 的 Flash 也不适合频繁擦写真正需要掉电保存的关键参数又用不上大容量存储。这种“冷热不均、频次不一、容量跨度大”的数据特征恰恰是分级存储的典型适用场景。如果你正在做类似的嵌入式控制器或者正准备在 STM32FPGA 的双芯方案里塞一套存储系统这篇内容应该能帮你省掉不少自己踩坑的时间。我会把每一级存储的选型逻辑、硬件连法、驱动实现、掉电保护策略以及最常见的几个故障排查过程都摊开来讲。1. 项目整体设计与需求拆解1.1 三类数据的存储特征设计分级存储之前先得把手里的数据按“温度”分个类。我习惯把工业控制器里的数据分成三档第一档是“极冷数据”比如设备序列号、校准系数、MAC 地址、PID 参数、开机次数——这些数据一年也改不了几次但改了必须立刻掉电保存且不能因为写入中途掉电而损坏第二档是“温数据”比如最近一个月的运行曲线、报警记录、工艺参数组别、操作日志写入频率大概分钟级或小时级需要按块擦写、按索引读取第三档是“热数据”比如高速采样的原始波形、连续几十小时的实时趋势记录这类数据写入频率极高、单条数据量大必须按流式写、按时间戳查询。这三档数据对应的体积也完全不是一个量级极冷数据通常几十字节到几KB温数据几百KB到几MB热数据动辄几十MB甚至上百GB。用单一存储介质硬扛这三档数据不是浪费就是不够这正是分级存储的出发点。1.2 为什么选 EEPROM、NOR Flash、SD 卡这三兄弟选型的时候不是看谁新、谁快而是看谁的“写入粒度、擦写寿命、掉电安全、成本”匹配哪一档数据。EEPROM按字节擦写寿命普遍在 100 万次以上写入时间毫秒级掉电保护容易做适合放校准参数和序列号。虽然容量小常见 2Kb~256Kb但冷数据本来就不大。NOR Flash按扇区擦除典型 4KB按字节或页编程寿命通常标称 10 万次擦写。支持 XIP 直接映射执行读取快放固件、工艺参数组、报警历史这种“中频中量”数据非常合适。SD 卡以块为单位读写有文件系统加持容量大、便宜适合放采样数据、日志归档这类大批量流式数据。SD 卡的寿命和可靠性天然不如 Flash 芯片但它胜在可更换、易导出。这套方案的容量和用途配合如下表存储层级典型容量写入频率数据内容掉电策略EEPROM2Kb~256Kb极低上电/配置变更序列号、校准系数、PID写后回读校验NOR Flash1MB~16MB低小时级参数组、报警记录双备份区交替写SD 卡4GB~32GB高秒级~毫秒级采样波形、运行日志FATFS 掉电检测硬件篇的核心思路就是把“写什么数据”和“用谁去写”彻底分开。STM32 管 EEPROM 和 NOR Flash 这两个“小而关键”的区域FPGA 管 SD 卡的高速数据流。为什么这么分因为 SD 卡的块写入和 FAT16/32 文件系统操作如果全压在 STM32 上主控频繁进中断、腾 DPRAM 缓冲区实时性就废了而高频采集数据恰恰从 FPGA 侧过来让 FPGA 直接写 SD 卡能省一次跨芯传输。1.3 存储架构图文字版整套存储路径是这样的FPGA 内部逻辑产生的采样数据 → FPGA 侧 SDIO 控制器 → SD 卡热数据流STM32 下发参数/配置 → STM32 驱动 NOR Flash → 参数组、报警历史温数据生产标定/上电加载 → STM32 驱动 EEPROM → 序列号、校准系数冷数据STM32 与 FPGA 之间通过 SPI 从机接口通信FPGA 把需要记录的波形数据“推到”STM32 再转存 NOR Flash或者直接由 FPGA 写 SD 卡STM32 则从 FPGA 读取状态把控制参数写入 NOR/EEPROM。这样设计的好处是STM32 的 Flash内部几乎不做频繁写操作只用来跑代码MCU 侧只负责低频、高可靠的数据管理把高频操作分流到 FPGA 侧。后面我会逐个讲实现细节。2. 核心细节解析与实操要点2.1 EEPROM 驱动的三个关键点EEPROM 是整套存储系统里最“皮实”但也最容易大意的一环。很多人觉得 I2C 读写 EEPROM 太基础了随便写写就行实际在工业现场最容易出问题的反而是这颗小芯片。第一个关键点是写周期等待。EEPROM 写入不是瞬间完成的AT24C256 这类芯片单字节写周期大约 5ms页写page write虽然能一次写 64 字节但内部仍然是逐个字节编程。写入之后如果立刻发下一个命令芯片会不响应。驱动里必须在每次写操作后等待 ACK 轮询也叫“写周期轮询”。我见过太多人写完 EEPROM 不延时导致读出来的数据是旧的排查半天找不到原因。第二个关键点是页写跨页边界。AT24C256 的页大小是 64 字节页写的时候地址会自动回绕roll over到本页起始。如果要写的数据跨了页边界必须拆成两次页写。驱动里的处理逻辑很简单先算出当前页剩余字节数取两者较小值作为本次写入长度写完再更新地址和剩余长度循环完成整个 buffer 的写入。第三个关键点是字节地址判断。AT24C256 的寻址是 16 位地址分高 8 位和低 8 位I2C 从机地址是 0xA0。很多人拿 AT24C028 位地址的驱动改过来用高位地址没写对数据就全部错位了。附上一段实际可用的 AT24C256 页写核心代码基于 STM32 HAL 库uint8_t eeprom_write_page(uint16_t addr, uint8_t *buf, uint16_t len) { uint16_t page_size 64; uint16_t page_offset addr % page_size; uint16_t chunk page_size - page_offset; if (chunk len) chunk len; HAL_I2C_Mem_Write(hi2c1, 0xA0, addr, I2C_MEMADD_SIZE_16BIT, buf, chunk, 100); eeprom_wait_ready(); // ACK 轮询等待内部写周期结束 if (chunk len) { HAL_I2C_Mem_Write(hi2c1, 0xA0, addr chunk, I2C_MEMADD_SIZE_16BIT, buf chunk, len - chunk, 100); eeprom_wait_ready(); } return 0; }eeprom_wait_ready()的实现就是循环发起始条件 从机地址直到收到 ACK 为止。这段代码看着简单但在产线上跑了两年没出过一例 EEPROM 数据损坏。2.2 NOR Flash 的分区设计与双备份策略NOR Flash 的驱动大家用得最多的是 W25Q64/128/256 系列。SPI 接口指令集统一几乎不用改驱动。这一级存储要解决的核心问题是参数区写入频率虽然不高但一旦在擦除过程中掉电整个扇区就废了直接影响设备启动。我的设计思路是把 Flash 分成三个区域启动参数区Boot Parameters、工艺参数区Recipe Area、报警/日志区Log Area。其中启动参数区和工艺参数区都做双备份。也就是说同一份参数存两份分别放在不同扇区写入的时候先写备份区再写主用区读取的时候先读主用区如果校验失败比如 CRC 不对自动回退读备份区。为什么拆成双区而不是三区因为对 NOR Flash 来说擦除次数是有限资源W25Q 系列典型 10 万次擦写周期。双备份已经把可靠性翻倍了三备份就浪费了容量和擦写寿命。具体分区我一般这样算W25Q64 总容量 8MB。我给启动参数区分配 4 个扇区每个扇区 4KB共 16KB工艺参数区分配 16 个扇区64KB报警日志区分配 256 个扇区1MB剩余空间留作固件升级暂存和扩展。这个分配比例不是拍脑袋定的而是通过对设备 3 个月运行数据的统计得出的报警记录平均每天 200 条每条 64 字节1MB 大概能存 8000 条撑一个月没问题配合 SD 卡上的历史归档正好形成“Flash 存近期、SD 存归档”的接力。2.3 SD 卡写入的“避死锁”设计SD 卡这一级看起来最“简单”——不就是 SPI/SDIO FATFS 吗实际上坑最多。FPGA 写 SD 卡不能像 STM32 那样频繁调用文件系统 API因为文件系统的目录项更新、FAT 表更新、簇分配这些操作都有“先写后擦”的顺序依赖中途掉电很容易导致文件系统损坏。我这边用的是一个比较“笨”但极其稳定的做法固定文件名 追加写模式。FPGA 侧每次开机创建一个新文件例如REC_20250101_000000.DAT之后纯追加写入写满一个簇再写下一个簇。不让 FPGA 做任何“删除文件”“创建目录”“修改文件大小”之类的操作把 FATFS 的复杂逻辑全部屏蔽在 STM32 侧。具体实现上FPGA 内部会维护一个“写入指针”和“当前簇号”每次写数据前先检查当前簇剩余空间不够了才申请新簇。这样一次 SD 卡写入只涉及两个操作数据区块写入 FAT 表更新。数据区块写入失败可以重试FAT 表更新失败就触发“写保护”状态把 FPGA 的采样数据先暂存到内部 BRAM 或 DPRAM等 SD 卡恢复正常再回写。这里最容易被忽视的是 SD 卡的总线宽度和时钟频率匹配。FPGA 侧用 SDIO 4-bit 模式时SD 卡内部擦写操作会拉低 SDIO 的时钟线busy 信号FPGA 必须处理这个 busy 等待否则直接发起下一笔写命令就会超时。好的做法是在 FPGA 的 SDIO 控制器里做一个“命令超时重试”状态机超时后重新发送 CMD13 查询卡状态等卡空闲后再继续。2.4 STM32 与 FPGA 的分工边界这套方案里有个很容易犯的认知错误“STM32 是主FPGA 是从”所以所有数据都应该经 STM32 中转。但其实高频数据流如果全走 STM32MCU 的中断负载和 DMA 占用会非常高而且 STM32 内部 SRAM 有限大块波形数据根本放不下。我最终确定的边界是控制链路STM32 下发控制字 → FPGA 执行 → FPGA 回传状态。频次高、数据短走 SPI 从机寄存器由 STM32 轮询或中断读取。记录链路FPGA 侧直接采集、直接写 SD 卡STM32 只负责初始化挂载 FATFS、创建文件和周期性的状态同步。STM32 不再参与高频数据的搬运。配置链路STM32 通过 SPI 读 EEPROM/NOR Flash把参数写进 FPGA 内部的寄存器组FPGA 侧不直接访问 EEPROM/NOR Flash避免跨芯争抢 I2C/SPI 总线。这样分工的好处一是实时性有保障二是让两边的存储职责互不干扰。FPGA 挂 SD 卡挂了不影响 STM32 读参数STM32 写 NOR Flash 的时候 FPGA 照样录数据互不阻塞。3. 实操过程与关键环节实现3.1 硬件连接与引脚规划先看硬件连接。因为这块板子的空间有限我尽量复用了 STM32 的 SPI 外设。EEPROMAT24C256接在 STM32 的 I2C1SCL 对应 PB6SDA 对应 PB7。上拉电阻用 4.7k靠近芯片放置。NOR FlashW25Q64接在 STM32 的 SPI1CS 用 PA4软件控制SCK 用 PA5MISO 用 PA6MOSI 用 PA7。SPI1 时钟设 18MHz。SD 卡接在 FPGA 的 SDIO 接口4-bit 模式CLK 约 25MHzCMD/Data0~Data3 分配在 FPGA 的专用引脚上。这里 FPGA 内部要做一个简单的电平转换接口因为 FPGA 的 IO bank 电压和 SD 卡 VDD 可能不一致我用的是 3.3V 和 3.3V 对接省了电平转换芯片。需要注意 Flash 的 WP写保护引脚不能悬空。W25Q 系列的 WP 引脚内部默认是“上拉灭能”如果悬空可能导致意外写保护或者误触发。我直接把它用 10k 电阻拉高并在 PCB 上预留了跳线方便产线上临时解锁写保护。3.2 初始化顺序先 EEPROM再 NOR Flash最后 SD 卡系统上电后的初始化顺序我自己踩过一个坑之后才总结出这个“三步走”第一步先初始化 I2C 和 EEPROM读取设备序列号和出厂校准系数。这一步必须放在最前面因为后面所有参数组的有效性判断都要依赖序列号做关联。如果 EEPROM 读出来 CRC 校验错误系统会进入“出厂默认参数”模式并在运行日志里标记一条“参数复位事件”。第二步初始化 SPI 和 NOR Flash读取工艺参数主区。这里要注意一个容易忽略的点NOR Flash 读取不需要等内部写周期但上电后第一次读状态寄存器Read Status Register-1最好做一次“软复位”指令0x66 0x99让 Flash 退出可能的异常状态。我遇到过一块 Flash 上电后 BUSY 位一直为 1 的情况软复位指令发完就好了。第三步初始化 FPGA 侧的 SD 卡。FPGA 内部有一个独立的 SD 卡初始化状态机上电后先执行 SD 卡复位CMD0、获取 CIDCMD2、设置块长度CMD16然后进入 4-bit 模式。STM32 只负责告诉 FPGA“可以开始录了”具体初始化时序由 FPGA 自己掌控。整个初始化过程要求在 1.5 秒内完成因为我这套控制器的上位机协议规定设备上电后 2 秒内必须回应“就绪”指令。3.3 NOR Flash 写入流程与掉电安全NOR Flash 写入一个参数组的标准流程如下计算参数组的 CRC32 和长度写入一个 64 字节的头部结构。从主用区找一块空扇区没有空扇区就选“最旧”的扇区擦除。先把参数写入备份区先写目标扇区的数据等待空闲后擦除另一个扇区再写入。写完后读回整个扇区内容比对 CRC32 是否一致。一致则更新主用区标记在扇区头写入 Magic Number完成。如果不一致再写一次备份区连续两次失败则报警提示“参数区损坏”。实际代码里还会做“写后回读”的联动校验不是只读几个字节而是全扇区回读。W25Q 系列的扇区擦除时间标称是 400ms整片擦除典型 4~8s所以参数写入的整个过程耗时在 400ms 以上这在设备运行中是不能接受的。我的做法是把参数写入放到“停机维护模式”下执行设备只在上下电、参数下发完成后的空闲窗口更新参数区运行时不做任何 NOR Flash 写操作。3.4 FPGA 写 SD 卡的关键时序FPGA 侧写 SD 卡的状态机我简化为这几个状态IDLE → WAIT_CMD → READ_CMD → SEND_DATA → GET_RESPONSE → CHECK_BUSY → UPDATE_FAT → DONE。其中最难调的是 CHECK_BUSY 状态。SD 卡在内部编程/擦除期间会把 D0/Data 线拉低表示 busy。FPGA 要在发送 CMD24WRITE_BLOCK之后等待 Data0 线变高再继续。具体的等待超时我设置为 500ms超过就进入错误处理状态记录错误计数并重新发送 CMD13 查询状态。在数据流方面FPGA 内部用双缓冲 BRAM一个缓冲区在写 SD 卡另一个缓冲区在收采样数据。缓冲区大小我设为 512 字节正好一个扇区这样每一次 SD 卡写操作都是整块写不产生半扇区尾部碎片。实测下来25MHz SDIO 时钟下连续写速度大概能到 1MB/s 左右对采样率 100kSPS、每个采样点 4 字节的场景完全够用。3.5 日志轮转与 SD 卡满容处理SD 卡写满是个无法回避的问题。我的方案是“分层轮转”FPGA 循环写固定个数的文件比如 8 个文件每个 64MB写完就覆盖最旧的文件。命名规则是REC_000.DAT、REC_001.DAT……依次循环。这样 SD 卡永远有 512MB 的最新数据不会出现“文件系统满了导致录不了数据”的情况。STM32 侧再做一次归档每次设备空闲时把 NOR Flash 里的报警记录合并进 SD 卡的ALARM_YYYYMM.TXT文本文件方便上位机直接读取。因为 FPGA 在写原始采样文件时完全不管文件系统全局状态所以这种“两个写者”并存的情况下必须保证它们写的是不同的文件。FPGA 只写REC_xxx.DATSTM32 只写ALARM_xxx.TXT接口上互不冲突。4. 常见问题与排查技巧实录4.1 EEPROM 写入后读回全 FF这个问题我遇到过两次。一次是芯片本身虚焊一次是驱动里没做写周期等待。如果读回的全是 0xFF说明数据根本没写进去。排查步骤先量 I2C 波形确认 SCL/SDA 上确实有信号翻转再用示波器抓写周期结束后的 ACK如果 ACK 一直没等到芯片可能处于忙状态最后检查是不是 I2C 地址错位——AT24C256 的地址是 0xA08-bit 写地址不是 0x507-bit 地址。4.2 NOR Flash 偶尔读错一个字节这不是硬件坏了大概率是 SPI 时序问题。W25Q64 的 SPI 模式是 Mode 0 和 Mode 3 都支持但 STM32 的 SPI1 默认是 Mode 0。如果多个 SPI 设备复用在同一个 SPI 总线上且其中一个设备工作在不同极性会导致时钟极性和相位错乱。我的处理方式是在每次切换到 NOR Flash 前重新配置 SPI 的 CPOL/CPHA 参数保证只用到 Mode 0别让其它设备把配置改掉。4.3 SD 卡频繁掉线FPGA 写 SD 卡掉线排查方向有四个第一SD 卡电源纹波。SD 卡写入瞬间电流可达 100mA 以上如果电源走线细、电容不够VDD 跌落就会导致卡自行复位。解决方法是给 SD 卡 VDD 加 10uF 100nF 双电容并尽量让电源走线短粗。第二CLK 线上的干扰。25MHz 时钟的边缘很陡峭如果 CLK 走线和高电流信号线并排容易串扰。在 FPGA 内部给 SDIO CLK 加一个 22 欧姆串阻可以有效抑制过冲。第三卡与卡之间的兼容性。不同品牌的 SD 卡对初始化时序的容忍度不一样有的卡非要先走 SPI 模式初始化再切 SDIO 模式。FPGA 初始化状态机里我做了“SPI 初始化握手 切换 SDIO”的双模式流程兼容性大幅提升。第四热插拔。工业设备一般不支持运行时拔卡但现场总有手欠的人。我加了卡检测引脚CD并做中断处理如果检测到卡拔出立即停止写操作并落一个“事件记录”到 NOR Flash卡重新插入后自动重新初始化。4.4 掉电瞬间 FPGA 写 SD 卡文件损坏这个是最难搞的问题。设备意外掉电时FPGA 可能正在写一个扇区数据数据写到一半卡上形成的扇区是坏的。文件系统层面 FAT 表如果还没更新坏扇区对应的簇会在下次写入时被重新分配一般不影响文件系统整体完整性。我做的保护措施有两层一层是给 FPGA 写 SD 卡增加“循环冗余写头”即每一条记录在数据区开头带一个 4 字节的 CRC 和时间戳上位机读取时能分辨出哪个记录是“半截”的丢弃即可另一层是硬件上在电源输入端加掉电检测电路当 5V 主电源跌落到 4.7V 时立刻拉低 FPGA 的一个紧急停止信号FPGA 在下一个 1ms 内完成当前扇区写入并停止 SD 卡操作。这一层检测电路大概花费 0.5 元但把文件损坏概率降到几乎为零。5. 掉电保护与数据一致性设计5.1 EEPROM 写序保护EEPROM 单字节写入的原子性比 Flash 好掉电只会导致当前字节没写完不会影响其它字节。但为了保险我的做法是每个参数存储两份一份当前值一份上次有效值。写的时候先写“当前值”等写完再更新“上次有效值”。读取时先读“上次有效值”如果格式校验失败再回退到“当前值”。这样即使掉电发生在两个字节之间系统也总能恢复到最近一次完整写入的状态。5.2 NOR Flash 的幻数Magic Number机制NOR Flash 每次参数区写入前我会在扇区头部写一个 16 字节的 Magic Number比如0xAA55 0x5AA5 0x1234 0x5678表征这个扇区是否“已完整写入”。如果读取时发现 Magic Number 不匹配就认为这个扇区数据无效直接改用备份区。Magic Number 本身是固定的掉电只会导致它没写全而不会导致它“错误地看似有效”这正是选择固定幻数而不是随机数的原因。5.3 SD 卡掉电后 FATFS 的一致性检查即使 FPGA 不做复杂文件操作掉电还是可能破坏 FAT 表。我的对策是STM32 每次开机挂载 SD 卡前先运行 FATFS 提供的一致性检查例程f_mount之后读取 FAT 表检查是否有交叉链接、簇计数错误发现问题直接格式化并重新初始化。数据文件可以丢一部分但设备不能因为一张坏卡就起不来。实际测试中这套方案的掉电恢复时间大约 2 秒EEPROM 2ms、NOR Flash 读取 3ms、SD 卡挂载最慢约 1.5 秒因为要先做一致性检查。在自动化产线上2 秒恢复完全可接受。6. 常见问题速查表问题现象可能原因排查步骤解决措施EEPROM 读回全 FF虚焊/地址错/未等待写周期量 I2C 波形、示波器抓 ACK检查焊接、修正地址、增加 ACK 轮询NOR Flash 偶发读错SPI 模式错乱、电源纹波抓 SPI 波形、检查 CPOL/CPHA复位 SPI 配置、加滤波电容SD 卡掉线电源跌落、CLK 过冲、卡兼容性量 VDD、串阻、换卡测试加电容、加串阻、双模式初始化掉电文件损坏写入中断、FAT 表未更新检查记录头 CRC加掉电检测、记录 CRC 头参数区写入很慢NOR Flash 擦除需 400ms看时序、调整写入窗口改到停机维护模式写入7. 关于这套方案的一些心得最后聊点“程序之外”的东西。很多人觉得存储方案嘛就是找来驱动、调通读写就完事但在工业控制器这种环境下存储方案的可靠性考核根本不是“能不能读写”而是“掉电 1000 次之后数据还在不在”“连续写入一年之后 Flash 还经不经受得住”“换了 20 张不同牌子 SD 卡之后有没有一张卡出幺蛾子”。所以我认为至少在工业控制器领域存储设计的重心 70% 要放在掉电处理、校验、降级策略上只有 30% 是读写驱动本身。还有个小建议如果你正在设计类似的存储分级优先把 EEPROM 这一级做扎实。因为参数数据的可靠性直接决定设备能不能正常启动、校准能不能通过。NOR Flash 和 SD 卡的数据丢失最多损失“记录”和“日志”但参数丢了设备就瘫了。所以 EEPROM 这一级我花了最多的时间做回读校验和双区冗余事实证明这笔投入是完全值得的。这套 STM32FPGA 分级存储方案从硬件定型到量产大约用了两个月其中 FPGA 侧 SD 卡状态机用了两周STM32 侧 EEPROM/NOR Flash 驱动用了三天剩下的时间全部耗在各种边界条件测试上掉电、毛刺、弱卡、长文件名、文件系统碎片化。如果你也要做类似的东西建议从一开始就把“异常测试”列进计划不要等活动板跑通了再补不然后面改结构会非常痛苦。
返回列表