☰
STM32C0系列SPI回环测试实战:从引脚配置到数据校验
2026/10/22 22:21:34 网站建设 项目流程

最近拿到一块基于 STM32C092RC 的评测板,顺手就把 SPI 回环测试安排上了。所谓回环,就是拿一根杜邦线把 MOSI 和 MISO 短接,让 SPI 主机自己发数据、自己收数据,然后比对收发结果是否一致。这个操作在很多人眼里属于“入门级动作”,但实际做下来你会发现,它远不止点亮一颗 LED 那么简单。SPI 外设的时钟开启、引脚复用、模式配置、极性和相位选择,任何一个环节出错,数据都对不上,而回环测试恰好能用最直观的方式把这些隐藏问题全部暴露出来。

这篇文章就从我自己的实测过程出发,讲讲这次 SPI Loopback 测试的完整思路、工程配置、代码实现、实测结果和踩坑记录。不管你是刚接触 STM32C0 系列的新手,还是想快速验证某个硬件平台 SPI 外设是否正常的开发者,这份记录都能让你少走弯路。

1. 为什么先做回环测试:思路拆解与验证价值

1.1 回环测试到底验证了什么

很多人对回环测试的理解还停留在“把发送脚和接收脚接一起,看看能不能收到数据”,其实它的验证范围比你想的要广得多。

SPI 通信要能正常工作,至少需要满足四个条件:外设时钟已经使能、GPIO 复用功能选对、SPI 模块的参数配置正确、主从设备之间的时序能够对齐。回环测试虽然没有真实的从设备参与,但主机发送数据时,字节要经过移位寄存器、经过引脚驱动电路,从 MOSI 引脚输出,然后通过外部导线回到 MISO 引脚,再经过输入路径进入接收移位寄存器,最后存进接收缓冲区。这条链路里,只要有一个环节配置错误,接收到的数据就会出现异常。

举个例子,如果 GPIO 复用功能没选对,MOSI 引脚可能根本不输出 SPI 信号,那 MISO 自然什么都收不到。如果时钟极性和相位配置得不合理,接收端采样时刻不对,收到的数据就会错位,出现二进制数据看起来“乱七八糟”的情况。这些问题的排查难度远高于“没收到数据”这种直观故障。

所以说,回环测试是验证“SPI 外设从寄存器到引脚再到寄存器的完整通路”是否正常的最好办法。它不需要外接任何 SPI 从设备,只需要一根杜邦线,就能把外设配置问题暴露得明明白白。

1.2 两种回环实现方式的选择

SPI 回环有两种常见做法:一种是硬件回环,用导线把 MOSI 和 MISO 短接;另一种是软件回环,把 SPI 配置成接收和发送内部短接的模式。

对于一些特定系列的 MCU,SPI 外设内置了回环模式(Loopback Mode),只需要设置寄存器里对应的位,就能在芯片内部把发送数据直接反馈到接收路径,连杜邦线都不用接。这样做的好处是不受外部干扰,测试结果更纯粹,适合跑自动化测试。

不过 STM32C0 系列的 SPI 外设相对入门,并没有像我之前用过的某些高端芯片那样提供独立回环模式位。所以在这次实测中,我选择的是硬件回环:用杜邦线把 SPI1 的 MOSI 引脚和 MISO 引脚短接。这种方式最接近真实通信场景,因为数据真的经过了引脚电平变化,能顺带把 GPIO 驱动能力、引脚复用配置、输入路径上拉下拉影响都验证到。

我个人也建议在刚开始调 SPI 的时候优先用硬件回环,因为软件回环跳过了引脚和 PCB 走线路径,一旦之后接入真实设备出现问题,你反而分不清是外设配置问题还是硬件连接问题。先把硬件路径验证通了,后面排查范围会小很多。

1.3 测试目标的拆解

这次测试我给自己定了几个明确的验证目标,而不是简单地跑一遍发收就完事。

  • 验证 SPI1 外设时钟是否正常使能,寄存器读写是否生效。
  • 验证 GPIO 复用映射是否正确,MOSI、MISO、SCK 引脚是否被正确配置成外设功能。
  • 验证 SPI 主模式下的发送和接收路径是否同时工作。
  • 验证不同数据长度、不同时钟分频系数下,收发数据是否保持一致。
  • 验证 NSS 引脚在软件管理模式下不会干扰通信。

把这些目标列清楚之后,测试方案就不是“跑一下看结果”了,而是有意识地构造不同测试条件,逐步缩小潜在问题范围。后面我会详细讲测试数据是怎么设计的。

2. 硬件准备与环境搭建

2.1 板卡与接线清单

这次用的板卡是一块基于 STM32C092RC 的评估板,芯片属于 STM32C0 系列,Cortex-M0+ 内核,主频最高可以跑到 64MHz,定位是成本敏感型应用。它的 SPI 外设分布在标准引脚上,我用的是 SPI1,引脚映射如下:

信号引脚功能说明
SCKPA5SPI1 时钟输出
MISOPA6SPI1 主输入从输出
MOSIPA7SPI1 主输出从输入
NSSPA4软件管理,未使用硬件控制

接线就一句话:用一根杜邦线把 PA7 和 PA6 短接,也就是把 MOSI 和 MISO 连起来。除此之外,不需要连接任何从设备。为了方便调试,我还把串口1(USART1)通过 USB 转串口模块连到了电脑,用来打印测试结果,串口配置为 115200-8-N-1。

注意:杜邦线要选短一点的,线越短,寄生电容和线间串扰越小。回环测试频率不高时可能无所谓,但如果你把 SPI 时钟分频调得很高,长线回环很容易出现误码,到时候你排查半天发现是线的问题,就有点冤了。

2.2 软件环境与工具版本

开发环境方面,我这次用的是 STM32CubeMX 生成初始化代码,然后基于 HAL 库做二次开发。IDE 选择的是常见的 GCC 工具链配合命令行编译,当然你用其他集成 IDE 也一样,核心逻辑没区别。必装组件有两个:

  • STM32CubeMX:用来生成工程模板,自动配置时钟树、引脚复用和外设参数。
  • STM32C0 系列支持包:如果 CubeMX 里找不到芯片型号,记得先在软件中心安装对应的器件支持包。

STM32C092RC 的芯片型号,全称末尾是 RC 后缀,表示 Flash 容量 256KB,不过我这次只是跑回环测试,容量根本不是瓶颈。厂商配套的开发工具链和 HAL 库版本尽量保持最新,因为 C0 系列属于较新的产品线,早期固件库版本对某些外设的支持存在一些小问题,更新到新版本之后省心很多。

2.3 最小工程初始化步骤

不少初学者在 CubeMX 里会把一堆外设全都勾上,然后生成的代码又长又乱,出现问题根本不知道从哪里查。我这次反其道而行之,建立一个最小工程,只保留三个外设:SPI1、USART1、以及一个 GPIO 输出口用来点灯。

CubeMX 里的关键配置如下:

  • RCC:选择外部晶振(HSE),如果板子上没有贴晶振,就直接用 HSI,也就是芯片内部时钟源。
  • 时钟树:把系统时钟配置到 64MHz。需要注意的是,SPI 外设时钟来自 APB 总线,APB 分频系数不同,SPI 的时钟频率上限也会变化。
  • SPI1:配置为 Full-Duplex Master,也就是全双工主机模式。
  • USART1:异步模式,波特率 115200,用于打印测试信息。
  • GPIO:如果需要点灯,把一个 LED 引脚配置为输出模式。

这样生成的工程非常干净,后续增加逻辑的时候不会牵扯到无关外设,排查起来也轻松。

3. SPI 外设配置与核心代码实现

3.1 从寄存器角度理解 SPI 发送接收流程

开始写代码之前,我先把 SPI 外设的发送接收流程捋一遍。SPI 主机发送一个字节,本质上是把数据写进发送数据寄存器,移位寄存器会把数据一位一位地从 MOSI 引脚送出去,同时从 MISO 引脚采回一位数据。这个过程是同步的,所以主机发送一个字节的同时,必然也会接收一个字节。

理解了这一点,就明白为什么回环测试只需要调用一次发送接收函数,而不是先调发送、再调接收。很多人第一次写 SPI 代码时容易犯一个错:先调一次发送函数发数据,然后再调一次接收函数去收数据,结果发现收到的永远不是自己想要的内容。原因就是发送和接收在硬件层面本来就绑定在同一个时钟时序里,你应该把这两个动作当成一次数据传输的两个面来看待。

HAL 库提供的 HAL_SPI_TransmitReceive() 函数就是干这件事的:它同时把一个缓冲区里的数据发出去,同时把接收到的数据存到另一个缓冲区里。回环测试的代码核心就是它。

3.2 CubeMX 参数配置的最佳实践

SPI1 在 CubeMX 中的参数设置,我给出实际使用的配置项:

参数项配置值说明
ModeFull-Duplex Master全双工主机模式
Data Size8 Bits8 位数据帧
First BitMSB First高位先行
Prescaler16时钟分频系数
Clock Polarity (CPOL)Low空闲状态为低电平
Clock Phase (CPHA)1 Edge第一个边沿采样
NSSSoftware软件管理片选
NSS Signal TypeSoft不使用硬件 NSS 引脚

这里重点说说 Prescaler 的选择逻辑。APB1 时钟在 HSI 64MHz 配置下,如果 APB 分频系数为 1,那么 SPI 外设时钟就是 64MHz,16 分频之后得到 4MHz 的 SCK 频率。对回环测试来说,4MHz 是一个非常保守但足够用的速度,即使杜邦线接触不太稳定,也不容易因为信号质量导致误码。

如果你用的是更高的分频,比如 4 分频甚至 2 分频,SCK 频率一下提到 16MHz 或 32MHz,回环测试也能跑通,但对杜邦线的质量、引脚接触状态、板卡的走线寄生参数都会更敏感。第一次做回环验证,建议从低频开始,先把通路跑通,再逐步往上提。

CPOL 和 CPHA 的选择,在回环测试里其实主机自己发自己收,只要收发采样时刻和输出时刻错开一个合适的窗口,大概率能跑通。但为了以后接真实 SPI 设备时有参考基准,我建议从最常用的模式 0,也就是 CPOL=Low、CPHA=1 Edge 开始,这个模式兼容性最好。

3.3 发送接收代码:轮询方式实测最稳

回环测试不需要追求极致性能,所以我直接用阻塞方式调用 HAL 库函数。核心代码如下:

#define TEST_BUFFER_SIZE 16 uint8_t txBuffer[TEST_BUFFER_SIZE]; uint8_t rxBuffer[TEST_BUFFER_SIZE]; static void fillTxBuffer(uint8_t *buf, uint8_t len) { for (uint8_t i = 0; i < len; i++) { buf[i] = i + 0xA0; // 用非连续且有规律的填充值 } } static uint8_t compareBuffer(uint8_t *buf1, uint8_t *buf2, uint8_t len) { for (uint8_t i = 0; i < len; i++) { if (buf1[i] != buf2[i]) { return i; // 返回第一个不一致的位置 } } return 0xFF; // 全部一致 }

在主循环里,我做了多次不同条件的测试。每一次测试都重新填充发送缓冲区,调用 HAL_SPI_TransmitReceive(),然后比较接收缓冲区和发送缓冲区的内容,把比对结果通过串口打印出来。

void spi_loopback_test(void) { uint8_t ret; uint8_t mismatchIndex; fillTxBuffer(txBuffer, TEST_BUFFER_SIZE); memset(rxBuffer, 0, TEST_BUFFER_SIZE); ret = HAL_SPI_TransmitReceive(&hspi1, txBuffer, rxBuffer, TEST_BUFFER_SIZE, 1000); if (ret != HAL_OK) { printf("SPI TransmitReceive Error: %d\n", ret); return; } mismatchIndex = compareBuffer(txBuffer, rxBuffer, TEST_BUFFER_SIZE); if (mismatchIndex == 0xFF) { printf("Loopback Test PASS, %d bytes matched\n", TEST_BUFFER_SIZE); } else { printf("Loopback Test FAIL, first mismatch at index %d\n", mismatchIndex); } }

这里有几个细节值得你注意。第一,发送缓冲区的数据不要全填一样的值,比如全填 0x55 或者 0xAA,虽然这种特殊值也有意义,但一旦接线反了或者采样边沿错了,你很难从错误数据中判断出问题类型。用递增序列 i + 0xA0,既有规律性,又能精确到字节位置地定位错误。

第二,调用 HAL_SPI_TransmitReceive() 之前,接收缓冲区一定要先清零。不要想当然地认为库函数会自动清空接收区,实际上它只是把收到的数据往缓冲区里写,之前残留的脏数据如果不处理,一旦通信超时返回错误,你查看接收缓冲区时会读到一堆不明来历的数据,干扰判断。

第三,超时时间设成 1000 毫秒。轮询方式下,16 个字节在 4MHz 时钟下瞬间就传完了,正常情况根本用不了 1 毫秒。如果 SPI 外设配置有问题,函数会一直卡在等待标志位,超时机制能帮你跳出死等状态。

3.4 主频与 SPI 时钟分频的关系推导

这里再展开讲讲时钟树配置对 SPI 实际波特率的影响,因为很多人直接在 CubeMX 里改了分频系数,却不知道 SCK 的实际频率最终由 APB 总线时钟决定。

在 STM32C092RC 上,系统时钟配置为 64MHz。HAL_RCC_GetPCLK1Freq() 返回的是 APB1 总线时钟频率。如果 APB1 分频器设置的是 1 分频,那 PCLK1 就是 64MHz,SPI 内核时钟也是 64MHz,16 分频后 SCK 就是 4MHz。计算公式很简单:

SCK = PCLK1 / Prescaler SCK = 64MHz / 16 = 4MHz

如果我把 APB1 分频设为 2,PCLK1 变成 32MHz,那同样的 16 分频,SCK 就只剩 2MHz 了。实测过程中我特意对比了这两种配置,发现 SCK 频率确实跟着变化,回环数据也都能正确收发。这说明一个道理:只看 CubeMX 界面里 Prescaler 的数字是不靠谱的,最终频率必须结合 APB 分频一起算。

这个知识点在后续接真实外设时尤其重要。比如你接一个最高支持 10MHz 的 SPI 屏幕,从 CubeMX 里看到 16 分频以为只有 4MHz,没细算 APB 分频就放心用了,结果实际 SCK 跑到 8MHz 甚至更高,屏幕显示乱码,那时候排查起来就很痛苦。

4. 实测运行:从接线到结果分析

4.1 实测操作记录

硬件连线确认无误后,我给板卡上电,打开串口终端,复位开发板,观察输出。第一次跑回环测试,串口打印结果如下:

SPI Loopback Test Start System Clock: 64MHz APB1 Clock: 64MHz SPI1 SCK Frequency: 4MHz Loopback Test PASS, 16 bytes matched

看到 PASS 的结果,心里那块石头算是落了地。不过我没有就此打住,而是接续做了几组变条件测试,把分频系数从 16 调整到 8,再把 CPOL 和 CPHA 组合成 SPI Mode 0、Mode 1、Mode 2、Mode 3 分别跑了一遍。

为了不反复改代码重新编译,我直接把 SPI 参数做成运行时可调整的结构体,调用 HAL_SPI_Init() 重新初始化外设。每次变更参数后,重新执行一次发送接收测试,并打印当前模式信息。这样就能在一个循环里把所有模式全部验证完。

实测下来的结果很有代表性:

测试条件发送字节数匹配结果备注
Mode 0, 16分频16全部匹配通过
Mode 1, 16分频16全部匹配通过
Mode 2, 16分频16全部匹配通过
Mode 3, 16分频16全部匹配通过
Mode 0, 8分频16全部匹配通过
Mode 0, 4分频16全部匹配通过
Mode 0, 2分频16部分位置读回数据异常需排查

前三种模式全部通过,说明这个芯片的 SPI 外设本身没什么问题,引脚复用的通路也是通的。真正有趣的是后面的高压测试:当分频系数降到 2,SCK 频率提高到 32MHz 时,回环数据开始出现偶发错误。这个结果完全符合预期,因为杜邦线回路在这个频率下信号完整性已经不够了,但我恰恰建议你亲手试一次这个操作,因为“高频误码”的直观感受,会让你在以后 PCB 布局时对 SPI 走线长度和阻抗匹配更加敏感。

4.2 测试日志的解读方法

如果你做回环测试时打印的日志是 FAIL,不要急着看代码,先看日志输出格式。我把我的打印格式设计成了“第一个不一致位置 + 收发具体值”的格式,这样能直接定位到出错字节。

改进后的比对输出大概是这样的:

Loopback Test FAIL, first mismatch at index 3 Tx[3] = 0xA3 Rx[3] = 0xA5

从 Tx 和 Rx 的差异就能猜出问题的大致方向:如果 Rx 的数据比 Tx 的数据“移位”了几个字节,说明接收采样时序有问题;如果 Rx 收到的全是 0x00,说明 MISO 引脚根本没有接收到有效电平;如果 Rx 的数据像是二进制反转,说明 MSB/LSB 配置不一致;如果所有字节都对,只有个别字节错,那大概率是信号完整性问题。

这种分层解读日志的方法,比直接看 PASS/FAIL 两个大字有意义得多。

5. 常见问题与排查经验

5.1 典型问题速查表

回环测试虽然简单,但我在不同板子、不同工程上都见过各种奇怪的失败现象。这里把最常见的问题整理成一张速查表:

现象可能原因排查方向
接收缓冲区全为 0x00MOSI 和 MISO 没接好,或 GPIO 复用配置错误检查杜邦线连接,检查 CubeMX 引脚映射,用万用表量通断
接收数据等于发送数据但整体左移或右移一位CPHA 配置错误,采样边沿不对切换 SPI Mode 0 到 Mode 3 分别测试
接收数据出现固定 bit 翻转MSB/LSB First 配置错误检查 First Bit 配置
接收缓冲区出现上一次残留数据接收缓冲区没清零,超时返回在调用传输函数前置零接收区
传输函数一直返回 HAL_BUSYSPI 总线忙标志未清除检查 NSS 配置,软件管理模式下是否有其他外设占用引脚
高频下偶发错误杜邦线过长、接触不良降低分频系数,更换短导线
程序跑飞或进入 HardFaultSPI 引脚配置冲突检查 GPIO 是否被其他外设复用

5.2 我最想强调的两个排查细节

第一个细节,用万用表蜂鸣挡量一下杜邦线两端是否真正导通。听起来很基础,但确实有人把线插到了相邻的排针孔位,看起来接上了,实际信号根本没通。回环测试跑不通,排查半天发现是线插歪了,这种感觉真的不好受。

第二个细节,检查 CubeMX 生成的 GPIO 初始化代码。SPI 的 MOSI 和 SCK 引脚需要配置为复用推挽输出,MISO 引脚配置为复用输入。有些版本的 CubeMX 在引脚冲突时会悄悄把某个引脚改回 GPIO 模式,你如果不看初始化代码,很难发现实际生效的配置和你计划的不一样。

// 检查生成的代码是否类似这样 GPIO_InitStruct.Pin = GPIO_PIN_5 | GPIO_PIN_7; GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); GPIO_InitStruct.Pin = GPIO_PIN_6; GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; // MISO 同样配置为复用功能 HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

注意 MISO 引脚配置为复用推挽输出是正常现象,因为 MISO 引脚在芯片内部也是由 SPI 外设驱动的,只是方向控制由外设自己管理。

6. 从回环测试到真实外设:后续扩展方向

6.1 用回环测试验证 DMA 传输路径

SPI 回环测试跑通之后,很多人的第一反应是“任务完成”,然后直接把 SPI 接到真实传感器或者屏幕上,开始调业务逻辑。但我的习惯是再做一步进阶测试,那就是把轮询方式改成 DMA 方式,再做一次回环。

DMA 回环测试能验证的,不只是 SPI 外设本身,还包括 DMA 控制器、中断优先级、内存缓冲区的地址对齐等更深层次的问题。回环测试的数据路径从 CPU 寄存器变成了内存缓冲区,如果 DMA 配置有误,比如数据长度参数不对、外设地址写错,测试立刻就会暴露问题。相比直接接真实外设,回环测试的排查面更小,定位速度更快,我强烈建议你在接真实外设之前,先花十分钟把 DMA 回环跑一遍。

6.2 扩展:利用回环测试自动校准 SPI 时序

还有一个更有意思的玩法,就是写一个自动扫描函数,让程序自动遍历所有 CPOL/CPHA 组合、多个分频系数,每轮都做回环测试和比对,最后打印出一份“当前环境下的最优 SPI 参数表”。

这种方式在我调试某些对时序敏感的 SPI 传感器时非常有用。不同传感器对 SPI 模式的支持不同,你以为手册上写的是 Mode 0,实际跑起来可能 Mode 1 更稳定。用回环测试先摸清主控端在不同参数组合下的稳定性,再结合传感器的需求去匹配,调试效率会提高很多。

void spi_parameter_sweep_test(void) { static const uint8_t prescalers[] = { SPI_BAUDRATEPRESCALER_16, SPI_BAUDRATEPRESCALER_8, SPI_BAUDRATEPRESCALER_4, SPI_BAUDRATEPRESCALER_2 }; for (uint8_t mode = 0; mode < 4; mode++) { for (uint8_t i = 0; i < sizeof(prescalers); i++) { // 更新 hspi1.Init 中的 CLKPolarity、CLKPhase、BaudRatePrescaler // 调用 HAL_SPI_Init(); // 执行 spi_loopback_test(); } } }

回环测试的最大价值,在于它是一个“可重复、可量化、可自动化”的验证工具。你不需要依赖任何外部设备,就能在最短时间内确认 MCU 的 SPI 外设是否被正确配置,这为后续接真实外设建立了一个可靠的信任基线。我在实际测试中的体会是,与其拿到一块新板子先急匆匆写业务代码,不如花半小时把 SPI、I2C、UART 这些基础外设全部用回环方式测一遍,把硬件和软件配置的底子打牢,后面开发会顺畅得多。最后再分享一个小技巧:做回环测试时,把测试代码封装成一个独立函数,并保留自动扫描模式,以后每换一块新板子、每移植一次工程,都能拿来即用,这比每次从零搭测试工程要高效得多。

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

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

立即咨询