27服务在UDS(Unified Diagnostic Services,统一诊断服务)里承担的是安全访问功能,也就是常说的 SecurityAccess。ECU 刷写、标定、下线配置、售后维修诊断,想进入受保护状态之前,基本都要先走一遍 27 服务。NRC 35、36、37 是这个过程里最容易被问到的三个负响应码,很多调试卡住的人会把它们当成同一种错误,实际上它们分别指向请求格式、访问状态、密钥算法三个完全不同的层面。
这篇文章就按实际排查顺序把这三个 NRC 拆开讲。不会教你绕过安全机制,而是讲清楚协议定义、触发条件、排查思路和自动化测试里该怎么处理。适合正在做 UDS 诊断开发、写诊断测试用例、或者拿到7F 27 xx响应但不知道往哪查的人读。
先记住一个简单的排查口诀:看到 35 查请求,看到 36 查状态,看到 37 查密钥。这个顺序能解决大部分调试问题。
1. 先还原 27 服务的完整流程:NRC 到底出在哪一步
1.1 27 服务是 seed/key 两步交互
27 服务不是单条指令,它最少包含两条请求报文。第一步请求种子,第二步发送密钥。我建议所有测试脚本都把这两步当成一个完整用例,不要单独只看某一次发送。
第一步,客户端发送请求种子:
27 01其中0x27是服务 ID,0x01是子功能。0x01表示请求第一组种子。ECU 如果接受,返回肯定响应:
67 01 23 45 67 890x67表示肯定响应,0x01是回显的子功能,后面的23 45 67 89是 seed 数据。具体 seed 长度由 ECU 定义,可能是 2 字节、4 字节、8 字节,甚至更长。
拿到 seed 后,客户端按约定算法生成 key,然后发送第二步:
27 02 5A 9C EF 210x02表示发送第一组密钥。ECU 校验通过后返回:
67 02到这一步,安全访问成功,ECU 从 locked 状态切到 unlocked 状态。后续才能执行标记为受保护的服务,例如 2E 写入标定参数、31 服务里的部分例程控制、34/36/37 刷写流程。
1.2 支持多组访问级别时,别用错组别
很多 ECU 支持多组安全访问。常见设计是:
01/02:第一访问级别03/04:第二访问级别05/06:第三访问级别
不同访问级别对应不同权限。比如第一级别允许读写普通配置,第二级别允许执行刷写,第三级别允许执行下线特殊操作。这样做的好处是降低单一路径泄露带来的风险。
测试时最容易犯的错,是在需要第二级别03/04的情况下,仍然用01/02的第一级别只解锁第一级。这种情况下即使第一级解锁成功,后面执行刷写也依然会失败。所以看到 27 服务相关的失败,先确认当前用例用的组别和 ECU 要求的是否一致。
1.3 NRC 在这条链路里的位置
失败情况下,ECU 返回的不是67,而是7F开头的负响应。格式是:
7F 27 NRC第一个字节0x7F固定表示负响应,第二个字节0x27回显服务 ID,第三个字节是负响应码。对 27 服务来说,最常见的就是0x35、0x36、0x37。
这三个 NRC 出现的位置不完全一样:
| NRC | 名称 | 含义 | 常见出现位置 |
|---|---|---|---|
| 0x35 | requestOutOfRange | 请求参数超出范围 | 请求 seed 或发送 key 时,子功能、数据长度、访问级别不支持 |
| 0x36 | securityAccessDenied | 安全访问被拒绝 | 前置条件不满足、时机不对、尝试次数超限 |
| 0x37 | invalidKey | 密钥无效 | key 已发送,但 ECU 校验不通过 |
我自己的经验是,别把第一个 NRC 当成最终失败原因。测试时最好把完整交互序列导出来,看看到底是第一步就失败,还是第二步才失败。
如果发的是27 01,收到7F 27 35或7F 27 36,说明问题出在请求种子阶段,还没到密钥校验。如果发的是27 02,收到7F 27 36或7F 27 37,说明第一步大概率已经成功,问题出在状态迁移或密钥本身。只有先定位到具体步骤,再去查参数,才不会在 NRC 上绕圈。
2. NRC 35、36、37 分别对应哪一层问题
2.1 NRC 0x35:请求报文本身超出范围
NRC 0x35 的标准名称是 requestOutOfRange。在 ISO 14229 里的通用解释是:请求报文包含的参数超出 ECU 支持的范围。
放到 27 服务里,常见触发条件有这些:
- 发送了不支持的子功能。比如 ECU 只支持
01/02一组安全访问,你发了03。 - 请求 seed 时,报文带了多余数据。有的实现严格校验长度,多一个字节也会返回 35。
- 发送 key 时,key 长度与当前组别要求的 seed 长度不匹配。
- 当前会话下,ECU 内部的访问级别范围不含你请求的组别。
- 请求报文的数据格式不符合诊断规范,例如保留位没有置成约定值。
这类问题在协议实现早期最容易出现。测试工具与 ECU 的 DBC 或 CDD 配置不一致时,长度和子功能范围经常对不上。
排查顺序我一般这样走:
- 确认当前处于哪个诊断会话。部分 ECU 要求 27 服务必须在扩展会话或编程会话下执行,默认会话下可能返回 35,也可能返回 36。
- 核对子功能范围,看是
01/02一组,还是01到06多组。 - 核对 seed 和 key 的长度。这两个长度通常由 ECU 的 CDD 或诊断规范给出,不是测试工具自己能推断的。
- 对照发送报文和规范里的 Message ID、数据长度,看看是不是多发了或少发了。
2.2 NRC 0x36:ECU 当前状态不允许安全访问
NRC 0x36 的标准名称是 securityAccessDenied,含义是:服务请求本身合法,但 ECU 根据当前内部状态判断,不能执行这次访问。
这个 NRC 最容易让人困惑,因为 ECU 往往不会在响应里告诉你到底缺了什么条件。常见触发条件包括:
- 没有先请求 seed,直接发送 key。
- 请求 seed 成功之后,间隔时间太长,ECU 内部的 seed 有效期已经超时。
- ECU 要求先进入扩展会话或编程会话,当前还停留在默认会话。
- 连续失败次数达到上限,ECU 进入延迟响应或锁定状态。
- 某些 ECU 还要求满足业务前置条件,比如车速为零、发动机停机、电压在某个范围,否则拒绝访问。
- 会话被切换或诊断仪断开后,ECU 已经回到 locked 状态,但测试脚本还认为处于 unlocked 状态。
也就是说,NRC 36 往往带着状态机判断。它不是一个简单的格式错误,而是“当前条件不允许”。不同 ECU 对 NRC 的映射还可能不同,有的 ECU 会在会话不对时返回 36,有的会返回 35,甚至有的返回 11。所以判断时要以 ECU 的诊断规范为准。
遇到 36,我的做法是先把会话、时点、失败计数列出来。排查顺序:
- 当前会话是不是规范里要求的会话,不是的话先发
10 03或10 02切换。 - 是否先发了
27 01并成功收到 seed。 - 从收到 seed 到发送 key 的间隔时间,是否存在超时限制。
- 在这之前是否已经失败过多次,ECU 是否处于锁定时间。
- 是否有其他前置条件,比如车速、电压、安全状态、外部使能信号。
2.3 NRC 0x37:密钥本身不对
NRC 0x37 的标准名称是 invalidKey,含义很直接:ECU 拿到 key 之后做了校验,发现和它期望的 key 不一致。
触发条件通常是:
- seed 取数或字节序处理错误。
- key 生成算法用错,或算法里的常数、轮次、初始向量不对。
- 发送 key 时,字节序高低位倒置。
- seed 在请求后被更新或过期,但客户端仍用旧 seed 计算 key。
- 多组安全访问时,用第一组算法去生成第二组的 key。
NRC 37 能出现,说明 ECU 已经走到密钥校验这一步。也就是说,请求格式和状态条件大概率是满足的,问题可以聚焦到 seed 和 key 这一对数据上。
排查时:
- 把收到的 seed 原样记录下来,不要只记录长度。
- 确认 key 计算算法与 ECU 定义的算法一致,包括字节序。
- 检查发送报文里的 key,是不是从 seed 计算出来的那串,中间有没有被工具做格式转换。
- 怀疑算法版本时,向 ECU 供应商确认算法参数,不要用盲猜的方式反复发送,因为盲猜会触发 36 的失败计数。
2.4 一句话区分三个 NRC
- 35 解决的是“请求报文是不是合法”。
- 36 解决的是“当前时机和状态允不允许访问”。
- 37 解决的是“算法算出来的 key 对不对”。
用一句话总结:35 是格式问题,36 是状态问题,37 是数据问题。看到 35 先查请求,看到 36 先查时序和状态,看到 37 再查密钥计算。三个混在一起排查,大概率会在错误方向上浪费时间。
3. 从一次失败响应反推排查方向
3.1 第一层:抓完整时序,不要只看最后一条
UDS 测试过程里最常见的问题,是只从最终结果反推。比如拿到一个7F 27 36,直接怀疑 ECU 难解锁,其实前面可能根本没有发过27 01。
我建议把一次安全访问拆成三条关键记录:会话控制请求、请求种子请求、发送密钥请求。抓取时记录时间戳,没有时间戳就很难判断是否超时。
正常情况下,时序是:
10 03 50 03 27 01 67 01 [seed] 27 02 [key] 67 02如果中间缺了任何一条,NRC 36 的出现就很好解释。抓包时既看诊断层日志,也看 CAN 或以太网物理层日志。物理层日志能反映总线负载、错误帧、仲裁丢失等诊断层看不到的问题。有时 ECU 根本没收到27 02,不是它拒绝响应,而是报文在总线上已经异常了。
3.2 第二层:对照会话状态和前置条件
很多 ECU 的 27 服务只允许在非默认会话下使用。测试前先发10 03进入扩展会话,或者发10 01进入编程会话,再发27 01。
如果 ECU 返回 36,先检查当前会话。检查方法很简单:重新发送一次10 03,如果返回50 03,说明刚刚会话被重置,或者原本就不是扩展会话。有些工具在发送 27 服务前会先发10 03,但如果脚本顺序写错,也会出现会话被反复重置的情况。
另外,确保测试设备没有同时发起其他服务干扰 ECU 状态。例如某些测试脚本在循环执行其他诊断服务,每个服务都会切换会话,就会导致 27 服务的时序被破坏。
3.3 第三层:检查 seed/key 长度与字节序
这里最容易踩的坑是字节序。seed 返回的字节排列,比如23 45 67 89,计算 key 时是按大端转成0x23456789,还是按小端转成0x89674523,不同 ECU 的实现完全可能不同。
key 发送时也是一样。算法算出的结果需要按 ECU 期望的字节序摆进报文。如果工具和 ECU 的字节序定义不一致,哪怕算法本身正确,也会被判定为 37。
处理方式建议:
- 在测试脚本里显式定义 seed 和 key 的字节序变量。
- 不要用工具默认的 hex 显示来代替计算输入。
- 计算完成后,把计算结果和发送报文的十六进制逐字节对比,确认没有发生数组翻转。
- 如果工具支持 seed/key 脚本回调,优先在脚本里打印中间值,方便定位是 seed 取数不对还是 key 生成不对。
3.4 第四层:确认尝试次数和锁定逻辑
ECU 通常会对安全访问失败的次数做限制。比如连续失败 3 次进入 30 秒延迟,失败 5 次进入 10 分钟锁定。
被锁定之后,即使后续发送的 key 是正确的,ECU 也可能返回 36 而不是 37。这就解释了为什么有些测试里第一步没问题、算法也没问题,却反复收到 36。
遇到这种场景,最稳妥的方式是等待锁定时间结束,再重新走完整流程。不要在锁定期间反复重试,因为重试只会刷新锁定计时,甚至延长锁定时间。测试脚本里要把锁定时间当成一个固定等待条件。
4. 五个高频场景实测拆解
4.1 场景1:跳过请求种子直接发送 key
现象:收到7F 27 36。
原因:客户端没有先请求 seed,直接发送27 02。ECU 当前没有可用的 seed 状态,拒绝访问。
处理:改成先发27 01,成功返回 seed 后再发27 02。测试脚本里要做成有依赖关系,不能两条并行发送,也不能随机调度。
4.2 场景2:seed 已经过期才发 key
现象:第一步成功,第二步收到7F 27 36。
原因:ECU 对 seed 设了有效期,比如 5 秒。测试人员在 seed 返回后停留太久,才开始计算 key。
处理:把 seed 记录时间到发送 key 的时间控制在 ECU 规定窗口内。如果工具本身支持自动发送,开启自动流程;如果手写脚本,把中间时间戳打印出来。有的工具会在 seed 返回后自动暂停等待人工处理,这种情况下最容易超时。
4.3 场景3:key 字节数不对
现象:发送密钥时收到7F 27 35,而不是 37。
原因:seed 长度是 4 字节,key 却只发送了 3 字节,或者多发了 1 字节。请求报文长度不符合 ECU 定义。
处理:核对 CDD 里 key 的长度字段,修正发送帧。这里需要留意,部分 ECU 会严格校验长度,部分 ECU 会读取有效长度后忽略多余字节,行为不一致。
4.4 场景4:字节序或算法边界错误
现象:key 长度正确,ECU 返回7F 27 37。
原因:seed 的字节序理解错误,或 key 生成算法里某一步的取数范围、进位边界不对。
处理:先用一条已知 seed 和正确 key 的样例做离线对比,确认算法与 ECU 一致,再放到线上去测。如果手头没有样例,向 ECU 供应商核对算法说明,不要靠暴力枚举。暴力枚举不仅触发失败计数,在实车上可能引发安全相关限制。
4.5 场景5:连续失败进入锁定状态
现象:一开始返回 37,多次重试后突然变成 36。
原因:失败计数到达阈值,ECU 进入延迟或锁定状态。延迟状态下,ECU 可能要求等待固定时间才允许下一次请求;锁定状态下,ECU 可能直接拒绝所有安全访问请求,直到条件解除。
处理:等待锁定解除,检查失败计数逻辑,减少无效重试。测试用例里要设计一个“失败 N 次后应进入锁定”的预期断言,避免脚本无限重发。
5. 测试工具和自动化脚本里怎么处理这三个 NRC
5.1 从响应里准确解析 NRC
以 CANoe、PEAK、诊断仪或自研脚本为例,判断一个响应是不是 27 服务负响应,需要同时满足:
- 响应首字节是
0x7F - 响应第二字节是
0x27 - 响应第三字节是 NRC
有的工具会把7F 27 36解析成字符串 securityAccessDenied,有的只显示原始字节。做自动化断言时,建议直接断言第三字节的值,不要依赖工具翻译后的文本。
# 伪代码示例 def parse_negative_response(response): if len(response) < 3: return "invalid" if response[0] == 0x7F and response[1] == 0x27: nrc = response[2] if nrc == 0x35: return "requestOutOfRange" elif nrc == 0x36: return "securityAccessDenied" elif nrc == 0x37: return "invalidKey" else: return "otherNrc"注意:有的工具会把 NRC 显示成十进制,例如0x37对应十进制55,0x36对应54,0x35对应53。脚本断言时不要写混。
5.2 测试用例设计建议
一个完整的 27 服务测试套件,至少要覆盖这些用例:
| 编号 | 用例 | 预期结果 |
|---|---|---|
| 1 | 默认会话下直接发 27 01 | 按规范返回 35 或 36 |
| 2 | 扩展会话下发 27 01 | 返回 67 和 seed |
| 3 | 不请求 seed 直接发 27 02 | 返回 36 |
| 4 | 请求 seed 后发送错误 key | 返回 37 |
| 5 | 请求 seed 后超过有效期再发 key | 返回 36 |
| 6 | 连续失败达到阈值 | 返回 36 并进入锁定 |
| 7 | 锁定结束后重新访问 | 能正常解锁 |
每个用例都要有预期 NRC,且要区分“允许失败”和“不允许失败”。有的 ECU 设计成第一次失败不计数,第二次才开始计数,用例要按规范精确设计。
5.3 日志记录要包含哪些字段
排查 NRC 时,最头疼的是只有一条最终结果,缺少上下文。建议日志至少记录:
- 请求 ID 或测试用例 ID
- 当前诊断会话状态
- 请求报文 hex
- 响应报文 hex
- 时间戳
- 从 seed 到 key 的耗时
- 本次用例是第几次尝试
- 工具是否自动切换过会话
有了这些字段,拿一个 NRC 就能快速定位到具体环节。没有字段,就只能在线上反复试。
6. 边界问题与工程建议
6.1 NRC 35 不只在 27 服务出现
0x35 是 UDS 通用 NRC,不局限于 27 服务。31 服务例程控制、2E 服务写入数据、2F 服务输入输出控制、34 服务请求下载,都会返回 35。
所以在看到 0x35 时,先确认响应里的服务 ID 是不是0x27。如果服务 ID 是其他值,排查方向要回到对应服务,不要只按 27 服务的思路查。
6.2 不同 ECU 对 NRC 的映射不完全一样
ISO 14229 给出了 NRC 的标准含义,但具体 ECU 实现里,边界条件落在哪个 NRC 上可能不同。比如有的 ECU 在会话不正确时返回 0x36,有的返回 0x35,有的甚至返回 0x11,也就是服务不支持。
这类差异不是代码写错,而是 ECU 设计时对负响应码的使用策略不同。测试用例要基于具体 ECU 的诊断规范,不能用一个 NRC 判断所有 ECU。遇到和标准描述不符的情况,以 ECU 提供的诊断规范或供应商说明为最终依据。
6.3 安全访问测试的合规边界
最后强调一点。27 服务的 seed/key 机制是 ECU 防盗刷、防误配置的核心手段。研发、测试和售后维修场景下,使用合法授权工具、基于供应商提供的算法或诊断规范做验证,是正常工程行为。
测试人员不应该绕过安全机制,也不应该尝试从明文报文中反推密钥算法。如果算法信息缺失,正确做法是向 ECU 供应商申请资料,而不是用枚举方式猜测。否则既可能触发 ECU 锁定,也可能涉及违反法规的问题。
工程上更值得做的,是完善测试流程:
- 先确认当前 ECU 的会话和前置条件。
- 再确认 27 请求格式和参数范围。
- 再核对 seed/key 计算。
- 最后再分析失败重试和锁定逻辑。
不要一上来就怀疑算法,也不要看到一个 NRC 就急着改代码。把请求、状态、数据这三层按顺序过一遍,NRC 35、36、37 的排查路径会清晰很多。