跳转至

第 9 章 USB、串口与硬件接入

机器人上位机与下游设备的物理接口层大多落在四类:USB(相机、激光雷达、USB-CAN 卡、调试口)、串口(底盘 MCU、电池 BMS、老式传感器)、GPIO / I2C / SPI(板端 IO 与低速传感器)、V4L2(视频设备)。这一层的排障高频问题——/dev/ttyUSB0 每次插拔都可能变成 ttyUSB1、两个相同芯片的设备互相抢名、CH340 插上就被 brltty 吃掉、gpiod 与旧的 sysfs GPIO 接口混用——都发生在这一章的机制上。

udev 规则是解决设备名漂移的正解;evtest / v4l2-ctl / gpiodetect 是设备侧的常用尺子。第 4 章「设备节点与权限」的权限视图在此展开到规则层;ROS 侧的设备驱动包与消息发布在 ROS2-note 第 7 章。

1. USB 拓扑与设备发现

USB 是主从 + 树状拓扑:主机(Host)↔ Hub ↔ Function(设备)。Linux 侧内核模块栈:xhci_hcd(USB 3.x)、ehci_hcd / ohci_hcd(USB 2/1.1);下面挂 usb-storage、cdc-acm、ftdi_sio、ch341、cp210x 等设备类驱动。

四把查询尺子:

lsusb                                  # 顶层视图:总线/设备号、VID:PID、厂商产品名
lsusb -t                               # 树状拓扑,含每层速率(480M / 1.5M / SuperSpeed)
lsusb -d 1a86:7523 -v                  # 详询某 VID:PID 的描述符(VID/PID 是设备识别主键)
sudo usb-devices                       # 更完整的每设备段(含驱动、serial、endpoint)

内核视图在 /sys/bus/usb/:

## 每个物理口对应一个 devices 目录;用 vendor/device 与 product 拼出人类可读名
for d in /sys/bus/usb/devices/*/product; do
    dir=$(dirname $d)
    printf "%-14s %s\n" "$(basename $dir)" "$(cat $d 2>/dev/null)"
done

## 反查一个 /dev/ttyUSB* 背后是哪条物理链路
readlink -f /sys/class/tty/ttyUSB0/device
## → .../usb3/3-1/3-1:1.0/ttyUSB0  表示在 bus 3 port 1 的接口 0

判据:USB 设备名漂移的物理成因是"枚举顺序"——内核按 xhci 端口扫描顺序分配 usbN,插拔顺序或别的设备加载时机变了就变号。换物理口能规避一部分(不同 port 走不同枚举路径),根治要 udev 规则。

坑:/dev/ttyUSB* 编号随插拔顺序变

编号不是设备身份:同一块板子这次是 ttyUSB0、下次可能是 ttyUSB1;脚本按编号写死,重启后就会连到别的设备。

2. USB 串口与设备名漂移

机器人上最常见的 USB 类是"USB 转串口",芯片三大家:

芯片 内核驱动 VID:PID 例 /dev 设备族 备注
FTDI FT232RL / FT232H ftdi_sio 0403:6001 ttyUSB* 老工业设备最常见;厂商 VCP 驱动生态成熟
WCH CH340 / CH341 ch341 1a86:7523 ttyUSB* 便宜、国产模块广泛;brltty 抢占问题的主角
Silicon Labs CP210x cp210x 10c4:ea60 ttyUSB* Arduino 某些板、传感器模块常用

CDC-ACM(USB 标准里的"抽象控制模型")走 cdc_acm 驱动,设备名 ttyACM*——STM32 / Arduino 原生 USB 串口、多数现代模块。区别:ttyUSB* 意味着外挂桥芯片(ftdi / ch34x / cp210x),ttyACM* 意味着设备本身实现 CDC 类。

dmesg | grep -E "ttyUSB|ttyACM|ftdi|ch34|cp210"     # 内核认驱动与分配设备名
stty -F /dev/ttyUSB0 -a                              # 当前串口参数:波特率、parity、flow
ls -l /dev/ttyUSB0                                    # 权限与属组(第 4 章「设备节点与权限」)

判据:插上"没设备"多半是内核没匹配到驱动,dmesg 会打 unsupported 或干脆不出现;modprobe ch341 / modprobe ftdi_sio 显式加载,或用 echo "0403 6001" > /sys/bus/usb-serial/drivers/ftdi_sio/new_id 手工绑定(临时手段)。

从物理口到固定设备名的链路:

flowchart LR
    topo["USB 拓扑<br/>bus / port / interface"] --> kern["内核驱动<br/>ftdi_sio · ch341 · cdc-acm"]
    kern --> node["/dev/ttyUSB0<br/>编号随插拔顺序变化"]
    node --> rule["udev 规则<br/>按 idVendor / idSerial 匹配"]
    rule --> alias["固定软链<br/>如 /dev/robot_base"]
    alias --> app["应用 / ROS 2 驱动节点"]

3. udev 规则与设备名固定

udev 是 systemd 的一部分,规则文件在 /etc/udev/rules.d/*.rules(数字小者优先)。核心动作:从 KERNELS(内核路径)、ATTRS(sysfs 属性)、ENV{ID_*}(预置变量)里匹配设备,然后 SYMLINK 建符号链接或改 MODE / GROUP。

3.1 固定设备名

同种芯片挂两个(例如两个 FTDI 都是 0403:6001),用序列号区分:

## /etc/udev/rules.d/99-robot-serial.rules
## 底盘 MCU 的 FTDI 有内置 serial
SUBSYSTEM=="tty", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6001", \
    ATTRS{serial}=="A50280SB", SYMLINK+="robot/chassis", MODE="0660", GROUP="dialout"

## 传感器板的 CH340 无 serial,用物理路径区分(插在固定 hub 的固定口)
SUBSYSTEM=="tty", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="7523", \
    KERNELS=="3-1.2", SYMLINK+="robot/imu", MODE="0660", GROUP="dialout"

生效流程:

sudo udevadm control --reload                       # 规则改了必须 reload
sudo udevadm trigger --subsystem-match=tty          # 已连设备重新触发一次

## 调试
udevadm monitor --property                          # 拔插时看 uevent 属性
udevadm info -q all -n /dev/ttyUSB0                 # 查已存在设备能匹配到什么
udevadm test $(udevadm info -q path -n /dev/ttyUSB0)  # 预演规则、打出将创建的链接

判据:符号链接比 MODE=0666 更稳妥——链接指向当前内核分配的真实 ttyUSBn,权限仍靠组,脚本读 /dev/robot/chassis 稳定不变。

写规则前用 udevadm info 把可匹配的键推导出来:udevadm info -q property <dev> 给左侧能用的 SUBSYSTEM / KERNEL / DEVNAME;udevadm info -a <dev> 沿父链列出全部 ATTRS{...} 与 KERNELS==,从里选出能唯一标识该设备的组合(上面的规则就是从中取 idVendor/idProduct/serial/KERNELS)。

规则落地要点:写入 /etc/udev/rules.d(或 /run/udev/rules.d,重启即失效、适合临时试验)后必须 udevadm control --reload 再 udevadm trigger 才对新事件生效;udevadm test <dev> 干跑会打印该设备命中的规则行与将创建的链接(如 LINK 'robot/...'、DEVLINKS=/dev/robot/...),据此确认 SYMLINK 是否按预期生成——这是预演,不改变当前设备状态。

3.2 udev 与服务触发

udev 规则可以带 RUN+= 让某个事件起一个服务,但生产环境不推荐:规则执行超时=内核 uevent 队列阻塞、失败无日志;改 systemd .device 单元的 BindTo= / BindsTo= 更稳。

坑:brltty 会抢占 CH340

盲文终端守护进程会把部分 USB 串口芯片当成显示设备接管,表现为“设备在、打不开”;禁用对应 udev 规则比反复重插有效。

4. 串口直连调试与固件烧录

4.1 串口三件套

## 配串口:115200 8N1 无流控(写脚本前先手工配一次)
stty -F /dev/robot/chassis 115200 cs8 -cstopb -parenb raw -echo

## 交互:picocom 更清爽,Ctrl+A Ctrl+X 退
picocom -b 115200 /dev/robot/chassis

## 通用兜底:screen 也能起串口(会话管理见第 2 章「命令效率工具与会话保持」)
screen /dev/robot/chassis 115200

判据:先 stty 配、再开交互;不少设备对 DTR/RTS 敏感,picocom --nolock --lower-dtr 或 --raise-dtr 决定要不要拉这两个信号(STM32 板进入 bootloader 就要靠它们组合)。

4.2 pyserial 最小样

import serial, time
with serial.Serial('/dev/robot/chassis', 115200, timeout=0.5) as s:
    s.write(b'ping\r\n')
    line = s.readline().decode(errors='replace').strip()
    print("reply:", line)

ROS 2 侧发布自写串口节点的最小路径在 ROS2-note 第 7 章「传感器驱动接入」的自写节点部分。

4.3 brltty 抢占串口

现象:CH340 / 部分 CP210x 插上,/dev/ttyUSB* 只存在几百毫秒就消失,dmesg 无异常。

触发条件落在具体规则文件:Ubuntu 桌面版安装 ubuntu-desktop 时作为依赖装入 brltty,其 /usr/lib/udev/rules.d/85-brltty.rules 以 ENV{PRODUCT}=="vid/pid/*" 匹配盲文屏的 VID:PID 名单,命中即置 ENV{SYSTEMD_WANTS}+="brltty-udev.service";守护进程经 libusb 声明 USB 接口,内核串口驱动随后把 ttyUSB 节点断开。CH340 中招的原因在名单本身:1a86/7523 在规则中对应 Baum 盲文屏(历史设备沿用了沁恒芯片的默认 VID:PID),而 CH340 模块上电即报同一组 ID,于是按默认芯片标识出厂的模块全部被误匹配。同一名单不含 FTDI 的标准 0403:6001——中招与否以 VID:PID 是否命中名单为准,名单随 brltty 版本变化。

解决:

sudo systemctl mask brltty brltty-udev
sudo udevadm control --reload && sudo udevadm trigger

更细的处置是按 VID:PID 写豁免规则或只掩蔽 brltty-udev.service,避免影响真正使用盲文终端的场景。

4.4 固件烧录与主机固件

用户态烧录 MCU 侧(不展开 MCU 开发本身):

sudo apt install dfu-util openocd
dfu-util -l                              # 列出 DFU 模式设备
dfu-util -d 0483:df11 -a 0 -s 0x08000000:leave -D firmware.bin     # STM32 常见
openocd -f interface/stlink.cfg -f target/stm32f4x.cfg -c "program firmware.elf verify reset exit"

openocd 的 interface 配置按调试器型号选择:ST-Link 用 interface/stlink.cfg,J-Link 用 interface/jlink.cfg,目标芯片配置取自 target/ 目录。

主机侧固件更新:fwupd 是 LVFS(Linux Vendor Firmware Service)的客户端:

fwupdmgr get-devices
fwupdmgr refresh
fwupdmgr get-updates
sudo fwupdmgr update

判据:UEFI / BIOS 更新前必须接 UPS——刷到一半断电就是变砖。生产上位机除非有明确 CVE 需要修,固件更新走维护窗口 + 回滚准备。

5. SocketCAN 与 slcand

CAN 接口在 Linux 里归入网络栈(can0 can1),fieldbus-note 第 9 章「工具链与抓包排障」是主线;此处只交代"USB 转 CAN 卡"的接入侧:

## SLCAN 固件的适配器(Lawicel、CANable 等)走串口;PEAK / Kvaser 由内核原生驱动接管
sudo apt install can-utils
sudo slcand -o -c -s6 /dev/ttyUSB0 can0   # -s6 = 500 kbps(0=10k … 6=500k、8=1M)
sudo ip link set can0 up
sudo ip link set can0 txqueuelen 1000

## SocketCAN-native(peak_usb、kvaser_usb、gs_usb)先配位速率再 up
sudo ip link set can0 type can bitrate 500000 restart-ms 1000
sudo ip link set can0 up
candump can0

slcand(serial line discipline CAN daemon)把串口线协议(Lawicel 编码)转成 SocketCAN 接口;peak_usb 之类内核驱动则直接把 USB 设备当 CAN 网卡。判据:内核模块加载与 dmesg | grep can 是第一步;ip -details link show can0 查位速率与状态。

6. GPIO 与总线接入

6.1 gpiod 主线

sysfs GPIO(/sys/class/gpio/)已被内核标记 deprecated,主线是 libgpiod 与字符设备 API:

sudo apt install gpiod
gpiodetect                                # 列出 gpiochip0 / gpiochip1
gpioinfo gpiochip0                        # 每根线的方向、偏置、标签
gpioset gpiochip0 17=1                    # 拉高第 17 号线
gpioget gpiochip0 17                      # 读
gpiomon gpiochip0 17 RISING_EDGE          # 等边沿

设备树 overlay(板端):Raspberry Pi 用 /boot/firmware/config.txt 里 dtoverlay=gpio-shutdown、gpio-key 等;I2C / SPI 也要 dtparam=i2c_arm=on dtparam=spi=on 打开。bcm2835 / wiringPi 等遗留库已停止维护,新项目用 libgpiod。

6.2 I2C / SPI 用户态

sudo apt install i2c-tools
sudo i2cdetect -y 1                        # 扫总线 1 上的 7-bit 地址
sudo i2cget -f -y 1 0x68 0x75               # 读某从寄存器
sudo i2cset -f -y 1 0x68 0x6B 0x00          # 写;-f 强制

sudo apt install spi-tools
spidev_test -D /dev/spidev0.0 -v            # 简单回环测 SPI

/dev/i2c-* 与 /dev/spidev*.* 权限默认在 i2c / spi 组;用户加组生效规则同第 4 章「设备节点与权限」。

6.3 V4L2 相机

USB 相机、RealSense、深度相机多数走 V4L2:

sudo apt install v4l-utils
v4l2-ctl --list-devices                    # 列所有 video node
v4l2-ctl -d /dev/video0 --list-formats-ext # 支持的分辨率与帧率
v4l2-ctl -d /dev/video0 --set-fmt-video=width=1280,height=720,pixelformat=MJPG
ffmpeg -f v4l2 -video_size 1280x720 -i /dev/video0 -frames:v 1 snap.jpg   # 抓一帧

判据:USB 带宽限制——多个 UVC 相机共享一个 USB 控制器时容易超出 isoc 带宽,表现为绿屏、掉帧;lsusb -t 看是否挂在同一 bus,必要时分线。ROS 2 侧 usb_cam / realsense2_camera 的启动参数在 ROS2-note 第 7 章。

6.4 evtest 输入设备

手柄、键盘、触摸板、按钮阵列都归入 input:

sudo apt install evtest
sudo evtest                                 # 交互列设备、选一个跟事件流
## 或直接看
sudo hexdump -C /dev/input/event0

设备节点 /dev/input/eventN 与 mouseN / kbd 存在漂移,用 udev 规则按 ATTRS{name} 固定符号链接是常见做法:

SUBSYSTEM=="input", ATTRS{name}=="Xbox Gamepad*", SYMLINK+="robot/joystick"

判据:/sys/class/input/eventN/device/modalias 里能拿到匹配用的 VID/PID/物理路径。

7. 要点

  • USB 设备名 = 枚举顺序,会漂;lsusb -t 看拓扑、readlink /sys/class/tty/... 反查物理路径。
  • ttyUSB* 与 ttyACM* 是分水岭:前者挂桥芯片(FTDI / CH340 / CP210x),后者走 CDC-ACM;驱动与 udev 匹配规则不同。
  • udev 规则 + SYMLINK 是根治设备名漂移的正解,chmod 666 每次插拔会失效;调试靠 udevadm monitor 与 udevadm test。
  • brltty 抢占 CH340 属高频坑,触发条件是 85-brltty.rules 的 VID:PID 名单命中(1a86:7523 在名单内,FTDI 标准 0403:6001 不在);systemctl mask brltty brltty-udev 是最短解;名单随版本变化。
  • slcand 与 SocketCAN-native 驱动分别对应"串口线协议转 CAN"与"USB 直接进内核 CAN 栈";ip -details link show can0 是运行时状态查询入口。
  • GPIO / I2C / SPI / V4L2 / 输入设备各自一把尺子:gpiodetect / i2cdetect / spidev_test / v4l2-ctl --list-devices / evtest;板端设备树 overlay 打开对应总线再谈用户态访问。

参考文献与延伸阅读

  • systemd. udev 手册页. 文档
  • libgpiod. 文档. 文档
  • Linux Kernel. USB 驱动文档. 文档