ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

J1939 DM1诊断报文全解析:从字节拆解到工程落地

J1939 DM1诊断报文全解析:从字节拆解到工程落地 车载电子和商用车通信这块干久了你就知道J1939绕不开而J1939里最常被提起、也最实用的一帧报文就是DM1诊断报文。无论你是做TBOX远程诊断、仪表报警逻辑还是ECU测试、诊断仪开发都免不了跟它打交道。说实话网上讲J1939协议的资料不少但能把这帧8字节报文从头到尾拆清楚再带着你落地的文章其实不算多。这篇我就打算把DM1从“它是什么”讲到“怎么解析”再讲到“工程里怎么用”全套实战流程铺开确保你照着做就能把一帧原始CAN数据变成一眼就能看懂的故障信息。这次分享适合几类人看刚入行做汽车电子、嵌入式通信的工程师正在做TBOX或仪表端诊断功能的开发人员以及所有需要调CAN报文、查故障码但还没系统看过J1939-73标准文档的朋友。1. 先从整车通信的视角认识DM1它是谁在哪一层1.1 J1939不是“一种报文”而是一套完整语言很多人一听到SAE J1939第一反应是“这不就是CAN总线的商用车协议吗”。这个理解方向对但有点糙。J1939不只是规定了波特率、帧格式它定义的是从物理层往上一直到应用层的完整通信语言。物理层大多还是CAN 2.0B也就是大家熟悉的500K或250K波特率、29位扩展帧ID。往上是数据链路层、网络层再到传输协议、应用层参数组。换句话说CAN总线负责把0和1搬来搬去J1939负责告诉这些0和1“每一块拼起来是什么含义”。在J1939里面互相关联的报文按“参数组编号”来组织PGN就是参数组编号Parameter Group Number。比如转速、油门位置、冷却液温度这些实时数据会打包在发动机参数组里周期性发送而诊断相关的信息则单独放在诊断报文组里。DM1的全称是Diagnostic Message 1也就是第一类诊断报文它的PGN是0xFECA用于ECU周期性上报自己检测到的激活故障码包括故障灯状态、故障码编号、故障类型以及故障出现次数。这套机制是J1939-73标准的重点内容也是整车诊断体系里最基础的一环。干这行时间长了你会发现J1939里很多PGN的解析逻辑高度相似但DM1值得单独拎出来讲原因是它结构紧凑、字段复用、信息密度极高而且在实际项目里踩坑率特别高。一旦把DM1吃透了后面看DM2、DM3这类诊断报文都会轻松很多。1.2 DM1在协议栈中的角色故障的实时喇叭如果说发动机转速、车速报文是ECU在“报日常数据”那DM1就是ECU在“报异常”。它做的事情非常简单周期性地告诉总线上其他节点我这个控制器当前认为有哪些故障码是存在的以及这些故障对应的报警灯状态是什么。最典型的场景就是仪表端。仪表不需要知道ECU内部复杂的诊断逻辑它只需要收到DM1后把灯点亮、把故障码显示出来就行。另一个典型场景是TBOX远程诊断当整车某个控制器报了故障TBOX在CAN总线上抓到DM1解析出发动机或变速箱的故障码再通过4G/5G网络上报到云端平台云端就能远程看到这台车出了什么问题。DM1在协议里的一个重要特点是它属于周期性报文绝大多数ECU的DM1发送周期是1秒左右。也就是说正常情况下每秒钟总线上都会有来自各个控制器的DM1帧。如果故障消失报文里的灯状态清零DTC字段归零或不再携带有效故障。如果严重故障出现灯状态字节会对应置位同时带上具体故障码。由于它周期固定、内容明确很多整车故障分析都把DM1作为第一突破口。我把DM1比作ECU的“实时喇叭”它不存历史、不追求全面只负责把你必须立刻知道的故障喊出来。历史故障的台账由DM2这类报文负责那是另一套逻辑。2. DM1报文逐字节拆解8字节里到底藏了什么2.1 29位CAN ID与源地址这条报文是谁发的在看DM1的数据场之前先看它的CAN ID。J1939用的是29位扩展帧ID这29位里分布着优先权、保留位、数据页、PDU格式PF、PDU特定场PS和源地址SA。DM1的标准ID是0x18FECAxx其中最后的xx就是源地址。比如发动机控制器地址一般是0x00变速箱控制器是0x03ABS是0x0B车身控制器、网关等各有各的地址。所以在实际抓报文时看到0x18FECA03你就知道是变速箱发的看到0x18FECA00就是发动机发的。这一点特别关键因为整车上很多控制器都会发DM1它们的PGN都一样、ID前缀都一样唯独靠最后的源地址区分。CAN ID里还有一个信息值得注意默认优先级。DM1的优先级通常配置为6属于较高优先级。这意味着当总线繁忙时DM1会比大多数普通数据报文更早获得传输机会。设计者这么安排是有道理的故障信息必须快速传达不能因为总线上数据多就被挤掉。实际抓包时你也可以验证发动机出故障的瞬间DM1在总线上的表现往往比普通数据帧更“积极”。有些朋友刚开始接触时习惯用CAN标准帧的思路去过滤ID结果发现始终抓不到DM1。原因就在这里J1939走的是扩展帧必须配置29位ID验收不是11位标准帧。调试工具里一定要把“扩展帧/标准帧”模式选对。2.2 8字节数据场灯状态、SPN、FMI、OC/CMDM1数据场一共8个字节但它的排布方式对新手不太友好。因为协议设计者为了在有限字节里塞下尽可能多的DTC把SPN拆成了三段散布在不同的字节里还用半字节、位域去复用空间。第一次看这个表的人十有八九会蒙。先看总布局字节位含义Byte 0Bit 0红色停止灯Red StopByte 0Bit 1琥珀色警告灯Amber WarningByte 0Bit 2保护灯ProtectByte 0Bit 3红色警告灯Red WarningByte 1全部第一个DTC的SPN低8位SPN bit 1-8Byte 2全部第一个DTC的SPN中8位SPN bit 9-16Byte 3Bit 7-5第一个DTC的SPN高3位SPN bit 17-19Byte 3Bit 4-0第一个DTC的FMI5位故障模式标识Byte 4Bit 7第一个DTC的CM转换/状态标志Byte 4Bit 6-0第一个DTC的OC发生次数7位Byte 5Bit 7-0第二个DTC的SPN和FMIByte 6Bit 7-0第三个DTC的SPN和FMIByte 7Bit 7-0第四个DTC的SPN和FMI这里要特别注意从第2个DTC开始字节里就没有独立的OC/CM字段了。原因是帧长度固定8字节放不下。所以一个DM1最多携带4个DTC其中只有第一个DTC带完整的灯状态、OC和CM信息。后面3个DTC只给你SPN和FMI不给你次数。灯状态字节在最前面这是整个J1939-73里故障分级的核心。红色停止灯亮了说明出现必须立即停车的故障比如机油压力过低、冷却液温度过高琥珀色警告灯亮了说明需要尽快维修但还能行驶保护灯和红色警告灯在不同厂商产品里定义略有差异但多数遵循SAE推荐逻辑。实际工程里仪表就是靠这个字节决定点亮哪个灯的。SPN、FMI、OC、CM这四个缩写也要一个一个说清楚。SPN全称Suspected Parameter Number可以理解为“可疑参数编号”它指向一个具体的物理量或子系统比如发动机冷却液温度、进气压力、车速信号等。FMI全称Failure Mode Identifier描述这个参数到底怎么坏了比如电压过高、信号丢失、超限等。OC是出现次数CM标志当前是否处于激活状态。如果把SPN和FMI翻译成人话SPN相当于“哪个部位出了问题”FMI相当于“这个部位是怎么个问题”。“发动机冷却液温度”“电压高于正常值”拼在一起就得到一条具体可读的故障描述。2.3 SPN/FMI到底怎么查标准文档和工程库解析DM1得到SPN和FMI之后还得把它们翻译成人类能读的内容。这一步需要标准定义。SAE J1939-71主要定义SPN的具体含义J1939-73定义诊断相关的报文结构和FMI含义。这两个文档是搞诊断绕不过去的参考书。FMI的数量有限一共5位最多0到31。工程上最常见的就那么几个FMI含义典型场景0数据有效但高于正常工作范围最高等级冷却液温度超高1数据有效但低于正常工作范围最高等级机油压力低3电压高于正常值或对高电平短路传感器信号线对电源短路4电压低于正常值或对低电平短路传感器信号线对地短路5电流低于正常值或开路传感器断线6电流高于正常值或对地短路执行器驱动过流7机械系统响应不正常执行器卡滞9报文更新率异常节点通信异常11故障模式无法识别未知故障SPN就不一样了它是个19位编号理论上能定义到524287个而实际标准里真正用到的SPN有好几千个。靠人脑去记不现实工程上一般用诊断数据库文件DBC或专有的诊断数据库管理SPN到文本的映射。你抓完报文把SPN丢进去查就能得到完整描述。如果没有现成数据库也可以先维护一张常用SPN表把项目里出现频次最高的几十个编号手工整理进去实战中完全够用。这里顺便提醒一句查SPN时要注意它是十进制还是十六进制。报文里字节组合出来的是二进制数值你打印成10进制之后再去查表别顺手转成16进制搞混了。我见过不少同事在这个细节上翻车查半天发现查的是个不存在的编号。3. 实际抓包解析完整演示从原始帧到一目了然的故障码3.1 准备工作CAN设备、波特率与过滤规则解析DM1之前得先把报文抓到手。硬件方面建议用带CAN接口的USBCAN卡或PCAN配合对应上位机软件比如周立功的CANTest、PCAN-View、CANalyzer都可以。J1939常用的波特率是250K也有部分车型用500K抓包前一定先确认。连接上总线之后配置软件里的“验收滤波”。如果只想看DM1就把29位ID设为0x18FECA00掩码设置成0xFFFFFF00。这样任何源地址发出来的DM1都能被过滤出来而其他报文直接忽略。如果接收到多个ECU的DM1想区分来源就看ID末尾的源地址。实际调试中还有一个细节有些工具默认只接收标准帧或只显示11位ID收到扩展帧不显示。用周立功软件时记得在CAN参数配置里选择“29位扩展帧”在接收显示区也切到扩展帧格式否则忙活半天一条报文都看不到。3.2 亲手拆一帧DM1逐字节还原一个真实故障假设我抓到一帧ID为0x18FECA00的CAN报文数据场是8个字节内容如下01 6E 00 03 20 00 00 00从ID的源地址0x00可以断定这是发动机控制器发出来的DM1。下面逐字节拆。Byte0 0x01转成二进制就是00000001最低位是1对应红色停止灯。这意味着发动机报了需要立即停车的故障仪表必须立刻提醒驾驶员。Byte1 0x6E也就是十进制的110这是第一个DTC的SPN低8位。 Byte2 0x00这是SPN的中8位。 Byte3 0x03高3位是000即SPN bits 17-19都是0低5位是00011即FMI3。组合SPN公式SPN ((Byte3 5) 16) | (Byte2 8) | Byte1 ((0x03 5) 16) | (0x00 8) | 0x6E 0 | 0 | 110 110SPN 110在SAE J1939-71里有标准定义发动机冷却液温度。再看FMI3查表就是“电压高于正常值或对高电平短路”。所以这条故障的完整描述就是发动机冷却液温度传感器电压高于正常值大概率是传感器信号线对电源短路了或者传感器本身损坏。Byte4 0x20二进制00100000。最高位是0也就是CM0表示该故障当前处于激活状态不是历史遗留。低7位是0OC0意思是没有计数从ECU内部逻辑来看这个故障不是那种反复出现、累计多次的类型。如果OC显示比如32就说明这个故障已经被记录过32次维修时可以拿这个数据判断故障是偶发还是持续存在。到这里我已经解析出了第一个DTC剩下的Byte5到Byte7都是0说明后面没有其他DTC。整帧报文翻译成人话就是发动机当前报了冷却液温度传感器对高电平短路的激活故障仪表红色停止灯亮。3.3 代码实现C语言与Python的解析落地光会手工拆还不够工程上一定得写成解析函数。C语言版本我一般这么写注意避开位域的坑#include stdint.h #include string.h typedef struct { uint8_t lamp_stop; uint8_t lamp_warning; uint8_t lamp_protect; uint8_t lamp_red_warning; uint32_t spn; uint8_t fmi; uint8_t cm; uint8_t oc; } dm1_dtc_t; int dm1_parse(const uint8_t *data, uint8_t len, dm1_dtc_t *dtc) { if (data NULL || len 6 || dtc NULL) { return -1; } dtc-lamp_stop (data[0] 0) 0x01; dtc-lamp_warning (data[0] 1) 0x01; dtc-lamp_protect (data[0] 2) 0x01; dtc-lamp_red_warning (data[0] 3) 0x01; dtc-spn ((uint32_t)(data[3] 5) 16) | ((uint32_t)data[2] 8) | (uint32_t)data[1]; dtc-fmi data[3] 0x1F; dtc-cm (data[4] 7) 0x01; dtc-oc data[4] 0x7F; return 0; }为什么不用位域结构体因为不同编译器的位域内存布局不一样如果嵌入式MCU和上位机PC共同维护同一套代码位域很容易产生可移植性问题。用移位和掩码虽然啰嗦一点但逻辑百分百确定。Python版本适合做上位机和离线分析脚本我平时调试时会用python-can直接抓包实时解析import can def parse_dm1(data): if len(data) 6: return None lamp data[0] spn ((data[3] 5) 16) | (data[2] 8) | data[1] fmi data[3] 0x1F cm (data[4] 7) 0x01 oc data[4] 0x7F return { lamp: { red_stop: bool(lamp 0x01), amber_warning: bool(lamp 0x02), protect: bool(lamp 0x04), red_warning: bool(lamp 0x08), }, spn: spn, fmi: fmi, cm: cm, oc: oc, } bus can.interface.Bus(channelcan0, interfacesocketcan, bitrate250000) for msg in bus: if msg.arbitration_id 0xFFFFFF00 0x18FECA00: dtc parse_dm1(msg.data) if dtc: print(dtc)这个脚本里用msg.arbitration_id 0xFFFFFF00做掩码过滤目的是不管源地址是发动机还是变速箱帧都能被捕获到。如果要在多ECU的车上区分故障来源再把msg.arbitration_id 0xFF拿出来用。Python脚本的好处是不需要编译在PC上插一个CAN盒就能跑。你可以把它扩展成带GUI的小工具也可以把输出直接转成JSON发给云平台后面做远程诊断系统会非常顺手。4. 应用实践从报文到业务系统的落地4.1 仪表报警与ECU故障处理联动DM1最常见的落地场景就是仪表报警。仪表通过CAN总线接收多个ECU发来的DM1解析出灯状态和故障码后显示给驾驶员。红色停止灯和琥珀色警告灯的优先级通常是不同的极限情况下红色灯会触发蜂鸣器甚至在仪表盘上弹出一个具体的故障码窗口。但要注意仪表显示的不一定只是灯状态很多主机厂会要求仪表同步显示故障码或中文描述。这就意味着仪表端得内置一张SPN/FMI映射表而且随着车型迭代持续更新。每次新车型导入新ECU这张表都要同步扩展一次。工程上比较推荐的做法是把SPN映射表做成配置文件放在仪表存储区通过OTA或诊断工具更新不要写死在固件里否则后期维护成本极高。4.2 车载终端远程诊断与故障上云TBOX接收DM1之后解析出的故障信息需要转换成标准格式上报到云端。目前行业里常见的做法是TBOX内部维护一个诊断模块收到DM1后把它转换成类似下面的JSON结构{ source_addr: 0, lamp: { red_stop: true, amber_warning: false, protect: false, red_warning: false }, dtc: { spn: 110, fmi: 3, oc: 0, cm: 0 }, timestamp: 1717387200 }这个JSON可以走MQTT或HTTPS上报到云端。云端再根据SPN/FMI查询故障知识库生成一条面向运维人员或驾驶员的可读记录。整车厂还能结合地理信息、车辆工况数据做进一步判断比如这台车长期在坡道行驶导致变速箱油温偏高这种识别就得靠云端的综合数据分析不是单靠DM1能解决的。我在做这类项目时有个经验TBOX端的解析逻辑不要做太复杂因为它资源有限。把SPN/FMI的字符串映射交给云端做就行TBOX只负责把结构化的SPN/FMI和灯状态发上去。这样TBOX不依赖庞大的故障码数据库升级故障码表也不用刷TBOX固件改云端配置就能生效。4.3 诊断仪与自动化测试的工程做法在售后诊断仪上DM1被用来快速判断车辆当前有没有激活故障。诊断仪上电后第一件事往往是先请求DM1看看故障有哪些如果DM1报出一大串故障再进一步通过诊断服务读取冻结帧或扩展诊断信息。因为DM1是周期报文诊断仪不需要发送任何请求帧只要在总线上侦听就能拿到这一点与UDS诊断问答式交互完全不同也非常方便。自动化测试里更依赖DM1。ECU测试台架或整车在环测试中工程师会构造各种异常工况然后断言DM1是否按预期报出指定故障码。这里有一个很实用的做法把DM1的期望值写进测试脚本比如“踩下油门踏板到大开度时DM1里SPN91、FMI0”脚本实时读取DM1并与期望匹配。如果超时未收到或字段不匹配直接判测试失败。用这种方式一天可以自动跑几百条故障注入用例比纯人工盯着CAN报文靠谱得多。5. 常见问题与排查技巧实录5.1 报文层面抓不到、抓不全、识别不了很多新手第一次抓DM1打开软件等半天一条报文都没有。先别急着怀疑ECU坏了按这个顺序排查波特率对不对、验收滤波是不是把ID过滤掉了、工具是不是在标准帧模式下。J1939是250K还是500K不同车型不一样抓不到先试试切换波特率。如果总线上有其他报文你能看到只是看不到DM1那大概率是滤波配置问题。还有一种情况是DM1确实存在但周期比较长。有些ECU只在故障状态变化后发送DM1有些则稳定周期发送1秒一次。如果你的工具记录时间太短可能刚好没抓到。建议至少监听30秒再看看有没有帧出现。5.2 解析层面数值对不上、含义查不到解析SPN最常见的错误是把Byte1和Byte2搞反。J1939-73的字节顺序是大端在前Byte1是SPN的低8位Byte2是高8位Byte3再补高3位。如果你用CAN工具直接显示的“Intel格式”小端看数据看到的顺序可能会反过来这时候一定要把工具的“字节顺序”显示切到Motorola格式或手动确认原始字节排列。另外FMI的取值范围是0到31但有些报文里Byte3低5位可能包含保留位或扩展位。解析时一定要先做 0x1F掩码不要直接拿去当FMI。Byte4的OC也要做 0x7F把最高位的CM剥掉。这个习惯养成之后能省去不少排查时间。SPN查不到意思时先确认你的数据库版本。SAE每年都会更新SPN定义新车型上经常出现老数据库里没有的SPN。如果你是做整车厂项目的尽量以主机厂发布的诊断规范为准而不是只依赖公开的通用数据库。5.3 应用层面灯状态、OC/CM、历史故障的坑灯状态和OC/CM这两个地方非常容易踩坑。先说灯状态。Byte0里bit0是红色停止灯bit1是琥珀色警告灯。有些ECU把这两个灯同时点亮比如温度过高同时触发了红色和琥珀色这时候你解析出的0x03能正确反映两个灯状态但如果你的仪表逻辑只判断了bit0没判断bit1就会漏报。诊断逻辑最好按位判断不要当成完整数值比较。有些工程师喜欢写if (data[0] 0x01)这就把同时亮两个灯的情况漏掉了正确的写法是if (data[0] 0x01)。再说OC/CM。CM0表示故障处于激活状态CM1表示这是曾经存在过、现在已经不激活的历史故障。但这个字段在不同ECU实现中未必完全一致。有的ECU对历史故障的OC计数会一直累加有的则清零所以不要完全依赖OC判断故障严重程度只把它当参考。很多资料里也会把OC直译为“发生次数”你看到数值很大别慌那不一定代表故障一直没解决可能是偶发故障的累计计数。还有DM1携带的DTC数量最多4个但整车可能同时存在几十个故障。超过4个时需要用DM2PGN 0xFECC去读取完整的历史故障列表。DM2有可能是多包传输用的是J1939的TP.CM/TP.DT机制不能按单帧DM1的方式解析。这里很多人容易混淆记住一句DM1是单帧上报当前激活故障DM2才管“全家桶”历史台账。最后再说一个我自己的经验用CAN工具看DM1时别只盯着十六进制原始数据尽量让工具帮忙解析出PGN和源地址。但工具解析只适合快速判断真到了写代码或者分析疑难故障的时候从头到尾自己拆一遍字节才最可靠。工具给的结果终究不如你亲手算出来的SPN/FMI让人踏实。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进