工业控制器这类设备有个很现实的问题它不像消费电子那样可以随便断电重启也不像纯软件系统那样能靠云存储兜底。现场跑着的设备参数、标定值、运行日志、故障记录这些东西一旦丢了轻则产线停线重则整批产品报废。所以数据存储方案在工业控制器里从来不是随便挂个Flash那么简单的事。我这些年做过的板子里STM32配FPGA这个组合出现频率很高。STM32负责控制逻辑、通信协议、人机交互FPGA负责高速采集、实时时序、并行处理两边各管一摊。但存储这件事往往是最容易被低估的部分——很多人画原理图的时候随手挂一片EEPROM就完事了等到现场发现参数写不进去、日志丢数据、SD卡挂载失败才开始回头补课。这篇内容就是围绕STM32FPGA架构下的分级存储方案展开把EEPROM、NOR Flash、SD卡这三种介质各自该管什么、怎么接、怎么用讲清楚。适合正在做工业控制器、数据采集终端、边缘网关这类项目的硬件和嵌入式工程师参考也适合刚接触存储设计、想搞明白为什么不能只用一种存储的读者。1. 为什么工业控制器必须做分级存储1.1 单一存储介质在工业场景下的真实困境先说说只用一种存储会出什么问题。我见过不少项目设计阶段图省事整块板子就挂一片大容量NOR Flash所有数据都往里塞。结果跑起来之后问题一个接一个。参数区被日志区覆盖是最常见的。NOR Flash按扇区擦除你写日志的时候如果没做好地址隔离一次擦除操作就可能把旁边的标定参数一起抹掉。这种问题在实验室很难复现因为实验室里参数改得少、日志写得也少到了现场设备连续跑几个月日志量上来了冲突概率就大了。另一个问题是擦写寿命。NOR Flash的擦写次数通常在十万次量级EEPROM能到百万次甚至更高。如果把频繁更新的运行计数、累计工时这类数据放在NOR Flash里用不了多久那块扇区就废了。而SD卡虽然容量大、单位成本低但它的写入机制决定了它不适合存关键参数——掉电瞬间正在进行的写入操作可能损坏整个文件系统而且SD卡的可插拔特性意味着它可能被人为拔走或者接触不良。所以分级存储的核心逻辑不是多挂几种存储显得专业而是让每种介质干它最擅长的事EEPROM管高频小数据NOR Flash管关键配置和固件SD卡管大容量非关键数据。1.2 三种介质的角色分工与选型依据把三种介质的特性摆在一起对比分工就清楚了。介质类型典型容量擦写寿命写入速度掉电安全性适合存什么EEPROM2KB~512KB100万次以上慢ms级高字节级写入标定参数、设备ID、运行计数NOR Flash1MB~64MB10万次左右中等较高扇区级操作固件、配置表、故障记录SD卡GB级取决于卡质量较快低需文件系统历史数据、日志文件、采集原始数据EEPROM选型上I2C接口的24系列是最常见的选择比如24C02、24C256、24C512。它按字节读写不需要擦除操作写一个字节就是一个字节这对频繁更新的参数特别友好。缺点是容量小、速度慢但存参数本来就不需要快。NOR Flash方面SPI接口的W25Q系列用得最多W25Q64、W25Q128这些型号在工业板子上随处可见。它支持扇区擦除和页编程适合存那些写一次读很多次的数据。固件升级的时候也是写到NOR Flash里STM32从NOR Flash启动或者搬运到内部Flash执行。SD卡就是标准的大容量存储走SDIO或者SPI接口。它的价值在于容量和可更换性适合存那些丢了也不影响设备运行的数据比如历史曲线、运行日志、采集样本。1.3 STM32与FPGA各自该管哪类存储在STM32FPGA的架构里存储的访问路径也需要分工。我的习惯是STM32负责管理EEPROM和NOR FlashFPGA负责高速数据流到SD卡的通道。原因很简单。EEPROM和NOR Flash的访问频率不高但对可靠性要求高STM32的I2C和SPI外设成熟稳定中断和DMA机制也方便做重试和校验。而SD卡写入涉及大量数据搬运FPGA可以做乒乓缓冲和流式写入不占用STM32的CPU资源。特别是采集类应用FPGA从ADC拿到数据之后直接打包写SD卡STM32只需要定期读取文件列表或者做索引管理。当然也有例外。如果FPGA资源紧张SD卡也可以挂在STM32的SDIO接口上用FatFs文件系统管理。这种方案开发快但写入带宽受限于STM32的总线性能高速采集场景下可能会丢数据。2. EEPROM参数存储的第一道防线2.1 I2C EEPROM的硬件连接要点24系列EEPROM走I2C总线硬件上看起来简单两根线一挂就完事但实际布线有几个坑要注意。上拉电阻的取值需要根据总线电容和速率来算。标准模式100kHz下4.7kΩ是常见值快速模式400kHz下建议用2.2kΩ到4.7kΩ之间。如果总线上挂了多个从机电容增大上拉电阻要相应减小否则上升沿变缓会导致通信失败。我遇到过一块板子I2C总线上挂了EEPROM、温度传感器和IO扩展芯片上拉用的10kΩ结果400kHz下偶尔读不到数据换成2.2kΩ之后稳定了。地址引脚A0/A1/A2不能悬空。很多原理图里把这三个脚直接接地这没问题但要注意如果总线上有多片同型号EEPROM必须通过地址引脚区分。悬空的话引脚电平不确定可能出现地址冲突。写保护引脚WP要处理好。WP接高电平的时候EEPROM只能读不能写接低电平才能写。有些设计为了安全把WP直接接VCC结果调试的时候死活写不进去查了半天才发现是WP的问题。我的做法是WP通过一个电阻下拉到地同时预留一个跳线或者GPIO控制需要写保护的时候再拉高。2.2 页写入与字节写入的取舍EEPROM的写入方式有两种字节写入和页写入。字节写入就是每次写一个字节写完等5ms左右的内部写周期再写下一个。页写入是一次性写入一页通常8到64字节页内地址自动递增跨页的时候需要重新发起写操作。从效率角度看页写入明显更快。写64个字节字节写入需要64次写周期大概320ms页写入只需要一次写周期5ms左右。但页写入有个限制写入的数据不能跨页边界。比如页大小是32字节你从地址0x1F开始写4个字节就会跨越到下一页这时候EEPROM的行为是回卷到本页开头覆盖数据而不是顺序写到下一页。我的经验是参数存储用页写入但每次写入前先算好地址对齐。如果参数结构体大小超过一页就拆成多次页写入每次保证页内对齐。下面是一个典型的页写入函数框架#define EEPROM_PAGE_SIZE 32 #define EEPROM_ADDR 0xA0 uint8_t EEPROM_WritePage(uint16_t memAddr, uint8_t *data, uint16_t len) { uint16_t pageOffset memAddr % EEPROM_PAGE_SIZE; uint16_t writeLen; while (len 0) { writeLen EEPROM_PAGE_SIZE - pageOffset; if (writeLen len) writeLen len; if (I2C_WriteBytes(EEPROM_ADDR, memAddr, data, writeLen) ! 0) return 1; HAL_Delay(6); // 等待内部写周期完成 memAddr writeLen; data writeLen; len - writeLen; pageOffset 0; } return 0; }这段代码的关键在于每次写入前重新计算页内偏移保证不会跨页。HAL_Delay(6)是等待EEPROM内部写周期不同型号的写周期时间不一样24C02是5ms24C256也是5ms但有些型号会到10ms查数据手册确认。2.3 参数区的双备份与校验机制工业现场最怕的就是参数丢失或者损坏。EEPROM虽然可靠但也不是不会出问题——电源异常、总线干扰、器件老化都可能导致数据出错。我的做法是参数区做双备份加CRC校验。具体来说把参数结构体存两份地址A和地址B。每次写入的时候先写A校验通过后再写B。读取的时候先读ACRC对就用AA不对再读BB对就用B并把B的内容回写到A。如果两个都不对就加载默认参数并记录一次故障。参数结构体的定义也要注意。不要直接把结构体按内存布局写进去因为编译器可能会做字节对齐填充不同编译选项下布局可能不一样。稳妥的做法是手动序列化成字节数组或者用#pragma pack(1)强制紧凑排列。typedef struct { uint32_t magic; // 固定标识用于判断参数区是否初始化 uint16_t version; // 参数版本号 float kp; float ki; float kd; uint32_t runHours; uint32_t powerOnCount; uint8_t reserved[16]; uint16_t crc; } ParamStruct_t;magic字段很重要。第一次上电的时候EEPROM里全是0xFF如果没有magic判断程序会把0xFFFFFFFF当成有效参数加载行为就不可控了。version字段用于固件升级后参数结构的兼容处理新版本固件发现旧版本参数时可以走迁移逻辑。2.4 写次数均衡与寿命估算EEPROM标称100万次擦写寿命听起来很多但如果某个地址每秒写一次算下来也就十几天。工业控制器里有些数据更新很频繁比如运行计时、累计产量这些不能直接往固定地址写。写次数均衡的思路是把频繁更新的数据分散到多个地址轮换写。比如分配16个槽位每次写下一个槽位读的时候取最新的那个。每个槽位带一个序号序号最大的就是最新数据。这样写次数就被均摊到16个地址上寿命延长16倍。#define LOG_SLOT_COUNT 16 #define LOG_SLOT_SIZE 8 typedef struct { uint16_t seq; uint32_t value; uint16_t crc; } LogSlot_t; uint16_t FindLatestSlot(void) { uint16_t maxSeq 0; uint16_t latestIdx 0; LogSlot_t slot; for (int i 0; i LOG_SLOT_COUNT; i) { EEPROM_Read(LOG_BASE_ADDR i * LOG_SLOT_SIZE, (uint8_t*)slot, sizeof(slot)); if (slot.crc CalcCRC16((uint8_t*)slot, sizeof(slot) - 2)) { if (slot.seq maxSeq) { maxSeq slot.seq; latestIdx i; } } } return latestIdx; }这种方案在电表、水表这类需要记录累计量的设备里很常见。代价是读取的时候要遍历所有槽位但对于EEPROM的读取速度来说16个槽位的遍历也就几毫秒的事。3. NOR Flash固件与关键记录的可靠载体3.1 SPI NOR Flash的扇区结构与擦除规则W25Q系列NOR Flash的内部结构是分层的页Page256字节扇区Sector4KB块Block64KB。写入的最小单位是页擦除的最小单位是扇区。这意味着你不能像EEPROM那样直接覆盖写一个字节必须先擦除整个扇区再写。这个特性决定了NOR Flash的使用方式它适合整块写、整块读的场景。固件存储就是典型例子升级的时候擦除目标扇区然后按页写入新固件。配置表也是类似把整个配置区当成一个整体来管理。擦除操作的时间比较长4KB扇区擦除典型值45ms64KB块擦除150ms左右。如果在擦除过程中断电那个扇区可能处于不确定状态。所以关键数据不能只存一份在NOR Flash里必须有备份或者校验恢复机制。3.2 固件存储区的地址规划固件在NOR Flash里的布局需要提前规划好。我的习惯是把NOR Flash分成几个固定区域区域名称起始地址大小用途Bootloader0x00000064KB启动引导负责固件校验和跳转App Firmware A0x010000512KB主固件区App Firmware B0x090000512KB备份固件区用于升级回滚Config0x11000064KB系统配置参数Fault Log0x120000256KB故障记录循环写入Reserved0x160000剩余预留扩展双固件区的设计是为了支持安全升级。升级的时候先把新固件写到B区校验通过后更新Bootloader里的启动标志重启后从B区启动。如果新固件有问题还可以回滚到A区。这种方案在工业设备里几乎是标配因为现场升级失败的成本太高了。Bootloader里的启动标志也要做备份。通常是在Bootloader区域里分配两个扇区各存一份启动信息带CRC校验。读取的时候取校验通过的那份。3.3 故障记录的循环写入策略故障记录的特点是写入频率不确定但一旦写就不能丢。设备正常运行的时候可能几天都不写一条出故障的时候可能短时间内连续写很多条。这种场景适合用循环缓冲区的方式管理。具体做法是把Fault Log区域分成固定大小的记录槽比如每条记录128字节256KB的区域可以存2048条。维护一个写指针每次写新记录的时候指针加一写到末尾就回绕到开头。每条记录带一个递增的序号和时间戳读取的时候按序号排序就能还原时间顺序。#define FAULT_LOG_BASE 0x120000 #define FAULT_LOG_SECTOR 4096 #define FAULT_RECORD_SIZE 128 #define FAULT_RECORDS_PER_SECTOR (FAULT_LOG_SECTOR / FAULT_RECORD_SIZE) typedef struct { uint32_t seq; uint32_t timestamp; uint16_t faultCode; uint8_t faultData[64]; uint16_t crc; } FaultRecord_t;写记录的时候要注意如果当前扇区已经写满需要先擦除下一个扇区再写。擦除操作会阻塞一段时间如果这时候有新的故障产生要有缓冲机制。我的做法是在RAM里开一个小的故障队列擦除完成后再把队列里的记录写进去。3.4 NOR Flash的磨损与坏块处理NOR Flash虽然比NAND Flash可靠但也不是完全没有坏块。出厂的时候厂家会保证前几个扇区无坏块但使用过程中扇区也可能因为擦写次数过多而失效。对于故障记录这种循环写入的区域每个扇区的擦写次数是均匀的因为循环缓冲区会依次使用每个扇区。但配置区如果频繁更新就需要做写均衡。简单的做法是配置区也做双备份每次更新交替写两个区域这样擦写次数减半。坏块检测方面写入后回读校验是最直接的方法。如果某个扇区写入后回读数据不一致连续几次都这样就标记为坏块从可用列表中剔除。这个逻辑在Bootloader里实现比较合适因为Bootloader是最后一道防线。4. SD卡大容量数据的归宿4.1 SDIO与SPI两种接口模式的取舍SD卡有两种访问模式SDIO和SPI。STM32的SDIO接口支持4位数据线理论带宽可以到48MHz时钟下的24MB/s4位模式。SPI模式只用一根数据线速度受限于SPI时钟通常也就几MB/s。选哪个取决于数据量。如果只是存日志文件每天几MBSPI模式足够了而且SPI接口通用性好引脚少布线简单。如果是高速采集比如FPGA做图像采集或者振动信号采集数据率到几MB/s甚至十几MB/s那就必须用SDIO的4位模式。SDIO的硬件设计要注意几点CLK线要尽量短走线阻抗控制好CMD和DAT线需要上拉通常用10kΩ到50kΩ电源去耦要到位SD卡在写入瞬间电流波动比较大建议在卡座电源脚旁边放一个100μF的电解电容加一个0.1μF的陶瓷电容。SPI模式就简单多了标准SPI四线接法CS、CLK、MOSI、MISO加上电源和地。但要注意SD卡在SPI模式下的初始化流程和SDIO模式不一样需要发送CMD0进入SPI模式然后CMD8、ACMD41、CMD58等一系列命令完成初始化和容量识别。4.2 FatFs文件系统的移植与配置STM32上跑SD卡FatFs是最常用的文件系统。移植FatFs主要做两件事实现diskio.c里的五个底层函数配置ffconf.h里的选项。diskio.c需要实现的函数是disk_initialize初始化SD卡返回0表示成功disk_status返回卡状态disk_read从指定扇区读数据disk_write向指定扇区写数据disk_ioctl获取扇区数量、扇区大小等信息ffconf.h里几个关键配置#define _FS_READONLY 0 // 需要写入设为0 #define _FS_MINIMIZE 0 // 不裁剪功能 #define _USE_STRFUNC 1 // 启用字符串函数 #define _USE_FIND 1 // 启用文件查找 #define _USE_MKFS 1 // 启用格式化功能 #define _USE_FASTSEEK 1 // 启用快速定位 #define _FS_LOCK 1 // 启用文件锁多任务环境下需要 #define _FS_REENTRANT 1 // 启用重入保护如果系统里有多任务同时访问SD卡_FS_REENTRANT必须打开并且要实现ff_req_grant和ff_rel_grant两个同步函数。我见过一个项目STM32跑FreeRTOS一个任务写日志一个任务读配置没开重入保护结果文件系统内部结构被破坏卡里的数据全乱了。4.3 掉电保护与文件完整性SD卡最大的风险是掉电时正在写入。FatFs在写入过程中会更新FAT表和目录项如果这时候断电文件系统可能处于不一致状态。轻则最后写入的文件损坏重则整个分区无法挂载。降低风险的办法有几个。第一是定期调用f_sync把缓存数据刷到卡里。FatFs默认有写入缓存不调用f_sync的话数据可能还在RAM里。但f_sync太频繁会影响写入速度需要根据数据重要性权衡。第二是采用写临时文件再重命名的策略。新数据先写到data.tmp写完后关闭文件再重命名为data.log。重命名操作在文件系统层面是原子的即使断电要么是旧的data.log完好要么是新的data.log完整不会出现半截文件。第三是在硬件上做掉电检测。用一个ADC通道监测电源电压当电压降到阈值以下时触发中断在中断里完成f_sync和f_close操作。这个时间窗口很短通常只有几毫秒到几十毫秒所以电源端的储能电容要足够大。4.4 FPGA直写SD卡的数据通路设计高速采集场景下数据从FPGA直接写SD卡可以减轻STM32的负担。FPGA实现SD卡控制器有两种思路一种是FPGA实现SPI主机按SPI协议访问SD卡另一种是FPGA实现SDIO控制器速度更快但逻辑更复杂。SPI方式的FPGA实现相对简单。FPGA内部维护一个发送FIFO和一个接收FIFO状态机负责发送命令、接收响应、读写数据块。SD卡的SPI协议规定数据以512字节块为单位传输FPGA需要把采集数据打包成512字节的块再写入。// SD卡SPI写入状态机简化框架 module sd_spi_writer ( input wire clk, input wire rst_n, input wire wr_en, input wire [7:0] wr_data, output reg wr_ready, output reg spi_cs_n, output reg spi_clk, output reg spi_mosi, input wire spi_miso ); localparam S_IDLE 3d0; localparam S_CMD 3d1; localparam S_RESP 3d2; localparam S_DATA 3d3; localparam S_WAIT 3d4; reg [2:0] state; reg [7:0] cmd_buf[5:0]; reg [5:0] cmd_idx; reg [8:0] byte_cnt; reg [7:0] data_buf[511:0]; // 状态机逻辑省略核心是发送CMD24写命令 // 等待响应0x00然后发送数据起始令牌0xFE // 接着发送512字节数据最后发送2字节CRC endmoduleFPGA直写SD卡的关键在于缓冲管理。采集数据是连续流SD卡写入是块操作中间需要乒乓缓冲。用两个512字节的BRAM一个在采集数据另一个在往SD卡写写完后交换。这样采集不会因为SD卡写入的等待时间而丢数据。STM32在这个架构里的角色是管理文件系统。FPGA只负责往SD卡的物理扇区写数据文件系统的维护还是由STM32来做。两边通过一个共享的RAM或者寄存器接口交换信息STM32告诉FPGA当前可写的扇区地址FPGA写完后更新写指针。5. 三种存储的协同管理与数据一致性5.1 上电初始化顺序与检测流程系统上电后三种存储的初始化顺序有讲究。我的做法是先初始化EEPROM再初始化NOR Flash最后初始化SD卡。EEPROM最先初始化因为参数加载依赖它。如果EEPROM读取失败系统应该能加载默认参数继续运行而不是卡死。参数加载完成后系统的基本行为就确定了。NOR Flash第二个初始化主要是读取配置区和检查固件完整性。如果Bootloader已经完成了固件校验这一步可以简化。配置区的读取和EEPROM类似也要做CRC校验和默认值回退。SD卡最后初始化因为它最耗时而且不是系统运行的必要条件。SD卡初始化失败不应该阻止系统启动只是标记为存储不可用日志改为写到NOR Flash的故障记录区。初始化流程里要加超时机制。I2C总线可能因为某个从机拉低时钟线而卡死SPI总线也可能因为SD卡接触不良而无响应。每个初始化步骤都要有超时计数超时后跳过并记录故障。5.2 数据分级写入的策略设计数据写入的策略要根据数据性质来定。我把数据分成三类第一类是关键参数比如PID系数、标定值、设备ID。这类数据变化不频繁但绝对不能丢。写入策略是先写EEPROM回读校验校验通过后更新RAM缓存。如果EEPROM写入失败重试三次三次都失败就报警并保持RAM中的值不变。第二类是重要记录比如故障记录、升级记录。这类数据写入频率中等允许少量丢失但不能大面积损坏。写入策略是写到NOR Flash的循环缓冲区每条记录带CRC。如果当前扇区写满需要擦除先把记录暂存到RAM队列擦除完成后再写入。第三类是批量数据比如采集日志、运行曲线。这类数据量大允许丢失最后几条。写入策略是通过FatFs写到SD卡定期f_sync。如果SD卡不可用数据直接丢弃并记录一次存储故障。5.3 掉电时刻的数据保护掉电保护需要硬件和软件配合。硬件上电源输入端要有足够的储能电容保证断电后系统还能运行几十毫秒。软件上要有一个掉电中断服务程序在检测到电压下降时快速执行关键操作。掉电中断里能做的事情有限因为时间窗口很短。我的做法是中断触发后立即停止所有非关键任务把RAM中待写入的关键数据写到EEPROM调用f_sync刷新SD卡文件然后进入死循环等待电源完全耗尽。这里有个细节掉电中断的优先级要设到最高而且要确保中断服务程序本身不会被打断。EEPROM的写入需要几毫秒如果在这期间被其他中断打断可能来不及完成。所以掉电中断里要关掉其他中断只做最必要的操作。void PVD_IRQHandler(void) { __disable_irq(); // 关闭所有中断 // 保存关键参数到EEPROM SaveCriticalParams(); // 刷新SD卡文件系统 if (sd_mounted) { f_sync(log_file); f_close(log_file); } // 标记掉电事件 MarkPowerLossEvent(); while (1); // 等待电源耗尽 }STM32的PVD可编程电压检测外设可以配置在电压降到某个阈值时触发中断。阈值要选在系统还能正常工作的电压之上比如3.3V系统选2.9V留出0.4V的余量给电容放电。5.4 存储故障的降级运行与告警工业控制器不能因为存储故障就停机。三种存储任何一种出问题系统都应该能降级运行。EEPROM故障时参数无法保存但RAM里的参数还能用。系统应该继续运行但每次修改参数时提示保存失败并记录故障。如果重启参数会回到上次成功保存的值。NOR Flash故障时故障记录无法写入但固件和配置还在。系统继续运行故障记录改为写到SD卡或者通过通信接口上传。SD卡故障时批量数据无法存储但关键功能不受影响。系统继续运行日志改为写到NOR Flash的故障记录区同时通过通信接口发送存储故障告警。降级运行的逻辑要在系统设计阶段就规划好不能等到出了问题再临时加。每个存储介质的健康状态要定期检测比如EEPROM每次写入后校验NOR Flash定期读取校验SD卡定期检查挂载状态和剩余空间。6. 实战中踩过的坑与排查思路6.1 EEPROM写入失败但读取正常的诡异现象有一次调试一块新板子EEPROM读取完全正常参数能读出来但写入之后回读发现数据没变。查了I2C波形写操作的时序也对ACK也正常。排查过程是这样的先确认WP引脚电平用万用表量了是低电平写保护没生效。然后怀疑是写周期等待时间不够把HAL_Delay从5ms加到10ms还是不行。接着用逻辑分析仪抓完整的写操作波形发现写命令发出后EEPROM回了ACK但紧接着的写周期里SDA线被拉低了很长时间而我们的程序在写周期结束前就发起了下一次操作。问题出在I2C总线的仲裁上。那块板子的I2C总线上还挂了一个RTC芯片RTC的中断输出偶尔会拉低SDA线干扰了EEPROM的写周期。解决办法是把RTC的中断输出改到别的引脚I2C总线上只保留EEPROM和温度传感器。这个坑的教训是I2C总线上的设备要尽量少特别是那些会主动拉低总线的设备。如果必须挂多个设备要仔细检查每个设备的引脚行为确保不会在总线空闲时干扰。6.2 NOR Flash擦除后数据全变0xFF的误判NOR Flash擦除后所有位都是1也就是0xFF。这个特性本身没问题但如果你在擦除后没有正确初始化数据结构读取的时候就会把0xFF当成有效数据。我遇到过一个问题故障记录区擦除后程序读取第一条记录发现seq是0xFFFFFFFFtimestamp也是0xFFFFFFFFCRC校验居然通过了因为CRC计算的是0xFF字节的校验值。结果系统认为这是一条有效记录显示了一个荒谬的故障时间和代码。解决办法是在记录结构里加一个有效标志字段写入的时候设置为特定值比如0x5A5A读取的时候先检查这个字段。擦除后的0xFFFF不等于0x5A5A就会被正确识别为无效记录。typedef struct { uint16_t validFlag; // 0x5A5A表示有效 uint32_t seq; uint32_t timestamp; uint16_t faultCode; uint8_t data[64]; uint16_t crc; } FaultRecord_t; int IsRecordValid(FaultRecord_t *rec) { if (rec-validFlag ! 0x5A5A) return 0; if (rec-crc ! CalcCRC16((uint8_t*)rec, sizeof(FaultRecord_t) - 2)) return 0; return 1; }6.3 SD卡挂载失败但卡本身正常的排查链路SD卡挂载失败是最常见的问题之一原因可能出在硬件、驱动、文件系统任何一个环节。我的排查顺序是这样的第一步确认卡本身没问题。把卡插到电脑上能正常读写就说明卡是好的。如果电脑上也读不了换张卡再试。第二步检查硬件连接。用示波器看SDIO的CLK线有没有波形CMD线在初始化时有没有响应。如果CLK没有波形说明SDIO外设没启动或者时钟配置有问题。如果CMD线一直高电平没有下拉说明卡没有响应可能是接触不良或者卡座焊接问题。第三步检查供电。SD卡在初始化瞬间电流会有一个尖峰如果电源带载能力不足电压会被拉低导致初始化失败。在卡座电源脚旁边并一个100μF电容试试。第四步检查初始化流程。SD卡的初始化命令序列比较长CMD0、CMD8、ACMD41、CMD58每一步都要检查响应。如果某一步响应错误根据错误码判断问题。比如CMD8响应错误说明卡不支持2.0协议可能是老卡ACMD41一直返回busy说明卡初始化还没完成需要循环等待。第五步检查文件系统。如果卡能初始化但挂载失败可能是卡上没有文件系统或者文件系统损坏。用f_mkfs格式化一下再试。如果格式化也失败检查diskio.c里的disk_ioctl函数是否正确返回了扇区数量和扇区大小。6.4 多任务环境下文件系统崩溃的复现与修复前面提到过FreeRTOS下多任务访问SD卡导致文件系统崩溃的问题这里详细说一下排查过程。现象是设备运行几个小时后SD卡里的日志文件突然变成0字节或者文件名变成乱码。重启后卡能正常挂载但之前的数据丢了。排查的时候先怀疑是掉电导致的但设备一直连着电源没有断电记录。然后怀疑是SD卡质量问题换了几张卡问题依旧。最后用调试器跟踪FatFs的内部状态发现FatFs结构体里的fs_type字段偶尔会变成0说明文件系统对象被破坏了。根因是两个任务同时调用了f_write一个在写日志一个在写配置。FatFs在没有开启重入保护的情况下内部使用的全局变量会被并发访问破坏。解决办法是打开_FS_REENTRANT实现同步函数int ff_req_grant(FIL *fp) { return (xSemaphoreTake(sd_mutex, pdMS_TO_TICKS(1000)) pdTRUE) ? 1 : 0; } void ff_rel_grant(FIL *fp) { xSemaphoreGive(sd_mutex); }同时所有访问SD卡的任务在调用FatFs函数前都要先获取互斥量。更稳妥的做法是只用一个任务专门负责SD卡操作其他任务通过队列发送请求。6.5 存储方案的成本与可靠性平衡最后说说成本。三种存储都加上BOM成本会增加不少。EEPROM便宜几毛钱NOR Flash看容量W25Q128大概几块钱SD卡座加卡十几块钱。对于大批量产品这个成本差异会被放大。我的建议是根据产品定位来取舍。高端工业控制器三种存储都上可靠性优先。中低端产品可以砍掉NOR Flash固件存在STM32内部Flash里故障记录写到EEPROM或者SD卡。如果连SD卡都砍掉那就只剩EEPROM适合参数简单、不需要历史记录的场景。但有一条底线EEPROM不能省。它是参数存储的最后一道防线没有它设备断电后所有配置都丢了用户体验会非常差。NOR Flash和SD卡可以根据需求裁剪EEPROM是必须的。选型的时候还要考虑供货和生命周期。工业产品的生命周期通常五到十年选的存储芯片要保证长期供货。W25Q系列和24系列EEPROM的供货比较稳定SD卡座选标准型号就行卡本身是消耗品坏了换一张。
