此博客是一份详细的教程,涵盖 STM32F103C8T6 驱动 W25Q64 存储器的完整过程。
教程包括:
- W25Q64 的工作原理和 SPI 通信协议详细讲解。
- STM32F103C8T6 的 SPI 外设配置方式。
- 硬件连接示意图,确保正确接线。
- 使用 HAL 库和标准外设库(SPL)分别实现 SPI 通信。
- 详细的 C 代码,包括丰富的注释,便于理解。
- W25Q64 读取、写入、擦除等操作的示例代码。
- 常见问题排查与优化建议。
1. W25Q64 介绍与 SPI 通信协议
W25Q64简介: Winbond W25Q64是一款容量为64M-bit(8M字节)的串行NOR Flash存储器,工作电压2.7V~3.6V,典型供电3.3V (STM32标准库——(16)SPI通信协议、W25Q64简介-优快云博客)。作为掉电不丢失数据的Flash,它内部采用页(Page)、**扇区(Sector)和块(Block)**等结构组织数据。W25Q64共有8,388,608个字节,按256字节为一页,内部共有32,768页 (W25Q64FV Datasheet)。16页构成一个4KB扇区,W25Q64共有2,048个可擦除扇区 (W25Q64FV Datasheet);16扇区构成一个64KB块,共128个块 (W25Q64FV Datasheet)。这种小扇区的设计使得在需要频繁更新参数的数据存储应用中更加灵活 (W25Q64FV Datasheet)。Flash存储器的擦写寿命通常在100,000次以上,数据保存期限可达20年 (W25Q64FV Datasheet),非常适合代码存储和数据日志等应用 (W25Q64FV Datasheet) (W25Q64FV Datasheet)。
SPI接口与通信协议: W25Q64通过标准的SPI接口与主机通信,包括四条主要信号线:CS(芯片选择,使能信号)、CLK(串行时钟)、MOSI/DI(主出从入,向Flash发送数据)和MISO/DO(主入从出,从Flash读取数据) (STM32标准库——(16)SPI通信协议、W25Q64简介-优快云博客)。此外还有两个控制引脚**/WP**(写保护)和**/HOLD**(挂起),用于硬件写保护和通信暂停等功能 (STM32标准库——(16)SPI通信协议、W25Q64简介-优快云博客)(如果不使用这些功能,应将/WP和/HOLD拉高以避免意外触发)。SPI总线为全双工同步通信,由主设备(STM32)产生时钟并发起数据传输。CS信号由主机拉低以选中从机,保持低电平期间通信有效;通信完成后将CS拉高取消选中,从机停止响应 (W25Q Flash Series || Part 1 || Read ID)。CLK时钟在通信时持续振荡,W25Q64支持最高约104 MHz的时钟频率(标准SPI模式) (W25Q64FV Datasheet)。W25Q64兼容SPI模式0和模式3,两种模式都被支持 (W25Q Flash Series || Part 1 || Read ID)。通常采用SPI模式0(CPOL=0, CPHA=0),即时钟空闲时为低电平,数据在时钟的上升沿采样 (W25Q Flash Series || Part 1 || Read ID)。在模式0下,主机在每个时钟上升沿读取从机输出的数据,在下降沿发送下一位数据,这保证从机能在上升沿前稳定输出数据位。
SPI时序和信号关键点: 下图(来自W25Q64数据手册)展示了读取指令的SPI时序示意(模式0): (W25Q Flash Series || Part 1 || Read ID)
(image) W25Q64 SPI模式0时序:时钟空闲低电平,CS拉低后,主机在每个CLK上升沿捕获数据、下降沿改变数据。图中显示发送命令及地址后,从Flash输出数据的时序。
-
时序开始与结束: 当主机将CS拉低时,Flash从机检测到片选有效,SPI传输开始;当CS拉高时,传输结束。从CS下降沿开始,主机送出的第一个字节通常是指令码。期间CLK开始振荡,按照CPHA设定在相应时刻采样数据。对于模式0,在CS下降后,CLK第一个上升沿时就采集第1位数据。通信期间若需要临时中断SPI而不断开CS(例如总线挂起),可以将HOLD引脚拉低,Flash将暂停并保持当前状态 (STM32标准库——(16)SPI通信协议、W25Q64简介-优快云博客)。恢复时将HOLD拉高,通信可继续进行。
-
数据交换: SPI总线每发送/接收1字节数据需要8个时钟周期。主机通过MOSI在每个时钟周期发送1位,从机通过MISO同步返回1位数据。以读操作为例:主机先发送读取指令码和目标地址,然后继续产生时钟并发送“占位”字节(例如0xFF),Flash会在这些时钟下通过MISO输出数据 (STM32标准库——(16)SPI通信协议、W25Q64简介-优快云博客)。写操作时则相反,主机发送指令和地址后,继续通过MOSI发送要写入的数据,从机接收并在内部执行写入。多字节传输通常是连续的,CS保持拉低,主机可以连续发送多个字节而无需在每字节间拉高CS。对于地址,W25Q64使用24位地址总线,需要发送3字节地址(高位先传)才能指定存储位置 (STM32标准库——(16)SPI通信协议、W25Q64简介-优快云博客)。例如,要访问地址0x123456,需要依次发送0x12、0x34、0x56三个字节的地址信息 (STM32标准库——(16)SPI通信协议、W25Q64简介-优快云博客)。
-
关键寄存器: W25Q64内部有状态寄存器(Status Register),包含重要状态位 (STM32标准库——(16)SPI通信协议、W25Q64简介-优快云博客)。其中**BUSY (WIP)**位表示芯片是否正忙于写入/擦除(1为忙,0为空闲),WEL位表示写使能锁存标志(Write Enable Latch,1表示已使能写入操作) (W25Q64FV Datasheet)。在执行写入、擦除等指令后,Flash会将BUSY位置1并开始内部操作,同时WEL自动清零(禁用进一步写操作)直到当前操作完成 (W25Q64FV Datasheet) (W25Q64FV Datasheet)。因此,每次写或擦除前需要发送写使能指令设置WEL=1,且在写/擦除后需要通过读取状态寄存器监视BUSY位,等待其清0表示操作完成。
常用SPI指令: W25Q64提供了丰富的SPI指令集,常用指令及功能如下:
-
读取类指令:
- 读取数据 (Read Data 0x03): 最基本的读取命令。主机发送0x03后,紧接3字节地址,然后从机开始输出该地址起的数据。此命令无需“空等待”周期,但速度相对受限。
- 快速读取 (Fast Read 0x0B): 与0x03类似,但在地址后三主机需再发送1个哑字节(Dummy Byte)以等待从机准备数据,然后Flash以更高速度连续输出数据。这允许在更高SPI时钟下可靠读取数据。
- 读取状态寄存器 (RDSR 0x05): 读取状态寄存器的值,返回8位状态位,用于判断WIP(BUSY)和WEL等状态。
- 读取ID (JEDEC ID 0x9F): 读取制造商ID和设备ID,用于识别芯片型号。例如Winbond制造商ID通常为0xEF (W25Q64FV Datasheet)。
-
写入及擦除类指令:
- 写使能 (WREN 0x06): 所有写或擦除操作前必须执行的指令,设置WEL=1以打开写许可 (W25Q64FV Datasheet)。不执行WREN则后续写操作会被忽略。对应的写禁用指令为0x04,可将WEL清0恢复写保护状态。
- 页编程 (Page Program 0x02): 写入数据的命令。主机发送0x02 + 24位地址,然后发送<=256字节的数据,Flash将这些数据写入指定地址所在的页。注意如果写入数据长度超过页剩余空间,会在页开头循环覆盖(因此应避免跨页写入)。
- 扇区擦除 (Sector Erase 0x20): 擦除4KB扇区的命令。发送0x20 + 24位目标地址(任意该扇区内地址即可),Flash擦除对应的整扇区(将该扇区所有位恢复为1,即0xFF)。擦除操作耗时较长(几个毫秒到数十毫秒不等)且期间Flash忙碌,不可接受其他操作。
- 块擦除 (Block Erase 32KB:0x52 / 64KB:0xD8): 擦除更大块区域的指令,原理同上但范围更大,耗时更长。
- 芯片擦除 (Chip Erase 0xC7 或 0x60): 擦除整个芯片的所有数据,耗时最多(可能达数秒),一般用于大量数据清空。
- 写状态寄存器 (WRSR 0x01): 可写入状态寄存器,用于设置保护位等(需要先执行WREN)。通常用于全片写保护配置,不常用。
每条指令的具体时序可参考W25Q64官方数据手册。发送写入/擦除指令后,主机应通过RDSR读取状态寄存器轮询WIP(BUSY)位,当其从1变为0时表示操作完成 (W25Q64FV Datasheet)。例如,页编程大约在数毫秒内完成,芯片擦除可能需要几秒。只有在BUSY=0空闲状态且WEL已重新置0后,才能进行下一次写使能或读写操作。
2. STM32F103C8T6 SPI 配置
SPI简介(主从模式、极性相位): SPI(Serial Peripheral Interface)是一种高速全双工同步串行总线,由主设备控制时钟 (STM32标准库——(16)SPI通信协议、W25Q64简介-优快云博客)。主设备(STM32)通过时钟信号协调数据传输,数据可同时在MOSI和MISO线上双向传输。STM32F103C8T6上集成有SPI外设,既可配置为主机模式,也可作为从机模式。本文场景中STM32作为主机,W25Q64作为从机。SPI有4种模式(Mode0~3)取决于时钟极性CPOL和相位CPHA设置。CPOL决定时钟空闲状态电平,高电平空闲(CPOL=1)或低电平空闲(CPOL=0);CPHA决定数据采样时机,在第1个沿采样(CPHA=0)或第2个沿采样(CPHA=1)。常用模式0(CPOL=0, CPHA=0):时钟空闲时为低,数据在上升沿采样 (W25Q Flash Series || Part 1 || Read ID)(对应STM32设置为CPHA_1Edge/CPOL_Low)。模式3则是CPOL=1, CPHA=1(空闲高电平,下降沿采样),在某些场合下Flash也支持模式3 (W25Q Flash Series || Part 1 || Read ID)。选错模式会导致数据位错位或读写错误,因此需与W25Q64要求匹配。根据数据手册和实践,W25Q64推荐使用SPI Mode0 (W25Q Flash Series || Part 1 || Read ID)。
STM32 SPI外设配置: 在STM32F103C8T6上使用SPI外设,需要按以下步骤配置:
-
时钟使能: 开启SPI模块和相关GPIO的时钟。以SPI1为例,应使能
RCC_APB2Periph_SPI1
以及SPI引脚所在GPIO端口(例如GPIOA)的时钟。使用STM32Cube HAL库时,CubeMX会自动在HAL_SPI_Init
前使能时钟;使用标准库(SPL)则需要调用RCC_APB2PeriphClockCmd
函数手动开启。 -
引脚配置: 将SPI引脚设置为适当模式。STM32F103C8T6的SPI1默认引脚为PA5 (SCK)、PA6 (MISO)、PA7 (MOSI),以及可选的PA4 (NSS)。需要将PA5和PA7配置为复用功能推挽输出(GPIO_Mode_AF_PP),PA6配置为输入浮空或上拉(GPIO_Mode_IN_FLOATING/GPIO_Mode_IPU)以接收MISO信号。若使用软件管理NSS片选(推荐),则PA4可作为普通GPIO手动控制CS信号。配置引脚的速度为50MHz以确保高速下信号完整。 (W25Q Flash Series || Part 1 || Read ID)
-
SPI参数配置: 设置SPI外设工作在主机模式,并根据W25Q64需求配置数据格式:
- 主从模式: 选择主机模式 (
SPI_Mode_Master
) (25. SPI—读写串行FLASH — [野火]STM32库开发实战指南——基于野火MINI开发板 文档)。 - 通信方向: 采用双线全双工 (
SPI_Direction_2Lines_FullDuplex
) (25. SPI—读写串行FLASH — [野火]STM32库开发实战指南——基于野火MINI开发板 文档)。 - 数据帧大小: 8位数据帧 (
SPI_DataSize_8b
) (25. SPI—读写串行FLASH — [野火]STM32库开发实战指南——基于野火MINI开发板 文档)。 - 时钟极性和相位: CPOL低电平,CPHA第一边沿,即模式0 (
SPI_CPOL_Low | SPI_CPHA_1Edge
) (W25Q Flash Series || Part 1 || Read ID)。 - NSS管理: 使用软件管理NSS (
SPI_NSS_Soft
) (25. SPI—读写串行FLASH — [野火]STM32库开发实战指南——基于野火MINI开发板 文档)。这样由软件控制GPIO来拉低/拉高CS,可灵活控制传输周期(硬件NSS模式在主机模式下要求NSS引脚一直为输出,高电平空闲,不易控制传输间歇)。 - 波特率分频: 设置合适的SPI时钟分频因子 (
SPI_BaudRatePrescaler_X
) (25. SPI—读写串行FLASH — [野火]STM32库开发实战指南——基于野火MINI开发板 文档)。SPI时钟频率=APB时钟/分频值。STM32F103的APB2时钟通常为72MHz,为保证信号稳定初期可用较低速率如9MHz(72MHz/8)或18MHz(72MHz/4)。待通信正常后可尝试提高速率,例如最高36MHz(72MHz/2),在线材短且信号完整情况下Flash可正常工作。 - 首位传输端: 设置为MSB先传 (
SPI_FirstBit_MSB
) (25. SPI—读写串行FLASH — [野火]STM32库开发实战指南——基于野火MINI开发板 文档)。W25Q64协议规定每字节高位先行。 - 关闭CRC校验(如有配置选项,可忽略或设为默认)。
使用HAL库,这些参数通过
SPI_HandleTypeDef.Init
结构体设置并调用HAL_SPI_Init()
应用配置。使用标准库,则填充SPI_InitTypeDef
结构并调用SPI_Init(SPI1, &initStruct)
完成配置。配置完成后,调用SPI_Cmd(SPI1, ENABLE)
开启SPI外设(HAL库中HAL_SPI_Init
内部已开启SPI)。 - 主从模式: 选择主机模式 (
-
通信初始化流程: 在主程序中,初始化步骤通常如下:
- 调用GPIO初始化函数配置SPI所需引脚模式(或使用CubeMX自动生成的
MX_SPI1_Init()
等函数,其中已包含GPIO和SPI配置)。 - 配置并使能SPI外设(主机模式,模式0等设置如上)。
- 将CS引脚(如PA4)配置为通用推挽输出,并设置为高电平(确保上电时Flash未被选中)。
- (如果使用DMA或中断,可额外配置相关NVIC或DMA,但基本通信可用轮询方式完成)。
- 调用GPIO初始化函数配置SPI所需引脚模式(或使用CubeMX自动生成的
完成以上初始化后,SPI总线即可使用。此时W25Q64应处于待机状态,CS保持高电平不响应总线信号。主机在每次读写Flash前应先将相应的CS拉低,发送指令和数据,操作完成后再拉高CS释放总线。
3. 硬件连接示意图
引脚连接: 下表列出了STM32F103C8T6 (SPI1接口) 与 W25Q64芯片的典型连接关系:
- W25Q64 VCC (引脚8) – 接 3.3V电源(确保供电稳定,靠近芯片加0.1µF去耦电容)。
- W25Q64 GND (引脚4) – 接 系统地(与STM32地相连)。
- W25Q64 /CS (引脚1) – 接 STM32 GPIO(例如PA4),作为SPI片选信号。低电平选中Flash,高电平空闲。 (STM32标准库——(16)SPI通信协议、W25Q64简介-优快云博客)
- W25Q64 CLK (引脚6) – 接 STM32 SPI_SCK (PA5引脚),SPI时钟线,由STM32产生。 (STM32标准库——(16)SPI通信协议、W25Q64简介-优快云博客)
- W25Q64 DI/MOSI (引脚5) – 接 STM32 SPI_MOSI (PA7引脚),主机输出数据线,连接Flash的DI输入。 (STM32标准库——(16)SPI通信协议、W25Q64简介-优快云博客)
- W25Q64 DO/MISO (引脚2) – 接 STM32 SPI_MISO (PA6引脚),Flash输出数据线,连接STM32的MISO输入。 (STM32标准库——(16)SPI通信协议、W25Q64简介-优快云博客)
- W25Q64 /WP (引脚3) – 写保护引脚,低电平有效。若不使用写保护功能,可将此引脚通过10k电阻上拉至3.3V(或直接接高)。这样确保Flash不会进入硬件写保护状态。
- W25Q64 /HOLD (引脚7) – 总线暂停引脚,低电平有效。通常不用该功能时,同样通过10k上拉至3.3V使其保持高电平 (W25Q Flash Series || Part 1 || Read ID)。这避免Flash通信被意外暂停。
上述连接示意如下:Flash的四个主要SPI引脚(/CS, CLK, DI, DO)分别连接到STM32的对应SPI引脚和CS控制引脚,另外两个控制引脚/WP和/HOLD拉高以保持正常操作。 (W25Q Flash Series || Part 1 || Read ID) (W25Q Flash Series || Part 1 || Read ID)。务必共地(将STM32的GND与Flash GND相连),保证信号有共同的参考基准。由于STM32F103C8T6的IO逻辑电平也是3.3V,与W25Q64匹配,因此不需要电平转换;但若使用5V MCU,则需采用电平移位器或限流电阻+二极管等手段保护Flash免受5V直接驱动。 (STM32标准库——(16)SPI通信协议、W25Q64简介-优快云博客)
电气特性注意事项: 确保W25Q64的供电稳定在规范范围内(2.7~3.6V) (STM32标准库——(16)SPI通信协议、W25Q64简介-优快云博客)。上电时Flash需要一定时间完成内部复位(典型上电复位时间在数毫秒内),初始化SPI通信前应延时上电稳定时间。连接线不宜过长,在高SPI时钟下如果连线过长或阻抗不匹配,可能产生信号完整性问题,必要时降低时钟频率或在CLK线串联阻值几十欧姆的阻尼电阻以减少振铃。/WP和/HOLD引脚上拉电阻建议使用 ~10kΩ。对于CS引脚,如果有多个从设备共享SPI总线,要确保同时只有一个CS为低,其余都拉高以避免冲突。
4. HAL库和 SPL 实现 SPI 通信
STM32既可以使用官方HAL库(硬件抽象层)也可以使用早期的SPL库(标准外设库)与W25Q64进行SPI通信。两种方式各有实现差异和优缺点 (学习STM32是用标准库好还是HAL库好?各自有什么优缺点?做项目应用哪种库比较合适?_stm32hal库和标准库哪个好-优快云博客):
-
使用HAL库: HAL库提供了高级别API,使SPI读写更简洁。开发者无需直接操作寄存器,而是通过函数调用完成数据传输。例如,使用HAL库读写W25Q64的流程如下:
- 配置并初始化
SPI_HandleTypeDef hspi
(例如hspi1),包括模式和引脚设置(通常由CubeMX自动生成)。 - 拉低CS引脚选中Flash:
HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET)
。 - 调用
HAL_SPI_Transmit
发送指令码和地址,例如:uint8_t cmd = 0x03; // Read Data 指令 HAL_SPI_Transmit(&hspi1, &cmd, 1, HAL_MAX_DELAY); HAL_SPI_Transmit(&hspi1, addr_bytes, 3, HAL_MAX_DELAY); // 发送24位地址
- 使用
HAL_SPI_Receive
读取数据,例如:
HAL库在Receive时会自动发送时钟并读取MISO数据(内部会发送默认的空字节,如0x00),接收到的数据保存在buffer中。HAL_SPI_Receive(&hspi1, buffer, length, HAL_MAX_DELAY);
- 拉高CS引脚结束通信:
HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET)
。
对于写操作,则类似地使用
HAL_SPI_Transmit
发送指令和数据。HAL库还提供HAL_SPI_TransmitReceive
可同时发送并接收数据,适合在发送的同时读取返回值的情况(例如读取状态寄存器时发送0x05后再发送一个空字节同时接收状态)。使用HAL库的优点是开发效率高、代码可读性好、兼容STM32全系列且易于移植 (学习STM32是用标准库好还是HAL库好?各自有什么优缺点?做项目应用哪种库比较合适?_stm32hal库和标准库哪个好-优快云博客)。缺点是封装较多,代码体积大且略有性能损耗(函数调用开销、逻辑判断较多) (学习STM32是用标准库好还是HAL库好?各自有什么优缺点?做项目应用哪种库比较合适?_stm32hal库和标准库哪个好-优快云博客)。但对于SPI这类高速外设,HAL的开销相对于总线速度来说通常可以接受。 - 配置并初始化
-
使用SPL库: 标准外设库直接提供对寄存器的封装,API相对低级。开发者需要手动控制数据发送和接收流程。下面演示使用SPL进行SPI收发的典型方法:
- 初始化SPI外设(主机模式、8位数据、模式0等)并使能。SPL使用
SPI_InitTypeDef
配置,如:SPI_InitTypeDef SPI_InitStructure; SPI_InitStructure.SPI_Mode = SPI_Mode_Master; SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; // ... 其他参数略 ... SPI_Init(SPI1, &SPI_InitStructure); SPI_Cmd(SPI1, ENABLE);
- 拉低CS引脚选中Flash:
GPIO_ResetBits(GPIOA, GPIO_Pin_4);
- 通过寄存器发送数据:调用
SPI_I2S_SendData(SPI1, data)
将数据写入SPI_DR发送寄存器 (SPI通信读取W25Q64-阿里云开发者社区)。然后轮询SPI_I2S_GetFlagStatus(SPI1, SPI_FLAG_TXE)
等待发送缓冲区空标志置位,表示数据已发出。 (SPI通信读取W25Q64-阿里云开发者社区) - 接收数据:在SPI全双工模式下,每发送一个字节,同时会从SPI_DR读取到一个字节(如果需要读取,需要先发送一个占位字节产生时钟)。例如:
上述代码发送0xFF并等待SPI_DR接收到从机回传的数据字节 (SPI通信读取W25Q64-阿里云开发者社区)。SPI_I2S_SendData(SPI1, 0xFF); // 发送空数据以产生时钟 while(SPI_I2S_GetFlagStatus(SPI1, SPI_FLAG_RXNE) == RESET); uint8_t recv = SPI_I2S_ReceiveData(SPI1);
- 根据需要重复发送/接收步骤完成整条指令的数据交换。最后拉高CS:
GPIO_SetBits(GPIOA, GPIO_Pin_4);
结束通信。
使用SPL需要开发者了解SPI状态位和数据寄存器的意义,比如TXE(发送缓冲空)和RXNE(接收缓冲非空)标志位,确保在正确的时机读取或写入数据寄存器。相比HAL,SPL代码量略多但更直观地反映硬件寄存器状态,优点是代码精简、效率高,直接操作寄存器开销小,适合对性能有要求的场合 (学习STM32是用标准库好还是HAL库好?各自有什么优缺点?做项目应用哪种库比较合适?_stm32hal库和标准库哪个好-优快云博客)。缺点是需要更深入的STM32寄存器知识,代码可移植性稍差;另外ST官方已停止更新F1以后的新器件SPL库,HAL成为新的官方标准。因此,在新的项目中,HAL更为常用,而老项目或资源受限场合仍可能见到SPL的身影。
- 初始化SPI外设(主机模式、8位数据、模式0等)并使能。SPL使用
两种方式的选择: 简单项目或对代码执行效率要求极高的场合,直接使用SPL或寄存器编程能够减小代码规模并提升速度;而HAL提供了丰富的封装和中间件(例如文件系统、RTOS接口等),适合更复杂的应用开发 (学习STM32是用标准库好还是HAL库好?各自有什么优缺点?做项目应用哪种库比较合适?_stm32hal库和标准库哪个好-优快云博客)。本教程后续的示例代码将主要采用HAL库实现W25Q64的读写操作,并给出相应注释说明。如果需要使用SPL,实现思路类似,读者可参考上述步骤将HAL调用替换为寄存器操作。
5. C代码实现(注释丰富)
下面提供使用STM32_HAL库驱动W25Q64的示例代码(针对SPI1接口),涵盖Flash初始化、读取、写入和擦除等功能。代码中包含详细的中文注释以解释每一步操作。请确保在使用该代码前已经正确配置并初始化SPI外设和相关GPIO(参考前述第2节内容)。注:实际工程中应根据需要拆分为.h
/.c
文件,这里为方便展示将关键部分汇总。
#include "stm32f1xx_hal.h" // HAL库头文件,根据具体工程路径调整
// 定义W25Q64相关指令码
#define W25Q64_CMD_READ_DATA 0x03 // 读取数据
#define W25Q64_CMD_FAST_READ 0x0B // 快速读取(需要dummy byte)
#define W25Q64_CMD_PAGE_PROGRAM 0x02 // 页编程(写数据)
#define W25Q64_CMD_SECTOR_ERASE 0x20 // 扇区擦除 (4KB)
#define W25Q64_CMD_BLOCK_ERASE_64K 0xD8 // 64KB块擦除
#define W25Q64_CMD_CHIP_ERASE 0xC7 // 芯片擦除 (C7h或60h均可)
#define W25Q64_CMD_WRITE_ENABLE 0x06 // 写使能
#define W25Q64_CMD_WRITE_DISABLE 0x04 // 写禁用
#define W25Q64_CMD_READ_STATUS 0x05 // 读状态寄存器
#define W25Q64_CMD_READ_JEDEC_ID 0x9F // 读JEDEC ID
// 定义状态寄存器中的标志位
#define W25Q64_STATUS_BUSY 0x01 // BUSY位(WIP, 写入/擦除进行中)
#define W25Q64_STATUS_WEL 0x02 // WEL位(写使能锁存)
// 假设CS引脚已配置为输出,这里定义控制CS的宏(根据实际连线调整GPIO端口和引脚)
#define FLASH_CS_HIGH() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET)
#define FLASH_CS_LOW() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET)
// 使用的SPI句柄(需确保已初始化)
extern SPI_HandleTypeDef hspi1; // 这里假设使用SPI1,如不同则修改名称
/**
* @brief 发送一个字节并接收一个字节数据(全双工传输)
* @param byte 要发送的字节
* @retval 收到的从机字节
* @note 可用于读取Flash返回的数据。对于纯发送可以忽略返回值,对于纯接收可发送0xFF占位。
*/
static uint8_t W25Q64_TransferByte(uint8_t byte)
{
uint8_t rxData = 0;
// HAL全双工发送接收一个字节,超时时间可以设置适当的数值
HAL_SPI_TransmitReceive(&hspi1, &byte, &rxData, 1, HAL_MAX_DELAY);
return rxData;
}
/**
* @brief 读取状态寄存器的值
* @retval 状态寄存器8位值
*/
static uint8_t W25Q64_ReadStatus(void)
{
uint8_t status = 0;
FLASH_CS_LOW(); // 拉低CS开始通信
uint8_t cmd = W25Q64_CMD_READ_STATUS;
HAL_SPI_Transmit(&hspi1, &cmd, 1, HAL_MAX_DELAY); // 发送读取状态寄存器命令0x05
HAL_SPI_Receive(&hspi1, &status, 1, HAL_MAX_DELAY); // 接收1字节状态值
FLASH_CS_HIGH(); // 拉高CS结束通信
return status;
}
/**
* @brief 等待芯片空闲
* @note 轮询状态寄存器的BUSY位,直到其为0或超时
*/
static void W25Q64_WaitBusy(void)
{
// 连续读取状态寄存器,等待BUSY位(WIP位)清0
// 为避免无限阻塞,这里可加入超时计数(实际应用中视情况处理)
while (W25Q64_ReadStatus() & W25Q64_STATUS_BUSY) {
// 可以选择在这里插入适当的延时
}
}
/**
* @brief 使能写操作
* @note 发送写使能命令0x06,使能后WEL置1
*/
static void W25Q64_WriteEnable(void)
{
FLASH_CS_LOW();
uint8_t cmd = W25Q64_CMD_WRITE_ENABLE;
HAL_SPI_Transmit(&hspi1, &cmd, 1, HAL_MAX_DELAY); // 发送0x06写使能指令
FLASH_CS_HIGH();
}
/**
* @brief 读取芯片JEDEC ID (制造商ID和设备ID)
* @param buf 存储ID的缓冲区,至少3字节
* @note 返回的ID通常3字节:制造商ID + 内存类型 + 容量ID。
* Winbond制造商ID一般为0xEF,容量ID 0x17对应64Mbit。
*/
void W25Q64_ReadID(uint8_t *buf)
{
FLASH_CS_LOW();
uint8_t cmd = W25Q64_CMD_READ_JEDEC_ID;
HAL_SPI_Transmit(&hspi1, &cmd, 1, HAL_MAX_DELAY); // 发送0x9F指令
// 连续读取3字节ID (Manufacturer, Memory Type, Capacity)
HAL_SPI_Receive(&hspi1, buf, 3, HAL_MAX_DELAY);
FLASH_CS_HIGH();
// buf[0]应为0xEF(Winbond), buf[1]为0x40(存储类型), buf[2]可能为0x17(64Mbit容量ID)
}
/**
* @brief 初始化W25Q64(唤醒及检查)
* @retval 0 表示初始化成功,非0表示失败
* @note 读取JEDEC ID验证Flash通信是否正常。如有需要可进一步设置。
*/
int W25Q64_Init(void)
{
// 可选:发送释放掉电/唤醒命令0xAB,确保Flash处于活动模式
// 有些情况下Flash可能在上次关机前被置于掉电模式,需要唤醒。
FLASH_CS_LOW();
uint8_t cmd = 0xAB; // Release Power-down / Device ID
HAL_SPI_Transmit(&hspi1, &cmd, 1, HAL_MAX_DELAY);
FLASH_CS_HIGH();
HAL_Delay(1); // 延时至少**tRES1**时间(通常数十微秒)让芯片恢复
// 读取JEDEC ID核对
uint8_t id_buf[3] = {0};
W25Q64_ReadID(id_buf);
if(id_buf[0] == 0xEF) {
// 识别到Winbond制造商ID,认为Flash存在
return 0;
} else {
// ID不符,可能通信不正常
return -1;
}
}
/**
* @brief 从W25Q64读取数据
* @param addr 24位起始地址 [0, 0x7FFFFF]
* @param buf 数据存储缓冲区指针
* @param len 要读取的字节数
*/
void W25Q64_ReadBytes(uint32_t addr, uint8_t *buf, uint32_t len)
{
FLASH_CS_LOW();
uint8_t cmd = W25Q64_CMD_READ_DATA;
HAL_SPI_Transmit(&hspi1, &cmd, 1, HAL_MAX_DELAY); // 发送0x03读数据指令
// 准备地址三个字节,高位在前
uint8_t addr_bytes[3];
addr_bytes[0] = (addr >> 16) & 0xFF; // 23-16位
addr_bytes[1] = (addr >> 8) & 0xFF; // 15-8位
addr_bytes[2] = addr & 0xFF; // 7-0位
HAL_SPI_Transmit(&hspi1, addr_bytes, 3, HAL_MAX_DELAY); // 发送24位地址
HAL_SPI_Receive(&hspi1, buf, len, HAL_MAX_DELAY); // 读取len字节数据到缓冲区
FLASH_CS_HIGH();
}
/**
* @brief 向W25Q64写入数据(页写入)
* @param addr 起始地址
* @param buf 待写入的数据缓冲区
* @param len 要写入的字节数(建议<=256,且不要跨页)
* @note 由于页编程一次最多256字节,且不能跨页写。本函数假设不会跨越页边界。
* 如果addr所在页剩余空间不足len字节,超出页部分将被卷绕到该页开头写入 ([STM32标准库——(16)SPI通信协议、W25Q64简介-优快云博客](https://blog.youkuaiyun.com/2301_78772787/article/details/136449433#:~:text=w25q64%20%E8%8A%AF%E7%89%87%E6%9C%898M%E5%AD%97%E8%8A%82%E7%9A%84%E5%AD%98%E5%82%A8%E7%A9%BA%E9%97%B4%EF%BC%8C%E4%B8%80%E4%B8%AA%E5%AD%97%E8%8A%82%E7%9A%84%E5%85%AB%E4%BD%8D%E5%9C%B0%E5%9D%80%E8%82%AF%E5%AE%9A%E4%B8%8D%E5%A4%9F%EF%BC%8C%E6%89%80%E4%BB%A5%E8%BF%99%E9%87%8C%E5%9C%B0%E5%9D%80%E6%98%AF24%E4%BD%8D%E7%9A%84%E5%88%86%E4%B8%89%E4%B8%AA%E5%AD%97%E8%8A%82%E4%BC%A0%E8%BE%93%EF%BC%8C%E6%88%91%E4%BB%AC%E7%9C%8B%E4%B8%80%E4%B8%8B%E6%97%B6%E5%BA%8F%EF%BC%8C%E9%A6%96%E5%85%88ss%E4%B8%8B%E9%99%8D%E6%B2%BF%E5%BC%80%E5%A7%8B%E6%97%B6%E5%BA%8F%EF%BC%8Cmosi%E7%A9%BA%E9%97%B2%E6%97%B6%20%E6%98%AF%E9%AB%98%E7%94%B5%E5%B9%B3%EF%BC%8C%E6%89%80%E4%BB%A5%E5%9C%A8%E4%B8%8B%E9%99%8D%E6%B2%BF%E4%B9%8B%E5%90%8E%EF%BC%8Csck%E7%AC%AC%E4%B8%80%E4%B8%AA%E6%97%B6%E9%92%9F%E4%B9%8B%E5%89%8D%E5%8F%AF%E4%BB%A5%E7%9C%8B%E5%88%B0mosi%E5%8F%98%E6%8D%A2%E6%95%B0%E6%8D%AE%E7%94%B1%E9%AB%98%E7%94%B5%E5%B9%B3%E5%8F%98%E4%B8%BA%E4%BD%8E%E7%94%B5%E5%B9%B3%EF%BC%8C%E7%84%B6%E5%90%8Esck%E4%B8%8A%E5%8D%87%E6%B2%BF%E6%95%B0%E6%8D%AE%E9%87%87%E6%A0%B7%E8%BE%93%E5%85%A5%EF%BC%8C%E5%90%8E%E9%9D%A2%E8%BF%98%E6%98%AF%E4%B8%80%E6%A0%B7%E4%B8%8B%E9%99%8D%E6%B2%BF%E5%8F%98%E6%8D%A2%E6%95%B0%E6%8D%AE%E4%B8%8A%E5%8D%87%E6%B2%BF%E9%87%87%E6%A0%B7%E6%95%B0%20%E6%8D%AE%EF%BC%8C%E5%85%AB%E4%B8%AA%E6%97%B6%E9%92%9F%E4%B9%8B%E5%90%8E%E4%B8%80%E4%B8%AA%E5%AD%97%E8%8A%82%E4%BA%A4%E6%8D%A2%E5%AE%8C%E6%88%90%EF%BC%8C%E6%88%91%E4%BB%AC%E7%94%A80x02%E6%8D%A2%E6%9D%A5%E4%BA%860xff%EF%BC%8C%E5%85%B6%E4%B8%AD%E5%8F%91%E9%80%81%E7%9A%840x02%E6%98%AF%E4%B8%80%E6%9D%A1%E6%8C%87%E4%BB%A4%EF%BC%8C%E4%BB%A3%E8%A1%A8%E8%BF%99%E6%98%AF%E4%B8%80%E4%B8%AA%E5%86%99%E6%95%B0%E6%8D%AE%E7%9A%84%E6%97%B6%E5%BA%8F%EF%BC%8C%E6%8E%A5%E6%94%B6%E5%88%B00xff%E4%B8%8D%E9%9C%80%E8%A6%81%E7%9C%8B%EF%BC%8C%E9%82%A3%E6%97%A2%E7%84%B6%E6%98%AF%E5%86%99%E6%95%B0%E6%8D%AE%E7%9A%84%20%E6%97%B6%E5%BA%8F%EF%BC%8C%E5%90%8E%E9%9D%A2%E5%BF%85%E7%84%B6%E8%BF%98%E8%A6%81%E8%B7%9F%E7%9D%80%E5%86%99%E7%9A%84%E5%9C%B0%E5%9D%80%E5%92%8C%E6%95%B0%E6%8D%AE%EF%BC%8C%E6%89%80%E4%BB%A5%E5%9C%A8%E6%9C%80%E5%90%8E%E4%B8%80%E4%B8%AA%E4%B8%8B%E9%99%8D%E6%B2%BF%E6%97%B6%E5%88%BB%EF%BC%8C%E5%9B%A0%E4%B8%BA%E6%88%91%E4%BB%AC%E5%90%8E%E7%BB%AD%E8%BF%98%E9%9C%80%E8%A6%81%E7%BB%A7%E7%BB%AD%E4%BA%A4%E6%8D%A2%E5%AD%97%E8%8A%82%EF%BC%8C%E6%89%80%E4%BB%A5%E5%9C%A8%E8%BF%99%E4%B8%AA%E4%B8%8B%E9%99%8D%E6%B2%BF%EF%BC%8C%E6%88%91%E4%BB%AC%E8%A6%81%E6%8A%8A%E4%B8%8B%E4%B8%80%E4%B8%AA%E5%AD%97%E8%8A%82%E7%9A%84%E6%9C%80%E9%AB%98%E4%BD%8D%E6%94%BE%E5%88%B0mosi%E4%B8%8A%EF%BC%8C%E5%BD%93%E7%84%B6))。
* 调用者有责任确保不跨页或分段写入。
*/
void W25Q64_WriteBytes(uint32_t addr, const uint8_t *buf, uint32_t len)
{
// 在发出写命令前需确保Flash空闲且已Write Enable
W25Q64_WaitBusy();
W25Q64_WriteEnable(); // 发送写使能,使能WEL位
FLASH_CS_LOW();
uint8_t cmd = W25Q64_CMD_PAGE_PROGRAM;
HAL_SPI_Transmit(&hspi1, &cmd, 1, HAL_MAX_DELAY); // 发送0x02页编程指令
// 发送24位地址
uint8_t addr_bytes[3];
addr_bytes[0] = (addr >> 16) & 0xFF;
addr_bytes[1] = (addr >> 8) & 0xFF;
addr_bytes[2] = addr & 0xFF;
HAL_SPI_Transmit(&hspi1, addr_bytes, 3, HAL_MAX_DELAY);
// 发送数据
HAL_SPI_Transmit(&hspi1, (uint8_t*)buf, len, HAL_MAX_DELAY);
FLASH_CS_HIGH();
W25Q64_WaitBusy(); // 等待写入完成(WIP=0)
// 可选:此时可读取状态寄存器确认WEL已被自动清0,以及数据写入是否成功(例如再读出验证)
}
/**
* @brief 擦除一个4KB扇区
* @param addr 扇区内任意地址,将按照此地址所在的扇区擦除
* @note 擦除操作耗时较长,期间需等待忙标志。
*/
void W25Q64_EraseSector(uint32_t addr)
{
// 对齐addr到扇区起始地址(可选,W25Q64实际忽略地址低12位)
addr &= 0xFFF000;
W25Q64_WaitBusy();
W25Q64_WriteEnable(); // 使能写入
FLASH_CS_LOW();
uint8_t cmd = W25Q64_CMD_SECTOR_ERASE;
HAL_SPI_Transmit(&hspi1, &cmd, 1, HAL_MAX_DELAY); // 发送0x20扇区擦除指令
// 发送24位地址
uint8_t addr_bytes[3];
addr_bytes[0] = (addr >> 16) & 0xFF;
addr_bytes[1] = (addr >> 8) & 0xFF;
addr_bytes[2] = addr & 0xFF;
HAL_SPI_Transmit(&hspi1, addr_bytes, 3, HAL_MAX_DELAY);
FLASH_CS_HIGH();
W25Q64_WaitBusy(); // 等待擦除完成
}
/**
* @brief 擦除整个芯片
* @note 此操作非常耗时(可能几秒),慎用。执行前需Write Enable。
*/
void W25Q64_EraseChip(void)
{
W25Q64_WaitBusy();
W25Q64_WriteEnable();
FLASH_CS_LOW();
uint8_t cmd = W25Q64_CMD_CHIP_ERASE;
HAL_SPI_Transmit(&hspi1, &cmd, 1, HAL_MAX_DELAY); // 发送芯片擦除命令0xC7
FLASH_CS_HIGH();
W25Q64_WaitBusy(); // 等待整个芯片擦除完成
}
以上代码实现了W25Q64的基本读写操作:
-
初始化:
W25Q64_Init()
中通过0xAB指令确保Flash退出掉电模式,并读取JEDEC ID验证通信 (W25Q64FV Datasheet)。实际应用中也可以省略0xAB步骤(上电默认已处于待机模式),直接读ID。读到正确的制造商ID(0xEF)即表示初始化成功。 -
读数据:
W25Q64_ReadBytes()
使用0x03指令读取任意地址、任意长度的数据。它先发送指令和24位地址,然后连续读出len
个字节数据存入缓冲区。注意,如果读取跨越Flash末尾地址,W25Q64通常会从地址0继续读取(地址会roll over),应避免越界。 -
写数据:
W25Q64_WriteBytes()
实现页写操作。每次写入前调用W25Q64_WriteEnable()
确保WEL置1,然后发送0x02和地址、数据。写完后通过W25Q64_WaitBusy()
等待内部写入完成(WIP清0)。该函数假定写入数据不会跨页;如果需要写入跨页的大数据,应在更高层逻辑上分多次调用本函数,或改进函数逻辑分段写入。 -
擦除扇区:
W25Q64_EraseSector()
用于擦除包含指定地址的4KB扇区。实际发送时将地址低12位清零以确保地址对齐扇区起始。芯片会擦除该扇区全部内容(0xFF表示擦除后的空值)。同样地,执行擦除前需要Write Enable,擦除后需等待操作完成。 -
擦除全片:
W25Q64_EraseChip()
提供整片擦除的示例,实现上较简单,但实际很少用到(除非需要一次性清空Flash)。
使用说明: 在main()
函数中,应先调用W25Q64_Init()
初始化Flash。如果返回0表示成功,可继续进行读写操作。例如:
if (W25Q64_Init() == 0) {
uint8_t data[256];
// 从地址0读取16字节数据
W25Q64_ReadBytes(0x000000, data, 16);
// 修改data内容...
// 写回地址0所在页(假设16字节以内,且此页原先已擦除或数据需要更新)
W25Q64_WriteBytes(0x000000, data, 16);
// 擦除第0扇区
W25Q64_EraseSector(0x000000);
}
实际应用中,写入前通常需要确保目标区域已被擦除(Flash中的位只能从1写为0,不能直接从0写回1,因此若需要改写某地址的数据,必须先擦除包含它的扇区,再重写新的数据)。芯片擦除后所有位默认变为1(0xFF)。因此建议在执行W25Q64_WriteBytes
写新数据前先调用W25Q64_EraseSector
擦除相应扇区,避免旧数据残留干扰。
6. 调试与优化
在将STM32与W25Q64的SPI通信付诸实践时,可能会遇到一些常见问题。以下是调试技巧和优化建议:
-
通信不通或读回ID错误: 首先检查硬件连接是否正确,重点确认MOSI、MISO是否没有接反,CS和CLK连接无误,且所有地线共地。确保/WP和/HOLD引脚已拉高,否则Flash可能保持在写保护或挂起状态无法响应。然后检查SPI配置:时钟模式必须匹配(W25Q64默认模式0),如CPOL/CPHA配置错误会导致数据位错位 (W25Q Flash Series || Part 1 || Read ID)。如果读JEDEC ID总是0xFF或0x00,通常是SPI未真正发送成功或未正确接收——可尝试降低SPI时钟频率,以排除高速信号问题。另外,可使用示波器或逻辑分析仪观察CS、CLK、MOSI、MISO波形,确认时序符合预期:CS应在传输前拉低,期间CLK应有脉冲,MOSI线上应见到指令码和地址比特,MISO线上应在地址发送完后出现数据。如果波形正常但数据不对,考虑Flash可能处于掉电模式,需发送0xAB唤醒(如上代码所示),或者检查是否忘记先发送WREN导致写操作无效。
-
写入无效或数据错误: 若写入后读取发现数据没有更新,可能原因有:未执行写使能(WREN),导致写命令被忽略;或者目标区域未擦除,Flash无法把0写回1,导致旧数据保留。确保执行
W25Q64_WriteEnable()
且检查状态寄存器WEL=1,再发送页编程指令。写入完成后应读取状态寄存器确认WEL已自动清0、BUSY清0。若Flash处于写保护状态(Status寄存器的BP位区域使能了保护,或/WP引脚为低),也会导致写失败 (W25Q64FV Datasheet)。可以通过WRSR命令调整BP位或者硬件上保持/WP高电平解除保护。另外,大容量写入时注意页边界:如果一次写入跨越页,Flash会在页末自动折回,继续从该页开始位置写入,造成未预期的位置被改写 (STM32标准库——(16)SPI通信协议、W25Q64简介-优快云博客)。为避免这种错误,应在软件上限制单次写入不跨页,或分段处理。 -
擦除问题: 扇区擦除或芯片擦除后若发现数据仍存在,检查是否正确等待了操作完成。擦除指令执行时间较长,在WIP=1期间任何读写操作都无效 (W25Q64FV Datasheet)。确保
W25Q64_WaitBusy()
逻辑正确,必要时增加超时防止死等。另一个问题是擦除粒度:如果只想更新某小段数据,却执行了整页擦除,会清除整个扇区的数据。根据应用需求选择合适的擦除范围,尽量避免过度擦除造成不必要的擦写损耗。 -
性能优化: 默认情况下,上述驱动在字节传输时采用轮询方式等待,每发送1字节都会进入中断临界区(HAL内部实现)并产生一定的软件开销。如果需要更高性能,可以从以下几方面优化:
- 提高SPI时钟频率: 在确认信号质量良好的前提下,将SPI时钟提高到STM32允许的最大值(对于F103,SPI1最高约36MHz)。W25Q64支持104MHz,因而36MHz完全可以工作 (W25Q64FV Datasheet)。这将直接提高读写吞吐率。
- 使用DMA传输: 利用STM32的DMA控制器,可以实现SPI收发的硬件自动搬运,减少CPU干预。对于大块数据读写,DMA方式可以显著降低CPU负载,边传输边处理其他任务。不过DMA设置较为复杂,需要注意缓存和总线仲裁,但在HAL库中使用
HAL_SPI_Transmit_DMA
等函数相对简化了流程。 - 批量操作合并: 尽量减少拉高/拉低CS的次数和指令开销。例如,读出大量连续数据时,可使用“Fast Read”指令配合一次性读完,而不是每读取一点就重新发指令。写入时可以先把待写的数据缓存,按页分批写入,减少重复的WREN和指令发送次数。
- 合理使用命令: W25Q64提供“连续读模式”和Quad SPI模式等高级特性,可大幅提升读取速度 (W25Q64FV Datasheet)。虽然F103不支持硬件QSPI,但仍可通过GPIO方式使用双线/四线读取模式,效率较单线SPI翻倍或更高。若项目对读取速度要求极高,可考虑使用更高端STM32型号的QSPI接口直接映射Flash为可执行存储(XIP) (W25Q64FV Datasheet) (W25Q64FV Datasheet)。
- 功耗优化: 在不访问Flash时,可以发送掉电指令0xB9使W25Q64进入掉电模式,此时耗电降至典型1µA (W25Q64FV Datasheet)。再次访问前发送0xAB唤醒即可。对于电池供电应用,这有助于延长待机时间。STM32本身也可在等待Flash操作完成时进入休眠,使用SPI完成后触发中断唤醒,减少忙等的CPU空转时间。
-
其他注意事项: 开发过程中,可参考W25Q64官方数据手册获取更详尽的信息,包括各指令的时序图、电气参数和使用建议。调试时多利用状态寄存器反馈的信息,例如每次写入后立即读状态寄存器,可以获取WEL和BUSY位判断操作是否成功开始或完成。建立可靠的超时机制以防止由于硬件故障导致的死循环等待。最后,在产品化时评估Flash的使用寿命,尽量均衡擦写(如文件系统采用磨损均衡算法),避免频繁擦写同一扇区造成过早老化。
通过上述步骤和代码示例,STM32F103C8T6即可稳定地驱动W25Q64 SPI Flash存储器,实现数据的读取、存储和擦除。在掌握HAL和底层SPI通信原理的基础上,可以根据项目需要进一步扩展功能,例如添加对W25Q64多片选通信、多IO模式或与文件系统(FATFS)结合,实现更高级的存储应用。