USB CDC设备在Linux系统(RK3506)通过udev枚举说明
做了一个USB转多路RS485接口的硬件,这里说明Linux下驱动它的方法。驱动运行后,会生成n个/dev/ttyACMx设备文件,'x'是"0,1...7"这样的端口号,实测发现这个端口号与硬件物理端口顺序并不一致,于是利用 Linux 系统的 udev 机制实现设备文件名称与物理端口一致。当枚举的USB设备VID=1a86且PID=fe0c时,会为每一个/dev/ttyACMx设备再创建一个软链接,这个软链接的名字与物理端口号一致。当然直接修改/dev/ttyACMx这个文件名也是可以的,喜欢这种方式的可以修改脚本。如果要支持VID,PID不一样的设备,也要适当修改脚本。

USB CDC Linux 设备驱动
Linux系统内核源码默认支持USB CDC设备,但是一般的嵌入式系统默认不选择编译,需要用户通过make menuconfig命令打开配置界面,如下图,找到:"Device Drivers > USB Support"目录下,选择"USB Modem (CDC ACM) support",它依赖于"USB Device support",这个一般默认被选上的。

选择后重新编译内核,烧录运行后,当USB CDC设备插入时就会创建/dev/ttyACMx设备文件。
Linux udev
udev 是 Linux 内核 2.6 引入的用户空间设备管理器,现在是 systemd 的一部分(systemd-udevd),负责动态管理 /dev 目录下的设备节点、处理热插拔事件、自定义设备权限和命名,是所有 Linux 硬件交互的核心基础。
udev 特点:
·只在设备连接时动态创建节点,断开时自动删除
·支持固定设备名(基于厂商 ID、产品 ID 等唯一标识)
·可自定义设备权限、属主、属组
·设备热插拔时自动运行脚本(比如自动挂载 U 盘、启动传感器采集程序)
文件说明:
·99-usb-cdc-1a86-fe0c.rules
·udev 规则文件。
·匹配 SUBSYSTEM=="tty"、KERNEL=="ttyACM*"、idVendor=="1a86"、idProduct=="fe0c" 的串口设备。
·每创建一个 /dev/ttyACMx 设备,都会触发一次该规则。
·通过 PROGRAM 调用 /usr/lib/udev/usb-cdc-1a86-fe0c-name,并用脚本输出结果创建 /dev/ttyUSBCDC* 软链接。
·usb-cdc-1a86-fe0c-name
·udev helper 脚本。
·根据 USB interface number 计算软链接编号。
·只处理偶数 interface number,例如 00、02、04、06、08、0a、0c。
·不调用 udevadm,避免在 udev PROGRAM 中死锁。
目标安装路径
在 RK3506 rootfs 中,两个文件应安装到:
/etc/udev/rules.d/99-usb-cdc-1a86-fe0c.rules /usr/lib/udev/usb-cdc-1a86-fe0c-name
注意:这两个文件要有可执行权限。 RK3506 SDK的 Buildroot overlay 中的对应位置是:
buildroot/board/rockchip/rk3506/fs-overlay/etc/udev/rules.d/99-usb-cdc-1a86-fe0c.rules buildroot/board/rockchip/rk3506/fs-overlay/usr/lib/udev/usb-cdc-1a86-fe0c-name
RK3506实机运行效果如下:
调试命令
修改规则后,板端可重新加载 udev 规则:
udevadm control --reload-rules udevadm trigger --subsystem-match=tty
查看当前软链接:
ls -l /dev/ttyUSBCDC*
查看设备 interface number:
udevadm info -a -n /dev/ttyACM0

注意事项
·udev 规则是按 /dev/ttyACMx 设备逐个触发,不是每插入一台 USB 设备只触发一次。
·脚本输出到 stdout 的内容会被 udev 用作 SYMLINK+="%c" 的名字,因此不要在脚本中随意 echo 调试信息。
·如果需要调试输出,建议临时写入 /tmp/*.log,不要写 stdout。
SYMLINK_STRIDE 当前按 7 口 USB CDC 设备设计,如设备串口数量变化,可手工修改该全局变量。
附录
·99-usb-cdc-1a86-fe0c.rules
# USB CDC 1a86:fe0c -> /dev/ttyUSBCDCy, ordered by interface number from 0 SUBSYSTEM=="tty", KERNEL=="ttyACM*", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="fe0c", \ IMPORT{builtin}="usb_id", \ PROGRAM="/usr/lib/udev/usb-cdc-1a86-fe0c-name %E{ID_USB_INTERFACE_NUM} %E{ID_PATH} %E{DEVPATH}", \ SYMLINK+="%c"
·usb-cdc-1a86-fe0c-name
#!/bin/sh # USB CDC (VID=1a86 PID=fe0c) -> /dev/ttyUSBCDCy symlink namer #y starts at hex(interface_number) / 2 #if /dev/ttyUSBCDCy exists, keep adding SYMLINK_STRIDE until it is free. # Args: ID_USB_INTERFACE_NUM ID_PATH DEVPATH # NOTE: must not call udevadm here (deadlock when run from udev PROGRAM). PATH=/sbin:/bin:/usr/sbin:/usr/bin VID="1a86" PID="fe0c" TTY_PREFIX="ttyACM" SYMLINK_PREFIX="ttyUSBCDC" SYMLINK_STRIDE=7 MAX_SYMLINK=511 iface_to_dec() { val="$(printf '%s' "$1" | tr 'A-F' 'a-f')" case "$val" in ''|*[!0-9a-f]*) return 1 ;; *[!0-9]*) printf '%d' "0x$val" ;; *) printf '%d' "$((10#$val))" ;; esac } iface_num_from_devpath() { devpath="$1" cur="$(readlink -f "/sys$devpath/device" 2>/dev/null)" || return 1 while [ -n "$cur" ] && [ "$cur" != "/" ]; do if [ -f "$cur/bInterfaceNumber" ]; then cat "$cur/bInterfaceNumber" return 0 fi cur="$(dirname "$cur")" done return 1 } first_available_symlink() { num="$1" while [ "$num" -le "$MAX_SYMLINK" ]; do if [ ! -e "/dev/${SYMLINK_PREFIX}${num}" ]; then printf '%s\n' "$num" return 0 fi num=$((num + SYMLINK_STRIDE)) done return 1 } iface_num="$1" devpath="$3" [ -n "$devpath" ] || exit 1 if [ -z "$iface_num" ]; then iface_num="$(iface_num_from_devpath "$devpath")" || exit 1 fi iface_dec="$(iface_to_dec "$iface_num")" || exit 1 [ $((iface_dec % 2)) -eq 0 ] || exit 1 index=$((iface_dec / 2)) link_num="$(first_available_symlink "$index")" || exit 1 printf '%s%d\n' "$SYMLINK_PREFIX" "$link_num"
(End)