首页 / 资讯中心 / 文章详情

发动机CAN通信诊断集成:J1939/J1587双通道与DM1解析

发动机CAN通信诊断集成:J1939/J1587双通道与DM1解析 ★ FEATURED ARTICLE
简介《康明斯发动机通讯协议及诊断.pdf》面向需将康明斯Quantum系列发动机集成到汽车、工业及发电设备中的OEM工程师、维修与诊断技术人员聚焦ECM诊断和J1587/J1939串行通信的落地理解。资源包仅含1个PDF文件约240KB篇幅紧凑却系统覆盖高级诊断、在线/离线诊断、Quantum串行通信、J1939安装信息以及与AEB 15.40、15.42、15.44配套的电子特性、OEM组件接口和安装建议。已有3563人学习适合作为发动机通信协议入门、故障码排查和OEM集成设计的案头参考。读者可借此梳理J1587与J1939报文差异、诊断代码读取与清除思路并对照AEB文档理解传感器、执行器、线束连接和安装推荐从而提升跨系统联调与故障定位效率。1. 从一台亮红灯的机器说起这份 64 页公告到底解决什么问题前段时间帮人排查一台改装设备发动机能正常着车仪表却一直亮红灯接入 CAN 卡后只看到零星几帧现场几个人争了半天谁也判断不了是线束问题还是报文根本没有发出来。最后把我从无从判断的状态里拉出来的是一份 2001 年出的 64 页工业应用工程公告AEB。它是某柴油机厂商写给整机厂看的发动机 CAN 通讯与诊断集成说明正文分成三块ECM 诊断机制、J1939/J1587 双通道串行通讯接入、以及覆盖几十个故障码的对照表。这类文档的定位很特别它是集成约定并非教程告诉你什么必须接、什么必须发、什么必须读到却基本不讲“为什么”。正在做发动机 CAN 通讯对接、仪表与远程终端开发、现场诊断排故的人手上要是没有原厂工具链这份东西比任何二手教程都硬。下面按我实际复现的顺序拆先架构再诊断再报文最后讲坑。2. 双通道通讯架构J1939 与 J1587 的物理层怎么接、怎么验2.1 两个独立端口250K 与 9600 波特率为什么必须都留这份公告里最关键的一句话是控制器设计了两个独立的串行通讯口一个对 SAE J1939兼容 J1939-11另一个对 SAE J1587兼容 J1708并且明确不支持更老的 J1922。这个描述决定了整机侧的硬件形态你不能只拉一根 CAN 线就完事诊断口上必须同时引出高速和低速两套差分对。两套总线的分工很清晰。J1939 跑 250 kbit/s物理层是差分驱动最大总线长度 40 米同一时刻最多 30 个节点承担的是控制、信息共享、多路复用和厂商私有通讯是“机器大脑之间说话”的通道。J1587 基于 J1708只有 9600 bit/s最多 20 个节点主要承担信息共享与诊断是“读状态、读故障”的通道。选型上我一般的判断是只要你的设备需要跨系统联动发动机和变速箱、液压控制器、整车控制器互相影响动作J1939 是必须的J1587 则看你有没有老仪表、老诊断终端或者第三方运维设备要接。别因为“CAN 更快”就把 J1587 剪掉很多现场排故的最后一根救命稻草恰恰是那条 9600 的低速线上还在稳定输出的 PID 数据。两个口在钥匙开关打开时就同时激活不存在“先起来一个再起来另一个”的时序问题这一点在写上位机初始化逻辑时可以直接依赖。还有一条容易忽略的公告说这两个口是独立的。独立意味着 J1939 那边总线挂了、终端电阻丢了、某个节点把 CAN_H 拉死了J1587 那条线大概率还活着。反过来也一样。做硬件设计时把两路的供电和地回流分开走排故时你就多一条退路。2.2 拓扑、线缆与 9 针诊断口把图纸上的约束落到压线钳上物理层参数必须照抄因为它们是硬约束而非建议值。项目J1939 侧J1587 侧速率250 kbit/s9600 bit/s线缆带排水线的屏蔽双绞J1939-11双绞最大总线长度40 m按负载与线径实测定最大节点数3020终端总线两端各 120 Ω一般不要求终端匹配施工时我按这个顺序走先确认总线的两个物理端点终端电阻只放在这两个端点中间节点一律不并联电阻再确认屏蔽层的接地点只留一处通常是靠近 ECM 那一端多点接地在整机上会引入地环流波形上表现为共模噪声抬高最后才压接连接器。顺序反了后面查起来就是一团乱麻。诊断口按 J1939-13 的 9 针 Deutsch 定义走常见分配是 A 为电池负极、B 为正极、C 为 CAN_H、D 为 CAN_L、E 为屏蔽、F 和 G 给 J1587 的正负、H 留给厂商自定义、I 是可选信号。这条是行业通行做法不是这份公告逐针写死的所以接线前一定拿万用表量一遍钥匙 OFF 时 C-D 之间应该是 60 Ω 左右两个 120 Ω 并联量到 120 Ω 说明只挂了一个终端量到无穷大说明一个都没挂量到几欧姆说明有人把电阻并在中间节点上了。这个 60 Ω 的检查我几乎每次上电前都会做比任何软件日志都快。连接器供应商公告里单独列了一节说明它当年就吃过“随便买个头”的亏。整机振动环境下连接器接触电阻漂移是间歇性丢帧的头号原因选带锁止结构、镀金端子的型号别在这上面省钱。2.3 用二十行代码验证链路是否活着线接完别急着上整车逻辑。先跑一个只做“监听 统计”的脚本确认总线上确实有帧、周期是否稳定、有没有非 J1939 的杂帧混进来。import time import can from collections import Counter BITRATE 250_000 WINDOW 10.0 bus can.interface.Bus(bustypesocketcan, channelcan0, bitrateBITRATE) def pgn_of(can_id: int) - int: 从 29 位 CAN ID 提取 PGN。 PDU1(PF240)PS 是目标地址不属于 PGN PDU2(PF240)PS 是组扩展属于 PGN。 pf (can_id 16) 0xFF ps (can_id 8) 0xFF if pf 240: return pf 8 return (pf 8) | ps seen Counter() bad 0 t0 time.time() try: while time.time() - t0 WINDOW: msg bus.recv(timeout1.0) if msg is None: continue if not msg.is_extended_id: # J1939 一律 29 位 ID bad 1 continue seen[pgn_of(msg.arbitration_id)] 1 finally: bus.shutdown() print(非扩展帧计数:, bad) for pgn, n in seen.most_common(): print(fPGN {pgn:6d} 帧数 {n:5d} 平均周期 {WINDOW*1000/n:7.1f} ms)逻辑说明三件事。第一pgn_of里的分支不能省。很多人图省事写(id 8) 0x3FFFF在 PDU2 报文上没问题但一遇到 PDU1比如请求帧、地址声明就会把目标地址算进 PGN统计表里凭空多出一堆不存在的 PGN后面拿这张表去核对文档报文清单怎么都对不上。第二is_extended_id的判断是低成本的自检只要出现标准帧就说明总线上混进了别的设备或者某个节点配置错了。第三样本窗口别短于 10 秒很多慢周期报文是 1 秒甚至 10 秒一帧窗口太短会误判为“没发”。参数怎么改bitrate必须和总线一致J1939 就是 250000写成 500000 你什么也收不到而且不会有任何报错这个坑后面还会提。channel在 Linux 下是 socketcan 接口名在 Windows 上换成对应硬件的通道号bustype同步改。WINDOW我一般第一次给 10 秒确认有帧之后延长到 60 秒专门看周期抖动。失败了看什么一帧都没有先量 60 Ω再确认钥匙开关是否在 ON两个数据链路都是上电即活有帧但全是错误帧查波特率和采样点有帧但计数很低、周期忽长忽短查终端匹配和屏蔽接地只看到少数几个源地址说明别的节点没上电或者地址冲突。3. 故障码从哪来ECM 三级诊断与闪码机制的拆解3.1 在线诊断故障检测、快照与三级灯语公告把诊断分成板载和离板两类板载这部分是整机厂最该吃透的因为它决定了机器在野外没有电脑时还能不能自救。故障检测发生在钥匙开关打开、机器运行的过程中。ECM 内部有比较完整的故障判定能力一旦某个故障变成“当前激活”它会做两件事把故障码写进内存同时记录一份发动机参数的快照。这个快照是排故时最值钱的东西——你拿到的不是一个孤零零的码而是故障发生那一刻的转速、温度、压力等一组上下文。我在写上位机日志时会把快照字段一起存下来事后复盘比单看故障码有用得多。灯语是另一层信息。系统最多用五个指示灯其中维护灯和发动机保护灯在不同机型上是二选一。停机灯红代表需要立即处置的关键信息警告灯琥珀代表需要及时关注的信息等待启动灯表示进气预热在进行中燃油含水灯亮说明要放水维护灯在 ECM 上电时若到了换油周期会闪运行中出现低优先级故障时也会亮。发动机保护灯则对应保护类故障公告还提到可以通过 OEM 侧接线把它和红色停机灯并到一起接的时候要注意串二极管隔离否则会反灌到其他回路。上电自检这个细节值得记住钥匙打开但诊断开关不动所有指示灯会亮大约两秒然后依次熄灭。这个顺序本身就是一次接线正确性验证。如果某个灯不亮、或者常亮不灭先别怀疑 ECM去查那个灯的驱动线和灯泡。3.2 闪码的两条进入路径与读数规则板载诊断最实用的功能是闪码。进入方式有两种公告写得很明确。第一种是诊断开关。钥匙 ON、发动机不转打开诊断开关ECM 会自动开始闪第一个故障码再用增量/减量开关往前或往后翻。第二种没有诊断开关时用油门踏板把踏板从 0 踩到 100% 来回三次就进入诊断模式之后每踩一次踏板往前翻一个码。读码规则是按闪烁次数组成数字位与位之间停顿码与码之间停顿更长。公告举了两个例子故障码 244 的闪法是 2 下、4 下、4 下故障码 112 的闪法是 1 下、1 下、2 下。红灯和警告灯都在参与这个编码不同机型的具体分工要看机型表。这里有个容易被忽略的点闪码输出的是当前激活故障不是历史故障。想要历史故障得走离板工具。很多人现场闪出来两个码回来一查历史记录有十几条对不上就开始怀疑设备其实是没搞清楚这两套东西的口径。3.3 用状态机把闪码序列解成数字人工数灯容易错尤其是黄昏或者振动环境下。我的做法是拿一个光电传感器或者干脆用手机慢动作录像记录灯的亮灭时刻再用一段状态机解码。def decode_blink(flashes, gap_digit0.8, gap_code2.0): flashes: [(亮起时刻, 熄灭时刻), ...]单位秒按时间升序 返回: [故障码, ...]例如 [244, 112] gap_digit: 位与位之间的最小熄灭间隔秒 gap_code : 码与码之间的最小熄灭间隔秒 codes, cur_code, cur_digit [], [], 0 prev_off None for on, off in flashes: if prev_off is not None: gap on - prev_off if gap gap_code: if cur_digit: cur_code.append(cur_digit); cur_digit 0 if cur_code: codes.append(cur_code); cur_code [] elif gap gap_digit: if cur_digit: cur_code.append(cur_digit); cur_digit 0 cur_digit 1 prev_off off if cur_digit: cur_code.append(cur_digit) if cur_code: codes.append(cur_code) return [int(.join(str(d) for d in c)) for c in codes] # 故障码 2442 闪、4 闪、4 闪位间隔约 1.1s码尾留 3s 空档 sample [(0.0, 0.5), (1.6, 2.1), (3.2, 3.7), (4.8, 5.3), (6.4, 6.9), (8.0, 8.5), (11.5, 12.0), (13.1, 13.6), (14.7, 15.2), (16.3, 16.8)] print(decode_blink(sample)) # [244]逻辑说明核心是把“连续闪几下”当成一个十进制位位与位之间用较短的灭灯间隔切开码与码之间用较长的灭灯间隔切开。cur_digit累加连续闪烁次数遇到间隔超过阈值就把这位结算掉。最后剩下没结算的部分要补一次否则最后一个码会丢。参数说明gap_digit和gap_code没有标准值必须按你的灯驱动实测。经验值是位间隔 0.81.2 秒、码间隔 23 秒取中间值偏安全。采样率要足够高flashes的时间戳分辨率优于 50 ms否则短闪会被合并。如果你的传感器抖动大先把亮灯时长小于 30 ms 的毛刺滤掉再送进函数。失败时怎么查解出来的码位数不对多半是阈值卡在了两个间隔之间把gap_code调大一点再试解出来是一长串单数字说明位间隔被误判成了码间隔把gap_digit调小一个码都没解出来先确认传感器有没有把红灯和琥珀灯分开——两个灯同时闪的时候混在一起采会得到完全错误的结果。3.4 离线诊断能做什么、不能做什么离板诊断靠的是原厂那套 Windows 平台服务工具公告里把它定位成“服务、编程、诊断一体”的 PC 工具。能力清单大致是查看激活与非激活故障、清除非激活故障、监测测量参数和执行器状态、主动驱动某些负载比如进气预热和输油泵、跑单缸断油这类诊断测试。边界同样清楚。第一各机型能力不一致公告直言“当前每个机型的能力不同”所以你不能拿 A 机型的功能清单去承诺 B 机型。第二它是应用层工具管不了物理层线束虚接、终端缺失、共模干扰这些它一概看不出来还是得靠示波器和万用表。第三编程功能有风险刷写过程中断电或者电压跌落后果比故障码严重得多动手前先接稳电源。我的习惯是现场先用闪码确认有没有激活故障再用 PC 工具看快照和历史最后才动执行器测试。顺序反了很容易把原始现场状态破坏掉等你想回头看的时候已经没有了。4. 报文落地从 PGN 清单到能跑的解析代码4.1 报文表怎么读先把 PGN、源地址和更新率对上公告里最长的一张表是 J1939 支持报文清单占了十几页列的是 PGN、优先级、源地址、数据长度、更新率和各字节定义。第二张表是 J1587 支持的 PID 清单第三张是故障码表还有一张专门讲哪些报文是多路复用的、哪些不是。读这类表有三个要点。第一先把“必须发的”和“可选发的”分开。公告在安装信息里明确列了某些数据链路选项是强制的这类报文缺了整机侧的功能就是不全不是“少个信息”这么简单。第二注意源地址。同一个 PGN 从不同源地址发出来含义可能不同解析时如果只按 PGN 匹配、不看 SA就会把两个设备的数据混成一锅。第三注意更新率。表里给的周期是设计值实际总线上会因为网络负载和节点调度有偏移写超时判定时要留足余量我一般给到标称周期的 3 倍。厂商自定义部分要单独处理。公告里有一张表专门列某个 PGN 的厂商特定信息意思是这个 PGN 的标准定义之外字节里塞了自家约定。这类段落在通用 DBC 里通常是空的或者错的必须照着文档手写解析别指望现成文件。4.2 用代码把 DM1 拆成 SPN、FMI 和发生次数诊断报文是落地时最先要用到的。DM1 携带当前激活故障一帧里最多塞四个故障每故障 4 字节。下面这段不依赖任何 DBC直接按位拆。def parse_dm1(data: bytes): 解析 DM1/DM2 数据域返回 [(SPN, FMI, OC), ...] dtcs [] for i in range(0, len(data) - 3, 4): b0, b1, b2, b3 data[i:i4] # SPN 19 位b0 低 8 位、b1 次 8 位、b2 高 3 位 spn b0 | (b1 8) | (((b2 5) 0x07) 16) fmi b2 0x1F # 故障模式标识5 位 oc b3 0x7F # 发生次数7 位 if spn 0 and fmi 0: continue # 填充位跳过 dtcs.append((spn, fmi, oc)) return dtcs # 一个 SPN100、FMI1、OC1 的样例帧 frame bytes([0x64, 0x00, 0x20, 0x01]) print(parse_dm1(frame)) # [(100, 1, 1)]逻辑说明SPN 是 19 位跨三个字节而且高 3 位在第三个字节的高位区位移量是 5 不是 3写错一位整个 SPN 就偏了。FMI 占第三个字节的低 5 位OC 占第四个字节的低 7 位最高位是保留的故障发生次数溢出标志。填充位判空不能只看 SPN要 SPN 和 FMI 同时为 0 才跳过。参数说明range(0, len(data) - 3, 4)这个边界很重要DM1 数据长度可能是 8 字节两故障、12 字节三故障或者变长少于 4 字节的尾巴直接丢弃否则会抛索引异常。如果你用 cantools 加载 DBCdecode_choicesTrue会把 FMI 转成可读字符串调试时好用但入库建议存原始数值字符串随 DBC 版本变化会导致历史数据对不上。4.3 J1587 侧帧格式与校验和J1587 跑在 J1708 之上帧结构简单中间标识 MID、参数标识 PID、若干数据字节最后一字节是校验和。校验规则是从 MID 到最后一个数据字节求和取低 8 位再对 256 取补。def j1708_checksum(body): body: [MID, PID, data...]不含校验和字节 return (0x100 - (sum(body) 0xFF)) 0xFF def build_j1708(body): return bytes(body [j1708_checksum(body)]) # 构造并自检 frame build_j1708([128, 190, 0x4E, 0x20]) assert (sum(frame) 0xFF) 0, 校验和自检失败 print(frame.hex()) # 80be4e20xx逻辑说明(sum(frame) 0xFF) 0这个自检比单独比对校验和字节更可靠因为它验证的是一条完整帧满足整字节和为零的性质能同时抓出数据位翻转和校验位算错两种情况。收帧时先做这一步不通过就整帧丢弃不要试图“修”。参数说明MID 是消息源标识同一台机器上不同控制器有不同 MIDPID 是参数标识公告的 PID 表按 PID 升序排列查表时注意有些 PID 的数据长度是固定的、有些是变长的变长的要按首字节判断后续长度。9600 波特率下一帧 10 个字节左右就要 10 ms所以低速线上别指望高频刷新所有依赖它的逻辑都要按秒级设计。4.4 网络利用率250K 的余量到底还剩多少公告专门用四页讲网络利用率还区分了多路复用帧和非多路复用帧这个视角很实用同一批数据用多路复用打包发和拆成多帧发占用带宽差好几倍。粗算方法是一帧 29 位 ID 的扩展帧考虑位填充线上大约 128 位左右250 kbit/s 下一帧约 0.5 ms。报文周期单帧占用若要承载 20 条这样的报文带宽占比10 ms0.5 ms10 ms约 100%50 ms0.5 ms10 ms / 50 ms约 20%100 ms0.5 ms10 ms / 100 ms约 10%1000 ms0.5 ms10 ms / 1000 ms约 1%这张表的意义是让你在设计阶段就砍掉不必要的高频报文。我见过把温度、油压这类慢变量按 10 ms 发的整机方案总线负载直接冲到 60% 以上仲裁延迟肉眼可见地变大最后靠降周期救回来。判断标准我一般用 30%常态负载压在 30% 以内突发诊断报文才有余量。5. 避坑与排查集成现场最容易翻车的五件事5.1 一帧都收不到但接线“看起来没问题”现象上电后 CAN 卡静默脚本打印零帧示波器上 CAN_H 和 CAN_L 是两条贴在一起的直线。原因最常见的是终端电阻缺失。没有终端差分电平建立不起来收发器连位都判不出来。其次是波特率配错硬件配了 500K 而总线上是 250K某些控制器会静默丢弃而不是报错。第三是 CAN_H 和 CAN_L 接反双绞线上这两根颜色接近压接时看错一次就全反。解决断电量 C-D 之间的电阻正常 60 Ω 左右再逐段断开确认电阻只出现在两个物理端点。波特率用示波器测一位宽度250K 下一位是 4 μs对不上就改配置。接反的检查办法是上电后量 C 对地的静态电平正常在 2.5 V 附近两根都在 2.5 V 而总线不动基本可以判定是收发器没使能或者线序反了。5.2 平时好使一加载就丢帧现象空载调试一切正常机器一动作、变频器一开报文错误计数飙升仪表间歇性掉线。原因屏蔽层多点接地引入地环流或者动力线与信号线捆在一起走。差分总线抗共模干扰能力不错但不是无限的整机上几十安的负载切换感应的共模电压能轻松超标。解决屏蔽层单点接地接地点放在 ECM 侧信号线与动力线分开走线交叉时尽量垂直必要时在总线入口加共模电感。我在一个振动台架项目上就是靠改这一条把错误帧从每千帧几十个降到零。5.3 解析出来的数据全是错的但帧确实收到了现象字节顺序、数值大小看着都不对温度解出来是负几十度转速解出来是天文数字。原因三类。一是字节序理解错J1939 多字节参数是低字节在前二是分辨率与偏移没用上原始值要乘分辨率再加偏移三是 PGN 提取时把 PDU1 的目标地址算进了 PGN导致解析到了另一份定义上。解决挑一个已知物理量的报文做基准比如发动机转速怠速时对着仪表读数反推分辨率和偏移再逐字节打印原始值比对文档定义。PGN 提取统一走前面那个带 PF 判断的函数别在十几个地方各写一份。5.4 闪码人工数出来是错的现象两个人轮流数同一个故障码数出来不一样录像回放也经常多一位少一位。原因位间隔和码间隔都不长肉眼在振动环境下判不准再加上有的机型红灯和警告灯都参与编码两个灯一起闪眼睛跟着一个跑就漏了另一个。解决别硬数。用光电传感器或者高帧率录像采集再送进解码函数。采集时把两个灯分别对准两个传感器通道绝不允许混采。阈值按实测标定一次之后整套设备通用。5.5 两灯方案改完线故障映射全乱了现象把原来的发动机保护白灯取消、改成两灯方案之后原来亮白灯的一批故障现在亮红灯操作手以为出了大问题频繁停机。原因两灯方案的原理是把原本映射到保护灯的所有故障改由红色停机灯提示只改接线、不改软件和标定。所以故障的严重级别并没有变变的只是呈现方式。现场如果没有提前告知操作手误判几乎是必然的。解决改线前先拿故障码表逐条过一遍把所有原本走白灯的码列出来评估其中哪些是“必须立刻停”的、哪些是“可以继续跑但要记录”的。如果确有需要区分可以在整车控制器侧做二次判断用 J1939 上的 SPN/FMI 组合做分级而不是指望灯。改完必须同步更新操作手册和培训这一步千万别省。6. 进阶把这份 PDF 变成能跑回归的报文用例集文档最大的价值不在“读”在“用”。我现在的做法是把它转成一套可执行的用例接到 CI 里每次改解析库都跑一遍。第一步把故障码表抄成 CSV。三列故障码、对应的 SPN 和 FMI 组合、严重级别停机/警告/维护。抄的时候注意文档里有些码是厂商特定的通用 DBC 里没有对应项这些单独打标记。第二步把 J1939 报文清单里的周期、长度、源地址抄成另一张表作为链路自检脚本的期望值。第三步给每条故障码手工构造一条 DM1 帧反解验证。import csv import pytest CASES list(csv.DictReader(open(dtc_cases.csv, encodingutf-8))) def build_dm1(spn, fmi, oc): b0 spn 0xFF b1 (spn 8) 0xFF b2 ((spn 16) 0x07) 5 | (fmi 0x1F) b3 oc 0x7F return bytes([b0, b1, b2, b3]) pytest.mark.parametrize(case, CASES, idslambda c: c[name]) def test_dm1_roundtrip(case): frame build_dm1(int(case[spn]), int(case[fmi]), int(case[oc])) assert frame.hex() case[payload], f{case[name]} 编码不一致 assert parse_dm1(frame) [(int(case[spn]), int(case[fmi]), int(case[oc]))]这段代码的价值是双向的build_dm1用于台架模拟灌给仪表或远程终端做故障注入测试parse_dm1用于实车回放把录制的总线数据批量转成故障列表。两边用同一份 CSV 做基准解析库改错了立刻能发现。再往上一层可以把链路自检脚本也参数化把报文清单里的期望周期和实测周期做比对偏差超过 50% 就报警。这套东西搭起来大概半天但之后每次换机型、换线束、换固件都能在十分钟内拿到一份“这条总线健不健康”的结论比现场靠经验猜快太多。血泪经验是我最早图省事用例只抄了故障码没抄严重级别结果有一次解析库把两个相邻 FMI 的映射搞反了用例全绿实车上却把警告级故障报成了停机级整机在现场反复熄火。从那以后我每次拿到新的协议文档都强制先把“码值 级别 快照字段”三件事一起抄完再动手写一行解析代码。这个习惯救过我好几回。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站