☰
MTK USB VCOM驱动源码:从USB枚举到ttyUSB串口实战指南
2026/10/8 5:00:17 网站建设 项目流程

简介:MTK USB VCOM驱动源码是一份面向嵌入式开发者的USB虚拟串口驱动程序实现,用于解决MediaTek芯片设备与PC之间通过USB进行固件升级、调试及数据交互的问题。源码完整覆盖驱动初始化、USB协议栈描述符处理、设备枚举、端点数据传输与虚拟串口创建等核心阶段,并包含错误检查和日志调试接口,适合希望深入理解MTK底层通信机制或进行驱动定制移植的开发人员参考。压缩包共8个文件,体积仅727KB,主要包含3个驱动安装程序exe、2个界面标识位图bmp、驱动配置inf与cat签名文件,以及1个XML描述文件,结构精简,既能直接部署使用,也便于按需修改。目前已有1243人学习,对研究USB转串口原理或MTK平台调试工具链的读者而言,这份源码提供了可运行、可扩展的实现范本,有助于快速打通设备与PC的通信链路。

1. MTK_USB_VCOM 驱动源码:别等插上没口才想起来补驱动

在 MTK 平台设备(旧款功能机、Android 板卡、带蜂窝模块的开发板)上跑 Linux 主机的工程师,多半都经历过这么一幕:拿着lsusb看到0e8d开头的 VID,系统却把设备认成“unconfigured device”,或者把一个调制解调器口识别成了ttyACM0,可同一台设备里那个用来抓日志、烧 BootROM 的 VCOM 口怎么都出不来。这个标题里的MTK_USB_VCOM,说白了就是 MediaTek 芯片通过 USB 枚举出来的虚拟串口组,而mtk驱动_源码则是把厂家那层私有驱动补回内核的整套源码。用户拿到手之后要做的不是“装个 exe 点下一步”,而是把源码匹配到自己那棵内核版本、编出.ko、加载后让/dev/ttyUSB0整整齐齐出现。下文按照“先看懂枚举行为,再把源码编成模块,最后带着 udev 规则落地”的顺序讲,适合 BSP 工程师、搞量产刷机工具的人,以及想自己维护 MTK 串口驱动的嵌入式开发者。

2. 先理解枚举:MTK 的 USB 口分四类,VCOM 是一类口不是某一个口

2.1 Modem 口、META 口、BROM 口和 VCOM 串口:驱动源码要处理的到底是什么

MTK 设备插入 USB 后,绝大多数情况不是直接暴露成一个大容量盘。它会在同一个 USB Device 下面按描述符挂出多个 Interface:接口 0 通常是 AT 命令口,也叫调制解调器口;接口 1 可能是工厂工具用的 META 口,专门和 Maui META 通信;接口 2 如果接上 RNDIS,就是网络共享口;真正把数据透传出去、让 PC 端像操作普通串口一样访问的才是 VCOM。这四类口有一个共同特征:接口 class 很多都是厂商自定义的0xFF,不遵守 USB-ACM 的0x02规范。

Linux 内核对这四类口的处理方式完全不同。CDC-ACM 这一层能直接认走0x02的接口,把 AT 口挂成ttyACM0;而0xFF的接口,usbcore 默认不知道交给谁,于是lsusb -t里显示成 unattached,看起来就是“有硬件但没口”。MTK_USB_VCOM 这套驱动源码的价值,并不是加了多少神秘算法,而是用一张明明白白的usb_device_id表,把0xFF接口的端点映射成ttyUSBx,并告诉 USB serial 子系统每个接口用几号端口、走哪几条 bulk 管道。

这张usb_device_id表存在哪个文件里,取决于你拿到的源码来自哪棵内核树。常见做法是这样:把驱动源码放在目标板内核源码树的drivers/usb/serial/下面,以mtk_vcom.c的形式挂上 Kconfig,再在usb_serial_driver的.name字段写上mtk_vcom。当 USB core 匹配到 VID0x0E8D的一系列 PID 时,自动调用它的port_probe回调,为每个接口分配一个 tty 端口。翻过drivers/usb/serial/option.c的老工程师应该马上能认出来:这套驱动骨架和 option 驱动同源,区别在于 ID 表是 MTK 定制的,属于典型的嵌入式内核源码维护动作。

2.2 为什么内核自带的 option、cdc_acm 不能直接覆盖

写驱动源码之前,最容易被挑战的问题是:Linux 内核不是自带option.c吗?不是能识别多半 USB 串口设备吗?答案是option.c本身不认识 MTK 全家桶。它内部维护一张庞大的 ID 表,但 MTK 每次芯片改版,PID 就要跟着变,厂商把预留给 BROM 和 preloader 的 PID 改掉之后,主机端手上没有新 ID,就只能跟着改源码。

还有一个更隐蔽的问题:内核的 CDC-ACM 驱动会抢在自定义串口驱动之前把接口领走。比如某款 MTK 4G 模块把 AT 口描述成了 ACM 类接口,内核加载cdc_acm后自动给它绑定ttyACM0。你再往option.c里加 ID 也不会生效,因为接口已经被别的驱动接管了,只有先把cdc_acm从该端口解绑,或者在自己的驱动源码里声明“对特定接口我优先用”,才能确保 VCOM 口挂到ttyUSB上。

内核提供了一套可行的机制:在usb_driver的 match 阶段做优先级判断,或者在设备描述符里把不需要的接口标记为忽略。很多 MTK 驱动的补丁,实际就是在usb_serial_driver里加probe判定,对同一 Device 的不同 Interface 分别配出端口。如果手上刚好有一份MTK_USB_VCOM驱动源码,解压后重点看两块:第一块是mtk_vcom_id_table[]这个数组,看它写了哪几个 VID/PID;第二块是.num_ports,看驱动给一个设备定义了几个串口。这两个地方改对,整个驱动源码的工作量就完成了一半。

3. 把 MTK USB VCOM 驱动源码编成 .ko 并加载到目标机

3.1 在内核源码树里建立一个可编译的 mtk_vcom 模块

动工之前,先明确工作场景。如果你是给 ARM 开发板交叉编译内核,把源码包里的文件放进目标内核树的drivers/usb/serial/目录;如果是在 x86 工控机、PC 上用来和 MTK 功能机通信,方法一样,只是把交叉工具链换成宿主机 gcc。下面先展示一个最简“新加 USB serial 驱动”的骨架,它包含模块主文件、Kconfig、Makefile 三个部分,写驱动源码的时候这三个文件缺一不可。

#include <linux/module.h> #include <linux/usb.h> #include <linux/usb/serial.h> #include <linux/tty.h> static const struct usb_device_id mtk_vcom_id_table[] = { /* MTK 默认 VID 0x0E8D,PID 随工程模式变化 */ { USB_DEVICE(0x0E8D, 0x0003) }, /* BROM/Download 模式 */ { USB_DEVICE(0x0E8D, 0x2008) }, /* Android 正常模式 USB 枚举 */ { USB_DEVICE_INTERFACE_CLASS(0x0E8D, 0x200A, USB_CLASS_VENDOR_SPEC) }, { } /* 结束符,不要漏 */ }; MODULE_DEVICE_TABLE(usb, mtk_vcom_id_table); static int mtk_vcom_port_probe(struct usb_serial_port *port) { /* 在这里做端口私有数据分配、锁初始化等 */ return 0; } static struct usb_serial_driver mtk_vcom_device = { .driver = { .owner = THIS_MODULE, .name = "mtk_vcom", }, .description = "MediaTek USB VCOM serial port", .id_table = mtk_vcom_id_table, .num_ports = 1, /* 单口模式;多口时改成接口数 */ .port_probe = mtk_vcom_port_probe, .port_remove = NULL, /* 一般留空或做收尾释放 */ }; static struct usb_serial_driver * const mtk_vcom_drivers[] = { &mtk_vcom_device, NULL, }; module_usb_serial_driver(mtk_vcom_drivers, mtk_vcom_id_table); MODULE_LICENSE("GPL v2");

这段代码的逻辑是:mtk_vcom_id_table是驱动能识别的设备 ID 表;module_usb_serial_driver是内核提供的宏,它把模块注册的样板动作组合起来,注册进 USB serial 子系统。收到一个 MTK 设备插入时,usbcore 遍历所有 USB driver 的id_table,匹配后调用port_probe,把这个 interface 挂到对应的struct usb_serial_port上。最需要改的参数是num_ports:目标板一个 USB 设备上同时给两个 VCOM 口,就写 2;写少了另一个口不出/dev/ttyUSB1,写多了会多出一些读写立刻报错的无用口。

然后是同一目录的 Kconfig 与 Makefile。内核里 USB serial 驱动都这么组织:

# drivers/usb/serial/Makefile obj-$(CONFIG_USB_SERIAL_MTK_VCOM) += mtk_vcom.o
config USB_SERIAL_MTK_VCOM tristate "MediaTek USB VCOM serial driver" depends on USB_SERIAL help Say Y here if you want to use MTK VCOM device. If unsure, choose M and compile as module.

配置参数说明:depends on USB_SERIAL是最容易漏的一条。usb_serial_driver注册函数本身来自内核里的usbserial基础模块,没有它这个驱动根本挂不上去。按 M 编译成模块,日后可单独加载,不用为了一个调试口把整个内核换成巨大镜像。编译好之后,drivers/usb/serial/mtk_vcom.ko就是你要的产物。

3.2 单独编一个模块:make M= 方式的边界条件

如果不想为了一个驱动重新整编内核,还有一条更快路径:只编译drivers/usb/serial/子目录,让它生成需要的.ko。以 ARM64 开发板的常见命令为例:

make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- \ M=drivers/usb/serial modules

这条命令里M=drivers/usb/serial告诉 kbuild 只进入这个目录,modulestarget 负责生成该子目录下obj-m对应的所有可加载模块。前提是编译机要有和目标板一致的.config,否则编译出来的模块会因为内核配置项(CONFIG_USB_SERIAL等)和运行时不匹配,一加载就报 version magic 错误。

如果只是在 x86 主机上调试源码格式,可以省掉ARCH和CROSS_COMPILE,内核 Makefile 默认走当前机器架构。我更建议,拿到驱动源码后先在开发机上编译一遍验证语法,再把交叉编译开关打开。这样能把“驱动源码写错了”和“交叉编译环境坏了”两个问题隔离开,配嵌入式内核源码时尤其省时间。

编译完成后,把产物拷贝到目标板。通常先这样检查依赖:

modinfo mtk_vcom.ko

modinfo会打印vermagic和depends两行关键信息。看到depends: usbserial,说明它依赖 USB serial 基础模块;vermagic里的版本字符串和目标板内核uname -r不一致时,先别 insmod,回头去编译机补上正确的内核版本。

3.3 加载后的期望行为:udev 规则、ttyUSB 节点的生成和验证

驱动加载顺序不要搞反。.ko之间有依赖关系,mtk_vcom.ko依赖usbserial.ko。用modprobe会自动读依赖关系并先加载依赖项;用insmod只能自己打两道命令:

modprobe usb_serial # 加载 usbserial 基础模块 modprobe mtk_vcom # 再加载 MTK 专用模块 sleep 3 # 等 USB 枚举稳定 dmesg | tail -n 50 # 看注册成功与否 ls -l /dev/ttyUSB* # 预期出现 ttyUSB0/ttyUSB1

这时设备上电插 USB,dmesg 里会多出类似usbserial: USB Serial support registered for MediaTek USB VCOM serial port的行。如果你用的发行版默认用户不在dialout组,访问/dev/ttyUSB0会拿不到权限。解决办法是写一条 udev 规则:

SUBSYSTEM=="tty", ATTRS{idVendor}=="0e8d", ATTRS{idProduct}=="2008", MODE="0666", SYMLINK+="mtk_vcom"

这条规则的含义是:tty 子系统事件里,凡是 vendor 为0e8d、product 为2008的设备,把设备节点权限设成 666,并额外创建/dev/mtk_vcom的兄弟链接。程序里如果写死/dev/ttyUSB0,系统动态分配一变就抓瞎;写死/dev/mtk_vcom,就不会因为口顺序变化而错乱,这是固定节点名的好处。

4. MTK VCOM 驱动排查实录:接口被抢、模块加载失败、口不通

4.1 现象:插上只看到 ttyACM0,我自己的 mtk_vcom 驱动完全没注册

原因通常是接口 0 被cdc_acm驱动抢先绑定,而且它不会自动释放。MTK 很多工程版本会故意把 AT 口描述成标准 CDC-ACM 类,内核一看到就挂在ttyACM0上,后面属于0xFF的 VCOM 接口因为你的模块根本没注册,自然没人认。

解决方向是把cdc_acm的抢占消掉。最粗暴做法是编译内核时去掉CONFIG_USB_ACM,但开发板上可能还要用其他 ACM 设备,不划算。更精细的做法是在本模块的usb_driver里加probe判定,对 VID0e8d直接返回-ENODEV表示不接管;调试期也可以rmmod cdc_acm再插模块,但要接受它影响机器上所有 ACM 设备。

4.2 现象:insmod 报 Error inserting …… Invalid module format

原因非常集中:.ko文件是基于某棵内核树的配置编出来的,目标板实际加载的内核版本不匹配。vermagic精确到 kernel release,还包含SMP、PREEMPT等配置,任何一项对不上,内核就拒绝加载。

解决顺序是:先用uname -a确认目标机内核版本,再到编译机找对应源码分支,把.config同步过去重编。如果没有目标机厂商给出的官方内核源码,这个问题基本无解,驱动源码文件再正确也救不了错配的 header。我的习惯是先看modinfo里的 vermagic,再决定要不要动手编译。

4.3 现象:insmod 成功,但 lsusb -t 里设备仍是 unattached

insmod 成功只代表驱动模块加载了,它没真正绑定到设备上,问题大多出在id_table。你写的是USB_DEVICE(0x0E8D, 0x2008),设备实际是0x0E8D:0x200A,因为 MTK 往往在 BROM、preloader、META、Android 正常模式之间切换 PID,同一个产品在不同模式下完全是不同的 USB 设备。

解决:用lsusb -vv看真实 VID/PID 和bInterfaceClass,再对照id_table里的宏。如果某个接口 class 是0xFF,改用USB_DEVICE_INTERFACE_CLASS(0x0E8D, pid, 0xFF)匹配;如果同一 PID 下每个接口 class 都是0xFF,记得配合num_ports处理多端口,别只写成 1。

4.4 现象:VCOM 口出现了,但发 AT 没有回包,cat 读也只是输出 0xff

这是 MTK 模块很典型的怪问题。USB 串口和物理 UART 不一样,波特率不直接影响传输,但流控仍然有意义。AT 口与 PC 之间如果要求硬件流控,而终端工具没开启 RTS/CTS,设备端会一直维持流控挂起,数据根本送不出来。

解决方法是终端参数显式带crtscts:

stty -F /dev/mtk_vcom 115200 crtscts raw

如果业务软件用open()配置端口,需要调用tcgetattr拿到参数后打开CRTSCTS标志。也可以用minicom -s进入设置页,把 Hardware Flow Control 改为 Yes,再连上去看是否回OK。流控打开仍没回包,就看 dmesg 里有没有 USB 收端点上的URB status -EPIPE,有的话多半是端点地址和驱动寄存的端点不一致,属于源码里配置端点那部分写错了。

4.5 现象:每次拔插之后 /dev/ttyUSB0 要等很久才出现,甚至出现重复副本

这往往是驱动和 udev 规则嵌套过多导致。每次拔插,内核释放旧节点、重新探测接口,udev 处理规则和应用链接之间有几十毫秒到几秒的时延。更头痛的是,如果写成SYMLINK+="mtk_vcom"且同一台设备有两个 VCOM 口,两个口会抢同一个符号名,互相挤掉。

解决是在 udev 规则里用KERNEL=="ttyUSB[0-9]*"限定内核分配名,再给每个口写不同的SYMLINK,不要统一覆盖。如果只是调试期,干脆去掉 SYMLINK,只保留MODE="0666",减少变量,等口稳定了再谈固定节点。

5. 进阶:用 usb-serial 框架动态加 ID,不做无脑全量加载

5.1 new_id 机制与 ModemManager 隔离

调试 MTK USB VCOM 驱动源码时,最值得记住的操作是给驱动动态添加设备 ID。不用每换一个 PID 就重编一次.ko,USB serial 子系统的 sysfs 接口允许向驱动写入新的匹配对:

echo "0e8d 200b" > /sys/bus/usb-serial/drivers/mtk_vcom/new_id

写入后,再插上0e8d:200b这个设备,usbcore 会尝试绑定 mtk_vcom 驱动,和你在id_table里写死效果一样。这个接口量产时别乱用,因为只对本次运行有效,重启就没了;但在验证一批新 PID、判断“该改源码还是该改 ID 表”的阶段,它是整个调试周期里少有的后悔药。确认好 PID 集合,再统一写进mtk_vcom_id_table[]重新编模块。

把“能用”变成“好用”,还要处理 ModemManager 抢占。桌面 Linux 或带 NetworkManager 的发行版里,ModemManager 会监听/dev/ttyACM0和/dev/ttyUSB0,自动发 AT、自动拨号,把你的 VCOM 口占掉。做专用工具时,建议在 udev 规则里加ENV{ID_MM_DEVICE_IGNORE}="1",让 ModemManager 明确忽略这些口。验证方法很简单:插上设备后看mmcli --list-modems,如果设备被标记为 ignore,就说明规则生效了。

5.2 校验驱动源码正确性的三个步骤

最后说一个我自己的习惯。代码搭好、能出/dev/ttyUSB0之后,别急着认为源码没问题。我会依次做三件事:第一,lsusb -vv把每个 interface 的 class 和 endpoint 拉出来,确认 ID 表里的匹配条件真实可查;第二,拔插三次,每拔插一次看/dev/mtk_vcom的链接是否还能解析到新端口,确认 udev 规则没写错、驱动没有竞态;第三,把mtk_vcom.ko拷贝到另一台同内核版本的机器上加载,验证“不依赖特定编译环境”的可复制性。做完这三步,这份 MTK USB VCOM 驱动源码才算真正能在产线工具里接手。希望帮到你。

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

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

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

立即咨询