第 9 章 工具链与抓包排障¶
工具链把前八章的机制落到"能观测、能定位故障"的最后一公里:SocketCAN 内核栈与 can-utils 命令行工具集、python-can 用户态库与 backend 差异、DBC 信号级解析、逻辑分析仪与示波器看位、总线负载率与丢包的算式、常见故障模式的定位流程。
Modbus 侧工具(pymodbus / diagslave / Wireshark)在第 6–7 章已内嵌示例,这里以 CAN 家族为主。分工上,这一层提供驱动开发之下的观测与调试手段,ros2_control 在其上做控制抽象,两者接口的衔接点见 ROS2-note 第 6 章。
按层排查,先物理后应用:
flowchart TD
phy["物理层:万用表 / 示波器<br/>终端电阻 / 电平"] --> link["链路层:candump、错误计数"]
link --> proto["协议层:candump -L、cantools 解码"]
proto --> app["应用层:DS 402 状态 / Modbus 寄存器"]
1. SocketCAN 栈与内核侧¶
SocketCAN 是 Linux 内核对 CAN 的原生支持(2.6.25 引入),把 CAN 接口暴露成 AF_CAN 套接字,配套网络子系统工具(ip link、ss)直接可用。整套栈分三层:
用户态:应用(自写 / ros2_control)→ python-can / 厂商 SDK → libc
内核态:SocketCAN core → CAN 设备驱动(peak_usb / kvaser_usb / mcp251x / vcan 等)
硬件: USB-CAN 适配器 / PCIe CAN 卡 / SoC 内置 CAN 控制器(如 i.MX、TI AM335x、STM32)
/sys/class/net/can0/ 下的属性与 ip -details -statistics link show can0 反映运行期状态与错误计数(berr-cnt、tx/rx error)。
1.1 vcan 虚拟总线与调试¶
vcan(Virtual CAN)是内核自带的虚拟 CAN 接口,无需任何硬件即可收发。用途:单元测试、CI、无硬件时的协议栈联调。
## 运行环境:Ubuntu 22.04(Linux 5.15),can-utils 20210224
sudo modprobe vcan
sudo ip link add dev vcan0 type vcan
sudo ip link set up vcan0
## 终端 A
candump vcan0
## 终端 B
cansend vcan0 123##4deadbeef # 与第 3 章「最小 SocketCAN 收发与错误观测」同款
vcan 默认 MTU 为 CAN_MTU(16 字节),只能承载经典帧;sudo ip link set vcan0 mtu 72 把 MTU 提到 CANFD_MTU,FD 帧即可完整跨进程收发。建议:CI 里跑 python-can 用例时用 vcan,避免绑定特定 USB-CAN 硬件。
1.2 slcand 与串口型 CAN¶
真实硬件接入有两条常见路径:
- 原生驱动:
peak_usb、kvaser_usb、gs_usb(CANable 等)、mcp251x(SPI-to-CAN 芯片)等内核模块直接暴露canX。插上 USB-CAN 适配器后dmesg | tail会看到新接口注册与重命名为can0的日志。 - 串口转换层:UART 上的 CAN 芯片(SLCAN 协议固件)通过
slcand(Serial Line CAN daemon)把串口字符流转成 CAN 帧并挂成canX;也可用slcan_attach手动挂载 tty line discipline。
slcand 最小接入:
## 运行环境:Ubuntu 22.04,can-utils 20210224;假设硬件通过 /dev/ttyUSB0 出 UART CAN 帧
sudo slcand -o -c -s6 /dev/ttyUSB0 can0 # -s6 = 500 kbps,见 slcan 波特码表
sudo ip link set can0 up
slcand 的 -s 参数用波特码(0=10k、1=25k、2=50k、3=100k、4=125k、5=250k、6=500k、7=800k、8=1M);-s6 = 500 kbps。
1.3 ip link 配置位时序¶
SocketCAN 通过 netlink 下发位时序;ip 命令行同时支持"直接给波特率"与"给四段参数"两种模式:
## 简单模式:只给 bitrate,让内核驱动自己算位时序
sudo ip link set can0 type can bitrate 500000
## FD 双速率:仲裁 500k、数据 2M
sudo ip link set can0 type can bitrate 500000 dbitrate 2000000 fd on
## 精细模式:给完整位时序段(与第 3 章「位时序与采样点」的 500 kbps 算例一致)
sudo ip link set can0 type can bitrate 500000 \
sample-point 0.800 sjw 2 \
tq 100 prop-seg 10 phase-seg1 5 phase-seg2 4
## 打开接口,观察实际生效参数
sudo ip link set can0 up
ip -details link show can0
ip -details 输出包含 bitrate 500000 sample-point 0.800、tq 100、prop-seg 10 phase-seg1 5 phase-seg2 4 sjw 2——与手算对照(1 + 10 + 5 + 4 = 20 tq,20 × 100 ns = 2 μs,即 500 kbps)。接口 down 才能改位时序:ip link set can0 down 后再 type can 配置。
2. can-utils 工具集¶
can-utils(linux-can 项目)是 SocketCAN 的配套工具集,全部走 AF_CAN 套接字。日常最常用六个:
| 命令 | 用途 | 关键选项 |
|---|---|---|
candump |
监听并按行打印帧 | -L(log 格式,可回放)、-t tz(时间戳)、-x(扩展帧格式)、can0,123:7FF(ID 过滤) |
cansend |
单帧发送 | can0 123#DEADBEEF(经典)、can0 123##1DEADBEEF(FD,## 后第一位是 BRS) |
cangen |
生成随机帧压总线 | -g 最小间隔、-i 随机 FD 使能;负载测试常用 |
canplayer |
回放 candump -L 日志 |
-I log、vcan0=can0(接口重映射)、-t(忽略时间戳立即发) |
cansniffer |
交互式监视字节变化 | 高亮"变化的字节",抓协议差异好用 |
cansequence |
发送递增字节序列、校验错乱 | 用于压力与位错诊断 |
组合示例:抓一条 CAN FD 帧日志并回放。
## 录制(终端 A)
candump -L can0 > fd_session.log
## 回放(终端 B,把日志里的 can0 帧发到 vcan0;-t 忽略原始时间戳立即发送)
canplayer -I fd_session.log vcan0=can0 -t
日志行示例:
(1698985000.123456) can0 123##1DEADBEEFCAFEBABE
括号内是 Unix 秒.微秒;##1 表示 FD + BRS;后面是数据(本例 10 字节)。第 8 章「on-the-fly 处理与逻辑寻址」提到的逻辑寻址,用 cansniffer 挂上总线即可直观看谁在什么周期改哪些字节。
3. python-can 与 backend¶
python-can(can.readthedocs.io)是 Python 生态最常用的 CAN 收发库,抽象出统一的 Bus / Message 接口,底下支持多种 backend。
3.1 Bus / Message 抽象¶
## 运行环境:Ubuntu 22.04(Python 3.10),python-can 4.3
import can
## Bus 打开一个接口;socketcan 的位速率由 ip link 预先配置,Bus 只负责套接字
## 前置命令:sudo ip link set can0 type can bitrate 500000 dbitrate 2000000 fd on
bus = can.Bus(interface="socketcan", channel="can0",
receive_own_messages=False, fd=True)
## 构造消息:Message 覆盖经典与 FD 属性
msg = can.Message(arbitration_id=0x123, is_extended_id=False,
is_fd=True, bitrate_switch=True,
data=bytes([0xDE, 0xAD, 0xBE, 0xEF]))
bus.send(msg)
## 同步接收,超时 1 秒
rx = bus.recv(timeout=1.0)
if rx is not None:
print(f"{rx.timestamp:.6f} {rx.arbitration_id:03X} "
f"dlc={len(rx.data)} data={rx.data.hex()}")
bus.shutdown()
Message 关键属性:arbitration_id、is_extended_id(29 位 ID)、is_remote_frame(远程帧)、is_error_frame(错误帧)、is_fd、bitrate_switch、error_state_indicator、dlc、data。
3.2 backend 差异与 FD 支持¶
常用 backend 一览:
| backend | 依赖 | FD 支持 | 平台 | 备注 |
|---|---|---|---|---|
socketcan |
Linux 内核原生 | 是 | Linux | 首选;无额外驱动 |
pcan |
PEAK 驱动 + libpcan | 是 | Linux / Windows | PCAN-USB 系列 |
kvaser |
Kvaser CANlib | 是 | Linux / Windows | Leaf / UsbCan 系列 |
ixxat |
IXXAT VCI | 是 | Windows 为主 | 支持 FD 的 SDK 版本 |
slcan |
无(走 pyserial) | 部分(取决于设备) | 跨平台 | CANable 等 UART CAN |
virtual |
同机自测(模拟总线) | 是 | 跨平台 | 与 vcan 类似,纯 Python |
candleLight |
candleLight 固件 | 是 | 跨平台 | CANable 2.0 等 |
cantact |
CANtact 固件 | 是 | 跨平台 | 开源硬件方案 |
跨 backend 一致性不保证:位时序参数命名、FD 数据段长度上限、错误帧上报格式各实现略有差异。工程上优先用 SocketCAN(Linux)或厂商 SDK 直连,再考虑 python-can 抽象。
3.3 Logger / Listener 与异步¶
python-can 提供广播接口(Notifier + Listener)替代 recv() 阻塞循环:
## 运行环境:同上
import can
class PrintListener(can.Listener):
# Listener 的回调方法是 on_message_received,不是 on_message
def on_message_received(self, msg: can.Message):
if not msg.is_remote_frame and not msg.is_error_frame:
print(f"rx: 0x{msg.arbitration_id:03X} dlc={msg.dlc}")
bus = can.Bus(interface="socketcan", channel="vcan0")
notifier = can.Notifier(bus, [PrintListener()])
## ... 运行若干秒 ...
notifier.stop()
bus.shutdown()
can.Logger 是内置 Listener,把消息按 .asc / .blf / .csv 格式落盘;can.LogReader 反向读取。can.AsyncBufferedReader 提供 asyncio 接口,用于异步栈(ros2_canopen 等项目使用)。
4. DBC 文件与信号级解析¶
CAN 帧本身只有 ID 与 8/64 字节数据;怎么把字节拆成"发动机转速""制动压力"这类有物理意义的信号交给描述文件。汽车与传统工业生态主要用 DBC(Vector DBC Format)文件,AUTOSAR 用 ARXML。机器人 CANopen 场景一般用 EDS(电子数据表)+ 对象字典替代 DBC。
4.1 DBC 结构¶
一个最小 DBC 片段:
BO_ 256 EngineData 8: ECU
SG_ RPM : 0|16@1+ (0.1,0) [0|6500] "rpm" Dashboard
SG_ Coolant : 16|8@1+ (1,-40) [-40|215] "degC" Dashboard
SG_ OilTemp : 24|8@1+ (1,-40) [-40|215] "degC" Dashboard
CM_ BO_ 256 "Engine status broadcast at 100 Hz.";
字段解读:
BO_ <ID> <Name> <DLC>: <TransmitterNode>:报文定义。ID 256 = 0x100、8 字节、发送者ECU。SG_ <Signal> : <StartBit>|<Length>@<Order><Sign> (<Factor>,<Offset>) [<Min>|<Max>] "<Unit>" <Receivers>:信号定义。@1+小端无符号;@0+大端 Motorola。物理值 = 原始值 × Factor + Offset。CM_:注释段,工具链展示用。
4.2 cantools 编解码¶
cantools(github.com/cantools/cantools)是 Python 生态事实标准:
## 运行环境:Ubuntu 22.04(Python 3.10),cantools 39.4.5
import cantools
db = cantools.database.load_file("vehicle.dbc")
msg_def = db.get_message_by_name("EngineData")
## 解码一帧原始字节(RPM=0x4E20,Coolant=0x82,OilTemp=0x77)
raw = bytes([0x20, 0x4E, 0x82, 0x77, 0x00, 0x00, 0x00, 0x00])
decoded = msg_def.decode(raw)
## 期望:{'RPM': 2000.0, 'Coolant': 90, 'OilTemp': 79} # 按 Factor/Offset 换算
## 编码回字节
encoded = msg_def.encode({'RPM': 2000.0, 'Coolant': 90, 'OilTemp': 79})
cantools 命令行:cantools decode vehicle.dbc < canlog 可把 candump 输出直接翻成信号名,比裸字节直观。
5. 逻辑分析仪与示波器¶
位级调试绕不开看电平波形与位边界。
5.1 位流采样与协议解码¶
- 逻辑分析仪(Saleae Logic 8 / 16、开源 DSView / PulseView + 兼容硬件):两通道接 CAN_H / CAN_L 或差分接收发器输出,采样率 ≥ 位速率 × 8(1 Mbps 需 ≥ 8 MSa/s、5 Mbps FD 需 ≥ 40 MSa/s),配合软件里的 CAN 协议解码器把位流转成帧。PulseView 内置 CAN / CAN FD 解码器,能识别 FDF / BRS / ESI 位。
- 示波器:看边沿与幅度——显性/隐性的差分电压、共模漂移、终端电阻缺失导致的振铃;用眼图模式(很多示波器有 CAN 眼图模板)判断信号完整性是否满足 ISO 11898-2 一致性测试。
5.2 显性 / 隐性电平判读¶
ISO 11898-2 定义的差分电平(在 60 Ω 等效负载下,终端 120 Ω × 2 并联):
| 状态 | CAN_H | CAN_L | 差分 \(V_{CAN\_H} - V_{CAN\_L}\) |
|---|---|---|---|
| 显性(Dominant,逻辑 0) | 约 3.5 V | 约 1.5 V | ≥ 1.5 V(门限以上) |
| 隐性(Recessive,逻辑 1) | 约 2.5 V | 约 2.5 V | ≤ 0.5 V(门限以下) |
常见示波器异常与判读:
- 差分幅度不到 1.5 V → 终端电阻偏大、线过长、驱动器老化。
- 显性→隐性边沿振铃过冲 > 4 V → 终端不匹配,反射叠加。
- CAN_H 与 CAN_L 共模在 2.5 V 上下对称漂移 → 正常;单边偏离 2.5 V 超过 ±0.5 V → 收发器或地参考异常。
- 隐性状态下差分不为 0 → 存在偏置电阻误接(CAN 上不该有 RS-485 的偏置)。
6. 总线负载率与丢包¶
总线负载率(bus load)是单位时间里"总线上有帧的时间占比",与位速率、帧长、发送频率相关。设计总线前手算一次,避免上量后翻车。
6.1 单帧位时间预算¶
经典 CAN 一帧最大位数(含位填充):
- SOF + 仲裁 + 控制 + 数据 + CRC + ACK + EOF + IFS(不含填充):标准帧 8 字节数据 ≈ 111 位;扩展帧 ≈ 130 位。
- 位填充最坏增加约 20%(每 5 位插 1 位),设计上限取 约 130–160 位。
以 500 kbps 为例:一条 8 字节标准帧典型 约 230–260 μs。
CAN FD 一帧(BRS 打开、数据段 5 Mbps):仲裁段 500 kbps、数据段 5 Mbps;64 字节帧位时间 ≈ 仲裁与帧尾(约 70 位 @ 500k = 140 μs)+ 数据与 CRC(约 550 位 @ 5M = 110 μs)≈ 250 μs,是经典 CAN 8 字节的约 1 倍时间装 8 倍数据。
6.2 综合负载算式与经验上限¶
\(N_i\) = 类型 \(i\) 的帧每秒发送次数、\(T_i\) = 该类型单帧时间(含位填充余量)。
例:一台六轴协作臂的关节 CAN FD 总线按 1 Mbps 仲裁 / 5 Mbps 数据运行,控制周期 1 ms;每轴每周期各一帧 TPDO 与 RPDO,16 字节载荷单帧约 100 μs;SYNC 一帧约 20 μs:
配置跑不动。把控制周期降到 500 Hz(2 ms)后为 61%;或每轴两个方向合并为一帧,1 kHz 下为 62%,均落在经验上限内。
经验上限:工程上建议总线负载 ≤ 50–70%,实时性要求高的段按 30–50% 设计——留出突发错误重发与诊断帧余量。> 80% 时仲裁冲突明显、高 ID 帧等待时间快速上升、错误惩罚可能触发 error-passive。
7. 典型故障模式与定位流程¶
| 现象 | 首查 | 常见定位手段 |
|---|---|---|
| 完全收不到帧 | 线是否接反(CAN_H ↔ CAN_L)、终端电阻是否缺失、接口是否 up | ip -details link show can0 看 state ERROR PASSIVE / BUS-OFF;示波器差分看电平 |
| 收到错乱 ID | Node-ID 冲突(同一 CANopen 域出现两个相同 Node-ID) | candump 观察同一 ID 上数据交替;NMT 逐个上线 |
| 周期不达标(> 预期) | 总线负载过高、优先级低帧饿死 | cangen 打压力 + candump -t tz 看间隔;算负载 |
偶发 ERROR 行 |
位时序不匹配、终端电阻不匹配、地环路 | 示波器看位边沿;ip -details -statistics link show can0 看 berr-cnt 增速 |
| 频繁进 bus-off | 驱动器"卡在显性"(发送器故障)、波特率不一致 | 隔离可疑节点、逐个上总线;观察 TEC / REC 增速 |
| Modbus 从站无响应 | 波特率 / 数据位 / 校验位不匹配、地址错、CRC 错 | 用串口助手直接看原始字节;打开 pymodbus 日志(logging.basicConfig(level=logging.DEBUG))看 ADU |
| EtherCAT 从站进 INIT / PREOP 卡住 | 主站周期不足以覆盖全线传输时延、DC 未收敛 | IgH / TwinCAT 主站日志、Wireshark 抓 0x88A4 帧、看 AL Status Code |
定位通用步骤,自下而上五层:物理层先用示波器 / 万用表确认差分电平、终端电阻与共模范围;再用 candump / tcpdump / EtherCAT 主站日志证明"帧在流";接着是时序与负载,用 cansniffer、逻辑分析仪与负载算式对齐设计预算;错误计数查 ip -details -statistics link 与 python-can 的 is_error_frame,TEC / REC 的增速区分故障出在发送侧还是接收侧;最后核对位时序、Node-ID / Slave-Addr、波特率与字序——两端不一致是绝大多数"看似随机"故障的根因。
8. 要点¶
- SocketCAN 是 Linux 原生方案:
ip link配置位时序、vcan 做无硬件调试、slcand桥接 UART 型 CAN;ip -details是运行期状态的第一观测点。 can-utils六个高频命令:candump 抓包、cansend 发单帧、cangen 压力、canplayer 回放、cansniffer 交互看字节变化、cansequence 校验位错——覆盖日常 95% 场景。python-can抽象 Bus / Message / Listener,backend 差异不保证:SocketCAN 首选;跨厂商 backend 的 FD 支持与位时序命名有差异,位速率仍由各平台驱动侧配置。- DBC 把字节拆成信号:
BO_定报文、SG_定信号;cantools编解码 +candump管道可直接看物理量;机器人 CANopen 场景改用 EDS + 对象字典。 - 负载率算式落到纸面:\(load = \sum N_i T_i / \text{period}\),经验上限 50–70%;超出时仲裁延迟非线性上升、error-passive 频繁触发。
- 定位从物理到协议:示波器看电平 → candump 看帧流 → cansniffer 看周期与负载 → 读错误计数器与状态机 → 核对两端配置。绝大多数"随机故障"最终落在配置一致性上。