资讯详情

CANoe 中 CAPL 诊断测试实战:从协议基础到安全解锁

📅 2026/10/24 4:02:19 | 华诺云谱 👁 阅读
CANoe 中 CAPL 诊断测试实战:从协议基础到安全解锁
前言在汽车电子开发与测试中诊断功能是保障 ECU 正常工作、快速定位故障的关键环节。CANoe 作为 Vector 公司推出的总线开发与测试工具提供了强大的诊断仿真和测试能力而 CAPL 语言则让工程师能够灵活地编写自动化诊断测试脚本。本文围绕 CANoe 中的 CAPL 诊断测试展开系统介绍从诊断协议基础到安全解锁的完整知识体系涵盖诊断描述文件、传输层与诊断层参数、常用诊断函数、通用函数封装以及安全解锁等核心内容。一、诊断协议介绍ISO 14229-1 是一项国际标准用于规范汽车诊断通信也被称为 UDS。标准定义了诊断通信的数据通信方式命令结构数据格式和错误码也定义了诊断仪器和车辆之间的通信方式包括物理层和传输层协议ISO 14229-1 处于 OSI 模型中的顶层该标准适用于所有的网络协议CAN 总线的物理层和传输层的协议为 ISO 11898-2 和 ISO 15765-2LIN 总线的物理层和传输层的协议为 ISO 17987-4 和 ISO 17987-2以太网的物理层和传输层的协议为 ISO 13400-3 和 ISO 13400-21. 诊断描述文件CANoe 的诊断功能非常强大常见的诊断描述文件的格式有 CDDODXPDXCDDCANdela Diagnostic Description是 Vector 公司开发的一种诊断描述文件可通过 CANdelaStudio 软件创建和编辑ODXOpen Diagnostic Data Exchange是全球通用的诊断描述格式可通过 ODXStudio 软件创建和编辑PDXOpen Diagnostic Data Exchange/Packed ODX可以将多个 ODX 文件打包成一个 PDX 文件2. 诊断描述信息ECU qualifier诊断目标 ECU 的名称该参数在 CANoe 环境中必须是唯一的。在 CAPL 代码中使用 diagSetTarget(char ecuName[]) 函数设置诊断对象时ecuName 参数传递的值就是 ECU qualifier 参数的值Diagnostic tester在 CANoe 中使用诊断控制台发送诊断请求时用户可以选择使用物理地址发送诊断或者使用功能地址发送诊断Simulation by如果对仿真的 ECU 发送诊断这里需要选择 ECU如果是真实在线的 ECU无需选择3. 传输层参数Override manually如果诊断描述文件的信息和实际总线信息不匹配可以勾选该选项手动更改参数Addressing定义了诊断请求的物理寻址功能寻址以及诊断响应的报文 IDSTmin多包之间最小的响应间隔时间Block size流控帧中接收方最多一次性接收的包数FC delay流控帧的延迟时间MaxLength诊断能支持的最大的包长4. 诊断层参数Override manually如果加载的诊断文件信息和实际总线信息不匹配可以勾选该选项手动更改参数Tester Presents request如果勾选这个选项在诊断仪和 ECU 建立了诊断会话之后CANoe 将周期行的发送 0x3E 服务来维持诊断会话在线。在 CAPL 中可以使用 diagStopTesterPresent() 和 diagStartTesterPresent() 禁用和启用该功能S3 client timeCANoe 自动发送会话保持诊断请求的周期S3 server time服务器离开非默认会话的超时时间这个时间必须要大于 S3 client timeP2 client客户端成功发送请求消息到接收到服务端的应答消息的超时时间P2 extended client客户端成功发送请求消息在接收到服务端响应的 NRC 0x78 之后客户端使用此时间再次等待 ECU 响应P2 Server服务端在接收到客户端的请求消息后开始答复消息的运行时间典型值为 50msP2 extended server如果服务端在 P2 Server 时间内无法给出答复则可以响应 NRC 0x78之后服务器必须在 P2 Server Extended 时间内给出响应也可以继续响应 NRC 0x78再重置该定时器Seed Key DLL如果客户端请求的服务器需要使用 0x27 服务安全解锁之后才能请求则需要在此加载 DLL 文件二、传输层协议一帧 CAN/LIN 报文最多可以携带 8 个字节的有效数据一帧 CANFD 报文最多可以携带 64 字节的有效数据一帧 Flexray 报文最多可以携带 254 字节的有效数据一帧标准以太网报文最多可以携带 1500 字节有效数据。在应用层ISO 14229-1 协议规定诊断数据的长度是可以大于单帧报文的负载长度的。传输层负责的内容就是将数据进行有效得分组传输。不同的总线类型对应得传输协议标准不同CAN 总线是基于 ISO 15765-2 进行传输的。协议控制信息N_PCI将 CAN 总线的诊断数据分为单帧Single FrameSF首帧First FrameFF连续帧Consecutive FrameCF和流控帧Flow Control FrameFC。N_PDU nameN_PCI BytesByte1 Bit7-4Byte1 Bit7-4Byte2Byte3Byte4Byte5Byte6SingleFrame(SF)(CAN_DL 8)0000SF_DL-----SingleFrame(SF)(CAN_DL 8)00000000SF_DL----FirstFrame(FF)(FF_DL 4095)0001FF_DL----FirstFrame(FF)(FF_DL 4095)000100000000 0000FF_DLConsecutiveFrame(CF)0010SN-----FlowControl(FC)0011FSBSSTmin---三、CAPL 诊断函数1. 发送诊断请求下列函数可以以诊断物理寻址或功能寻址的方式发送诊断请求long diagSendRequest(diagRequest request)以物理寻址方式发送诊断请求long diagSendFunctional(diagRequest request)以功能寻址方式发送诊断请求2. 诊断目标设置如果 CANoe 工程中加载了多个 ECU 的诊断描述文件则不建议直接发送诊断请求而是要先设置诊断目标 ECU指定了诊断的 ECU代码就只能用于测试当前 ECUlong diagSetTarget(char ecuName[])设置诊断目标3. 等待诊断请求发送long testWaitForDiagRequestSent(diagRequest request, dword timeout)返回地响应值为 1表示诊断请求发送到总线上在工程应用中需要再次使用这个函数确认4. 等待接收诊断响应long testWaitForDiagResponse(diagRequest request, dword timeout)等待接收诊断响应long testWaitForDiagResponse(dword timeout)不指定诊断请求等待接收诊断响应返回值为 1 表示接收到诊断响应返回值为 0 表示超时指定时间内没接收到诊断响应返回值为 0 表示内部错误函数收到肯定响应或者否定响应都会立即退出如果收到 NRC 0x78该函数会使用 P2*Server 时间继续等待知道 timeout 参数超时5. 判断响应类型testWaitForDiagResponse() 函数只能判断 ECU 是否返回响应但无法判断是肯定响应还是否定响应long diagGetLastResponseCode(diagRequest request)获取最后一次响应结果long diagGetLastResponseCode()获取最后一次响应结果返回值 -1肯定响应返回值 0超时无响应返回值 0否定响应返回 NRC 响应码6. 打印诊断数据到测试报告下列函数可以把诊断请求数据和诊断响应数据输出到测试报告中long testReportWriteDiagObject(diagRequest request)打印诊断请求数据long testReportWriteDiagResponse(diagRequest request)打印诊断响应数据7. 代码示例variables { diagRequest * request; char ecuName[20] Door; } testcase TC_001_diagFunction_Test() { byte data[2] {0x10, 0x01}; long ret; // 1. 指定诊断节点 diagSetTarget(ecuName); // 2. 设置诊断请求 diagResize(request, 2); diagSetPrimitiveData(request, data, elCount(data)); // 3. 发送诊断请求 ret diagSendRequest(request); testStep(, pysical address, ret %d, ret); if(ret ! 0) return; // 4. 确认诊断请求发送到总线上 ret testWaitForDiagRequestSent(request, 1000); if(ret 1) { testStepPass(, Request was sent successfully); testReportWriteDiagObject(request); // 打印诊断请求 } else { testStepFail(, Request could not be sent!); return; } // 5. 确认接收到诊断响应 ret testWaitForDiagResponse(request, 5000); if(ret ! 1) { testStepFail(No response received or internal error.); return; } testStepPass(Received ECU response successfully.); testReportWriteDiagResponse(request); // 打印诊断响应 // 6. 判断响应类型 ret diagGetLastResponseCode(request); if(ret 0) { testStepFail(, Negative response, NRC code is 0x%X, ret); return; } testStepPass(Received ECU positive response successfully); }测试结果四、诊断通用函数一基于上述诊断函数可以将上述代码封装成通用的函数提高代码的复用度void diag_sendRequest(diagRequest request)发送诊断请求并期望 ECU 肯定应答void diag_SendRequest(diagRequest* request) { long ret; // 1. 发送诊断请求 ret diagSendRequest(request); testStep(, pysical address, ret %d, ret); if(ret ! 0) { testStepFail(Request was sent failed); return; } // 2. 确认诊断请求发送到总线上 ret testWaitForDiagRequestSent(request, 1000); if(ret ! 1) { testStepFail(, Request could not be sent!); return; } testStepPass(, Request was sent successfully); testReportWriteDiagObject(request); // 打印诊断请求 // 5. 确认接收到诊断响应 ret testWaitForDiagResponse(request, 5000); if(ret ! 1) { testStepFail(No response received or internal error.); return; } testStepPass(Received ECU response successfully.); testReportWriteDiagResponse(request); // 打印诊断响应 // 6. 判断响应类型 ret diagGetLastResponseCode(request); if(ret 0) { testStepFail(, Negative response, NRC code is 0x%X, ret); return; } testStepPass(Received ECU positive response successfully); }测试代码variables { diagRequest * request; char ecuName[20] Door; } testcase TC_002_sendRequestFunction_Test() { byte data[2] {0x10, 0x01}; diagSetTarget(ecuName); diagResize(request, 2); diagSetPrimitiveData(request, data, elCount(data)); diag_SendRequest(request); }测试结果五、诊断服务数据设置1. 诊断参数设置long diagSetParameter(diagRequest request, char parameterName[], double newValue)更改请求的参数值发送不同的诊断数据long diagSetParameter(diagRequest request, char parameterName[], char newValue[])更改请求的参数值发送不同的诊断书数据这个函数能够修改诊断参数需要提前知道诊断参数的名称实际测试中并不好用如果使用 testWaitForDiagResponse() 接收诊断响应容易遇到接收不到诊断响应的问题testcase TC_003_setRequestParameter_Test() { diagRequest Door.DefaultSession_Start request; long ret; diagSetTarget(ecuName); diagSetParameter(request, DiagSessionType, 0x03); diag_SendRequest(request); }实际上查看 log 确实可以接收到响应但是使用 testWaitForDiagResponse() 接收不到2. 单字节设置诊断服务long diagSetPrimitiveByte(diagRequest request, dword bytePos, dword newValue)设置诊断服务指定位置的字节进行重新赋值testcase TC_004_setPrimitiveByte_Test() { diagRequest Door.DefaultSession_Start request; long ret; diagSetTarget(ecuName); DiagSetPrimitiveByte(request, 1, 0x03); diag_SendRequest(request); }3. 设置全部的诊断服务long diagSetPrimitiveData(diagRequest request, byte* buffer, dword buffersize)重置诊断服务的全部数据variables { diagRequest * request; char ecuName[20] Door; } testcase TC_005_setPrimitiveData_Test() { long ret; byte data[4095]; diagSetTarget(ecuName); diagResize(request, 2); data[0] 0x10; data[1] 0x02; diagSetPrimitiveData(request, data, 2); diag_SendRequest(request); }通常来说设置全部的诊断服务更加常用它可以搭配通用的诊断请求使用通配符定义的诊断请求在测试中更加灵活搭配通用的诊断请求需要注意一定要设置诊断请求的字节数diagResize(diagRequest request, dword buteNum)否则无法确定诊断请求有多少个字节。六、诊断通用函数二1. 通用诊断函数一我们可以基于上面的诊断服务数据设置的方法封装一个通用性更强的函数这个函数不必依赖于 CDD 文件。void diag_SendRequest_PrimitiveData(byte primitiveData[], long dataSize) { diagRequest * request; long ret; // 1. 设置诊断请求的字节数 diagResize(request, dataSize); // 2. 设置诊断请求的全部字节 diagSetPrimitiveData(request, primitiveData, dataSize); // 3. 发送诊断请求 ret diagSendRequest(request); testStep(, pysical address, ret %d, ret); if(ret ! 0) { testStepFail(Request was sent failed); return; } // 4. 确认诊断请求发送到总线上 ret testWaitForDiagRequestSent(request, 1000); if(ret ! 1) { testStepFail(, Request could not be sent!); return; } testStepPass(, Request was sent successfully); testReportWriteDiagObject(request); // 打印诊断请求 // 5. 确认接收到诊断响应 ret testWaitForDiagResponse(request, 5000); if(ret ! 1) { testStepFail(No response received or internal error.); return; } testStepPass(Received ECU response successfully.); testReportWriteDiagResponse(request); // 打印诊断响应 // 6. 判断响应类型 ret diagGetLastResponseCode(request); if(ret 0) { testStepFail(, Negative response, NRC code is 0x%X, ret); return; } testStepPass(Received ECU positive response successfully); }测试代码testcase TC_006_sendRequestFunction_Test() { byte primitiveData[4095]; primitiveData[0] 0x10; primitiveData[1] 0x03; diagSetTarget(ecuName); diag_SendRequest_PrimitiveData(primitiveData, 2); }2. 通用诊断函数二上述的函数都期望诊断响应为正响应并且都是以物理寻址的方式发送诊断请求。在诊断测试中往往也需要注入错误的诊断数据期望 ECU 不响应或者否定响应并且还可以选择物理寻址或者功能寻址基于这个需求封装通用的诊断函数long diag_SendRequest_Generic(byte primitiveData[], long dataSize, long expResCode, byte sendType) { diagRequest * request; long ret; // 1. 设置诊断请求字节数 diagResize(request, dataSize); // 2. 设置诊断请求数据 diagSetPrimitiveData(request, primitiveData, dataSize); // 3. 发送诊断请求 if(sendType 0) // 物理寻址 { ret diagSendRequest(request); testStep(, Physical Address, ret %d, ret); } else // 功能寻址 { ret diagSendFunctional(request); testStep(, Functional Address, ret %d, ret); } if(ret ! 0) { testStepFail(Request could not be sent!); return 0; } // 4. 确认诊断请求是否发送成功 ret testWaitForDiagRequestSent(request, 1000); if(ret ! 1) { testStepFail(Request could not be sent!); return 0; } testStepPass(Request was sent successfully!); testReportWriteDiagObject(request); // 5. 确认诊断响应 ret testWaitForDiagResponse(request, 5000); switch(ret) { case 0: // 超时 if(expResCode 0) { testStepPass(No response was received, as expected.); } else { testStepFail(No response was received, but expected receive response.); } break; case 1: // 收到响应 testReportWriteDiagResponse(request); ret diagGetLastResponseCode(request); if(ret -1) // 肯定响应 { if(expResCode -1) { testStepPass(Positive response was received, as expected.); } else { testStepFail(, Positive response was received, but NRC 0x%X was expected, expResCode); } return 1; } else { if(ret expResCode) { testStepPass(, NRC 0x%X was received, as expected., ret); } else { testStepFail(, NRC 0x%X was received when 0x%X was expected., ret, expResCode); } } break; default: // 内部错误 testStepFail(, There a Error Code received %d, ret); } return 0; }测试代码variables { char ecuName[20] Door; const long No_Response 0; const long Positive_Response -1; const long NRC_Response_12 0x12; const long NRC_Response_13 0x13; const byte gPhys 0; const byte gFunc 1; } testcase TC_007_sendRequestFunction_Test() { byte data[4095]; diagSetTarget(ecuName); data[0] 0x10; data[1] 0x04; diag_SendRequest_Generic(data, 2, NRC_Response_12, gPhys); }七、获取诊断响应数据1. 获取诊断响应参数值double diagGetRespParameter(diagRequest request, char parameterName[])返回诊断响应的参数值long diagGetRespParameter(diagRequest request, char parameterName[], char buffer[], dword bufferLen)以字符串的方式返回诊断响应的参数值long diagGetRespParameterRaw(diagRequest request, char parameterName[], byte buffer[], dword bufferLen)以 byte 数组的方式返回诊断响应的参数值上述函数都需要知道参数名才能获取对应的参数值实际应用中用的不多2. 获取诊响应的所有数据long diagGetRespPrimitiveSize(diagRequest request)返回诊断响应的数据大小long diagGetRespPrimitiveByte(diagRequest request, dword bytePos)根据字节索引读取诊断数据每次获取一个字节通常放在循环里面使用上述函数都不需要知道参数名就可以直接获取所有的响应数据通常搭配循环使用variables { diagRequest * request; char ecuName[20] Door; } testcase TC_008_diagResponseData_Test() { long ret; byte primitiveData[4095] {0x10, 0x01}; long size; long i; byte resp[4095]; diagSetTarget(ecuName); diagResize(request, 2); diagSetPrimitiveData(request, primitiveData, 2); diag_SendRequest(request); size DiagGetRespPrimitiveSize(request); for(i 0; i size; i) { resp[i] DiagGetRespPrimitiveByte(request, i); testStep(, resp[%d] 0x%X, i, resp[i]); } }八、通用诊断函数三前面的通用函数实现了诊断请求的发送和诊断响应结果的判断下面实现一个包含诊断请求发送和获取诊断响应数据的函数long diag_SendRequestAndGetResponse(byte requestData[], long requestSize, byte sendType, byte responseData[]) { diagRequest * request; long ret; long size; long i; // 1. 调整诊断请求的大小和数据 diagResize(request, requestSize); diagSetPrimitiveData(request, requestData, requestSize); // 2. 发送诊断请求 if(sendType 0) { ret diagSendRequest(request); testStep(, Physical Address, ret %d, ret); } else { ret diagSendFunctional(request); testStep(, Functional Address, ret %d, ret); } if(ret ! 0) { testStepFail(Request could not be sent!); return 0; } // 3. 确认诊断请求发送成功 ret testWaitForDiagRequestSent(request, 2000); if(ret ! 1) { testStepFail(Request could not be sent!); return 0; } testStepPass(Request was sent successfully!); testReportWriteDiagObject(request); // 4. 确认接收到诊断响应 ret testWaitForDiagResponse(request, 5000); if(ret ! 1) { testStepFail(Not Received ECU Response!); return 0; } testStepPass(Receive ECU Response successfully!); // 5. 解析诊断响应 size DiagGetRespPrimitiveSize(request); testStep(, Response size %d, size); for(i 0; i size; i) { responseData[i] DiagGetRespPrimitiveByte(request, i); } return size; }测试代码testcase TC_009_diagRequestAndResponseFunction_Test() { byte requestData[4095] {0x10, 0x03}; byte responseData[4095]; long size; long i; diagSetTarget(ecuName); size diag_SendRequestAndGetResponse(requestData, 2, 0, responseData); for(i 0; i size; i) { testStep(, responseData[%d] 0x%X, i, responseData[i]); } }九、安全解锁有些诊断服务需要安全解锁后才能获取响应的读写权限CAPL 也内置了一些相关的函数来实现解锁功能。1. testWaitForUnlockEcu 函数long testWaitForUnlockEcu(long securityLevel)long testWaitForUnlockEcu(char ecuQualifier, dword securityLevel)上述函数传入安全等级进行解锁返回值为 0 表示安全解锁成功使用上述函数需要在诊断配置窗口加载 Seed Key DLL.dll 文件。上述函数的优点是能够直接按照 DLL 的算法解锁优点是快但无法观察到解锁的细节testcase TC_010_unlockEcu_Test() { byte reqData[4095] {0x10, 0x03}; long ret; diagSetTarget(ecuName); diagResize(request, 2); diagSetPrimitiveData(request, reqData, 2); diag_SendRequest(request); ret testWaitForUnlockEcu(ecuName, 0x01); testStep(, ret %d(0 表示成功, 否则失败), ret); }2. diagGenerateKeyFromSeed 函数在某些场景下需要注入错误的密钥验证 ECU 的防攻击性和安全性是否满足规范要求。此时 testWaitForUnlockEcu 函数就不适用了。long diagFenerateKeyFromSeed(byte seedArray[],dword seedArraySize,dword securityLevel,char variant[],char ipOption[],byte keyArray[],dword maxKeyArraySize,dword keyActualSizeOut)seedArray输入的 seed 数组seedArraySize输入的 seed 数组的长度securityLevel安全解锁等级variant诊断描述文件中的 variant 参数值一般传入空字符串ipOption可选参数一般传入空字符串keyArray该函数计算出来的 key 数组maxKeyArraySize输入的 keyArray 数组允许的最大字节数keyActualSizeOut输出的 keyArray 数组的实际字节数封装通用的安全解锁函数void securityAccess(long securityLevel) { byte reqData[4095]; diagRequest * request; long ret; long size; byte seedArray[255]; byte keyArray[255]; long i; dword actualKeySize; dword seedSize; reqData[0] 0x27; reqData[1] securityLevel; // 1. 设置诊断请求 diagResize(request, 2); diagSetPrimitiveData(request, reqData, 2); // 2. 发送诊断请求并判断肯定响应 ret diag_SendRequest(request); if(ret ! 1) { testStepFail(can not generate key.); return; } // 3. 接收诊断响应, 获取 seedArray size DiagGetRespPrimitiveSize(request); seedSize size - 2; for(i 2; i size; i) { seedArray[i - 2] DiagGetRespPrimitiveByte(request, i); } // 5. 计算密钥 ret diagGenerateKeyFromSeed(seedArray, seedSize, securityLevel, , , keyArray, elCount(keyArray), actualKeySize); if(ret ! 0) { testStepFail(cant generate key!); return; } // 6. 发送密钥 reqData[0] 0x27; reqData[1] securityLevel 1; for(i 0; i actualKeySize; i) { reqData[i 2] keyArray[i]; } diagResize(request, actualKeySize 2); diagSetPrimitiveData(request, reqData, actualKeySize 2); diag_SendRequest(request); }测试代码testcase TC_011_securityAccess_Test() { byte reqData[4095] {0x10, 0x03}; diagSetTarget(ecuName); diagResize(request, 2); diagSetPrimitiveData(request, reqData, 2); diag_SendRequest(request); securityAccess(0x01); }通过上面的方式可以将诊断过程中的请求数据和响应数据都打印到测试报告中。在故障注入的测试中能够将故障注入可视化看到种子和密钥的信息实现了整个解锁过程总结本文系统介绍了 CANoe 中 CAPL 诊断测试的完整流程从 UDS 诊断协议、诊断描述文件到传输层与诊断层参数再到诊断请求的发送、响应判断和通用函数封装最后讲解了安全解锁的两种实现方式。通过封装通用诊断函数可以显著提高测试代码的复用度和开发效率。读者掌握这些方法后即可根据实际测试需求灵活编写诊断测试脚本提升诊断测试的效率和可维护性。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑