ARTICLE DETAIL

资讯详情

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

Linux下USB CDC设备驱动配置:CDC-ACM与CDC-NCM的选型与调试指南

Linux下USB CDC设备驱动配置:CDC-ACM与CDC-NCM的选型与调试指南 简介面向Linux驱动开发、内核移植和嵌入式系统调试场景这份压缩包以USB通信设备类CDC的v2.13.6实现为对象集中分析CDC网络控制模型NCM与CDC抽象控制模型ACM两个子类。网络控制模型专注于将USB设备虚拟成网络接口适用于以太网卡、4G/5G模块的上网场景抽象控制模型则将USB通道模拟为串口常见于蓝牙模块、GSM模块、调制解调器以及工业采集设备。压缩包内共1个文件类型为C源文件包体约3KB属于微型代码包但代码中包含设备枚举、配置描述符解析、中断与批量传输处理、错误恢复等关键逻辑适合用来理解Linux内核中USB设备驱动的工作机制。目前已有690人学习下载。通过逐行阅读该源文件能够掌握CDC设备的注册流程、URB提交方式以及数据收发的底层处理也可以为定制USB虚拟网卡或虚拟串口驱动提供参考对需要快速上手USB类驱动的软件工程师很有帮助。1. CDC 是 USB 设备最通用的通信语言但选错子类会在 Linux 上白折腾半天一台从 Windows 上带过来的 USB 设备插到 Linux 主机lsusb能认到厂商但/dev下干干净净。很多人第一反应是去装 USB 转串口驱动结果翻遍ttyUSB0、ttyACM0都没有最后才在ip addr里看到多了一张网卡——这种我以为它是串口其实它是网卡的场景就是 CDCUSB Communications Device Class家族最常见的翻车现场。标题里的 CDC-NCM、CDC-ACM、linux_usb 指向的其实是同一件事Linux 主机到底怎样跟 USB CDC 设备建立一条稳定、高速、不丢包的通信链路。这篇文章面向做网关、车载、工控和物联网设备的工程师核心就是把这个链路讲透什么场景选 ACM什么场景选 NCM怎么配置内核、怎么在主机侧绑定驱动、怎么在设备侧用 configfs 实现一个 gadget最后把性能和稳定性量化出来。2. CDC-ACM 与 CDC-NCM同一份 USBCDC 规范下的两条分岔路2.1 USB 枚举时 Linux 是怎么区分 ACM 和 NCM 的USB 设备插入主机后主机通过枚举过程读取设备描述符、配置描述符和接口描述符。当接口描述符里的bInterfaceClass等于 0x02Communications Class时这个接口就被归入 CDC 家族。真正决定它走哪条驱动的是接口描述符里的bInterfaceSubClass0x02Abstract Control Model对应 CDC-ACMLinux 驱动是cdc-acm用户态看到的是/dev/ttyACMx。这是传统USB 虚拟串口的标准定义MCU、4G 模组、PLC 调试口大多走这条路。0x0DNetwork Control Model对应 CDC-NCMLinux 驱动是cdc_ncm配合 Communication Class 里附带的一个 Network Data Interface最终在用户态体现为一块 Ethernet 网卡比如usb0。0x06Ethernet Networking Control ModelCDC-ECM老一代的 USB 网卡方案NCM 是它的高速继任者。刚上手的人最容易把 ACM 和 NCM 混为一谈因为两者在枚举报文里都带着 CDC 的 Header、Union 功能描述符。Linux 内核的驱动匹配逻辑其实很直白拿cdc-acm的源码来说它的usb_device_id表里通过宏自动匹配了通信设备类和 ACM 子类// 来自内核源码 drivers/usb/class/cdc-acm.c示意 static const struct usb_device_id acm_ids[] { /* 匹配接口类 0x02CDC接口子类 0x02ACM */ { USB_INTERFACE_INFO(USB_CLASS_COMM, 0x02, USB_CDC_ACM_PROTO_AT_V25TER) }, { USB_INTERFACE_INFO(USB_CLASS_COMM, 0x02, 0x00) }, { USB_INTERFACE_INFO(USB_CLASS_COMM, 0x02, USB_CDC_ACM_PROTO_AT_VENDOR) }, ... };而cdc_ncm的匹配则集中在接口子类 0x0D 上内核的drivers/net/usb/cdc_ncm.c里同样是一张USB_INTERFACE_INFO表。理解这套匹配逻辑的意义在于当你的设备在 Windows 下一切正常、插到 Linux 上却枚举失败时第一个要查的不是芯片厂商的驱动而是设备描述符里到底写了哪个子类。函数库里折腾再多描述符里的字节不对内核就是不去碰它。2.2 CDC-ACM 的形态串口语义为什么在 4G 模组时代仍然活跃CDC-ACM 的模型里有一个管理接口Interrupt IN 端点负责收发控制状态一个数据接口负责收发串口数据。用户态看到的是字符设备写什么就发什么语义就是一根串口线。它的优势在于生态成熟termios属性、stty命令、各种串口调试工具都能直接用不需要配置 IP 地址不需要管服务端和客户端。4G 模组是 CDC-ACM 最典型的存量市场。模组上电后枚举出多个ttyACMx接口一个跑 AT 指令、一个跑 PPP 拨号、一个跑日志。很多工程师习惯用ATQCFG把模组分装成 NCM 网口以规避 PPP 拨号的丢包问题但前提是模组厂商提供了对应的固件配置不是每个模组都允许你在 ACM 和 NCM 之间自由切换。CDC-ACM 的瓶颈很明显一次 USB 传输装一个串口包包与包之间还要等轮询周期吞吐量受限于端点最大包长和操作系统的调度间隔。跑大流量数据时经常出现数据到了但应用层读不完的背压问题这其实不是驱动坏了是串口语义本身就不适合大带宽传输。所以在我经手的项目里凡是单次会话稳定超过 5Mbps 的数据通道基本不会用 ACM。2.3 CDC-NCM 的形态用网卡语义和 NTB 聚合把吞吐拉起来CDC-NCM 的核心思路是把 USB 当网线用。它在管理接口之外定义了一个 Network Data Interface 和一个可选的 Status 接口。数据不是逐包发送而是多个网络包被凑进一个 NTBNetwork Transfer Block里再通过一次 USB 事务送出去。这就是 NCM 比 ECM 高效的原因——ECM 一个 USB 传输只装一个以太网帧NCM 可以批量拼装。NTB 里有两个关键字段wNdpInBlock和dwSignature。驱动收到 NTB 后不是把它当成一个大 blob 交给协议栈而是解析 NDPNCM Datagram Pointer索引表把小包依次摘出来。Linux 的cdc_ncm驱动在接收路径上做了聚合处理发送路径则通过tx_timer和一个可配置的聚合缓冲来控制攒包的时机。你会在/sys/class/net/usb0/下看到一堆tx_*参数这些参数直接决定了小包场景下的吞吐表现。对我来说选 ACM 还是 NCM 的决策标准很简单传命令、做调试、波特率敏感的设备用 ACM要跑视频流、文件同步、批量采集的场景用 NCM。标题里的 CDC-NCM_V2 往往指设备端实现了 USB NCM 2.0 规范NTB buffer 支持更大的配置比如把单块 NTB 上限扩展到 256KB在高速设备上吞吐表现更好。后面第 4 章会单独展开。## 3. Linux host 侧配置把设备识别成网卡还是串口动手改内核与驱动绑定 ### 3.1 内核裁剪时哪些 USB 选项不能省 嵌入式 Linux 的发行版内核经常被裁剪得只剩骨头设备插上后 lsusb 有输出但 /dev 和网卡都不出现多半是内核没编对应驱动。裁剪内核时我建议把下面五类选项全部编进内核不要用模块调试时省一步 insmod 的依赖排查 text Device Drivers USB support * USB Gadget Support // device 侧 gadget 用 * USB Gadget precomposed configurations * USB Network Adapters // 这里开 cdc_ncm、cdc_eem [*] USB Modem (CDC ACM) support // ttyACM 设备的 host 驱动 * USB Mass Storage support // 选 NCM 设备同时带 U 盘模式时不能省CONFIG_USB_ACM编译出来是cdc-acm对应/dev/ttyACM0CONFIG_USB_NET_CDC_NCM编译出来是cdc_ncm对应usb0网卡。这两个 config 项目在 menuconfig 里一个在 Serial 类目下、一个在 Network Adapters 类目下新手经常只找到其中一个。如果你拿到的cdc.rar压缩包里有驱动源码十有八九是厂商针对某个内核版本打过补丁的cdc_ncm.c解压后不要急着覆盖内核源码先看它对应的内核版本——3.x 和 5.x 的 USB 驱动 API 差异很大硬编很可能编不过。内核编好后用一个已知兼容的 CDC-NCM 设备验证插上后dmesg | grep usb里能看到usb 2-1: Manufacturer: xxx和cdc_ncm 2-1:1.0 usb0: register cdc_ncm at usb...这类日志然后ip link set usb0 up即可出现网卡。3.2 设备不匹配驱动时的手动绑定方法有时候设备描述符的子类字节写得不符合规范Linux 驱动表匹配不上但厂商自带的驱动又是基于标准 CDC-NCM 改的。这种非法设备不会出现在/sys/class/net/下但会在/sys/bus/usb/devices/下暴露为2-1:1.0、2-1:1.1这样的接口目录。此时可以手动把接口跟驱动绑在一起试试# 先看当前接口被哪个驱动占着通常显示 (none) ls -l /sys/bus/usb/devices/2-1:1.0/driver # 解绑默认驱动如果占用的话 echo 2-1:1.0 /sys/bus/usb/drivers/usb/unbind # 手动绑定到 cdc_ncm echo 2-1:1.0 /sys/bus/usb/drivers/cdc_ncm/bind # 绑定成功后确认网卡是否出现 ip link show usb0这个操作能成功的前提是接口描述符里bInterfaceClass确实是 0x02、子类虽然不是 0x0D 但厂商驱动兼容 NCM 协议。如果子类差得太远bind会返回No such device。有个笨办法可以绕用usb_modeswitch或者设备的 AT 指令把设备模式切到 NCM让设备自己重新做一遍枚举往往比在主机侧强行 bind 靠谱得多。常见做法是在接入 Linux 前先把设备上电并发送切换命令再插入 USB 口这样枚举出来的就是正确的 NCM 接口。3.3 需要切换设备模式时的 udev 与 usb_modeswitch 配置4G 模组、工业数传设备经常设计成第一次上电是 U 盘/串口需要发一条指令才切到 NCM 网卡。Windows 下有厂商的 Setup 程序自动做到了 Linux 下就得靠usb_modeswitch加 udev 规则。你需要在/etc/usb_modeswitch.d/下写一个针对该设备 VID/PID 的配置文件# /etc/usb_modeswitch.d/ 该文件命名规则一般是 厂商ID:产品ID # 例子具体值按你的设备填 DefaultVendor0xabcd DefaultProduct0x1234 MessageEndpoint0x01 MessageContent5553424312345678000000000000061100000000000000000000000000000000 ThenceMode1对应 udev 规则放在/etc/udev/rules.d/# 80-cdc-ncm-switch.rules ACTIONadd, SUBSYSTEMusb, ATTR{idVendor}abcd, ATTR{idProduct}1234, RUN/usr/sbin/usb_modeswitch -c /etc/usb_modeswitch.d/abcd:1234这里有两个参数容易踩坑一是MessageEndpoint必须是设备的控制传输端点不是所有设备都支持 0x01接口描述符里找bEndpointAddress即可二是切换类的 AT 指令要写在MessageContent里字节序按设备手册来抄错一个字节设备就会直接忽略。配置完成后重新插拔设备dmesg里能看到设备先枚举成串口、然后又重新枚举成 NCM 接口的过程。## 4. 设备端 gadget 实现用 configfs 造出一个 CDC-NCM 或 CDC-ACM ### 4.1 为什么用 configfs 而不是旧式 g_ether 设备侧要实现 USB CDC最简单省事的是编一个 g_ether 内聚 gadget但这东西在量产项目里是灾难——它把 VID、PID、MAC 地址全编在固定配置里改一个字节都要重编内核。而我一般会用 configfs 方案它在运行时动态创建 gadget 功能改描述符、改 MAC、加一个 RNDIS 功能都是写文件的事不需要重新编译内核。 configfs 的依赖是内核需要编 CONFIG_USB_CONFIGFS且设备侧的平台要支持 UDCUSB Device Controller。嵌入式 Linux 上一般先确认 /sys/class/udc/ 非空再动手写脚本。下面这份脚本是我在 AM335x 和 i.MX6 平台上都跑通过的 NCM gadget 创建流程。 ### 4.2 一个完整的 CDC-NCM gadget 脚本与参数说明 bash #!/bin/bash # /usr/local/bin/create_ncm_gadget.sh # 依赖内核开启 CONFIG_USB_CONFIGFS CONFIG_USB_CONFIGFS_NCM modprobe libcomposite mount -t configfs configfs /sys/kernel/config 2/dev/null || true GADGET_DIR/sys/kernel/config/usb_gadget/g1 if [ ! -d $GADGET_DIR ]; then mkdir $GADGET_DIR cd $GADGET_DIR # 描述符VID/PID 需要根据自己的产品号改 echo 0x1d6b idVendor # 示例用 Linux Foundation 的 VID echo 0x0104 idProduct # 示例 PID echo 0x0100 bcdDevice echo 0x0200 bcdUSB # 创建 NCM 功能实例这一行同时生成了usb0网卡设备节点 mkdir functions/ncm.usb0 # 设置网卡 MAC注意抓包和 DHCP 时这两个地址不能一样 echo 02:00:11:22:33:44 functions/ncm.usb0/host_addr echo 02:00:11:22:33:45 functions/ncm.usb0/dev_addr # 绑定配置 mkdir configs/c.1 ln -s functions/ncm.usb0 configs/c.1/ # 绑定 UDC值必须是 /sys/class/udc 下真实存在的控制器名 echo $(ls /sys/class/udc | head -n1) UDC fi # 最后把网卡拉起来配 IP ip link set usb0 up ip addr add 192.168.42.20/24 dev usb0脚本的逻辑分三段第一段加载libcomposite并挂载 configfs第二段创建 gadget 目录并逐项写描述符第三段创建 NCM 功能实例、链接到配置并绑定 UDC。命令的次序不能乱如果在写idVendor之前就创建了ncm.usb0configfs 会报Invalid argument。三个值得注意的参数bcdUSB建议按硬件能力写USB 2.0 设备写0x0200如果硬件支持 3.0 就写0x0300写高了会在枚举时报 no devicehost_addr和dev_addr不能相同曾经为了省事复制同一个 MAC结果 DHCP 地址冲突排查了大半天才发现是这个问题UDC 绑定前必须确认设备侧只有一个 UDC多控制器平台上要显式指定名字而不是用head -n1碰运气。如果设备端 SDK 是带厂商封装的 CDC-NCM_V2 版本驱动里会额外提供tx_qlen、rx_max_buffers这类的模块参数可以在 modprobe 时传入。也可以在 gadget 创建后通过/sys/class/net/usb0/下的属性调整比如调大聚合窗口让小包场景的吞吐更稳定。4.3 设备端 V2 版本和标准 NCM 的差异在哪很多人拿到一份标着 CDC-NCM_V2 的驱动源码会误以为它跟标准 NCM 协议不兼容。实际不是这样。USB 的 NCM 1.0 规范定义了 NTB 最大 64KB对高速 USB 2.0 和早期的 USB 3.0 设备够用但到了 USB 3.x 时代64KB 的聚合窗口反而限制了吞吐。NCM 2.0 规范把 NTB 的最大尺寸放宽到更大的范围256KB 甚至 1MB并且允许收发两端在枚举时通过功能描述符协商具体值。所以你看到的V2驱动本质上是实现了更宽松 NTB 上限的 NCM 驱动协议上仍然向下兼容标准 NCM。在主机侧如果内核版本较老它可能只认 64KB 的 NTB 上限此时设备端单方面把 NTB 配到 256KB会导致主机静默丢包。解决办法是把设备端能力描述符里的dwNtbInMaxSize调回 64KB或者升级主机内核到支持大 NTB 的版本5.x 的内核驱动已经能处理 256KB。## 5. Linux USB CDC 排查5 个高频踩坑现象与修复路径 ### 5.1 枚举失败dmesg 里只有 Unknown device 现象插上设备后 dmesg 只有 USB device not accepting new address 或者 Unknown device 0x1234:0xabcdlsusb 能看到厂商号但打不出设备名。 原因设备端的枚举应答超时常见诱因是 USB 线缆太长导致信号完整性差、设备端 D 上拉电阻不足、或者设备描述符里 bMaxPacketSize0 写成 0。这些不是驱动问题是物理层问题。 解决先换 1 米以内的带屏蔽 USB 线再查设备端的 D 上拉是否接到 3.3V 且用了正确的电阻值。如果是批量生产的板子这个阶段我一般直接上示波器抓 D 的枚举波形比反复改软件快得多。排除硬件后再用 lsusb -v 确认设备描述符里 bcdUSB 和 bMaxPacketSize0 的值是否符合 USB 2.0 规范。 ### 5.2 ttyACM0 出现了但一读就卡住 现象设备被识别成 ttyACM0cat /dev/ttyACM0 直接挂起或读到一半卡死CtrlC 都杀不掉。 原因串口打开时没有发 DTR/RTS 信号。CDC-ACM 的很多设备有开串口后需要 DTR 拉高才启动数据通道的固件机制比如 4G 模组。cat 命令不会主动拉 DTR于是设备侧一直不吐数据看起来就像驱动死了其实是把 AT 通道当数据通道打开了。 解决用 stty -F /dev/ttyACM0 -echo 打开设备后立刻 stty -F /dev/ttyACM0 -clocal或者写一个简单脚本 bash #!/bin/bash DEV/dev/ttyACM0 stty -F $DEV raw -echo -clocal 115200 # 让 DTR 有效再打开读取 exec 3$DEV cat 3-clocal让 modem 控制信号接管 DTR 状态比单纯cat可靠得多。如果应用层用 Python 的pyserial记得ser.dtr True之后再read()顺序反了照样卡。5.3 NCM 网卡起来了但吞吐只有几十 Mbps现象usb0IP 能 ping 通iperf3 TCP 测速稳定在 30-50Mbps无法逼近 USB 2.0 的 480Mbps 理论值。原因可能是 NCM 聚合没有生效也可能是链路协商落在了 USB 1.1 的 12Mbps 档位。先lsusb -t看端口速率lsusb -t # 期望看到 480M如果是 12M 则说明设备端只枚举成了 Full Speed如果链路速率正常接下来检查 NCM 聚合参数。内核 5.4 之后cdc_ncm会通过tx_timer控制攒包间隔默认值比较保守。调优时把 /sys/class/net/usb0/ 下的tx_timer和rx_max_buffers一起调整echo 50 /sys/class/net/usb0/tx_timer # 单位是us50us 的攒包窗口 echo 64 /sys/class/net/usb0/rx_max_buffers # 接收侧允许的缓冲块数量tx_timer太小比如 1-5会导致聚合还没凑满就把包发出去了吞吐反而掉太大500会引入可感知的延迟。我一般从 32us 到 128us 之间做一组梯度测试取吞吐和延迟的折中点。另外 MTU 建议开到 9000NCM 的 NTB 聚合对 jumbo frame 的收益比标准 USB 网卡更大实测能再拉高 10-20%。5.4 设备休眠唤醒后 USB 连接消失现象Linux 主机进入 suspend 再唤醒usb0网卡消失dmesg出现 reset 相关日志但设备不重新枚举。原因主机的 USB autosuspend 在唤醒时没有正确重枚举设备或设备侧 UDC 在 sleep 期间掉电无法响应复位信号。这不是 CDC 协议问题是电源管理的问题。解决禁掉该端口的 autosuspendecho -1 /sys/bus/usb/devices/2-1/power/autosuspend echo on /sys/bus/usb/devices/2-1/power/control更稳定的做法是在 udev 规则里按 VID/PID 禁用ACTIONadd, SUBSYSTEMusb, ATTR{idVendor}abcd, ATTR{power/control}on。设备侧的 UDC 驱动一般还有disable / enable的 sysfs 接口唤醒后手动unbind再bind一次也能救回来但这种方案只能撑到下一轮休眠根治还是要改内核里 UDC 驱动的suspend/resume回调。5.5 MTU 超过了对端 NTB 能力导致大包丢失现象小包 ping 正常scp大文件到了 95% 时卡住最后 session 超时。dmesg里有一堆cdc_ncm: rx error -EILSEQ或ncm: frame error。原因设备端声明的 NTB 最大尺寸是 64KB而主机端把 MTU 和聚合参数调大后单次 USB 传输里的帧超过了设备的 NTB buffer。NCM 的聚合是双方协定但 MTU 可以由用户随意设置一旦ifconfig usb0 mtu 9000后完整的大帧在设备端解包时超出预期。解决一致性验证时确认设备端NtbInMaxSize的协商值# 看内核打印的协商结果 dmesg | grep -i ntb这个值是由枚举协商定死的主机端没有可干预的 sysfs 接口。所以如果设备端报 64KBMTU 就不要开 90009000 的 jumbo 帧加上 IP/TCP 头已经逼近 9KB聚合多个帧后很容易顶到上限稳妥的做法是把 MTU 设回 1500否则就要升级设备端驱动支持更大的 NTB。这个坑是典型的黑匣子问题——表面上是网卡丢包实际上卡在 USB 聚合层的 buffer 上不看 dmesg 根本猜不到。## 6. 最后的验证技巧用 usbmon 和 iperf3 把链路性能量化 驱动跑通只是开始真正决定能不能量产的是这个 CDC 通道的极限吞吐是多少、在长时间跑小包场景下会不会崩、USB 复位之后能不能自动恢复。我习惯用 usbmon 抓取 USB 层的真实传输量和错误计数再用 iperf3 和 ping 打流做双向验证。 先加载 usbmon 抓 URB 层的数据 bash modprobe usbmon # 抓取 usb0 对应总线上的所有 URBs tcpdump -i usbmon0 -w /tmp/usb_trace.pcap抓完用 Wireshark 打开 pcap过滤usb.urb_type URB_SUBMIT数一下同一个 NTB 里包含多少个小包就能确认设备端聚合是否像预期那样工作。如果看到一秒钟几百个 URB 但每个只有一个包说明聚合参数没调对。吞吐量化用 iperf3 双向打流# host 侧作为客户端向设备端 192.168.42.20 打 UDP 100Mbps 流 iperf3 -c 192.168.42.20 -u -b 200M -t 120 -i 1 # 注意观察 jitter 和 lostNCM 链路丢包应小于 0.01%我踩坑比较深的地方是只测 TCP 不测 UDP。TCP 有丢包重传掩盖了 USB 底层丢帧的问题UDP 一上来丢包率原形毕露。测试完把usbmon记录和iperf3结果留档作为该批设备的性能基线后续换驱动、改内核配置都用同一组命令复测性能回退一眼就能看出来。最后说一个血泪教训调试 NCM 网卡吞吐时一定先确认对端设备是 USB High-Speed 还是 Full-Speed。我有一回拿了一块只支持 Full-Speed 的老开发板测 NCM调了一周的tx_timer和 MTU吞吐始终卡在 80Mbps 上不去最后看lsusb -t才发现走的是 12Mbps 链路。从那之后任何 CDC 类设备到手上电的第一件事就是查链路速率再谈参数调优。希望你不用重复这一段弯路这套流程跑通之后你的 USB 设备在 Linux 下会稳定得像一根网线。本文还有配套的精品资源点击获取
返回列表