这问题我太熟了。上个月用 NUCLEO-H7S3L8 调试一块外置 ADC,CubeMX 里把 SPI3 的 MISO 指定到 PC11,代码生成、接线、上电,结果 HAL_SPI_TransmitReceive 读回来的缓冲区永远是一排 0xFF。示波器戳上去,PC11 引脚上明明有波形,MISO 线上也有数据在跳,可就是进不了 SPI3 的接收寄存器。这种"信号看着有、数据读不到"的现象,十有八九不是芯片坏了,而是 PC11 这个引脚的复用功能(AF)根本没配到 SPI3 外设上去。
如果你也在 NUCLEO-H7S3L8 上用 SPI3,并且恰好把 MISO 放在 PC11,这篇文章值得看完。我会把从现象、接线、波形、寄存器验证,到最终根因和修复的完整链路拆开讲,顺带把我在 H7 系列上踩过的几个复用功能相关的坑一并列出来。无论你是刚上手 STM32H7 的新手,还是从 F4 老工程迁过来的老手,这套排查思路都能直接抄。
1. 一段真实翻车记录:SPI3 在 PC11 上收到的全是 0xFF
先还原一下我当时的场景。板子是 NUCLEO-H7S3L8,主控是 STM32H7S3L8H6,外接一个 SPI 接口的 16 位 ADC。CubeMX 里 SPI3 配置为全双工主机模式,引脚是这样分配的:
| 信号 | 引脚 | 说明 |
|---|---|---|
| SPI3_SCK | PC10 | 时钟输出 |
| SPI3_MISO | PC11 | 主机数据输入 |
| SPI3_MOSI | PC12 | 主机数据输出 |
| CS | PD14 | 软件控制的片选,普通 GPIO 输出 |
参数这边,8 位数据、MSB First、CPOL=0、CPHA=0,波特率先压到 1MHz,软件 NSS。生成工程后,我在主循环里做了一件事:拉低 CS,发一个读寄存器命令,然后收两个字节,拉高 CS。代码逻辑很简单,但串口打印出来的结果就是0xFF 0xFF,而且还挺稳定,每次都是0xFF 0xFF。
很多人看到 0xFF 的第一反应是"外部器件没工作"。确实,SPI 总线上的 MISO 在没人驱动时,会被上拉电阻或者接收端内部上拉拉高,读回 0xFF 是常态。也就是说,从设备可能根本没把数据送到 MISO 线上,或者 MISO 线上的数据根本没有进入 MCU 的 SPI3 外设。这两种情况,现象完全一样,但排查路径完全不同。
我那会儿先换了 ADC 模块、换了杜邦线、把波特率降到了 125kHz,折腾了半天,问题依旧。后来冷静下来,把排查思路从"怀疑外部器件"切换回"怀疑 MCU 引脚配置",才算真正开始走向根因。
这里有个非常容易误导人的点:如果你用示波器或者逻辑分析仪去点 PC11,往往能看到外部器件正在输出的数据波形。很多人在这一步就懵了——明明有波形,为什么寄存器里没有?原因在于,引脚有外部电平变化,只代表 GPIO 的输入通路是通的;但 SPI 外设能不能读到这个电平,取决于引脚是否被正确切换到"复用功能"模式,并且复用编号选中了 SPI3_MISO。这两个链路是独立的。这个认知,是这次排查里最重要的一课。
2. 把 PC11 从上电状态到 SPI3_MISO 的全链路拿出来看
既然怀疑 PC11 的配置,那就别在 HAL 库的封装里瞎猜了,直接从上电后的默认状态开始,一级一级往下查。
2.1 先做环回测试:把 MOSI 和 MISO 短接
排查外设问题,我向来是先做环回测试。这里很简单:拿一根短导线,把 PC12(SPI3_MOSI)和 PC11(SPI3_MISO)直接短接,然后发一个已知字节,看能不能原样收回来。
uint8_t txData = 0xA5; uint8_t rxData = 0; HAL_GPIO_WritePin(GPIOD, GPIO_PIN_14, GPIO_PIN_RESET); HAL_SPI_TransmitReceive(&hspi3, &txData, &rxData, 1, 100); HAL_GPIO_WritePin(GPIOD, GPIO_PIN_14, GPIO_PIN_SET); if (rxData == 0xA5) { // 环回成功:SPI3 外设和引脚配置基本没问题 } else { // 环回失败:需要继续查引脚复用/时钟/外设配置 }我那次执行完,rxData依然是0xFF,说明 SPI3 的接收链路在 MCU 内部就没打通。这基本上把问题锁定在"引脚复用配置"或者"外设时钟/参数"上,而不是外部器件的问题。
环回测试在 SPI 调试里是最高性价比的操作,没有之一。它能帮你把"MCU 内部外设链路"和"外部器件/接线"彻底切开。如果你一上来就对着外部传感器查手册查时序,大概率是在错误的方向上浪费时间。
2.2 示波器/逻辑分析仪的第二次验证
环回失败后,我把逻辑分析仪接上。观察到的波形很有迷惑性:
- SCK(PC10):时钟正常,频率和预分频设置一致;
- MOSI(PC12):发出的 0xA5 字节波形正确;
- MISO(PC11):在 MOSI 发送期间有跳动,但是一种"弱驱动"的随机电平,不是干净的方波。
注意这个细节。MISO 引脚上的波形"模糊"且不稳定,不是外部器件那种干净的推挽输出,这说明引脚处于高阻输入状态,电平是浮空的。要是引脚被正确配置为复用功能,环回测试时 MISO 会经过内部线路直接收到 MOSI 的信号,波形应该干净、与 MOSI 完全一致。波形"脏",说明内部通路没建立起来。
2.3 GPIO 和复用寄存器逐位解读
到这里,答案其实已经摆在寄存器里了。STM32 每个 GPIO 引脚都有几个关键寄存器位:
MODER:每引脚 2 位,决定输入(00)、输出(01)、复用(10)、模拟(11);AFR[1]:每引脚 4 位,决定具体复用编号(AF0~AF15);PUPDR:每引脚 2 位,决定上拉/下拉状态。
PC11 是端口 C 的第 11 个引脚,属于"下半区"(8~15),所以它的复用编号在AFR[1]里。我在调试代码里加了一段寄存器回读:
uint32_t moder = GPIOC->MODER; uint32_t afr1 = GPIOC->AFR[1]; uint32_t pupdr = GPIOC->PUPDR; uint32_t mode11 = (moder >> 22) & 0x3; // PC11 偏移 11*2=22 uint32_t af11 = (afr1 >> 12) & 0xF; // PC11 在 AFR[1] 中偏移 (11-8)*4=12通过调试器看这几个变量值,mode11是 0(输入模式),af11是 0(AF0)。这就有意思了——PC11 既不在复用模式下,也没选 SPI3 的复用编号,等于一个普通的浮空输入引脚。外部器件送来的数据虽然能让引脚电平变化,但 SPI3 外设根本没有和这个引脚建立内部连接。
2.4 SPI3 外设时钟和 GPIO 时钟是否打开过
顺带把时钟也查了。SPI3 在 STM32H7 系列上挂在 APB1 总线上,初始化时必须先开外设时钟,同时开对应 GPIO 端口时钟。我当时的代码是后来手写补进去的,检查了__HAL_RCC_SPI3_CLK_ENABLE()和__HAL_RCC_GPIOC_CLK_ENABLE()确实都执行了。如果你的外设时钟没开,SPI3 寄存器读出来全是复位值,HAL_SPI_TransmitReceive会一直超时。这次虽然不是时钟问题,但在完整排查里它必须被验证到。
3. 根因确认:H7S3 的复用功能表里 PC11 的 AF 编号被"平移"了
排查到这,问题已经很明确了:PC11 没有被设为正确的复用功能。但为什么 CubeMX 生成的工程还会出这种低级问题?我得说,这次还真不是 CubeMX 的锅,是我自己的过渡操作埋的雷。
3.1 我从老工程复制了 F4 的初始化代码
说来惭愧,我当时手头有一份以前在 STM32F4 上跑通的 SPI 初始化代码,几乎一样的引脚布局:PC10、PC11、PC12。因为赶时间,我直接把HAL_SPI_MspInit里的 GPIO 初始化部分复制到了 H7 工程里,只改了外设句柄,没有仔细核对复用编号。
结果就是:F4 工程里我写的是GPIO_AF5_SPI3,到了 STM32H7S3 上,PC11 的 AF5 对应的根本就不是 SPI3_MISO,而是另一个外设功能。H7 系列在不少引脚的复用编号上确实和 F4 不一样,翻数据手册的"Alternate function mapping"表,PC11 这一行,SPI3_MISO 对应的是AF6,也就是 HAL 里的GPIO_AF6_SPI3。
| 系列 | PC11 上的 SPI3_MISO 复用编号 | 备注 |
|---|---|---|
| STM32F4 | 不同参考手册中常写成 AF5 | 老工程里常见的写法 |
| STM32H7 | AF6 | H7 系列的复用编号和 F4 并不镜像 |
| STM32H7S3 | AF6(GPIO_AF6_SPI3) | 以官方数据手册为准 |
这个"平移"就是最大的坑。网上很多帖子里的代码是 F4 时代的,直接抄到 H7 上,十有八九会翻车。你看起来初始化代码"该写的都写了",实际上 AF 编号选错了,引脚的内部通路根本没连到 SPI3。
3.2 PC11 上还有哪些"隐形占用"
除了 AF 编号错位,PC11 上的"隐形占用