资讯详情

STM32G0 SPI接收调试实战:从FIFO到中断/DMA完整指南

📅 2026/9/10 1:49:30 | 华诺云谱 👁 阅读
STM32G0 SPI接收调试实战:从FIFO到中断/DMA完整指南
简介这是一份基于STM32G0系列芯片的SPI从机接收示例工程使用HAL库进行开发面向嵌入式初学者和工程师解决从设备接收数据时的中断处理与参数配置问题。压缩包内文件共1138个整体约9.21MB其中C语言源文件662个、头文件260个另有汇编启动文件、链接脚本、CubeMX初始化配置和Keil工程文件汇编文件用于启动初始化链接脚本定义内存布局ioc配置则方便重新生成工程。同时包内还包含HAL库的定时器、I2C、加密等外设驱动源码便于延伸到其他模块的学习。目前已有235人学习下载。读者可借助完整的工程结构快速编译运行对比HAL库的分层实现并参考驱动中的回调机制理解SPI从机接收时数据寄存器、状态标志和中断优先级的协作过程。该示例保留了较为独立的驱动分层和清晰注释适合直接移植到实际项目中作为通信基础模块也可作为学习HAL库源码的辅助材料。 直接说结论这个压缩包十有八九是拿STM32G0做SPI主/从机接收的工程示例。如果你也是因为调试G0的SPI接收调到怀疑人生想找一份能跑通的参考代码那这篇内容应该能帮上忙。我基于这个包和实际调试经验把G0的SPI接收从硬件底层到CubeMX配置、从三种接收方式到踩坑实录完整捋一遍。1. 拿到压缩包之后先搞清G0的SPI接收链路由什么构成很多人在G0上调SPI接收喜欢直接套F1或F4的老代码结果发现要么收到一堆乱码要么干脆进不了接收中断。这不是运气问题而是G0的SPI外设和F1/F4在内部结构上本来就不一样。先弄清接收链路是什么后面所有的代码和配置才有意义。1.1 G0的SPI外设与F1/F4的差异STM32G0系列的SPI外设大体上延续了STM32L4之后的架构内部带了一个8位深度的接收FIFO收发共用一个移位寄存器但接收缓冲区的行为跟F1时代完全不同。F1的SPI接收特别简单收到一字节RXNE置1你读DR就清掉没有FIFO没有阈值配置逻辑直白。G0不一样接收路径上多了一个FIFO层RXNE标志的实际行为取决于FIFO阈值阈值可以配置成1/4、1/2、3/4或满也就是说你开启接收中断后不是每收一个字节就立刻进中断而是要等FIFO里的数据量达到你设定的阈值才触发。如果沿用F1的思维把RXNE当“来一个中断一次”在G0上很可能出现中断频率远低于字节到达频率的情况。尤其在做连续接收时读DR的时机一旦和FIFO阈值不匹配就会发生数据覆盖或漏读。搞清楚这一层是G0 SPI接收的起点。1.2 接收方向的数据通路从引脚到内存简化来看G0的SPI接收链路是外部信号从MISO或三线模式下的MOSI引脚进入经过输入驱动和采样逻辑进入移位寄存器每收满一个数据帧硬件把数据压入接收FIFO同时把状态寄存器里的RXNE置位。接下来由软件或DMA把FIFO里的数据搬走。这条通路里有三个容易忽视的环节。第一是采样点SPI没有独立的时钟握手主机的SCK边沿必须和从机的数据输出时序对上否则采到的就是半电平或跳变沿上的毛刺。第二是FIFO溢出保护如果软件/DMA来不及搬数据FIFO满了之后硬件会丢掉新数据同时置OVR标志。第三是NSS片选信号对接收状态机的控制主机模式下NSS可以不参与但从机模式下NSS一旦释放接收状态机直接复位当前收到的数据可能作废。这三件事是后面所有坑的根源。你现在可以打开压缩包里对应的中断或DMA回调函数对照这条链路想想它从引脚收了数据放进了FIFO然后软件多久搬一次搬的时候有没有检查溢出NSS是不是一直在有效电平2. CubeMX配置接收链路时最容易忽略的三个开关CubeMX能生成能编译的工程但生成不了“一定正确”的配置。SPI接收的很多问题本质上是在CubeMX里少勾了一个选项或多选了一个模式。下面这三个配置项是接收调试中最常出问题的位置。2.1 主从模式与NSS管理的匹配关系第一步要明确你到底是主机接收还是从机接收。这个方向不明确后面全乱。主机接收的场景通常是主机发命令给从机比如W25Q64、SD卡、传感器然后从机把数据推回来主机一边发时钟一边收。这种情况下NSS一般做软件管理把它设成输出并拉到低电平即可硬件NSS可以不参与。从机接收的场景是外部主机主动发起通信把数据发给你你被动接收。这种情况下NSS最好是硬件管理由外部主机拉低你的NSS引脚来选中你。CubeMX里对应的是“Hardware NSS Input”模式。我见过最多的问题是把从机配置成软件NSS管理然后忘记把NSS引脚内部上拉。结果就是外部主机发来的数据其实已经被SPI外设接收了但因为NSS引脚悬空电平抖动导致外设的状态机反复复位数据根本进不了FIFO。此类问题在示波器上看MISO和其他信号全正常但代码里就是收不到任何字节。2.2 CPOL/CPHA相位不匹配的直接后果是数据整体错半拍SPI有四种模式由时钟极性CPOL和时钟相位CPHA组合决定。主机和从机必须用完全相同的模式否则接收数据会整体错位。CPOL决定空闲时SCK是高还是低CPHA决定数据采样发生在第一个跳变沿还是第二个跳变沿。很多传感器的数据手册会明确写“SPI Mode 0”或“SPI Mode 3”但也有一些手册画了时序图却不标注模式需要自己对着图判断。判断方法很简单看时序图里SCK空闲电平再找数据变化和采样点。如果数据在SCK下降沿变化、上升沿采样且空闲时SCK为低那就是Mode 0如果空闲时SCK为高数据在上升沿变化、下降沿采样就是Mode 3。拿不准的时候先用逻辑分析仪抓主机抓不到数据时的波形对比从机手册的时序图一次就能对上。2.3 数据长度与FIFO阈值决定了RXNE中断多久进一次G0的SPI支持4到16位的数据帧长度。CubeMX里Prescaler旁边的“Data Size”选项如果被改成非8位那接收缓冲区里每个元素的大小就不一样中断回调取数据时按8位去读必然出错。FIFO阈值这一项在CubeMX的Advanced Parameters里面默认是1/4。对于8位数据帧1/4阈值意味着FIFO里满2个字节才触发RXNE事件。如果预期接收频率不高建议直接把阈值设成1/4并保持默认不要为了“看起来高效”调成3/4或满否则低速率通信时接收中断迟迟不触发容易误判为总线无数据。提示在配置完成后打开生成的main.c检查SPI初始化结构体里Direction是否设成了“2Lines RxOnly”或“2Lines FullDuplex”。如果选成TxOnly接收路径根本不工作。3. 轮询、中断、DMA三种接收方案的取舍与代码实现G0的SPI接收在代码层面有三种实现方式分别是轮询、中断和DMA。三者的本质区别是“谁来搬数据、什么时候搬、搬的时候CPU在干嘛”。3.1 轮询接收只在调试阶段推荐轮询方式的核心逻辑是不断读取SPI状态寄存器检查RXNE标志置位就去DR里取数据。代码最少逻辑最直白。uint8_t spi_poll_receive(SPI_HandleTypeDef *hspi, uint8_t *buf, uint16_t len) { for (uint16_t i 0; i len; i) { while (__HAL_SPI_GET_FLAG(hspi, SPI_FLAG_RXNE) RESET) { if (__HAL_SPI_GET_FLAG(hspi, SPI_FLAG_OVR) SET) { __HAL_SPI_CLEAR_OVRFLAG(hspi); return 1; } } buf[i] *((uint8_t *)hspi-Instance-DR); } return 0; }在主机往从机发数据并同时收返回数据的场景里轮询接收必须配合发数据一起做。每次写一字节DR等RXNE再读一字节。轮询的最大问题是占CPU且卡死风险高。如果从机长时间不拉低MISO程序就死等在while循环里。所以轮询只建议在调试阶段用产品代码能不用就不用。3.2 中断接收中等数据量的平衡点中断接收的思路是每收到一个或一阈值数量的数据帧硬件触发SPI中断在中断服务函数里把数据搬到用户缓冲区。G0的标准库和HAL库都有现成的回调函数。// 在main.c里开启接收中断 HAL_SPI_Receive_IT(hspi1, rx_buf, RX_LEN); // 在stm32g0xx_it.c的中断处理函数里转发 void SPI1_IRQHandler(void) { HAL_SPI_IRQHandler(hspi1); } // 在回调里取数据 void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi-Instance SPI1) { // 数据已经由HAL库搬进了rx_buf process_rx_data(rx_buf, RX_LEN); } }HAL库的HAL_SPI_Receive_IT会把接收长度记录下来然后一个字节一个字节地在中断里接收直到收满指定长度才调用RxCpltCallback。这里要注意G0的SPI中断向量只有一个SPI1_IRQHandler无论RXNE、OVR还是EOT都进同一个函数HAL库内部会自动判断。3.3 DMA接收批量数据的正确姿势DMA接收和中断接收的区别是数据搬运由DMA控制器完成CPU只在DMA搬完一整个缓冲区后收到一个完成中断。对于连续大流量接收比如从SD卡读数据块、从屏幕控制器读显存这是唯一靠谱的方式。// DMA接收完整流程 static uint8_t dma_rx_buf[256]; // 初始化阶段打开DMA接收数据直接进缓冲区 HAL_SPI_Receive_DMA(hspi1, dma_rx_buf, 256); // DMA传输一半时触发半传输中断 void HAL_SPI_RxHalfCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi-Instance SPI1) { // 前半段数据已就绪可以处理 process_rx_data(dma_rx_buf, 128); } } // DMA全部传输完成 void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi-Instance SPI1) { process_rx_data(dma_rx_buf 128, 128); } }DMA方式有一个关键前提DMA的触发源必须配置为SPI接收请求并且DMA的通道优先级要合理设置。CubeMX里在DMA Settings选项卡添加SPI1_RX通道后要确保Mode设为Normal不要设成Circular否则数据会从头覆盖缓冲区你根本不知道当前处理的数据是新是旧。Direction也必须是PeripheralToMemory。3.4 接收缓冲区管理的hidden细节无论哪种接收方式缓冲区管理都有两个容易被忽略的点。第一是缓冲区大小的选择。中断接收时缓冲区必须大于等于你预期接收的最大帧长否则HAL库底层会数组越界。DMA方式更严格缓冲区大小最好能覆盖DMA传输长度的两倍配合半传输中断形成流水线处理。第二是共享缓冲区的互斥保护。如果主循环在读写rx_buf的同时DMA/中断回调也在往rx_buf里写就会出现数据竞争。轻则拿到半个包重则程序跑飞。最稳妥的做法是定义双缓冲区一个给DMA/中断写入一个给主循环读出通过标志位在两者之间切换角色。4. 实测中“收不到/收不对”的四个高频原因代码看起来没问题、CubeMX配置也对、波形也正常但收进来的数据就是不对。这个阶段最磨人。我直接把实测中遇到最多的四个原因列出来可以对号入座。4.1 首字节丢失或错位现象接收到的数据整体少一个字节或者第一个字节是上一帧的残留。原因通常在主机侧。SPI主机的SCK在传输开始前已经存在但MISO上的数据还没有稳定。如果主机在发出第一个SCK边沿的瞬间就去采样采到的往往是无效电平。从机这边表现为第一个SCK周期打进移位寄存器的数据是垃圾值真正有效数据从第二字节开始。解决办法是给从机一个片选时间窗口主机在拉低NSS之后等待一小段时间通常是一个字节的时间再开始发SCK。比如用软件延时或等待从机的准备标志。从机代码侧可以在NSS下降沿中断里先清一下接收缓冲区保证第一字节不会跟上一帧的残留混在一起。4.2 RXNE标志清不掉的死循环现象轮询接收时程序卡在while等待RXNE的循环里出不来或者读DR之后RXNE仍然置位。原因多数是读错了寄存器。G0的SPI数据寄存器是32位地址读DR时返回的数据在低16位有些参考代码是从F1抄来的读的是SPI1-DR的8位指针这在G0上行不通。更隐蔽的问题在HAL库__HAL_SPI_GET_FLAG里读的RXNE标志实际上对应FIFO的阈值状态。如果FIFO阈值配的是3/4而数据流只到了1/2RXNE就一直不置位。轮询死循环还要检查OVR标志。FIFO满了以后硬件停止接收RXNE不再置位。程序如果只等RXNE不查OVR就永远出不来。最稳妥的做法是在等待循环里同时检查OVR一旦溢出直接退出并清标志。4.3 从机模式下NSS引脚的“隐藏逻辑”G0从机模式的NSS引脚行为比想象中更严格。硬件NSS输入模式下只有NSS为低电平时SPI从机才会对外发送数据NSS拉高后从机的发送逻辑立即停止。问题在于很多外部主机在传输完一帧后会拉高NSS此时从机的MISO引脚会切换到高阻态这是一条正常的时序。但如果外部主机的NSS信号在传输过程中出现毛刺哪怕只有几十纳秒从机的接收状态机也会被打断。处理办法从机模式下NSS引脚不要直接接外部主机可以先接一个RC滤波R取10k、C取100pF左右能滤掉大部分毛刺。如果你确定外围主机的NSS信号很干净也可以试着把NSS配置成软件管理但此时必须把NSS引脚内部上拉防止悬空抖动。4.4 DMA半传输中断与缓冲区错位现象DMA接收模式下数据是正确的但处理时发现前半段和后半段顺序颠倒或者重复处理同一段数据。原因几乎都是Mode配错。DMA的Mode如果误设成Circular传输完成后自动回到起始地址继续接收每次进入RxCpltCallback时缓冲区起点已经漂移。必须把Mode改成Normal然后每次处理完后重新调用HAL_SPI_Receive_DMA。另一个隐蔽问题是半传输中断和完成中断的先后顺序。配置了半传输中断但回调里没有开启下一次接收会导致后半段数据一直没人处理。我的建议是如果数据量不大干脆不要用半传输中断只用完成中断简单可靠。5. 调试SPI接收的实用套路与信号排查如果你已经改了很多配置还是收不到数据别再盲试了按下面这套排查链路走一遍基本能定位到问题层。5.1 从波形到寄存器四级排查法第一级查波形。用逻辑分析仪或示波器抓SCK、MOSI、MISO、NSS四根线。重点看SCK有没有正常输出主机模式或输入从机模式NSS有没有被拉低MISO上有没有数据跳变。这一级能排除硬件连接、引脚复用、时钟配置错误。第二级查SCK与数据的相位关系。对照从机数据手册的时序要求确认CPOL和CPHA。最简单的方法是把四线波形完整截下来跟手册里的时序图叠加比对。波形对不上直接改配置不需要看代码。第三级查SPI外设寄存器状态。在调试器里打开外设寄存器视图观察SPI_SR的RXNE、OVR、BSY位以及SPI_DR的数据。如果RXNE一直不置位说明硬件没收到有效数据问题在物理层或时钟层。如果RXNE置位但读出来的数据是错的问题在采样点或数据帧格式。第四级查DMA/中断链路。确认DMA通道是否使能、请求源是否挂到SPI_RX、中断优先级是否被其他中断抢占。在HAL库回调里打断点看能不能进回调进了回调数据对不对。5.2 用逻辑分析仪验证“接收到了但软件没收到”有一种情况特别迷惑MISO波形上明显有数据但代码里收不到。这时候逻辑分析仪就能发挥作用。把分析仪的协议解析选成SPI设置好CPOL/CPHA和数据位宽解码出来的十六进制数据就是实际线上的数据。如果解码正确但代码里看不到那就是FIFO/中断/DMA链路的问题如果解码都是错的问题就在硬件连接或相位配置。注意逻辑分析仪的采样率至少要是SCK频率的4倍以上不然解码结果不可信。G0的SPI常用速率在几MHz分析仪20M以上的采样率比较保险。5.3 一个屡试不爽的“最小验证工程”排查到最后如果还找不到问题最有效的手段是删掉所有业务代码只留SPI初始化和一个接收裸循环把SPI配置成最普通的8位、Mode 0、软件NSS、轮询接收。主机或另一块开发板循环发送0x55、0xAA这类交替字符。从机循环读DR收到一个字节就用UART打印一个字节。这个最小工程能跑通说明硬件和CubeMX配置都没问题再往上加业务逻辑。能跑通的抽象模型排错是嵌入式调试里最快的手段。6. 根据个人经验G0 SPI接收的几条心得整个调试过程走下来我对STM32G0的SPI接收有几点体会属于代码之外但又直接影响成败的工程判断。第一个心得是G0的数据手册和CubeMX的配置项是有细微出入的别只信其中一方。CubeMX生成的初始化代码有些情况下不会把FIFO阈值和RXNE事件映射关系暴露清楚遇到底层行为不符合预期时去翻参考手册比反复试配置更高效。第二个心得是SPI接收不像UART那样有“起始位”和“停止位”它对时序的容忍度低得多。UART出问题大概率是波特率SPI出问题大概率是相位或片选管理。凡是接收数据一帧对、一帧错先怀疑NSS信号它不稳定的话SPI状态机就会出现比软件逻辑更诡异的行为。第三个心得是调试期至少保留一个UART打印口。SPI是没有数据后“自己会报错”的通信方式很多时候你根本不知道收得对不对只有通过UART把SPI收到的内容打出来才能判断“收到了”和“收对了”是两件完全不同的事。这个习惯帮我省了至少一半的定位时间。如果你的工程压缩包里现在接收部分还没调通我建议你先别急着改代码拿逻辑分析仪抓一次波形把SCK、MISO、NSS三根线的实际时序和传感器手册对一遍。大部分“收不到数据”的问题在这一步就已经水落石出了。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。