
简介一份面向STM32F4开发者的SPI通信实战资源基于意法半导体Cortex-M4内核MCU讲解SPI主从模式、四种工作模式、MISO/MOSI/SCLK/NSS信号线以及HAL/LL库编程方法适合入门至中级嵌入式工程师参考学习。压缩包共5个文件包含SPI初始化与收发逻辑的.c/.h源文件、链接脚本.ld、启动文件.s以及Makefile工程构建配置整体仅9KB结构精简便于快速阅读和移植到实际项目中。资源已有222人学习浏览示例中给出了SPI外设配置、片选管理、传输函数调用及多设备组网要点并补充了调试检查思路能帮助读者系统掌握STM32F4 SPI外设的配置流程与常见应用场景。1. STM32F4 的 SPI 不是会接四根线就行SPI.rar_STM32F4 SPI 这类压缩包名在嵌入式群里很常见解压后多半是一个 CubeMX 工程加几个 .c/.h。真正决定工程能否跑起来的不是这几行代码而是你是否清楚 STM32F4 这个 SPI 外设的边界四种 CPOL/CPHA 组合、8/16 位帧长、主从模式、硬件与软件片选、轮询/中断/DMA 三条收发路径以及独立的 DMA 请求映射。SPI 与 I2C/UART 最大的不同是四线全双工、上限速率高接 Flash、OLED、SD 卡和 ADC 全靠它网上大量SPI 不通的求助恰恰出在最基础的环节时序模式和从机 datasheet 对不上、软件片选忘了拉低、BSY 卡死、OVR 溢出、DMA 回调没写。下文按 CubeMX 到 HAL 再到 DMA 的顺序把最小可行工程讲透最后用逻辑分析仪做时序验证。适合正在用 F4 接外部芯片的工程师也适合从 51/MSP430 平台转过来的新手。2. 先立住 SPI 协议与 STM32F4 的 SPI 外设边界2.1 SPI 四线时序CPOL 与 CPHA 的四种组合怎么对SPI 的信号一共四根SCK 提供时钟MOSI 主出从入MISO 主入从出CS 片选低有效。主从两边像移位寄存器一样对接每个时钟沿移出 1 位、移入 1 位所以全双工效率比 I2C 高一个量级这也是接高速 Flash 和 ADC 时优先选 SPI 的原因。协议本身没有规定空闲电平和采样沿这两件事由 CPOL 和 CPHA 决定从机 datasheet 的时序图里一定会标 CPOL/CPHA 或 Mode 数字照着填即可。SPI 模式CPOLCPHASCK 空闲电平数据采样沿000低第 1 个边沿上升沿101低第 2 个边沿下降沿210高第 1 个边沿下降沿311高第 2 个边沿上升沿记忆法CPOL 决定空闲电平CPHA0 表示在第一个边沿采样CPHA1 表示在第二个边沿采样。W25Q128、GD25Q128 这类 SPI NOR Flash 和 SD 卡在模式 0 与模式 3 都能跑SSD1306 OLED 通常也是 Mode 0/3但 ADC 类器件往往只支持一种必须逐位对照 datasheet 的时序图。另一个容易忽略的点是 MISO 在没有数据时可能是高阻态SD 卡的 MISO 建议加 10kΩ 上拉否则低速时偶尔会读到乱码——TF 卡 SPI 需要上拉指的就是这条线。顺带回答一个常被问的问题ESP8266 模块能连接 SPI 接口芯片吗它的硬件 SPI 引脚通常被内部 Flash 和系统功能占用外部设备更常见的做法是 GPIO 软件模拟 SPI或者用 HSPI如果对应引脚空闲。软件模拟在低速下完全够用代价是占 CPU后面的 5.3 会拿它做一个非常实用的调试手段。Proteus 里模拟 SPI OLED 只能验证逻辑通顺真机上引脚压摆率、上拉电阻和走线电容都会影响采样窗口最终还是要拿逻辑分析仪核对。2.2 STM32F4 的 SPI 外设边界时钟上限、引脚映射和寄存器STM32F4 有三个 SPI 外设挂在两条不同的 APB 总线上这直接决定了最高时钟SPI1 在 APB2 上SPI2/3 在 APB1 上。以 SYSCLK168 MHz、APB284 MHz、APB142 MHz 的典型配置为例SPI 分频最小是 2所以 SPI1 最高 42 MHzSPI2/3 最高 21 MHz。不少人直接把 SPI1 配成 42 MHz 去接 Flash读 ID 都错原因多半是从机支持不了这个速率或者走线太长导致采样窗口不够。SPI 外设时钟总线最高 SPI 时钟常用引脚SCK/MISO/MOSISPI1APB242 MHzPA5/PA6/PA7或 PB3/PB4/PB5SPI2APB121 MHzPB13/PB14/PB15SPI3APB121 MHzPB3/PB4/PB5或 PC10/PC11/PC12像 EtherCAT 从站控制器这类芯片通过 SPI 与 MCU 通信时起步就是 20 MHz 以上加双 DMASPI1 的带宽优势立刻体现吞吐还不够再考虑 QUADSPIF42x/F43x 以上型号的 QUADSPI 外设那是另一套体系不在此文范围。寄存器层面长期盯住 SPI_SR 的 TXE、RXNE、BSY、OVR、MODF 五个标志和 SPI_DR 数据寄存器。F4 的 SPI 在 RX 侧有一个 32 位 FIFO8 位帧长时把 SPI_CR2 的 FRXTH 位置 1接收阈值降为 8 位否则 RXNE 要等攒满两个字节才置位单字节读会一直等不到数据CubeMX 默认配置未必帮你处理这个位遇到收一个字节就卡死先查这里。2.3 硬件片选还是软件片选NSS 的三种用法推荐软件片选NSS 引脚有三种命运硬件输出、硬件输入、软件控制。硬件输出模式下外设会在传输前自动拉低 NSS听起来省事但全双工多从机时要处理总线冲突硬件输入模式下主设备 NSS 一旦被外部拉低硬件立即置 MODF 并关闭 SPI生产线上一个毛刺就可能让通信停摆。所以工程里最常见的做法是软件片选把 NSS 配置禁用SSM1、SSI1CS 接到任意 GPIO事务开始拉低、事务结束拉高。Linux 下软件拉片选控制从机也是同一个思路。/* 软件片选事务边界由 GPIO 控制动作前后都要避开 BSY */ #define FLASH_CS_L() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET) #define FLASH_CS_H() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET) while (hspi1.Instance-SR SPI_SR_BSY); /* 等待上一帧总线空闲 */ FLASH_CS_L(); /* 这里执行发送/接收操作见第 3 章 */ while (hspi1.Instance-SR SPI_SR_BSY); /* 最后一拍移出完毕 */ FLASH_CS_H();软件片选的关键是拉高 CS 之前必须等 BSY 清零。F4 在某些场景下 BSY 会一直为 1常见诱因是 NSS 引脚悬空、从机 MISO 异常强拉排查时先确认 NSS 复用没被意外打开再用 __HAL_SPI_DISABLE(hspi1) 关一次、__HAL_SPI_ENABLE(hspi1) 重开多数能复位。硬件片选模式下则要查 MODF 标志通过 __HAL_SPI_CLEAR_MODF 清掉后继续。多从机共线时每个从机 MISO 必须有三态控制能力软件片选只解决 CS 管理不解决总线争用。3. 用 CubeMX 配置 STM32F4 SPI 的最小工程CubeMX 生成代码的流程本身不复杂重点是把参数表看懂。下面以 SPI1、全双工主机、PA5/PA6/PA7、软件片选PA4 作 GPIO 输出为例逐个过一遍配置项再给出可直接抄的读写代码。3.1 CubeMX 的 SPI 参数区6 个必调项参数区与推荐值的对应关系如下其中 Prescaler 会实时换算成 BaudRate注意时钟树里 RCC 必须按实际晶振和 PLL 配置否则这里的速率是错的。参数推荐值说明ModeFull-Duplex Master只发不收可选 Simplex TransmitHardware NSS SignalDisable对应软件片选CS 用 GPIOFrame FormatMotorolaTI 模式极少用Data Size8 Bits大多数从机默认 8 位First BitMSB FirstLSB First 是少数器件的特例Prescaler按器件定Flash 用 4 或 8OLED/传感器用 32 以上CPOL / CPHA查从机 datasheet模式组合见 2.1 表格这些参数生成的寄存器落点Mode、Prescaler、CPOL/CPHA、DataSize、软件 NSS 的 SSM/SSI 都在 SPI_CR1Frame Format 的 FRF 位在 SPI_CR2。生成代码之后不要手改寄存器参数要从 CubeMX 改再重新生成否则容易漏位留下隐患。Prescaler 的选择逻辑按从机上限来比如 GD25Q128 官方标称 120 MHz实际用 21 MHz 的 SPI2 完全没问题SSD1306 这类慢速屏超过 10 MHz 就可能花屏分频给到 32 或 64 更稳妥。3.2 HAL 的三种收发方式轮询、中断与 DMA 怎么选HAL 把 SPI 操作封装成 Transmit/Receive/TransmitReceive 的轮询版、IT 版和 DMA 版。轮询版适合一帧几十字节以内、CPU 可以等的场景比如读传感器状态寄存器中断版每字节触发一次中断适合中等长度数据且不想让 CPU 空转DMA 版由 DMA 搬运数据CPU 只在完成回调里收结果适合连续大块数据。判断标准很简单一帧在 1 ms 内能发完就轮询发不完且频繁就上 DMA。/* 轮询回环自测把 MOSI 与 MISO 短接收发一致说明外设链路正常 */ uint8_t txb[4] {0xA5, 0x5A, 0x3C, 0xC3}; uint8_t rxb[4] {0}; HAL_StatusTypeDef st HAL_SPI_TransmitReceive(hspi1, txb, rxb, 4, 100); if (st ! HAL_OK) { /* 超时或溢出看 hspi1-ErrorCodeHAL_SPI_ERROR_OVR 最常见 */ } if (memcmp(txb, rxb, 4) ! 0) { /* 回环不一致先查 MOSI/MISO 是否真的短接、SCK 是否有输出 */ }HAL_SPI_TransmitReceive 是同时收发MOSI 输出 txb 的同时把 MISO 采样写进 rxb第 5 个参数 100 是超时毫秒数超时返回 HAL_TIMEOUT。只收不发时要分清楚函数HAL_SPI_Receive 会在 MOSI 上自动发 0xFF 来产生时钟。提示HAL_SPI_Receive 自发 0xFF 是外设特性不是 bug读操作发的占位字节由硬件固定不需要手动配置。中断版调 HAL_SPI_TransmitReceive_IT 后实现 HAL_SPI_TxRxCpltCallback 才能拿到完成通知回调里可以置标志位注意不要在中断回调里做耗时打印或分配内存。3.3 可抄的最小实例软件片选读 GD25Q128 的 JEDEC IDGD25Q128/W25Q128 这类 SPI NOR Flash 的读 ID 命令是 0x9F主机发命令字节从机回 3 字节厂商与容量信息。软件片选模式下整个命令加读回期间 CS 必须持续拉低Flash 才认为这是一个完整事务。/* 依赖 2.3 的 FLASH_CS_L/H 宏SPI1 工作在全双工主机模式 */ uint32_t spi_flash_read_jedec_id(void) { uint8_t cmd 0x9F; uint8_t rx[3] {0}; uint32_t id 0; FLASH_CS_L(); HAL_SPI_Transmit(hspi1, cmd, 1, 100); /* 发命令期间 MISO 无有效数据 */ HAL_SPI_Receive(hspi1, rx, 3, 100); /* 每收一字节硬件补 0xFF 产生时钟 */ FLASH_CS_H(); id ((uint32_t)rx[0] 16) | ((uint32_t)rx[1] 8) | rx[2]; return id; }返回值第一个字节能区分厂商0xEF 开头是华邦0xC8 开头是兆易创新。三个注意点一是三个字节的排列顺序要与 datasheet 的 ID 字段定义一致不同厂家有差异二是 CS 必须在三次 Receive 完成后才拉高中间任何一次拉高都会中断事务三是 HAL_SPI_Receive 的第三个参数是字节数不是位数。这段跑通之后补上写使能 0x06、页编程 0x02、扇区擦除 0x20就是一个完整的 Flash 驱动雏形。4. SPI 的 DMA 到底要不要两个映射、配置与三个必踩的坑4.1 先回答SPI 需要两个 DMA 吗答案是看事务方向。SPI 是全双工外设如果收发同时进行而且要持续很久比如连续读 Flash、从 AD7124 这类 ADC 流式取数就需要两个 DMA 流一条把 TX 数据搬给外设一条把外设 RX 数据搬回内存两条流分别由 SPI_TX 和 SPI_RX 请求触发。如果只有写没有读比如刷 OLED、驱动 LED 矩阵或 DAC一个 TX DMA 流就够。还有一类读多写少场景TX DMA 循环发 0xFFRX DMA 同步收数据本质上还是在用两条流。低速、短帧场景直接轮询更省资源把 DMA 让给 UART 和定时器。4.2 F4 的 SPI DMA 映射流、通道和 CubeMX 勾选以 F407 为例三个 SPI 外设的 DMA 映射如下。CubeMX 的 DMA Settings 里勾选 SPI1_TX 和 SPI1_RX 两个请求后会自动分配流和通道不要手工改 Stream除非你清楚中断优先级与总线带宽的影响。SPITX DMARX DMASPI1DMA2_Stream3_Channel3DMA2_Stream0_Channel3SPI2DMA1_Stream4_Channel0DMA1_Stream3_Channel0SPI3DMA1_Stream7_Channel0DMA1_Stream0_Channel0不同型号 F4 的 DMA 映射可能存在差异以你手里芯片 datasheet 的 DMA 请求表和参考手册 RM0090 为准引脚复用表也随型号不同接错线时先查表再查代码。DMA 模式下 SPI 自身中断基本不用开完成事件由 DMA 传输完成中断触发 HAL 回调前提是 NVIC 里 DMA 中断优先级已配好否则数据照样传但应用层永远等不到回调。4.3 双 DMA 的 SPI 全双工读写最小代码CubeMX 里把 SPI1 的 DMA 请求加上Mode 选 Normal。下面的代码演示一次 128 字节的连续读命令阶段用 3.3 的轮询方式完成数据阶段切 DMA。#define DMA_BUF_LEN 128 uint8_t dma_tx[DMA_BUF_LEN] {0x00}; /* 读操作的占位字节 */ uint8_t dma_rx[DMA_BUF_LEN] {0}; volatile uint8_t g_spi_done 0; /* 启动 DMA 全双工传输函数立即返回 */ HAL_StatusTypeDef st HAL_SPI_TransmitReceive_DMA(hspi1, dma_tx, dma_rx, DMA_BUF_LEN); if (st ! HAL_OK) { /* 启动失败检查 DMA 请求是否配置、缓冲区长度是否为 0 */ } /* 传输完成回调DMA 中断里执行回调后 rx 数据才有效 */ void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi-Instance SPI1) { g_spi_done 1; /* 置标志主循环消费 dma_rx */ } }这里 TX DMA 和 RX DMA 同时启动SPI 硬件自动用 dma_tx 的数据产生时钟并把 MISO 采样结果由 RX DMA 写进 dma_rx函数不等传输结束就返回。128 是字节数必须与缓冲区大小一致缓冲区要用全局或静态变量DMA 未完成前不要修改或释放。软件片选场景下拉高 CS 的时机要在 g_spi_done 置位之后完整顺序是拉低 CS、启动 DMA、等回调标志、等 BSY 清零、拉高 CS。4.4 SPI DMA 模式下三个必踩的坑第一个是 OVR 溢出。F4 的 SPI 在 RXNE 到来后不及时读 DR 就会置 OVRDMA 正常跑时几乎不会触发但混合使用轮询和 DMA或中途调用 HAL_SPI_DMAStop 时残留的 RX 数据会引发溢出。发现 ErrorCode 出现 HAL_SPI_ERROR_OVR先 __HAL_SPI_CLEAR_OVRFLAG(hspi1) 清掉再重发一帧。第二个是 BSY 卡死。DMA 结束那一刻SPI 硬件可能还差最后一拍才移完高位所以回调里要检查 __HAL_SPI_GET_FLAG(hspi, SPI_FLAG_BSY)确认清零后再拉高 CS 或切换模式。常见误区是读 hspi-State 判断忙闲HAL 的 State 是软件状态BSY 是硬件状态两者不同步。第三个是缓冲区生命周期。局部变量做 DMA 缓冲区是最典型的事故函数返回后栈空间被复用DMA 还在往里写轻则数据错乱重则直接 HardFault。规范做法是全局数组或状态机管理的静态缓冲完成回调置位后再交出缓冲。另外两个 DMA 流的优先级建议 RX 高于 TX 或同级避免 RX 请求被 TX 抢占导致溢出。5. 最后可抄的一个技巧用逻辑分析仪把 SPI 时序量出来5.1 逻辑分析仪的接线与抓取参数调试时 SPI 是隐形信号配置对没对、片选有没有漏拉看波形比看代码快得多。没有示波器的话几十块钱的 8 通道逻辑分析仪够用CH0 接 SCKCH1 接 CSCH2 接 MOSICH3 接 MISO所有通道与目标板共地。采样率设为 SPI 时钟的 5 到 10 倍50 MHz 的机器抓 10 MHz 以下信号余量充足超过 15 MHz 建议用更高采样率或先把分频降下来验证逻辑。触发方式选 CH1CS下降沿能稳定抓到完整一帧。5.2 SPI 异常波形对照与验证顺序抓到波形后按顺序核对CS 低电平期间 SCK 是否完整产生了 N×8 个脉冲缺拍说明 BSY 处理或 DMA 停止有问题SCK 空闲电平是低还是高与 2.1 表格的 CPOL 对应MOSI 数据跳变是否在采样沿前已经稳定MISO 逐位与 datasheet 时序图比对。MOSI 有数据而 MISO 恒高时先别怀疑芯片确认地线接好、从机已上电、MISO 上拉是否存在。波形现象常见原因SCK 有脉冲但 CS 没拉低软件片选没在事务前执行MOSI 正常MISO 恒高未共地、MISO 悬空、从机未上电前几字节正常后面错位分频太快或走线过长降速验证RXNE 等不到或单字节卡死8 位帧长下 FRXTH 未置 15.3 治本的一招软件 SPI 单拍钳位法逻辑分析仪只能看不能帮你确认某一拍从机到底回了什么。这时可以暂时绕开 SPI 外设用 GPIO 软件模拟 SPI 打一拍手动把 MOSI 置成要发的位拉高 SCK 产生采样沿再读 MISO。下面是模式 0 单字节软件 SPI 的写法逐位移 8 次就能观察每一位输入。/* 模式 0SCK 空闲低上升沿采样。MOSI/SCK/MISO 已配置为普通 GPIO */ uint8_t spi0_soft_byte(uint8_t out) { uint8_t in 0; for (int i 7; i 0; i--) { HAL_GPIO_WritePin(MOSI_PORT, MOSI_PIN, (out i) 1 ? GPIO_PIN_SET : GPIO_PIN_RESET); HAL_GPIO_WritePin(SCK_PORT, SCK_PIN, GPIO_PIN_RESET); delay_us(1); /* 数据建立时间 */ HAL_GPIO_WritePin(SCK_PORT, SCK_PIN, GPIO_PIN_SET); /* 采样沿 */ delay_us(1); /* 数据保持时间 */ if (HAL_GPIO_ReadPin(MISO_PORT, MISO_PIN)) in | (1u i); } return in; }用这把软尺量过的每一位如果与 datasheet 完全一致问题就锁定在外设配置而非从机这时再切回硬件 SPI若波形差异变大基本就是 CPOL/CPHA 或分频的问题。单拍调试还能避开 BSY 和 OVR 的干扰把通信问题拆成外设与从机两半逐半排查比一味降低时钟或换线更接近根因。本文还有配套的精品资源点击获取