
最近在 STM32MP257F-DK 上调 SPI 通信的时候遇到一个很典型但不好查的问题SPI 的 CS 片选信号每次传输都能正常翻转但逻辑分析仪挂在 SCK 时钟引脚上从头到尾一条脉冲都没有。更让人头大的是RCC 里 SPI 时钟明明在跑ETZPC 防火墙也确认开放了该外设的访问权限。所有常规检查项都是绿的SCK 就是不工作。这个现象其实非常有价值。CS 能翻转说明“发起传输”这个动作本身是成功的——要么是 SPI 控制器进入了传输流程主动拉低了硬件 NSS要么是某个 GPIO 被软件正确拉低了。可 SCK 作为主模式的核心输出信号完全死寂问题几乎可以锁定在 SCK 输出链条上的某个特定环节而不是整个 SPI 外设瘫痪。顺着这个思路往下查比从头把所有配置推倒重来快得多。下面把我这次在 STM32MP257F-DK 上的完整排查过程和解决方案整理出来给同样踩坑的朋友一个参考。1. 先搞懂 STM32MP257F-DK 上 SPI 的“放行链路”1.1 CS 翻转到底能说明什么不要急着改代码先把已有现象的价值榨干。CS 翻转这个事实在 SPI 系统里至少能证明三件事。第一主控端确实在尝试发起传输。无论是对/dev/spidevX.Y发起了一次 read/write还是裸机代码里对 SPI 的 TX FIFO 写了一笔CS 电平的变化都说明传输触发逻辑已经走到了“拉低片选”这一步。第二CS 引脚的 GPIO 控制器和基础时钟是通的。CS 翻转不是凭空来的如果 GPIO 的 PCLK 没开、引脚模式配置错误、输出驱动没使能CS 根本不会动。所以至少这个引脚的底层链路是好的。第三如果 CS 用的是 SPI 控制器的硬件 NSS也就是片选引脚复用到了 SPI 的 NSS 功能上那说明 SPI 控制器的寄存器写入至少在部分上是生效的状态机没有被完全锁死。这点很重要。硬件 NSS 的输出由 SPI 控制器的传输逻辑驱动它能在 CS 上产生时序说明控制器内部已经进入了工作状态。可 SCK 完全没动静这就不太可能是“软件忘了发传输”这种低级问题而应该往时钟源、引脚复用、权限隔离这些偏硬件链路的方向去找。1.2 STM32MP2 的 SPI 和普通 MCU 上的 SPI 不是一回事用过 STM32F4、STM32H7 的朋友对 SPI 的套路很熟RCC 打开外设时钟GPIO 配置成复用功能然后 HAL_SPI_Init 就能干活了。但在 STM32MP257F 上这套惯性思维会坑人。STM32MP257F 是双核 Cortex-A35 加单核 Cortex-M33 的异构处理器。SPI 外设可以挂在 A35 侧的 Linux 下也可以挂在 M33 侧的裸机或 RTOS 下还可能是两侧共享。为了让这种共享安全可控SoC 里加了两层普通 MCU 没有的关卡。第一层是 ETZPCExtended TrustZone Protection Controller负责把外设资源分配给 secure 域或 non-secure 域。Linux 通常跑在 non-secureM33 的 RTOS 或裸机可能跑在 secure。如果 SPI 被 ETZPC 配置成了 secure-onlyLinux 侧去访问 SPI 寄存器时总线事务会被硬件静默拦截。所谓“静默”就是你写寄存器读回来像是成功了但实际是影子值硬件根本不认。第二层是 RCC 内部的时钟门控和复位控制也被纳入了 TrustZone 管控。某些时钟使能位、复位释放位只允许 secure 域写。如果安全配置没放开Linux 驱动去clk_prepare_enable时可能得到一个已使能的假象但硬件时钟树里那个节点终究没通。这两层拦截有一个共同特点它们都不影响 GPIO。CS 如果是普通 GPIO完全不受 ETZPC 和 RCC 安全位的限制照常翻转。于是就会形成“CS 活得很好SCK 完全没信号”的割裂现象。这就是为什么光看防火墙开放、时钟在跑问题依然存在。1.3 为什么“防火墙打开 时钟运行”仍然不够在 STM32MP2 上“防火墙打开”不是一个全局开关。以 SPI2 为例你需要在多个配置层面同时放行它才能真正属于当前运行域。Linux 设备树中的访问控制属性比如access-controllers etzpc 对应ID。OP-TEE 编译时内置的设备树。OP-TEE 启动时会用自己的安全策略覆盖外设分配如果你只改了 Linux DTS 而没有同步 OP-TEE 的配置启动后 SPI 照样被锁在 secure 域。RCC 中对应的时钟使能位是否被安全域锁定。如果有运行时重新配置的脚本或者 U-Boot 环境变量覆盖了原有设置那还要把这一层也考虑进来。同样“时钟在跑”也要看是哪一级在跑。SPI 的 APB 接口时钟只是用于寄存器读写SCK 的产生依赖的是 SPI 内核时钟Kernel Clock。如果内核时钟没有被正确选择、PLL 没有锁定、分频链路配置错误寄存器读着正常SCK 也照样出不来。2. SCK 输出链条从控制器内部到引脚2.1 主模式是 SCK 输出的大前提SCK 只有在 SPI 配置为主模式时才会由控制器主动驱动。如果 CR1 或 CFG 寄存器里的 MSTR 位是 0SPI 工作于从模式SCK 引脚作为输入存在自然不会有任何时钟输出。这个原因听着很蠢但真的经常发生。裸机初始化时 HAL 结构体里的 Mode 字段写成了SPI_MODE_SLAVE或者初始化函数返回错误后配置被回滚都会造成这个结果。在 Linux 设备树里如果 SPI 控制器的状态不是okay或者驱动 probe 失败SPI 可能根本没有被正确初始化但 CS 如果被一个独立的 GPIO 控制它依然会翻转。判断方法很简单。如果是裸机读一下 SPI 控制寄存器确认 MSTR 位是否为 1。如果是 Linux先确认/dev/spidevX.Y设备节点是否存在以及/sys/class/spi_master/下有没有对应的 master。节点不存在说明驱动就没绑定成功后面的一切都无从谈起。2.2 内核时钟和 APB 接口时钟两个都要活着SPI 外设其实有两个时钟域。一个是接口时钟也叫 PCLK用于访问 SPI 的寄存器另一个是内核时钟也叫 KCLK用于驱动 SCK 产生、移位寄存器等功能。这两个时钟在某些 MCU 上绑在一起一个使能就都通但在 STM32MP2 这样复杂的时钟树上它们可能来自不同的 PLL 或分频器。如果只开了接口时钟寄存器读起来完全正常但 SCK 生成逻辑没有任何时钟源一个脉冲都出不来。在 HAL 代码里除了__HAL_RCC_SPI2_CLK_ENABLE()使能接口时钟外还需要检查时钟选择寄存器的配置比如__HAL_RCC_SPI2_CONFIG()选择正确的内核时钟源。有些移植过来的参考代码会漏掉这一句或者把时钟源选到了一个 PLL 输出无效的分支上。Linux 下确认这个状态很直观。在目标板上执行cat /sys/kernel/debug/clk/clk_summary | grep spi如果spi2_k这一行显示enable_count为 0或者频率为 0那么这个时钟链路就没通。正常工作的 SPI 节点enable_count至少是 1频率应该落在合理范围内。2.3 ETZPC 的“部分放行”才是最隐蔽的坑ETZPC 的保护粒度可以精确到单个外设实例。ST 的参考实现里每个外设节点在设备树中通过一个 ID 关联到 ETZPC 的配置项。这个机制本身不复杂复杂的是它会跟 OP-TEE 的启动策略交互。很多时候你改了 Linux 设备树觉得防火墙已经打开了。但 OP-TEE 启动时读的是它自己内置的 device tree 和安全策略不会去读 Linux 那份 DTS。如果 OP-TEE 里的配置还是老的启动时就会把你的新设置覆盖掉。于是你看到的现象就是设备树明明改成了 non-secure编译也通过了但运行时 SPI 寄存器就是写不进去。这种“配置看起来对实际运行不对”的问题最有效的验证手段是读 ETZPC 寄存器实际值而不是看源代码。在 Linux 下用 devmem 读取对应寄存器换算一下安全位。如果显示 secure那就是 OP-TEE 或 U-Boot 在启动时锁死了外设必须去改那部分配置。2.4 引脚复用AF 编号错了就是全错SPI 的 SCK 引脚必须配置为复用功能Alternate Function而且 AF 编号必须完全匹配该 SoC 的复用表。注意CS 可以用普通 GPIO但 SCK 一般不能因为它需要由 SPI 外设直接驱动输出时钟。这里就出现一个非常常见的错误复制了某一个 SPI 实例的 pinctrl 配置但忘了改 AF 编号。或者 SCK 引脚被配置成了输出模式但没选 AF而是普通的 GPIO 输出。这种配置下CS 如果由另一个 GPIO 驱动它照常翻转而 SCK 引脚因为没有被正确映射到 SPI 外设只能是静默状态。在 Linux 下排查 pinctrl可以用这个命令cat /sys/kernel/debug/pinctrl/pinctrl-handles正常绑定 SPI 后SCK 引脚应该处于alt function状态并且显示的 AF 编号与 datasheet 一致。如果显示的是gpio状态说明 pinctrl 配置没有生效。2.5 异步时钟模式下的分频问题STM32MP2 的 SPI 支持异步时钟模式SCK 频率由 SPI 内核时钟和配置寄存器里的分频系数共同决定。如果分频系数配置异常比如内核时钟被分到极低频率示波器上会看到一条“看似没有”的平线但实际延长捕获时间后可能能看到极慢的时钟脉冲。这个坑很容易被忽略。很多人把示波器时基设在微秒级SCK 实际只有几 kHz 甚至几百 Hz一个脉冲都看不到自然判断为“没有时钟”。所以排查 SCK 是否真的没有输出之前先把示波器时基拉长到毫秒级或者用逻辑分析仪长时间采样然后再下结论。另外在异步模式下SPI 的分频系数不是随便给的。STM32 系列 SPI 的波特率分频寄存器是 3 位对应 2、4、8、16、32、64、128、256 分频。如果你配置的分频值让 SCK 频率超出了从机支持的极限即便波形存在通信也完全异常。这种问题一般不会表现为“SCK 完全平”但容易被误判为链路问题。3. 完整排查过程实录3.1 第一步确认 CS 到底是硬件片选还是软件 GPIO我先检查了板级 DTS 中的 cs-gpios 定义。如果节点里有cs-gpios属性那 CS 就是普通 GPIO 控制的软件片选如果没有而 SPI 配置了SPI_NSS_HARD那 CS 才是 SPI 控制器输出的硬件片选。这一步非常关键直接决定排查方向。如果 CS 是硬件 NSS那 SPI 控制器已经进入了传输流程问题偏向时钟链路。如果 CS 是 GPIO 软件片选那 CS 翻转只能证明 GPIO 是好的与 SPI 是否真正工作无关。问题可能更大范围地存在于 SPI 初始化失败或者外设权限被锁。我的这次案例里CS 被设计成了 GPIO 软件片选所以“CS 翻转”这个现象其实只能指出 GPIO 链路正常无法证明 SPI 已经发起传输。排查范围一下子拓宽了SPI 控制器可能根本没动。3.2 第二步读寄存器验证 MSTR 和 SPE 的真实状态在 Linux 下用 devmem 读取 API 也没有问题但首先得知道 SPI 外设的基地址和相关寄存器偏移。更稳妥的方式是通过调试器连接 JTAG 直接读但 devmem 在目标板上更快。我先把 SPI 外设配置成大致的预期状态然后读取配置寄存器确认 MSTR 是否为 1、SPE 是否为 1、TX 相关位是否配置正确。如果读回来的值跟我写入的不一致尤其是那些关键位写 1 读 0那基本可以断定寄存器访问被硬件拦截了。这次的检查结果很微妙——MSTR 和 SPE 都读到了 1说明寄存器访问本身没有被 ETZPC 拦住SPI 控制器确实处于主模式且已使能。既然控制器的核心状态是正确的那问题就不在权限层而更可能在内核时钟源、分频链路或引脚复用。3.3 第三步检查 SPI 内核时钟的使能和频率这个步骤是这次排查的分水岭。在 Linux 里执行cat /sys/kernel/debug/clk/clk_summary | grep -i spi2一开始我注意到 SPI2 的接口时钟和内核时钟都在 Clk Summary 列表里enable_count 为 1频率也有显示。看起来的确“时钟在跑”。但随后我仔细观察发现这个 enable_count 是 Linux clk 框架计数它只能说明 Linux 在时钟树层面成功调用了clk_enable并不能完全替代硬件寄存器层面的确认。于是用 devmem 手动读取 RCC 中 SPI2 内核时钟选择的分频控制位结果发现时钟源选择被写到了一个 PLL 输出异常的分支。这就解释了为什么 Clk Summary 显示频率存在但实际 SCK 完全无输出——因为那个 PLL 分支在硬件上根本没有有效的时钟信号。3.4 第四步回头检查 pinctrl 的 AF 配置虽然前面已经定位到时钟链路但 pinctrl 还是要确认否则修完时钟还得返工。这次重新核对了 SCK 引脚在设备树 pinctrl 节点里的 AF 编号发现果然是复制了另一个 SPI 实例的节点AF 编号对不上导致 SCK 引脚没有连到 SPI 外设上。这解释了为什么现象具有迷惑性。SCK 的 pinctrl 是错的但设备树编译不报错因为引脚定义本身是合法的只是 AF 功能不对。而 CS 用的是 GPIO 功能完全不受影响。3.5 第五步硬件链路验证软件配置基本确认后我又用示波器表笔直接点在 STM32MP257F-DK 的 SPI 扩展接口上确认 PCB 走线没有断开、扩展板上没有其他器件把 SCK 拉低。一些开发板的扩展接口会有跳线或默认的负载电阻如果跳线帽没插好SCK 信号根本到不了你测量点但你以为测的是 SCK。这一步虽然简单但偶尔就是会出现这种低级问题。4. 定位后的修复方案与代码调整4.1 Linux 设备树时钟源、pinctrl、访问控制一个都不能少修复的第一步是纠正 SPI2 节点里的内核时钟源配置。以设备树为例SPI 节点应该显式指定内核时钟并确保时钟源有效spi2 { pinctrl-names default, sleep; pinctrl-0 spi2_pins_a; pinctrl-1 spi2_sleep_pins_a; access-controllers etzpc ETZPC_SPI2_ID; clocks rcc SPI2_K, rcc SPI2_PCLK; clock-names kclk, pclk; status okay; spidev0 { compatible rohm,dh2228fv; reg 0; spi-max-frequency 1000000; }; };注意几个容易错的点。assigned-clock-rates如果没配内核会使用设备树里默认的频率但这个默认频率可能依赖一个不存在的 PLL 配置。最好在时钟控制器节点里确认 SPI2_K 的时钟源比如rcc { assigned-clocks rcc SPI2_K; assigned-clock-parents rcc PLL2_P; assigned-clock-rates 100000000; };这样 spi2 内核时钟就明确绑到了 PLL2_P并且频率被强制设定为 100 MHz后续 SPI 分频配置就有了一个已知的基准。pinctrl 节点也同步修正。以spi2_pins_a为例确保 SCK/MOSI/MISO 都使用了正确的 AF 编号spi2_pins_a: spi2-0 { pins1 { pinmux STM32PINMUX(I, 1, AF5), /* SPI2_SCK */ STM32PINMUX(I, 2, AF5), /* SPI2_MOSI */ STM32PINMUX(I, 3, AF5); /* SPI2_MISO */ bias-disable; drive-push-pull; slew-rate 1; }; };修改 DTS 后重新编译并烧录设备树。需要特别提醒的是不要只替换 Linux 的 DTB。如果板卡使用 OP-TEE必须同步确认 OP-TEE 的 DTS 中 SPI2 的访问控制也被设置为 non-secure否则启动后外设权限被覆盖改成什么都没用。4.2 裸机 M33 侧HAL 库初始化的正确顺序如果你是在 M33 核上跑裸机或 RTOS初始化 SPI 时的HAL流程如下/* 1. 使能 SPI2 接口时钟 */ __HAL_RCC_SPI2_CLK_ENABLE(); /* 2. 选择 SPI2 内核时钟源这一步最容易被忽略 */ __HAL_RCC_SPI2_CONFIG(RCC_SPI2CLKSOURCE_PLL2P); /* 3. 配置 GPIO 复用 */ __HAL_RCC_GPIOI_CLK_ENABLE(); GPIO_InitTypeDef GPIO_InitStruct {0}; GPIO_InitStruct.Pin GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3; GPIO_InitStruct.Mode GPIO_MODE_AF_PP; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_VERY_HIGH; GPIO_InitStruct.Alternate GPIO_AF5_SPI2; HAL_GPIO_Init(GPIOI, GPIO_InitStruct); /* 4. 配置 SPI 外设 */ hspi2.Instance SPI2; hspi2.Init.Mode SPI_MODE_MASTER; hspi2.Init.Direction SPI_DIRECTION_2LINES; hspi2.Init.DataSize SPI_DATASIZE_8BIT