STM32F429 SPI例程精讲:从配置到DMA收发与排错
简介STM32F429 SPI外设例程是基于STM32F429单片机的外设开发资料面向嵌入式初学者与项目开发者重点讲解SPI同步串行通信协议在STM32F429上的配置与应用。资源共1445个文件压缩包约84.34MB以C源码、H头文件、预编译库和工程配置文件为主并附带PDF、CHM、MD等说明文档覆盖从芯片外设库到Keil工程构建的完整开发链路。已有237人学习下载。例程详细演示GPIO复用配置、SPI时钟极性/相位选择、主从模式设定、NSS引脚管理以及全双工收发流程并对发送完成与接收非空标志位进行轮询处理工程内还包含可用于二次开发的库文件与器件描述文件能帮助读者快速迁移至传感器、存储芯片、显示模块等常见SPI设备通信场景减少底层驱动调试时间。1. 把 STM32F429 的 SPI 从“能跑”调到“好用”卡点不在代码量SPI 大概是外设例程里最“两头堵”的通信接口接一个 OLED 屏按照数据手册把 CPOL 和 CPHA 都配成 0几乎第一次就能点亮可一旦从机换成 W25Q128 这类 Flash或者要去读一颗带状态寄存器的传感器问题就来了——波形看着对读回的数据却全是 0xFF或者从机根本不拉低 MISO。STM32F429 的 SPI 外设在 F4 系列里属于完整度高的一档支持 8/16 位帧、单线双向、硬件 NSS 和 DMA 触发但正因为选项多例程里最容易出错的反而不是“怎么发一个字节”而是“主从双方对时序的解释不一致”。这篇顺着 STM32F429 的 SPI 例程把协议、初始化、收发和排错串起来讲适合刚把单片机跑起来、准备接 SPI 从机的开发者也适合回去补时序细节的熟手。2. STM32F429 SPI 硬件特征与协议对应关系2.1 SPI 四线信号与 F429 引脚复用映射SPI 通信一共四根线STM32F429 的 SPI 外设把它们分别映射到一组 GPIO 的复用功能上SCK主机产生时钟决定数据移位的节奏MOSI主机输出、从机输入主机在时钟边沿把数据推到这根线上MISO主机输入、从机输出从机在这根线上回数据NSS片选信号低电平有效主机用它告诉从机“我要跟你说话”F429 有 SPI1 到 SPI6 共六个 SPI 外设其中 SPI1、SPI4、SPI5、SPI6 挂在 APB2 总线上最高时钟可达 90MHzSPI2 和 SPI3 挂在 APB1 上上限 45MHz。选哪个外设不只看引脚够不够还要算一下你能接受的最高 SCK。比如要把 W25Q128 跑到 40MHz挂在 APB1 的 SPI2 就撑不住得用 APB2 上的 SPI1 或 SPI5。引脚映射是新手第一个坑。F429 的 SPI 引脚不是随便拿一个 GPIO 就能用的必须选中该引脚的 AF 复用功能。以 SPI1 为例PA5 对应 SPI1_SCKPA6 对应 SPI1_MISOPA7 对应 SPI1_MOSIPA4 可以映射为 SPI1_NSS同样的 SPI1 信号也出现在 PB3、PB4、PB5 上。用 CubeMX 配置时软件会自动把 GPIO 设为 Alternate Function 模式但如果是手写寄存器或抄别人的初始化代码务必查数据手册的 AF 复用表否则 SCK 上不会有时钟。GPIO 速度对 SPI 影响容易被忽略。SCK 和 MOSI 是输出引脚GPIO 速度应配置为 High 或 Very High否则在低速从机上不明显一旦把分频系数调小波形上升沿变缓从机采样点就会漂移表现为偶发错位。2.2 四种模式由 CPOL 和 CPHA 决定从时序图看 F429 寄存器位SPI 协议里最容易出问题的是“什么时候采样、什么时候切换数据”。这两个时间点由 CPOL 和 CPHA 两个位决定二者组合成四种模式很多芯片手册直接写“Mode 0”“Mode 3”不会告诉你它们对应寄存器的哪一位。模式CPOLCPHASCK 空闲电平数据采样沿典型设备Mode 000低第一个边沿上升沿W25Q 系列 Flash、OLED、NRF24L01Mode 101低第二个边沿下降沿部分 ADCMode 210高第一个边沿下降沿少数传感器Mode 311高第二个边沿上升沿SD 卡、部分音频芯片对应到 STM32F429 的寄存器 SPIx_CR1CPOL 写在 bit1CPHA 写在 bit0。逻辑上记住一句话CPOL 决定空闲电平CPHA 决定在第几个边沿采样。两个位不匹配最典型的症状是能读到数据但全部错位比如本该 0x9F 读到 0x3E这是“差半个位”的典型表现。调试手法上用手头的逻辑分析仪抓 SCK 和 MOSI对照从机数据手册里的时序图看空闲电平。如果 SCK 空闲时是低电平从机却要求高电平有效那就是 CPOL 配反了如果空闲电平对了但数据全错优先怀疑 CPHA。先把这两项固定再谈分频和帧格式。2.3 帧格式的柔性8 位、16 位与 LSB 先出STM32F429 的 SPI 除了常见的 8 位帧还能配 16 位帧。DFF 位在 SPIx_CR1 的 bit11置 1 表示 16 位数据帧置 0 表示 8 位。例程里默认 8 位但有两个派生问题值得注意。第一个是 16 位帧下的 FIFO 阈值。F429 的 SPI 自带收发 FIFORXFIFO 的阈值由 SPIx_CR2 的 FRXTH 位控制8 位帧时阈值应为 1 字节16 位帧时阈值应为 2 字节。如果 16 位帧下 FRXTH 还停留在 1 字节读回来的数据会错位。第二个是 MSB 还是 LSB 先出。SPIx_CR1 的 LSBFIRST 位控制置 1 时最低位先发置 0 时最高位先发。绝大多数 SPI 从机默认 MSB first但个别芯片比如某些 Nordic 无线芯片支持 LSB first。这个位配错的后果是数据完全反序比如 0xC4 发成 0x23。处理这两类问题时我的做法是先把帧格式和位序写死在初始化里然后从从机手册里确认。凡是手册没明确写 LSB first 的一律用 MSB first这能省掉大量排查时间。3. 用 HAL 库初始化 STM32F429 SPI 的最小可用代码3.1 用 STM32CubeMX 分配引脚和配置时钟现在做 STM32F429 的开发初始化 SPI 最稳的路子是先用 CubeMX 生成工程框架再在生成的代码上做修改。这不丢人反而能避免引脚配置遗漏。CubeMX 里的操作顺序是选好单片机型号后在 Pinout 视图里点击要用的 SPI 外设引脚或者直接在左侧列表勾选 SPI1软件会自动把默认引脚分配好。接着在 Configuration 里打开 SPI1 的参数页需要确认四件事时钟源保持 Internal Clock 即可分频系数先选一个保守值比如 32 分频帧格式8 Bit极性/相位看从机手册多数选 Low/1Edge也就是 Mode 0时钟树要单独看。默认情况下 SPI1 挂 APB2如果 APB2 时钟是 90MHz32 分频后 SCK 约 2.8MHz这个速度对大多数从机都安全。想提速就降分频但要把 GPIO 速度同步调上去。CubeMX 生成的HAL_SPI_MspInit里会配置 GPIO 复用注意检查 SCK 和 MOSI 的速度等级是否是GPIO_SPEED_FREQ_VERY_HIGH如果不是在 HAL 库里手动改掉。3.2 手写 SPI_HandleTypeDef 并逐项解释五个参数CubeMX 生成的是 HAL 库风格的初始化结构体。如果不用 CubeMX手写初始化也不复杂核心就是填一个SPI_HandleTypeDefSPI_HandleTypeDef hspi1; void MX_SPI1_Init(void) { hspi1.Instance SPI1; hspi1.Init.Mode SPI_MODE_MASTER; /* 主机模式 */ hspi1.Init.Direction SPI_DIRECTION_2LINES; /* 全双工两根线 */ hspi1.Init.DataSize SPI_DATASIZE_8BIT; /* 8 位数据帧 */ hspi1.Init.CLKPolarity SPI_CLOCK_POLARITY_LOW; /* CPOL0SCK 空闲低 */ hspi1.Init.CLKPhase SPI_CLOCK_PHASE_1EDGE; /* CPHA0第一个沿采样 */ hspi1.Init.NSS SPI_NSS_SOFT; /* 软件片选 */ hspi1.Init.BaudRatePrescaler SPI_BAUDRATEPRESCALER_16; /* 16 分频 */ hspi1.Init.FirstBit SPI_FIRSTBIT_MSB; /* MSB 先出 */ hspi1.Init.TIMode SPI_TIMODE_DISABLE; /* 关闭 TI 模式 */ hspi1.Init.CRCCalculation SPI_CRCCALCULATION_DISABLE; /* 关闭 CRC */ hspi1.Init.CRCPolynomial 10; if (HAL_SPI_Init(hspi1) ! HAL_OK) { Error_Handler(); } }逐个说参数Mode决定主从Direction选2LINES是全双工读 Flash 这类设备必须要全双工如果只是写 OLED 屏可以选1LINE_TX单线发送。CLKPolarity和CLKPhase前面已讲过它们直接决定从机能不能正确采样。NSS设置为SPI_NSS_SOFT是这里最值得强调的一步。F429 的硬件 NSS 在主机模式下会由外设自动控制片选电平但很多从机要求片选在整条命令期间保持低电平硬件自动控制反而会在字节间产生毛刺。用软件控制后NSS 引脚退化成一个普通 GPIO你可以在命令开始前拉低、结束后拉高控制粒度更细。BaudRatePrescaler的值要结合从机最高频率算。16分频在 APB2 90MHz 下约 5.6MHz适合 W25Q 系列如果换成老式 SPI 传感器上限可能只有 1MHz分频就要加大到64或128。FirstBit固定MSB除非从机手册特别注明。HAL_SPI_Init内部会调用HAL_SPI_MspInitGPIO 时钟和复用配置需要在这个回调函数里完成void HAL_SPI_MspInit(SPI_HandleTypeDef *hspi) { GPIO_InitTypeDef GPIO_InitStruct {0}; __HAL_RCC_SPI1_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitStruct.Pin GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7; GPIO_InitStruct.Mode GPIO_MODE_AF_PP; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_VERY_HIGH; GPIO_InitStruct.Alternate GPIO_AF5_SPI1; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); }这段代码的要点GPIO_AF5_SPI1是 F429 数据手册里规定的替代功能编号PA5、PA6、PA7 的 SPI1 复用都是 AF5。Pull选NOPULLSPI 是推挽输出内部上下拉反而可能改变空闲电平。3.3 寄存器直写方案了解 HAL 背后在做什么如果嫌 HAL 封装太厚或者想在中断服务函数里做快速切换可以直接操作寄存器。SPI1 的寄存器基地址是0x40013000STM32F429 参考手册里 SPI 外设的寄存器不多主初始化只需要操作 CR1 和 CR2/* SPI1 主机模式、CPOL0、CPHA0、MSB first、8 位帧 */ SPI1-CR1 SPI_CR1_MSTR | SPI_CR1_SSI | SPI_CR1_SSM | SPI_CR1_BR_2; /* 分频系数 BR[2:0] 100即 16 分频 */SPI_CR1_MSTR置主机模式SPI_CR1_SSM配合SPI_CR1_SSI启用软件从机管理。这里有个寄存器直写常见的坑启用SSM后SSI位必须同时置 1否则 SPI 会认为自己被拉低了片选直接进入从机模式SCK 不输出MOSI 也没反应。这是 STM32 系列从 F1 到 F4 都会出现的通用问题。发送一个字节的寄存器写法如下while (!(SPI1-SR SPI_SR_TXE)); /* 等待 TXE 置位 */ *(volatile uint8_t *)SPI1-DR tx_data; while (!(SPI1-SR SPI_SR_RXNE)); /* 等待接收完成 */ volatile uint8_t rx_data *(volatile uint8_t *)SPI1-DR;注意DR寄存器 8 位和 16 位帧共用写 8 位数据要用uint8_t指针转换直接写SPI1-DR data会按 32 位写入虽然实际只取低 8 位但代码意图不明确容易和 16 位帧混用。4. 数据收发实战轮询、中断与 DMA 三条路怎么选4.1 阻塞式收发与超时参数的意义最直接的收发方式是阻塞式调用 HAL 函数。对需连续交互的从机比如读 Flash ID代码最简单uint8_t cmd[4] {0x9F, 0x00, 0x00, 0x00}; /* W25Q 读 JEDEC ID 命令 */ uint8_t resp[4] {0}; uint8_t rx_buf[4]; HAL_GPIO_WritePin(SPI1_CS_GPIO_Port, SPI1_CS_Pin, GPIO_PIN_RESET); HAL_SPI_TransmitReceive(hspi1, cmd, rx_buf, 4, 100); HAL_GPIO_WritePin(SPI1_CS_GPIO_Port, SPI1_CS_Pin, GPIO_PIN_SET);HAL_SPI_TransmitReceive的最后一个参数是超时时间单位毫秒。全双工模式下这个函数同时完成发送和接收发送缓冲区的数据从 MOSI 移出同时 MISO 上的数据被移入接收缓冲区。对 W25Q 这类从机主机必须在发送完命令字节后继续发时钟从机才会把数据推出来所以cmd和rx_buf长度设为一致。用阻塞方式时有个隐性问题如果从机没接好或者 MISO 悬空SPI 外设仍会照常产生时钟并收进数据只不过收到的全是 0xFF。代码不会报错程序会一直等到 timeout 才结束。所以阻塞收发适合低速、低延迟、调试阶段的场景比如确认从机 ID、读写寄存器不适合大量数据的持续搬运。4.2 中断收发用回调处理不定长应答数据量稍大、但又不想让 CPU 在TransmitReceive里空转时改用中断方式HAL_StatusTypeDef status; status HAL_SPI_Transmit_IT(hspi1, tx_buf, len);调用后函数立即返回CPU 可以干别的事。传输完成时HAL 会调用弱定义的回调函数void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi-Instance SPI1) { /* 发送完成可以在这里拉高片选 */ HAL_GPIO_WritePin(SPI1_CS_GPIO_Port, SPI1_CS_Pin, GPIO_PIN_SET); } }中断方式的收要单独看清楚如果只调用HAL_SPI_Receive_ITF429 的 SPI 外设并不会自动在接收间隙发送时钟——SCK 是由发送行为驱动的。在主机模式下没有数据要发出去就没有时钟MISO 上的数据也不会被采样。所以读从机数据时正确做法是调用HAL_SPI_TransmitReceive_IT让发送缓冲区和接收缓冲区长度一致通过发送空数据来产生时钟。中断方式的另外一个注意点是使能 NVIC。CubeMX 里点开 SPI1 的 NVIC Settings勾选 SPI1 global interrupt。如果这个没开状态标志置位后不会触发HAL_SPI_IRQHandler回调永远不执行代码看起来就是卡死了。4.3 DMA 收发全双工为什么需要两个 DMA 通道数据量到了 KB 级别比如从 W25Q128 读一页 256 字节或者驱动 SD 卡CPU 中断每字节进一次会占用大量主频这时候要用 DMA。F429 的 SPI1 可以触发 DMA2 的流 3 和流 2分别对应 RX 和 TX 方向。“SPI 需要两个 DMA 吗”是例程讨论里常被问到的点。答案是用全双工就需要两个通道一个往 SPI1_TX 写发送数据一个从 SPI1_RX 读接收数据只用发送方向比如单向写 OLED则只需 TX 通道。反过来只做接收的主机也必须配 TX 通道因为没有发送就没有时钟。最小 DMA 发送代码如下HAL_SPI_Transmit_DMA(hspi1, tx_buf, len);配置 DMA 时要注意SPI_RX_DMA和SPI_TX_DMA的中断优先要高于普通外设否则总线繁忙时 DMA 传输可能被其他中断打断。DMA 模式下 SPI 的回调会变成void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) { /* DMA 传输完成数据已全部移出 */ }DMA 方式有个值得写进例程的细节发送缓冲区必须是const修饰或全局变量不能是栈上的局部数组。DMA 是“异步搬运”函数返回时栈变量可能已被释放DMA 还在读那块内存表现就是数据随机变化。4.4 硬件片选与软件片选的取舍片选可以说是 SPI 例程里最容易被当成“能用就行”的部分。F429 的硬件片选有两种模式NSS_HARD下主机模式会自动把 NSS 引脚拉低来选中从机从机模式下则通过 NSS 引脚的电平变化来判断总线占用。硬件片选看似省事但在多从机或复合命令场景下有明显局限对比项硬件片选软件片选字节间片选行为可能在每个字节间产生短暂释放完全由 GPIO 控制可保持全程低电平多从机扩展每个从机需独立硬件 NSS外设资源有限任意 GPIO 都可做片选可扩展更多从机命令时序控制受外设状态机约束灵活可精确控制拉低和拉高的时间点例程调试难度时序不直观出错难追溯示波器直接看 GPIO 电平即可我的做法是默认全部用软件片选。即便只有一个从机软件片选也能避免硬件 NSS 在字节间隙自动释放导致的命令中断。一个常见误用是硬件 NSS 模式下忘了配置 NSS 引脚结果外设的片选状态不确定从机时通时不通。5. 用回环测试和波形核对 SPI 时序与排错5.1 Loopback 回环测试验证收发链路写再多例程不如先做一个最小验证把 MOSI 和 MISO 直接用杜邦线短接然后发送一串已知数据并接收回来如果收到的与原数据完全一致说明 SPI 外设本身没有配置问题。uint8_t tx_data[] {0xA5, 0x5A, 0x0F, 0xF0}; uint8_t rx_data[4] {0}; HAL_GPIO_WritePin(SPI1_CS_GPIO_Port, SPI1_CS_Pin, GPIO_PIN_RESET); HAL_SPI_TransmitReceive(hspi1, tx_data, rx_data, 4, 100); HAL_GPIO_WritePin(SPI1_CS_GPIO_Port, SPI1_CS_Pin, GPIO_PIN_SET); /* 比较 rx_data 与 tx_data 是否一致 */回环通过后还不能直接说时序是对的。MOSI 和 MISO 短接只能验证数据路径验证不了 CPOL 和 CPHA 是否与从机匹配因为回环模式下发送和采样用的是同一个外设的同一套参数。它真正的价值是排除引脚配置错误、时钟未使能、DMA 通道错乱这一类底层问题。5.2 判断从机应答的排查顺序挂上真实从机后读不到预期数据时按下面的顺序排查每一步都有明确的观察对象排查步骤观察对象判定标准1. 检查 SCK 是否有时钟示波器或逻辑分析仪发送期间应有连续脉冲2. 检查片选电平示波器命令期间应为低电平且无毛刺3. 检查模式匹配抓 MOSI 数据和 SCK 边沿从机采样点与 MOSI 数据稳定4. 检查从机应答读 MISO 电平应有非 0xFF 数据返回5. 检查分频逐步降低 SPI 时钟再试高速出错、低速正常说明时序余量不足W25Q128 这类 Flash 如果读回全 0xFF最先怀疑的不是 SPI 配置而是 W25Q 的“写使能”状态。SPI 命令本身正常但 Flash 未进入可读状态时也会返回 0xFF。把排查顺序反过来先确认从机侧的状态寄存器再回头调时序能省一半时间。5.3 两个实务技巧片选提前量与 DMA 启动窗口片选提前量是指“先拉低片选再启动 SPI 主机发送”两者之间要留一个 GPIO 写操作的延时。F429 的 GPIO 写操作是快速指令但如果从机要求 CS 拉低后有至少几百纳秒的稳定时间直接先写 GPIO 再填 DR 寄存器通常没问题不过为了稳妥在片选拉低后加一个__NOP()或轻量延时更保险。相反如果发送结束后立即拉高片选而最后一字节还在移位寄存器里没完全移出最后一个字节会被截断。正确做法是用发送完成标志或 DMA 回调来确认传输结束再释放片选。DMA 启动窗口是指HAL_SPI_Transmit_DMA调用后SPI 外设从“空闲”到“开始产生时钟”之间存在一个启动窗口如果此时立刻拉高片选同样会截断首字节。例程里常见做法是在 DMA 传输完成的回调里拉高片选而不是在调用后紧接着拉高。这两处时序是软件片选例程里最后会踩到的坑也都是示波器上能直接看到的现象。把以上要点落到自己的例程里就能把 F429 的 SPI 从“能用”推到“稳用”对后续接 Flash、SD 卡这类高速从机会少走很多弯路。本文还有配套的精品资源点击获取