news 2026/10/7 1:15:10

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 这份驱动包采用用户态 daemon(gsm0710muxd)而不是内核驱动的原因。

默认配置下 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 CC=arm-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 这类实际节点。虚拟节点不是驱动一启动就会全部创建,而是要等模块端收到 AT+CMUX 命令、建立了复用连接之后,虚拟节点才会真正可用。这个先后顺序是新手最容易踩的坑——先把 pppd 拉起来再开 CMUX,结果虚拟节点根本不存在,业务自然起不来。正确顺序一定是先让模块进入 CMUX 模式,再打开对应的 serial_mux 节点发起业务。

3. Linux 编译与加载:从 Makefile 到 /dev/serial_mux 节点能被 AT+CMUX 激活

3.1 交叉编译准备:确认工具链和内核头文件版本

在 Linux 环境里用这份源码,第一步不是直接 make,而是确认目标平台。x86 的 Ubuntu 开发机可以直接本机编译,但实际部署到 ARM 板子或者跑在定制 Linux 的工业主板上,必须交叉编译。我的习惯是先查看用户指南里对内核版本的要求,然后确认交叉编译器版本跟目标板的 glibc 兼容,否则编出来的二进制拷到板子上会报 "No such file or directory",实际是动态库加载不了。

# 查看当前交叉编译器版本 arm-linux-gnueabihf-gcc --version # 检查目标板的架构和 glibc 版本 file /sbin/init

file 命令输出里如果显示 ARM 32-bit,就用 arm-linux-gnueabihf 工具链;如果是 aarch64,就用 aarch64-linux-gnu 工具链。判断了架构之后,编译命令可以写成这样:

make clean make CC=arm-linux-gnueabihf-gcc CFLAGS="-g -Wall -O2 -c" LDFLAGS="-lpthread" # 编译成功后检查产物架构 file gsm0710muxd

file 输出里看到 "ARM, EABI5" 之类的字样说明架构正确。这里我特意把 CFLAGS 写全,因为源码里的 Makefile 默认带了-c选项,如果你在命令行覆盖 CFAGS 时写成CFLAGS="-O2",会把-c漏掉,编译时 gcc 会尝试直接链接生成可执行文件,因为没有 .o 文件而报错。这个玄学问题我吃过一次亏,后来每次编译都强制把-c带上。

3.2 加载并激活 CMUX:AT+CMUX 命令与虚拟节点出现的先后逻辑

编译出可执行文件后,把它拷到目标板,用 udev 规则或者手动启动守护进程。这里要注意 muxd 进程启动时指定的物理串口参数必须与 EC20 模块保持一致,否则对不上波特率会直接出现乱码或者完全无响应。

# 启动守护进程绑定 /dev/ttyUSB0,波特率保持默认 115200 ./gsm0710muxd -s /dev/ttyUSB0 -b 115200 & # 打开原始物理串口发送 AT+CMUX 命令 echo -e "AT+CMUX=0\r" > /dev/ttyUSB0 # 等待片刻后再次查看虚拟节点 sleep 2 ls -l /dev/serial_mux*

AT+CMUX=0,0,5,127,30,3这段命令的含义值得拆开讲。第一个参数0表示启用基本选项模式,5表示最大帧长 127 字节(默认值),30是唤醒时间控制,3是优先级。实际使用中我一般保持默认参数,只有当出现帧过长、大吞吐场景下丢包时,才会去调最大帧长这个值。需要注意的是,虚拟节点的创建完全依赖模块端对 AT+CMUX 命令的响应,如果命令没回 OK,后面所有操作都是空中楼阁。

参数位置含义常见取值说明
AT+CMUX 第一个参数复用模式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。如果前面 AT+CMUX 用的是 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 CC=aarch64-linux-android-gcc CFLAGS="-DANDROID -g -Wall -O2 -c"

4.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 现象:AT+CMUX 命令不回 OK,虚拟节点一直不出现

原因:最常见的是物理串口被其他进程先打开了。muxd 绑定串口时如果发现端口被占用,会直接退出或者反复提示 open failed。另外波特率不匹配也会导致模块根本不解析 AT 命令。

解决:先停掉所有占用物理串口的进程,用lsof /dev/ttyUSB0或者fuser -k /dev/ttyUSB0清理占用。然后用最简单的串口工具单独发 AT 命令确认模块活着,再启动 muxd。我一般会把串口打开方式调成 raw 模式,关闭流控,避免系统把特殊字符做了转换。

stty -F /dev/ttyUSB0 raw -echo 115200

5.2 现象:多路虚拟通道同时收发时偶发乱码和错帧

原因:CMUX 协议对时序非常敏感。帧标志 0xF9 出现在数据载荷里时,协议规定要转义成两个字节,但如果实现里没有正确处理转义,就会出现错帧。另一个可能原因是串口接收缓冲区太小,物理层一次到达的数据超过了驱动单次读取的长度,导致帧被拆成两段解析。

解决:检查 gsm0710muxd_bp.c 里对 0xF9 的处理逻辑,确认转义和逆转义都按标准实现。同时把串口的接收缓冲区调大,或者在 muxd 的读循环里改成一次读完再解析,而不是每次只读固定长度。遇到持续错帧,最有效的定位办法是开一个抓包脚本模拟满载数据,观察 FCS 校验失败报错有没有规律——如果错误集中在某个通道,优先检查那个通道对应的虚拟节点应用层有没有并发写同一个 fd。

5.3 现象:pppd 拨号可以连通,但一有大流量就断线重启

原因:这是经典的水位线问题。CMUX 物理链路的总带宽被多路通道共享,数据通道 DLCI 2 占满带宽时,控制通道 DLCI 1 的 AT 命令无法及时送达模块,模块侧看门狗超时,直接重启了复用链路。

解决:调大 AT+CMUX 命令里的唤醒时间参数,或者把数据通道的 MTU 调小,例如 PPP MTU 从 1500 降到 1200。更重要的一点是,在设计业务逻辑时不要让数据通道完全占满物理带宽,至少留出 10% 的余量给控制通道。为了做到这一点,我一般会限制 pppd 的带宽上限,或者在上层做流量整形。

# pppd 配置里限制 MTU 和发送速率 mtu 1200 rate 80

5.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 bs=1024 count=1024 & # 步骤二:从对端读回并统计吞吐 dd if=/dev/serial_mux2 of=/dev/null bs=1024 count=1024 # 步骤三:观察 muxd 阻塞状况 strace -p $(pidof gsm0710muxd) -e trace=read,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 并记录日志,而不是傻等业务超时。这个检查逻辑帮我省掉了至少三次现场排障,希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/7 1:14:32

Icepak PCB散热仿真三大物理跃迁与建模硬核关节

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/7 1:14:19

渲染管线全解析:从应用阶段到光栅化的性能优化实战

1. 从一次画面撕裂说起&#xff1a;渲染管线到底在解决什么问题很多人第一次接触“渲染管线”这个词&#xff0c;是在调试一个画面异常的时候。比如模型明明在场景里&#xff0c;屏幕上却只出现半个&#xff1b;或者改了光照参数&#xff0c;画面却毫无反应&#xff1b;再或者帧…

作者头像 李华
网站建设 2026/10/7 1:13:07

0-1背包一维DP:倒序遍历、先遍历物品与先遍历背包

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/7 1:12:56

嵌入式DMA驱动开发实战:从原理到STM32应用与避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/7 1:12:56

MOS管发热原因与解决办法:损耗计算、热阻与驱动排查

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华