拓冰建站拓冰建站
首页 / 资讯中心 / 正文

STM32硬件SPI驱动W25Q64:跨页写入拆分、WEL自动清零陷阱与擦写时序实测验证

文章目录摘要前言一、NOR Flash 的物理规则:所有问题的源头1.1 存储结构与操作单位1.2 位只能从 1 写成 01.3 WEL 写使能锁存器:每条指令只生效一次1.4 一次页编程的完整时序1.5 方案级决策:为什么选 SPI Flash 而不是别的二、SPI 模式与片选:两个必须想清楚的配置2.1 CPOL/CPHA 与 Mode 0/32.2 配置级决策:软件 NSS,不要用硬件 NSS2.3 配置级决策:SPI1 还是 SPI2三、硬件设计与 CubeMX 配置3.1 接线定义3.2 CubeMX 配置项与寄存器位对照3.3 容易遗漏的步骤:CS 引脚的上电默认电平四、驱动实现:跨页拆分与超时保护4.1 驱动分层与状态机4.2 底层字节收发与 JEDEC ID 校验4.3 写使能校验与 BUSY 等待4.4 跨页拆分写入(核心算法)4.5 扇区擦除与"先擦后写"封装五、测试验证5.1 SPI 时钟频率参数扫描5.2 理论值 vs 实测值对照5.3 压力测试与失败路径记录六、故障排查:5 类典型问题的完整排查链问题一:JEDEC ID 读回 0x000000 或 0xFFFFFF(最常见)问题二:写入 300 字节后开头 44 字节被"调包"问题三:第一次页编程成功,第二次静默无效问题四:直接写入未擦除区域,读回数据 = 新旧按位与问题五:擦除等待期间系统随机复位七、总结版本备注参考资料摘要SPI NOR Flash 是嵌入式系统掉电数据保存的主流方案,但页编程跨页回卷、写使能锁存器(WEL)自动清零、片选时序三类问题让不少驱动"看似能用、实则埋雷"。本文基于 STM32F103C8T6 + W25Q64(8 MB NOR Flash),从 SPI_CR1 寄存器位与 NOR 存储物理约束两个层面拆解页编程机制,实现跨页自动拆分写入、WEL 逐条回读校验与带超时保护的 BUSY 轮询驱动层。实测:18 MHz 时钟下 4 KB 连读吞吐 2.07 MB/s(理论值 2.25 MB/s 的 92%);页编程耗时均值 0.51 ms,与数据手册典型值 0.4 ms 偏差 +28%,远低于 3 ms 上限;10⁵ 次随机长度读写逐字节校验零错误。文末附 5 类典型故障的完整排查链与 36 MHz 高速飞线场景的翻车实录。前言几乎每个嵌入式产品都绕不开"数据掉电不丢"这个需求:传感器的校准系数、设备的运行日志、用户的配置参数。可选方案无非三档——片内 Flash、I2C EEPROM、SPI NOR Flash。F103C8T6 片内 Flash 只有 64 KB,擦写期间还会阻塞取指,跑着固件根本不敢动;24C02 这类 EEPROM 接口省事,但容量通常不超过 4 KB,价格还比同容量 NOR Flash 贵一截。一颗 W25Q64 只要几块钱就有 8 MB,页编程 0.4 ms、扇区擦除 30 ms,是这类需求的最优解。但 SPI Flash 驱动有个特点:入门十分钟,踩坑一星期。它不像串口那样"发出去就对",也不像 I2C 有 ACK 可以检查——一条指令发错了没有任何反馈,只能在回读数据时才发现不对。我第一次写这套驱动时,先后栽在三个地方:写入 300 字节后开头 44 字节莫名变成数据尾部(跨页回卷)、第二次页编程静默无效(WEL 自动清零)、擦除等待期间看门狗复位(死等 BUSY)。这三个坑网上资料都有提及,但很少有人说清楚现象怎么判别、排查顺序怎么走。本文就把这套驱动从寄存器层面讲透,并给出完整的实测数据。读完你可以得到:CR1 配置位与物理含义的对照关系、一套可直接移植的跨页拆分驱动实现、2.25~36 MHz 五档时钟的实测吞吐与错误率扫描,以及 5 类典型故障的完整排查链。前置条件:熟悉 STM32 的 GPIO 与时钟树、会使用 CubeMX 生成 HAL 工程、了解 C 语言。硬件需要 STM32F103C8T6 最小系统板、W25Q64 模块(或裸芯片 + 10 kΩ 上拉电阻)、ST-Link,以及一台 24 MHz 以上采样率的逻辑分析仪(没有的话,第五章实测数据可以直接当参考结论用)。本文完整工程代码可在 CSDN 下载频道 获取(VIP 免费)。一、NOR Flash 的物理规则:所有问题的源头1.1 存储结构与操作单位先看整个存储链路的架构。MCU 侧分两层:应用层只关心"把这段数据存到这个地址",驱动层负责把请求翻译成 SPI 指令序列,并处理擦除、等待、越界这些脏活;W25Q64 通过四线 SPI 总线挂在 PA4~PA7 上。STM32F103C8T6CS=PA4 / SCK=PA5MISO=PA6 / MOSI=PA7Mode 0, 最高 36 MHz擦写等待 + 失败重试应用层配置/日志/参数存储Flash 驱动层跨页拆分 + 超时保护SPI1 硬件外设CR1/CR2/SRW25Q648 MB NOR FlashW25Q64 内部是三级结构,每一级的用途完全不同,混用就是踩坑的开始:单位大小用途数量(W25Q64)页(Page)256 B编程的最小/最大单位,一次最多写一页32768 页扇区(Sector)4 KB擦除的最小单位,想改 1 字节也要擦 4 KB2048 个块(Block)64 KB大块擦除单位,批量清空时比逐扇区快128 个这套结构的直接推论是:修改任意一个字节,标准流程都是"读出整个扇区到 RAM → 在 RAM 里改 → 擦除整个扇区 → 整扇区写回"。应用层以为自己在"写一个字节",驱动层背后是 4 KB 的搬运。1.2 位只能从 1 写成 0NOR Flash 的物理机制决定了它和 RAM 有本质区别:编程(Program)操作只能把位从 1 改成 0,不能从 0 改成 1;把位恢复成 1 的唯一手段是擦除,而擦除以扇区为单位,擦完全部变 0xFF。这个约束有个很有迷惑性的表现:如果往一个没擦过的地址直接编程,不会报错,也不会写失败,而是得到新旧数据的按位与。比如原值 0xAA(1010 1010),直接编程 0x55(0101 0101),回读得到 0x00(两个值的按位与)。数据"写进去了",但面目全非——这就是"先擦后写"铁律的来源,第六章问题四会给出完整的排查过程。1.3 WEL 写使能锁存器:每条指令只生效一次W25Q64 上电默认处于写保护状态,任何编程/擦除指令都会被静默忽略。解锁靠写使能指令 0x06,它把状态寄存器 SR1 的 WEL 位(bit 1)置 1。关键的坑在于 WEL 的生命周期:每条编程/擦除指令执行完毕后,WEL 自动清零——不是"解锁一次管到断电",而是"解锁一次只管一条指令"。所以正确的驱动写法是每条编程/擦除指令前都重新发 0x06,并且发完回读 SR1 确认 WEL 真的变成 1。第一次写成功、第二次静默无效,是新手最常见的"玄学故障",根因就是想当然地认为写使能是持久状态。相关阅读:《STM32——SPI通信W25Q64》 — 对 WEL 生命周期与"只能 1 写 0"约束的独立梳理,可与本文 1.2/1.3 节对照1.4 一次页编程的完整时序页编程是"写使能 → 送指令 → 拉高片选提交 → 轮询等待"四步,对应的 SPI 事务序列如下:从机 W25Q64主机 F103从机 W25Q64主机 F103SR1.WEL = 1内部编程 tPP ≈ 0.4 ms期间 WEL 自动清零loop[轮询 SR1]CS 拉低0x06 写使能CS 拉高CS 拉低0x02 页编程 + 24 位地址数据字节 ×N(N ≤ 256 且不跨页)CS 拉高(上升沿触发内部编程)0x05 读状态BUSY = 0 表示完成时序里有两个容易被忽略的细节。其一,CS 的上升沿是"提交"动作:数据字节发完必须立刻拉高 CS,Flash 收到上升沿才开始内部编程;如果发完最后一个字节忘了拉高,这条指令等于没发。其二,编程/擦除期间 Flash 会忽略除读状态(0x05/0x35)之外的所有指令,此时强行发读数据指令不会有任何响应,表现为主机收到一串 0xFF 或 0x00。1.5 方案级决策:为什么选 SPI Flash 而不是别的掉电存储的候选方案各有限制,按本文场景(配置参数 + 运行日志,单条写入几字节到几 KB)逐项对比:对比维度片内 Flash(F103)I2C EEPROM(24C02)SD 卡(SPI 模式)SPI NOR Flash(W25Q64)容量64 KB(与固件共享)2~4 KBGB 级8 MB写入粒度页 1 KB,擦页阻塞取指字节扇区 512 B页 256 B擦除耗时页擦除约 20~40 ms无需擦除需按 512 B 块写扇区擦除约 30 msCPU 占用高(擦写停流水线)低(100~400 kHz)中(协议栈复杂)低(18 MHz 下 5%)移植复杂度低低高(FAT/协议栈)中(自写驱动约 200 行)参考价格—~¥1~¥15(含卡座)~¥3片内 Flash 擦写期间 CPU 停摆对实时系统不可接受;EEPROM 容量天花板太低;SD 卡对"存几个参数"来说协议栈太重。SPI NOR 在容量、速度、复杂度之间是最平衡的落点——代价是要自己把 1.2/1.3 节的物理规则处理对,这正是第四章驱动层的全部工作。二、SPI 模式与片选:两个必须想清楚的配置2.1 CPOL/CPHA 与 Mode 0/3SPI 的时钟极性(CPOL)与时钟相位(CPHA)组合出四种模式,主机和从机必须配成同一种,否则采样沿错位、数据整体偏移:模式CPOLCPHA含义W25Q64 支持Mode 000SCK 空闲低电平,第一个(上升)边沿采样✅Mode 101SCK 空闲低电平,第二个(下降)边沿采样❌Mode 210SCK 空闲高电平,第二个(上升)边沿采样❌Mode 311SCK 空闲高电平,第一个(下降)边沿采样✅在 STM32 侧,这两个参数落在 **SPI_CR1 寄存器的 CPOL 位(bit 1)和 CPHA 位(bit 0)**上,CubeMX 的界面选项与寄存器值一一对应。本文选 Mode 0:W25Q64 复位期间引脚状态不确定,SCK 空闲保持低电平的 Mode 0 不容易在初始化窗口引入虚假时钟边沿,同时与 OLED、TFT 等常见外设的模式一致,共总线时更省心。2.2 配置级决策:软件 NSS,不要用硬件 NSS片选是 SPI Flash 驱动最容易配置错的点。STM32 的 NSS 有三种玩法,对 Flash 驱动来说只有一个正确答案:NSS 方式寄存器配置对 Flash 驱动的适用性硬件输出模式CR1.SSM=0,CR2.SSOE=1❌ SPE=1 期间 NSS 持续拉低,多条指令被黏成一帧,CS 上升沿的"提交"语义彻底失效硬件输入模式CR1.SSM=0,SSOE=0❌ 主机模式下 NSS 仅检测冲突,仍需外部拉高,等于没管片选软件 NSSCR1.SSM=1(bit 9),SSI=1(bit 8)✅ 片选改用普通 GPIO 精确控制,帧首拉低、帧尾拉高,完全掌控提交时机原因在 1.4 节已经埋下:Flash 的每条指令是"命令 + 地址 + 数据"的一整帧,帧与帧之间靠 CS 上升沿分界。硬件输出模式在 SPE 使能期间把 NSS 一直压在低电平,写使能和页编程两条指令会被 Flash 当成一条超长指令解析,行为完全不可预期。所以 CubeMX 里 Hardware NSS Signal 必须选 Disable,CS 引脚单独配成 GPIO 推挽输出。2.3 配置级决策:SPI1 还是 SPI2C8T6 上有两个可用 SPI,挂在不同的时钟域上,直接决定速度上限:外设挂载总线总线时钟SCK 上限(分频 2)默认引脚SPI1APB272 MHz36 MHzPA4~PA7 / PB3~PB5SPI2APB136 MHz18 MHzPB12~PB15W25Q64 数据手册标称标准读指令最高支持 50 MHz 时钟,所以 SPI1 的 36 MHz 上限才有发挥空间;SPI2 封顶 18 MHz。本文要选 SPI1,还有一个附带好处:PA4~PA7 四个引脚在 Blue Pill 排针上相邻,飞线最短。这个"总线归属决定分频基数"的细节和 I2C 挂 APB1 是同一类坑——分频算错不会报错,只会让实际时钟悄悄偏离预期。三、硬件设计与 CubeMX 配置3.1 接线定义W25Q64 引脚STM32F103C8T6说明CSPA4软件 NSS,GPIO 推挽输出,外加 10 kΩ 上拉(见 3.3)CLKPA5SPI1_SCKDO (MISO)PA6SPI1_MISO,配内部上拉(未选中时 Flash 输出高阻,防浮空)DI (MOSI)PA7SPI1_MOSIWP3.3V写保护释放;用模块则板载已上拉,裸芯片必须接HOLD3.3V挂起释放;处理方式同 WPVCC / GND3.3V / GNDW25Q64 供电范围 2.7~3.6 V,不能接 5 VWP 和 HOLD 悬空是"裸芯片能用、量产必挂"的经典隐患:两者内部都接了写保护/挂起逻辑,悬空引脚感应到的电平可能随机触发写保护,表现为同一批板子里个别芯片偶发写入失败。模块化开发可以直接用 GY-25Q 这类带排针的成品模块,板载上拉已处理。3.2 CubeMX 配置项与寄存器位对照下表把 CubeMX 图形界面上的每个配置项落到 SPI_CR1/SPI_CR2 的具体位上。这样即使不用 CubeMX,直接操作寄存器也知道该怎么写:CubeMX 配置项配置值对应寄存器/位这样配的理由ModeFull-Duplex MasterCR1.MSTR = 1(bit 2)主机全双工,收发共用移位寄存器Hardware NSS SignalDisableCR1.SSM = 1(bit 9),SSI = 1(bit 8)软件片选,理由见 2.2Frame FormatMotorola不涉及 TI 模式位W25Q64 是 Motorola SPI 时序Data Size8 BitsCR1.DFF = 0(bit 11)指令/地址/数据全部是 8 位帧First BitMSB FirstCR1.LSBFIRST = 0(bit 7)Flash 指令与地址均高位先行Prescaler4(起步用 8)CR1.BR[2:0](bit 5:3)fPCLK2/4 = 18 MHz,见下方推导CPOLLowCR1.CPOL = 0(bit 1)Mode 0,见 2.1CPHA1 EdgeCR1.CPHA = 0(bit 0)Mode 0,见 2.1CRC CalculationDisabledCR1.CRCEN = 0(bit 13)Flash 通信校验靠回读比对,CRC 用不上PA6 MISO 上拉Pull-upGPIOx_PUPDR 对应位防止片选释放期间 MISO 浮空引入干扰分频推导写在明面上,换主频或换速率时直接套公式。SPI1 挂在 APB2 上,SCK = fPCLK2 ÷ 分频系数,BR[2:0] 从 000 到 111 对应 2/4/8/16/32/64/128/256 分频。72 MHz 的 APB2 依次得到 36/18/9/4.5/2.25 MHz。第五章的参数扫描就是沿这五档做的。BR 系数写成宏而不是魔法数字,换时钟域时只改一处。3.3 容易遗漏的步骤:CS 引脚的上电默认电平这是本文最重要的配置细节之一。CubeMX 生成 GPIO 初始化代码时,Output Level 默认是 Low——它不知道你把 PA4 当片选用,也不会替你拉高。于是上电瞬间的时序变成:Flash 供电建立 → CS 处于低电平 → MCU 复位释放期间 SCK/MOSI 引脚状态抖动 → Flash 把这些抖动当成时钟边沿锁存成垃圾位。此后第一条真指令可能被拼进垃圾位里,解析完全错乱。遗漏这一步的症状很有辨识度:偶发性的上电首读异常,我在修复前的 100 次上电测试中复现了 3 次 JEDEC ID 读错,断电重上就恢复——典型的"初始化窗口竞态"。根因不在 Flash 也不在 SPI 外设,而在 GPIO 初始化顺序。CubeMX 不会替你做这件事,因为"这个 GPIO 是不是片选"只有设计者知道。正确的处理是双保险:CubeMX 里把 PA4 的 GPIO output level 显式设为 High;驱动初始化的最开头再写一次拉高(防止工程被重新生成后配置丢失),硬件上再并一颗 10 kΩ 上拉兜底,覆盖复位期间引脚高阻的窗口。/* w25q_port.c —— 必须在 MX_SPI1_Init() 之前调用 */voidw25q_cs_release_force(void){GPIO_InitTypeDef gpio={0};/* 1. 先拉高 CS: 复位窗口内 Flash 不得被选中 */HAL_GPIO_WritePin(GPIOA,GPIO_PIN_4,GPIO_PIN_SET);/* 2. 再配成推挽输出, 覆盖 CubeMX 可能的默认低电平 */gpio.Pin=GPIO_PIN_4;gpio.Mode=GPIO_MODE_OUTPUT_PP;gpio.Pull=GPIO_NOPULL;gpio.Speed=GPIO_SPEED_FREQ_HIGH;HAL_GPIO_Init(GPIOA,gpio);}四、驱动实现:跨页拆分与超时保护4.1 驱动分层与状态机驱动层用一个轻量状态机管理健康度:每次操作前确认器件空闲,操作失败带着错误码向上抛,而不是内部死等。上层据此决定重试还是降级运行。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门