☰
软件SPI实战:用GPIO模拟时序驱动W25Q64 Flash读写
2026/10/21 4:41:13 网站建设 项目流程

简介:一套基于STM32F103的软件SPI读写W25Q64 Flash完整工程,面向学习SPI通信的单片机开发者,解决不使用硬件SPI外设时,如何用GPIO模拟时序完成Flash数据读写。案例以W25Q64为从机、STM32为主机,按SPI1外设引脚给出接线:PA4接CS、PA5接CLK、PA6接DO、PA7接DI,便于后续切换至硬件SPI继续学习。资源包共83个文件,以36个h头文件、33个c源文件为主,涵盖外设库、延时、按键、LED、OLED及W25Q64驱动等模块,另有启动文件、工程配置和接线图,压缩包仅1004KB,结构清晰,适合直接编译或移植复用。已有2052人学习,可掌握软件SPI时序、W25Q64读写流程及工程模块划分方法。

1. 软件SPI总线:为什么非要用GPIO硬拼出W25Q64的时序

做单片机开发久了你会发现一个反直觉的结论:硬件SPI外设并不总是最省事的选择。某次我在一块开发板上调试W25Q64 flash存储器,硬件SPI怎么都读不出正确的JEDEC ID,示波器抓波形发现时钟线毛刺严重,翻遍了芯片手册才发现是引脚复用冲突加上时钟极性配置被库函数默认值坑了。换了软件SPI——直接用GPIO模拟时序——十分钟就跑通了。这篇就拆一下这个思路:不依赖STM32的SPI外设,用普通GPIO模拟出W25Q64所需的全部时序,实现读写flash存储器。适合刚接触STM32 SPI通信、或者被硬件SPI折腾到想换方案的单片机开发者,也适合想彻底搞懂SPI时序本质的嵌入式入门者。

2. 整体架构与工程结构:软件SPI的层次设计

2.1 为什么选W25Q64和软件SPI

W25Q64是华邦生产的SPI NOR Flash,容量64Mbit也就是8MByte,256字节为一页,4KB为一个扇区,64KB为一个块。这个容量在嵌入式里非常百搭——存字库、存配置参数、存OTA固件都够用。它支持标准SPI、双输出、四输出等多种模式,但最基础、最通用的还是标准SPI时序,这就给了软件模拟一个很好的切入点。

选择软件SPI而不是硬件SPI,通常出于三个考虑。第一是引脚自由度,硬件SPI往往绑定固定引脚,PCB布线不方便的时候软件SPI可以随意映射到任意GPIO;第二是抗干扰调试便利,软件SPI的时序完全由代码控制,时钟极性和相位怎么配、每bit之间延时多久都可以现场调,出问题可以直接用逻辑分析仪对比代码逻辑;第三是某些芯片的SPI时序比较特殊,硬件外设适配起来反而麻烦,比如需要在时钟中间切换数据线方向的双向IO场景。

我之前在一个项目里用过某款带SPI接口的传感器,硬件SPI死活读不到正确数据,换成软件SPI把时钟拉慢到几百kHz,一次就读通了。这不是说硬件SPI不好,而是软件SPI在调试阶段确实更有掌控感。

2.2 引脚分配与接线

软件SPI的接线比硬件SPI直观很多,四根线加电源地线,全部接到普通GPIO上就行。这里以某常见开发板为例,我选用了PA4、PA5、PA6、PA7四个引脚,对应关系如下:

信号引脚GPIO模式说明
CSPA4推挽输出片选,低电平有效
CLKPA5推挽输出时钟线,空闲时为低
MOSIPA7推挽输出主机输出从机输入
MISOPA6浮空输入或上拉输入主机输入从机输出
VCC3.3V-W25Q64供电
GNDGND-共地

注意MISO这根线,软件SPI下它不需要配置为复用功能,普通输入模式就够了。但是建议配置为上拉输入,因为W25Q64的DO引脚在片选拉高后会变成高阻态,如果没有上拉电阻,读数据时第一个bit可能是随机电平。

这里有个常见误用:有人把MISO也配成推挽输出,读数据的时候两个输出短接在一起,轻则读数错误,重则烧引脚。读方向时MISO必须是输入,写方向时MOSI是输出,这两根线的方向是固定的,不需要像I2C那样切换方向,这是SPI比I2C简单的地方。

2.3 工程目录结构解析

一个典型的软件SPI读写W25Q64工程,目录结构大致如下:

Project/ ├── Core/ │ ├── Inc/ │ │ ├── main.h │ │ ├── soft_spi.h │ │ └── w25q64.h │ └── Src/ │ ├── main.c │ ├── soft_spi.c │ └── w25q64.c ├── Drivers/ │ ├── CMSIS/ │ └── STM32F1xx_HAL_Driver/ ├── Hardware/ │ └── usart.c └── MDK-ARM/

重点看soft_spi.c和w25q64.c这两个文件。soft_spi.c负责最底层的GPIO模拟时序,只做四件事:起始、结束、读写一个字节;w25q64.c是基于底层时序封装出来的芯片操作层,包含读ID、擦除、页编程、读数据等功能。这种分层方式非常重要,后续如果要把软件SPI换成硬件SPI,只需要改soft_spi.c,上面的w25q64.c一行都不用动。

main.c里则是测试入口,初始化GPIO、初始化串口、调用W25Q64的读写函数,把结果通过串口打印到PC端观察。整个工程没有复杂的RTOS依赖,裸机一套跑下来,非常适合初学者理解完整的数据流。

3. 核心代码实现:从GPIO模拟到读写数据的完整链路

3.1 软件SPI的时序基础

SPI时序的基础是四个相位:时钟极性(CPOL)和时钟相位(CPHA)。W25Q64支持模式0和模式3,我们常用模式0,即CPOL=0、CPHA=0,意思是空闲时时钟为低电平,数据在时钟上升沿采样,在下降沿改变。

这个细节对软件模拟至关重要。因为你是用GPIO手掰时序,不是硬件自动处理,必须明确知道:什么时候拉高时钟、什么时候数据线上放数据、什么时候采样。搞反了数据就会错位。

// W25Q64要求的数据时序:MSB先行,时钟上升沿采样 // 写数据时,在时钟上升沿之前把数据放到MOSI上 // 读数据时,在时钟下降沿之后从MISO上读取

实际调试中,我习惯先用逻辑分析仪抓一次读ID的波形,对照芯片手册确认时钟极性和数据采样点是否正确。这一步省下来,后面排查问题会花十倍时间。

3.2 GPIO初始化与SPI起始/结束

先看底层GPIO初始化。软件SPI的核心就是把这几个引脚设置成普通GPIO,开启时钟,配置模式。

void Soft_SPI_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // CS、CLK、MOSI配置为推挽输出,速度50MHz GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); // MISO配置为上拉输入 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; GPIO_Init(GPIOA, &GPIO_InitStructure); // 空闲状态:CS拉高,CLK拉低 GPIO_SetBits(GPIOA, GPIO_Pin_4); GPIO_ResetBits(GPIOA, GPIO_Pin_5); }

这里的重点是CS空闲时必须拉高,CLK空闲时拉低,对应模式0的CPOL=0。GPIO速度配50MHz是为了保证边沿足够陡峭,如果配成低速模式,时钟频率提上来之后波形会变圆,可能触发误采样。

SPI通信的起始和结束很简单,就是控制CS这根片选线。CS拉低表示主机开始与从机通信,CS拉高表示通信结束。

void Soft_SPI_Start(void) { GPIO_ResetBits(GPIOA, GPIO_Pin_4); // CS拉低,选中W25Q64 } void Soft_SPI_Stop(void) { GPIO_SetBits(GPIOA, GPIO_Pin_4); // CS拉高,释放W25Q64 }

有人可能会问,CS拉高之后W25Q64的DO引脚会变成高阻态,这个要不要处理?不需要,硬件上高阻态就相当于断开了,不会影响MOSI上的数据。

3.3 SPI读写一个字节

读写一个字节是整个软件SPI最核心的函数,也是唯一真正模拟时序的地方。SPI是同步全双工协议,主机发一个bit的同时会收到一个bit,所以读和写在同一个函数里完成。

uint8_t Soft_SPI_ReadWriteByte(uint8_t data) { uint8_t i; uint8_t receive = 0; for (i = 0; i < 8; i++) { // 先拉低时钟,准备发送数据 GPIO_ResetBits(GPIOA, GPIO_Pin_5); // 发送当前bit:MSB先行 if (data & 0x80) GPIO_SetBits(GPIOA, GPIO_Pin_7); else GPIO_ResetBits(GPIOA, GPIO_Pin_7); data <<= 1; // 给一点延时,让数据稳定 Soft_SPI_Delay(); // 拉高时钟,上升沿采样数据 GPIO_SetBits(GPIOA, GPIO_Pin_5); // 在上升沿读取MISO引脚 if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6)) receive |= 0x01; receive <<= 1; Soft_SPI_Delay(); } // 最后拉低时钟,结束一个字节的传输 GPIO_ResetBits(GPIOA, GPIO_Pin_5); return receive; }

逻辑说明:每个bit的传输分为四步——拉低时钟、在MOSI上放数据、拉高时钟模拟上升沿、在上升沿后读取MISO。发送的数据从最高位开始,每发送一个bit左移一次;接收的数据也是从最高位开始累积,每次或上一个bit后左移。循环8次,完成一个字节。

参数说明:data是主机要发送的字节,如果只是读取数据(比如读ID操作),传入0xFF即可,因为SPI协议里主机不发时钟从机就无法输出数据,发送0xFF只是提供时钟信号。

Soft_SPI_Delay()是一个简单的空循环延时函数,控制SPI时钟频率。在调试初期我一般把延时调大一点,让时钟降到几百kHz,方便逻辑分析仪抓波形;确认时序正确后再逐步减小延时提高速度。这个延时也可以不用,直接让GPIO翻转的固有延迟充当时序间隔,但那样时钟频率就不可控了。

3.4 读取JEDEC ID

读取JEDEC ID是验证SPI通信是否正确的第一步,也是最有成就感的步骤——如果ID读对了,说明底层时序没问题,后面操作flash就有底气。W25Q64的JEDEC ID指令是0x9F,发送该指令后芯片会返回三个字节:厂商ID、存储器类型、容量ID。W25Q64的三字节固定是0xEF、0x40、0x17。

void W25Q64_ReadID(uint8_t *mid, uint8_t *type, uint8_t *did) { Soft_SPI_Start(); Soft_SPI_ReadWriteByte(0x9F); // 发送JEDEC ID读取指令 *mid = Soft_SPI_ReadWriteByte(0xFF); // 读取厂商ID,如0xEF *type = Soft_SPI_ReadWriteByte(0xFF); // 读取存储器类型,如0x40 *did = Soft_SPI_ReadWriteByte(0xFF); // 读取容量ID,如0x17 Soft_SPI_Stop(); }

逻辑说明:发送0x9F指令后,W25Q64会从DO引脚连续输出三个字节的设备信息。主机只需要持续发送0xFF产生时钟,同时读取MISO数据。Soft_SPI_Stop()拉高CS结束通信。

这里有个细节值得注意:读ID之前不需要发送写使能指令,也没有写保护的限制,因为读操作不改变flash内容。如果读ID都是0xFF,说明时序大概率有问题,常见原因是CLK极性和数据采样点不对。

3.5 写使能与页编程

写入数据是W25Q64操作中最复杂的一步,因为flash写入前必须保证目标区域是擦除状态。W25Q64的写入单位是页,一页256字节。写数据之前要发送写使能指令0x06,让状态寄存器的WEL位变成1,否则写操作会被忽略。

void W25Q64_WriteEnable(void) { Soft_SPI_Start(); Soft_SPI_ReadWriteByte(0x06); // 写使能指令 Soft_SPI_Stop(); } void W25Q64_PageProgram(uint32_t addr, uint8_t *data, uint16_t len) { uint8_t i; W25Q64_WriteEnable(); Soft_SPI_Start(); Soft_SPI_ReadWriteByte(0x02); // 页编程指令 Soft_SPI_ReadWriteByte((addr >> 16) & 0xFF); // 地址高字节 Soft_SPI_ReadWriteByte((addr >> 8) & 0xFF); // 地址中字节 Soft_SPI_ReadWriteByte(addr & 0xFF); // 地址低字节 // 写入数据 for (i = 0; i < len; i++) { Soft_SPI_ReadWriteByte(data[i]); } Soft_SPI_Stop(); }

逻辑说明:页编程指令0x02后面跟24位地址,然后是待写入的数据。W25Q64内部会在页缓冲区内写入数据,然后再编程到存储阵列。注意一次最多只能写256字节,如果地址跨越了页边界,数据会回卷到页首,这就需要额外处理。

参数说明:addr是24位地址,范围0x000000到0x7FFFFF,对应8MB空间。data是待写入数据的缓冲区指针,len是数据长度,最大256。地址分解为三个字节发送时要注意移位和掩码的配合,(addr >> 16) & 0xFF取最高字节,addr & 0xFF取最低字节。

写使能操作每次写操作前都要执行,读操作不需要。如果写完发现数据不对,先检查是不是忘了调W25Q64_WriteEnable()。

3.6 读取数据

相比写入,读取就简单多了,不需要擦除、不需要写使能,直接发送读数据指令0x03加24位地址,然后持续读字节就行。

void W25Q64_ReadData(uint32_t addr, uint8_t *buf, uint16_t len) { uint16_t i; Soft_SPI_Start(); Soft_SPI_ReadWriteByte(0x03); // 读数据指令 Soft_SPI_ReadWriteByte((addr >> 16) & 0xFF); Soft_SPI_ReadWriteByte((addr >> 8) & 0xFF); Soft_SPI_ReadWriteByte(addr & 0xFF); // 连续读取len个字节 for (i = 0; i < len; i++) { buf[i] = Soft_SPI_ReadWriteByte(0xFF); // 发送0xFF产生时钟 } Soft_SPI_Stop(); }

逻辑说明:读数据指令0x03后面跟24位地址,然后主机会连续发送0xFF来产生时钟脉冲,W25Q64从DO引脚逐bit输出数据,主机在每个时钟上升沿采样。这个操作可以连续读取多个字节,地址自动递增,直到CS拉高停止。

注意buf缓冲区必须由调用者分配足够空间,函数本身不做越界检查。我之前就犯过这个错误——读256字节数据,缓冲区只开了100字节,结果后面的变量全被冲掉了。嵌入式开发中这种低级错误最容易让人血压升高。

3.7 状态寄存器与忙等待

flash编程和擦除都需要时间,W25Q64内部有一个忙状态位(BUSY),位于状态寄存器的bit 0。写入或擦除期间这个位为1,完成后变为0。主机需要不断读取状态寄存器,直到BUSY位清零才能进行下一次操作。

void W25Q64_WaitBusy(void) { uint8_t status; Soft_SPI_Start(); Soft_SPI_ReadWriteByte(0x05); // 读状态寄存器指令 do { status = Soft_SPI_ReadWriteByte(0xFF); } while (status & 0x01); // 等待BUSY位清零 Soft_SPI_Stop(); }

逻辑说明:读状态寄存器指令0x05后面不跟地址,主机发送0xFF读取状态寄存器的值。status & 0x01判断BUSY位是否为1,如果为1说明flash还在忙,继续循环读取,直到为0。

这个函数看着简单,实际坑不少。如果读ID正常但写入不成功,多半是WaitBusy这里出了问题。比如读状态寄存器时CS拉高早了,或者时钟极性和数据采样点没对上,读到的状态永远不对,导致死循环。在等待循环里加一个超时计数是更稳妥的做法,防止flash异常时程序卡死。

4. 运行与验证:从编译到串口打印实验

4.1 工程配置与编译

拿到工程后第一步是配置编译环境。这里以Keil MDK为例,也可以换成IAR或者STM32CubeIDE,代码本身不挑编译器。打开工程后先检查芯片型号是否选对——用的是STM32F103系列就选对应的Device,用的是其他系列就替换启动文件和设备头文件。

编译前需要在魔术棒选项卡里检查两个配置:一个是C语言标准,建议选C99,因为代码里用了在for循环内声明变量的写法,C89标准会报错;另一个是宏定义,如果用HAL库版本,需要确保USE_HAL_DRIVER和STM32F103xE这类芯片宏有定义。标准外设库版本则不需要。

配置完成后直接编译,如果代码是从头开始写的,大概率会遇到一些基础错误。最常见的是GPIO时钟没使能就调用GPIO_Init,这时候程序会跑飞或者直接HardFault。检查的顺序是:时钟使能 → 引脚配置 → 外设初始化,这个顺序不能乱。

4.2 测试代码设计

在main函数里,我习惯的测试流程是:初始化LED和串口 → 读取JEDEC ID并打印 → 擦除一个扇区 → 写入一组测试数据 → 读回数据并比对 → 打印测试结果。

int main(void) { uint8_t mid, type, did; uint8_t write_buf[256]; uint8_t read_buf[256]; uint8_t i; delay_init(); USART_Init(115200); Soft_SPI_Init(); // 第一步:读取JEDEC ID W25Q64_ReadID(&mid, &type, &did); printf("MID:0x%02X, Type:0x%02X, DID:0x%02X\r\n", mid, type, did); // 第二步:擦除扇区0,地址0x000000 W25Q64_EraseSector(0x000000); W25Q64_WaitBusy(); // 第三步:写入测试数据 for (i = 0; i < 256; i++) { write_buf[i] = i; } W25Q64_PageProgram(0x000000, write_buf, 256); W25Q64_WaitBusy(); // 第四步:读回数据并校验 W25Q64_ReadData(0x000000, read_buf, 256); for (i = 0; i < 256; i++) { if (write_buf[i] != read_buf[i]) { printf("Compare Error at %d\r\n", i); break; } } if (i == 256) { printf("Test Passed!\r\n"); } while(1); }

逻辑说明:整个测试流程覆盖了读ID、擦除、写入、读回比对四个核心操作。测试数据从0到255写入每个字节,如果擦除不彻底或者写入时序有问题,比对过程会立刻暴露出来。

这里有个值得注意的地方:擦除操作必须在写入之前执行,而且擦除的单位是扇区而不是页。如果只往一个没擦除过的地址写入数据,读回的可能是旧数据和写入数据按位与的混合结果,看起来就像写入失败。

4.3 串口打印与结果判断

串口打印是嵌入式开发中最常用的调试手段。测试代码里用printf输出结果,需要把printf重定向到串口,标准做法是在代码里实现fputc函数:

int fputc(int ch, FILE *f) { USART_SendData(USART1, (uint8_t)ch); while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); return ch; }

注意在Keil里要勾选微库(MicroLib),否则printf会占用大量代码空间,F103这种小容量芯片很容易爆flash。

连接到PC后打开串口助手,波特率设置和代码里一致(我一般用115200),复位开发板,串口助手应该能看到三行输出。第一行是JEDEC ID,正常情况是0xEF 0x40 0x17;第二行是测试结果,如果写入和读回一致会打印Test Passed!。

如果ID读出来全是0xFF,先别慌,按经验排查三步:先检查SPI四根线的接线有没有松动,特别是CS和CLK;再用逻辑分析仪抓CLK和MOSI的波形,看有没有正常的脉冲输出;最后检查代码里GPIO配置,MOSI是不是被配置成了输入模式。这三步走完,90%的问题都能解决。

5. 避坑指南:软件SPI调试中的常见问题与排查

软件SPI的调试看起来简单,实际踩坑不少。我总结了四个高频问题,每个都是真实调试经历的血泪经验。

坑一:读ID全是0xFF,一个有效数据都没有

现象方面,调用W25Q64_ReadID之后打印结果是FF FF FF,或者最后一个字节能看到点变化。

原因通常是以下几种:CS片选时序不对,发送指令之前CS没拉低,或者指令发送完CS就拉高了,导致W25Q64认为通信没开始就已经结束;MISO引脚配置错误,比如配成了输出模式,读到的永远是高电平;还有一种可能是时钟极性和相位跟芯片期望的不一致。

解决方法是先确认接线没问题,然后用逻辑分析仪抓时序。看CS是否在指令发送前拉低、数据是否在时钟上升沿稳定、时钟空闲时是否为低。如果没有逻辑分析仪,可以降低SPI时钟频率,把延时函数调大,再用万用表量MISO引脚看是否有电平变化。

坑二:写入后读回全是0xFF,写入看起来失败了

现象方面,擦除操作正常,但写入的数据读不回来,打印比对结果全是错误。

原因大概率是忽略了写使能。W25Q64的写操作前必须发送0x06指令设置WEL位,否则芯片会忽略写操作。另一个常见原因是往没擦除的区域写入数据,flash的编程操作只能把1变成0,不能把0变成1,所以未擦除区域写入就会得到意料之外的结果。

解决方法是每次写操作前调用W25Q64_WriteEnable(),并且在写之前先执行扇区擦除。确认这两个步骤做到了,写入问题基本就解决了。

坑三:页编程超过256字节边界,数据错乱

现象方面,写入超过256字节后,超出的数据没有写到期望地址,而是写到了页首位置。

原因是W25Q64的页编程原理决定的。一页256字节,如果写入的数据跨越页边界,芯片会把超出部分回卷到当前页的起始地址,覆盖之前写入的数据。

解决方法是把写入逻辑做分页处理。比如写300字节,先写第一页256字节,再写第二页44字节,每次写入前判断剩余空间是否足够,不够就先写满当前页,再切换到下一页。嵌入式里处理这种逻辑我一般封装一个跨页写入函数,上层调用时不用关心页边界,函数内部自动分段。

坑四:WaitBusy死循环,程序卡死在等待状态

现象方面,页编程或者擦除之后调用W25Q64_WaitBusy(),程序跑不到下一行。

原因是读状态寄存器的时序有问题。比如读状态指令0x05后面少发了产生时钟的0xFF字节,或者CS拉高早了,导致读到的状态值永远是0x00,BUSY位永远为0,但flash实际上还在写。也可能是flash芯片虚焊或者供电不稳定,导致状态寄存器异常。

解决方法是先检查读状态时序是否符合预期,然后在WaitBusy里加一个超时计数器,比如循环100万次还没等到底就报错退出,避免程序死磕到底。硬件方面检查flash供电是否稳定,3.3V有没有明显纹波。

6. 二次开发:把软件SPI改造成可复用、可扩展的存储层

6.1 参数化改造:换引脚不改代码

工程跑通之后,第一个值得做的改造是把引脚定义从代码里抽出来。现在把GPIO操作直接写在函数里,换个引脚就得改一大片代码。常见的做法是用宏把引脚封装起来:

#define SPI_CS_PIN GPIO_Pin_4 #define SPI_CLK_PIN GPIO_Pin_5 #define SPI_MOSI_PIN GPIO_Pin_7 #define SPI_MISO_PIN GPIO_Pin_6 #define SPI_GPIO_PORT GPIOA

这样换引脚的时候只需要修改宏定义,函数体里的逻辑不用动。更进一步可以把这个宏定义放到一个单独的头文件里,移植到其他板子时只改这个文件。这是嵌入式工程最值得养成的习惯——硬件相关的代码尽可能收敛到一个位置。

6.2 向上抽象:提供统一存储接口

第二个值得做的改造是加一层存储抽象。现在上层逻辑直接调用W25Q64_ReadData和W25Q64_PageProgram,如果哪天要换存储介质,比如换成SD卡或者内部flash,所有上层代码都要重写。

我一般会定义一个简单的存储接口结构体:

typedef struct { void (*init)(void); int (*read)(uint32_t addr, uint8_t *buf, uint16_t len); int (*write)(uint32_t addr, uint8_t *buf, uint16_t len); int (*erase)(uint32_t addr); } storage_ops_t;

再写一个全局实例指向W25Q64的具体实现函数,上层代码只跟这个结构体打交道。这样之后换存储介质只需要新增一个实现文件,替换实例指针,上层逻辑不用动。这个抽象能省下后面无数改动存储方案的返工时间。

6.3 扩展方向:从裸机到FATFS文件系统

W25Q64加上软件SPI之后,最自然的扩展方向是挂一个文件系统。把flash的读写擦除接口适配到FATFS的底层磁盘驱动接口,就能在8MB的flash上创建文件、写日志、存配置。FATFS的底层接口需要实现disk_initialize、disk_read、disk_write、disk_ioctl这几个函数,适配起来并不复杂,W25Q64的扇区擦除4KB对应FATFS的擦除粒度也正合适。

另一个方向是做掉电安全的参数存储。把W25Q64按扇区管理,用双扇区备份加校验和的方式存储系统参数,掉电时即使写入一半,下次启动也能从备份扇区恢复。这个需求在产品化项目里很常见,比裸写flash要实用得多。

从那以后我每次拿到一块新板子,第一件事就是写一个软件SPI的读写demo,先把芯片时序跑通,再考虑要不要切到硬件SPI。这个习惯帮我避开了很多硬件外设引脚的坑。希望这篇拆解对你也有点帮助。

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

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

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

立即咨询