ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32H7 SPI从机DMA输出0xFF?从物理层到寄存器逐层排查

STM32H7 SPI从机DMA输出0xFF?从物理层到寄存器逐层排查 1. 初遇问题从机发给主机的数据全是 0xFF先说结论SPI 从机 DMA 模式下连续输出 0xFF本质上就是“从机根本没把有效数据放到 MISO 线上”。这个问题不是 STM32H7 独有的F1、F4、G4 系列也会遇到但在 H7 上更容易踩坑原因是 H7 的 SPI 和 DMA 架构跟老一代芯片相比做了不少改动网上的老经验很多不能直接套用。我调试这块板子时用的是 STM32CubeMX 生成工程SPI 从机配置为主机读取外部传感器数据的应答端。主机 SPI 时钟 4MHzCPOL Low、CPHA 1Edge数据帧 8bitMSB First。从机端开了 DMA 发送DMA 通道配置为内存到外设方向数据长度 256 字节。结果一跑起来主机通过 SPI 读回来的数据流是FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF ...一眼望过去全是 0xFF而且是稳定、连续、一个有效字节都没有的 0xFF。这跟我预期的“偶发错误”完全不一样——偶发错误一般会夹杂几个正确字节而全 FF 是另一种信号从机根本没在总线上驱动数据。为什么这么说因为 SPI 总线在没有设备驱动 MISO 的时候线路默认电平取决于外部上拉/下拉电阻。多数板子的 MISO 线在空闲时是弱上拉主机读到的就是高电平也就是 0xFF。换句话说MISO 一直是被动的“空”状态而不是从机在主动发送有效数据。这个现象特别迷惑人的地方在于你明明配置了 DMA明明调用了HAL_SPI_Transmit_DMA()函数返回也是HAL_OK甚至HAL_SPI_TxCpltCallback都在跑——但总线上的数据依然全是 FF。这说明问题不在“DMA 有没有搬数据”而在于SPI 外设的发送通道根本没有把数据送到 MISO 上去。这一条思路如果能在一开始就建立起来排查方向就不会跑偏。下文按“先从物理层和配置层确认再深入协议层和 DMA 架构最后结合软件栈逐层排雷”的顺序把这几天踩过的坑和解决办法完整梳理一遍。文章涉及的代码均在 STM32CubeIDE 1.13 以上版本 STM32CubeH7 1.11 环境中验证过SPI 时钟最高测到 8MHzDMA 数据长度从 16 字节到 1K 字节都能稳定工作。2. 先搞清楚 0xFF 是谁产生的物理层判断与定位2.1 SPI 空闲电平和 MISO 的“悬空”状态SPI 从机不驱动总线时MISO 引脚通常处于高阻态Hi-Z。主机侧如果不带下拉电阻读到的电平就是不确定的。实际开发板上MISO 往往被主机内部上拉或者板载电阻拉到了高电平于是读到的字节就是 0xFF。这个推论可以用一个最简单的实验验证把从机程序停掉或者干脆不初始化 SPI 外设主机依然能读回 0xFF。如果停掉从机后主机读到的还是 FF那就证明 0xFF 并非从机主动产生而是总线空闲电平被读了出来。所以这里有一个很重要的调试习惯看到满屏 0xFF先别急着查代码逻辑先用“有没有可能从机压根没驱动 MISO”这个角度去思考。我见过不少同事折腾 DMA 半天最后发现是 SPI 外设的 TXE 标志位就没置位过数据根本没往移位寄存器里送。2.2 如何用示波器确认从机到底有没有动作排查这类问题强烈建议把示波器探针夹到从机的 MISO 引脚比如 Nucleo-H753ZI 板子的 PA6具体看你自己用什么引脚。观察点在主机发起 SPI 传输时MISO 上有没有电平翻转动作。判断逻辑很简单观察到的现象说明什么问题MISO 持续为高/低完全不翻转SPI 外设或 DMA 完全没有发送动作MISO 在时钟启动后有电平翻转但数据符号不对数据是发出去了但字节内容有误MISO 只有 CS 拉低瞬间有电平变化SCK 来了之后没变化从机没有被正确选通或者 CS 检测逻辑有问题MISO 翻转频率和 SCK 完全对应但主机读的还是 FF波形可能是对的主机侧采样参数不匹配如果示波器显示 MISO 完全没有翻转那就说明问题出在从机侧的发送链路里跟主机侧怎么配置没关系。这时候可以缩小范围把“SPI - DMA - 内存”这条链路的每个环节单独拉出来测。2.3 最简单的“写寄存器”测试法在深入 DMA 之前建议先做一个最粗暴的试验不走 DMA直接在 SPI 发送中断或者轮询模式下向 SPI 的发送数据寄存器写几个固定字节。如果这时 MISO 上能出来数据说明 SPI 外设本身没问题如果还是不输出那就是 SPI 配置的问题。这个测试在 H7 上特别有效因为 H7 的 SPI 用了新的寄存器组和 F1/F4 系列差异很大不能盲目参考老代码。我当时的做法是在main()里初始化完 SPI 外设后把 DMA 相关代码全部注释掉直接调用一次HAL_SPI_Transmit(hspi1, testData, 8, 1000)。注意这个函数在从机模式下是“阻塞等待主机时钟”的主机不发时钟函数就一直卡在那里。如果你在调试器里看到程序停在函数内部而主机那边确实没有发起读操作那其实是正常现象不是死机。当主机发起一次读时序后如果 MISO 上能读到0x01 0x02 0x03 ...这种有规律的数据说明 SPI 硬件链路是完全通的。这个时候再把 DMA 加回去如果数据又变回 FF那问题就锁定在 DMA 配置和 SPI-DMA 交互这一层。说到这我想插一句网上很多帖子一上来就让人查 DMA 的优先级、FIFO 阈值、突发模式这些参数当然重要但如果物理层都没通调这些参数纯属浪费时间。所以我的默认排查顺序永远是物理层 - 寄存器层 - 外设配置层 - DMA 层 - 软件栈层逐层缩小范围。3. SPI 从机配置的隐形雷区参数匹配与 NSS 管理3.1 CPOL/CPHA 主从不匹配数据移出但主机读错如果物理层已经确认 MISO 有翻转但主机的逻辑分析仪读到的还是 FF那首先要怀疑的就是 CPOL/CPHA 主从不匹配。SPI 有四种模式组合主机和从机必须完全一致否则数据移位时机对不上从机发出去的数据主机根本采不到。H7 的 SPI 初始化结构体中有SPI_InitTypeDef关键参数是hspi1.Init.CLKPolarity SPI_POLARITY_LOW; // CPOL 0 hspi1.Init.CLKPhase SPI_PHASE_1EDGE; // CPHA 0我这次用到的组合是 CPOL Low、CPHA 1Edge。如果你的主机是别的模式从机必须完全对应。常见错误是把从机配置成 CPHA 2Edge导致主机在第一个边沿采样时 MISO 上的数据还没稳定读到的就是 FF。这里有一个实操技巧把 CubeMX 里的 SPI 参数配置截图发给主机的开发者两个人对照着确认。因为“时钟极性低电平”和“第一个时钟边沿采集数据”这种描述在不同文档里叫法不太一样有的叫“Mode 0”有的叫“CPOL0 CPHA0”对照确认可以避免鸡同鸭讲。3.2 数据帧格式位宽、MSB/LSB 和字节顺序H7 的 SPI 支持 4bit 到 32bit 的数据帧长度从机和主机的DataSize必须一致。CubeMX 默认是 8bit这块一般不容易错。但有一个很容易忽略的点是FirstBit——如果主机是 MSB First从机配成了 LSB First那主机读到的每一个字节都会是反序的。比如从机想发 0x01二进制 0000 0001LSB First 发出去变成 0x801000 0000主机如果按 MSB 解析读到的就是 0x80 而不是 0x01。另外H7 的 SPI 还支持FIFO接收/发送缓冲FIFO 阈值可以通过HAL_SPI_Init()之后的底层配置寄存器调整。在 DMA 模式下FIFO 阈值会影响 DMA 请求的触发时机。默认配置下一般没问题但如果你的收发数据长度不是 FIFO 深度的整数倍尾部数据可能会卡住。这个后面在 DMA 章节详细说。3.3 硬件 NSS 与软件 NSS选通信号没拉低从机永远在“隐身”这是我从机场景里踩得最隐蔽的一个坑。Nucleo-H753ZI 的 SPI 从机如果使用硬件 NSS 模式NSS SPI_NSS_HARD_INPUT从机必须检测到 NSS 引脚被拉低才会把 MISO 切换为输出驱动状态。如果你的主机和从机之间没有连接 NSS 引脚——比如只接了 SCK、MISO、MOSI、GND 四根线——那么从机的 NSS 引脚就悬空或保持高电平从机永远不会被选通MISO 一直处于高阻态主机读到的自然全是 0xFF。解决办法有两个方案一使用硬件 NSS。确保从机的 NSS 引脚接到了主机的 CS片选引脚并且在 CubeMX 中把 NSS 配置为硬件模式。硬件 NSS 的优点是片选信号完全由主机控制从机硬件自动检测不需要软件干预。缺点是需要多接一根线而且 SPI 外设的 NSS 引脚一般是固定映射的在 H753ZI 上是 PA4不能随便换。方案二使用软件 NSS。把 SPI 的 NSS 配置为软件模式SPI_NSS_SOFT然后在发送数据前手动拉低 NSS 引脚。不过这里有个容易误解的地方软件 NSS 模式下很多教程让你直接操作 GPIO 拉低 NSS 引脚电平但从机侧真正起作用的是 SPI 外设内部的一个SSIInternal Slave Select位而不是 GPIO 的外部电平。在 STM32H7 上软件 NSS 的从机配置通常是这样hspi1.Init.NSS SPI_NSS_SOFT; // ... 初始化 ... // 发送前使能内部从机选择 SET_BIT(hspi1.Instance-CFG1, SPI_CFG1_SSI);我实测过这个SSI位如果不置 1就算你把外部 GPIO 拉低SPI 从机照样不会响应。这也是很多人明明接对了线从机却毫无反应的原因之一。CubeMX 生成代码默认不会帮你设置这个位需要手动加一行。注意软件 NSS 模式下SSI位的置位时机要在使能 SPI 之前还是之后H7 参考手册里没有特别强调。我反复试过在__HAL_SPI_ENABLE(hspi1)之后设置同样有效而且如果发送过程中 NSS 被拉高再拉低SSI位不会自动清掉所以不用担心下次传输前要重新置位。3.4 从机模式下的通信时钟缺失问题还有一个从机模式特有的现象值得单独提出来从机发送数据是完全依赖主机时钟的。也就是说即便你把从机的 DMA 配置得再完美如果主机不发起 SPI 时钟从机的发送数据就永远不会从移位寄存器里移出去。这一点在调试时容易产生误判你从代码层面看HAL_SPI_Transmit_DMA()返回了HAL_OK以为数据已经开始发了实际上 DMA 只是把数据从内存搬到了 SPI 外设的 TX 缓冲区真正的移位发送要等主机时钟来了才会发生。如果你在从机代码里打断点查看 DMA 计数寄存器可能会发现计数从 256 变到了 0但这只代表数据进了外设缓冲不代表已经发出去了。所以排查时一定要确认主机那边确实在发起传输。我这次就是因为主机程序里读操作被一个延时卡住了导致主机根本没发时钟从机也就一直在“等待”状态看起来像是从机坏了。把主机那边的逻辑理顺之后数据马上正常了。4. H7 的 SPI 与 DMA 交互被低估的架构变化4.1 STM32H7 的 DMAMUX请求映射不再是固定绑定这是 H7 系列和老一代 STM32 在 DMA 方面最大的区别。F1/F4 的 DMA 请求是固定的比如 SPI1_TX 永远连在 DMA1 的某个通道上你照着参考手册配置就行。而 H7 引入了 DMAMUXDMA 请求多路复用器外设的 DMA 请求可以映射到任意一个 DMA 流stream上通过 DMAMUX 寄存器来指定。CubeMX 生成代码时这个映射关系是自动配置的但如果你手动移植老代码就很容易漏掉这一步。错误的表现就是 DMA 配置函数返回HAL_OK但 DMA 从不会真正被触发——因为 DMA 不知道自己该监听哪个外设的请求信号。用 CubeMX 配置时注意看 DMA Settings 标签页里 Request 这一列比如DMA1_Stream0, SPI1_TX, MemoryToPeripheral, High Priority这里的SPI1_TX就是 DMAMUX 的请求源。如果你看到 Request 显示的是0或者空白那多半是手动修改代码时把 DMAMUX 配置弄丢了。正确的做法是重新通过 CubeMX 生成初始化代码或者手动补上__HAL_LINKDMA(hspi1, hdmatx, hdma_spi1_tx); hdma_spi1_tx.Init.Request DMA_REQUEST_SPI1_TX;这句hdma_spi1_tx.Init.Request DMA_REQUEST_SPI1_TX;在 H7 上是必须的没有它 DMA 就不会响应 SPI 的请求。而如果你看 F4 的代码这个字段很可能不存在或者不需要指定。这个差异是移植老代码时最大的坑。4.2 从机模式下 DMA 传输的本质数据先填充、时钟再移出理解了 DMAMUX 之后再来看从机 DMA 的完整数据流调用HAL_SPI_Transmit_DMA()后HAL 库使能 SPI 的 TX DMA 请求SPI 外设的 TXFIFO 空时向 DMAMUX 发出 DMA 请求DMA 响应请求从内存读取一个字节/半字/字写入 SPI 的 TXDR 数据寄存器TXDR 的数据进入移位寄存器主机产生 SCK 时钟移位寄存器按位移出到 MISO 引脚TXFIFO 空时再次触发 DMA 请求循环往复注意第 2 步和第 5 步是异步的。从机侧 DMA 填充数据的速度可以比主机时钟快只要 TXFIFO 里有数据SCK 来了就能移出去如果 TXFIFO 为空SCK 来了移出去的就是 0x00 或者 0xFF取决于移位寄存器里残留的数据。在这个流程中有一个关键点经常被忽视SPI 从机使能 DMA 发送之前TXFIFO 必须是空的否则老的残留数据会被先发出去。H7 的 SPI 在每次传输完成后TXFIFO 里可能残留上一帧的最后几个字节。如果你不清理就直接开始下一次 DMA 发送开头几个字节可能是残留数据然后才是新数据。排查时可以通过读SPI_SR的状态位来确认比如RXWNE、TXC这些位。更简单粗暴的方法是每次发送前调用__HAL_SPI_CLEAR_OVRFLAG(hspi1)清除溢出标志同时确认TXC被置位表示上次传输已完成。4.3 FIFO 阈值与 DMA 请求触发时机H7 的 SPI 内部有一个 16 字节或 8 字具体看数据宽度的 TXFIFO。DMA 请求的触发策略与 FIFO 阈值相关。CubeMX 默认配置下阈值为 1 字节这意味着 FIFO 空出一个字节的位置就会触发一次 DMA 请求。这个配置在大多数场景下没问题。但如果你传输的数据宽度是 16bit 或 32bit或者打开了 DMA 的突发模式FIFO 阈值和突发长度的配合就容易出岔子。比如 DMA 配置为突发 4 次每次搬 1 字节但 FIFO 阈值却设置为 4 字节那 DMA 请求就永远不会被触发因为 FIFO 空出来的位置始终不够 4 字节。这个问题的典型现象是第一次 DMA 传输可能正常之后的传输就卡死或者数据错乱。因为第一次传输时 FIFO 是空的足够触发突发条件传输结束之后 FIFO 残留了部分数据下一次阈值判断就不满足了。解决办法是把 DMA 的 FIFO 设置为 Disable或者把突发模式改为 Single。对于从机场景我测试下来最稳定的配置是DMA 模式 Normal、FIFO Disable、MemDataAlignment Byte、PeriphDataAlignment Byte。这种配置虽然效率不是最高但兼容性最好出问题的概率最小。4.4 一个完整可用的从机 DMA 初始化代码下面给出一份精简但完整的配置代码包含了上面提到的关键处理。CubeMX 生成的初始化用户代码区可以照这个思路调整/* SPI1 从机配置 */ hspi1.Instance SPI1; hspi1.Init.Mode SPI_MODE_SLAVE; hspi1.Init.Direction SPI_DIRECTION_2LINES; hspi1.Init.DataSize SPI_DATASIZE_8BIT; hspi1.Init.CLKPolarity SPI_POLARITY_LOW; hspi1.Init.CLKPhase SPI_PHASE_1EDGE; hspi1.Init.NSS SPI_NSS_SOFT; hspi1.Init.FirstBit SPI_FIRSTBIT_MSB; hspi1.Init.TIMode SPI_TIMODE_DISABLE; hspi1.Init.CRCCalculation SPI_CRCCALCULATION_DISABLE; hspi1.Init.CRCPolynomial 7; hspi1.Init.CRCLength SPI_CRC_LENGTH_8BIT; hspi1.Init.NSSPMode SPI_NSS_PULSE_DISABLE; HAL_SPI_Init(hspi1); /* DMA 发送配置 */ hdma_spi1_tx.Instance DMA1_Stream0; hdma_spi1_tx.Init.Request DMA_REQUEST_SPI1_TX; hdma_spi1_tx.Init.Direction DMA_MEMORY_TO_PERIPH; hdma_spi1_tx.Init.PeriphInc DMA_PINC_DISABLE; hdma_spi1_tx.Init.MemInc DMA_MINC_ENABLE; hdma_spi1_tx.Init.PeriphDataAlignment DMA_PDATAALIGN_BYTE; hdma_spi1_tx.Init.MemDataAlignment DMA_MDATAALIGN_BYTE; hdma_spi1_tx.Init.Mode DMA_NORMAL; hdma_spi1_tx.Init.Priority DMA_PRIORITY_HIGH; hdma_spi1_tx.Init.FIFOMode DMA_FIFOMODE_DISABLE; HAL_DMA_Init(hdma_spi1_tx); __HAL_LINKDMA(hspi1, hdmatx, hdma_spi1_tx); /* 使能 SPI 内部从机选择软件 NSS 必须 */ SET_BIT(hspi1.Instance-CFG1, SPI_CFG1_SSI); /* 启动 NVIC 中断 */ HAL_NVIC_SetPriority(SPI1_IRQn, 5, 0); HAL_NVIC_EnableIRQ(SPI1_IRQn); HAL_NVIC_SetPriority(DMA1_Stream0_IRQn, 5, 0); HAL_NVIC_EnableIRQ(DMA1_Stream0_IRQn);发送数据时调用HAL_SPI_Transmit_DMA(hspi1, txBuffer, bufferLen);如果使用了环形缓冲区持续发送可以选择HAL_SPI_Transmit_DMA在传输完成后再次调用或者在HAL_SPI_TxCpltCallback里重新启动下一次传输。这里的注意事项是在回调里重新调用前一定要确认上一次传输已经完全结束否则会触发 HAL 库的状态机断言。5. HAL 库状态机与回调机制程序“看起来在跑”其实啥也没干5.1 HAL_SPI_Transmit_DMA 内部到底做了什么很多人在排查 DMA 问题时忽略了 HAL 库本身的状态机设计。HAL_SPI_Transmit_DMA()并不是简单地“把地址和长度塞给 DMA”它内部做了一堆状态判断if (hspi-State HAL_SPI_STATE_READY) { // 设置状态为 BUSY_TX // 配置外设地址、内存地址、数据长度 // 使能 SPI 的 TX DMA 请求 // 使能外设中断 } else { return HAL_BUSY; }如果 SPI 的状态机不在HAL_SPI_STATE_READY函数会直接返回HAL_BUSY而不会启动任何 DMA 传输。常见的导致状态机卡住的原因包括上一次 DMA 传输没有正常完成hspi-State还停在HAL_SPI_STATE_BUSY_TX某个回调返回了错误代码导致 HAL 库认为传输失败SPI 发生 OVR溢出错误后没有调用HAL_SPI_ErrorCallback状态机没有被重置更隐蔽的情况是你调用了HAL_SPI_Transmit_DMA()返回HAL_OK但因为之前清除状态时没有正确复位hspi-ErrorCode导致后续 HAL 库内部检查到错误标志自动中止了 DMA。这种问题从外部看你只会看到“DMA 启动了但没有数据”查起来特别费劲。5.2 恢复 HAL 状态机的标准操作如果你怀疑状态机卡住最简单的办法是在启动 DMA 之前手动复位状态hspi1.State HAL_SPI_STATE_READY; hspi1.ErrorCode HAL_SPI_ERROR_NONE;但这个方法只能作为调试时的临时手段正式代码里不建议频繁手动操作状态机字段因为 HAL 库内部有可能有自己的缓存状态手动修改容易引起其他字段不一致。更稳妥的办法是调用HAL_SPI_Abort(hspi1)来中止当前传输并复位状态机。注意HAL_SPI_Abort()是阻塞式的如果 DMA 正在传输大块数据它会等待 DMA 停止完成才返回。在中断服务函数里调用要格外小心不要造成死锁。5.3 中断优先级与回调执行时机另一个容易出问题的点是中断优先级配置。H7 的 NVIC 中断分组默认是 4全部用于抢占优先级SPI 中断和 DMA 中断的优先级需要合理设置。如果 DMA 中断优先级设置得太低而主循环里的某个高优先级中断频繁打断 DMA 传输就可能导致 DMA 的传输完成中断延迟响应进而影响 HAL 库状态机的更新。从机模式下还有一点特殊SPI 的 RX 溢出中断OVR和 TX 完成中断TXC在从机模式下触发时机和主机不同。主机是主动控制时钟的中断只在特定时刻出现从机是被动响应的主机随时可能发起时钟所以中断有可能在任何时刻触发。如果中断服务函数处理时间过长或者优先级配置不当就可能丢失中断事件。我的建议是从机场景下SPI 全局中断优先级不低于 DMA 中断优先级保证事件按顺序处理。比如两个都设为 5同优先级下按自然悬空顺序处理也可以接受。但不要把 SPI 中断优先级设得比 DMA 低否则 DMA 完成后 SPI 的完成标志没人处理HAL 库状态机就会一直卡在 BUSY。5.4 关于HAL_SPI_TxCpltCallback的一个容易忽略的细节很多人在这个回调里写了发送完成后的处理逻辑比如翻转一个 GPIO 指示灯。但注意一个细节从机模式下TXCTransfer Complete标志是在“SPI 外设的移位寄存器发送完毕”时设置的而不是在“数据从内存搬运到 SPI 外设”时设置的。也就是说即使你看到HAL_SPI_TxCpltCallback被调用了也不代表 SPI 时钟已经全部结束——在从机模式下主机的时钟可能还在继续。如果这时候你在回调里修改了发送缓冲区的内容可能会影响下一帧数据。这个问题在调试时有一个很典型的表现主机连续读多个字节从机用 DMA 发送固定缓冲区前几个字节是对的后面的字节全是 FF。原因就是从机在回调里提前修改了发送缓冲区导致后续 DMA 搬运到的数据已经被改掉了。解决办法如果需要更新发送内容只在回调里设置一个标志位然后在主循环里再更新缓冲区不要直接在回调里操作。6. 从现象到结论的完整排查清单与故障速查表6.1 一次完整的诊断走查流程结合我这几天排查 H753ZI SPI 从机 DMA 的经验整理一个可以直接照着做的排查清单。这个清单的顺序是我实践后确定的从“看灯”到“抠寄存器”都有覆盖每一步都有明确的结论判定不会让你白干。第一步物理层确认。用示波器或逻辑分析仪同时抓 SCK、CS、MISO 三个信号。观察主机发起读操作时从机的 CS 是否被拉低、SCK 是否持续翻转、MISO 是否有对应的数据输出。如果 MISO 完全不动直接跳转到第三步如果 MISO 有数据但主机读回来的不对跳转到第二步。第二步配置参数核对。确认主从机的 CPOL、CPHA 一致数据帧宽度一致MSB/LSB 顺序一致。这两组参数只要有一个不匹配就会导致数据错位或者全 FF。可以建立一个简单的测试用例从机发送固定序列0x01 0x02 0x03 0x04...主机读回来对比能快速定位是位序问题还是电平时序问题。第三步从机选通信号确认。确认 NSS 模式是硬件还是软件硬件模式检查接线是否连接正确、主机是否配置了推挽输出软件模式检查CFG1寄存器中的SSI位是否置位。这一步排查完后MISO 应该有动作了。第四步DMA 配置确认。打开dma.c或 CubeMX 的 DMA 设置界面确认 DMA 请求源选择的是SPIx_TXH7 必须方向是MemoryToPeripheral外设地址是 SPI 的 TXDR 寄存器。如果这些都对在HAL_SPI_Transmit_DMA()调用处打断点观察hspi-State是否成功从 READY 转为 BUSY_TX。第五步测试“阻塞模式发数据”与“DMA 模式发数据”的差异。如果阻塞模式能正常发数据、DMA 模式全是 FF那问题就锁定在 DMA 与 SPI 外设的交互上。这时候可以检查 DMA 传输的计数寄存器LIFCR、NDTR 等确认 DMA 是否真的把数据传输到了 SPI 外设。第六步禁用所有无关的中断优先级和 FreeRTOS 调度使用最简环境测试。很多从机问题在裸机环境下能复现但加上 RTOS 后行为就变了原因往往是中断优先级和调度时序发生了变化。用最简环境把问题复现出来再逐步加入软件组件能更准确地定位。6.2 这篇文章踩过的坑问题现象、原因、解决方式速查表问题现象根本原因解决方式/调试要点MISO 完全无翻转主机读全 FF软件 NSS 模式下SSI位未置位确认SPI_CFG1_SSI为 1硬件 NSS 模式下检查连线MISO 无翻转但寄存器配置看似正常DMA 请求源未配置为SPIx_TX检查hdma.Init.Request确认是 DMAMUX 的正确请求 IDMISO 有翻转但数据是错的CPOL/CPHA 主从不匹配或位序不对主从机参数逐项对照用0x01 0x02序列测试定位是位序还是电平时序问题前几个字节正确后面全是 FF发送缓冲区在HAL_SPI_TxCpltCallback里被提前修改回调中只置标志位数据更新移到主循环主机读到的不是 FF而是全 0x00MISO 被下拉从机未驱动总线和全 FF 属于同类问题只是外部引脚电平不同DMA 第一次传输正常后续不稳定FIFO 阈值与突发传输长度不匹配关闭 DMA FIFO或把突发模式改为 SingleHAL_SPI_Transmit_DMA 返回 HAL_BUSYSPI 状态机卡在 BUSY_TX 未恢复调用HAL_SPI_Abort()复位状态机检查错误标志并清除FreeRTOS 环境下程序卡死DMA 中断优先级设置过高导致调度异常合理设置 NVIC 优先级SPI 与 DMA 中断优先级不差两级以上主机发时钟但从机没数据从机 TXFIFO 为空没有数据可移出确认HAL_SPI_Transmit_DMA()已经调用检查 DMA 计数是否变化数据里有偶发的错乱字节从机被非预期地重新选通或 NSS 抖动检查 CS 线上是否有毛刺硬件上可适当加滤波电容6.3 从“全是 FF”到“调试工具怎么选”调试 SPI 从机 DMA 这类问题工具选择直接决定排查速度。我这次用到的工具按优先级排列示波器或逻辑分析仪必须要有而且带宽不需要很高100MHz 就够用SPI 协议分析功能有最好没有的话用 GPIO 翻转来辅助判断时序也行。示波器的用法有三个关键点一是抓图时把 SCK、CS、MISO 三个通道同时打开这样能整体判断从机的响应时序二是触发方式设置为 CS 下降沿触发而不是 SCK 触发因为 CS 标志一次传输的开始三是观察 MISO 数据时要注意从机的数据是“在 SCK 上升沿或者下降沿采样”的要在正确的沿上判断数据电平。如果没有示波器逻辑分析仪也能凑合但要注意采样率不能太低。SPI 时钟 4MHz 时逻辑分析仪的采样率至少要 20MHz 以上越低越容易在边沿处采到不确定电平造成误判。6.4 移植中容易出问题的 GPIO 配置最后提一个很多人会忽略的细节GPIO 模式配置。SPI 从机的 MISO 引脚必须配置为复用功能推挽输出AF Push-Pull而不是开漏输出。这一点在 Nucleo 板上通常 CubeMX 已经帮你配好了但如果你手动从 F1 代码移植过来就容易把 MISO 配成开漏——因为 F1 的 SPI 从机在某些配置下 MISO 是开漏输出的H7 的 GPIO 电气特性不一样直接照搬就会导致 MISO 电平驱动能力不足主机读到的电平异常。另外从机的 SCK 和 NSS 引脚要配置为上拉输入或者带上拉的复用输入避免引脚悬空时电平抖动导致误触发。CS 引脚如果有长线连接建议在从机端加一个 10kΩ 左右的上拉电阻确保主机未拉低时 CS 保持高电平从机不会被意外选中。7. 写在最后SPI 从机调试永远先问“总线上的电平是谁产生的”这次排查 H753ZI 从机 DMA 全 FF 问题最大的心得可以用一句话总结调试 SPI 从机永远先问“总线上的电平是谁产生的”而不是先问“我的代码哪里错了”。0xFF 本身不是错误它是总线空闲时 MISO 的自然状态真正的问题是你期望从机去驱动总线但总线没有被驱动。所以每当你看到数据全是 0xFF先别急着查 DMA 参数和 HAL 函数按照“物理层有没有动作 - 从机选通有没有生效 - SPI 参数是否匹配 - DMA 是否真的搬运了数据 - HAL 状态机是否正常”这个顺序逐一排查大部分问题都能在两小时内定位。从技术演进的角度看H7 的 DMAMUX 架构让 SPI 与 DMA 的搭配更加灵活但也带来了新的配置门槛。如果你是从 F1/F4 老工程迁移过来的建议先完全删除 CubeMX 生成的 DMA 配置重新按 H7 的方式生成一次不要手动改老代码这样能避免 DMAMUX 请求映射丢失这类隐蔽问题。另外调试这类问题的时候建议把 CubeMX 的配置文件、主机的 SPI 时序图、从机的初始化代码三份材料放到一起对照着看。很多时候主机和从机两边各自都觉得自己的配置是对的但就是参数没有对齐把关键参数列成一张表逐项核对比一个人闷头查快得多。最后再分享一个小技巧在HAL_SPI_Transmit_DMA()启动之后、数据发送完成之前的这段窗口期从SPI_SR寄存器读一下TXNP位。如果这个位为 0说明 SPI 外设正在等待数据被移出如果为 1说明 TXFIFO 还有数据没发完。配合调试器实时监视这个寄存器能非常直观地看到 DMA 和 SPI 外设之间的配合是否正常。有时候看到的变化趋势比任何日志都好用。
返回列表