
1. SPI协议基础与STM32F4硬件特性SPISerial Peripheral Interface是一种高速全双工同步串行通信协议由摩托罗拉在1980年代提出。它通过四根信号线实现主从设备之间的数据交换最高传输速率可达几十MHz。在STM32F4系列微控制器中SPI接口被广泛应用于Flash存储、显示屏、传感器等外设的通信。我第一次使用SPI是在一个工业传感器项目中当时需要以10MHz的速率采集数据。相比I2C协议SPI的传输速度确实快得多但硬件连接也更复杂。STM32F4的SPI控制器有几个显著特点支持主从模式切换、时钟极性/相位可调、内置硬件CRC校验以及DMA传输支持。硬件连接上STM32F4的SPI接口通常使用以下引脚SCKSerial Clock时钟信号线由主机产生MOSIMaster Out Slave In主机输出从机输入MISOMaster In Slave Out主机输入从机输出NSSSlave Select从机选择信号低电平有效实测中发现STM32F407的SPI1接口最高时钟可达42MHz当APB2时钟为84MHz时而SPI2/3最高21MHzAPB1时钟为42MHz。这个性能对于大多数应用场景已经足够比如我们接下来要操作的W25Q128 Flash芯片。2. W25Q128 Flash芯片特性解析W25Q128是华邦电子推出的一款128Mbit16MB容量的SPI Flash存储器采用3.3V供电支持标准SPI、Dual SPI和Quad SPI模式。在实际项目中我经常用它来存储固件备份、配置文件或日志数据。这款芯片有几个关键特性值得注意组织结构将16MB空间分为256个块Block每块64KB每个块又分为16个扇区Sector每扇区4KB。擦除操作最小以扇区为单位。耐久性每个扇区可擦写约10万次数据保存期限20年。指令集支持标准SPI指令如页编程Page Program、扇区擦除Sector Erase、整片擦除Chip Erase等。保护机制可通过状态寄存器设置写保护区域。在使用过程中我发现三个需要特别注意的地方写入前必须先擦除擦除后所有位变为1只能将1改为0不能将0改为1页编程操作不能跨页每页256字节有一次项目中出现数据异常后来发现是因为没有正确等待芯片就绪状态。W25Q128在执行写或擦除操作时需要一定时间必须通过读取状态寄存器的BUSY位来确认操作完成。3. STM32F4的SPI初始化与配置配置STM32F4的SPI外设主要涉及以下几个步骤3.1 引脚复用与时钟使能首先需要使能SPI和GPIO的时钟并将相关引脚配置为复用功能// 使能SPI1和GPIOB时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE); RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE); // 配置PB3/PB4/PB5为SPI复用功能 GPIO_PinAFConfig(GPIOB, GPIO_PinSource3, GPIO_AF_SPI1); GPIO_PinAFConfig(GPIOB, GPIO_PinSource4, GPIO_AF_SPI1); GPIO_PinAFConfig(GPIOB, GPIO_PinSource5, GPIO_AF_SPI1); GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF; GPIO_InitStructure.GPIO_OType GPIO_OType_PP; GPIO_InitStructure.GPIO_PuPd GPIO_PuPd_UP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_100MHz; GPIO_InitStructure.GPIO_Pin GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_5; GPIO_Init(GPIOB, GPIO_InitStructure);3.2 SPI参数配置STM32的标准库提供了SPI_InitTypeDef结构体来配置SPI工作参数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; // 空闲时SCK为高 SPI_InitStructure.SPI_CPHA SPI_CPHA_2Edge; // 第二个边沿采样 SPI_InitStructure.SPI_NSS SPI_NSS_Soft; // 软件控制片选 SPI_InitStructure.SPI_BaudRatePrescaler SPI_BaudRatePrescaler_4; SPI_InitStructure.SPI_FirstBit SPI_FirstBit_MSB; SPI_InitStructure.SPI_CRCPolynomial 7; SPI_Init(SPI1, SPI_InitStructure); SPI_Cmd(SPI1, ENABLE);这里有几个关键参数需要根据实际设备调整CPOL/CPHA必须与从设备一致W25Q128通常使用模式0或模式3波特率分频根据从设备最高时钟频率选择数据大小W25Q128使用8位数据帧我曾经遇到过SPI通信不稳定的问题后来发现是CPHA设置错误。通过逻辑分析仪抓取波形后确认W25Q128是在时钟的第二个边沿采样数据将CPHA改为SPI_CPHA_2Edge后问题解决。4. W25Q128驱动实现与优化4.1 基本读写函数实现SPI数据传输的基础函数是单字节读写uint8_t SPI1_ReadWriteByte(uint8_t TxData) { while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) RESET); SPI_I2S_SendData(SPI1, TxData); while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) RESET); return SPI_I2S_ReceiveData(SPI1); }基于这个基础函数我们可以实现W25Q128的读取操作void W25Q128_Read(uint8_t *pBuffer, uint32_t ReadAddress, uint16_t Num) { W25Q128_CS 0; SPI1_ReadWriteByte(W25X_ReadData); SPI1_ReadWriteByte((uint8_t)((ReadAddress)16)); SPI1_ReadWriteByte((uint8_t)((ReadAddress)8)); SPI1_ReadWriteByte((uint8_t)ReadAddress); for(uint16_t i0; iNum; i) { pBuffer[i] SPI1_ReadWriteByte(0xFF); } W25Q128_CS 1; }写操作稍微复杂一些需要先发送写使能指令然后执行页编程void W25Q128_Write_Page(uint8_t* pBuffer, uint32_t WriteAddress, uint16_t Num) { W25Q128_Write_Enable(); W25Q128_CS 0; SPI1_ReadWriteByte(W25X_PageProgram); SPI1_ReadWriteByte((uint8_t)((WriteAddress)16)); SPI1_ReadWriteByte((uint8_t)((WriteAddress)8)); SPI1_ReadWriteByte((uint8_t)WriteAddress); for(uint16_t i0; iNum; i) { SPI1_ReadWriteByte(pBuffer[i]); } W25Q128_CS 1; W25Q128_Wait_Busy(); }4.2 擦除操作实现W25Q128支持三种擦除方式扇区擦除4KB块擦除32KB/64KB整片擦除最常用的是扇区擦除void W25Q128_Erase_Sector(uint32_t SectorAddress) { SectorAddress * 4096; // 转换为实际地址 W25Q128_Write_Enable(); W25Q128_Wait_Busy(); W25Q128_CS 0; SPI1_ReadWriteByte(W25X_SectorErase); SPI1_ReadWriteByte((uint8_t)((SectorAddress)16)); SPI1_ReadWriteByte((uint8_t)((SectorAddress)8)); SPI1_ReadWriteByte((uint8_t)SectorAddress); W25Q128_CS 1; W25Q128_Wait_Busy(); }需要注意的是擦除操作耗时较长典型值400ms/扇区期间BUSY位会保持为1。我曾经因为没有正确等待擦除完成就进行写操作导致数据写入失败。4.3 驱动优化技巧在实际项目中我总结了几个优化SPI Flash性能的技巧使用DMA传输对于大数据量读写可以配置SPI的DMA通道减少CPU占用// 配置SPI1的DMA DMA_InitTypeDef DMA_InitStructure; RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE); DMA_InitStructure.DMA_Channel DMA_Channel_3; DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)(SPI1-DR); DMA_InitStructure.DMA_Memory0BaseAddr (uint32_t)buffer; DMA_InitStructure.DMA_DIR DMA_DIR_MemoryToPeripheral; DMA_InitStructure.DMA_BufferSize length; 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_FIFOMode DMA_FIFOMode_Disable; DMA_Init(DMA2_Stream3, DMA_InitStructure); DMA_Cmd(DMA2_Stream3, ENABLE); SPI_I2S_DMACmd(SPI1, SPI_I2S_DMAReq_Tx, ENABLE);双缓冲技术准备下一批数据时DMA正在传输当前数据合理规划擦除在系统空闲时预擦除可能需要使用的扇区使用Fast Read指令W25Q128支持0x0B快速读取指令最高时钟可达80MHz5. 实战文件系统与磨损均衡对于需要频繁读写Flash的应用直接操作底层接口不够高效。我在几个项目中移植了FatFs文件系统并实现了简单的磨损均衡算法。5.1 FatFs文件系统移植实现diskio.c中的底层接口DRESULT disk_write(BYTE pdrv, const BYTE *buff, LBA_t sector, UINT count) { W25Q128_Write_NoCheck((uint8_t*)buff, sector*4096, count*4096); return RES_OK; } DRESULT disk_read(BYTE pdrv, BYTE *buff, LBA_t sector, UINT count) { W25Q128_Read(buff, sector*4096, count*4096); return RES_OK; }配置ffconf.h#define _USE_MKFS 1 #define _FS_READONLY 0 #define _FS_MINIMIZE 0 #define _CODE_PAGE 936 #define _USE_LFN 2 #define _VOLUMES 1 #define _MAX_SS 4096 #define _MIN_SS 40965.2 简易磨损均衡实现基本思路是维护一个映射表将逻辑扇区映射到物理扇区#define SECTOR_COUNT 4096 // 16MB/4KB uint16_t sector_map[SECTOR_COUNT]; uint32_t erase_count[SECTOR_COUNT]; void wear_leveling_init() { // 初始化时所有扇区直接映射 for(int i0; iSECTOR_COUNT; i) { sector_map[i] i; erase_count[i] 0; } } uint32_t get_physical_sector(uint32_t logical_sector) { // 查找使用次数最少的物理扇区 uint32_t min_erase 0xFFFFFFFF; uint32_t target logical_sector; for(int i0; i10; i) { // 随机查找10个候选 uint32_t candidate rand() % SECTOR_COUNT; if(erase_count[candidate] min_erase) { min_erase erase_count[candidate]; target candidate; } } // 迁移数据 if(target ! logical_sector) { uint8_t buffer[4096]; W25Q128_Read(buffer, sector_map[logical_sector]*4096, 4096); W25Q128_Erase_Sector(target); W25Q128_Write_NoCheck(buffer, target*4096, 4096); erase_count[target]; sector_map[logical_sector] target; } return sector_map[logical_sector]; }这个简易算法在我的一个数据采集项目中将Flash寿命从预估的1年延长到了5年以上。当然更完善的方案应该考虑坏块管理和ECC校验。6. 常见问题与调试技巧在SPI Flash开发过程中我遇到过各种奇怪的问题这里分享几个典型案例6.1 数据写入后读取异常现象写入后立即读取数据不正确断电重启后读取正常。原因W25Q128的写入操作需要一定时间完成典型值1-3ms在此期间如果尝试读取会得到不可靠的结果。解决方案所有写操作后必须检查状态寄存器的BUSY位void W25Q128_Wait_Busy(void) { while((W25Q128_ReadSR()0x01) 0x01); }6.2 SPI时钟速率问题现象低时钟频率工作正常提高频率后通信失败。原因可能是以下问题导致线路过长或未阻抗匹配未正确配置GPIO速度为Very High从设备不支持该时钟速率解决方案缩短走线长度加匹配电阻确保GPIO配置正确GPIO_InitStructure.GPIO_Speed GPIO_Speed_100MHz;逐步提高时钟分频测试最高支持频率6.3 多从设备干扰现象系统中多个SPI设备互相干扰。原因SPI的片选信号管理不当。解决方案确保任何时候只有一个设备的CS为低切换设备时添加小延时对于共用MISO线的设备配置不用的设备为高阻态void select_device(uint8_t device) { // 先取消所有片选 W25Q128_CS 1; LCD_CS 1; Sensor_CS 1; delay_us(1); // 小延时确保信号稳定 // 选择目标设备 switch(device) { case DEV_FLASH: W25Q128_CS 0; break; case DEV_LCD: LCD_CS 0; break; case DEV_SENSOR: Sensor_CS 0; break; } delay_us(1); }7. 进阶应用内存映射与XIP执行对于需要高速读取的场景可以将SPI Flash配置为内存映射模式实现XIPeXecute In Place执行。STM32F4通过QUADSPI外设支持这一特性。7.1 QUADSPI配置初始化QUADSPI时钟和引脚RCC_AHB3PeriphClockCmd(RCC_AHB3Periph_QSPI, ENABLE); GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin GPIO_Pin_2 | GPIO_Pin_6 | GPIO_Pin_7 | GPIO_Pin_8 | GPIO_Pin_9 | GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF; GPIO_InitStructure.GPIO_Speed GPIO_Speed_100MHz; GPIO_InitStructure.GPIO_OType GPIO_OType_PP; GPIO_InitStructure.GPIO_PuPd GPIO_PuPd_UP; GPIO_Init(GPIOB, GPIO_InitStructure); GPIO_PinAFConfig(GPIOB, GPIO_PinSource6, GPIO_AF10_QSPI); // 配置其他引脚...设置QUADSPI工作参数QSPI_InitTypeDef QSPI_InitStructure; QSPI_InitStructure.QSPI_ClockPrescaler 2; // 42MHz/221MHz QSPI_InitStructure.QSPI_FifoThreshold 4; QSPI_InitStructure.QSPI_SampleShifting QSPI_SampleShifting_HalfCycle; QSPI_InitStructure.QSPI_FlashSize 23; // 16MB 2^24 QSPI_InitStructure.QSPI_CSHighTime QSPI_CSHighTime_2Cycle; QSPI_InitStructure.QSPI_ClockMode QSPI_ClockMode_Low; QSPI_InitStructure.QSPI_FlashID QSPI_FlashID_1; QSPI_InitStructure.QSPI_DualFlash QSPI_DualFlash_Disable; QSPI_Init(QSPI_InitStructure);7.2 内存映射模式启用void QSPI_Enable_MemMapped(void) { QSPI_CommandTypeDef sCommand; QSPI_MemoryMappedTypeDef sMemMappedCfg; sCommand.InstructionMode QSPI_InstructionMode_1Line; sCommand.Instruction 0xEB; // Fast Read Quad I/O sCommand.AddressMode QSPI_AddressMode_4Lines; sCommand.AddressSize QSPI_AddressSize_24bits; sCommand.AlternateByteMode QSPI_AlternateByteMode_None; sCommand.DataMode QSPI_DataMode_4Lines; sCommand.DummyCycles 6; sCommand.DdrMode QSPI_DdrMode_Disable; sCommand.DdrHoldHalfCycle QSPI_DdrHoldHalfCycle_Disable; sCommand.SIOOMode QSPI_SIOOMode_Disable; sMemMappedCfg.TimeOutActivation QSPI_TimeOutActivation_Disable; sMemMappedCfg.TimeOutPeriod 0; HAL_QSPI_MemoryMapped(hqspi, sCommand, sMemMappedCfg); }启用后Flash内容将映射到0x90000000开始的地址空间可以直接通过指针访问uint8_t *flash_ptr (uint8_t *)0x90000000; uint8_t data flash_ptr[offset]; // 直接读取在实际项目中我将UI资源存储在Flash中通过内存映射直接读取性能比传统SPI读取提升了3倍以上。不过要注意内存映射模式只支持读取写入操作仍需通过标准SPI指令完成。