STM32 SPI通信与Flash存储实战指南
1. STM32 SPI通信基础与硬件连接
SPI(Serial Peripheral Interface)是一种高速全双工的同步串行通信接口,由摩托罗拉公司提出,广泛应用于ADC、DAC、存储器、传感器等外设与微控制器的通信。STM32系列微控制器内置了硬件SPI外设,支持主从模式配置,最高通信速率可达fpclk/2(对于STM32F103系列,APB2总线上的SPI1最高支持36MHz,APB1总线上的SPI2/3最高支持18MHz)。
1.1 SPI物理层连接
一个典型的SPI系统包含以下信号线:
- SCK(Serial Clock):时钟信号,由主机产生
- MOSI(Master Output Slave Input):主机输出,从机输入
- MISO(Master Input Slave Output):主机输入,从机输出
- NSS(Slave Select):从机选择信号(低电平有效)
在STM32与SPI Flash(如W25Q64)的典型连接中:
- PA4(GPIO)作为NSS信号(软件控制)
- PA5作为SCK时钟线
- PA6作为MISO数据输入
- PA7作为MOSI数据输出
注意:虽然STM32的硬件NSS引脚可以实现自动片选控制,但在实际应用中更常使用普通GPIO进行软件控制,这样更灵活且节省硬件资源。
1.2 SPI协议层关键参数
SPI通信有四个关键工作模式,由CPOL(Clock Polarity)和CPHA(Clock Phase)决定:
| 模式 | CPOL | CPHA | 时钟空闲状态 | 数据采样边沿 |
|---|---|---|---|---|
| 0 | 0 | 0 | 低电平 | 奇数边沿(第一个上升沿) |
| 1 | 0 | 1 | 低电平 | 偶数边沿(第一个下降沿) |
| 2 | 1 | 0 | 高电平 | 奇数边沿(第一个下降沿) |
| 3 | 1 | 1 | 高电平 | 偶数边沿(第一个上升沿) |
对于W25Q64 Flash芯片,支持模式0和模式3。实际工程中通常选择模式3(CPOL=1,CPHA=1),这是因为它能提供更好的信号稳定性。
2. STM32 SPI外设配置详解
2.1 SPI初始化结构体
STM32标准库提供了SPI_InitTypeDef结构体来配置SPI外设:
typedef struct { uint16_t SPI_Direction; // 通信方向(双线全双工/单工等) uint16_t SPI_Mode; // 主/从模式 uint16_t SPI_DataSize; // 数据帧长度(8/16位) uint16_t SPI_CPOL; // 时钟极性 uint16_t SPI_CPHA; // 时钟相位 uint16_t SPI_NSS; // NSS硬件/软件管理 uint16_t SPI_BaudRatePrescaler; // 波特率预分频 uint16_t SPI_FirstBit; // MSB/LSB先行 uint16_t SPI_CRCPolynomial; // CRC多项式 } SPI_InitTypeDef;典型配置示例(模式3,8位数据,MSB先行):
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_High; // 模式3 SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge; // 模式3 SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; // 软件控制NSS SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4; // 18MHz @72MHz PCLK SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; SPI_InitStructure.SPI_CRCPolynomial = 7; SPI_Init(SPI1, &SPI_InitStructure); SPI_Cmd(SPI1, ENABLE);2.2 GPIO配置要点
SPI引脚需要配置为复用功能模式(除NSS外):
GPIO_InitTypeDef GPIO_InitStructure; // SCK, MOSI, MISO配置为复用推挽输出 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7; // SCK, MOSI GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; // MISO GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &GPIO_InitStructure); // NSS配置为普通推挽输出(软件控制) GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_Init(GPIOA, &GPIO_InitStructure);2.3 数据收发实现
基本的字节收发函数实现:
uint8_t SPIx_SendByte(uint8_t byte) { // 等待发送缓冲区空 while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET); // 发送数据 SPI_I2S_SendData(SPI1, byte); // 等待接收完成 while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET); // 返回接收到的数据 return SPI_I2S_ReceiveData(SPI1); }实际工程中应添加超时处理,防止程序死锁。超时时间应根据SPI时钟频率合理设置,通常为几个毫秒。
3. SPI Flash操作实战(以W25Q64为例)
3.1 Flash基本指令集
W25Q64支持的标准SPI指令包括:
| 指令名称 | 指令码 | 描述 |
|---|---|---|
| Write Enable | 0x06 | 写使能 |
| Write Disable | 0x04 | 写禁止 |
| Read Status Register | 0x05 | 读状态寄存器 |
| Page Program | 0x02 | 页编程(最大256字节) |
| Sector Erase | 0x20 | 扇区擦除(4KB) |
| Block Erase | 0xD8 | 块擦除(64KB) |
| Chip Erase | 0xC7 | 整片擦除 |
| Read Data | 0x03 | 读数据 |
| JEDEC ID | 0x9F | 读制造商和设备ID |
3.2 Flash操作关键流程
3.2.1 写使能(Write Enable)
在执行任何写入或擦除操作前必须发送写使能命令:
void W25Qxx_WriteEnable(void) { SPI_FLASH_CS_LOW(); // 片选拉低 SPIx_SendByte(0x06); // 发送写使能指令 SPI_FLASH_CS_HIGH(); // 片选拉高 }3.2.2 等待写操作完成
Flash内部写入需要时间(典型值3ms/page),需检查状态寄存器BUSY位:
void W25Qxx_WaitForWriteEnd(void) { uint8_t status; SPI_FLASH_CS_LOW(); SPIx_SendByte(0x05); // 读状态寄存器指令 do { status = SPIx_SendByte(0xFF); // 继续读取状态 } while(status & 0x01); // 检查BUSY位 SPI_FLASH_CS_HIGH(); }3.2.3 扇区擦除(Sector Erase)
Flash写入前必须先擦除(将位由0变为1):
void W25Qxx_SectorErase(uint32_t SectorAddr) { W25Qxx_WriteEnable(); // 写使能 SPI_FLASH_CS_LOW(); SPIx_SendByte(0x20); // 扇区擦除指令 SPIx_SendByte((SectorAddr >> 16) & 0xFF); // 地址高字节 SPIx_SendByte((SectorAddr >> 8) & 0xFF); // 地址中字节 SPIx_SendByte(SectorAddr & 0xFF); // 地址低字节 SPI_FLASH_CS_HIGH(); W25Qxx_WaitForWriteEnd(); // 等待擦除完成 }注意:擦除的最小单位是扇区(4KB),地址必须4K对齐(低12位为0)
3.2.4 页编程(Page Program)
每次最多写入256字节(一页):
void W25Qxx_PageWrite(uint8_t* pBuffer, uint32_t WriteAddr, uint16_t NumByteToWrite) { W25Qxx_WriteEnable(); SPI_FLASH_CS_LOW(); SPIx_SendByte(0x02); // 页编程指令 SPIx_SendByte((WriteAddr >> 16) & 0xFF); SPIx_SendByte((WriteAddr >> 8) & 0xFF); SPIx_SendByte(WriteAddr & 0xFF); while(NumByteToWrite--) { SPIx_SendByte(*pBuffer++); } SPI_FLASH_CS_HIGH(); W25Qxx_WaitForWriteEnd(); }3.2.5 数据读取
读取操作没有长度限制:
void W25Qxx_ReadData(uint8_t* pBuffer, uint32_t ReadAddr, uint16_t NumByteToRead) { SPI_FLASH_CS_LOW(); SPIx_SendByte(0x03); // 读数据指令 SPIx_SendByte((ReadAddr >> 16) & 0xFF); SPIx_SendByte((ReadAddr >> 8) & 0xFF); SPIx_SendByte(ReadAddr & 0xFF); while(NumByteToRead--) { *pBuffer++ = SPIx_SendByte(0xFF); // 发送dummy字节获取数据 } SPI_FLASH_CS_HIGH(); }4. 高级应用与性能优化
4.1 DMA加速SPI传输
对于大数据量传输,使用DMA可以显著提高效率并降低CPU占用:
void SPIx_DMA_Init(void) { DMA_InitTypeDef DMA_InitStructure; // 使能DMA时钟 RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // 配置TX DMA DMA_DeInit(DMA1_Channel3); DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&SPI1->DR; DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)0; // 运行时设置 DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; DMA_InitStructure.DMA_BufferSize = 0; // 运行时设置 DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; DMA_InitStructure.DMA_Priority = DMA_Priority_High; DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; DMA_Init(DMA1_Channel3, &DMA_InitStructure); // 配置RX DMA(类似上述配置,方向为PeripheralSRC) // ... // 使能SPI DMA请求 SPI_I2S_DMACmd(SPI1, SPI_I2S_DMAReq_Tx | SPI_I2S_DMAReq_Rx, ENABLE); }4.2 双线/四线快速读模式
某些SPI Flash支持双线或四线快速读模式,可大幅提高读取速度:
void W25Qxx_FastReadData(uint8_t* pBuffer, uint32_t ReadAddr, uint16_t NumByteToRead) { SPI_FLASH_CS_LOW(); SPIx_SendByte(0x0B); // 快速读指令 SPIx_SendByte((ReadAddr >> 16) & 0xFF); SPIx_SendByte((ReadAddr >> 8) & 0xFF); SPIx_SendByte(ReadAddr & 0xFF); SPIx_SendByte(0xFF); // dummy字节 while(NumByteToRead--) { *pBuffer++ = SPIx_SendByte(0xFF); } SPI_FLASH_CS_HIGH(); }4.3 文件系统集成
对于需要存储大量数据的应用,可以集成FatFs等文件系统:
DSTATUS disk_initialize(BYTE pdrv) { // 初始化SPI Flash SPI_FLASH_Init(); // 检查Flash ID if(SPI_FLASH_ReadID() != sFLASH_ID) { return STA_NOINIT; } return RES_OK; } DRESULT disk_read(BYTE pdrv, BYTE* buff, LBA_t sector, UINT count) { uint32_t addr = sector * FLASH_SECTOR_SIZE; for(uint32_t i=0; i<count; i++) { W25Qxx_ReadData(buff, addr, FLASH_SECTOR_SIZE); buff += FLASH_SECTOR_SIZE; addr += FLASH_SECTOR_SIZE; } return RES_OK; }5. 调试技巧与常见问题
5.1 典型问题排查
通信失败
- 检查硬件连接:SCK、MOSI、MISO、NSS是否接反
- 确认SPI模式:CPOL/CPHA设置是否与从设备匹配
- 测量SCK信号:用示波器检查时钟是否正常输出
写入不成功
- 确保发送了Write Enable指令(0x06)
- 检查状态寄存器BUSY位是否清除
- 确认目标区域已擦除(Flash只能将1改为0)
数据校验错误
- 降低SPI时钟频率测试(可能存在信号完整性问题)
- 检查电源稳定性(Flash在写入时可能需要更大电流)
- 确认时序参数(建立/保持时间)是否满足要求
5.2 性能优化建议
合理设置SPI时钟
- 对于短距离板级连接,可尝试最高时钟频率
- 长线传输或干扰环境应适当降低频率
使用DMA传输
- 大数据量传输时使用DMA减少CPU开销
- 合理设置DMA缓冲区大小(通常256-512字节为宜)
批量操作优化
- 尽量合并小数据写入(先缓存再整页写入)
- 合理安排擦除操作(避免频繁小擦除)
5.3 实际项目经验
Flash寿命管理
- SPI Flash典型擦写寿命为10万次
- 实现磨损均衡算法延长使用寿命
- 关键数据区应预留备份空间
异常处理机制
- 添加超时检测(发送/接收等待)
- 实现CRC校验确保数据完整性
- 重要操作前检查设备ID
低功耗设计
- 空闲时进入Power Down模式(指令0xB9)
- 降低SPI时钟频率减少动态功耗
- 必要时完全断开Flash电源
通过以上详细的配置说明和实战经验,开发者可以快速掌握STM32 SPI通信的核心技术,并能够根据实际项目需求进行灵活调整和优化。
