☰
没有DIVA?CAPL手写UDS自动化诊断测试框架实战
2026/10/21 16:50:51 网站建设 项目流程

先说一个在场很多人都有过的处境:CANoe装好了,DIVA没买,但需求单上写着“基于UDS的自动化回归测试”。如果只会手动发报文、肉眼对NRC,几十条用例还能扛,上百条谁都受不了。我后来用CAPL在CANoe里自建了一套基于ISO 14229的UDS自动化测试脚本,不依赖DIVA,照样跑完整个项目的诊断回归。这篇文章就把这套方案的思路、框架、代码骨架和踩坑记录完整放出来,适合正在用CANoe做ECU诊断测试、手里却没有DIVA授权的朋友参考。

1. 为什么没有DIVA时,我坚持自己写CAPL测试脚本

1.1 没有DIVA不等于没有自动化手段

先聊清楚DIVA到底帮你做了什么。DIVA是CANoe里专门做诊断自动化验证的插件,导入诊断描述文件之后,它能自动生成一套覆盖UDS常用服务的测试序列,跑完自动出报告。功能确实强,但现实问题是:它属于额外授权,不是每个CANoe安装包都带;而且它的用例生成流程偏向“开箱即用”,对测试细节的可控性反而没那么高。我认识一位同事,项目上只买了基础CANoe,DIVA没配,诊断测试却一点没少,最后就是靠CAPL脚本一条条把用例写出来的。

CAPL本身就是CANoe的标准组成部分,只要能写CAPL,就相当于免费拿到了一个可以自定义的测试框架。你不需要额外申请工具授权,不需要等供应商给你配置测试序列,直接在一台装了CANoe的电脑上就能开工。这套方案的核心价值不是“替代DIVA”,而是“在无DIVA的情况下,把自动化测试跑起来”。

从实际工作量看,UDS自动化测试里最高频的场景是有数的:会话切换、安全访问、读写数据、例程控制、DTC操作、保活。这些服务的请求格式固定、响应结构可预期,非常适合用CAPL封装成通用函数,然后按用例组合。只要把这几个能力封装好,后面的测试用例就像搭积木一样,写一条新用例的成本会非常低。

1.2 自建CAPL的边界与适用场景

自己写脚本不意味着所有场景都硬扛。我给自己定的判断标准是:用例量在一百条上下、诊断服务相对固定、需要深度定制断言和报告逻辑的项目,自建CAPL是性价比最高的路线。反过来,如果你要做的是整个OEM全量诊断描述验证,几百上千条服务、复杂的DID/子功能矩阵,那种规模还是老实买商业诊断自动化工具。

还有一点,自建CAPL对协议理解的要求更高。你至少需要知道请求帧的字节是怎么组织的、NRC在哪个字节、多帧怎么拼。但也正因为如此,脚本出问题的时候你能直接看到字节级现场,排查起来比黑盒工具透明得多。我后来做故障复现时,经常是直接在脚本里加一行日志,把请求和响应的原始字节打出来,对照14229协议文档就能定位问题。

2. 动手前先把14229和CANoe寻址这两块理清

2.1 UDS请求/响应结构与NRC

UDS的报文结构很清晰:请求是一个SID加若干参数,正响应的SID是请求SID加0x40,负响应则是固定的三个字节:0x7F、请求SID、NRC。自动化脚本里所有的断言逻辑,本质就是在校验这三件事:响应SID是不是预期值,子功能或参数区对不对,有没有NRC。

常用的服务我列一下,脚本里基本都会用到:

功能描述请求SID正响应SID典型子功能/参数
诊断会话控制0x100x500x01默认、0x02编程、0x03扩展
安全访问0x270x670x01请求种子、0x02发送密钥
按DID读数据0x220x62DID低两字节
按DID写数据0x2E0x6EDID低两字节+数据
例程控制0x310x710x01启动、0x02停止、0x03查询结果
读取DTC信息0x190x590x01按状态掩码读
清除DTC0x140x54DTC三字节
保活0x3E0x7E0x00/0x80

负响应的NRC需要单独维护一张表。常见的有:0x12子功能不支持,0x13长度错误,0x22条件不满足,0x31请求超出范围,0x33安全访问失败,0x35会话不允许,0x36安全访问被延迟。测试脚本里如果收到负响应,我习惯把NRC单独存到一个全局变量,断言失败时打印出来,这样报告里可以直接看到“为什么没通过”。

2.2 物理寻址、功能寻址与CAN ID约定

在CANoe里写脚本之前,先搞清寻址模式。UDS测试常用两种:功能寻址和物理寻址。功能寻址是发给总线上所有ECU的,典型CAN ID是0x7DF,响应不一定有,因为每个ECU都可能回。物理寻址是点对点发给某一个ECU,比如请求用0x7E0,对应ECU的响应用0x7E8。这一个节点一个ID,多ECU时就是0x7E1/0x7E9依次往后排。

这个基础如果搞错,最常见的现象就是脚本发出去一个请求,on message里死活等不到响应。你以为是波特率问题、报文周期问题,其实只是ID方向配反了,请求发到了0x7E8,响应却回了0x7E0,跟我预期完全是反的。

注意,这些ID并不是14229强制规定的,而是CAN总线诊断应用里的常规约定。不同OEM可能改ID,所以写脚本前一定要先确认诊断规范里的地址配置。

2.3 没有DIVA,CDD/ODX还要不要加载

很多朋友以为“没有DIVA”就等于“没有诊断描述文件”,其实这是两码事。DIVA是自动化测试执行和报告生成的工具,而CDD/ODX是诊断数据库,描述ECU支持哪些服务、DID、DTC、安全访问参数。CANoe工程加载了CDD之后,CAPL里可以用诊断对象来发请求,ISO-TP多帧、寻址和定时参数都交给工具处理,脚本会干净很多。

如果连CDD都没有,那就只能走裸CAN帧路线:自己组装CAN message,自己处理单帧/多帧,判断响应也要自己解析字节。这两条路我在后面都会给出思路。我的建议是,能拿到CDD就先加载CDD;没有CDD再上裸帧,但要做好多帧处理的准备。

3. 一个不依赖DIVA的CAPL诊断测试框架怎么搭

3.1 工程层面的准备工作

CANoe工程的配置不复杂,但有几个点会影响脚本运行。建工程时选对CAN通道,把波特率配成和被测ECU一致;如果网络里挂了好几个ECU,测试脚本只关心目标ECU,通道上最好单独连那个节点的模拟部分。然后定义两条报文:一条作为诊断请求,ID设为物理请求ID;一条作为诊断响应,ID设为目标ECU的响应ID。有DBC就直接在数据库里建,没有DBC也可以在CAPL里直接使用ID常量。

另外强烈建议在工程里单独建一个CAPL Test Module,而不是把逻辑全堆在on key按键事件里。Test Module自带测试用例管理、报告生成、测试分组,写起来更接近一个真正的自动化测试工程。按键脚本适合临时验证,不适合回归,回归测试一定要走Test Module。

3.2 裸CAN方式:请求发送与响应接收的核心封装

在没有CDD的情况下,我最常用的做法是封装两个基础能力:一个函数负责发请求,一个on message事件负责收响应。

发请求函数:

byte gRespData[4096]; dword gRespLen; byte gRespSID; byte gNRC; void SendUDSRequest(byte data[], dword len) { message *req; req.id = 0x7E0; // 物理请求ID,按项目改 req.DLC = len; int i; for (i = 0; i < len; i++) { req.byte(i) = data[i]; } output(req); } on message 0x7E8 // 物理响应ID,按项目改 { gRespLen = this.DLC; gRespSID = 0x00; gNRC = 0xFF; int i; for (i = 0; i < this.DLC; i++) { gRespData[i] = this.byte(i); } if (gRespLen >= 3 && gRespData[1] == 0x7F) { gNRC = gRespData[2]; // 负响应码 } else if (gRespLen >= 1) { gRespSID = gRespData[0]; // 正响应SID } }

这段代码把“发送”和“接收”解耦了。业务逻辑只关心三个全局变量:gRespData存放响应原始字节,gRespLen是响应长度,gNRC在没有负响应时保持0xFF。后面的测试用例都基于这三个变量做断言,非常直接。

这里有个容易被忽略的点:请求和响应之间要留出足够的时间。ECU处理诊断请求不是瞬时的,有的服务还要擦写Flash、跑校验算法,几十毫秒到几百毫秒都可能。我用TestWaitForTimeout并传入比ECU超时时间略大的等待值,比如150到300毫秒。

3.3 ISO-TP多帧处理的简化策略

上面的封装默认请求和响应都在单帧内完成。单帧的特点是第一个字节是PCI,高四位是0x00到0x0F,表示长度,所以长度不超过7字节时一帧就能装下。很多UDS请求都满足这个条件。

但响应可能超长,比如0x22读VIN加一堆车辆信息、0x19读一长串DTC,这时ECU会走ISO-TP多帧。多帧的第一帧PCI是0x10开头,后面跟着总长度和第一个数据块;后续帧以0x21、0x22……开头,每帧最多7个字节数据。

如果在裸CAN方式下解析多帧,我需要维护一个接收状态机,用on message事件接收后按PCI头判定是第一帧还是后续帧,然后把负载按序拼进gRespData。这个逻辑不算难,但纯手写容易在后续帧序号、超时复位、长度越界这几个地方出bug。所以我的实际建议是:如果工程能加载CDD,就用诊断层对象,让CANoe自己完成ISO-TP拼帧;如果实在没有CDD,再手写状态机。

3.4 测试用例、断言与报告组织

测试用例的组织方式我用的是Test Module加testcase:

testcase TC_10_02_ProgrammingSession() { byte req[2]; req[0] = 0x10; req[1] = 0x02; TestStep("Session", "Send 10 02"); SendUDSRequest(req, 2); TestWaitForTimeout(300); if (gRespLen >= 2 && gRespData[0] == 0x50 && gRespData[1] == 0x02) { TestStep("Session", "Positive response, session switched"); TestPass(); } else { char buf[128]; snprintf(buf, elcount(buf), "Unexpected response, NRC=0x%02X", gNRC); TestFail(buf); } }

每个testcase里有几个固定动作:发请求、等待、断言、通过或失败标记。TestPass和TestFail的输出会自动进入CANoe的测试报告,报告里能区分哪些用例通过、哪些失败、失败时打的是什么日志。

报告要能真正辅助排查问题,就一定要把请求和响应的原始字节打印进去。我习惯用一个工具函数把gRespData[0..gRespLen-1]整体转成十六进制字符串,再在断言失败时打出来。否则报告里只有“响应不对”四个字,事后还要重现现场,效率很低。

4. 六个高频UDS用例的CAPL实现示例

4.1 0x10会话切换

会话切换是UDS测试里最基础的服务,很多功能只在扩展会话或编程会话下才允许执行,所以自动化脚本的第一步通常是切到目标会话。

testcase TC_10_01_DefaultSession() { byte req[2] = {0x10, 0x01}; SendUDSRequest(req, 2); TestWaitForTimeout(200); if (gRespLen >= 2 && gRespData[0] == 0x50 && gRespData[1] == 0x01) { TestPass(); } else { char buf[100]; snprintf(buf, elcount(buf), "Session switch failed, NRC=0x%02X", gNRC); TestFail(buf); } }

会话切换的断言很简单,正响应SID加0x50,子功能原样返回。要额外注意一点:部分ECU在编程会话下会关闭CAN通信或调整波特率,切换前要确认测试环境能承受这种状态变化。

4.2 0x27安全访问

安全访问是UDS里最典型的流程型测试:先发0x27 0x01拿种子,然后计算密钥,再发0x27 0x02把密钥送过去。种子长度和密钥算法完全取决于ECU项目,协议本身不规定。我的做法是把密钥算法单独封装成一个函数,测试脚本不关心算法细节,只关心结果。

testcase TC_27_01_SecurityAccess() { byte req[2] = {0x27, 0x01}; SendUDSRequest(req, 2); TestWaitForTimeout(300); if (gRespLen < 4 || gRespData[0] != 0x67) { TestFail("Seed request failed"); return; } byte seed[8]; dword seedLen = gRespLen - 2; // 去掉0x67和子功能 int i; for (i = 0; i < seedLen; i++) { seed[i] = gRespData[i + 2]; } byte key[8]; dword keyLen = 0; SecurityKeyGen(seed, seedLen, key, keyLen); // 项目相关算法 byte keyReq[10]; keyReq[0] = 0x27; keyReq[1] = 0x02; for (i = 0; i < keyLen; i++) { keyReq[2 + i] = key[i]; } SendUDSRequest(keyReq, 2 + keyLen); TestWaitForTimeout(300); if (gRespLen >= 2 && gRespData[0] == 0x67 && gRespData[1] == 0x02) { TestPass(); } else { char buf[100]; snprintf(buf, elcount(buf), "Security access failed, NRC=0x%02X", gNRC); TestFail(buf); } }

安全访问测起来有几个特点:种子一般是4到8字节,响应常常走多帧;密钥算法错了往往返回0x13或者0x33,排查时先分清是算法问题还是报文长度问题;连续失败几次后ECU可能进入延迟状态,返回0x36,所以安全访问用例最好独立跑,不要排在频繁失败的用例后面。

4.3 0x22按DID读数据

读数据在回归测试里主要干两件事:验证某个DID的值符合预期;配合写服务做“写后读回”校验。以读VIN为例,VIN的DID是0xF190,请求22 F1 90,正响应是62 F1 90后面跟17个ASCII字符。

testcase TC_22_F190_ReadVIN() { byte req[3] = {0x22, 0xF1, 0x90}; SendUDSRequest(req, 3); TestWaitForTimeout(300); if (gRespLen != 20 || gRespData[0] != 0x62) { char buf[120]; snprintf(buf, elcount(buf), "Unexpected VIN response, len=%d", gRespLen); TestFail(buf); return; } if (gRespData[1] == 0xF1 && gRespData[2] == 0x90) { TestStep("VIN", "DID OK"); TestPass(); } else { TestFail("VIN DID mismatch"); } }

如果项目里已经定义了CDD,这个用例甚至可以更简洁:CAPL诊断对象可以自动把DID解析成结构化字段。但裸CAN方式下,DID就是两个字节,别把DID的字节序搞反,有的ECU高位在前,有的低位在前,写死之前先抓一条参考报文核对。

4.4 0x2E写数据后再读回

写数据测试比单纯读数据多一层风险:如果DID是保护性参数,比如标定值、配置字,写错可能让ECU进入异常状态。因此我强烈建议“写后读回”作为标准动作,写进去什么,再读出来就应该是什么。

testcase TC_2E_F192_WriteAndReadBack() { byte writeReq[6] = {0x2E, 0xF1, 0x92, 0x01, 0x02, 0x03}; SendUDSRequest(writeReq, 6); TestWaitForTimeout(300); if (gRespLen < 3 || gRespData[0] != 0x6E) { char buf[100]; snprintf(buf, elcount(buf), "Write failed, NRC=0x%02X", gNRC); TestFail(buf); return; } // 读回同样的DID byte readReq[3] = {0x22, 0xF1, 0x92}; SendUDSRequest(readReq, 3); TestWaitForTimeout(300); if (gRespLen >= 6 && gRespData[0] == 0x62 && gRespData[3] == 0x01 && gRespData[4] == 0x02 && gRespData[5] == 0x03) { TestPass(); } else { TestFail("Write back mismatch"); } }

这种用例很适合做成参数化框架:把DID、写入数据、期望读回数据放到一个数组表里,循环跑,减少重复代码。但注意不要无限制扩展参数表,否则用例失败时定位到具体哪一行会比较费劲。

4.5 0x31例程控制

例程控制的典型场景是运行一个自检例程或擦写例程。请求格式是31 01 RID_HI RID_LO [参数],正响应是71 01 RID_HI RID_LO加状态字节。有些例程是异步的,启动后要等一段时间才能查询结果,脚本不能只等一个响应就收工。

testcase TC_31_01_StartRoutine() { byte req[5] = {0x31, 0x01, 0xFF, 0x00, 0x00}; SendUDSRequest(req, 5); TestWaitForTimeout(300); if (gRespLen < 5 || gRespData[0] != 0x71 || gRespData[3] != 0xFF) { char buf[100]; snprintf(buf, elcount(buf), "Routine start failed, NRC=0x%02X", gNRC); TestFail(buf); return; } // 例程执行结果状态字节为0x01表示通过 if (gRespData[4] == 0x01) { TestPass(); } else { TestFail("Routine result abnormal"); } }

例程测试最需要注意的地方:RID的含义每个ECU都不同,例程执行时间和是否自动终止也差异很大。跑大面积回归前,先人工跑一遍例程,搞清楚返回的状态字节含义,否则脚本断言会写得和实际行为对不上。

4.6 0x3E保活与0x19读DTC

保活请求本身很简单:3E 00或3E 80,正响应7E 00或7E 80。但保活在测试里通常是辅助手段,比如长时间待在扩展会话里做后续测试,脚本要按ECU的会话超时时间周期性地发。比较稳妥的做法是用一个单独的CAPL定时器,每2秒发一次,Timer的周期不能大于ECU的T_P2_Session超时时间。

DTC读取稍微复杂一点。19 01 FF是读所有DTC的状态,正响应的数据区可能很长,多帧拼接后才能解析。裸CAN处理多帧时,要把DTC数量、状态掩码、DTC三字节逐项解析。我的实测经验是,这一块用CDD加载会让CAPL自动完成,测试代码只需要校验0x59开头的正响应,然后比对DTC原始列表即可。

5. 调试实录:这几个坑,DIVA替你踩不了

5.1 响应一直收不到

这是我见过最多的问题。现象很统一:脚本发完请求,gRespLen一直是0,或者on message根本没有进。排查顺序我建议固定如下:

  • 先确认CAN通道、波特率和总线负载正常,用CANoe的Trace窗口抓总线,看请求帧到底有没有发出去。
  • 再确认响应ID对不对。物理寻址方向反了是最常见的低级错误。
  • 然后看DLC长度和字节填充。有些ECU对DLC有严格要求,多发了填充字节会直接回NRC。
  • 最后检查有没有其他节点也在发同样的响应帧,抢占了你脚本的监听。

5.2 NRC反复出现0x13或0x31

0x13在UDS里叫报文长度错误,0x31是请求超出范围。这两个NRC在写脚本时特别常见,原因通常是请求长度没有算对。比如0x2E写数据时,请求长度应当是4字节(SID+DID+数据),多一个少一个都会触发0x13。0x31多见于DID不在当前会话支持列表里,或者安全访问还没做就访问了受限DID。

我的做法是在框架里加一个配置文件,把每个服务允许的最小/最大长度写清楚,发请求前先自检一遍,能挡掉一半以上的低级错误。

5.3 多帧响应数据被截断

裸CAN解析多帧时,最容易出的问题是第一帧里的总长度字段没拿对,或者后续帧序号从0x21开始递增,代码里却写成了统一的递增方式。还有一个很隐蔽的坑:多帧接收过程中如果超时,ECU侧可能已经终止传输了,但脚本这边状态机还停在“等后续帧”,导致下一次响应来了被当成上一帧的数据继续拼。我最终是靠每次发送新请求前,主动清空响应状态和长度缓存来解决这个问题的。

5.4 用例失败但报告里看不到关键信息

测试报告里只有一句“失败”是最让人无语的。我的整改措施是在框架里加了统一的日志点,每个testcase开头打印请求字节,断言处打印响应字节和NRC。后续排查任何失败,先看这两行日志就能圈定大概范围,不需要重新接调试器复现。

把DIVA方案和自建CAPL方案放在一起看,各有各的定位:

维度商业诊断自动化插件自建CAPL脚本
额外成本需要购买授权CANoe已具备,零额外成本
用例生成速度快,但依赖诊断描述质量慢,但完全可控
失败排查透明度中,黑盒居多高,可看到字节级现场
多帧/协议封装全封装有CDD可用诊断层,无CDD需手写
最适用的项目规模大规模全量诊断验证百条以内回归和专项验证

5.5 关于加载CDD后的CAPL诊断对象写法

如果你手头有CDD,我建议不要把时间花在手写ISO-TP上。CANoe加载CDD后,CAPL测试模块里可以通过诊断请求对象发UDS服务,工具会自动处理寻址、多帧、定时参数。典型的流程是:创建诊断请求对象,填充SID和子功能,调用发送接口,然后等待响应。这种方式下,代码量更少,协议稳定性更高,也更容易阅读。

但即便用了诊断层对象,脚本的整体框架仍然离不开testcase、断言和报告组织。换句话说,CDD解决的是协议传输问题,CAPL解决的是测试逻辑和报告问题,两者不冲突,可以组合使用。

最后再分享一个小经验

这套无DIVA的CAPL脚本方案,我实际跑过完整的一轮项目迭代,覆盖了会话、安全访问、写读回、例程和DTC等核心服务。最大的体会是:不要一上来就追求覆盖所有服务,先从一个高频服务打通,比如把0x10会话切换做成完整闭环,再扩展成安全访问和读写,框架自然就成型了。所有无论做多么简单的用例,请求和响应的十六进制字节都要留在报告里,这是后期排查故障最值钱的信息。没有DIVA不是问题,真正的问题是你有没有一套自己能看懂的测试框架。

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

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

立即咨询