如果你手头也有一块RK3588开发板,并且正在被PCIe初始化卡死问题折磨,那这篇DTS修改实战笔记应该能帮你省下至少一周的排查时间。我踩这个坑的起因很简单:想给板子挂一张PCIe接口的扩展卡,结果内核启动到PCIe驱动probe阶段直接卡死,看门狗超时后整板重启,周而复始——那个"Duang"的声响与其说是蜂鸣器,不如说是我内心的崩溃声。
先说现象。串口日志一路正常输出,从bootrom到U-Boot到内核解压,驱动逐个初始化,一直到出现rockchip-pcie或者dw-pcie相关的打印时一切戛然而止。没有panic、没有oops、没有堆栈回溯,就是单纯地停住。这时候如果手边有示波器,你会发现SoC的PCIe参考时钟管脚有波形,但对应的复位管脚(PERST#)一直处于无效电平,说明链路训练压根没跑起来。再等几秒,硬件看门狗把系统踢了,重启后又卡在同一个地方。
很多第一次遇到这个问题的人会以为是内核配置不对,或者U-Boot里没初始化好,甚至怀疑是不是芯片本身有问题。但根据我在RK3588多个板卡上反复验证的经验,这类"静默卡死"有九成以上是DTS(设备树)对PCIe控制器、PHY、电源和复位这几个资源的描述不到位导致的。这篇文章我就从现象出发,把排查链路、DTS修改实战和硬件配合项完整地过一遍。
1. 卡死现场还原:串口日志停在哪一行
1.1 "静默卡死"和普通异常崩掉的本质区别
嵌入式Linux开发里,系统异常通常分两种:一种是有明确的错误输出,比如kernel panic、oops、栈回溯,这种问题定位起来相对容易,打印里通常直接指明了出错的函数和调用链;另一种就是这种"静默卡死",系统没有任何报错,线程直接卡在某处不再往下走,CPU占用率可能还在跳动,中断也可能还在触发,但关键路径上的逻辑已经停摆。
PCIe初始化卡死属于典型的第二种。RK3588的PCIe控制器驱动在probe时要完成以下几步:使能PHY、等待PHY的PLL锁定、配置控制器寄存器、拉高PERST#释放端点复位、启动LTSSM链路训练、轮询等待linkup。这中间的任何一个环节卡住,现象都是一样的:日志停住不动,系统看似还活着,但PCIe子系统永远初始化不完。
我遇到的情况是日志停在:
[ 2.183723] rockchip-pcie 3c0000000.pcie: host bridge /pcie@3c0000000 ranges: [ 2.190803] rockchip-pcie 3c0000000.pcie: Parsing ranges...然后就没了下文。这个位置说明驱动已经识别到了PCIe控制器,并且开始解析地址范围,但接下来要做的PHY power-on或者link up操作没能完成。整个系统就这么卡着,直到看门狗超时。
1.2 为什么这种卡死最让人头疼
这种问题的麻烦之处在于:它不像编译错误那样有个明确的报错行,也不像驱动加载失败那样在日志里打一句"failed"然后继续。它就是一个"停"的动作,没有任何解释。你甚至不确定到底是驱动代码的哪个while循环在里面死转,还是等待某个硬件事件永远等不到。
更头疼的是,RK3588上PCIe的初始化卡死往往不是单一原因。我后来复盘发现,我当时那块板子的DTS里同时存在三个问题:电源节点缺失、PHY模式没有显式声明、pinctrl引脚复用不对。这三个问题单独看都不致命,但叠加在一起,正好把PCIe枚举过程堵死在半路上。
所以不要指望有一个"万能补丁"能解决所有人的问题,你需要的是系统化的排查方法。这就是我写这篇文章的核心目的。
2. RK3588的PCIe控制器矩阵与DTS节点对应关系
动手改DTS之前,必须先搞清楚RK3588这颗SoC里PCIe资源是怎么分布的。这也是很多教程没说透的地方——很多人拿着一份公版DTS就开改,改完发现现象没变,原因就是改错了节点。
2.1 四个控制器、三种PHY,别搞混了
RK3588一共有4个PCIe控制器,它们使用的物理层(PHY)资源各不相同:
| 控制器节点 | 协议/宽度 | 使用的PHY | 典型用途 |
|---|---|---|---|
| pcie3x4 | PCIe 3.0 x4 | pcie30phy(独立PHY) | NVMe SSD、高性能加速卡 |
| pcie3x2 | PCIe 3.0 x2 | combphy2_ps | 显卡/网卡 |
| pcie2x1l0 | PCIe 2.0 x1 | combphy0_ps | 低速外设 |
| pcie2x1l1 | PCIe 2.0 x1 | combphy1_ps | 低速外设 |
这里有两个容易踩的坑。第一,pcie3x4用的是独立的PCIe3 PHY,和另外三个控制器使用的Combo PHY不是一回事,前者的配置入口在DTS里对应的是单独的PHY节点。第二,combphy0_ps、combphy1_ps、combphy2_ps是"三合一"PHY,同一时刻只能工作在PCIe、SATA或USB3.0中的一种模式。
2.2 Combo PHY的协议互斥:SATA和PCIe只能二选一
这个互斥特性在实际板卡上非常容易踩雷。很多RK3588开发板把PCIe和SATA设计成物理复用的——同一个接口,通过拨码开关或者DTS配置来决定走SATA还是走PCIe。如果你在DTS里同时把SATA0和pcie2x1l0都配成使用combphy0_ps,就会出现两种结果:有的内核版本启动时打错误日志但继续跑,有的版本直接在PHY初始化环节卡死,表现就是"卡在PCIe probe不动了"。
排查方法很简单,用cat /sys/kernel/debug/phy/phy_register查看当前PHY的注册和使用状态,或者直接在DTS里搜索各个combphy节点的引用关系,看看有没有被多个控制器同时引用。
2.3 DTS里PCIe节点的核心字段
一个典型的PCIe控制器节点在DTS里长这样:
&pcie2x1l0 { num-lanes = <1>; phys = <&combphy0_ps PHY_TYPE_PCIE>; phy-names = "pcie-phy"; reset-gpios = <&gpio4 RK_PA2 GPIO_ACTIVE_HIGH>; vpcie3v3-supply = <&vcc3v3_pcie>; pinctrl-names = "default"; pinctrl-0 = <&pcie2x1l0_reset>; status = "okay"; };一个PCIe控制器节点牵扯到PHY引用、PHY模式、复位GPIO、引脚复用、电源供给和状态开关。任何一个字段和实际硬件对不上,初始化就会出问题。下面一节我会讲排查链路,你会看到这几个字段到底各自扮演什么角色。
3. 根因定位链路:从内核打印到硬件管脚的一路排除
这一节我按照自己当时的排查顺序写,你可以照着走一遍。
3.1 第一步:用initcall_debug锁定卡死位置
先用带initcall_debug参数的内核启动,或者在内核命令行里加上loglevel=8,把日志级别拉到最高。启动后观察最后一个有效的内核打印。如果日志停在了PHY初始化附近(搜索rockchip-combphy相关打印),问题大概率出在PHY配置上;如果PHY初始化都过了,卡在rockchip-pcie的probe里,那就要往控制器本身、复位时序和电源上查。
我当时拿到的关键日志就是第一节里贴的那两行,Parsing ranges...之后就没有任何输出了。这说明驱动在解析完地址映射之后,去执行某项硬件操作时没能返回。
3.2 第二步:排查Combphy模式冲突
由于我那块板子的PCIe接口走的是combphy0_ps,我先检查了DTS里是否有其他节点同时引用它。一查果然有问题:SATA0节点是okay状态,同样引用了combphy0_ps,虽然SATA0在物理上没接设备,但驱动还是会去初始化PHY,这就和PCIe抢占了同一个PHY资源。
解决方式是给SATA0加status = "disabled",或者把它的phys引用去掉,把PHY让给PCIe。这一步做完,日志前进了几行,但又卡在了新的位置。
3.3 第三步:示波器量复位和参考时钟,别急着改代码
硬件排查这一步很多人会跳过,但这恰恰是最快区分"软件问题"和"硬件问题"的方法:
- 示波器探头接PCIe参考时钟差分对的其中一个管脚(REFCLKP或REFCLKN),上电后应该能看到100MHz的正弦波或方波。如果没有波形,说明PHY的PLL没有输出参考时钟,或者板上的时钟芯片/晶振没起振。
- 找PERST#信号,测量它从电源稳定到拉高(释放复位)的延时。PCIe规范要求PERST#必须在所有电源轨稳定之后才能释放,如果板子的复位逻辑不满足这个时序,端点设备还没准备好,链路训练就会反复失败。
- 如果是转接卡或者扩展槽,用万用表量一下3.3V和12V供电是否到位。
我当时测量后发现:参考时钟有、PERST#释放也正常,但链路无论如何都训练不起来。这就基本排除了外部硬件的问题,焦点回到了SoC内部的PHY配置和DTS描述上。
3.4 第四步:翻驱动源码,确认卡死的具体函数
到这一步,我打开了drivers/pci/controller/dwc/pcie-designware.c和Rockchip的适配层rockchip-dw-pcie.c,在关键打印后面补了调试信息,最终定位到卡在dw_pcie_wait_for_link这个函数里——驱动启动了链路训练,然后进入轮询循环等待linkup信号,但一直没有等到,且这个等待在某些条件下不会按时退出。
这里引出DTS里一个很重要的参数:max-link-speed。如果你的PCIe设备和主控之间信号质量不好(走线过长、耦合电容位置不对、连接器接触不良),Gen3速率下训练成功率会很低。把速率限制到Gen2甚至Gen1,链路反而能稳定跑起来。这是一个性能和稳定性之间的权衡,但在"初始化卡死"这个场景下,先让系统跑起来永远是第一优先级。
4. 实战修改:完整DTS补丁及逐字段说明
下面给出我当时的完整修改,分四块:PHY节点、PCIe控制器节点、电源节点和pinctrl配置。
4.1 PHY节点:显式声明PCIe模式
&combphy0_ps { rockchip,phy-mode = "pcie"; status = "okay"; }; &combphy1_ps { status = "disabled"; }; &combphy2_ps { status = "okay"; };这里把combphy0_ps显式设置为PCIe模式。这个rockchip,phy-mode属性在部分BSP内核里不是必须的(驱动会根据phys引用的PHY_TYPE_PCIE自动判断),但显式写上可以避免某些版本的内核在PHY初始化顺序上的不确定性。
4.2 PCIe控制器节点:电源、复位、速率逐一补齐
&pcie2x1l0 { num-lanes = <1>; max-link-speed = <2>; phys = <&combphy0_ps PHY_TYPE_PCIE>; phy-names = "pcie-phy"; reset-gpios = <&gpio4 RK_PA2 GPIO_ACTIVE_HIGH>; vpcie3v3-supply = <&vcc3v3_pcie>; pinctrl-names = "default"; pinctrl-0 = <&pcie2x1l0_reset>; status = "okay"; };逐字段解释一下:
num-lanes = <1>:声明这个控制器只使用1条lane。如果板子只接了一根lane但DTS里默认写成x2甚至x4,主控会在多条lane上做接收检测,没接的lane会拖慢甚至阻塞整个训练过程。max-link-speed = <2>:把最高速率限制在PCIe 2.0(Gen2,5GT/s)。如果你用的是PCIe 3.0设备且确定信号完整性没问题,可以去掉这一行;但如果你正处在"莫名其妙卡死"的阶段,先加上它,把变量降到最少。phys = <&combphy0_ps PHY_TYPE_PCIE>:这里的PHY_TYPE_PCIE是枚举常量,用来告诉combphy驱动把PHY切到PCIe模式。这个宏定义在include/dt-bindings/phy/phy.h里。很多DTS模板文件漏掉了这第二个参数,导致PHY驱动用默认的PHY_TYPE_SATA去初始化,链路自然起不来。reset-gpios:对应PERST#信号。要确认引脚号和你的原理图一致。我见过好几块板子,DTS里写的GPIO和原理图差了一两个pin,导致复位一直没释放。vpcie3v3-supply:端点设备的3.3V主电源。如果这个regulator没有使能,端点设备上电不完整,接收检测永远失败。
4.3 电源节点:确保regulator默认使能
vcc3v3_pcie: vcc3v3-pcie-regulator { compatible = "regulator-fixed"; regulator-name = "vcc3v3_pcie"; regulator-always-on; regulator-boot-on; gpio = <&gpio3 RK_PC3 GPIO_ACTIVE_HIGH>; enable-active-high; };regulator-always-on和regulator-boot-on是"简单粗暴但有效"的写法,确保电源在PCIe驱动probe之前就已经稳定输出。如果你的板子有复杂的电源管理(比如通过PMIC控制),则需要按照PMIC的regulator节点来配置,核心目标是:在PCIe驱动开始做链路训练之前,电压必须稳定、PERST#必须处于正确状态、参考时钟必须有效。这三个条件缺一个,训练就起不来。
4.4 pinctrl:别忘了引脚复用
&pinctrl { pcie { pcie2x1l0_reset: pcie2x1l0-reset { rockchip,pins = <4 RK_PA2 0 &pcfg_pull_none>; }; }; };这一节容易被忽略。RK3588的引脚默认状态不一定是GPIO功能,如果对应引脚没有被复用为GPIO输出,reset-gpios拉高拉低就是不生效的。检查方法是看原理图上PERST#连到SoC的哪个bank的哪个pin,然后在pinctrl里严格对应。
改完这四块,重新编译内核和设备树(设备树可以单独编成dtb文件,通过U-Boot的tftp加载,或者直接烧到boot分区),重启后PCIe链路终于训练起来了。串口里能看到类似这样的日志:
[ 3.152381] rockchip-pcie 3c0800000.pcie: Link up, gen: 2, speed: 5GT/s, width: x1那一刻的感觉就俩字:解脱。
5. 验证与避坑:链路训练超时、耦合电容和那些容易犯的错
链路起来之后,别急着庆祝。PCIe这个玩意儿,训练成功只是第一步,后面还有稳定性、带宽、功耗管理一堆事情等着你。
5.1 枚举和带宽验证:确认链路真正可用
系统起来后,用lspci -vvv看设备枚举情况和链路状态:
lspci -vvv -s 01:00.0重点看LnkSta这一行,确认速率和宽度和你预期一致:
LnkSta: Speed 5GT/s (downgraded), Width x1 (downgraded)如果显示downgraded,说明最终协商出来的速率低于设备能力,多半是信号完整性还有余量问题。带宽实测推荐两个工具:挂NVMe SSD就用dd直接读写,比如dd if=/dev/nvme0n1 of=/dev/null bs=1M count=1000;挂PCIe网卡就用iperf3打流。实测下来,PCIe 3.0 x1链路大约能跑到700~800MB/s的实际吞吐,如果只测出几十MB/s,检查一下是不是ASPM省电把链路降速了,先在内核命令行加pcie_aspm=off排除。
5.2 耦合电容摆放:信号完整性的第一道关
做硬件的朋友对"pcie耦合电容摆放位置"这个问题应该不陌生。PCIe的每条TX差分对上都要求串联AC耦合电容,典型值是100nF,作用是隔离收发双方的直流偏置。摆放位置的核心要求是:尽量靠近发送端(TX)放置,业界参考设计一般建议耦合电容到发送端引脚的距离控制在12.7mm(500mil)以内,越近越好。
如果电容放得离发送端太远,中间那一小段走线就成了短桩,在5GT/s甚至8GT/s的速率下会引入严重的信号反射,轻则链路降速,重则训练完全失败。如果你自己画了RK3588的板子遇到PCIe训练问题,先拿游标卡尺量一下耦合电容到SoC引脚的距离,再回来做软件侧调整。我见过一块板子就是因为耦合电容放在了连接器旁边而不是SoC旁边,Gen3死活训练不起来,改成Gen2就稳定了——典型的信号完整性问题。注意:RK3588作为主控(Root Complex)时,它发出的TX信号线上必须有耦合电容,这个电容可能已经做在了核心板或者SoC模组内部,如果你拿到的是核心板+底板的结构,要确认底板上的PCIe插槽前端是否还需要再放一对耦合电容,参考你的核心板硬件手册。
5.3 ASPM、训练超时和其他进阶坑
RK3588的PCIe驱动对ASPM(Active State Power Management)的支持在部分内核版本上不够完善。当端点设备支持ASPM L1 substate时,可能出现进入低功耗状态后无法正常唤醒的情况,表现是系统休眠后PCIe设备消失,或者高负载时突然卡死。遇到这类问题先在内核命令行加pcie_aspm=off验证,稳定后再逐个开启L0s/L1,找到具体是哪个状态有问题。
链路训练超时方面,dw_pcie_wait_for_link的默认超时时间大概在100ms这个量级。但如果你的端点设备初始化特别慢(比如某些FPGA加速卡需要几百毫秒),就可能出现"主控已经放弃等待,设备才刚准备好"的竞态。这种情况下的解法不是改DTS,而是需要调大驱动里的超时时间。不过这些都是后话——先把DTS修正,让系统稳定跑起来,再回来做性能层面的优化。
最后给个善意的提醒:RK3588的PCIe调试,一定不要一上来就怀疑芯片或者内核版本。绝大多数情况下,问题出在DTS对硬件资源的描述和物理连接不一致上。先理清控制器矩阵,再用示波器确认三个关键信号(参考时钟、PERST#、电源),最后动DTS。每一步都稳扎稳打,那个"Duang"的声音自然就会离你远去。类似的排查思路其实也适用于RK3588上其他外设的调试,比如GMAC网卡的DTS调试——核心都是先确认硬件连接,再核对DTS的资源描述,最后才考虑代码层面的问题。