ARTICLE DETAIL

资讯详情

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

Linux USB CDC驱动解析:NCM与ACM源码实现及移植调试

Linux USB CDC驱动解析:NCM与ACM源码实现及移植调试 简介一套面向Linux平台的USB CDC设备类参考源码v2.13.6适合内核驱动开发、嵌入式系统设计与USB外设移植的工程师。资源围绕CDC-NCM网络控制模型与CDC-ACM抽象控制模型展开前者用于模拟USB网络接口卡实现以太网级通信后者提供虚拟串口能力常用于蓝牙模块、GSM模组等需要串行通信的设备。包体为1个C源文件压缩后3KB虽小却覆盖设备枚举、配置、数据传输和错误处理等关键路径能直接反映Linux对USB CDC类的定义与驱动处理方式。目前已有690人学习在调试自研USB设备、梳理驱动移植流程或理解CDC子类机制时这份代码可作为快速入手的参照。通过逐段阅读源码可掌握主机端与设备端的交互时序、配置请求应答流程以及数据收发异常处理方式并为后续按需扩展网络或串口功能打下基础。1. 一份cdc.c源码包把 Linux 的 USB 网络与串口驱动链路放在同一张桌上手头这份带cdc.c的 CDC-NCM 源码包版本号停在 2.13.6解决的正是 Linux 下 USB CDC 设备从枚举到收发的整条链路问题。干嵌入式 Linux 的人大概率都遇到过这种局面设备插上后dmesg里能看到 USB 设备枚举成功但/dev/ttyACM0不出现或者usb0网卡起不来查来查去问题往往不在硬件而在驱动对 CDC 子类的选择与描述符校验上。CDC-NCM 和 CDC-ACM 是同一套 USB Communications Device Class 标准下两条不同的实现路径前者把 USB 设备模拟成网卡后者模拟成串口而cdc.c里同时覆盖了这两块的驱动代码逻辑。这篇笔记我会按「协议区别 → 源码主链路 → 移植验证 → 踩坑记录 → 吞吐调优」的顺序把这份资源里值得复现和深挖的点逐一拆开讲。2. 分割 CDC-NCM 与 CDC-ACM为什么一张 USB 设备类标准要拆两条实现路径2.1 CDC-NCM 比 CDC-ECM 强在哪聚合帧与批量端点CDC-NCMNetwork Control Model是 CDC 标准里专门为网络设备设计的子类核心思路是让 USB 设备扮演网卡角色主机侧出现的是usb0或enx...这样的网络接口。早期设备多用 CDC-ECM但 ECM 每个 USB 帧只能承载一个完整的 Ethernet 帧MTU 1500 的包在 USB 高速模式下效率很低。NCM 引入了聚合机制把多个网络帧拼进一个 USB 传输块里配合批量端点Bulk Endpoint传输吞吐上限明显比 ECM 高。这份 v2.13.6 的源码里NCM 部分主要处理的就是聚合与解聚逻辑发送方向把 skb 队列里的帧按最大聚合长度打包接收方向从 URB 缓冲区里拆出一个个 Ethernet 帧。搞清楚这个差异对读cdc.c很关键你会看到代码里大量围绕struct sk_buff *和 URB 缓冲区做 memcpy 的地方本质上都是在做聚合与拆包。2.2 CDC-ACM 的串口语义ttyACM0 是怎么冒出来的CDC-ACMAbstract Control Model走的是另一条路它把 USB 设备抽象成串口主机侧出现的是/dev/ttyACM0之类的字符设备。GSM 模块、蓝牙模块、部分单片机调试口都走这个类。ACM 规范里定义了两种接口通信控制接口Communication Interface负责发送 AT 命令和控制信息数据接口Data Interface负责传业务数据。cdc.c里对 ACM 的初始化顺序通常是先拿通信接口的 interrupt IN 端点做状态通知再拿数据接口的批量端点做读写。和 NCM 相比ACM 不涉及网络栈数据路径短但难点在流控和线路编码baud rate、数据位、停止位这些参数要通过SET_LINE_CODING请求下发到设备端。看源码时注意acm_set_line相关的函数它直接决定了你用stty能不能正常配置串口参数。2.3 一份源码两个子类宏开关与模块依赖这份资源把两个子类的实现放在同一个cdc.c里靠编译宏区分常见的做法是CONFIG_USB_CDC_ACM和CONFIG_USB_NET_CDC_NCM各自开启后编译出不同的驱动对象。两者对 USB 描述符的解析起点一样都在probe阶段读取接口描述符之后根据bInterfaceClass、bInterfaceSubClass的值分岔。子类bInterfaceClassbInterfaceSubClass主机侧设备节点典型场景CDC-ACM0x020x02/dev/ttyACMx串口、GSM 模块CDC-NCM0x020x0Dusb0 / enxUSB 网卡所以你看源码时别急着往下读先确认手里的板子 config 里开了哪个宏否则后面 probe 流程看一半会晕。3. 拆解 cdc.c 主链路从 probe 到 URB 收发的五个关键代码位3.1 设备枚举与接口匹配usb_driver 结构怎么命中你的设备USB 驱动的入口是一个usb_driver结构体probe是否被调用取决于设备接口描述符和id_table是否匹配。这套逻辑对 ACM 和 NCM 通用我一般先把probe函数整体读一遍找以下三个关键点第一个是使用usb_interface来做设备实例隔离第二个是从interface-cur_altsetting-desc里取接口描述符第三个是拿到bInterfaceClass后做子类判断。源码里常见的判断逻辑如下static int cdc_probe(struct usb_interface *intf, const struct usb_device_id *id) { struct usb_host_interface *alt intf-cur_altsetting; struct usb_cdc_union_desc *union_desc NULL; int ret; /* 先确认接口描述符确实存在 */ if (alt-desc.bInterfaceClass ! USB_CLASS_COMM) return -ENODEV; /* 然后看子类区分 NCM 和 ACM 的处理路径 */ switch (alt-desc.bInterfaceSubClass) { case USB_CDC_SUBCLASS_NCM: /* 走网络设备注册流程 */ ret cdc_ncm_bind(intf); break; case USB_CDC_SUBCLASS_ACM: /* 走串口设备注册流程 */ ret cdc_acm_bind(intf); break; default: return -ENODEV; } return ret; }这段代码的逻辑很直白驱动先判断接口是不是USB_CLASS_COMM0x02再根据子类分流。你实际调试时如果probe没被调用多半是id_table里没加设备的VID/PID或者设备上报的子类值和宏判断对不上。3.2 描述符校验与 set_interface参数对不上就翻车probe通过只是第一步紧接着就是描述符校验。USB CDC 设备在配置描述符里会带一个 CDC 功能描述符集合包括 Header、Union、Ethernet Networking 等。cdc.c里通常会逐个解析这些功能描述符并校验它们是否齐全。这个环节最常见的现象是dmesg报missing union descriptor原因就是设备端固件没有按规范发送 Union 描述符导致驱动找不到数据接口和通信接口的对应关系。我处理过一块自研板现象是插上后dmesg里报cdc_acm: Interface error抓描述符一看厂商板子把bMasterInterface0和bSlaveInterface0都填成 0而通信接口实际编号是 1。解决方法是让固件修正 Union 描述符的内容或者在驱动侧加容错逻辑。下面是常见的描述符解析片段static int cdc_parse_union_desc(struct usb_interface *intf, struct usb_cdc_union_desc *union_desc) { int master union_desc-bMasterInterface0; int slave union_desc-bSlaveInterface0; /* master 和 slave 必须指向合法的接口号 */ if (master intf-num_altsetting || slave intf-num_altsetting) { dev_err(intf-dev, invalid union desc: %d/%d\n, master, slave); return -EINVAL; } return 0; }这里的bMasterInterface0和bSlaveInterface0是 Union 描述符里的字段分别对应通信接口和数据接口。很多翻车现场都出在这两个数字上值得用lsusb -v重点核对。3.3 收发路径URB、ring buffer 与流量控制驱动绑定成功之后真正的数据搬运靠 URB。ACM 设备的数据读写在acm_read_bulk_callback和acm_write_bulk_callback里完成核心是一个 URB 池和环形缓冲区。NCM 的路径稍微复杂因为要处理网络帧聚合。一个典型的 NCM 接收回调逻辑是先检查 URB 状态然后从 URB 缓冲区解析出 NCM 头再根据载荷长度逐个构造 skb 交给网络栈。static void cdc_ncm_rx_callback(struct urb *urb) { struct cdc_ncm_ctx *ctx urb-context; u8 *buf urb-transfer_buffer; int len urb-actual_length; int offset 0; /* 循环拆包直到缓冲区内没有完整帧 */ while (offset CDC_NCM_HEADER_LEN len) { struct usb_cdc_ncm_nth16 *nth (void *)(buf offset); u16 payload_len le16_to_cpu(nth-wLength); if (payload_len 0 || offset payload_len len) break; /* 构造 skb 送往网络协议栈 */ skb netdev_alloc_skb(ctx-netdev, payload_len); skb_put_data(skb, buf offset sizeof(*nth), payload_len); skb-protocol eth_type_trans(skb, ctx-netdev); netif_rx(skb); offset payload_len; } /* 重新提交 URB进入下一次接收循环 */ usb_submit_urb(urb, GFP_ATOMIC); }这段代码里wLength是每次聚合块的长度netif_rx把数据交出去后 URB 立即重新提交。你可以留意一下实际驱动里的 NCM 头解析是 NTH16 还是 NTH32老版本设备基本用 16 位头吞吐偏高时偶尔会听说 32 位头的实现这是踩坑点后面细说。4. 移植到你的板子内核配置、交叉编译与设备节点验证4.1 核对内核版本与 config四个必须开启的选项把这份源码落到自己板子上之前先确认内核配置。v2.13.6 这套代码适用于老版本内核对 CDC 的通用封装新的内核把 NCM 和 ACM 的驱动拆成了独立文件但核心机制没变。配置时四个选项别漏CONFIG_USBy CONFIG_USB_SUPPORTy CONFIG_USB_CDCy CONFIG_USB_NET_CDC_NCMy CONFIG_USB_ACMy如果你的内核是 5.x 以上drivers/usb/class/cdc-acm.c和drivers/usb/net/cdc_ncm.c是分开的直接把对应项set成y或m即可。老内核则可能只有一个cdc.c这时要确认编译单元包含它。4.2 模块编译与 insmod 加载流程我把源码解压后一般先走一遍单独编译流程确认依赖头文件路径没问题再考虑合入内核树。单独编译时Makefile 里指定内核源码目录和交叉编译链前缀指令如下make -C /path/to/kernel M/path/to/cdc_src \ ARCHarm CROSS_COMPILEarm-linux-gnueabihf-编译完成产物是cdc_acm.ko和cdc_ncm.ko老内核可能合成一个cdc.ko拷贝到板子后加载的顺序有讲究先modprobe usbcore再加载驱动模块依赖的usbnet模块需要提前确认存在。加载指令insmod /lib/modules/$(uname -r)/kernel/drivers/usb/class/cdc_acm.ko insmod /lib/modules/$(uname -r)/kernel/drivers/usb/net/cdc_ncm.ko我一般加上lsmod | grep cdc和dmesg | tail -20两步确认加载结果。modprobe和insmod的区别你肯定知道前者会解析依赖后者不会如果你只insmodNCM 模块而usbnet还没加载会直接报未知符号。4.3 设备节点与网络接口的验证命令组合驱动加载成功只是开始真正验证要看设备节点和网络接口是否如期出现。ACM 设备验证dmesg里的设备节点分配信息和读写测试dmesg | grep tty ls -l /dev/ttyACM* stty -F /dev/ttyACM0 115200 cs8 -cstopb -parenb echo AT /dev/ttyACM0NCM 的验证要分两步先确认网络接口名再用ip配地址并检查链路状态ip link show ip link set usb0 up ip addr add 192.168.7.2/24 dev usb0 ping -I usb0 192.168.7.1如果你的设备支持 RNDIS 或者 CDC-ECM接口名可能变成enx...或eth0不要死等usb0。另外 NCM 设备在ip link里看不到 MAC 地址时ping大概率不通先用ethtool usb0查一下链路是否 up。5. CDC 驱动调试避坑五个高频翻车现场与对应解法5.1 插上没反应dmesg 只显示 new full-speed USB device现象设备插入 USB 口后dmesg只提示new full-speed USB device之后没有任何cdc相关日志lsusb能看到设备但probe未被调用。 原因最常见的是id_table里缺少设备的 VID/PID或者驱动被其他模块抢先绑定了接口。 解决用lsusb -v查出 VID/PID在id_table里补一行。另一种情况是板子上同时加载了usb-storage或 HID 驱动它们对bInterfaceClass的判断优先级不同可在probe入口加dev_info打印确认谁抢了设备。5.2 /dev/ttyACM0 不出现/dev/ttyUSB0 却出现了现象设备插上没有/dev/ttyACM0反而出现/dev/ttyUSB0功能基本正常但总感觉不对。 原因ttyUSB0是ftdi_sio或pl2303这类 vendor 专用驱动创建的节点这类驱动直接读取设备厂商定义的协议没有走 CDC-ACM 的抽象层。设备本身可能被识别成了厂商专用串口而非标准 ACM。 解决先确认设备的bDeviceClass和接口描述符如果接口描述符里bInterfaceClass不是 0x02那它默认就不是标准 ACM 设备你别指望cdc_acm能接管。反过来如果确实是 ACM 设备却被 vendor 驱动抢走用modprobe -r卸载 vendor 驱动再加载cdc_acm。5.3 usb0 起不来Network connection not yet established现象ip link set usb0 up执行成功但ip link显示NO-CARRIERping不通。 原因NCM 网卡驱动里有一个连接状态机制依赖设备端发送的Notification事件比如NETWORK_CONNECTION通知。设备端如果没发这个通知主机侧就一直认为链路没有建立。 解决抓dmesg看有没有cdc_ncm: network connection not yet established这行日志。有的话检查设备固件是否发送了连接状态通知或者查看驱动里对USB_CDC_NOTIFY_NETWORK_CONNECTION的处理是否被某个if条件挡住了。这块是 CDC-NCM 调试里最经典的玄学区域排错思路是先把notify端点用 usbmon 抓一遍。5.4 传输速度上不去批量端点与 NAK现象NCM 网卡 ping 正常但iperf吞吐只有预期的一半tcpdump看数据帧间隔不规律。 原因批量端点在高负载下会产生 NAKUSB 主机控制器会不断重试如果驱动里 URB 数量太少或者缓冲太小设备端的接收窗口很快就满了。另外 NCM 聚合头是 16 位还是 32 位直接影响单帧可携带的载荷上限。 解决增大rx_urb_size和提交的 URB 数量。老代码里这两个参数多半是编译期宏修改后重新编译。用usbmon抓 URB 看实际actual_length如果长时间小于缓冲上限问题在设备端发送节奏而不是驱动侧。5.5 休眠唤醒后设备失联autosuspend 惹的祸现象板子从 suspend 唤醒后lsusb看不到设备必须重新插拔才恢复。 原因USB 设备的 autosuspend 机制在系统休眠时把设备挂起了但cdc_acm或cdc_ncm驱动没实现reset_resume回调唤醒时设备状态没有重新初始化。 解决在驱动里实现reset_resume并在其中重新执行set_interface和 URB 重提交。临时验证的办法是打开CONFIG_USB_AUTOSUSPEND_DELAY或者直接用echo -1 /sys/bus/usb/devices/1-1/power/autosuspend_delay_ms关掉自动挂起。6. 数据链路再深一步用 usbmon 和 URB 参数调优 CDC-NCM 吞吐6.1 调整 URB 数量与缓冲大小NCM 的吞吐上限很大程度上由 URB 池大小决定。默认情况下驱动会分配rx_urbs个接收 URB每个的大小是rx_urb_size。把这两个参数调大能显著降低批量端点在高速传输时的 NAK 概率。我一般先看dmesg里驱动的初始化参数再按设备端缓冲区大小折算cat /sys/module/cdc_ncm/parameters/rx_urb_size echo 16384 /sys/module/cdc_ncm/parameters/rx_urb_size如果是老内核的cdc.c参数多半不支持 sysfs 动态修改那就得改源码里的宏定义重新编译。调 URB 数量不如调rx_urb_size见效快因为批量端点传输本身有每微帧的 token 上限缓冲增大后设备端可以连续填充减少主机控制器发起传输的次数。6.2 用 usbmon 定位丢包发生在哪一层排查 NCM 吞吐问题时我习惯先用usbmon抓 USB 总线上的原始 URB把问题和协议栈解耦。开启方式modprobe usbmon cat /sys/kernel/debug/usb/usbmon/1u | grep usb0如果抓到的 URB 数量正常但ping丢包问题大概率在驱动把数据交给网络栈之后的路径如果 URB 本身就有丢先查设备端固件的发送节奏。一回我调一块 4G 模块板卡iperf吞吐只有 20Mbpsusbmon 一把抓下来发现设备端每个 URB 只有 512 字节的实际载荷改设备端聚合策略后直接翻到 80Mbps。从那以后我每次拿到新的 CDC 设备第一步永远是先跑一轮 usbmon 看原始数据形态再动驱动参数。希望帮到你。本文还有配套的精品资源点击获取
返回列表