目录
在纷繁复杂的车载网络协议中(如用于车身控制的LIN、用于多媒体传输的MOST、用于高速自动驾驶的以太网),CAN总线始终扮演着最基础、最核心的角色。在车载通信测试中,CAN通信测试尤为重要。
CAN报文通常包含标识符(ID)、数据长度(DLC)、数据字段(Data)、收发方向(RX/TX),报文类型(CAN经典/CANFD)等核心属性。本文提供了两种CAN报文类的实现方法,用于封装CAN报文的相关操作。
1. 第三方库:can
1.1 can库安装
pip install can
1.2 Message类
安装完can库之后,可以在can/message.py中找到Message类的定义,部分代码如下:
# from can/message.py
class Message: # pylint: disable=too-many-instance-attributes; OK for a dataclass
"""
The :class:`~can.Message` object is used to represent CAN messages for
sending, receiving and other purposes like converting between different
logging formats.
Messages can use extended identifiers, be remote or error frames, contain
data and may be associated to a channel.
Messages are always compared by identity and never by value, because that
may introduce unexpected behaviour. See also :meth:`~can.Message.equals`.
:func:`~copy.copy`/:func:`~copy.deepcopy` is supported as well.
Messages do not support "dynamic" attributes, meaning any others than the
documented ones, since it uses :obj:`~object.__slots__`.
"""
__slots__ = (
"timestamp",
"arbitration_id",
"is_extended_id",
"is_remote_frame",
"is_error_frame",
"channel",
"dlc",
"data",
"is_fd",
"is_rx",
"bitrate_switch",
"error_state_indicator",
"__weakref__", # support weak references to messages
)
def __init__( # pylint: disable=too-many-locals, too-many-arguments
self,
timestamp: float = 0.0,
arbitration_id: int = 0,
is_extended_id: bool = True,
is_remote_frame: bool = False,
is_error_frame: bool = False,
channel: Optional[typechecking.Channel] = None,
dlc: Optional[int] = None,
data: Optional[typechecking.CanData] = None,
is_fd: bool = False,
is_rx: bool = True,
bitrate_switch: bool = False,
error_state_indicator: bool = False,
check: bool = False,
):
"""
To create a message object, simply provide any of the below attributes
together with additional parameters as keyword arguments to the constructor.
:param check: By default, the constructor of this class does not strictly check the input.
Thus, the caller must prevent the creation of invalid messages or
set this parameter to `True`, to raise an Error on invalid inputs.
Possible problems include the `dlc` field not matching the length of `data`
or creating a message with both `is_remote_frame` and `is_error_frame` set
to `True`.
:raises ValueError:
If and only if `check` is set to `True` and one or more arguments were invalid
"""
self.timestamp = timestamp
self.arbitration_id = arbitration_id
self.is_extended_id = is_extended_id
self.is_remote_frame = is_remote_frame
self.is_error_frame = is_error_frame
self.channel = channel
self.is_fd = is_fd
self.is_rx = is_rx
self.bitrate_switch = bitrate_switch
self.error_state_indicator = error_state_indicator
if data is None or is_remote_frame:
self.data = bytearray()
elif isinstance(data, bytearray):
self.data = data
else:
try:
self.data = bytearray(data)
except TypeError as error:
err = f"Couldn't create message from {data} ({type(data)})"
raise TypeError(err) from error
if dlc is None:
self.dlc = len(self.data)
else:
self.dlc = dlc
if check:
self._check()
类的成员属性具体含义可参考下个章节——《自定义CANMessage类》
2. 自定义CANMessage类
参考can库中的Message类,对其进行了简化设计,自定义了CANMessage类。
代码如下:
import time
from typing import Union, Optional
import dataclasses
__all__ = ["CANMessage"]
class ProtocolError(ValueError):
"""CAN 协议规范违反异常"""
def __init__(self, message="", *, code: int = 0xEF):
super().__init__(f"[CAN_ERROR:{code:#x}] {message}")
self.error_code = code
@dataclasses.dataclass(frozen=False, slots=True)
class CANMessage:
"""符合 ISO 11898 标准的 CAN Message
实现 CAN 2.0B 和 CAN FD 规范的核心数据结构,支持:
- 自动协议合规性检查
- 数据格式智能转换
- 动态字段校正
- 协议参数组(PGN)解析
Attributes:
arbitration_id (int): 11/29位报文标识符,范围:
- 标准帧:[0x000, 0x7FF]
- 扩展帧:[0x00000000, 0x1FFFFFFF]
data (Optional[Union[bytes, bytearray, List[int]]]): 有效载荷数据,规则:
- 远程帧:必须为空
- 错误帧:≤8字节
- CAN FD:最长64字节
channel (int): 物理通道编号,取值范围 [0, 63],对应Vector Hardware Config中的配置
period (int): 周期发送间隔(毫秒 ms),0表示单次传输
timestamp (float): 纳秒级时间戳(单调时钟)
is_extended_id (bool): 扩展帧标识
is_remote_frame (bool): 远程传输请求
is_error_frame (bool): 错误帧标识
is_fd (bool): CAN FD格式标识
is_rx (bool): 接收方向标识
bitrate_switch (bool): CAN FD比特率切换(仅发送有效)
error_state_indicator (bool): CAN FD错误状态指示
Raises:
ProtocolError: 违反CAN协议规范时抛出,包含错误代码
Examples:
>>> msg = CANMessage(0x123, data=[0xFF] * 8)
"""
arbitration_id: int
data: Optional[Union[bytes, bytearray, list[int]]] = None
channel: int = dataclasses.field(
default=0,
metadata={"min": 0, "max": 63,
"description": "物理通道编号 (0=默认通道)"}
)
period: int = dataclasses.field(
default=0,
metadata={"min": 0,
"description": "周期发送间隔 (0=单次传输), uint: ms"}
)
timestamp: float = dataclasses.field(
default_factory=time.monotonic,
repr=False,
metadata={"unit": "seconds",
"precision": "nanoseconds"}
)
is_extended_id: bool = dataclasses.field(
default=False,
)
is_remote_frame: bool = dataclasses.field(
default=False,
)
is_error_frame: bool = dataclasses.field(
default=False,
)
is_fd: bool = dataclasses.field(
default=False,
metadata={"version": "ISO 11898-7"}
)
is_rx: bool = dataclasses.field(
default=True,
metadata={"description": "True=接收帧, False=发送帧"}
)
bitrate_switch: bool = dataclasses.field(
default=False,
repr=False,
metadata={"condition": "is_fd=True"}
)
error_state_indicator: bool = dataclasses.field(
default=False,
repr=False,
metadata={"condition": "is_fd=True"}
)
def __post_init__(self):
"""后初始化处理流程"""
self._convert_data_format()
self._validate_protocol_compliance()
self._auto_correct_fields()
def _convert_data_format(self):
"""统一数据格式为 bytes"""
if self.data is None:
self.data = bytes()
return
if isinstance(self.data, (list, bytearray)):
try:
self.data = bytes(self.data)
except ValueError as e:
raise ProtocolError(f"Invalid data bytes: {str(e)}") from None
elif not isinstance(self.data, bytes):
raise TypeError("Data payload must be bytes/bytearray/list[int]")
def _validate_protocol_compliance(self):
"""协议合规性验证"""
self._validate_id_range()
self._validate_frame_type_conflict()
self._validate_fd_features()
self._validate_remote_frame()
self._validate_error_frame()
# self._validate_bitrate_switch()
def _validate_id_range(self):
"""ID 范围校验"""
max_id = 0x1FFFFFFF if self.is_extended_id else 0x7FF
if not 0 <= self.arbitration_id <= max_id:
raise ProtocolError(f"ID out of range ({max_id:#x})")
def _validate_frame_type_conflict(self):
"""帧类型冲突检查"""
if sum([self.is_remote_frame, self.is_error_frame, self.bitrate_switch]) > 1:
raise ProtocolError("Frame type flags conflict")
def _validate_fd_features(self):
"""CAN FD 特性验证"""
if not self.is_fd and (self.bitrate_switch or self.error_state_indicator):
raise ProtocolError("CAN FD features require is_fd=True")
def _validate_remote_frame(self):
"""远程帧规范检查"""
if self.is_remote_frame:
if self.is_fd:
raise ProtocolError("CAN FD prohibits remote frames")
if len(self.data) > 0:
raise ProtocolError("Remote frame must have empty payload")
def _validate_error_frame(self):
"""错误帧规范检查"""
if self.is_error_frame and len(self.data) > 8:
raise ProtocolError("Error frame payload exceeds 8 bytes")
def _validate_bitrate_switch(self):
"""比特率切换条件检查"""
if self.bitrate_switch and len(self.data) <= 8:
raise ProtocolError("BRS requires payload >8 bytes")
def _auto_correct_fields(self):
"""字段自动校正"""
if self.is_error_frame and not self.is_rx:
self.is_rx = True # 错误帧只能为接收帧
@property
def dlc(self) -> int:
"""获取数据长度码"""
length = len(self.data)
return length if length <= 8 else 8 + (length - 8) // 4
@property
def is_standard_frame(self) -> bool:
"""是否为标准帧"""
return not any([self.is_remote_frame, self.is_error_frame, self.is_fd])
def to_pgn(self) -> int:
"""解析参数组编号 (PGN)"""
if self.is_extended_id:
return (self.arbitration_id >> 8) & 0x1FF00
return 0
3 总结
本文提供了两种CAN Message类的设计方法,其实内容上大同小异,通过对CAN报文进行类的封装设计,后续CAN通信的自动化测试中,可以更方便地处理CAN通信测试中的报文操作,提高测试效率和代码可维护性。