1. 问题初现:SPI3从机模式下PB1始终“Claim”不上
最近在STM32MP257F-EV1这块板子上调SPI3从机模式,卡了一个非常典型的引脚问题:NSS引脚PB1怎么都claim不上。整条链路是MP257作为SPI3从机,外部主机通过片选信号(NSS)选中它,PB1负责接收片选电平。现象很直接——从机端的中断触发了,但每次尝试claim总线时都会失败,日志里反复出现NSS pin claim失败的信息。先说明一下,“claim”这个词在SPI从机场景下不是抽象概念,它指的是从机确认自己已经被主机选中、可以开始收发数据的状态。如果NSS引脚没有正确配置成输入监听,或者被别的外设占用,这个claim动作就永远落不了地。
复现的条件不难搭:EV1板上的SPI3接到另一个主机的SPI控制器,主机正常发起通信,但MP257这边从机状态起不来。单独看引脚电平是正常的,PB1也能测到片选信号拉低,但软件层就是认为这个NSS没有归属权,直接放弃后续的DMA收发。这里最迷惑人的地方在于:你用示波器量PB1,波形完全正常;你用GPIO读取寄存器,电平状态也对;但一旦让SPI驱动去claim这个NSS,它就会报错。后来我意识到,问题不在硬件电平,而在于这个引脚在软件内核里被赋予了多重身份,而这些身份互相冲突,导致驱动层拿不到它。
日志最初的样子大概是这样的:
spi3: failed to claim NSS pin (PB1) spi3: slave mode disabled第一次看到这条日志时,我第一反应是GPIO子系统里PB1被别的驱动申请了。因为通常“claim失败”就是gpiod_request返回-EBUSY。但查了一圈之后发现不是这么简单。SPI控制器在设备树里同时存在两种使用方式——普通GPIO和复用功能。PB1如果被pinctrl设成了SPI3_NSS的AF功能,那么从GPIO子系统的角度看,它就不属于“可claim”的状态;如果驱动尝试把它当普通GPIO来用,内核就会拒绝。
这个看似边缘的问题,实际上把设备树里引脚复用、SPI从机控制器、GPIO中断监听三块机制全部串了一遍。接下来的内容就是这次排查的完整记录,包括为什么会出现这种冲突、我是怎么一层一层定位到根的、最终怎么改才真正修好,以及以后再遇到类似问题时可以快速复用的判断思路。
2. 从硬件到软件:PB1在MP257F-EV1上的真实身份
2.1 硬件侧:PB1在板子上的实际走线
先回到板子本身。STM32MP257F-EV1的SPI3对应的NSS硬件引脚就是PB1,这在芯片数据手册的alternate function表里写得很明确,PB1可以复用为SPI3_NSS。但要注意,EV1板是一块功能非常多的评估板,PB1并不是只连到了SPI3的片选接口,它同时可能被板载的其它外设占用,比如某个扩展排针、某个电平转换电路、甚至是板载ST-LINK部分的调试辅助功能。我仔细翻了一遍板子的原理图,确认了这块板上PB1在默认状态下会连接到另一个MCU的GPIO或者某个外设。虽然默认跳帽没有接通,但在某些配置组合下,这条信号线会同时被两边的驱动电路影响。
这个硬件背景很重要,因为我最开始排查时只在软件层找问题,后来发现即使软件配置完全正确,只要板子上跳线帽的位置不对,也会导致NSS电平被强制拉低或拉高,进而让从机产生误触发。所以在排查“claim失败”时,第一件值得做的事永远是确认原理图上PB1的附属连接,而不是直接冲进设备树改配置。这块板子的用户手册里有一个跳线配置表,里面专门列出了PB1在不同模式下需要断开的连接点。
2.2 软件侧:设备树里SPI3从机模式的三层配置
MP257F-EV1跑的是标准Linux内核,SPI控制器由设备树描述。SPI3要从机模式工作,涉及设备树里至少三处配置。第一处是控制器节点的mode属性,要明确标注slave模式;第二处是pinctrl引脚的复用状态,PB1必须被选为SPI3_NSS的AF功能或者作为GPIO输入;第三处是中断资源,因为从机模式下NSS的下降沿通常要触发中断来通知驱动开始准备收发。这三层缺一不可,而且它们之间还会互相牵制。
我当时最初写的设备树是这样的:
&spi3 { pinctrl-names = "default"; pinctrl-0 = <&spi3_slave_pins>; spi-slave; status = "okay"; };spi3_slave_pins在pinctrl节点里将PB1复用为SPI3_NSS:
spi3_slave_pins: spi3-slave-pins { pins1 { pinmux = <STM32MP_PINMUX('B', 1, AF5)>; /* SPI3_NSS */ bias-pull-up; }; /* SCLK, MISO, MOSI 等 */ };这样配置之后,从硬件功能上看PB1确实是SPI3_NSS,由SPI控制器内部的NSS逻辑来解析片选信号。但问题来了:Linux内核的SPI从机框架在处理slave模式时,对NSS的处理方式并不是单纯依赖控制器硬件。它需要驱动能感知到NSS电平的变化,才能正确执行总线claim操作。如果你的驱动通过GPIO中断来检测片选信号,那PB1就不能被设成AF功能,而应该作为普通GPIO输入。一旦AF和GPIO两种角色同时被不同模块要求,claim失败就是必然结果。
2.3 角色冲突的根源:AF、GPIO、EXTI三方抢同一个引脚
PB1这个引脚在内核里主要有三种使用模式。第一种是作为IP的复用功能(AF),由SPI3_NSS硬件逻辑接管;第二种是作为普通GPIO输入,由gpiolib管理;第三种是作为EXTI外部中断输入,用于在NSS电平跳变时产生中断。这三种模式互不兼容,同一个时刻一个引脚只能处于其中一种状态。
MP257的SPI3从机驱动,在NSS claim这件事上通常有两种实现路径。一种是完全依赖硬件NSS,驱动读SPI_SR寄存器里的FRLVL或NSS状态位来判断是否被选中;另一种是用GPIO中断监听NSS引脚,在中断回调里完成claim动作。第二种方式在Linux SPI slave框架下更常见,因为从机驱动要主动地把DMA准备好,必须有一个明确的中断触发点来启动整个流程。
而当时设备树里把PB1做成了AF功能,驱动却试图用gpiod_get来申请这个引脚作为中断输入。gpiolib发现引脚已经被pinctrl子系统设成了AF模式,直接拒绝请求,返回-EBUSY。驱动在申请失败后就会打印“failed to claim NSS pin”。这不是SPI控制器的问题,而是引脚所有权归属混乱导致的系统性问题。
3. 逐层排查:从日志到寄存器,定位问题根因的完整链路
3.1 第一步:确认pinctrl实际生效状态
一开始我先确认设备树里的pinctrl是否真的生效。Linux内核的pinctrl子系统已经接管了引脚复用,单纯看设备树不一定准确,要看运行时状态。通过debugfs可以查到引脚的实时配置:
cat /sys/kernel/debug/pinctrl/pinctrl-handles cat /sys/kernel/debug/pinctrl/pinctrl-devices更直接的命令是查看某个引脚当前的function和group:
cat /sys/kernel/debug/pinctrl/48000000.pinctrl/pinmux-pins | grep "PB1"结果让我明确了问题:PB1确实处于AF5功能状态,也就是SPI3_NSS复用模式。这跟设备树配置一致,说明pinctrl侧没有问题。但问题恰恰就出在这里,系统里另一个驱动(SPI3从机驱动)试图把PB1当作GPIO来申请,这显然会和当前状态冲突。
查日志也能看到类似这样的信息:
gpiochip: GPIO 33 (PB1) has status 0x1, claimed by pinctrlGPIO编号在不同芯片上映射不同,但意思很明确:引脚被pinctrl占用了,gpiolib无法再分配。
3.2 第二步:确认SPI3控制器进入了slave模式
为了排除控制器本身没有配置成slave模式的可能,我检查了spi3控制器节点的运行时状态。Linux内核的SPI框架有一个判断逻辑,如果节点里有spi-slave属性,控制器会注册为slave控制器;否则默认是master控制器。查看内核打印或者sysfs都能确认:
cat /sys/class/spi_slave/spi3/device/uevent或者直接看内核里注册的spi_controller类型:
ls /sys/bus/spi/drivers/spi-slave-time/这几步确认下来,控制器本身确实是以slave模式注册的。那问题就进一步缩小到了NSS引脚的所有权上。控制器能进slave模式,但驱动缺一个能用的片选监听引脚。
3.3 第三步:用gpiochip信息排查PB1被谁占用
既然怀疑引脚所有权冲突,就直接用gpiolib的用户空间接口来查。在debugfs里有一个非常关键的文件:
cat /sys/kernel/debug/gpio输出会列出所有gpiochip下面每个引脚的使用状态。PB1对应的GPIO如果被某个驱动申请过,会出现类似“gpio-33 (spi3-nss) held”这样的标记。如果被pinctrl占用,则会显示为“pinmux”或者直接被sysfs指认为不可用状态。
我当时的输出里PB1根本没有出现在gpiolib的占用列表里,因为pinctrl接管之后,它根本不会被注册为GPIO请求对象。这时基本可以断定:驱动里调用gpiod_get(GPIO-based NSS)时,内核返回了-ENODEV或者-EBUSY,导致claim失败。
3.4 第四步:根因浮出水面——硬件NSS和软件NSS被混用了
整个链路查到这里,真相已经清楚了。设备树里PB1被配置成SPI3_NSS的AF功能,这本身没有错,如果是纯裸机HAL开发,这种配置完全没问题。但在Linux内核的SPI从机框架下,驱动对NSS的处理走的是GPIO中断路径。两条路径不能共享同一个引脚。
这个问题的本质是:硬件上PB1只能属于一种模式,但软件逻辑里同时需要“硬件SPI3_NSS信号”和“GPIO中断信号”两种身份。这两个需求无法同时满足,只能二选一。实际场景中大多数SPI从机驱动都倾向于使用GPIO中断来感知NSS变化,因为这样可以在中断回调里灵活地把DMA缓冲准备好,而不是依赖硬件NSS标志位的轮询。
所以修复方向也明确了:要么把PB1配置为GPIO输入并触发中断,让驱动用软件方式处理NSS claim;要么完全依赖硬件NSS,让驱动修改成轮询SPI状态寄存器的方式。考虑到现有驱动架构和DMA收发流程,前者改动量最小,也最稳定。
4. 修改方案:把PB1还给GPIO,让驱动真正“Claim”成功
4.1 设备树最终写法
正确的方式是把PB1从AF功能改为GPIO输入,并允许它作为EXTI中断引脚。这样SPI控制器本身仍工作在slave模式,但NSS的claim判断全部交给GPIO中断来完成。设备树应改成:
&spi3 { pinctrl-names = "default"; pinctrl-0 = <&spi3_slave_pins_sck_miso_mosi>; spi-slave; status = "okay"; slave { compatible = "some-spi-slave-driver"; reg = <0>; spi-max-frequency = <1000000>; interrupt-parent = <&gpioa>; interrupts = <11 IRQ_TYPE_EDGE_FALLING>; /* 根据PB1实际EXTI中断号调整 */ }; };这里有一个容易踩坑的细节:从机节点的reg值。SPI从机模式下没有真正意义的片选地址,但内核框架要求必须有reg属性,一般填0即可。同时驱动会自己监听NSS引脚中断,所以不需要在SPI子节点里声明cs-gpios,也不应该再声明spi-cs-high之类的属性。
对应的pinctrl只保留时钟和数据的复用功能:
spi3_slave_pins_sck_miso_mosi: spi3-slave-pins { pins1 { pinmux = <STM32MP_PINMUX('B', 0, AF5)>, /* SPI3_SCK */ <STM32MP_PINMUX('B', 4, AF5)>, /* SPI3_MISO */ <STM32MP_PINMUX('B', 5, AF5)>; /* SPI3_MOSI */ bias-disable; drive-push-pull; slew-rate = <1>; }; };PB1不再出现在pinctrl里,它作为GPIO输入由驱动单独管理。为了确保输入电平稳定,需要在驱动初始化时把PB1配置为带上拉的输入模式,防止片选信号浮空导致误中断。
4.2 驱动侧NSS claim逻辑调整
驱动里原来的代码大概是这样做的:
nss_gpio = gpiod_get(dev, "nss", GPIOD_IN); if (IS_ERR(nss_gpio)) { dev_err(dev, "failed to claim NSS pin\n"); return PTR_ERR(nss_gpio); }前面已经分析了,这行代码在PB1被pinctrl占用时必然会失败。修改后,PB1不再被pinctrl声明,gpiod_get就能成功拿到引脚。然后需要申请中断:
nss_irq = gpiod_to_irq(nss_gpio); ret = request_irq(nss_irq, nss_irq_handler, IRQF_TRIGGER_FALLING | IRQF_NO_SUSPEND | IRQF_ONESHOT, "spi3-nss", spi3_slave_dev);中断处理函数里做两件事:一是关掉后续的NSS中断,防止重复触发;二是唤醒等待claim的工作队列,让DMA和SPI传输逻辑继续执行。释放claim时再打开中断,等待下一次片选信号。
这里有个很小的细节:IRQF_ONESHOT标志是必须的,因为中断处理线程在运行期间片选信号可能已经拉高,如果不在线程开始时把中断屏蔽掉,很容易产生中断风暴。处理完成后重新使能中断,刚好能捕捉下一次NSS下降沿。
4.3 实测效果和验证
修改完成后,重新编译内核设备树和驱动,上电加载。这次日志干净得多:
spi3: successfully claimed NSS pin (PB1) spi3: slave mode enabled, waiting for NSS falling edge然后给主机那边发送一组测试数据,从机通过中断检测到NSS拉低,进入claim状态,执行DMA接收,接收完成后释放claim。来回跑了一千多次,没有出现一次claim失败。用示波器观察PB1的波形,可以看到片选信号的边沿非常干净,没有毛刺,和中断触发时刻完全对齐。
DMA传输的数据也逐一对比过,收发完全一致。这说明整个“GPIO中断检测NSS → claim → DMA收发 → 释放claim”的链路是稳定可靠的。
5. 避坑总结:这类问题的通用排查思路
5.1 从这次问题里提炼出的几条经验
第一,SPI从机模式下,NSS的处理方式必须先确定好,是硬件NSS还是软件NSS。这个选择决定了PB1在设备树里的角色。如果你用Linux内核,软件NSS通常是更友好的方案,因为内核的SPI slave框架天然支持通过GPIO中断来感知片选信号。硬件NSS也能工作,但你需要自行轮询状态寄存器,实时性和CPU占用率都不如中断方案。
第二,设备树里pinctrl和GPIO的冲突极其隐蔽。你很难一眼看出PB1已经被pinctrl设置为AF功能,因为这种配置不会在启动日志里报错,只有当你尝试用gpiod_get去申请它时才会暴露。遇到这种问题,先去查debugfs下的pinmux-pins和gpio两个文件,确认引脚的所有权状态。
第三,不要被示波器上的正常波形迷惑。PB1电平有变化不代表软件层能正确处理。片选信号的物理变化和Linux内核里claim状态是两码事,前者是模拟世界的事,后者是gpiolib、pinctrl、interrupt子系统协作的结果。任何一个环节没对齐,都会导致“电平正常但claim失败”这种诡异现象。
第四,EV1这类评估板尤其要注意硬件跳线和板载外设。很多时候问题不是出在你写的软件上,而是板子上一根跳线帽把PB1接到了别的地方,导致即使你软件完全正确也测不出期望行为。排查时先把原理图打开,把PB1相关的所有网络看一遍。
5.2 快速自检清单
再遇到类似的SPI从机NSS问题,我建议直接按照这个顺序检查。
- 第一,用示波器或逻辑分析仪确认PB1实际有没有收到片选信号,确认电平极性是否符合预期。
- 第二,看内核日志有没有gpiod相关的告警,比如“can't claim GPIO”或“failed to request”之类的字样。
- 第三,打开debugfs查pinmux状态,确认PB1是否被复用为其它功能。
- 第四,确认设备树里是否有spi-slave属性,控制器是否真的以从机模式注册。
- 第五,确认GPIO中断号是否正确,EXTI映射是否覆盖PB1。
这几项检查通常在十分钟内就能完成,能过滤掉绝大多数“假故障”。真正刁钻的问题反而在最后一步,就是中断处理函数里有没有正确屏蔽和恢复中断、有没有在nss下降沿和DMA准备之间处理好时间竞争。这次调通之后,我把这套检查流程固化成了一个脚本,以后每块新板子要调SPI从机,直接在串口终端跑一遍,输出结果一目了然。
最后再分享一个小技巧。如果你的SPI从机驱动对实时性要求比较高,建议在DMA描述符里提前预置好接收缓冲区,同时把DMA的传输方向设置成双缓冲模式。这样NSS中断到来时,DMA已经在等待数据了,可以做到片上延迟只取决于SPI时钟周期和数据长度,而不是软件调度延迟。实测下来吞吐率比中断后手动启动DMA高了将近一倍。这次我在MP257F-EV1上跑通之后,又顺手测了1MHz到20MHz之间几档SPI时钟,从机都能稳定响应,NSS claim再也没有掉过链子。