news 2026/9/28 1:26:26

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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,也就是 CPOL=0、CPHA=0,时钟空闲为低电平,第一个边沿采样。波特率预分频我设的是 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 的格式是这样的:第一个字节是厂商 ID,MT25QL256 的厂商是 Micron,ID 是 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.3V,GND 是不是共地,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 时序。这个测试我一般在新板子第一次调试的时候做,能快速判断硬件有没有问题。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 1:26:17

超声乳腺图像分割实战:从数据集预处理到U-Net训练全流程

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/28 1:26:16

Vim实战:用编辑器高效处理图像分类任务全流程

简介&#xff1a;本资源面向计算机视觉方向的开发者与研究者&#xff0c;提供使用Vim视觉模型完成图像分类任务的完整工程包。Vim凭借计算与内存效率高、处理高分辨率图像能力强的特点&#xff0c;被视为下一代视觉基础模型的理想选择&#xff0c;适合希望复现轻量级视觉骨干网…

作者头像 李华
网站建设 2026/9/28 1:26:16

SC7A20三轴加速度计I2C驱动开发实战:从时序到滤波

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/28 1:26:13

计算机408中断系统详解:核心部件、响应流程与多重中断屏蔽字

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/28 1:24:55

YOLOv5知识蒸馏实战:从教师模型到损失函数设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/28 1:24:37

SSA-Transformer-GRU 负荷预测实战:Matlab 超参数自动寻优与避坑指南

简介&#xff1a;本资源面向计算机、电子信息工程、数学等专业的大学生及科研人员&#xff0c;提供一套基于Matlab实现的麻雀搜索优化算法SSA-Transformer-GRU负荷数据回归预测完整方案&#xff0c;适用于课程设计、期末大作业与毕业设计等场景。压缩包共16个文件&#xff0c;约…

作者头像 李华