搞OBD诊断或者ECU开发的朋友,对UDS协议肯定不陌生。但说实话,0x19服务(ReadDTCInformation,读取诊断信息)里的子功能特别多,平时用的最多的可能就是0x02(按状态掩码读DTC)和0x04(读快照/扩展数据)。我今天想重点聊聊0x19服务里一个容易被忽略却又非常实用的子功能——0x06,也就是按记录号读取DTC扩展数据记录(ReportDTCExtendedDataRecordByRecordNumber)。
这个子功能什么意思呢?简单说,每个DTC除了故障码本身,还可以挂一堆“环境数据”,比如故障发生时的车速、水温、电压、累计里程等。这些数据按记录号(Record Number)一条条存着,0x06就是让你指定DTC和记录号,精准地把某一条捞出来。它比0x04(一次读全部)更省带宽、更灵活。我在做售后诊断仪和产线EOL检测工具时,0x06基本是必调的。这篇文章我会直接切入报文级解析,把请求响应格式、参数计算、NRC处理讲透,再把实战中踩过的坑一条条列出来,希望对正在做协议栈开发或者诊断功能测试的朋友有帮助。
1. 0x19服务的整体图景与0x06的定位
1.1 0x19服务的子功能家族
0x19服务在ISO 14229-1里是“读取诊断信息”服务,它的子功能非常多,从0x01到0x0E,后面还有扩展,每个子功能有不同的用途。我先把常用的几个列出来,大家感受一下0x06在整个家族里的位置。
- 0x01:报告DTC数量,按状态掩码统计有多少个DTC。
- 0x02:报告DTC列表,按状态掩码返回符合条件的DTC。
- 0x03:报告DTC快照数据(SnapShot Data),也就是故障发生瞬间的“冻结帧”。
- 0x04:报告DTC扩展数据记录(Extended Data Record),把一个DTC关联的全部扩展数据记录都读出来。
- 0x06:报告DTC扩展数据记录(按记录号),也就是指定DTC+指定记录号,读单条扩展数据。
- 0x0A:报告支持的DTC列表,以及每个DTC支持哪些快照、扩展数据记录号。
- 0x0B:报告第一个DTC快照,用于快速确认当前故障状态。
- 0x0C:报告第一个DTC扩展数据记录,同样用于快速访问。
你会发现,0x06和0x04很像,都是读扩展数据记录,差别就在“按记录号”这4个字。0x04是把某个DTC下所有扩展数据一股脑返回,而0x06是指定某一条返回。很多ECU的DTC会挂多条扩展数据,比如记录1是故障发生时的环境数据,记录2是故障消失时的环境数据,记录3可能是ECU内部状态。要是你用0x04,每次都得把这几条全读完,数据量上去了,协议栈处理时间也变长;用0x06就可以按需读取,效率高很多。
1.2 0x06子功能为什么值得单独拿出来讲
我在早期的项目里,一直用0x04读扩展数据,觉得够用了。后来做售后诊断工具,客户反馈某车型网络信号不好,诊断仪读DTC扩展数据经常超时。排查下来,问题就出在0x04一次性返回的数据量太大,CAN帧得分包发送,网络拥堵时会丢帧。后来改成用0x06按记录号读取,每次只拉一条,报文短了,稳定性明显好很多。
还有一个场景是OTA刷写之后做功能验证。刷写完需要确认某些DTC的状态和扩展数据是否符合预期,如果全量用0x04读,不仅慢,还得逐个解析;用0x06按记录号精准读取,脚本写起来也简单。更关键的是,0x06的请求格式很“干净”,就一行报文,特别适合设备端快速实现和调试。所以虽然0x04用得多,但从工程实践角度,0x06的“精准读取”能力是无可替代的。
2. 报文格式逐字节拆解
2.1 请求报文:“你说你要什么”
0x06子功能的请求报文格式是固定的,一共6个字节。我直接给出字节排列:
请求:19 06 DTC_High DTC_Middle DTC_Low RecordNumber其中:
- 19是服务ID(SID),表示这是读DTC信息的服务。
- 06是子功能,表示按记录号读取扩展数据。
- DTC_High、DTC_Middle、DTC_Low是目标DTC的三字节编码。
- RecordNumber是你要读取的记录号,1个字节。
这里最容易犯错的地方是DTC的字节序。ISO 14229里DTC定义是三字节的,比如P0101,对应的编码是0x01 0x01 0x01。很多工程师第一次看到0x01 0x01 0x01,会以为这是三个独立的字节,搞不清哪个是高、哪个是低。实际上,DTC编码的高字节(High Byte)在最前面,中间字节(Middle Byte)在中间,低字节(Low Byte)在最后。你从DTC码本里查到P0101,查表是0x010101,那么发送时就是0x01 0x01 0x01,这刚好是三字节顺序天然一致。但遇到P0204这种,就得查对应编码,不要想当然按ASCII拆。
RecordNumber的范围是0x00~0xFF。0x00通常是无效值,因为扩展数据记录号从1开始定义,如果ECU返回NRC 0x31(请求超出范围),先查你填的记录号是不是从1开始的。
给个实际例子:我要读取DTC P0101(0x010101)的记录号为1的扩展数据记录,那请求就是:
19 06 01 01 01 01就这么简单。很多OBD工具里显示的“Read DTC Extended Data Record by Record Number”底层就是这条报文。
2.2 正响应报文:“我给你你要的”
正响应的格式比请求要复杂一些,因为返回的数据长度不固定。ISO 14229-1规定的0x06正响应格式如下:
正响应:59 06 DTC_High DTC_Middle DTC_Low RecordNumber StatusOfDTC ExtendedDataRecord其中:
- 59是响应SID,等于请求SID(0x19)+ 0x40。
- 06是子功能,原样返回。
- DTC_High、DTC_Middle、DTC_Low:跟请求里的DTC相同,用于确认。
- RecordNumber:跟请求里的记录号相同。
- StatusOfDTC:这个字节表示DTC当前的状态,比如是否确认、是否历史故障、是否测试失败等,每一位定义见ISO 14229-1的DTC状态掩码。
- ExtendedDataRecord:长度不固定,内容由ECU厂商定义,通常包含故障发生时的各种环境数据。
这里有一个重点:响应里的ExtendedDataRecord到底有多长?协议栈不会主动告诉你这个字段的长度,而是通过响应报文的整体长度来隐式表示。比如你收到一帧CAN诊断报文(通常最多8字节),或者多帧ISO-TP传输的连续帧,最后解析出来59 06 01 01 01 01 DTC状态 + 后面一堆数据字节,剩下的全是ExtendedDataRecord。所以你在做解析时,不能只按固定长度去截,而是要先解析出DTC、记录号和状态字节,后面剩下多少字节就是多少。这一点,在我做CAPL测试脚本的时候踩过坑,原因就是提前写死了长度。
再补充一点,DTC状态字节(StatusOfDTC)是0x04、0x06子功能响应中都带有的,它是故障码当前状态的实时体现。比如它是0x20(已确认当前存在故障)还是0x08(测试通过但历史存在),能直接辅助判断故障是否真的存在,这个比单纯看故障码本身要可靠得多。
2.3 负响应:NRC码背后的含义
如果诊断仪发出的请求,ECU没法正常处理,ECU会回负响应。负响应报文格式是:
7F SID NRC其中SID是0x19,NRC(Negative Response Code,负响应码)告诉你“为什么不行”。0x06子功能常见的NRC有下面几种:
| NRC | 含义 | 常见触发原因 |
|---|---|---|
| 0x11 | 服务不支持 | ECU压根没实现0x19服务,或者此服务只在某些诊断会话下可用 |
| 0x12 | 子功能不支持 | ECU实现了0x19服务,但没实现0x06子功能(比如有些老ECU只有0x01~0x04) |
| 0x13 | 报文长度错误 | 请求报文字节数不对,少于或多于6字节 |
| 0x22 | 条件不满足 | 当前ECU状态不允许读取扩展数据,比如需要进入编程会话或安全解锁状态 |
| 0x31 | 请求超出范围 | DTC不存在,或RecordNumber超过了该DTC拥有的记录数量,或RecordNumber为0 |
| 0x7F | 服务未在活动会话中支持 | 在默认会话(Default Session)下请求,但ECU限制该功能仅在扩展会话中可用 |
我调试过程中遇到最多的就是0x31。原因五花八门,有的是DTC编码查错了,有的是记录号没从1开始,有的是ECU把0号保留用于“全部记录”。还有一种情况是,DTC本身存在,但这个DTC当前没有生成扩展数据记录,所以ECU认为“你要的记录根本不存在”,也会回0x31。这个时候你最好先用0x0A子功能去查一下该DTC支持哪些记录号,再发起0x06请求。
3. 0x06与0x04的对比:别用错子功能
3.1 功能边界差异
0x04和0x06都能读DTC扩展数据记录。很多新手容易混淆,我干脆把两者的差异摊开了讲。
0x04的请求格式是:
19 04 DTC_High DTC_Middle DTC_Low响应格式是:
59 04 DTC_High DTC_Middle DTC_Low StatusOfDTC ExtendedDataRecord_1 ExtendedDataRecord_2 ...也就是说,0x04会把这个DTC下面所有记录号从1到N的扩展数据记录全部返回。如果ECU定义了3条记录,那响应里就会有3段数据,每段包含记录号、数据长度和数据内容(但具体每段的内部结构不同ECU不同)。0x06则只返回你指定的那一条。
这两个功能在ISO 14229-1的设计里其实是互补的:
- 0x04适合“全量审计”,比如下线检测时需要把故障关联的所有环境数据都拉取归档。
- 0x06适合“定向查询”,比如远程诊断或售后排查时,你明确知道要看记录2,那直接读记录2。
3.2 实际选型建议
实际工程选型,我给你几点建议:
第一,如果你在用诊断仪做常规读码,优先用0x02读DTC列表,再用0x06按需读扩展数据,不要一上来就0x04全量拉取。这样报文少、效率高,对CAN总线带宽压力也小。
第二,如果你的ECU定义了特别多的扩展数据记录,0x04一次响应可能会超过ISO-TP的连续帧能力(比如超过4095字节),导致某些工具解析异常。用0x06拆分请求,可以规避单帧响应过长的问题。
第三,0x04响应里的扩展数据记录具体怎么组织,在不同ECU平台上可能会有差异。有些ECU习惯在每段记录前面加一个记录号和长度,有些则直接拼接。相比之下,0x06响应里记录号是单独字段返回的,解析起来更明确、更不容易出错。从协议栈开发角度,0x06更好实现,因为不用自己去维护记录号的隐式偏移。
所以我的结论是:新项目做诊断功能设计时,优先保证0x06可用,并把记录号的定义整理成一份清晰的DBC或Excel表格;0x04可以作为辅助工具,给产线或售后用,但不要依赖它做精确解析。
4. 实战中的关键参数与配置细节
4.1 DTC格式和字节序的确认
前面提到DTC是三字节编码,但实际项目中有几个细节容易被坑到。
第一,DTC编码表不是“P0101直接就是0x010101”这么简单的。ISO 15031-6定义了DTC的标准编码方式,每个DTC对应一个三字节数值。比如P0101对应0x010101,这个没错;但P1234对应的编码可能是0x0234,具体要查表。千万不要把ASCII码“P0101”的字符值直接填进报文里,那是错的。做协议栈时,建议把所有DTC和扩展记录号的映射关系做成配置文件,启动时一次性加载,避免每次请求时查表出错。
第二,字节序在不同工具上显示方法不同。CANoe的Diagnostic Console里可能直接显示“DTC with 3 bytes”,而有的工具显示“0x010101”,有的显示“01 01 01”。本质是一样的。我习惯统一用十六进制字节序列表示,因为写CAPL脚本或Python脚本时,直接按字节组包,不容易错。
第三,DTC里还有一类“pending DTC”(待定DTC)和“permanent DTC”(永久DTC),它们在三字节编码上没有区别,区别在状态字节。0x06读扩展数据时,DTC编码一样,但状态字节不同,返回的扩展数据记录内容可能也不同。比如同一个P0301,第一次发生时记录1存储的是第一次故障的环境数据,第二次发生时记录5存储的是第二次的。所以不要以为“DTC相同,记录号相同,数据就永远一样”。
4.2 记录号的定义一致性
记录号(RecordNumber)是0x06的核心参数,但它很“个性”,因为ISO 14229-1并没有规定记录号的具体含义,只规定“每个DTC可以关联一个或多个扩展数据记录,每条记录有唯一记录号”。具体记录号代表什么,完全由ECU厂商自己定义。
我在某个项目里就遇到这种情况,厂商定义的记录号如下:
- 记录号1:故障发生时的环境数据(车速、转速、冷却液温度、进气温度、燃油修正值等)
- 记录号2:故障发生时的ECU内部电压值(5V、12V、参考电压等)
- 记录号3:故障发生时的行车里程
- 记录号4:故障消失时的环境数据
- 记录号5~8:保留给后续扩展
所以我在测试用例里,必须严格按这个定义去验证。如果测试人员不知道记录号含义,随手写了个记录号3,结果返回的是“行车里程”,而不是“电压”,就会误判产品问题。这里我强烈建议,项目启动时就要推动OEM或ECU供应商提供一份“DTC扩展数据记录号定义表”,并把它纳入软件开发配置库,而不是放在某个工程师的本地Excel里,不然换个人就找不到了。
另外,记录号是1个字节,范围0~255。0通常不可用。255有时候会被厂商定义为“所有记录”,但这不是ISO标准的强制要求,所以用之前一定要确认,别想当然。
4.3 扩展数据记录的内容组织方式
0x06正响应的ExtendedDataRecord字段,内容完全由厂商自定义,ISO 14229-1只建议了一些常用数据参数(比如ISO 14229-1 Annex D里列出的DID、DTC severity等)。有的ECU会把数据按DID形式组织,即每两个字节一个参数ID,后跟数据值;有的则直接按固定顺序拼接,一个字节一个参数。
举个例子,某ECU的扩展数据记录1内容如下:
0x01 0x02 0x03 0x04 0x05 ...厂商定义的参数顺序可能是:车速(2字节)、发动机转速(2字节)、冷却液温度(1字节)。那解析时就得按这个固定模板去拆。还有一种方式是带长度字段,比如:
0x01 0x02 0x04 0x1A 0x00 0x00 ...第一个字节表示参数ID或数据字段编号,第二个字节表示数据长度,后面跟数据。这种带自描述的结构,解析起来更安全,但占用的字节会多一些。
我的建议是,如果由你来设计这个扩展数据的格式,尽量采用“ID + Length + Data”的自描述格式,因为后续车厂需求变更多了,增加新参数不用改整个数据结构,解析端也能通过Length字段跳过未知字段,向前兼容性会好很多。如果你只是解析别人的ECU定义,那就老老实实按照对方提供的模板来,不要自创解析规则。
5. 踩坑实录与排查建议
5.1 常见问题速查表
我把在0x06调试过程中真实遇到的问题列成一个速查表,方便大家对照排查。
| 问题现象 | 可能原因 | 解决思路 |
|---|---|---|
| 请求后无响应 | 物理层没通、CAN ID不对、ECU处于busy状态 | 抓总线报文,确认请求发出去了;检查诊断ID和功能寻址/物理寻址配置 |
| 负响应0x12 | ECU没实现0x06子功能 | 用0x0A查支持的子功能列表;或更新ECU软件 |
| 负响应0x13 | 请求报文长度不对,少于或多于6字节 | 检查报文长度,尤其注意ISO-TP发送时不能多带填充字节 |
| 负响应0x31 | DTC或记录号非法 | 用0x0A确认DTC支持的记录号;确认DTC编码查表正确;记录号不能为0 |
| 负响应0x22 | 当前会话/状态不允许读取 | 先发10 03进入扩展会话;或完成安全解锁流程 |
| 响应解析出来的数据完全不对 | 扩展数据记录解析模板不对,字节序或参数顺序搞错 | 找供应商要最新的解析模板,按模板逐字段比对;用已知数据反推验证 |
| 数据总长度超出一帧CAN范围 | 使用了ISO-TP多帧传输 | 确认工具/驱动支持连续帧接收和重组;加大接收超时时间 |
5.2 排查思路和工具建议
真正的实战中,排查问题不能只靠肉眼看报文。我强烈建议手头准备一个CANoe或者PCAN加Wireshark的抓包环境。
第一步,抓CAN总线日志,确认请求报文有没有发出去。很多“无响应”问题其实是物理链路或者ID配置错了。你去看总线日志,干净的报文在总线上一定清清楚楚。
第二步,核对请求报文的每个字节。把请求报文逐字节和标准格式比对,重点看DTC顺序和RecordNumber。这个步骤能排除至少一半的0x31。
第三步,查ECU的软件需求文档。0x06的响应内容格式、每个记录号的含义、DTC编码表,这些在需求文档里一定有。如果文档缺失,就去找ECU供应商要,或者用0x0A子功能主动试探,看ECU自己怎么描述。
第四步,如果0x06读出来的数据和你预期不符,可以用0x04做交叉验证。0x04会返回所有记录,通过对比确认是不是单条记录解析的问题。不过要注意,0x04可能因为数据量太大导致抓包时间较长,别在总线负载高的场景下测试。
第五步,别忽视“时序”问题。有些ECU在DTC清除之后,扩展数据记录会被重置;如果刚清完DTC,紧跟着读0x06,很可能读到0xFF或空数据。所以测试流程里,建议先注入故障、触发DTC,再读扩展数据;确定数据稳定后再清DTC。
在上手0x06子功能的过程中,我个人的体会是:它的逻辑比很多看似复杂的服务更“小而美”,报文结构固定、响应明确,难点全在对DTC编码、记录号和扩展数据格式的约定理解上。做诊断开发,工具链顺手与否很重要,但真正决定进度的还是对自己负责的DTC定义和数据字典熟不熟。如果你刚开始接触0x06,最有效的做法就是先把ISO 14229-1里的0x19服务章节通读一遍,再拿着诊断仪在一台真实ECU上把所有子功能都试一遍,尤其是用0x0A去对比0x06能读到的记录号列表。踩过一次坑,后面就会顺手很多。