第 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 打开对应总线再谈用户态访问。