前言
在汽车电子控制单元(ECU)的诊断测试中,CAPL(CAN Access Programming Language)作为 Vector 工具链的核心脚本语言,提供了丰富的内置诊断函数,能够帮助测试工程师快速完成常规的诊断测试任务。然而,在实际项目中,我们常常会遇到一些内置函数无法覆盖的场景,尤其是针对传输层(TP 层)的专项测试。
TP 层位于 CAN 网络的应用层与数据链路层之间,负责诊断数据的拆分与重组。当诊断数据长度超过单帧 CAN 报文的最大载荷时,就需要将数据拆分为多个帧进行传输,这一过程涉及 ISO 15765-2 协议中的单帧(SF)、首帧(FF)、连续帧(CF)和流控帧(FC)等概念。CAPL 内置的诊断函数通常封装了完整的协议栈处理逻辑,但在某些测试场景下,我们需要绕过这些封装,手动控制每一帧的发送与接收,以验证 TP 层协议实现的正确性和健壮性。
本文不加载 CDD(CAN Diagnostic Description)文件,基于诊断 ID 手动实现 ISO 15765-2 的部分功能,封装 4 个通用的诊断函数:发送单帧诊断请求、接收单帧诊断响应、发送多帧诊断请求、接收多帧诊断响应。通过这些函数,测试工程师可以灵活地控制诊断报文的发送与接收过程,深入验证 TP 层的分包与重组逻辑,从而弥补内置函数在 TP 层测试方面的不足。
本文将从测试步骤和日志采集、测试用例编写、CAN 传输层功能手动实现三个方面展开,结合实际代码示例,帮助读者掌握基于 CAPL 手动实现诊断 TP 层测试的方法。
一、测试步骤和日志采集
1. 日志封装
在实际测试中,为了提高代码的复用性,需要封装采集日志的函数,将采集的日志保存到工程目录的日志目录下:
void startLoggingTrace(char testCaseTitleContent[]) { char loggingFile[200]; snprintf(loggingFile, elCount(loggingFile), "%s\\%s_{UserName}_{LocalTime}.blf", projectLogPath, testCaseTitleContent ); setLogFileName("Logging", loggingFile); startLogging("Logging", 100); }2. 测试步骤打印封装
为了让测试报告的可读性更强,可以封装一个打印测试步骤的函数:
void testStepDescriable(long& mainStep, long& minorStep, char descriable[]) { char testStepNum[10]; snprintf(testStepNum, elCount(testStepNum), "Step:%d.%d", mainStep, minorStep); testStep(testStepNum, descriable); if(minorStep == 0) { mainStep++; } }二、测试用例编写
1. NRC 响应码测试
在诊断测试中,除了测试诊断服务的正响应外,还需要测试诊断服务的 NRC 功能;
以 0x10 服务为例,测试 0x10 服务的 NRC 响应码,是否复合规范。
测试 0x10 服务的 NRC 0x7E, 0x12, 0x13 功能;
diag_SendRequest_Generic() 函数封装了发送诊断请求,接收并解析诊断响应,以及判断响应码是否复合预期的功能,具体代码实现可以参考上一篇文章。
testcase TC_UDS_S10_NRC_Test() { char testCaseTitleContent[100]; char loggingFile[200]; long mainStep; long minorStep; byte reqData[4095]; testGetCurrentTestCaseTitle(testCaseTitleContent, elCount(testCaseTitleContent)); testCaseTitle(testCaseTitleContent, "test 0x10 service nrc code"); startLoggingTrace(testCaseTitleContent); diagSetTarget(ecuName); mainStep = 0; minorStep = 0; // 1. 进入默认会话 testStepDescriable(mainStep, minorStep, "Physical Address: send 10 01 to enter default session"); reqData[0] = 0x10; reqData[1] = 0x01; diag_SendRequest_Generic(reqData, 2, Positive_Response, gPhys); // 2. 发送 10 02, 期望 ECU 响应 NRC 0x7e testStepDescriable(mainStep, minorStep, "Physical Address: send 10 02 and expect NRC is 0x7E"); reqData[0] = 0x10; reqData[1] = 0x02; diag_SendRequest_Generic(reqData, 2, 0x7E, gPhys); // 3. 发送 10 04, 期望 ECU 响应 NRC 0x12 testStepDescriable(mainStep, minorStep, "Physical Address: send 10 04 and expect NRC is 0x12"); reqData[0] = 0x10; reqData[1] = 0x04; diag_SendRequest_Generic(reqData, 2, NRC_Response_12, gPhys); // 4. 发送 10 03 00, 期望 ECU 响应 NRC 0x13 testStepDescriable(mainStep, minorStep, "Physical Address: send 10 03 00 and expecte NRC is 0x13"); reqData[0] = 0x10; reqData[1] = 0x03; reqData[2] = 0x00; diag_SendRequest_Generic(reqData, 3, NRC_Response_13, gPhys); stopLogging("Logging", 100); }2. 读DID测试
读取并验证 ECU 的DID是否正确。
测试步骤:
进入扩展会话,安全解锁,读取DID,同期望值进行比较。
securityAccess() 函数封装了安全解锁的步骤,具体代码实现可以参考上一篇文章;
diag_SendRequestAndGetResponse() 函数封装了发送诊断请求和获取诊断响应数据的功能,具体代码实现可以参考上一篇文章;
testcase TC_UDS_S22_0107_Test() { byte reqData[4095]; byte respData[4095]; byte swVersion[5] = {0x62, 0x01, 0x07, 0x00, 0x00}; char testCaseTitleContent[200]; long mainStep, minorStep; long respSize; long ret; testGetCurrentTestCaseTitle(testCaseTitleContent, elCount(testCaseTitleContent)); testCaseTitle(testCaseTitleContent, "test 0x22 service, read DID content"); diagSetTarget(ecuName); startLoggingTrace(testCaseTitleContent); mainStep = 1; minorStep = 0; // 1. 进入扩展会话 testStepDescriable(mainStep, minorStep, "send 10 03, enter extend session"); reqData[0] = 0x10; reqData[1] = 0x03; diag_SendRequest_Generic(reqData, 2, Positive_Response, gPhys); // 2. 安全解锁 testStepDescriable(mainStep, minorStep, "security access"); securityAccess(0x01); // 3. 读取软件版本号 testStepDescriable(mainStep, minorStep, "send 22 0107, read DID"); reqData[0] = 0x22; reqData[1] = 0x01; reqData[2] = 0x07; respSize = diag_SendRequestAndGetResponse(reqData, 3, gPhys, respData); // 4. 获取软件版本号 testStepDescriable(mainStep, minorStep, "check DID content"); ret = memcmp(respData, swVersion, elCount(swVersion)); if(ret == 0) { testStepPass("DID content is as expected."); } else { testStepFail("DID content is wrong."); } stopLogging("Logging", 100); }三、CAN 传输层功能手动实现
1. 单帧诊断请求
void sendSingleFrame(dword reqId, byte reqData[], long reqDataSize, long channel):发送单帧报文,需要自行构建数据,考虑 CAN 的传输层格式,函数只负责发送报文;
void sendSingleFrame(dword reqId, byte reqData[], long reqDataSize, long channel) { long i; message * msg; CANSettings defaultArbSettings; CANSettings defaultDbrSettings; char comment[200]; // 1. 获取总线配置 canFdGetConfiguration(channel, defaultArbSettings, defaultArbSettings); // 2. 配置并发送请求报文 msg.id = reqId; msg.can = channel; msg.DataLength = reqDataSize; msg.FDF = (defaultArbSettings.flags & 0x100) > 0 ? 1 : 0; for(i = 0; i < reqDataSize; i++) { msg.byte(i) = reqData[i]; } output(msg); // 3. 将报文数据转换为字符串, 进行输出 convertByteArrayToStr(reqData, reqDataSize, comment, ""); #if TEST_NODE testStep("CAN TX:", comment); #endif } void convertByteArrayToStr(byte hexRawData[], long length, char outStr[], char option[]) { long i; strncpy(outStr, "", elCount(outStr)); for(i = 0; i < length; i++) { if(hexRawData[i] < 0x10) { snprintf(outStr, elCount(outStr), "%s0%X ", outStr, hexRawData[i]); } else { snprintf(outStr, elCount(outStr), "%s%X ", outStr, hexRawData[i]); } } }测试用例:
testcase UDS_SendSingleFrame_Test() { byte sendData[8] = {0x02, 0x10, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00}; sendSingleFrame(0x601, sendData, 8, 2); }2. 单帧诊断响应
long getSingleFrame(dword recId, byte recData[], long& recDLC, char busName[], long waitMaxTime):获取单帧响应报文,直接把单帧响应报文的数据复制到 recData 中,并获取响应报文的数据长度;
实现思路:
- 先在超时时间 P2_SERVER 内,获取到控制器的响应,再判断 NRC 是否为 0x78;
- 如果不是 0x78,使用 testGetWaitEventMsgData() 获取响应数据;
- 如果是 0x78,更新超时时间为 P2_EXTENDED_SERVER,继续等待控制器的响应;
long getSingleFrame(dword recId, byte recData[], long& recDLC, char busName[], long waitMaxTime) { const long P2_SERVER = 50; const long P2_EXTENDED_SERVER = 5000; long context; long ret; long waitTime; long i; dword startTime; message * recMsg; char comment[200]; // 1. 多总线测试, 要先获取总线上下文 if(strncmp(busName, "", elCount(busName)) != 0) { context = getBusNameContext(busName); if(context != 0) { setBusContext(context); } } // 2. 接收响应报文 waitTime = P2_SERVER; startTime = timeNow(); while((timeNow() - startTime) / 100 < waitMaxTime) // 换算成 ms { ret = testWaitForMessage(recId, waitTime); if(ret == 1) { // 3. 提取响应报文的数据 ret = testGetWaitEventMsgData(recMsg); if(ret == 0) { recDLC = recMsg.DataLength; for(i = 0; i < recDLC; i++) { recData[i] = recMsg.byte(i); // 提取报文数据 } convertByteArrayToStr(recData, recDLC, comment, ""); testStepPass("Rx:", comment); if(recData[1] == 0x7F && recData[3] == 0x78) // 考虑 ECU 处理复杂任务 { waitTime = P2_EXTENDED_SERVER; continue; } else // 提取到数据 { return 1; } } else { testStepFail("", "received CAN message 0x%x, but get message data failed.", recId); return 0; } } else { testStepFail("", "not received CAN message 0x%x", recId); return 0; } } }测试用例:
testcase UDS_GetSingleFrame_Test() { byte sendData[8] = {0x02, 0x10, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00}; byte getData[64]; long dataLength; sendSingleFrame(0x601, sendData, 8, 2); getSingleFrame(0x605, getData, dataLength, "CAN2", 100); }3. 多帧诊断请求
long sendMultiFrames(dword reqId, byte reqData[], dword recId, char busName[], byte channel):发送多帧报文;如果数据只够一个单帧,则发送单帧,如果够多帧,就按多帧的格式发送,先发送首帧,再接收流控帧,接收到流控帧之后再发送连续帧。
实现思路:
- 先判断总线类型,是 CAN 还是 CANFD;
- 再根据要发送的多包数据的内容,计算发送的数据的长度,以及要拆分的包数;
- 解析帧类型,判断是否是多帧数据,还是单帧数据;
- 如果是单帧数据,直接发送单帧报文即可;
- 如果是多帧报文,先发送首帧报文,再接收流控帧;
- 解析流控帧,获取 FS,BS,STmin 等参数;
- 循环发送多包数据。
long sendMultiFrames(dword reqId, byte reqData[], dword recId, char busName[], byte channel) { byte N_PCItype; byte TX_SN, FS, BS, STmin; byte payload[64]; long sendDLC, recDLC; long dataLength; long frameNum; long byteCounter; long ret; long i, j; // 1. 判断总线类型 sendDLC = CANFD_Flag(channel) == 1 ? 64 : 8; // 判断总线类型(CAN, CANFD) // 2. 计算数据长度和总包数 CalculateFramesSequence(reqData, sendDLC, dataLength, frameNum); // 计算数据长度和总包数 // 3. 解析帧类型 N_PCItype = (reqData[0] >> 4) & 0x0F; TX_SN = 1; byteCounter = sendDLC; if(N_PCItype == 0x01) // 发送多帧 { sendSingleFrame(reqId, reqData, sendDLC, channel); // 发送首帧 ret = getSingleFrame(recId, payload, recDLC, busName, 2000); // 接收流控帧 if(ret == 1) { // 解析流控帧 FS = payload[0] & 0x0F; BS = payload[1]; STmin = payload[2]; for(i = 0; i < frameNum - 1; i++) // 发送连续帧 { payload[0] = (TX_SN++) % 16 | 0x20; for(j = 1; j < sendDLC; j++) { payload[j] = reqData[byteCounter++]; } testWaitForTimeout(STmin * 1.1); sendSingleFrame(reqId, payload, sendDLC, channel); } } else { testStepFail("", "not received flow control frame."); return -1; } return byteCounter - 2; } else { sendSingleFrame(reqId, payload, sendDLC, channel); // 发送单帧 } return 1; }long CalculateFramesSequence(byte payload[], long frameDLC, long& dataLength, long& frameNum):计算数据长度和多帧的包数,含首帧;
long CalculateFramesSequence(byte payload[], long frameDLC, long& dataLength, long& frameNum) { long divisor; long remainder; long canDLCType; canDLCType = 0; // 计算要发送数据的长度 dataLength = ((payload[0] << 8) | payload[1]) & 0x0FFF; if(dataLength != 0) // <= 4095 bytes { // 计算包数 remainder = (dataLength + 1) % (frameDLC - 1); divisor = (dataLength + 1) / (frameDLC - 1); if(remainder != 0) { divisor++; } frameNum = divisor; canDLCType = 2; } else // > 4095 bytes { dataLength = (payload[2] << 24) | (payload[3] << 16) | (payload[4] << 8) | payload[5]; if(dataLength != 0) { remainder = (dataLength + 5) / (frameDLC - 1); divisor = (dataLength + 5) % (frameDLC - 1); if(remainder != 0) { divisor++; } frameNum = divisor; canDLCType = 6; } else { testStepFail("multiframes frist frame data length is 0."); } } return canDLCType; }long CANFD_Flag(long channel):判断总线是 CAN 总线,还是 CANFD 总线;
long CANFD_Flag(long channel) { CANSettings canArbSettings; CANSettings canDrbSettings; canFdGetConfiguration(channel, canArbSettings, canDrbSettings); return (canArbSettings.flags & 0x100) == 1 ? 1 : 0; }测试代码:
testcase UDS_SendMultiFrames() { byte sendData[500] = {0x11, 0x2C, 0x2E, 0xF1, 0x89}; byte getData[64]; long dataLength; sendMultiFrames(0x601, sendData, 0x605, "CAN2", 2); getSingleFrame(0x605, getData, dataLength, "CAN2", 2000); }4. 多帧诊断响应
long getMultiFramesData(dword recId, byte recData[], char busName[], byte channel, dword reqId, byte FC_Frame[], long reqSize):获取多帧数据,获取的数据保存在 recData 中,获取多帧数据的过程中需要发送流控帧,保存在 FC_Frame 中;
实现思路:
- 接收第一帧响应报文,判断是否是多帧数据;
- 如果不是多帧数据,则直接保存数据,函数返回;
- 如果是多帧数据则解析首帧,计算数据长度以及要接收的包数;
- 发送流控帧,并接收连续帧;
- 接收最后一包数据的时候,需要根据首帧返回的数据长度,过滤掉填充的字节;
long getMultiFramesData(dword recId, byte recData[], char busName[], byte channel, dword reqId, byte FC_Frame[], long reqSize) { long dataLength, retDLC; long frameNum; long ret; long i, j, k; long byteCounter; byte payload[64]; byte N_PCItype; byteCounter = 0; // 1. 接收单帧数据 ret = getSingleFrame(recId, payload, retDLC, busName, 5000); if(ret == 1) { N_PCItype = (payload[0] >> 4) & 0x0F; if(N_PCItype == 1) // 首帧 { k = CalculateFramesSequence(payload, retDLC, dataLength, frameNum); testStep("", "CAN message length %d, %d bytes, %d frames", retDLC, dataLength, frameNum); for(i = k; i < dataLength; i++) { recData[byteCounter++] = payload[i]; } // 2. 发送流控帧 sendSingleFrame(reqId, FC_Frame, reqSize, channel); // 3. 接收连续帧数据 for(i = 0; i < frameNum - 1; i++) { ret = getSingleFrame(recId, payload, retDLC, busName, 2000); if(ret == 1) { for(j = 1; j < retDLC; j++) { if(byteCounter < dataLength) // 过滤掉填充字节 { recData[byteCounter++] = payload[j]; } } } else { testStepFail("", "received CF timeout, expect %d frames, received %d frames actually", frameNum, i + 1); return -1; } } } else { dataLength = payload[0] & 0x0F; for(i = 1; i < retDLC; i++) { recData[byteCounter++] = payload[i]; } } return byteCounter; } else { testStepFail("not received can message."); return -1; } return 1; }测试用例:
testcase UDS_Get_multiframes() { byte sendData[8] = {0x06, 0x19, 0x04, 0x00, 0x00, 0x01, 0x01, 0x00}; byte sendDataFC[8] = {0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; byte getData[4095]; char getDataStr[4095]; long retDataSize; sendSingleFrame(0x703, sendData, 8, 2); retDataSize = getMultiFramesData(0x603, getData, "CAN2", 2, 0x703, sendDataFC, 8); convertByteArrayToStr(getData, retDataSize, getDataStr, ""); testStep("", "receive data (0x%X): %s", getDataStr); }总结
本文围绕基于 CAPL 手动实现 CAN 诊断 TP 层测试这一主题,从测试步骤和日志采集、测试用例编写、CAN 传输层功能手动实现三个方面进行了系统讲解。首先,通过封装日志采集和测试步骤打印函数,为后续测试提供了清晰、可复用的日志输出基础;其次,以 NRC 响应码测试和读 DID 测试为例,展示了如何编写规范的诊断测试用例;最后,重点介绍了单帧诊断请求、单帧诊断响应、多帧诊断请求和多帧诊断响应四个核心函数的实现思路与代码细节,覆盖了 ISO 15765-2 协议中 SF、FF、CF、FC 等关键帧类型的处理逻辑。
通过手动实现这些传输层功能,测试工程师可以摆脱对内置诊断函数的依赖,更灵活地控制诊断报文的发送与接收过程,深入验证 TP 层的分包与重组逻辑,从而有效弥补内置函数在 TP 层专项测试方面的不足。希望本文的代码示例和实现思路能够为读者在实际项目中开展 CAN 诊断 TP 层测试提供有价值的参考。