☰
软件SPI驱动W25Q64:从时序原理到避坑实战
2026/10/17 7:44:58 网站建设 项目流程

简介:本资源面向STM32单片机与嵌入式开发初学者及进阶学习者,聚焦软件SPI通信方式读写W25Q64 Flash存储器这一典型实战场景。W25Q64内部可存储8M字节数据且掉电不丢失,案例完整演示了主机STM32与从机W25Q64模块之间的通信实现,接线采用SPI1外设引脚方案,PA4至PA7分别对应NSS、SCK、MISO、MOSI,便于后续过渡到硬件SPI章节。压缩包共83个文件,约1004KB,以36个h头文件与33个c源文件为核心,涵盖MySPI、W25Q64、OLED、Key等驱动模块,另含8个s启动文件及uvprojx、uvoptx等Keil工程配置,目录按User、Hardware、Library、Start分层组织,结构清晰。目前已有2052人学习下载。读者可从中获得软件SPI时序模拟、Flash读写指令封装、OLED显示调试等完整代码与排错思路,适合作为嵌入式存储外设学习的参考范例。

1. 软件SPI读写W25Q64:为什么时序比外设更值得先搞懂

很多人第一次在 STM32 上驱动 W25Q64 时,都会下意识打开 CubeMX,把 SPI1 配成 Mode 0、8 位数据、MSB 先出,然后调用 HAL_SPI_Transmit 就等着读 ID。结果要么读回 0xFF,要么读回 0x00,要么第一次能读、第二次就卡死。翻车之后才发现,问题往往不在 W25Q64,而在自己根本没搞清楚 SPI 的时钟极性和相位到底在干什么。硬件 SPI 外设把这些细节封装得太好,好到你根本不知道它替你做了什么。而软件 SPI 用普通 GPIO 一位一位地翻转电平,反而把整个通信过程摊开在你面前:什么时候拉低片选、第几个边沿采样、MOSI 在时钟高电平还是低电平改变,全部由你决定。搞懂软件 SPI,再去用硬件 SPI,你才知道那些配置项到底在配什么。W25Q64 这颗 8MB 的 Flash 存储器,恰好是一个足够简单又足够典型的 SPI 从设备,它的指令集、状态寄存器、页编程和扇区擦除机制,几乎覆盖了嵌入式开发中 SPI 通信的全部核心问题。

2. SPI 的四种模式:CPOL 和 CPHA 到底在采样什么

2.1 从一根时钟线说起

SPI 是全双工同步串行总线,四根线各司其职:SCK 提供时钟节拍,MOSI 是主机输出从机输入,MISO 是主机输入从机输出,CS 是片选。通信的本质是移位寄存器交换:主机和从机各有一个 8 位(或更多位)移位寄存器,在 SCK 的驱动下,每个时钟周期双方各移出一位、移入一位,8 个周期后两个寄存器的内容就完成了交换。这个模型解释了为什么 SPI 没有应答机制也能工作——发送和接收在物理上是同一个动作,你发一个字节出去,必然同时收一个字节回来。

但这里有一个关键问题:数据在时钟的哪个边沿被采样?又在哪个边沿被改变?如果主机在上升沿改变数据、从机也在上升沿采样,那从机采到的就是正在跳变的不稳定电平。所以 SPI 协议规定,采样边沿和改变边沿必须错开。CPOL 决定时钟空闲时是高电平还是低电平,CPHA 决定在第一个边沿采样还是第二个边沿采样。两者组合出四种模式,W25Q64 支持 Mode 0(CPOL=0,CPHA=0)和 Mode 3(CPOL=1,CPHA=1),最常用的是 Mode 0。

2.2 软件 SPI 的位操作实现

软件 SPI 的核心就是一个循环,每次处理一个 bit。下面这段代码展示了 Mode 0 下发送并接收一个字节的完整过程:

// 软件 SPI 读写一个字节,Mode 0:CPOL=0, CPHA=0 // 空闲时 SCK 低电平,第一个边沿(上升沿)采样 uint8_t SoftSPI_ReadWriteByte(uint8_t txData) { uint8_t rxData = 0; for (int i = 7; i >= 0; i--) // MSB 先出,从 bit7 到 bit0 { // 1. 主机在 SCK 低电平期间把数据放到 MOSI 上 if (txData & (1 << i)) GPIO_SetBits(GPIOA, GPIO_Pin_7); // MOSI 输出高 else GPIO_ResetBits(GPIOA, GPIO_Pin_7); // MOSI 输出低 // 2. 拉高 SCK,产生上升沿,从机在此刻采样 MOSI GPIO_SetBits(GPIOA, GPIO_Pin_5); // 3. 主机在上升沿之后读取 MISO,获取从机输出的位 if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6)) rxData |= (1 << i); // 4. 拉低 SCK,产生下降沿,为下一个 bit 做准备 GPIO_ResetBits(GPIOA, GPIO_Pin_5); } return rxData; }

这段代码里每一步的先后顺序都不是随便写的。第 1 步在 SCK 低电平期间改变 MOSI,保证数据在上升沿到来之前已经稳定。第 2 步拉高 SCK,从机在上升沿采样 MOSI 上的数据。第 3 步在 SCK 还是高电平的时候读取 MISO,因为从机也是在上升沿把数据放到 MISO 上的,此时读取最稳定。第 4 步拉低 SCK,完成一个周期。如果换成 Mode 3,CPOL=1 意味着空闲时 SCK 是高电平,CPHA=1 意味着在第二个边沿采样,代码里拉高拉低的顺序就要反过来。

注意:软件 SPI 的速率完全取决于 GPIO 翻转速度和循环开销。在 72MHz 的 STM32F103 上,上面这个循环大概能跑到 1-2MHz 的 SCK 频率。W25Q64 支持最高 80MHz 的时钟,所以软件 SPI 的速度瓶颈非常明显,这也是为什么实际产品中更常用硬件 SPI 的原因。

2.3 为什么 W25Q64 选 Mode 0 和 Mode 3

W25Q64 的数据手册明确写了支持 Mode 0 和 Mode 3。这两种模式的共同点是:数据都在时钟的上升沿被采样(对于 Mode 0,上升沿是第一个边沿;对于 Mode 3,上升沿是第二个边沿)。区别只在于空闲电平不同。这样设计的好处是,从设备只需要在上升沿采样即可,下降沿用于数据切换,逻辑统一。如果你的软件 SPI 用 Mode 0 能读到 ID 但读数据出错,大概率是某个环节的边沿顺序搞反了,而不是 W25Q64 本身有问题。

3. W25Q64 的指令交互:从读 ID 到页编程的完整链路

3.1 读 JEDEC ID:验证通信是否打通

在驱动任何 SPI 从设备之前,第一步永远是读 ID。W25Q64 的 JEDEC ID 指令是 0x9F,发送后会返回 3 个字节:制造商 ID(0xEF)、存储器类型(0x40)、容量(0x18,表示 8MB)。读 ID 的好处是它不涉及任何写操作,不需要等待忙状态,纯粹验证时序是否正确。

// 读取 W25Q64 的 JEDEC ID void W25Q64_ReadID(uint8_t *mid, uint8_t *did) { W25Q64_CS_LOW(); // 拉低片选,选中从机 SoftSPI_ReadWriteByte(0x9F); // 发送 JEDEC ID 指令 *mid = SoftSPI_ReadWriteByte(0xFF); // 读制造商 ID,0xEF *did = SoftSPI_ReadWriteByte(0xFF); // 读设备 ID W25Q64_CS_HIGH(); // 拉高片选,结束通信 }

这里发送 0xFF 是一个常见做法:SPI 是全双工,主机需要产生时钟才能接收数据,发送什么内容无所谓,0xFF 只是习惯用法。片选 CS 的拉低和拉高必须完整包裹整个指令序列,中间不能松开,否则 W25Q64 会认为指令结束,后续数据全部无效。

3.2 状态寄存器与忙等待

W25Q64 在执行擦除或编程操作时,内部需要时间完成,这段时间内除了读状态寄存器指令(0x05)之外,其他指令一律不响应。状态寄存器的 bit0 是 BUSY 位,1 表示忙,0 表示空闲。所以每次擦除或写入之后,必须轮询等待 BUSY 位清零,否则后续操作会直接失败。

// 等待 W25Q64 空闲 void W25Q64_WaitBusy(void) { uint8_t status; W25Q64_CS_LOW(); SoftSPI_ReadWriteByte(0x05); // 读状态寄存器指令 do { status = SoftSPI_ReadWriteByte(0xFF); } while (status & 0x01); // bit0 为 BUSY 位 W25Q64_CS_HIGH(); }

这段代码有一个容易踩坑的地方:片选在轮询期间一直保持低电平。有些实现会在每次读状态寄存器之间拉高片选再拉低,这样虽然也能工作,但效率更低,而且如果中间被中断打断,可能引入额外的时序问题。保持片选低电平连续轮询是更稳妥的做法。

3.3 扇区擦除与页编程的边界

W25Q64 的存储结构是:8MB 分为 128 个块(Block),每块 64KB;每块分为 16 个扇区(Sector),每扇区 4KB;每扇区分为 16 页(Page),每页 256 字节。擦除的最小单位是扇区(4KB),编程的最小单位是页(256 字节)。这意味着你无法单独擦除一个字节,也无法跨页写入。

写数据之前必须先擦除对应扇区,因为 Flash 的物理特性是只能把 1 写成 0,不能把 0 写回 1。擦除操作把整个扇区恢复为 0xFF。页编程指令(0x02)最多写入 256 字节,如果写入超过 256 字节,地址会自动回卷到本页开头,覆盖之前的数据。这个回卷行为是硬件层面的,不会报错,但数据会静默损坏。

// 页编程:在指定地址写入数据,长度不超过 256 字节且不跨页 void W25Q64_PageProgram(uint32_t addr, uint8_t *data, uint16_t len) { W25Q64_WriteEnable(); // 必须先发送写使能指令 0x06 W25Q64_CS_LOW(); SoftSPI_ReadWriteByte(0x02); // 页编程指令 SoftSPI_ReadWriteByte(addr >> 16); // 发送 24 位地址,高字节先出 SoftSPI_ReadWriteByte(addr >> 8); SoftSPI_ReadWriteByte(addr); for (uint16_t i = 0; i < len; i++) SoftSPI_ReadWriteByte(data[i]); W25Q64_CS_HIGH(); W25Q64_WaitBusy(); // 等待编程完成 }

写使能指令 0x06 是每次编程或擦除之前都必须发送的,W25Q64 内部有一个写使能锁存器(WEL),发送 0x06 后置 1,编程或擦除完成后自动清零。如果忘记发写使能,编程指令会被直接忽略,数据写不进去,而且不会有任何错误提示。

4. 避坑与排查:软件SPI驱动W25Q64的五个血泪教训

4.1 读回全是 0xFF 或 0x00

现象:调用读 ID 函数,返回的制造商 ID 不是 0xEF,而是 0xFF 或 0x00。

原因:0xFF 通常意味着 MISO 线一直处于高电平,可能是从机没有响应,或者片选没有正确拉低。0x00 则可能是 MISO 线一直低电平,或者 GPIO 模式配置错误。常见的情况是 MISO 引脚没有配置成浮空输入或上拉输入,而是配成了推挽输出,导致主机自己把自己的输出拉死了。

解决:检查 MISO 引脚的 GPIO 模式,必须配置为输入模式(GPIO_Mode_IN_FLOATING 或 GPIO_Mode_IPU)。同时用示波器或逻辑分析仪确认 CS 是否在通信期间保持低电平,SCK 是否有 8 个完整的脉冲。

4.2 第一次能读、第二次卡死

现象:上电后第一次读 ID 成功,但紧接着读数据就卡在忙等待循环里出不来。

原因:W25Q64 在上电后如果 CS 引脚悬空或受到干扰,可能进入一种异常状态。另外,如果上一次操作没有正确结束(比如 CS 没有拉高),W25Q64 会一直等待后续时钟,状态机无法复位。

解决:在初始化时先拉高 CS,发送 0xFF 几个周期让 W25Q64 内部状态机复位。每次操作结束后确保 CS 拉高。忙等待循环里加超时计数,避免死循环。

4.3 写入数据后读出来不对

现象:页编程写入 0x01 0x02 0x03,读出来却是 0xFF 0xFF 0xFF 或者部分数据错误。

原因:最常见的原因是忘记在编程前擦除扇区。Flash 只能把 1 写成 0,如果目标地址之前有数据且不是 0xFF,写入结果就是旧数据和新数据的按位与。另一个原因是写入跨页了,超过 256 字节后地址回卷,覆盖了本页开头的数据。

解决:写入前先擦除对应扇区,并等待擦除完成。确保单次页编程不超过 256 字节,且起始地址到页末尾的剩余空间足够。如果需要写入超过 256 字节,拆分成多次页编程,每次重新发送写使能和页编程指令。

4.4 擦除后读出来还是旧数据

现象:调用了扇区擦除指令,等待忙状态结束,再读取该扇区,发现数据没有变成 0xFF。

原因:擦除指令发送前没有发送写使能指令 0x06。W25Q64 的写使能锁存器在每次擦除或编程后自动清零,所以每次操作前都必须重新发送。另一个可能的原因是擦除的地址没有对齐到 4KB 边界,W25Q64 会忽略低 12 位地址,实际擦除的是包含该地址的整个扇区,但如果你传的地址本身有误,可能擦错了扇区。

解决:每次擦除前调用写使能函数。擦除地址按 4KB 对齐,即 addr & 0xFFFFF000。擦除后读回验证,确认数据确实变成了 0xFF。

4.5 软件SPI速率太慢导致超时

现象:用软件 SPI 连续写入大量数据时,上位机或看门狗超时复位。

原因:软件 SPI 每个字节需要 8 次循环,每次循环包含多次 GPIO 操作,在 72MHz 主频下实际 SCK 频率可能只有几百 kHz。写入 1MB 数据需要的时间可能达到数十秒,如果看门狗没有及时喂狗,就会复位。

解决:在忙等待和长循环中插入喂狗操作。如果对速度有要求,考虑切换到硬件 SPI,或者优化软件 SPI 的 GPIO 操作,比如直接操作 BSRR 寄存器代替 GPIO_SetBits/GPIO_ResetBits,减少函数调用开销。

5. 软件SPI与硬件SPI的取舍:什么时候该用哪个

软件 SPI 和硬件 SPI 的选择,本质上是在灵活性和性能之间做权衡。软件 SPI 的优势在于引脚任意映射,不受硬件外设引脚复用的限制。比如某个项目里 SPI 外设的引脚被其他功能占用了,或者需要同时挂多个 SPI 从设备但硬件 CS 引脚不够用,软件 SPI 就能派上用场。另外,软件 SPI 的时序完全可控,遇到一些时序要求奇怪的从设备,可以精确调整每个边沿的延迟。

但软件 SPI 的代价也很明显。前面代码里每个字节的读写需要几十条指令,在 72MHz 的 STM32F103 上实测 SCK 频率大约在 1MHz 左右,而硬件 SPI 可以轻松跑到 18MHz 甚至 36MHz。对于 W25Q64 这种支持 80MHz 的存储器来说,软件 SPI 完全是在浪费它的性能。如果只是偶尔读写几个字节的配置参数,软件 SPI 够用;如果要存储大量数据、做文件系统、或者需要快速启动,硬件 SPI 是必然选择。

我一般会这么理解:软件 SPI 是学习 SPI 协议的最佳工具,因为它强迫你面对每一个时序细节。当你用软件 SPI 把 W25Q64 完整驱动起来之后,再去看硬件 SPI 的配置寄存器,CPOL、CPHA、波特率预分频、数据帧格式这些位域就不再是黑匣子了。你知道每个配置项对应的是哪个物理行为,出了问题也知道从哪个环节去排查。从那以后我每次调试新的 SPI 从设备,都会先用软件 SPI 把 ID 读出来,确认时序无误,再切换到硬件 SPI 跑高速。这个习惯帮我省下了大量猜测硬件配置的时间。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询