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_SendByte
4: 所有操作的固定套路(四步模式)
5: 读 ID
6: 擦除扇区
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: 策略:命令+地址轮询,数据走 DMA
3: 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; // ④ CPHA=1 SpiHandle.Init.CLKPolarity = SPI_POLARITY_HIGH; // ⑤ CPOL=1 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 个 SPI(SPI1~SPI6),W25Q256 挂在 SPI5(PF7=SCK, PF8=MISO, PF9=MOSI, PF6=CS)。
② BaudRatePrescaler — SPI 时钟 = APB 时钟 / 分频数
_2(二分频)。SPI5 在 APB2(90MHz),90/2 =45MHz。
| 分频 | 时钟 | 场景 |
|---|---|---|
_2 | 45MHz | Flash 高速读写 |
_256 | 352KHz | 慢速外设 |
UART 叫 BaudRate,I2C 叫 ClockSpeed,SPI 叫 BaudRatePrescaler——本质都是"通信速率"。
③ Direction — 通信方向
SPI_DIRECTION_2LINES(全双工):MOSI + MISO 两根数据线,发的同时也在收。I2C 也是 2 根线但半双工(同一时刻只能发或收),SPI 全双工是真正的收发并行。
④⑤ CLKPolarity + CLKPhase — SPI 模式
CLKPolarity = HIGH → CPOL=1(空闲时 SCK 高电平) CLKPhase = 2EDGE → CPHA=1(第二个边沿采样) → 模式 3
| 模式 | CPOL | CPHA | 空闲 SCK | 采样边沿 |
|---|---|---|---|---|
| 模式 0 | 0 | 0 | 低 | 第 1 个(上升) |
| 模式 1 | 0 | 1 | 低 | 第 2 个(下降) |
| 模式 2 | 1 | 0 | 高 | 第 1 个(下降) |
| 模式 3 | 1 | 1 | 高 | 第 2 个(上升) |
W25Q256 支持模式 0 和 3,野火选了模式 3。
⑥⑦ CRCCalculation + CRCPolynomial
CRC 校验关掉,多项式值无效。
⑧ DataSize — 数据位宽
8BIT。W25Q256 指令和数据都是 8 位。
⑨ FirstBit — 先发高位还是低位
MSB(高位先行),SPI 协议标准做法。
⑩ NSS — 片选引脚控制方式
NSS_SOFT(软件控制)。手动调用CS_LOW()拉低选芯片,CS_HIGH()拉高释放。选硬件模式的话硬件自动管,但不灵活。
⑪ TIMode — TI 专用模式
关掉,德州仪器的特殊 SPI 变体,用不到。
⑫ Mode — 主设备还是从设备
SPI_MODE_MASTER。STM32 控制时钟 + CS,W25Q256 是从设备,等着被指挥。
为什么HAL_SPI_Init后还要__HAL_SPI_ENABLE?
I2C 的HAL_I2C_Init内部自动使能,SPI 需要手动补一刀才能跑起来。
跟 UART / I2C Init 对比
| 字段 | UART | I2C | SPI |
|---|---|---|---|
| 速率 | BaudRate | ClockSpeed | BaudRatePrescaler |
| 数据位宽 | WordLength | AddressingMode | DataSize |
| 模式 | Mode=TX_RX | — | Mode=MASTER |
| 特有 | StopBits/Parity | DualAddress/ClockStretch | CPOL/CPHA/NSS |
12 个字段,6 个跟 I2C/UART 同理,4 个 SPI 特有(CPOL/CPHA/NSS/FirstBit),2 个关了不用。
二、野火源码流程分析
1: 分清 HAL 函数和野火封装函数
HAL 库自带的函数(ST 提供):
| HAL 函数 | 作用 |
|---|---|
HAL_SPI_Init() | SPI 初始化 |
__HAL_SPI_ENABLE() | 使能 SPI 硬件 |
__HAL_SPI_GET_FLAG() | 读 SPI 状态标志(TXE、RXNE) |
WRITE_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_SendByte
uint8_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: 读 ID
CS_LOW(); SendByte(W25X_JedecDeviceID); // ② 命令 0x9F Temp0 = SendByte(Dummy_Byte); // ③ 收 3 字节 Temp1 = SendByte(Dummy_Byte); Temp2 = SendByte(Dummy_Byte); CS_HIGH(); // ④ // ID = (Temp0<<16) | (Temp1<<8) | 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 对比
| AT24C02(I2C) | W25Q256(SPI) | |
|---|---|---|
| 选中芯片 | 发设备地址 0xA0 | CS 引脚拉低 |
| 发命令 | 不需要 | 先发 1 字节命令 |
| 地址字节 | 1 字节(0~255) | 4 字节(0~32MB) |
| 写前擦除 | 不需要 | 必须擦 |
| 完成等待 | HAL_Delay(5) | WaitForWriteEnd(轮询忙标志) |
| 页大小 | 8 字节 | 256 字节 |
| 底层收发 | HAL_I2C_Mem_Write/Read | 自己写SendByte(逐字节轮询) |
12: W25Q256 芯片参数
| 参数 | 值 |
|---|---|
| 容量 | 256Mbit =32MB |
| 制造商 | Winbond(华邦) |
| JEDEC ID | 0xEF4019(EF=Winbond, 40=SPI Flash, 19=型号) |
| 页大小 | 256 字节 |
| 扇区大小 | 4KB(4096 字节) |
| 擦除单位 | 扇区(4KB)/ 块(64KB)/ 全片 |
| 写入限制 | 必须先擦再写(Flash 特性),擦后全为0xFF |
| 写入前 | 需发写使能命令0x06 |
| 写入后 | 需等 WIP 忙标志清零(读状态寄存器0x05) |
| 地址模式 | 上电默认 3 字节(24 位),可切 4 字节(32 位) |
| SPI 模式 | 支持模式 0 和模式 3 |
| 最大 SPI 时钟 | 133MHz |
| 接口 | SPI5,PF7(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.c | Init → SendByte(HAL版) → WriteEnable → WaitBusy → ReadID → EraseSector → WritePage → BufferWrite(跨页) → BufferRead |
main.c | 读 ID 验证 → 擦 → 写 16 字节 → 读回 → 对比 |
hal_conf.h | 启用HAL_SPI_MODULE_ENABLED |
关键改动
| 野火原版 | 我们的简化版 |
|---|---|
| MSP 回调里配 GPIO | Init 里直接写 |
SendByte用WRITE_REG/READ_REG | HAL_SPI_TransmitReceive一步搞定 |
CS 用BSRR寄存器 | HAL_GPIO_WritePin |
SPI_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 跟 EEPROM(AT24C02 可以按字节写不用擦)最大的区别。
四、SPI 模式 0 vs 模式 3
切模式只改 Init 里两行:
/* ── 模式 0:CPOL=0, CPHA=0 ── */ hspi_flash.Init.CLKPolarity = SPI_POLARITY_LOW; // 空闲低 hspi_flash.Init.CLKPhase = SPI_PHASE_1EDGE; // 第一个边沿采样 /* ── 模式 3:CPOL=1, CPHA=1 ── */ //hspi_flash.Init.CLKPolarity = SPI_POLARITY_HIGH; // 空闲高 //hspi_flash.Init.CLKPhase = SPI_PHASE_2EDGE; // 第二个边沿采样
时序对比:
模式3:SCK 空闲高  ̄ ̄\___/ ̄ ̄\___/ ̄ ̄ 第二个边沿锁存 模式0:SCK 空闲低 ___/ ̄ ̄\___/ ̄ ̄\___ 第一个边沿锁存
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 写时传入 |
|---|---|---|
pTxData | spi_dummy(全 0xFF,只为产生时钟) | pData(要写进 Flash 的真实数据) |
pRxData | pData(收到的就是 Flash 读出的数据) | spi_dummy(收到的垃圾,覆盖了无所谓) |
Size | len | len |
调用后立刻返回,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 IT | I2C IT | |
|---|---|---|
| ISR 数量 | 1 个(SPI5_IRQHandler) | 2 个(EV+ER) |
| 回调 | TxRxCpltCallback(收发同一个) | MemTxCpltCallback+MemRxCpltCallback |
| 命名坑 | 无(就一个回调) | Master vs Mem 两个家族 |
| 命令+地址 | 轮询(省事) | 全走 IT |
六、SPI DMA 读写
1: DMA 映射
| 方向 | DMA | Stream | Channel |
|---|---|---|---|
| SPI5_TX | DMA2 | Stream 4 | Channel 2 |
| SPI5_RX | DMA2 | Stream 3 | Channel 2 |
2: 策略:命令+地址轮询,数据走 DMA
IT 和 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_TransmitReceive | HAL_SPI_TransmitReceive_IT | HAL_SPI_TransmitReceive_DMA |
| 搬运工 | CPU 逐字节 | CPU 每字节进一次 ISR | DMA 全包,CPU 旁观 |
| 命令+地址 | 轮询 | 轮询 | 轮询 |
| 数据段中断次数 | 0(阻塞等) | N 次 TXE(每字节 1 次) | 0 次(DMA 收尾 2 次中断) |
| 回调 | 无 | TxRxCpltCallback | 同一个TxRxCpltCallback |
| ISR 数量 | 0 | 1(SPI5_IRQHandler) | 3(DMA2_Stream4+DMA2_Stream3+SPI5) |
| CPU 在干嘛 | 死等 | 每字节中断存一次 | 全程旁观 |
| 适合场景 | 小数据、调试 | 中等数据 | 大数据、跨页 |
SPI 比 I2C 简单的原因:只有一个回调、没有 Master vs Mem 家族之分、不用手搓 IDLE——HAL 数到 Size 归零自动回调。