这块板子卡在哪里
TC-EB5 是基于 Qualcomm QRB5165 / SM8250 的板子。有线网经过 PCIe1、ASM2806 交换芯片,再连到两颗 Realtek RTL8168。厂商 4.19 内核能枚举完整拓扑,早期主线移植却只能看到 root port,桥后的网卡没有出现。
这篇文章最初记录的是 Linux 6.19 的尝试。后续把实验基线固定到 Linux 6.13.12,在 2026 年 9 月完成了 PCIe Gen3 ×2、两颗网卡驱动绑定和持久启动验证。保留原 URL,正文改为这次真正跑通的复盘;下面的结果不代表 6.19 已通过验证。
最终交付是两个树外模块和一份板级设备树。Linux 源码保持 kernel.org 原样,原生 pcie-qcom.c、r8169 不打补丁;QMP PHY 仍使用我们树外维护的已验证实现,因此不能说所有驱动都已经是纯上游配置。
代码:Evsio0n/tc-eb5-oot。发行版本:v0.2.1。仓库提供源码、DTS/DTB、测试和匹配配置的预编译模块,不包含整机 boot、rootfs 或私有备份。
先纠正旧记录中的几个结论
旧文保留了很多“这次应该就是原因”的即时判断,其中几项后来没有成立,不应继续作为安装步骤:
| 早期判断或做法 | 后来的证据与处理 |
|---|---|
| 把 GPIO141 当作 ASM2806 enable/reset 输出 | vendor DT 把它定义为 wake 输入;真正的复位脉冲落在 GPIO82。最终保留 141 输入上拉,不驱动它 |
删除 iommu-map,认为旁路 SMMU 才能枚举 | 成功版本保留 iommu-map,没有额外设置 bypass;撤回旧文那条未经实证的“已确认根因” |
| 用反复 rescan / Secondary Bus Reset 补回初始化 | 板级准备应发生在 host 使用 PERST、训练和枚举之前;最终不依赖后补 rescan 或 SBR |
| 资源分配函数只改软件结构,必须手写桥窗口 | 当前 6.13.12 的分配路径包含桥窗口配置,最终没有接管原生资源分配 |
| 降到 Gen1 ×1 就能解决问题 | 降速只能隔离变量,不能补上错误的管脚和缺失的板级时序 |
这不是说 IOMMU、桥窗口或 PHY 永远不会有问题,而是本次成功没有依赖这些旧假设。0xffffffff 的配置空间读数本身也不足以证明某个具体原因。
第一个转折:恢复可信的 vendor 对照
中途 vendor 内核也出现过启动后进入 9008 的情况,/firmware 看起来又是空目录。如果直接判断固件被格式化,vendor 就不能继续充当硬件正常的对照组。
我们先完整、只读地备份固件分区,再检查 FAT 结构和文件分段。分区中的 331 个文件仍能提取,ADSP、CDSP、Venus 等 MDT 对应的必需分段也在。空的是挂载点:firmware-mount.service 被 mask,而原固件搜索路径又依赖 /firmware/image。
恢复原只读挂载后,vendor 恢复到可以稳定采集 GPIO、PCI 和 PHY 状态的程度,也能枚举 ASM2806 与两颗 RTL8168。不能先把交换芯片损坏当作解释,也不需要为了“修复空目录”重刷固件分区。
这与主线 PCIe 的根因是两件事。它的作用是恢复可信的对照,不是证明挂载服务就是所有重启问题的唯一原因。
真正的突破:把整数 GPIO 还原成物理时序
PHY 寄存器相同,不代表板外条件相同
到 Boot175 时,已经读回 133 项 PHY 寄存器,与当时 vendor 的对照一致,下游却仍然没有出现。这缩小了问题范围,但没有覆盖 SoC 外部的控制线,也没有证明全部 PHY 行为等价。
于是回到 vendor 内核,用 IDA 和 MCP 找到 msm_pcie_probe() 中的 ASM2806 分支。判断条件也一起核对:它读取 use-pcie-bridge-asm2806,本板走的是 nvme=0 / asm2806=1 路径,避免把另一个板型的初始化搬过来。
反编译结果没有直接照抄。我们同时检查原始汇编中的 GPIO 参数和延时调用、vendor DT 的管脚用途,以及运行时 gpiochip 的编号范围。
漏掉的是编号空间
vendor 使用旧式全局 GPIO 整数编号,DTS 中的 <&tlmm N ...> 则是控制器内的局部编号。该次 vendor 运行时,TLMM 的 base=1100、ngpio=180。
汇编里的全局 GPIO 1182 对应:
1182 - 1100 = 82
它不是 GPIO141,也不能把 1182 原样填进 DTS。这个 base 只用于解释那份 vendor 运行时证据,新的 helper 使用 GPIO descriptor,不硬编码全局编号。
原先用局部编号去 grep vendor 的全局 debugfs 编号,搜不到输出,也不能证明厂商驱动没有申请那根 GPIO。
还原后的九步序列
下面都是 TLMM 局部编号和电气 raw 电平,不是 active-low descriptor 的逻辑值。等待时间来自 vendor 延时路径,并与计时换算交叉检查。
| 顺序 | GPIO | raw 电平 | 随后等待 |
|---|---|---|---|
| 1 | 82,PERST | high | 100 ms |
| 2 | 82,PERST | low | 200 ms |
| 3 | 88 | low | 10 ms |
| 4 | 88 | high | 10 ms |
| 5 | 89 | low | 10 ms |
| 6 | 89 | high | 10 ms |
| 7 | 121 | high | 5000 ms |
| 8 | 127 | high | 无独立延时 |
| 9 | 126 | high | 120 ms |
然后进入常规 PHY / PCIe 初始化和最终 PERST 释放流程。不能把 GPIO82 恢复成输入来代替后半段。
GPIO88、89、121、126、127 对应的准确电气网络名仍缺原理图,本文不把它们擅自命名为某条电压轨。也没有因为 5 秒看起来长就删掉它:先保留对照中的行为,缩短时序需要另做冷启动实验。
对照失败实现,差异很直接:首项错用了 GPIO141,驱动还只消费数组的第一项;vendor 对其余控制线的操作没有完整执行。运行时快照中,vendor 的相关管脚已是输出高,而失败版本的多根脚仍是输入低。
怎样验证它不是另一个猜测
第一轮修正只替换错误的板级 GPIO 操作,保留已有 PHY 配置、lane 数和其他参数。GPIO82 复用既有 PERST descriptor,不重复申请;其余描述符全部拿到并验证后,再执行输出。
Boot176 随后在 ×1 设置下枚举出了四个 ASM2806 桥功能和两颗 RTL8168。再从这个成功镜像出发,仅把 DT 的 num-lanes 从 1 改为 2,得到 Gen3 ×2。五分钟、11 次采样中链路和拓扑保持存在,可读 AER 计数为零。
这给出了有对照的结果:修正错误映射并补全 vendor 板级序列,是这轮从“只有 root port”到完整枚举的关键变化。 此前保留的 PHY 调整没有逐一撤除,因此不把全部历史补丁都说成必需,也不宣称纯原版 PHY 已经足够。
跑通之后,如何把定制从 Linux 树里拿出去
最初为了隔离变量,板级操作放在 pcie-qcom.c 的正确时机里。确认行为有效后,下一个目标是不再维护 Linux 源码补丁。
单纯把代码编译成 .ko 不能解决顺序:如果原生 PCIe 已经训练和枚举完,再加载 helper,就晚了。modprobe 的命令先后也不等于所有异步 probe 已完成。
最终利用的是驱动本来就要申请的资源——PERST GPIO。
用真实资源依赖,而不是一个假时钟
tc-eb5-pcie-helper.ko 持有真实 GPIO82 和另外五根控制线,完成板级准备后,才注册单线 GPIO controller,向原生 PCIe 导出虚拟 PERST。
TLMM82 和板级控制线
│ 由 helper 独占
▼
tc-eb5-pcie-helper:准备完成后发布 GPIO provider
│ perst-gpios 引用
▼
原生 qcom-pcie:取得 PERST → PHY/链路初始化 → PCI 枚举
helper 尚未准备好时,host 的资源依赖不满足,probe 会等待或 defer。不需要保证 .ko 比内核里的 PCIe 驱动“更早注册”;需要保证的是:host 真正拿到并使用这条资源之前,provider 已准备好。 这依赖标准 GPIO provider / consumer 接口。GPIO 驱动接口
本板根文件系统在 UFS,不依赖 PCIe1,因此可以先进入 rootfs,再自动加载外部 PHY 和 helper,让 PCIe1 继续 probe。如果根盘本身在 ASM2806 后面,就要把模块及依赖一起放进 initramfs,不能照搬这份加载位置。
原生 host 最后释放 PERST 时,helper 的 GPIO 回调在物理切换前保留 10 ms、切换后保留 200 ms 等待,provider 声明 can_sleep。active-low 的逻辑转换交给 GPIO 框架,内部明确操作 raw 电平,避免再次反相。
保留的是已验证的操作和显式等待。原生 host 自己的 1 ms 等待会在回调返回后发生,不声称整个启动调度与旧实现逐周期一致。
PHY 仍是一份需要维护的树外实现
tc-eb5-qmp-pcie.ko 保留此前已验证的 PHY 设置和读回诊断。构建配置关闭内核自带的 QMP PCIe PHY 驱动,避免竞争绑定;原生 PCIe 控制器和内建 r8169 继续启用。
Linux 源码与官方 6.13.12 归档逐项比对:87,188 个普通文件、62 个符号链接一致,没有额外源码路径。构建输出使用独立 O= 目录,DTS 和模块也在树外。
这回答的是“不修改 Linux 源码”,不是“已经合入上游”或“以后任何版本都能直接编译”。外部 PHY 还引用目标内核的私有头文件,升级需要单独适配。
第二层顺序:网卡不能在 host 的 async probe 里同步请求模块
模块化后,两颗网卡能枚举,却出现 kernel/module/kmod.c 的 WARN。调用路径是:
qcom_pcie_probe(async worker)
→ pci_host_probe
→ r8169 / rtl_init_one
→ 创建 PHY / MDIO 设备
→ phy_request_driver_module
→ __request_module(wait=true)
内核检查的是 wait && current_is_async():异步 probe 上下文里同步等待模块加载,存在死锁风险。即使相关 PHY 驱动已内建,这条路径仍会请求模块;预加载没有改变调用上下文。6.13.12 的检查位置
所以“helper 在 PCIe 使用资源之前准备好”只解决了一层顺序。还需要让 网卡驱动在原生 host 完成 probe 后再绑定,而不是阻止 PCI 核心发现网卡。
v0.2.1 的 managed device-link
helper 在 GPIO 准备完成后、发布 provider 之前建立绑定 gate。它只匹配本板 PCI domain 1、总线 04/05、function 0 的 10ec:8168 两个设备,不影响其他 PCI 设备。
PCI 核心添加这两个设备时,为它们建立到 readiness supplier 的 managed device-link。supplier 暂时返回 -EPROBE_DEFER,所以设备可以被枚举,但 r8169 还不能 probe。
普通 delayed work 等到原生 1c08000.pcie 真正 bound,再标记 supplier ready,并调用 device_attach()。driver core 根据 DL_FLAG_AUTOPROBE_CONSUMER 重新推进网卡绑定,实机 bound 通知中的 async 此时为 0。Device links 文档
helper:完成 GPIO 序列 → 建立 gate → 发布 PERST provider
↓
原生 host:训练 / 枚举 → RTL 暂缓绑定 → probe 完成
↓
普通 worker:确认 host bound → supplier ready → r8169 绑定
没有全局关闭 PCI autoprobe,没有改写用户的 driver_override,也没有 kprobe、ftrace、内核文本 patch 或私有函数地址。等待有界,超时撤销 supplier,由核心回收 managed links,避免永久扣住网卡。
v0.2.0 失败在哪里
中间的 v0.2.0 实验确实崩过。它把 supplier 下一次 probe 的输入放在 drvdata 里,而 driver core 会在失败的 probe 清理路径清掉它;第一次 defer 后再次 probe,就读到了空指针。
v0.2.1 改用生命周期覆盖 deferred retry 的 platform data,并加空值检查。测试模型也补上“defer 后清空 drvdata、再重试”的行为,旧实现作为负对照能被 ASan 检出。
最终通过的是 v0.2.1:23 项绑定 gate 模型测试、48 项 GPIO 测试,以及实机启动和五分钟观察。v0.2.0 不作为可用版本发布。
枚举之后,还要把固件放在正确的启动阶段
PCIe 起来不代表固件和用户空间已适配。之后还查到 rtl8168h-2.fw、ADSP/CDSP/Venus 的主线路径缺失,以及更容易误判的情况:蓝牙固件和 regulatory.db 在根文件系统存在,启动却报 -2。
时间戳给出解释:蓝牙和监管数据库在约 11.98 秒请求,真实根分区在 13.73 秒才挂载,原 initramfs 又没有这些文件。它们不是“没有安装”,而是在请求发生时不可见。固件搜索路径
这次从 Debian firmware-qcom-soc、firmware-realtek 的 20250410-2 包中提取所需文件;蓝牙和 regdb 使用板上已有版本。拿到的 Realtek 下载文件与 Debian 包逐字节相同,包里的 qcom/sm8250/a650_zap.mbn 也与本板 vendor 的 a650_zap.elf 完全相同。Debian 固件包清单
给新 initramfs 加入 14 个固件文件时,原有 1,222 个条目——包括 /init、权限和链接元数据——保持不变。rootfs 同时补齐缺的 10 个文件,已有 4 个文件不改,因为两颗网卡约在 25 秒、切根以后才绑定,只改 initramfs 仍然不够。
这一轮只重新打包 boot,内核、DTB、helper 均未改。先完整备份旧 A 槽,再写入新镜像;写后回读一致,B 槽前后哈希不变,然后正常重启验收。没有把这些文件写回 /firmware 分区,该分区继续只读挂载。
重启后 ADSP/CDSP 都进入 running,蓝牙完成 patch/NVM 下载和 UART setup,两个 r8169 都报告固件版本 rtl8168h-2_0.0.2 02/26/15。Venus 注册了编码、解码节点,但还有版本读取告警,不能据此宣称硬编解码已验收。
目前到底跑通到哪一层
| 层次 | 已观察到的结果 | 仍未覆盖 |
|---|---|---|
| 启动 | 新 boot 从 A 槽持久启动,SSH 恢复 | 开机 SSH 认证延迟仍需排查 |
| PCIe | Gen3 ×2,四个 ASM2806 桥功能、两颗 RTL8168;观察窗口内 AER 为零 | 多轮断电冷启动、休眠恢复、长期压力 |
| 有线网 | 两个 r8169 绑定且加载固件;eth0 千兆全双工、DHCP 租约、实际 SSH 通信 | eth1 尚未接线,未测双口吞吐 |
| DSP / 蓝牙 | ADSP/CDSP running,蓝牙 UART setup 完成 | DSP 负载、蓝牙扫描/配对/音频 |
| 视频 / GPU / 声音 | Venus 编解码节点、ADSP APR 子设备出现 | Venus 版本告警;LT9611UXC I2C 超时、无 DRM 节点,声卡等待 HDMI codec |
最新固件镜像单次开机检查持续到 326 秒,保持同一 boot ID;没有再看到这批固件文件缺失、WARN 或 Oops。此前模块 v0.2.1 的独立五分钟检查也通过了。它们是不同阶段的证据,不合并成“整板所有外设已经完成”的结论。
开机 DHCP 使用 Netplan / networkd,eth0、eth1 都设为 dhcp4: true、optional: true,route metric 分别为 100、110。原热点 dhcpcd 通过 denyinterfaces eth0 eth1 排除有线口,保留无线配置,避免两个网络管理器争接口。
使用发布的模块
Evsio0n/tc-eb5-oot 的发行包包含两份 .ko、完整树外源码、DTS/DTB、内核配置、安装脚本、测试和 review patches。patches/ 是对树外项目的补丁,不要应用到 Linux 源码树里。
预编译模块只对应 6.13.12-tc-eb5-oot1 及所附配置。仅 uname -r 看起来相近不够,不要对另一内核强制加载。外部 PHY 所用的私有头文件也不一定在发行版 headers 包中,需要对应的完整源码和构建输出。
准备好源码和独立输出后,在模块仓库内构建:
# KSRC:原版 Linux 源码;KDIR:匹配配置、已构建的独立输出目录
# 原生 AArch64 构建示例;交叉编译另加 ARCH/CROSS_COMPILE。
make -C "$KDIR" M="$PWD/modules" modules
KDIR="$KDIR" KSRC="$KSRC" sh scripts/build-dtb.sh
python3 tests/test-sequence.py
python3 tests/test-bind-gate.py
构建使用 Linux 的 external modules 接口。安装前按 README 核对 checksum、板型、配置和备份;安装器拒绝覆盖已有 helper/unit,也不会替你刷 boot、重启或改网络配置。
不要把另一块板子的分区号照抄进 dd。公开的是可审阅、可重建的模块和设备树,不分发本机 vendor ramdisk、完整 boot 分区、rootfs 或固件原始备份。实际打包和回退使用自己板子的启动参数与已验证镜像,运行中不要强制卸载 PCIe 正持有的 GPIO / PHY 模块。
接下来优先处理 LT9611UXC / HDMI 依赖和双网口实测。PHY 参数精简、冷启动和跨内核版本支持分别做回归,不混进这次已经固定下来的启动基线。