资讯动态

STM32+FPGA工业控制器三级存储架构:EEPROM、NOR Flash与SD卡的选型与设计实践

发布时间:2026/9/29 2:50:45 来源:尧图企业网站定制
1. 为什么工业控制器要把存储拆成三级1.1 数据不一样“存法”就不能一样做工业控制器这几年我最大的感受就是数据存储这件事看着不起眼真出事的时候最要命。PLC 的配方参数丢一次整条产线就得停下来重新下发配置伺服驱动器的报警日志丢了售后工程师过去就是盲修设备每天采集的电流波形找不回来你做的所谓“健康诊断”就是个摆设。说回到 STM32FPGA 这套组合。为什么要分三级存储因为控制器里的数据从属性上根本就不是一类东西配置参数类设备地址、校准系数、用户配方、零点标定值。这类数据量极小可能几十到几百字节但改得比较频繁而且断电之后绝对不能丢。运行数据类报警记录、操作日志、运行统计、最近一段时间的采样快照。数据量从几 KB 到几十 MB要求快速写入允许按块覆盖但要有一定历史保留。批量采集类振动波形、电流曲线、温度趋势或者 FPGA 高速采集的 ADC 原始数据。一天可能产生几十 MB 甚至 GB 级文件要求容量大、写入连续、读取便捷。这就像家里存东西身份证、存折要放保险柜EEPROM常用的工具和药品放床头柜NOR Flash换季的衣物和杂物塞进储物间SD 卡。你把被褥塞进保险柜肯定不现实把存折扔储物间也迟早要出事。所以标题里的“分级”这两个字是这套方案的核心。不是简单地“多挂几个存储芯片”而是根据数据的价值密度、访问频率、容量需求做一次正交划分每一级存储干它最擅长的事。STM32 负责上面两层EEPROM、NOR FlashFPGA 做前端采集和高速数据的缓冲搬运SD 卡挂在 STM32 的文件系统下这样整个数据链路才是完整的。1.2 分级存储的总体思路与数据流向这套架构里单片机和 FPGA 的分工其实很清晰FPGA 管高速、实时、确定性的活STM32 管协议、文件系统和“不着急”的活。数据从采集到落盘的路径是这样走的传感器信号进 ADCFPGA 并行采样并以固定速率把数据写入内部的 FIFOFIFO 满到一定水位后通过并行总线或 SPI 高速接口通知 STM32 来搬数据STM32 把数据先暂存在 NOR Flash 的缓冲区里积累到一定量比如一个 4KB 扇区再批量写入 SD 卡的文件中。这个方法说白了就是先把数据存在 FPGA 的“临时仓库”需要时再搬到“正式的仓库里”。那么 EEPROM 和 NOR Flash 的边界怎么定我的习惯是小于 1KB、需要频繁改写、每一条数据都要保证独立完整性的放 EEPROM大于 1KB、按块改写可以接受、需要有日志追加能力的放 NOR Flash需要长周期留存、要能被 USB 读出来分析的历史数据才考虑 SD 卡。分级越多管理越复杂实际项目中两级往往也能凑合但加上 SD 卡之后设备的“数据格局”才真正打开。2. 三种存储介质怎么选EEPROM、NOR Flash、SD 卡2.1 EEPROM存“丢了就要命”的小数据EEPROM 是这几种存储里性格最“靠谱”的一个。按字节读写写之前不用先擦除掉电不丢单字节写周期一般 5ms 左右。工业控制器里最常见的型号就是 AT24Cxx 系列走 I2C 接口容量从 2Kbit 到 512Kbit 都有。选型时盯三个指标容量、写寿命、页写缓冲。AT24C02 是 256 字节存不了多少东西一般建议直接上 AT24C128 或 AT24C256容量适中也便宜。写寿命方面EEPROM 标称 100 万次擦写但这是按页测试的理论值高温环境下要打折。如果你的设备每秒都在写参数那寿命也就是十来天的事这可能是个隐患。所以设计时要有意识地减少写次数——只有当参数值真的变化时才启动写操作而不是周期性把同一份数据刷进去。I2C 硬件上要注意几个细节SCL/SDA 必须有上拉电阻阻值 4.7k 到 10k 之间取决于总线上的器件数量和速率地址引脚 A0/A1/A2 不要悬空该接地就接地WP 写保护引脚建议拉出一个 0 欧电阻到地想打开写保护的时候可以直接断开电阻、上拉到 VCC。这里有一个来自实践的建议EEPROM 的页写缓冲要利用好。AT24C256 一次页写最多 64 字节你要写一个 128 字节的配方分两次页写每次起始地址按 64 对齐能省一半时间。2.2 NOR Flash当“中间仓库”用别当 U 盘用NOR Flash 经常被人拿来和 NAND 比较。在工业控制器里选 NOR 而不是 NAND 的原因很简单NOR 可以按字节随机读读速度快XIP 执行代码也方便而且坏块率低得多。NAND 容量大、价格低但块擦除、坏块管理、位翻转纠错这些事太折腾只有上了文件系统加 ECC 才敢放心用对单片机来说负担很重。NOR Flash 最大的特性是“先擦后写”而且擦除是以扇区Sector为单位的常见的扇区大小是 4KB。这一条直接决定了它的用法你不能像 EEPROM 一样改一个字节就写一个字节而是要先读出整个扇区、修改、擦除、再整扇区写回。频繁小数据更新在 NOR 上效率很低所以它适合存“顺序追加型”的数据——比如报警记录一次追加一条写满一个扇区后再换下一个扇区。另一个问题是擦写寿命。NOR 的擦写次数一般标 10 万次听起来不少但如果日志每 10 秒写一条一天就是 8640 次10 万次十几天就耗光了。所以使用 NOR Flash 必须做两个设计一是软件磨损均衡扇区轮流做循环写入而不是死磕在 0 号扇区二是双区备份关键配置数据写两份交替更新启动时校验有效区。我做项目时经常用 W25Q64/1288MB 到 16MB 容量SPI 接口QSPI 模式能跑到 80MHz性价比很高。有些工程师看到“模拟 EEPROM”这个词是在用 Flash 模拟 EEPROM 的行为——在 STM32 内部 Flash 的专用扇区实现参数存取其实就是把上述“读-改-写”的流程做成一个封装好的驱动。原理相同好处是省一颗外部芯片坏处是占用了代码 Flash 空间且同样存在擦写次数限制。设计中要注意的是这个思路能否作为外扩 Flash 驱动逻辑的基础答案是可以只是外部 Flash 容量大没那么容易写满。2.3 SD 卡大容量历史数据进文件系统SD 卡这类存储介质本来不是为工业设计的但架不住它容量大、便宜、通用性最强。板载 Flash 存不下的波形数据录制到 SD 卡里拔出来插到电脑上就能分析对客户来说体验极好。SD 卡和单片机连接有两种方式SDIO 和 SPI。SDIO 速度快能跑到 48MHz但占用的引脚多CLK/CMD/D0-D3通常 6 根而且 SDIO 控制器的时序配置比 SPI 复杂SPI 模式速度上限一般是 25MHz 左右胜在引脚少、代码成熟绝大多数工业控制器的采样率用 SPI 模式也够用了。如果不是做高速连续录波建议优先走 SPI 模式省 GPIO 也省事。文件系统方面首选 FatFS这是嵌入式领域事实标准。需要留意的是 FatFS 的配置项要仔细调_FS_TINY 选 1 可以省内存_MAX_SS 按扇区大小设 512_USE_LFN 如果文件名是纯 ASCII 或者 8.3 格式就关掉否则长文件名缓冲区会比较吃内存。SD 卡读写是块操作底层接口基于 disk_read/disk_write 实现SPI 模式下需要注意 SD 卡初始化时的时钟频率不能太高——很多卡在 400kHz 以下才能完成上电初始化流程识别之后才能切到高速模式这是 SD 协议规范里明确规定的直接用 10MHz 时钟初始化容易导致sdcard not mount这类问题。3. 硬件电路设计与接线实操3.1 STM32 侧总线分配I2C 与 SPI 的引脚规划硬件设计先从引脚规划说起。拿到一个 STM32 型号先把外设资源分配清楚。以我常用的 STM32F407 为例这个项目里分配给存储系统的引脚大致如下功能接口引脚备注EEPROM (AT24C256)I2C1PB6 (SCL), PB7 (SDA)都需要 4.7k 上拉电阻NOR Flash (W25Q64)SPI1PA5 (SCK), PA6 (MISO), PA7 (MOSI), PB0 (CS)SPI1 的 SCK 最大 42MHzSD 卡SPI2PB13 (SCK), PB14 (MISO), PB15 (MOSI), PB12 (CS)SPI2 模式初始化时钟降频FPGA 数据/握手FSMC/并行PD0-PD15 控制线FPGA 作为外部存储器映射几个容易踩坑的细节多个 SPI 挂在同一总线上很多同学想省事把 Flash 和 SD 卡挂在同一个 SPI 上共用 SCK/MISO/MOSI只靠 CS 区分。理论上可行但 W25Q 和 SD 卡的最高时钟频率不同初始化时序要求也不同。实践中很容易出现“Flash 正常、SD 卡偶发初始化失败”的怪问题因为总线速率必须迁就慢的那个设备。我建议分两个 SPI 独立接成本几乎为零排查问题能少掉一半头发。片选信号必须用硬件控制不要把 CS 当成普通 GPIO 来软件翻转尤其是有 DMA 参与的数据搬运时。GPIO 翻转速度和 DMA 传输速率不匹配加上中断延迟极容易导致传输错位。选一个带有 SPI 硬件 CS 功能的引脚配置成 NSS 硬件管理模式。I2C 上拉电阻的取值I2C1 的标准模式上拉电阻取值 4.7k 比较保守总线如果挂载多个设备且走线较长可以降到 3.3k但不要低于 2.2k否则上升沿会太慢、低电平驱动能力不够。高速模式400kHz下建议 2.2k这算是一个实用判断经验。3.2 FPGA 侧缓存桥接数据先入 FIFO 再搬家FPGA 在整个架构里的职责是“快进快出”。ADC 采集速率很高可能 1MHz 甚至更高STM32 直接实时接收根本来不及处理——即使跑中断一个字节一个字节地收CPU 就被占满了。所以正确的做法是FPGA 内部做一个异步 FIFO把采集数据源源不断地缓冲起来等到 FIFO 半满或达到阈值时再通知 STM32 一次批量读取。这个 FIFO 的设计有几个关键参数宽度如果 ADC 是 12 位建议直接按 16 位存储以便后续处理也避免跨字节拼接的麻烦。深度取决于 STM32 的平均搬移速度。比如 FIFO 每满 4KB 就触发一次中断STM32 在中断里通过 DMA 搬走 4KB耗时约几毫秒。在这几毫秒内 FIFO 不能溢出所以深度要大于“最大搬移延时 × 采集速率”。一次经验取值是 8KB 到 16KB。读写时钟域写入时钟是 ADC 采样时钟读时钟是 STM32 FSMC 总线时钟两者完全异步FIFO 必须做格雷码跨时钟域同步。数据流上电之后FPGA 先跑一个采集状态机数据从 ADC 进来就写 FIFOFIFO 满到阈值拉高一个“数据就绪RDY”信号STM32 收到中断后置高“读请求REQ”信号FPGA 端把 FIFO 读出端接在并行总线上STM32 连续读若干次读完后 FPGA 拉低 RDY等下一次 FIFO 满。这套握手协议简单可靠也不会有因跨时钟域丢数据的隐患。我在实际的项目里还会在 FPGA 里同时开一条低速 UART 通道做辅助调试。串口发一条命令就能把 FPGA 内部 FIFO 水位、握手状态寄存器全部回读出来排查问题省力不少。这也算是对网上常问的“FPGA 怎么正确写 testbench”的一个回应——testbench 里最该做的就是模拟这种握手时序验证 FIFO 读写水位和信号时序避免到板子上才发现状态机跑飞。3.3 供电与信号完整性这些不起眼的坑存储系统对供电的要求没有那么苛刻但有两个地方确实要专门处理一个是 SD 卡的 3.3V 供电另一个是地回路。SD 卡在 SPI 模式下也能吃 3.3V 逻辑电平这一点要注意考虑周全——如果你选的主控是 5V 的板子就需要电平转换。我遇到过的案例是某人用 5V 单片机挂 SD 卡想着 SD 卡座子自带稳压结果做出来的板子一开始偶尔能识别后面直接完全无法挂载。查了一圈最后发现是逻辑电平不匹配——SD 卡的数据线和时钟线直接接到 5V 引脚上把卡内部电平判定电路烧到异常。所以电平转换芯片或者电阻分压在混合电压系统里绝不能省。另外SD 卡座子的 VCC 引脚要就近放一个 10uF 钽电容 0.1uF 陶瓷电容。SD 卡内部 NAND 写操作瞬时电流可能到 100mA 级别没有储能电容的话电源纹波会让卡在写入过程中掉电退出非常难以排查。地回路通常是“低级错误制造高级故障”的根源。FPGA、ADC、Flash、SD 卡分散在不同的区块如果地平面被切割得支离破碎高速 SPI 的回流路径就被迫绕路信号完整性急剧恶化。设计时注意SPI 时钟线尽量避免长距离平行走线MISO/MOSI 不要和时钟线靠得太近数据线和时钟线之间最好留出一倍线宽的距离至少 4mil 以上。这些不是玄学都是示波器上亲眼看到的振铃和过冲。4. 分层软件架构与掉电保护4.1 ARM 端的“分层存储服务”硬件画好了代码结构也要跟上。这套存储系统在软件上不能东写一块西写一块必须统一封装成一个存储服务层。我常用的分层方式是应用层设备的业务逻辑比如“保存配方”“记录报警”“追加波形数据”。应用层只调用存储接口完全不知道底层用的是哪种介质。接口层定义统一的数据读写服务如Storage_WriteParam(uint16_t id, uint8_t *buf, uint16_t len)、Storage_AppendLog(LOG_T *log)、Storage_CreateWaveFile(...)。在这一层根据数据 id 和参数类型决定路由到 EEPROM 还是 NOR Flash。驱动层EEPROM 的 I2C 驱动、Flash 的 SPI 驱动、SD 卡的 FatFS SPI 驱动以及各个介质的底层时序处理。应用层绝不能直接调用W25Q_WriteSector()这类底层函数。否则三个月后你自己都记不清哪个参数写在哪个扇区维护成本极高。接口层里最核心的是“数据类型路由表”。比如数据类型存储介质写入策略备注设备配置参数EEPROM变化即写双备份最多 2KB校准系数EEPROM变化即写带 CRC出厂后极少改报警/事件日志NOR Flash循环扇区追加保留最近 10000 条采集波形文件SD 卡批量写入FATFS按日期命名路由表可以做成静态配置数组每个存储对象包含介质类型、起始地址、容量、备份策略这些属性。这样加一个新的存储对象只需要在数组里加一行。4.2 FPGA 状态机与握手协议FPGA 端不要在寄存器层面散写散读把采集控制、FIFO 管理、握手应答做成一个清晰的状态机。下面是一个简化版的采集控制状态机框架localparam IDLE 3d0; localparam ACQUIRE 3d1; localparam FIFO_FULL 3d2; localparam WAIT_HOST 3d3; localparam HOST_READ 3d4; reg [2:0] state; wire fifo_half fifo_wr_count HALF_DEPTH; wire host_req ext_host_req; always (posedge clk) begin case (state) IDLE: begin if (start_acq) state ACQUIRE; end ACQUIRE: begin if (fifo_half) state WAIT_HOST; end WAIT_HOST: begin // 拉高 RDY等待外部主机置高 REQ if (host_req) state HOST_READ; end HOST_READ: begin // 读取计数满一帧后回到 ACQUIRE if (read_count BURST_LEN) state ACQUIRE; end default: state IDLE; endcase end这里要特别强调的是握手信号的时序关系。RDY拉高之后STM32 端会进入中断处理然后拉高REQ。但中断响应时间是不确定的可能在几个微妙到几十个微妙之间波动。所以 FPGA 端收到REQ之后不能立刻认为数据总线稳定需要同步打两拍甚至三拍确认REQ稳定后再开始驱动数据总线。这就是跨时钟域处理省了这一步就会偶发读到全 0 或全 F 的脏数据。STM32 端的中断处理里读数据的动作要尽可能快、尽可能简单。中断里只做一件事触发 DMA 把 FSMC 数据总线上的一帧数据搬到内存缓冲区。DMA 搬完后在 main 循环里做后续的日志写入。这一设计能避免中断处理时间过长导致后续数据来不及采的情况发生把这些实现的思路写出来对入门 FPGA 和 STM32 联调的同学很有参考价值。4.3 掉电保护三板斧工业现场最恶心的场景就是“写数据写到一半掉电”。从硬件上来讲控制器的电源通常会有一个掉电检测PVD中断能在电压跌到阈值以下之前留出几毫秒的反应时间。但从软件层面还必须有对应的防御机制。我总结为三板斧第一板斧双区备份 事务标志。无论是 EEPROM 里的参数区还是 NOR Flash 里的关键配置块都准备两份物理区域。写入前先记录一个“事务进行中”标志写完主区更新事务标志写完备份区清除事务标志。上电启动时检查事务标志无事务 → 两份数据一致正常启动。正在事务中 → 主区可能写到一半直接用备份区恢复主区。备份区损坏且主区完好 → 用主区重启备份。这样任何瞬间掉电都不会导致“数据彻底丢失”最多回退到上一次完整写入的状态。第二板斧NOR Flash 循环写 磨损均衡。报警日志如果每次都从扇区 0 写起几个月后扇区 0 就报废了。正确做法是维护一个“当前写入扇区索引”把它也存储在 Flash 的固定块里。每次追加日志时先写当前扇区满了之后索引加 1索引循环回 0 前先擦除对应扇区。这样既实现了日志覆盖策略也让擦写分布在所有扇区上寿命提升好几个量级。第三板斧SD 卡掉电保护。FatFS 本身不是掉电安全的。写入过程中断电FAT 表和目录项很容易损坏轻则文件打不开重则整个卡的分区无法识别。我的做法是文件以固定大小块写入比如每 512 字节刷一次缓存不要一次攒几 KB 才 flush。写文件前先创建一个.tmp文件全部写完后f_rename成正式文件。上电后发现.tmp存在就说明上一次写入未完成直接删除。日志文件名按日期区分比如LOG_20240615.BIN第二天自动开新文件。这样即使某一天的文件坏了也只丢一天的数据不会波及历史档案。这一组合下来绝大多数掉电场景都能兜住。这也是“工业级”和“实验室级”最大的区别实验室里不担心掉电工业现场什么诡异场景都有可能发生。5. 实测排障与经验补遗5.1 高频故障速查表这部分直接上干货把我在调试这套系统过程中遇到的高频问题整理成了一张速查表新做的板子可以直接照着这个方向排查故障现象可能原因排查手段解决方向EEPROM 写操作一直 ACK 超时I2C 地址错误、WP 引脚拉高、总线无上拉示波器量 SCL/SDA 波形确认地址字节检查地址引脚电平WP 串 0Ω 电阻接地EEPROM 数据偶发错误页写跨页边界或电源纹波核对写入长度是否超过页边界页写长度限制 64 字节内且地址按页对齐NOR Flash 读回全 0xFF片选没拉低、SPI 模式不对、时钟极性配置错误读 ID 寄存器 0x90/0x9F确认 SPI 模式 0CPOL0, CPHA0Flash 写失败扇区未擦除就写入调试器读扇区内容写入前先执行扇区擦除SD 卡初始化失败初始化时钟过高、供电不足、卡座虚焊初始化阶段降频到 400kHz 重试增加延时顺序补齐电源电容SD 卡偶发写入失败文件系统碎片、掉电导致 FAT 损坏检查返回值挂载时做一致性检查固定块写入损坏时自动重建文件系统待评估FPGA 与 STM32 数据错位握手信号未做同步、数据总线时序不满足逻辑分析仪抓 RDY/REQ/总线波形增加跨时钟域同步打拍上电偶发存储区参数全丢初始化顺序错误先读后写导致误判加打印日志追踪初始化流程启动时先检查 FPGA 复位状态再初始化外设5.2 几个值得单独拎出来讲的实战细节第一个是SD 卡内部寄存器锁死的问题。这个案例特别典型SPI 模式下做初始化时连续发送了 CMD0 之后卡片没有进入 SPI 模式后续的 CMD8/ACMD41 全部失败。反复排查之后发现问题出在上电时序——SD 卡要求至少等待 1ms有些规范是 2ms让内部电源稳定然后时钟线要先输出至少 74 个时钟脉冲片选要处于高电平。这个顺序错了卡片就会进入一个“半初始化”状态寄存器状态混乱也就是网上经常说的“寄存器锁死”。解决办法是每次上电后严格按 74 个时钟 CMD0 等待响应 CMD8 ACMD41 的标准流程走中间每一步都加足够延时不要追求“快”。第二个是I2C 用硬件外设还是软件模拟的问题。STM32 的硬件 I2C 在某些系列上确实有坑典型的就是总线忙检测误判、卡死在 BUSY 状态。我自己的习惯是如果时间充裕且存储量不大EEPROM 用硬件 I2C配合超时复位机制如果项目功耗成本压力大直接用 GPIO 模拟 I2C 反而更可控。软件模拟不需要处理复杂的硬件状态寄存器时序完全由代码控制对于低速 EEPROM 来说完全够用而且代码更直观。FPGA 那边如果也要挂 EEPROM 做 FPGA 配置信息存储则用 Verilog 写一个 I2C 控制器在读取上比较直接——网上能找到很多现成的例子关键是起始条件和 ACK 时序要严格对。我见过有人直接抄了一个初始化序号写反的例程结果 FPGA 侧 EEPROM 读取死活不对这种细节要自己有判断力。第三个是存储服务分层收益的延迟体现。最初我把所有存储代码都写在一块自测没有问题但后面加了一个“历史曲线回放”功能时发现新功能需要同时读 EEPROM 里的设备参数和 SD 卡里的历史文件业务逻辑和存储逻辑耦合在一起改一处要动三处。花了半天时间把存储部分重构成分层架构之后扩展就顺畅多了。这给我的教训是写存储代码的第一天就要按分层来做即使当时觉得小题大做。到后期再补课重构成本往往是第一时间的十倍。这也是为什么我在这个系列里反复强调“先设计、后编码”的原因——存储系统的复杂度前期看不出来后期爆发起来相当麻烦。最后再分享一个对新手很实用的小技巧在联调 STM32 与 FPGA 存储链路时不要在真实采集数据上做验证先用一个固定递增序列比如 0x00-0xFF 循环灌进 FIFO然后在 STM32 端读出后做比对。数据比对一致之后再去接真实 ADC 信号。这样一旦出错你只需要判断是链路问题还是采集问题不需要同时排查两个变量。这套方法也适用于 Flash 和 SD 卡驱动的验证——写入一个模式读回比对确认无误后再接实际业务数据。还是那句话数据存储的设计决定了一台设备在严苛环境里到底能不能扛住。硬件方案选型、电路布局、分层软件、掉电保护每一步都是躲不开的细节活。希望这篇记录能给正在做类似控制器存储设计的朋友提供一些参考。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑