STM32F103C8T6硬件SPI驱动W25Q64 Flash全流程(附完整工程代码)
STM32F103C8T6硬件SPI驱动W25Q64 Flash全流程实战指南
在嵌入式开发中,外部Flash存储器的使用是一个常见需求。W25Q64作为一款性价比极高的8MB SPI Flash芯片,被广泛应用于各种嵌入式系统中。本文将手把手带你完成从零开始搭建STM32F103C8T6(蓝桥杯开发板常用MCU)硬件SPI驱动W25Q64的全过程,包括工程搭建、硬件连接、底层驱动编写以及数据验证。
1. 硬件准备与连接
在开始编码前,我们需要确保硬件连接正确。STM32F103C8T6与W25Q64的连接方式如下:
| STM32引脚 | W25Q64引脚 | 功能说明 |
|---|---|---|
| PA4 | CS | 片选信号 |
| PA5 | CLK | 时钟信号 |
| PA6 | DO/MISO | 主机输入从机输出 |
| PA7 | DI/MOSI | 主机输出从机输入 |
| 3.3V | VCC | 电源正极 |
| GND | GND | 电源地 |
注意:W25Q64的工作电压为2.7V-3.6V,务必连接到3.3V电源,不可接5V,否则会损坏芯片。
硬件连接时还需要注意以下几点:
- 确保所有GND连接在一起
- 信号线长度尽可能短
- 如果布线较长,可考虑在SCK线上串联22Ω电阻减少振铃
- 在VCC和GND之间放置0.1μF去耦电容
2. 工程环境搭建
首先我们需要创建一个基本的STM32工程。这里以Keil MDK为例:
- 打开Keil uVision,选择Project → New μVision Project
- 选择保存路径并命名工程(如"W25Q64_SPI_Demo")
- 设备选择STMicroelectronics → STM32F103C8
- 在Manage Run-Time Environment中勾选:
- CMSIS → CORE
- Device → Startup
- STM32Cube Framework → CMSIS → STM32F1xx
- 点击OK创建工程
接下来添加必要的驱动文件:
- 在工程中新建Hardware/SPI和Hardware/W25Q64文件夹
- 分别创建spi.h/spi.c和w25q64.h/w25q64.c文件
- 配置工程包含路径指向这些文件夹
3. 硬件SPI初始化配置
硬件SPI的初始化是驱动W25Q64的关键。我们需要正确配置SPI的工作模式和参数:
// spi.h #ifndef __SPI_H__ #define __SPI_H__ #include "stm32f10x.h" void SPI1_Init(void); void SPI1_Start(void); void SPI1_Stop(void); uint8_t SPI1_ReadWriteByte(uint8_t TxData); #endif// spi.c #include "spi.h" #include "stm32f10x_gpio.h" #include "stm32f10x_spi.h" // SPI1引脚初始化 void SPI1_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE); // PA4(CS) 推挽输出 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); // PA5(SCK), PA7(MOSI) 复用推挽输出 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_Init(GPIOA, &GPIO_InitStructure); // PA6(MISO) 浮空输入 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &GPIO_InitStructure); // 默认CS高电平 GPIO_SetBits(GPIOA, GPIO_Pin_4); } // SPI1模式配置 void SPI1_Mode_Init(void) { SPI_InitTypeDef SPI_InitStructure; SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; SPI_InitStructure.SPI_Mode = SPI_Mode_Master; SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4; SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; SPI_InitStructure.SPI_CRCPolynomial = 7; SPI_Init(SPI1, &SPI_InitStructure); SPI_Cmd(SPI1, ENABLE); } void SPI1_Init(void) { SPI1_GPIO_Init(); SPI1_Mode_Init(); } void SPI1_Start(void) { GPIO_ResetBits(GPIOA, GPIO_Pin_4); // CS拉低 } void SPI1_Stop(void) { GPIO_SetBits(GPIOA, GPIO_Pin_4); // CS拉高 } uint8_t SPI1_ReadWriteByte(uint8_t TxData) { while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET); SPI_I2S_SendData(SPI1, TxData); while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET); return SPI_I2S_ReceiveData(SPI1); }关键配置参数说明:
- SPI模式:全双工主模式
- 数据大小:8位
- 时钟极性(CPOL):低电平
- 时钟相位(CPHA):第一个边沿采样
- 波特率预分频:4分频(系统时钟72MHz时,SPI时钟为18MHz)
- 数据顺序:MSB优先
4. W25Q64驱动实现
W25Q64的驱动需要实现基本的读写擦除功能。首先定义W25Q64的指令集:
// w25q64.h #ifndef __W25Q64_H__ #define __W25Q64_H__ #include "stm32f10x.h" // W25Q64指令集 #define W25Q64_WriteEnable 0x06 #define W25Q64_WriteDisable 0x04 #define W25Q64_ReadStatusReg1 0x05 #define W25Q64_ReadStatusReg2 0x35 #define W25Q64_WriteStatusReg 0x01 #define W25Q64_PageProgram 0x02 #define W25Q64_SectorErase 0x20 #define W25Q64_BlockErase32K 0x52 #define W25Q64_BlockErase64K 0xD8 #define W25Q64_ChipErase 0xC7 #define W25Q64_PowerDown 0xB9 #define W25Q64_ReleasePowerDown 0xAB #define W25Q64_ManufactDeviceID 0x90 #define W25Q64_ReadUniqueID 0x4B #define W25Q64_JedecDeviceID 0x9F #define W25Q64_ReadData 0x03 #define W25Q64_FastRead 0x0B #define W25Q64_DummyByte 0xFF // 函数声明 void W25Q64_Init(void); void W25Q64_ReadID(uint8_t *ManufacturerID, uint16_t *DeviceID); void W25Q64_WriteEnable(void); void W25Q64_WaitBusy(void); void W25Q64_SectorErase(uint32_t SectorAddr); void W25Q64_PageWrite(uint32_t Addr, uint8_t *pBuffer, uint16_t NumByteToWrite); void W25Q64_ReadData(uint32_t Addr, uint8_t *pBuffer, uint16_t NumByteToRead); #endif接下来实现具体的驱动函数:
// w25q64.c #include "w25q64.h" #include "spi.h" #include "delay.h" // 初始化W25Q64 void W25Q64_Init(void) { SPI1_Init(); } // 读取制造商ID和设备ID void W25Q64_ReadID(uint8_t *ManufacturerID, uint16_t *DeviceID) { SPI1_Start(); SPI1_ReadWriteByte(W25Q64_JedecDeviceID); *ManufacturerID = SPI1_ReadWriteByte(W25Q64_DummyByte); *DeviceID = SPI1_ReadWriteByte(W25Q64_DummyByte); *DeviceID <<= 8; *DeviceID |= SPI1_ReadWriteByte(W25Q64_DummyByte); SPI1_Stop(); } // 写使能 void W25Q64_WriteEnable(void) { SPI1_Start(); SPI1_ReadWriteByte(W25Q64_WriteEnable); SPI1_Stop(); } // 等待忙状态结束 void W25Q64_WaitBusy(void) { uint32_t timeout = 500000; // 超时计数 SPI1_Start(); SPI1_ReadWriteByte(W25Q64_ReadStatusReg1); while((SPI1_ReadWriteByte(W25Q64_DummyByte) & 0x01) == 0x01) { if(timeout-- == 0) break; } SPI1_Stop(); } // 扇区擦除(4KB) void W25Q64_SectorErase(uint32_t SectorAddr) { W25Q64_WriteEnable(); SPI1_Start(); SPI1_ReadWriteByte(W25Q64_SectorErase); SPI1_ReadWriteByte((SectorAddr >> 16) & 0xFF); SPI1_ReadWriteByte((SectorAddr >> 8) & 0xFF); SPI1_ReadWriteByte(SectorAddr & 0xFF); SPI1_Stop(); W25Q64_WaitBusy(); } // 页编程(最大256字节) void W25Q64_PageWrite(uint32_t Addr, uint8_t *pBuffer, uint16_t NumByteToWrite) { uint16_t i; W25Q64_WriteEnable(); SPI1_Start(); SPI1_ReadWriteByte(W25Q64_PageProgram); SPI1_ReadWriteByte((Addr >> 16) & 0xFF); SPI1_ReadWriteByte((Addr >> 8) & 0xFF); SPI1_ReadWriteByte(Addr & 0xFF); for(i=0; i<NumByteToWrite; i++) { SPI1_ReadWriteByte(pBuffer[i]); } SPI1_Stop(); W25Q64_WaitBusy(); } // 读取数据 void W25Q64_ReadData(uint32_t Addr, uint8_t *pBuffer, uint16_t NumByteToRead) { uint16_t i; SPI1_Start(); SPI1_ReadWriteByte(W25Q64_ReadData); SPI1_ReadWriteByte((Addr >> 16) & 0xFF); SPI1_ReadWriteByte((Addr >> 8) & 0xFF); SPI1_ReadWriteByte(Addr & 0xFF); for(i=0; i<NumByteToRead; i++) { pBuffer[i] = SPI1_ReadWriteByte(W25Q64_DummyByte); } SPI1_Stop(); }5. 功能测试与验证
完成驱动编写后,我们需要测试W25Q64的各项功能。这里我们使用OLED显示屏来显示测试结果:
// main.c #include "stm32f10x.h" #include "delay.h" #include "oled.h" #include "w25q64.h" int main(void) { uint8_t ManufacturerID; uint16_t DeviceID; uint8_t WriteBuffer[256]; uint8_t ReadBuffer[256]; uint16_t i; // 初始化 Delay_Init(); OLED_Init(); W25Q64_Init(); // 显示标题 OLED_ShowString(1, 1, "W25Q64 Test"); // 读取ID并显示 W25Q64_ReadID(&ManufacturerID, &DeviceID); OLED_ShowString(2, 1, "MID:0x"); OLED_ShowHexNum(2, 7, ManufacturerID, 2); OLED_ShowString(3, 1, "DID:0x"); OLED_ShowHexNum(3, 7, DeviceID, 4); // 准备测试数据 for(i=0; i<256; i++) { WriteBuffer[i] = i; } // 擦除扇区0 OLED_ShowString(4, 1, "Erasing Sector 0..."); W25Q64_SectorErase(0x000000); OLED_ShowString(4, 1, "Erase Complete! "); // 写入数据 OLED_ShowString(5, 1, "Writing Data... "); W25Q64_PageWrite(0x000000, WriteBuffer, 256); OLED_ShowString(5, 1, "Write Complete! "); // 读取数据 OLED_ShowString(6, 1, "Reading Data... "); W25Q64_ReadData(0x000000, ReadBuffer, 256); OLED_ShowString(6, 1, "Read Complete! "); // 验证数据 for(i=0; i<256; i++) { if(WriteBuffer[i] != ReadBuffer[i]) { OLED_ShowString(7, 1, "Verify Failed! "); break; } } if(i == 256) { OLED_ShowString(7, 1, "Verify Success! "); } while(1) { } }测试流程说明:
- 初始化系统时钟、延时函数、OLED和W25Q64
- 读取W25Q64的制造商ID和设备ID并显示
- 准备256字节的测试数据(0x00-0xFF)
- 擦除扇区0(地址0x000000开始的4KB)
- 将测试数据写入扇区0
- 从扇区0读取数据
- 比较写入和读取的数据是否一致
- 显示验证结果
6. 性能优化与高级功能
在基本功能实现后,我们可以进一步优化性能和实现更高级的功能:
6.1 SPI时钟速度优化
W25Q64支持最高80MHz的SPI时钟。我们可以调整预分频值来提高传输速率:
// 修改SPI初始化中的预分频值 SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_2; // 36MHz注意:提高SPI时钟速度需要考虑信号完整性和PCB布线质量。如果出现通信错误,可以适当降低时钟速度或改善硬件设计。
6.2 多扇区连续写入
W25Q64的页编程操作每次最多写入256字节。要实现更大数据量的连续写入,可以使用以下方法:
void W25Q64_MultiPageWrite(uint32_t Addr, uint8_t *pBuffer, uint32_t NumByteToWrite) { uint32_t PageRemain; uint32_t WriteLen; uint32_t i; for(i=0; i<NumByteToWrite; ) { PageRemain = 256 - (Addr % 256); // 当前页剩余空间 WriteLen = (NumByteToWrite - i) > PageRemain ? PageRemain : (NumByteToWrite - i); W25Q64_PageWrite(Addr, pBuffer + i, WriteLen); Addr += WriteLen; i += WriteLen; } }6.3 快速读取模式
W25Q64支持快速读取模式(Fast Read),相比普通读取模式可以提高读取速度:
void W25Q64_FastReadData(uint32_t Addr, uint8_t *pBuffer, uint16_t NumByteToRead) { uint16_t i; SPI1_Start(); SPI1_ReadWriteByte(W25Q64_FastRead); SPI1_ReadWriteByte((Addr >> 16) & 0xFF); SPI1_ReadWriteByte((Addr >> 8) & 0xFF); SPI1_ReadWriteByte(Addr & 0xFF); SPI1_ReadWriteByte(W25Q64_DummyByte); // 快速读取需要一个dummy字节 for(i=0; i<NumByteToRead; i++) { pBuffer[i] = SPI1_ReadWriteByte(W25Q64_DummyByte); } SPI1_Stop(); }6.4 写保护功能
W25Q64提供了写保护功能,可以防止意外修改数据:
// 启用写保护 void W25Q64_WriteProtect(void) { SPI1_Start(); SPI1_ReadWriteByte(W25Q64_WriteDisable); SPI1_Stop(); } // 检查写保护状态 uint8_t W25Q64_IsWriteProtected(void) { uint8_t status; SPI1_Start(); SPI1_ReadWriteByte(W25Q64_ReadStatusReg1); status = SPI1_ReadWriteByte(W25Q64_DummyByte); SPI1_Stop(); return (status & 0x02) ? 1 : 0; }7. 常见问题与解决方案
在实际开发中,可能会遇到各种问题。以下是一些常见问题及其解决方法:
7.1 无法读取设备ID
现象:读取的制造商ID或设备ID不正确或全为0xFF。
可能原因及解决方法:
- 硬件连接错误
- 检查所有连线是否正确,特别是CS、SCK、MOSI、MISO
- 确认电源电压为3.3V
- SPI配置错误
- 确认SPI模式设置为模式0或模式3(CPOL=0, CPHA=0或CPOL=1, CPHA=1)
- 检查SPI时钟速度是否过高,可尝试降低预分频值
- 芯片未正确复位
- 尝试重新上电
- 发送释放掉电指令(0xAB)
7.2 写入数据后读取不正确
现象:写入数据后读取的数据与写入的不一致。
可能原因及解决方法:
- 未先擦除扇区
- Flash存储器必须先擦除才能写入,确保在写入前执行了扇区擦除
- 跨页写入未处理
- 单次写入不能跨页(256字节边界),需要分多次写入
- 写入后未等待操作完成
- 在写入操作后调用W25Q64_WaitBusy()等待操作完成
7.3 擦除或写入操作失败
现象:擦除或写入操作没有效果。
可能原因及解决方法:
- 未发送写使能指令
- 在执行擦除或写入操作前必须先发送写使能指令(0x06)
- 写保护启用
- 检查状态寄存器的写保护位,必要时禁用写保护
- 电压不稳定
- 检查电源电压是否稳定,在VCC和GND之间添加更大的滤波电容
7.4 长时间使用后数据丢失
现象:存储的数据在一段时间后丢失或损坏。
可能原因及解决方法:
- 擦写次数超过限制
- W25Q64每个扇区的擦写寿命约为10万次,需优化算法减少擦写频率
- 电源干扰
- 加强电源滤波,确保在写入操作期间电源稳定
- 环境温度过高
- 避免在高温环境下长期工作,必要时降低SPI时钟速度
8. 工程代码结构优化
为了使工程更加模块化和易于维护,我们可以优化代码结构如下:
W25Q64_SPI_Demo/ ├── CMSIS/ # CMSIS核心文件 ├── STM32F10x_StdPeriph_Driver/ # 标准外设驱动 ├── User/ │ ├── main.c # 主程序 │ ├── delay.c # 延时函数 │ ├── oled.c # OLED驱动 │ ├── spi.c # SPI驱动 │ ├── w25q64.c # W25Q64驱动 │ ├── inc/ # 头文件目录 │ │ ├── delay.h │ │ ├── oled.h │ │ ├── spi.h │ │ └── w25q64.h │ └── src/ # 源文件目录 └── MDK-ARM/ # Keil工程文件在Keil中设置包含路径时,确保包含以下路径:
- User/inc
- CMSIS
- STM32F10x_StdPeriph_Driver/inc
9. 实际应用案例
W25Q64在实际项目中有广泛的应用场景,下面介绍几个典型应用:
9.1 固件存储与升级
W25Q64可以用于存储设备固件,实现IAP(In Application Programming)功能:
// 固件更新函数 void Firmware_Update(uint8_t *NewFirmware, uint32_t FirmwareSize) { uint32_t i; uint32_t SectorAddr; uint32_t SectorCount = (FirmwareSize + 4095) / 4096; // 计算需要的扇区数 // 擦除必要的扇区 for(i=0; i<SectorCount; i++) { SectorAddr = FIRMWARE_START_ADDR + i * 4096; W25Q64_SectorErase(SectorAddr); } // 写入新固件 W25Q64_MultiPageWrite(FIRMWARE_START_ADDR, NewFirmware, FirmwareSize); // 验证固件 // ... // 更新成功,设置标志准备重启 // ... }9.2 数据日志存储
在需要记录设备运行数据的应用中,W25Q64可以作为数据存储器:
// 日志数据结构 typedef struct { uint32_t timestamp; float temperature; float humidity; uint16_t pressure; } LogEntry; // 写入日志 void Write_Log(LogEntry *entry) { static uint32_t logAddr = LOG_START_ADDR; // 检查是否需要擦除新扇区 if((logAddr % 4096) == 0) { W25Q64_SectorErase(logAddr); } // 写入日志条目 W25Q64_PageWrite(logAddr, (uint8_t *)entry, sizeof(LogEntry)); // 更新写入地址 logAddr += sizeof(LogEntry); // 处理循环写入 if(logAddr >= LOG_END_ADDR) { logAddr = LOG_START_ADDR; } }9.3 字库存储与显示
在需要显示多种语言的设备中,W25Q64可以存储字库数据:
// 从Flash读取字模数据 void Get_FontData(uint16_t fontCode, uint8_t *buffer) { uint32_t addr = FONT_START_ADDR + fontCode * FONT_SIZE; W25Q64_ReadData(addr, buffer, FONT_SIZE); } // 显示字符 void Show_Char(uint16_t x, uint16_t y, uint16_t fontCode) { uint8_t fontData[FONT_SIZE]; Get_FontData(fontCode, fontData); OLED_ShowFont(x, y, fontData); }10. 扩展思考与进阶方向
掌握了W25Q64的基本驱动后,可以进一步探索以下方向:
10.1 文件系统集成
在W25Q64上实现文件系统(如FATFS、LittleFS等)可以更方便地管理存储数据:
// 初始化FATFS文件系统 FATFS fs; FIL file; FRESULT res; res = f_mount(&fs, "0:", 1); // 挂载文件系统 if(res == FR_OK) { res = f_open(&file, "0:/data.log", FA_READ | FA_WRITE | FA_OPEN_ALWAYS); if(res == FR_OK) { // 文件操作... f_close(&file); } f_mount(NULL, "0:", 0); // 卸载文件系统 }10.2 双SPI/四SPI模式
W25Q64支持双SPI和四SPI模式,可以进一步提高数据传输速率:
// 切换到四SPI模式 void W25Q64_EnableQuadMode(void) { uint8_t status; // 读取状态寄存器2 SPI1_Start(); SPI1_ReadWriteByte(W25Q64_ReadStatusReg2); status = SPI1_ReadWriteByte(W25Q64_DummyByte); SPI1_Stop(); // 设置QE位 if((status & 0x02) == 0) { W25Q64_WriteEnable(); SPI1_Start(); SPI1_ReadWriteByte(W25Q64_WriteStatusReg); SPI1_ReadWriteByte(0x00); // 状态寄存器1保持原值 SPI1_ReadWriteByte(0x02); // 设置状态寄存器2的QE位 SPI1_Stop(); W25Q64_WaitBusy(); } }10.3 磨损均衡算法
为了延长Flash寿命,可以实现简单的磨损均衡算法:
// 简单的磨损均衡实现 uint32_t Get_NextWriteAddr(void) { static uint32_t currentAddr = DATA_START_ADDR; static uint16_t writeCount = 0; uint32_t nextAddr; nextAddr = currentAddr; currentAddr += DATA_BLOCK_SIZE; // 检查是否需要擦除新区块 if((currentAddr % 4096) == 0) { W25Q64_SectorErase(currentAddr); } // 处理循环写入 if(currentAddr >= DATA_END_ADDR) { currentAddr = DATA_START_ADDR; writeCount++; // 每循环一定次数后移动起始地址实现简单均衡 if(writeCount % WEAR_LEVELING_FACTOR == 0) { currentAddr += 4096; // 移动一个扇区 if(currentAddr >= DATA_END_ADDR) { currentAddr = DATA_START_ADDR; } } } return nextAddr; }10.4 掉电保护机制
在关键数据写入时实现掉电保护:
// 安全写入函数 uint8_t Safe_Write(uint32_t addr, uint8_t *data, uint16_t len) { uint8_t status = 0; uint32_t crc = Calculate_CRC(data, len); // 写入数据 W25Q64_PageWrite(addr, data, len); // 写入CRC校验值 W25Q64_PageWrite(addr + len, (uint8_t *)&crc, 4); // 验证写入 uint8_t verifyBuf[len]; uint32_t verifyCrc; W25Q64_ReadData(addr, verifyBuf, len); W25Q64_ReadData(addr + len, (uint8_t *)&verifyCrc, 4); if(memcmp(data, verifyBuf, len) == 0 && crc == verifyCrc) { status = 1; // 写入成功 } return status; }通过本文的详细讲解,你应该已经掌握了STM32F103C8T6硬件SPI驱动W25Q64 Flash的全流程。从硬件连接到软件实现,从基础功能到高级应用,这些知识将帮助你在实际项目中灵活运用SPI Flash存储器。
