F429-HAL-SPI5读取W25Q256(2026/7/25)

发布时间:2026/7/26 0:27:51
F429-HAL-SPI5读取W25Q256(2026/7/25) 目录一、SPI Init 字段逐个解析① Instance — 用哪个 SPI 硬件② BaudRatePrescaler — SPI 时钟 APB 时钟 / 分频数③ Direction — 通信方向④⑤ CLKPolarity CLKPhase — SPI 模式⑥⑦ CRCCalculation CRCPolynomial⑧ DataSize — 数据位宽⑨ FirstBit — 先发高位还是低位⑩ NSS — 片选引脚控制方式⑪ TIMode — TI 专用模式⑫ Mode — 主设备还是从设备为什么 HAL_SPI_Init 后还要 __HAL_SPI_ENABLE跟 UART / I2C Init 对比二、野火源码流程分析1: 分清 HAL 函数和野火封装函数2: 跟 I2C 的关键区别3: 最核心的底层函数SPI_FLASH_SendByte4: 所有操作的固定套路四步模式5: 读 ID6: 擦除扇区7: 写页8: 读数据9: 跨页写入逻辑10: WaitForWriteEnd — 等芯片忙完11: 跟 I2C AT24C02 对比12: W25Q256 芯片参数三、写我们自己的 SPI_Flash 驱动轮询版改动清单关键改动驱动架构四、SPI 模式 0 vs 模式 3五、SPI 中断读写1: 策略数据走中断4: HAL_SPI_TransmitReceive_IT 形参2: IT 写完整跳转链以 16 字节为例3: 跟 I2C 对比六、SPI DMA 读写1: DMA 映射2: 策略命令地址轮询数据走 DMA3: DMA 写完整跳转链以 16 字节为例4: 跟 IT 版对比5: IT/DMA 跨页写入6: 三种模式对比一、SPI Init 字段逐个解析SpiHandle.Instance SPI5; // ① 选硬件 SpiHandle.Init.BaudRatePrescaler SPI_BAUDRATEPRESCALER_2; // ② 时钟分频 SpiHandle.Init.Direction SPI_DIRECTION_2LINES; // ③ 全双工 SpiHandle.Init.CLKPhase SPI_PHASE_2EDGE; // ④ CPHA1 SpiHandle.Init.CLKPolarity SPI_POLARITY_HIGH; // ⑤ CPOL1 SpiHandle.Init.CRCCalculation SPI_CRCCALCULATION_DISABLE; // ⑥ CRC 关 SpiHandle.Init.CRCPolynomial 7; // ⑦ 用不到 SpiHandle.Init.DataSize SPI_DATASIZE_8BIT; // ⑧ 8 位 SpiHandle.Init.FirstBit SPI_FIRSTBIT_MSB; // ⑨ 高位先行 SpiHandle.Init.NSS SPI_NSS_SOFT; // ⑩ 软件控制 CS SpiHandle.Init.TIMode SPI_TIMODE_DISABLE; // ⑪ TI 模式关 SpiHandle.Init.Mode SPI_MODE_MASTER; // ⑫ STM32 做主 ​ HAL_SPI_Init(SpiHandle); __HAL_SPI_ENABLE(SpiHandle); // ← SPI 需要手动使能① Instance — 用哪个 SPI 硬件SPI5。F429 有 6 个 SPISPI1~SPI6W25Q256 挂在 SPI5PF7SCK, PF8MISO, PF9MOSI, PF6CS。② BaudRatePrescaler — SPI 时钟 APB 时钟 / 分频数_2二分频。SPI5 在 APB290MHz90/2 45MHz。分频时钟场景_245MHzFlash 高速读写_256352KHz慢速外设UART 叫 BaudRateI2C 叫 ClockSpeedSPI 叫 BaudRatePrescaler——本质都是通信速率。③ Direction — 通信方向SPI_DIRECTION_2LINES全双工MOSI MISO 两根数据线发的同时也在收。I2C 也是 2 根线但半双工同一时刻只能发或收SPI 全双工是真正的收发并行。④⑤ CLKPolarity CLKPhase — SPI 模式CLKPolarity HIGH → CPOL1空闲时 SCK 高电平 CLKPhase 2EDGE → CPHA1第二个边沿采样 → 模式 3模式CPOLCPHA空闲 SCK采样边沿模式 000低第 1 个上升模式 101低第 2 个下降模式 210高第 1 个下降模式 311高第 2 个上升W25Q256 支持模式 0 和 3野火选了模式 3。⑥⑦ CRCCalculation CRCPolynomialCRC 校验关掉多项式值无效。⑧ DataSize — 数据位宽8BIT。W25Q256 指令和数据都是 8 位。⑨ FirstBit — 先发高位还是低位MSB高位先行SPI 协议标准做法。⑩ NSS — 片选引脚控制方式NSS_SOFT软件控制。手动调用CS_LOW()拉低选芯片CS_HIGH()拉高释放。选硬件模式的话硬件自动管但不灵活。⑪ TIMode — TI 专用模式关掉德州仪器的特殊 SPI 变体用不到。⑫ Mode — 主设备还是从设备SPI_MODE_MASTER。STM32 控制时钟 CSW25Q256 是从设备等着被指挥。为什么HAL_SPI_Init后还要__HAL_SPI_ENABLEI2C 的HAL_I2C_Init内部自动使能SPI 需要手动补一刀才能跑起来。跟 UART / I2C Init 对比字段UARTI2CSPI速率BaudRateClockSpeedBaudRatePrescaler数据位宽WordLengthAddressingModeDataSize模式ModeTX_RX—ModeMASTER特有StopBits/ParityDualAddress/ClockStretchCPOL/CPHA/NSS12 个字段6 个跟 I2C/UART 同理4 个 SPI 特有CPOL/CPHA/NSS/FirstBit2 个关了不用。二、野火源码流程分析1: 分清 HAL 函数和野火封装函数HAL 库自带的函数ST 提供HAL 函数作用HAL_SPI_Init()SPI 初始化__HAL_SPI_ENABLE()使能 SPI 硬件__HAL_SPI_GET_FLAG()读 SPI 状态标志TXE、RXNEWRITE_REG()CMSIS 宏直接写硬件寄存器READ_REG()CMSIS 宏直接读硬件寄存器野火自己写的封装函数bsp_spi_flash.c 里层函数做了什么底层SPI_FLASH_SendByte()逐字节收发用WRITE_REG/READ_REG操作 DR 寄存器底层SPI_FLASH_CS_LOW/HIGH()宏手动拉低/拉高 CS 引脚中层SPI_FLASH_WriteEnable()发写使能命令CS 低 → 发 0x06 → CS 高中层SPI_FLASH_WaitForWriteEnd()死等 WIP 忙标志清零中层SPI_FLASH_ReadID()发 0x9F → 收 3 字节 ID高层SPI_FLASH_SectorErase()擦除写使能 → 发 0x20地址 → 等完成高层SPI_FLASH_PageWrite()页写CS 低 → 0x02 → 地址 → 数据 → CS 高高层SPI_FLASH_BufferWrite()跨页自动拆分高层SPI_FLASH_BufferRead()CS 低 → 0x03 → 地址 → 收数据 → CS 高2: 跟 I2C 的关键区别I2C AT24C02 SPI W25Q256 ─────────── ─────────── HAL_I2C_Mem_Write() ❌ HAL 没有 SPI Flash 专用的读写函数 HAL_I2C_Mem_Read() 需要自己用 SendByte 搭 CS 低 → 发命令 → 发地址 → 收发数据 → CS 高I2C 的 HAL 提供了Mem_Write/Mem_Read一键读写 EEPROM。SPI 的 HAL没有Flash 专用函数——因为不同厂家 Flash 的命令集不一样ST 没法统一封装。野火从底层 SendByte 开始自己搭了一套。3: 最核心的底层函数SPI_FLASH_SendByteuint8_t SPI_FLASH_SendByte(uint8_t byte) { while (__HAL_SPI_GET_FLAG(SpiHandle, SPI_FLAG_TXE) RESET); // ① 等 TXE发送缓冲区空 WRITE_REG(SpiHandle.Instance-DR, byte); // ② 写进 DR → 硬件开始发 ​ while (__HAL_SPI_GET_FLAG(SpiHandle, SPI_FLAG_RXNE) RESET); // ③ 等 RXNE接收缓冲区有数据 return READ_REG(SpiHandle.Instance-DR); // ④ 读走收到的字节 }SPI 全双工的本质发一个字节的同时必然收到一个字节。即使不想收比如发命令也得读走 DR否则下一个字节堵住。发 0x9F读 ID 命令 │ ▼ SCK: ┌─┐┌─┐┌─┐┌─┐┌─┐┌─┐┌─┐┌─┐ MOSI: 1 0 0 1 1 1 1 1 ← STM32 发出 0x9F MISO: x x x x x x x x ← 同时收到垃圾数据不管 ↑ 每个时钟周期一发一收同时发生4: 所有操作的固定套路四步模式① CS 拉低 — 选中芯片我要跟你说话了 ② 发命令 — 告诉芯片我要读/写/擦/读 ID ③ 发/收数据 — 具体数据在这一步 ④ CS 拉高 — 释放芯片我说完了5: 读 IDCS_LOW(); SendByte(W25X_JedecDeviceID); // ② 命令 0x9F Temp0 SendByte(Dummy_Byte); // ③ 收 3 字节 Temp1 SendByte(Dummy_Byte); Temp2 SendByte(Dummy_Byte); CS_HIGH(); // ④ // ID (Temp016) | (Temp18) | Temp2收 ID 时SendByte(Dummy_Byte)——发 0xFF 无关数据目的是产生时钟让芯片把 ID 推到 MISO 上给我们收。6: 擦除扇区CS_LOW(); SendByte(W25X_SectorErase); // ② 命令 0x20 SendByte((addr 24) 0xFF); // ③ 4 字节地址 SendByte((addr 16) 0xFF); SendByte((addr 8) 0xFF); SendByte(addr 0xFF); CS_HIGH(); // ④ SPI_FLASH_WaitForWriteEnd(); // 等芯片擦完写之前必须先擦W25Q256 写之前必须把对应扇区擦成0xFF全 1。这是 NOR Flash 的物理特性。7: 写页CS_LOW(); SendByte(W25X_PageProgram); // ② 命令 0x02 SendByte((addr 24) 0xFF); // ③ 4 字节地址 // ... while (NumByteToWrite--) { SendByte(*pBuffer); // 逐个字节发数据 } CS_HIGH(); // ④ SPI_FLASH_WaitForWriteEnd(); // 等写完一页最多 256 字节写多了会绕回页开头覆盖。8: 读数据CS_LOW(); SendByte(W25X_ReadData); // ② 命令 0x03 // ③ 4 字节地址 while (NumByteToRead--) { *pBuffer SendByte(Dummy_Byte); // 逐个字节收 } CS_HIGH();读没有跨页限制可以一口气读到底。9: 跨页写入逻辑W25Q256 一页 256 字节。如果起始地址不对齐页边界SPI_FLASH_BufferWrite拆成多次PageWrite想从地址 0x00F0离页边界差 16 字节写 300 字节 第 1 页: 0x00F0, 16 字节 → 凑满当前页 第 2 页: 0x0100, 256 字节 → 整页 第 3 页: 0x0200, 28 字节 → 剩余跟 AT24C02 的跨页逻辑一样只是页大小从 8 变成了 256。10: WaitForWriteEnd — 等芯片忙完void SPI_FLASH_WaitForWriteEnd(void) { uint8_t status; CS_LOW(); SendByte(W25X_ReadStatusReg); // 发 0x05读状态寄存器 do { status SendByte(Dummy_Byte); // 读状态 } while (status WIP_Flag); // 忙标志1 继续等 CS_HIGH(); }擦除和写入不是瞬间完成的——擦 4KB 扇区要几百毫秒。芯片干活期间忙标志置 1写完清零。11: 跟 I2C AT24C02 对比AT24C02I2CW25Q256SPI选中芯片发设备地址 0xA0CS 引脚拉低发命令不需要先发 1 字节命令地址字节1 字节0~2554 字节0~32MB写前擦除不需要必须擦完成等待HAL_Delay(5)WaitForWriteEnd轮询忙标志页大小8 字节256 字节底层收发HAL_I2C_Mem_Write/Read自己写SendByte逐字节轮询12: W25Q256 芯片参数参数值容量256Mbit 32MB制造商Winbond华邦JEDEC ID0xEF4019EFWinbond, 40SPI Flash, 19型号页大小256 字节扇区大小4KB4096 字节擦除单位扇区4KB/ 块64KB/ 全片写入限制必须先擦再写Flash 特性擦后全为0xFF写入前需发写使能命令0x06写入后需等 WIP 忙标志清零读状态寄存器0x05地址模式上电默认 3 字节24 位可切 4 字节32 位SPI 模式支持模式 0 和模式 3最大 SPI 时钟133MHz接口SPI5PF7(SCK) / PF8(MISO) / PF9(MOSI) / PF6(CS)JEDEC ID 读回来是 3 字节24 位用uint32_t存是因为 C 语言没有uint24_t类型——高位自动补零。地址模式下所有地址操作读/写/擦都跟 3 字节地址切 4 字节模式后跟 4 字节。三、写我们自己的 SPI_Flash 驱动轮询版改动清单文件改了啥SPI_Flash.h引脚宏 W25Q256 命令 函数声明SPI_Flash.cInit → SendByte(HAL版) → WriteEnable → WaitBusy → ReadID → EraseSector → WritePage → BufferWrite(跨页) → BufferReadmain.c读 ID 验证 → 擦 → 写 16 字节 → 读回 → 对比hal_conf.h启用HAL_SPI_MODULE_ENABLED关键改动野火原版我们的简化版MSP 回调里配 GPIOInit 里直接写SendByte用WRITE_REG/READ_REGHAL_SPI_TransmitReceive一步搞定CS 用BSRR寄存器HAL_GPIO_WritePinSPI_FLASH_Mode_Init()4 字节模式去掉地址 0 不需要一堆调试宏砍掉驱动架构main ├─ SPI_Flash_ReadID() ───── CS低→发0x9F→收3字节→CS高 ├─ SPI_Flash_EraseSector() ─ 写使能→发0x20地址→等忙完 ├─ SPI_Flash_BufferWrite() ─ 自动跨页拆分→循环调WritePage └─ SPI_Flash_BufferRead() ── CS低→发0x03地址→收N字节→CS高 │ ├─ WritePage ────────── 写使能→CS低→0x02→地址→数据→CS高→等忙完 ├─ WriteEnable ──────── CS低→发0x06→CS高 └─ WaitBusy ─────────── CS低→发0x05→死等WIP清零→CS高 │ └─ SendByte ────── HAL_SPI_TransmitReceive(发1收1)W25Q256 32MB 1 页 256 字节 → 共 131,072 页 1 扇区 4KB 16 页 → 共 8,192 扇区 1 块 64KB 16 扇区 → 共 512 块擦除只能按扇区4KB、块64KB、或全片来不能只擦一页或几个字节。这是 Flash 跟 EEPROMAT24C02 可以按字节写不用擦最大的区别。四、SPI 模式 0 vs 模式 3切模式只改 Init 里两行/* ── 模式 0CPOL0, CPHA0 ── */ hspi_flash.Init.CLKPolarity SPI_POLARITY_LOW; // 空闲低 hspi_flash.Init.CLKPhase SPI_PHASE_1EDGE; // 第一个边沿采样 /* ── 模式 3CPOL1, CPHA1 ── */ //hspi_flash.Init.CLKPolarity SPI_POLARITY_HIGH; // 空闲高 //hspi_flash.Init.CLKPhase SPI_PHASE_2EDGE; // 第二个边沿采样时序对比模式3SCK 空闲高 \___/\___/ 第二个边沿锁存 模式0SCK 空闲低 ___/\___/\___ 第一个边沿锁存W25Q256 两种都支持实测模式 0 和模式 3 均能正常读 ID、擦写、回读验证通过。五、SPI 中断读写1: 策略数据走中断Flash 操作分两段——命令地址就 4 字节轮询处理批量数据才走 IT/* ── IT 读 ── */ void SPI_Flash_BufferRead_IT(uint8_t *pData, uint32_t addr, uint32_t len) { spi_rx_done 0; FLASH_CS_LOW(); SPI_Flash_SendByte(CMD_READ_DATA); // ① 命令轮询 SPI_Flash_SendByte((addr 16) 0xFF); // ② 地址轮询 SPI_Flash_SendByte((addr 8) 0xFF); SPI_Flash_SendByte(addr 0xFF); // ③ 填充哑字节启动 IT 传输 for (uint32_t i 0; i len; i) spi_dummy[i] DUMMY_BYTE; HAL_SPI_TransmitReceive_IT(hspi_flash, spi_dummy, pData, len); }/* ── IT 写单页── */ void SPI_Flash_BufferWrite_IT(uint8_t *pData, uint32_t addr, uint32_t len) { spi_tx_done 0; SPI_Flash_WriteEnable(); // ① 写使能轮询 FLASH_CS_LOW(); SPI_Flash_SendByte(CMD_PAGE_PROGRAM); // ② 命令轮询 SPI_Flash_SendByte((addr 16) 0xFF); // ③ 地址轮询 SPI_Flash_SendByte((addr 8) 0xFF); SPI_Flash_SendByte(addr 0xFF); // ④ 数据段走中断 HAL_SPI_TransmitReceive_IT(hspi_flash, pData, spi_dummy, len); }/* ── 收发完成回调读和写都走这里── */ void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi-Instance SPI5) { FLASH_CS_HIGH(); spi_tx_done 1; spi_rx_done 1; } }4: HAL_SPI_TransmitReceive_IT 形参HAL_StatusTypeDef HAL_SPI_TransmitReceive_IT( SPI_HandleTypeDef *hspi, // ① SPI 句柄 uint8_t *pTxData, // ② 发送数据来源发完地址自增从这读下一字节 uint8_t *pRxData, // ③ 接收数据去处每收一字节往这存地址自增 uint16_t Size // ④ 收发字节数发完 Size 个自动回调 TxRxCpltCallback );形参IT 读时传入IT 写时传入pTxDataspi_dummy全 0xFF只为产生时钟pData要写进 Flash 的真实数据pRxDatapData收到的就是 Flash 读出的数据spi_dummy收到的垃圾覆盖了无所谓Sizelenlen调用后立刻返回SPI 硬件在后台逐字节收发CPU 该干嘛干嘛。Size 发完后触发TxRxCpltCallback。2: IT 写完整跳转链以 16 字节为例SPI_Flash_BufferWrite_IT 调用 │ ▼ HAL_SPI_TransmitReceive_IT 启动装第一个字节进 DR │ ▼ CPU 返回 main卡在 while (!spi_tx_done) │ ▼ 硬件 SPI 发出第一个字节 → TXE 中断 → SPI5_IRQHandler │ └─ HAL_SPI_IRQHandler从 buf 取下一个字节装进 DR收到的垃圾存 spi_dummy │ └─ 没发完不调回调直接返回 │ ▼ 硬件 SPI 发出第二个字节 → TXE 中断 → 同上循环... │ ▼ ...发完最后一字节 → TXE 中断 → HAL_SPI_IRQHandler │ └─ 计数归零调 HAL_SPI_TxRxCpltCallback │ └─ FLASH_CS_HIGH() spi_tx_done 1 │ ▼ main 看到 spi_tx_done 1 → 退出 while → 继续往下CPU 每字节进一次 TXE 中断但 HAL 内部自动处理——取下一字节、装 DR、判断是否发完。你只在最后一次收到回调。3: 跟 I2C 对比SPI ITI2C ITISR 数量1 个SPI5_IRQHandler2 个EVER回调TxRxCpltCallback收发同一个MemTxCpltCallbackMemRxCpltCallback命名坑无就一个回调Master vs Mem 两个家族命令地址轮询省事全走 IT六、SPI DMA 读写1: DMA 映射方向DMAStreamChannelSPI5_TXDMA2Stream 4Channel 2SPI5_RXDMA2Stream 3Channel 22: 策略命令地址轮询数据走 DMAIT 和 DMA 共用同一个回调HAL_SPI_TxRxCpltCallback。3: DMA 写完整跳转链以 16 字节为例SPI_Flash_BufferWrite_DMA 调用 │ ① 写使能 命令 地址轮询 4 字节 │ ② HAL_SPI_TransmitReceive_DMA 启动 │ DMA 被配好源你的buf目标SPI5 DR次数16 │ 立刻返回 ← CPU 走了 │ ▼ CPU 返回 main卡在 while (!spi_tx_done) │ ▼ DMA2_Stream4 自己干活从 buf 取字节 → 装进 SPI5 DR → 下一个 → ... │ CPU 完全不知道也没有中断 │ ▼ DMA 搬完第 16 个字节 → DMA 计数器归零 → TC 信号 │ ├─ DMA2_Stream4_IRQHandler ← ① 第一波DMA 中断 │ └─ HAL_DMA_IRQHandler ← 清 DMA 标志 │ └─ 同时硬件 TC 信号触发 SPI 传输完成 │ ▼ SPI5_IRQHandler ← ② 第二波SPI 中断 └─ HAL_SPI_IRQHandler ← HAL 知道传输完成 └─ HAL_SPI_TxRxCpltCallback ← ③ 回调IT 和 DMA 共用同一个 └─ FLASH_CS_HIGH() spi_tx_done 1 │ ▼ main 看到 spi_tx_done 1 → 退出 while → 继续往下4: 跟 IT 版对比IT 写 DMA 写 每字节触发 1 次 TXE 中断 DMA 全包16 字节 0 次数据中断 16 字节 16 次 IRQ DMA 只触发 1 组收尾中断DMA_TC → SPI_TC CPU 每字节进一次 ISR CPU 全程旁观收尾时被通知一次5: IT/DMA 跨页写入轮询版的跨页逻辑搬到 IT/DMA每页之间等done标志 WaitBusy。300 字节从地址 0x00F0 写自动拆为 16 256 28 三页实测通过。6: 三种模式对比轮询 (Polling)中断 (IT)DMA底层 HAL 函数HAL_SPI_TransmitReceiveHAL_SPI_TransmitReceive_ITHAL_SPI_TransmitReceive_DMA搬运工CPU 逐字节CPU 每字节进一次 ISRDMA 全包CPU 旁观命令地址轮询轮询轮询数据段中断次数0阻塞等N 次 TXE每字节 1 次0 次DMA 收尾 2 次中断回调无TxRxCpltCallback同一个TxRxCpltCallbackISR 数量01SPI5_IRQHandler3DMA2_Stream4DMA2_Stream3SPI5CPU 在干嘛死等每字节中断存一次全程旁观适合场景小数据、调试中等数据大数据、跨页SPI 比 I2C 简单的原因只有一个回调、没有 Master vs Mem 家族之分、不用手搓 IDLE——HAL 数到 Size 归零自动回调。