详解UDS CAN诊断:ReadDTCInformation Service(SID:0x19)
测试台架上,诊断仪发出
19 02 08,ECU 返回了一串很长的报文。有人把第一字节后的02当成 DTC 数量,把2F当成某个故障码,最后得出的结论自然完全错误。0x19 的难点不在服务号,而在于:同一个服务下有多组子功能,响应格式随子功能变化,DTC 状态字节还会受到监控周期、老化和掉电保存策略影响。本文按工程排查的顺序,把 0x19 的分层边界、子功能、状态掩码、CAN/ISO-TP 报文、时序、NRC 和 C 语言实现放在一起说明。示例中的 CAN ID、DTC 值、会话权限和存储器编号均为演示值,实际项目应以 ODX、诊断数据库和 ECU 诊断规范为准。
1. 先从一组可复现的抓包开始
假设 ECU 支持 ISO 14229-1 DTC 格式,DTCStatusAvailabilityMask 为 0x2F。诊断仪想读取所有 confirmedDTC 位置 1 的 DTC,发送 reportDTCByStatusMask:
| UDS 请求:19 02 08 |
其中 0x19 是 ReadDTCInformation 的服务标识符,0x02 是子功能,0x08 是 DTC 状态掩码,表示只匹配状态字节 bit 3 为 1 的记录。
假设 ECU 中有两个匹配项:DTC 0x080511 的状态为 0x2C,DTC 0xC12345 的状态为 0x08,应用层正响应可以写成:
| 59 02 2F 08 05 11 2C C1 23 45 08 |
方向:诊断仪 → ECU
CAN ID:0x7E0
CAN 数据区:03 19 02 08 00 00 00 00
说明:ISO-TP 单帧,UDS 长度 3
方向:ECU → 诊断仪
CAN ID:0x7E8
CAN 数据区:10 0B 59 02 2F 08 05 11
说明:ISO-TP 首帧,UDS 总长 11 字节,本帧携带 6 字节
方向:诊断仪 → ECU
CAN ID:0x7E0
CAN 数据区:30 00 00 00 00 00 00 00
说明:ISO-TP 流控,允许 ECU 继续发送
方向:ECU → 诊断仪
CAN ID:0x7E8
CAN 数据区:21 2C C1 23 45 08 00 00
说明:连续帧,剩余 5 字节有效
这里必须分三层看:
0x7E0和0x7E8是项目常见的物理寻址 CAN ID,不由 0x19 服务规定;03、10 0B、21是 ISO-TP PCI,分别表示单帧、首帧和序号为 1 的连续帧;- 从
19或59开始的内容才是 ISO 14229 应用层数据。
ECU 若只返回一条 DTC,应用层可能只有 7 字节:
| 59 02 2F 08 05 11 2C |
此时经典 CAN 可以使用单帧:
| CAN ID 0x7E8 07 59 02 2F 08 05 11 2C |
单帧的长度字段 07 只统计 UDS 应用层长度。这一帧由 1 字节 PCI 和 7 字节 UDS PDU 组成,恰好占满 8 字节 CAN 数据区,没有填充字节。上面的请求帧和连续帧末尾才有填充 00。本例采用经典 CAN、普通寻址、8 字节数据区,不能直接套用到扩展寻址或所有 CAN FD 配置。
2. 0x19 到底负责读取哪些信息
ISO 14229-1:2020 第 12.3 节把 ReadDTCInformation 放在 Stored data transmission functional unit 中。它读取的是服务器内已经维护的 DTC 信息,内容不只是一组 DTC 编号,还可以包含状态、快照、扩展数据、首次或最近一次故障记录等。
先按用途定位常用子功能,再查对应的请求和响应格式:
子功能:0x01
名称:reportNumberOfDTCByStatusMask
典型用途:统计匹配某个状态掩码的 DTC 数量
请求附加参数:DTCStatusMask
子功能:0x02
名称:reportDTCByStatusMask
典型用途:返回匹配状态掩码的 DTC 和状态
请求附加参数:DTCStatusMask
子功能:0x03
名称:reportDTCSnapshotIdentification
典型用途:列出已有快照记录的 DTC 和记录号
请求附加参数:无
子功能:0x04
名称:reportDTCSnapshotRecordByDTCNumber
典型用途:读取某个 DTC 的快照数据
请求附加参数:DTCMaskRecord、SnapshotRecordNumber
子功能:0x05
名称:reportDTCStoredDataByRecordNumber
典型用途:按记录号读取存储数据
请求附加参数:StoredDataRecordNumber
子功能:0x06
名称:reportDTCExtDataRecordByDTCNumber
典型用途:读取某个 DTC 的扩展数据
请求附加参数:DTCMaskRecord、ExtDataRecordNumber
子功能:0x07 / 0x08
名称:reportNumberOfDTCBySeverityMaskRecord / reportDTCBySeverityMaskRecord
典型用途:按严重程度和状态组合统计或读取
请求附加参数:DTCSeverityMask、DTCStatusMask
子功能:0x09
名称:reportSeverityInformationOfDTC
典型用途:读取指定 DTC 的严重程度与功能单元
请求附加参数:DTCMaskRecord
子功能:0x0A
名称:reportSupportedDTC
典型用途:返回 ECU 支持的全部 DTC 及状态
请求附加参数:无
子功能:0x0B / 0x0D
名称:reportFirstTestFailedDTC / reportMostRecentTestFailedDTC
典型用途:定位首次或最近一次测试失败的 DTC
请求附加参数:无
子功能:0x0C / 0x0E
名称:reportFirstConfirmedDTC / reportMostRecentConfirmedDTC
典型用途:定位首次或最近一次确认的 DTC
请求附加参数:无
子功能:0x14
名称:reportDTCFaultDetectionCounter
典型用途:读取当前 prefailed DTC
请求附加参数:无
子功能:0x15
名称:reportDTCWithPermanentStatus
典型用途:读取永久 DTC
请求附加参数:无
子功能:0x16
名称:reportDTCExtDataRecordByRecordNumber
典型用途:按扩展数据记录号查找 DTC
请求附加参数:ExtDataRecordNumber
子功能:0x17–0x19
名称:用户定义 memory 相关查询
典型用途:查询指定用户 DTC 存储区
请求附加参数:状态掩码、DTC、记录号、MemorySelection
子功能:0x1A
名称:reportDTCExtendedDataRecordIdentification
典型用途:查询支持某个扩展记录号的 DTC
请求附加参数:ExtDataRecordNumber
子功能:0x42、0x55、0x56
名称:WWH-OBD 或 readiness 查询
典型用途:排放法规相关场景
请求附加参数:功能组或 readiness 参数
本文先把通用状态查询和最常见的 DTC 详细数据查询讲清楚。0x17 之后的用户定义 memory、WWH-OBD 和 readiness 参数,必须结合项目或法规文档解释,不能把某个 ECU 的值域当成通用 UDS 规则。
3. 子功能字节的 bit 7 不能漏读
0x19 请求的第二个字节是 SubFunction。ISO 14229 的 SubFunction 字节通常由 bit 7 的 suppressPosRspMsgIndicationBit 和 bit 6~0 的 reportType 组成:
| bit 7 | bit 6~0 |
|---|---|
| suppressPosRspMsgIndicationBit | reportType |
例如:
| SubFunction 字节 | 含义 |
|---|---|
0x02 | 请求 reportDTCByStatusMask,正常等待正响应 |
0x82 | 同一子功能,但请求抑制正响应 |
0x0A | 请求 reportSupportedDTC |
0x8A | 请求 reportSupportedDTC,抑制正响应 |
0x19 使用带子功能的通用响应规则,不能由应用随意忽略 bit 7。物理寻址下,置位只抑制成功的正响应,错误请求仍按规则返回负响应;功能寻址还要执行第 8.7 节的部分 NRC 抑制规则。另一个例外是:一旦服务器发送过 7F 19 78,最终响应必须发送,即使请求中 bit 7 为 1。测试脚本如果直接用整个第二字节与 0x02 比较,会把 0x82 错误地判成“不支持的子功能”。正确做法是先取:
report_type = (uint8_t)(sub_function & 0x7Fu);suppress_positive = ((sub_function & 0x80u) != 0u);
4. 请求报文的长度由子功能决定
4.1 状态掩码查询:0x01 和 0x02
reportNumberOfDTCByStatusMask 与 reportDTCByStatusMask 的请求格式相同:
| 19 reportType DTCStatusMask |
字节:1
字段:ReadDTCInformation SID
示例:19
说明:固定为 0x19
字节:2
字段:SubFunction
示例:01 或 02
说明:bit 7 可作为抑制正响应位
字节:3
字段:DTCStatusMask
示例:08
说明:按位匹配 statusOfDTC
DTCStatusMask 的匹配规则是:
| (statusOfDTC & DTCStatusMask) != 0 |
因此 DTCStatusMask = 0x08 的意思是“只要 bit 3 为 1 就匹配”,不是“状态字节必须等于 0x08”。如果需要同时要求多个位都为 1,不能直接套用这条标准匹配规则,必须在测试工具或项目扩展规则中另行处理。
请求中不支持的状态位应忽略,参与筛选的有效掩码可写为 DTCStatusMask & DTCStatusAvailabilityMask。全零掩码或有效位全为零,结果应为空列表(0x02)或数量 0(0x01),不能仅因掩码包含不支持位就返回 0x31。先读 0x01 再读 0x02 也不是一次原子操作,两次查询之间状态可能变化,数量和列表不保证始终相等。
4.2 指定 DTC 或记录:固定 3 字节高位在前
涉及 DTCMaskRecord 的子功能,DTC 请求格式通常是:
| 19 reportType DTCHighByte DTCMiddleByte DTCLowByte recordNumber |
例如读取 DTC 0x080511 的扩展记录 0x12:
| 19 06 08 05 11 12 |
DTCMaskRecord 只允许填写一个具体 DTC 值,不能把“故障组”作为通配符。DTC 的三字节编码方式由 DTCFormatIdentifier 说明,可以采用 SAE J2012-DA、ISO 14229-1、SAE J1939 -73 或其他标准定义的格式。发送顺序始终是高字节、中间字节、低字节,不能因为 MCU 内部结构体采用小端就倒序发送。
4.3 Snapshot、StoredData 和 ExtendedData 的记录号不是同一个地址空间
下面三个记录号经常被误用:
- DTCSnapshotRecordNumber:快照记录号,
0xFF可请求该 DTC 的全部快照记录; - DTCStoredDataRecordNumber:存储数据记录号,
0xFF可请求全部存储数据记录; - DTCExtDataRecordNumber:扩展数据记录号,
0xFF请求全部扩展记录,0xFE请求 OBD 扩展记录范围0x90~0xEF。
ISO 14229-1 明确指出这些记录号不共享地址空间。不能把快照记录号 0x12 直接当作扩展记录号 0x12 来解释,项目诊断数据库需要分别定义每一种记录的内容和长度。
4.4 用户定义 memory 的 MemorySelection
0x17、0x18、0x19 等用户定义 memory 子功能会在请求中追加 MemorySelection。例如:
| 19 17 08 01 |
这里的 08 是状态掩码,01 是 MemorySelection。01 代表哪个存储区不是 ISO 14229-1 的统一规定,可能是 RAM 影子区、历史故障区、法规专用区或项目自定义 Flash 分区。ECU 如果不支持该 memory,应按 0x31 的范围错误处理。
5. 正响应要先识别子功能,再解析后续字段
所有 0x19 正响应使用 0x59 作为服务 SID,但第二个字节仍然是 reportType。不能只判断 response[0] == 0x59 就认为解析成功。
5.1 0x01:数量响应包含格式标识符
请求:
| 19 01 08 |
正响应:
| 59 01 DTCStatusAvailabilityMask DTCFormatIdentifier DTCCountHigh DTCCountLow |
示例:
| 59 01 2F 01 00 02 |
字节:1
字段:正响应 SID
示例:59
说明:0x19 + 0x40
字节:2
字段:reportType
示例:01
说明:回显请求子功能
字节:3
字段:DTCStatusAvailabilityMask
示例:2F
说明:ECU 支持用于掩码的状态位
字节:4
字段:DTCFormatIdentifier
示例:01
说明:示例为 ISO 14229-1 DTC 格式
字节:5~6
字段:DTCCount
示例:00 02
说明:2 字节无符号数量,高字节在前
DTCStatusAvailabilityMask 只表示服务器支持哪些状态位,不表示当前有哪些 DTC。DTCFormatIdentifier 说明三字节 DTC 的解码方式,工具必须根据它选择显示格式。
5.2 0x02、0x0A 等列表响应由 4 字节 DTCAndStatusRecord 组成
reportDTCByStatusMask、reportSupportedDTC、首次/最近一次故障和永久 DTC 等子功能的公共结构是:
| 59 reportType DTCStatusAvailabilityMask |
| [DTC_H DTC_M DTC_L statusOfDTC]... |
示例:
| 59 02 2F 08 05 11 2C C1 23 45 08 |
从第 4 个字节开始,每 4 字节是一条记录:
记录:1
DTC:08 05 11
statusOfDTC:2C
解析:bit 5、bit 3、bit 2 为 1,符合 confirmedDTC 筛选
记录:2
DTC:C1 23 45
statusOfDTC:08
解析:confirmedDTC 为 1
如果没有匹配的 DTC,正响应可以只包含 59 reportType DTCStatusAvailabilityMask,后面没有记录。不要把“没有记录”误判成 NRC 0x31;只要请求参数有效,空结果仍然是正响应。
ISO 14229-1 还要求,同一个 DTC 值通常不能在一条正响应中重复出现;读取多个快照记录时是例外,因为同一个 DTC 可能拥有多个不同的快照记录号。
5.3 0x03:SnapshotIdentification 只给记录索引
响应中的每一项包含 3 字节 DTC 和 1 字节 DTCSnapshotRecordNumber:
| 59 03 |
| DTC_H DTC_M DTC_L SnapshotRecordNumber |
| DTC_H DTC_M DTC_L SnapshotRecordNumber |
| ... |
它不返回快照中的电压、转速或温度。读取实际数据时,客户端应先拿到记录号,再发送 19 04 + DTCMaskRecord + SnapshotRecordNumber。如果一个 DTC 发生过多次,ECU 可以为每次发生保留不同记录号,具体数量和“发生一次”的判定属于项目定义。
5.4 0x04:快照记录中带 DataIdentifier 和数据
请求读取 DTC 0x080511 的全部快照:
| 19 04 08 05 11 FF |
一个简化的正响应示例:
| 59 04 08 05 11 2C 01 02 12 34 2A 12 34 12 35 00 64 |
字段的工程解析方式如下:
字段:DTCAndStatusRecord
示例:08 05 11 2C
说明:DTC + statusOfDTC
字段:SnapshotRecordNumber
示例:01
说明:本条快照记录号
字段:NumberOfIdentifiers
示例:02
说明:后续有两个 DataIdentifier/data 组合
字段:DataIdentifier
示例:12 34
说明:本例项目分配的 DID,数据长度 3 字节
字段:snapshotData
示例:2A 12 34
说明:长度由数据字典定义
字段:DataIdentifier
示例:12 35
说明:本例第二个项目 DID,数据长度 2 字节
字段:snapshotData
示例:00 64
说明:第二段数据
示例只展示结构,不代表所有 ECU 都使用这些 DID 或长度。同一快照记录可以包含多组 dataIdentifier/snapshotData,真正的长度必须从 ODX、诊断数据库或 ECU 接口定义中取得。
5.5 0x06:扩展数据记录可以一次返回多条记录
读取 DTC 0x080511 的扩展记录 0x12:
| 19 06 08 05 11 12 |
典型响应结构:
| 59 06 08 05 11 2C 12 <extendedData bytes> |
当记录号为 0xFF 时,ECU 可以在一次响应中返回该 DTC 的全部扩展记录;每条记录包含记录号和对应数据。0xFE 请求 0x90~0xEF 的 OBD 扩展记录;如果 ECU 不支持该范围的任何记录,应返回 0x31。具体记录的内容和长度仍需查项目定义,不能假设每条扩展记录等长。
5.6 0x0B~0x0E:首次/最近一次记录没有 DTC 时仍可正响应
这些子功能只返回一个 DTCAndStatusRecord,或者在没有符合条件的记录时只返回状态可用掩码:
| 59 0D DTCStatusAvailabilityMask |
如果读取的是最近一次测试失败 DTC,记录的保留周期与 confirmedDTC 的老化过程不是一回事。确认位已经因为老化清零,并不自动表示“最近一次失败记录”也被删除;成功执行 0x14 后,首次/最近一次记录才按标准清除,其他记录策略仍可能受项目存储策略影响。
6. DTCStatusMask 和 statusOfDTC 的八个位
DTC 状态字节的位定义在 ISO 14229-1 附录 D.2。常用解析表如下:
Bit:0
名称:testFailed
语义重点:最近一次完整测试是否失败
成功执行 0x14 后的标准初始值:0
Bit:1
名称:testFailedThisOperationCycle
语义重点:当前操作周期内是否曾失败
成功执行 0x14 后的标准初始值:0
Bit:2
名称:pendingDTC
语义重点:当前或上一个完成的操作周期是否报告失败
成功执行 0x14 后的标准初始值:0
Bit:3
名称:confirmedDTC
语义重点:是否达到确认阈值并确认存储
成功执行 0x14 后的标准初始值:0
Bit:4
名称:testNotCompletedSinceLastClear
语义重点:清除后是否还没有完成一次测试
成功执行 0x14 后的标准初始值:1
Bit:5
名称:testFailedSinceLastClear
语义重点:清除后是否曾出现失败测试
成功执行 0x14 后的标准初始值:0
Bit:6
名称:testNotCompletedThisOperationCycle
语义重点:当前操作周期是否还没有完成测试
成功执行 0x14 后的标准初始值:1
Bit:7
名称:warningIndicatorRequested
语义重点:是否请求点亮相关警告指示
成功执行 0x14 后的标准初始值:通常为 0,失效保护策略可影响
因此,清除后常见的状态字节是 0x50,并不一定是 0x00。0x19 的验证脚本应根据状态位语义判断,不要写死“读取到 0x00 才算成功”。
状态位的更新还受到下面几个工程条件影响:
- operation cycle 的起止由 ECU 或法规场景定义;
- confirmedDTC 的确认阈值和 aging 阈值由车辆制造商或法规定义;
- testNotCompleted 位需要等监控真正运行并完成,不能因为发送了 0x19 就自动清零;
- warningIndicatorRequested 与故障策略、灯控和 failsafe 状态有关,清除 DTC 不一定立即关闭所有外部指示。
7. CAN、ISO-TP 与 UDS 的边界怎么排查
7.1 单帧请求的长度字段
对 19 01 08,经典 CAN 的单帧写法是:
| 0x7E0: 03 19 01 08 00 00 00 00 |
03 表示后面有 3 个 UDS 字节,不能把 CAN 数据区的 8 字节都当作 0x19 请求。诊断工具如果启用了 CAN FD,单帧 PCI 和填充空间会变化,但 UDS 应用层仍然是 19 01 08。
7.2 多帧响应的边界
以 19 02 返回两条 DTC 为例:
| 应用层:59 02 2F 08 05 11 2C C1 23 45 08 |
| 首帧: 10 0B 59 02 2F 08 05 11 |
| 流控: 30 00 00 00 00 00 00 00 |
| 连续帧:21 2C C1 23 45 08 00 00 |
诊断客户端应先由 ISO-TP 层完成重组,再把完整 UDS PDU 交给 0x19 解析器。不要在每个 CAN 帧到达时直接按“第 4 字节是 DTC 高字节”解析,否则首帧中的 PCI 和长度字段会把结果整体错位。
7.3 物理寻址和功能寻址
物理寻址通常是一请求一响应,适合读取某个 ECU 的完整 DTC 列表。功能寻址可能让多个 ECU 同时返回 0x59,测试脚本需要:
- 按 CAN ID 或网络节点区分响应来源;
- 为每个 ECU 单独执行 ISO-TP 重组和超时计时;
- 不能收到第一个正响应后就停止等待;
- 对 0x02、0x0A 这类长列表考虑总线带宽和响应队列。
功能寻址下,0x11、0x12、0x31、0x7E、0x7F 等通用规则指定的负响应应被抑制(发送过 0x78 后的最终响应规则另论)。因此,某个节点没有响应,不能直接判断为无 DTC 或链路故障。其他应发送的负响应与正响应需要按节点分别记录。长响应的 FF/FC/CF 通道和并发能力,应按 ISO 15765-2、ISO 14229-3 及网络配置验证;需要完整列表时优先使用物理寻址逐 ECU 查询。
8. 服务时序:P2、P2* 和分页读取
0x19 的查询可能需要扫描 RAM 中的 DTC 索引、读取 NVM 缓存、拼接快照数据并通过 ISO-TP 发送长响应。服务时序可以抽象为:
服务时序与职责
| 阶段 | 执行与边界 |
|---|---|
| 01 完整请求 | ISO-TP 重组完成,将 19 + 子功能 + 参数交给诊断服务。 |
| 02 条件检查 | 诊断框架检查会话、安全及连接状态,服务处理器校验长度与参数。 |
| 03 查询数据 | DEM 或项目 DTC 模块取得稳定索引;需要时通过 NvM 读取持久化数据。 |
| 04 处理挂起 | 无法在当前窗口内准备响应时,框架按规则安排 7F 19 78,客户端转入 P2*Client 等待。 |
| 05 开始响应 | ECU 在相应 P2/P2* 窗口内开始发送 59 正响应或最终负响应。 |
| 06 完成传输 | 长响应先发送 FF,诊断仪发送 FC,ECU 发送 CF;传输由 ISO-TP 计时器监督。 |
实现上要区分三类超时:
- ISO-TP N_As、N_Bs、N_Cr 等传输层超时,负责 CAN 帧和流控;
P2Server:完整请求接收后,到首个响应开始发送的服务器时间窗口;P2*Server:发送0x78 responsePending后,到下一次响应开始发送的扩展时间窗口。
当 DTC 数据还没有准备好时,服务器可以按照 8.7 的通用行为返回:
| 7F 19 78 |
测试脚本收到 0x78 后切换到 P2*Client 等待;再次收到合法 0x78 时重新装载扩展等待窗口,期间不要重发原请求。首帧在窗口内开始,并不要求所有连续帧都在 P2 内收完,后续 ISO-TP 传输由相应 N 系列计时器监督。客户端时间预算还需包含传输和调度裕量,不能直接把 P2Server_max 当作 P2Client。服务器数值通常来自 0x10 的会话参数;项目可以另外设置总事务期限以终止异常长时间挂起,这个期限不是标准统一的毫秒值。
读取大量 DTC 时,ECU 可能采用分页缓冲或快照式索引。ISO 14229-1 说明,如果构造响应期间匹配的 DTC 数量减少,分页响应中可以使用 DTC 000000、状态 00 填充,客户端应把它们视为“不存在的记录”。客户端不能把这个占位记录当成真实 DTC。
9. NRC 排查顺序
ISO 14229-1 第 12.3.4 节规定了 0x19 的服务级负响应码:
NRC:0x12
名称:subFunctionNotSupported
标准触发条件:子功能不支持
工程排查重点:去掉 bit 7 后核对 reportType 与 ECU 配置
NRC:0x13
名称:incorrectMessageLengthOrInvalidFormat
标准触发条件:请求长度错误
工程排查重点:按子功能逐字节检查长度和记录号
NRC:0x31
名称:requestOutOfRange
标准触发条件:DTC、功能组、记录号或 MemorySelection 不被识别
工程排查重点:查 ODX、DTC 数据库、memory 配置和值域
以下三种现象要分开:
7F 19 12:服务号正确,但 ECU 不实现该子功能;7F 19 13:字段长度或格式错误,通常还没进入 DTC 查询;7F 19 31:格式可解析,但某个 DTC、记录号或存储区不在支持范围。
如果返回 7F 19 78,它属于通用 responsePending 行为,应结合 P2/P2* 继续等待。它不是 Table 336 中针对参数范围的 0x31。
推荐固定按以下顺序排查:
响应排查路径
| 观察到的报文 | 下一步 |
|---|---|
| 59 + reportType | 严格验证回显子功能、最小长度及完整记录;再核对状态掩码、记录格式和 DTC 去重。 |
| 7F 19 12 | 检查子功能值及 bit 7 的拆分;区分物理和功能寻址响应规则。 |
| 7F 19 13 | 按对应子功能重新核对请求长度与参数位置。 |
| 7F 19 31 | 检查 DTC、记录号、MemorySelection 是否在项目支持范围。 |
| 7F 19 78 | 保持原事务,装载 P2*Client 继续等待;不要重发原请求。 |
| 其他或无响应 | 核对通用 NRC、功能寻址抑制、连接路由和 CAN/ISO-TP 超时证据。 |
10. C 语言客户端:解析和等待分开处理
下面实现物理寻址的 19 02 mask。ISO-TP 适配层只收发完整 UDS PDU;客户端负责 P2Client、P2*Client 和 0x78,不会在挂起期间重发请求。64 条记录是本例的资源上限,超限明确报错,不能截断后仍返回“成功”。
10.1 接口和解析器
#include <stdbool.h>#include <stddef.h>#include <stdint.h> #define DTC_CAPACITY 64u typedef enum { DTC_OK = 0, DTC_ARGUMENT, DTC_PROTOCOL, DTC_OUTPUT_FULL, DTC_TRANSPORT, DTC_P2_TIMEOUT, DTC_P2STAR_TIMEOUT, DTC_TOTAL_TIMEOUT, DTC_NEGATIVE} DtcResult; typedef struct { uint32_t number; uint8_t status; } DtcRecord;typedef struct { uint8_t availability; DtcRecord records[DTC_CAPACITY]; size_t count;} DtcList; static DtcResult ParseList(const uint8_t *p, size_t n, uint8_t request_mask, DtcList *out){ size_t count = 0u; if ((p == NULL) || (out == NULL)) return DTC_ARGUMENT; out->count = 0u; /* 错误返回时,调用者不得消费部分结果。 */ if ((n < 3u) || (p[0] != 0x59u) || (p[1] != 0x02u) || (((n - 3u) % 4u) != 0u)) return DTC_PROTOCOL; for (size_t i = 3u; i < n; i += 4u) { uint32_t dtc = ((uint32_t)p[i] << 16) | ((uint32_t)p[i + 1u] << 8) | p[i + 2u]; uint8_t status = p[i + 3u]; if ((dtc == 0u) && (status == 0u)) continue; /* 分页占位 */ if ((dtc == 0u) || ((status & (uint8_t)~p[2]) != 0u) || ((status & request_mask & p[2]) == 0u)) return DTC_PROTOCOL; for (size_t j = 0u; j < count; ++j) { if (out->records[j].number == dtc) return DTC_PROTOCOL; } if (count == DTC_CAPACITY) return DTC_OUTPUT_FULL; out->records[count].number = dtc; out->records[count].status = status; ++count; } out->availability = p[2]; out->count = count; return DTC_OK;} typedef enum { TP_PDU, TP_TIMEOUT, TP_TOO_LARGE, TP_ERROR } TpRx;typedef struct { void *ctx; bool (*send)(void *, const uint8_t *, size_t); TpRx (*receive)(void *, uint8_t *, size_t, size_t *, uint32_t); uint32_t (*now_ms)(void *); void (*finish)(void *);} DtcIo; typedef struct { uint32_t p2_client_ms; uint32_t p2star_client_ms; uint32_t total_ms; /* 项目事务期限,包含传输和等待。 */} DtcTiming;
receive 的最后一个参数限制“等待响应开始”的时间。收到首帧后由 ISO-TP 层继续监督 N 系列计时器;它必须检查 PCI、序号、总长度和缓冲区容量。返回 TP_PDU 时,n 不含 PCI 和填充。底层不得越界写入;上层再检查返回长度也不能补救已经发生的内存破坏。finish 负责释放连接占用,并清理超时事务的残留接收状态,避免迟到响应串入下一请求。
10.2 收到 0x78 后继续等待
static DtcResult WaitList(const DtcIo *io, const DtcTiming *t, uint8_t mask, DtcList *out, uint8_t *nrc){ uint8_t response[3u + 4u * DTC_CAPACITY]; uint32_t start = io->now_ms(io->ctx); uint32_t stage_start = start; uint32_t window = t->p2_client_ms; bool pending = false; for (;;) { uint32_t now = io->now_ms(io->ctx); uint32_t elapsed = now - start; uint32_t stage_elapsed = now - stage_start; if (elapsed >= t->total_ms) return DTC_TOTAL_TIMEOUT; if (stage_elapsed >= window) return pending ? DTC_P2STAR_TIMEOUT : DTC_P2_TIMEOUT; uint32_t wait = window - stage_elapsed; uint32_t remaining = t->total_ms - elapsed; if (wait > remaining) wait = remaining; size_t n = 0u; TpRx rx = io->receive(io->ctx, response, sizeof(response), &n, wait); if (rx == TP_TIMEOUT) { if ((io->now_ms(io->ctx) - start) >= t->total_ms) return DTC_TOTAL_TIMEOUT; return pending ? DTC_P2STAR_TIMEOUT : DTC_P2_TIMEOUT; } if (rx == TP_TOO_LARGE) return DTC_OUTPUT_FULL; if (rx != TP_PDU) return DTC_TRANSPORT; if ((n == 0u) || (n > sizeof(response))) return DTC_PROTOCOL; if ((io->now_ms(io->ctx) - start) >= t->total_ms) return DTC_TOTAL_TIMEOUT; if (response[0] == 0x7Fu) { if ((n != 3u) || (response[1] != 0x19u)) return DTC_PROTOCOL; if (response[2] != 0x78u) { *nrc = response[2]; return DTC_NEGATIVE; } pending = true; window = t->p2star_client_ms; stage_start = io->now_ms(io->ctx); continue; /* 不重新发送 19 02 mask。 */ } return ParseList(response, n, mask, out); }} DtcResult ReadDtcList(const DtcIo *io, const DtcTiming *t, uint8_t mask, DtcList *out, uint8_t *nrc){ const uint8_t request[] = {0x19u, 0x02u, mask}; if ((io == NULL) || (t == NULL) || (out == NULL) || (nrc == NULL) || (io->send == NULL) || (io->receive == NULL) || (io->now_ms == NULL) || (io->finish == NULL) || (t->p2_client_ms == 0u) || (t->p2star_client_ms == 0u) || (t->total_ms == 0u) || (t->p2_client_ms >= UINT32_C(0x80000000)) || (t->p2star_client_ms >= UINT32_C(0x80000000)) || (t->total_ms >= UINT32_C(0x80000000))) return DTC_ARGUMENT; out->count = 0u; *nrc = 0u; if (!io->send(io->ctx, request, sizeof(request))) { io->finish(io->ctx); return DTC_TRANSPORT; } /* send 返回成功表示请求已发送完成,此时开始 P2Client。 */ DtcResult result = WaitList(io, t, mask, out, nrc); io->finish(io->ctx); return result;}

now_ms 使用单调递增的 32 位毫秒计数,减法允许一次回绕;本例所有预算均小于半个计数周期,适配层还必须保证接收调用在有限时间内返回。DTC_OUTPUT_FULL 与报文格式错误不同,调用方应扩大资源或改用项目允许的读取策略,不能当成“ECU 没有更多故障”。若使用操作系统任务,应将收发改造成事件驱动,避免阻塞主循环。
本接口每个物理连接同时只允许一个事务。总事务期限、缓冲区大小和连接锁属于项目实现;P2* 窗口属于诊断时序。调用方分别记录 DTC_NEGATIVE 的 NRC、P2 超时、扩展等待超时和传输错误,抓包才能与日志对应。
11. ECU 端服务分发:先校验,再查询稳定快照
在 AUTOSAR 分层中,DCM 的 DSL 管理协议、连接和时序,DSD 分发服务,DSP 执行服务处理;DEM 提供事件/DTC 数据,NvM 管理持久化。ISO-TP 重组由 CanTp 等传输层完成,并非 DSL 自己拼接 CAN 帧。非 AUTOSAR 项目可以采用同样的职责划分,但接口名称不是 ISO 14229 的要求。
下面的入口只实现 0x01、0x02、0x0A。会话、安全访问、车辆条件以及多诊断连接的准入策略先由诊断框架检查;哪些子功能在哪个会话开放,应查 ECU/OEM 配置,不能说所有 0x19 都必须先执行 0x27。
11.1 查询接口必须说明容量和一致性
#include <stdbool.h>#include <stddef.h>#include <stdint.h> #define ECU_DTC_CAPACITY 64utypedef struct { uint32_t number; uint8_t status; } EcuDtc;typedef enum { QUERY_OK, QUERY_BUSY, QUERY_FULL, QUERY_ERROR } QueryRc;typedef enum { ECU_READY, ECU_SUPPRESSED, ECU_PENDING, ECU_TOO_LARGE, ECU_INTERNAL, ECU_ARGUMENT} EcuRc; /* 项目适配接口,不是 AUTOSAR 标准 DEM API 名称。 * all=true: 包含全部支持的 DTC,忽略 mask;否则按有效 mask 筛选。 * records=NULL: 只计数。records 非空: 返回同一稳定版本的完整列表。 * 容量不足返回 QUERY_FULL,禁止以截断列表返回 QUERY_OK。 * QUERY_BUSY 时由诊断框架调度后续调用,禁止在此自旋等 NvM。 */typedef QueryRc (*DtcQuery)(bool all, uint8_t mask, EcuDtc *records, size_t capacity, size_t *count); static EcuRc Negative(uint8_t *p, size_t *n, uint8_t nrc, bool functional, bool pending_sent){ bool suppress = (nrc == 0x11u) || (nrc == 0x12u) || (nrc == 0x31u) || (nrc == 0x7Eu) || (nrc == 0x7Fu); if (functional && suppress && !pending_sent) return ECU_SUPPRESSED; p[0] = 0x7Fu; p[1] = 0x19u; p[2] = nrc; *n = 3u; return ECU_READY;}

11.2 组包时禁止静默截断
EcuRc Handle19(const uint8_t *req, size_t req_len, uint8_t *rsp, size_t cap, size_t *rsp_len, uint8_t availability, uint8_t format, DtcQuery query, bool functional, bool pending_sent){ EcuDtc records[ECU_DTC_CAPACITY]; size_t count = 0u; if ((req == NULL) || (rsp == NULL) || (rsp_len == NULL) || (query == NULL) || (cap < 3u)) return ECU_ARGUMENT; *rsp_len = 0u; if ((req_len == 0u) || (req[0] != 0x19u)) return ECU_ARGUMENT; if (req_len < 2u) return Negative(rsp, rsp_len, 0x13u, functional, pending_sent); uint8_t type = (uint8_t)(req[1] & 0x7Fu); bool suppress = ((req[1] & 0x80u) != 0u) && !pending_sent; if ((type != 0x01u) && (type != 0x02u) && (type != 0x0Au)) return Negative(rsp, rsp_len, 0x12u, functional, pending_sent); size_t expected = (type == 0x0Au) ? 2u : 3u; if (req_len != expected) return Negative(rsp, rsp_len, 0x13u, functional, pending_sent); uint8_t mask = (type == 0x0Au) ? 0u : (req[2] & availability); bool count_only = (type == 0x01u) || suppress; QueryRc q = query(type == 0x0Au, mask, count_only ? NULL : records, count_only ? 0u : ECU_DTC_CAPACITY, &count); if (q == QUERY_BUSY) return ECU_PENDING; if (q == QUERY_FULL) return ECU_TOO_LARGE; if (q != QUERY_OK) return ECU_INTERNAL; if ((type != 0x0Au) && (mask == 0u) && (count != 0u)) return ECU_INTERNAL; if ((type == 0x01u) && (count > UINT16_MAX)) return ECU_INTERNAL; if (suppress) return ECU_SUPPRESSED; if (type == 0x01u) { if (cap < 6u) return ECU_TOO_LARGE; rsp[0] = 0x59u; rsp[1] = type; rsp[2] = availability; rsp[3] = format; rsp[4] = (uint8_t)(count >> 8); rsp[5] = (uint8_t)count; *rsp_len = 6u; return ECU_READY; } if (count > ECU_DTC_CAPACITY) return ECU_INTERNAL; if (count > ((cap - 3u) / 4u)) return ECU_TOO_LARGE; for (size_t i = 0u; i < count; ++i) { if ((records[i].number == 0u) || (records[i].number > UINT32_C(0x00FFFFFF)) || ((records[i].status & (uint8_t)~availability) != 0u) || ((type == 0x02u) && ((records[i].status & mask) == 0u))) return ECU_INTERNAL; for (size_t j = 0u; j < i; ++j) { if (records[j].number == records[i].number) return ECU_INTERNAL; } } rsp[0] = 0x59u; rsp[1] = type; rsp[2] = availability; size_t offset = 3u; for (size_t i = 0u; i < count; ++i) { rsp[offset++] = (uint8_t)(records[i].number >> 16); rsp[offset++] = (uint8_t)(records[i].number >> 8); rsp[offset++] = (uint8_t)records[i].number; rsp[offset++] = records[i].status; } *rsp_len = offset; return ECU_READY;}

ECU_READY 才允许发送 rsp_len 字节;ECU_SUPPRESSED 表示正常结束但不发送。ECU_PENDING 是内部调度结果,不等于已经发送 NRC 0x78:诊断框架必须在当前 P2Server/P2*Server 截止前安排响应挂起、继续查询或终止事务。发送过 0x78 后置 pending_sent=true,最终正响应和最终负响应都按通用规则发送。断开连接、复位或服务取消时释放查询上下文,不能把挂起任务留给下一连接。
ECU_TOO_LARGE 与 ECU_INTERNAL 也不是线上 NRC。框架应根据实际限制选择分页/流式组包、扩容或适用的通用错误路径;确实超过服务器可支持的响应长度时,可按通用 responseTooLong (0x14) 处理。不能把缓冲区不足伪装成“请求参数超范围 0x31”,也不能返回一个缺少尾部 DTC 的成功列表。生产代码应在评审中明确每个内部返回值的处理,禁止把非 READY 结果一律丢弃。
12. 并发、重入和非易失性存储一致性
12.1 读取期间 DTC 状态可能变化
DTC 监控任务与诊断任务通常是并发运行的。0x19 扫描过程中,某个监控可能刚好更新 statusOfDTC,或者新的故障记录写入 NVM。工程上要明确采用哪一种策略:
- 先复制一份稳定的 DTC 索引和状态,再构造响应;
- 对 DEM 查询加短临界区,禁止诊断服务长时间占用锁;
- 使用版本号,发现查询期间数据版本变化就重新取快照;
- 对分页响应保留读取游标,避免同一 DTC 重复或漏报。
不能在持有 NVM 写锁的情况下等待 ISO-TP 连续帧,否则一个长 0x19 响应可能阻塞故障记录写入。
12.2 快照和扩展数据的读取边界
快照数据、扩展数据通常来自 NVM 或启动时加载的 RAM 缓存。读取接口需要返回“记录不存在”“记录有效但数据为空”“记录号不支持”三种不同结果:
- 参数有效但没有发生过故障:正响应只返回 DTCAndStatusRecord;
- 参数不在 ECU 支持范围:返回 0x31;
- 记录损坏或 NVM 校验失败:先进入项目定义的存储恢复或诊断错误路径,并保留内部原因;不能仅因内部数据损坏就随意套用表示参数错误的
0x31。若向外返回通用 NRC,应逐项核对其适用条件和 ECU 规范。
不能把“数据为空”直接转换成 0x31,否则诊断仪会误以为记录号写错,无法区分“从未发生”与“配置不支持”。
12.3 掉电窗口
0x19 本身是读服务,但它读到的结果取决于 DTC 状态和记录是否已经从 RAM 同步到 NVM。以下时序需要在项目中明确:
- 监控任务更新 RAM 状态;
- DEM 或 DTC 管理模块产生快照/扩展数据;
- NvM 写入并完成校验;
- 诊断任务允许 0x19 读取稳定版本;
- 掉电或复位后从 NVM 恢复状态。
如果测试脚本在故障刚触发后立即读取 0x19,可能读到状态已更新但快照还未落盘的中间状态。测试用例应等待项目规定的写入完成事件,或者读取并比较版本号,而不是只等待固定的经验毫秒数。
13. 测试用例和抓包验证
编号:TC-01
场景:状态掩码计数
请求/前置条件:19 01 08,存在 2 条 confirmed DTC
预期结果:59 01 + 掩码、格式、数量 2
验证重点:DTCCount 高字节在前
编号:TC-02
场景:状态掩码列表
请求/前置条件:19 02 08
预期结果:每条记录 3 字节 DTC + 1 字节状态
验证重点:(status & mask) != 0
编号:TC-03
场景:空结果
请求/前置条件:有效掩码但无匹配 DTC
预期结果:只返回 59 02 DTCSAM
验证重点:不应返回 0x31
编号:TC-04
场景:全部支持 DTC
请求/前置条件:19 0A
预期结果:返回全部支持 DTC,不能重复
验证重点:记录长度和状态可用掩码
编号:TC-05
场景:子功能 bit7
请求/前置条件:19 82 08
预期结果:按 0x02 解析,抑制正常正响应;发过 0x78 则发送最终响应
验证重点:不要把 0x82 判为未知子功能
编号:TC-06
场景:长响应
请求/前置条件:多条 DTC 或快照记录
预期结果:FF/CF/FC 顺序正确
验证重点:PCI、序号、BS/STmin
编号:TC-07
场景:长度错误
请求/前置条件:19 02 或 19 06 08 05
预期结果:7F 19 13
验证重点:先做长度检查
编号:TC-08
场景:子功能不支持
请求/前置条件:19 7F
预期结果:物理寻址返回 7F 19 12;功能寻址抑制
验证重点:bit7 清除后判断值域
编号:TC-09
场景:DTC/记录号不支持
请求/前置条件:19 06 12 34 56 99
预期结果:7F 19 31
验证重点:查数据库和记录号配置
编号:TC-10
场景:响应挂起
请求/前置条件:构造慢查询或 NVM 忙
预期结果:可能先返回 7F 19 78,随后完成
验证重点:扩展等待、重复 0x78 与事务期限
编号:TC-11
场景:状态变化
请求/前置条件:查询过程中触发一次监控失败
预期结果:结果符合快照或版本策略
验证重点:并发一致性
编号:TC-12
场景:掉电恢复
请求/前置条件:写入快照后执行复位
预期结果:0x19 读到的状态与 NVM 策略一致
验证重点:RAM/NvM 校验和恢复
编号:TC-13
场景:容量不足
请求/前置条件:列表超过 64 条或响应缓冲区过小
预期结果:明确容量错误,不静默截断
验证重点:容量、长度及错误映射
编号:TC-14
场景:异常响应
请求/前置条件:59 82、重复 DTC、非整条记录
预期结果:客户端报告协议错误
验证重点:回显子功能、去重、长度边界
编号:TC-15
场景:分页占位
请求/前置条件:列表中含 00 00 00 00
预期结果:忽略占位,不计作真实 DTC
验证重点:有效记录数量
编号:TC-16
场景:不支持状态位
请求/前置条件:19 02 40,DTCSAM 为 2F
预期结果:59 02 2F,空列表
验证重点:有效掩码为 0
对长报文,建议同时保存三个层次的证据:原始 CAN 帧、ISO-TP 重组后的 UDS PDU、解析后的 DTC 表。只保存最后一层会丢失 PCI、流控和超时信息,遇到丢帧或序号错误时很难定位。
14. 工程边界和常见误区
- 把
0x19的第二字节直接当成 DTC 数量;它是 reportType,数量只在 0x01 等特定响应中出现。 - 把
DTCStatusAvailabilityMask当成当前 DTC 状态;它只是服务器支持的状态位集合。 - 把
statusOfDTC == DTCStatusMask当成匹配条件;标准使用按位与后非零的规则。 - 把 CAN ID、ISO-TP PCI、UDS SID 混为一个层次;解析前先完成 ISO-TP 重组。
- 把不同记录类型的编号当成同一张表;Snapshot、StoredData、ExtendedData 各自有独立地址空间。
- 把 DTC 三字节倒序发送;诊断报文是高字节在前,和 MCU 内部端序无关。
- 把没有匹配 DTC 当成 0x31;有效请求的空列表通常是正响应。
- 把 ECU 支持的 DTC 格式写死为 SAE J2012;应先读取响应中的 DTCFormatIdentifier,并结合项目定义解码。
- 把 P2/
P2*、ISO-TP 超时和 NVM 写入时间合成一个固定等待值;三者是不同层次的时序问题。 - 把 0x19 读到的状态当成永久真相;监控任务、老化、掉电恢复和并发写入都可能改变结果。
- 把 ISO 14229 的服务格式等同于 AUTOSAR DEM 行为;模块名称、接口和存储策略由 ECU 项目决定。
15. 结语
ReadDTCInformation(SID:0x19)真正的工程价值,是把“ECU 里有故障”变成一组可以复核的数据:哪一个 DTC、处于什么状态、何时发生、保存了哪些快照和扩展记录。使用它时,先按子功能确定请求长度,再按响应子功能确定字段布局;把 DTCStatusMask 的按位匹配、statusOfDTC 的八个位、ISO-TP 长响应和 P2/P2* 时序一起验证,最后再把数据与 DEM、NvM 和故障监控任务的状态对应起来。
本文对应 ISO 14229-1:2020 第 12.3 节 ReadDTCInformation、表 302~336,以及附录 D.2 的 DTC 状态位定义。DTC 编码、MemorySelection、记录内容、会话/安全条件和存储策略应以具体 ECU 的诊断规范为准。



