首页/新闻资讯/正文详情

华大HC32F460驱动MT25QL256 SPI NOR FLASH实战指南

发布时间:2026/9/28 1:21:22 来源:云帆数科 栏目:资讯中心
华大HC32F460驱动MT25QL256 SPI NOR FLASH实战指南
1. 拿到MT25QL256之后先搞清楚它到底是个什么东西MT25QL256这颗料在嵌入式圈子里出现的频率相当高尤其是做工业控制、数据采集、通信设备这类需要存储配置参数、日志数据、固件备份的场景。它是一颗256Mbit也就是32MB容量的NOR FLASH采用SPI接口通信支持标准SPI、Dual SPI和Quad SPI三种模式。很多人第一次接触它的时候容易把它和W25Q系列搞混虽然两者在引脚和基本操作上兼容度很高但在细节上还是有差异的后面我会具体说。先说说为什么选NOR FLASH而不是NAND FLASH或者EEPROM。NOR FLASH的特点是随机读取速度快、支持字节级寻址、可靠性高适合存储代码和关键数据。NAND FLASH容量大、成本低但读取需要按页操作不适合直接跑代码。EEPROM容量太小一般只有几十KB到几MB而且写入速度慢。MT25QL256的32MB容量对于大多数嵌入式项目来说已经绰绰有余了存个固件备份、跑个文件系统、记录运行日志都不在话下。我这次用的主控是华大半导体HDSC的MCU具体型号是HC32F460系列。华大的MCU在国产替代的大背景下用得越来越多它的SPI外设功能很完整支持DMA、支持硬件片选、支持多种时钟极性和相位配置。但说实话华大的官方例程和文档相比ST的HAL库还是有一定差距的很多细节需要自己啃datasheet和实测验证。这篇文章就把我从零开始调通MT25QL256的完整过程记录下来包括ID读取、状态寄存器操作、4KB扇区擦除、页写入、数据读取这些核心操作代码基于华大HC32F460的SPI外设编写但思路和流程对STM32、GD32、国民技术等平台同样适用。注意MT25QL256的供电范围是2.7V到3.6V典型值3.3V。如果你的MCU是1.8V或者5V系统必须加电平转换芯片否则要么读不到数据要么直接烧掉FLASH。我见过有人用5V的STM32F103直接怼上去结果FLASH的IO口当场冒烟。在开始写代码之前有几个基础概念必须先理清楚否则后面调不通的时候你会很痛苦。SPI通信的本质是一个移位寄存器环主机和从机各自有一个移位寄存器主机每发一个bit从机就收一个bit同时从机也发一个bit回来。所以SPI是全双工的你发命令的同时也在收数据。MT25QL256的所有操作都是基于这个机制的先发命令字节再发地址或数据然后在时钟的驱动下把数据移出来。SPI有四种模式由CPOL时钟极性和CPHA时钟相位组合决定。MT25QL256支持模式0CPOL0CPHA0和模式3CPOL1CPHA1。模式0的意思是时钟空闲时为低电平数据在时钟的上升沿采样模式3的意思是时钟空闲时为高电平数据在时钟的下降沿采样。我一般用模式0因为大多数MCU的SPI外设默认就是模式0配置起来最省事。但你要注意如果你的SPI总线上挂了多个设备必须确保所有设备都支持同一种模式否则就得用软件片选分开控制。2. 硬件连接与SPI外设配置的取舍2.1 引脚连接与片选方式的选择MT25QL256的封装常见的有SOP16和WSON8两种引脚定义基本一致。核心引脚就六个CS片选、CLK时钟、MOSI主机输出从机输入、MISO主机输入从机输出、VCC、GND。另外还有WP写保护和HOLD/RESET引脚这两个引脚在标准SPI模式下可以悬空或者上拉但在Quad SPI模式下会复用为数据线。我这次用的是硬件片选也就是把FLASH的CS引脚接到MCU的SPI_NSS引脚上。硬件片选的好处是时序由硬件自动控制不需要软件干预传输效率高。但硬件片选有个坑华大的SPI外设在配置为硬件片选模式时NSS引脚的电平控制是自动的但如果你在传输过程中需要拉高CS来结束命令必须确保SPI外设已经完成了所有数据的发送。我一开始就是没注意这一点导致CS拉高的时机不对FLASH收不到完整的命令读出来的ID全是0xFF。软件片选就是用普通的GPIO来控制CS引脚传输前拉低传输后拉高。这种方式灵活适合总线上挂多个SPI设备的场景但需要手动控制时序稍微麻烦一点。我的建议是如果SPI总线上只有FLASH一个设备用硬件片选如果有多个设备用软件片选每个设备分配一个独立的GPIO作为片选。引脚名称功能说明连接建议CS片选低电平有效接MCU的SPI_NSS或普通GPIOCLK时钟最高支持133MHz接MCU的SPI_SCKMOSI主机输出从机输入接MCU的SPI_MOSIMISO主机输入从机输出接MCU的SPI_MISOWP写保护低电平有效上拉到VCC或接GPIO控制HOLD保持低电平有效上拉到VCC或悬空2.2 SPI时钟配置与分频计算华大HC32F460的SPI时钟源来自PCLK我用的系统时钟是200MHzPCLK配置为100MHz。SPI外设的分频系数可以设置为2、4、8、16、32、64、128、256。MT25QL256的标准SPI模式最高支持133MHz但实际能跑多快取决于你的PCB布线和信号完整性。我一开始直接用了2分频也就是50MHz结果读出来的数据偶尔会错位。后来用示波器看波形发现CLK的上升沿有振铃MISO的数据建立时间不够。降到8分频12.5MHz之后波形干净了数据也稳定了。这里有个经验SPI时钟频率不是越高越好尤其是当你的PCB走线比较长或者没有做阻抗匹配的时候。我建议先用低速比如1MHz到5MHz把功能调通然后再逐步提高频率同时用示波器观察波形。如果发现数据出错优先检查时钟频率和信号质量而不是怀疑代码。// 华大HC32F460 SPI初始化配置示例 static void SPI_Flash_Init(void) { stc_spi_init_t stcSpiInit; stc_gpio_init_t stcGpioInit; // 使能SPI和GPIO时钟 PWC_Fcg1PeriphClockCmd(PWC_FCG1_PERIPH_SPI1, Enable); PWC_Fcg0PeriphClockCmd(PWC_FCG0_PERIPH_GPIOA, Enable); PWC_Fcg0PeriphClockCmd(PWC_FCG0_PERIPH_GPIOB, Enable); // 配置SPI引脚PA5SCK, PA6MISO, PA7MOSI, PA4NSS GPIO_StructInit(stcGpioInit); stcGpioInit.u16PinDir PIN_DIR_OUT; stcGpioInit.u16PinDrv PIN_HIGH_DRV; GPIO_Init(GPIO_PORT_A, GPIO_PIN_05 | GPIO_PIN_07, stcGpioInit); stcGpioInit.u16PinDir PIN_DIR_IN; stcGpioInit.u16PinInputType PIN_IN_TYPE_SMT; GPIO_Init(GPIO_PORT_A, GPIO_PIN_06, stcGpioInit); // 配置NSS引脚为复用功能 GPIO_SetFunc(GPIO_PORT_A, GPIO_PIN_04, GPIO_FUNC_4); GPIO_SetFunc(GPIO_PORT_A, GPIO_PIN_05, GPIO_FUNC_4); GPIO_SetFunc(GPIO_PORT_A, GPIO_PIN_06, GPIO_FUNC_4); GPIO_SetFunc(GPIO_PORT_A, GPIO_PIN_07, GPIO_FUNC_4); // SPI参数配置 SPI_StructInit(stcSpiInit); stcSpiInit.u32WireMode SPI_4_WIRE; // 四线模式 stcSpiInit.u32TransMode SPI_FULL_DUPLEX; // 全双工 stcSpiInit.u32MasterSlave SPI_MASTER; // 主机模式 stcSpiInit.u32SpiMode SPI_MODE_0; // 模式0 stcSpiInit.u32BaudRatePrescaler SPI_BR_PCLK_DIV8; // 8分频 stcSpiInit.u32DataBits SPI_DATA_SIZE_8BIT; // 8位数据 stcSpiInit.u32FirstBit SPI_FIRST_MSB; // MSB先行 stcSpiInit.u32NssMode SPI_NSS_HARDWARE; // 硬件片选 SPI_Init(SPI_UNIT, stcSpiInit); // 使能SPI SPI_Cmd(SPI_UNIT, Enable); }上面这段代码是华大HC32F460的SPI初始化关键参数我都加了注释。注意u32NssMode这个参数如果你用软件片选就改成SPI_NSS_SOFTWARE然后自己用GPIO控制CS引脚。另外u32BaudRatePrescaler是分频系数实际SPI时钟 PCLK / 分频值。我这里是100MHz / 8 12.5MHz。3. 从读ID开始验证通信链路3.1 读ID命令的时序细节读ID是验证SPI通信是否正常的第一步也是最关键的一步。MT25QL256支持多个读ID命令常用的有0x9F读制造商和设备ID、0x90读制造商和设备ID需要地址、0xAB读电子签名。我一般用0x9F因为它最简单不需要发地址直接发命令然后读3个字节就行。0x9F命令的时序是这样的CS拉低发送0x9F然后连续读3个字节。第一个字节是制造商IDMT25QL256的制造商是MicronID是0x20。第二个字节是存储类型0xBA。第三个字节是容量0x19表示256Mbit。所以正常情况下你应该读到0x20、0xBA、0x19这三个字节。我一开始读出来的是0xFF、0xFF、0xFF这说明MISO线一直是高电平FLASH根本没有响应。排查过程是这样的先用示波器看CS、CLK、MOSI、MISO四根线。CS拉低了CLK有波形MOSI上有0x9F的数据但MISO一直是高电平。这说明FLASH没有驱动MISO线可能的原因有三个一是FLASH没供电二是CS没拉低三是FLASH处于某种保护状态。量了一下FLASH的VCC3.3V正常。CS引脚在传输时确实被拉低了。那就只剩保护状态了。MT25QL256有一个状态寄存器里面的BPBlock Protect位和WELWrite Enable Latch位会影响写操作但读ID不应该受影响。后来发现是WP引脚的问题我把WP引脚悬空了而WP是低电平有效悬空时电平不确定FLASH可能进入了写保护状态导致所有命令都不响应。把WP上拉到VCC之后再读ID就正常了。提示MT25QL256的WP和HOLD引脚在标准SPI模式下必须上拉到VCC否则FLASH可能不工作。很多人会忽略这两个引脚以为悬空就行实际上悬空的引脚电平是不确定的FLASH的行为也会不确定。3.2 读ID的代码实现与调试技巧// 读MT25QL256的ID uint8_t MT25QL256_ReadID(void) { uint8_t cmd 0x9F; uint8_t id[3] {0}; // 拉低CS SPI_CS_LOW(); // 发送读ID命令 SPI_TransmitReceive(cmd, 1); // 读取3个字节的ID SPI_TransmitReceive(id, 3); // 拉高CS SPI_CS_HIGH(); // 打印ID printf(Manufacturer ID: 0x%02X\r\n, id[0]); printf(Memory Type: 0x%02X\r\n, id[1]); printf(Capacity: 0x%02X\r\n, id[2]); // 验证ID是否正确 if (id[0] 0x20 id[1] 0xBA id[2] 0x19) { return 1; // ID正确 } return 0; // ID错误 }这段代码很简单但有几个细节要注意。第一SPI_TransmitReceive函数在发送数据的同时也在接收数据所以读ID的时候发送的3个字节可以是任意值通常是0xFF因为FLASH不关心你发什么它只管把ID移出来。第二CS的拉低和拉高必须严格包围整个命令序列中间不能有间隙否则FLASH会认为命令结束。第三如果你用的是硬件片选CS的控制是自动的你只需要调用SPI的发送接收函数就行不需要手动控制GPIO。调试的时候如果读出来的ID不对可以按这个顺序排查先确认供电和引脚连接再确认SPI模式CPOL和CPHA然后确认时钟频率是否过高最后检查CS的时序。我遇到过一种情况SPI模式配置成了模式1结果读出来的ID错位了一个bit变成了0x40、0x75、0x32。这是因为模式1的采样沿和FLASH不匹配数据在错误的时刻被采样了。4. 状态寄存器与写使能擦写操作的前置条件4.1 状态寄存器的位定义与读取方法MT25QL256有两个状态寄存器状态寄存器1和状态寄存器2。状态寄存器1的位定义如下bit0是WIPWrite In Progress表示FLASH是否正在执行擦写操作bit1是WELWrite Enable Latch表示写使能锁存是否有效bit2到bit6是BPBlock Protect位用于设置块保护范围bit7是SRWDStatus Register Write Disable用于保护状态寄存器本身。读状态寄存器的命令是0x05读状态寄存器1和0x35读状态寄存器2。时序和读ID类似CS拉低发命令读一个字节CS拉高。WIP位是最重要的因为擦除和写入操作都需要时间在WIP为1的时候FLASH不接受任何新的命令。所以每次擦写之前必须轮询WIP位直到它为0。// 读状态寄存器1 uint8_t MT25QL256_ReadStatusReg1(void) { uint8_t cmd 0x05; uint8_t status 0; SPI_CS_LOW(); SPI_TransmitReceive(cmd, 1); SPI_TransmitReceive(status, 1); SPI_CS_HIGH(); return status; } // 等待FLASH空闲 void MT25QL256_WaitForIdle(void) { uint8_t status 0; uint32_t timeout 0; do { status MT25QL256_ReadStatusReg1(); timeout; if (timeout 1000000) { printf(FLASH wait idle timeout!\r\n); break; } } while (status 0x01); // 等待WIP位变为0 }这里有个经验轮询WIP位的时候一定要加超时机制。我遇到过FLASH因为供电不稳导致WIP位一直为1的情况如果没有超时程序就死循环了。超时时间可以根据擦除操作的最大时间来估算MT25QL256的4KB扇区擦除典型时间是45ms最大时间是400ms。所以超时时间至少要是400ms我一般设1秒左右。4.2 写使能命令的时机与注意事项写使能命令是0x06它的作用是把WEL位置1。每次擦除或写入之前都必须先发写使能命令否则FLASH会忽略擦写操作。写使能命令的时序很简单CS拉低发0x06CS拉高。不需要发数据也不需要读数据。但这里有个坑写使能命令必须在擦除或写入命令之前单独发送不能和擦除命令连在一起。我一开始图省事把0x06和0x20扇区擦除命令连续发送中间没有拉高CS结果擦除操作根本没执行。后来看datasheet才发现每个命令都必须以CS拉高作为结束标志写使能和擦除是两个独立的命令必须分开。// 写使能 void MT25QL256_WriteEnable(void) { uint8_t cmd 0x06; SPI_CS_LOW(); SPI_TransmitReceive(cmd, 1); SPI_CS_HIGH(); } // 写禁止 void MT25QL256_WriteDisable(void) { uint8_t cmd 0x04; SPI_CS_LOW(); SPI_TransmitReceive(cmd, 1); SPI_CS_HIGH(); }写使能之后WEL位会保持为1直到你执行擦除或写入操作或者手动发送写禁止命令0x04。如果你发完写使能之后没有执行擦写操作WEL位会一直保持这本身没什么问题但如果你在WEL为1的时候发送了其他命令可能会意外触发擦写。所以我的习惯是写使能之后立即执行擦写操作擦写完成之后如果不再需要写入就发一个写禁止命令。5. 4KB扇区擦除的完整流程与实测数据5.1 扇区擦除命令的时序与地址计算MT25QL256的擦除操作有三种粒度4KB扇区擦除命令0x20、32KB块擦除命令0x52、64KB块擦除命令0xD8、整片擦除命令0xC7或0x60。4KB扇区擦除是最常用的因为它的粒度小适合按需擦除不需要擦掉整个块。4KB扇区擦除的时序是CS拉低发送0x20然后发送3个字节的地址24位地址CS拉高。地址必须是4KB对齐的也就是说地址的低12位必须是0。比如0x000000、0x001000、0x002000都是合法的扇区地址而0x000800就不是。MT25QL256的地址范围是0x000000到0x1FFFFFF总共32MB。每个扇区4KB所以总共有8192个扇区。地址的计算很简单扇区号乘以4096就是扇区的起始地址。比如第0个扇区的地址是0x000000第1个扇区的地址是0x001000第2个扇区的地址是0x002000以此类推。// 4KB扇区擦除 void MT25QL256_SectorErase(uint32_t sector_addr) { uint8_t cmd 0x20; uint8_t addr[3]; // 检查地址是否4KB对齐 if (sector_addr % 4096 ! 0) { printf(Error: Sector address must be 4KB aligned!\r\n); return; } // 发送写使能 MT25QL256_WriteEnable(); // 构造地址字节 addr[0] (sector_addr 16) 0xFF; addr[1] (sector_addr 8) 0xFF; addr[2] sector_addr 0xFF; // 发送擦除命令和地址 SPI_CS_LOW(); SPI_TransmitReceive(cmd, 1); SPI_TransmitReceive(addr, 3); SPI_CS_HIGH(); // 等待擦除完成 MT25QL256_WaitForIdle(); printf(Sector 0x%06X erased.\r\n, sector_addr); }这段代码的关键点是地址对齐检查和等待擦除完成。地址不对齐的话FLASH的行为是不确定的可能会擦除错误的扇区也可能会忽略命令。等待擦除完成是必须的因为擦除操作需要时间如果你不等就发下一条命令FLASH会忽略它。5.2 擦除时间的实测数据与影响因素我实测了几次4KB扇区擦除的时间用示波器抓CS引脚的电平变化从CS拉低到WIP位变为0的时间就是擦除时间。实测数据如下擦除次数擦除时间ms环境温度第1次5225°C第2次4825°C第3次5525°C第4次5025°C第5次4725°C可以看到4KB扇区擦除的时间在47ms到55ms之间平均大约50ms。这个数据和datasheet上的典型值45ms基本吻合。擦除时间受温度影响比较大温度越低擦除时间越长。我在-20°C的环境下测试过擦除时间增加到了80ms左右。所以如果你的产品要在低温环境下工作超时时间要留足够的余量。另外擦除时间还和FLASH的寿命有关。NOR FLASH的擦写寿命一般是10万次随着擦写次数的增加擦除时间会逐渐变长。当擦除时间明显超过datasheet的最大值时说明FLASH的寿命快到了需要考虑更换。注意擦除操作是FLASH寿命的主要消耗者每次擦除都会对存储单元造成不可逆的损伤。所以不要频繁擦除同一个扇区尽量把数据分散到不同的扇区或者用磨损均衡算法来延长FLASH的寿命。6. 页写入与数据读取的实操细节6.1 页写入命令的256字节限制MT25QL256的写入操作是以页为单位的每页256字节。页写入命令是0x02时序是CS拉低发送0x02发送3字节地址然后发送最多256字节的数据CS拉高。地址可以是任意地址不需要页对齐但如果你写入的数据超过256字节或者跨越了页边界FLASH会自动回卷到页的起始地址覆盖之前写入的数据。举个例子假设你从地址0x0000F0开始写入300字节的数据。前256字节会写入0x0000F0到0x0001EF但剩下的44字节会回卷到0x000000到0x00002B覆盖掉之前写入的数据。所以写入的时候必须确保不跨页或者手动分页写入。// 页写入最多256字节 void MT25QL256_PageProgram(uint32_t addr, uint8_t *data, uint16_t len) { uint8_t cmd 0x02; uint8_t addr_bytes[3]; // 检查长度是否超过256字节 if (len 256) { printf(Error: Page program length must not exceed 256 bytes!\r\n); return; } // 检查是否跨页 if ((addr % 256) len 256) { printf(Error: Page program crosses page boundary!\r\n); return; } // 发送写使能 MT25QL256_WriteEnable(); // 构造地址字节 addr_bytes[0] (addr 16) 0xFF; addr_bytes[1] (addr 8) 0xFF; addr_bytes[2] addr 0xFF; // 发送写入命令、地址和数据 SPI_CS_LOW(); SPI_TransmitReceive(cmd, 1); SPI_TransmitReceive(addr_bytes, 3); SPI_TransmitReceive(data, len); SPI_CS_HIGH(); // 等待写入完成 MT25QL256_WaitForIdle(); printf(Page program at 0x%06X, length %d bytes.\r\n, addr, len); }页写入的时间比擦除快得多典型值是0.7ms最大值是5ms。我实测下来256字节的页写入大约需要1ms左右。写入时间同样受温度和寿命影响但影响程度比擦除小。6.2 数据读取的三种模式与选择建议MT25QL256支持三种读取模式标准SPI读取命令0x03、快速读取命令0x0B、双输出快速读取命令0x3B、四输出快速读取命令0x6B。标准读取的最高时钟频率是50MHz快速读取的最高时钟频率是133MHz双输出和四输出的频率更高但需要配置相应的模式。标准读取的时序是CS拉低发送0x03发送3字节地址然后连续读取数据CS拉高。数据可以一直读没有长度限制FLASH会自动递增地址。快速读取的时序多了一个dummy字节空字节在发送地址之后需要发送一个0x00然后再读数据。这个dummy字节的作用是给FLASH留出准备数据的时间。// 标准SPI读取 void MT25QL256_ReadData(uint32_t addr, uint8_t *buffer, uint32_t len) { uint8_t cmd 0x03; uint8_t addr_bytes[3]; // 构造地址字节 addr_bytes[0] (addr 16) 0xFF; addr_bytes[1] (addr 8) 0xFF; addr_bytes[2] addr 0xFF; // 发送读取命令、地址然后读取数据 SPI_CS_LOW(); SPI_TransmitReceive(cmd, 1); SPI_TransmitReceive(addr_bytes, 3); SPI_TransmitReceive(buffer, len); SPI_CS_HIGH(); } // 快速读取需要dummy字节 void MT25QL256_FastRead(uint32_t addr, uint8_t *buffer, uint32_t len) { uint8_t cmd 0x0B; uint8_t addr_bytes[3]; uint8_t dummy 0x00; addr_bytes[0] (addr 16) 0xFF; addr_bytes[1] (addr 8) 0xFF; addr_bytes[2] addr 0xFF; SPI_CS_LOW(); SPI_TransmitReceive(cmd, 1); SPI_TransmitReceive(addr_bytes, 3); SPI_TransmitReceive(dummy, 1); // dummy字节 SPI_TransmitReceive(buffer, len); SPI_CS_HIGH(); }选择哪种读取模式取决于你的SPI时钟频率。如果时钟频率低于50MHz用标准读取就行简单直接。如果时钟频率高于50MHz必须用快速读取否则数据会出错。我一般用快速读取因为它的兼容性更好而且dummy字节的开销可以忽略不计。7. 调试过程中踩过的坑与排查思路7.1 读出来的数据全是0xFF或0x00这是最常见的现象原因可能有以下几种第一FLASH没供电或者供电电压不对量一下VCC引脚确保是3.3V。第二CS引脚没有正确拉低用示波器看CS的波形确保在传输期间是低电平。第三SPI模式配置错误MT25QL256只支持模式0和模式3如果你配置成了模式1或模式2数据会错位。第四时钟频率过高导致信号完整性变差降低频率试试。第五WP或HOLD引脚悬空导致FLASH进入保护状态把这两个引脚上拉到VCC。我遇到过一次读出来全是0x00的情况排查了很久才发现是MISO引脚配置错了。华大的GPIO复用功能有多个选项我选错了复用编号导致MISO引脚没有正确连接到SPI外设。后来查了HC32F460的数据手册确认SPI1的MISO是PA6复用功能是Func_4改过来就正常了。7.2 擦除成功但写入失败擦除操作能正常执行说明SPI通信和写使能都没问题。写入失败的原因通常是写使能没有正确发送或者写入地址跨页了。检查一下写使能命令是否在写入命令之前单独发送并且CS有拉高拉低的过程。另外检查写入的数据长度是否超过256字节地址是否跨页。还有一种可能是FLASH的块保护位BP位被设置了导致某些区域被保护无法写入。读一下状态寄存器1看看BP位的值。如果BP位不为0发送写状态寄存器命令0x01把BP位清零。但要注意写状态寄存器之前必须先发写使能命令而且状态寄存器的写入也需要时间写完之后要等待WIP位变为0。7.3 擦除时间过长或超时擦除时间过长通常是FLASH寿命快到了或者供电电压偏低。量一下擦除期间的VCC确保电压稳定在3.3V。如果电压正常但擦除时间明显超过datasheet的最大值考虑更换FLASH。另外低温环境下擦除时间会变长这是正常现象但超时时间要相应调整。我遇到过一种情况擦除时间突然从50ms变成了500ms排查后发现是电源纹波太大。用示波器看VCC引脚发现擦除期间有200mV的纹波导致FLASH内部电荷泵工作不稳定。在VCC引脚旁边加了一个10uF的钽电容和0.1uF的陶瓷电容之后纹波降到了50mV以内擦除时间也恢复到了50ms左右。提示FLASH的VCC引脚旁边必须加去耦电容推荐10uF钽电容加0.1uF陶瓷电容的组合。很多人只加0.1uF对于擦除这种大电流操作来说是不够的。8. 华大SPI例程的完整代码结构与移植建议8.1 代码分层与模块化设计我把MT25QL256的驱动分成了三层硬件抽象层、命令层、应用层。硬件抽象层负责SPI的初始化和数据的发送接收命令层负责实现FLASH的各种命令应用层负责具体的业务逻辑。这样的分层设计便于移植如果你换到STM32平台只需要修改硬件抽象层命令层和应用层基本不用动。硬件抽象层提供三个函数SPI_Flash_Init、SPI_TransmitReceive、SPI_CS_LOW/HIGH。命令层提供MT25QL256_ReadID、MT25QL256_ReadStatusReg1、MT25QL256_WriteEnable、MT25QL256_SectorErase、MT25QL256_PageProgram、MT25QL256_ReadData这些函数。应用层就是你的业务代码比如存储配置参数、记录日志、读写文件系统。// 硬件抽象层SPI发送接收 void SPI_TransmitReceive(uint8_t *buf, uint32_t len) { for (uint32_t i 0; i len; i) { // 等待发送缓冲区空 while (SPI_GetFlag(SPI_UNIT, SPI_FLAG_TXE) 0); // 发送数据 SPI_SendData(SPI_UNIT, buf[i]); // 等待接收缓冲区非空 while (SPI_GetFlag(SPI_UNIT, SPI_FLAG_RXNE) 0); // 接收数据 buf[i] SPI_ReceiveData(SPI_UNIT); } }这个函数是SPI通信的核心它同时完成发送和接收。注意buf既是输入也是输出发送的时候是你要发的数据接收的时候是FLASH返回的数据。如果你只想发送不想接收可以把返回值忽略如果你只想接收不想发送可以把buf填成0xFF再调用。8.2 移植到STM32或其他平台的注意事项移植到STM32平台的话硬件抽象层需要改成HAL库的函数。STM32的HAL库提供了HAL_SPI_TransmitReceive函数可以直接替换我的SPI_TransmitReceive。CS的控制可以用HAL_GPIO_WritePin来实现。SPI的初始化用CubeMX生成就行注意选择模式0分频系数根据你的系统时钟来定。移植到GD32平台的话GD32的SPI外设和STM32高度兼容基本可以直接用STM32的代码只需要改一下头文件和寄存器定义。国民技术的MCU也类似SPI外设的寄存器布局和STM32差不多移植工作量不大。不管移植到哪个平台有几个关键点必须注意第一SPI模式必须是模式0或模式3第二CS的时序必须严格包围整个命令序列第三擦写操作之前必须发写使能命令第四擦写之后必须轮询WIP位等待完成。这四点做到了基本就不会有大问题。8.3 实测性能数据与优化建议我用12.5MHz的SPI时钟测试了一下读写速度。标准读取模式下读取1MB数据大约需要0.67秒换算下来大约是1.5MB/s。快速读取模式下读取1MB数据大约需要0.65秒速度提升不明显因为12.5MHz的时钟频率还没有达到标准读取的50MHz上限。如果把时钟提高到50MHz标准读取的速度可以达到6MB/s左右快速读取可以达到10MB/s以上。写入速度方面256字节的页写入大约需要1ms加上写使能和等待时间实际写入1MB数据大约需要4到5秒。擦除速度方面4KB扇区擦除大约50ms擦除1MB数据256个扇区大约需要12.8秒。所以如果你的应用需要频繁写入大量数据建议用更大的擦除粒度比如64KB块擦除或者用文件系统来管理数据减少擦除次数。优化建议第一尽量用DMA来传输数据减少CPU占用第二把频繁写入的数据缓存起来攒够一页再写入第三用磨损均衡算法把数据分散到不同的扇区第四如果不需要掉电保存可以用RAM缓存数据定期写入FLASH。我在实际项目中的体会是MT25QL256这颗FLASH的稳定性很好只要硬件设计没问题软件时序正确基本不会出幺蛾子。最容易出问题的地方是电源和引脚配置尤其是WP和HOLD引脚一定要上拉到VCC。另外擦除和写入的超时时间要留足够的余量不要卡着datasheet的典型值来设否则在低温或FLASH老化的情况下容易超时。

相关推荐

高压三相BLDC驱动实战:GC4938霍尔检测与自举电路设计笔记
高压三相BLDC驱动实战:GC4938霍尔检测与自举电路设计笔记

/* 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:21:22

民宿评论分析:基于Python的采集、主题提取与细粒度情感分析
民宿评论分析:基于Python的采集、主题提取与细粒度情感分析

/* 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:21:16

DC综合中retiming与compile_ultra协同优化实战指南
DC综合中retiming与compile_ultra协同优化实战指南

/* 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:21:16

Win11直装ISE 14.7:跳过虚拟机,老FPGA工具链完美运行
Win11直装ISE 14.7:跳过虚拟机,老FPGA工具链完美运行

/* 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:55:49

SoC存储体系详解:从Cache到eFuse,嵌入式芯片存储选型与设计
SoC存储体系详解:从Cache到eFuse,嵌入式芯片存储选型与设计

/* 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:55:43

QNX内存排查利器:pmap命令详解与实战技巧
QNX内存排查利器:pmap命令详解与实战技巧

/* 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:55:43

Windows内核I2C驱动实战:从用户态到KMDF的完整通信链路
Windows内核I2C驱动实战:从用户态到KMDF的完整通信链路

简介:Sensy 是一套面向嵌入式与 Windows 内核驱动学习者的教育性质源码项目,围绕 I2C 设备通信展开,从用户模式逐步深入到 KMDF 驱动开发,适合具备一定 C 基础、希望理解 Windows 驱动框架与 SPB 总线机制的开发者参考实践。资源包… · 2026/9/28 1:55:43

C#上位机集成Unet语义分割:ONNX模型GPU推理实战与踩坑
C#上位机集成Unet语义分割:ONNX模型GPU推理实战与踩坑

/* 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:55:42

OpenCV预处理+CRNN识别:车牌识别毕设落地全链路
OpenCV预处理+CRNN识别:车牌识别毕设落地全链路

/* 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:55:42

MATLAB雷达信号脉冲压缩仿真:LFM线性调频、匹配滤波与距离分辨率实现
MATLAB雷达信号脉冲压缩仿真:LFM线性调频、匹配滤波与距离分辨率实现

简介:这套Matlab仿真工具完整呈现雷达信号脉冲压缩过程,从线性调频(LFM)信号生成、目标回波仿真到匹配滤波压缩处理均有可运行代码支撑,面向电子信息工程、计算机、数学等专业学生,适用于课程设计、期末大作… · 2026/9/27 0:00:01

汕头网站建设制作厂家避坑指南:5大注意事项救急
汕头网站建设制作厂家避坑指南:5大注意事项救急

汕头网站建设制作厂家避坑指南:5大注意事项救急 改个需求建站公司拖一周,这种憋屈事我见得太多了。 很多汕头老板找本地建站团队,签合同前看着方案挺美,一上线就变脸。 今天不聊虚的,直接拆解找 汕头网站建设制作厂家 时的5个核心 注意事项… · 2026/9/27 0:00:01

多模态虚假新闻检测实战:BERT+ResNet双塔与对比学习
多模态虚假新闻检测实战:BERT+ResNet双塔与对比学习

简介:基于PyTorch的多模态虚假新闻检测项目完整代码包,面向自然语言处理与计算机视觉交叉方向的开发者、科研人员及毕业设计选题者,解决社交媒体中文本与图像联合识别虚假新闻的问题。系统以BERT预训练模型提取文本语义特征,以Res… · 2026/9/27 0:00:01

制作网页比较方便的软件怎么选?一文搞懂避坑指南
制作网页比较方便的软件怎么选?一文搞懂避坑指南

制作网页比较方便的软件怎么选?一文搞懂避坑指南 很多老板一上来就问:做个网站多少钱?但我反问他:你的域名买了吗?服务器租了吗?他一脸懵。这就是典型的“域名服务器搞不懂”。别急,今天咱们不聊虚的,直接 一文搞懂 那些让你头秃的技术名词。… · 2026/9/28 0:00:06

婚恋网站实战案例:避开3个高价坑,省钱50%还能跑赢流量
婚恋网站实战案例:避开3个高价坑,省钱50%还能跑赢流量

婚恋网站实战案例:避开3个高价坑,省钱50%还能跑赢流量 找婚恋网站建站公司,最怕的就是被坑高价。很多同行跟我吐槽,报价单上写得模棱两可,功能栏里全是“高级定制”、“专属UI”,结果落地全是套壳。今天不聊虚的,直接甩几个我经手的 实战案例… · 2026/9/28 0:00:19

济南做网站多少钱:3个案例拆解,防黑源码下载全攻略
济南做网站多少钱:3个案例拆解,防黑源码下载全攻略

济南做网站多少钱:3个案例拆解,防黑源码下载全攻略 上周济南一个做建材的老板找我,脸都绿了。他的官网首页弹出了赌博广告,后台被植入了挖矿脚本。他慌得问我:“网站被黑挂马不知道怎么办?能不能直接找之前的外包公司要源码下载,看看哪里被动了手脚?… · 2026/9/28 0:00:25

了解更多?预约专属演示

我们的顾问将为您一对一讲解产品与方案

企业微信二维码