调试一块 STM32MP257F_EV1 评估板时,我碰到一个挺让人头疼的问题:SPI3 已经按从机模式配置好了,NSS 引脚用的是 PB1,主机那边片选信号也确实拉低了,但从机就是不理人。SPI 状态寄存器里 RXNE 不置位,中断不触发,MISO 保持高阻一样的平静。标题里那句“NSS PIN (PB1) Fails to claim despite correct configuration”说的就是这种状态——从软件配置上看一切正常,但硬件层面 NSS 从来没被“认领”过。
这个问题我断断续续查了两天,最后拉出来的根因跟 SPI 寄存器、跟设备树节点都没有直接关系,而是出在 STM32MP257 这颗芯片特有的资源隔离(RIF)配置上。这趟排查很有代表性,因为它几乎走了一遍从 MCU 思维迁移到 MPU 平台时会遇到的所有坑。如果你在 STM32MP25 系列上调试 SPI 从机,或者正好被 NSS 不生效这类问题卡住,这篇内容应该能帮你把排查范围缩小一大半。
1. 问题现场:配置看着全对,NSS 就是不认账
1.1 硬件连接与业务场景
项目是一套数据采集系统,EV1 做接收端,SPI3 配成从机;另一块用 MCU 做的采集板是主机,SPI 速率定在 1MHz,标准 Mode 0(CPOL=0、CPHA=0)。一共拉了四根线:SPI3_SCK、SPI3_MISO、SPI3_MOSI,以及 SPI3_NSS,接到 EV1 的 PB1。主机板通过 EV1 板载的 40pin 扩展接口连出来,电平统一 3.3V,地线也共了,物理连接没有悬念。
从机侧软件跑在 Linux 里,用的是内核的 SPI slave 框架。设备树里配了 spi3 节点,pinctrl 选了 SPI3 的 AF 复用,加了 spi-slave 属性,还注册了一个从机设备节点。这套配置我们在旧平台上跑过类似方案,基本是复用的,所以最初完全没怀疑到软件配置层。
1.2 现象细节:MISO 纹丝不动
主机侧的控制逻辑非常简单:每 50ms 拉低一次 NSS,沿 SCK 发送一帧 16 字节数据,然后释放 NSS。正常流程下,EV1 应该在 NSS 有效期间把数据收进 FIFO,在 RXNE 中断里取走数据。可实测下来,MISO 始终没有任何驱动动作,主机也从来没收到回应信号。
示波器上看 PB1 的电平变化非常干净,低脉冲宽度、上升沿、噪声都正常,没有虚接、没有毛刺。这说明信号确实物理到达了引脚。但芯片内部的 SPI3 外设像完全没看到这个信号:SPI_SR 的 RXNE 不置位,FIFO 为空,错误标志一个都没有,中断一条都不进。这局面就很别扭——硬链路通着,软配置看着也对,但中间某个环节把片选信号吞掉了。
1.3 先把“配置正确”这句话拆开
我这些年做嵌入式攒下一条经验:只要现象是“配置看起来全对但功能不对”,就先别急着怀疑 SPI 本身的寄存器配置,而是把“配置正确”拆成很多层来验证。尤其在 MPU 平台上,配置不只是写几个寄存器那么简单,它是一整条链路。
在 STM32MP257 上,一个 SPI 从机要被 NSS 成功“激活”,至少要同时满足四个前提:外设时钟打开、引脚确实复用成了 AF、SPI 控制器的硬件 NSS 管理开关是开的(SSM=0),以及当前 CPU 上下文对这个外设和对应 GPIO bank 有合法的访问权限。任何一环断了,表象都是一样的——NSS 拉低了,外设毫无反应。
2. 从机模式 NSS 是怎么工作的:理解“claim”的本质
2.1 一次片选“认领”需要哪些条件
SPI 从机模式里的 NSS(也叫 CS、SS),本质上是从机参与总线事务的“入场券”。主机拉低 NSS,等于告诉所有挂在总线上的从设备:都听好,现在开始发一帧数据,时钟来了谁都不许乱动,只有被选中的那个从机可以响应。
对从机来说,能不能“认领”这次事务,至少依赖三个基本条件:第一,SPI 外设总开关 SPE 已经置位;第二,片选信号确实接进了 SPI 外设的内部输入——这一步取决于引脚复用不是 GPIO 模式而是 AF 模式;第三,NSS 的有效电平和 SPI_CFG2 里配置的极性一致。串行接口里最常用的模式 0 下,NSS 是低有效,也就是 SSP 位为 0。
这三个条件缺一个,从机就不会进入接收状态。你可以把 NSS 想象成门禁卡,SPI 外设是门禁读卡器:你的卡得是真的(电平正确),读卡器得通电(时钟使能),而且卡得刷到读卡器上(引脚被正确复用成 AF)。门禁不开,不是卡的问题就是读卡器的问题,但很多人第一反应都是去怀疑卡。
2.2 硬件 NSS 与软件 NSS:被很多人忽略的 SSM 位
STM32 系列 SPI 里有一个非常关键的开关,叫 SSM(Software Slave Management,软件从机管理位),在 SPI_CFG1 寄存器里。这个位决定 NSS 信号到底听谁的:
| SSM 位 | NSS 信号来源 | 行为说明 |
|---|---|---|
| 0 | 硬件 NSS 引脚 | SPI 外设直接采样 NSS 引脚,PB1 的电平变化能直接影响从机状态 |
| 1 | 软件 SSI 位 | NSS 引脚被忽略,从机是否有效由 SPI_CFG1.SSI 这位的值决定 |
调试中一个典型翻车操作就是:代码里为了快速验证功能,把 SSM 设成了 1,用 SSI 位模拟片选。功能验证完了,后面又想把模式改成硬件 NSS,结果只改了设备树,没把固件里的 SSM 改回 0。于是你示波器上看到 PB1 确实拉低了,但 SPI 外设内部根本不看这个引脚,它只听 SSI 位的指令,NSS 当然“认领”不了。
所以排查的第一步,我会建议用调试器读出 SPI_CFG1 的当前值,确认 MSTR=0(从机模式)、SSM=0(硬件 NSS)、SSI 的值不重要。这一步很便宜,几分钟就能排除一个常见大坑。
2.3 NSSP 极性、同步等容易被忽略的细节
除了 SSM,还有两个和 NSS 切入相关的位容易被忽略。一个是 SPI_CFG2.SSP,也就是片选极性。SPI 标准里绝大多数设备是低有效,SSP=0;但如果你设备树或者初始化代码里把极性配反了,比如 SSP=1,从机会把 PB1 的高电平当成有效,拉低时反而视为无效,表现跟本次问题一模一样。
另一个是 NSSP(NSS Pulse,NSS 脉冲检测)模式和 SYNC 同步模式。NSSP 启用后,外设要求 NSS 在每帧之间必须有一个完整的无效电平脉冲,用来做帧同步。如果主机在连续传输多帧时一直保持 NSS 低电平不释放,NSSP 模式下从机可能只会识别第一帧,后面的帧全部丢掉。SYNC 同步模式则会影响 NSS 输入和内部时钟的同步方式,如果主机侧信号边沿太靠近采样沿,同步条件不满足也可能出现偶发丢失。这些位在调试中不常见,但它们都符合“配置看起来对但表现不对”的症状特征。
本章结论:SPI 从机 NSS 不生效,先按“极性 → SSM → 引脚复用 → 时钟 → 权限”的顺序排查,效率远高于反复看设备树。
3. STM32MP257 的配置链路:为什么“全对”仍然不工作
3.1 从 MCU 到 MPU:外设不再只由寄存器决定
如果你之前主要玩 STM32F4、H7 这类单片机的 SPI,你会习惯一个思维:把 NSS 引脚切到 AF 模式、配好 SPI 寄存器、打开时钟,事情就结束了。但在 STM32MP257 上,事情没有这么简单。
STM32MP257F_EV1 用的是一颗双核 Cortex-A35 + Cortex-M33 的 MPU,SPI3 这种外设可以被分配给不同执行上下文。你运行 Linux 时,A35 核在非安全世界访问 SPI3,但这颗芯片里还有 OP-TEE、还有可能独立运行的 M33 固件、还有 BootROM 阶段就配置好的安全隔离。芯片出厂后,外设默认归属并不总能满足你的使用场景。
所以我认为,到了 MPU 平台,SPI 从机的“配置”应该是一条完整的链路,任何一个环节断开都会导致等效的“NSS 失效”:
- 资源隔离(RIF)与安全上下文:决定当前 CPU 有没有权限访问这个外设和对应 GPIO bank;
- 时钟(RCC):决定 SPI3 外设时钟和 GPIOB 的 AF 系统时钟是否开启;
- 引脚复用(Pinctrl/AF):决定 PB1 这个物理引脚的电平能不能送进 SPI3 内部的 NSS 输入;
- SPI 控制器寄存器:决定 NSS 是否被使能、极性、同步等参数;
- 中断与数据通路:决定看到有效数据后能不能产生中断、能不能被读取。
在 MCU 上,第 1 步通常不存在,第 2 到第 4 步也因为库函数封装得好而容易被忽略。但在 MPU 上,第 1 步和第 3 步往往是最大的坑。
3.2 RIF 与 GPIO Bank 归属:一个极易踩中的盲区
STM32MP25 系列引入了一套资源隔离框架,简称 RIF(Resource Isolation Framework)。它替代了 STM32MP1 时代的 ETZPC,在每个外设甚至每个 GPIO bank 上都有安全配置、特权配置、CID(上下文标识)配置寄存器。RIF 决定了一件事:某个 CPU 上下文(比如 A35 非安全)能不能访问这个外设的寄存器、能不能触发它的中断。
GPIO 是特别容易中招的一类。每个 GPIO bank(GPIOA、GPIOB、GPIOI 等)都有自己的 RIF 寄存器组。如果 GPIOB 在启动配置里被分配给了 M33 上下文(MCU 上下文),那 A35 侧 Linux 的 pinctrl 驱动去改 GPIOB_MODER、GPIOB_AFRL 时,写操作可能会被硬件直接忽略,甚至触发总线错误。
这里还有个更隐蔽的细节:不是只有 GPIOB 本身要可访问,引脚复用所需要的 AF 时钟和 GPIO 外设时钟也必须使能。MPU 里普通 GPIO 和 AF 的时钟可能挂在不同的总线门控下,RIF 开了但时钟没开、或者时钟开了 RIF 没开,现象都类似。很多人看到“设备树 pinctrl 明明配了 AF7”,就觉得引脚肯定切过去了,但实际上寄存器层面根本没生效。
3.3 设备树里的隐藏坑:cs-gpios 与硬件 NSS 的冲突
再补充一个在 Linux SPI slave 模式中非常隐蔽的坑:cs-gpios 属性与硬件 NSS 的关系。
Linux SPI 框架里,控制器的片选默认是 GPIO 方式。如果你在 spi3 节点里写了 cs-gpios = <&gpiob 1 GPIO_ACTIVE_LOW>,那 SPI 核心层会把这个 GPIO 当作软件片选去控制。问题是,在从机模式下,真正的片选信号应该由外部主机驱动,硬件 NSS 输入只是被动接收。一旦 cs-gpios 指向 PB1,Linux 就会把 PB1 切到 GPIO 模式去“控制”它,你费劲配置的 SPI3_NSS AF 复用反而被覆盖了。
更麻烦的是,如果 GPIOB 在 RIF 里归 M33 管,Linux 驱动在请求这个 GPIO 时直接返回失败,spi3 节点 probe 失败,你在 /sys/bus/spi/devices/ 下都看不到这个从机。这种情况下,问题的直接表现同样可能是“NSS 不生效”,但根子已经在 GPIO 权限层了。
所以,当你看到设备树里既有 pinctrl 把 PB1 复用成 AF,又有 cs-gpios 引用 PB1 时,一定要停下来确认这两个配置是否冲突。通常二选一:要么完全用硬件 NSS(不写 cs-gpios),要么完全用 GPIO 片选(不使用硬件 NSS)。
4. 逐层排查实录:从设备树一路挖到 RIF
4.1 第一轮:确认软件层面的“配置正确”
问题刚报上来时,我首先在 EV1 上做了一套基础检查。dmesg 里确实能看到 spi3 相关的初始化日志,没有 probe 失败。ls /sys/bus/spi/devices/也能看到注册的从机设备节点。设备树里有 spi-slave 属性,pinctrl 也挂上了,用的引脚组里明确包含了 PB1。
我还特意用cat /sys/kernel/debug/pinctrl/<pinctrl-device>/pinmux-pins之类的 debugfs 节点确认过 pin 的状态,显示的就是pin 25 (PB1): (SPI3) (AF7),从内核视角看,PB1 已经被这个 SPI 设备占用了。
这让我一度非常疑惑。软件每一条都验证过了,没有报错,没有冲突,但硬件层面就是不动。现在回想起来,当时缺了一个关键动作:没有用 devmem 直接回读 GPIO 寄存器,去验证 pinctrl 驱动声称的配置到底有没有真正写进硬件。
4.2 第二轮:示波器与寄存器交叉验证
第二轮的思路是:既然软件层都“正确”,那就用硬件手段验证配置是否真的生效。
先用示波器抓 PB1。前面说过,电平干净,主机片选确实拉低了,上升沿下降沿都没有异常。接下来用 devmem 去读 GPIOB 的寄存器。GPIOB 基地址我按参考手册 RM0481 查到的值(不同板子可能有差异),MODER 寄存器在偏移 0x00,AFRL 在偏移 0x20。命令大概是这样:
# 读 PB1 对应的 MODER 位(MODER[3:2],应该是 10 表示 AF) devmem 0x44210000 # 读 PB1 对应的 AFRL 位(AFRL[7:4],应该是 0111 表示 AF7) devmem 0x44210020结果大跌眼镜:MODER 读回来是 0xFFFFFFFF,AFRL 读回来是 0x00000000。这不合理。MODER 复位值应该是 0,读回全 1 通常表示这个寄存器对当前访问者不可读,或者总线访问被硬件拦住了。我又尝试写一个测试值进去,再读回来,发现写操作完全没有效果,寄存器值纹丝不动。
到这里,我基本可以断定:问题不在 SPI 寄存器,而在 GPIOB 这个外设本身根本无法被 A35 侧 Linux 正常访问。pinctrl 驱动日志里显示“配置成功”,只是因为驱动写操作没有报错,但总线层面的内容根本到不了硬件。这是一种特别容易误导人的现象——软件以为自己在操作硬件,硬件却根本没接收到指令。
4.3 第三轮:定位 RIF 配置
确定了 GPIOB 访问异常后,排查方向就从“SPI 为什么不工作”转到了“GPIOB 为什么不可访问”。
STM32MP25 的 RIF 配置通常在启动早期就定下来了。我这边用的是板级 SDK 自带的 OP-TEE,它在引导阶段会读取一份配置清单,决定每个外设的归属和安全性。查 RIF 配置的第一步,是看对应 GPIOB 的 CIDCFGR、SECCFGR、PRIVCFGR 寄存器。ARM 调试器直接读不方便,我是通过 OP-TEE 的早期日志和启动配置源文件定位的,最终发现 GPIOB 被明确分配到了 M33 上下文,安全属性也被标记为安全。
SPI3 本身也有各自的 RIF 配置寄存器,检查后发现 SPI3 同样被分配给了 M33 上下文。也就是说,即便我把 GPIOB 的权限改过来,SPI3 的寄存器访问大概率也会被拦。所以这一版固件的意图,很可能是希望用 M33 核心来完成实时性要求高的数据采集,把 A35 Linux 排除在 SPI3 的访问路径之外。但我们对这个默认配置并不知情,还在用 A35 侧 Linux 去配 SPI3,自然处处碰壁。
另外我顺手确认了 cs-gpios 这个坑:我们的设备树里最初没有写 cs-gpios,所以这个坑当时没有踩到。但如果你的设备树里既有 pinctrl 的 AF 复用又写了 cs-gpios,建议你在这一步一起排查掉。
4.4 第四轮:修复后验证
定位到根因后,修复方案实际上是配置策略的选择问题。我们最终选择把 SPI3 和 GPIOB 的 RIF 归属改给 A35 非安全侧,让现有 Linux 方案的改动最小。
修改主要是两块:一是 OP-TEE 启动配置里去掉对这个外设的 M33 安全占用,改配成非安全、CID 指向 A35;二是确认设备树里 spi3 节点的 status、pinctrl、spi-slave 都正确。改完重新编译、烧录、重启后,我第一时间用 devmem 回读 GPIOB_MODER,这次写进去的值能被正常读回。再配合设置 PB1 为 AF 后,示波器上明确能看到 PB1 的电平变化对 SPI 内部状态产生了影响——主机拉低 NSS,从机的 RXNE 中断正常触发,MISO 也开始在该回应的时候驱动数据了。
整个验证过程花了大约半小时,对比前面两天的排查,差距全在“是否提前检查了 RIF 归属”上。
5. 根因复盘:三层配置里藏着两层陷阱
5.1 直接原因与深层原因
直接原因很简单:GPIOB 和 SPI3 的外设访问权限被 RIF 配置关在了 A35 侧 Linux 门外,导致 pinctrl 的引脚复用配置从来没有真正写入硬件。PB1 物理上接收到了 NSS 低电平,但芯片内部根本没有把 PB1 连到 SPI3 的 NSS 输入上,因为引脚停留在复位状态,没有切到 AF。
深层原因则需要反思:从 MCU 平台迁移到 MPU 平台时,我没有把“资源隔离”纳入外设配置的必查清单。设备树、pinctrl、SPI 寄存器看起来都对,那只代表“Linux 希望硬件是这样”,不代表硬件真的能让它这样。RIF 是所有 MPU 外设访问的前置闸门,在单片机时代没有这个概念,所以尤其容易漏掉。
5.2 两个修复路线怎么选
如果你的板子也遇到类似问题,有两条路线可以走,取决于你的业务需求:
| 修复路线 | 适用场景 | 改动成本 | 风险点 |
|---|---|---|---|
| 路线 A:把 SPI3/GPIOB 释放给 A35 Linux | Linux 承担所有通信逻辑,希望用现成 SPI 驱动、用户态程序 | 低,改 OP-TEE 配置 + 设备树 | 需确认不会和 M33 侧其他应用冲突 |
| 路线 B:SPI3 从机逻辑全部放 M33 固件 | 对时延、可靠性要求高,M33 做主处理 | 高,需要写 M33 固件、IPC 通信 | M33 固件复杂度、与 Linux 的同步机制需要额外设计 |
我们选了路线 A,原因是项目里数据量不大,Linux 的非实时性可以接受,而且改动最小、便于快速验证。如果你的场景对时序要求严苛,比如 NSS 脉冲宽度只有几十微秒,我更建议认真考虑路线 B,让 M33 就近处理 SPI 中断,再通过 RPMsg 和 A35 通信。在 MPU 上,把外设放在最合适的核心旁边,比在 Linux 里跟调度器搏斗要明智得多。
5.3 修复后的完整验证清单
修复完成不代表结束,我建议按下面这份清单做一次端到端验证,确保不是“碰巧能跑”:
- 启动后 dmesg 无 spi3 相关错误,从机设备节点注册成功;
- devmem 回读 GPIOB_MODER、AFRL,确认 PB1 处于 AF 模式且 AF 编号正确;
- devmem 回读 SPI3 的 SPI_CFG1、SPI_CFG2,确认 MSTR=0、SSM=0、SSP=0;
- 示波器同时抓 SCK、MISO、NSS 三路