
提到UDS诊断服务$19 ReadDTCInformation几乎是所有ECU诊断实现里都绕不开的一个服务。它的任务就一个字读。读的对象是DTCDiagnostic Trouble Code诊断故障码也就是我们常说的故障码但要读的不只是故障码编号还有故障码状态、故障发生时刻的环境快照、老化计数这些扩展数据。这篇文章从协议原理讲到CANoe实操抓包解析再到我这些年实际踩过的坑适合正在做ECU软件开发、诊断测试DET、售后诊断工具支持的朋友也适合刚入门UDS、想搞懂$19到底是什么的工程师。先说结论$19不是一个只有“读故障码”这个动作的服务。它下面挂了十几种子功能每一种都在回答不同问题有多少故障码有哪些故障码故障发生瞬间发生了什么故障码严重程度多高如果只把$19当成一个读码命令很多诊断需求是做不好的。1. 先看懂$19在UDS诊断体系里的位置1.1 UDS诊断服务与$19的职责UDSUnified Diagnostic Services统一诊断服务是ISO 14229-1定义的一套应用层诊断协议平时我们讨论的“UDS诊断”一般就是指这一套标准。它跑在CAN、CAN FD、DoIP、LIN等不同总线上最常见的还是CAN承载的UDS也就是ISO 15765-2ISO-TP传输层上加UDS应用层协议。诊断仪和ECU之间通过一问一答的方式交互诊断仪发请求ECU回肯定响应或者否定响应。$19这个数字是服务IDSIDService Identifier全称ReadDTCInformation作用是读取与DTC相关的所有信息。和它配对的另一个服务是$14 ClearDiagnosticInformation负责清除DTC。一个负责读一个负责清这是故障诊断里最基本的闭环。$19的响应SID是0x59也就是请求SID加0x40。如果ECU因为某些原因没法执行请求就会回0x7F后面跟着0x19和对应的NRCNegative Response Code否定响应码。在整张诊断服务地图里$19属于“读取类”服务。同类里常用的还有$22 ReadDataByIdentifier按标识符读数据、$23 ReadMemoryByAddress按地址读内存等。$19特殊在它专门面向DTC不读普通传感器值也不操作IO它输出的内容是故障发生后的结果。换句话说$22回答的是“现在数据是多少”$19回答的是“出了什么故障、什么状态下出的、这个故障现在是什么状态”。调试一个偶发故障时$19的价值往往比$22还要大因为快照数据能还原故障发生瞬间的环境。1.2 为什么需要$19从OBD读码到DTC状态管理很多人第一次接触读故障码是在OBDOn-Board Diagnostics接口上。OBD标准里有模式$03读取已确认DTC和模式$07读取待定DTC但它面向的是排放和整车OBD需求查询维度很固定基本就是动力系统故障码。UDS $19不一样它把“读故障码”这件事做成了完整的查询框架。举个例子售后车间里一辆车报“发动机故障灯亮”但诊断仪读OBD模式$03可能只看到一个P0300多缸失火。这个时候UDS $19 02可以按状态掩码把当前ECU记忆的所有DTC翻出来包括正在测试失败、已确认、上次清除后失败、待确认等不同状态。你不仅能知道哪个缸失火还能知道这个DTC是刚失败还是已经确认过、有没有伴随的冻结帧数据Snapshot、有没有扩展记录Extended Data比如失败计数和第一次发生时间。这个设计思路是分层级的。ECE车辆诊断法规里要求持续监测排放相关故障诊断仪需要区分“当前正在发生的故障”“曾经发生的故障”和“历史存储但可能已恢复的故障”。没有状态掩码这种机制只靠一个二进制“有/无故障”基本没法支撑维修决策。$19把所有DTC状态用一个字节表达再用掩码选择性过滤诊断仪可以先查数量再查明细也可以直接按状态批量捞列表这种灵活性就是它和传统OBD读码方式的本质区别。2. $19子功能拆解你看到的0x01到0x0A都在干什么2.1 子功能全景与选择逻辑$19请求的第二字节是子功能SubFunction它决定了接下来读什么类型的数据。ISO 14229-1里定义的子功能覆盖很广查询数量、查询DTC列表、查询快照、查询扩展数据、按严重程度查、按测试状态查等等。实际量产ECU通常只实现一部分0x01、0x02、0x03、0x04是最常见的组合0x05到0x0A属于可选扩展很多项目会按需求裁剪。子功能名称作用常见程度0x01reportNumberOfDTCByStatusMask按状态掩码统计DTC数量几乎必选0x02reportDTCByStatusMask按状态掩码返回DTC列表几乎必选0x03reportDTCSnapshotRecord读取指定DTC的快照记录常见0x04reportDTCExtendedDataRecord读取指定DTC的扩展数据常见0x05reportDTCBySeverityMask按严重程度读取DTC可选0x06reportDTCByDTCAndMask按特定DTC编号和掩码查询可选0x07reportDTCBySeverityAndMask按严重程度加状态组合查询可选0x08reportSupportedDTC读取ECU支持的DTC列表可选较旧版本0x0AreportDTCByTestStatus按测试状态读取DTC新协议常见0x0BreportDTCExtendedAndSnapshotInfo混合查询快照和扩展数据较少见选择哪个子功能完全取决于需求。产线末端测试想知道这辆车下线时有没有DTC发一个$19 01 FF看数量是多少就够了数量不为0就是有问题。售后维修要定位故障发$19 02 FF拿全部DTC列表再用$19 03和$19 04取细节。开发阶段调试偶发故障重点用$19 0A按测试状态看DTC在每个诊断周期里的变化轨迹。2.2 核心子功能逐个解析$19 01和$19 02是一对一个统计数量一个返回明细。$19 01请求格式是19 01后面跟1字节DTC状态掩码。例如03 19 01 FF单帧PCI03意思是统计所有状态下DTC的数量。响应格式通常是59 01后跟DTCStatusAvailabilityMask、DTCFormatIdentifier和两字节的DTC数量。比如06 59 01 00 00 00 03最后的00 03就是“当前有3个DTC”。有的AUTOSAR协议栈在这个响应里第一个字节会填0xFF不要慌它表达的是支持的状态位集合解析的重点是最后两字节的数量字段。$19 02请求格式是03 19 02 FF响应则是59 02后直接挂DTC记录列表。每条DTC记录固定3字节结构是DTC高字节、DTC低字节、DTC状态字节。比如ECU回08 59 02 C0 01 28 C1 00 28PCI08表示后面还有8字节去掉59 02还剩6字节正好两条DTC记录第一条是C0 01 28第二条是C1 00 28。C0 01和C1 00是DTC编号最后的28是状态字节。这里没有DTC数量前缀解析时靠一个约定按3字节为单位切分直到CAN帧或报文序列结束。$19 03读取快照记录最常见的用途就是读冻结帧。请求格式是19 03后跟DTC高字节、DTC低字节、DTC状态掩码、快照记录号。比如07 19 03 C0 01 FF 01意思是读取DTC C0 01、状态掩码为FF、快照记录号01的环境数据。请求里的FF表示不管DTC处于什么状态只要它有快照就返回记录号01表示读第一条快照。响应是59 03后跟DTC记录、快照记录号、快照记录总条数、快照数据长度和快照数据内容。快照数据就是ECU在检测到故障瞬间卡住的整车环境变量比如发动机转速、车速、水温、蓄电池电压具体变量列表由ECU配置决定。2.3 按严重程度和测试状态读取的扩展玩法$19 05 reportDTCBySeverityMask提供了一个很有意思的维度按严重程度筛选。SeverityMask的定义包括bit0维护类maintenanceOnly、bit1建议近期检查checkAtNextManufacturing、bit2需要立即处理checkImmediately。比如一个故障码标记为“需要立即处理”它会影响行车安全另一个只是“提示保养”优先级就低很多。售后诊断应用里可以通过这个子功能先捞高优先级故障不用把所有DTC都倒出来再排序。$19 0A reportDTCByTestStatus是较新协议里常用的按诊断测试状态查询的玩法。它和0x02的区别在于0x02的掩码过滤的是DTC当前状态字节0x0A过滤的是诊断测试完成情况。用它可以筛选出哪些DTC本周期还没测、哪些已经测完但失败、哪些在清除后仍然失败。这个子功能对开发阶段调试“诊断种子测试”和执行DTC验证特别有用比如你改了一个故障判断逻辑想确认故障注入后DTC状态位有没有按预期流转$19 0A能更快看到测试过程里的状态变化而不是只看到最终结果。3. DTC状态掩码$19的灵魂3.1 三个字节讲清楚一个DTCDTC记录在UDS协议里固定3字节两个字节的DTC编号一个字节的DTC状态。对很多新手来说最容易忽略的恰恰是这第三个字节。同样是C0 01这个DTC如果状态字节是0x08表示它是一个已确认的故障如果状态字节是0x01表示它只是在当前诊断周期测试失败还没走到确认那一步。同一个DTC两种状态对应完全不同的维修处置方式。DTC编号本身也有编码规则。ISO 15031-6里定义了两字节DTC和传统OBD五位DTC编号的对应关系。比如P0300对应的两字节编码是0x03 0x00第一位标定了DTC所属系统P开头是动力系统C开头是底盘系统B开头是车身系统U开头是网络通信系统。不过在UDS的框架里ECU厂家也可以用内部自定义的DTC编号不一定严格映射到OBD规定的P/C/B/U序列。关键是要知道每个DTC在UDS世界里就是两个字节ID加一个状态字节这三个字节构成了诊断交互的最小单元。3.2 状态掩码的8个bit逐个掰开说DTC状态字节的每一个bit都有特定含义这是ISO 14229-1标准里定义的也是$19服务最核心的“字典”。bit名称含义bit0testFailed当前诊断测试失败bit1testFailedThisOperationCycle本次操作循环内测试失败bit2pendingDTC待确认DTC测试失败但未达到确认阈值bit3confirmedDTC已确认DTC达到确认阈值通常关联故障灯bit4testNotCompletedSinceLastClear自上次清除DTC后测试未完成bit5testFailedSinceLastClear自上次清除DTC后测试曾失败bit6testNotCompletedThisOperationCycle本次操作循环内测试未完成bit7warningIndicatorRequested请求点亮警告指示灯这8个bit之间不是互斥关系同一个DTC可以同时多个bit置1。比如一个历史上确认过、当前测试又失败的DTC状态字节可能是bit3bit0同时置1也就是0x09。一个刚刚达到确认阈值、诊断仪还没来得及点灯的DTC可能是bit2bit3都置1也就是0x0C。理解这8个bit的动态流转比记住请求格式更重要因为它决定了你读到列表后怎么解读。拿一个实际场景来说ECU对某个传感器回路每100ms做一次断路检测。第一次检测失败ECU内部诊断逻辑会把这个DTC的testFailed置1但因为失败次数还没到确认阈值confirmedDTC还是0pendingDTC可能是1。如果故障是偶发的下一次检测通过了testFailed会清掉但testFailedSinceLastClear会保持1。这个DTC就不再是“活跃故障”而是“历史上出过问题”的记录。维修技师看到这样的状态码思路应该是检查线束接触而不是上来就换传感器。3.3 掩码匹配逻辑按位与不等于0$19请求里的DTC状态掩码StatusMask不是严格的“相等匹配”而是“只要有任何一个bit都对上就返回”。这个规则很关键新手经常在这里栽跟头。假设ECU里有个DTC状态字节是0x0Abit1和bit3为1你用掩码0x08去查0x08与0x0A按位与结果是0x08不等于0这个DTC会被返回。用掩码0x04去查0x04与0x0A结果是0这个DTC不返回。所以掩码FF代表“所有状态位都参与匹配”实际上就是返回所有状态字节非零的DTC。掩码01只查正在失败的DTC掩码08只查已确认的DTC掩码04只查自上次清除后失败过的DTC。多个bit组合成掩码时可以跨状态查询比如掩码0x0A等于bit1和bit3同时参与匹配可以同时捞“本周期失败的”和“已确认的”两类DTC。这种按位与的非零匹配逻辑本质上是把状态过滤做成了“白名单OR”而不是“精确等于”大大提高了查询灵活性。4. 核心环节实操从发一帧$19请求到完整解析响应4.1 实操环境与准备工作我用得最多的组合是CANoe VN1640 一个支持UDS的ECU或者直接连实车OBD口。不管用什么工具第一步都是把物理层和传输层准备好。CAN通道波特率常见的有500kbit/s和250kbit/s以项目规范为准。物理寻址请求一般发到ECU特定的诊断CAN ID比如标准11位CAN ID框架下Tester到ECU是0x7E0ECU响应是0x7E8也可以走功能寻址0x7DF但功能寻址会触发总线上所有节点响应多ECU同时回复容易仲裁冲突所以我自己的习惯是用物理寻址逐ECU诊断只有做整车DTC扫描时才考虑功能寻址。准备阶段还要确认两件事。第一诊断会话是否允许执行$19。很多ECU在默认会话下允许读DTC但某些OEM会限制快照和扩展数据读取必须进扩展会话$10 03。第二ISO-TP的流控参数比如STmin站间最小时间和BlockSize多帧传输用得着。单帧请求一般不用考虑这些但读取大量DTC或快照时响应一定是多帧ECU发送连续帧时诊断仪需要按STmin节奏回复流控帧这些参数没配好就会丢帧。4.2 第一步用$19 01查数量$19 02捞DTC列表在CANoe的Send窗口里发单帧测试Tester到ECU发送ID为0x7E0CAN数据场内容03 19 01 FF。这里03是ISO-TP单帧PCI表示后面有3字节应用层数据19是SID01是子功能FF是状态掩码。ECU正常响应会从0x7E8回来一帧常见格式是06 59 01 00 00 00 03。这就是标准肯定响应“当前有3个DTC”。分析时先把PCI去掉剩下59 01 00 00 00 0359是响应SID01是回显的子功能后面四个字节里的前两字节是DTCStatusAvailabilityMask和DTCFormatIdentifier最后两字节00 03就是DTC数量。查数量确认有货以后再发$19 02 FF捞列表03 19 02 FF。假设ECU回应08 59 02 C0 01 28 C1 00 28。去掉PCI59 02后面是两条DTC记录C0 01 28和C1 00 28。C0 01和C1 00是DTC编号最后的28是状态字节。0x28换算回bit等于bit3和bit5置10x080x20也就是“已确认”且“自上次清除后失败过”。注意$19 02响应里每条记录固定3字节所以判断响应结束的规则很清楚从59 02后面开始每3字节切一条DTC记录切完为止。如果用一个带图形界面的诊断工具它会自动拆分但自己写脚本解析时这个固定长度切分逻辑非常重要。4.3 第二步用$19 03和$19 04读快照与扩展数据拿到DTC编号后就该深挖了。$19 03请求格式是19 03后跟DTC高字节、DTC低字节、StatusMask、快照记录号。比如想读DTC C0 01的第一条快照发送07 19 03 C0 01 FF 01。注意这里PCI变成07是因为应用层数据有6字节。响应可能很长单帧放不下就走ISO-TP多帧。假设响应展开后是59 03 C0 01 28 01 ...59 03是响应头和子功能C0 01是DTC编号28是DTC状态01是快照记录号后面接着快照数据。快照数据里所以存储的变量由ECU配置决定但一般至少包含发动机转速、车速、电池电压、冷却液温度这些常规环境变量。$19 04的请求结构和$19 03非常像只是最后一位从快照记录号换成了扩展数据记录号。例如07 19 04 C0 01 FF 01。扩展数据的内容通常是DTC的故障发生次数、老化计数器、第一次失败时间戳这类“元信息”。快照和扩展数据容易混淆我个人的理解是快照相当于故障现场的“照片”存的是故障瞬间的物理量扩展数据相当于ECU内部统计的“账本”存的是这个故障被记录了多少次、持续了多久。故障间歇性出现时光看快照可能判断不出来但扩展数据里的计数器增量会告诉你这个故障的复现频率这对复现偶发故障非常有价值。4.4 第三步结合$14验证清除逻辑实操里$19和$14几乎是绑定使用的一套组合动作。诊断流程经常是先用$19 02读DTC列表和分析状态然后维修处理最后用$14清DTC再重新跑一遍诊断测试验证故障是否解决。$14请求格式是04 14 FF FF FF三个FF表示清除所有DTC也可以按系统分组清除比如只清动力系统相关DTC请求里用具体的DTC组掩码。清除完成后刚才读到的状态字节应该全部归零。但有个最容易踩坑的点清除不等于永久消失。清除DTC只是把状态字节清零pendingDTC、confirmedDTC、testFailed这些位都会归零但如果故障原因没有解决下个诊断周期一到诊断测试会再次运行DTC会从testFailed重新开始积累再次达到确认阈值后confirmedDTC恢复置1。所以验证维修效果的正确步骤是清码→运行相关诊断测试条件→再读$19 01看数量是否为0。如果清完立刻读是干净的开了一段路后又出现同一DTC大概率是故障真实存在不是清除逻辑有问题。5. 常见问题与排查技巧实录5.1 否定响应NRC速查与处理$19请求被拒时ECU回的格式是7F 19 [NRC]。我整理了实际工作中遇到最多的几个NRC配合排查思路一起列出来。NRC含义常见触发原因排查方向0x11serviceNotSupportedECU根本实现了$19确认ECU诊断规范是不是用错了SID0x12subFunctionNotSupported该子功能未实现查ECU支持的子功能列表避免用0x0A在旧ECU上0x13incorrectMessageLengthOrInvalidFormat报文长度不对或数据格式错误检查PCI长度是否等于实际数据字节数0x14responseTooLong响应数据超过ECU或总线限制缩小查询范围或改用分页/多次查询策略0x31requestOutOfRangeDTC编号或记录号超范围确认DTC编号是ECU实际支持的快照记录号有没有超上限0x72generalProgrammingFailure内部处理异常查ECU诊断实现状态重新上电或切换会话后重试印象最深的一次是客户反馈$19 03请求永远返回0x31。排查后发现快照记录号写成了0x00但协议的记录号范围是0x01到0xFE0x00是保留值。直接把记录号改成0x01就通了。类似这种“格式对、语义错”的问题用抓包工具逐字节比对协议文档是效率最高的解决方式。5.2 超时、无响应与ISO-TP多帧的坑另一个高频问题是没有响应。请求发出去了CAN报文在Trace里也看到了但ECU就是不回。最先检查物理寻址ID是否正确比如ECU的响应ID可能不是默认的0x7E8而是根据配置偏移的地址然后检查会话安全某些子功能要求非默认会话或安全访问没做$27解锁就发请求ECU可能直接不理。再下来看通信参数标准P2ServerResponseTime默认是500ms但很多OEM规范会收紧到50ms如果诊断仪侧的P2超时设得太短ECU的响应还在路上诊断仪先报超时了。多帧传输的坑也多。$19 02返回大量DTC或$19 03返回一大包快照数据时ECU会发首帧FFPCI以0x10开头然后跟连续帧CFPCI以0x20开头。诊断仪收到首帧后必须回一个流控帧FCPCI以0x30开头否则ECU不会继续发。如果用CANoe做测试一般会自动处理ISO-TP如果自己用脚本操作CAN底层层收帧流控逻辑没写好就会看到首帧之后一片寂静。排查这种问题直接看Trace里有没有FF帧后面跟着ECU的连续帧没有就说明诊断仪侧的流控没回或者回得不对。另外流控帧里的STmin设太大也会拖慢整包数据的传输速度在强调实时性的产线测试里尤其注意。5.3 DTC状态和故障现象对不上多半是诊断周期问题调试中最常被问的问题是现象都出来了故障灯也亮了为什么$19 02读到DTC的状态字节不是0x08这就是没有理解DTC状态位的更新时间点。UDS读到的DTC状态不是实时刷新的它是ECU在每个诊断周期结束后更新一次的状态汇总。比如一个故障只持续了200ms这个周期里诊断测试检测到了状态位被置为testFailed但下一个操作循环开始前状态又会被翻转或保留“已失败过”的历史记录。所以读到的状态字节表达的是“截至当前诊断周期结束这个DTC处于什么状态”不是发送请求那一刻的实时状态。另外还有白名单过滤的坑。有些ECU出厂默认把所有DTC存在NVM里但只有部分DTC会在当前环境下参与测试。如果一个DTC的使能条件不满足比如某传感器诊断需要车速大于某个门限才能跑它的状态字节可能是0x00表示“存了这个DTC但没在测试”。这时$19 02 FF不会返回它因为状态字节和FF按位与的结果是0但$19 08ReportSupportedDTC能查到底层支持哪些DTC。诊断仪显示层面如果只简单过滤了状态为0的记录用户就会误以为ECU不认识这个DTC。生产环境下判断一个DTC是否真正“发生”过一定不能只看DTC列表里有没有它还要看状态字节的bit3、bit5这些历史记录位。6. 最后分享一点实操经验我个人体会最深的一点是诊断开发阶段手里一定要有一套能按字节层级显示CAN报文原始数据的工具不要依赖工具自动解析出来的“友好界面”。自动解析帮你把DTC号和状态都显示成可读文本后很容易忽略某些边界情况比如状态字节多了一个bit、DTC编号的高字节有特殊含义。自己手动解析几帧$19 01、$19 02响应以后对DTC记录结构和状态掩码的匹配规则会形成直觉这个直觉在线上问题排查时特别管用。另外一个实在的建议是写诊断测试脚本时把状态掩码先写成可配置参数不要硬编码0xFF。产线场景里经常要区分“当前下线车辆是否带任何故障”和“是否带已确认的故障”前者用0xFF后者用0x08。同一套脚本如果掩码写死后面推广到不同项目就很被动。$19是整个UDS体系里看似简单、实际信息密度最高的服务之一把它吃透再去理解$14清除、$2E写入相关配置和DTC生成条件会有一种一通百通的感觉。