“你们看日志里的这个报错,第一反应是不是又去翻PCIe链路?”
前段时间调一块高通Wi-Fi 6模组驱动时,群里有人抛了这么一句。我当时正被一个诡异问题卡得头皮发麻:驱动probe成功、firmware加载正常、PCIe设备也枚举出来了,但wlan0就是迟迟不出现。常规的Linux网络驱动排查套路全试了一遍,dmesg翻了几十屏,Firmware日志也拉出来了,所有线索都指向同一个区域,但谁都没说清根因是什么。
后来我冷静下来,重新把高通平台的启动链路捋了一遍,才意识到问题很可能不在驱动本身,而在Wi-Fi驱动和firmware之间的那条隐形通道——高通QRTR协议栈。今天想把这段排查实录和协议栈的底层逻辑完整写出来,给还在跟高通Wi-Fi驱动纠缠的朋友一个参考。QRTR平时不声不响,但只要它一出问题,整个Wi-Fi驱动就会变成“看起来活着,实际瘫痪”的状态。
1. QRTR到底管什么:Wi-Fi驱动里那些看不见的消息通道
1.1 高通的“核间总线”是怎么串起AP和Wi-Fi固件的
很多人第一次接触高通Wi-Fi驱动时,都会下意识把它当成“PCIe网卡驱动”来处理。硬件形态确实支持这种理解:Wi-Fi模组通过PCIe或SDIO挂在主控上,驱动加载后注册一个net_device,然后内核网络栈正常收发数据。但如果你只盯着这个视角,遇到问题基本会走弯路。
高通SoC不是单一大核跑天下的架构。AP(应用处理器)、Wi-Fi/BT固件、Modem、ADSP、CDSP这些子系统各自有独立的内核或RTOS,它们之间需要一种跨处理器通信机制。PCIe在你眼里是“总线”,但在高通内部它只是承载物理数据的一条“路”。控制面消息、状态同步、能力查询、事件通知这些“带外信息”,走的往往是另一套逻辑通道。
这套逻辑通道的底层就是QRTR。它的全称通常写作Qualcomm Remote Transport,是高通私有的远程传输协议栈。在Linux内核里对应的是AF_QIPCRTR这个socket地址族,代码在net/qrtr目录下。如果你去看高通CAF(Code Aurora Forum)内核,会发现这个目录几乎每个平台都在,而且和Wi-Fi、Modem、音频DSP的驱动都有交集。
打个比方,QRTR相当于在一栋大楼里装了独立的内部对讲系统,而不是靠楼外的邮政系统寄信。PCIe、共享内存这些物理媒介只是“管道”,QRTR负责在管道两端把消息准确地交给要服务的那个“房间”。
1.2 从socket到服务发现:QRTR和TCP/IP像又不像
QRTR从接口形态上很像网络协议:它有socket,有地址,有收发函数;但它和TCP/IP有两个本质区别。
第一,QRTR不依赖IP地址和MAC地址。它使用的地址是node + port两个维度:node表示高通系统里的哪一个处理器节点,port表示该节点上的服务端口。你可以把它理解成一个园区里的“楼栋号+房间号”,而不是互联网里的IP。
第二,QRTR最核心的机制是“服务发现”。每个处理器上的服务启动后,会向一个全局的name service注册自己的服务名和端口。其他处理器或进程想使用这个服务,先去name service查一下,或者直接订阅该服务的公告,就能拿到端口并开始通信。
这个设计非常贴近“微服务”的思想,在高通这种多核异构场景下尤其好用。Wi-Fi驱动加载后,它可能需要向firmware查询能力集、上报主机状态、接收固件异常事件。这些消息在协议栈上可能就是一段很短的QRTR数据包,但对驱动能不能正常工作起着决定性作用。
1.3 Wi-Fi驱动里QRTR和QMI的分工
提到Wi-Fi驱动,经常会看到两个缩写一起出现:QRTR和QMI。不少刚开始搞的人会把它们混为一谈,这里需要理清楚。
QMI(Qualcomm MSM Interface)是高通的设备间消息接口协议,定义了消息的格式、方法和错误码。可以把它看成“业务层的语言”。而QRTR是QMI消息的载体之一,或者说是QMI跑在上面的“传输层信封”。在部分高通Wi-Fi 6/6E方案的ath11k驱动里,firmware和host之间的QMI控制通道就是通过QRTR承载的。你会在日志里同时看到qmi和qrtr相关字符串,其实是一回事:在QRTR这个信使身上背着QMI格式的信件。
所以,当你在Wi-Fi驱动日志里看到QMI请求超时,不要只盯着QMI层去查消息格式,也要回头看看QRTR层是否已经把信送到了。很多“QMI超时”问题,根子是QRTR服务订阅没生效。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 一次真实的QRTR排查:Wi-Fi驱动起不来的背后是服务订阅错位
2.1 故障现象:驱动加载成功但wlan0不出现
有一次调试的板子是用高通QM215平台配一颗Wi-Fi 6模组。开机后,系统日志里能看到Wi-Fi驱动模块加载成功,lspci也能看到设备,dmesg | grep wlan里也没有明显的致命报错。但执行ifconfig -a时,wlan0压根不在列表里。
更奇怪的是,我们反复reload驱动,偶发性能出现:十次里面有两三次wlan0能起来,其余全部失败。这种“碰运气”的现象在驱动开发里最让人头疼,因为它往往指向初始化时序问题。
当时我第一反应是看寄存器和中断,怀疑PCIe链路状态不对。但反复测试PCIe link training都是正常的,带宽、速率都没问题。后来有位同事提醒了一句:“你查过QRTR服务列表吗?”我这才把注意力从PCIe物理层转到了协议栈层。
2.2 排查路径:从dmesg到QRTR节点列表
排查QRTR问题,第一步永远是看内核里有没有相关报错。
bash复制dmesg | grep -i qrtr
dmesg | grep -i qmi
dmesg | grep -i wlan | tail -100
如果内核打开了QRTR调试,有时能看到服务注册、名字公告、连接建立等提示。但很多产品内核为了方便用户,默认把QRTR日志等级压得很低,所以dmesg看到的可能是一片干净。
接着要确认QRTR模块本身加载了:
bash复制lsmod | grep qrtr
cat /proc/modules | grep qrtr
如果编入内核而不是模块,可以用/sys/module/qrtr/parameters/或/proc/net/qrtr来确认。不同内核版本暴露的调试节点不完全一样,但通常会有类似的服务列表或节点信息。
然后就是用用户态工具去枚举当前QRTR上注册了哪些服务。
bash复制qrtr-lookup
这条命令会连接本机的QRTR name service,拉出一份类似“节点ID -> 服务ID -> 服务实例”的列表。如果Wi-Fi固件相关的服务不在列表里,说明firmware的服务注册没到达AP侧,或者AP侧的name service没有正确广播出去。
当时我执行qrtr-lookup后,看到列表里只有少量Modem相关服务,Wi-Fi相关的服务名称一个都没有。问题范围瞬间就缩小到了“服务发现”环节。
2.3 根因复现:name service公告和驱动订阅的时机竞赛
后面通过多次开关Wi-Fi、增加驱动日志、对比代码路径,终于定位到根因:Wi-Fi驱动在加载时会向QRTR name service订阅某个Wi-Fi服务,但firmware侧的服务公告在某个异常快的启动路径下已经提前发出,驱动订阅动作落在公告之后,就这样错过了。
QRTR的name service机制有点像小区里的公告栏。服务方上线后会在公告栏贴一张纸条,订阅方需要定期来看,或者事先登记好“有新纸条就通知我”。如果订阅方登记的动作比贴纸条的时间更晚,那张纸条不会自动补送。这在协议层面并不是什么bug,但在真正的SoC启动时序里,Wi-Fi固件启动、驱动加载、QMI控制通道建立这几个事件处在不同处理器上,彼此没有全局时钟同步,很容易出现先后错位。
我们的修改方案是在驱动确认firmware ready之后,主动调用一次QRTR服务查询,而不是只依赖被动订阅。这样即使第一次公告已经错过,后续依然能主动发现服务并重建通道。改完后连续测试三十多次,问题没有再复现。
这个案例让我深刻认识到:在高通Wi-Fi驱动里,QRTR的“服务发现”不是可选优化,而是整个控制面通信的地基。地基没对齐,上面盖什么都白搭。
3. 实操环节:把QRTR消息从“幕后”拉到“台前”
3.1 内核侧:确认QRTR是编译进内核还是模块
很多高通平台的源码里,QRTR的Kconfig开关是CONFIG_QRTR。有些内核版本还会拆成CONFIG_QRTR_SMD、CONFIG_QRTR_TUN、CONFIG_QRTR_MHI等子项,分别对应不同的物理传输后端。调试之前,先确认两件事:
- 当前内核是否打开了QRTR;
- Wi-Fi驱动依赖的QRTR后端是不是也被编进来了。
如果是编译成模块,但某个后端模块没加载,那Wi-Fi驱动在创建QRTR socket时可能报Address family not supported by protocol,或者干脆创建成功但消息发不出去。这类问题比服务时序问题更隐蔽,因为驱动代码本身没有崩溃,只是消息一直悬空。
3.2 用户态工具:qrtr-lookup和qrtr-send怎么用
linux-qrtr这组用户态工具在高通CAF树里很常见,AOSP里也经常能找到。常用的有这么几个:
| 命令 | 作用 | 典型用法 |
|---|---|---|
qrtr-lookup |
查询当前系统所有QRTR服务 | qrtr-lookup |
qrtr-send |
向指定节点/端口发送一段数据 | qrtr-send <node> <port> <message> |
qrtr-call |
尝试调用远端服务方法 | 依赖具体服务协议,不通用 |
以qrtr-lookup为例,如果Wi-Fi固件的服务已经上线,你会看到类似这样的输出行:
text复制node 2 port 3 service 0x1234 instance 0
node 2是Wi-Fi固件所在节点,port 3是服务端口,service 0x1234是服务ID。instance 0表示默认实例。
有些工具版本还支持按服务名方式查找,不过高通内部服务ID和名字的映射关系不总是对外公开,所以实际调试时经常要对着代码里的宏定义去查。
如果QRTR服务列表里什么都没有,先别急着怀疑Wi-Fi固件,检查一下Modem或其他子系统的QRTR服务是否还在。如果整个系统的QRTR服务都消失了,问题大概率出在公共的name service或共享内存传输通道上。
3.3 对照CAF代码:wlan驱动注册QRTR回调的典型位置
如果Wi-Fi驱动自己就是QRTR服务方,它通常会在probe流程中创建socket并绑定端口。典型代码路径类似于:
c复制int wlan_qrtr_init(struct wlan_ctx *ctx)
{
struct sockaddr_qrtr sq;
int ret;
ctx->qrtr_fd = socket(AF_QIPCRTR, SOCK_DGRAM, PF_QIPCRTR);
if (ctx->qrtr_fd < 0)
return -errno;
sq.sq_family = AF_QIPCRTR;
sq.sq_node = QRTR_NODE_AP;
sq.sq_port = ctx->qrtr_port;
ret = bind(ctx->qrtr_fd, (struct sockaddr *)&sq, sizeof(sq));
if (ret < 0) {
close(ctx->qrtr_fd);
return -errno;
}
/* 接收消息回调 */
ctx->qrtr_thread = kthread_run(wlan_qrtr_rx_thread, ctx, "wlan-qrtr");
return 0;
}
这里的AF_QIPCRTR和struct sockaddr_qrtr是内核公开的QRTR接口。很多驱动不会直接控制socket,而是通过高通的QMI封装库再包一层,但底层创建的还是QRTR socket。
在CAF源码里搜索时,可以重点看这几个关键词:
socket(AF_QIPCRTRqrtr_endpoint_registerQRTR_NODE_APstruct sockaddr_qrtr
如果你不是在改驱动,只是想定位问题,那么用系统自带的/proc或debugfs去观察服务节点是最快的。比如内核开了CONFIG_QRTR_DEBUG之后,有时会有/sys/kernel/debug/qrtr/ports之类的节点,能看到当前各个处理器的端口状态。不同平台差异较大,建议先在目标板上执行find /sys/kernel/debug -name '*qrtr*'和find /proc -name '*qrtr*'探一下。
3.4 常见的命令和日志组合
调试时我习惯把这几条命令组合成一组基线操作:
bash复制uname -a
cat /proc/net/qrtr 2>/dev/null
dmesg | grep -i qrtr
dmesg | grep -i qmi
qrtr-lookup
如果板子上没有qrtr-lookup,可以临时编译一个静态版放进去,或者用adb在Android环境里跑。很多高通方案的Android机器其实内置了QRTR调试工具,只是可执行文件路径藏在/vendor/bin/下面,不一定在PATH里。执行一下find /vendor/bin -name '*qrtr*'往往有惊喜。
4. 复盘QRTR协议栈的四个容易翻车的细节
4.1 不要把QRTR地址当成IP地址
这个问题在团队里出现过不止一次。有同事调试时看到QRTR的node和port,下意识画了个“192.168.x.x:8080”类比的图,然后拿着tcpdump去抓PCIe网卡上的包,折腾一晚上毫无收获。
QRTR不是IP协议栈,它的寻址空间和IP完全不同。它可能跑在共享内存、SMD、MHI或者PCIe之上,但从Linux网络栈的角度看,它属于独立地址族。你没法用常规的IP路由、ARP、ping那一套去操作QRTR。遇到QRTR问题,第一反应应该是“服务注册了吗?订阅了吗?消息发到哪个node的哪个port?”,而不是“路由表配了吗?防火墙挡了吗?”
4.2 不解决“订阅时机”问题,重启Wi-Fi可能只是碰运气
我遇到过很多团队,遇到Wi-Fi启动不稳定,最先想到的是在用户态脚本里反复rmmod/modprobe驱动。这种重启大法有时确实能恢复,但掩盖了真正的时序问题。
QRTR的name service机制天然带有“异步公告”的属性。服务方上线和订阅方注册之间,没有一个同步握手来保证“一定不漏”。所以驱动代码里最好主动查询或重试订阅。如果你在某个平台上一提到Wi-Fi不稳定,大家第一反应就是“重试一下”,那你要小心,这大概率是QRTR订阅逻辑没写好,而不是玄学。
4.3 不同平台、不同CAF版本的QRTR服务号可能完全不同
高通平台之间的QRTR服务分配经常不一致。同一个Wi-Fi服务,在SM8250平台可能是0x1234,换到SM8450平台可能是0x4321。如果你在代码里硬编码了某个服务ID,跨平台复用驱动时很容易“看起来驱动加载了,但事件永远收不到”。
解决方法是尽量通过name service按名称或服务类型去发现,而不是硬编码端口号。如果拿到的SDK里没有动态发现接口,那在做平台适配时一定要专门列一个“QRTR服务清单”的checklist,逐个平台确认服务ID和实例号。
4.4 tcpdump这类传统抓包方式在QRTR面前基本失灵
很多做网络出身的人,遇到协议栈问题第一反应就是抓包。但QRTR本身不是链路层协议,普通网卡上的tcpdump抓不到它。要让QRTR消息“显形”,只能借助内核调试接口、驱动自带的日志开关,或者在高通内部的工具链里开trace。
如果你发现某条命令在Wi-Fi固件事件触发时没有任何网络包输出,不代表事件没发生,更可能是你观察错了层面。这时候去翻驱动代码里QRTR消息入口的日志开关,往往比在网络上打点更有用。
5. 如果真要动QRTR:从改代码到调试的实用路径
5.1 在内核里找到QRTR相关配置和代码入口
想在Linux内核中基于高通平台做二次开发,先找到QRTR目录:
bash复制ls net/qrtr/
你会看到类似qrtr.c、qrtr_endpoint.c、qrtr_mhi.c、qrtr_smd.c这样的文件。其中qrtr_endpoint.c抽象了“QRTR端点”的概念,不同的物理传输后端只需要提供收发函数,就能把数据接入QRTR核心。
Wi-Fi驱动侧一般不在net/qrtr里,而是通过标准socket API来通信。也就是说,你不需要改动QRTR核心代码,只要在驱动里创建socket并注册service就行。除非你要新增一种物理传输通道,才需要动qrtr_endpoint层。
5.2 自定义一个简单的QRTR服务做验证
有时候为了验证QRTR在目标板上是否正常工作,我会写一个极简的内核模块,自己创建一个QRTR服务节点,然后从用户态发消息去测试。这样能确认协议栈本身是通的,排除Wi-Fi驱动的干扰因素。
一个简化版的服务端伪代码大致长这样:
c复制static int test_qrtr_init(void)
{
struct sockaddr_qrtr sq;
int s;
s = socket(AF_QIPCRTR, SOCK_DGRAM, PF_QIPCRTR);
if (s < 0)
return -EINVAL;
memset(&sq, 0, sizeof(sq));
sq.sq_family = AF_QIPCRTR;
sq.sq_node = 1;
sq.sq_port = 0x7777;
bind(s, (struct sockaddr *)&sq, sizeof(sq));
/* 然后启动一个线程接收消息 */
return 0;
}
用户态测试同样用socket:
c复制int fd = socket(AF_QIPCRTR, SOCK_DGRAM, PF_QIPCRTR);
struct sockaddr_qrtr saddr = {
.sq_family = AF_QIPCRTR,
.sq_node = 1,
.sq_port = 0x7777,
};
sendto(fd, "ping", 4, 0, (struct sockaddr *)&saddr, sizeof(saddr));
如果消息能收到,说明QRTR底层没问题。这一招能在最短时间内把“QRTR协议栈坏”和“Wi-Fi驱动自己的状态机坏”区分开。
5.3 实战调试建议:先查服务,再看消息,最后改代码
我在实际调试中最常用的顺序是:
qrtr-lookup -> 确认服务在不在;如果服务在,再开驱动日志确认消息有没有收到;如果消息没收到,再查驱动里的订阅和过滤逻辑;最后才轮得到改代码。
很多新手朋友一上来就加打印、改超时、调优先级,结果越调越玄。QRTR的调试和普通网络协议栈不一样,它的核心是先确认“服务注册”和“服务发现”是否成功。这两个环节没有打通,后面所有消息传递都是空谈。
另外,不同平台对QRTR的调试开关差别很大,建议不要把一套平台上的调试日志配置照搬到另一套平台。每换一个平台,第一件事应该是把该平台SDK里默认的QRTR日志开关逻辑读一遍,搞清楚哪些服务是开机自启动、哪些服务是Wi-Fi驱动加载时才注册的。如果能画出一张“Wi-Fi启动时QRTR事件时序图”,很多问题都会变得非常清晰。
我个人在调这类问题时的习惯是,先让板子上的QRTR服务列表稳定复现,再去动Wi-Fi驱动代码。记住,QRTR本身不是应用,它只是那个躲在幕后递话的信使。信使跑错了门,你再怎么喊话也传不到收件人手里。
