工业控制器上跑的往往是“不能重启”的业务,数据存储这件事看着简单,真正调起来是真的会吐血的。最近在搞一套 STM32+FPGA 的控制器,存储部分用了 EEPROM、NOR Flash 和 SD 卡三种介质做分级,整套方案过了连续上电掉电测试,数据基本没丢过。这篇是硬件篇第 12 篇,把分级存储的选型依据、硬件连接、驱动实现和现场调试完整捋一遍,适合正在用 STM32 和 FPGA 做数据采集、边缘网关或者运动控制的同学参考。内容偏实操,尽量捡项目里直接能用、能避坑的信息写。
1. 整体方案设计与思路拆解
1.1 控制器里到底有哪些数据,分别该怎么存
先想明白一个问题:控制器里的数据,不是同一类数据。有的数据要求改一百万次都不坏,有的数据要求断电后立刻能恢复,有的数据要求能塞下几十 MB 的曲线。你不可能指望一颗芯片通吃所有场景。
我把工业控制器里的数据分成三类:
第一类是配置参数,比如设备编号、校准系数、通信地址、PID 参数、报警阈值。这类数据量很小,一个典型设备可能只需要几百字节到几 KB,但对可靠性的要求极高。现场调试经常要现场改参数,一天可能改几十次,设备生命周期内改写几万次很正常。这类数据如果放在普通 Flash 里,擦写寿命很快就会被磨掉,所以早期工业卡上直接焊 EEPROM,字节级写入、擦写次数大、单字节操作简单,是这类参数的最佳落点。
第二类是运行固件、关键运行日志、最近一段时间的报警记录。这类数据量中等,从几十 KB 到几 MB。要求掉电不丢、启动时能快速读取、偶尔会整块更新。NOR Flash 最合适,因为它支持随机读取、可以在线执行代码(XIP),每次更新也只是整体擦掉重写,正好匹配“固件升级”和“日志轮转”这种使用方式。更关键的是,FPGA 直接驱动 NOR Flash 非常顺,不像 NAND 那样需要坏块管理和 ECC,工业现场的维护成本低得多。
第三类是海量过程数据,比如高速采集的电压电流波形、传感器历史曲线、统计报表、设备运行记录。这类数据动辄几十 MB 甚至几个 GB,而且一般不要求实时读取,只需要在某个时间点整批导出到上位机就行。低成本大容量存储的首选就是 SD 卡,或者工规级 SD 卡、eMMC。SD 卡自带文件系统管理所需的控制器,STM32 上跑个 FATFS 就能实现“插卡拔卡拷数据”,对上位机十分友好。
所以整个存储设计不是“选一颗最强芯片”,而是按数据特性把不同介质组合起来,让每类数据都落到合适的载体上。这就是分级存储的核心逻辑。
1.2 为什么是 EEPROM + NOR Flash + SD 卡这个组合
为了说明选型逻辑,我把三种介质的关键参数放在一起对比,方便直接参考。
| 介质 | 典型容量 | 擦写寿命 | 写粒度 | 读特性 | 掉电保护 | 典型接口 |
|---|---|---|---|---|---|---|
| EEPROM | 2KB ~ 512KB | 100万次左右 | 字节/页 | 按字节随机读 | 极好,原子写 | I2C / SPI |
| NOR Flash | 1MB ~ 64MB | 约10万次/扇区 | 按扇区擦除,按页编程 | 可按字节随机读,可XIP | 好,断电后数据不丢 | SPI / 并行 |
| SD 卡 | 数GB以上 | 视卡等级,一般有均衡磨损 | 按扇区写 | 按块读 | 依赖文件系统同步 | SDIO / SPI |
从这个表格能看出,EEPROM 的特点是“耐操”,单字节改写方便,适合频繁更新小数据;NOR Flash 的特点是“中等容量+快速读取”,适合固件和缓存;SD 卡的特点是“容量大+可移动”,适合做最终归档存储。
有些工程师会问:为什么中间不用 NAND Flash,而是用 NOR Flash?我在 NAND 方案上也试过。FPGA 直接驱动 NAND,首先就要处理坏块表、ECC 校验、写放大这些事,在工业控制器这种“宁稳勿快”的场景,这些复杂度很容易变成稳定性炸弹。NOR Flash 的状态机简单,擦除编程指令固定,出问题一眼就能看清,而且带 SPI 接口的 NOR Flash 对于低速采集场景绰绰有余。再者,NOR Flash 支持随机读,FPGA 里可以实现从 Flash 按地址直接 socket 读取数据,配合内部 FIFO 就能做到微秒级响应。要我排序可靠性,NOR Flash 明显更稳。
SD 卡则是因为“可插拔”天然适合数据归档。现场运维人员不需要 CAD 里拆机器,直接把 SD 卡拔下来插入电脑就能看数据。虽然 SD 卡也有兼容性和掉电损坏的问题,但只要你选对卡、在软件上做好 sync,工业场合用起来完全没问题。
1.3 STM32 和 FPGA 怎么分工,才不会越搞越乱
一个控制器里同时有 STM32 和 FPGA,最容易出现的失误是两个人干同一个活。我在设计和调试中总结出的分工原则是:STM32 管“管理面”,FPGA 管“数据面”。
STM32 负责需要复杂逻辑的功能:协议解析、配置管理、EEPROM 读写、SD 卡文件系统、网络通信、状态机调度。这些任务在 ARM 核上跑很简单,代码也好维护。FPGA 则负责需要实时、带宽高、确定性强的功能:多通道高速采集、波形缓存、信号触发、与 ADC/DAC 交互。FPGA 里跑一个 SPI 主机去操作 NOR Flash,用状态机实现擦除、编程、读取,特点是每次操作的时钟周期都可预测,不会像 CPU 那样被中断打扰。
于是整个数据链路变成这样:
- 外部信号进入 FPGA 的采集逻辑,FPGA 把实时数据流写入一个内部 FIFO;
- FIFO 到一定水位后,FPGA 自动把数据块搬运到 NOR Flash 的临时缓冲区,这是第一级存储,保证实时采集不阻塞;
- 当一段数据采集完或者定时周期到了,FPGA 通过中断通知 STM32:我已经把数据准备好放在 NOR Flash 的某个地址了;
- STM32 收到通知后,通过它与 FPGA 之间的通信接口(我这里用 SPI,也可以用双口 RAM)发送“读取指定地址数据”的命令;
- FPGA 从 NOR Flash 把数据块读出来,通过通信接口送回 STM32;
- STM32 拿到数据后直接调用 FATFS 写入 SD 卡,生成 CSV 或二进制记录文件。
用这个结构的好处是:STM32 完全不碰实时数据流,不用在中断里搬大量数据;FPGA 不跑文件系统,不用去理解 SD 卡的复杂协议栈。双方各干各擅长的,配合起来非常顺。
2. 核心细节解析与实操要点
2.1 EEPROM:I2C 接口的地址、页写与写周期坑
EEPROM 最常挂在 STM32 的 I2C 总线上,常见型号像 AT24C256、M24C64,接口是 I2C,速率最高可以到 400kHz 或更快,但实际工业板卡上我一般控制在 400kHz 以内,走线长的话还会降到 100kHz。这个速度对参数配置来说完全足够,重要的是稳定。
EEPROM 有三个非常容易踩坑的点。
第一个是设备地址。以 AT24C256 为例,它内部有 256Kbit,需要 2 字节地址,芯片的 A0/A1/A2 引脚如果全接地,那么 8 位设备写地址就是 0xA0,读地址是 0xA1。很多同学用 STM32 的 HAL 库时发现I2C_Mem_Write的地址参数和逻辑分析仪抓的不一样,就是因为没搞清楚 HAL 传的是“设备地址左移后的 8 位地址”还是“原始 7 位地址”。我的习惯是统一用 8 位地址,并且每次写完后立刻读回校验,反正控制器不是高频应用,校验一次也就几微秒。
第二个是页写限制。EEPROM 内部写入时是按页组织的,AT24C256 的页大小是 64 字节。你可以在一次写操作内连续写 64 字节,但如果地址跨过页边界,EEPROM 会“回卷”,把原本想写到下一页的数据覆盖到当前页头。我在实际项目里见过真的有人连续写 70 个字节,结果前 6 个字节被覆盖成垃圾数据。解决方式很简单:写之前判断剩余长度和页边界的距离,逐页拆分。
第三个是写周期。EEPROM 每次写入内部完成后才响应下一次指令,写周期典型值 5ms。如果写完立即去读,读到的可能是旧数据。需要软件延时 5ms 以上,或者通过 ACK 轮询判断是否忙。ARM 上最简单的做法是每次写完后HAL_Delay(5),如果系统里有 RTOS,可以改成等待 I2C 忙状态搞定,效果一样。
EEPROM 还有功耗和掉电写入的问题。工业现场如果 VCC 跌落时正好在写内部状态,最糟糕会导致数据损坏。所以我一般会在 3.3V 电源入口加一个电压检测芯片,当电压跌落到阈值时,STM32 进入快速保护流程,停止一切写操作,同时把写保护引脚拉死。对特别重要的参数,可以在 EEPROM 里存两个副本,启动时比较校验,主副不一致就恢复默认。
2.2 NOR Flash:FPGA 直接驱动的 SPI 状态机注意事项
FPGA 驱动 NOR Flash 通常走 SPI 接口,比如 W25Q64、W25Q128 这类 SPI NOR Flash,容量 8MB/16MB,工业上很普及。有些场合用并行 NOR Flash,但管脚多,FPGA 布线压力大,我在控制器里优先选了 SPI NOR。
SPI NOR 的操作逻辑比 NAND 简单很多,但有几个概念必须先说清楚:Flash 的最小擦除单位是扇区(比如 4KB),一旦擦除,扇区内的位会变成 1;编程是 0xFF 变成指定数据,只能把 1 变成 0,所以写之前必须先擦除;页编程最大一般 256 字节。这些特性直接决定了你不能像内存一样直接覆盖写,必须按“擦除-编程-读取”三步走。
FPGA 里写 NOR Flash 驱动,核心是一个 SPI 主机 + 一个状态机。以 W25Q64 为例,状态机大概是这样:
- 上电或复位后,进入 IDLE;
- 要写数据时,先发
Write Enable指令(0x06),把状态寄存器里的 WEL 置 1; - 发
Sector Erase(0x20),带上要擦除的扇区地址,然后等待 Flash 的内部擦除完成; - 擦除完成后,发
Page Program(0x02),带上目标地址和数据,长度不能超过页剩余空间,然后等待编程完成; - 等待 Busy 位释放的方法是循环读状态寄存器(0x05),看位 0(BUSY)是否为 0。
这里最容易出问题的地方是擦除等待时间。有些工程师在仿真里忽略了擦除时间,直接在发完擦除指令后立刻发编程指令,结果写进去的东西全是乱的。擦除一个 4KB 扇区,W25Q64 的典型时间是 45ms 左右,最坏可以到 400ms,状态机必须能稳定等这么久。我一般会在状态机里加一个 1ms 的定时计数,超时 500ms 就报错,而不是无限等。
编程时序也值得一提。Page Program可以一次写 1~256 字节,但地址如果跨 256 字节页边界,Flash 会停止写入或者数据错位。FPGA 逻辑里必须对起始地址和长度做边界检查。数据宽度上,FPGA 一次拿到的可能是 32 位并行数据,转成 SPI 字节流时要处理好字节顺序,小端还是大端在协议里定死,避免和 STM32 侧对不上。
代码层面不用写得太复杂,状态机加一个 fifo 就够了。我早期实现过一版用“读改写”来维护环形缓冲,后来发现直接定义固定块大小、按块擦写,逻辑会清晰很多。每次都整块擦除、整块写入,不搞部分页更新,稳定性和可调试性都会大幅提升。
2.3 SD 卡:STM32 上的 FATFS 与掉电安全
SD 卡这层相对简单,STM32 跑 FATFS,底层驱动用 SDIO 或者 SPI。SDIO 模式速度更高,但我一开始在板卡上用的是 SPI 模式,因为管脚少、逻辑简单,最大速度也能到 12.5MB/s,足够把归档数据写进去了。后来因为 SD 卡兼容性问题换成了 SDIO + 4 位模式,不过这是后话,后面章节会讲。
FATFS 要做的事不多:
FATFS fs; FIL fil; UINT bw; char path[] = "0:/data.csv"; f_mount(&fs, "", 1); f_open(&fil, path, FA_WRITE | FA_OPEN_ALWAYS); f_lseek(&fil, fil.fsize); // 追加写 f_write(&fil, buffer, len, &bw); f_sync(&fil); f_close(&fil);这段代码就是我日常在 STM32 上的写法。f_mount只挂载一次,f_open每次写入打开一次,写完f_sync把文件系统缓存刷到卡里,最后f_close。看起来简单,但很多人不会写f_sync,认为f_write完就万事大吉。事实上一旦掉电,FATFS 的缓存可能还没落到 SD 卡内部,文件就损坏了。所以凡是关键数据,写完必须 sync。更稳妥的做法是每写一个记录块就 sync,虽然牺牲一点速度,但比掉电丢数据划算。
SD 卡硬件上还有一个细节:SD 卡座和卡本身有接触电阻,瞬时电流在写入时可能达到几十毫安以上,如果供电线太细或者 LDO 余量不够,电压跌落就会导致卡进入保护状态甚至无法识别。我在板上给 SD 卡电路单独加了一颗 10µF 钽电容,并联 0.1µF 陶瓷电容,靠近卡座放置,实测插卡写入时电压纹波从 200mV 降到了 50mV 以内。
另外,SD 卡的文件系统格式化时要注意簇大小。默认 FAT32 格式化的 4KB 簇在频繁小文件写入时会浪费很多空间,但工业控制器一般写入次数不多,反而是文件很大,所以用 32KB 簇更合适。不过这个可以在 SD 卡出厂时决定,代码里不用管。
2.4 硬件设计上不能省的几个环节
三级存储方案里,EEPROM、NOR Flash、SD 卡都是 3.3V 器件,STM32 的 IO 也是 3.3V,FPGA 的 bank 可以根据需要配置为 3.3V 或 1.8V。如果 FPGA 的 IO bank 是 2.5V 电平,而 NOR Flash 是 3.3V,之间必须加电平转换,不能硬接。工业控制器里我宁可多用一两个电平转换芯片,也不去冒险超压。
电源方面,EEPROM 功率很低,不用特殊处理。NOR Flash 编程时电流一般在 20mA 左右,擦除时稍大,但瞬时冲击不大,主要是要保证 3.3V 主电源稳定。SD 卡是变化最剧烈的,插卡瞬间、写入瞬间电流波动很大,必须在 SD 卡供电处加去耦电容和磁珠,避免 SD 卡的电流尖峰干扰到模拟采集部分。
信号完整性和保护,我的经验是给所有外部可插拔接口(比如 SD 卡、外部通信口)加 TVS 管,同时注意 ESD。控制器可能在机柜里呆几年,操作人员插拔 SD 卡时如果释放静电,损坏的不只是卡,可能是主控芯片。EEPROM 和 NOR Flash 在板内,倒不用太担心,但 STM32 的 I2C 总线上拉电阻一定要算好,过小会让总线上升沿太慢,过大又会让功耗升高,常见范围 2.2kΩ~4.7kΩ。
还有一个容易忽略的是上电时序。SD 卡不是上电立即能操作的,卡内部初始化需要时间。STM32 在上电后最好不要立刻f_mount,等 100ms 左右再操作。NOR Flash 比较简单,电源稳定后就可以访问。EEPROM 也一样。但如果 STM32 和 FPGA 共用电源,FPGA 配置加载会拉电流,可能会让 3.3V 短暂跌落,如果此时 SD 卡正在写入,就有可能导致文件系统损坏。所以我会把 SD 卡的写操作放在系统完全稳定之后,并在软件里加入电压检测。
3. 实操过程与核心环节实现
3.1 搭建最小系统的硬件连接参考
为了让整个方案可落地,我给出一个具体的连接参考,不代表唯一解,但这是一套我实测能跑通的接法。
STM32 这边:
| STM32引脚 | 外设 | 目标器件 |
|---|---|---|
| PB8 / PB9 | I2C1 | EEPROM AT24C256 |
| PC8 / PC9 / PC10 / PC11 / PC12 / PD2 | SDIO | SD 卡 |
| PA5 / PA6 / PA7 / PB0 | SPI1 | FPGA(控制/数据接口) |
| PD12 | GPIO 输入 | FPGA 的中断通知引脚 |
FPGA 这边:
| FPGA引脚 | 功能 | 目标器件 |
|---|---|---|
| FPGA_SPI_CS / SCLK / MOSI / MISO | SPI主机 | NOR Flash W25Q64 |
| FPGA_SPI_CS2 / SCLK2 / MOSI2 / MISO2 | SPI从机 | STM32(通过SPI访问FPGA寄存器) |
| 若干IO | FIFO状态/中断 | 与STM32之间握手 |
这里有一个关键选择:我将 NOR Flash 直接挂在 FPGA 的 SPI 主机接口上,而 STM32 通过另一个通信 SPI 访问 FPGA。STM32 不直接读写 NOR Flash,而是向 FPGA 发命令:“把地址 0x10000 开始的 4KB 读出来,通过 SPI 从机发给我”。这样 FPGA 可以按自己的时序最优化地操作 Flash,STM32 完全不用关心等待擦除和编程这些细节。
3.2 EEPROM 读写流程的 STM32 代码示例
EEPROM 的驱动代码其实很固定,我这里贴一段实际在用的函数,按页面边界拆分写入:
#define EEPROM_DEV_ADDR 0xA0 #define EEPROM_PAGE_SIZE 64 #define EEPROM_I2C hi2c1 uint8_t eeprom_write_bytes(uint16_t addr, uint8_t *buf, uint16_t len) { while (len > 0) { uint16_t chunk = len; uint16_t page_left = EEPROM_PAGE_SIZE - (addr % EEPROM_PAGE_SIZE); if (chunk > page_left) chunk = page_left; if (HAL_I2C_Mem_Write(&EEPROM_I2C, EEPROM_DEV_ADDR, addr, I2C_MEMADD_SIZE_16BIT, buf, chunk, 200) != HAL_OK) { return 0; } HAL_Delay(5); // 等待内部写周期 addr += chunk; buf += chunk; len -= chunk; } return 1; } uint8_t eeprom_read_bytes(uint16_t addr, uint8_t *buf, uint16_t len) { if (HAL_I2C_Mem_Read(&EEPROM_I2C, EEPROM_DEV_ADDR, addr, I2C_MEMADD_SIZE_16BIT, buf, len, 200) != HAL_OK) { return 0; } return 1; }使用时,可以把校准数据封装成一个struct,里面加一个 magic 和 CRC32,写入时整体写,启动时读取并校验。如果校验失败,就说明 EEPROM 里可能没有数据或者已被破坏,需要进入默认参数加载流程。我见过很多产品初始化时不做校验,结果设备第一次上电就读到 0xFF 垃圾参数,整条产线都跑飞。
3.3 FPGA 读写 NOR Flash 的状态机与核心代码
FPGA 操作 W25Q64,我用一个小状态机就完成了。这里贴出编程和擦除的核心流程,以 Verilog 为例,读者可以自行套用到自己的模块里。
localparam [3:0] IDLE=0, WREN=1, ERASE=2, ERASE_WAIT=3, PROGRAM=4, PROGRAM_WAIT=5, DONE=6; reg [3:0] state; reg [6:0] bit_cnt; reg [31:0] addr; reg [7:0] data_buf[0:255]; always @(posedge clk) begin case (state) IDLE: begin if (start) state <= WREN; end WREN: begin // 发送 0x06 spi_tx(8'h06); state <= ERASE; end ERASE: begin // 发送 0x20 + 24bit地址 spi_tx(8'h20); spi_tx(addr[23:16]); spi_tx(addr[15:8]); spi_tx(addr[7:0]); state <= ERASE_WAIT; end ERASE_WAIT: begin // 读状态寄存器,bit0==0 表示完成 spi_tx(8'h05); if (spi_rx_data[0] == 1'b0) state <= PROGRAM; end PROGRAM: begin // 发送 0x02 + 地址 + 数据 spi_tx(8'h02); spi_tx(addr[23:16]); spi_tx(addr[15:8]); spi_tx(addr[7:0]); for (i=0;i<len;i=i+1) spi_tx(data_buf[i]); state <= PROGRAM_WAIT; end PROGRAM_WAIT: begin spi_tx(8'h05); if (spi_rx_data[0] == 1'b0) state <= DONE; end DONE: begin done <= 1'b1; state <= IDLE; end endcase end注意这个代码只是示意,spi_tx和spi_rx_data需要自己实现一个 SPI 主机模块。关键点是每一个 Flash 指令之间必须等当前命令完成,比如擦除和编程都要等 BUSY 拉低。千万不要在仿真里觉得时序没问题就直接上板,实际擦除时间远远长于仿真时钟,必须靠状态机轮询状态寄存器而不是固定延时。
我在实际编码中还加了一个“环形缓冲”模块。NOR Flash 被划分为若干个 4KB 扇区,FPGA 维护一个写指针,写满一个扇区后自动擦除下一个扇区。当采集停止或者外部请求时,FPGA 把当前读指针指向的数据从 Flash 读出来,通过 SPI 从机发送给 STM32。环形缓冲区的好处是,数据采集是持续不断的,SD 卡又没办法保证每次写入都具备实时性,两者解耦后,实时采集和归档写入就不会互相拖后腿。
3.4 SD 卡挂载、写入与导出数据的完整流程
STM32 收到 FPGA 的中断后,进入数据搬运流程。这段流程我用一个简单状态函数实现:
void handle_fpga_data_ready(void) { uint8_t block[4096]; uint32_t fpga_addr = fpga_get_read_ptr(); f_open(&fil, "0:/log.bin", FA_WRITE | FA_OPEN_ALWAYS); f_lseek(&fil, fil.fsize); // 分批从 FPGA 读取 NOR Flash 数据 for (uint32_t offset = 0; offset < BLOCK_SIZE; offset += 512) { fpga_read_flash(fpga_addr + offset, block, 512); f_write(&fil, block, 512, &bw); } f_sync(&fil); f_close(&fil); }如果直接生成 CSV,就在写入前把原始数据转换成文本,再调用f_write。CSV 的好处是现场人员可以直接用 Excel 打开,坏处是写文本效率低,数据量大时 SD 卡写入时间会比较长。我一般优先写二进制文件,同时在文件头存一个参数表;上位机解析时再转成 CSV。不是所有现场都需要直接打开 CSV,二进制文件不仅写入快,占用的卡容量也小很多。
写入过程中还有一个细节:f_open每次打开文件后,最好f_lseek到文件末尾,否则如果此前文件不是以FA_OPEN_ALWAYS方式追加,新数据总是从文件头覆盖。这个错误我在第一版调试时出现过,当时看着日志文件永远只有最后一个块,排查了很久才发现是f_open后没有 seek 到尾巴。
SD 卡写入速度,实测在 STM32F407 上 SDIO 4 位模式、FATFS 默认缓冲区配置,连续写 512 字节块大概能达到 800KB/s~1.5MB/s,看卡的实际性能。对于本项目采集的 64KB/s 数据量完全够用,甚至可以把采集速度再翻几倍。
3.5 把三级存储落地成一套数据流策略
到这里,整套方案具体化成了这样一条数据流:
- EEPROM:保存设备序列号、校准系数、用户配置,每次写入回读校验,启动时载入;
- NOR Flash:作为 FPGA 的实时缓存,里面维护环形缓冲,大小为 16 个扇区,每个 4KB,一共 64KB,实际如果采集频率高,可以再加大;
- SD 卡:保存长期历史数据,STM32 定时或按事件触发搬运,搬运成功后用 CRC 校验确认文件完整。
分级不仅仅是存储介质不同,还包括数据生命周期管理。在 FPGA 内部,数据进入 NOR Flash 之前会先经过 FIFO,FIFO 保证了 SPI Flash 擦除和编程造成的等待时间不会直接影响采集入口。这里的 FIFO 深度我一般做到 2K 字节,足够覆盖 Flash 擦除最坏情况下的数据冲击。
而在 STM32 侧,每次搬运数据都必须做两件事:一是从 FPGA 读回的数据块要在内存里做一次 CRC 校验;二是写入 SD 卡后要f_sync。CRC 校验能防止 SPI 通信瞬时错误把坏数据写进 SD 卡,sync 则防止掉电时文件系统崩溃。这两步看似简单,但能避免后期大量“现场数据对不上”的扯皮。
4. 常见问题与排查技巧实录
4.1 EEPROM 写不进数据、写进去读出来不对
出现这个问题时,我一般先查三件事:单片机地址是否和设备地址匹配、是否触发了跨页写、写完有没有等足写周期。
如果写进去了但读出来不对,多半是跨页写。AT24C256 一次写 64 字节没问题,但写地址如果从 60 开始写 10 字节,就会跨到下一页并回卷覆盖。我建议把所有 EEPROM 写函数都做成按页拆分,无论调用方写多少字节,内部都在页边界切开。另一个隐蔽的问题是,HAL 库中断优先级如果设置不对,I2C 传输会被打断,但 HAL 返回成功,实际数据没发完。这种情况要重点检查 I2C 总线的错误标志,必要时开启超时重试。
如果整片都读不出来,先量 I2C 的 SCL/SDA 波形,看有没有毛刺或者低电平拉不低。常见原因是上拉电阻过大,总线上升沿太慢,SDA 在时钟高电平期间还不稳定。示波器实测是最快的排查方法,波形干净了再怀疑软件。
4.2 NOR Flash 擦除超时、编程后数据错误
NOR Flash 最容易出现的两类问题,一类是擦除超时,另一类是编程数据错位。
擦除超时,首先要确认写保护引脚有没有被拉低,很多 NOR Flash 的 WP 引脚低电平时写保护生效,指令发了但不会执行。其次是 3.3V 电源在擦除瞬间跌落,Flash 内部逻辑会异常,擦除操作永远不完成。我遇到过一版板卡,NOR Flash 和 LCD 背光共用一个电源,背光开启瞬间电压掉了 300mV,Flash 就开始随机擦除超时,后来单独给 Flash 供电并加大储能电容就解决了。
编程数据错误,很大概率是页编程跨页没有拆分。W25Q64 的页大小是 256 字节,如果你从地址 0x100 开始写 200 字节,那么页面已经接近尾部,你写出去的数据会“回卷”写到页开头。解决办法和 EEPROM 一样,在 FPGA 状态机里按页计算可写长度,超过一页就拆成多次编程。
另外一个很容易忽视的问题是,FPGA 里 SPI 时序的时钟极性和相位必须和 Flash 匹配。W25Q64 默认是模式 0(CPOL=0,CPHA=0),也就是空闲时 SCLK 低电平,数据在上升沿采样。如果你在 FPGA 里写了一个模式 3 的 SPI 主机,读回来的数据会错得毫无规律。我在调试时用逻辑分析仪抓过 MISO 线上的数据,发现总是延迟一拍,最后查出来是主机的采样沿设置不对。所以无论是 STM32 还是 FPGA 驱动 SPI Flash,先把 SPI mode 确认好,再谈后面的协议。
4.3 SD 卡挂载失败、写入过程中死机、掉电损坏
SD 卡的问题五花八门,但如果按出现频率排,卡兼容性排第一。市面上的 SD 卡有不同厂商、不同容量、不同速度等级,FATFS 和 SDIO 驱动在某些型号上会初始化失败。我最终做法是维护一个“兼容卡列表”,并做初始化失败重试,每次初始化失败后重新上电再试一次,还不行就给出明确指示灯,不让系统悄悄卡死。
写入过程中死机,先排查 SDIO 线速。STM32 的 SDIO 时钟初始频率太高会导致通信不稳定,我用的是 25MHz 时钟,如果卡支持再往上提,但工业现场原则上不追求极致。如果是在 SPI 模式下,时钟频率超过 20MHz 也容易出现数据错乱。降低时钟后,很多偶发死机都会消失。
掉电损坏是 SD 卡最痛的问题。软件上唯一可靠的防护就是f_sync和f_mount周期卸载。我设计了一个掉电检测中断,当 3.3V 电源检测到低压时,系统立即停止采集任务,执行f_sync关闭当前文件退出 FATFS。这个动作必须在几十毫秒内完成,所以检测到掉电后第一时间就放弃其他无关操作,只做同步。
还有一个细节:SD 卡插拔检测。在卡座上加一个 CD 引脚,连接到 STM32 的 GPIO,检测到卡被拔掉后,立即停止所有文件操作,避免在无卡状态下写入导致忙等到超时。现场运维确实可能带电拔卡,没有检测机制的话,FATFS 会出现不可预测的错误。
4.4 STM32 和 FPGA 协同调试时的几个实战建议
每次调试这种跨架构的系统,我都采用“先单独验证,再联调”的顺序。先把 STM32 和 FPGA 各自存储部分单独调好:EEPROM 读写单测通过、FPGA 直接操作 NOR Flash 的回环测试通过、SD 卡读写文件通过。全部通过后才把两者连起来,这时候如果出问题,问题一定出在通信协议,而不是存储介质本身。
通信协议方面,我强烈建议在 STM32 和 FPGA 之间的数据帧里加 CRC 校验。STM32 发命令给 FPGA,FPGA 返回逻辑从 Flash 读到的原始数据,途中任何一位翻转都会让最终文件出现坏块。CRC 加在每一帧的尾部,校验失败就重发,不影响实时性,但能避免“数据稀里糊涂就写进 SD 卡”的尴尬。
最后是测试环境。工业控制器存储方案必须经历全温度、全供电电压、频繁掉电的测试。我专门做了一个自动化脚本,让 STM32 每隔 10 秒写一次 EEPROM 参数,FPGA 每 200ms 写一段 NOR Flash 数据,STM32 每 5 秒搬运一次到 SD 卡,同时随机断电。跑一晚上,第二天检查 EEPROM 校验值、NOR Flash 环形缓冲完整性、SD 卡文件数量和 CRC。只有这种折腾式测试通过了,才敢把设备发到现场。我在实际项目里因为偷懒跳过掉电测试,结果现场三台设备出现了同样的文件系统损坏,后来补做测试才发现是上电瞬间电压跌落导致 SD 卡误操作,改了电源和时序后问题消失。
做这套方案时,我最深的体会是:存储方案设计得再花哨,不如老老实实按数据特性分好层,把每一层的驱动和异常处理做扎实。EEPROM 管参数,NOR Flash 管中间缓存,SD 卡管最终归档,三者各司其职,控制器才能在长年累月的运行中保持可靠。如果你也在做类似的 STM32+FPGA 控制器,建议先从最小系统把它跑通,不要一上来就堆功能,存储这件事,稳比快重要得多。