3、Python与CAN通信:Python-can库安装、CAN消息发送与接收、CAN ID与数据帧解析、总线监控基础

好,咱们进入第三讲。

说实话,CAN通信是ECU测试里最基础、也最绕不开的一环。你想想看,不管你是刷写程序、读取故障码,还是监控传感器数据,最终都要通过CAN总线来交互。我当年刚入行时,总觉得CAN协议很神秘,后来用Python写了几套自动化脚本后,发现其实核心就那几件事:装库、发消息、收消息、解析数据。

今天我就带你把这几个环节彻底打通。

3.1 Python-can库安装与环境配置

Python-can是目前最主流的CAN通信库。我个人习惯用这个库,因为它跨平台支持好,而且对Vector、Kvaser、PCAN这些主流硬件都兼容。

安装很简单,一行命令搞定:

pip install python-can

嗯,这里要注意:如果你用的是Linux系统,建议加上sudo。我在项目中遇到过权限问题导致打不开设备,后来发现是用户组没加进去。

装完之后,建议验证一下:

import can
print(can.__version__)

如果能正常打印版本号,说明环境没问题了。

小提示: 如果你用的是Vector VN1610这类设备,记得先装好Vector的驱动。我曾经因为驱动版本不对,折腾了整整一个下午。

3.2 CAN消息发送与接收

说白了,CAN通信就是发报文和收报文。我们先从发送开始。

3.2.1 发送一条CAN消息

先看一个最简单的例子:

import can

bus = can.interface.Bus(bustype='vector', channel=0, bitrate=500000)
msg = can.Message(arbitration_id=0x123, data=[0x11, 0x22, 0x33, 0x44], is_extended_id=False)
bus.send(msg)
print("消息已发送")

这里有几个关键点:

  • bustype:指定硬件类型,'vector'、'kvaser'、'socketcan'等
  • channel:通道号,一般从0开始
  • bitrate:波特率,ECU常用的是500k或250k
  • arbitration_id:CAN ID,标准帧是11位,扩展帧是29位
  • data:数据域,最多8个字节

我刚开始做测试时,经常忘记设置is_extended_id。有一次发扩展帧没设这个参数,ECU死活不响应,查了半天才发现ID被截断了。

3.2.2 接收CAN消息

接收消息有两种方式:轮询和回调。我个人更推荐回调方式,效率更高。

方式一:轮询接收

import can

bus = can.interface.Bus(bustype='vector', channel=0, bitrate=500000)

while True:
    msg = bus.recv(timeout=1)
    if msg:
        print(f"ID: {hex(msg.arbitration_id)}, Data: {msg.data.hex()}")

方式二:回调接收

import can

def msg_handler(msg):
    print(f"收到消息: ID={hex(msg.arbitration_id)}, 数据={msg.data.hex()}")

bus = can.interface.Bus(bustype='vector', channel=0, bitrate=500000)
notifier = can.Notifier(bus, [msg_handler])

# 程序保持运行
input("按回车退出...")
重点: 回调方式不会阻塞主线程,适合做长时间的总线监控。我在做耐久测试时,经常用回调方式跑几天几夜。

3.3 CAN ID与数据帧解析

收到原始报文后,下一步就是解析。你想想看,ECU发过来的数据是十六进制字节流,你得知道每个字节代表什么含义。

3.3.1 CAN ID的解析

CAN ID不仅仅是标识符,很多时候它包含了优先级、源地址、目标地址等信息。比如在J1939协议中,ID的29位被拆分成多个字段:

位范围 字段 说明
28-26 优先级 3位,值越小优先级越高
25-8 PGN 参数组编号
7-0 源地址 发送节点的地址

解析代码示例:

def parse_j1939_id(can_id):
    priority = (can_id >> 26) & 0x07
    pgn = (can_id >> 8) & 0x3FFFF
    source_addr = can_id & 0xFF
    return priority, pgn, source_addr

# 使用示例
can_id = 0x18F00400
prio, pgn, addr = parse_j1939_id(can_id)
print(f"优先级: {prio}, PGN: {hex(pgn)}, 源地址: {hex(addr)}")

3.3.2 数据帧的解析

数据解析的核心是「信号提取」。一个字节可能包含多个信号,或者一个信号跨多个字节。

举个例子,车速信号通常占2个字节,精度0.01 km/h:

def parse_speed(data_bytes):
    # 假设车速在字节0和字节1,小端格式
    raw_value = data_bytes[0] | (data_bytes[1] << 8)
    speed = raw_value * 0.01
    return speed

# 假设收到数据 [0x10, 0x27, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
data = [0x10, 0x27, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
speed = parse_speed(data)
print(f"当前车速: {speed} km/h")  # 输出 100.00 km/h
避坑指南: 我曾经在解析一个温度信号时,忘了考虑符号位。结果零下温度显示成了65535度,差点把测试报告搞砸。记住:有符号数要用struct模块的'i'格式解析。

3.4 总线监控基础

总线监控,说白了就是「偷听」总线上所有的通信。这在故障排查和逆向分析时特别有用。

3.4.1 实现一个简单的总线监控器

import can
import time

def bus_monitor():
    bus = can.interface.Bus(bustype='vector', channel=0, bitrate=500000)
    
    print("开始监控CAN总线...")
    print("按Ctrl+C停止")
    
    try:
        while True:
            msg = bus.recv(timeout=0.1)
            if msg:
                timestamp = time.strftime("%H:%M:%S.%f")[:-3]
                print(f"[{timestamp}] ID: {hex(msg.arbitration_id):>8}  "
                      f"DL: {msg.dlc}  Data: {msg.data.hex():>16}")
    except KeyboardInterrupt:
        print("\n监控已停止")

if __name__ == "__main__":
    bus_monitor()

3.4.2 过滤特定ID的消息

实际测试中,总线上可能有几百条消息。你只关心某几个ID,这时候就要用过滤:

bus.set_filters([{"can_id": 0x123, "can_mask": 0x7FF, "extended": False}])

这个过滤器只接收ID为0x123的标准帧。can_mask是掩码,0x7FF表示精确匹配。

实用技巧: 我经常用掩码来做范围过滤。比如要监控0x100到0x1FF的所有ID,可以把掩码设为0x700,这样只匹配高5位。

3.4.3 记录总线日志

监控时最好把数据记录下来,方便事后分析。Python-can支持多种日志格式:

import can
from can.io import Logger

bus = can.interface.Bus(bustype='vector', channel=0, bitrate=500000)
logger = Logger("can_log.asc")  # ASC格式,Vector的CANalyzer可以直接打开

while True:
    msg = bus.recv(timeout=1)
    if msg:
        logger(msg)
        print(f"已记录: {hex(msg.arbitration_id)}")

支持的格式包括:

  • .asc:Vector ASC格式,兼容性好
  • .blf:Vector BLF格式,二进制,体积小
  • .csv:CSV格式,方便用Excel打开
  • .log:CANoe的日志格式

嗯,到这里,CAN通信的基础知识就讲完了。你可能会觉得内容有点多,但这些都是我实际项目中每天都要用的东西。下一讲我们会把这些知识串起来,做一个完整的ECU自动化测试脚本。

记住:多动手写代码,比看十遍理论都有用。我在带新人时,总是让他们先写一个总线监控器,跑一晚上看看总线上到底在传什么。你试试看,会有意想不到的收获。