3. UDS网络层(ISO 15765-2):单帧与多帧传输、流控制机制、分段与重组、网络层超时处理
好,咱们今天聊聊网络层。ISO 15765-2,也就是大家常说的CAN TP层。
说实话,很多刚入行的工程师觉得UDS应用层才是核心,网络层嘛,不就是个打包拆包吗?
嗯,我当年也这么想。直到有一次在实车上,一个多帧报文死活收不全,ECU直接进入BusOff状态。排查了三天,最后发现是流控制帧的STmin参数设得太激进,ECU处理不过来。
从那以后,我再也不敢小看网络层了。
3.1 单帧传输:小数据包的直通车
先说说最简单的场景。你要发送的数据很少,比如就发一个"读取VIN码"的请求,总共才3个字节。
这种情况下,一个CAN帧就搞定了。这就是单帧(Single Frame,SF)。
单帧格式:
- PCI字节的高4位:固定为0,表示这是单帧
- PCI字节的低4位:数据长度(0-7)
- 后续字节:实际数据
举个例子,发送一个"0x22 0xF1 0x90"的请求(按地址读取数据):
CAN ID: 0x7E0
数据: 03 22 F1 90
^^
||__ 数据长度 = 3字节
|____ 单帧标识 = 0
这里有个细节我提醒一下。单帧的数据长度最大是7字节,因为CAN帧数据场总共8字节,PCI占1字节,剩下7字节给数据。
那如果数据超过7字节怎么办?别急,这就是多帧传输的用武之地了。
3.2 多帧传输:大块头的拆分艺术
当你要发送的数据超过7字节,比如一个完整的诊断响应可能有几十甚至上百字节,这时候就需要把数据拆成多个CAN帧来发送。
多帧传输涉及三种帧类型:
- 首帧(First Frame,FF):告诉接收方"我要发大包了,准备接好"
- 流控制帧(Flow Control,FC):接收方说"我准备好了,你发吧"或者"慢点,我处理不过来"
- 连续帧(Consecutive Frame,CF):实际的数据帧,一帧一帧发过去
我个人习惯:在调试多帧传输时,我会先用CANoe抓一下总线上的报文流。首帧、流控制、连续帧,这三个阶段的时序必须严格对齐。我曾经遇到过一个项目,ECU的流控制帧总是晚发200ms,导致发送方超时重传,整个诊断链路卡死。
3.3 流控制机制:谁说了算?
流控制帧(FC)是接收方用来控制发送节奏的。它包含三个关键参数:
| 参数 | 含义 | 典型值 |
|---|---|---|
| FS(Flow Status) | 流状态:0=继续发送,1=等待,2=溢出/中止 | 0x00 |
| BS(Block Size) | 块大小:连续发送多少帧后需要等待下一个FC | 0x00(无限制)或0x01-0xFF |
| STmin(Separation Time minimum) | 最小间隔时间:连续帧之间的最小时间间隔 | 0x00-0xF9(毫秒级)或0xF1-0xF9(微秒级) |
说白了,这就是一个"你发我收"的握手协议。
接收方说:"你一次发5帧,然后等我确认,帧与帧之间至少隔10ms。"
发送方就得乖乖照做。
避坑指南:我曾经在标定一个ECU时,把STmin设成了0x00(表示无间隔)。结果ECU的CAN控制器根本跟不上,连续丢帧。后来查手册才发现,0x00表示"由发送方决定最小间隔",但很多ECU的实现并不标准。我建议STmin至少设0x10(1ms),稳妥一点。
3.4 分段与重组:拆得开,还得装得上
发送方把大包拆成小帧,接收方收到后要能原样拼回去。这个过程叫分段与重组。
发送方(分段):
- 首帧:发送总数据长度(12位)和部分数据
- 连续帧:每个帧带一个序列号(4位,从1开始,到15后回绕到0)
- 按流控制帧的要求控制发送节奏
接收方(重组):
- 收到首帧,分配缓冲区,记录总长度
- 按序列号依次接收连续帧
- 检查序列号是否连续,有没有丢帧
- 收齐所有数据后,拼成完整的诊断报文
这里有个容易踩的坑:序列号回绕。
连续帧的序列号只有4位,范围0-15。如果你要发20个连续帧,序列号会从1走到15,然后回到0,再走到4。
接收方必须能正确处理这种回绕。我见过一个ECU,序列号回绕后直接丢弃后续帧,导致多帧传输永远收不完。
3.5 网络层超时处理:别让总线等太久
网络层定义了多个超时时间,确保通信不会卡死。这些超时时间在ISO 15765-2中有明确规定:
| 超时名称 | 缩写 | 典型值 | 说明 |
|---|---|---|---|
| N_As | 发送超时 | 1000ms | 发送方等待帧被成功发送的最大时间 |
| N_Bs | 接收准备超时 | 1000ms | 发送方等待流控制帧的最大时间 |
| N_Cr | 连续帧接收超时 | 1000ms | 接收方等待下一个连续帧的最大时间 |
| N_Ar | 接收响应超时 | 50ms | 接收方发送流控制帧的响应时间 |
为什么要有这些超时?你想想看,如果发送方发了首帧,接收方一直不给流控制帧,发送方总不能无限等下去吧?
N_Bs就是干这个的。超时了,发送方就认为通信失败,该报错报错,该重试重试。
实际项目中的经验:
我在做TBOX项目时,遇到过N_Ar超时的问题。ECU收到首帧后,需要先处理内部任务,再发流控制帧。但ECU的任务调度优先级没设好,导致流控制帧总是晚于50ms发出。
解决方案有两个:
- 调整ECU的任务优先级,把CAN接收中断的优先级提高
- 或者把N_Ar放宽到100ms(但需要和客户确认,因为标准值就是50ms)
最后我们选了第一种方案,毕竟标准不能随便改。
3.6 代码实现:一个简单的多帧发送函数
说了这么多理论,咱们看看代码怎么实现。下面是一个简化的多帧发送函数,用Python写的:
def send_multi_frame(can_bus, can_id, data):
"""
发送多帧诊断报文
:param can_bus: CAN总线对象
:param can_id: 发送ID
:param data: 要发送的完整数据(bytes)
"""
total_len = len(data)
if total_len <= 7:
# 单帧发送
pci = (0 << 4) | total_len # 单帧标识 + 长度
frame_data = bytes([pci]) + data
can_bus.send(can_id, frame_data)
return
# 多帧发送
# 1. 发送首帧
ff_pci = (1 << 4) | ((total_len >> 8) & 0x0F) # 首帧标识 + 长度高4位
ff_data = bytes([ff_pci, total_len & 0xFF]) + data[:6]
can_bus.send(can_id, ff_data)
# 2. 等待流控制帧
fc_frame = wait_for_fc(can_bus, timeout=1000)
if fc_frame is None:
raise TimeoutError("等待流控制帧超时")
bs = fc_frame[2] # 块大小
stmin = fc_frame[3] # 最小间隔时间
# 3. 发送连续帧
seq_num = 1
offset = 6
frame_count = 0
while offset < total_len:
# 计算本帧数据长度
chunk_size = min(7, total_len - offset)
# 构造连续帧
cf_pci = (2 << 4) | (seq_num & 0x0F)
cf_data = bytes([cf_pci]) + data[offset:offset+chunk_size]
can_bus.send(can_id, cf_data)
offset += chunk_size
seq_num = (seq_num + 1) & 0x0F
frame_count += 1
# 检查块大小限制
if bs != 0 and frame_count >= bs:
# 等待下一个流控制帧
fc_frame = wait_for_fc(can_bus, timeout=1000)
if fc_frame is None:
raise TimeoutError("等待流控制帧超时")
frame_count = 0
# 等待STmin时间
time.sleep(stmin / 1000.0)
这段代码我简化了很多,比如没有处理流控制帧的FS=1(等待)和FS=2(溢出)的情况。实际项目中,这些边界情况必须全部覆盖。
一个小技巧:调试多帧传输时,我习惯在代码里加一些日志打印,把每个帧的PCI、序列号、数据长度都打出来。这样一旦出问题,看日志就能定位到是哪一帧丢了或者顺序错了。
3.7 总结
网络层说白了就是解决一个问题:如何在CAN这种小数据包的总线上传输大块数据。
单帧搞定小数据,多帧搞定大数据。流控制帧负责协调节奏,超时机制防止死锁。
嗯,内容就这些。下一章咱们聊聊应用层的具体服务,比如诊断会话控制、安全访问这些实战内容。