3. 车载网络通信原理
CAN总线上的节点,不光能主动发数据,还能“伸手要”。这个“伸手要”的动作,就是远程帧。而错误帧,则是总线上的“交警”——谁违规了,它立刻出来喊停。
这两个概念,很多初学者容易搞混。远程帧到底怎么用?错误帧有几种?我踩过不少坑,今天一次性给你讲透。
3.1 远程帧:总线上的“请求信号”
远程帧,说白了就是一个节点告诉另一个节点:“把你那个ID的数据发给我”。它本身不携带数据字节,只带一个标识符(ID)和RTR位。
3.1.1 远程帧的结构
| 字段 | 标准帧 | 扩展帧 |
|---|---|---|
| 帧起始 | 1位显性 | 1位显性 |
| 标识符 | 11位 | 29位 |
| RTR位 | 隐性(1) | 隐性(1) |
| 控制场 | IDE=0, r0=0, DLC=0 | IDE=1, r1=0, DLC=0 |
| 数据场 | 无 | 无 |
| CRC场 | 15位+定界符 | 15位+定界符 |
| ACK场 | 2位 | 2位 |
| 帧结束 | 7位隐性 | 7位隐性 |
注意看,RTR位在远程帧里是隐性(1),而数据帧里是显性(0)。这就是总线仲裁时区分两者的关键。
核心要点:远程帧的DLC必须为0。如果DLC非0,接收节点会认为这是一个数据帧,导致通信混乱。我见过有人把DLC写成8,结果对方一直等数据,总线直接卡死。
3.1.2 远程帧的工作机制
举个例子。假设节点A需要发动机转速数据(ID=0x100),它发一个远程帧,ID=0x100,RTR=1。节点B收到后,如果它负责这个ID,就会立刻发一个数据帧回来。
流程是这样的:
- 节点A发送远程帧(ID=0x100,RTR=1)
- 节点B收到远程帧,识别到ID匹配
- 节点B准备数据,发送数据帧(ID=0x100,RTR=0)
- 节点A收到数据帧,读取数据
避坑指南:我曾经在项目中遇到一个问题——两个节点同时请求同一个ID的远程帧。结果总线仲裁时,远程帧的优先级比数据帧低,导致数据帧一直被延迟。后来我加了一个“请求间隔”机制,才解决。
3.1.3 远程帧的Python模拟
写个简单的脚本,模拟远程帧的发送和响应:
import can
import time
def send_remote_frame(bus, arb_id):
"""发送远程帧"""
msg = can.Message(
arbitration_id=arb_id,
is_remote_frame=True,
dlc=0
)
bus.send(msg)
print(f"发送远程帧: ID=0x{arb_id:X}")
def wait_for_data(bus, arb_id, timeout=1.0):
"""等待数据帧响应"""
start = time.time()
while time.time() - start < timeout:
msg = bus.recv(timeout=0.1)
if msg and msg.arbitration_id == arb_id and not msg.is_remote_frame:
return msg
return None
# 使用示例
bus = can.interface.Bus(channel='can0', bustype='socketcan')
send_remote_frame(bus, 0x100)
response = wait_for_data(bus, 0x100)
if response:
print(f"收到数据: {response.data.hex()}")
else:
print("超时,未收到响应")
这个脚本很简单,但实际项目中要考虑更多——比如重试机制、超时处理、多节点并发请求等。
3.2 错误帧:总线的“纠错机制”
CAN总线为什么可靠?因为它有一套完整的错误检测和处理机制。错误帧就是这套机制的“执行者”。
3.2.1 错误类型
CAN协议定义了5种错误:
| 错误类型 | 检测位置 | 触发条件 |
|---|---|---|
| 位错误 | 发送节点 | 发送的位与总线电平不一致 |
| 填充错误 | 所有节点 | 连续6个相同位(违反位填充规则) |
| CRC错误 | 接收节点 | 计算CRC与接收CRC不匹配 |
| 形式错误 | 所有节点 | 固定格式位(如CRC定界符)为显性 |
| 应答错误 | 发送节点 | ACK槽未被显性覆盖 |
注意:位错误和填充错误是“即时检测”的,一旦发现立刻报错。CRC错误和形式错误是在帧结束时才检测。应答错误则是在ACK阶段检测。
3.2.2 错误帧的结构
错误帧由两部分组成:
- 错误标志:6个连续显性位(主动错误)或6个连续隐性位(被动错误)
- 错误定界符:8个连续隐性位
为什么是6个?因为位填充规则规定最多5个相同位。6个连续显性位,所有节点都能识别出这是错误标志。
3.2.3 错误状态机
每个CAN节点都有三个状态:
- 主动错误(Error Active):正常状态,可以发送主动错误帧(6个显性位)
- 被动错误(Error Passive):发送被动错误帧(6个隐性位),不能破坏总线
- 总线关闭(Bus Off):完全断开,不参与任何通信
状态切换由两个计数器控制:
- 发送错误计数器(TEC):发送错误时+8,成功时-1
- 接收错误计数器(REC):接收错误时+1,成功时-1
当TEC或REC超过127,节点进入被动错误状态。当TEC超过255,节点进入总线关闭状态。
经验之谈:我遇到过一台ECU频繁进入总线关闭状态。排查后发现是它的CAN收发器供电不稳,导致发送位错误。后来加了滤波电容,问题解决。所以,硬件问题也会导致错误帧激增。
3.3 错误处理与自动化测试
在自动化测试中,我们需要模拟各种错误场景,验证DUT(被测设备)的错误处理能力。
3.3.1 常见测试场景
| 测试场景 | 模拟方法 | 预期结果 |
|---|---|---|
| 位错误 | 在发送时强制翻转某一位 | DUT检测到位错误,发送错误帧 |
| CRC错误 | 修改数据帧的CRC字段 | DUT丢弃该帧,发送错误帧 |
| 填充错误 | 发送连续6个相同位 | DUT检测到填充错误 |
| 总线关闭恢复 | 让DUT反复发送错误帧 | DUT进入总线关闭,然后恢复 |
3.3.2 Python模拟错误帧
用Python模拟一个CRC错误:
import can
import random
def send_corrupted_frame(bus, arb_id, data):
"""发送一个CRC错误的帧"""
# 构造正常帧
msg = can.Message(
arbitration_id=arb_id,
data=data,
is_extended_id=False
)
# 手动修改CRC(通过发送原始字节)
# 这里用简单方法:发送后立即发送错误帧
bus.send(msg)
# 发送主动错误帧(6个显性位)
error_msg = can.Message(
arbitration_id=0,
data=[],
is_error_frame=True
)
bus.send(error_msg)
print(f"发送错误帧: ID=0x{arb_id:X}")
def monitor_error_frames(bus, duration=5.0):
"""监控错误帧"""
start = time.time()
error_count = 0
while time.time() - start < duration:
msg = bus.recv(timeout=0.1)
if msg and msg.is_error_frame:
error_count += 1
print(f"检测到错误帧: {msg.timestamp}")
return error_count
# 使用示例
bus = can.interface.Bus(channel='can0', bustype='socketcan')
send_corrupted_frame(bus, 0x200, [0x01, 0x02, 0x03])
errors = monitor_error_frames(bus, 2.0)
print(f"共检测到 {errors} 个错误帧")
提示:实际测试中,不要真的让DUT进入总线关闭状态,否则需要等待128个11位隐性位才能恢复。我一般用“错误注入”工具,精确控制错误帧的发送时机。
3.4 知识体系图
下面这张图帮你理清远程帧和错误帧的关系:
3.5 避坑总结
最后,把我这些年踩过的坑总结一下:
- 远程帧DLC不为0:接收节点会当成数据帧处理,导致数据错乱。我建议在发送前强制检查DLC。
- 错误帧风暴:一个节点频繁报错,会导致总线被错误帧占满。我曾经遇到过,最后发现是终端电阻虚焊。
- 总线关闭恢复时间:128个11位隐性位,大约需要1-2ms。测试时要注意这个延迟。
- 被动错误帧不破坏总线:被动错误帧是隐性位,不会影响其他节点。但发送节点自己知道出错了。
重要提醒:在自动化测试中,模拟错误帧时一定要控制频率。如果每秒发几百个错误帧,DUT可能会直接挂掉。我一般控制在每秒10个以内,观察DUT的反应。
远程帧和错误帧,是CAN总线通信的“左右手”。一个负责请求,一个负责纠错。理解了它们,你就能更好地设计测试用例,也能更快地定位总线问题。