STM32硬件SPI半双工三线模式配置与驱动开发详解

发布时间:2026/7/30 5:47:36
STM32硬件SPI半双工三线模式配置与驱动开发详解 1. 从四线到三线为什么我们需要半双工SPI最近在做一个需要连接多个传感器的项目主控用的是STM32F4系列。其中一个传感器比较特殊它只支持三线SPI接口。刚开始我有点不以为然心想SPI不就是四根线吗MOSI、MISO、SCLK、CS标准配置库函数一调数据不就来了结果一上手就发现不对劲常规的HAL库HAL_SPI_TransmitReceive根本没法用因为硬件上只有一根数据线在来回切换方向。这才让我重新审视这个看似“简化”的三线半双工SPI。所谓三线SPI就是省掉了一根独立的数据线。全双工的四线SPI主机发数据用MOSI线收数据用MISO线可以同时进行效率高。而三线模式通常只保留一根数据线我们叫它SIMO/MISO或SDIO这根线在主机发送时作为输出在主机接收时作为输入通信方向需要动态切换这就是“半双工”。你可能会问这不多此一举吗直接用软件模拟SPISoftware SPI不就行了想怎么控制就怎么控制。确实软件模拟最灵活但代价是CPU占用率高时序精度受中断和任务调度影响在高波特率或需要同时处理其他任务时就很吃力。硬件SPI的优势在于它的时钟和数据由专用外设生成时序精准不占用CPU核心去翻转GPIO解放出来的算力可以干别的。所以研究STM32硬件SPI的半双工三线模式核心目标就一个在享受硬件SPI高效、精准的时序优势的同时去适配那些为了节省引脚或遵循特定协议而设计的三线从设备。常见的应用场景包括一些温湿度传感器、压力传感器、Flash存储器某些型号的QSPI也可配置为单线模式、以及一些老式的或引脚极简的通信模块。如果你也遇到了类似的需求或者单纯想深入了解一下STM32 SPI外设的另一种工作模式那么我折腾的这几天的经验或许能帮你少走点弯路。2. 硬件配置的“陷阱”CubeMX设置与底层原理要使用半双工模式第一步就是在STM32CubeMX里正确配置。这里有几个关键点如果配错了后面代码怎么写都可能白搭。2.1 SPI模式与数据方向的选择在CubeMX的SPI配置页你会看到“Mode”选项。对于半双工通常选择“Transmit only master”或“Receive only master”作为起点但这只是初始方向。更关键的是下面这个“Data Size”和“Frame Format”。Data Size数据大小这个要和你的从设备协议严格对应通常是8位或16位。Frame Format帧格式这里有个大坑标准SPI通常是“Motorola”帧格式也就是最常见的CPOL和CPHA相位控制。但有些三线设备可能使用其他格式比如TI的SSP模式不过绝大多数情况下我们还是选“Motorola”。重点在于“NSS Pulse Mode”和“NSS Signal Type”。NSS Signal Type片选信号类型对于三线SPI我强烈建议选择“Software”。这意味着片选引脚CS你将用一个普通的GPIO来控制而不是让SPI硬件自动管理。原因很简单在半双工收发切换的间隙硬件NSS可能会产生我们不希望的脉冲导致从设备状态混乱。手动控制GPIO拉高拉低时机更可控。NSS Pulse Mode如果你坚持用硬件NSS这个模式会产生一个脉冲但在半双工复杂时序下容易出问题新手建议先关掉。配置完成后生成代码。你会发现HAL库生成的初始化代码里会调用HAL_SPI_Init并且根据你的配置SPI外设的CR1、CR2寄存器已经被设置好了。但这仅仅是个开始半双工模式的核心开关并不在CubeMX的图形化配置里。2.2 关键寄存器SPI_CR1的BIDIMODE和BIDIOESTM32的SPI外设支持半双工的秘密藏在控制寄存器1SPI_CR1的两个位里BIDIMODEBidirectional data mode enable这个位必须设置为1。它告诉SPI“嘿咱们现在只用一根数据线了别惦记着那两根独立的了。”BIDIOEBidirectional data mode output enable这个位控制着那根唯一数据线的方向。当BIDIOE 1时数据线处于输出模式SPI作为发送器。当BIDIOE 0时数据线处于输入模式高阻态SPI作为接收器。CubeMX默认生成的代码不会帮你设置这两个位。你需要手动开启半双工模式。通常的做法是在MX_SPIx_Init()函数之后或者在你自己的SPI初始化函数里加上这么几行// 假设 hspi1 是你的SPI句柄 __HAL_SPI_ENABLE(hspi1); // 先使能SPI SET_BIT(hspi1.Instance-CR1, SPI_CR1_BIDIMODE); // 开启双向数据模式半双工 // 初始方向设为输出准备发送 SET_BIT(hspi1.Instance-CR1, SPI_CR1_BIDIOE);注意BIDIOE位是可以在通信过程中动态修改的这是我们实现先发后收的关键。但修改它需要一点技巧最好在SPI不忙SPI_SR寄存器的BSY位为0的时候进行并且要注意操作时序。2.3 引脚复用与硬件连接硬件连接上三线SPI通常连接这三根线SCLK时钟主机输出从机输入。SDIO数据输入输出这就是那根复用线。在STM32端你需要将这个引脚配置为复用推挽输出Alternate Function Push-Pull。是的即使它也要做输入但在半双工模式下硬件会根据BIDIOE位自动切换方向我们软件上只需配置为复用输出即可。千万不要配置成开漏输出开漏模式在作为输入时无法被外部正确驱动至高电平。CS片选如前所述建议用一个普通GPIO控制配置为推挽输出初始状态为高不选中。接线图很简单但务必确认从设备的数据线也是双向IO口。有些设备的数据线是单向的只能输入或只能输出那它就不是真正的三线半双工SPI可能是其他协议。3. 软件驱动设计如何优雅地切换收发方向配置好硬件接下来就是软件逻辑的重头戏。我们最终的目标是封装出两个函数SPI_Write()和SPI_Read()。但实现它们不能简单调用HAL库的发送和接收函数。3.1 发送数据流程发送相对简单因为此时数据线方向是输出BIDIOE1。流程如下拉低片选GPIOCS选中从设备。可选发送命令字节或寄存器地址。很多传感器需要先发一个读/写命令。调用HAL_SPI_Transmit()发送数据。注意此时函数内部操作的是MOSI相关的数据寄存器但由于我们处于半双工模式数据会从SDIO引脚发出。发送完成后拉高片选GPIO。代码示例void SPI_Write(uint8_t *pData, uint16_t Size) { CS_LOW(); // 自定义宏拉低片选引脚 // 确保方向为输出 SET_BIT(hspi1.Instance-CR1, SPI_CR1_BIDIOE); HAL_SPI_Transmit(hspi1, pData, Size, HAL_MAX_DELAY); CS_HIGH(); }3.2 接收数据流程——核心难点接收数据是半双工模式最需要小心的地方。你不能直接调用HAL_SPI_Receive因为此时数据线方向还是输出从设备的数据发不过来。必须先将方向切换为输入。一个典型的“先发后收”的读取传感器数据的流程如下拉低片选选中设备。设置方向为输出BIDIOE1发送读取命令。例如发送一个字节0xAA告诉从设备“我要读数据了”。等待发送完成并切换方向为输入。这是最关键的步骤。你不能发送完命令后立刻切换方向因为SPI的移位寄存器可能还没空最后一个bit可能还在线上。必须等待SPI_SR寄存器的TXE发送缓冲区空和BSYSPI忙标志位都变为0。// 发送命令后等待发送完成 while((__HAL_SPI_GET_FLAG(hspi1, SPI_FLAG_BSY) ! RESET)); // 切换方向为输入 CLEAR_BIT(hspi1.Instance-CR1, SPI_CR1_BIDIOE);重要提示在切换BIDIOE后需要插入一个小的延时几个NOP指令或__DSB()内存屏障让硬件状态稳定。有些应用笔记建议在切换方向后先进行一次“哑元Dummy”接收来启动时钟对于STM32切换方向后直接开始接收即可。开始接收数据。此时由于方向是输入主机需要产生时钟来读取数据。我们可以调用HAL_SPI_Receive。这个函数内部会向数据寄存器写入数据为了产生时钟同时读取接收到的数据。我们传入的缓冲区在接收完成后里面存放的就是从设备发来的数据。uint8_t rx_buffer[3]; HAL_SPI_Receive(hspi1, rx_buffer, 3, HAL_MAX_DELAY);接收完成拉高片选。在拉高片选前可以先将方向切回输出为下一次操作做准备。3.3 封装与超时处理把上述步骤封装起来一个健壮的SPI_Read函数需要考虑超时。HAL库的收发函数本身有超时参数但方向切换的等待while循环最好也加上超时判断防止程序卡死。HAL_StatusTypeDef SPI_Read(uint8_t cmd, uint8_t *pRxData, uint16_t Size) { HAL_StatusTypeDef status; uint32_t tickstart HAL_GetTick(); CS_LOW(); // 1. 确保输出方向发送命令 SET_BIT(hspi1.Instance-CR1, SPI_CR1_BIDIOE); status HAL_SPI_Transmit(hspi1, cmd, 1, 100); if (status ! HAL_OK) { CS_HIGH(); return status; } // 2. 等待发送彻底完成 while((__HAL_SPI_GET_FLAG(hspi1, SPI_FLAG_BSY) ! RESET)) { if((HAL_GetTick() - tickstart) 100) { CS_HIGH(); return HAL_TIMEOUT; } } // 3. 切换为输入方向 CLEAR_BIT(hspi1.Instance-CR1, SPI_CR1_BIDIOE); __DSB(); // 数据同步屏障确保指令执行顺序也可用__NOP()延时几个周期 // 4. 接收数据 status HAL_SPI_Receive(hspi1, pRxData, Size, 100); // 5. 恢复为输出方向可选为下次发送准备 SET_BIT(hspi1.Instance-CR1, SPI_CR1_BIDIOE); CS_HIGH(); return status; }4. 实战踩坑与排错指南理论很美好调试很残酷。下面是我在调试过程中遇到的几个典型问题及解决办法。4.1 问题一数据全为0xFF或0x00现象无论发送什么接收到的数据总是0xFF或0x00。排查思路检查硬件连接用示波器或逻辑分析仪看SCLK、CS、SDIO三根线。首先确认CS有拉低SCLK有时钟输出。这是最基本的。检查方向切换时机这是最可能的原因。用逻辑分析仪抓取SDIO线上的波形。如果你在发送命令后SDIO线立刻变成了高阻态电平可能被上拉电阻拉高看起来像一直为1然后主机开始产生时钟那么从设备的数据可能无法驱动这条已经变为高阻的线。确保在发送命令的最后一个时钟边沿结束后再切换BIDIOE位。我上面代码中的等待BSY标志位清零就是为了这个。检查从设备是否响应有些从设备需要在命令后等待几个时钟周期t_{CSH}才开始输出数据。查看你的传感器数据手册确认时序要求。如果有等待要求在切换方向为输入后先发几个哑元时钟比如调用HAL_SPI_Receive读一个字节但丢弃再开始正式接收。检查SPI相位和极性CPOL/CPHA这是SPI通信的经典问题。你的从设备要求时钟空闲时是高电平还是低电平CPOL数据在第一个时钟边沿还是第二个时钟边沿采样CPHA必须和从设备严格匹配。用逻辑分析仪看波形对照数据手册的时序图一个边沿一个边沿地对。4.2 问题二只能发送不能接收程序卡在接收函数现象发送命令正常但一执行HAL_SPI_Receive就超时。排查思路确认BIDIOE位已正确清零在调用接收函数前打印或调试查看SPI_CR1寄存器的值确认BIDIOE位是0。有时候寄存器操作没生效可能是因为SPI还处于使能状态某些寄存器是写保护的。尝试在修改CR1前先__HAL_SPI_DISABLE()修改后再__HAL_SPI_ENABLE()但这会复位SPI状态不是最佳实践。通常确保SPI不忙!BSY时修改即可。检查DMA或中断配置如果你使用了DMA或中断确保接收相关的DMA流或中断是正确配置和使能的。在半双工模式下收发使用同一个数据寄存器DMA配置要格外小心。检查从设备是否真的在输出数据逻辑分析仪是终极武器。看SDIO线在主机产生接收时钟时线上是否有数据变化如果没有问题出在从设备端命令不对、供电不足、模式未进入等。4.3 问题三通信不稳定偶尔出错现象大部分时间通信正常但偶尔会读回错误数据。排查思路电源与地线检查电源是否干净地线连接是否良好。高速SPI对电源噪声比较敏感。上拉电阻SDIO作为双向线在方向切换为输入时主机端是高阻态。如果从设备驱动能力弱或者线路有电容可能导致电平建立缓慢在时钟采样点到来时电平不确定。在SDIO线上加一个4.7kΩ - 10kΩ的上拉电阻到VCC可以显著改善信号质量尤其是在总线空闲或方向切换的瞬间。软件时序的“滑窗”在方向切换和开始接收之间如果系统有高优先级中断打断可能会引入不可预知的延迟。确保这一小段关键代码的原子性可以临时关闭全局中断。时钟频率过高降低SPI的波特率试试。过高的时钟频率可能导致信号边沿不陡峭容易受干扰。5. 进阶优化DMA与中断下的半双工当数据量较大或者不想让CPU等待SPI传输时我们会考虑使用DMA或中断。在半双工模式下这变得更具挑战性。5.1 中断模式中断模式相对简单。你可以使用HAL_SPI_Transmit_IT和HAL_SPI_Receive_IT。关键在于在发送完成中断回调函数HAL_SPI_TxCpltCallback中进行方向切换然后启动接收中断。同样在接收完成回调HAL_SPI_RxCpltCallback中拉高片选并可能切换回输出方向。void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) { if(hspi-Instance SPI1.Instance) { // 发送完成切换方向为输入 CLEAR_BIT(hspi-Instance-CR1, SPI_CR1_BIDIOE); // 启动接收 HAL_SPI_Receive_IT(hspi, rx_buf, RX_SIZE); } }这种模式需要仔细管理状态机避免回调函数重入。5.2 DMA模式DMA模式效率最高但也最复杂。问题在于发送和接收使用的是同一个物理数据线但DMA通常配置为从内存到外设发送或从外设到内存接收的单向传输。一种可行的方案是配置两条DMA流一条用于发送Memory-to-Peripheral一条用于接收Peripheral-to-Memory。但它们都连接到SPI的同一个数据寄存器SPI_DR。分时复用先启动发送DMA发送命令。在发送DMA传输完成中断或查询标志位中停止发送DMA切换BIDIOE为输入然后启动接收DMA。“哑元”发送HAL_SPI_Receive_DMA函数内部实际上会先配置DMA为从外设到内存然后使能SPI的发送器TXE中断。因为SPI接收数据需要主机产生时钟而产生时钟就需要向数据寄存器SPI_DR写数据。在接收DMA模式下硬件会自动向SPI_DR写入一个预定义的值通常是0xFFFF或0x00来驱动时钟。这个值就是“哑元”数据。你需要确认这个机制在半双工模式下是否正常工作。个人经验对于简单的三线SPI读操作数据量不大时使用阻塞模式或中断模式代码更清晰更容易调试。除非是高速、大数据量的持续传输否则引入DMA带来的复杂度可能得不偿失。如果一定要用DMA务必参考ST官方对应系列芯片的参考手册中关于SPI DMA的说明以及HAL库中BIDIMODE下的DMA例程如果有的话。6. 与软件模拟SPI及标准SPI的对比思考折腾完硬件半双工SPI我们再来回头看看它和软件模拟SPI、标准四线SPI的对比就能更清楚它的定位。特性软件模拟SPI标准四线硬件SPI三线半双工硬件SPI时序精度低受CPU中断、任务调度影响高由硬件时钟生成高由硬件时钟生成CPU占用高需CPU持续操作GPIO低传输由硬件/DMA完成低传输由硬件/DMA完成引脚占用灵活任意GPIO固定4根MOSI, MISO, SCLK, CS固定3根SDIO, SCLK, CS通信效率低速率受限于CPU翻转速度高全双工同时收发中半双工收发需切换开发难度低逻辑完全自控低标准库/HAL库完善中高需手动管理方向切换适用场景低速、非实时、引脚受限高速、实时、标准从设备专用三线从设备、引脚节省、中高速所以选择哪种方式是一个权衡的过程。如果你的从设备只支持三线又对通信速率和CPU占用有要求那么深入研究STM32的硬件半双工SPI就是必由之路。它绝不是最方便的那个选项但却是能在性能和资源之间取得较好平衡的方案。最后调试这种底层通信逻辑分析仪几乎是必需品。它能直观地展示时钟、数据和片选线上的每一个跳变帮你快速定位是命令没发对、方向切早了、还是从设备根本没响应。没有它很多问题就像在黑暗中摸索效率极低。花点时间研究一下SPI的波形图对照数据手册你会发现一切问题都有迹可循。