3、UDS网络层(ISO 15765-2):单帧与多帧传输、流控制机制、网络层定时参数、分段与重组

聊到UDS诊断,很多人一上来就盯着应用层的服务ID(比如0x22、0x2E)猛看。这没错,但我要说一句:真正让诊断协议在车上跑得稳的,其实是网络层。ISO 15765-2,说白了就是给UDS数据包做“打包、拆包、流量控制”的底层功夫。

我最早接触这个协议时,觉得它不就是个CAN报文封装嘛,有啥难的?结果第一次做Bootloader刷写,多帧传输老是超时,ECU刷到一半就挂了。嗯,从那以后我再也不敢小看网络层了。

3.1 单帧传输:最基础的数据单元

单帧(SingleFrame,SF)是最简单的形式。当你要发送的诊断数据长度不超过7个字节(CAN标准帧数据场为8字节,减去1字节的PCI,剩下7字节),直接用单帧搞定。

PCI(Protocol Control Information)是网络层协议控制信息,它藏在CAN数据场的第一个字节里。单帧的PCI格式如下:

Byte 0: [ 0x0 (4bit) | 数据长度 (4bit) ]
Byte 1-7: 诊断数据(最多7字节)

举个例子,发送一个“0x22 0xF1 0x90”的读取请求,总共3字节,完全可以用单帧:

CAN ID: 0x7DF
Data: 0x03 0x22 0xF1 0x90 0x00 0x00 0x00 0x00

这里0x03就是PCI,高4位是0(表示单帧),低4位是3(表示数据长度3字节)。

我的习惯: 单帧虽然简单,但要注意CAN数据场剩余字节最好填充0x00或0x55,不要留随机值。有些严格的ECU会检查填充字节,我曾经遇到过因为填充了0xAA导致ECU误判的案例。

3.2 多帧传输:当数据超过7字节时

诊断数据超过7字节怎么办?比如你要读取一个DTC快照数据,或者刷写一个Bootloader块,数据量可能几百甚至几千字节。这时候就需要多帧传输了。

多帧传输分为三个阶段:

  1. 首帧(FirstFrame,FF): 发送方告诉接收方“我要发一大包数据,总共多少字节”
  2. 流控制帧(FlowControl,FC): 接收方回应“我知道了,你按这个节奏发”
  3. 连续帧(ConsecutiveFrame,CF): 发送方按流控制帧的要求,一帧一帧把数据发完

首帧的PCI格式:

Byte 0: [ 0x1 (4bit) | 数据长度高4位 (4bit) ]
Byte 1: 数据长度低8位
Byte 2-7: 诊断数据(最多6字节)

注意,首帧的数据长度是12位(最大4095字节),数据场只能带6字节的载荷。剩下的数据通过连续帧发送。

连续帧的PCI格式:

Byte 0: [ 0x2 (4bit) | 序列号 (4bit) ]
Byte 1-7: 诊断数据(最多7字节)

序列号从1开始,到15后回绕到0(模16计数)。

关键点: 连续帧的序列号是循环使用的。如果你发送了16个连续帧,第16帧的序列号是0(因为1+15=16,模16后为0)。很多新手在这里踩坑,以为序列号错了。

3.3 流控制机制:谁说了算?

流控制帧(FC)是接收方用来控制发送节奏的。它的PCI格式:

Byte 0: [ 0x3 (4bit) | 流状态 (4bit) ]
Byte 1: 块大小(BlockSize,BS)
Byte 2: 最小间隔时间(SeparationTime,STmin)

流状态有三种:

流状态值 含义 说明
0x0 继续发送(CTS) 允许发送方继续发连续帧
0x1 等待(WAIT) 接收方忙,请等待(我项目中很少用到)
0x2 溢出(OVFLW) 接收方缓冲区不够,传输失败

块大小(BS)表示发送方在收到下一个流控制帧之前,最多可以发送多少个连续帧。BS=0表示“你一口气发完,不用等我回应”。

STmin表示连续帧之间的最小间隔时间,单位是毫秒(0x00-0x7F)或微秒(0xF1-0xF9)。

我曾经踩过的坑: 有一次做Bootloader刷写,ECU回复BS=0,STmin=0x10(16ms)。我心想BS=0就是随便发,结果连续帧发得太快,ECU的CAN控制器缓冲区溢出了。后来才明白,BS=0只是说“不用发FC帧”,但STmin还是要遵守的。说白了,BS=0不代表你可以无限制地狂发。

3.4 网络层定时参数:时间就是生命

网络层定义了四个关键定时参数,它们决定了多帧传输的成败:

参数 名称 典型值 说明
N_As 发送帧时间 0-1000ms 从发送请求到CAN帧实际发出的最大时间
N_Ar 接收帧时间 0-1000ms 从CAN帧到达到网络层处理完成的最大时间
N_Bs 流控制帧等待时间 0-1000ms 发送方等待接收方回复FC帧的最大时间
N_Cr 连续帧接收时间 0-1000ms 接收方等待下一个连续帧的最大时间

这些时间参数在ISO 15765-2中给出了推荐范围,但实际项目中OEM通常会定义更严格的值。比如某德系车厂要求N_Bs不超过500ms,N_Cr不超过250ms。

你想想看,如果N_Bs设置得太短,ECU稍微忙一点,发送方就超时了,整个传输就失败了。反过来,如果设置得太长,诊断响应就慢,用户体验差。

我的建议: 在开发阶段,先把定时参数设得宽松一些(比如1000ms),等基本功能调通了,再根据实际总线负载和ECU处理能力,逐步收紧。我曾经在一个项目里,因为N_Cr设得太紧(100ms),导致刷写成功率只有60%,后来放宽到300ms,成功率直接到99%。

3.5 分段与重组:拆包的艺术

分段(Segmentation)是发送方把大数据拆成多个CAN帧的过程。重组(Reassembly)是接收方把这些帧重新拼回完整数据的过程。

分段的关键规则:

  • 首帧携带数据的前6个字节
  • 连续帧每个携带7个字节(最后一个帧可能少于7字节)
  • 连续帧的序列号从1开始,循环使用0-15
  • 如果BS不为0,发送方每发BS个连续帧后,必须等待接收方发新的FC帧

举个例子,发送一个100字节的诊断数据:

首帧: PCI=0x10 0x64 (数据长度100=0x0064),数据字节0-5
CF 1: PCI=0x21,数据字节6-12
CF 2: PCI=0x22,数据字节13-19
...
CF 14: PCI=0x2E,数据字节90-96
CF 15: PCI=0x2F,数据字节97-99(最后3字节)

注意,这里用了15个连续帧(序列号1-15),最后一个帧只有3字节数据。如果数据超过15×7=105字节,序列号会回绕到0继续。

重组时的注意事项: 接收方必须按序列号顺序重组数据。如果收到乱序的连续帧(比如先收到序列号2,再收到序列号1),应该丢弃并触发超时处理。我见过一些实现不严谨的ECU,乱序后还能重组成功,但那是运气好,不是规范的做法。

3.6 实战中的常见问题

最后,分享几个我在项目中遇到的实际问题:

  1. BS=0与STmin的配合: 有些ECU的BS=0实现有bug,STmin设了等于没设。解决办法是主动把BS设成非零值(比如5),强制ECU每发5帧就等一次FC。
  2. N_Bs超时后重试: 如果发送方没收到FC帧,应该重发首帧还是直接报错?规范建议重试3次,每次间隔N_Bs时间。我习惯重试2次,第三次直接报错,避免无限循环。
  3. 连续帧丢帧处理: 如果接收方发现某个序列号的连续帧没收到(N_Cr超时),应该丢弃整个多帧传输,并回复一个否定响应(NRC=0x78,请求正确接收-挂起)。
  4. CAN总线负载的影响: 高速CAN(500kbps)下,一个标准帧大约占用135μs总线时间。如果连续帧间隔STmin=0ms,理论上可以连续发送,但实际总线负载高时,仲裁延迟会导致帧间隔变大。我建议STmin至少设1ms,给总线留点喘息空间。

嗯,网络层的内容就这些。说白了,ISO 15765-2就是一套“怎么把大象装进冰箱”的协议——先切块(分段),再一块块送(连续帧),中间还要问冰箱“你还能装吗”(流控制)。掌握了这些,UDS诊断的底层你就拿下了。