拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

CANoe DIVA工程中基于CAPL的UDS诊断服务前置条件自动化验证实践

CANoe DIVA工程中基于CAPL的UDS诊断服务前置条件自动化验证实践 1. 为什么要在DIVA工程里做服务前置条件自动化验证做过车载诊断测试的人都知道DIVADiagnostic Integration and Validation Assistant在CANoe里扮演的角色是把诊断描述文件CDD/ODX里的诊断服务、会话、安全等级、DTC这些抽象定义变成可以实际执行和验证的测试序列。但真正在项目里跑起来之后你会发现一个很现实的问题大部分诊断服务不是你想调就能调的。举个最常见的例子。UDS协议里很多服务比如0x2E写数据、0x31例程控制、0x27安全访问之后的各类操作都要求ECU先处于扩展会话0x10 03有些还要求先通过安全访问0x27甚至有些OEM自定义的服务还要求特定的前置条件组合比如扩展会话安全等级1特定例程已启动。如果你在DIVA里直接去调这些服务ECU会回你一个7F否定响应常见的就是0x7F 22 33条件不满足或者0x7F 2E 7E子功能不支持当前会话。手工测试的时候你可以在CANoe的Diagnostic Console里一步步手动切会话、做安全访问然后再去点目标服务。但一旦你要做批量回归、自动化验证手工操作就完全不可行了。这时候就需要用CAPL脚本把前置条件这一整套动作自动化掉。我自己的项目里一个典型的诊断测试用例集大概有200到400条如果每条都手工准备前置条件一个人一天最多跑几十条而且容易漏步骤、点错顺序。用CAPL把前置条件封装成函数之后整个测试序列可以一键跑完效率提升不是一点半点。这篇文章就把我在DIVA工程里做服务前置条件自动化验证的完整思路和实操细节拆开讲包括CAPL脚本怎么写、DIVA工程怎么配、踩过哪些坑尽量让刚接触CANoe诊断测试的朋友也能照着做出来。提示本文假设你已经对CANoe的基本操作、DIVA工程结构、UDS诊断协议有初步了解。如果完全没接触过DIVA建议先把CANoe自带的Diagnostic示例工程跑一遍理解诊断描述文件是怎么导入的。2. 前置条件自动化验证的整体设计思路2.1 先搞清楚前置条件到底包含哪些东西在动手写脚本之前必须先把前置条件分类。不同项目的ECU要求不一样但归纳下来无非这么几类会话状态默认会话0x01、编程会话0x02、扩展会话0x03。大部分需要写操作的服务都要求扩展会话。安全等级0x27服务的奇数子功能请求种子偶数子功能发送密钥。安全等级可能是1、2、3甚至更多取决于ECU设计。通信控制有些ECU要求先发0x28通信控制比如关闭非诊断报文的发送才能进入某些特殊状态。例程状态0x31例程控制某些服务要求特定例程已经处于运行状态。DTC设置状态有些测试要求先清除DTC或者先制造特定DTC。时间条件比如安全访问之后有超时时间S3 timer会话保持也有超时通常5秒超时后会自动回默认会话。这六类里前两类是最高频的几乎每个需要写操作的服务都要用到。第三到第五类属于项目特定需要根据CDD里的定义来。第六类是最容易被忽略的也是自动化脚本里最容易出问题的地方。2.2 为什么选择CAPL而不是DIVA自带的测试序列DIVA本身是支持配置测试序列的你可以在DIVA的Test Case里配置一系列诊断请求按顺序执行。那为什么还要用CAPL原因有几个。第一DIVA的测试序列是静态配置的它适合做固定的、线性的测试流程但如果你要根据ECU的响应动态决定下一步做什么比如安全访问种子长度不固定、需要根据种子计算密钥DIVA的配置能力就不够了。第二CAPL可以访问CANoe的系统变量、环境变量、信号可以把诊断和其他总线行为联动起来比如等某个信号变成特定值之后再发诊断请求。第三CAPL可以写循环、条件判断、定时器做批量测试的时候灵活得多。我一般的做法是DIVA负责诊断描述文件的导入和服务定义CAPL负责测试逻辑和前置条件封装。两者配合DIVA提供能调什么服务CAPL决定怎么调、什么时候调。2.3 整体架构设计我的工程里前置条件自动化验证的架构大概是这样分层的底层基础诊断函数库。封装会话切换、安全访问、通信控制这些原子操作每个函数只做一件事带返回值。中层前置条件组合函数。比如EnsureExtendedSession()、EnsureSecurityLevel1()内部调用底层函数并做状态检查。上层测试用例脚本。每个测试用例开头调用前置条件函数确认条件满足后再执行目标服务。这种分层的好处是前置条件的逻辑只写一遍所有测试用例复用。如果某个ECU的安全访问算法变了只需要改底层的一个函数上层不用动。注意不要把前置条件和测试用例混在一起写。我见过有同事在每个测试用例里都手写一遍切扩展会话安全访问的代码结果ECU换了之后要改几百个地方非常痛苦。3. CAPL脚本核心细节与实操要点3.1 会话切换的CAPL实现与状态判断先看最基础的会话切换。在CAPL里调用诊断服务有两种方式一种是直接用diagRequest对象另一种是用DiagSendRequest函数。我推荐用诊断描述文件里定义好的请求对象因为这样CANoe会自动帮你处理寻址、长度、子功能这些细节。假设你的CDD里已经定义了会话切换服务CAPL里大概这样写variables { diagRequest ECU1.SessionControl_Extended reqSessionExt; diagResponse ECU1.SessionControl_Extended respSessionExt; msTimer tSessionTimeout; int gSessionState 0; // 0默认, 3扩展, 2编程 } int EnsureExtendedSession() { // 如果已经是扩展会话直接返回成功 if (gSessionState 3) { return 1; } // 发送扩展会话请求 reqSessionExt.SetSubFunction(0x03); DiagSendRequest(reqSessionExt); // 等待响应超时时间设500ms if (TestWaitForDiagResponse(reqSessionExt, 500) 1) { // 检查肯定响应 if (diagGetLastResponseCode(reqSessionExt) 0x50) { gSessionState 3; return 1; } } write(切换扩展会话失败); return 0; }这里有几个关键点。第一用全局变量记录当前会话状态避免每次都重复发请求。ECU的会话是有超时的通常5秒所以这个状态变量需要配合定时器来维护。第二超时时间要合理。500ms是我实测下来比较稳妥的值太短了容易误判ECU还没响应完太长了会拖慢整个测试序列。第三必须检查响应码。0x50是会话切换的肯定响应如果收到0x7F就是否定响应要区分是条件不满足还是其他原因。关于会话超时我一般会起一个定时器在会话切换成功后启动超时时间设为ECU实际S3 timer的80%左右。比如ECU的S3 timer是5秒我就设4秒在定时器回调里把gSessionState重置为0。这样上层函数在调用前置条件时如果发现状态变量是0就会重新切会话避免因为超时导致后续服务失败。3.2 安全访问的种子密钥处理安全访问是前置条件里最麻烦的一环因为涉及种子和密钥的计算。在CAPL里0x27服务的处理有两种模式模式一用CDD里配置的DLL自动计算密钥。如果你的CDD里已经配置了安全算法DLL就是热词里提到的canoe基于aes 128算法的seedkey dll那么CAPL里只需要发请求CANoe会自动调用DLL算密钥。这种模式下代码很简单int EnsureSecurityLevel1() { diagRequest ECU1.SecurityAccess_Seed reqSeed; diagRequest ECU1.SecurityAccess_Key reqKey; // 请求种子子功能0x01 reqSeed.SetSubFunction(0x01); DiagSendRequest(reqSeed); if (TestWaitForDiagResponse(reqSeed, 500) ! 1) { write(请求种子超时); return 0; } // 发送密钥子功能0x02密钥由DLL自动填充 reqKey.SetSubFunction(0x02); DiagSendRequest(reqKey); if (TestWaitForDiagResponse(reqKey, 500) ! 1) { write(发送密钥超时); return 0; } if (diagGetLastResponseCode(reqKey) 0x67) { return 1; } return 0; }模式二在CAPL里手动实现密钥算法。有些项目出于保密原因不把算法做成DLL而是要求测试方自己实现。这时候就需要在CAPL里写算法。CAPL支持基本的位运算、循环、数组操作实现常见的异或、移位、查表类算法没问题。但如果是AES 128这种复杂算法CAPL写起来就很吃力了一般还是建议用DLL。我踩过的一个坑是种子长度不固定。有些ECU的种子是4字节有些是8字节甚至16字节。如果你在CAPL里手动处理一定要先读种子的实际长度不要写死。用diagGetParameter可以获取响应里的参数值具体用法取决于CDD里的参数定义。还有一个细节安全访问失败后的锁定。很多ECU在连续多次通常是3次密钥错误后会锁定一段时间比如10秒期间不再接受安全访问请求。自动化脚本里如果没处理这个一旦失败就会陷入死循环。我的做法是在EnsureSecurityLevel1里加一个重试计数器失败超过2次就返回失败让上层决定是跳过还是终止。3.3 通信控制与例程控制的前置处理通信控制0x28和例程控制0x31属于项目特定的前置条件。不是所有ECU都需要但一旦需要就必须在会话和安全访问之后、目标服务之前执行。通信控制的典型用法是关闭非诊断报文的发送让总线安静下来避免干扰诊断通信。CAPL里这样写int EnsureCommunicationControl() { diagRequest ECU1.CommControl_DisableRxAndTx reqComm; reqComm.SetSubFunction(0x03); // 关闭接收和发送 reqComm.SetCommunicationType(0x01); // 应用报文 DiagSendRequest(reqComm); if (TestWaitForDiagResponse(reqComm, 500) 1) { if (diagGetLastResponseCode(reqComm) 0x68) { return 1; } } return 0; }例程控制的前置条件通常是启动某个例程。比如某些ECU要求先启动诊断准备例程才能执行后续的写操作。这个例程的标识符RoutineIdentifier在CDD里有定义CAPL里直接引用即可。这里要特别注意执行顺序。我总结的通用顺序是会话切换 → 安全访问 → 通信控制 → 例程控制 → 目标服务。但这个顺序不是绝对的有些ECU要求先做通信控制再切会话具体要看CDD里的状态机定义。最稳妥的办法是查ECU的诊断规范文档或者用CANoe的Trace窗口观察手工操作时的报文顺序照着复现。3.4 前置条件的状态缓存与失效机制前面提到用全局变量缓存会话状态其实安全等级、通信控制状态、例程状态都可以缓存。但缓存有一个核心问题什么时候失效。我的经验是以下几种情况必须让缓存失效会话超时用定时器监控超时后重置所有状态。收到ECU的会话切换通知有些ECU会主动发会话变更通知比如0x10服务的响应里带P2时间这时候要同步更新状态。测试用例主动重置有些测试用例需要从干净状态开始会主动调用ResetPreconditions()把所有状态清零。总线通信中断如果检测到总线错误或者ECU掉线所有缓存都要失效。实现上我会定义一个结构体来管理状态variables { struct PreconditionState { int session; int securityLevel; int commControl; int routineActive; }; struct PreconditionState gPrecond; msTimer tSessionKeepAlive; }然后在ResetPreconditions()里把所有字段清零在定时器回调里重置会话和安全等级。这样上层函数每次调用前置条件时先检查缓存缓存有效就直接返回无效就重新执行。实操心得状态缓存能大幅提升测试速度。我实测过一个400条用例的测试集加缓存之前跑完要25分钟加缓存之后降到12分钟左右因为大量用例共享同一套前置条件不需要重复执行。4. DIVA工程配置与CAPL脚本的联动实操4.1 DIVA工程里诊断描述文件的导入要点DIVA工程的核心是诊断描述文件。在CANoe里你可以通过Diagnostic/ISO TP配置或者DIVA的Import功能导入CDD/ODX文件。导入的时候有几个关键点第一确认诊断寻址方式。物理寻址和功能寻址的ID要配对功能寻址通常用于会话切换和安全访问的广播物理寻址用于具体服务的点对点通信。如果寻址配错了ECU根本不会响应。第二检查服务定义是否完整。有些CDD文件里只定义了部分服务或者子功能不全。导入之后要在DIVA的Diagnostic Description里逐个检查确认你要用的服务都在。特别是0x27安全访问要确认种子和密钥的子功能都定义了。第三安全算法DLL的配置。如果CDD里引用了DLL要确保DLL文件放在CANoe能搜索到的路径下通常是工程目录或者CANoe安装目录的DLL文件夹。DLL的位数要和CANoe一致32位CANoe只能用32位DLL这个坑我踩过配了半天发现是位数不匹配。4.2 CAPL节点在DIVA工程中的挂载方式CAPL脚本在DIVA工程里通常挂在一个独立的网络节点上。配置步骤是在CANoe的Simulation Setup里添加一个Network Node。给这个节点关联一个CAPL程序.can文件。在节点的CAPL程序里通过diagRequest对象引用DIVA里定义的诊断服务。这里有个细节CAPL节点和DIVA的关联是通过诊断描述文件的命名空间。比如你的CDD里ECU的名字叫ECU1那么CAPL里就要用diagRequest ECU1.服务名来引用。如果名字对不上编译会报错。另外CAPL节点的总线上下文要配对。如果诊断走CAN节点就要挂在CAN总线上如果走CAN FD或者DoIP配置方式不同。我一般会在节点的on start事件里做一些初始化比如注册诊断响应回调、启动状态监控定时器。4.3 测试用例与前置条件的集成方式在DIVA里测试用例可以配置成调用CAPL函数。具体做法是在DIVA的Test Case编辑器里添加一个CAPL Function Call类型的步骤选择你封装好的前置条件函数。但更灵活的方式是完全用CAPL写测试用例DIVA只负责诊断描述。这样测试逻辑、前置条件、结果判断都在CAPL里维护起来更集中。我的工程里就是这种模式DIVA导入CDDCAPL写所有测试逻辑通过CANoe的Test Module或者Test Node来组织和执行。如果要用CANoe的Test Module.vtest文件可以在Test Case里调用CAPL函数。Test Module的好处是有现成的测试报告生成机制每个用例的通过/失败状态会自动记录。配置的时候在Test Case的CAPL Test Case类型里选择对应的CAPL函数即可。4.4 一个完整的前置条件验证流程示例把前面的内容串起来一个完整的流程大概是这样testcase VerifyWriteDataService() { // 第一步确保扩展会话 if (EnsureExtendedSession() ! 1) { TestStepFail(前置条件失败无法进入扩展会话); return; } // 第二步确保安全等级1 if (EnsureSecurityLevel1() ! 1) { TestStepFail(前置条件失败安全访问未通过); return; } // 第三步执行目标服务0x2E写数据 diagRequest ECU1.WriteDataByIdentifier reqWrite; reqWrite.SetParameter(DataIdentifier, 0xF190); reqWrite.SetParameter(DataRecord, 0x01 0x02 0x03 0x04); DiagSendRequest(reqWrite); if (TestWaitForDiagResponse(reqWrite, 1000) 1) { if (diagGetLastResponseCode(reqWrite) 0x6E) { TestStepPass(写数据服务执行成功); } else { TestStepFail(写数据服务返回否定响应); } } else { TestStepFail(写数据服务响应超时); } }这个例子里前置条件的验证和目标服务的执行是分开的任何一步失败都会明确报告是哪一步出的问题。这样调试的时候很容易定位。注意TestStepFail和TestStepPass是CANoe Test Module里的函数如果你用的是纯CAPL节点而不是Test Module需要用write输出日志或者用testStepFail小写开头取决于CANoe版本。函数名大小写在CAPL里是敏感的写错了编译不过。5. 常见问题与排查技巧实录5.1 前置条件执行失败的典型原因在实际项目里前置条件失败的原因五花八门我整理了一个速查表现象可能原因排查方法会话切换无响应诊断寻址ID配错检查Trace窗口是否有请求发出对比手工操作的ID会话切换返回7F 10 12子功能不支持确认CDD里定义的子功能与ECU实际支持的是否一致安全访问返回7F 27 35密钥错误检查DLL算法是否匹配或手动计算密钥验证安全访问返回7F 27 36尝试次数超限等待锁定时间过后重试或重启ECU安全访问返回7F 27 37超时种子和密钥之间的间隔太长缩短发送间隔目标服务返回7F XX 33前置条件不满足确认会话、安全等级、通信控制是否都到位目标服务返回7F XX 7E当前会话不支持该子功能检查是否真的进入了扩展会话间歇性失败会话超时检查S3 timer设置确认状态缓存是否失效这个表里的每一行我都在项目里遇到过。最坑的是最后一行间歇性失败有时候跑10次成功8次失败2次。后来发现是会话超时导致的——测试用例执行时间超过了ECU的S3 timer会话自动回默认后续服务就失败了。解决办法是在测试用例执行过程中定期刷新会话比如每3秒发一次会话保持请求0x3E TesterPresent。5.2 安全访问的坑与解决方案安全访问是问题最集中的地方。除了上面表格里的还有几个细节种子长度不一致。有些ECU在不同安全等级下种子长度不同比如等级1是4字节等级2是8字节。CAPL里如果用固定长度的数组接收会出错。解决办法是用diagGetParameterSize动态获取长度或者直接用CDD里定义好的响应对象让CANoe自动处理。密钥计算的时间。如果密钥算法复杂DLL计算需要时间。如果种子收到后立刻发密钥可能DLL还没算完。我一般会在种子和密钥之间加一个10到50ms的延迟。CAPL里的延迟函数用testWaitForTime或者waitForTime具体用哪个取决于你的CANoe版本和测试环境。安全等级的回退。有些ECU在会话切换后会重置安全等级。比如你做了扩展会话安全等级1然后切到编程会话安全等级会回到0。这时候如果切回扩展会话需要重新做安全访问。所以状态缓存里会话和安全等级要关联管理会话一变安全等级缓存就要失效。5.3 CAPL脚本调试的实用技巧CAPL脚本不像普通编程语言那样有强大的调试器调试主要靠write输出和Trace窗口。我常用的几个技巧第一在每个关键步骤加日志。比如write(进入扩展会话结果%d, result)这样跑测试的时候能看到执行到哪一步了。第二用CANoe的Write窗口过滤。Write窗口支持按关键字过滤我给前置条件的日志都加一个前缀比如[PRECOND]这样过滤出来只看前置条件相关的日志。第三用Trace窗口看原始报文。CAPL的日志是应用层的Trace窗口是链路层的。有时候CAPL说发送成功了但Trace窗口里根本没有报文说明是诊断层配置问题。反过来Trace窗口有报文但CAPL收不到响应可能是响应过滤配置问题。第四分段测试。不要一上来就跑完整的测试用例先把前置条件函数单独拿出来测。比如写一个只调用EnsureExtendedSession()的测试用例确认会话切换没问题了再加安全访问一步步来。5.4 性能优化与批量测试的注意事项当测试用例数量上去之后性能就成了问题。几个优化点减少不必要的重复请求。这就是状态缓存的价值。如果10个连续用例都需要扩展会话安全等级1第一个用例执行完前置条件后后面9个直接复用缓存不需要重复发请求。合理设置超时时间。超时时间太长会拖慢测试太短会误判。我的经验值是会话切换500ms安全访问种子500ms、密钥500ms目标服务1000ms。如果ECU响应慢可以适当放宽但不要超过2000ms。批量测试的错误处理。批量跑的时候某个用例失败不应该影响后续用例。我的做法是在每个用例开头调用ResetPreconditions()确保从干净状态开始。同时前置条件失败时不要直接终止整个测试而是标记该用例失败继续跑下一个。测试报告的生成。如果用Test Module报告是自动生成的。如果用纯CAPL需要自己写报告生成逻辑比如把结果写到CSV文件里。我一般会在测试结束后用CAPL的文件操作函数把结果汇总输出。实操心得批量测试的时候建议在测试开始前先做一次冒烟测试就是跑一个最简单的用例确认ECU在线、诊断通信正常、前置条件能走通。冒烟测试过了再跑全量能避免大量用例因为同一个基础问题全部失败。6. 从单ECU到多ECU的前置条件扩展思路前面讲的都是单ECU的场景。实际项目里一个CANoe工程可能同时连接多个ECU每个ECU的前置条件可能不同。这时候前置条件的管理就要做扩展。我的做法是按ECU维度组织前置条件。每个ECU有一套独立的状态缓存和前置条件函数函数名加ECU前缀比如ECU1_EnsureExtendedSession()、ECU2_EnsureExtendedSession()。底层的基础函数可以共用但状态变量要分开。如果多个ECU之间有依赖关系比如ECU2的某个服务要求ECU1先进入特定状态那就需要在测试用例里显式处理这种依赖。CAPL里可以按顺序调用不同ECU的前置条件函数确保依赖关系满足。另外多ECU场景下诊断寻址的冲突要特别注意。如果两个ECU用相同的诊断ID报文会冲突。这种情况下要么用不同的物理寻址ID要么用功能寻址做广播具体取决于网络设计。还有一个扩展方向是前置条件的参数化。比如不同项目的ECU会话切换的子功能号可能不同安全等级可能不同。可以把这些参数提取到CAPL的变量或者CANoe的系统变量里通过配置文件或者环境变量来设置。这样同一套CAPL脚本可以适配不同的ECU不需要改代码。我在最近一个项目里就是这么做的把会话子功能、安全等级、超时时间都做成系统变量测试工程师在CANoe的Panel里可以修改这些值CAPL脚本读取系统变量来决定行为。这样非开发人员也能调整前置条件不用改脚本重新编译。最后分享一个小技巧前置条件的日志要足够详细但不要刷屏。我见过有同事在每个前置条件函数里写十几条日志结果跑批量测试的时候Write窗口几万行根本看不过来。我的做法是正常流程只写一条汇总日志比如[PRECOND] ECU1 扩展会话安全等级1 就绪只有失败的时候才输出详细的分步日志。这样既能快速定位问题又不会让日志爆炸。
返回列表