Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.cDcm_Dsp.c里反复见到。但现在你已经清楚了它最核心的底层编码规则和对诊断仪/ECU的关系。


本篇小结

  1. UDS的SID编码遵循(SID & 0x40) == 0是请求、(SID & 0x40) != 0是正响应的固定规则——不需要查表。
  2. SubFunction字节是UDS里密度最高的字节——bit7控制是否发正响应(suppressPosRsp),bit6-0是真实的子功能码。
  3. 正响应SID = 请求SID + 0x40。正响应的后续字节回声子功能码或附加数据取决于该SID是否支持子功能。
  4. 负响应固定为三字节{0x7F, RequestSID, NRC}——独立的消息类别,不是附加在正响应里的附属字段。
  5. suppressPosRsp是UDS对“不必要回复“的优化——减少总线负载的同时保留故障感知能力。

【下集预告】:语法学完了,可以说话了。第一句话:‘挂哪个科?’——诊断会话控制(0x10)。为什么工程师决定要把ECU的状态空间分成default/programming/extended/safety四类?为什么P2定时器用两个值(正常和增强)控制诊断仪的等待时间?为什么S3超时你必须不停地发0x3E保活?打开0x10——从挂了很久的牌开始。