2、DoIP协议栈架构:OSI模型映射、DoIP实体与网关的概念、报文结构

好,咱们进入正题。这一节我打算把DoIP的协议栈架构彻底讲透。你可能会问,学这个有什么用?我举个例子——有一次我在现场排查一台车连不上诊断仪,折腾了半天,最后发现是DoIP实体和网关的配置搞混了。从那以后,我就特别重视这部分基础。

2.1 OSI模型映射:DoIP到底在哪个层级干活?

先看一张经典的OSI七层模型。DoIP不是凭空冒出来的,它老老实实地坐在传输层和应用层之间。我个人习惯把DoIP理解为「诊断数据的快递员」——它负责把UDS诊断请求打包,然后通过TCP/IP网络送出去。

OSI层 DoIP对应协议/功能 我的一点理解
应用层(7) UDS诊断服务(ISO 14229) 真正干活的,比如读故障码
表示层(6) DoIP报文封装/解封装 把UDS数据包上DoIP头部
会话层(5) DoIP连接管理、路由激活 谁连上来了?权限够不够?
传输层(4) TCP(可靠通信)、UDP(发现) TCP传数据,UDP做广播
网络层(3) IPv4/IPv6 IP地址、路由
数据链路层(2) 以太网MAC 物理地址,帧格式
物理层(1) 100BASE-TX / 1000BASE-T 网线、接口

嗯,这里要注意:DoIP主要工作在会话层和表示层。传输层以下的事,它基本不管,交给操作系统协议栈就行。我在项目里见过有人非要在DoIP层做重传机制,其实没必要——TCP已经帮你干好了。

核心要点:DoIP = UDS诊断数据 + TCP/IP网络传输。它不负责「诊断什么」,只负责「怎么传过去」。

2.2 DoIP实体(Entity)与网关(Gateway)的概念

这两个概念特别容易搞混。我刚开始接触DoIP时,以为实体就是网关,网关就是实体。后来被老工程师骂了一顿才明白——完全两码事。

2.2.1 DoIP实体(Entity)

说白了,DoIP实体就是「能说话的那个节点」。它具备完整的DoIP协议栈,能接收诊断请求、发送响应。一辆车上可以有多个DoIP实体,比如:

  • 主控ECU:直接连接以太网,自己就是DoIP实体
  • 网关ECU:如果它实现了DoIP协议栈,那它也是实体
  • 远程信息处理单元(T-Box):经常充当DoIP实体

每个DoIP实体都有一个唯一的逻辑地址(Logical Address),就像人的身份证号。我记得有一次调试,发现两个ECU的逻辑地址冲突了,结果诊断仪死活连不上其中一个。查了半天,原来是配置表写错了。

小技巧:逻辑地址在DoIP报文的源地址和目标地址字段里体现。建议在项目初期就做好地址分配表,避免后期打架。

2.2.2 DoIP网关(Gateway)

网关的角色更像「翻译官」或「路由器」。它连接着以太网和车内其他网络(CAN、LIN、FlexRay)。外部诊断仪通过以太网发来DoIP报文,网关负责:

  1. 解析DoIP头部,取出UDS数据
  2. 把UDS数据转换成CAN报文(或其他总线格式)
  3. 转发给目标ECU
  4. 把响应再封装回DoIP报文,发回诊断仪

你想想看,如果车内全是CAN节点,没有以太网能力,那网关就是唯一的DoIP实体。它既要处理自己的诊断,还要帮别人传话。我曾经在一个项目里,网关同时要转发20多个CAN节点的诊断数据,CPU负载直接飙到80%。后来优化了报文调度才压下来。

避坑指南:我曾经遇到过网关的DoIP缓冲区设置太小,导致并发诊断请求时丢包。建议把接收缓冲区至少设到64KB,别省这点内存。

2.3 DoIP报文结构:通用头部详解

好,到了最硬核的部分。DoIP报文结构其实不复杂,但细节很多。我习惯把通用头部叫做「信封」——它告诉接收方这封信是谁写的、写给谁、有多长、是什么类型。

2.3.1 通用头部格式

所有DoIP报文(不管是TCP还是UDP)都包含这个8字节的通用头部:

字节偏移 | 字段名          | 长度 | 描述
---------|-----------------|------|-------------------------------
0        | 协议版本        | 1    | 当前固定为0x02(ISO 13400-2)
1        | 反向协议版本    | 1    | 同样为0x02
2-3      | 负载类型        | 2    | 标识报文用途(见下表)
4-7      | 负载长度        | 4    | 后续数据的字节数(不含头部)

嗯,这里有个坑:负载长度字段是4字节,但实际项目中很少超过65535字节。不过标准留了余量,咱们也别自作聪明去截断它。

2.3.2 常见的负载类型(Payload Type)

我整理了一份常用列表,你在实际抓包时肯定用得上:

负载类型值(十六进制) 名称 用途
0x0001 Generic DoIP header negative acknowledge 通用否定应答(比如版本不匹配)
0x0002 Vehicle Identification Request 车辆识别请求(UDP广播)
0x0004 Vehicle Identification Response 车辆识别响应
0x0005 Routing Activation Request 路由激活请求(TCP连接后第一步)
0x0006 Routing Activation Response 路由激活响应
0x8001 Diagnostic Message 真正的UDS诊断数据(TCP)

为什么会有0x0001这种否定应答?我遇到过一种情况:诊断仪用旧版协议(0x01)发请求,但车端只支持新版(0x02)。这时候车端就会回复0x0001,告诉对方「版本不对,请升级」。说白了,这就是协议层面的握手检查。

2.3.3 一个完整的DoIP报文示例

假设诊断仪要发送一个UDS诊断请求(比如读取VIN码),完整的DoIP报文长这样:

02 02 80 01 00 00 00 0A  // 通用头部:版本0x02,负载类型0x8001,长度10字节
FD 00 00 0E 00 00 00 00  // DoIP诊断消息头部:源地址0xFD00,目标地址0x000E
22 F1 90                 // UDS请求:读取VIN(22服务,F190标识符)

我来拆解一下:

  • 02 02:协议版本和反向版本,都是0x02
  • 80 01:负载类型,表示这是诊断消息
  • 00 00 00 0A:负载长度,10字节(后面所有数据的总和)
  • FD 00:源逻辑地址,诊断仪的地址
  • 00 0E:目标逻辑地址,目标ECU的地址
  • 22 F1 90:UDS请求体

注意:DoIP诊断消息头部(源地址+目标地址+用户数据)是包含在负载长度里的。也就是说,负载长度 = 4字节(源+目标) + N字节(UDS数据)。这个细节我见过不少人算错。

2.4 小结与个人经验

这一节内容不少,我帮你捋一下重点:

  • DoIP在OSI模型中跨会话层和表示层,底层依赖TCP/UDP
  • DoIP实体是「能独立处理DoIP协议的节点」,网关是「连接不同总线的翻译官」
  • 通用头部8字节,固定格式,负载类型决定了报文用途
  • 诊断消息(0x8001)是实际传输UDS数据的载体

最后说句掏心窝的话:DoIP协议栈看着条条框框很多,但只要你亲手抓一次包、拆一次报文,立马就通了。我建议你找个开源DoIP实现(比如Python的python-doenet),在本地搭个虚拟环境跑一跑。纸上得来终觉浅,绝知此事要躬行。

下一节咱们聊「车辆发现机制」,也就是诊断仪怎么在网络上找到车。那个过程很有意思,有点像「网络版的捉迷藏」。