简介:STM32G0系列微控制器的SPI从机HAL库接收示例工程,专门面向嵌入式开发初学者和需要快速实现SPI通信的工程师。压缩包基于STM32 HAL库提供完整的从机接收代码,通过一个可运行的实验演示SPI在设备间通信中的实际运作方式,帮助读者理解从机模式下数据接收的初始化、配置与处理流程,也适合作为工业控制、传感器数据采集等场景的参考模板。包内共1138个文件,以C语言源码(662个)、头文件(260个)和汇编语言文件(103个)为主,同时包含IAR与Keil工程配置、启动文件、链接脚本及实用脚本等,整体约9.21MB,目录结构清晰便于按模块检索。目前已有235人学习下载,可供初学者对照实践并快速上手。此外,包内还附带了定时器、I2C等外设驱动源码,能够帮助使用者一并了解STM32G0系列的外设编程方法,提升整体开发效率。 直接上一个干活的例子。前阵子从同事手里接过来一个工程包,名字就叫stmg0-spi-receive,一看就是STM32G0系列做SPI接收的参考工程。我本来以为跟F1的老代码差不多,结果仔细翻了配置和中断处理才发现,G0的SPI外设跟老系列玩法差异不小,尤其是接收方向,配置不对是真的会掉数据。这篇就围绕这个工程,把G0做SPI从机接收的完整思路、关键配置、状态机设计和排查经验一次讲清楚。适合正在用G0做从机通信、被SPI接收丢数据折磨的朋友参考。
1. STM32G0的SPI接收架构,跟老系列差别在哪
1.1 G0 SPI外设的硬件升级点
STM32G0虽然定位是入门级,但SPI外设反而比F1时代强了不少。最直观的变化是数据寄存器支持8位、16位和32位帧格式,而且收发路径上带了小FIFO,不像F1那样只有一个可怜巴巴的缓冲寄存器。以前F1收高频小数据包时,稍不注意RXNE没及时清,数据就被覆盖了;G0的小FIFO至少给了你一点缓冲时间,不会一压就崩。
另外G0的SPI外设把错误标志细分了,像OVR(溢出错误)、MODF(模式错误)、UDR(欠载错误)都各自独立,调试时能更快定位问题。老F1虽然也有OVR,但标志位混在一起,出了问题还要猜。还有一个容易被忽略的地方:G0从机模式下,NSS引脚的时序要求比F1更敏感。这个后面会细说,它直接决定了你要不要用硬件片选。
1.2 为什么接收方向更容易翻车
发送方向出错,主机那边回读比对马上就能发现。接收方向出错就不一样了,数据可能卡在FIFO里没被读走,也可能因为一个多余的时钟边沿多收了一个垃圾字节,表现出来就是莫名其妙多一个0x00、少一个起始字节、或者整帧错位。
我最开始在这个工程上踩的坑就是上电后FIFO里有残留数据——G0的SPI从机刚初始化完,如果NSS被拉低过,哪怕主机一个时钟都没发,也有可能产生一个假接收事件。这个"幽灵数据"问题,老F1基本碰不到,G0上就必须在初始化后主动清一次FIFO和OVR标志,不然第一次读取的数据都是脏的。
2. CubeMX里SPI接收的关键配置
2.1 参数配置里最容易踩的三个坑
用CubeMX配置G0的SPI从机接收,界面上看起来跟老系列差不多,但实际有几个坑。
第一个坑是时钟极性(CPOL)和时钟相位(CPHA)。必须跟主机端完全一致:模式0是空闲低电平、第二个边沿采样,模式3是空闲高电平、第二个边沿采样,这个大家都知道。但G0有个细节,从机模式下SPI时钟频率必须小于PCLK的1/2,CubeMX不会帮你自动限制。如果你把从机SPI时钟设得很高,短线实测可能没问题,但稳定性会下降,我习惯保守一点,不超过PCLK/4。
第二个坑是数据帧格式。主机如果是8位帧,从机老老实实选8位。但有些主控支持7位或12位帧长,这时候从机必须跟主机设置成一样的帧长,不能用8位去兼容——不然收出来的数据是乱的。
第三个坑关系到数据打包。G0的SPI数据寄存器支持8位、16位、32位三种模式,如果你的主机每次发送一个固定长度的数据块,建议从机把帧宽设成跟主机一致,这样每次RXNE事件正好对应一个完整的数据单元,处理逻辑清爽很多。比如主机每次发16位,从机也设16位,一个RXNE读一次就够了,不用拼两个字节。
2.2 接收方式选中断还是DMA
CubeMX里SPI接收你看到三种选项:中断、DMA和轮询。轮询在从机接收场景里基本没用,因为你不知道主机什么时候发数据来,总不能一直空转刷标志位。
中断方式适合数据量不大、帧长不固定的场景。每次RXNE触发中断,在中断里读数据、写入自己的缓冲区。G0的SPI中断向量在NVIC里是独立的,不像老F1还要复用,配置起来很干净。
DMA方式适合数据量大、帧长固定的场景,比如从SPI Flash读一大块数据、接收音频流。G0的SPI DMA请求有独立的通道映射,需要在CubeMX里把SPI的接收DMA请求打开,并配置好数据宽度。DMA的好处是CPU完全不用管字节搬运,坏处是帧边界不好判断——你不知道主机这一帧到底发了多少个字节,除非配合片选信号。
我的建议是:如果做的是命令响应型通信(主机时不时发一条指令,长度不定),用中断+软件状态机。如果做的是数据流传输(固定长度、高频率),用DMA+硬件片选。这个工程里的STMF4那套DMA配合包装的代码,思路可以参考,但注册表可以直接沿用。后面第三、第四章分别给这两套方案的实现细节。
3. 中断接收不定长数据的实战写法
3.1 从机接收状态机的设计
主机发来的数据长度不固定,从机怎么判断一帧数据结束?业界最常见的方案是用片选信号作为帧边界:CS拉低表示帧开始,CS拉高表示帧结束。所以G0从机端要把NSS配置成硬件片选或者外部中断引脚。
我建议直接用GPIO外部中断监控CS上升沿。具体做法是:NSS引脚复用为SPI的NSS功能,再把同一个引脚的外部中断使能,配置为上升沿触发。这样既能正常参与SPI通信,又能在主机拉高CS时立刻知道"这一帧结束了"。
整个从机接收的状态机可以简化成三个状态:
- IDLE:等待CS拉低,不接收数据
- RECV:CS为低,持续接收数据,每收到一个字节存入缓冲区
- FRAME_DONE:CS上升沿触发,表示一帧数据接收完成,处理这一帧内容
核心伪代码如下:
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin == NSS_Pin) { if (HAL_GPIO_ReadPin(NSS_Port, NSS_Pin) == GPIO_PIN_RESET) { // CS拉低,帧开始 rx_state = STATE_RECV; rx_len = 0; } else { // CS拉高,帧结束 rx_state = STATE_FRAME_DONE; process_frame(rx_buffer, rx_len); } } } void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi->Instance == SPI1) { if (rx_state == STATE_RECV && rx_len < RX_BUF_SIZE) { rx_buffer[rx_len++] = rx_byte; // 准备接收下一字节 HAL_SPI_Receive_IT(&hspi1, &rx_byte, 1); } } }注意HAL的HAL_SPI_Receive_IT每次只接收一字节,接收完成后会调用回调,然后中断会暂时关闭,必须在回调里再次调用HAL_SPI_Receive_IT才能继续接收。这个机制在G0上要特别留意,因为G0的SPI中断标志位比较灵敏,如果回调里处理太慢,或者忘了重新打开接收,数据直接丢掉。
3.2 溢出处理与环形缓冲区
CS作为帧边界的方案有个隐患:如果主机在CS拉高后又快速拉低开始下一帧,而你已经进入了FRAME_DONE处理状态,处理耗时较长,就会漏掉第二帧的开头。所以FRAME_DONE状态里应该只做"拷贝缓冲区、置标志位"这种轻量操作,真正的业务解析放到主循环里做。
另一个需要重点关注的是溢出问题。G0的SPI从机如果接收端没有及时读走FIFO里的数据,新来的字节就会触发OVR错误,之后通信直接卡死。HAL库里HAL_SPI_Receive_IT在OVR后返回的状态不是HAL_OK,很多人没查返回值,导致后续数据全丢。
我的做法是给接收缓冲区加个环形结构,中断里只负责写指针移动,主循环负责读指针移动,当两者相等时认为缓冲区已读空。同时在SPI错误回调里加OVR恢复逻辑:
void HAL_SPI_ErrorCallback(SPI_HandleTypeDef *hspi) { if (hspi->Instance == SPI1) { if (__HAL_SPI_GET_FLAG(hspi, SPI_FLAG_OVR)) { __HAL_SPI_CLEAR_OVRFLAG(hspi); // 重新开启接收 HAL_SPI_Receive_IT(&hspi1, &rx_byte, 1); } } }注意:G0的OVR标志清除方式跟F1不太一样,直接读DR寄存器再读SR寄存器的老方法不好使了,推荐用
__HAL_SPI_CLEAR_OVRFLAG宏,它会自动处理寄存器访问顺序。
4. DMA接收配合硬件片选的使用细节
4.1 硬件片选与DMA的组合要点
如果主机每次发来的数据长度是固定的,比如100字节一帧,那可以用DMA,让硬件把数据自动搬运到内存缓冲区,CPU零参与。但DMA模式下帧边界的判断更麻烦,这时候必须靠硬件片选。
G0的SPI在从机模式下,NSS可以配置为硬件片选(NSS hardware mode)。当NSS被拉低时,从机被选中,所有时钟信号都被接收;NSS拉高后,接收停止。硬件片选的好处是,片选变化由外设内部处理,不需要GPIO中断额外介入。
但要注意,CubeMX默认配的硬件片选模式可能是需要NSS输出使能的,做从机时要选"NSS hardware"而不是"NSS output"。选错了,从机会一直认为自己被选中,数据乱收。
DMA接收配置时需要把SPI的RX DMA请求打开,并把DMA通道设为循环模式或正常模式。正常模式下一帧数据刚好填满缓冲区,然后DMA传输完成中断触发,在中断里处理数据。循环模式适合连续数据流。
4.2 一帧接收完成后的数据装配
DMA有个特点:它是无感的,主机发了多少字节它不知道,只有缓冲区满了或者DMA传输完才知道。所以用DMA接收固定长度帧时,必须保证DMA配置的传输长度跟主机发送长度完全一致。
举个例子,主机每次发64字节,从机DMA传输长度就配置成64。主机开始发送前先拉低CS,从机收到CS下降沿之后开始DMA搬运,等64字节收齐,DMA传输完成中断触发。如果主机发到一半突然不发了,DMA就会一直等,直到超时。
我在这个工程里给DMA方案加了个CS超时保护:在片选上升沿时,如果DMA还没传输完成,就读取当前DMA计数器的剩余值,算出实际收到多少字节,然后手动终止DMA传输,按实际字节数去解析数据。这样就算主机发了一半CS就拉高了,从机也能正确处理半包数据。
void on_nss_rising_edge(void) { uint32_t remaining = __HAL_DMA_GET_COUNTER(&hdma_spi1_rx); uint32_t received = RX_FIXED_LEN - remaining; HAL_DMA_Abort(&hdma_spi1_rx); process_frame(rx_buffer, received); }提示:DMA计数器返回的是剩余未传输的数据单元数,单位跟DMA数据宽度一致。如果数据宽度是8位,那数量就是字节数;如果宽度是16位,要乘以2才是字节数。别搞错了。
5. 常见问题与排查技巧实录
5.1 SPI接收不到数据的三个排查方向
遇到"从机收不到数据"或者"收到的数据全是0xFF"这类问题,我一般按下面顺序排查,基本能覆盖八成情况。
先查时钟配置。G0从机的SPI时钟必须来自主机,但如果APB时钟或SPI预分频配错了,哪怕主机发了时钟,从机这边也可能因为采样窗口不对而收不到。用逻辑分析仪抓SCK和MOSI的波形,确认时钟极性和相位是否真的匹配。
再查NSS片选。最常见的问题是NSS引脚内部上拉没启用。主机端可能默认输出高电平,但从机端如果不把NSS引脚上拉,引脚悬空或状态不稳定,从机会随机进入选中状态,数据收发全部错乱。CubeMX里把NSS引脚设为输入模式并打开内部上拉,是大多数人漏掉的一步。
最后查FIFO残留。如果G0上电后直接进接收状态,很有可能FIFO里有脏数据。我建议在初始化完成后做一次软复位:把SPE位清零,然后读DR寄存器把FIFO清空,确保FIFO深度和RXNE标志都归零,再重新置位SPE。
5.2 时序误区与调试手段
SPI接收的调试,我第一次做的时候走了不少弯路,还好这次G0工程里总算是摸清了路数。有一个特别容易踩的坑是:从机完全不需要配置波特率。SPI波特率只对主机有意义,从机是跟随外部时钟的。但很多人习惯性在CubeMX里把从机SPI波特率也调低,结果反而影响了内部位时序判断,尤其在40MHz主频下,SPI的过采样逻辑可能出问题。
调试手段方面,最实用的是加一个GPIO翻转来测量中断处理耗时。比如在SPI接收中断入口把PB0拉高,出口拉低,用示波器看PB0的高电平时间,就能算出中断处理占用了多久。如果这个时间超过了从机的字节间隔,就会丢数据。这个办法我一直在用,比看时序图猜效率高得多。
另外,如果手头没有逻辑分析仪,也有个土办法:主机端用固定数据模式循环发送0xAA、0x55这种交替位数据,从机接收后把数据通过串口打印出来。0xAA和0x55交替出现说明位序对了;如果收到的是0x55和0xAA反过来,说明LSB/MSB设置不一致;如果收到的是0xFF和0x00,说明采样沿或者CPOL/CPHA有偏差。
5.3 硬件片选和软件片选怎么选
排查的时候经常会纠结一个问题:到底用硬件片选还是软件片选。
我的建议很明确:从机端优先用硬件片选。软件片选虽然可以省一个专用的NSS引脚,把它当普通GPIO用,但需要自己控制引脚状态,而且GPIO翻转的速度比不上硬件片选外设内部处理的稳定性。尤其是主机速度跑到10Mbps以上的时候,软件片选完全扛不住,帧边界容易漂移。
但硬件片选在G0上有个小坑:SPI外设的NSS引脚会直接参与接收逻辑,如果主机没有把NSS拉高拉低的时序处理好,从机可能在一帧中间失去选中状态,导致数据截断。所以硬件片选模式下,主机端的NSS控制一定要可靠,不能通过简单的软件延时去模拟片选时序,最好也用硬件NSS输出。
| 对比项 | 硬件片选 | 软件片选 |
|---|---|---|
| 引脚占用 | 占用一个专用NSS引脚 | 任意GPIO即可 |
| 接收稳定性 | 高,外设内部处理 | 低,受GPIO翻转速度影响 |
| 帧边界检测 | 外设自动识别 | 需要外部中断辅助 |
| 适合场景 | 高速、固定帧长、DMA传输 | 低速、命令型通信、引脚受限 |
我做这个工程的时候,最终确定的是"硬件片选+中断+DMA混合"的架构:命令帧用中断接收,配合CS上升沿判断帧结束;数据块用DMA搬运,配合硬件片选保证边界。这套组合在G0上实测稳定,还是那句老话,通信这东西,硬件能少操心的就让硬件去管,人为干预越少,出错的概率越低。
最后再分享一个小技巧。如果手头的逻辑分析仪只能抓到主机波形,抓不到从机波形,可以在从机接收中断里加一个计数器,每收到一字节就把一个GPIO翻转一次,用示波器看这个GPIO的波形频率,就能反推主机实际发送的字节速率。我在排查一个主机时钟不稳的问题时就是这么做的,几分钟就定位到了原来是主机端PLL配置带来的抖动。调试嵌入式通信,思路比工具重要,先把变量的位置锁定准确,再去看波形和数据,才不会浪费时间。
本文还有配套的精品资源,点击获取