STM32 C8T6实战:用SPI读写W25Q64 Flash存储芯片(附完整代码与调试心得)
STM32 C8T6实战:用SPI读写W25Q64 Flash存储芯片(附完整代码与调试心得)
1. 硬件连接与SPI基础
在嵌入式系统中,SPI(Serial Peripheral Interface)是一种高速、全双工的同步串行通信接口,特别适合与Flash存储芯片等外设进行数据交换。STM32F103C8T6作为一款性价比极高的Cortex-M3内核MCU,其SPI外设功能完善,能够很好地驱动W25Q64这类SPI Flash芯片。
硬件连接要点:
- W25Q64的CS引脚连接到STM32的任意GPIO(如PA4),采用软件控制方式
- SCK、MOSI、MISO分别对应连接到SPI1的PA5、PA7、PA6引脚
- WP和HOLD引脚直接接高电平,不使用写保护和通讯暂停功能
注意:虽然STM32的SPI接口有专用的NSS引脚,但在实际项目中更推荐使用普通GPIO作为片选信号,这样可以更灵活地控制时序。
SPI模式选择是配置的关键点之一。W25Q64要求使用SPI模式3(CPOL=1,CPHA=1),即:
- 时钟空闲时为高电平
- 数据在时钟第二个边沿采样
2. SPI初始化与配置
2.1 GPIO初始化
首先需要配置SPI相关的GPIO引脚:
void SPI_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct; // 使能GPIO时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 配置SCK和MOSI为复用推挽输出 GPIO_InitStruct.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7; GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStruct); // 配置MISO为浮空输入 GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6; GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &GPIO_InitStruct); // 配置CS为推挽输出并初始化为高电平 GPIO_InitStruct.GPIO_Pin = GPIO_Pin_4; GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_Init(GPIOA, &GPIO_InitStruct); GPIO_SetBits(GPIOA, GPIO_Pin_4); }2.2 SPI外设配置
接下来配置SPI1外设的工作参数:
void SPI_Init(void) { SPI_InitTypeDef SPI_InitStruct; // 使能SPI1时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE); // 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_2; SPI_InitStruct.SPI_FirstBit = SPI_FirstBit_MSB; SPI_InitStruct.SPI_CRCPolynomial = 7; SPI_Init(SPI1, &SPI_InitStruct); // 使能SPI SPI_Cmd(SPI1, ENABLE); }关键参数说明:
| 参数 | 值 | 说明 |
|---|---|---|
| BaudRatePrescaler | 2 | 36MHz/2=18MHz,不超过W25Q64的50MHz限制 |
| CPOL | High | 时钟空闲时为高电平 |
| CPHA | 2Edge | 数据在第二个边沿采样 |
| NSS | Soft | 软件控制片选信号 |
3. W25Q64驱动实现
3.1 基本读写函数
实现SPI字节发送/接收的基础函数:
uint8_t SPI_ReadWriteByte(uint8_t data) { // 等待发送缓冲区空 while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET); // 发送数据 SPI_I2S_SendData(SPI1, data); // 等待接收完成 while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET); // 返回接收到的数据 return SPI_I2S_ReceiveData(SPI1); }3.2 读取芯片ID
读取JEDEC ID是验证硬件连接是否正常的重要方法:
uint32_t W25Q64_ReadID(void) { uint32_t id = 0; // 拉低CS GPIO_ResetBits(GPIOA, GPIO_Pin_4); // 发送读ID指令 SPI_ReadWriteByte(0x9F); // 读取3字节ID id = SPI_ReadWriteByte(0xFF) << 16; id |= SPI_ReadWriteByte(0xFF) << 8; id |= SPI_ReadWriteByte(0xFF); // 拉高CS GPIO_SetBits(GPIOA, GPIO_Pin_4); return id; }正常情况应该返回0xEF4017,其中:
- 0xEF表示厂商是Winbond
- 0x40表示W25Q64系列
- 0x17表示64Mbit容量
3.3 扇区擦除
Flash写入前必须先擦除,W25Q64最小擦除单位为4KB扇区:
void W25Q64_SectorErase(uint32_t addr) { // 发送写使能 W25Q64_WriteEnable(); // 拉低CS GPIO_ResetBits(GPIOA, GPIO_Pin_4); // 发送扇区擦除指令 SPI_ReadWriteByte(0x20); // 发送24位地址 SPI_ReadWriteByte((addr >> 16) & 0xFF); SPI_ReadWriteByte((addr >> 8) & 0xFF); SPI_ReadWriteByte(addr & 0xFF); // 拉高CS GPIO_SetBits(GPIOA, GPIO_Pin_4); // 等待擦除完成 W25Q64_WaitBusy(); }3.4 数据读写
实现页编程和数据读取功能:
// 写入数据(不超过256字节) void W25Q64_PageProgram(uint32_t addr, uint8_t *data, uint16_t len) { uint16_t i; // 发送写使能 W25Q64_WriteEnable(); // 拉低CS GPIO_ResetBits(GPIOA, GPIO_Pin_4); // 发送页编程指令 SPI_ReadWriteByte(0x02); // 发送24位地址 SPI_ReadWriteByte((addr >> 16) & 0xFF); SPI_ReadWriteByte((addr >> 8) & 0xFF); SPI_ReadWriteByte(addr & 0xFF); // 写入数据 for(i=0; i<len; i++) { SPI_ReadWriteByte(data[i]); } // 拉高CS GPIO_SetBits(GPIOA, GPIO_Pin_4); // 等待写入完成 W25Q64_WaitBusy(); } // 读取数据 void W25Q64_ReadData(uint32_t addr, uint8_t *buf, uint32_t len) { uint32_t i; // 拉低CS GPIO_ResetBits(GPIOA, GPIO_Pin_4); // 发送读数据指令 SPI_ReadWriteByte(0x03); // 发送24位地址 SPI_ReadWriteByte((addr >> 16) & 0xFF); SPI_ReadWriteByte((addr >> 8) & 0xFF); SPI_ReadWriteByte(addr & 0xFF); // 读取数据 for(i=0; i<len; i++) { buf[i] = SPI_ReadWriteByte(0xFF); } // 拉高CS GPIO_SetBits(GPIOA, GPIO_Pin_4); }4. 实战调试经验
4.1 常见问题排查
问题1:读取ID返回0xFFFFFF或0x000000
可能原因:
- 硬件连接错误,检查SCK、MOSI、MISO是否交叉连接
- SPI模式配置错误,W25Q64需要模式3
- 片选信号未正确控制,CS需要在每个命令前后有高低电平变化
问题2:写入数据后读取不一致
解决方案:
- 确保写入前执行了扇区擦除
- 检查写入地址是否4KB对齐
- 写入后调用W25Q64_WaitBusy()等待操作完成
4.2 性能优化技巧
- 使用DMA传输:对于大数据量读写,可以配置SPI DMA减少CPU开销
// 配置SPI1 TX DMA DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&SPI1->DR; DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)txBuffer; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; DMA_InitStructure.DMA_BufferSize = bufferSize; DMA_Init(DMA1_Channel3, &DMA_InitStructure);双缓冲机制:在需要持续写入数据时,可以交替使用两个缓冲区
合理设置SPI时钟:在满足W25Q64规格的前提下,尽可能提高SPI时钟频率
4.3 扩展功能实现
实现文件系统:可以基于W25Q64实现简单的文件存储系统,关键数据结构:
typedef struct { uint32_t startAddr; // 文件起始地址 uint32_t length; // 文件长度 uint8_t status; // 文件状态 char name[16]; // 文件名 } FileEntry; typedef struct { FileEntry entries[MAX_FILES]; // 文件条目数组 uint32_t freeAddr; // 下一个空闲地址 } FileSystem;实现双SPI主从通信:利用STM32的多个SPI接口实现设备间高速数据交换
// SPI2从机初始化 void SPI2_SlaveInit(void) { // ...类似SPI1初始化,但模式设为SPI_Mode_Slave // 启用接收中断 SPI_I2S_ITConfig(SPI2, SPI_I2S_IT_RXNE, ENABLE); NVIC_EnableIRQ(SPI2_IRQn); } // SPI2中断服务程序 void SPI2_IRQHandler(void) { if(SPI_I2S_GetITStatus(SPI2, SPI_I2S_IT_RXNE) != RESET) { uint8_t data = SPI_I2S_ReceiveData(SPI2); // 处理接收到的数据... } }5. 完整代码示例
以下是整合后的W25Q64驱动头文件:
#ifndef __W25Q64_H #define __W25Q64_H #include "stm32f10x.h" // 指令定义 #define W25Q64_CMD_READ_ID 0x9F #define W25Q64_CMD_READ_DATA 0x03 #define W25Q64_CMD_PAGE_PROGRAM 0x02 #define W25Q64_CMD_SECTOR_ERASE 0x20 #define W25Q64_CMD_WRITE_ENABLE 0x06 #define W25Q64_CMD_READ_STATUS 0x05 // 函数声明 void W25Q64_Init(void); uint32_t W25Q64_ReadID(void); void W25Q64_ReadData(uint32_t addr, uint8_t *buf, uint32_t len); void W25Q64_PageProgram(uint32_t addr, uint8_t *data, uint16_t len); void W25Q64_SectorErase(uint32_t addr); uint8_t W25Q64_ReadStatus(void); void W25Q64_WriteEnable(void); void W25Q64_WaitBusy(void); #endif实际项目中,建议将SPI底层驱动与W25Q64高层功能分离,便于移植和维护。调试时可以先用逻辑分析仪抓取SPI波形,验证时序是否正确。
