软件SPI驱动W25Q64实战:从GPIO模拟到双区存储与数据校验
简介本资源面向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也不直接开硬件外设刚入行那会儿我接手一个数据采集板子主控是STM32F103外挂一颗W25Q64做本地日志存储。硬件SPI引脚被板子布线锁死了偏偏那两根线还跟JTAG复用调试时一烧录就打架。当时第一反应是改板但项目周期不允许只能硬着头皮用软件SPI把W25Q64驱动起来。结果这一搞反而让我摸清了SPI通信的底层时序——时钟极性、相位、片选建立时间这些平时被硬件外设自动处理掉的细节全得自己拿捏。软件SPI读写W25Q64说白了就是用普通GPIO口模拟SCK、MOSI、MISO三根线的电平翻转配合CS片选按W25Q64的指令集一帧一帧地收发数据。它解决的核心问题是当硬件SPI引脚冲突、数量不够或者你想完全掌控时序细节时还能把Flash存储器跑起来。适合谁手头有STM32开发板、想搞懂SPI协议底层、或者被硬件资源逼到墙角的嵌入式开发者。这篇笔记就把我踩过的坑和能直接抄的代码全倒出来从GPIO配置到页编程、扇区擦除一步步走通。2. 软件SPI的时序底子先把W25Q64的脾气摸透2.1 SPI四种模式与W25Q64的固定档位SPI通信有四个模式由CPOL时钟极性和CPHA时钟相位组合而成。W25Q64只支持模式0CPOL0CPHA0和模式3CPOL1CPHA1。我一般用模式0SCK空闲时为低电平数据在SCK上升沿采样下降沿输出。软件模拟时这个时序必须用GPIO的置位和清零精确复现差一个微秒都可能读到0xFF。为什么强调这个因为很多新手直接用硬件SPI的默认配置CPOL和CPHA没对齐读回来的ID全是0xFF还以为芯片坏了。软件SPI的好处就在这——你可以用示波器或者逻辑分析仪直接抓SCK和MOSI的波形一眼看出相位对不对。2.2 软件SPI的GPIO配置要点软件SPI需要三根输出线SCK、MOSI、CS和一根输入线MISO。SCK和MOSI配置为推挽输出CS也配推挽输出但初始拉高。MISO必须配成上拉输入或浮空输入因为W25Q64在CS拉高时MISO是高阻态如果配成推挽输出会跟Flash的输出打架烧芯片不至于但读数据肯定乱。我习惯把SCK和MOSI的GPIO速度设成50MHz虽然软件翻转根本跑不到那么快但边沿陡峭一点时序更干净。CS线单独拎出来不跟其他外设共用避免片选误触发。// 软件SPI GPIO初始化以STM32F103为例 void SoftSPI_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // SCK - PA5 推挽输出 GPIO_InitStructure.GPIO_Pin GPIO_Pin_5; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); // MOSI - PA7 推挽输出 GPIO_InitStructure.GPIO_Pin GPIO_Pin_7; GPIO_Init(GPIOA, GPIO_InitStructure); // CS - PA4 推挽输出初始拉高 GPIO_InitStructure.GPIO_Pin GPIO_Pin_4; GPIO_Init(GPIOA, GPIO_InitStructure); GPIO_SetBits(GPIOA, GPIO_Pin_4); // MISO - PA6 上拉输入 GPIO_InitStructure.GPIO_Pin GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; GPIO_Init(GPIOA, GPIO_InitStructure); }这段代码里MISO配成上拉输入是关键。上拉保证在Flash没驱动总线时MCU读到的是确定的高电平而不是悬空的玄学值。CS初始拉高让Flash处于待命状态等你要通信时再拉低。2.3 软件SPI字节收发函数字节收发是软件SPI的核心。模式0下SCK空闲低电平先发MOSI的最高位然后SCK拉高从机在上升沿采样MCU在上升沿之后读MISO。一个bit一个bit循环8次拼成一个字节。// 软件SPI收发一个字节模式0 uint8_t SoftSPI_SwapByte(uint8_t byte) { uint8_t i, recv 0; for (i 0; i 8; i) { // 准备MOSI数据先发最高位 if (byte 0x80) GPIO_SetBits(GPIOA, GPIO_Pin_7); else GPIO_ResetBits(GPIOA, GPIO_Pin_7); byte 1; // SCK拉高从机采样 GPIO_SetBits(GPIOA, GPIO_Pin_5); // 读MISO主机采样 recv 1; if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6)) recv | 0x01; // SCK拉低 GPIO_ResetBits(GPIOA, GPIO_Pin_5); } return recv; }逻辑说明每次循环先摆好MOSI电平再拉高SCK。从机在SCK上升沿把MOSI上的数据读走同时把自己的数据推到MISO上。MCU在SCK高电平期间读MISO然后拉低SCK准备下一个bit。参数上byte是要发送的字节返回值是同时接收到的字节。注意这里没有加延时因为GPIO翻转本身就有一定间隔STM32F103跑72MHz时这个循环的速率大概在几百kHz到1MHz之间W25Q64最高支持80MHz所以完全跟得上。3. W25Q64指令集落地从读ID到页编程的完整链路3.1 读JEDEC ID验证通信是否打通拿到一颗新Flash第一件事不是急着写数据而是读它的JEDEC ID。W25Q64的ID是0xEF4017厂商ID 0xEF设备ID 0x4017。读ID的指令是0x9F拉低CS后发0x9F然后连续读3个字节。// 读取W25Q64的JEDEC ID uint32_t W25Q64_ReadID(void) { uint32_t id 0; GPIO_ResetBits(GPIOA, GPIO_Pin_4); // CS拉低 SoftSPI_SwapByte(0x9F); // 发送读ID指令 id | SoftSPI_SwapByte(0xFF) 16; // 厂商ID id | SoftSPI_SwapByte(0xFF) 8; // 存储器类型 id | SoftSPI_SwapByte(0xFF); // 容量 GPIO_SetBits(GPIOA, GPIO_Pin_4); // CS拉高 return id; }如果读回来是0xEF4017说明硬件连线、GPIO配置、SPI时序全对了。如果读回0xFFFFFF或0x000000先查CS有没有拉低再查MISO是不是配成了推挽输出最后用逻辑分析仪看SCK有没有波形。这一步过了后面才有意义。3.2 写使能、页编程与忙等待W25Q64写入前必须先发写使能指令0x06否则数据写不进去。页编程指令是0x02后面跟24位地址再跟最多256字节数据。注意一页是256字节跨页写入会回卷到页首覆盖数据这是血泪教训。// 写使能 void W25Q64_WriteEnable(void) { GPIO_ResetBits(GPIOA, GPIO_Pin_4); SoftSPI_SwapByte(0x06); GPIO_SetBits(GPIOA, GPIO_Pin_4); } // 等待忙状态结束 void W25Q64_WaitBusy(void) { uint8_t status; GPIO_ResetBits(GPIOA, GPIO_Pin_4); SoftSPI_SwapByte(0x05); // 读状态寄存器1 do { status SoftSPI_SwapByte(0xFF); } while (status 0x01); // bit0为BUSY位 GPIO_SetBits(GPIOA, GPIO_Pin_4); } // 页编程地址需256字节对齐 void W25Q64_PageProgram(uint32_t addr, uint8_t *data, uint16_t len) { uint16_t i; W25Q64_WriteEnable(); GPIO_ResetBits(GPIOA, GPIO_Pin_4); SoftSPI_SwapByte(0x02); // 页编程指令 SoftSPI_SwapByte((addr 16) 0xFF); SoftSPI_SwapByte((addr 8) 0xFF); SoftSPI_SwapByte(addr 0xFF); for (i 0; i len; i) SoftSPI_SwapByte(data[i]); GPIO_SetBits(GPIOA, GPIO_Pin_4); W25Q64_WaitBusy(); // 等待写入完成 }参数说明addr是24位地址W25Q64容量8MB地址范围0x000000到0x7FFFFF。len不能超过256且addr的低8位加上len不能跨过256边界。忙等待是必须的Flash内部擦写需要时间页编程典型1.4ms扇区擦除典型45ms不等待就发下一条指令数据必丢。3.3 扇区擦除与数据读取Flash的物理特性是只能把1写成0不能把0写成1。所以写入前必须先擦除擦除后全变0xFF。W25Q64最小擦除单位是扇区4KB。扇区擦除指令0x20读数据指令0x03。// 扇区擦除4KB对齐 void W25Q64_SectorErase(uint32_t addr) { W25Q64_WriteEnable(); GPIO_ResetBits(GPIOA, GPIO_Pin_4); SoftSPI_SwapByte(0x20); SoftSPI_SwapByte((addr 16) 0xFF); SoftSPI_SwapByte((addr 8) 0xFF); SoftSPI_SwapByte(addr 0xFF); GPIO_SetBits(GPIOA, GPIO_Pin_4); W25Q64_WaitBusy(); } // 读取数据任意地址任意长度 void W25Q64_ReadData(uint32_t addr, uint8_t *buf, uint32_t len) { uint32_t i; GPIO_ResetBits(GPIOA, GPIO_Pin_4); SoftSPI_SwapByte(0x03); SoftSPI_SwapByte((addr 16) 0xFF); SoftSPI_SwapByte((addr 8) 0xFF); SoftSPI_SwapByte(addr 0xFF); for (i 0; i len; i) buf[i] SoftSPI_SwapByte(0xFF); GPIO_SetBits(GPIOA, GPIO_Pin_4); }读数据没有长度限制可以跨页跨扇区连续读地址会自动递增。但写数据必须按页对齐擦除必须按扇区对齐。我一般会在应用层做一个简单的磨损均衡把日志轮流写到不同扇区避免同一个扇区反复擦写提前报废。4. 避坑排查软件SPI驱动W25Q64的五个翻车现场4.1 读ID返回0xFF波形却正常现象逻辑分析仪看SCK、MOSI、CS都正常但MISO始终高电平读回0xFFFFFF。 原因MISO配置成了推挽输出MCU和Flash同时驱动这根线电平被拉死。 解决把MISO改成上拉输入或浮空输入确保MCU只读不驱。4.2 页编程后读回数据错位现象写入0x01到0x0A读回来发现0x0A跑到0x00去了。 原因写入地址没有256字节对齐跨页后Flash内部地址回卷到页首。 解决应用层保证每次页编程的起始地址低8位加上长度不超过256跨页就拆成两次写。4.3 擦除后忘了写使能数据写不进去现象扇区擦除成功读回全0xFF但页编程后数据没变。 原因W25Q64每次写操作前都必须发0x06写使能擦除和编程各需要一次。 解决把写使能封装成独立函数在每次擦除和编程前调用别嫌麻烦。4.4 忙等待没加连续写丢数据现象连续写多个扇区只有第一个成功后面全失败。 原因Flash内部擦写需要时间上一条指令没完成就发下一条状态机乱了。 解决每次擦除和编程后调用忙等待函数轮询状态寄存器bit0直到为0。4.5 软件SPI速率过高导致读数据不稳定现象读ID正常但连续读大量数据时偶尔出现位错误。 原因GPIO翻转太快W25Q64在3.3V下最高80MHz但软件模拟加上循环开销实际边沿可能不够干净加上杜邦线长了有分布电容。 解决在SCK翻转之间插入几个NOP空指令或者降低GPIO速度配置用示波器看SCK上升沿是否过冲。5. 进阶技巧用软件SPI做双Flash交替存储与数据校验软件SPI跑通之后我习惯在应用层加一层简单的数据校验和双区备份。W25Q64有8MB空间划成两个4MB区域交替写入。每次写新数据前先擦除备用区写入完成后更新一个标志扇区记录当前有效区。这样即使写入过程中断电重启后还能从旧区恢复。具体做法定义一个结构体存数据头包含魔数、长度、CRC16校验值。写入时先算CRC连同数据一起写入。读取时先校验魔数和CRC不通过就切到备用区读。typedef struct { uint32_t magic; // 0x5A5A5A5A uint32_t len; uint16_t crc; uint8_t data[250]; } LogRecord_t; // 简单CRC16校验 uint16_t CRC16_Calc(uint8_t *buf, uint32_t len) { uint16_t crc 0xFFFF; uint32_t i; uint8_t j; for (i 0; i len; i) { crc ^ buf[i]; for (j 0; j 8; j) { if (crc 0x01) crc (crc 1) ^ 0xA001; else crc 1; } } return crc; }写入流程先读当前有效区标志确定备用区地址擦除备用区对应扇区构造LogRecord_t计算CRC页编程写入最后更新标志扇区。读取流程反过来先读标志区再读数据区校验CRC失败就切另一个区。这套机制跑下来最明显的收益是断电恢复能力。以前单区写入写到一半掉电整个日志区就废了。现在最坏情况是丢最后一条记录前面的数据完好。代价是Flash寿命减半但W25Q64擦写次数10万次按每天写100次算也能撑好几年。从那以后我每次用软件SPI驱动Flash都强制走一遍读ID、擦除、写、读回校验的完整流程确认时序和指令集全对了再往上叠业务逻辑。希望帮到你。本文还有配套的精品资源点击获取

相关新闻

纯前端实现视频转音频:FileReader、decodeAudioData与OfflineAudioContext实战

纯前端实现视频转音频:FileReader、decodeAudioData与OfflineAudioContext实战

1. 浏览器里把视频拆成音频,到底难在哪很多人第一次听到"前端视频转音频"这个需求,第一反应是:这不就是调个接口的事吗?实际上,浏览器原生并没有提供一个叫"video to audio"的按钮。你要做的事情&…

2026/10/17 3:33:53 阅读更多 →
知道却做不到?从道德认知到行动实践的自我训练指南

知道却做不到?从道德认知到行动实践的自我训练指南

我们总在某个深夜突然想明白一件"对的事":该向某个人道歉,该拒绝某个无理的要求,该承认自己错了,该为一件不关己的事站出来。但天亮之后,我们往往什么都没做。问题很少出在"不知道什么是对的"&…

2026/10/17 3:32:53 阅读更多 →
YOLOv8光伏EL检测实战:从PVEL-AD数据集到模型部署

YOLOv8光伏EL检测实战:从PVEL-AD数据集到模型部署

光伏EL(电致发光)检测,是组件出厂前绕不开的一道硬门槛。通电后的电池片会在近红外波段发出特定波长的光,缺陷区域和正常区域在亮度、纹理上有明显差异,但因为整片电池面积大、缺陷形态杂、背景干扰多,纯人…

2026/10/17 3:32:53 阅读更多 →

最新新闻

制造业智能体实践:从数据到满分Word的规则引擎与工作流

制造业智能体实践:从数据到满分Word的规则引擎与工作流

1. 从一份“满分Word”说起:制造业智能体到底在解决什么问题第一次看到“制造业智能体实践(满分Word)”这个标题,很多人会以为重点在“Word”上——是不是教人用AI写文档、排版、做申报材料?但真正在制造业信息化一线待…

2026/10/17 5:08:18 阅读更多 →
OpenLayers 2 WMS图层彻底清理:removeLayer与destroy的正确用法

OpenLayers 2 WMS图层彻底清理:removeLayer与destroy的正确用法

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

2026/10/17 5:08:18 阅读更多 →
固定翼无人机俯仰姿态控制中的模糊神经网络FNN控制器Matlab仿真详解

固定翼无人机俯仰姿态控制中的模糊神经网络FNN控制器Matlab仿真详解

做固定翼无人机的俯仰姿态控制仿真,是我这几年搭过的最容易"写出一千行代码、最后发现全白搭"的方向之一。不是算力问题,也不是建模工具问题,而是飞行对象本身太善变:空速一变,气动导数跟着变;重…

2026/10/17 5:08:18 阅读更多 →
2020后台开发暑期实习全攻略:从简历到技术面复盘

2020后台开发暑期实习全攻略:从简历到技术面复盘

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

2026/10/17 5:08:18 阅读更多 →
鸿蒙跨平台开发最后一公里:KuiklyUI华为云真机部署实战

鸿蒙跨平台开发最后一公里:KuiklyUI华为云真机部署实战

做鸿蒙跨平台开发,写代码的阶段其实挺痛快的。用KuiklyUI这类框架把UI描述成一套声明式结构,再让它跑到多个端上,大部分时候你在预览器里看到的和最终效果差别不大。真正折磨人的是"最后一公里"——应用要装到真实的鸿蒙设备上看效…

2026/10/17 5:08:18 阅读更多 →
基于MP2642GR-Z与STM32L476RG的两串电芯被动均衡方案设计与实现

基于MP2642GR-Z与STM32L476RG的两串电芯被动均衡方案设计与实现

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

2026/10/17 5:07:18 阅读更多 →

日新闻

DeepSeek选型与部署:V3/R1、Ollama、API全攻略

DeepSeek选型与部署:V3/R1、Ollama、API全攻略

简介:《DeepSeek使用方法.pdf》是一份面向DeepSeek入门及进阶用户的完整使用指南,重点围绕V3与R1两款模型的能力差异与适用场景展开,帮助读者根据任务类型与成本选择合适的模型。资源共1个PDF文件,压缩包大小约311KB,篇…

2026/10/17 0:00:25 阅读更多 →
200+Deepseek润色指令拆解:从底层原理到场景落地的完整指南

200+Deepseek润色指令拆解:从底层原理到场景落地的完整指南

简介:这是一份聚焦DeepSeek提示词优化的《200Deepseek润色指令》PDF文档,共收录两百余条可直接套用的润色指令,涵盖语法修正、风格统一、逻辑梳理、表达精炼等维度,并针对商务写作、学术论文、广告文案等场景给出差异化建议。文档…

2026/10/17 0:00:25 阅读更多 →
OpenCV轮廓法物体计数系统:轻量、可控、可落地的工业级实现

OpenCV轮廓法物体计数系统:轻量、可控、可落地的工业级实现

简介:本资源是一份基于OpenCV的本科课程设计完整报告,面向计算机视觉初学者、图像处理学习者及高校通信/信息类专业学生,解决物体智能计数与检测结果结构化记录的实际问题。项目覆盖图像预处理、轮廓检测、边界框绘制、轨迹栏交互式参数调节、…

2026/10/17 0:00:25 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/16 21:35:51 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/17 1:44:59 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/16 15:04:24 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

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

2026/10/16 21:54:38 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

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

2026/10/16 3:49:43 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

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

2026/10/16 14:23:36 阅读更多 →