STM32G0 SPI接收调试实战:从FIFO到中断/DMA完整指南
2026/9/10 1:49:17 网站建设 项目流程

简介:这是一份基于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/2,RXNE就一直不置位。

轮询死循环还要检查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初始化和一个接收裸循环:

  1. 把SPI配置成最普通的8位、Mode 0、软件NSS、轮询接收。
  2. 主机(或另一块开发板)循环发送0x55、0xAA这类交替字符。
  3. 从机循环读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三根线的实际时序和传感器手册对一遍。大部分“收不到数据”的问题,在这一步就已经水落石出了。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询