Android串口多路复用:GSMMUX协议原理与n_gsm驱动实战
2026/9/9 13:52:54 网站建设 项目流程

简介:面向Android底层驱动开发与硬件适配工程师,这份资料包聚焦GSMMUX驱动在Android内核中的实现,解决Mux复用器初始化、硬件交互、设备树配置等实际问题。包内共10个文件,涵盖C源码、头文件与Android.mk/Makefile编译脚本,另有PDF说明和备份文件,压缩包约220KB,结构精简,便于对照学习。GSMMUX驱动以Linux设备驱动模型为基础,涉及设备注册、硬件探测、I/O读写、中断处理、并发同步锁、电源管理及用户空间接口等关键环节,资源包内的源码与文档可帮助读者逐项确认驱动加载流程和引脚功能映射方法。已有185人学习下载,对于需要快速理解Android平台Mux驱动工作机制、进行内核裁剪或外设适配的开发者,这是一份可直接查阅的参考样本。

1. GSMMUX 这个项目到底在解决什么问题

1.1 一根串口号几个人用:从 RIL 到 Modem 的通信困局

GSMMUX,全称 GSM Multiplexer,说的是 ETSI GSM 07.10 标准里定义的多路复用协议。放在 Android 驱动开发这个语境里,它解决的是个很实在的问题:应用处理器(AP)和基带(Modem)之间只有一条物理串口,却同时要跑 AT 命令、语音、上网数据,怎么办?答案就是靠 GSMMUX driver 在软件层把一条物理串口拆成多条逻辑通道,每条通道都能独立收发数据,互不干扰。

我最初接触这个项目是在做一款带 4G 模块的工业平板。Modem 通过板载 UART 接到 AP,硬件上只有一组 TX/RX,RIL 要发 AT 指令、业务进程要跑 TCP 数据、偶尔还要上语音,全都挤在一根线上。如果不用多路复用,就只能轮询或串行排队,业务一忙,AT 指令就长时间得不到响应,基本没法用。GSMMUX 的价值就在这里:它让一套物理串口变成若干条虚拟链路,链路之间在协议层隔离,类似把一条公路分出多个车道,不同方向的车各走各的。

很多人觉得 GSMMUX 是古董技术,现代手机早就不这么干了。但做车机、DTU、工业平板、外挂 4G/5G 模块方案的工程师应该深有体会:现网项目里这种串口型 Modem 依旧大量存在,厂商 BSP 里的 GSMMUX 驱动也一直没人敢删。搞清楚它的原理和调试方法,很多奇奇怪怪的"网络时通时断""AT 指令卡死"问题,排查起来会快得多。

1.2 哪些 Android 场景还会用到 GSMMUX 驱动

第一类是嵌入式/物联网形态的 Android 设备,比如工业 PDA、手持收银机、物流终端。它们常用一颗独立的 4G/5G 模块通过串口或 USB 虚拟串口挂在 SoC 上,为了省成本、省 GPIO,硬件设计上往往不给 Modem 单独拉多路串口,于是只能靠 MUX 复用。第二类是车机方案,很多车载娱乐主机要同时保持和 T-Box、Telematics 网关的多路数据交互,GSMMUX 在这类方案里的出场率不低。第三类是存量产品维护——早年基于高通、联发科、展锐部分平台的方案,软件栈里就带着 GSMMUX 驱动,做维护和移植时绕不开。

需要说明的是,现代主流智能手机上,AP 与 Modem 之间大多改用共享内存(如高通 SMD/QRTR、联发科 IPC 等)或 PCIe 接口,GSMMUX 确实不再是主流。但它作为一套成熟的串口多路复用协议,理解起来并不复杂,而且 n_gsm 这个内核驱动本身也不局限于 GSM 场景——任何需要在一根串口上跑多个逻辑链路的场景都能借鉴它的实现思路。所以这篇东西适合从事 Android 系统开发、驱动移植、嵌入式通信开发的朋友,尤其是项目中要跟串口型 Modem 打交道的人。

2. GSM 07.10 协议原理:驱动与 Modem 之间的语言

2.1 DLCI 逻辑通道与帧结构

GSM 07.10 协议的核心概念是 DLCI(Data Link Connection Identifier,数据链路连接标识符)。在一条物理链路上,最多可以建立 64 条逻辑通道,编号从 0 到 63。其中 DLCI 0 被固定为控制通道,专门用于链路管理,比如建立/释放其他通道、传控制信令;DLCI 1 到 63 才是真正的业务通道,可以承载 AT 命令、PPP 数据、语音或用户自定义数据。

协议帧结构大致由地址字段、控制字段、长度字段和信息字段组成。地址字段里最关键的是 EA(扩展位)和 C/R(命令/响应位)以及 DLCI 值,控制字段用来区分帧类型,比如 SABM 帧是请求建立链路,UA 帧是对端确认建立,UIH 是高效无确认帧。实际在 n_gsm 驱动里,绝大多数数据帧走的是 UIH 模式,因为它的开销小、无确认、适合传输实时数据。

我用生活化类比来说:一条物理串口相当于一根水管,DLCI 是在水管里跑的虚拟管道编号。DLCI 0 是总调度室,负责协调各条管道的开关;DLCI 1、2、3 是不同住户的专用管,谁家的水流谁家。驱动收到一个数据包,先拆开帧头看 DLCI 是多少,就知道该往哪个 /dev/gsmtty 设备里灌。

2.2 从 AT+CMUX 到 SABM/UA:一条逻辑通道的诞生过程

GSMMUX 的建链流程可以分成两个阶段。第一阶段是单通道模式下的切换协商,AP 先以普通串口模式向 Modem 发送 AT+CMUX=0(参数 0 表示 basic option),Modem 收到后如果支持多路复用,会回一个 OK。这个 OK 只表示"我同意切到复用模式",此时物理链路尚未做逻辑分帧。

第二阶段是真正的物理链路上跑协议帧。AP 侧的驱动要把底层串口切换到 n_gsm line discipline,然后作为 initiator(发起方)向 Modem 发送 SABM 帧,请求建立某个 DLCI 的逻辑链路。Modem 作为 responder(响应方),收到后回复 UA 帧,表示"这条通道建立成功"。之后 AP 才能在该 DLCI 上放心大胆地传业务数据。

这里有一个在实际项目里容易忽略的点:部分 Modem 在响应 AT+CMUX 后,还需要 AP 主动发送 MSC(Modem Status Command,调制解调器状态命令)帧来握手。MSC 帧的作用是交换串口的控制信号状态,比如 DTR、RTS、DCD 等。如果驱动没有完整实现 MSC 处理,或者 Modem 固件对 MSC 的要求比较严格,即使 SABM/UA 已经完成,后续数据也可能发不出去。我见过不止一次"UA 都回了但 AT 没反应"的怪问题,最后查下来就是 MSC 没配对。

2.3 控制面与数据面为什么要分开

DLCI 0 控制通道和其他业务通道分开设计,是 GSM 07.10 的一种很聪明的做法。控制通道永远存在,专门承载建链、拆链、状态查询、错误恢复等管理帧;业务通道则按需动态建立,用完就释放,避免长期占用资源。

这么做的好处是,数据面出现异常时,控制面还能继续工作,AP 和 Modem 之间仍然可以通过 DLCI 0 做诊断和恢复。比如某条业务通道的 DLC 因为信号波动异常断开,驱动可以在控制通道上重新发起 SABM,把链路拉回来,而不需要重启整个串口链路。Android 的 RIL 层对这个特性很依赖,因为 RIL 与 Modem 之间那条 AT 通道通常被设计为"常驻",不允许因为某次数据抖动就彻底挂掉。

这也解释了为什么 GSMMUX 驱动在软件架构上要分成两层:一层负责协议解析和帧分发,另一层负责向用户空间暴露不同的 tty 设备节点。前者是驱动的心脏,后者是用户空间程序的手和脚,二者通过内核内部的队列和调度机制衔接。

3. Android 里的 GSMMUX 驱动实现:n_gsm 的前世今生

3.1 从字符设备到 line discipline 的架构选择

Linux 内核早期确实有独立的字符设备驱动来做 GSM 多路复用,路径是 drivers/char/gsmmux.c,把协议栈直接绑死在某个设备驱动上。这种方式的问题很明显:换一种串口硬件就得改驱动,UART、USB 串口、SPI 模拟串口都要分别适配,维护成本高。

后来主线内核统一采用 n_gsm 驱动,也就是 drivers/tty/n_gsm.c,它的身份是 TTY 子系统里的一种 line discipline(线路规程),编号为 N_GSM0710。Line discipline 是挂在 tty 设备中间层的一层"协议皮肤":底层可以是任何 tty 设备——普通 UART、USB 转串口、甚至虚拟串口,只要先把底层 tty 打开,再通过 ioctl 把 line discipline 切换为 N_GSM0710,GSM 07.10 协议栈就开始在这条 tty 上工作。

这个架构的好处是极大的解耦。驱动开发人员面对的是一个标准的 tty 接口,不需要关心底下是具体哪颗 UART 控制器;上层用户空间拿到的是 /dev/gsmtty* 这样的标准终端设备,可以像操作普通串口一样读写。对 Android 系统来说尤其方便,RIL daemon 只需要打开 /dev/gsmtty 节点,不需要关心底层是 PCIe 还是 UART。我在项目里最大的感受是,n_gsm 的设计让"移植一款新 Modem"的工作量,从改驱动变成了配参数。

3.2 n_gsm 的数据流与关键接口

n_gsm 驱动内部有两个核心数据结构:gsm_mux 代表一个多路复用会话实例,gsm_dlci 代表一条逻辑通道。一个 gsm_mux 实例对应底层一条物理 tty 链路;每个 gsm_dlci 对应一个从 1 开始的 DLCI,并且关联一个由驱动注册的 gsmtty 终端设备。

数据流的走向分成两条路。收数据时,底层 tty 收到原始字节流,进入 gsmld_receive 函数,由驱动按帧格式解析,识别出 DLCI 后把数据塞到对应 gsm_dlci 的接收队列,最终被用户空间从 /dev/gsmttyN 读出。发数据时,用户空间写 /dev/gsmttyN,数据进入对应 gsm_dlci 的发送队列,驱动把它封装成 GSM 07.10 帧,再通过 gsmld_write 交给底层 tty 发送出去。

用户空间交互的关键 ioctl 接口主要有这几个:

  • TIOCSETD:设置 line discipline 为 N_GSM0710。
  • GSMIOC_GETCONF / GSMIOC_SETCONF:读取/设置复用参数。
  • GSMIOC_GETFIRST / GSMIOC_GETLAST:查询当前实例可用的 gsmtty 节点范围。

struct gsm_config 里最常用的字段包括 initiator(1 表示本地主动发起建链)、encapsulation(0 表示 basic option)、mru 和 mtu(收发最大帧长,默认一般 2048)。实际项目中,波特率越高,mru 可以适当调大以减少帧数量,但也要考虑 Modem 侧支持的上限。

3.3 内核配置与设备节点

内核侧需要使能 CONFIG_N_GSM。可以编进内核(=y),也可以编成模块(=m),Android 项目里考虑到 rootfs 和 init 加载顺序,我通常直接编进内核,省得处理模块加载时序。配置开启后,驱动成功注册会创建一批 tty 设备,名称形如 /dev/gsmtty0、/dev/gsmtty1,对应 DLCI 1、2……以此类推。用户空间通过 mdev/udev 或者 init.rc 里的 chmod/chown 来设置这些节点的访问权限。

需要注意的是,Android 系统有 SELinux 安全策略,即使内核里 /dev/gsmtty 节点已经创建,RIL 进程去打不开的事也经常发生。我自己的习惯是把 /dev/gsmtty 节点的权限和 SELinux label 在 device 的 file_contexts 和 init.rc 里显式声明,避免用户空间程序被 SELinux 挡在门外。

4. 实操:在 Android 设备上从零跑通 GSMMUX

4.1 内核侧:开启 n_gsm 并确认驱动加载

第一步是在内核的 defconfig 里找到 CONFIG_N_GSM,如果原本没开,手动加上。以我常用的 4.19 内核举例,在 arch/arm64/configs/项目_defconfig 里加入:

CONFIG_N_GSM=y

然后重新编译 boot.img(如果 pstore 不支持,建议先在设备树/ramdisk 阶段确认驱动符号已经编进去)。刷机后启动设备,查看驱动是否注册成功:

adb shell cat /proc/tty/drivers | grep gsmtty

正常会看到类似gsmtty的设备驱动记录。如果 CONFIG_N_GSM=m,还需要先 insmod 加载模块,注意 Android GKI 设备下模块必须与内核签名匹配,否则 insmod 会报 signature verification failed,这就比较麻烦,所以我前面才说尽量编进内核。

4.2 用户态:用 C 代码建立多路复用会话

驱动就绪后,用户态要做三件事:打开底层串口、把 line discipline 切到 N_GSM0710、设置 mux 参数并打开 gsmtty 节点。下面这段伪代码演示了完整流程,实际项目里我已经按这个框架跑了很多版本:

#include <stdio.h> #include <fcntl.h> #include <termios.h> #include <linux/tty.h> #include <linux/gsmmux.h> #include <sys/ioctl.h> int main(void) { int ldisc = N_GSM0710; int fd, dlci_fd; struct termios tio; struct gsm_config cfg; // 1. 打开底层串口设备,根据实际硬件可能是 ttyS0/ttyUSB0/ttyHS0 fd = open("/dev/ttyUSB0", O_RDWR | O_NOCTTY); if (fd < 0) { perror("open uart"); return -1; } // 2. 配置波特率:必须与 Modem 侧的协商速率一致 tcgetattr(fd, &tio); cfsetispeed(&tio, B115200); cfsetospeed(&tio, B115200); cfmakeraw(&tio); tcsetattr(fd, TCSANOW, &tio); // 3. 切换 line discipline 到 N_GSM0710,这是进入复用模式的关键 if (ioctl(fd, TIOCSETD, &ldisc) < 0) { perror("TIOCSETD"); return -1; } // 4. 读取并修改 mux 配置,AP 作为 initiator if (ioctl(fd, GSMIOC_GETCONF, &cfg) < 0) { perror("GSMIOC_GETCONF"); return -1; } cfg.initiator = 1; cfg.encapsulation = 0; cfg.mru = 2048; cfg.mtu = 2048; if (ioctl(fd, GSMIOC_SETCONF, &cfg) < 0) { perror("GSMIOC_SETCONF"); return -1; } // 5. 打开一条逻辑通道,比如 gsmtty0 对应 DLCI 1 dlci_fd = open("/dev/gsmtty0", O_RDWR | O_NOCTTY); if (dlci_fd < 0) { perror("open gsmtty"); return -1; } // 到这里就可以对 dlci_fd 读写 AT 命令了 write(dlci_fd, "ATI\r\n", 5); char buf[128]; int n = read(dlci_fd, buf, sizeof(buf)); if (n > 0) { write(1, buf, n); } close(dlci_fd); close(fd); return 0; }

代码里我特意把"切换 line discipline"和"设置 mux 参数"两步分开,因为很多人容易搞反顺序。必须先 TIOCSETD 把串口从普通终端模式变成 n_gsm 模式,再通过 GSMIOC_SETCONF 配置复用参数;反过来会直接报 invalid argument。

实际运行时还有一个前置命令不能漏:需要在普通串口模式下向 Modem 发送 AT+CMUX=0,让 Modem 进入多路复用等待状态。这一步一般在用户空间首次打开底层串口时完成,或者直接放在 Modem 的上电初始化脚本里。

4.3 验证:两个逻辑通道并发通信

建链之后,最简单的验证方式是同时在两条逻辑通道上发 AT 命令。我用一个 shell 脚本做过快速验证:

# 通道1:查询模块信息 echo -e "ATI\r" > /dev/gsmtty0 # 通道2:查询信号强度,注意这里不同设备节点对应不同 DLCI echo -e "AT+CSQ\r" > /dev/gsmtty1

如果在两条节点上都分别能读回 Modem 的正常响应,说明复用链路已经建立,两个 DLCI 都能独立收发。注意不同内核下 gsmttyN 与 DLCI 的对应关系略有差异,一般 gsmtty0 对应 DLCI 1,以此类推,具体以驱动注册节点时设置的 first/last 为准。

项目里真正要上量的话,建议做一轮更严格的双通道并发测试:一个进程用 PPP 拨号拉流量,另一个进程持续发 AT+CSQ 查询信号,观察 AT 通道是否还能稳定响应。如果发觉 AT 响应延迟变大或者数据丢包,优先检查硬件流控和缓冲区配置,这也是我下一节要讲的踩坑点。

5. 常见问题排查与实战避坑清单

5.1 现象-原因-排查速查表

下面这张表是我在多个 GSMMUX 项目里沉淀下来的排查清单,按出现频率排序:

现象可能原因排查与处理
AT+CMUX=0 无响应或返回 ERROR波特率不匹配、Modem 固件不支持 MUX先确认 ATI/ATE0 等基础指令能通;查阅 Modem 手册确认是否支持 CMUX 以及参数取值
TIOCSETD 返回 EPERM内核未开 CONFIG_N_GSM,或当前进程权限不足检查内核配置与 /proc/tty/drivers;Android 上确认 adb shell 是否已 root
/dev/gsmtty* 节点不存在驱动加载失败、节点未创建、udev/mdev 规则缺失查看 dmesg 有无 gsmtty 注册信息;手动 mknod 或补 init.rc 创建规则
SABM 发出去但收不到 UAinitiator 配置错误、Modem 未进入 MUX 等待状态确认 AT+CMUX 成功后再切换 ldisc;确认 cfg.initiator=1
数据传一会儿就卡死MSP/MSC 握手不完整、流控不匹配抓串口日志核对 MSC 帧;打开 RTS/CTS 硬件流控
两路通道互相污染DLCI 映射错误、驱动缓冲区溢出确认 gsmttyN 与 DLCI 的对应关系;调大 mru/mtu 或接收缓冲区
RIL 进程打不开设备SELinux 拦截或节点权限不对查看 avc log,给对应 domain 补 te 规则和 file_contexts 标签

5.2 我踩过的三个坑:流控、SELinux、Init 时序

第一个坑是硬件流控。最初我调一块 4G 模块时只配置了波特率,没管 RTS/CTS,低速率下 AT 命令没问题,一旦跑大流量数据就乱码、丢帧。原因是高速率下串口 FIFO 容易溢出,必须开硬件流控让对端能暂停发送。调试时可以先在普通串口模式下用 AT+IFC=2,2 开启 RTS/CTS 流控,再进 CMUX 模式,这样 n_gsm 底层的数据面才稳定。

第二个坑是 SELinux。Android 用户空间程序打开 /dev/gsmtty 节点时,经常被 SELinux 拦死,而内核驱动本身完全正常。现象是程序 open 返回 EACCES,但 dmesg 里看不到任何串口错误。解决办法是在 device 的 te 文件里给进程 domain 放行对应节点,并且在 file_contexts 里给 /dev/gsmtty 打标签。这套流程对做系统集成的朋友来说几乎是必踩的。

第三个坑是 Modem 侧的初始化时序。有些 Modem 固件要求上电后在规定时间内收到 AT+CMUX 指令,否则自动进入某种特定工作模式,后面再发 CMUX 就不生效了。项目里如果遇到"驱动已经配置好但链路就是建不起来",建议在 init.rc 里把 Modem 复位和串口初始化做成有依赖关系的启动阶段,确保 AT+CMUX 在 Modem 就绪后立刻执行,别等到 Android 系统完全启动后再去触发。

最后再分享一个个人经验:GSMMUX 的驱动层一旦跑通,就不要再频繁改 mru、mtu 和流控参数,这些参数和 Modem 固件是强相关的,两边必须一致。我曾经为了"优化性能"把 mru 从 2048 调到 4096,结果 Modem 侧不支持大帧,直接出现链路中断。稳定运行的方案,往往是参数最保守的那一套。

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

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

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

立即咨询