ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Android GSMMUX 内核驱动移植与调试实战:从串口复用到多路通道

Android GSMMUX 内核驱动移植与调试实战:从串口复用到多路通道 简介这是一份围绕GSMMUX driver for Android的驱动源码与说明文档面向安卓系统底层开发、驱动移植及硬件适配工程师。资源聚焦GSMMUX复用器驱动讲解如何在设备上创建和管理Mux将多路输入信号合并为单路输出实现GPIO、串口等硬件通道的复用与切换帮助读者理解驱动注册、硬件探测、读写操作等核心机制也适用于学习Linux设备驱动模型的开发者。压缩包共10个文件整体约220KB。内容以C源码和头文件为主可展开驱动初始化和I/O操作细节Android.mk与Makefile提供编译配置方便集成到安卓系统README和PDF文档说明GSM0710驱动协议及Mux使用方式另含备份文件参考结构紧凑且各文件分工明确。已有185人学习下载。通过此资源可系统梳理中断处理、同步互斥、用户空间接口、电源管理及设备树配置等关键知识点对照源码与文档还能掌握Mux初始化流程和硬件交互细节对安卓底层开发、硬件适配与驱动调试都具有直接参考价值。1. 项目概述GSMMUX driver 到底是什么Android 里为什么用得着GSMMUXGSM 07.10 Multiplexer在 Android 生态里属于那种“平时没人提但遇到问题能把人逼疯”的内核驱动模块。很多做车机、做工业平板、做双卡双待方案、做智能对讲机和扫码支付终端的兄弟迟早都会在 dmesg 里、或者内核 defconfig 里跟它打照面。简单说GSMMUX 是一套运行在 Linux/Android 内核里的多路复用协议实现。它的底层标准是 ETSI 的 GSM 07.10 规范本来是用在 GSM 通信模块和主机之间让一条物理串口UART能同时承载多路逻辑通道。打个不那么严谨但很好懂的比方普通串口像一条单车道公路一次只能跑一辆车GSMMUX 像是给这条单车道画上了分道线变成了能并排跑多辆车的多车道公路。每条车道也就是 DLCIData Link Connection Identifier可以独立收发数据互不干扰。在 Android 设备上GSMMUX 最常见的三个使用场景我列一下你们感受下双卡双待方案一颗 4G/5G Modem 和手机主控之间只有一条物理 UART但是 Android 的 RILRadio Interface Layer需要同时跟 Modem 上的两个协议栈通信比如卡一跑数据、卡二跑语音单串口根本扛不住需要用 MUX 把串口拆出来两个独立逻辑通道。蓝牙共用串口场景某些物联网方案中蓝牙芯片和蜂窝 Modem 共用同一路 UART这时 MUX 能把物理串口切成多个虚拟口一个给蓝牙 HCI一个给 AT 指令通道。外接模块扩展场景像工业扫码模块、金融 POS 终端的密码键盘、二代身份证读卡器这类 RS232 外设如果主板串口不够用也能用 MUX 扩展出多个逻辑串口来挂设备。写这篇文章的初衷是因为我发现中文社区里关于 GSMMUX 在 Android 上移植、调试、踩坑的内容几乎为零而英文资料又散落在一堆内核邮件列表和厂商 BSP 文档里。这篇文章会把我在实际项目中验证过的移植步骤、用户态配置方法、以及那些隐藏很深的坑一次性交代清楚适合正在调车机/工业板卡/通信模块的 BSP 工程师也适合想搞懂 Android 串口复用的应用层开发同学。2. 核心原理GSM 07.10 复用协议的帧格式与工作流程2.1 协议栈定位与帧结构GSM 07.10 在蓝牙和蜂窝通信领域都有很广泛的应用——蓝牙的 HCI 三线 UART 传输层用的就是它的变种。GSMMUX 驱动的实现位置在内核的drivers/tty/n_gsm.c归入的是 tty 子系统或者说它是一个 line discipline线路规程。内核里的线路规程这个概念很多只做应用开发的 Android 工程师会比较陌生。线规程是串口驱动之上的一层抽象它负责解释从物理串口拿到的一串裸字节流。默认的N_TTY线规程就是把字节流原样交给用户态而N_GSM0710GSMMUX 使用的线规程则会把字节流切分成帧根据帧头解析出是哪个逻辑通道的数据再分发到对应的ttyGSM*虚拟终端上。GSM 07.10 帧结构并不复杂内核驱动里有现成的定义。基本格式分三层地址字段Address Field1 个字节包含 C/R 位命令/响应和 6 位的 DLCI 号另外有一位 EA 扩展位。DLCI 的取值范围是 0 到 63但基本复用方式Basic Option只支持到 7高 DLCI 需要走高级复用Advanced Option。控制字段Control Field和 HDLC 类似包含 SABM、UA、DISC、UIH 等帧类型。SABM 用于建立一条逻辑通道UA 是对端确认DISC 用于释放通道UIH 用于传用户数据且不要求应答。长度字段Length Field1 或 2 字节指明后面数据部分长度。整个帧还需要做位填充bit stuffing来保证 0x7E 这种帧定界符不会在数据里出现内核的gsm_dlci_data_output等函数里已经把这些底层细节全部处理掉了驱动使用方不需要关心。2.2 数据路径与虚拟 TTY 设备映射理解了帧结构之后GSMMUX 驱动在 Android 侧的工作流程可以拆成四步物理串口第一次打开时驱动会注册一个 line discipline。用户态通过tty子系统把物理串口从默认的N_TTY切到N_GSM0710。这一步打通了物理层。用户态通过ioctl(fd, GSMIOC_ENABLE, conf)发出启用 MUX 的指令配置结构体里可以指定n_gsm协议模式、初始化标志、是否启用高级复用、最大帧大小等。驱动收到 GSMIOC_ENABLE 后会在内部枚举出ttyGSM0、ttyGSM1……这一组虚拟串口设备节点。这些节点对应 DLCI 1、DLCI 2 等逻辑通道。DLCI 0 永远保留给控制通道用于发送 SABM、UA 这类信令帧。用户态拿到ttyGSM*节点后对这些虚拟串口的读写会被驱动封装进 UIH 帧通过物理串口发到对端设备对端返回的数据帧被驱动解包后再分发回对应的虚拟 tty。整个数据路径全在内核态完成用户态感知到的就是操作几个独立串口而已。2.3 control channel 与数据通道的关系这里要专门强调一下 DLCI 0 的作用很多第一次上手的人理解不了为什么自己明明只开了两个数据通道dmesg 里却出现了三个 tty 设备。DLCI 0 是协议强制保留的控制通道负责管理其他数据通道的建立、释放和参数协商。SABM 帧里的 DLCI 字段指向哪个 DLCI就表示要激活哪条通道。控制通道不承载业务数据但它在 MUX 生命周期内必须保持畅通。实际调试时有一个快速检查技巧如果 DLCI 0 通道建立失败不管是 AT 指令还是 PPP 拨号所有数据通道都会跟着失败。先查控制通道再查数据通道可以少走很多弯路。3. Android 内核侧移植从 defconfig 到设备树配置3.1 内核选项开启与编译配置GSMMUX 依赖的是 Linux 主线内核里的n_gsm驱动Android 内核只要不是精简得太离谱基本都保留了这个驱动的源码关键看配置有没有打开。相关内核配置项主要有三个CONFIG_N_GSMy CONFIG_TTYy CONFIG_INPUT_UINPUTy // 部分厂商BSP依赖非必需在 Android 内核里不同厂商放配置的地方不一样。高通的 BSP 一般在arch/arm64/configs/vendor_defconfig或者arch/arm64/configs/defconfig里MTK 的平台通常在arch/arm64/configs/*_defconfig里。打开方式就是去掉CONFIG_N_GSM前面的#注释或者手动追加一行CONFIG_N_GSMy然后完整编译内核。编译完成后检查生成的内核印象文件里是否真有这个驱动可以用# 解压 boot.img 后用 strings 检查内核镜像 strings kernel | grep -i gsm如果输出里能看到n_gsm、gsmld、ttyGSM这些关键字说明驱动编译进去了。有个坑是部分厂商会把这个驱动编成模块CONFIG_N_GSMmAndroid 的 ramdisk 里又不会自动加载就会导致整个功能不可用。我见过不止一个项目死在“内核配置看着是开了但/dev/ttyGSM*就是不出现”这种诡异问题上。建议直接编进内核而不是模块。3.2 平台数据与设备树绑定GSMMUX 本身不是一个平台驱动它不需要在设备树里专门声明一个节点。它依附在某个具体的物理串口比如ttyS0或ttyHS0之上通过 line discipline 机制动态绑定。因此设备树层面需要保证的是物理串口本身的status okay、pinctrl正确、以及时钟和 DMA 配置没问题。有些厂商的 BSP 为了适配特定 Modem 芯片会在设备树里加一个mux或者bluetooth子节点然后在对应的串口驱动里做特殊处理。这里要留意厂商的drivers/tty/serial/msm_serial.c或8250驱动补丁它们可能会拦截GSMIOC_ENABLEioctl 做额外配置。如果换了一颗芯片原来的 BSP 里还残留旧的 MUX 钩子要及时清理否则会出现莫名其妙的数据错乱。3.3 物理串口选型与流控配置物理串口选哪一路通常由项目硬件原理图决定。MUX 对物理串口的波特率有要求因为一条物理链路要承载多路逻辑通道等效带宽是之前的好几倍。比如 Modem 和 AP 之间的物理串口配 115200bps开一路 AT 通道加一路 PPP 数据通道后数据吞吐会明显跑不满要开两路以上逻辑通道物理串口最好提到 460800 或 921600。流控配置在 MUX 场景下是个重灾区。GSMMUX 驱动依赖硬件流控RTS/CTS来避免物理层数据溢出。如果串口驱动没正确配置CRTSCTS高负载场景下会出现字符丢失、帧校验失败表现就是 AT 指令偶尔超时、PPP 拨号频繁掉线。打开 MUX 之前至少要在 shell 里确认一次串口的当前配置# 在 adb shell 里检查串口参数 stty -F /dev/ttyS0 -a-crtscts后面是否带负号直接决定了硬件流控开没开。没有硬件流控的板子必须想办法在 Modem 侧或外设侧做软件流控补偿否则 MUX 链路很难稳定工作。4. 用户态通道配置与实操流程4.1 最小可运行示例手动挂载 line discipline我们项目里在调试阶段用的是一套最朴素的 shell 流程不依赖厂商私有工具先验证驱动和物理通路是否正常。这个流程建议你们也保留下来出问题的时候做基本隔离特别好用。第一步把物理串口从默认线规程切到 N_GSM0710。这里需要一个小工具gsmts或者用 Android 里的ldattach。系统自带的话# 假设物理串口是 /dev/ttyS0 # 切换到 N_GSM0710编号 30 ldattach 30 /dev/ttyS0注意ldattach在 Android 的 toybox 里不一定有很多板子上是没有这个命令的。我们当时是找了个静态编译的gsmts小工具或者干脆用 C 代码直接调ioctl(TIOCSETD)几行代码就能搞定。#include linux/tty.h #include sys/ioctl.h #include fcntl.h #include unistd.h int main(int argc, char **argv) { int ldisc N_GSM0710; int fd open(argv[1], O_RDWR | O_NOCTTY); if (fd 0) return 1; if (ioctl(fd, TIOCSETD, ldisc) 0) { perror(TIOCSETD); return 1; } close(fd); return 0; }编译的时候注意要包含linux/tty.h交叉编译工具链里如果没有这个头文件可以用linux/tty_flip.h替代路径查找。总之只要能成功调通TIOCSETD说明线规程已经挂上。第二步启用 MUX 并创建 DLCI 通道。这一步可以用内核自带的mux_tty或者gsmuxd工具也可以用写得极其简单的 C 程序#include linux/gsmmux.h #include sys/ioctl.h #include fcntl.h #include string.h #include unistd.h int main() { struct gsm_config conf; int fd open(/dev/ttyS0, O_RDWR | O_NOCTTY); if (fd 0) return 1; // 读取并修改配置 if (ioctl(fd, GSMIOC_GETCONF, conf) 0) { perror(GETCONF); return 1; } conf.init 0; // 由本地主动发起 SABM conf.encapsulation 0; // 基本选项 conf.baud_rate 921600; if (ioctl(fd, GSMIOC_SETCONF, conf) 0) { perror(SETCONF); return 1; } if (ioctl(fd, GSMIOC_ENABLE, NULL) 0) { perror(ENABLE); return 1; } close(fd); return 0; }启用成功后/dev/ttyGSM0、/dev/ttyGSM1这些设备节点就会出现在/dev下。可以用ls -l /dev/ttyGSM*确认。第三步验证通道。虚拟 tty 和物理 tty 一样可以用stty配置可以 open、read、write。可以用一个很土的验证办法把 AT 指令从 DLCI 1 通道发出去看 Modem 返回OKecho -e AT\r /dev/ttyGSM1如果至少有OK或者ERROR返回说明 DLCI 1 通道已经建立而且 Modem 侧解析正常。如果完全没响应先量物理串口的 TX/RX 波形再查 Modem 是否配置成自动接受 MUX 的模式最后查驱动有没有报错。4.2 Android 系统集成启动脚本与权限控制手动流程验证 ok 之后就要做成开机自启。在 Android 的init.rc里写一个服务让系统在串口驱动加载完成后自动挂 MUX 并创建通道service gsmmux /system/bin/gsmuxd class main user root group system seclabel u:r:gsmuxd:s0 oneshotSELinux 策略在 Android 上是个坑新版本对/dev/ttyGSM*节点的访问控制很严。如果 RIL 进程要打开这些虚拟串口必须给对应的 te 文件加规则allow ril_device ttyGSM_device:chr_file rw_file_perms;具体规则写法看厂商的 SEPolicy 版本Android 9 之后可能还要配合file_contexts给虚拟 tty 节点打标签。没有权限的问题在 logcat 里会显示Permission denied同时avc: denied日志带上scontext和tcontext照着加规则即可。4.3 多通道应用场景的时延与缓存调优多路逻辑通道跑起来之后时延问题常被业务侧嫌弃。MUX 通道数据的调度完全靠内核的 tty 层排队如果物理串口波特率本来就低多通道同时收发时会出现明显的“链路饥饿”。实际项目里我们用的调优手段不外乎这几种提高物理串口波特率这是性价比最高的方案能跑 921600 就不要停在 115200。加大tty内核缓冲区部分 BSP 的RX_BUF_SIZE和TX_BUF_SIZE默认值太小在drivers/tty/n_gsm.c里GSM_TTY_MAGIC附近的GSM_RX_BUFFER_SIZE可以调大重新编译内核验证。如果对某个 DLCI 的实时性要求高可以在用户态单独开一个高优先级线程钉死在这个串口上减少调度抖动。5. 常见问题与排查技巧实录5.1/dev/ttyGSM*节点不出现的原因链现象最普遍原因也最多。从外到内一层层查物理串口本身是否正常。echo test /dev/ttyS0后用示波器看 TX 引脚有没有波形或者回环测试一下。串口本身就是坏的后面全白搭。ldattach或者TIOCSETD是否真的成功。有个隐蔽的问题ioctl(TIOCSETD)在串口被其他进程占用时并不一定立刻失败但 MUX 驱动初始化可能没跑完。dmesg里如果有gsm: ... line discipline already in use之类的报错找出来占用进程杀掉再试。用户态调用GSMIOC_ENABLE之后驱动会去打开物理串口的底层硬件。如果当时串口的 baud rate 和 Modem 侧不匹配SABM 帧发出去之后对端要么不回 UA要么返回的错误帧导致驱动直接放弃建立 DLCI。这时需要把串口速率两端都核对一遍。5.2 AT 指令有去无回协商失败与唤醒问题AT 指令发出去ttyGSM1那边一点动静都没有这在我调试过程中是第二高频问题。Modem 侧的 MUX 功能没有打开。很多模组默认工作在普通的 AT 透传模式不会主动解析 SABM 帧。需要先通过普通串口发ATCMUX0之类指令打开 MUX 模式然后再切 line discipline 和建立通道。不同模组指令差异很大有的要ATCMUX3进入高级模式有的直接支持ATCMUX0。查模组手册。物理串口的休眠策略搞鬼。很多 SoC 的串口有 autosleep链路空闲后驱动会把时钟关掉来省电SABM 帧发出后进不了唤醒流程对端响应收不到。内核串口驱动里通常有NO_AUTOSLEEP或者WAKEUP配置或者干脆通过 GPIO 拉高某根线来禁止休眠。帧格式不匹配。GSM 07.10 有两种模式基本选项Basic Option和高级选项Advanced Option。大多数 Modem 默认用基本选项你把conf.encapsulation设成 1两边就不在同一个频道上。读取配置的时候务必拿GSMIOC_GETCONF先看一眼当前值再结合对端能力去SETCONF。5.3 PPP 拨号掉线与吞吐上不去如果 DLCI 通道能建立、AT 指令交互也正常但 PPP 拨号一上去就掉线优先查 MTU。GSMMUX 每个 DLCI 的默认最大帧大小可能只有几十字节PPP 报文会被反复拆分重组任何一帧丢失都可能造成 PPP 链路崩溃。可以尝试在gsm_config里调整mru接收最大单元和mtu发送最大单元或者干脆改大内核n_gsm.c里MAX_MTU的宏。注意物理侧和逻辑侧都要匹配Modem 侧也有限制。我们项目里最后把 MTU 定在了 512PPP 带上 1500 的 IP 包就非常吃力只能让上层应用注意分片策略。吞吐上不去的最直接原因还是物理串口速率。如果一个 921600 的物理串口开了两个 DLCI一个跑 AT 信令一个跑 PPP用户能体验到的上行吞吐也就是 60~80KB/s 左右这是硬件天花板协议栈再怎么优化都突破不了。做产品立项的时候就得跟业务对好预期。5.4 多核调度与数据乱序问题开车机方案时遇到过一种很刁钻的问题MUX 通道偶发性数据乱序表现为 Modem 上报的 NMEA 语句里偶尔出现前一句和后一句调换位置。查到的根因是内核 tty 层在接收数据时如果 IRQ 分发到了不同 CPU core接收路径可能被中断抢占导致gsm_dlci_data的输出顺序被打乱。解法是在物理串口的irq注册里绑核irq_set_affinity_hint或者把处理串口中断的线程irq/xxx-ttyS0绑定到某一个 CPU core。这个问题的排查用trace-cmd看中断时序比较直观如果 BSP 不带这个工具也可以用/proc/interrupts观察串口中断在不同 CPU 上的分布频率。6. 经验总结把 GSMMUX 用好功夫都在细节里最后分享几个我们踩过很多次坑之后总结出来的实操心得不写空话全是教训内核配置务必确认编进内核镜像而不是模块。Android 的模块加载机制碎片化严重厂商 init 脚本什么时候挂载vendor分区、什么时候调用insmod完全不可控。编进内核能省掉 80% 的“驱动在但设备节点不出现”类问题。宏定义不要乱调。n_gsm.c里有一堆#define控制缓冲区大小、最大 DLCI 数、超时时间看着很好改实际上跟tty_flip_buffer_push等机制深度耦合。改之前先看 GIT 历史里其他厂商的 patch很多改了之后会出现内核 panic 或内存泄漏别问我怎么知道的。初始化流程要固化成一键脚本。GSMMUX 的初始化步骤多、依赖串口状态每次手动敲命令太容易漏掉某一步。我们最终把ldattach GSMIOC_ENABLE 权限设置全部封装成了一个 native 守护进程gsmuxd用init.rc拉起在系统属性vendor.gsm.mux.status里实时上报状态。业务层只需要轮询这个属性就知道 MUX 链路健不健康调起 RIL 和蓝牙前先检查属性省得链路没起来的时候一堆进程同时抢串口。调试 MUX 问题先信示波器再信日志。GSMMUX 链路问题很多时候是底层电气问题电平不对、地线没接好、串扰等等。你在驱动日志里看到帧校验错误、超时这些表面现象往下一层追往往发现是物理层的问题。条件允许的话逻辑分析仪或示波器比dmesg靠谱得多。如果让我给刚接触 GSMMUX 的工程师一个建议不要先去啃 GSM 07.10 协议原文也不要一头扎进厂商 BSP 的海量代码。先把n_gsm.c的gsmld_open、gsm_dlci_establish、gsmld_receive三个函数啃明白再把物理串口调通剩下的配置和使用都是水到渠成的事。协议栈是死的真正灵活的是你对整条数据链路的把握。本文还有配套的精品资源点击获取
返回列表