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字节)。
3.2 多帧传输:当数据超过7字节时
诊断数据超过7字节怎么办?比如你要读取一个DTC快照数据,或者刷写一个Bootloader块,数据量可能几百甚至几千字节。这时候就需要多帧传输了。
多帧传输分为三个阶段:
- 首帧(FirstFrame,FF): 发送方告诉接收方“我要发一大包数据,总共多少字节”
- 流控制帧(FlowControl,FC): 接收方回应“我知道了,你按这个节奏发”
- 连续帧(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计数)。
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)。
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稍微忙一点,发送方就超时了,整个传输就失败了。反过来,如果设置得太长,诊断响应就慢,用户体验差。
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继续。
3.6 实战中的常见问题
最后,分享几个我在项目中遇到的实际问题:
- BS=0与STmin的配合: 有些ECU的BS=0实现有bug,STmin设了等于没设。解决办法是主动把BS设成非零值(比如5),强制ECU每发5帧就等一次FC。
- N_Bs超时后重试: 如果发送方没收到FC帧,应该重发首帧还是直接报错?规范建议重试3次,每次间隔N_Bs时间。我习惯重试2次,第三次直接报错,避免无限循环。
- 连续帧丢帧处理: 如果接收方发现某个序列号的连续帧没收到(N_Cr超时),应该丢弃整个多帧传输,并回复一个否定响应(NRC=0x78,请求正确接收-挂起)。
- CAN总线负载的影响: 高速CAN(500kbps)下,一个标准帧大约占用135μs总线时间。如果连续帧间隔STmin=0ms,理论上可以连续发送,但实际总线负载高时,仲裁延迟会导致帧间隔变大。我建议STmin至少设1ms,给总线留点喘息空间。
嗯,网络层的内容就这些。说白了,ISO 15765-2就是一套“怎么把大象装进冰箱”的协议——先切块(分段),再一块块送(连续帧),中间还要问冰箱“你还能装吗”(流控制)。掌握了这些,UDS诊断的底层你就拿下了。