ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

UDS诊断0x31请求超出范围?可能是ECU状态机变了

UDS诊断0x31请求超出范围?可能是ECU状态机变了

台架上调试一个ECU的诊断功能,我遇到过一个特别拧巴的问题:诊断仪明明还连着OBD口,总线上的电平也正常,可刚才还能正常执行的一条写入请求,原封不动再发一遍,对面就回了一个负响应码 0x31。界面上就一行英文:requestOutOfRange,翻译过来是“请求超出范围”。线还插着,物理连接一点问题没有,为什么“刚才能用”的操作现在就不能用了?这段时间我在好几个项目里反复碰到类似情况,也帮同事排查过几回,结论几乎都一样:大多数时候并不是真的“范围超了”,而是ECU内部的诊断状态机已经悄悄换了一副面孔。

UDS调试这事儿,最迷惑人的就是这种“看不见的状态”。这篇就把这类“0x31谜案”的常见原因、排查思路和避坑经验整理一下,给同样被这个NRC折磨过的人做个参考。

1. 现象与本质:0x31不是“线断了”,而是“状态变了”

1.1 先看懂UDS的应答规则

UDS,全称Unified Diagnostic Services,统一诊断服务,是汽车诊断ECU之间通信的标准协议,几乎所有现代控制器都走这套规则。它的工作方式很直白:诊断仪(Tester)发一条请求,ECU回一条响应。请求和响应都通过CAN总线承载,收不收得到、回不回得对,就是诊断会话的基本盘。

在UDS里,ECU收到请求后只有两种答复路径:要么回正响应,要么回负响应。正响应的格式是把请求的服务ID(SID)加上0x40,再带上数据。比如你用0x22服务去读一个DID(数据标识符),正响应就是0x62 + DID + 读取到的数据。如果ECU认为请求有问题,就回负响应,格式是0x7F + 请求的SID + 负响应码NRC。比如你发0x22,它回0x7F 22 31,说明这条请求被拒绝了,拒绝原因码是0x31。

这个“0x31”就是标题里说的“#31”。很多工程师习惯在聊天里把它叫“31线”,意思是“报31了”,跟“掉线”其实是两码事。线插没插好,是物理层问题;报什么NRC,是协议层和状态机的问题。搞清楚这个区别,你排查0x31的速度能快一半。

1.2 0x31在报文里长什么样

在ISO 14229标准里,NRC 0x31的官方定义是requestOutOfRange,字面意思是请求超出允许范围。它的大致意思是:ECU收到了请求,也识别了这个服务,但是请求里的参数、数据、或者执行条件不在它允许的范围内,于是拒绝执行。

问题是,“超出范围”这四个字的边界非常模糊。我在实际项目里见过的0x31,至少能对应七八种完全不同的情况:请求的服务ID本身不被支持;服务ID支持但子功能不支持;请求的DID根本不存在;数据段长度不对;数据值不在允许范围内;当前诊断会话不允许访问这个服务;安全等级不够;某个前置条件没满足……这些全都有可能被归到0x31上。

为什么这么杂?因为很多ECU的诊断协议栈在实现时,并没有严格按照标准把不同错误细分成0x11、0x12、0x13、0x22、0x33那一堆NRC,而是把“参数校验失败”“访问条件不满足”“执行环境不对”统一扔进了0x31这个大筐。尤其在应用层诊断栈里,这种简化的实现相当普遍。结果就是,0x31成了诊断调试里最高频、也最让人头疼的一个NRC。

提示:看到0x31,别急着查参数表。先问一句:这个ECU的协议栈是不是把各种错误都归拢到0x31了?如果是,排查重点要放在“状态和条件”,而不是单纯“报文内容”。

1.3 关键认知:物理层正常不等于逻辑层正常

诊断线还插着,只能说明CAN物理链路是通的,0x7DF寻址地址能发出去,ECU也能收到。但UDS是一个会话型协议,ECU内部维护着一套完整的诊断状态机,里面至少包含三组状态:当前诊断会话模式(默认、扩展、编程)、安全访问等级(锁定、解锁)、以及一堆条件标志(比如例程执行状态、前置条件是否满足、写保护是否置位)。

物理线没断,不代表状态机还在你上次离开的位置。只要其中任何一组状态发生了迁移,刚才还合法的请求,下一秒就可能变成0x31。所以说,“线还插着”只是排查工作的最表层,真正的诊断对象,是ECU内部那些看不见的状态。

2. 藏在“刚才”里的三类状态变迁

“刚才还能用”这句话,是排查0x31的关键线索。既然刚才能用,说明报文的格式本身大概率没问题,问题出在“从刚才到现在”这段时间里,ECU的状态发生了什么变化。我总结了三个最典型的状态变迁方向,基本覆盖了90%以上的场景。

2.1 会话超时,ECU悄悄退回默认会话

做诊断开发的人都熟悉S3Server这个概念,它就是非默认会话的保持时间。ECU处于扩展会话(0x03)或编程会话(0x02)时,如果在一段时间内没有收到任何诊断请求,就会自动退回默认会话(0x01)。ISO 14229标准里S3Server的默认值是5000ms,但很多厂商会自定义成10秒、30秒甚至更长,具体看车型的诊断规范。

问题通常出在这里:你在扩展会话里读某个DID,或者执行某个例程,操作间隙超过了会话超时时间,ECU已经退回默认会话了,但诊断仪端的界面还停留在扩展会话的功能菜单上。下一次请求发出去,ECU一看:当前是默认会话,而这个DID或例程只允许在扩展会话里访问,于是直接给你一个0x31。

这种场景在手工操作诊断仪的时候特别容易踩。人点鼠标的速度,远没有状态机切换得快。你翻一翻菜单、查一下资料再回来点执行,三五秒就过去了,ECU早就回默认会话了。解决办法很简单:先发一条10 03切回扩展会话,再重新执行目标请求。或者,如果你用的诊断软件有“自动保持会话”功能,打开它,软件会在空闲间隙自动发TesterPresent(3E 00)保活,避免会话静默超时。

这里还要多说一句:为什么会话不支持的时候,有的ECU回0x31而不是0x7F或0x7E?按照ISO 14229,服务在当前会话不被允许时,标准做法是回0x7F(serviceNotSupportedInActiveSession)或0x7E(subFunctionNotSupportedInActiveSession)。但不少老一代协议栈没有实现这两个NRC,或者校验顺序是先查参数、后查会话权限,导致参数先被判定“超范围”,抛出的就成了0x31。理解这个实现差异,能帮你少走很多弯路。

2.2 安全访问失效,解锁并不永久

第二个高频原因是Security Access,也就是安全访问服务,报文里是0x27。很多受保护的诊断服务,比如写入标定数据、执行特殊例程,前置条件是先做安全解锁。流程一般是:Tester发27 01请求种子,ECU回种子,Tester用算法算出密钥发27 02,ECU验证通过后解锁,然后才能执行后续的高权限操作。

问题在于,解锁状态并不是永久有效的。很多ECU对解锁状态设了时间上限,比如5秒、60秒,超时后自动重新上锁;有些ECU在会话切换、上下电、ECU复位之后也会强制清除解锁状态;还有的ECU对解锁尝试次数做了限制,超过次数直接锁定。你刚才解锁后成功写入了一条数据,过一会儿再写第二条,中间隔的时间超过了解锁有效期,ECU已经重新上锁,新的写入请求自然就被0x31拒之门外。

遇到这种情况,先别急着怀疑报文格式。发一条27 01看看ECU还能不能正常回种子:如果能拿到种子,说明ECU的解锁流程本身是活的,你重新走一遍27 01/27 02再做目标操作就行;如果27 01直接回0x33(securityAccessDenied)或者0x36(exceedNumberOfAttempts),说明安全状态已经彻底变了,有时候还需要先等一段时间或者给ECU下电重启才能恢复。

注意:0x31和0x33之间没有绝对的分界线。有的ECU实现里,安全未解锁的访问会明确回0x33,但有些ECU会把这类“访问条件不满足”的情况也合并成0x31。所以排查时不要看到0x31就把安全访问排除掉,先发一条27 01探一下,成本极低,收益很高。

2.3 前置条件、操作顺序与“写一次”约束

ECU越复杂,诊断服务就越像一套严格的门禁系统。写VIN、写配置信息、执行标定例程,ECU在执行前会先校验一堆前置条件:当前车速是不是0、档位是不是在P挡、高压电是不是已经下电、生命计数器对不对、有没有先写“解锁标志”、当前是不是在允许的会话模式……任何一个条件不满足,都有可能回0x31。

这里面最隐蔽的是“只能写一次”的DID。最典型的就是VIN码,很多ECU内部有一个“VIN已写入”的标志位,第一次用2E服务写入成功后,这个标志位就被置起来,从此以后哪怕你再发完全相同的请求,ECU也坚决不接受,直接回0x31。这种机制在量产和售后场景特别常见:车已经下线交付,诊断仪想重写VIN,ECU死活不让写,其实就是因为内部写保护已经生效了。

还有一种情况是操作顺序问题。有的项目要求先写A请求再写B请求,或者必须先执行例程R1再执行R2,如果你跳过中间步骤直接发最终请求,ECU的回执同样是0x31。原因是协议栈的校验逻辑不只是“报文参数合不合法”,它还查了内部状态寄存器——顺序不对,状态就不对,请求自然被拒。这类问题没法从单条报文里看出来,必须看整个操作序列的时间线。

3. 实操排查:从一次真实复现走完整个流程

3.1 复现与抓包,先把两次操作对齐

遇到“刚才能用现在不能用”的问题,我一般不会凭感觉乱试,先把“能用”和“不能用”两个场景完整记录下来。用CAN分析工具抓两段报文:一段是刚才成功时从开始到结束的全部请求和响应,一段是现在失败时发出的请求和返回的负响应。然后放在同一个时间线上对比。

重点看三件事:第一,成功请求和失败请求之间隔了多久,这个时间差是否超过了会话超时或安全解锁的有效期;第二,两段报文之间有没有夹着会话切换命令,比如10 01、10 02、10 03;第三,失败请求之前,总线上有没有其他设备发出的复位命令或诊断请求,这一点在台架上尤其容易忽略——车上可能同时挂着好几套工具,别人的一条请求就把ECU状态给改了。

3.2 逐字节对比:请求报文本身的差异

如果状态对比没看出问题,就把两条请求报文一帧一帧地对比。检查这些点:服务ID是否一致;子功能字节是否相同;DID的字节顺序对不对(有的ECU用大端,有的用小端,抄错字节序就会0x31);数据长度是否一致;数据区里有没有额外的前缀;服务ID是不是从物理寻址变成了功能寻址。

我印象很深的一个案例是VIN写入。某ECU的2E F1 90服务要求数据区正好是17个ASCII字符,第一次写17字节成功了,第二次脚本里一个字段拼接错误,数据区前头多了一个字节,结果就变成0x31。报文看下来,就一个字节的差异,但ECU校验长度严格,多一个少一个都算“超出范围”。所以逐字节对比这条建议,真不是凑字数,是实打实的排障刚需。

3.3 用只读请求探测当前会话状态

很多时候,与其对着0x31猜原因,不如主动发几条只读请求去“问”ECU现在的状态。先发10 01读一下当前会话模式,看答复里的子功能字节是01、02还是03;再发27 01请求种子,看ECU还认不认这个安全等级;有条件的话,用22服务去读诊断DID里的状态信息,比如会话状态、写保护标志、例程执行状态。

把这些探测请求的答复和失败请求放在同一个时间轴上,问题范围基本就划出来了:会话变了就切会话,安全失效了就重新解锁,条件不满足就查前置条件。这里有个特别快的验证技巧:如果怀疑是会话超时,就在失败之后先发一条3E 00 TesterPresent保活,然后再发目标请求看0x31是否消失。如果消失,基本可以锁定就是S3Server的问题。这个方法比翻诊断规范快得多,推荐优先尝试。

4. 常见原因速查表

下面这个表是我在实际项目里整理出来的“0x31排查速查表”,按照出现频率排序,遇见问题可以先对照着过一遍。

触发场景最常见原因快速判断方法解决方向
操作间隙过长后报0x31S3Server超时,会话退回默认看两次请求的时间戳间隔先发10 03切扩展会话,或开自动保活
执行高权限写入时报0x31安全解锁失效发27 01看能否拿到种子重新走27 01/27 02解锁流程
同一请求换工具后报0x31功能寻址/物理寻址权限差异查看请求的CAN ID改为物理寻址发送
上报文中数据长度与预期不符数据区多字节或少字节对照诊断规范逐字节比对修正脚本或软件字段拼接
请求的DID/例程ID不存在字面意义的“超出范围”对照DID/例程调查表改用正确ID,或确认软件版本是否支持
写VIN等数据第二次失败只写一次写保护查ECU写保护状态DID用厂商工具解除写保护或走刷写流程
执行例程时条件不满足车速/档位/高压等前置条件查车载状态量将整车置于目标工况再执行
ECU刚复位或刷写完成状态全面清零看复位报文或下电事件重新切会话、重新解锁后再操作

这张表看起来都是刚需,但有一个共同点:它们都印证了“0x31不是单点问题,而是状态机问题”。排查的时候要习惯性地把“状态变了”放在候选原因的第一位,而不是一上来就怀疑报文格式。

5. 几个容易忽略的坑

5.1 功能寻址还是物理寻址

UDS请求可以通过功能寻址发给总线上所有ECU,也可以通过物理寻址发给指定ECU。很多整车厂和零部件厂的项目里都规定:安全访问、写入、例程这类服务只允许物理寻址,功能寻址请求会被直接拒绝,有的ECU在收到功能寻址的非法请求时回的就是0x31。如果你手上有两台工具,一个走功能寻址,一个走物理寻址,就会出现“这台工具能用、那台工具不能用”的诡异现象。排查方法很简单:看失败请求的CAN ID,如果是功能寻址地址,改成物理寻址再试一次。

5.2 多个诊断客户端抢占会话

台架上经常会同时挂好几套工具,抓包软件、诊断仪、自动刷写脚本一起上,总线上热闹得很。UDS一般同一时刻只允许一个客户端持有诊断会话,如果一个工具发了10 03切到扩展会话,另一个工具再发同样的请求,ECU的状态可能已经被第一个工具改过了,第二个工具就会收到意想不到的NRC。这种场景下“线一直插着”,但ECU的会话已经被另一个工具“偷”走了。用CAN工具看总线上的所有报文,而不只是自己发出的报文,才能看到真相。

5.3 ECU复位与刷写后的状态清零

ECU一旦发生复位,不管是下电、看门狗复位、还是刷写完成后的自动重启,诊断会话和安全状态都会被清掉。这是做刷写相关诊断时最容易踩的坑:刷写流程结束后ECU自动复位,你没有重新进入扩展会话、重新解锁就接着执行写入操作,0x31几乎是必然的。所以刷写脚本里,刷完以后一定要记得加一个“重建诊断会话”的步骤,先10 03、再27 01/27 02,然后再做后续操作。

5.4 开发端如何减少“0x31谜案”

如果你正好在做ECU端的诊断栈开发,有一个建议值得听进去:失败日志里一定要记录内部失败原因码,而不是只输出一个NRC。协议栈对外统一回0x31完全没问题,但内部日志必须能区分“DID不支持”“长度不合法”“安全等级不足”“条件未满足”这些具体原因。我自己在调试的时候,最绝望的瞬间就是看日志看到一行“Send NRC 0x31”,什么上下文都没有,等于没记。

提示:一条高质量诊断日志至少应该包含:请求的SID、子功能、接收时间、失败原因码、当前会话ID、当前安全等级。写日志的时候多写两行,排查时能少熬两个夜。

这个习惯,救过我好几次。有一次某个ECU的0x31在台架上复现不出来,客户那边却天天报,后来是让客户那边的测试工程师用诊断仪重新录了一段带时间戳的日志发过来,配合内部原因码才定位到是前置条件里“车速不为0”的问题。从那以后,我对“日志必须能回答状态问题”这件事深信不疑。

说实话,踩过几次这种坑之后,我现在遇到0x31的第一反应已经不是去翻参数表了,而是先问三件事:ECU当前的会话在哪个模式?安全状态是解锁还是锁定?距离上一次真正成功的请求过去了多久?这三件事一确认,八成的问题都有了方向。诊断调试讲究的就是“状态先行、报文随后”,物理线插没插,永远只是最表层的那一问。诊断仪、测试脚本、售后工具,每个角色都会遇到自己的0x31,但背后的排查逻辑是同一套——学会“猜ECU的状态”,再用工具验证你的猜测。希望这篇经验能帮你在下一次遇到“#31”的时候,少一点迷惑,多一点方向感。

RELATED READING

延伸阅读

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