☰
UDS诊断0x1906服务详解:Python模拟读取ECU故障发生次数
2026/9/28 8:15:50 网站建设 项目流程

1. 从一次售后数据复盘说起:为什么我要盯上0x1906

前阵子帮一个做商用车电控的朋友复盘售后数据,发现一个挺有意思的现象:同一批出厂的ECU,有的车跑了三年一次故障灯没亮过,有的车半年内反复进站,读出来的DTC翻来覆去就那么两三个。当时我们想量化一下"这个ECU到底有多爱出故障",第一反应是去翻诊断仪的冻结帧记录,但冻结帧只保留最后一次或最近若干次,历史累计次数根本拿不到。后来翻ISO 14229的文档,才把目光落到0x1906这个服务上——Report DTC Extended Data Record by DTC Number,按DTC编号读取扩展数据记录,其中就包含故障发生次数这个计数器。

这个服务在UDS诊断里属于0x19服务的一个子功能,日常大家用得最多的是0x1902(按状态掩码读DTC),0x1906相对冷门,但恰恰是它能把"故障次数"这种统计类信息挖出来。对于做整车下线检测、售后质量分析、三包索赔判定的同行来说,这个数据比单纯的"当前有没有故障"有价值得多。我这次就把整个从协议理解到Python模拟验证的流程整理一遍,代码可以直接跑,逻辑可以直接搬到你的诊断工具链里。

适合谁看?如果你写过UDS诊断脚本、调过CANoe或者用过python-can,这篇能帮你把0x1906的请求响应格式彻底吃透;如果你刚接触UDS,只要懂一点Python基础,跟着代码走一遍也能明白诊断报文是怎么一来一回的。全文围绕UDS、0x1906、ECU、Python、ISO 14229这几个核心点展开,不绕弯子。

2. 0x1906到底读的是什么:协议层拆解

2.1 0x19服务的家族谱系与0x1906的定位

UDS的0x19服务全称是ReadDTCInformation,中文叫"读取诊断故障码信息",它是ISO 14229-1里定义的一个大服务,下面挂了一堆子功能。常见的几个:

子功能名称典型用途
0x01reportNumberOfDTCByStatusMask按状态掩码统计DTC数量
0x02reportDTCByStatusMask按状态掩码列出所有DTC
0x04reportDTCSnapshotRecordByDTCNumber读某个DTC的快照(冻结帧)
0x06reportDTCExtDataRecordByDTCNumber读某个DTC的扩展数据记录
0x0AreportSupportedDTC读所有支持的DTC

0x1906的完整语义是:给定一个3字节的DTC编号,加上一个1字节的扩展数据记录编号,ECU返回该DTC对应的扩展数据记录内容。故障发生次数(Occurrence Counter)就是众多扩展数据记录中的一种,通常记录编号为0x01或0x02,具体取决于主机厂的定义。

这里有个容易踩的坑:0x1906本身不规定扩展数据记录里装什么,ISO 14229只规定了请求响应的框架,记录内容由各主机厂的诊断规范(Diagnostic Specification)定义。所以你在A项目上读0x01记录拿到的是发生次数,换到B项目可能0x01是老化计数器,0x02才是发生次数。这一点后面讲实操时会重点说。

2.2 请求与响应的字节级结构

先看请求报文。0x1906的请求格式是:

19 06 [DTC_High] [DTC_Mid] [DTC_Low] [Record_Number]
  • 第1字节0x19:服务ID
  • 第2字节0x06:子功能
  • 第3~5字节:3字节DTC编号,高字节在前
  • 第6字节:扩展数据记录编号,比如0x01

响应报文分肯定响应和否定响应。肯定响应的格式:

59 06 [DTC_High] [DTC_Mid] [DTC_Low] [StatusOfDTC] [Record_Number] [Data...]
  • 0x59:0x19 + 0x40,肯定响应固定加0x40
  • 0x06:回显子功能
  • 3字节DTC编号:回显
  • StatusOfDTC:该DTC当前的状态字节
  • Record_Number:回显记录编号
  • Data:扩展数据记录的实际内容,长度由记录定义决定

否定响应则是7F 19 [NRC],常见NRC有0x31(请求超出范围,DTC不存在)、0x33(安全访问被拒)、0x22(条件不满足)。

注意:DTC编号的3字节顺序是High-Mid-Low,不是小端。很多新手用struct打包时习惯性写成小端,结果ECU直接回0x31。这个细节我在第一次写脚本时栽过,排查了半天。

2.3 故障发生次数在扩展数据记录里的编码

假设主机厂定义记录0x01为"故障发生次数",长度1字节,那么响应里Data部分就是1个字节,值0x00~0xFF,表示该DTC从ECU首次上电到当前累计发生的次数。有些规范会定义2字节,支持更大计数范围;还有的会把"发生次数"和"老化次数"打包在同一个记录里,比如前2字节发生次数、后2字节老化计数。

这里必须强调:读到的次数是累计值还是自上次清除后的值,取决于ECU实现。ISO 14229没有强制规定计数器在ClearDiagnosticInformation(0x14服务)后是否归零。实际项目中,大部分ECU在收到0x14后会清零发生次数,但也有例外。做质量分析时,如果拿到的次数明显偏小,先确认一下中间有没有被清过码。

3. 动手前的准备:环境与工具选型

3.1 Python环境搭建的务实选择

网上Python安装教程一抓一大把,我这边只说跟UDS模拟相关的部分。推荐用Python 3.8以上,原因是python-can和udsoncan这些库对新版本支持更好。安装方式我倾向用官方安装包或者conda,不推荐在Windows上折腾WSL再装Python,除非你本来就在Linux环境下做开发。

装完Python后,核心依赖就两个:

pip install can pip install udsoncan

python-can负责底层CAN报文的收发,udsoncan是上层UDS协议栈,帮你处理服务ID、NRC、会话控制这些。如果你只是想模拟验证0x1906的报文逻辑,不接真实ECU,那其实一个udsoncan加一个虚拟总线就够了。要是想接真实硬件,还得配一个CAN接口,比如PCAN、Kvaser或者便宜的USB-CAN分析仪,装好对应驱动后在python-can里配置interface即可。

提示:udsoncan的版本更新比较快,不同版本API有差异。我写代码时用的是udsoncan 1.3.x,如果你装的是更早的版本,client.change_session之类的调用方式可能不一样,建议先pip show udsoncan确认版本。

3.2 虚拟总线还是真实ECU

做0x1906验证,我建议分两步走。第一步用虚拟总线把请求响应逻辑跑通,确认字节序、记录编号、解析方式都对;第二步再接真实ECU或者ECU仿真工具(比如CANoe的CAPL仿真节点)做联调。虚拟总线的好处是不依赖硬件,随时随地能跑,而且可以人为构造各种边界响应,比如返回0x31、返回超长数据,用来测试你脚本的健壮性。

python-can自带的virtual接口就能干这事:

import can bus = can.interface.Bus(interface='virtual', channel='vcan0')

两个脚本分别往同一个channel收发,就能模拟一次完整的诊断交互。下面第4节我会给出完整的模拟代码。

3.3 诊断数据库文件的作用

真实项目里,扩展数据记录的定义通常写在ODX或CDD文件里。如果你手上有这些文件,可以用odxtools之类的库解析,自动拿到记录编号和长度,不用靠猜。没有的话,就得翻主机厂的诊断规范文档,找到0x1906那一节,确认记录编号和字节定义。我见过不少团队直接拿CANoe的Diagnostic Console手动发一帧看响应,反推出记录格式,这招在紧急排查时也好用。

4. 用Python模拟0x1906的完整实操

4.1 搭建虚拟ECU与诊断客户端

先写一个模拟ECU的脚本,它监听虚拟总线,收到0x1906请求后按预设逻辑返回响应。为了贴近真实,我构造了三个DTC,每个DTC的发生次数不同,并且对不存在的DTC返回0x31。

# mock_ecu.py import can import time # 预设DTC数据库:DTC编号 -> (状态字节, 发生次数) DTC_DB = { 0xP123456: (0x2F, 5), # 占位,实际用整数 }

上面这个写法是伪代码,DTC编号是3字节,我用整数表示。重新写清楚:

# mock_ecu.py import can import time # DTC编号用整数表示,例如 0x123456 # 值:(StatusOfDTC, OccurrenceCounter) DTC_DB = { 0x123456: (0x2F, 5), 0xABCDEF: (0x08, 1), 0x00A001: (0x2F, 128), } RECORD_NUMBER = 0x01 # 假设记录0x01为发生次数 def build_positive_response(dtc, status, count): dtc_high = (dtc >> 16) & 0xFF dtc_mid = (dtc >> 8) & 0xFF dtc_low = dtc & 0xFF # 59 06 DTC_H DTC_M DTC_L Status Record Data return [0x59, 0x06, dtc_high, dtc_mid, dtc_low, status, RECORD_NUMBER, count] def build_negative_response(nrc): return [0x7F, 0x19, nrc] def main(): bus = can.interface.Bus(interface='virtual', channel='vcan0') print("Mock ECU started, waiting for 0x1906 requests...") while True: msg = bus.recv(timeout=1.0) if msg is None: continue data = list(msg.data) # 判断是否为0x1906请求 if len(data) >= 6 and data[0] == 0x19 and data[1] == 0x06: dtc = (data[2] << 16) | (data[3] << 8) | data[4] record = data[5] print(f"Received 0x1906 for DTC=0x{dtc:06X}, Record=0x{record:02X}") if dtc in DTC_DB and record == RECORD_NUMBER: status, count = DTC_DB[dtc] resp = build_positive_response(dtc, status, count) else: resp = build_negative_response(0x31) resp_msg = can.Message(arbitration_id=0x7E8, data=resp, is_extended_id=False) bus.send(resp_msg) print(f"Sent response: {[hex(b) for b in resp]}") if __name__ == "__main__": main()

这段代码里几个关键点:DTC编号的拼接用移位,(data[2] << 16) | (data[3] << 8) | data[4],保证高字节在前;响应ID用0x7E8,这是11位CAN下ECU响应的标准ID(请求ID 0x7E0);记录编号不匹配或DTC不存在时统一回0x31。

4.2 客户端发送请求并解析响应

客户端脚本负责发请求、收响应、解析出发生次数:

# uds_client.py import can import time def send_1906(bus, dtc, record=0x01): dtc_high = (dtc >> 16) & 0xFF dtc_mid = (dtc >> 8) & 0xFF dtc_low = dtc & 0xFF req = [0x19, 0x06, dtc_high, dtc_mid, dtc_low, record] msg = can.Message(arbitration_id=0x7E0, data=req, is_extended_id=False) bus.send(msg) print(f"Sent: {[hex(b) for b in req]}") # 等待响应 deadline = time.time() + 2.0 while time.time() < deadline: resp = bus.recv(timeout=0.5) if resp is None: continue data = list(resp.data) if data[0] == 0x59 and data[1] == 0x06: r_dtc = (data[2] << 16) | (data[3] << 8) | data[4] status = data[5] r_record = data[6] count = data[7] print(f"Positive: DTC=0x{r_dtc:06X}, Status=0x{status:02X}, " f"Record=0x{r_record:02X}, Count={count}") return count elif data[0] == 0x7F and data[1] == 0x19: nrc = data[2] print(f"Negative: NRC=0x{nrc:02X}") return None print("Timeout waiting for response") return None def main(): bus = can.interface.Bus(interface='virtual', channel='vcan0') # 依次查询三个DTC for dtc in [0x123456, 0xABCDEF, 0x00A001, 0x999999]: print(f"\n--- Query DTC 0x{dtc:06X} ---") send_1906(bus, dtc) time.sleep(0.5) if __name__ == "__main__": main()

跑的时候开两个终端,先启动mock_ecu.py,再跑uds_client.py。你会看到客户端依次拿到5、1、128,最后一个不存在的DTC返回NRC 0x31。整个交互过程跟真实ECU的报文格式完全一致,只是走的是虚拟总线。

4.3 用udsoncan封装,代码更贴近工程实践

上面手写报文适合理解原理,但工程里更推荐用udsoncan,它帮你处理了会话、超时、NRC解析这些。用udsoncan重写客户端:

# uds_client_udsoncan.py import can import udsoncan from udsoncan.connections import PythonIsoTpConnection from udsoncan.client import Client import isotp # 配置ISO-TP参数 isotp_params = { 'stmin': 10, 'blocksize': 8, 'wftmax': 0, 'll_data_length': 8, 'tx_padding': 0x00 } # 虚拟总线 bus = can.interface.Bus(interface='virtual', channel='vcan0') addr = isotp.Address(isotp.AddressingMode.Normal_11bits, txid=0x7E0, rxid=0x7E8) stack = isotp.CanStack(bus=bus, address=addr, params=isotp_params) conn = PythonIsoTpConnection(stack) config = dict(udsoncan.configs.default_client_config) config['data_identifiers'] = {} with Client(conn, config=config) as client: client.change_session(0x03) # 扩展会话 # 读取DTC 0x123456 的扩展数据记录0x01 response = client.read_dtc_extended_data_record( dtc=0x123456, record_number=0x01) print(response)

udsoncan的read_dtc_extended_data_record方法内部就是发0x1906,返回的response里能拿到原始数据。不过要注意,udsoncan默认不知道你的记录格式,解析出来的data是bytes,还得你自己按规范拆。这也是为什么前面强调要先搞清楚记录定义。

实操心得:用udsoncan时,如果ECU要求先做安全访问(0x27)才能读扩展数据记录,记得在change_session之后加security_access。我遇到过几个ECU,0x1902不需要安全访问,但0x1906需要,这个差异在调试时很容易忽略。

5. 真实项目里的坑与排查技巧

5.1 记录编号猜错,返回0x31

这是最常见的。0x1906的请求里记录编号是必填的,但很多诊断规范文档写得含糊,只写"支持扩展数据记录",没明确列出编号。我一般的做法是:先发0x1901(reportNumberOfDTCByStatusMask)确认ECU支持的DTC数量,再发0x190A(reportSupportedDTC)拿到DTC列表,然后对第一个DTC依次试记录编号0x01到0x0F,看哪个返回肯定响应。这个暴力枚举法虽然笨,但在没有ODX文件时最有效。

5.2 发生次数读出来是0,但DTC确实存在

这种情况通常有三个原因。一是该DTC刚被清除过,计数器归零;二是记录编号对应的不是发生次数,而是别的字段(比如老化计数,刚清除后也是0);三是ECU实现里发生次数只在特定条件下更新,比如故障从"未确认"转为"已确认"时才加一。排查时可以先读0x1902看DTC状态字节,确认DTC当前状态,再结合0x1904读快照,交叉验证。

5.3 多帧响应处理不当导致数据截断

如果扩展数据记录比较长(比如包含时间戳、里程、电压等多个字段),响应会超过7字节,走ISO-TP多帧传输。手写脚本时如果只收第一帧,后面的连续帧就丢了。用udsoncan或者python-can的isotp模块能自动处理,但如果你自己写,必须实现流控帧(FC)和连续帧(CF)的拼接。我早期写的一个脚本就是因为没处理多帧,读出来的发生次数总是错的,后来发现是把连续帧的第一个字节当成了数据。

5.4 常见问题速查表

现象可能原因排查方向
返回0x7F 19 31DTC不存在或记录编号不支持用0x190A确认DTC列表,枚举记录编号
返回0x7F 19 33未做安全访问先执行0x27服务
返回0x7F 19 22会话或条件不满足切到扩展会话或编程会话
响应超时多帧未处理或ECU未响应检查ISO-TP配置,确认ECU在线
次数明显偏小计数器被清除或定义不同确认0x14是否执行过,核对记录定义
次数不变计数器更新条件未满足查规范确认更新触发条件

5.5 关于0x1906与0x1904的配合使用

做故障分析时,我习惯把0x1906和0x1904(读快照)搭配用。0x1904给你故障发生时的环境数据,比如车速、水温、电压;0x1906给你累计次数。两者结合,能判断这个故障是偶发还是频发。比如某个DTC发生次数128次但快照显示每次都是同一工况,那大概率是设计问题;如果次数少但快照工况分散,可能是偶发干扰。这个分析思路在售后质量报告里很吃香。

6. 从模拟到量产:脚本工程化的几点建议

6.1 把DTC数据库和记录定义外置

模拟脚本里我把DTC_DB写死在代码里,实际项目千万别这么干。DTC列表、记录编号、字段长度这些应该放到配置文件(JSON或YAML)里,代码只负责解析。这样换项目时不用改代码,改配置就行。我现在的做法是维护一个dtc_config.json,结构大概是:

{ "records": { "0x01": {"name": "occurrence_counter", "length": 1, "type": "uint8"}, "0x02": {"name": "aging_counter", "length": 1, "type": "uint8"} }, "dtcs": { "0x123456": {"description": "示例故障A", "records": ["0x01", "0x02"]} } }

6.2 加日志和重试机制

诊断脚本跑在产线或售后环境时,网络抖动、ECU忙都可能导致单次请求失败。我的经验是给每个0x1906请求加2~3次重试,重试间隔100ms,同时把每次请求响应的原始字节记到日志里。出问题时翻日志比重新复现快得多。日志格式建议包含时间戳、请求字节、响应字节、解析结果,方便后续做数据分析。

6.3 批量读取时的节流

如果要统计几十个DTC的发生次数,别一股脑连续发。ECU的诊断栈处理能力有限,连续高频请求可能触发流控或者导致响应延迟。我一般每个请求之间间隔20~50ms,具体看ECU规范里的P2Server时间。有些ECU的P2Server是50ms,那你间隔就得大于这个值,否则会收到0x78(响应挂起)或者直接超时。

6.4 数据可视化与趋势分析

读出来的发生次数如果只存数据库,价值有限。我习惯用pandas做一层聚合,按车型、按DTC、按生产批次统计平均发生次数和分布,再用matplotlib出图。比如发现某个批次的ECU某个DTC平均发生次数是其他批次的5倍,那基本能锁定是这批次的硬件或标定问题。这套分析流程在质量部门很受欢迎,比单纯给一张DTC列表有说服力得多。

7. 关于0x1906的几个延伸思考

0x1906这个服务本身不复杂,但它在整车诊断体系里的位置挺微妙。它不像0x1902那样天天被用,也不像0x27、0x2E那样涉及安全,但它是少数能提供"历史统计"信息的服务之一。随着OTA和远程诊断的普及,我判断这类统计类服务的价值会越来越高——因为远程诊断带宽有限,不可能把全部冻结帧传上去,但传一个发生次数计数器成本极低,却能支撑很多质量判断。

另外,ISO 14229-1在2013版和2020版里对0x1906的定义基本没变,但主机厂对扩展数据记录的自定义越来越丰富。我见过把发生次数、首次发生时间、末次发生时间、累计运行时长打包在一个记录里的,也见过把发生次数拆成"本次点火循环次数"和"历史总次数"两个字段的。所以做这个服务,协议层的东西是死的,真正花时间的是吃透每个项目的诊断规范。

最后分享一个我常用的调试技巧:当你怀疑ECU返回的发生次数不对时,先发0x14清除故障,再发0x1906读一次,然后人为触发一次故障,再读一次。如果两次差值符合预期,说明计数器工作正常;如果清除后读数不为0,那说明这个ECU的计数器不随0x14清零,你的分析逻辑就得相应调整。这个"清除-触发-复读"的三步验证法,比对着文档猜要靠谱得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询