J1939诊断应用层协议(SAE J1939-73)是重型车辆诊断的核心标准,它规范了故障码(DTC)的格式、诊断报文的定义以及多包传输机制。本文结合协议原理与C++代码示例,深入解析DM1报文的解析与发送过程。

打开网易新闻 查看精彩图片

1. J1939诊断应用层协议简介

J1939的诊断应用层(对应SAE J1939-73标准)不仅覆盖基础的故障检测功能,还涵盖了监视系统、内存存取、数据转换、引导载入、标定等一系列复杂的交互功能。其核心目的之一是标准化不同厂商、不同车型的诊断信息交互方式,避免重复开发。

这些功能通过不同类型的诊断报文(Diagnostic Message, DM) 实现,例如:

  • DM1 :当前激活故障码

  • DM2 :历史故障码

  • DM3 :历史故障码清除/复位

  • DM11 :冻结帧数据

2. 核心概念解析

在深入代码之前,需要理解J1939诊断的几个核心术语。

2.1 SPN (可疑参数编号)

SPN用于标识具体的子系统、部件或故障对象(如发动机、传感器)。它是一个19位的编号,前511个与SAE J1587兼容,之后的由SAE定义或厂商自定义。

2.2 DTC (诊断故障码) 的构成

J1939的DTC结构与常见的UDS(3字节)不同,它由4个字段组成,共占4个字节

字段

位数

描述

SPN

19位

指示故障发生在哪个部件/子系统

FMI

5位

故障模式指示器,描述故障类型(如电压高、数据不稳定)

OC

7位

故障发生次数

CM

1位

SPN转换方式(通常设为0,表示使用最新版本转换)

2.3 PGN (参数组编号)

诊断报文在总线上通过PGN进行识别。例如,**DM1报文的PGN是65226 (0x00FECA)**。

3. 诊断应用示例:DM1报文(当前故障码)

DM1是诊断中最核心的报文,用于周期性广播当前处于激活状态的故障码。根据SAE J1939-73规范,当存在激活故障时,ECU必须每秒发送一次DM1报文;如果故障状态发生变化,必须立即发送。

3.1 DM1报文格式

DM1报文数据域格式如下(Intel格式,小端模式):

Byte

参数

描述

1

指示灯状态

Bit 7-6: MIL, Bit 5-4: 红色停止灯, Bit 3-2: 琥珀色警告灯, Bit 1-0: 保护灯

2

预留

通常为0xFF

3-6

DTC

SPN (19位) + FMI (5位) + OC (7位) + CM (1位)

7-8

DTC

如果存在多个故障,继续排列

3.2 C++ 代码示例:解析DM1报文

以下代码演示如何从CAN原始数据中提取DM1中的第一个DTC,并解析相关的指示灯状态。

#include  
          
#include

// 定义DTC结构体
struct J1939_DTC {
uint32_t spn; // 可疑参数编号 (0 - 524287)
uint8_t fmi; // 故障模式标识 (0 - 31)
uint8_t oc; // 发生次数 (0 - 126)
bool cm; // 转换方式 (通常为0)
};

// 解析DM1报文的函数
// data: 指向CAN数据域8字节的指针
void Parse_DM1(const uint8_t* data) {
// 1. 解析指示灯状态 (Byte 1)
uint8_t lampStatus = data[0];
bool mil_on = (lampStatus >> 7) & 0x01; // Malfunction Indicator Lamp
bool red_stop_on = (lampStatus >> 5) & 0x01; // Red Stop Lamp
bool amber_warning_on = (lampStatus >> 3) & 0x01; // Amber Warning Lamp
bool protect_on = (lampStatus >> 1) & 0x01; // Protection Lamp

std::cout << "=== DM1 解析结果 ===" << std::endl;
std::cout << "MIL灯: " << (mil_on ? "点亮" : "熄灭") << std::endl;
std::cout << "红色停止灯: " << (red_stop_on ? "点亮" : "熄灭") << std::endl;
std::cout << "琥珀色警告灯: " << (amber_warning_on ? "点亮" : "熄灭") << std::endl;

// 2. 解析第一个DTC (Byte 3-6)
// 注:J1939通常使用Intel格式,即低字节在前
uint32_t raw_spn_fmi = data[3] | (data[4] << 8) | (data[5] << 16) | (data[6] << 24);
J1939_DTC dtc;
// 提取SPN (低19位: bits 0-18)
dtc.spn = raw_spn_fmi & 0x7FFFF;
// 提取FMI (bits 19-23) -> 右移19位,取低5位
dtc.fmi = (raw_spn_fmi >> 19) & 0x1F;
// 提取CM (bit 24) -> 右移24位,取低1位
dtc.cm = (raw_spn_fmi >> 24) & 0x01;
// 提取OC (bits 25-31) -> 右移25位,取低7位
dtc.oc = (raw_spn_fmi >> 25) & 0x7F;

std::cout << "\n=== 当前激活故障码 (DTC ) ===" << std::endl;
std::cout << "SPN: " << dtc.spn << " (部件标识)" << std::endl;
std::cout << "FMI: " << (int)dtc.fmi << " (故障模式)" << std::endl;
std::cout << "OC: " << (int)dtc.oc << " (发生次数)" << std::endl;
std::cout << "CM: " << dtc.cm << " (转换方式)" << std::endl;
}

int main() {
// 模拟总线接收到的一帧DM1报文
// 场景:发动机进气歧管压力(SPN 102)电压低于正常值(FMI 4),发生2次
// 数据排列 (Intel 格式):
// Byte1: 指示灯(0x04) -> AMBER灯亮
// Byte2: 0xFF
// Byte3-6: SPN=102, FMI=4, OC=2, CM=0
// 计算 Raw Value: SPN(102) | (FMI(4)<<19) | (CM(0)<<24) | (OC(2)<<25)
// 结果 = 0x00040066 (小端存储在内存中: 0x66, 0x00, 0x04, 0x00)
uint8_t simulated_dm1[8] = {0x04, 0xFF, 0x66, 0x00, 0x04, 0x00, 0xFF, 0xFF};
Parse_DM1(simulated_dm1);
return 0;
}
3.3 C++ 代码示例:发送DM1报文

在实际ECU开发中,需要构造并发送DM1。以下示例展示如何打包一个DTC并发送(此处模拟传输层发送函数)。

#include  
          
#include
#include

// 模拟CAN发送函数(实际开发中替换为驱动层函数)
bool CAN_Send(uint32_t id, const uint8_t* data, uint8_t len) {
std::cout << "发送CAN ID: 0x" << std::hex << id << std::endl;
std::cout << "数据: ";
for (int i = 0; i < len; i++) {
std::cout << std::hex << (int)data[i] << " ";
}
std::cout << std::dec << std::endl;
return true;
}

// 发送DM1报文的函数
void Send_DM1(uint8_t lamp_status, const J1939_DTC* dtc_list, uint8_t count) {
uint8_t data[8];
memset(data, 0xFF, sizeof(data)); // 初始化填充0xFF
// 设置Byte 1: 指示灯状态
data[0] = lamp_status;
// 设置Byte 2: 预留
data[1] = 0xFF;
if (count > 0 && dtc_list != nullptr) {
// 打包第一个DTC到 Bytes 3-6
// 构造32位原始DTC值: Bytes = SPN + (FMI<<19) + (CM<<24) + (OC<<25)
uint32_t raw_dtc = dtc_list[0].spn;
raw_dtc |= (dtc_list[0].fmi << 19);
raw_dtc |= ((dtc_list[0].cm ? 1 : 0) << 24);
raw_dtc |= (dtc_list[0].oc << 25);
// 写入数组 (Intel格式: 低字节在低地址)
data[3] = raw_dtc & 0xFF;
data[4] = (raw_dtc >> 8) & 0xFF;
data[5] = (raw_dtc >> 16) & 0xFF;
data[6] = (raw_dtc >> 24) & 0xFF;
// 注意:若有多个DTC,需要利用传输层协议(TP)发送多包,此处仅演示单包
}
// DM1的PGN是65226,对应CAN ID通常为 0x18FECA00 + Source Address
uint32_t can_id = 0x18FECA00; // 假设源地址为0
CAN_Send(can_id, data, 8);
}

int main() {
J1939_DTC active_dtc;
active_dtc.spn = 100; // 发动机机油压力
active_dtc.fmi = 1; // 数据低于正常范围
active_dtc.oc = 3; // 发生3次
active_dtc.cm = 0; // 标准转换
// 点亮琥珀色警告灯
uint8_t lamp = 0x04; // Amber灯亮
std::cout << "发送激活故障码 DM1..." << std::endl;
Send_DM1(lamp, &active_dtc, 1);
return 0;
}
4. 多包传输机制

当DM1报文包含的激活故障码超过1个时(即数据超过8字节),必须使用J1939传输层协议(TP) 进行多包传输。

  1. **BAM (广播公告报文)**:发送方先发出PGN 60416的报文,声明即将发送消息的字节总数、分包数量及目标PGN(如65226)。

  2. **DT (数据传输报文)**:随后发送一系列PGN 60160的报文,每一包包含7字节的有效载荷,顺序拼接成完整的DM1报文。

下图展示了多包DM1在总线上的逻辑结构:

[ BAM Packet ] --> 告知: 我要发送22字节数据,分4包发,内容属于DM1
[ DT Packet ] --> 序列号1 + Data(Byte 0-6)
[ DT Packet ] --> 序列号2 + Data(Byte 7-13)
[ DT Packet ] --> 序列号3 + Data(Byte 14-20)
[ DT Packet ] --> 序列号4 + Data(Byte 21-22 + 填充)
5. 总结

J1939诊断应用层协议(J1939-73)通过标准化的SPNFMIDM1报文,为重型车辆提供了高效的故障自诊断方案。理解其DTC的位/字节布局以及多包传输(BAM/DT) 机制,是进行商用车控制器(ECU)开发和故障诊断的基础。

与乘用车常用的UDS协议不同,J1939诊断具有更强的周期性广播特性(每秒一次的固定心跳),这要求开发者在设计ECU软件时,需严格遵循其发送时序,以防止因报文速率过高导致总线负载异常。