
简介这份PDF版学习笔记面向汽车电子、嵌入式测试及诊断开发方向的初学者与工程师系统梳理ISO 14229标准下的UDS统一诊断服务协议知识帮助读者建立从服务分类到请求响应机制的整体认知。内容按功能单元组织涵盖诊断和通信管理0x10会话控制、0x11复位、0x27安全访问、0x3E保持连接等、数据传输0x22、0x2E、0x2C等、已存储数据传输0x14、0x19、输入输出控制、例程控制以及上传下载0x340x37等常用服务并说明“问-答”模式下请求与响应的对应关系。资源包为1个PDF文件大小约10.49MB便于离线阅读与查阅。目前已有1911人学习适合作为UDS入门速查与协议框架梳理的参考材料。1. 从一次刷写失败说起这份 UDS 笔记到底能帮你省多少时间去年帮一个朋友排查一台模拟 ECU 刷写失败的问题现象很典型诊断仪发 0x34 请求下载ECU 回了 7F 34 33SecurityAccess 没通过。他翻了一晚上 ISO 14229 原文愣是没定位到问题出在 0x27 的 seed-key 配对逻辑上。后来我把手头一份整理好的 UDS 学习笔记 PDF 丢给他第二天早上他说搞定了——问题出在 27 01 和 27 02 的子功能配对关系搞反了。这件事让我意识到UDS 协议本身不算复杂但服务多、参数碎、NRC 码散落在各个章节里没有一个系统性的整理排查效率会非常低。这份《汽车 UDS 诊断协议学习笔记 PDF 版》就是干这个的它把 ISO 14229 里诊断和通信管理、数据传输、已存储数据传输、输入输出控制、例程控制、上传下载六大功能单元的服务定义、请求/响应格式、子功能参数、NRC 码全部梳理成册适合做汽车诊断协议开发、ECU 测试、诊断仪固件开发的从业者当案头速查用。不管你是刚接触 UDS 的新手还是已经写过 CAPL 脚本的老手这份笔记都能帮你少翻几次标准原文。2. 诊断会话与安全访问0x10、0x11、0x27 的请求构造与参数拆解2.1 0x10 诊断会话控制所有诊断命令的前置条件0x10 服务是整个 UDS 诊断的入口。DefaultSession 下很多服务不支持你必须先切到扩展会话或编程会话才能执行后续操作。请求格式很简单10 xx其中 xx 是会话类型。ISO 14229 定义了几个通用值主机厂也可以自定义保留位。正响应格式为50 xx yy其中 xx 是请求子功能的回显yy 是 sessionParameterRecord通常包含 P2 和 P2* 时间参数。P2 是服务器收到请求后到发出响应之间的最大时间P2* 是收到 NRC 0x78responsePending后延长的时间窗口。这两个参数直接决定你诊断仪的超时设置设短了会误判超时设长了会拖慢自动化测试节奏。负响应格式固定为7F 10 xxxx 是 NRC 码。0x10 服务下常见的 NRC 包括 0x12subFunctionNotSupported、0x13incorrectMessageLengthOrInvalidFormat、0x22conditionsNotCorrect。如果你发 10 03 收到 7F 10 12说明这个 ECU 不支持编程会话不是通信问题。注意从 DefaultSession 切到其他会话后如果一段时间内没有 TesterPresent0x3E保活ECU 会自动回落到 DefaultSession。这个超时时间一般在 5 秒左右具体看项目定义。2.2 0x11 ECU 重置复位类型与自动化中的坑0x11 服务用于请求 ECU 执行复位。请求格式11 xxxx 是 resetType。ISO 定义了多个子功能0x01 是 hardReset0x02 是 keyOffOnReset0x03 是 softReset0x04 是 enableRapidPowerShutDown0x05 是 disableRapidPowerShutDown。正响应格式为51 xx当子功能为 0x04 时还会跟一个 powerDownTime 参数取值范围 00~FEFF 表示时间不可用。这个参数在自动化测试里容易被忽略——如果你发了 11 04 但没等 powerDownTime 结束就发下一条请求ECU 可能还没完成下电流程后续通信直接超时。关键点ECU 复位成功后会自动回到 DefaultSession。这意味着你在扩展会话里做了一堆配置一个 0x11 下去全部清零。自动化脚本里如果 0x11 后面紧跟其他服务请求必须重新走一遍 0x10 切会话的流程。2.3 0x27 安全访问seed-key 配对逻辑与 NRC 35/36/37 的触发条件0x27 是 UDS 里最容易翻车的服务之一。流程分两步先请求 seed再发送 key。子功能参数有个硬性规则——奇数子功能是 requestSeed偶数子功能是 sendKey。27 01 请求 seed对应的 key 用 27 02 发送27 03 请求 seed对应的 key 用 27 04 发送。配对关系是「相邻且大于 seed 请求的最小偶数」。请求 seed 的格式27 01securityAccessDataRecord 是可选项一般不赋值。正响应为67 01 [seed...]seed 长度由项目定义常见 4 字节或 8 字节。发送 key 的格式27 02 [key...]。key 是根据 seed 经过 ECU 内部算法计算得到的seed 变了 key 就变。正响应只有67 02不带额外数据。NRC 方面0x27 服务下有三个需要特别关注的错误码0x35invalidKey、0x36exceedNumberOfAttempts、0x37requiredTimeDelayNotExpired。典型触发逻辑是第一次发错误 key 返回 0x35等待 5 秒后再发错误 key 返回 0x36再等 5 秒发第三次错误 key 返回 0x37。这个延迟时间和最大尝试次数由主机厂定义不同项目不一样。下面是一段 CAPL 脚本中安全解锁的核心逻辑展示了 seed 获取、key 计算和发送的完整流程void Diag_SecurityLevel_1_Unlock() { diagRequest ECU.RequestSeed_Request SeedReq_1; diagRequest ECU.SendKey_Send KeySend_1; const dword SENDING_TIMEOUT 2000; // 请求发送超时单位 ms const dword RESPONSE_TIMEROUT 1500; // 响应等待超时单位 ms byte seedArray[8]; byte keyArray[8]; dword actualLevel; char variant[12]; char ipOption[2]; dword KeyActualSize; long status; int i; diagSetTarget(ECU); // 设置诊断目标 ECU 名称 actualLevel 1; // 安全等级 1 ipOption[0] A; ipOption[1] 0; KeyActualSize 8; // key 长度 8 字节 // 第一步发送 seed 请求 diagSendRequest(SeedReq_1); if (testWaitForDiagRequestSent(SeedReq_1, SENDING_TIMEOUT) 1) { testStepPass(000, Request Seed Successfully); } else { testStepFail(000, Request send failed); } // 第二步等待响应并提取 seed if (testWaitForDiagResponse(SeedReq_1, RESPONSE_TIMEROUT) 1) { status diagGetLastResponseCode(SeedReq_1); if (status ! -1) { testStepFail(000, Seed request failed); } // 从响应 byte 2 开始提取 seedbyte 0/1 是 SID 和子功能 for (i 0; i elCount(seedArray); i) { seedArray[i] DiagGetRespPrimitiveByte(SeedReq_1, i 2); } } // 第三步通过 DLL 计算 key status diagGenerateKeyFromSeed(seedArray, elCount(seedArray), actualLevel, variant, ipOption, keyArray, elCount(keyArray), KeyActualSize); if (status ! 0) { testStepFail(000, diagGenerateKeyFromSeed failed); } else { testStepPass(000, diagGenerateKeyFromSeed success); } // 第四步将 key 写入请求参数并发送 diagSetParameterRaw(KeySend_1, SecurityKey, keyArray, elCount(keyArray)); diagSendRequest(KeySend_1); }这段脚本里几个参数需要根据项目实际调整SENDING_TIMEOUT和RESPONSE_TIMEROUT要匹配 ECU 的 P2/P2* 时间actualLevel对应安全等级不同等级 seed-key 算法可能不同variant和ipOption是diagGenerateKeyFromSeed的入参variant 是 ECU 名称ipOption 从diagGetCurrentEcu获取。最容易出错的是diagSetParameterRaw里的参数名SecurityKey这个名字必须和 CDD 文件里定义的一致写错了请求发出去的数据就不是你算出来的 key。3. 数据传输与故障码读取0x22、0x2E、0x19、0x14 的实操要点3.1 0x22 按标识符读数据DID 寻址与多 DID 批量读取0x22 是日常诊断中用得最多的服务。请求格式22 [DID_H] [DID_L]DID 是 2 字节的数据标识符。正响应为62 [DID_H] [DID_L] [data...]。一个请求可以带多个 DIDECU 会按顺序返回所有 DID 的数据。DID 的分配有标准化的部分比如 0xF186 是 ActiveDiagnosticSession0xF187 是 VehicleManufacturerSparePartNumber0xF190 是 VIN 码。主机厂自定义的 DID 一般从 0xFD00 以上开始分配。读 VIN 就是发22 F1 90返回62 F1 90 [17 字节 VIN]。实操中容易踩的坑是 DID 长度不匹配。有些 ECU 对某个 DID 返回的数据长度是固定的你按最大长度去解析会多读垃圾数据。建议在 CDD 或 ODX 文件里确认每个 DID 的实际长度别靠猜。3.2 0x2E 按标识符写数据写入前的会话与安全条件0x2E 用于向 ECU 写入数据。请求格式2E [DID_H] [DID_L] [data...]正响应6E [DID_H] [DID_L]。写操作通常需要满足两个前提处于非 DefaultSession且通过了对应安全等级的 SecurityAccess。常见 NRC 包括 0x13长度不对、0x22条件不满足比如没切会话、0x31请求超出范围、0x33安全访问被拒。如果你发 2E 收到 7F 2E 33说明安全访问没做或者等级不够。写入的数据长度必须和 DID 定义的长度严格一致。比如某个 DID 定义是 4 字节你发 5 字节数据ECU 直接回 0x13。这个在自动化脚本里可以用 CDD 文件自动校验别手动拼。3.3 0x19 读 DTC 与 0x14 清除 DTC子功能选择与状态掩码0x19 服务用于读取故障码信息子功能很多。最常用的是 0x02reportDTCByStatusMask请求格式19 02 [statusMask]statusMask 是 1 字节的状态掩码用于过滤特定状态的 DTC。正响应格式为59 02 [statusAvailabilityMask] [DTC_H] [DTC_L] [status] ...每个 DTC 占 4 字节3 字节 DTC 编号加 1 字节状态。状态字节的 8 个 bit 分别代表 testFailed、confirmedDTC、pendingDTC、testNotCompletedSinceLastClear 等。如果你只关心已确认的故障码statusMask 设为 0x08。0x14 清除诊断信息请求格式14 [groupOfDTC_H] [groupOfDTC_M] [groupOfDTC_L]。groupOfDTC 设为 0xFFFFFF 表示清除所有组。正响应54。清除后 DTC 状态位会复位但有些 ECU 需要几秒到几十秒才能完成清除清除后立刻读可能还能读到残留状态。注意0x14 清除操作通常需要安全访问解锁且只能在扩展会话下执行。DefaultSession 下发 14 会直接收到 7F 14 22。4. 上传下载与例程控制0x34/0x36/0x37 刷写流程与 0x31 例程调用4.1 0x34 请求下载内存地址与数据格式的协商0x34 是刷写流程的起点。请求格式为34 [dataFormatIdentifier] [addressAndLengthFormatIdentifier] [memoryAddress...] [memorySize...]。dataFormatIdentifier 指定压缩和加密方式0x00 表示无压缩无加密0x10 表示无压缩有加密0x01 表示有压缩无加密。addressAndLengthFormatIdentifier 的高 4 位表示 memorySize 占几个字节低 4 位表示 memoryAddress 占几个字节。比如 0x24 表示地址 4 字节、长度 2 字节。正响应74 [lengthFormatIdentifier] [maxNumberOfBlockLength...]。maxNumberOfBlockLength 告诉诊断仪每次 TransferData 最多能发多少字节。这个值直接决定你的分块策略设小了刷写慢设大了 ECU 缓冲区可能溢出。4.2 0x36 传输数据与 0x37 退出传输块序号与超时处理0x36 请求格式36 [blockSequenceCounter] [data...]。blockSequenceCounter 从 0x01 开始每发一包递增到 0xFF 后回绕到 0x00。ECU 靠这个序号检测丢包和重复包。正响应76 [blockSequenceCounter]。0x37 请求格式37无参数。正响应77。发送 0x37 之前必须确保所有数据块都已成功传输否则 ECU 可能回 0x24requestSequenceError。刷写过程中最常见的翻车场景是 0x36 超时。ECU 在接收数据块时需要写入 Flash这个时间可能超过默认的 P2 超时。ECU 会先回 NRC 0x78responsePending诊断仪收到 0x78 后必须把超时延长到 P2*。如果你的诊断仪不支持 0x78 处理刷写必挂。4.3 0x31 例程控制startRoutine、stopRoutine 与 requestRoutineResults0x31 服务用于控制 ECU 内部的例程执行。请求格式31 [subFunction] [routineIdentifier_H] [routineIdentifier_L] [routineControlOptionRecord...]。子功能有三个0x01 启动例程0x02 停止例程0x03 请求例程结果。正响应71 [subFunction] [routineIdentifier_H] [routineIdentifier_L] [routineStatusRecord...]。routineStatusRecord 的内容由具体例程定义常见的是 1 字节状态码。刷写流程中0x31 常用于擦除 Flash、校验编程依赖、检查编程前提条件。比如发31 01 FF 00执行擦除例程ECU 返回71 01 FF 00 00表示擦除成功。如果返回71 01 FF 00 01说明擦除失败后续 0x34 请求下载会直接收到 NRC 0x22。例程控制的一个关键点是routineControlOptionRecord 是可选的但有些例程需要传入参数比如擦除地址范围。这个必须查 CDD 或 ODX 文件确认不能凭经验猜。5. 避坑与排查UDS 诊断中五个高频翻车场景5.1 现象发 0x22 读 DID 返回 7F 22 31原因requestOutOfRange。DID 不存在或者当前会话不支持该 DID。有些 DID 只在扩展会话下可读DefaultSession 下发直接拒绝。解决先确认 DID 是否在 CDD 文件里定义再确认当前会话模式。如果是会话问题先发 10 03 切扩展会话再读。5.2 现象0x27 安全访问第一次就返回 7F 27 36原因exceedNumberOfAttempts。ECU 内部有失败计数器之前的错误尝试还没被清除。这个计数器通常掉电不清零或者需要等待延迟时间到期。解决等待延迟时间一般 10 秒后重试。如果还是 0x36可能需要给 ECU 断电重启或者用特定例程复位失败计数器。5.3 现象0x36 传输数据过程中突然收到 7F 36 73原因wrongBlockSequenceCounter。blockSequenceCounter 不连续ECU 检测到丢包或序号跳变。解决检查发送端的计数器逻辑确保从 0x01 开始且每包递增。如果中间有重发重发时序号必须和原包一致不能递增。5.4 现象0x11 ECU 复位后后续请求全部超时原因ECU 复位需要时间期间不响应任何诊断请求。如果你在复位后立刻发下一条请求物理层可能还没初始化完成。解决0x11 正响应收到后等待至少 2 秒再发下一条请求。更稳妥的做法是循环发 3E 00 直到收到响应确认 ECU 恢复通信。5.5 现象0x19 读 DTC 返回的 DTC 数量比预期少原因statusMask 设置过严过滤掉了部分 DTC。或者 ECU 的 DTC 存储区已满新 DTC 覆盖了旧 DTC。解决先用 statusMask 0xFF 读全部 DTC确认总数。如果确实少了检查 ECU 的 DTC 存储容量和覆盖策略。有些 ECU 在 DTC 数量达到上限后会停止记录新故障。6. 从笔记到实战用 Python 脚本验证 UDS 服务响应的技巧笔记看完了怎么验证自己真的理解了我的习惯是写一个小脚本用 python-udsoncan 库模拟诊断仪对模拟 ECU 发几个典型服务请求看响应是否符合预期。下面这段代码演示了 0x10、0x22、0x27 三个服务的串联调用import udsoncan from udsoncan.connections import IsoTPSocketConnection from udsoncan.client import Client from udsoncan.services import DiagnosticSessionControl, ReadDataByIdentifier, SecurityAccess # 配置连接参数根据实际项目调整 config dict(udsoncan.configs.default_client_config) config[p2_timeout] 5 # P2 超时单位秒 config[p2_star_timeout] 10 # P2* 超时收到 0x78 后生效 config[security_algo] None # 安全算法由外部 DLL 或自定义函数提供 conn IsoTPSocketConnection(127.0.0.1, 13400) # DoIP 连接示例 with Client(conn, configconfig) as client: # 第一步切换到扩展会话 resp client.change_session(DiagnosticSessionControl.Session.extendedDiagnosticSession) print(f会话切换响应: {resp.service_data.session_echo}) # 第二步读取 VIN 码DID 0xF190 vin_resp client.read_data_by_identifier(0xF190) print(fVIN: {vin_resp.service_data.values[0xF190].decode(ascii)}) # 第三步安全访问需要自定义算法函数 # seed client.request_seed(0x01) # key calculate_key(seed) # 替换为实际算法 # client.send_key(0x02, key)这段代码的关键参数是p2_timeout和p2_star_timeout必须和 ECU 实际定义一致。security_algo需要替换成项目对应的 seed-key 算法通常是一个 DLL 或 Python 函数。DoIP 连接地址和端口也要按实际项目改。验证的时候按这个顺序走先确认 0x10 能切会话再确认 0x22 能读到已知 DID最后测 0x27 的 seed-key 流程。每一步的响应都打印出来和笔记里的格式定义对照。如果某一步返回 NRC直接查笔记里对应服务的 NRC 列表定位速度比翻标准原文快得多。从那以后我每次拿到新的 ECU 或者新的诊断需求都会先跑一遍这个三步验证流程确认基础通信和安全访问没问题再往下做刷写或标定。这个习惯帮我省了很多「以为是协议问题、其实是会话没切」的无效排查时间。希望帮到你。本文还有配套的精品资源点击获取