2.2 UDS报文格式——请求与响应的语法树
ISO 14229里的最简单东西,反而没人讲
你在网上搜“UDS报文格式“,大概率会被带进一个全是十六进制dump的页面——左边的SID高亮、右边的子功能解码。那些页面会告诉你0x10是诊断会话控制、0x22是按ID读数据——但它们不告诉你为什么SID的最高位(bit6)决定一切,不告诉你SubFunction字节里的bit7是诊断响应流量的总开关——设为1时,ECU在成功时不回复,只在失败时回复,不告诉你哪条规则是无论什么ECU、什么传输层都永远不变的。
我们这一节不跳进任何一个具体SID。我们只讲SID和正/负响应编码最底层的那几条规则——把报文格式讲透了。后面的17节在讲解每个具体服务时都会反复用到这些规则,所以我不会在这里“简单带过“——你必须把正响应偏移(+0x40)、子功能码的suppressPosRsp位、负响应的三字节结构——当成你的肌肉记忆。
规则0:所有SID的编码铁律
ISO 14229-1:2013的表2规定了一个简单到几乎不可能错的规则,但它又是整个UDS编解码体系的基础——每一个SID工程师都应该刻在脑子里:
请求SID: bit6 = 0 (即: 0x00 ~ 0x3F 范围, 但仅 0x10~0x3E 分配了; 0x3F保留)
正响应SID: bit6 = 1 (即: 请求SID + 0x40 = 0x50~0x7E)
负响应SID: 固定为 0x7F
用C代码表达就是:
uint8_t request_sid; // 请求SID, 如 0x10
uint8_t positive_resp; // 正响应SID = request_sid | 0x40
uint8_t negative_resp; // 负响应SID 固定 0x7F
核心洞察:+0x40不只是算术偏移——它是一个单比特掩码(bit6=0→请求, bit6=1→响应)。这意味着你不需要查表找“0x22的响应是什么“——0x22 | 0x40 = 0x62。不需要查表找“0x62的请求SID是什么“——0x62 & ~0x40 = 0x22。你的ECU诊断栈在SID分发的第一行就可以完成“是请求还是响应“的判断。
规则1:报文第一字节永远是SID
UDS报文的第一个字节永远是SID。无论是请求还是响应(正响应或负响应),诊断仪和ECU通信的唯一入口就是报文的第一个字节。ECU收到一个CAN帧,取第一个字节——如果是请求(bit6=0),进入服务分发;如果是正响应(bit6=1),进入响应处理;如果是0x7F,进入负响应处理。没有例外,不需要第二个字节来辅助判断。
// 报文第一字节决定一切
switch (pdu[0] & 0xC0) { // 检查bit6和bit7
case 0x00 ... 0x3F: // bit6=0 → 请求
dispatch_service(pdu);
break;
case 0x40 ... 0x7E: // bit6=1, 非0x7F → 正响应
handle_positive_response(pdu);
break;
case 0x7F: // 精确匹配0x7F → 负响应
handle_negative_response(pdu);
break;
}
请求报文格式:SID + SubFunction? + Data
UDS的请求报文是变长的——字节1固定为SID,后续字节取决于该SID是否支持子功能:
A类:有子功能的请求(绝大多数UDS服务)
Byte 0 Byte 1 Byte 2 ... Byte N
┌──────────┬──────────────┬─────────────────────┐
│ SID │ SubFunction │ Data Parameter[] │
│ (1 byte) │ (1 byte) │ (0..N bytes) │
└──────────┴──────────────┴─────────────────────┘
SubFunction字节的结构是UDS设计里密度最高的一个字节:
Bit 7 Bit 6-0
┌──────┬─────────────────────┐
│suppr │ sub-function code │
│essPo │ (0x00 ~ 0x7F) │
│sRsp │ │
└──────┴─────────────────────┘
| 字段 | 含义 |
|---|---|
| bit7 (suppressPosRspMsgIndicationBit) | 0=需要正响应;1=抑制正响应(仅发负响应或无响应) |
| bit6-0 (sub-function) | 实际子功能码(0~127范围) |
核心洞察:SubFunction字节里的bit7做了一件非常巧妙的事——诊断仪可以在请求时直接告诉ECU“这个问题不需要回答“。 这在功能寻址时尤其重要——你向30个ECU同时RequestDownload进入编程模式,但只有实际执行下载的那个ECU需要回复正响应——其余29个ECU收到后知道自己不处理,沉默即可——从而节省极大的总线带宽和各自ECU的处理时间。
B类:无子功能的请求
Byte 0 Byte N
┌──────────┬─────────────────────┐
│ SID │ Data Parameter[] │
│ (1 byte) │ (N bytes) │
└──────────┴─────────────────────┘
哪些SID没有子功能?
- 0x22 ReadDataByIdentifier——后面直接跟字节串 DID
- 0x23 ReadMemoryByAddress
- 0x2E WriteDataByIdentifier
- 0x34/35/36/37 Upload/Download
- 0x3D WriteMemoryByAddress
- 0x2F InputOutputControlByIdentifier
正响应报文格式:SID+0x40 开头
正响应报文有两个关键设定:
第一:正响应SID = 请求SID + 0x40。 没有例外。
正响应报文的后续字节格式只有两种模式,取决于该SID是否支持子功能:
模式一(有子功能):SID+0x40 + 子功能回声 + 服务特定数据
模式二(无子功能):SID+0x40 + 服务特定数据
下面分别展开。
有子功能的正响应格式
Byte 0 Byte 1 Byte 2..N
┌──────────┬──────────────────┬────────────────────────────┐
│SID+0x40 │ echoback of │ service-specific data │
│ 0x50 │ sub-function=0x01│ (e.g. timing parameters) │
└──────────┴──────────────────┴────────────────────────────┘
核心洞察:为什么正响应的第二个字节要回声子功能码?因为UDS从KWP2000继承了一套老习惯——诊断仪发出的Subfunction是什么,ECU要把这个值一字不差回传——诊断仪不必记着“我上次对ECU发的是什么Subfunction“,ECU帮我记着。这就是回声保护——也是一种确认。“你不是要求扩展会话吗——是,我确认,你要的是扩展会话。这是P2和P2*的定时参数给你作参考。
无子功能的正响应格式
Byte 0 Byte 1..N
┌──────────┬────────────────────────────┐
│SID+0x40 │ DID (16-bit) + dataRecord │
│ 0x62 │ │
└──────────┴────────────────────────────┘
对于无子功能的SID——以0x22和0x62为例——正响应的格式是天然的:诊断仪发出了0x22 + DID;ECU返回0x62 + DID + dataRecord[]——请求和正响应共享前2~3个字节,后边是ECU回传的数据。
负响应报文格式:0x7F + SID + NRC
负响应是UDS里唯一有自己独立SID的一种“服务“。它不是SID——它是NR_SI(Negative Response Service Identifier)。
Byte 0 Byte 1 Byte 2
┌──────────┬──────────────┬──────────────────┐
│ 0x7F │ Request SID │ NRC │
│ Negative │ (回显) │ (为什么不行) │
└──────────┴──────────────┴──────────────────┘
三个字节,三种信息:
| 字节 | 含义 | 用处 |
|---|---|---|
| 0x7F | 负响应标记 | 让诊断仪知道这一次不行。不是正响应流量里面的一个附加字段——是独立的SID、独立的消息类别 |
| Request SID(回显) | “我不支持哪个请求” | 诊断仪同时发了多条请求(虽然实践中少见),但标准规定必须回显原SID——诊断仪可以根据这个区分不同的请求 |
| NRC | 为什么不行 | 全体NRC代码参见第18节;当前语境下最重要的几个NRC是 0x11(serviceNotSupported)、0x12(subFunctionNotSupported)、0x22(conditionsNotCorrect)、0x33(securityAccessDenied)、0x78(requestCorrectlyReceivedResponsePending) |
注意0x7F的双重身份:0x7F既是负响应的SID(报文第一字节),也是NRC码之一(例如NRC 0x7F = serviceNotSupportedInActiveSession)。两者不冲突——报文第一字节的0x7F表示“这是负响应“,报文第三字节的0x7F表示“具体的拒绝原因“。它们在报文中各司其职:
0x7F | 0x10 | 0x7F= “ECU说不支持0x10,原因是当前会话不支持此服务”。
suppressPosRsp的工作机制
回顾SubFunction的bit7——它不是装饰性的设置。在UDS通信中有许多场景是诊断仪不需要从所有目的ECU收到正响应——只从目标ECU取得并避开不必要的应答。
场景1:物理寻址 + suppressPosRsp=1
ECU收到后,如果正常运行,它不发送任何正响应,等于“零响应流量“——直接处理完毕。但如果失败了(条件不对、地址溢出、DID不存在)——它仍然向诊断仪发送负响应。
场景2:功能寻址 + suppressPosRsp=1
诊断仪的目的可能不是从每个ECU收到正响应——它只是通知所有ECU“你们都执行这个操作(比如停止通信)“。任何负响应在功能寻址时也被抑制——以避免2台ECU同时发NRC 0x11淹没总线。
核心洞察:suppressPosRsp的工作逻辑体现了UDS的设计哲学——不是每一个确认都需要回复。 只有“条件不满足需要让我知道“才回复——这条路最少有响应,但保留了故障感知能力。这和TCP ACK、HTTP 1xx/2xx响应是本质不同的——UDS不追求完全的带内通信确认,它更多是诊断仪和ECU之间信任度的协商。
一个完整交互的时间线
把请求、等待、正/负响应放在时间轴上:
诊断仪 ECU
│ 0x10 0x03 │
├────────────────────────>│ ← 诊断仪请求:进入扩展诊断会话
│ │
│ │ 检查SID 0x10是否支持
│ │ 检查子功能0x03是否存在
│ │ 检查会话切换是否允许
│ │ 更新会话=0x03
│ │ 安全等级复位→LOCKED
│ │ 构建正响应
│ │
│ 0x50 0x03 0x0032 0x00FA │
│<────────────────────────┤ ← ECU正响应:已进入扩展会话
│ │
│ 0x27 0x01 │
├────────────────────────>│ ← 请求种子(安全等级1)
│ │
│ 0x67 0x01 0xA3D45F12 │
│<────────────────────────┤ ← 返回种子(4字节)
│ │
│ 0x27 0x02 0xKEYDATA │
├────────────────────────>│ ← 发送密钥
│ │
│ 0x67 0x02 │
│<────────────────────────┤ ← 密钥正确→已解锁安全等级1
│ │
│ 0x19 0x01 0x09 │
├────────────────────────>│ ← 报告conformed+testFailed DTC数量
│ │
│ 0x59 0x01 0x09 0x0003 │
│<────────────────────────┤ ← 3个DTC
│ │
│ 0x3E 0x00 │ (不包含suppressPosRsp)
├────────────────────────>│ ← 保活. 重置S3定时器(任何合法的诊断请求都能重置S3,0x3E是专门为此设计的轻量请求)
│ 0x7E 0x00 │
│<────────────────────────┤ ← 确认医生还在
│ │
... seconds pass ...
│ │
│ │ S3定时器 → 0
│ │ 自动切回默认会话
│ │ 安全等级→LOCKED
核心洞察:注意顺序——每一步:ECU先把肯定响应送到诊断仪才执行副作用。ECU复位是先发回应再写复位——“我的计划是这个收据确认了我才关机。” S3超时是到时间ECU自己主动变回默认会话——不给诊断仪任何一个机会再同ECU通信。“你在哪个会话来我不关心——超时了就回默认。
最小请求检查:一个ECU收到’0x10 0x03’后做了什么
把这个编码规则翻译成ECU诊断栈代码——用伪C:
uint8_t pdu[2] = {0x10, 0x03}; // 从CAN帧收到的数据
uint8_t request_sid = pdu[0]; // 0x10
if ((request_sid & 0x40) != 0) {
// bit6=1 → 这是正响应!ECU不能收到正响应——非法
return NEGATIVE_RESPONSE_SID_INVALID;
}
uint8_t subfunc = pdu[1]; // 0x03
uint8_t suppress = (subfunc >> 7) & 1; // bit7 → suppressPosRsp
uint8_t subfunc_code = subfunc & 0x7F; // bit6-0 → 0x03 (extended)
// 在SID表中查找 0x10
SIDTableEntry *entry = lookup_service(request_sid);
if (entry == NULL) {
return send_negative_response(request_sid, NRC_SERVICE_NOT_SUPPORTED);
}
// 校验会话:0x10在默认会话中必须可用(无条件检查)
if (current_session != entry->allowed_sessions) {
return send_negative_response(request_sid, NRC_SERVICE_NOT_SUPPORTED_IN_ACTIVE_SESSION);
}
// 查找子功能 0x03
SubFuncEntry *sub = lookup_subfunction(entry, subfunc_code);
if (sub == NULL) {
return send_negative_response(request_sid, NRC_SUBFUNCTION_NOT_SUPPORTED);
}
// 执行服务
execute_session_change(subfunc_code);
// 构建正响应
if (!suppress) {
uint8_t response[] = {0x50, subfunc_code, p2_high, p2_low, p2star_high, p2star_low};
send_positive_response(response, sizeof(response));
}
// suppress=1 → 不发送正响应
这个过程——查找SID、查找子功能、校验会话、执行服务、构建响应——你在后面的源码分析章节(Chapter 4)中会在Arctic Core的Dcm_Dsd.c和Dcm_Dsp.c里反复见到。但现在你已经清楚了它最核心的底层编码规则和对诊断仪/ECU的关系。
本篇小结
- UDS的SID编码遵循
(SID & 0x40) == 0是请求、(SID & 0x40) != 0是正响应的固定规则——不需要查表。 - SubFunction字节是UDS里密度最高的字节——bit7控制是否发正响应(suppressPosRsp),bit6-0是真实的子功能码。
- 正响应SID = 请求SID + 0x40。正响应的后续字节回声子功能码或附加数据取决于该SID是否支持子功能。
- 负响应固定为三字节
{0x7F, RequestSID, NRC}——独立的消息类别,不是附加在正响应里的附属字段。 - suppressPosRsp是UDS对“不必要回复“的优化——减少总线负载的同时保留故障感知能力。
【下集预告】:语法学完了,可以说话了。第一句话:‘挂哪个科?’——诊断会话控制(0x10)。为什么工程师决定要把ECU的状态空间分成default/programming/extended/safety四类?为什么P2定时器用两个值(正常和增强)控制诊断仪的等待时间?为什么S3超时你必须不停地发0x3E保活?打开0x10——从挂了很久的牌开始。