提到UDS诊断服务,$19 ReadDTCInformation几乎是所有ECU诊断实现里都绕不开的一个服务。它的任务就一个字:读。读的对象是DTC(Diagnostic Trouble Code,诊断故障码),也就是我们常说的故障码,但要读的不只是故障码编号,还有故障码状态、故障发生时刻的环境快照、老化计数这些扩展数据。这篇文章从协议原理讲到CANoe实操抓包解析,再到我这些年实际踩过的坑,适合正在做ECU软件开发、诊断测试(DET)、售后诊断工具支持的朋友,也适合刚入门UDS、想搞懂$19到底是什么的工程师。先说结论:$19不是一个只有“读故障码”这个动作的服务。它下面挂了十几种子功能,每一种都在回答不同问题:有多少故障码?有哪些故障码?故障发生瞬间发生了什么?故障码严重程度多高?如果只把$19当成一个读码命令,很多诊断需求是做不好的。
1. 先看懂$19在UDS诊断体系里的位置
1.1 UDS诊断服务与$19的职责
UDS(Unified Diagnostic Services,统一诊断服务)是ISO 14229-1定义的一套应用层诊断协议,平时我们讨论的“UDS诊断”一般就是指这一套标准。它跑在CAN、CAN FD、DoIP、LIN等不同总线上,最常见的还是CAN承载的UDS,也就是ISO 15765-2(ISO-TP)传输层上加UDS应用层协议。诊断仪和ECU之间通过一问一答的方式交互:诊断仪发请求,ECU回肯定响应或者否定响应。
$19这个数字是服务ID(SID,Service Identifier),全称ReadDTCInformation,作用是读取与DTC相关的所有信息。和它配对的另一个服务是$14 ClearDiagnosticInformation,负责清除DTC。一个负责读,一个负责清,这是故障诊断里最基本的闭环。$19的响应SID是0x59,也就是请求SID加0x40。如果ECU因为某些原因没法执行请求,就会回0x7F,后面跟着0x19和对应的NRC(Negative Response Code,否定响应码)。
在整张诊断服务地图里,$19属于“读取类”服务。同类里常用的还有$22 ReadDataByIdentifier(按标识符读数据)、$23 ReadMemoryByAddress(按地址读内存)等。$19特殊在它专门面向DTC,不读普通传感器值,也不操作IO,它输出的内容是故障发生后的结果。换句话说,$22回答的是“现在数据是多少”,$19回答的是“出了什么故障、什么状态下出的、这个故障现在是什么状态”。调试一个偶发故障时,$19的价值往往比$22还要大,因为快照数据能还原故障发生瞬间的环境。
1.2 为什么需要$19:从OBD读码到DTC状态管理
很多人第一次接触读故障码,是在OBD(On-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属于可选扩展,很多项目会按需求裁剪。
| 子功能 | 名称 | 作用 | 常见程度 |
|---|---|---|---|
| 0x01 | reportNumberOfDTCByStatusMask | 按状态掩码统计DTC数量 | 几乎必选 |
| 0x02 | reportDTCByStatusMask | 按状态掩码返回DTC列表 | 几乎必选 |
| 0x03 | reportDTCSnapshotRecord | 读取指定DTC的快照记录 | 常见 |
| 0x04 | reportDTCExtendedDataRecord | 读取指定DTC的扩展数据 | 常见 |
| 0x05 | reportDTCBySeverityMask | 按严重程度读取DTC | 可选 |
| 0x06 | reportDTCByDTCAndMask | 按特定DTC编号和掩码查询 | 可选 |
| 0x07 | reportDTCBySeverityAndMask | 按严重程度加状态组合查询 | 可选 |
| 0x08 | reportSupportedDTC | 读取ECU支持的DTC列表 | 可选,较旧版本 |
| 0x0A | reportDTCByTestStatus | 按测试状态读取DTC | 新协议常见 |
| 0x0B | reportDTCExtendedAndSnapshotInfo | 混合查询快照和扩展数据 | 较少见 |
选择哪个子功能完全取决于需求。产线末端测试想知道这辆车下线时有没有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(单帧,PCI=03),意思是统计所有状态下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 28,PCI=08表示后面还有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 三个字节讲清楚一个DTC
DTC记录在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 | 名称 | 含义 |
|---|---|---|
| bit0 | testFailed | 当前诊断测试失败 |
| bit1 | testFailedThisOperationCycle | 本次操作循环内测试失败 |
| bit2 | pendingDTC | 待确认DTC,测试失败但未达到确认阈值 |
| bit3 | confirmedDTC | 已确认DTC,达到确认阈值,通常关联故障灯 |
| bit4 | testNotCompletedSinceLastClear | 自上次清除DTC后测试未完成 |
| bit5 | testFailedSinceLastClear | 自上次清除DTC后测试曾失败 |
| bit6 | testNotCompletedThisOperationCycle | 本次操作循环内测试未完成 |
| bit7 | warningIndicatorRequested | 请求点亮警告指示灯 |
这8个bit之间不是互斥关系,同一个DTC可以同时多个bit置1。比如一个历史上确认过、当前测试又失败的DTC,状态字节可能是bit3+bit0同时置1,也就是0x09。一个刚刚达到确认阈值、诊断仪还没来得及点灯的DTC,可能是bit2+bit3都置1,也就是0x0C。理解这8个bit的动态流转,比记住请求格式更重要,因为它决定了你读到列表后怎么解读。
拿一个实际场景来说:ECU对某个传感器回路每100ms做一次断路检测。第一次检测失败,ECU内部诊断逻辑会把这个DTC的testFailed置1,但因为失败次数还没到确认阈值,confirmedDTC还是0,pendingDTC可能是1。如果故障是偶发的,下一次检测通过了,testFailed会清掉,但testFailedSinceLastClear会保持1。这个DTC就不再是“活跃故障”,而是“历史上出过问题”的记录。维修技师看到这样的状态码,思路应该是检查线束接触,而不是上来就换传感器。
3.3 掩码匹配逻辑:按位与不等于0
$19请求里的DTC状态掩码(StatusMask)不是严格的“相等匹配”,而是“只要有任何一个bit都对上就返回”。这个规则很关键,新手经常在这里栽跟头。假设ECU里有个DTC状态字节是0x0A(bit1和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是0x7E0,ECU响应是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为0x7E0,CAN数据场内容:03 19 01 FF。这里03是ISO-TP单帧PCI,表示后面有3字节应用层数据;19是SID;01是子功能;FF是状态掩码。ECU正常响应会从0x7E8回来一帧,常见格式是06 59 01 00 00 00 03。这就是标准肯定响应:“当前有3个DTC”。分析时先把PCI去掉,剩下59 01 00 00 00 03,59是响应SID,01是回显的子功能,后面四个字节里的前两字节是DTCStatusAvailabilityMask和DTCFormatIdentifier,最后两字节00 03就是DTC数量。
查数量确认有货以后,再发$19 02 FF捞列表:03 19 02 FF。假设ECU回应:08 59 02 C0 01 28 C1 00 28。去掉PCI,59 02后面是两条DTC记录:C0 01 28和C1 00 28。C0 01和C1 00是DTC编号,最后的28是状态字节。0x28换算回bit,等于bit3和bit5置1(0x08+0x20),也就是“已确认”且“自上次清除后失败过”。注意,$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 | 含义 | 常见触发原因 | 排查方向 |
|---|---|---|---|
| 0x11 | serviceNotSupported | ECU根本实现了$19 | 确认ECU诊断规范,是不是用错了SID |
| 0x12 | subFunctionNotSupported | 该子功能未实现 | 查ECU支持的子功能列表,避免用0x0A在旧ECU上 |
| 0x13 | incorrectMessageLengthOrInvalidFormat | 报文长度不对或数据格式错误 | 检查PCI长度是否等于实际数据字节数 |
| 0x14 | responseTooLong | 响应数据超过ECU或总线限制 | 缩小查询范围,或改用分页/多次查询策略 |
| 0x31 | requestOutOfRange | DTC编号或记录号超范围 | 确认DTC编号是ECU实际支持的,快照记录号有没有超上限 |
| 0x72 | generalProgrammingFailure | 内部处理异常 | 查ECU诊断实现状态,重新上电或切换会话后重试 |
印象最深的一次是客户反馈$19 03请求永远返回0x31。排查后发现快照记录号写成了0x00,但协议的记录号范围是0x01到0xFE,0x00是保留值。直接把记录号改成0x01就通了。类似这种“格式对、语义错”的问题,用抓包工具逐字节比对协议文档是效率最高的解决方式。
5.2 超时、无响应与ISO-TP多帧的坑
另一个高频问题是没有响应。请求发出去了,CAN报文在Trace里也看到了,但ECU就是不回。最先检查物理寻址ID是否正确,比如ECU的响应ID可能不是默认的0x7E8,而是根据配置偏移的地址;然后检查会话安全,某些子功能要求非默认会话或安全访问,没做$27解锁就发请求,ECU可能直接不理。再下来看通信参数,标准P2(ServerResponseTime)默认是500ms,但很多OEM规范会收紧到50ms,如果诊断仪侧的P2超时设得太短,ECU的响应还在路上,诊断仪先报超时了。
多帧传输的坑也多。$19 02返回大量DTC,或$19 03返回一大包快照数据时,ECU会发首帧(FF,PCI以0x10开头)然后跟连续帧(CF,PCI以0x20开头)。诊断仪收到首帧后必须回一个流控帧(FC,PCI以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 08(ReportSupportedDTC)能查到底层支持哪些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生成条件,会有一种一通百通的感觉。