STM32 SPI通信与Flash存储实战指南
1. STM32 SPI通信基础与硬件连接SPISerial Peripheral Interface是一种高速全双工的同步串行通信接口由摩托罗拉公司提出广泛应用于ADC、DAC、存储器、传感器等外设与微控制器的通信。STM32系列微控制器内置了硬件SPI外设支持主从模式配置最高通信速率可达fpclk/2对于STM32F103系列APB2总线上的SPI1最高支持36MHzAPB1总线上的SPI2/3最高支持18MHz。1.1 SPI物理层连接一个典型的SPI系统包含以下信号线SCKSerial Clock时钟信号由主机产生MOSIMaster Output Slave Input主机输出从机输入MISOMaster Input Slave Output主机输入从机输出NSSSlave Select从机选择信号低电平有效在STM32与SPI Flash如W25Q64的典型连接中PA4GPIO作为NSS信号软件控制PA5作为SCK时钟线PA6作为MISO数据输入PA7作为MOSI数据输出注意虽然STM32的硬件NSS引脚可以实现自动片选控制但在实际应用中更常使用普通GPIO进行软件控制这样更灵活且节省硬件资源。1.2 SPI协议层关键参数SPI通信有四个关键工作模式由CPOLClock Polarity和CPHAClock Phase决定模式CPOLCPHA时钟空闲状态数据采样边沿000低电平奇数边沿第一个上升沿101低电平偶数边沿第一个下降沿210高电平奇数边沿第一个下降沿311高电平偶数边沿第一个上升沿对于W25Q64 Flash芯片支持模式0和模式3。实际工程中通常选择模式3CPOL1CPHA1这是因为它能提供更好的信号稳定性。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;典型配置示例模式38位数据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扇区擦除4KBBlock Erase0xD8块擦除64KBChip Erase0xC7整片擦除Read Data0x03读数据JEDEC ID0x9F读制造商和设备ID3.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 EraseFlash写入前必须先擦除将位由0变为1void 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位为03.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 i0; icount; 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通信的核心技术并能够根据实际项目需求进行灵活调整和优化。