做车载ECU诊断和总线测试这些年,J1939 DM1报文是我遇到最多、也最容易被新同事问晕的一块。无论你是在做发动机、变速箱、整车控制器,还是后处理系统,只要涉及故障上报,基本都绕不开DM1。很多人拿着CAN抓包工具看到一帧18FECA11,数据是03 00 00 7B 00 03 00 00,第一反应往往是“这串十六进制到底什么意思”。更麻烦的是当ECU同时报出七八个故障码时,单帧8字节CAN装不下了,总线上就出现了18ECFF11、18EBFF11这类传输层帧,直接把人看晕。这篇就把J1939 DM1的报文结构、多包故障传输机制和工程实现逻辑完整梳理一遍,尤其适合刚接触J1939协议栈、正在做J1939报文解读或准备做J1939协议栈移植的工程师参考。
1. DM1报文结构拆解:灯状态、闪烁计数与故障码编码
1.1 先看一个真实场景:故障灯亮了,总线上发生了什么
想象一台工程机械在工地上跑,发动机冷却液温度高、车速信号丢失、喷油器驱动异常同时发生。仪表盘上的红色停止灯和琥珀色警告灯都亮了,驾驶员打电话给服务工程师,工程师接上诊断仪,读到的不是一堆文字,而是一串CAN帧。这件事的本质是,ECU通过J1939协议把当前的故障状态广播到总线上,仪表、远程终端、诊断仪都在等这帧数据。
DM1是J1939-73里明确定义的诊断消息,PGN是65226,按十六进制写就是0xFECA。它是一帧周期性广播报文,负责把“当前有哪些激活故障”“这些故障的严重程度有多高”分享给整条总线。故障灯亮不亮、亮哪个灯,其实不是ECU直接点亮仪表灯的物理信号,而是DM1报文里的灯状态位在起作用。仪表收到DM1后解析灯状态,再决定点亮哪个指示灯。
所以DM1翻译成人话就是:ECU的“自述病情单”。它既要告诉你怎么显示灯,也要告诉你具体是哪个部位坏了、坏到什么程度、这个故障出现了多少次。理解了这一点,后面解析的每个字节就都有实际含义了。
1.2 DM1报文到底含哪些东西
DM1报文的长度是变的,不是每个版本都固定8字节。标准结构是:3字节的灯状态和闪烁计数区,后面跟着若干个DTC,每个DTC占4字节。一个故障码时总共7字节,单帧CAN刚好放得下;故障码一多,报文就超过8字节,必须走多包传输。
前3个字节按通用定义来看,第一个字节是灯状态,行业内最常用的解释是Bit0代表红色停止灯、Bit1代表琥珀色警告灯、Bit2代表保护灯;第二个字节通常作为MIL故障指示灯的灯状态,部分厂商也用它做其他灯状态的补充;第三个字节是红色停止灯闪烁计数,表示红色停止灯闪了几下。这里必须提醒一句,不同ECU厂商对灯状态位会有自己的映射方式,解析时优先看该ECU配套的1939-73文档或厂商规格书。
从第4个字节开始就是DTC列表。每个DTC固定4字节,按顺序排列。比如6E 00 04 01就是一个DTC的完整编码,它代表“发动机冷却液温度传感器电压过低,故障发生1次”。DTC在DM1里和诊断仪显示的那个“SPN 110 FMI 4”是对应的,只不过报文里把它压成了4字节的紧凑格式。
1.3 SPN、FMI、OC:故障码的编码规则
DM1里最核心的就是SPN、FMI、OC这三个量。SPN是可疑参数编号,用来标识是哪个部件或参数出问题,比如SPN 110是发动机冷却液温度、SPN 84是车速、SPN 190是发动机转速;FMI是故障模式标识,表示故障的类型,比如电压过高、电压过低、信号丢失、数据异常等;OC是故障发生次数,ECU用7位二进制记录这个故障累计出现多少回,最大126次,超过就饱和在126。
这三者被塞进4字节DTC里,布局非常有规律。假设取DTC的4个字节为byte0~byte3,那么SPN的低8位在byte0、次8位在byte1、第17到第19位在byte2的高3位;byte2的低5位是FMI;byte3是OC的低7位。换算关系是:
SPN = byte0 | (byte1 << 8) | (((byte2 & 0xE0) >> 5) << 16) FMI = byte2 & 0x1F OC = byte3 & 0x7F拿6E 00 04 01来算,0x6E是110,byte1是0,byte2是0x04也就是二进制00000100,高3位是0,低5位是4,所以SPN=110、FMI=4,byte3是0x01,OC=1。对应的故障描述就是“发动机冷却液温度传感器电压过低,发生1次”。这里提到了FMI,顺便放几个常见的FMI对照,FMI 0表示数据有效但高于正常范围、FMI 1表示低于正常范围、FMI 2表示数据不稳定/漂移、FMI 3表示电压过高、FMI 4表示电压过低、FMI 5表示电流过低、FMI 9表示通讯异常、FMI 15表示供电电压过高、FMI 31表示未定义或未知故障。
SPN的编号范围很大,实际工作中也不用背,解析工具里通常带数据库。但要注意一点,标准DTC里SPN默认按19位处理,如果厂商使用了21位扩展SPN,协议栈解析时必须按扩展方式做,否则高位被丢掉,故障码会直接对不上。
2. 为什么DM1会触发多包传输
2.1 8字节CAN帧的硬限制与DM1可变长度
CAN总线的经典帧数据段最长就是8字节,这是CAN协议从设计时就定死的。J1939跑在CAN上,自然继承了这个限制。DM1报文的结构是3字节头加N个4字节DTC,所以报文长度可以算出:
DM1长度 = 3 + 4 × DTC数量一个故障码时长度是7字节,CAN单帧刚好能塞进去。两个故障码时长度变成11字节,超过了8字节的物理限制,单帧就发不下了。这在整车实际运行中并不少见,因为现代ECU往往同时监控几十上百个信号,任何一个信号异常都可能产生DTC,几个故障同时激活是很常见的。
多包传输就是J1939为这种“用户数据超过8字节”的场景专门设计的传输机制,学术一点叫传输协议层,简称TP层。它负责把一大包数据拆成若干个CAN帧发出去,接收方再按顺序拼回去。对于DM1这种周期性广播的诊断报文,当故障码多到单帧放不下时,总线上的表现形式就会从一帧18FECA11变成一组18ECFF11加若干18EBFF11。
2.2 单帧、多包的切换判断
很多人抓包时有个困惑:同一台车同一个ECU,今天看到的是单帧DM1,明天抓到的却是多包帧。原因就是激活故障数在变。一个故障码时ECU走单帧,直接把7字节放一帧;两个及以上故障码时,ECU自动切换成TP多包模式。
这个切换是J1939-73协议栈内部自动完成的,对上层应用来说,发送方只负责把DM1数据交给传输层,由传输层决定怎么拆分;接收方则要根据收到的帧类型自动判断当前是单帧还是多包。具体在协议栈实现上,发送侧会判断待发送数据长度,小于等于8字节就走普通CAN帧,大于8字节就调用TP层组包发送。接收侧相反,先把CAN帧按PGN分类,如果是TP.CM和TP.DT的PGN就进TP组包流程,组完包后再解出原始DM1数据。
实际调试中最容易出的问题,是解析器只处理了单帧DM1,当ECU突然切到多包模式,上层应用直接卡住。这个问题很隐蔽,因为故障码少的时候一切正常,故障一多就开始丢数据。所以做J1939协议栈移植时,一定要把DM1的单帧和多包两条路径都打通,并且用测试工具反复切换故障数量来验证。
2.3 为什么DM1多包选BAM而不是RTS/CTS
J1939的传输层提供了好几种连接管理模式,常见的有BAM、RTS/CTS和Abort。DM1多包传输选的是BAM,也就是广播公告方式。这不是拍脑袋定的,跟DM1本身的通信特征有直接关系。
DM1是广播报文,发送方发给总线上所有节点,没有固定的目标地址。BAM的设计刚好适合这种一对多场景:发送方发一帧广播公告告诉所有人“我要发一个多包,总字节数多少、分几包”,然后直接连续发数据包,不需要等待任何节点确认。RTS/CTS则是一对一的握手方式,发送方发RTS请求,接收方回CTS应答,之后发送方再发数据,适合诊断请求和响应这种点对点传输。如果DM1用RTS/CTS,多个接收节点都要回CTS,很容易造成总线冲突,而且每周期都要握手,效率太低。
所以DM1多包用BAM是协议设计的必然选择。这也解释了为什么你在总线上抓DM1多包时看到的公告帧总是以18ECFF开头,而不是点到点的18ECEC。控制字节0x20表示BAM,0x10表示RTS,0x11表示CTS,0xFF表示Abort,这几个值需要记清楚。
3. 多包故障传输机制核心细节
3.1 TP.CM_BAM:连接管理帧里装了哪些信息
多包传输的第一步是发送TP.CM_BAM帧,也就是连接管理帧。它的PGN是60416,十六进制0xEC00,CAN ID格式是18ECFFxx,其中xx是发送方源地址。这帧8字节数据是有严格格式的,可以对照看:
第一个字节是控制字符,BAM固定填0x20。第二个和第三个字节是待传输用户数据的总字节数,低字节在前,比如总长39字节就填27 00。第四个字节是总包数,表示后面要跟着多少个TP.DT数据包,按公式总包数 = ceil(总字节数 / 7)计算。第五个字节是保留位,固定填0xFF。第六到第八字节是目标PGN,也就是原始报文的PGN,低字节在前。DM1的PGN是0xFECA,所以在BAM帧里就表现为CA FE 00。
看一个实际例子:某ECU源地址是0x11,要发送总长度39字节的DM1数据,分6包传输,那么这帧TP.CM_BAM就是:
18ECFF11 20 27 00 06 FF CA FE 00这个帧很容易理解,它就像是快递面单,写着“包裹总重39字节,分6个箱子,每个箱子最多装7字节,货物类型是DM1”。
3.2 TP.DT:数据包与包序号的约定
TP.DT是真正的数据包,PGN是60160,十六进制0xEB00,CAN ID格式是18EBFFxx。每个TP.DT数据段的第一个字节是包序号,从1开始递增,这个序号非常重要。后面的7个字节是用户数据的切片,也就是DM1真实数据的某一段。
比如总长39字节的DM1,第一包放用户数据的第1到第7字节,第二包放第8到第14字节,依此类推。最后一包如果不够7字节,剩余位置用0xFF填充。这里有个关键点:接收方必须严格按照序号1、2、3、4、5、6的顺序接收,如果中间丢了一包,整个DM1就无法还原,只能等下一个广播周期重传。
包序号最大可以到255,对DM1这种几十字节的报文来说完全够用。但实际总线环境中,如果总线上有其他多包报文在抢时间片,TP.DT包之间的间隔可能被拉长。接收方一般会设置一个超时时间,比如200到500毫秒没等到下一包就判定接收失败,清空缓冲区等待下一次BAM。这个超时值可以在协议栈里配置,具体看项目要求。
3.3 完整实例:9个故障码如何通过6个包传到总线上
为了把整个流程讲透,我构造一个完整例子。假设某ECU源地址是0x11,当前同时激活9个故障码,灯状态是红色停止灯和琥珀色警告灯都亮。9个故障码的信息如下表:
| 序号 | 故障描述 | SPN | FMI | OC |
|---|---|---|---|---|
| DTC1 | 燃油泵故障 | 123 | 3 | 0 |
| DTC2 | 喷油器驱动异常 | 157 | 2 | 0 |
| DTC3 | 发动机转速信号不正常 | 190 | 0 | 0 |
| DTC4 | 车速信号丢失 | 84 | 9 | 0 |
| DTC5 | 冷却液温度传感器电压过低 | 110 | 4 | 1 |
| DTC6 | 进气温度信号异常 | 105 | 0 | 0 |
| DTC7 | 后处理DPF压差异常 | 723 | 15 | 0 |
| DTC8 | 油门踏板位置传感器电压过高 | 132 | 3 | 0 |
| DTC9 | 差速器故障 | 627 | 2 | 0 |
总共9个DTC,每个4字节,加上3字节头,DM1数据总长度是3 + 9 × 4 = 39字节。39除以7向上取整,共6个TP.DT包。按SPN/FMI/OC编码规则,9个DTC依次编码为:
7B 00 03 00 9D 00 02 00 BE 00 00 00 54 00 09 00 6E 00 04 01 69 00 00 00 D3 02 0F 00 84 00 03 00 73 02 02 00灯状态部分,红色停止灯和琥珀色警告灯都亮,第一个灯状态字节填0x03,第二个灯状态字节填0x00,闪烁计数填0x00。完整39字节的DM1数据是:
03 00 00 7B 00 03 00 9D 00 02 00 BE 00 00 00 54 00 09 00 6E 00 04 01 69 00 00 00 D3 02 0F 00 84 00 03 00 73 02 02 00接下来按7字节切片。切片时不关心DTC边界,纯粹按字节顺序切。各TP.DT帧实测抓出来就是这样:
| 阶段 | CAN ID | CAN数据 |
|---|---|---|
| 公告 | 18ECFF11 | 20 27 00 06 FF CA FE 00 |
| 数据包1 | 18EBFF11 | 01 03 00 00 7B 00 03 |
| 数据包2 | 18EBFF11 | 02 9D 00 02 00 BE 00 |
| 数据包3 | 18EBFF11 | 03 00 54 00 09 00 6E |
| 数据包4 | 18EBFF11 | 04 04 01 69 00 00 00 |
| 数据包5 | 18EBFF11 | 05 02 0F 00 84 00 03 |
| 数据包6 | 18EBFF11 | 06 73 02 02 00 FF FF FF |
接收方先把6个TP.DT的序号剥掉,按顺序拼出39字节,再根据BAM帧里的总字节数截断末尾的0xFF填充,就拿到完整的DM1原始数据,可以进入DTC解析了。这张表建议收藏,自己在CANoe或PCAN里抓抓看,对上了就说明你对多包传输机制的理解已经到位了。
4. 工程实践:接收组包、发送组包与解析代码
4.1 接收侧状态机设计与C语言实现
接收多包DM1,本质上是一个状态机。最简版本有三个状态:空闲、收包中、完成。空闲状态下收到TP.CM_BAM并且目标PGN是0xFECA,就记录总字节数、总包数,初始化缓冲区,进入收包中状态。收包中状态下收到TP.DT,先检查包序号是否等于当前期望序号,对上了就把7字节写入缓冲区对应位置,序号加一;如果收满了总包数,就进入完成状态,调用上层解析函数解析DM1。任何一步出错,或者超时没等到下一包,都回到空闲状态,等待下一周期BAM。
我用C语言写一个精简的接收处理函数,方便对照:
#include <string.h> #include <stdint.h> #define DM1_PGN 0xFECA #define TP_CM_PGN 0xEC00 #define TP_DT_PGN 0xEB00 #define TP_CTRL_BAM 0x20 static uint8_t tp_buf[256]; static uint16_t tp_total_len; static uint8_t tp_total_pkt; static uint8_t tp_rx_cnt; static uint8_t tp_rx_expect; void j1939_rx(uint32_t pgn, uint8_t sa, const uint8_t *data, uint8_t len) { if (pgn == TP_CM_PGN && len >= 8 && data[0] == TP_CTRL_BAM) { /* 校验目标PGN,确认是DM1 */ uint32_t target_pgn = data[5] | (data[6] << 8) | (data[7] << 16); if (target_pgn != DM1_PGN) { return; } tp_total_len = data[1] | (data[2] << 8); tp_total_pkt = data[3]; tp_rx_cnt = 0; tp_rx_expect = 1; memset(tp_buf, 0, sizeof(tp_buf)); return; } if (pgn == TP_DT_PGN && len >= 8 && tp_rx_expect != 0) { uint8_t seq = data[0]; if (seq != tp_rx_expect) { return; /* 序号不对,直接丢弃,等下一周期 */ } memcpy(&tp_buf[tp_rx_cnt * 7], &data[1], 7); tp_rx_cnt++; tp_rx_expect++; if (tp_rx_cnt == tp_total_pkt) { parse_dm1(tp_buf, tp_total_len); tp_rx_cnt = 0; tp_rx_expect = 0; /* 回到空闲 */ } } }这段代码把BAM校验、DT序号校验、组包、截断都覆盖了。工程上还要再加一个软件定时器,周期检查收包过程中是否超过规定时间没收到下一包,超时就清空状态。超时时间一般取200到500毫秒,具体要看你项目里DM1的发送周期。DM1的标准发送周期通常是1秒,但也有部分ECU异常时会加速到100毫秒,这个要根据实测调。
4.2 发送侧BAM组包流程
发送侧逻辑相对简单,主要分四步。第一步是把上层给的DM1数据拷贝到发送缓冲区,同时计算总字节数和总包数。第二步发送一帧TP.CM_BAM,控制字节填0x20,后面跟着总字节数低字节、总字节数高字节、总包数、0xFF、目标PGNCA FE 00。第三步循环发送TP.DT,每包首字节是包序号,从1开始,后面跟上7字节数据。第四步是末尾不足7字节时补0xFF,然后结束本次发送。
有个工程细节特别重要:BAM模式下的TP.DT包必须连续发送,中间不能夹着其他长报文,否则接收方可能因为等待超时而丢掉整组包。在某些多任务调度系统里,如果发送任务被其他高优先级任务抢占,时间间隔就容易拉大。一个稳妥做法是,在发送TP.DT期间临时提高发送任务优先级,或者把整组包放到一个不可抢占的临界区内一次性发完。
发送侧伪代码大致是这样:
void j1939_send_dm1(const uint8_t *dm1_data, uint16_t len) { uint8_t pkt_num = (len + 6) / 7; uint8_t cm[8]; cm[0] = 0x20; cm[1] = len & 0xFF; cm[2] = (len >> 8) & 0xFF; cm[3] = pkt_num; cm[4] = 0xFF; cm[5] = 0xCA; cm[6] = 0xFE; cm[7] = 0x00; can_send(0x18ECFF00 | SA, cm, 8); for (uint8_t i = 0; i < pkt_num; i++) { uint8_t dt[8]; dt[0] = i + 1; memset(&dt[1], 0xFF, 7); uint16_t offset = i * 7; for (uint8_t j = 0; j < 7; j++) { if (offset + j < len) { dt[1 + j] = dm1_data[offset + j]; } } can_send(0x18EBFF00 | SA, dt, 8); } }总包数用(len + 6) / 7这种整数向上取整方式,比调ceil函数更高效,嵌入式里常用。
4.3 用Python快速验证DM1解析
调试DM1不一定非得用商业软件。我习惯抓包后用一个小Python脚本快速验证解析结果,判断手里的数据是不是按预期解出SPN/FMI/OC。把抓到的TP组包后的原始DM1数据喂进去就行:
def parse_dm1(data: bytes): lamp_status = data[0] mil_status = data[1] flash_count = data[2] dtc_list = [] for i in range(3, len(data) - 3, 4): b0 = data[i] b1 = data[i + 1] b2 = data[i + 2] b3 = data[i + 3] spn = b0 | (b1 << 8) | (((b2 & 0xE0) >> 5) << 16) fmi = b2 & 0x1F oc = b3 & 0x7F dtc_list.append((spn, fmi, oc)) return lamp_status, mil_status, flash_count, dtc_list raw = bytes.fromhex( "03 00 00 7B 00 03 00 9D 00 02 00 BE 00 00 00 " "54 00 09 00 6E 00 04 01 69 00 00 00 D3 02 0F 00 " "84 00 03 00 73 02 02 00" ) lamp, mil, flash, dtcs = parse_dm1(raw) print("灯状态:", hex(lamp), "MIL:", hex(mil), "闪烁:", flash) for spn, fmi, oc in dtcs: print(f"SPN={spn}, FMI={fmi}, OC={oc}")拿前面9个故障码的例子跑一下,输出应该和预期完全一致。这个脚本可以当做一个随手用的“验算器”,遇到可疑的DTC编码,人工不算,直接丢进去跑一遍,很快就能定位是编码问题还是组包问题。
4.4 移植J1939协议栈时最容易踩的坑
移植J1939协议栈,表面上是把代码从一个MCU搬到另一个MCU,实际搬的是对总线和时序的理解。我在移植过程中踩过不少坑,挑几个最典型的说。
CAN控制器过滤配置是重灾区。很多以太网背景的工程师第一次用CAN协议栈,下意识以为只要使能接收中断就能收到所有帧,实际CAN硬件有验收过滤器,默认过滤表可能只放行少数PGN。如果只放行了0xFECA而没放行0xEC00和0xEB00,那DM1单帧能收到,一旦ECU切到多包模式,BAM和DT全被硬件过滤掉了,上层怎么等都等不到。排查方法很简单,抓包软件能看到,但MCU侧收不到,先关掉全部过滤再一只一只加白名单。
时序问题也常被忽视。BAM模式下,发送端连续发送TP.DT,接收端组包是线性写入,处理时间极短。但如果在RTOS里收包中断只做了FIFO入队,实际组包逻辑在低优先级任务里执行,而该任务又被其他任务阻塞,就容易出现缓冲区已收到新数据但解析任务迟迟不跑的情况。此时用户观感就是“故障码出得很慢”或者“偶发丢故障”。解决办法是把J1939接收处理放到中断下半部或较高优先级任务,确保一个DM1周期内处理完。
另一个坑是填充字节的过滤。TP.DT最后一包不足7字节时补0xFF,解析时如果没按BAM帧里的总字节数截断,会把0xFF当成DTC数据解析。比如39字节的DM1,最后一包只有4字节有效,补了三个0xFF,若不截断,解析器可能多出一个SPN为0xFFFFF的非法故障码。所以正确做法是以BAM里记录的总字节数为准,解析DTC时用len(data) - 3来计算DTC个数,而不是用TP.DT的包数乘以7。
还有一个小坑是发送侧改了SPN的字节顺序。不同厂商对DTC内SPN位数理解不一致,有的按19位,有的强行塞到21位。移植协议栈时,SPN位宽必须作为一个可配置项留出来,不要写死成19位。否则遇到扩展SPN的ECU,解析出来的SPN会整体偏移,排查起来非常痛苦。
5. 常见问题速查与排查建议
5.1 多包DM1常见故障现象与对策
把平时调试遇到的典型问题整理成一张速查表,按“现象、可能原因、排查思路”三个维度去看,能少走很多弯路。
| 现象 | 可能原因 | 排查思路 |
|---|---|---|
| 只看到单帧DM1,看不到多包 | 激活故障码只有1个,DM1长度≤8字节,不需要TP | 用诊断仪或测试工具同时触发多个故障再观察 |
| 收到TP.CM_BAM后不见TP.DT | 发送任务被阻塞、总线错误导致连续发送失败 | 查CAN错误帧和总线占用率,检查发送周期和调度 |
| TP.DT包序号乱序或丢帧 | 总线干扰、接收缓冲区溢出 | 检查终端电阻、CAN位时间配置,抓包统计错误帧 |
| 拼包后DTC数量莫名其妙多出几个 | 没按BAM总字节数截断0xFF填充 | 解析前先按BAM里记录的总长度裁剪数据 |
| 只收到TP.DT没收到TP.CM | 接收过滤表屏蔽了0xEC00 | 检查CAN硬件过滤器和白名单配置 |
| DM1单帧时正常,故障多时解析失败 | 状态机没实现单帧/多帧切换 | 统一入口先判断PGN和长度,再决定走单帧还是TP路径 |
| 解析出的SPN与诊断仪不一致 | SPN位宽配置错误或厂商自定义 | 确认ECU文档中的SPN位数,调整宏定义 |
这个表里的问题,我在不同项目里几乎全遇到过。其中最多的是第一行和最后一行,第一行是因为测试时没制造足够故障,最后一行则是厂商兼容性问题,都需要在项目早期就确认清楚。
5.2 验证多包传输稳定性的几个思路
多包DM1的稳定性验证,不能只在干净总线上测,要主动制造干扰。我个人的做法是分三步走。
第一步是功能验证,用CANoe或PCAN模拟一个ECU发送多包DM1,验证接收端能否正确解析出所有DTC。第二步是边界验证,把DM1的长度从7字节慢慢加到50字节,覆盖单帧到多包的临界点,确认切换过程没有丢包。第三步是抗干扰验证,在总线上周期插入错误帧、拉高总线负载到70%以上,观察接收端是否能在1到2个周期内自动恢复正常。如果连续丢包超过3个周期,说明状态机恢复能力不足,要回查超时重置逻辑。
另外建议在接收端打一组统计日志,记录BAM接收次数、DT成功次数、DT丢弃次数、超时次数。量产后的故障定位,很大程度依赖这些统计量。没有统计日志的协议栈,一旦出现偶发丢故障,基本只能靠猜。
多说一个测试中的小细节:测试ECU从单帧切到多包时,可以先用诊断仪清除故障码,再故意制造两个故障,观察CANoe里是否能立刻看到BAM帧。不要直接在故障码满屏的状态下测,那样可能同时触发多个ECU发多包,总线上的帧交错在一起,对新手来说很难辨别是哪台ECU发的。一台一台来,问题会清楚很多。
我自己在实际调试中的体会是,多包DM1的难点从来不在协议本身,而在于测试环境和状态机细节。只要把BAM公告、DT序号、填充截断这三件事想明白,再配合一个带过滤配置的CAN驱动,大部分问题都能快速定位。遇到解析对不上的故障码,也不要急着怀疑协议栈,先用Python脚本徒手解一遍,往往能发现是字节序或者SPN位宽的问题。这个习惯帮我省了大量时间。