资讯动态

STM32 SPI从机DMA输出0xFF问题根因与解决

发布时间:2026/8/30 10:28:07 来源:尧图企业网站定制
Nucleo-H753ZI做SPI从机、配好DMA之后用逻辑分析仪怼在MOSI引脚上看到一整片整齐划一的 0xFF 0xFF 0xFF 0xFF。任你往DMA发送缓冲区里写什么主机读回来的都是这一副“我没数据可发”的嘴脸。这个问题标题我猜不少人都搜索过我第一次遇到时也折腾了一整个晚上。想清楚之后发现SPI从机加DMA这套组合坑不在“怎么配”而在于“从机到底什么时候才算是准备好”。这篇文章直接把从现象到根因的完整链路拆开把最终能跑通的配置方法、代码骨架和排查顺序都写出来给同样被0xFF折磨的开发者一条捷径。1. 先从现象说起0xFF到底是谁发出来的1.1 用逻辑分析仪确认第一手的波形排查这种问题第一步永远是抓波形不是看代码。拿一个逻辑分析仪或者干脆上示波器把SCK、MOSI、MISO、CS这四根线全部接上触发条件设成CS下降沿然后让主机发起一次SPI传输。我当时抓到的结果是CS正常拉低SCK正常输出8个时钟脉冲但从机的MOSI线上从头到尾都是高电平。主机把收到的数据打出来清一色0xFF。主机端看起来一切正常时钟也有、片选也有就是从机那边“不说话”。这里要特别强调一下MOSI线上全是高电平和“从机发了0xFF”是两回事。高速连续的高电平只能说明一件事——从机的发送数据线上压根没有任何有效数据被移位出来。1.2 0xFF的本质空闲电平和空寄存器SPI是同步串行协议从机的数据输出完全靠主机的SCK时钟一点一点“挪”出来。从机的发送移位寄存器里有什么SCK一来就串行地把这些bit送出去。问题在于如果从机发送移位寄存器里什么都没有那么SCK来了之后MOSI引脚会保持一个默认的空闲电平。这个空闲电平由CPOL决定CPOL配置空闲电平主机读到的字节CPOL0低电平0x00CPOL1高电平0xFF大部分默认配置CPOL0但STM32的HAL库默认初始化时如果你没仔细看时钟极性或者主机的配置和从机不统一从机输出高电平空闲态太常见了。再说了就算CPOL0如果发送寄存器里有个残留的0xFF同样会输出0xFF。所以看到0xFF先别急着怀疑“数据被谁加密了”本质就是发送端没有任何有效数据可供移位。1.3 认清一个事实从机没有自己的时钟SPI从机是一个完全被动的角色。它不能主动发起传输它只能等主机的SCK来了才把当前发送移位寄存器里的内容推出去。如果你理解了这一点就会明白一个关键结论从机的数据必须“预装载”。也就是说在主机的SCK到达从机引脚之前从机的发送数据寄存器TXDR里必须已经有一个完整的字节在待命。否则SCK第一拍打过来的时候移位寄存器里是空的产生了第一个0xFF。这个“预装载”概念就是整篇文章的核心。后面所有关于DMA启动时机的坑都源于此。2. 从机DMA传输的完整链路以及链路上的断点2.1 一个字节是怎么从内存走到MOSI引脚的要排查0xFF必须先清楚从机在DMA模式下一个正常字节的完整流动路径CPU或者主循环调用HAL_SPI_Transmit_DMA把内存中的发送缓冲区地址和长度告诉DMA。DMA控制器往SPI的发送数据寄存器TXDR写入第一个字节。TXDR里的数据被硬件自动装载到发送移位寄存器。主机的SCK时钟脉冲到达从机发送移位寄存器里的8个bit逐位从MOSI引脚输出。发送移位寄存器空了之后TXDR也空了硬件置位TXE标志。TXE置位触发DMA请求DMA搬运第二个字节到TXDR。重复第2到第6步直到全部数据发完。这条链路里任何一环断了最终表现都是MOSI线上没有有效数据主机读到0xFF。2.2 这张链路上最常见的五个断点根据我自己的经验以及群里帮人看过的类似问题断点基本集中在五个地方第一DMA压根没启动。很多人以为SPI初始化完成之后DMA就会自动工作实际上从机侧必须显式调用发送函数DMA才会被使能。CubeMX生成的初始化代码里DMA通道只是配置好了但并没有启动传输。第二DMA请求映射错误。STM32H7的DMA通过DMAMUX把外设请求映射到具体的DMA通道。如果在CubeMX里把SPI1的TX请求错配到别的外设或者生成代码后手动改过DMA配置DMA永远收不到TXE触发。第三DMA方向配置反了。发送要用MEMORY_TO_PERIPH也就是从内存搬到外设。如果配成PERIPH_TO_MEMORY方向反过来DMA会把SPI收到的数据写到内存里去发送线上自然什么都没有。第四NSS片选没把从机“选中”。使用硬件NSS模式时如果主机的CS引脚没有接到从机的NSS或者电平逻辑不对从机在内部就没有进入被选中状态SCK来了也不干活。第五初始化顺序问题。DMA还没启动主机就已经开始发SCK了。第一批时钟全部打在空寄存器上从机输出的就是连续0xFF。等你后面启动DMA数据是能发了但第一批数据已经丢了。2.3 为什么“先启动DMA”是从机模式的铁律前面提到的“预装载”落实到代码里就是一句话从机侧必须在主机发起传输之前先把DMA传输函数调用起来。具体过程是这样的调用HAL_SPI_Transmit_DMA之后DMA会立即把第一个字节写入TXDR。此时TXDR满TXE不再置位DMA进入等待状态数据就“挂”在发送寄存器里待命。主机的SCK一到第一个字节就能正常发出。发出后TXE置位DMA继续搬运下一个字节环环相扣。如果反过来等主机SCK已经开始跑了才调HAL_SPI_Transmit_DMA那么从SCK开始到DMA写TXDR这段空窗期发送移位寄存器里什么都没有输出就是0xFF。而且这个0xFF会占据第一个字节的位置后续数据整体错开一个字节。我之前用过一个月饼模子的类比主机是压模机SCK是压模的动作从机的TXDR是模具里的馅料。你要在压模机压下来之前就把馅料放进去压出来才是完整的月饼。压完了才放馅压出来的只能是空壳。3. 我的实际排查过程从CubeMX到寄存器逐级验证3.1 第一步核对CubeMX里的SPI和DMA配置当时我先把CubeMX配置一项一项过了一遍。SPI1初始化里Mode选择的是SlaveDirection是2 Lines Full DuplexData Size是8 BitCPOL和CPHA都设成了Low和1 Edge。这几个都是常规配置看起来没什么问题。DMA配置那页才是重点。我打开DMA Settings看到SPI1_TX和SPI1_RX两个请求都加上了。TX的DMA方向是MemoryToPeripheralRX的DMA方向是PeripheralToMemory模式都是Normal数据宽度是Byte。到这里还是没什么异常。但有一个细节当时没注意后来才发现问题CubeMX里DMA请求下拉框里选择的通道如果在生成代码之后被手动改过或者选择时没注意是否匹配SPI1的DMAMUX请求很容易出现DMA通道配置了但请求信号对不上的情况。这个在后面用寄存器验证时暴露出来了。3.2 第二步确认NSS到底有没有把从机“选中”NSS是个容易被忽略的环节。在CubeMX里SPI的NSS配置有两种Hardware和Software。如果选了Hardware从机就会监控NSS引脚的电平来决定是否接受SCK。我当时的接线是从机的NSS引脚接到了主机的CS引脚理论上CS拉低的时候从机应该被选中。但示波器一量发现从机NSS引脚上的电平确实被拉低了所以问题不在这里。不过多提一句如果你的硬件NSS没有接对线或者主机的CS配置成了软件控制而不是硬件自动拉低那么从机的NSS可能一直处于高电平。在STM32H7上硬件NSS模式下NSS为高意味着从机内部就认为“没被选中”SCK来了也白来。这种情况下你看到的现象同样是MOSI持续0xFF。如果你不想依赖外部片选信号可以改用软件NSS模式让从机始终保持被选中状态。前提是你的SPI总线上只有一个从机或者你有其他方式管理片选。我这里最终用的还是硬件NSS因为主机端的片选控制更直观。3.3 第三步用寄存器现场判断DMA有没有被触发软件配置看完了接下来就得看运行时的实际状态。我当时把调试器连上在主机发起传输的同时暂停从机程序读几个关键寄存器。第一个看的是SPI的状态寄存器SR。重点看两个位TXP发送数据包等待和EOT传输结束。如果TXP是1说明发送数据寄存器已经准备好接收新数据但没有数据写进去如果EOT有过置位记录说明传输已经发生过但被卡住了。第二个看的是DMA的NDTR寄存器也就是剩余传输计数。这个寄存器非常直观如果DMA被触发过NDTR会随着数据传输逐步递减。如果NDTR一直保持初始值说明DMA压根没有收到任何请求信号链路在DMA这一环就断了。我读了一下DMA通道的NDTR果然一动不动。这时候基本可以断定DMA从未被SPI的TXE信号触发过。第三个确认点是DMA中断状态寄存器。我看了LISR和HISR没有任何传输完成标志也没有错误标志。这说明DMA连一次搬运都没发生过连搬错方向的机会都没有。然后回头再去查DMAMUX配置果然——CubeMX生成的代码里SPI1的TX DMA请求映射到了DMAMUX的某个输入通道但那个输入通道对应的不是SPI1_TX而是另一个外设的请求。这种问题在H7上特别容易出因为H7的DMA不再是F1那种固定的“DMA1的通道3等于SPI1_TX”而是要通过DMAMUX做一层请求映射配置错了表面上完全看不出来。3.4 第四步最终定位到的根因问题到这里就清楚了DMA请求映射错误导致SPI发送寄存器空了之后TXE置位但DMA根本不知道要干活TXDR一直没有新数据写入发送移位寄存器只能持续输出空闲电平主机读到的自然就是0xFF。修好DMAMUX映射之后问题立刻消失。从机DMA正常搬运数据主机读到的字节和发送缓冲区完全一致。如果你排查时发现DMA其实能触发NDTR也在跑那我建议你把注意力放到NSS和初始化顺序上。这三个方向——DMA映射、NSS未选中、DMA未提前启动——基本覆盖了90%的SPI从机0xFF问题。4. 能跑通的配置和代码直接抄4.1 CubeMX中需要确认的配置项下面的表格是我最终确定的一套能正常工作的配置基于Nucleo-H753ZISPI1接口。你可以对照着检查自己的工程。配置项推荐值说明SPI ModeSlave注意不是Slave with CRCDirection2 Lines Full Duplex全双工避免只发不收Data Size8 Bit与主机保持一致CLKPolarityLow按主机配置来CLKPhase1 Edge按主机配置来NSSHardware使用硬件片选DMA TX DirectionMemoryToPeripheral内存到SPI发送DMA RX DirectionPeripheralToMemorySPI回收到内存DMA ModeNormal单次传输不要用CircularDMA Data WidthByte和SPI数据宽度匹配DMA的优先级可以设High尤其在SPI速率比较高、系统还有其他DMA任务时避免TXE信号来了之后DMA被其他任务抢占导致数据断流。4.2 从机侧的完整代码骨架CubeMX生成的基础代码我就不贴了重点说几个必须手动确认和修改的关键点。第一个是DMA请求映射。在H7上生成代码后要打开main.c或者其他初始化文件找到MX_DMA_Init()函数检查DMA通道的请求ID是否和SPI1匹配。具体可以参考STM32H7参考手册中DMAMUX的请求映射表。如果不匹配要修改DMA_HandleTypeDef的Init.Request参数。第二个是启动DMA的时机。下面这段代码推荐放在main函数里所有外设初始化完成之后、主循环开始之前int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_DMA_Init(); MX_SPI1_Init(); // 从机发送缓冲区数据可以来自任何地方 uint8_t spi_tx_buf[8] {0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88}; uint8_t spi_rx_buf[8] {0}; // 关键在主机SCK到来之前先把DMA跑起来 if (HAL_SPI_TransmitReceive_DMA(hspi1, spi_tx_buf, spi_rx_buf, 8) ! HAL_OK) { Error_Handler(); } while (1) { // 主循环不需要处理SPIDMA会自己搬 // 可以在这里处理其他任务 } }注意这里我用的是HAL_SPI_TransmitReceive_DMA而不是HAL_SPI_Transmit_DMA。原因后面专门讲先记住从机侧尽量用收发一体的函数。第三个是传输完成回调。当主机发完预期的字节数后从机的DMA传输完成会触发回调void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi-Instance SPI1) { // 这一轮传输结束可以处理spi_rx_buf里的数据 // 如果需要持续服务可以在这里再次调用HAL_SPI_TransmitReceive_DMA // 注意如果是单次传输需求不需要重复调用 } }如果你需要从机反复响应主机的多次传输就在回调里重新调用一次DMA启动函数让数据始终保持“预装载”状态。如果只做单次应答回调里处理完数据就够了。4.3 主机侧最简单的验证程序没有主机侧的配合很难确认从机到底有没有发对。我当时用了另一块Nucleo当主机写了一段最朴素的验证代码// 主机侧SPI1配置为主模式 uint8_t host_rx_buf[8] {0}; uint8_t host_tx_buf[8] {0xA5, 0x5A, 0xA5, 0x5A, 0xA5, 0x5A, 0xA5, 0x5A}; // 片选拉低 HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET); // 主机发送8个字节全双工同时接收从机返回的8个字节 HAL_SPI_TransmitReceive(hspi1, host_tx_buf, host_rx_buf, 8, 1000); // 片选拉高 HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET); // 打印host_rx_buf如果从机工作正常应该收到0x11 0x22 0x33 0x44 0x55 0x66 0x77 0x88主机发送的是什么无所谓关键是持续给SCK从机的DMA才有机会把数据搬出去。4.4 故意制造相同的0xFF来验证排查结果我调试的时候有个习惯确定了根因之后会故意把故障条件重新触发一遍确认现象一致再从反方向验证修复有效。当时我做了三个对照实验第一个实验注释掉主循环前的那行HAL_SPI_TransmitReceive_DMA让从机不启动DMA。主机立刻读回来全部的0xFF。这说明“DMA未启动”确实是0xFF的充分条件。第二个实验把NSS引脚强制拉高即使DMA跑着主机同样读回0xFF。这说明NSS未选中也可以独立导致同样的现象。第三个实验恢复NSS拉低恢复DMA提前启动主机读到了预期数据。三个实验做完整个因果链就闭环了。如果你也遇到0xFF不妨照这个思路做对照实验。找到那个能稳定复现0xFF的操作你就找到了根因。5. 这几个连环坑建议一次看完5.1 数据宽度与FIFO阈值的组合STM32H7的SPI带FIFO数据宽度和FIFO阈值对DMA请求的行为有直接影响。如果你SPI配了8位数据宽度DMA也配Byte通常没问题。但如果你把SPI配成16位DMA还是Byte或者反过来就会出现一个字节拆成两次搬、数据错位的情况。H7 SPI的FIFO阈值通过SPI_CFG1寄存器里的TXFTH和RXFTH配置。当数据宽度是8位时FIFO阈值保持默认即可改成16位时要注意DMA的数据宽度必须同步改成HalfWord否则每次DMA搬运的字节数和SPI期望的数据格式对不上数据流会乱。如果数据流乱掉了主机读到的不一定是连续的0xFF而可能是第一个字节正常、后面的字节全乱。这种错位现象和0xFF的成因不同千万别混在一起排查。5.2 溢出错误OVR会让一切卡死从机如果只调用了HAL_SPI_Transmit_DMA也就是只发送不接收主机那边发过来的数据会被SPI硬件接收填入RXDR。如果RXDR满了你又不去读硬件会置位溢出标志OVR。OVR一旦置位SPI外设会进入错误状态需要软件清标志才能恢复。在HAL库中溢出后SPI的收发都会停摆这时候主机看到的同样是0xFF或者干脆没有数据。避免这个问题最省事的方案就是我从一开始就强调的从机侧用HAL_SPI_TransmitReceive_DMA把接收通道同时打开。哪怕你不关心主机发来的数据也要开一个接收缓冲区和接收DMA让RXDR及时被读走防止溢出。5.3 Circular模式的滥用DMA的Circular模式适合无限循环的数据流比如ADC连续采样、音频播放。但在SPI从机应答这种“发完一批就停”的场景里Circular模式会带来一个隐藏问题传输结束后DMA会自动从头开始搬运如果发送缓冲区里的数据恰好都是0xFF看起来就像MOSI在持续不断地输出0xFF。有些开发者为了图省事把DMA设成Circular以为可以少管一些启动逻辑结果就是莫名其妙多出一堆0xFF。我的建议是从机应答场景老老实实用Normal模式配合DMA传输完成回调来管理下一轮传输。这样每一轮的数据边界都清晰可控。5.4 时钟极性和相位的潜移默化CPOL和CPHA配置错误不会直接导致0xFF但会影响数据在SCK的哪个边沿被采样。如果主从两边的极性相位不一致主机采样到的bit可能全部偏移看起来就像收到了一串乱码或全1。尤其注意一点CPOL1时SPI总线空闲电平是高。当你的从机发送寄存器为空时MOSI保持高电平主机按位采样读出的字节自然就是0xFF。这种0xFF和“完全没数据”的0xFF在外观上是一样的但根因不同。如果抓波形发现MOSI确实有数据只是主机采错边沿读成全1或全0优先检查两边CPOL/CPHA是否一致。5.5 复位后SPI寄存器的默认状态STM32H7的SPI外设复位之后SPE默认是0外设处于关闭状态。如果你的初始化顺序有误比如DMA配置好了但SPI的SPE位没有置1那么即使DMA开始搬运数据也进不了移位寄存器。在HAL库中HAL_SPI_Init调用后SPE的置位由后续的传输函数完成。这意味着在第一次调用HAL_SPI_TransmitReceive_DMA之前SPI从机实际上并没有处于完全工作状态。所以在CubeMX生成的MX_SPI1_Init函数之后最好先确认SPE已经被使能再启动DMA。你可以直接读一下SPI1的CR1寄存器SPE位应该是1。如果不是就要检查初始化流程里是否有报错或者hspi的State状态是不是进入了异常分支。6. 最后再做一轮实测收尾6.1 用示波器看波形修复之后我重新抓了一轮波形作为验证。这次MOSI线上不再是连续高电平而是能清楚看到0x11、0x22、0x33这些数据字节对应的电平翻转。CS拉低期间每个SCK时钟周期都对应一个有效的输出bitCS拉高之后MOSI回到空闲电平。这个波形和之前的0xFF波形形成了非常鲜明的对比所有关于“从机坏没坏”的疑虑在看到这个波形的一瞬间全部打消了。6.2 最终的工作状态描述从机程序最终的状态是这样的上电初始化之后发送缓冲区就通过HAL_SPI_TransmitReceive_DMA“预装载”到了SPI发送寄存器。主机任意时刻发起传输从机都能第一时间响应无需额外握手。每次传输完成之后DMA回调会把接收缓冲区里的数据做一次拷贝或标记然后重新启动下一轮DMA。用这套方案我在H753ZI的SPI1上跑通了从机DMA的完整收发主机频率从1MHz一路拉到几十MHz数据都稳定一致。6.3 一套一页纸的排查顺序最后分享一个我自己总结的排查顺序遇到SPI从机DMA发不出数据按这个顺序走一遍大部分问题都能定位第一抓波形。确认MOSI线上是持续高电平还是偶尔有翻转。要是不抓波形直接改代码很容易白忙活。第二查NSS。用示波器或万用表确认从机NSS引脚的电平确实被拉低硬件NSS模式下从机没有选中就不会干活。第三查DMA映射。核对DMAMUX请求是否对应SPI的TX和RX。这一步最容易出错因为H7的DMA请求不是固定通道配置错误表面看不出任何异常。第四查DMA方向。发送是MemoryToPeripheral接收是PeripheralToMemory反了就废了。第五查启动时机。确认在主机SCK开始之前从机已经调用了HAL_SPI_TransmitReceive_DMA。第六查溢出标志。看看SPI的SR寄存器里OVR有没有置位置位了就清掉再说。这六步走完不敢说100%解决所有问题但“从机输出0xFF”这个现象背后绝大多数原因都逃不出这些检查项。如果你照着排查完还是碰到0xFF大概率是某个寄存器状态和预期不一致这时候建议把调试器挂上单步停在主机发数据的那一刻把SPI和DMA的所有相关寄存器全部读出来对比数据手册那个时刻的寄存器快照会告诉你最直接的答案。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价