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)决定:

模式CPOLCPHA时钟空闲状态数据采样边沿
000低电平奇数边沿(第一个上升沿)
101低电平偶数边沿(第一个下降沿)
210高电平奇数边沿(第一个下降沿)
311高电平偶数边沿(第一个上升沿)

对于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 Enable0x06写使能
Write Disable0x04写禁止
Read Status Register0x05读状态寄存器
Page Program0x02页编程(最大256字节)
Sector Erase0x20扇区擦除(4KB)
Block Erase0xD8块擦除(64KB)
Chip Erase0xC7整片擦除
Read Data0x03读数据
JEDEC ID0x9F读制造商和设备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 典型问题排查

  1. 通信失败

    • 检查硬件连接:SCK、MOSI、MISO、NSS是否接反
    • 确认SPI模式:CPOL/CPHA设置是否与从设备匹配
    • 测量SCK信号:用示波器检查时钟是否正常输出
  2. 写入不成功

    • 确保发送了Write Enable指令(0x06)
    • 检查状态寄存器BUSY位是否清除
    • 确认目标区域已擦除(Flash只能将1改为0)
  3. 数据校验错误

    • 降低SPI时钟频率测试(可能存在信号完整性问题)
    • 检查电源稳定性(Flash在写入时可能需要更大电流)
    • 确认时序参数(建立/保持时间)是否满足要求

5.2 性能优化建议

  1. 合理设置SPI时钟

    • 对于短距离板级连接,可尝试最高时钟频率
    • 长线传输或干扰环境应适当降低频率
  2. 使用DMA传输

    • 大数据量传输时使用DMA减少CPU开销
    • 合理设置DMA缓冲区大小(通常256-512字节为宜)
  3. 批量操作优化

    • 尽量合并小数据写入(先缓存再整页写入)
    • 合理安排擦除操作(避免频繁小擦除)

5.3 实际项目经验

  1. Flash寿命管理

    • SPI Flash典型擦写寿命为10万次
    • 实现磨损均衡算法延长使用寿命
    • 关键数据区应预留备份空间
  2. 异常处理机制

    • 添加超时检测(发送/接收等待)
    • 实现CRC校验确保数据完整性
    • 重要操作前检查设备ID
  3. 低功耗设计

    • 空闲时进入Power Down模式(指令0xB9)
    • 降低SPI时钟频率减少动态功耗
    • 必要时完全断开Flash电源

通过以上详细的配置说明和实战经验,开发者可以快速掌握STM32 SPI通信的核心技术,并能够根据实际项目需求进行灵活调整和优化。