ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

MT25QL256 SPI NOR FLASH驱动实战:从ID读取到4KB扇区擦除

2026/9/28 1:26:26 拓冰建站 浏览量
MT25QL256 SPI NOR FLASH驱动实战:从ID读取到4KB扇区擦除 1. 项目缘起与整体设计思路MT25QL256 这颗料做嵌入式存储的兄弟应该都不陌生。256Mbit 容量、SPI 接口、支持 4KB 扇区擦除在工业控制、物联网终端、车载记录仪这些场景里出镜率极高。我最近在做一个基于华大 MCU 的数据采集项目板子上挂的就是这颗 NOR FLASH用来存配置参数和历史记录。调试过程中踩了不少坑从 ID 读取异常到扇区擦除超时几乎把能遇到的问题都遇了一遍。这篇文章就把整个操作流程从头到尾捋一遍把踩过的坑和验证过的代码都摊开来讲。先说清楚这个项目要解决什么问题。MT25QL256 是一颗 256Mbit也就是 32MB的 SPI NOR FLASH支持标准 SPI、Dual SPI、Quad SPI 三种模式。我们用的是标准 SPI 模式因为华大这颗 MCU 的 SPI 外设配置起来相对简单而且数据采集场景对读写速度要求没那么极致。核心需求就三个第一能正确读到芯片 ID确认通信链路没问题第二能往指定地址写数据第三能按 4KB 扇区擦除这是 NOR FLASH 最基本的操作单元。为什么选 NOR FLASH 而不是 NAND这个问题我在选型阶段纠结过。NOR 的特点是随机读取快、支持字节级编程、可靠性高缺点是容量做不大、写入速度慢。NAND 容量大、写入快但需要坏块管理读取延迟也高。我们的场景是频繁读配置、偶尔写记录数据量不大但对可靠性要求高NOR 显然是更合适的选择。MT25QL256 的 4KB 扇区擦除粒度也是我看中的点比那些 64KB 块擦除的芯片灵活太多存小数据不用浪费空间。整体设计上我采用的是“命令 地址 数据”三段式操作框架。SPI NOR FLASH 的所有操作都遵循这个模式先拉低片选发送操作码然后根据命令类型决定是否发送地址和数据最后拉高片选结束。这个框架理解了后面所有操作都是套模板。华大的 SPI 外设用起来还算顺手但有几个寄存器配置的细节容易翻车后面会详细说。提示SPI NOR FLASH 的操作时序对片选信号非常敏感片选拉低到第一个时钟沿之间需要足够的建立时间拉高后也需要保持时间。华大 MCU 的 SPI 外设可以通过配置寄存器调整这些时序参数别用默认值。2. 硬件连接与SPI外设配置要点2.1 引脚连接与硬件检查清单MT25QL256 的引脚不多但每一根都不能接错。标准 SPI 模式下用到的是 CS、CLK、MOSI、MISO 四根线加上 VCC 和 GND。如果是 Quad 模式还要接 WP 和 HOLD但我们用标准模式这两根可以上拉或悬空。实际接线的时候有几个地方容易出问题我列个清单你对照检查。CS 片选信号接 MCU 的任意 GPIO 或 SPI 外设的 NSS 引脚。我建议用普通 GPIO 软件控制因为 NOR FLASH 的操作需要精确控制片选时序硬件 NSS 有时候会插入不必要的延迟。CLK 时钟接 SPI 外设的 SCK 引脚。注意走线尽量短时钟频率高的时候长走线会引入反射。MOSI 和 MISO分别接 MCU 的 MOSI 和 MISO。这两根线别接反接反了读 ID 会返回 0x00 或 0xFF。VCC 供电MT25QL256 是 2.7V 到 3.6V 供电典型值 3.3V。别用 5V会烧。GND 接地必须共地否则通信不稳定。WP 和 HOLD标准 SPI 模式下这两个引脚要上拉到 VCC否则芯片可能进入保护状态。我第一次调试的时候就是 WP 引脚忘了上拉结果写操作一直失败读 ID 却正常。这个坑很隐蔽因为读操作不受 WP 影响只有写和擦除才会被阻止。后来用万用表量了一下 WP 引脚电压发现是悬空的补了个 10K 上拉电阻就好了。2.2 华大SPI外设初始化配置华大 MCU 的 SPI 外设配置有几个关键寄存器需要设置。我以 HC32F460 为例其他型号的华大 MCU 逻辑类似寄存器名字可能不同。初始化流程分四步使能时钟、配置 GPIO 复用、配置 SPI 参数、使能 SPI。// 使能 SPI 外设时钟 SET_BIT(RCC-APB2EN, RCC_APB2EN_SPI1EN); // 配置 GPIO 复用功能 GPIO_SetFunc(GPIO_PORT_A, GPIO_PIN_5, GPIO_FUNC_1); // SCK GPIO_SetFunc(GPIO_PORT_A, GPIO_PIN_6, GPIO_FUNC_1); // MISO GPIO_SetFunc(GPIO_PORT_A, GPIO_PIN_7, GPIO_FUNC_1); // MOSI // CS 用普通 GPIO 控制 GPIO_SetFunc(GPIO_PORT_A, GPIO_PIN_4, GPIO_FUNC_0); GPIO_Init(GPIO_PORT_A, GPIO_PIN_4, GPIO_MODE_OUT_PP); // 配置 SPI 参数 SPI_InitTypeDef spiInit; spiInit.u32Mode SPI_MODE_MASTER; spiInit.u32DataWidth SPI_DATA_WIDTH_8BIT; spiInit.u32ClkPolarity SPI_CLK_POLARITY_LOW; spiInit.u32ClkPhase SPI_CLK_PHASE_1EDGE; spiInit.u32BaudRatePrescaler SPI_BAUDRATE_PRESCALER_8; spiInit.u32FirstBit SPI_FIRST_BIT_MSB; SPI_Init(SPI_UNIT_1, spiInit); // 使能 SPI SPI_Enable(SPI_UNIT_1);这里有几个参数需要解释一下。时钟极性 CPOL 和时钟相位 CPHA 决定了 SPI 的采样时刻。MT25QL256 支持模式 0 和模式 3我用的是模式 0也就是 CPOL0、CPHA0时钟空闲为低电平第一个边沿采样。波特率预分频我设的是 8华大 HC32F460 的 SPI 时钟源是 100MHz分频后是 12.5MHz。这个频率对 MT25QL256 来说不算高芯片最高支持 133MHz但调试阶段先用低速保证稳定性后面再往上提。数据位宽必须是 8 位因为 NOR FLASH 的所有命令和地址都是按字节传输的。MSB 优先也是必须的SPI NOR FLASH 的标准就是高位先出。这些参数看着简单但配错一个就读不到 ID。注意华大 MCU 的 SPI 外设在使能之前必须先配置好所有参数使能之后再改配置可能不生效。我遇到过使能后改波特率没反应的情况后来发现是寄存器有写保护需要先关闭 SPI 再改。2.3 片选信号的控制逻辑片选信号的控制是 SPI NOR FLASH 操作里最容易被忽视的环节。很多人以为片选就是操作前拉低、操作后拉高但实际上时序要求比这精细得多。MT25QL256 的数据手册里明确写了片选拉低到第一个时钟沿之间至少需要 5ns 的建立时间最后一个时钟沿到片选拉高之间至少需要 5ns 的保持时间。华大 MCU 跑在 100MHz 的时候一个时钟周期是 10ns所以软件控制 GPIO 翻转的延迟本身就够满足这个要求。但如果你把 SPI 时钟配到 50MHz 以上一个周期只有 20ns软件翻转的延迟就可能不够了。这时候要么降低 SPI 速度要么用硬件 NSS 配合 DMA 传输。我实测下来用软件控制片选在 12.5MHz 下非常稳定连续读写几万次没有出现数据错误。但如果你要跑高速建议还是用硬件 NSS。另外片选信号在两次操作之间必须拉高不能一直保持低电平否则芯片会认为你还在传输导致命令解析错误。// 片选控制宏定义 #define FLASH_CS_LOW() GPIO_ResetPins(GPIO_PORT_A, GPIO_PIN_4) #define FLASH_CS_HIGH() GPIO_SetPins(GPIO_PORT_A, GPIO_PIN_4) // 单字节传输函数 uint8_t SPI_TransferByte(uint8_t data) { while (SPI_GetFlag(SPI_UNIT_1, SPI_FLAG_TXE) 0); SPI_SendData(SPI_UNIT_1, data); while (SPI_GetFlag(SPI_UNIT_1, SPI_FLAG_RXNE) 0); return SPI_ReceiveData(SPI_UNIT_1); }这个SPI_TransferByte函数是后面所有操作的基础。注意发送和接收是同时进行的SPI 是全双工协议你发一个字节出去的同时也会收到一个字节。读 ID 的时候就是发命令字节然后发空字节把数据挤出来。3. ID读取与4KB扇区擦除实操3.1 读取MT25QL256芯片ID的完整流程读 ID 是验证通信链路的第一步也是最关键的一步。MT25QL256 支持好几个读 ID 的命令常用的有 0x9E读电子签名、0x9F读 JEDEC ID、0xAB读设备 ID。我一般先用 0x9F 读 JEDEC ID因为这个命令返回三个字节能同时看到厂商 ID 和容量信息。JEDEC ID 的格式是这样的第一个字节是厂商 IDMT25QL256 的厂商是 MicronID 是 0x20第二个字节是存储类型NOR FLASH 是 0xBA第三个字节是容量256Mbit 对应 0x19。所以正常读出来应该是 0x20 0xBA 0x19。如果你读出来是 0x00 0x00 0x00 或者 0xFF 0xFF 0xFF那说明通信有问题。uint32_t MT25QL256_ReadID(void) { uint32_t id 0; uint8_t temp[3]; FLASH_CS_LOW(); SPI_TransferByte(0x9F); // JEDEC ID 命令 temp[0] SPI_TransferByte(0xFF); // 厂商 ID temp[1] SPI_TransferByte(0xFF); // 存储类型 temp[2] SPI_TransferByte(0xFF); // 容量 FLASH_CS_HIGH(); id (temp[0] 16) | (temp[1] 8) | temp[2]; return id; }这段代码看着简单但有几个细节要注意。发送 0x9F 之后要发送三个 0xFF 空字节来产生时钟把芯片返回的数据挤出来。为什么是 0xFF 而不是 0x00其实发什么都行因为 MOSI 上的数据芯片不关心它只负责在 MISO 上输出 ID。但发 0xFF 有个好处就是如果 MISO 线断了或者芯片没响应读回来的是 0xFF你能立刻判断出是通信问题而不是数据问题。我调试的时候遇到过一个诡异的情况读 ID 返回 0x20 0xBA 0x19看起来完全正常但写数据就是失败。后来查了半天发现是写使能命令没发。NOR FLASH 在每次写或擦除之前都必须先发送写使能命令 0x06否则操作会被忽略。这个机制是 NOR FLASH 的标准设计但新手很容易忘。提示读 ID 正常不代表写操作正常。读操作不需要写使能但写和擦除必须。如果你读 ID 没问题但写数据失败第一个要检查的就是写使能命令有没有发。3.2 写使能与状态寄存器轮询写使能命令 0x06 是每次写或擦除操作的前置动作。发送完 0x06 之后芯片内部的写使能锁存器 WEL 会被置位状态寄存器的 bit1 会变成 1。你可以通过读状态寄存器命令 0x05 来确认 WEL 是否置位。void MT25QL256_WriteEnable(void) { FLASH_CS_LOW(); SPI_TransferByte(0x06); FLASH_CS_HIGH(); } uint8_t MT25QL256_ReadStatus(void) { uint8_t status; FLASH_CS_LOW(); SPI_TransferByte(0x05); status SPI_TransferByte(0xFF); FLASH_CS_HIGH(); return status; } void MT25QL256_WaitForWriteEnd(void) { uint8_t status; do { status MT25QL256_ReadStatus(); } while (status 0x01); // 等待 BUSY 位清零 }状态寄存器的 bit0 是 BUSY 位写或擦除操作进行中会置 1完成后自动清零。bit1 是 WEL 位写使能后置 1操作完成后自动清零。bit2 到 bit6 是块保护位默认情况下都是 0表示没有保护。如果你发现写操作一直失败读一下状态寄存器看看 bit1 是不是 0如果是 0 说明写使能没生效。写使能没生效的常见原因有两个一是片选时序不对命令没被正确识别二是 WP 引脚被拉低硬件写保护生效了。我前面提到的 WP 悬空问题就是第二种情况。用万用表量一下 WP 引脚电压正常应该是 3.3V。3.3 4KB扇区擦除的完整实现4KB 扇区擦除是 MT25QL256 最常用的擦除粒度。命令是 0x20后面跟 24 位地址。MT25QL256 的地址范围是 0x000000 到 0x1FFFFFF总共 32MB。擦除操作会把指定地址所在的整个 4KB 扇区全部变成 0xFF。void MT25QL256_SectorErase(uint32_t addr) { MT25QL256_WriteEnable(); MT25QL256_WaitForWriteEnd(); FLASH_CS_LOW(); SPI_TransferByte(0x20); // 4KB 扇区擦除命令 SPI_TransferByte((addr 16) 0xFF); // 地址高 8 位 SPI_TransferByte((addr 8) 0xFF); // 地址中 8 位 SPI_TransferByte(addr 0xFF); // 地址低 8 位 FLASH_CS_HIGH(); MT25QL256_WaitForWriteEnd(); }地址是 24 位的分三个字节发送高位在前。这里有个容易搞错的地方地址要对齐到 4KB 边界。如果你传的地址是 0x000801芯片会自动擦除 0x000800 到 0x000FFF 这个扇区而不是从 0x000801 开始擦。所以传地址之前最好先做个对齐处理addr 0xFFFFF000就能把低 12 位清零。擦除操作的时间比较长典型值是 45ms最大可能到 400ms。所以WaitForWriteEnd函数里的轮询不能太快否则会占用太多 CPU 时间。我一般加个延时每次轮询间隔 1ms 左右。如果你用的是 RTOS可以在轮询的时候让出 CPU 给其他任务。void MT25QL256_WaitForWriteEndWithDelay(void) { uint8_t status; do { status MT25QL256_ReadStatus(); if (status 0x01) { Delay_ms(1); // 等待 1ms 再查 } } while (status 0x01); }擦除完成后指定地址的数据应该全是 0xFF。你可以用读数据命令 0x03 来验证。读命令的格式是发 0x03然后发 24 位地址接着连续读数据时钟不停数据就一直出。void MT25QL256_ReadData(uint32_t addr, uint8_t *buffer, uint32_t len) { FLASH_CS_LOW(); SPI_TransferByte(0x03); SPI_TransferByte((addr 16) 0xFF); SPI_TransferByte((addr 8) 0xFF); SPI_TransferByte(addr 0xFF); for (uint32_t i 0; i len; i) { buffer[i] SPI_TransferByte(0xFF); } FLASH_CS_HIGH(); }3.4 页编程写入数据NOR FLASH 的写入操作叫页编程命令是 0x02。MT25QL256 的页大小是 256 字节也就是说一次最多写 256 字节而且不能跨页。如果你要写的数据超过 256 字节或者跨越了页边界必须分多次写。void MT25QL256_PageProgram(uint32_t addr, uint8_t *data, uint16_t len) { if (len 256) len 256; // 限制单次最多 256 字节 MT25QL256_WriteEnable(); MT25QL256_WaitForWriteEnd(); FLASH_CS_LOW(); SPI_TransferByte(0x02); SPI_TransferByte((addr 16) 0xFF); SPI_TransferByte((addr 8) 0xFF); SPI_TransferByte(addr 0xFF); for (uint16_t i 0; i len; i) { SPI_TransferByte(data[i]); } FLASH_CS_HIGH(); MT25QL256_WaitForWriteEnd(); }页编程的时间比擦除短很多典型值 0.7ms最大 5ms。但同样需要等待 BUSY 位清零才能进行下一次操作。写入之前必须确保目标地址已经被擦除过因为 NOR FLASH 只能把 1 变成 0不能把 0 变成 1。如果你往一个没擦除的地址写数据结果会是新旧数据的按位与而不是覆盖。这个特性是 NOR FLASH 和 EEPROM 最大的区别。EEPROM 可以字节级覆盖写NOR FLASH 必须先擦后写。所以你的数据管理逻辑里每次写之前都要判断目标扇区是否需要擦除。我的做法是维护一个扇区状态表记录每个扇区的使用情况写之前先查表。注意跨页写入是新手最容易犯的错误。比如你要从地址 0x0000F0 写 32 字节地址范围是 0x0000F0 到 0x00010F跨越了 0x0000FF 这个页边界。这时候必须分两次写第一次写 0x0000F0 到 0x0000FF 共 16 字节第二次写 0x000100 到 0x00010F 共 16 字节。如果一次性写 32 字节芯片会在页边界处回卷把后面的数据写到页开头导致数据错乱。4. 常见问题排查与实战避坑指南4.1 ID读取异常排查思路读 ID 返回异常是最常见的问题排查思路可以按以下顺序来。先确认硬件连接用万用表量 VCC 是不是 3.3VGND 是不是共地CS、CLK、MOSI、MISO 四根线有没有接反或虚焊。然后确认 SPI 配置CPOL 和 CPHA 是不是设对了波特率是不是太高了。最后确认软件逻辑片选时序对不对命令字节有没有发错。我整理了一个排查速查表按现象分类现象可能原因排查方法返回 0x00 0x00 0x00MISO 线没接或短路到地量 MISO 对地电阻返回 0xFF 0xFF 0xFFMISO 线悬空或芯片没供电量 VCC 和 MISO 电压返回 0x20 0xBA 0x19 但写失败WP 引脚悬空或写使能没发量 WP 电压检查写使能代码返回随机值SPI 时序不对或波特率太高降低波特率检查 CPOL/CPHA返回 0x20 0xBA 0x18读到了同系列小容量型号确认芯片型号是否正确这个表是我调试过程中总结的基本上覆盖了 90% 的 ID 读取问题。其中返回 0x20 0xBA 0x18 这个情况比较特殊0x18 对应的是 128Mbit 容量说明你读到的可能是 MT25QL128 而不是 MT25QL256。这两个芯片引脚兼容但容量差一倍采购的时候要注意别拿错料。4.2 擦除失败与超时处理擦除失败的表现是数据擦不干净读回来不是 0xFF。原因可能有几个写使能没生效、地址没对齐、芯片处于保护状态、擦除过程中断电了。写使能的问题前面说过了这里重点说地址对齐和保护状态。地址对齐的问题我踩过一次坑。当时我要擦除地址 0x001234 开始的扇区以为芯片会从 0x001234 擦到 0x002233结果发现 0x001000 到 0x001233 的数据也被擦了。后来查手册才知道4KB 扇区擦除命令会擦除包含指定地址的整个扇区扇区边界是 4KB 对齐的。所以 0x001234 落在 0x001000 到 0x001FFF 这个扇区里整个扇区都会被擦除。保护状态的问题更隐蔽。MT25QL256 的状态寄存器里有块保护位可以通过写状态寄存器命令 0x01 来设置。如果块保护位被置位了对应区域的写和擦除都会被拒绝。出厂默认是没有保护的但如果你之前误操作写了状态寄存器就可能把保护打开了。排查方法是读状态寄存器看看 bit2 到 bit6 是不是有置位的。void MT25QL256_CheckProtection(void) { uint8_t status MT25QL256_ReadStatus(); if (status 0x7C) { // bit2 到 bit6 printf(芯片处于保护状态状态寄存器值0x%02X\n, status); // 解除保护写状态寄存器清零保护位 MT25QL256_WriteEnable(); FLASH_CS_LOW(); SPI_TransferByte(0x01); // 写状态寄存器命令 SPI_TransferByte(0x00); // 清零所有保护位 FLASH_CS_HIGH(); MT25QL256_WaitForWriteEnd(); } }擦除超时的问题一般是轮询逻辑写错了。BUSY 位在擦除完成后会自动清零但如果你读状态寄存器的频率太高可能会读到还没更新的值。我建议每次轮询之间加 1ms 延时这样既能及时检测到完成又不会占用太多 CPU。另外擦除时间跟芯片的磨损程度有关旧芯片的擦除时间可能会比手册标称值长所以超时判断要留足余量我一般设 1 秒超时。4.3 数据写入后读出来不对写进去的数据读出来不一样这个问题比擦除失败更让人头疼因为原因更多。我遇到过的情况有跨页写入导致数据回卷、写之前没擦除导致按位与、写使能没发导致写入被忽略、SPI 速率太高导致数据位错位。跨页写入的问题前面说过了这里重点说按位与的问题。假设某个地址原来的数据是 0x0F你写 0xF0 进去读出来会是 0x00因为 0x0F 0xF0 0x00。如果你写 0xFF 进去读出来是 0x0F因为 0x0F 0xFF 0x0F。所以写入之前必须确保目标区域是 0xFF 状态也就是擦除过的状态。SPI 速率太高导致的数据位错位比较难排查因为读 ID 可能正常但写数据就是不对。原因是写操作对时序更敏感高速下建立时间和保持时间不够。我的建议是调试阶段把 SPI 速率降到 1MHz 以下等所有功能都验证通过了再逐步提高。MT25QL256 在 3.3V 供电下最高支持 133MHz但实际能跑多高取决于你的 PCB 走线和 MCU 的 SPI 外设能力。提示如果你怀疑是 SPI 速率问题可以做一个简单的压力测试往同一个地址反复写不同的数据模式0xAA、0x55、0xFF、0x00每次写完读回来对比。如果低速下没问题高速下出错那就是时序问题。4.4 华大SPI外设的独家避坑经验华大的 SPI 外设跟 STM32 的用法很像但有几个细节不一样我踩过坑之后总结了几条。第一华大 MCU 的 SPI 发送和接收 FIFO 是分开的发送的时候要等 TXE 标志接收的时候要等 RXNE 标志不能只等一个。第二华大 SPI 的片选如果配成硬件 NSS 模式在连续传输之间会自动拉高这个行为跟某些 NOR FLASH 的期望不符所以建议用软件 GPIO 控制片选。第三华大 MCU 的 SPI 时钟源默认是 PCLK如果你改了系统时钟SPI 速率也会跟着变调试的时候要注意。还有一个比较隐蔽的问题华大 MCU 的 SPI 在主机模式下如果 MISO 引脚没有外部上拉空闲时可能会读到随机值。虽然 NOR FLASH 在片选拉高时 MISO 会进入高阻态但 PCB 上的干扰可能让 MISO 电平不确定。我的做法是在 MISO 上加一个 10K 上拉电阻到 3.3V这样空闲时 MISO 稳定在高电平不会产生误触发。// 华大 SPI 全双工传输的完整实现 uint8_t HC32_SPI_Transfer(uint8_t data) { uint32_t timeout 0xFFFF; // 等待发送缓冲区空 while (SPI_GetFlag(SPI_UNIT_1, SPI_FLAG_TXE) 0) { if (--timeout 0) return 0xFF; } SPI_SendData(SPI_UNIT_1, data); timeout 0xFFFF; // 等待接收缓冲区非空 while (SPI_GetFlag(SPI_UNIT_1, SPI_FLAG_RXNE) 0) { if (--timeout 0) return 0xFF; } return SPI_ReceiveData(SPI_UNIT_1); }这个函数加了超时机制避免死循环。实际项目中如果 SPI 通信失败程序卡在 while 循环里是最糟糕的情况看门狗都救不回来。加超时之后即使通信异常函数也能返回上层可以做错误处理。5. 完整操作流程与代码组织5.1 从复位到读写的标准流程一个完整的 MT25QL256 操作流程应该是这样的系统上电后先做 GPIO 和 SPI 初始化然后读 ID 确认芯片在位接着检查状态寄存器确认没有保护最后才能进行读写操作。如果是第一次使用芯片建议先做一次全片擦除把所有数据清零然后再写入初始配置。全片擦除的命令是 0xC7 或 0x60两者效果一样。全片擦除的时间比较长256Mbit 的芯片典型值是 150 秒最大可能到 300 秒。所以一般只在出厂初始化的时候做一次平时用扇区擦除就够了。void MT25QL256_Init(void) { // 1. 硬件初始化 HC32_SPI_Init(); FLASH_CS_HIGH(); Delay_ms(10); // 等待芯片上电稳定 // 2. 读 ID 验证通信 uint32_t id MT25QL256_ReadID(); if (id ! 0x20BA19) { printf(MT25QL256 ID 读取失败实际值0x%06X\n, id); return; } printf(MT25QL256 检测成功\n); // 3. 检查保护状态 MT25QL256_CheckProtection(); // 4. 等待芯片就绪 MT25QL256_WaitForWriteEnd(); }这个初始化函数我用了很久在各种板子上都验证过。关键是每一步都有检查ID 不对就报错返回保护状态不对就解除保护最后等待芯片就绪。这样上层应用调用MT25QL256_Init()之后就可以放心地读写不用再关心底层状态。5.2 数据存储结构设计建议MT25QL256 有 32MB 空间怎么规划这些空间是个值得思考的问题。我的做法是分成几个区域前 1MB 存系统配置和参数中间 20MB 存历史数据最后 11MB 预留做固件升级备份。每个区域内部再按 4KB 扇区划分维护一个扇区状态表。扇区状态表我放在 MCU 的 RAM 里每个扇区用一个字节表示状态0x00 表示空闲0x01 表示已写入0x02 表示待擦除。系统启动的时候扫描一遍 FLASH根据数据内容重建状态表。这个扫描过程大概需要几秒钟但只在启动时做一次不影响运行时的性能。写入数据的时候先查状态表找到空闲扇区擦除后写入数据然后更新状态表。如果所有扇区都用完了就触发垃圾回收把有效数据搬到新扇区擦除旧扇区。这个逻辑跟文件系统的磨损均衡类似能延长 FLASH 的使用寿命。注意NOR FLASH 的擦写寿命是 10 万次左右虽然比 NAND 好很多但也不能无限次擦写。如果你的应用需要频繁写入一定要做磨损均衡否则某些扇区会被写坏。我见过一个项目因为没做磨损均衡半年就把 FLASH 写坏了。5.3 性能优化与高速读写技巧标准 SPI 模式下MT25QL256 的读取速度受限于 SPI 时钟。12.5MHz 下读 1MB 数据大概需要 0.8 秒如果换成 Quad SPI 模式时钟可以提到 50MHz 以上读取速度能快 4 倍。但 Quad 模式需要额外的引脚和更复杂的配置如果你的应用对读取速度要求不高标准 SPI 就够了。写入速度的瓶颈不在 SPI 时钟而在 FLASH 内部的编程时间。页编程典型值 0.7ms擦除典型值 45ms这些时间跟 SPI 速率无关。所以提高 SPI 速率对写入速度的提升有限主要影响的是读取速度。如果你需要频繁读取大量数据可以考虑用 DMA 传输。华大 MCU 的 SPI 支持 DMA 请求配置好 DMA 通道后SPI 传输不需要 CPU 干预能腾出 CPU 做其他事情。我用 DMA 读过 1MB 数据CPU 占用率从 100% 降到了 5% 以下。void MT25QL256_ReadDataDMA(uint32_t addr, uint8_t *buffer, uint32_t len) { FLASH_CS_LOW(); SPI_TransferByte(0x03); SPI_TransferByte((addr 16) 0xFF); SPI_TransferByte((addr 8) 0xFF); SPI_TransferByte(addr 0xFF); // 配置 DMA 接收 DMA_InitTypeDef dmaInit; dmaInit.u32SrcAddr (uint32_t)SPI_UNIT_1-DR; dmaInit.u32DstAddr (uint32_t)buffer; dmaInit.u32DataWidth DMA_DATA_WIDTH_8BIT; dmaInit.u32BlockSize len; DMA_Init(DMA_CH0, dmaInit); DMA_Enable(DMA_CH0); // 等待 DMA 完成 while (DMA_GetFlag(DMA_CH0, DMA_FLAG_TC) 0); DMA_ClearFlag(DMA_CH0, DMA_FLAG_TC); DMA_Disable(DMA_CH0); FLASH_CS_HIGH(); }DMA 传输的代码比轮询方式复杂一些但效率提升明显。注意 DMA 传输期间片选必须保持低电平传输完成后才能拉高。另外 DMA 的源地址是 SPI 数据寄存器目标地址是接收缓冲区别搞反了。5.4 实际项目中的经验总结这个项目做下来我对 MT25QL256 的理解深了不少。最大的体会是数据手册一定要仔细看很多坑手册里都写了只是你没注意到。比如 4KB 扇区擦除的地址对齐要求手册里明确写了“地址会被自动对齐到扇区边界”但我第一次看的时候直接跳过了结果调试了半天。另一个体会是调试工具很重要。我一开始只用 printf 打印调试信息效率很低。后来用逻辑分析仪抓 SPI 波形一眼就能看出时序问题。比如片选拉低到第一个时钟沿的间隔太短或者数据在时钟边沿不稳定这些用 printf 根本看不出来。如果你经常调 SPI 设备建议入手一个便宜的逻辑分析仪几百块钱能省很多时间。还有一点代码要分层。底层是 SPI 读写函数中间是 FLASH 命令封装上层是数据管理逻辑。这样分层之后换 MCU 或者换 FLASH 型号只需要改底层中间层和上层基本不用动。我后来把华大 MCU 换成另一家的 MCU只改了 SPI 初始化部分FLASH 操作代码一行没动半天就移植完了。最后分享一个验证 FLASH 好坏的小技巧写一个测试模式到 FLASH比如 0xAA、0x55 交替然后读回来对比。如果读回来的数据跟写进去的一样说明 FLASH 读写正常。如果不一样先检查是不是没擦除再检查 SPI 时序。这个测试我一般在新板子第一次调试的时候做能快速判断硬件有没有问题。