STM32驱动W25Q64 SPI Flash实战:从硬件连接到驱动封装

发布时间:2026/10/21 2:35:08
STM32驱动W25Q64 SPI Flash实战:从硬件连接到驱动封装 1. 项目缘起与整体设计思路1.1 为什么选 SPI Flash 作为存储方案做过嵌入式项目的人大概都有这种体会MCU 自带的 Flash 空间永远不够用。以 STM32F103C8T6 为例64KB 的 Flash 里塞完协议栈、驱动和业务逻辑之后能留给数据存储的空间往往只剩几 KB。一旦需要记录运行日志、保存配置参数、缓存传感器采样数据这点空间立刻捉襟见肘。这时候外挂一颗 SPI Flash 是最经济实惠的选择。W25Q64 这颗芯片在圈子里几乎是“标配”级别的存在——8MB 容量64MbitSPI 接口2.7V~3.6V 供电价格便宜到可以忽略不计随便一个电子市场或者网购平台都能买到。它支持标准 SPI、Dual SPI 和 Quad SPI 三种模式标准 SPI 模式下最高时钟可以跑到 80MHz快速读模式下对于 STM32F103 这种最高 SPI 时钟 18MHzAPB2 72MHz 六分频的芯片来说完全够用。我选它的理由很直接容量够、接口简单、驱动成熟、资料多。你不需要去啃什么复杂的协议栈只要理解 SPI 的基本时序和 Flash 的几条核心指令就能把它跑起来。对于初学者来说这是理解“存储器操作”这件事最好的切入点之一。1.2 整体方案架构与数据流整个系统的数据流其实很清晰STM32F103 作为 SPI 主机W25Q64 作为从机。主机通过 SPI 总线发送指令、地址和数据从机根据指令执行对应的操作——读状态寄存器、写使能、擦除扇区、页编程、读数据等等。硬件连接上除了 SPI 的四根线SCK、MISO、MOSI、CSW25Q64 还有 WP写保护和 HOLD保持两个引脚。在实际项目中WP 一般直接拉高禁用硬件写保护HOLD 也拉高禁用保持功能这样软件层面完全控制读写操作避免硬件层面的意外干扰。软件架构我习惯分成三层底层 SPI 硬件驱动层、中间 W25Q64 指令封装层、上层应用接口层。底层负责 SPI 外设的初始化和字节收发中间层把 Flash 的各种操作封装成函数比如W25Q64_ReadID()、W25Q64_SectorErase()、W25Q64_PageWrite()上层就是业务逻辑调用比如保存配置、读取日志。这样分层的好处是以后换一颗 Flash 芯片只需要改中间层上层业务代码基本不用动。注意W25Q64 的页大小是 256 字节扇区大小是 4KB块大小是 64KB。一次页编程最多写 256 字节且不能跨页。这是新手最容易踩的坑——以为可以连续写任意长度结果数据在页边界处回卷覆盖了。1.3 关键参数选型与计算依据SPI 时钟频率的选择需要平衡速度和稳定性。STM32F103 的 SPI1 挂在 APB2 总线上时钟源为 72MHz。通过预分频器可以设置 2、4、8、16、32、64、128、256 分频。我一般选 8 分频得到 9MHz 的 SPI 时钟。为什么不用更高的 4 分频18MHz因为 W25Q64 在标准 SPI 模式下除了“快速读”指令支持 80MHz 以外普通读指令最高只支持 50MHz。9MHz 留足了余量走线稍微长一点也不会出问题。W25Q64 的供电范围是 2.7V~3.6VSTM32F103 是 3.3V 供电两者可以直接对接不需要电平转换。但要注意如果你的开发板上有 5V 器件共用 SPI 总线一定要加电平转换芯片或者至少串一个电阻否则可能烧毁 Flash 芯片。2. 核心细节解析与实操要点2.1 SPI 模式配置与时序匹配W25Q64 支持 SPI 模式 0CPOL0CPHA0和模式 3CPOL1CPHA1。这两种模式的区别在于空闲时 SCK 的电平不同。我一般用模式 0因为这是最常用的配置STM32 的 SPI 外设默认也是模式 0。配置的时候有几个关键点数据帧格式设为 8 位MSB 先行软件控制 NSS 引脚因为我们要手动控制片选时序波特率预分频器设为 8。这里特别说一下 NSS 引脚——如果你用硬件 NSS 模式STM32 会在每个字节传输间隙自动拉高片选导致多字节传输时 Flash 误判指令结束。所以必须用软件 NSS 模式手动在传输开始前拉低 CS传输结束后拉高。SPI_InitTypeDef spi; spi.SPI_Direction SPI_Direction_2Lines_FullDuplex; spi.SPI_Mode SPI_Mode_Master; spi.SPI_DataSize SPI_DataSize_8b; spi.SPI_CPOL SPI_CPOL_Low; spi.SPI_CPHA SPI_CPHA_1Edge; spi.SPI_NSS SPI_NSS_Soft; spi.SPI_BaudRatePrescaler SPI_BaudRatePrescaler_8; spi.SPI_FirstBit SPI_FirstBit_MSB; spi.SPI_CRCPolynomial 7; SPI_Init(SPI1, spi); SPI_Cmd(SPI1, ENABLE);2.2 W25Q64 指令集与状态寄存器解读W25Q64 的指令集不算复杂常用的就那么十来条。我整理了一个速查表方便你写驱动的时候对照指令名称指令码功能说明地址字节数数据字节数Write Enable0x06写使能00Write Disable0x04写禁止00Read Status Reg10x05读状态寄存器101~NRead Data0x03读数据31~NPage Program0x02页编程31~256Sector Erase0x20扇区擦除4KB30Block Erase0xD8块擦除64KB30Chip Erase0xC7全片擦除00JEDEC ID0x9F读制造商和设备ID03Fast Read0x0B快速读数据311~N状态寄存器 1 的 bit0 是 BUSY 位这是整个驱动里最重要的一个标志位。任何擦除或编程操作开始后BUSY 位会置 1操作完成后自动清零。你在发下一条指令之前必须轮询等待 BUSY 位变 0否则指令会被忽略。我见过太多人在这里翻车——擦除还没完成就急着写数据结果写进去的全是 0xFF。2.3 页编程的边界陷阱与地址计算前面提过页编程不能跨页这里展开说一下。W25Q64 的页大小是 256 字节地址的低 8 位决定页内偏移高位移位决定页号。如果你从地址 0x0000F0 开始写 32 字节写到 0x0000FF 正好是页尾没问题。但如果你从 0x0000F0 开始写 64 字节写到 0x0000FF 之后下一个字节会回卷到 0x000000而不是自动跳到下一页 0x000100。这就是所谓的“页回卷”现象。正确的做法是在写数据之前先计算当前地址到页尾还剩多少字节如果剩余空间不够就分多次写。我一般写一个W25Q64_Write()函数内部自动处理跨页逻辑void W25Q64_Write(uint32_t addr, uint8_t *buf, uint16_t len) { uint16_t page_remain 256 - (addr % 256); if (len page_remain) { W25Q64_PageWrite(addr, buf, len); } else { W25Q64_PageWrite(addr, buf, page_remain); W25Q64_Write(addr page_remain, buf page_remain, len - page_remain); } }这个递归写法简洁明了但要注意递归深度——最坏情况下 len 很大递归层数会比较多。实际项目中我一般用循环代替递归避免栈溢出风险。提示擦除操作的最小单位是扇区4KB不能按字节擦除。这意味着如果你要修改一个字节必须先把整个扇区的数据读到 RAM 里修改后再擦除整个扇区最后把修改后的数据写回去。这个“读-改-写”流程是 Flash 存储管理的核心逻辑。3. 完整实操流程与核心环节实现3.1 硬件连线与上电检查先确认硬件连接。以 STM32F103C8T6 最小系统板为例STM32 引脚W25Q64 引脚说明PA5CLKSPI1 时钟PA6DOSPI1 MISOPA7DISPI1 MOSIPA4CS片选软件控制3.3VVCC电源GNDGND地3.3VWP写保护禁用3.3VHOLD保持禁用连线完成后先别急着写代码。用万用表量一下 VCC 和 GND 之间有没有短路确认 3.3V 供电正常。然后上电用示波器或者逻辑分析仪看一下 SCK 引脚有没有信号——如果没有说明 SPI 初始化有问题如果有信号但 Flash 没反应检查 CS 引脚是否在传输期间正确拉低。我个人的习惯是第一次调试的时候先不接 Flash用逻辑分析仪抓 STM32 发出的波形确认指令码和地址字节都正确然后再接上 Flash 看响应。这样可以排除是主机问题还是从机问题。3.2 SPI 底层驱动编写与调试底层驱动就两个函数一个字节发送一个字节接收。但 SPI 是全双工发送和接收是同时进行的。所以通常写一个SPI_ReadWriteByte()函数发送一个字节的同时接收一个字节uint8_t SPI_ReadWriteByte(uint8_t tx_data) { while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) RESET); SPI_I2S_SendData(SPI1, tx_data); while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) RESET); return SPI_I2S_ReceiveData(SPI1); }这个函数看起来简单但有两个地方容易出问题。第一等待 TXE 标志的时候不能加超时否则在 Flash 忙的时候会误判。第二读 DR 寄存器之前必须等 RXNE 置位否则读到的数据是旧的。我早期写驱动的时候为了“提高效率”把两个等待合并成一个结果读出来的数据偶尔会错位查了半天才发现是时序问题。调试的时候可以先发一条Read Status Register指令0x05然后读一个字节。如果返回 0x00 或者 0xFF说明通信基本正常。如果返回其他值检查一下 SPI 模式配置和 CS 时序。3.3 读 ID 验证通信链路读 JEDEC ID 是验证通信链路最直接的方法。W25Q64 的 JEDEC ID 是 0xEF4017——0xEF 是制造商 IDWinbond0x4017 是设备 ID。操作流程是拉低 CS发送 0x9F然后连续读 3 个字节最后拉高 CS。uint32_t W25Q64_ReadID(void) { uint32_t id 0; W25Q64_CS_LOW(); SPI_ReadWriteByte(0x9F); id | SPI_ReadWriteByte(0xFF) 16; id | SPI_ReadWriteByte(0xFF) 8; id | SPI_ReadWriteByte(0xFF); W25Q64_CS_HIGH(); return id; }如果读出来是 0xEF4017恭喜你通信链路没问题。如果读出来是 0x000000 或者 0xFFFFFF检查 CS 引脚是否正常工作、SPI 时钟是否太快、供电是否稳定。我遇到过一种情况读出来是 0xEF4017 但偶尔会变成 0xEF0017后来发现是杜邦线太长导致信号完整性不好换短一点的线就稳定了。3.4 扇区擦除与数据写入完整流程擦除和写入是 Flash 操作的核心。完整流程是这样的发送写使能指令0x06发送扇区擦除指令0x20 24 位地址轮询状态寄存器等待 BUSY 位清零再次发送写使能指令0x06发送页编程指令0x02 24 位地址 数据轮询状态寄存器等待 BUSY 位清零发送读数据指令0x03 24 位地址读取验证这里有个细节每次擦除或编程之前都必须发写使能指令因为 W25Q64 在执行完一次擦除或编程后会自动清除写使能锁存器。如果你忘了发操作会被直接忽略而且状态寄存器不会报错——数据就是写不进去你会很困惑。void W25Q64_SectorErase(uint32_t addr) { W25Q64_WriteEnable(); W25Q64_CS_LOW(); SPI_ReadWriteByte(0x20); SPI_ReadWriteByte((addr 16) 0xFF); SPI_ReadWriteByte((addr 8) 0xFF); SPI_ReadWriteByte(addr 0xFF); W25Q64_CS_HIGH(); W25Q64_WaitBusy(); }W25Q64_WaitBusy()函数就是不断读状态寄存器直到 BUSY 位为 0。这里建议加一个超时计数器防止 Flash 损坏时死循环void W25Q64_WaitBusy(void) { uint32_t timeout 0xFFFFFF; while (timeout--) { W25Q64_CS_LOW(); SPI_ReadWriteByte(0x05); uint8_t status SPI_ReadWriteByte(0xFF); W25Q64_CS_HIGH(); if ((status 0x01) 0) break; } }3.5 数据读取与校验方法读数据相对简单发送 0x03 24 位地址然后连续读 N 个字节。但要注意读操作没有 BUSY 等待的问题可以直接读。读完之后我习惯做一次校验——把写入的数据和读出的数据逐字节比较确认无误。void W25Q64_Read(uint32_t addr, uint8_t *buf, uint16_t len) { W25Q64_CS_LOW(); SPI_ReadWriteByte(0x03); SPI_ReadWriteByte((addr 16) 0xFF); SPI_ReadWriteByte((addr 8) 0xFF); SPI_ReadWriteByte(addr 0xFF); for (uint16_t i 0; i len; i) { buf[i] SPI_ReadWriteByte(0xFF); } W25Q64_CS_HIGH(); }校验的时候我一般写一个W25Q64_Verify()函数把写入缓冲区和读出缓冲区用memcmp()比较。如果返回非零说明写入失败需要检查擦除是否完成、写使能是否发送、地址是否越界。4. 常见问题与排查技巧实录4.1 读出来全是 0xFF 或 0x00这是最常见的问题。0xFF 通常意味着擦除成功但写入失败0x00 通常意味着通信完全没建立。排查顺序如下现象可能原因排查方法读出全 0xFF写使能未发送检查每次编程前是否发 0x06读出全 0xFF扇区未擦除先擦除再写Flash 只能 1 变 0读出全 0x00CS 引脚未拉低用逻辑分析仪看 CS 波形读出全 0x00SPI 模式错误确认 CPOL/CPHA 配置读出全 0x00MISO 未接检查硬件连线我踩过最坑的一次是读出来全是 0xFF查了半天代码没问题最后发现是 WP 引脚悬空了。W25Q64 的 WP 引脚内部有弱上拉但悬空时电平不确定导致写保护状态随机。把 WP 直接接到 3.3V 之后问题消失。4.2 写入数据偶尔出错或错位数据错位通常和 CS 时序有关。SPI 传输要求 CS 在整個指令序列期间保持低电平如果在字节之间 CS 被拉高Flash 会认为指令结束后续数据被当作新指令解析。用软件 NSS 模式时确保W25Q64_CS_LOW()和W25Q64_CS_HIGH()成对出现中间不要插入其他操作。另一个可能是 SPI 时钟太快。虽然 W25Q64 标称支持 50MHz 普通读但在实际布线上9MHz 以上就可能因为线间电容导致波形畸变。如果你用的是杜邦线连接建议把预分频调到 16 甚至 32先保证稳定再逐步提高。4.3 擦除时间过长导致看门狗复位W25Q64 的扇区擦除典型时间是 45ms最大 400ms块擦除典型 150ms最大 2s全片擦除典型 20s最大 100s。如果你在擦除期间喂狗不及时看门狗就会复位。解决方法有两个一是擦除前先喂狗擦除后立即喂狗二是把擦除操作放在低优先级任务里或者用中断方式处理。我一般会在W25Q64_WaitBusy()里面加喂狗操作确保长时间等待不会触发复位。但要注意喂狗频率不能太高否则看门狗形同虚设。4.4 Flash 寿命与磨损均衡的考量W25Q64 的每个扇区擦写寿命是 10 万次。如果你频繁写同一个地址比如每秒写一次配置参数不到 28 小时就能把一个扇区写坏。对于需要频繁写入的场景必须做磨损均衡。最简单的磨损均衡是“日志式写入”每次写新数据都追加到下一个空闲位置而不是覆盖旧数据。记录一个写指针当写指针到达扇区末尾时擦除整个扇区把有效数据搬到扇区开头写指针重置。这样每个扇区的擦写次数就均匀了。当然对于大多数应用场景配置参数不会频繁修改10 万次寿命完全够用。但如果你做的是数据记录仪每秒采样一次那就必须考虑这个问题。4.5 常见问题速查表问题现象优先排查解决方案读 ID 返回 0x000000硬件连线检查 VCC、GND、CS、SCK读 ID 返回 0xFFFFFFMISO 未接或 CS 未拉低检查 MISO 连线确认 CS 时序写入后读回全 0xFF写使能未发或擦除未完成补发 0x06等待 BUSY 清零数据在页边界错乱页编程跨页分页写入每页不超过 256 字节擦除后仍能读出旧数据擦除指令地址错误确认地址按 4KB 对齐偶尔通信失败SPI 时钟太快或线太长降低时钟缩短连线看门狗复位擦除等待时间过长在等待循环中喂狗5. 驱动封装与项目集成建议5.1 分层封装与接口设计在实际项目中我习惯把 W25Q64 驱动封装成独立的模块对外只暴露几个简单接口// w25q64.h void W25Q64_Init(void); uint32_t W25Q64_ReadID(void); void W25Q64_Read(uint32_t addr, uint8_t *buf, uint32_t len); void W25Q64_Write(uint32_t addr, uint8_t *buf, uint32_t len); void W25Q64_EraseSector(uint32_t addr); void W25Q64_EraseChip(void);上层应用不需要知道 SPI 怎么配置、指令怎么发只需要调用这些接口就行。这样以后换芯片比如换成 W25Q128只需要改驱动层上层代码一行不用动。5.2 与文件系统或参数管理模块的对接如果你的项目需要管理多个参数可以考虑在 W25Q64 上实现一个简单的键值存储。比如用前 4KB 作为参数区每个参数占 16 字节4 字节键、4 字节长度、8 字节数据。读取的时候遍历查找写入的时候追加或覆盖。更复杂的场景可以移植 FatFS 或者 LittleFS。LittleFS 特别适合 SPI Flash因为它自带磨损均衡和掉电保护。不过对于 STM32F103 这种资源有限的芯片LittleFS 的 RAM 占用需要仔细评估。我实测下来LittleFS 在 STM32F103 上跑起来大概需要 2KB RAM 和 16KB Flash如果你的芯片是 C8T664KB Flash、20KB RAM勉强够用。5.3 调试经验与效率提升技巧最后分享几个提高调试效率的小技巧。第一用逻辑分析仪抓 SPI 波形比用 printf 打印调试信息直观得多。你可以清楚地看到每一条指令的字节序列快速定位是主机发错了还是从机没响应。第二写一个W25Q64_Dump()函数把指定地址范围的数据以十六进制打印出来。调试的时候看一眼就知道数据对不对。第三养成“先擦后写再读校验”的习惯。每次写入操作后都读回来比较虽然多花一点时间但能及早发现问题避免数据错乱了还不知道。第四如果条件允许在 Flash 的 VCC 和 GND 之间并一个 0.1uF 的陶瓷电容越靠近芯片引脚越好。SPI 通信对电源噪声比较敏感这个电容能显著提高稳定性。我遇到过好几次通信偶发失败加了电容之后问题就消失了。这个 W25Q64 的驱动我前后改了好几版从最开始只能读 ID到后来能稳定读写几 MB 数据中间踩的坑基本都写在上面了。你如果照着这个流程走应该能少走不少弯路。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询