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诊断会话,从车辆上电到诊断结束,大致分五步。我画了一张流程图,方便你理解。
步骤详解
- 车辆上电:ECU启动后,DoIP模块初始化,准备接收诊断请求。
- 车辆发现:诊断仪发送UDP广播消息(0x0004),车辆回复车辆声明(0x0005)。这一步用来发现网络上有哪些DoIP节点。
- TCP连接建立:诊断仪与车辆建立TCP连接,三次握手完成后,双方进入通信状态。
- 路由激活:诊断仪发送路由激活请求(0x0005),车辆验证后回复激活确认。这一步是为了绑定诊断仪的逻辑地址。
- 诊断通信:路由激活成功后,诊断仪可以发送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工具),抓一遍完整的通信流程,看看每个步骤的报文长什么样。等你把车辆发现、路由激活、诊断通信这三个阶段看熟了,再动手实现,会少走很多弯路。