高通平台新增QMI接口实战:协议规划、实现链路与关键细节
2026/9/7 14:34:11 网站建设 项目流程

简介:面向高通平台基带、协议栈及应用层开发者的技术说明,聚焦在现有QMI框架中新增接口时最易出错的两个环节——IDL生成后的消息体(message)定位与类型表(type table)关联,从高通的IDL编译生成原理出发,系统梳理了二者在QMI_IDL_TABLE_MESSAGE_FLOW中的指向关系,能有效帮助开发者跨过初始接触时常见的表结构理解障碍。资源为单个docx格式的独立文档,压缩包总大小仅291KB,内容精炼、层级分明,适合作为案头速查手册随时翻阅。目前已有12212人学习或下载,足以印证该主题在实际项目中的高关注度与实用价值。文档结合LE2.2 GNSS DRV FLOW实例,围绕uim_remote_message_table_v01、loc_message_table_v02等具体表项展开,逐一说明新增消息必须加至message_table末尾、消息类型由其在message_table中的位置决定等关键规则,并明确user_data类型在uim_remote_type_table_v01及referenced_tables中的索引含义,特别是如何在uim_remote_message_table_v01中定位FLC_QMI_EFS_WRITE_USER_DATA_RESP_V01等响应消息,为规范化扩展QMI接口提供了直接可用的排错思路,有助于开发者在面对同类表结构时实现举一反三。

1. 被业务倒逼出来的一次接口新增

在高通平台做底层通信开发的人,早晚都会撞上QMI接口。我这次接到的需求是:上层应用希望实时读取Modem侧某个私有状态值,既不想走AT指令那种“一问一答”的慢路径,又不愿意把数据通过debug口偷偷拉出来。想来想去,最合适的方案就是新增一个QMI接口,把这条私有通道正规化。QMI接口在高通平台上并不是什么新鲜概念,它负责AP侧和Modem侧之间的控制面通信,但真正动手在现有协议栈上增加一个新功能时,需要处理的问题比想象中多不少。这篇文章我会把增加QMI接口的关键点拆开讲,结合我在CAF内核和用户态框架上踩过的坑,尽量让准备动手的同行少走弯路。

1.1 为什么不用AT,也不用DIAG

接到需求后,我的第一反应不是提QMI,而是先评估已有的几种通道。AT指令是最容易实现的,Modem里通常已经支持大量扩展AT命令,上下行都是纯文本,调试也方便。但它有两个硬伤:一是交互模式偏串行,高频率轮询或并发请求时吞吐量上不去;二是AT通道的语义太简单,不适合承载结构化参数,比如多个字段嵌套地传回AP侧。

DIAG是另一种选择,它适合在调试阶段抓log、做校准,本质上带有工程口属性。如果业务逻辑直接走DIAG长期运行,会占用调试通道,还会给产线和后期的故障定位带来麻烦。QMI接口恰恰站在两者中间:它是一套有层次、有TLV编码、有异步事务ID的协议,可以把复杂请求塞进单个消息里,同时提供可靠的错误码和指示上报能力。对产品化需求来说,QMI是更正规的承载通道。

1.2 QMI接口在整个链路里的位置

很多刚接触QMI的同事容易把它理解成一组socket或几个ioctl,其实它远不止这些。从AP应用层往下数,大致是:应用进程通过libqmi或qmi-framework发起调用,经由QRTR或共享内存传输层到达Modem侧对应的QMI服务进程,服务端解析消息后调用Modem内部模块,最后把结果再沿原路返回。整个过程里,每一个环节都要遵守QMI协议格式,但真正决定“新接口能干什么”的,是服务端的处理函数在哪里实现。

这个定位很重要。增加QMI接口,不是简单地在AP侧拼一个包发出去就完事,而是要决定从哪个层级开始改。改AP侧的用户态库,可以快速验证协议;改内核的QRTR收发逻辑,会影响所有QMI服务;真正要落地业务,还得在Modem侧添加对应的服务处理逻辑。理解了这个链路,后面的规划和实现才会有明确边界。

2. 动手之前先定三张表:ID、TLV、事务号

QMI是一种看似松散、实际细节很多的协议。如果拿到需求就急着写代码,很容易在协议解析上栽跟头。我的习惯是先把三张表定下来:服务ID与消息ID对应关系、TLV类型规划、事务号使用规则。这三张表不只是在开发时有用,后续联调、认证、维护阶段都要靠它们对齐Modem侧和AP侧的实现。

2.1 服务ID与消息ID规划表

每个QMI服务都对应一个唯一的service ID,服务内再划分不同message ID。高通官方已经有大量默认服务,例如DMS、WDS、PDS等。新增接口时,除非确实要做一个全新的能力域,否则优先挂到已有相关服务下,减少服务注册和发现层面的改动。若必须新建独立服务,service ID就要尽量避开高通运营商的保留段,选择厂商自定义区域,并确保整机软件里不冲突。

我在规划时通常会用一张表格把每个消息的编号方向列清楚:哪一个是请求,哪一个是响应,哪一个是主动指示。例如新服务0x60,请求消息ID是0x01,对应响应ID是0x02,状态变化指示ID是0x03。这样做的好处是,不管后续在Modem侧还是AP侧查代码,都能用这张表快速定位报文,不需要翻几十个源文件。

2.2 TLV结构:把可变数据塞进固定协议

QMI消息的核心是TLV,即Type-Length-Value。每条请求可以携带多个TLV,每个TLV里先是一个字节的type,然后是两个字节的length,后面跟着真正的内容。定义新接口时,要和Modem侧同事约定好每个TLV的type值、长度含义以及字段排列方式。

尤其是变长字段,最容易扯皮。比如传输一个字符串参数,QMI规范里通常明确字符串本身的length字段指的是字节数,且不包含结尾的空字符。如果AP侧传了一个带“\0”的字符串,Modem侧又按固定长度读取,就会出现长度错位或者尾部残留数据。定义TLV表时,我会把“最大长度、是否必须携带、字符串编码方式”这三点直接写进协议文档,避免实现阶段来回扯。

2.3 事务号与异步响应

QMI天然是异步协议。发送请求时会分配一个事务号,响应报文中携带同一个事务号,调用方才能把结果匹配到正确的请求上。很多定制接口出问题,表面是“响应超时”,实际是事务号生命周期没管理好,要么重用了未释放的号,要么在回调里丢失了上下文。

在规划阶段就要明确事务号的分配范围:是把事务号绑定到单独的客户端句柄,还是全局统一分配。对于低频的私有接口,我倾向于单独维护一组事务号,并且给请求设置超时时间。这样即使Modem侧真的出现长时间处理,AP侧也能主动报错,不会把整个客户端机制拖死。

3. 新增接口的服务端实现:三条链路各有取舍

把协议表定完,接下来就是选实现路径。高通平台上增加QMI接口,服务端可以放在用户态、内核态,也可以直接落在Modem侧固件里。这三条链路不是随意选的,它们决定了调试效率、稳定性边界以及后续维护成本。

3.1 纯用户态:libqmi与qmi-framework

在Android或嵌入式Linux环境里,最快的方式是用libqmi或qmi-framework在用户态新增一个服务处理模块,监听QRTR上对应service ID的socket,然后解析消息并返回结果。这个方案的优点是开发效率高,可以直接复用现成的编解码框架,出现问题也好用gdb或日志定位。

缺点也同样明显:用户态服务依赖系统正常启动,如果业务要支持Modem冷启动早期的查询,用户态服务可能还没拉起,接口自然不可用。另外,用户态进程一旦崩溃,QMI服务就断了,上层请求会全部失败。对稳定性要求很高的产品场景,我通常不会把关键接口只放在用户态,而是让用户态作为辅助路径。

3.2 内核态:QRTR Socket裸发

如果要求接口在开机早期就可用,或者需要直接从内核模块发起Modem请求,那就得绕过用户态框架,在内核态创建QRTR socket并发送QMI消息。这条链路离协议栈更近,收发延迟更低,也不受用户空间重启影响。

但裸写解析逻辑很费劲。QMI报文的TLV解析、事务号匹配、超时重传,这些原本由libqmi做好的事情,在内核态都要自己实现一遍。而且要特别小心内核态内存分配,不能频繁在数据包里做变长头申请,否则很容易引入内存碎片。我自己的经验是:内核态适合做“透传优先”的简单请求,一旦业务逻辑复杂,还是交给用户态更稳妥。

3.3 Modem侧处理与接口真正的落点

上面两种路径只是解决了“AP侧怎么把QMI消息送到Modem”,真正要读取的私有状态往往存放在Modem侧内存或NV项里,所以Modem固件内部也要实现对应的处理模块。这一步通常需要接触高通的Modem源码工程,编译独立的modem镜像,再通过烧录工具整包部署,改动成本和风险都比AP侧高很多。

很多团队会在这里犯一个认知错误:以为AP侧自定义一个服务ID,把同一段消息映射到Modem侧的某个已有处理函数就算完事。实际上,Modem侧的QMI框架会检查服务ID和消息ID的合法性,不在服务范围内的请求会被直接丢弃。也就是说,新增接口的工作量至少一半在Modem侧。只有把Modem侧的处理函数、错误码映射、日志打印全部打通,整个接口才算真正落地。

4. 编码实现时最容易翻车的四个细节

把路径选好、协议表定好,进入编码阶段后,依然有一堆细节等着埋人。下面这四个细节,我都出过线上或联调事故,写出来给大家提个醒。

4.1 字节序、对齐与TLV长度计算

QMI协议与底层硬件采用的字节序保持一致。在高通平台上,通常是小端序。很多解析问题都出在结构体对齐上,特别是当你在C语言里用结构体直接映射报文时,编译器默认会插入填充字节,导致读取偏移错位。

我的习惯是显式使用__attribute__((packed)),并对单个字段用宏或函数手动取字节,而不是直接强转结构体指针。比如下面这个TLV头部的定义:

struct qmi_tlv_header { uint8_t type; uint16_t length; uint8_t value[]; } __attribute__((packed));

length字段的计算不能直接写sizeof,因为value是柔性数组,sizeof(struct qmi_tlv_header)只代表头部长度。正确做法是先固定头部大小,再根据实际内容长度填充。如果用了编译器的自动对齐,发送端和接收端只要一边开了packed一边没开,就会解析出完全错误的数据。

4.2 字符串与变长数组的越界风险

QMI协议对字符串的处理方式和普通C字符串不同。它会用独立的length字段表示字符串数据长度,而不依赖结尾的“\0”。在AP侧收到响应后,如果直接调用strlen去计算字符串长度,极有可能读到Buffer尾部以外的内存。

正确做法是:收到TLV后,先取出length字段,动态分配一个长度为length+1的buffer,把数据拷贝进去并在末尾补“\0”,再交给上层使用。发送方向同理,只填充实际字符串字节,不要在TLV里多塞一个空字符。只要两边对length定义一致,基本不会出越界问题。

4.3 Indication的注册与生命周期

QMI除了请求-响应模型,还有主动上报的Indication机制。Modem侧某个状态发生变化时,会主动往AP侧推消息。新增这类接口时,容易忽略的是客户端需要走“订阅/使能”流程,而且Modem重启后订阅关系会丢失。

如果上层在重启后没有重新订阅,用户就会看到“偶发收不到主动上报”的怪象。排查这类问题,不要只盯Modem侧有没有发,还要确认AP侧客户端在QRTR端口更换或服务重新注册后,是否触发了重新订阅流程。把订阅逻辑挂在服务注册通知回调里,是目前比较稳妥的做法。

4.4 错误码:不是“能回包”就代表成功

QMI的响应消息分为两层:传输层成功只代表Modem收到消息并回了包,不代表业务执行成功。真正的执行结果要看响应里的QMI错误码字段。比如请求读取一个不存在的NV项,Modem可能正常返回一个response,但包里的错误码是QMI_ERR_INVALID_ARG

我以前遇到过联调双方各执一词的情况:AP侧说“我收到响应了”,Modem侧说“我返回错误码了”。其实就是AP侧解析代码没检查错误码,只看到包就认为成功。建议在自己的QMI客户端框架里增加一道默认检查,凡是返回包必看错误码,非成功状态直接抛错,避免业务上层误判。

5. 从编译到验证:把新接口真正用起来

接口代码写完,真正的考验才开始。一个新增QMI接口从代码合并到最终可被业务调用,中间涉及编译范围、镜像部署、日志工具和回归测试。这块做得越规范,排障成本越低。

5.1 编译范围决定排障效率

如果只改了AP侧的用户态框架或自定义服务,编译范围通常集中在system/vendor分区,单独push可执行文件就能验证,迭代速度很快。如果Modem侧也改了,情况就不一样了。Modem编译耗时长,还需要把生成的mbn或bin包烧进对应分区,一旦出现问题就要重新整包烧录,调试周期会拉长。

所以我的实施策略是“AP侧先行验证协议格式,Modem侧再固化实现”。先在AP侧用假数据模拟响应,等AP侧的收包解析逻辑都OK了,再去动Modem侧代码。这样把链路拆成两段,哪一段出问题,定位范围直接砍半。

5.2 用现成工具与新写的客户端做双向确认

验证新接口时,不能只依赖业务上层那套调用。最好自己写一个独立的命令行小工具,直接构造QMI消息,发到目标服务,并打印原始返回包。高通平台上有一些现成工具可以用,但不要忘记它们很多都是针对标准服务开发的。对自定义服务,往往需要用自己的代码来验证。

我通常的做法是三步走。第一步用log工具确认Modem侧收到了请求;第二步在Modem侧加临时日志,打印传入参数和传出响应;第三步在AP侧打印收到的TLV原始字节,与Modem侧发送的内容逐字节比对。只要这三步数据一致,接口基本算真正打通。

5.3 发布前建议做一轮回归

新增接口之后,哪怕改动不大,也建议做一轮与Modem状态相关的回归。比如Modem重启、AP侧进程重启、飞行模式切换、异常断电恢复,这几种场景最容易暴露出事务号丢失、订阅失效和socket重建问题。很多定制QMI接口在正常开机流程里一切正常,但一旦遇到重启或恢复,就出现响应超时或内存泄漏。这些问题如果在联调阶段不抓出来,到量产阶段会非常被动。

回归测试的用例最好固化成脚本,每次构建后自动跑一遍。我见过太多项目因为“这次只加了一个小接口”就跳过回归,结果积攒到后期爆出一堆协议栈稳定性问题,这个成本远比多跑一轮测试要高。

6. 几次实战后我觉得最关键的事

QMI接口的新增从表面看是一个工程任务,实际上更像一次“协议设计+嵌入式实现+系统稳定性”的综合考验。如果只盯着代码能不能编过,接口能不能通,后续维护时很容易被各种边角问题反复纠缠。

我最深的体会是:新接口一定要做成可开关、可降级。上线初期可以用配置项控制这个接口是否注册、是否响应,遇到Modem侧异常时可以快速关闭,而不需要紧急出一个新镜像。同时,所有关键打印要带服务ID和消息ID前缀,这样在大量系统日志中过滤时,能一眼看到新接口自己的运行轨迹。

另外,别忽略文档。每次新增QMI接口,我都会把第二章节那些表整理成一份短文档,附上Modem侧和AP侧的源码位置。半年后再回来改代码时,这份文档能省掉一半的回忆时间。接口本身写得再好,如果只有代码没有说明,对团队来说仍然是一笔沉重负担。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询