ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

UDS诊断协议核心机制:会话状态、安全访问与负响应排查

UDS诊断协议核心机制:会话状态、安全访问与负响应排查

简介:《汽车UDS诊断协议学习笔记PDF版》面向汽车电子初学者、诊断测试工程师及需要系统梳理ISO 14229的读者,帮助建立UDS服务清单与请求响应逻辑。全包仅含1个PDF文件,约10.49MB,便于在电脑或平板上直接查阅。内容按诊断和通信管理、数据传输、已存储数据传输、输入输出控制、例程控制、上传下载六大功能单元展开,覆盖0x10会话控制、0x11 ECU重置、0x27安全访问、0x19读取DTC、0x22/0x2E读写数据、0x31例程控制及0x34~0x37上传下载等常用服务,并说明诊断仪与ECU之间“问—答”模式、正负响应和NRC的基本含义。已有1932人学习,适合作为服务速查、协议入门和项目联调前的知识铺垫。

1. 从一条 7F 说起:UDS 难的不是码表,是隐式状态

把 UDS(Unified Diagnostic Services)当成一张 ISO 14229 的服务码表来背,大概是理解这套协议最常见的翻车起点。从 0x10 到 0x87,二十多个 SID 谁都能抄下来,可一旦真车上的请求发出去回了一条7F 22 7F,码表救不了你——因为 UDS 的每一次问答改变的不是数据本身,而是 ECU 内部的一份隐式状态:当前处在哪个会话、安全等级解到第几级、DTC 记录开关是开还是关、下载块计数器走到第几块。这些状态诊断仪看不见,却直接决定下一条请求会不会被受理。

所以真正值得搞懂的问题不是“0x22 是读数据”,而是“为什么同一条 0x22,在默认会话下被拒、在扩展会话下能过”。这份笔记把二十多个服务按功能单元拆开逐条讲,价值也在这儿:它给的不只是服务清单,而是一张能反推 ECU 内部状态的地图。

2. 会话与状态机:0x10 为什么是所有诊断命令的基石

2.1 DefaultSession 的门槛到底设在哪儿

0x10 的请求只有两个字节:10 xx,xx 是 diagnosticSessionType,取值范围 00 到 FF。协议自身定义了一批通用值——默认会话、编程会话、扩展会话、安全系统会话——剩下的码位留给主机厂自定义,所以不同项目里10 03未必都是扩展会话,这一点在跨项目复用脚本时非常容易踩空。

关键在于:默认会话是 ECU 跟着整车一起运行的常态,此时它允许你读,但不允许你写、不允许你刷、不允许你接管执行器。像 0x27 安全访问、0x2E 写 DID、0x31 例程控制、0x34 到 0x37 的下载链,通常都要求处在非默认会话下。这不是协议在为难人,而是一种失效安全的设计:只要诊断仪拔掉或者超时,ECU 会自动退回默认会话,那些高危服务就自动关门了。会话在这里承担的其实是“授权时效”的角色,比单纯的模式切换含义更重。

2.2 正响应 0x50 与负响应 7F 的编码规则

UDS 的响应编码有一条贯穿全篇的规则:正响应的第一个字节等于请求 SID 加 0x40,所以 0x10 的正响应是 0x50。紧随其后的是子功能回显,你发10 03,它就回50 03;第三个字段 sessionParameterRecord 是服务器上报的会话相关参数,最常见的实现思路是把它拆成 P2 和 P2* 两个时序参数,告诉诊断仪“我这条响应最多多久回来、收到 0x78 之后最多再等多久”。

负响应的格式则是固定的三字节:7F+ 请求 SID + NRC。第一个字节恒为 0x7F,第二个字节回显你刚才请求的是哪个服务,第三个字节才是真正的错误原因。这种设计的巧妙之处在于,诊断仪不需要理解任何服务的内部细节,只看第一个字节就能判断成败,第二个字节定位到具体服务,第三个字节查表即可。代价是 NRC 的表达能力有限,同一个 0x22 可能因为七八种完全不同的原因失败,后面排查那一章会专门拆这个。

# 0x10 请求构造与响应解析:把编码规则固化成函数 def build_session_request(session_type: int, suppress_pos_rsp: bool = False) -> bytes: # bit7 是抑制正响应位,置 1 后 ECU 成功时不回 0x50,失败仍然回 7F sub = session_type | (0x80 if suppress_pos_rsp else 0x00) return bytes([0x10, sub]) def parse_session_response(data: bytes) -> dict: if data[0] == 0x7F: # 负响应:7F + 请求SID + NRC return {"ok": False, "req_sid": data[1], "nrc": data[2]} assert data[0] == 0x10 + 0x40, "正响应 SID 必须等于请求 SID + 0x40" return { "ok": True, "session": data[1] & 0x7F, # 子功能回显,去掉抑制位 "p2_ms": int.from_bytes(data[2:4], "big"), # 常见实现:P2 server max "p2star_10ms": int.from_bytes(data[4:6], "big"), # 常见实现:P2* 以 10ms 为单位 }

这段代码里最容易被忽略的是抑制位。10 83和10 03在语义上是同一个请求,只差“成功时要不要回话”。在总线负载紧张的刷写流程里,抑制正响应能省下大量带宽,但它同时意味着你失去了“请求已送达”的确认信号,只能靠后续请求的响应来间接判断。我一般不建议在会话切换这种关键节点上用抑制位,省下的那几毫秒不值得。

2.3 0x11 的副作用:重置把会话打回原点

0x11 ECUReset 看起来很简单,实际上是个典型的“有副作用”服务。请求格式是11 xx,xx 是 resetType,常见的有硬复位、钥匙开关复位、软复位,以及一个特殊值 0x04——快速下电使能。只有请求 0x04 时,正响应里才会多出一个 powerDownTime 参数,取值范围 00 到 FE 有效,FF 表示时间不可用或出错。

协议在这里有一条硬性约束:服务器在执行重置之前,必须先把肯定响应发出去。因为一旦真的复位,通信链路就断了,响应根本没机会发出。这条约束对自动化测试的影响非常大——脚本里收到 0x51 的正响应,不代表 ECU 已经重启完成,只代表它答应要重启了。

更要命的是后半句:重置成功后,服务器应激活 DefaultSession。这意味着你前面辛苦建立起来的扩展会话、解掉的安全等级,在 0x11 之后全部清零。血泪经验是,很多刷写脚本在例程里插了一次复位,后面所有依赖扩展会话的请求集体报 7F,日志上看像是“突然不支持了”,实际只是会话被打回原点。复位之后必须重新走一遍 0x10 切会话、0x27 解锁的完整流程,没有捷径。

2.4 0x3E 与 0x28、0x85:维持状态的三类服务

有一类服务不产生任何业务数据,但它们决定了别的服务能不能工作。0x3E TesterPresent 是最典型的一个:ECU 内部有一个 S3 计时器,常见默认值是 5 秒,只要这段时间内没收到任何诊断请求,它就自动退回默认会话。诊断仪靠周期性发3E 00来续命,工程上常写成3E 80,即打开抑制正响应位,让 ECU 别回话,省带宽。

0x28 CommunicationControl 控制的是非诊断报文的收发,刷写前把应用报文关掉能显著降低总线负载、避免刷写过程中被无关报文打断。0x85 ControlDTCSetting 控制的是故障码记录开关,刷写和某些标定例程执行期间必须关掉,否则 ECU 会把刷写过程中的异常电压、通信中断统统记成 DTC,刷完之后整车读出一堆莫名其妙的故障码——这是新手最常翻的车之一,明明刷成功了,诊断仪一读码却像出了大事故。

至于 0x83 访问时序参数、0x87 链路控制、0x86 事件响应,属于通信参数层的服务,主机厂用不用全看项目,实际工程里碰到的频率远低于前面几个。但它们的定位值得知道:0x83 改的是 P2/P2* 这类时序参数,0x86 让 ECU 在指定事件发生时主动上报,0x87 管的是链路层切换。理解了这一层,你再看那些“为什么这个项目里 0x83 一发出 ECU 就不理人”的问题,就能反应过来是没实现,而不是发错了。

3. 权限层:0x27 的 seed-key 与 0x29 的证书信任模型差在哪

3.1 seed 与 key 的奇偶分工:0x27 报文的字节布局

0x27 SecurityAccess 解决的是“凭什么让你读写受限区域”的问题。往 ECU 里下载例程或数据、从特定内存位置读数据,都可能损坏电子设备或影响排放、安全指标,所以协议要求先证明身份。它的机制是经典的挑战应答:ECU 出一个随机数(seed),诊断仪用约定的算法算出应答(key)送回去,ECU 自己再算一遍比对。

字节布局上有一个必须记牢的规则:子功能为奇数时是请求 seed,为偶数时是发送 key,且两者成对出现。27 01对应的 key 请求是27 02,不是27 03。协议里的表述是“seed 请求对应的 key 子功能,是相邻且大于 seed 子功能的最小整数”,落到实际就是 01→02、03→04、05→06 这样一一配对。多个安全等级就是靠这个序号区分的,01/02 是一级,03/04 是二级,依此类推。

seed 请求的请求报文里还有个 securityAccessDataRecord 字段,协议定义为可选项,实际项目里基本不赋值,空着发就行。响应格式两边不对称:seed 请求的正响应带 securitySeed,也就是 ECU 生成的那串随机数;而 key 请求的正响应只有 Service ID 和 securityAccessType 两个字段,不带任何数据。这个不对称很重要,写解析代码时如果把 key 响应当成也带数据来处理,就会越界读。

3.2 从 0x35 到 0x37:错误次数是怎么被阶梯放大的

0x27 支持的 NRC 里,0x35、0x36、0x37 这三个需要特别注意,因为它们是唯一一组“会随时间变化”的错误码。0x35 是密钥无效,0x36 是超过尝试次数,0x37 是要求的延时未到。

一个真实的项目里是这样表现的:第一次发错误的 key,上报 0x35;等待 5 秒,第二次发错误的 key,上报 0x36;再等 5 秒,第三次发错误的 key,上报 0x37,之后就锁死了,必须复位或者等更长时间才能重来。这三个码构成了一个递进的惩罚阶梯,具体的时间阈值和次数上限因项目而异,协议把决定权交给了主机厂。

这个机制对自动化测试的直接影响是:脚本不能在失败后立刻重试。很多测试框架的默认行为是断言失败就重跑一次用例,放到 0x27 上,第二次跑的时候 ECU 已经处在“延时未到”状态,报的错跟第一次完全不是一回事,日志看下来会非常迷惑。正确的做法是在脚本里显式处理这三个 NRC,遇到 0x36 或 0x37 就主动 sleep 足够长的时间,而不是急着发下一条。

3.3 把 seed 换成 key:脚本里最容易填错的三个参数

自动化实现安全解锁,核心就四步:发 seed 请求、从响应里抠出 seed、用 ECU 侧的算法库算出 key、把 key 填进请求发出去。听起来直白,但脚本层面的坑集中在参数上。下面这段 CAPL 精简版把四个步骤串起来了,注意看注释里标出的位置。

void SecLevel1_Unlock() { diagRequest ECU_A.RequestSeed_Request SeedReq_1; diagRequest ECU_A.SendKey_Send KeySend_1; const dword SEND_TIMEOUT = 2000; // 等待请求真正发上总线的上限 const dword RESPONSE_TIMEOUT = 1500; // 等待 ECU 首帧响应的上限 byte seedArray[8]; byte keyArray[8]; char variant[12]; char ipOption[2]; dword keyActualSize = 8; long status; diagSetTarget("ECU_A"); // 先绑定目标 ECU,后面取 variant 才有依据 ipOption[0] = 'A'; ipOption[1] = 0; diagSendRequest(SeedReq_1); // 27 01:奇数次子功能 = 请求 seed if (testWaitForDiagRequestSent(SeedReq_1, SEND_TIMEOUT) != 1) { testStepFail("000", "seed send failed"); return; } if (testWaitForDiagResponse(SeedReq_1, RESPONSE_TIMEOUT) != 1) { testStepFail("000", "no seed response"); return; } if (diagGetLastResponseCode(SeedReq_1) != -1) { testStepFail("000", "negative response"); return; } // 响应帧 byte0=0x67(0x27+0x40),byte1=子功能回显,seed 从 index 2 开始 for (int i = 0; i < elCount(seedArray); i++) seedArray[i] = DiagGetRespPrimitiveByte(SeedReq_1, i + 2); // 用 ECU 侧下发的算法库把 seed 换算成 key,返回 0 才算成功 status = diagGenerateKeyFromSeed(seedArray, elCount(seedArray), 1 /*安全等级*/, variant, ipOption, keyArray, elCount(keyArray), keyActualSize); if (status != 0) { testStepFail("000", "key generation failed"); return; } // 参数名必须与 CDD 中定义完全一致,写错不报错,但发出去的是空数据 diagSetParameterRaw(KeySend_1, "SecurityKey", keyArray, elCount(keyArray)); diagSendRequest(KeySend_1); // 27 02:偶数次子功能 = 发送 key }

三个容易填错的参数。第一个是取 seed 时的偏移量 i+2:因为响应帧的前两个字节被 SID 和子功能占掉了,真正的 seed 从第三个字节开始,少加这个 2 就会把67 01也当成密钥材料算进去,算出来的 key 必然错,而且错得毫无提示。第二个是 variant,它是诊断目标 ECU 的名字,必须和 diagSetTarget 设置的那个字符串一致,或者从工程配置里取;第三个是 ipOption,是 ECU 的代号,通常要用另一个函数去拿当前 ECU 的标识,硬编码一个字母在单 ECU 工程里能跑,多 ECU 工程里必翻车。

还有一个隐藏的坑:diagSetParameterRaw 里的参数名。这个名字必须和工程里 CDD 文件中的定义一字不差,写错了不会报任何错,脚本照常执行到“发送 key”这一步,然后 ECU 回一个 0x35,你会以为是算法错了,实际上是根本没把 key 填进去,发出去的是全零。这种静默失败最难查,因为所有返回值都正常。

3.4 0x29 的两套信任模型:证书交换与挑战应答

0x29 Authentication 是后来补进协议的一套更完整的身份体系,和 0x27 的定位有明显区别:0x27 证明的是“你知道某个秘密派生出来的答案”,0x29 证明的是“你是谁”。协议给了它两种安全概念,一条基于 PKI 证书交换,另一条基于带软件认证令牌的请求应答。

PKI 那条路要求客户端和服务器各自持有证书集和对应私钥。证书格式上,协议允许用符合 ISO 7816-8 的 CVC,或者符合 ISO/IEC 9594-8、RFC 5280、IEEE 1609.2 这一路的 X.509。单向认证里,客户端带着自己的证书和可选的 challengeClient 发起 verifyCertificateUnidirectional,服务器验证证书、生成 challengeServer,如果在请求里声明了要基于临时 Diffie-Hellman 建立会话密钥,还会额外生成一对临时公私钥一起回过去。客户端随后用私钥对 challengeServer 签名,构造出 POWN(所有权证明)通过 proofOfOwnership 发回,服务器用证书里的公钥验签,验证通过就按证书里挂的权限授予访问权。

双向认证多出来的那一步,是服务器也要证明自己:它用服务器证书的私钥对 challengeClient 签名,把自己的证书、challengeServer、POWN 一起回给客户端,客户端验完才继续。这个差异不是形式上的对称,而是安全边界的不同——单向认证里客户端无法确认对端是不是合法服务器,链路上如果有中间人冒充 ECU,客户端是察觉不到的。

3.5 会话密钥与 POWN:双向认证多出来的那一步

另一条路是挑战应答,不带证书,靠预共享的对称密钥或者成对的非对称密钥。流程从 requestChallengeForAuthentication 开始,客户端在这一步就要声明打算用哪种算法、要不要建立会话密钥。服务器回 challengeServer,同时告诉客户端需不需要补附加参数。

接下来的 POWN 计算分两种情况。用非对称加密时,客户端构造一段厂商自定义的令牌内容,里面塞进权限、身份、角色、服务器端挑战,可能还有客户端挑战和附加参数,然后用客户端私钥对这段内容签名,令牌加签名整体就是 POWN。用对称加密时简单得多,直接对服务器挑战做一次 HMAC、CMAC 或 GMAC,密钥是双方预先共享的那一把。双向认证里服务器同样要算一份 POWN 回给客户端,用的自然是服务器侧的私钥或同一把共享密钥。

会话密钥建立是整个 0x29 里最容易被忽略的部分。如果客户端在第一步就声明了要建会话密钥,服务器会在验证通过后创建或派生密钥、启用它,并把会话密钥信息随成功响应一起发下来;客户端从中提取或者自行派生,之后的安全诊断通信就走加密通道了,通常还配合 0x84 SecuredDataTransmission 使用。这条链路的意义在于,0x29 不只是“开个门”,它还能顺手把门后面的通道加密了,而 0x27 做不到这一点。

对比维度0x27 SecurityAccess0x29 Authentication
证明对象知道某个派生答案身份本身及权限角色
凭据形式seed-key 对,密钥在 ECU 内部证书链或预共享密钥
是否支持双向否,只验证诊断仪是,可选双向认证
能否协商会话密钥不能能,基于临时 DH 或派生
典型落地场景常规售后诊断、产线解锁远程诊断、OTA、安全通信

4. 数据层:从 0x22 读到 0x37 写,四条通道各自的分工

4.1 DID 寻址:0x22、0x2E、0x2C 共用一套标识空间

数据传输功能单元是整个 UDS 里用得最频繁的部分,核心是 DID(Data Identifier)这个概念。0x22 ReadDataByIdentifier 的请求是22加两字节 DID,正响应是62加同样的 DID 再加数据。DID 是一个两字节的标识空间,同一套编号被读、写、动态定义几个服务共用,所以你在规范里会看到某个 DID 既可读又可写,另一个 DID 只读,这本身就是一种权限声明。

0x2E WriteDataByIdentifier 是它的镜像,请求带 DID 和数据,正响应回6E加 DID,不带数据。这个不对称设计有实际意义:写入是否成功不需要回读数据来确认,ECU 回了 0x6E 就代表写进去了,诊断仪能省一次回读。

0x2C DynamicallyDefineDataIdentifier 是这套体系里最容易被低估的一个。它的思路是把多个 DID 或者内存片段打包成一个逻辑 DID,之后用 0x22 一次性读回来。动机很直接:整车总线上有几十个信号要实时监控,逐个 DID 请求会把总线塞满,动态定义一个复合 DID 能显著降低请求数量。代价是定义本身要发好几次请求,且定义是易失的,掉电或者 ECU 复位之后就没了,脚本里必须每次重新定义一遍。

至于 0x23 按地址读内存、0x3D 按地址写内存,属于绕过 DID 的底层通道,直接给地址和长度,配合 addressAndLengthFormatIdentifier 描述地址和长度的字节数。这类服务权限极高,一般只在产线和工程模式下开放,正常售后诊断根本用不到。0x24 ReadScalingDataByIdentifier 则是让 ECU 把某个 DID 的缩放信息(单位、偏移、比例因子)吐出来,诊断仪据此把原始值换算成物理值;0x2A 按周期标识符读数据让 ECU 主动周期上报,省掉反复请求的开销。

4.2 0x19 的四种读法与 0x14 的清除边界

已存储数据传输功能单元只有两个服务,但分量很重。0x19 ReadDTCInformation 不是“读一个故障码”这么简单,它有一整套子功能:按状态掩码读 DTC 数量、按状态掩码读 DTC 列表、读某个 DTC 的快照记录、读某个 DTC 的扩展数据。快照记录记的是故障发生那一刻的环境数据——车速、水温、电压——这是排查间歇性故障的唯一线索,比 DTC 本身有用得多。

配套的 0x14 ClearDiagnosticInformation 负责清除,请求里带 groupOfDTC,全 F 表示清全部。这里有个细节值得注意:清除不是“删记录”,而是把 DTC 的状态位重置,ECU 之后如果再次检测到同样的故障条件,同一个 DTC 会重新被置位,快照也会被刷新。所以调试阶段反复清码读码,读到的快照可能是你上一次清码之后新产生的,不是最初那次故障的现场,这一点在复盘时会误导人。

4.3 0x2F 与 0x31:把执行权短暂交出去

输入输出控制 0x2F 和例程控制 0x31 属于“让 ECU 干活”的服务。0x2F 按 DID 接管某个执行器,控制选项有归还控制权、恢复默认、冻结当前状态、短时调节四种。这是所有服务里风险最高的一类——接管了水泵、风扇、喷油嘴之后,整车状态就不在正常控制逻辑下了,所以它几乎必然要求非默认会话加安全解锁,且必须设置超时自动归还。

0x31 例程控制则是让 ECU 跑一段预定义的程序,三个子功能分别是启动、停止、查询结果。刷写流程里的擦除 Flash、检查编程预条件、刷完后的校验,都是靠 0x31 完成的。请求里除了 routineIdentifier,还有一段 routineControlOptionRecord,内容由厂商定义,常见用法是传校验和或者区块编号。它的正响应会带回例程的执行结果,很多时候这个结果比 NRC 更有信息量。

4.4 0x34、0x36、0x37:块序号为什么是刷写的第一道坎

上传下载功能单元是刷写的骨架。0x34 RequestDownload 发起下载,请求里带数据格式标识、地址和长度,响应74里最关键的是 maxNumberOfBlockLength,告诉诊断仪每块最多能塞多少字节。0x35 RequestUpload 是反方向,把 ECU 里的数据读出来。0x36 TransferData 负责搬数据,0x37 RequestTransferExit 收尾。

class TransferSession: """管理 0x36 的块序号:从 0x01 起,逐块自增,到 0xFF 后回绕到 0x00""" def __init__(self, max_block_len: int): self.max_block_len = max_block_len self.seq = 0x00 # 初始为 0,第一次 next_seq 之后变成 0x01 def next_seq(self) -> int: self.seq = (self.seq + 1) & 0xFF # 0xFF + 1 自然回绕到 0x00 return self.seq def build_transfer(self, chunk: bytes) -> bytes: if len(chunk) > self.max_block_len: raise ValueError("块长度超过 ECU 上报的 maxNumberOfBlockLength") return bytes([0x36, self.next_seq()]) + chunk def on_nrc_0x73(self, expected_seq: int): """收到 wrongBlockSequenceCounter 时,按 ECU 期望的序号对齐后重发""" self.seq = (expected_seq - 1) & 0xFF

块序号是这条链上最容易出错的地方,因为它是整个 UDS 里少数几个“有记忆”的字段。协议要求第一块的序号从 0x01 开始,之后每块加一,加到 0xFF 之后回绕到 0x00 继续。ECU 侧严格按这个规则校验,序号对不上就回 NRC 0x73。

这个设计的存在理由其实很朴素:诊断链路本身可能丢帧、可能重传,光靠请求-响应的一问一答不足以确认数据块的顺序正确。给每块打上序号,ECU 就能发现“第 5 块之后直接来了第 7 块”这种情况,直接拒收,而不是把数据错位地写进 Flash——后者才是真正的灾难。代价就是诊断仪必须精确维护这个计数器,重发同一块时序号不能自增,否则一样报 0x73。上面代码里 on_nrc_0x73 那个方法就是为此准备的:ECU 在 NRC 响应里通常会带上它期望的序号,照着对齐再发即可,不用整个流程重来。

5. 排查:一条 7F 负响应背后的定位路径

5.1 负响应的三层信息:7F 之后该看什么

拿到7F xx yy,第一件事是把三个字节拆开看:0x7F 说明这是负响应,第二个字节告诉你失败的是哪个服务,第三个字节才是原因。第二步是查这个服务支持哪些 NRC,因为同一个码在不同服务下含义可能不一样,协议是按服务分别定义支持列表的。

第三步,也是最容易被跳过的一步:区分“这次请求本身有问题”和“ECU 还在处理”。NRC 0x78(requestCorrectlyReceived-ResponsePending)表示请求收下了、正在处理,让诊断仪继续等,这是唯一一个不算失败的负响应。收到 0x78 之后必须把等待时间延长到 P2*,而不是立刻判失败。很多测试脚本在刷写擦除这种耗时操作上反复报超时,就是因为没处理 0x78。

5.2 读 DID 回 7F 22 7F:会话没对齐

现象:脚本对着某个 DID 发22 F1 90,回7F 22 7F,换一个 DID 还是同样的 NRC,换一台 ECU 却能读通。

原因:0x7F 是 serviceNotSupportedInActiveSession,意思是这个服务在当前会话下不支持。ECU 还停在默认会话,而 0x22 在这个项目里被配置成只在扩展会话开放。这里的关键认知是:NRC 说的不是“这个服务不存在”,而是“在当前状态下不存在”,两者差了一个状态维度。换 ECU 能通,是因为那台 ECU 的默认会话策略宽松一些。

解决:在请求之前确认当前会话,必要时补一条10 03,并检查切换是否真的成功——正响应是 0x50 而不是 0x7F。更稳妥的做法是在脚本的 setup 阶段就建立会话,把会话状态当作前置条件显式管理,而不是每次请求都隐式依赖“上一步应该切过了”。我见过太多因为中间插了一条 0x11 复位,导致后续全部服务失效却查不出原因的案例,根子都在这里。

5.3 发 key 回 7F 27 35:子功能配对与 seed 时效

现象:seed 请求正常拿到了一串数据,按算法算出 key 发过去,回7F 27 35,重试一次变成 0x36,再试变成 0x37,之后彻底不动。

原因:0x35 是密钥无效。可能是算法实现和 ECU 侧不一致,也可能是 seed 已经过期(很多 ECU 的 seed 有几十秒的有效期,调试时代码断点停太久就废了),还可能是子功能配错了——27 01必须配27 02,配成27 03在支持多安全等级的 ECU 上会走另一条校验分支,必然失败。后续的 0x36、0x37 是错误次数累积触发的惩罚,不是新问题,是上一次失败的后遗症。

解决:先在单步环境下确认 seed 与 key 的配对关系,再核对算法库版本;调试时把断点放在发送 key 之前,避免 seed 超时。遇到 0x36 或 0x37 不要立刻重试,先等过延时窗口或者复位 ECU,否则只会把锁定时间越拖越长。

5.4 传数据回 7F 36 73:块序号的自增与回绕

现象:刷写进行到中途突然中断,回7F 36 73,重发同一块还是 0x73,重发下一块也是 0x73。

原因:0x73 是块序号错误。常见三种:一是一开始就没从 0x01 起,把第一块编成了 0x00;二是数据量超过 0xFF 块之后回绕逻辑写错,成了 0x100 而不是 0x00;三是某一块发送失败后重试,代码里调用了自增再发,序号凭空多跳了一格。

解决:把块序号当成一个独立的状态对象管理,自增只发生在“确认上块已成功”之后。收到

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进