
开工之前先讲一个我踩过的坑前几年做一款工业采集设备现场反馈说设备偶尔上电后配置参数会丢有时候又会变成全 FF 的默认值。查了很久最后用逻辑分析仪抓了 SPI 波形才发现是主控在上电瞬间给 Flash 发了写命令而 Flash 的供电还没稳定到规格书要求的范围内导致写入的数据根本没落进去。这种事在非易失存储器读写的开发里太常见了——大家都觉得“写进去就完了”但真正的坑全藏在时序、供电、擦写寿命、文件系统掉电保护这些看不见的细节里。这篇文章我围绕“非易失存储器读写”这条主线把 Flash、EEPROM、FRAM 这些常见介质的选型思路、硬件接口、底层读写代码、文件系统方案、调试方法和可靠性设计从头到尾捋一遍顺便把热词里大家高频搜的那些点——SPI/I2C 读写时序、FPGA 控制 DDR3、STM32 HAL 库操作 SPI Flash、文件读写流程、读写速度优化——都揉进去讲。不管你是刚入门嵌入式的新手还是被线上问题折腾得焦头烂额的工程师这篇文章都能给你一些可以直接用的思路和排查方法。1. 非易失存储器的选型思路与场景拆解1.1 先分清 NOR Flash、NAND Flash、EEPROM、FRAM 到底谁是谁很多初学者一上来就是“Flash 嘛掉电不丢数据就行”结果方案评审时才发现选错了介质轻则成本超标重则产品根本没法量产。这里我先把四种最常见的非易失存储器掰开揉碎讲清楚。NOR Flash的特点是支持随机读取片内执行XIP很方便但写入要先擦除擦除粒度是扇区常见 4KB而且擦写寿命通常在 10 万次左右。它的容量一般从 1Mbit 到 256Mbit 不等工业现场最常见的型号就是 Winbond W25Q 系列比如 W25Q128128Mbit/16MB、W25Q64、GD25Q128 这些接口都是标准的 SPI/QSPI。适合存固件、字库、配置文件这类“读多写少”的数据。NAND Flash容量大动辄 1Gbit 起跳单位比特成本低但坏块管理、ECC 纠错、页/块的组织方式让操作复杂度高了一大截。很多朋友第一次拿 NAND 是想直接像 NOR 那样读出字节流结果发现要先把“坏块表”处理好还要自己做 OOB 区域的 ECC 校验这才意识到 NAND 压根不是给裸机用户玩的。一般只有在做 SD 卡、eMMC、U 盘这类需要大容量存储的产品时才会直接跟 NAND 打交道而且底层一般由主控自带的 FTL 层帮你处理了。EEPROM最大的优势是“可字节级擦写”也就是说你不需要像 Flash 那样先擦一整个扇区再写入。它寿命长常见的 AT24Cxx 系列标称 100 万次擦写适合存设备配置、校准参数、MAC 地址这类“高频低频写但每次数据量很小”的内容。缺点是容量小最大的常见型号也就 1Mbit 左右而且 I2C 接口读写速度相对慢。FRAM铁电存储器则是另一个维度——它既有 RAM 的读写速度不需要擦除可字节级改写写寿命高达 100 亿次又有非易失特性。缺点是容量普遍不大成本偏高。在对数据完整性要求极高的场合比如做电能表计量数据、医疗设备关键参数存储FRAM 简直是最省心的选择因为它没有“写一半掉电导致数据损坏”这一说至少从原理上规避了 Flash 的掉电写坏问题。从选型思路上说我个人的经验法则很简单需求特征推荐介质典型案例存代码/固件、字库、大块配置表NOR FlashW25Q128 存字库、BootLoader大容量文件存储、音视频/日志NAND Flash/eMMC/SD行车记录仪、数据采集终端小数据、高频写、字节级读写EEPROMAT24C02 存参数、校准值高频写、强实时、不能容忍写坏FRAMFM24C04、MB85RC256V 存计量数据选错了介质导致的返工成本是最高的所以我强烈建议在画原理图之前先花半天时间把“写频率、单次数据量、掉电保护要求、环境温度、成本预算”这五个维度列一张表再对照上面的表格定介质不要凭感觉选。1.2 从热词看大家真正卡在哪底层读写和文件系统是两座大山我看了下“非易失存储器读写”相关的搜索热词排在前面的基本是这几类SPI 读写时序、I2C 读写 EEPROM 代码、STM32 HAL 库 SPI 读写、FPGA 读写 Flash、DDR3 读写控制、文件读写流程、磁盘读写速度。这些热词其实暴露了两个普遍卡点。第一个卡点是底层驱动怎么过。不管是 MCU 还是 FPGA要操作一颗具体的存储芯片“时序”永远是绕不开的关口。比如 I2C 读 EEPROM 需要“伪写”操作来设置内部地址SPI 读 Flash 要先发 8 位命令字再跟地址再收数据DDR3 更是要经过初始化、刷新、激活、预充电一整套状态机。很多人的困惑其实不在于“命令怎么写”而是“时序图怎么读”规格书里那些 tSU、tH、tCLCL 的符号看得一头雾水。第二个卡点是文件系统和擦写寿命。裸机开发时想存一堆配置文件很多人会自然而然想到 FATFS 文件系统但把文件系统跑起来之后又发现掉电时文件系统超级块损坏、写文件时丢失簇链这些问题比想象中频繁。那是因为文件系统本身只负责“逻辑组织”不负责“物理介质管理”——磨损均衡Wear Leveling和掉电恢复得你自己做或者靠专用方案。这也能解释为什么热词里有“hdfs读写流程”“mariadb数据库读写分离”这类偏系统和数据库的词本质上大家都是在找一个“数据怎么才能可靠高效地落盘”的通用答案。只不过嵌入式场景里这个“盘”可能是 SPI Flash、SD 卡、eMMC、DDR 里的数据缓冲也可能是数据库系统里的一整套读写分离架构。这篇文章后面的内容会先解决第一个卡点底层驱动再深入第二个卡点文件系统和可靠性最后补上调试和性能优化的实操经验。2. 硬件接口与读写协议底层认知2.1 SPI 读写 Flash 的时序到底在讲什么一次完整的页编程为例SPI 大概是嵌入式领域最常用的存储接口了。以 W25Q128 为例上电后要先发 0x06Write Enable命令把状态寄存器里的 WEL 位置 1然后才能发 0x02Page Program命令跟着 24 位地址、接着是一串要写入的数据。写入过程中主控要不断读 0x05Read Status Register-1寄存器等 BUSY 位bit0变为 0才算真正写完。很多朋友第一次调不通过不是因为命令写错了而是漏了以下细节写使能命令 0x06 必须在每一次编程/擦除操作之前发送。这颗 Flash 在上一次写入完成后WEL 位会自动清零你如果偷懒只在初始化时发一次 0x06后面所有写操作都会失效。写命令发送完地址之后数据是连续的不能中间再插入读状态寄存器的操作。有一次我把页编程的数据段和轮询 BUSY 的代码顺序写反了导致写进去的数据只有前 256 字节是对的后面的全乱套。页编程最多一次写 256 字节不能跨页。如果你要写 512 字节必须拆成两次页编程或者在主控侧写一个跨页封装的函数每次看看剩余字节数和页尾距离自动拆分。下面是一个基于 STM32 HAL 库的 SPI Flash 页编程最小示例很多项目的底层驱动都是从这一段扩展出来的#define SPI_FLASH_PAGE_SIZE 256 #define CMD_WRITE_ENABLE 0x06 #define CMD_PAGE_PROGRAM 0x02 #define CMD_READ_DATA 0x03 #define CMD_READ_STATUS 0x05 static void flash_write_enable(void) { uint8_t cmd CMD_WRITE_ENABLE; HAL_GPIO_WritePin(FLASH_CS_GPIO_Port, FLASH_CS_Pin, GPIO_PIN_RESET); HAL_SPI_Transmit(hspi1, cmd, 1, HAL_MAX_DELAY); HAL_GPIO_WritePin(FLASH_CS_GPIO_Port, FLASH_CS_Pin, GPIO_PIN_SET); } static void flash_wait_busy(void) { uint8_t cmd CMD_READ_STATUS; uint8_t status 0; do { HAL_GPIO_WritePin(FLASH_CS_GPIO_Port, FLASH_CS_Pin, GPIO_PIN_RESET); HAL_SPI_Transmit(hspi1, cmd, 1, HAL_MAX_DELAY); HAL_SPI_Receive(hspi1, status, 1, HAL_MAX_DELAY); HAL_GPIO_WritePin(FLASH_CS_GPIO_Port, FLASH_CS_Pin, GPIO_PIN_SET); } while (status 0x01); } void flash_write_page(uint32_t addr, uint8_t *data, uint32_t len) { uint8_t cmd[4]; flash_write_enable(); cmd[0] CMD_PAGE_PROGRAM; cmd[1] (addr 16) 0xFF; cmd[2] (addr 8) 0xFF; cmd[3] addr 0xFF; HAL_GPIO_WritePin(FLASH_CS_GPIO_Port, FLASH_CS_Pin, GPIO_PIN_RESET); HAL_SPI_Transmit(hspi1, cmd, 4, HAL_MAX_DELAY); HAL_SPI_Transmit(hspi1, data, len, HAL_MAX_DELAY); HAL_GPIO_WritePin(FLASH_CS_GPIO_Port, FLASH_CS_Pin, GPIO_PIN_SET); flash_wait_busy(); }这里有一个很容易被忽略但特别重要的事CS 片选信号必须在整个命令地址数据发送期间保持低电平只有在全部发完后才能拉高。有些新手在 HAL_SPI_Transmit 之间顺手操作了别的器件结果 CS 被拉高Flash 认为命令被中止直接丢弃当前操作数据就丢了。这一点也解释了为什么 SPI 总线上挂多个设备时最好把每个设备的 CS 用独立 GPIO 控制不要在传输中途切换。2.2 I2C 读写 EEPROM伪写地址和多字节页写的完整时序I2C 读 EEPROM 的时序之所以难理解是因为它跟 SPI 的纯命令方式差别很大。以 AT24C022Kbit/256 字节为例按字节读Current Address Read和随机读Random Read都要先发一个“伪写”Dummy Write——即先发送设备地址写标志再发送要读取的字节地址然后重新发起一次起始条件发设备地址读标志才能把数据读出来。如果你用 FPGA 写 Verilog 实现这个时序最需要注意的是 SDA 在三段操作中的方向切换以及 ACK/NACK 的采样时点。下面是一段经典的 I2C 读 EEPROM 多字节的 Verilog 状态机核心描述我写的是伪代码风格重点看状态转移逻辑// 状态机关键状态定义 localparam IDLE 4d0; localparam START1 4d1; localparam SEND_ADDR_W 4d2; localparam CHECK_ACK1 4d3; localparam SEND_MEM_ADDR 4d4; localparam CHECK_ACK2 4d5; localparam RESTART 4d6; localparam SEND_ADDR_R 4d7; localparam CHECK_ACK3 4d8; localparam READ_DATA 4d9; localparam SEND_ACK 4d10; localparam STOP 4d11; // 关键点1: 伪写阶段 SDA 是输出方向读取阶段需要释放 SDA 转为输入 // 关键点2: 每个 bit 的建立保持时间要满足 tSU:DAT( 250ns for 100kHz) // 关键点3: 在 SCL 高电平期间采样 ACK不能在低电平时采样写多字节Page Write时EEPROM 内部也有类似 Flash 的页限制。AT24C02 的页大小是 8 字节超过 8 字节就会回卷覆盖本页开头的数据。我有一次用 MCU 直接写 16 字节到 AT24C02头 8 字节是对的后 8 字节把第 0~7 字节给覆盖了查了半天才发现规格书里就写着“Page Write limited to 8 bytes”。I2C 和 SPI 还有一个截然不同的地方I2C 是漏极开路的需要外部上拉电阻。上拉电阻阻值的选择直接决定了通信速率和总线功耗。常见做法是 4.7kΩ100kHz、2.2kΩ400kHz左右但还要看总线上挂了多少设备、走线长度。总线上设备多或者走线长时电阻太小会让低电平驱动电流过大电阻太大则会让上升沿变缓SCL/SDA 的时序就过不了关。2.3 DDR3/DDR4 这类内存颗粒为什么不算“简单存储外设”热词里出现了“DDR3读写控制”“DDR4读写测试”“AXI 读写 DDR”这些搜法我得专门说明一下DDR 颗粒跟 EEPROM/Flash 不是一个难度层级的它背后有 DDR Controller、物理层PHY、初始化训练这些内容裸机手写一个 DDR3 控制器需要处理刷新、激活、读写延迟、突发长度这类大量时序参数。很多 FPGA 工程师直接用 Xilinx 的 MIG IP 核或者 Intel 的 EMIF IP 核来生成 DDR 控制器省心也稳定。从某种意义上说DDR 作为“非易失存储”的缓冲层出现在系统里通常是配合 SSD 控制器、文件系统缓存等场景跟“非易失存储器读写”的关系是先把数据在 DDR 里暂存再由算法决定什么时候落盘到 Flash/SSD。如果你只是想测 DDR 颗粒的好坏可以借助主控板自带的 DDR 测试工具或者用 FPGA 端的 IP 核做读写测试例如 Xilinx MIG 例程里自带一个简单的读写测试模块会对全部地址空间写 0xA5/0x5A 这类固定模式然后再读回比对。这类“读写测试”的本质还不只是检查位线短路/断路更是验证 DDR 控制器的时序和延迟参数是否配置合理。3. 软件操作与读写代码实现3.1 裸机读写、文件系统还是专用 Flash 管理方案怎么取舍在嵌入式设备里数据存储的实现方式通常可以分三个层次用一张表说明实现方式优点缺点适用场景裸机地址命令读写简单直接、无额外开销地址管理麻烦掉电易损坏代码量小、数据结构固定的产品文件系统FATFS/LittleFS数据组织清晰、易上位机查看需要移植、有掉电保护风险需要导出文件、日志、固件升级专用 Flash 管理算法wear-leveling 掉电恢复可靠性高、寿命最大化实现复杂度高工业级、车规级、频繁写存储裸机读写适合 SD 卡里存个几百字节的配置直接定义好地址映射就行。但如果你要存日志、固件升级包、历史数据文件系统几乎成了必然选择。FATFS 是最常见的但它在小容量 SPI Flash 上写的时候要注意簇大小配置如果簇太小每写一次文件的元数据更新频繁容易加剧 Flash 擦写损耗。LittleFS 则是一个专为嵌入式设计的掉电安全文件系统它自己集成了写入后复制COW和磨损均衡在 IoT 设备上越来越多地被使用。我个人的经验是产品里只要有“断电可能发生在任意时刻”的现实就不要对 FATFS 的掉电安全性抱太高期望。FATFS 在写入过程中掉电轻则丢最后一个文件重则整个文件系统结构损坏。如果你的产品属于“随时可能被用户拔电”的类型要么给文件系统加上双分区备份要么直接上 LittleFS 这种带掉电恢复能力的方案。热词里的“hdfs读写流程”其实也折射出同样的本质——大数据场景下的 HDFS 之所以要搞 NameNode、DataNode、副本机制目的就是为了解决“分布式环境下写入中途失败导致数据不一致”的问题。巧的是嵌入式文件系统里的双份备份、日志记录、掉电恢复和 HDFS 的副本机制在思想上非常相似。3.2 STM32 HAL 库操作 SPI Flash 的完整套路从初始化到擦写读接着 2.1 的马甲代码扩展一下。在 STM32 上很多人的疑惑是“HAL 库里有现成的 SPI 读写函数吗”答案是当然有HAL_SPI_Transmit、HAL_SPI_Receive、HAL_SPI_TransmitReceive 这三个就覆盖了几乎所有阻塞式 SPI 通信。问题是Flash 的多字节数据流通常要同时“发命令、发地址、收数据”所以你要么用 HAL_SPI_Transmit 分三段发要么用 HAL_SPI_TransmitReceive 在发送读命令的同时接收数据。下面是一个标准读取函数注意读数据时主控需要同时持续发送 0xFF哑字节来产生时钟void flash_read(uint32_t addr, uint8_t *buf, uint32_t len) { uint8_t cmd[4]; cmd[0] CMD_READ_DATA; cmd[1] (addr 16) 0xFF; cmd[2] (addr 8) 0xFF; cmd[3] addr 0xFF; HAL_GPIO_WritePin(FLASH_CS_GPIO_Port, FLASH_CS_Pin, GPIO_PIN_RESET); HAL_SPI_Transmit(hspi1, cmd, 4, HAL_MAX_DELAY); // 读数据时持续发哑字节同时接收数据 uint8_t dummy 0xFF; for (uint32_t i 0; i len; i) { HAL_SPI_TransmitReceive(hspi1, dummy, buf[i], 1, HAL_MAX_DELAY); } HAL_GPIO_WritePin(FLASH_CS_GPIO_Port, FLASH_CS_Pin, GPIO_PIN_SET); }如果你用的是 GD32F450 这类国产芯片操作内部 Flash 的流程也类似只不过命令寄存器、地址寄存器、页大小这些都有差异。GD32F450 内部 Flash 一般是 4KB 扇区擦除写操作按 32 位字或 16 位半字进行操作前要解锁 FLASH_CTL 寄存器、等待 BUSY 位清零这些细节在数据手册里都有移植时不要照搬 STM32 代码。3.3 文件读写从 C 语言 fprintf 到嵌入式 FAL 抽象层热词里“c语言文件读写操作代码”“qt5 pdf文件读写和预览”“c 内存读写”这些搜索从侧面反映了大家不只是关心底层外设驱动还关心应用层的数据持久化。在 PC Linux/Windows 平台上C 语言的文件读写就是 open/read/write 或者 fopen/fread/fwrite 这套我不赘述。但嵌入式场景中文件系统的上层接口通常是 POSIX 风格或 FatFs 的 f_open/f_read/f_write 接口底层再对接一个“块设备驱动”。为了把底层介质和上层文件系统解耦很多团队会用 RT-Thread 的 FALFlash 抽象层分区管理组件。FAL 把一块物理 Flash 划分成若干个分区比如 bootloader、app、config、factory每个分区对应一个逻辑块设备文件系统只挂载在某个分区上。这样做的好处是升级固件时只改 app 分区不会误伤配置文件恢复出厂设置时只擦 config 分区不影响其他数据。这里给一个 FAL partition 表的示例配置static const struct fal_partition partition_table[] { {FAL_PART_MAGIC_WORD, bootloader, onchip_flash, 0, 64 * 1024, 0}, {FAL_PART_MAGIC_WORD, app, onchip_flash, 64 * 1024, 256 * 1024, 0}, {FAL_PART_MAGIC_WORD, config, spi_flash, 0, 512 * 1024, 0}, {FAL_PART_MAGIC_WORD, factory, spi_flash, 512 * 1024, 512 * 1024, 0}, };有了分区表上层代码里就可以直接说“我要往 config 分区存一条参数”完全不管底层是内部 Flash 还是 SPI Flash也不管地址偏移多少。这种抽象在项目后期维护时价值非常明显。3.4 少有人提但很重要的读写权限与介质寿命热词里“d盘是否有读写权限”“ubuntu读写emmc分区”“mariadb数据库读写分离”其实都指向一个共同点读写在系统层面不只是一个“调用”问题还是“权限”“并发”和“寿命”问题。工程上至少有三件事你需要提前设计而不是等到出了问题再补权限管理Linux 下访问 /dev/mtdblock 或 /dev/mmcblk0p1 需要 root 权限应用层部署时一定要确认当前用户是否有读写权限否则程序运行起来才发现 open 失败线上事故就来了。还有 FAT32 分区挂载时默认可能会有 umask 限制导致文件写入权限不对这些小细节很容易被忽略。写放大和磨损均衡同一份数据反复写到同一个 Flash 地址时间长了该扇区就会“写穿”。专门做数据记录的产品如果不做 Wear Leveling可能用不到一年存储就废了。实现磨损均衡最简单的方式是“日志结构写入”每次写新数据都写到下一个空白区域并把最新的有效数据索引记录在一个固定的小区域里。复杂但更通用的方式是用算法把逻辑地址映射到物理扇区类似 SSD 的 FTL 做的事。读写分离思想热词里的“mariadb数据库读写分离”虽然是大数据/Web 领域的术语但嵌入式里也有类似思路——把“频繁读”的数据放在内存/RAM 缓存里把“需要持久化”的数据批量落盘减少对 Flash 的写次数既提高了响应速度又延长了 Flash 寿命。很多设备用掉电检测电路配合 RAM 缓存断电瞬间把关键数据回写到 EEPROM就是这个思路的经典实现。4. 调试环境搭建与读写速度优化4.1 没有逻辑分析仪别跟我说你能调好 I2C/SPI我见过太多人调 SPI 靠“全速运行串口打印”这种做法的效率低到让人抓狂。SPI/I2C 这种高速串行协议波形细节转瞬即逝你根本不知道是哪一拍时序没对齐。专业的做法是把逻辑分析仪接到 CLK/MOSI/MISO/CS 或 SCL/SDA 上用 20MHz 以上采样率抓一整段通信波形然后对照规格书的时序图一帧一帧核对。对于 I2C 这种多主机协议还要重点检查起始/停止条件、ACK 位方向、SDA 在 SCL 高电平期间是否稳定。有一类非常隐蔽的问题是多主机冲突导致的 SDA 被拉死——总线上有一个设备在 SCL 高电平期间改变了 SDA另一个设备还在等 ACK结果整个总线卡死。这种问题在逻辑分析仪上能看到 SDA 一直低电平但如果你没有抓波形可能就会一直怀疑是 GPIO 配置错了。逻辑分析仪选型上几十块钱的 USB 逻辑分析仪比如基于 Saleae 克隆方案配 PulseView 完全够用了关键是采样率要够。SPI 跑 36MHz 时至少选 100MS/s 的设备如果只调 I2C 100kHz那 24MS/s 的也够。我自己常备两台一台 8 通道 400MS/s 的抓高速信号一台便宜货调试低速总线分工明确。4.2 读写速度怎么算又怎么优化很多项目做到后期发现瓶颈在存储读写速度。比如设备启动时要读取 512KB 的字库如果用 W25Q128 的标准 SPI 模式时钟 20MHz单 bit 传输率是 20Mbit/s实际读取 512KB 大约需要 512*8/20 204.8ms忽略命令开销。这时间放在用户体验上可能就让人觉得“卡”。优化方向主要有四个提高 SPI 时钟频率W25Q128 支持最高 104MHz标准 SPI甚至更高QSPI 模式如果你用的是 STM32F103 这类主控SPI1 最高 18MHzAPB2 36MHz 分频SPI2 最高 9MHz芯片本身支持再高也没有意义。换更高主频的 MCU 或 FPGA 才能吃满 Flash 的时钟上限。开启 QSPI 四线模式这是性价比最高的提速手段。标准 SPI 是 1 bit 收/发QSPI 用 4 根数据线理论上同等时钟下速度提升 4 倍。STM32F4 系列内置 QSPI 外设可以直接映射内存空间读 Flash实现“内存映射读取”代码里甚至可以像访问数组一样读 Flash 内容不需要搬数据到 SRAM。很多 UI 方案的图片字库就是这么加速的。开启 D-Cache 或 DMACPU 参与每次字节搬运会浪费大量时间。用 DMA 把数据从 SPI 外设搬运到内存CPU 可以趁这段时间做别的运算。对于 FPGA 工程师AXI DMA 配合 DDR 缓存是常见架构对于 STM32用 HAL_SPI_Transmit_DMA 或者 HAL_SPI_Receive_DMA 就能把事情办了。减少不必要的擦除Flash 擦除是耗时大户擦一个 4KB 扇区通常要 100ms 甚至更久。当你只想更新扇区内某几个字节时先读出整个扇区到 RAM修改后再整扇区擦除回写比直接擦整个扇区再写要快得多也能避免频繁擦写缩短 Flash 寿命。下面是一个实测数据对比我在同一块板子上用不同方式读 1MB 的 Flash 数据给大家一个感性认识SPI 时钟均为 40MHz读取方式耗时备注阻塞式逐字节 HAL_SPI_TransmitReceive~420msCPU 全程等待效率最低阻塞式 HAL_SPI_TransmitReceive 但数据长度 64 字节一批~280ms减少函数调用和 CS 翻转开销DMA 批量读取 256 字节一批~210ms接近理论极限QSPI 内存映射读取~100ms仅需读命令地址之后按内存读4.3 热词中的“硬盘读写速度怎么看”Ubuntu 下的存储测速思路形如“硬盘读写速度怎么看 ubuntu”“raid阵列读写速度”“emmc读写烧录工具下载”这些搜索说明大家在线上一旦碰到性能问题第一反应就是想测速。在 Ubuntu 下做非易失存储器测速最常见的工具是 dd 和 hdparm。用 dd 测 SSD/磁盘顺序写速度dd if/dev/zero of/mnt/testfile bs1M count1024 convfdatasync这条命令会向 /mnt/testfile 写入 1GB 数据最后一条 fdatasync 确保数据真正落盘而不是只进 page cache。测读速度echo 3 /proc/sys/vm/drop_caches dd if/mnt/testfile of/dev/null bs1M count1024drop_caches 的作用是清空页缓存避免读到的数据其实都缓存在内存里导致测出的速度“虚高”。测完记得把这个文件删掉别给系统留个 1GB 的垃圾。hdparm 则是更底层地测磁盘缓存读速度和设备读速度hdparm -T /dev/sda # 缓存读速度 hdparm -t /dev/sda # 设备缓冲读速度对 eMMC/SD 卡这类嵌入式存储设备我建议也跑一下 fio 这种专业工具做随机读写测试因为顺序测速表现好不代表随机 4K 读写性能好而 4K 随机性能恰恰决定了数据库、小文件日志这类真实场景的体验。这些方法不只是 PC/服务器用得上你拿到一块新 eMMC 芯片做 Linux 系统盘前先在读卡器上快测一下能提前避免不少坑。5. 可靠性设计数据写丢了才叫大事5.1 掉电保护怎么做三份备份、启动检查、数据校验写 Flash/EEPROM 时最怕的就是掉电。MCU 写入一个扇区通常需要先擦除、再写入时间窗口很长如果在这期间掉电Flash 里可能剩下一个“擦了一半、写了一半”的中间态下次上电读出来就是乱码。工业产品的正确姿势是“事务”思想用三份备份或者至少双份备份来保证数据可恢复。具体实现上我会把配置数据在 Flash 里划分出两个或三个相同大小的区域每次写入时遵循以下流程在区域 A 写入新数据写完校验和字段。校验通过后在区域 B 写入同样的数据。更新“当前有效区域”的标志位。下次上电时启动代码先检查哪个区域的校验和有效就加载哪个区域。如果发现区域 A 校验失败但区域 B 有效就把区域 B 覆盖到区域 A实现自动恢复。如果两个区域都不对才进入出厂默认值。这样做虽然牺牲了一倍存储空间但换来的是极高的数据可靠性。另外数据记录型产品建议在每条记录前加上类型、长度、CRC32 这类元信息启动时扫描一遍发现 CRC 不对就直接跳过并修复。我之前做过的一个环境监测终端就是按这条思路设计的两年多现场运行没有因为掉电损坏出现过一次数据丢失。5.2 ECC、坏块管理与 Flash 寿命的博弈NAND Flash 在出厂时就有坏块使用过程中还会不断产生新坏块。如果你不做坏块管理用着用着数据就可能写到坏块上然后再也读不出来。SD 卡和 eMMC 主控内部都集成了 FTL 和 ECC用户无感但如果你直接买 NAND 颗粒裸片用那就得自己实现坏块表和 ECC 校验。ECC 的常见实现是 BCH 或 RS 码。基于 BCH 的 ECC 在软件层实现时写数据时计算出校验字节存放在每个页的 OOBOut of Band区域读数据时再重新计算并与 OOB 中的校验值比对能纠正一定数量的 bit 翻转。对 SLC NAND一般要求 1bit/512B 或 4bit/512B 的纠错能力MLC 通常需要更强的 24bit/1KB 纠错能力。寿命博弈就更直观了同样是 10 万次擦写寿命的 NOR Flash如果每天写 50 次理论能用 2000 天约 5.5 年。但如果你做的产品要求 10 年寿命那就必须引入磨损均衡。最简陋的磨损均衡是把“当前写指针”记录在 Flash 里每写一条记录就往后挪一个扇区写完一圈后再从头上擦除重写保证每个扇区被抹除的次数尽量均匀。这种方法实现简单对顺序日志型数据足够用但对随机地址覆盖写就得上更复杂的哈希映射表甚至开源 FTL 方案。5.3 热词“i2c读写eeprom代码 verilog”和“基于fpga的spi读写flash”里的 FPGA 可靠性陷阱FPGA 工程师读写存储器的可靠性坑跟 MCU 工程师不太一样。FPGA 最常犯的错有两个第一个是跨时钟域处理不到位。比如你设计 DDR 控制器时用户逻辑时钟是 100MHzDDR 控制器时钟是 200MHz接口信号没有做两级同步或 FIFO 缓冲就会偶发性地读到错误数据。这种问题非常难复现上线后偶尔跑一天错一次排查起来简直要命。我个人的习惯是在所有跨时钟域数据通路上都加异步 FIFOXilinx 的 FIFO IP 或自己写并且地址/控制信号一律打两拍同步。第二个是复位顺序不对。很多 FPGA 板子用的是外部复位芯片上电瞬间 Flash/DDR 控制器复位还没释放就开始发命令结果初始化失败。FPGA 里所有对存储器的访问模块都要在复位释放后额外等待至少几百微秒再启动初始化有些 DDR 颗粒规格书明确要求上电后 CKE 保持低电平至少 200us。你可以在状态机里加一个计数器先数到固定计数值再进入正常流程。这种“多等一拍”的小习惯能帮你避开大量诡异的上电偶发故障。6. 快速定位问题的调试工具与常见故障清单日常开发中我把非易失存储器读写问题分成四类驱动类、时序类、可靠性类、性能类。每一类的排查思路完全不同这里整理成速查表也是我这些年踩坑的经验浓缩症状可能原因排查手段写入后读回全是 0xFF没发写使能、供电不稳、CS 时序错误示波器看供电、逻辑分析仪抓命令帧数据只能写一次第二次开始异常Flash 未擦除就写入、跨页写入覆盖了旧数据读整扇区内容检查确认擦除流程I2C 总线卡死SCL/SDA 一直低从机把总线拉死、主从时序不匹配看波形找哪个阶段拉死必要时复位总线掉电后再上电数据部分乱码写中途掉电数据校验不足增加双备份、CRC、启动自检读速度远低于理论值SPI 时钟没拉满、逐字节调用开销大改用 DMA/QSPI/内存映射传输偶尔出错且只在高温时明显信号完整性、电源纹波、布线过长降速测试、改差分/加端接、查电源文件系统挂载失败 / 簇损坏文件系统元数据写入被掉电打断换 LittleFS或做双分区备份FPGA 偶发读到旧数据跨时钟域未同步、DDR 训练不完全加 FIFO、重跑训练、看训练结果寄存器这里单独强调一下“供电不稳”有多隐蔽。很多设备上电瞬间主控和 Flash 是同时上电的如果电源斜率过慢或者有毛刺Flash 内部可能处于“未完全初始化但又能响应部分命令”的状态。此时如果你意外发了一个写命令并等待 BUSY它会一直不完成最后导致看门狗复位。针对这个问题工业设备里常用复位监控芯片如 TPS3823、MAX809给 Flash 提供一个受控的上电时序确保 VCC 稳定到规格书要求的电压后再允许主控操作存储器件。另一个很实用的小技巧调试这类读写问题先在代码里把 SPI/I2C 时钟降到最低速比如 SPI 1MHz、I2C 100kHz。如果低速下一切正常高速下出错那就是信号完整性问题如果低速下也出错那大概率是协议逻辑本身有问题。这种“降速二分法”在开发初期能帮你迅速缩小问题范围比纠结示波器上测量具体时序快得多。最后再分享一点我个人的体会做非易失存储器读写这么多年最深的感受是这个领域“入门容易精通极难”。写个简单驱动让 MCU 能往 Flash 里存数据一天就能跑通但要做到“产品在恶劣环境里跑十年数据不丢”却需要你在介质选型、时序设计、文件系统、掉电保护、磨损均衡、信号完整性这些方面都下足功夫。我的建议是哪怕是做小项目也要从第一天起就按产品化的标准要求自己每次读写都做校验、每次上电都做数据完整性检查、每次设计都留出冗余空间。因为这些习惯在项目初期看起来是“多花时间”但在产品出问题的那一刻你会发现它们替你省下的可不止几个通宵还有产品口碑和客户的信任。如果这篇文章里的某个思路帮你少踩了一个坑那我觉得就很值了。后面有空我再单独写一篇关于 QSPI 内存映射和 LittleFS 掉电保护底层实现的文章到时候见。