3. UDS协议核心概念
UDS(Unified Diagnostic Services,统一诊断服务)是ISO 14229定义的一套应用层诊断协议。在嵌入式开发中,无论你面对的是CAN、CAN FD还是DoIP,UDS报文的“骨架”始终由四个核心概念构成:服务标识符SID、子功能Sub-function、数据参数Data Parameter,以及肯定响应与否定响应格式。理解这四者,是读懂诊断请求、构造诊断响应、排查诊断失败的第一步。
一、SID:诊断服务的“身份证”
SID(Service Identifier)是UDS报文的第一个字节,用来告诉ECU“你要我做什么”。它决定了后续字节如何被解析。SID通常分为两类:
- 请求SID:客户端(诊断仪)发给ECU,例如0x10、0x22、0x2E、0x31。
- 响应SID:ECU回复给客户端。肯定响应的SID = 请求SID + 0x40;否定响应则固定使用0x7F。
例如,请求0x22(ReadDataByIdentifier)的肯定响应SID是0x62;请求0x2E(WriteDataByIdentifier)的肯定响应SID是0x6E。这个“+0x40”规则是UDS报文解析中最基础的规律,也是嵌入式代码中判断响应类型的第一道分支。
二、子功能:同一服务下的“动作开关”
很多UDS服务在SID之后紧跟一个字节,称为子功能(Sub-function)。它用于在同一服务下选择不同操作。例如0x10(DiagnosticSessionControl)的子功能有0x01默认会话、0x02编程会话、0x03扩展会话;0x31(RoutineControl)的子功能有0x01启动例程、0x02停止例程、0x03请求例程结果。
子功能字节的最高位(bit7)是抑制肯定响应位(suppressPosRspMsgIndicationBit)。当该位为1时,ECU执行成功后不发送肯定响应,只发否定响应。这在刷写或高频轮询场景中能显著减少总线负载。嵌入式开发中,解析子功能时必须先屏蔽bit7,再判断实际子功能值。
关键规则:子功能字节 = 实际子功能值 | 抑制位。例如0x10服务的扩展会话请求,若抑制肯定响应,则字节为0x83(0x03 | 0x80)。
三、数据参数:SID与子功能之后的“载荷”
数据参数是UDS报文中承载具体信息的部分,其格式由SID和子功能共同决定。常见的数据参数包括:
- DID(Data Identifier):2字节,用于0x22/0x2E/0x2F等服务,标识要读写的具体数据对象。
- RoutineIdentifier:2字节,用于0x31服务,标识要执行的例程。
- 数据记录:读写操作中实际的数据内容,长度由DID定义决定。
- 安全等级种子/密钥:用于0x27服务,种子和密钥长度由ECU安全算法决定。
在嵌入式代码中,数据参数通常以字节数组形式接收,需要根据SID进行长度校验和边界检查。例如0x22请求必须至少包含2字节DID,否则应返回否定响应0x13(incorrectMessageLengthOrInvalidFormat)。
四、肯定响应与否定响应格式
UDS响应格式是诊断开发中必须严格遵循的“语法”。肯定响应和否定响应结构不同,处理逻辑也不同。
| 响应类型 | 首字节 | 后续结构 | 示例 |
|---|---|---|---|
| 肯定响应 | SID + 0x40 | 子功能(若请求含子功能)+ 数据参数 | 请求22 F1 90 → 响应62 F1 90 + 数据 |
| 否定响应 | 0x7F | 请求SID + NRC(否定响应码) | 请求22 F1 90 → 响应7F 22 31 |
否定响应中的NRC(Negative Response Code)是排障的核心线索。常见NRC包括:
- 0x11:服务不支持(ServiceNotSupported)
- 0x12:子功能不支持(SubFunctionNotSupported)
- 0x13:报文长度或格式错误(IncorrectMessageLengthOrInvalidFormat)
- 0x22:条件不满足(ConditionsNotCorrect)
- 0x31:请求超出范围(RequestOutOfRange)
- 0x33:安全访问被拒绝(SecurityAccessDenied)
- 0x78:响应挂起(RequestCorrectlyReceived-ResponsePending)
0x78不是错误:它表示ECU已收到请求,但需要更多时间处理,后续会再发一个最终响应。嵌入式开发中,收到0x78后应继续等待,而不是立即判定失败。
五、完整报文解析案例
假设诊断仪请求读取DID 0xF190(VIN码),ECU支持该DID且当前会话允许读取。报文交互如下:
请求:22 F1 90
22 → SID:ReadDataByIdentifier
F1 90 → DID:VIN码
肯定响应:62 F1 90 57 50 30 5A 5A 5A ...
62 → 0x22 + 0x40,肯定响应
F1 90 → 回显DID
57 50 ... → VIN数据(ASCII)
若DID不支持:
否定响应:7F 22 31
7F → 否定响应固定SID
22 → 原请求SID
31 → NRC:RequestOutOfRange
在嵌入式代码中,解析这段报文通常按以下流程:先判断首字节是否为0x7F,若是则提取NRC并进入错误处理;否则判断首字节是否等于请求SID+0x40,再校验DID回显是否一致,最后提取数据。这个顺序能避免把否定响应误判为肯定响应。
六、常见误区与复盘
- 误区一:忽略子功能抑制位。直接把0x83当作子功能0x83处理,导致分支判断错误。正确做法是先与0x7F做按位与。
- 误区二:把0x78当失败。0x78是“响应挂起”,需要继续等待最终响应,否则会误报诊断超时。
- 误区三:肯定响应不校验DID回显。某些ECU可能返回错误DID,若不校验会导致数据错位。
- 误区四:否定响应只记录不分类。NRC应分类处理:0x11/0x12属于配置问题,0x13属于报文构造问题,0x33属于安全访问顺序问题,0x31属于参数范围问题。
开发建议:在ECU诊断栈中,建议为每个SID建立独立的请求解析函数和响应构造函数,并统一维护NRC返回表。这样既能保证格式一致,也便于后续增加新服务时快速扩展。
掌握SID、子功能、数据参数以及两类响应格式,就相当于掌握了UDS诊断的“字母表”。后续无论是实现0x27安全访问、0x31例程控制,还是0x34/0x36/0x37刷写序列,都只是在这套基础语法上组合出更长的“句子”。在嵌入式开发中,建议把SID解析、子功能屏蔽、NRC返回封装成独立模块,让诊断栈的每一层都只关心自己的职责,这样代码才经得起30个章节所覆盖的复杂场景考验。