前言:在前面的文章中,我们已经系统介绍了SPI 通信原理以及W25Q64 的存储结构和操作特性。
本篇文章将进入实战阶段,基于STM32F103C8T6,通过软件 SPI(GPIO 模拟 SPI)的方式,实现对W25Q64 外部 Flash 的初始化、ID 读取、扇区擦除、页写入和数据读取,并通过 OLED 显示结果进行验证。
目录
一、接线图
二、硬件连接说明
三、软件 SPI 分层设计思想
四、软件 SPI 底层实现
五、W25Q64 指令宏定义
六、W25Q64 驱动实现
七、主函数测试与实验现象
八、总结
一、接线图
二、硬件连接说明
本实验使用 STM32F103C8T6 与 W25Q64 通过 SPI 方式连接,引脚定义如下:
| W25Q64 | STM32 |
|---|---|
| CS | PA4 |
| SCK | PA5 |
| MISO | PA6 |
| MOSI | PA7 |
| VCC | 3.3V |
| GND | GND |
SPI 工作模式:Mode 0(CPOL = 0,CPHA = 0)
三、软件 SPI 分层设计思想
为了让代码结构清晰,本工程将软件 SPI 分为两层:
1.引脚配置层(GPIO 操作)
只关心:SS / SCK / MOSI / MISO
提供“写引脚 / 读引脚”的接口
2. 协议层(SPI 时序)
基于引脚操作实现:
SPI 起始 / 终止
字节交换(8 位时序)
W25Q64 驱动层完全不关心 GPIO 细节,只调用 SPI 接口,结构非常清晰。
四、软件 SPI 底层实现
1. SPI 引脚操作函数
#include "stm32f10x.h" // Device header /*引脚配置层*/ void MySPI_W_SS(uint8_t BitValue) { GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue); } void MySPI_W_SCK(uint8_t BitValue) { GPIO_WriteBit(GPIOA, GPIO_Pin_5, (BitAction)BitValue); } void MySPI_W_MOSI(uint8_t BitValue) { GPIO_WriteBit(GPIOA, GPIO_Pin_7, (BitAction)BitValue); } uint8_t MySPI_R_MISO(void) { return GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6); }2. SPI GPIO 初始化
void MySPI_Init(void) { RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; GPIO_Init(GPIOA, &GPIO_InitStructure); MySPI_W_SS(1); // CS 默认拉高 MySPI_W_SCK(0); // SPI Mode0:SCK 空闲为低 }3. SPI 起始 / 终止
void MySPI_Start(void) { MySPI_W_SS(0); } void MySPI_Stop(void) { MySPI_W_SS(1); }4. SPI 字节交换(Mode 0)
uint8_t MySPI_SwapByte(uint8_t ByteSend) { uint8_t i, ByteReceive = 0x00; for (i = 0; i < 8; i++) { MySPI_W_MOSI(ByteSend & (0x80 >> i)); MySPI_W_SCK(1); if (MySPI_R_MISO()) { ByteReceive |= (0x80 >> i); } MySPI_W_SCK(0); } return ByteReceive; }说明:
上升沿发送数据
上升沿采样 MISO
完全符合 SPI Mode 0 时序
五、W25Q64 指令宏定义
#ifndef __W25Q64_INS_H #define __W25Q64_INS_H #define W25Q64_WRITE_ENABLE 0x06 #define W25Q64_READ_STATUS_REGISTER_1 0x05 #define W25Q64_PAGE_PROGRAM 0x02 #define W25Q64_SECTOR_ERASE_4KB 0x20 #define W25Q64_READ_DATA 0x03 #define W25Q64_JEDEC_ID 0x9F #define W25Q64_DUMMY_BYTE 0xFF #endif六、W25Q64 驱动实现
1. 初始化
void W25Q64_Init(void) { MySPI_Init(); }2. 读取芯片 ID(验证通信是否成功)
void W25Q64_ReadID(uint8_t *MID, uint16_t *DID) { MySPI_Start(); MySPI_SwapByte(W25Q64_JEDEC_ID); *MID = MySPI_SwapByte(W25Q64_DUMMY_BYTE); *DID = MySPI_SwapByte(W25Q64_DUMMY_BYTE); *DID <<= 8; *DID |= MySPI_SwapByte(W25Q64_DUMMY_BYTE); MySPI_Stop(); }3. 写使能与忙等待
void W25Q64_WriteEnable(void) { MySPI_Start(); MySPI_SwapByte(W25Q64_WRITE_ENABLE); MySPI_Stop(); } void W25Q64_WaitBusy(void) { uint32_t Timeout = 100000; MySPI_Start(); MySPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1); while (MySPI_SwapByte(W25Q64_DUMMY_BYTE) & 0x01) { if (--Timeout == 0) break; } MySPI_Stop(); }4. 扇区擦除(4KB)
void W25Q64_SectorErase(uint32_t Address) { W25Q64_WriteEnable(); MySPI_Start(); MySPI_SwapByte(W25Q64_SECTOR_ERASE_4KB); MySPI_SwapByte(Address >> 16); MySPI_SwapByte(Address >> 8); MySPI_SwapByte(Address); MySPI_Stop(); W25Q64_WaitBusy(); }5. 页写入(不跨页)
void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count) { uint16_t i; W25Q64_WriteEnable(); MySPI_Start(); MySPI_SwapByte(W25Q64_PAGE_PROGRAM); MySPI_SwapByte(Address >> 16); MySPI_SwapByte(Address >> 8); MySPI_SwapByte(Address); for (i = 0; i < Count; i++) { MySPI_SwapByte(DataArray[i]); } MySPI_Stop(); W25Q64_WaitBusy(); }6. 数据读取
void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count) { uint32_t i; MySPI_Start(); MySPI_SwapByte(W25Q64_READ_DATA); MySPI_SwapByte(Address >> 16); MySPI_SwapByte(Address >> 8); MySPI_SwapByte(Address); for (i = 0; i < Count; i++) { DataArray[i] = MySPI_SwapByte(W25Q64_DUMMY_BYTE); } MySPI_Stop(); }七、主函数测试与实验现象
uint8_t ArrayWrite[] = {0x01, 0x02, 0x03, 0x04}; uint8_t ArrayRead[4]; int main(void) { OLED_Init(); W25Q64_Init(); W25Q64_ReadID(&MID, &DID); W25Q64_SectorErase(0x000000); W25Q64_PageProgram(0x000000, ArrayWrite, 4); W25Q64_ReadData(0x000000, ArrayRead, 4); while (1) {} }实验现象:
OLED 正确显示 MID / DID
写入数组与读取数组完全一致
说明:
软件 SPI + W25Q64 读写擦除功能完全正常
八、总结
通过本实验,我们完整实现了:
软件 SPI 的 GPIO 模拟
W25Q64 的 ID 读取
扇区擦除、页写入、数据读取
STM32 与外部 Flash 的稳定通信