简介:面向自动驾驶感知与决策控制研究者、算法工程师及机器学习开发者,这是一套以CAN总线车载数据为核心的端到端数据集。压缩包共2000个json文件,整体约744.8MB,文件按场景切片组织,内容涵盖车速、引擎转速、油门刹车状态等车辆实时状态,以及摄像头、激光雷达、雷达等传感器环境感知记录,既可用于模型训练与端到端控制策略优化,也适合在仿真环境中回放验证。目前已有126人学习下载。数据采用标准json格式存储,便于直接接入深度学习管线进行批量解析;按场景拆分的目录结构,使研究者能够选取特定路段、特定传感器组合做针对性分析。借助这类真实总线数据,可复现车辆动态响应、观察多传感器融合特征,也可为行为预测、规划控制等模块提供训练样本,从而降低实车路测成本,为更安全高效的自动驾驶系统提供数据支撑。
1. can_bus.zip 是什么:CAN 总线调试资源包里到底装了什么
can_bus.zip 这个压缩包,名字取得很直白:它给出的不是某个单一程序,而是一整套 CAN 总线调试资源——驱动说明、抓包命令、报文解析脚本、DBC 样例,以及最关键的排错套路。我第一次拿到类似资源时,手边是一块 USB-CAN 适配器,目标是读出一个模拟项目X里电池管理模拟器的报文,结果 candump 一开,屏幕上刷的全是错误帧,折腾一下午才发现只是终端电阻没接。这个包要解决的正是这一类问题:让新人别在硬件链路上反复试错,让熟手能快速定位「连不上、收不到、全是错帧」三大现场。适合人群很明确:刚入手适配器的嵌入式工程师,以及做充电桩、工业设备、车辆诊断但没系统碰过 CAN 的开发者。
2. CAN 物理层与硬件链路:收发器、终端电阻和适配器选型
2.1 差分信号与 120Ω 终端电阻为什么是标配
CAN 物理层用的是差分信号,CAN_H 和 CAN_L 两条线配对传输。显性位时 CAN_H 约 3.5V、CAN_L 约 1.5V,隐性位时两条线都回到 2.5V。接收端不关心对地电压,只看两条线之间的压差,所以抗干扰能力比单端串口强得多。这个设计决定了 CAN 总线必须用双绞线,而且两端要各接一个 120Ω 终端电阻,用来吸收信号到达线缆末端时的反射。
很多人一开始不把终端电阻当回事,觉得「两条线能通就行」。实际表现是:短距离低速(比如 250K、两米线)确实能跑,但一旦距离拉长或挂的节点变多,波形反射会让采样点落在不稳定的电平上,偶发错帧、总线错误就全来了。用万用表量 CAN_H 与 CAN_L 之间,正常值应该在 60Ω 左右——这说明总线两端各有一个 120Ω 电阻并联。量到约 120Ω 说明只有一端有终端电阻,量到接近 0Ω 则是短路,这些都是最基础的链路判断。
终端电阻的位置也有讲究。我见过有人把电阻焊在适配器里面,想着省事,结果节点一多,电阻位置不对,反射照样发生。正确做法是放在物理线缆的最远端节点上,分段式菊花链结构里尤其要注意这一点。
| 检查对象 | 工具 | 正常值 | 异常解读 |
|---|---|---|---|
| CAN_H 与 CAN_L 间电阻 | 万用表 | 约 60Ω | 120Ω 表示少一端终端,0Ω 表示短路 |
| CAN_H 对地电压 | 万用表 | 约 2.5V | 明显偏低/偏高说明总线被拉死 |
| CAN_L 对地电压 | 万用表 | 约 2.5V | 同上 |
| 线缆类型 | 目视 | 双绞线 | 平行线在高速率下不可靠 |
2.2 USB-CAN 适配器怎么选:隔离、通道数和驱动兼容性
拿到 can_bus.zip 之后,我一般会先翻里面的驱动说明,确认包里配套的适配器型号是什么。常见适配器分三类:USB-CAN、PCIe-CAN 和串口转 CAN。日常调试我首选 USB-CAN,免拆机、笔记本即插即用,唯一要注意的是驱动是否支持当前系统内核。某常见 USB-CAN 适配器在 Linux 下有两种工作模式:一种是插入后识别为 ch341 之类的串口设备,另一种是直接识别为 SocketCAN 网络接口,前者需要额外写协议转换,后者在 Linux 下最顺手。
工业场景下我倾向于选带隔离的适配器。CAN 总线上某个节点短路、或者电机驱动器反电动势串进来,非隔离适配器可能直接烧掉电脑 USB 口。隔离型适配器内部有磁耦或光耦,成本高几十块,但能保住笔记本主板,这笔钱不能省。通道数方面,单通道够用,做网关或中继调试才需要双通道。
选型时还要看适配器是否支持 CAN FD。如果车上或产线里已经有节点在用 CAN FD,普通 CAN 适配器抓不到完整报文,显示出来全是截断数据和错误帧。can_bus.zip 里如果提到 FD 场景,大概率是为了提醒你:先确认总线模式,再下单买适配器。
2.3 动手前的链路自检:半小时排除七成硬件故障
不插电、不开软件,先把物理链路过一遍。我的固定流程是:量终端电阻、量线序、量静态电压。终端电阻前面说过了,线序问题则隐蔽得多。CAN_H 和 CAN_L 接反时,总线不会完全不通,而是表现为适配器能初始化、但收不到任何正常帧,错误帧偶尔冒一条。原因是差分对极性反转后,显性电平变成负压差,接收端仍然能识别跳变,但协议层对电平阈值有要求,临界状态下收发器会偶尔判错。
静态电压检查有一招很好用:适配器不接入,直接用万用表量总线上 CAN_H 和 CAN_L 的对地电压。正常空闲状态两条线都接近 2.5V,如果量到一边 2V 一边 3V 且相差很大,说明某个节点的收发器已经故障,或者线缆被强拉到了某个固定电平。这种情况光靠抓包是看不出来的,必须回到万用表。
链路自检的结论可以给得很直接:三分之二的「连不上」问题出在终端电阻和线序,不是软件配置。can_bus.zip 里通常也会放一张检查清单,本质上就是把这套物理层排查写成了可勾选的表格,省得每次都在命令行里瞎试。
3. SocketCAN 从零跑通:虚拟接口与真实设备的最小收发链路
3.1 在 Linux 上用 ip link 和 vcan0 先跑通流程
Linux 下的 CAN 开发绕不开 SocketCAN,它是内核自带的网络子系统,把 CAN 总线当成一个网络接口来用。没有硬件也能先验证代码逻辑,用虚拟接口 vcan0 模拟真实总线,这是我最常用的起步方式。
# 加载 vcan 内核模块并创建虚拟 CAN 接口 sudo modprobe vcan sudo ip link add dev vcan0 type vcan sudo ip link set vcan0 up这三条命令分别做了三件事:加载内核模块、创建名为 vcan0 的虚拟 CAN 设备、把接口状态置为 up。这里不需要指定波特率,因为 vcan0 不涉及真实物理层。它的价值在于:后续所有抓包和分析命令都能在这个虚拟接口上先跑一遍,验证脚本逻辑没问题,再接真实硬件。
真实适配器在 SocketCAN 下通常叫 can0,配置命令略有不同:
# 配置 can0,波特率 500 kbit/s,总线关闭后 100ms 自动重启 sudo ip link set can0 up type can bitrate 500000 restart-ms 100bitrate 500000 表示 500 kbit/s,这是汽车动力总成和工业设备最常见的速率。restart-ms 100 是总线进入 bus-off 状态后的自动恢复时间,单位为毫秒。我一般会加上这个参数,否则一旦发生严重错误,接口会一直停留在 bus-off,必须手动 down 再 up 才能恢复。
配置完成后先开一个终端跑抓包:
candump can0再开另一个终端发一帧测试报文:
cansend can0 123#1122334455667788此时抓包终端应该看到can0 123 [8] 11 22 33 44 55 66 77 88。ID 是十六进制的 0x123,[8]是数据长度,后面是 8 个数据字节。如果这一步通了,整个 SocketCAN 通道就没问题,接下来要做的只是业务逻辑。
3.2 用 python-can 封装收发:复用度和可读性都更好
命令行工具适合快速验证,但要写自动化测试或解析逻辑,直接调 SocketCAN 的 C 接口太重。我习惯用某辆常用的 CAN 开发库来封装,它内部调用 SocketCAN,而且代码可以无缝切换到其他后端。
import can # 创建总线对象,channel 对应 SocketCAN 接口名 bus = can.Bus(interface='socketcan', channel='can0', bitrate=500000) # 构造一帧标准帧,ID 为 0x123,8 字节数据 msg = can.Message( arbitration_id=0x123, data=[0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88], is_extended_id=False ) # 发送并等待 1 秒接收回执 bus.send(msg) rx = bus.recv(timeout=1.0) if rx is not None: print(hex(rx.arbitration_id), rx.data.hex())这里有几个参数值得说清楚。arbitration_id是帧 ID,is_extended_id=False表示用 11 位标准帧;如果对端节点用 29 位扩展帧,这个参数必须改成 True,否则对方会把这帧当作错误帧丢弃。timeout单位是秒,收不到返回 None,用国内网口术语来说就是「读超时」。这个例子里读到的rx.data.hex()会把字节数组转成十六进制字符串,方便直接和 candump 输出对照。
如果想让程序自动处理持续到达的报文,不要用轮询,用 notifier 回调更干净:
import can def on_message(msg): print(f"{hex(msg.arbitration_id)}: {msg.data.hex()}") bus = can.Bus(interface='socketcan', channel='can0', bitrate=500000) notifier = can.Notifier(bus, [on_message])notifier 启动后会另起线程持续读总线,收到一帧就回调一次。这个模式适合做后台监听服务,配合 DBC 解析脚本就可以实时把原始报文转成物理量。
4. 抓包与分析:candump 日志格式和 DBC 物理值换算
4.1 candump 参数详解:落盘、过滤和时间戳格式
抓包是整个 CAN 调试中最有价值的一步。candump 看起来简单,但参数选错了,后续分析就会很痛苦。我常用的组合是:
candump -l can0-l把日志自动落盘,文件名默认带日期时间戳,避免长时间抓包刷爆终端。需要控制日志大小时可以加-b指定单个文件大小,超过阈值自动换新文件:
candump -b 10 -l can0-b 10表示超过 10MB 就滚动写新文件。做一整天路试数据采集时,这个参数比事后手动分割靠谱得多。
终端实时看时,我习惯加-e显示错误帧,加-t d用带日期的绝对时间戳,方便和车辆日志对齐:
candump -e -t d can0candump 默认输出格式是时间戳 接口名 ID#数据。注意这里的 ID 不会自动补全到 8 位,标准帧在输出里是 3 位十六进制,扩展帧是 8 位。比如can0 18FF50E5#...表示一个扩展帧,ID 是 0x18FF50E5;而can0 123#...是标准帧 0x123。解析日志时若直接按字符串长度判断是否是扩展帧,会漏掉前导零,这是后面脚本最容易踩的坑。
按 ID 过滤可以用-f:
candump can0 -f 0x123:0x1FFFFFFF这个命令只抓 ID 与掩码匹配的帧,0x123是 ID,0x1FFFFFFF是掩码。掩码位为 1 的位置必须与 ID 完全一致,生产调试时先过滤到目标节点,日志量能降两个数量级。
4.2 写一个日志解析脚本:从 candump 文本到信号物理值
抓回来的日志是文本,真正要分析的是里面的物理量,比如电池电压、电机转速、温度。这些映射关系写在 DBC 文件里。can_bus.zip 里大概率包含 DBC 样例和解析脚本模板,我会先看脚本支持的是 Intel 字节序还是 Motorola 字节序,这是最容易翻车的地方。
先从最基本的日志解析开始:
import re from pathlib import Path def parse_candump_line(line): # 匹配格式: (1600000000.123456) can0 123#1122334455667788 m = re.match(r'\(\d+\.\d+\)\s+can\d+\s+([0-9A-Fa-f]+)#([0-9A-Fa-f]*)', line) if not m: return None can_id = int(m.group(1), 16) data = bytes.fromhex(m.group(2)) return can_id, len(data), data for line in Path("candump.log").read_text().splitlines(): frame = parse_candump_line(line) if frame: can_id, dlc, data = frame print(hex(can_id), dlc, data.hex())这段脚本先把每一行文本拆成帧 ID 和数据字节。bytes.fromhex会把连续十六进制字符串转成字节数组,所以日志里 ID 前导零丢失的问题在这里不成立——int("123", 16)和int("18FF50E5", 16)都能正确解析,只要不依赖字符串长度判断帧类型就行。
拿到原始数据字节后,按 DBC 信号定义做物理值换算。DBC 文件里对每个信号定义了起始位、长度、字节序、缩放因子和偏移量,我一般把它们抽成一张配置表:
| DBC 字段 | 含义 | Python 对应参数 |
|---|---|---|
| start_bit | 信号起始位 | int |
| length | 信号长度(bit) | int |
| byte_order | Intel/Motorola | big_endian 布尔值 |
| scale | 缩放因子 | float |
| offset | 偏移量 | float |
| signed | 是否有符号 | bool |
对应的换算核心代码如下:
def signal_raw(data: bytes, start_bit: int, length: int, big_endian: bool) -> int: # 先把字节展开成位串,注意 CAN 字节序是小端位序 bit_str = ''.join(f'{b:08b}' for b in data) if big_endian: bit_str = ''.join(reversed(bit_str)) raw = int(bit_str[start_bit:start_bit + length], 2) return raw def to_physical(raw: int, scale: float, offset: float, signed: bool = False) -> float: if signed and raw >= 1 << (length - 1): raw -= 1 << length return raw * scale + offsetbig_endian为 False 时按 Intel 格式处理,即 LSB 在低字节,这是绝大多数国产 BMS 报文采用的格式;Motorola 格式会先把整帧字节翻转再取位,代码里用reversed处理了这一点。要特别注意 start_bit 的数值语义:Intel 格式下 start_bit 是信号最低位在整帧位串中的位置,Motorola 格式下指向的是最高位,不区分这两种语义,解析结果会错得毫无规律。
这套逻辑写好后,把 DBC 里每个信号导成一行配置,搭一个字典循环跑完整个日志,就能输出 CSV 曲线用于后续分析。
5. CAN 总线避坑指南:5 个高频翻车现场与排查手段
5.1 适配器插上后收不到任何帧,连错误帧都没有
现象:candump 启动后终端完全静默,等几分钟一条消息都没有。这时候先别怀疑软件,多半是物理链路没通。我用万用表量 CAN_H 与 CAN_L 之间电阻,只有 120Ω——说明总线上只有适配器一端有终端电阻,对端设备根本没接进来。
原因:最常见的是线没接上或接错引脚。适配器端子是 DB9 接口,CAN_H 在 2 脚,CAN_L 在 7 脚,不少人把 2 和 3 搞混。还有一些设备本身不带终端电阻,需要外接。
解决:量电阻,60Ω 才是正常的;如果只剩 120Ω,优先检查线缆是否完整、线序是否正确,再用万用表对地量两条线的静态电压确认总线有没有被拉死。
5.2 抓包里全是错误帧,总线直接 bus-off
现象:candump 带-e时刷出一堆(error)帧,适配器状态变成 error passive,严重时 ip link 显示 bus-off,数据完全中断。
原因:CAN 总线上各节点波特率不一致时会报错。某公司产线调试时出现过适配器配置 500K,控制板实际跑 250K,两边都能初始化,但一收发就瞬间打挂总线。另外终端电阻缺失、CAN_H 和 CAN_L 接反也会产生类似表现,但错误帧频率通常没有波特率不一致那么密。
解决:首先确认链路两端设备标称波特率一致,不要依赖适配器「自动识别」。把两端的 bitrate 显式配置成同一个值:ip link set can0 up type can bitrate 500000。再量终端电阻,排除物理层问题。如果还报错,把 CAN_H 与 CAN_L 对调试一下,极性接反在低速时会表现为偶发错误帧而不是完全不通。
5.3 偶发丢帧,高负载时批量超时
现象:低负载时收发正常,一旦总线负载超过 60%,或者两个节点同时上电,发送方出现大量send timeout,对端少收到不少帧。
原因:CAN 是半双工总线,高优先级帧会一直占用总线,低优先级帧只能等待。发送缓冲区默认较小,队列满了之后 send 操作直接超时。这与txqueuelen配置有关,也和帧 ID 仲裁优先级有关。
解决:先看统计信息里有没有丢包:
ip -s -details link show can0重点看TX dropped和bus error两个计数,TX dropped 不为 0 说明缓冲区溢出。可以调大txqueuelen:
sudo ip link set can0 txqueuelen 1000更根本的办法是降低发送频率、合并多个小信号到一帧 DLC=8 的报文里,或者把低优先级帧的仲裁 ID 调小,让它在仲裁时胜出。注意 CAN 协议里 ID 数值越小优先级越高,别把优先级配反了。
5.4 解析日志时 ID 总是对不上,查了半天发现是被前导零坑了
现象:日志解析脚本算出来的 ID 是 0x18FF50E5,但 DBC 里定义的是 0x18FF50E5,怎么比对都不匹配。后来发现解析脚本把18FF50E5按字符串转 int 时用了int(str, 16),结果正确,但在转回 hex 时打印出来变成了18ff50e5。
原因:candump 输出扩展帧不补前导零,而且默认字母小写。解析时若按固定 8 位补齐字符串再转 int,没有补零就会解析出错误 ID。
解决:解析时直接用int(can_id_str, 16),不要先补零。判断是否是扩展帧也不要用字符串长度,而是先看 DBC 里定义的 ID 范围:标准帧 ID 范围 0x000-0x7FF,扩展帧是 0x00000000-0x1FFFFFFF,超过 0x7FF 就按扩展帧处理。
5.5 波特率「自动识别」的玄学,换台电脑结果就变了
现象:同一个总线上,用某适配器的上位机工具能正常抓包,换一个开源工具就不通,或者同一套配置在不同电脑上一边正常一边疯狂错帧。
原因:一些上位机宣称「自动识别波特率」,实际是枚举常见速率逐帧盲扫,这个过程中会向总线注入大量错误帧。如果目标节点对错误容忍度低,可能直接进入 bus-off,结果就是越「自动识别」越乱。
解决:不要用自动识别,手动固定波特率。500K 和 250K 是绝对主流,选之前询问设备资料里写的工作速率。另外采样点设置也有影响,默认 75% 的采样点大概率能用,但长距离低速总线可以尝试 87.5%,遇到偶发错帧时往这个方向调一档。
6. 进阶验证技巧:用双向并发压测和错误计数器判断链路质量
跑通收发只是第一步,真正要交付前,我会做一次十分钟的双向并发压测,专门验证链路在满负载下的稳定性。原理很简单:CAN 是半双工,单向收发永远暴露不了仲裁冲突和反射问题,只有两个节点同时高频收发,波形畸变和优先级问题才会现形。
先用一台机器开两个虚拟接口模拟双节点:
sudo modprobe vcan sudo ip link add dev vcan0 type vcan sudo ip link add dev vcan1 type vcan sudo ip link set vcan0 up sudo ip link set vcan1 up然后写一个对发脚本,每个接口以 5ms 周期向对方发送 8 字节数据,连续跑 600 秒。结束后检查ip -details -statistics link show can0里的错误计数。判定标准很朴素:bus error计数保持为 0,RX overflow不增长,TX dropped不增长,链路才算通过。任何一项有数值,回到第 5 章逐项排查。
如果只有一块真实适配器,可以接一个回环头,配合ip link set can0 up type can bitrate 500000 loopback on打开控制器内部回环,验证发送路径是否干净。但这测不到线缆质量,交付前仍然要回到双节点互发。
我早期习惯只验单向,结果在某次产线联调时被一次批量丢帧狠狠教训了一回——两台上位机同时订阅同一个节点,低优先级帧大量超时,现场换了几根线都没用,最后发现是终端电阻位置不对导致高速下波形反射。现在每次验收都跑十分钟双向压测再下结论,省下的返工时间远超这几分钟测试成本。这套方法希望帮到你。
本文还有配套的精品资源,点击获取