STM32F103C8T6驱动W25Q64实战:手把手教你用SPI读写外部Flash(附完整代码)

张开发
2026/4/21 16:52:44 15 分钟阅读

分享文章

STM32F103C8T6驱动W25Q64实战:手把手教你用SPI读写外部Flash(附完整代码)
STM32F103C8T6与W25Q64深度实战从SPI协议到Flash存储的完整指南1. 硬件准备与SPI基础拿到STM32F103C8T6开发板和W25Q64模块时首先要理解两者的硬件特性。STM32F103C8T6作为Cortex-M3内核的经典微控制器内置了丰富的外设接口其中就包含我们需要的SPI模块。W25Q64则是Winbond公司推出的64Mbit串行Flash存储器采用SPI接口通信。关键硬件参数对比参数STM32F103C8T6W25Q64工作电压2.0-3.6V2.7-3.6V通信接口SPI1/SPI2SPI最大时钟频率18MHz (SPI2)104MHz (快速模式)数据存储方式易失性非易失性硬件连接时推荐使用SPI2接口避免与调试接口冲突具体引脚对应关系PB12 - CS (片选)PB13 - SCK (时钟)PB14 - MISO (主入从出)PB15 - MOSI (主出从入)注意W25Q64的HOLD和WP引脚如果不使用应该接到VCC而非悬空避免意外进入保护状态。2. SPI初始化与底层驱动实现2.1 SPI外设配置STM32的SPI配置需要考虑多项参数以下是经过优化的初始化代码void SPI2_Init(void) { GPIO_InitTypeDef GPIO_InitStruct; SPI_InitTypeDef SPI_InitStruct; // 使能时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2, ENABLE); // 配置SPI引脚 GPIO_InitStruct.GPIO_Pin GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15; GPIO_InitStruct.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStruct.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStruct); // 配置CS引脚为普通输出 GPIO_InitStruct.GPIO_Pin GPIO_Pin_12; GPIO_InitStruct.GPIO_Mode GPIO_Mode_Out_PP; GPIO_Init(GPIOB, GPIO_InitStruct); GPIO_SetBits(GPIOB, GPIO_Pin_12); // 默认不选中 // SPI参数配置 SPI_InitStruct.SPI_Direction SPI_Direction_2Lines_FullDuplex; SPI_InitStruct.SPI_Mode SPI_Mode_Master; SPI_InitStruct.SPI_DataSize SPI_DataSize_8b; SPI_InitStruct.SPI_CPOL SPI_CPOL_High; // 时钟极性 SPI_InitStruct.SPI_CPHA SPI_CPHA_2Edge; // 时钟相位 SPI_InitStruct.SPI_NSS SPI_NSS_Soft; SPI_InitStruct.SPI_BaudRatePrescaler SPI_BaudRatePrescaler_4; // 9MHz 72MHz PCLK SPI_InitStruct.SPI_FirstBit SPI_FirstBit_MSB; SPI_Init(SPI2, SPI_InitStruct); SPI_Cmd(SPI2, ENABLE); }2.2 基础通信函数实现字节级读写是后续所有操作的基础// 发送并接收一个字节 uint8_t SPI2_ReadWriteByte(uint8_t byte) { while(SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_TXE) RESET); SPI_I2S_SendData(SPI2, byte); while(SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_RXNE) RESET); return SPI_I2S_ReceiveData(SPI2); } // 片选控制宏 #define W25Q64_CS_LOW() GPIO_ResetBits(GPIOB, GPIO_Pin_12) #define W25Q64_CS_HIGH() GPIO_SetBits(GPIOB, GPIO_Pin_12)3. W25Q64操作指令集详解W25Q64支持丰富的操作指令以下是核心指令的封装实现3.1 状态管理与设备识别// 常用指令定义 #define W25X_WriteEnable 0x06 #define W25X_ReadStatusReg1 0x05 #define W25X_PageProgram 0x02 #define W25X_SectorErase 0x20 #define W25X_ReadData 0x03 #define W25X_JedecID 0x9F // 等待Flash就绪 void W25Q64_WaitBusy(void) { uint8_t status; do { W25Q64_CS_LOW(); SPI2_ReadWriteByte(W25X_ReadStatusReg1); status SPI2_ReadWriteByte(0xFF); W25Q64_CS_HIGH(); } while(status 0x01); // 检查BUSY位 } // 读取JEDEC ID uint32_t W25Q64_ReadID(void) { uint32_t id 0; W25Q64_CS_LOW(); SPI2_ReadWriteByte(W25X_JedecID); id | SPI2_ReadWriteByte(0xFF) 16; id | SPI2_ReadWriteByte(0xFF) 8; id | SPI2_ReadWriteByte(0xFF); W25Q64_CS_HIGH(); return id; // 正常应返回0xEF4017 }3.2 存储结构解析W25Q64的存储组织方式直接影响擦写策略容量64Mb (8MB)块(Block)128个每块64KB扇区(Sector)16个/块每扇区4KB页(Page)16页/扇区每页256字节重要特性写入操作只能将位从1改为0要改回1必须擦除整个扇区或更大区域。4. 数据读写实战与性能优化4.1 基础读写函数实现// 读取数据 void W25Q64_Read(uint8_t* pBuffer, uint32_t addr, uint16_t len) { W25Q64_CS_LOW(); SPI2_ReadWriteByte(W25X_ReadData); SPI2_ReadWriteByte((addr 16) 0xFF); SPI2_ReadWriteByte((addr 8) 0xFF); SPI2_ReadWriteByte(addr 0xFF); while(len--) { *pBuffer SPI2_ReadWriteByte(0xFF); } W25Q64_CS_HIGH(); } // 页编程最大256字节 void W25Q64_PageWrite(uint8_t* pBuffer, uint32_t addr, uint16_t len) { W25Q64_CS_LOW(); SPI2_ReadWriteByte(W25X_WriteEnable); W25Q64_CS_HIGH(); W25Q64_CS_LOW(); SPI2_ReadWriteByte(W25X_PageProgram); SPI2_ReadWriteByte((addr 16) 0xFF); SPI2_ReadWriteByte((addr 8) 0xFF); SPI2_ReadWriteByte(addr 0xFF); while(len--) { SPI2_ReadWriteByte(*pBuffer); } W25Q64_CS_HIGH(); W25Q64_WaitBusy(); }4.2 擦除操作与高级写入// 扇区擦除4KB void W25Q64_SectorErase(uint32_t addr) { W25Q64_CS_LOW(); SPI2_ReadWriteByte(W25X_WriteEnable); W25Q64_CS_HIGH(); W25Q64_CS_LOW(); SPI2_ReadWriteByte(W25X_SectorErase); SPI2_ReadWriteByte((addr 16) 0xFF); SPI2_ReadWriteByte((addr 8) 0xFF); SPI2_ReadWriteByte(addr 0xFF); W25Q64_CS_HIGH(); W25Q64_WaitBusy(); // 擦除耗时约100ms } // 智能写入函数自动处理跨页和擦除 void W25Q64_Write(uint8_t* pBuffer, uint32_t addr, uint16_t len) { uint16_t pageRemain; uint32_t secPos addr / 4096; uint32_t secOff addr % 4096; // 先擦除第一个涉及的扇区 if(secOff len 4096) { W25Q64_SectorErase(secPos * 4096); } while(len) { pageRemain 256 - (addr % 256); if(len pageRemain) { pageRemain len; } W25Q64_PageWrite(pBuffer, addr, pageRemain); len - pageRemain; addr pageRemain; pBuffer pageRemain; // 需要跨扇区时提前擦除 if((addr % 4096) len 4096) { W25Q64_SectorErase((addr / 4096) * 4096); } } }4.3 性能优化技巧批量操作尽量集中写入数据减少擦除次数缓存管理建立RAM缓存区减少小数据直接写入磨损均衡动态分配写入位置延长Flash寿命状态检测操作前检查状态寄存器避免无效等待// 示例带缓冲区的批量写入 #define BUFFER_SIZE 512 uint8_t writeBuffer[BUFFER_SIZE]; uint32_t bufferAddr 0; uint16_t bufferIndex 0; void Buffer_WriteByte(uint8_t data, uint32_t addr) { if(bufferAddr ! addr || bufferIndex BUFFER_SIZE) { Flush_Buffer(); // 写入当前缓冲区 bufferAddr addr; bufferIndex 0; } writeBuffer[bufferIndex] data; } void Flush_Buffer(void) { if(bufferIndex 0) { W25Q64_Write(writeBuffer, bufferAddr, bufferIndex); bufferIndex 0; } }5. 调试技巧与常见问题5.1 硬件调试要点信号完整性检查使用示波器观察SCK、MOSI信号质量确认CS信号在通信期间保持低电平检查电源纹波建议加0.1μF去耦电容逻辑分析仪抓包设置SPI解码验证指令发送顺序检查时钟频率是否符合预期5.2 典型问题排查问题1读取到的ID不正确检查硬件连接是否反接特别是MISO/MOSI确认SPI模式CPOL/CPHA设置正确测量VCC电压是否在2.7-3.6V范围内问题2写入后读取数据异常确认写入前已执行擦除操作检查地址对齐页编程不能跨页验证写入后是否等待足够时间tPP典型值0.7ms问题3操作速度慢提高SPI时钟频率最高支持104MHz减少不必要的状态检查采用批量写入代替单字节操作调试建议先实现并验证读取操作再逐步添加写入和擦除功能。使用已知数据模式如0xAA、0x55交替便于验证数据完整性。6. 进阶应用文件系统与数据管理虽然本文聚焦基础驱动但了解后续应用方向很有必要FAT文件系统集成将W25Q64划分为FAT兼容的存储结构实现底层磁盘IO接口数据日志系统设计循环写入的日志结构添加时间戳和校验机制固件存储与更新划分Bootloader和应用程序区域实现安全固件更新流程// 简单的日志记录示例 typedef struct { uint32_t timestamp; uint16_t eventType; uint8_t data[8]; } LogEntry; void Write_LogEntry(LogEntry* entry) { static uint32_t logAddr 0; // 检查是否需要擦除新扇区 if((logAddr % 4096) 0) { W25Q64_SectorErase(logAddr); } W25Q64_Write((uint8_t*)entry, logAddr, sizeof(LogEntry)); logAddr sizeof(LogEntry); // 循环写入避免溢出 if(logAddr 8*1024*1024) { logAddr 0; } }在实际项目中我发现最实用的调试手段是在关键操作前后添加状态检查并将结果通过串口输出。例如每次擦除操作后读取相应扇区的首字节确认已变为0xFF。这种眼见为实的验证方式往往比单纯依赖超时机制更可靠。

更多文章