DoIP协议基础:从物理层到诊断通信

DoIP,全称是Diagnostic over Internet Protocol。说白了,就是把传统的车载诊断协议(比如UDS on CAN)搬到以太网上跑。为什么要这么干?因为现在的车,电子系统越来越复杂,ADAS、自动驾驶、OTA升级,这些功能对带宽和实时性的要求,CAN总线已经扛不住了。

我记得2018年做第一个DoIP项目时,客户要求通过以太网刷写ECU固件,当时我们团队还在纠结要不要保留CAN诊断作为备份。后来发现,DoIP的传输速率是CAN的几百倍,刷一个3GB的ADAS地图包,CAN要跑几个小时,DoIP十几分钟就搞定了。从那以后,我再也没在量产项目里用过CAN刷写。

DoIP协议栈架构

DoIP的协议栈,从下往上分四层。我习惯把它想象成一个快递系统:物理层是公路,传输层是卡车,诊断层是包裹,应用层是收件人。

层级 协议/标准 核心功能 我踩过的坑
应用层 UDS (ISO 14229-5) 诊断服务封装(读取DTC、刷写、例程控制) UDS over DoIP的时序要求比CAN更严格
诊断层 DoIP (ISO 13400-2) 路由激活、车辆发现、诊断消息封装 逻辑地址分配不当会导致路由失败
传输层 TCP/UDP (IPv4/IPv6) 可靠传输(TCP)与发现/心跳(UDP) TCP窗口大小没调好,刷写速度上不去
物理层 100BASE-T1 / 1000BASE-T1 单对差分线,满足车载EMC要求 线束屏蔽层接地不良,丢包率飙升

关键点:DoIP不是简单地把CAN诊断帧塞进以太网包里。它有自己的消息头(DoIP Header),包含协议版本、负载类型、负载长度等信息。你想想看,如果直接把CAN的11位ID映射过来,那以太网的带宽优势就全浪费了。

物理层与传输层

物理层:100BASE-T1 vs 1000BASE-T1

车载以太网的物理层和普通以太网不一样。普通家用网线是四对差分线,车载以太网只用一对。为什么?因为车里的线束每多一根,成本就多一分,重量也多一分。100BASE-T1跑100Mbps,1000BASE-T1跑1Gbps。

我曾经在一个项目中,测试DoIP刷写时发现丢包率高达5%。排查了三天,最后发现是连接器的屏蔽层没有焊接到PCB的地平面。嗯,这里要注意:车载以太网的物理层对EMC非常敏感,尤其是1000BASE-T1,信号频率高,屏蔽处理不好就会出问题。

避坑指南:我曾经在实验室用普通CAT6网线连接DoIP测试台架,结果发现通信不稳定。后来查了标准才知道,车载以太网要求使用专用的屏蔽双绞线(STP),而且终端电阻匹配也有讲究。千万别图省事用普通网线。

传输层:TCP与UDP的分工

DoIP在传输层用了两种协议,分工很明确:

  • UDP (端口13400):用于车辆发现、车辆声明、心跳报文。这些消息不需要可靠传输,丢了就丢了,下次再发。
  • TCP (端口13400):用于诊断消息、路由激活、固件刷写。这些数据必须可靠,一个bit都不能错。

我见过不少新手工程师,在实现DoIP客户端时,把诊断请求也发到了UDP上。结果就是,诊断响应偶尔会丢,测试用例跑不过。记住:诊断消息必须走TCP,这是ISO 13400-2的硬性规定。

DoIP诊断通信流程

一个完整的DoIP诊断会话,从车辆上电到诊断结束,大致分五步。我画了一张流程图,方便你理解。

DoIP诊断通信完整流程 1. 车辆上电 2. 车辆发现 (UDP广播) 3. TCP连接建立 4. 路由激活 5. 诊断通信 (UDS over DoIP) 说明:路由激活成功后,诊断仪才能发送UDS请求 每个步骤都有超时机制,超时未响应则断开连接

步骤详解

  1. 车辆上电:ECU启动后,DoIP模块初始化,准备接收诊断请求。
  2. 车辆发现:诊断仪发送UDP广播消息(0x0004),车辆回复车辆声明(0x0005)。这一步用来发现网络上有哪些DoIP节点。
  3. TCP连接建立:诊断仪与车辆建立TCP连接,三次握手完成后,双方进入通信状态。
  4. 路由激活:诊断仪发送路由激活请求(0x0005),车辆验证后回复激活确认。这一步是为了绑定诊断仪的逻辑地址。
  5. 诊断通信:路由激活成功后,诊断仪可以发送UDS请求(0x8001),车辆回复UDS响应。

实战技巧:我在调试DoIP通信时,习惯先用Wireshark抓包,过滤条件设为"udp.port==13400 or tcp.port==13400"。如果看不到车辆声明报文,先检查物理层连接,再检查UDP广播是否被防火墙拦截。有一次,我发现车辆声明报文发出来了,但诊断仪收不到,最后定位到是交换机没有开启IGMP Snooping。

DoIP消息类型速查

ISO 13400-2定义了多种消息类型,我整理了一张速查表,开发时对照着用很方便。

负载类型 (Payload Type) 值 (十六进制) 方向 说明
车辆识别请求 0x0001 诊断仪 → 车辆 获取VIN、EID、GID
车辆识别响应 0x0002 车辆 → 诊断仪 返回车辆信息
路由激活请求 0x0005 诊断仪 → 车辆 激活诊断路由
路由激活响应 0x0006 车辆 → 诊断仪 返回激活结果
诊断消息 0x8001 双向 封装UDS请求/响应
诊断消息确认 0x8002 车辆 → 诊断仪 确认收到诊断消息
Alive Check请求 0x0007 双向 心跳检测
Alive Check响应 0x0008 双向 心跳回复

注意:诊断消息(0x8001)和诊断消息确认(0x8002)是成对出现的。车辆收到诊断消息后,必须先回复确认,再处理UDS请求。这个机制是为了防止TCP粘包导致UDS解析错误。我见过有些实现把确认和响应合并了,结果导致诊断仪超时。

DoIP报文结构

一个完整的DoIP报文,由三部分组成:

+----------------+----------------+------------------+------------------+
| DoIP Header    | Payload Type   | Payload Length   | Payload          |
| (8 bytes)      | (2 bytes)      | (4 bytes)        | (N bytes)        |
+----------------+----------------+------------------+------------------+
| 协议版本 1B    | 0x8001         | 0x00000008       | UDS请求数据      |
| 反向版本 1B    |                |                  |                  |
| 负载类型 2B    |                |                  |                  |
| 负载长度 4B    |                |                  |                  |
+----------------+----------------+------------------+------------------+

举个例子,发送一个读取VIN的UDS请求(0x22 F1 90),DoIP报文长这样:

02 FD 80 01 00 00 00 05 22 F1 90
|    |    |    |    |    |    |
|    |    |    |    |    |    +-- UDS数据 (3 bytes)
|    |    |    |    |    +-- 负载长度 (4 bytes, 值为5)
|    |    |    |    +-- 负载类型 (2 bytes, 0x8001)
|    |    |    +-- 反向协议版本 (1 byte, 0xFD)
|    |    +-- 协议版本 (1 byte, 0x02)
+-- DoIP Header (8 bytes)

避坑指南:我曾经在实现DoIP客户端时,把负载长度写成了UDS数据的长度,忘了加上UDS服务ID的长度。结果车辆解析时发现长度不匹配,直接回复NRC 0x13(消息长度错误)。记住:负载长度是从Payload Type之后开始算的,包括UDS数据的所有字节。

小结

DoIP协议的基础,说白了就是三件事:物理层怎么连,传输层怎么传,诊断层怎么封装。你只要把ISO 13400-2的报文结构吃透,把TCP/UDP的分工搞清楚,剩下的就是UDS over DoIP的适配工作了。

我个人建议,刚开始接触DoIP时,先不要急着写代码。拿一个DoIP测试工具(比如Vector的CANoe或Peak的DoIP工具),抓一遍完整的通信流程,看看每个步骤的报文长什么样。等你把车辆发现、路由激活、诊断通信这三个阶段看熟了,再动手实现,会少走很多弯路。