ARTICLE DETAIL

资讯详情

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

EC20 CMUX驱动实战:UART多路复用与Linux/Android串口通信

EC20 CMUX驱动实战:UART多路复用与Linux/Android串口通信 简介面向Linux与Android平台开发者的Quectel EC20 CMUX驱动V2.0.1资源包解决单个UART通道上数据、语音、短信等多业务并发传输问题。包内共3个文件包含驱动源码C文件、Makefile构建脚本以及官方用户指南PDF整体约354KB结构紧凑可直接参考。已有349人浏览学习。该版本针对Linux内核与Android HAL层做了适配驱动源码基于gsm0710muxd实现CMUX通道复用清晰展示了物理通道与逻辑通道的映射关系可帮助开发者理解EC20的时分复用机制Makefile便于快速编译和定制用户指南详尽说明安装、配置与调试步骤对排查串口异常也有参考价值。适用于物联网设备、车载信息娱乐系统等场景可显著提升串口利用率与多业务并行处理能力无论从入门学习还是实际项目落地看都有较好的实践指导意义。1. EC20 CMUX 驱动是什么一条 UART 干三份活的真相做物联网网关时我碰过这个尴尬场景板子只有一个物理串口接在 EC20 模块上业务却同时要数据透传、收短信、跑协议心跳。换双串口模块要改硬件不改就得把三个业务轮询挤在一个口子上调度稍慢就丢帧。Quectel 这份 Linux Android CMUX Driver V2.0.1 就是干这个的它把 GSM 07.10 的 CMUX通道复用协议落地成可编译的驱动源码让一个物理 UART 上能同时跑多条独立逻辑通道。我拿到压缩包后第一反应是拆开看 gsm0710muxd_bp.c 到底怎么写协议帧因为这份源码才是整套方案的灵魂。这篇笔记我按协议原理 → 编译加载 → Android 适配 → 踩坑 → 验证技巧的顺序讲适合做 EC20 数传设备、车载中控或工控 DTU 的 Linux/Android 工程师也适合刚接手模块调试、被串口多业务并发折腾到头秃的新手。2. 从 gsm0710muxd_bp.c 看 CMUX 协议实现GSM 07.10 报文是怎么被拆出来的2.1 CMUX 协议的核心思想物理一条线逻辑分五路CMUX 全称 Channel Multiplexing在 GSM 07.10 标准里定义了如何在一条物理链路上分时复用出多条数据链路。EC20 模块侧本身就支持这个协议只要主控侧也用同样的协议去“拆包”就能把 AT 命令、数据业务、语音控制分配在不同的 DLCI数据链路连接标识符上。我之前一直把 CMUX 当成玄学直到扒完源码才发现它本质就是三层东西物理串口收发原始字节流协议层负责把字节流按帧格式切分成不同通道的报文应用层只管读写对应的虚拟 tty 节点。帧格式是这套协议最值得记的部分。每一帧以 0xF9 标志字节开头和结尾中间依次是地址字段、控制字段、长度字段、数据、FCS 帧校验。地址字段里的 DLCI 决定了这帧数据属于哪个逻辑通道控制字段标识帧类型——数据帧、控制帧、无编号信息帧。源码里对帧头和校验的处理很直白没有用复杂的内核机制完全靠一个循环从串口读字节、拼帧、校验、分发。这种协议栈放在用户态做比做成内核模块更灵活升级驱动不用重新编译内核这也是 Quectel 这份驱动包采用用户态 daemongsm0710muxd而不是内核驱动的原因。默认配置下 DLCI 0 是控制通道DLCI 1 通常留给 AT 命令DLCI 2/3/4 可以给数据业务。实际使用中我一般只用 DLCI 1 跑 AT 指令、DLCI 2 做 PPP 数据剩下的作为预留通道。通道数量不是越多越好因为每条通道都要分摊物理串口的带宽通道多了单路吞吐反而下降。如果业务只需要一路数据加一路控制保持默认的 3~5 路就够用。2.2 源码包解析gsm0710muxd_bp.c 的文件构成与关键函数解压后核心文件就是一个 gsm0710muxd_bp.c 和一个 Makefile外加一份 User Guide PDF。这个 gsm0710muxd_bp.c 文件里主要完成了四件事初始化串口、解析 CMUX 帧、维护多条虚拟通道的读写缓冲、处理控制通道的信令。同目录的 Makefile 设计得比较简洁支持通过命令行覆盖编译器和编译参数这一点在实际交叉编译时非常关键。/* 通道控制块每一条虚拟串口对应一个实例 */ struct mux_channel { int dlci; /* 数据链路连接标识符2~4 为数据通道 */ int fd; /* 打开 /dev/serial_muxN 后返回的文件描述符 */ unsigned char buf[2048]; int buf_len; int state; /* 通道状态IDLE / ESTABLISHED / CLOSED */ };这段代码定义的是每个虚拟通道的核心数据结构。dlci 决定了通道号fd 是上层应用打开虚拟节点后拿到的句柄buf 存放从物理串口拆分出来的载荷state 标记当前通道是否建立成功。实际收发数据时驱动从物理串口读到一个完整 CMUX 帧根据地址字段里的 DLCI 找到对应的 mux_channel 实例把数据复制到它的 buf 里再唤醒等待在这个虚拟节点上的读操作反过来应用往虚拟节点写数据时驱动把数据封装成 CMUX 帧从物理串口发出去。理解了这个数据流后面排查某个虚拟通道发不出数据这类问题就会快很多。Makefile 里最值得关注的是变量覆盖逻辑。原本的 Makefile 直接用 gcc 编译但在 ARM 板子或 Android 环境里必须改成交叉编译工具链。CC ? gcc CFLAGS -g -Wall -O2 -c LDFLAGS -lpthread TARGET gsm0710muxd all: $(TARGET) $(TARGET): gsm0710muxd_bp.o $(CC) $(LDFLAGS) -o $ $^ gsm0710muxd_bp.o: gsm0710muxd_bp.c $(CC) $(CFLAGS) -o $ $^ clean: rm -f $(TARGET) *.o这里的 CC 和 CFLAGS 都用了?和也就是说如果用户在命令行显式传入 CC就会覆盖默认的 gcc。我一般在 Linux 主机上交叉编译时用make CCarm-linux-gnueabihf-gcc CFLAGS-g -Wall -O2这种方式传入参数不需要改 Makefile 本身。有一点要注意-c选项表示只编译不链接真正的链接在第二条规则里完成所以用make CFLAGS...覆盖时别把-c弄丢否则编译阶段就会因为缺少目标文件而报错。2.3 驱动工作模型物理串口、虚拟节点与上层应用的关系CMUX 驱动跑起来后的整体结构可以理解成一条水管上接了三四个水龙头。底层是物理串口对应 /dev/ttyUSB0 或 /dev/ttyS2 这类真实设备节点中间层是 gsm0710muxd 这个后台进程它负责把物理口的数据拆分成不同通道上层是 /dev/serial_mux0、/dev/serial_mux1、/dev/serial_mux2 这些虚拟节点应用只跟这些节点交互完全不知道底层是复用的同一条物理串口。# 先确认物理串口存在且没被占用 ls -l /dev/ttyUSB0 # 启动 muxd 守护进程绑定物理串口 ./gsm0710muxd -s /dev/ttyUSB0 # 查看虚拟节点是否创建成功 ls -l /dev/serial_mux*启动参数里-s指定物理串口路径这是最常见的用法如果板子用的是 UART 总线而不是 USB 虚拟串口就把路径换成 /dev/ttyS2 这类实际节点。虚拟节点不是驱动一启动就会全部创建而是要等模块端收到 ATCMUX 命令、建立了复用连接之后虚拟节点才会真正可用。这个先后顺序是新手最容易踩的坑——先把 pppd 拉起来再开 CMUX结果虚拟节点根本不存在业务自然起不来。正确顺序一定是先让模块进入 CMUX 模式再打开对应的 serial_mux 节点发起业务。3. Linux 编译与加载从 Makefile 到 /dev/serial_mux 节点能被 ATCMUX 激活3.1 交叉编译准备确认工具链和内核头文件版本在 Linux 环境里用这份源码第一步不是直接 make而是确认目标平台。x86 的 Ubuntu 开发机可以直接本机编译但实际部署到 ARM 板子或者跑在定制 Linux 的工业主板上必须交叉编译。我的习惯是先查看用户指南里对内核版本的要求然后确认交叉编译器版本跟目标板的 glibc 兼容否则编出来的二进制拷到板子上会报 No such file or directory实际是动态库加载不了。# 查看当前交叉编译器版本 arm-linux-gnueabihf-gcc --version # 检查目标板的架构和 glibc 版本 file /sbin/initfile 命令输出里如果显示 ARM 32-bit就用 arm-linux-gnueabihf 工具链如果是 aarch64就用 aarch64-linux-gnu 工具链。判断了架构之后编译命令可以写成这样make clean make CCarm-linux-gnueabihf-gcc CFLAGS-g -Wall -O2 -c LDFLAGS-lpthread # 编译成功后检查产物架构 file gsm0710muxdfile 输出里看到 ARM, EABI5 之类的字样说明架构正确。这里我特意把 CFLAGS 写全因为源码里的 Makefile 默认带了-c选项如果你在命令行覆盖 CFAGS 时写成CFLAGS-O2会把-c漏掉编译时 gcc 会尝试直接链接生成可执行文件因为没有 .o 文件而报错。这个玄学问题我吃过一次亏后来每次编译都强制把-c带上。3.2 加载并激活 CMUXATCMUX 命令与虚拟节点出现的先后逻辑编译出可执行文件后把它拷到目标板用 udev 规则或者手动启动守护进程。这里要注意 muxd 进程启动时指定的物理串口参数必须与 EC20 模块保持一致否则对不上波特率会直接出现乱码或者完全无响应。# 启动守护进程绑定 /dev/ttyUSB0波特率保持默认 115200 ./gsm0710muxd -s /dev/ttyUSB0 -b 115200 # 打开原始物理串口发送 ATCMUX 命令 echo -e ATCMUX0\r /dev/ttyUSB0 # 等待片刻后再次查看虚拟节点 sleep 2 ls -l /dev/serial_mux*ATCMUX0,0,5,127,30,3这段命令的含义值得拆开讲。第一个参数0表示启用基本选项模式5表示最大帧长 127 字节默认值30是唤醒时间控制3是优先级。实际使用中我一般保持默认参数只有当出现帧过长、大吞吐场景下丢包时才会去调最大帧长这个值。需要注意的是虚拟节点的创建完全依赖模块端对 ATCMUX 命令的响应如果命令没回 OK后面所有操作都是空中楼阁。参数位置含义常见取值说明ATCMUX 第一个参数复用模式00 为基本选项1 为高级选项EC20 常用 0第二个参数子模式0一般为 0表示无子模式第三个参数最大帧长127/512越大吞吐越高但单帧错误代价也越大第四个参数唤醒时间30单位 10ms低功耗场景可以调大第五个参数波特率因子3不常用保持默认即可3.3 用户态验证用 AT 命令和 pppd 确认虚拟通道可收发虚拟节点出现之后先把每条通道的收发验证一遍再上业务代码。做法是逐个打开 serial_mux 节点发送 AT 命令能回 OK 就说明这条通道链路建立成功。# 打开通道 0发送 AT 测试命令 exec 3/dev/serial_mux0 echo -e AT\r 3 head -n 1 3正常情况下这条命令会返回 OK 或空行加 OK。如果前面 ATCMUX 用的是 DLCI 2 做数据通道那么 /dev/serial_mux0 对应的就是 DLCI 2 的数据业务在它上面跑 pppd 前要用 ATD*99# 拨号echo -e ATD*99#\r /dev/serial_mux1 # 然后在这个节点上拉起 pppd pppd call ec20-cmux pppd 的拨号配置里有一个细节必须把modem参数去掉改用local和crtscts因为虚拟串口本身不提供 modem 控制信号沿用默认参数会导致 pppd 一直等待载波检测而超时。同目录下的用户指南里也提到了这一点建议拿到源码后先翻一遍很多坑 PDF 里其实都写明白了只是大家习惯性先看代码。4. Android 平台适配从 HAL 到 SELinux 的一条龙改造4.1 Android 与 Linux 的驱动差异为什么同一份源码要分开适配虽然 Android 底层是 Linux 内核但设备节点的创建方式和权限管理跟传统 Linux 完全是两套逻辑。传统 Linux 里 muxd 进程启动后用 mknod 或者 udev 就能创建设备节点Android 里设备节点由 init 进程根据 ueventd.rc 文件统一创建而且 SELinux 策略管控着所有进程对节点文件的访问权限。同一份 gsm0710muxd_bp.c 在 Android 上编译时通常在 Makefile 里定义了一个 ANDROID 宏源码内部会根据这个宏决定日志输出方式。Linux 版用 fprintf(stderr) 直接打日志Android 版走 logcat 输出到系统日志缓冲区。这个差别处理好了调试 Android 时就能直接在 logcat 里看到 muxd 的打印而不用再去翻串口日志文件。我在做 Android 车载项目时就是因为没注意这个宏编译默认版本后 logcat 里完全看不到驱动运行痕迹白白排查了半天。# Android 交叉编译指定 ANDROID 宏 make CCaarch64-linux-android-gcc CFLAGS-DANDROID -g -Wall -O2 -c4.2 设备节点与权限ueventd.rc、SELinux policy 怎么配Android 上把 muxd 编译好放进系统后第一件事不是启动它而是保证 init 进程会在 /dev 下创建 serial_mux 节点并赋予正确权限。默认的 ueventd.rc 里没有 serial_mux 相关条目如果不加init 会以默认权限创建节点导致 muxd 进程打不开设备。# ueventd.rc 追加片段 /dev/serial_mux* 0660 radio radio权限配好只是第一步SELinux 策略才是 Android 上真正卡脖子的环节。muxd 作为后台守护进程要能访问物理串口对应的 tty 设备节点同时要能写入 serial_mux 节点。常见做法是给 muxd 单独定义一个 te 文件给它分配一个 domain再写 allow 规则。# muxd.te 里至少要包含下面三条规则 allow muxd serial_mux_device:chr_file { open read write ioctl }; allow muxd tty_device:chr_file { open read write ioctl }; allow muxd self:process { execmem };最后一条 allow self:process execmem 是很多移植项目遗漏的地方。Android 从某个版本开始对进程的可执行内存做了严格限制muxd 这类用 C 语言编写、运行时需要动态申请可执行内存的程序如果不加这条规则启动时会直接被 SELinux 拒绝并触发 avc: denied 日志。判断问题是不是出在 SELinux 上最直接的方法是看 dmesg 或者 logcat 里有没有 avc denied 记录有就说明策略没放通。4.3 HAL 层集成把虚拟串口接入 Android RIL 或数据业务设备节点和权限打通后剩下的是把 serial_mux 通道对接到上层的 RIL无线电接口层或者数据业务。Android 侧因为进程沙箱和安全机制比较稳妥的做法是写一个轻量 HAL 模块把 /dev/serial_mux1 封装成可供上层调用的接口。外面接 RIL 的流程是这样的RIL 启动后通过 HAL 拿到虚拟串口的 fd再通过 HAL 的 read/write 回调收发 AT 命令和数据。此时 RIL 侧感知不到底层是 CMUX 复用出来的虚拟通道整个接缝就藏在 HAL 内部。/* HAL 接口示例封装 serial_mux 节点的读写 */ int mux_hal_open(int channel, int flags) { char path[32]; snprintf(path, sizeof(path), /dev/serial_mux%d, channel); return open(path, flags); } int mux_hal_read(int fd, void *buf, int len) { return read(fd, buf, len); } int mux_hal_write(int fd, const void *buf, int len) { return write(fd, buf, len); }这套 HAL 封装逻辑上跟 Linux 普通应用直接 open/read/write 没有本质区别多出来的工作就是权限校验、节点存在性检查、以及把错误码转换成 Android 的 status_t 返回值。Android 平台相比 Linux 在 CMUX 适配上的成本其实主要不在驱动源码本身而在权限体系、设备节点管理、日志通道这三个外围工程。很多项目最后踩坑都不是 gsm0710muxd_bp.c 里协议解析出错而是系统服务没有给足权限。5. CMUX 驱动避坑指南四个最容易翻车的调试现场5.1 现象ATCMUX 命令不回 OK虚拟节点一直不出现原因最常见的是物理串口被其他进程先打开了。muxd 绑定串口时如果发现端口被占用会直接退出或者反复提示 open failed。另外波特率不匹配也会导致模块根本不解析 AT 命令。解决先停掉所有占用物理串口的进程用lsof /dev/ttyUSB0或者fuser -k /dev/ttyUSB0清理占用。然后用最简单的串口工具单独发 AT 命令确认模块活着再启动 muxd。我一般会把串口打开方式调成 raw 模式关闭流控避免系统把特殊字符做了转换。stty -F /dev/ttyUSB0 raw -echo 1152005.2 现象多路虚拟通道同时收发时偶发乱码和错帧原因CMUX 协议对时序非常敏感。帧标志 0xF9 出现在数据载荷里时协议规定要转义成两个字节但如果实现里没有正确处理转义就会出现错帧。另一个可能原因是串口接收缓冲区太小物理层一次到达的数据超过了驱动单次读取的长度导致帧被拆成两段解析。解决检查 gsm0710muxd_bp.c 里对 0xF9 的处理逻辑确认转义和逆转义都按标准实现。同时把串口的接收缓冲区调大或者在 muxd 的读循环里改成一次读完再解析而不是每次只读固定长度。遇到持续错帧最有效的定位办法是开一个抓包脚本模拟满载数据观察 FCS 校验失败报错有没有规律——如果错误集中在某个通道优先检查那个通道对应的虚拟节点应用层有没有并发写同一个 fd。5.3 现象pppd 拨号可以连通但一有大流量就断线重启原因这是经典的水位线问题。CMUX 物理链路的总带宽被多路通道共享数据通道 DLCI 2 占满带宽时控制通道 DLCI 1 的 AT 命令无法及时送达模块模块侧看门狗超时直接重启了复用链路。解决调大 ATCMUX 命令里的唤醒时间参数或者把数据通道的 MTU 调小例如 PPP MTU 从 1500 降到 1200。更重要的一点是在设计业务逻辑时不要让数据通道完全占满物理带宽至少留出 10% 的余量给控制通道。为了做到这一点我一般会限制 pppd 的带宽上限或者在上层做流量整形。# pppd 配置里限制 MTU 和发送速率 mtu 1200 rate 805.4 现象Android 上一切配置正确但 logcat 干脆没有 muxd 启动日志原因大部分情况是 SELinux 拦截了进程启动或者二进制权限没可执行位。还有一种隐蔽情况是编译时没有定义 ANDROID 宏日志走了 stdout/stderr在 Android 后台被丢弃logcat 里自然什么都没有。解决先adb shell ps -A | grep muxd看进程在不在。进程存在就说明是权限或日志路径问题把宏加上重新编译。进程不存在的执行adb shell dmesg | grep avc查 SELinux 拦截记录按 4.2 节的方法补 allow 规则。我自己的习惯是每次改完权限策略都执行一次adb shell setenforce 0先临时放行验证功能确认没有问题后再把策略固化到 sepolicy 里。注意不要在产品上长期开着 setenforce 0否则过不了安全测试。6. 进阶验证手法压测、日志与分析工具配合排查当编译、启动、连通性都搞定之后剩下的核心问题就是性能边界。CMUX 驱动虽然源码里有完善的读写缓冲机制但实际能跑多少吞吐依赖物理串口质量、内核调度、应用层读写模式这三者的配合。我的做法是分三步做压测先用 dd 灌数据测虚拟通道的原始吞吐再用 pppd 跑 iperf 测网络吞吐最后用 strace 看 muxd 进程有没有反复出现 EAGAIN 或者大段阻塞。# 步骤一往 data 通道灌测试数据 dd if/dev/urandom of/dev/serial_mux2 bs1024 count1024 # 步骤二从对端读回并统计吞吐 dd if/dev/serial_mux2 of/dev/null bs1024 count1024 # 步骤三观察 muxd 阻塞状况 strace -p $(pidof gsm0710muxd) -e traceread,write -o /tmp/muxd.log -f跑完这三步后重点看两个地方。第一是 /tmp/muxd.log 里有没有大量 retry 或者操作耗时特别长的记录正常读写基本微秒级返回一旦某个 read 阻塞超过几十毫秒说明内核调度或者串口驱动有问题。第二是实际吞吐和理论宽带的差距例如 115200 波特率下理论吞吐约 11.5KB/s扣掉 CMUX 帧头和控制开销实际能达到 9KB/s 就属于正常范围如果连 6KB/s 都不到优先怀疑流控或者帧长参数没调好。如果压测过程中出现丢包我还习惯抓一份寄存器级别的调试日志。在驱动的收发函数里临时加打印把每次读到的帧头数据打出来对比 FCS 是否正确。这一步能很快区分是物理层电气干扰导致的数据位反转还是协议层缓存越界导致的内存损坏。如果是物理层问题损失集中在特定波特率下换更低波特率后错误率明显下降如果是协议层问题跟波特率关系不大反而跟通道数量和帧长相关此时把最大帧长从 512 调回 127往往能稳定下来。最后分享一个习惯从那以后我每次做 CMUX 驱动集成都强制按先确认物理串口独立可用 → 再开 CMUX 复用 → 最后挂业务通道这个顺序走一遍并且在启动脚本里加一条节点存在性检查发现 /dev/serial_mux2 缺失就重启 muxd 并记录日志而不是傻等业务超时。这个检查逻辑帮我省掉了至少三次现场排障希望帮到你。本文还有配套的精品资源点击获取
返回列表