3、DoIP报文结构:通用DoIP报文头部与Payload结构

好,咱们今天来聊聊DoIP报文的结构。说实话,这个知识点是整个DoIP协议的基础。你如果搞不懂报文头里每个字节是干嘛的,后面做诊断通信的时候,出了问题你都不知道从哪查起。

我个人习惯,拿到一个协议,先看它的报文头。就像你收到一封信,先看信封上的地址、邮戳一样。DoIP的报文头,就是那个「信封」。

3.1 通用DoIP报文头部

先看一张表,这是DoIP报文头的固定结构。一共8个字节,一个不多,一个不少。

字节偏移 字段名称 长度(字节) 说明
0 协议版本 1 当前固定为 0x02
1 逆向协议版本 1 固定为 0xFD(即 ~0x02)
2-3 负载类型 2 标识DoIP消息的具体类型
4-7 负载长度 4 Payload部分的字节数

嗯,这里要注意。协议版本字段,目前ISO 13400-2里定义的是0x02。为什么不是0x01?我记得早期草案里确实是0x01,后来正式发布时改成了0x02。你在实际项目中如果抓到0x01的报文,那大概率是某个老旧的测试工具或者非标实现。

逆向协议版本这个字段,说白了就是个校验机制。它的值等于协议版本按位取反。比如0x02取反就是0xFD。接收方收到报文后,会检查这两个字段是否满足取反关系。如果不满足,直接丢弃报文。

重要:我曾经在项目联调时遇到一个奇怪的问题——Tester发出去的诊断请求,ECU就是不响应。抓包一看,协议版本填的是0x02,但逆向协议版本填成了0x02的补码(0xFE),而不是按位取反(0xFD)。就这一个字节的差异,导致整个通信失败。所以,这两个字段一定要成对出现,千万别搞混。

3.2 负载类型(Payload Type)

负载类型占2个字节,它告诉接收方:这个DoIP报文到底是干嘛用的。是路由激活?是诊断请求?还是车辆信息查询?

我整理了一些常用的负载类型值,你记一下:

负载类型值(十六进制) 含义 方向
0x0001 通用DoIP头部否定确认 双向
0x0002 车辆识别请求 Tester → ECU
0x0003 车辆识别响应 ECU → Tester
0x0004 路由激活请求 Tester → ECU
0x0005 路由激活响应 ECU → Tester
0x8001 诊断消息(DoIP封装UDS) 双向

你想想看,为什么诊断消息的负载类型是0x8001,而不是0x0001?其实这是ISO故意设计的。高位为1的负载类型,表示这个报文携带的是诊断数据(即UDS消息)。而高位为0的,是DoIP自身的控制消息。这样设计,接收方在解析时只需要检查最高位,就能快速区分是控制报文还是诊断报文。

实战技巧:我在做DoIP协议栈时,会在报文接收的入口处先判断负载类型的最高位。如果是0x8xxx,直接走诊断消息处理通道;如果是0x0xxx,走控制消息处理通道。这样代码逻辑清晰,也方便后续扩展。

3.3 负载长度(Payload Length)

负载长度占4个字节,它表示紧跟在报文头后面的Payload部分有多少个字节。注意,这个长度不包含报文头本身的8个字节。

举个例子:

// 一个完整的DoIP报文(十六进制)
02 FD 80 01 00 00 00 05  10 03 00 00 00 00 00 00

// 解析:
// 02          → 协议版本
// FD          → 逆向协议版本
// 80 01       → 负载类型(诊断消息)
// 00 00 00 05 → 负载长度 = 5
// 10 03 00 00 00 00 00 00 → Payload(5字节)

这里负载长度是5,表示后面的Payload有5个字节。Payload里装的就是UDS诊断消息——0x10 03(诊断会话控制请求)加上3个字节的源地址和目标地址信息。

注意:负载长度字段是大端序(Big-Endian)存储的。也就是说,高位字节在前,低位字节在后。如果你用的小端序处理器(比如某些ARM Cortex-M),读取这个字段时一定要做字节序转换。我曾经见过一个同事,直接memcpy读取这个4字节字段,结果解析出来的长度是0x05000000(约83886080字节),程序直接崩溃了。

3.4 DoIP Payload结构

说完了报文头,咱们来看看Payload。Payload的结构其实取决于负载类型。不同的负载类型,Payload的格式完全不同。

我以最常见的两种类型为例:

3.4.1 诊断消息(负载类型 0x8001)的Payload

诊断消息的Payload,说白了就是把UDS报文「装」进DoIP里。它的结构如下:

字节偏移 字段名称 长度(字节) 说明
0-1 源地址 2 发送方的逻辑地址
2-3 目标地址 2 接收方的逻辑地址
4 UDS报文开始 N 实际的UDS诊断数据

你看,DoIP在UDS报文外面包了一层「地址信息」。源地址和目标地址各占2个字节,用来标识这条诊断消息是谁发给谁的。这样做的好处是,一条TCP连接上可以同时承载多个ECU之间的诊断通信。

3.4.2 路由激活请求(负载类型 0x0004)的Payload

路由激活请求的Payload就简单多了:

字节偏移 字段名称 长度(字节) 说明
0-1 源地址 2 Tester的逻辑地址
2 激活类型 1 0x00=默认, 0x01=WWH-OBD
3-6 保留(OEM特定) 4 通常填0

嗯,这里有个坑。激活类型字段,ISO标准里只定义了0x00和0x01。但有些OEM会自己定义其他值,比如0x02表示「安全路由激活」。如果你在做Tester工具,最好把这个字段做成可配置的,免得换一个项目就要改代码。

避坑指南:我曾经在一个项目中,ECU对路由激活请求的响应总是超时。排查了半天,发现是Tester发送的源地址和ECU期望的源地址不匹配。ECU内部有一个「允许列表」,只有列表里的源地址才能激活路由。所以,路由激活之前,一定要确认Tester的逻辑地址是否被ECU认可。

3.5 小结

好了,咱们总结一下。DoIP报文结构其实不复杂,核心就是8字节的固定头部加上可变长度的Payload。头部里的协议版本和逆向协议版本是一对「校验码」,负载类型告诉你怎么解析Payload,负载长度告诉你Payload有多长。

我个人建议,你在写DoIP解析代码时,先把这8个字节的头部解析出来,做一次完整性检查。检查通过后,再根据负载类型去解析Payload。这样分层处理,代码可读性和可维护性都会好很多。

下一章,咱们会深入聊一聊路由激活的具体流程。到时候我会拿一个实际项目中的抓包数据,带大家一步步分析。敬请期待。