专栏《CAPL 脚本编写实战指南》第 9 篇作者一线汽车电子测试工程师适合人群已掌握 CAPL 基础的测试人员、想提高开发效率的工程师开篇为什么需要模板库这是我工作第 3 年的一个重要发现。当时的情况公司某德系 Tier 1项目同时做 5 个 ECU 测试项目问题每个项目都从头写脚本效率低问题暴露项目 A写了 1000 行测试脚本项目 B又写了 1000 行80% 代码重复项目 C同事写的脚本我看懂要花 1 天项目 D我要休假没人能接手我的脚本我的解决方案把常用代码整理成模板建立团队模板库新人来了直接用模板效率提升 5 倍效果脚本开发时间3 天 → 0.5 天代码复用率20% → 80%新人上手时间1 周 → 1 天团队效率提升5 倍我的体会不要重复造轮子。把常用代码整理成模板效率提升 10 倍。这篇教程我会把所有常用模板都分享给你。一、测试用例模板1.1 基础测试模板/* * * 基础测试模板 * * 功能基础测试框架 * 版本1.0 * 作者测试工程师 * 日期2025-03-18 * */ #include Canoe2.inc variables { long g_test_total; long g_test_passed; long g_test_failed; } on start { g_test_total 0; g_test_passed 0; g_test_failed 0; write(); write(测试开始); write(); } on test { // 测试用例 1 testStepBegin(TC001_测试用例 1); g_test_total; // 测试代码... if (result 0) { testStepPass(通过); g_test_passed; } else { testStepFail(失败); g_test_failed; } testWaitForTimeout(50); // 测试用例 2 testStepBegin(TC002_测试用例 2); g_test_total; // 测试代码... if (result 0) { testStepPass(通过); g_test_passed; } else { testStepFail(失败); g_test_failed; } } on end { write(); write(); write(测试结束); write(); write(总用例数%d, g_test_total); write(通过数%d, g_test_passed); write(失败数%d, g_test_failed); if (g_test_total 0) { float pass_rate (float)g_test_passed * 100.0 / (float)g_test_total; write(通过率%.1f%%, pass_rate); } write(); }1.2 信号测试模板/* * * 信号测试模板 * * 功能测试 CAN 信号 * 版本1.0 * 作者测试工程师 * 日期2025-03-18 * */ #include Canoe2.inc variables { message CANMessage msg_Test; long g_test_total; long g_test_passed; long g_test_failed; } proc void test_signal(long expected, long actual, char test_name[]) { g_test_total; testStepBegin(test_name); if (expected actual) { testStepPass(期望值%d, 实际值%d, expected, actual); g_test_passed; } else { testStepFail(期望值%d, 实际值%d, expected, actual); g_test_failed; } } on start { g_test_total 0; g_test_passed 0; g_test_failed 0; // 配置报文 msg_Test.canID 0x123; msg_Test.dlc 8; } on test { // 测试最小值 msg_Test.SignalName 0; output(msg_Test); testWaitForTimeout(100); test_signal(0, msg_Test.SignalName, 最小值测试); testWaitForTimeout(50); // 测试中间值 msg_Test.SignalName 5000; output(msg_Test); testWaitForTimeout(100); test_signal(5000, msg_Test.SignalName, 中间值测试); testWaitForTimeout(50); // 测试最大值 msg_Test.SignalName 10000; output(msg_Test); testWaitForTimeout(100); test_signal(10000, msg_Test.SignalName, 最大值测试); } on end { write(总用例数%d, g_test_total); write(通过数%d, g_test_passed); write(失败数%d, g_test_failed); }1.3 周期发送测试模板/* * * 周期发送测试模板 * * 功能测试报文周期发送 * 版本1.0 * 作者测试工程师 * 日期2025-03-18 * */ #include Canoe2.inc variables { message CANMessage msg_Test; msTimer sendTimer; long sendCount; long expectedPeriod; long lastSendTime; long periodError; } on start { sendCount 0; expectedPeriod 100; // 期望周期 100ms lastSendTime 0; periodError 0; msg_Test.canID 0x123; msg_Test.dlc 8; setTimer(sendTimer, expectedPeriod); } on timer sendTimer { sendCount; msg_Test.byte(0) sendCount; output(msg_Test); // 检查周期 if (lastSendTime 0) { long actualPeriod timeNow() - lastSendTime; if (actualPeriod expectedPeriod * 0.9 || actualPeriod expectedPeriod * 1.1) { periodError; write(周期异常期望%dms, 实际%dms, expectedPeriod, actualPeriod); } } lastSendTime timeNow(); // 发送 100 次后停止 if (sendCount 100) { write(发送完成共%d次周期错误%d次, sendCount, periodError); } else { setTimer(sendTimer, expectedPeriod); } }二、报文操作模板2.1 报文发送模板/* * * 报文发送模板 * * 功能发送 CAN 报文 * 版本1.0 * 作者测试工程师 * 日期2025-03-18 * */ #include Canoe2.inc variables { message CANMessage msg_Test; } proc void send_can_message(long can_id, int dlc, byte data[]) { msg_Test.canID can_id; msg_Test.dlc dlc; for (int i 0; i dlc; i) { msg_Test.byte(i) data[i]; } output(msg_Test); write(发送报文ID0x%x, DLC%d, can_id, dlc); } on test { byte data[8]; // 测试数据 data[0] 0x10; data[1] 0x20; data[2] 0x30; data[3] 0x40; data[4] 0x50; data[5] 0x60; data[6] 0x70; data[7] 0x80; send_can_message(0x123, 8, data); }2.2 报文接收模板/* * * 报文接收模板 * * 功能接收和处理 CAN 报文 * 版本1.0 * 作者测试工程师 * 日期2025-03-18 * */ #include Canoe2.inc variables { long msgCount; long lastMsgTime; long timeoutCount; long expectedPeriod; } on start { msgCount 0; lastMsgTime 0; timeoutCount 0; expectedPeriod 100; // 期望周期 100ms } on message msg_Test { msgCount; // 检查周期 if (lastMsgTime 0) { long actualPeriod timeNow() - lastMsgTime; if (actualPeriod expectedPeriod * 1.5) { timeoutCount; write(报文超时周期%dms, actualPeriod); } } lastMsgTime timeNow(); // 处理报文数据 long data this.byte(0); write(收到报文 [%d]: ID0x%x, Data%d, msgCount, this.canID, data); } on end { write(总计收到%d个报文超时%d次, msgCount, timeoutCount); }2.3 报文过滤模板/* * * 报文过滤模板 * * 功能过滤和统计特定报文 * 版本1.0 * 作者测试工程师 * 日期2025-03-18 * */ #include Canoe2.inc variables { long filterCount[10]; // 过滤计数 long totalMsg; } on start { totalMsg 0; for (int i 0; i 10; i) { filterCount[i] 0; } } on message * { totalMsg; // 按 ID 过滤 switch (this.canID) { case 0x123: filterCount[0]; write(收到 VCU 报文); break; case 0x456: filterCount[1]; write(收到 BCM 报文); break; case 0x789: filterCount[2]; write(收到 EPS 报文); break; default: // 其他报文忽略 break; } } on end { write(总计收到%d个报文, totalMsg); write(VCU 报文%d个, filterCount[0]); write(BCM 报文%d个, filterCount[1]); write(EPS 报文%d个, filterCount[2]); }三、诊断测试模板3.1 UDS 诊断测试模板/* * * UDS 诊断测试模板 * * 功能测试 UDS 诊断服务 * 版本1.0 * 作者测试工程师 * 日期2025-03-18 * */ #include Canoe2.inc variables { message CANMessage msg_Diag_Request; message CANMessage msg_Diag_Response; long g_test_total; long g_test_passed; long g_test_failed; boolean waitResponse; } proc void send_diag_request(byte service, byte subfunc, byte data[]) { msg_Diag_Request.canID 0x7DF; // OBD 广播地址 msg_Diag_Request.dlc 8; msg_Diag_Request.byte(0) 0x02; // 长度 msg_Diag_Request.byte(1) service; msg_Diag_Request.byte(2) subfunc; for (int i 0; i 5; i) { msg_Diag_Request.byte(3 i) data[i]; } output(msg_Diag_Request); waitResponse true; } on message msg_Diag_Response { if (waitResponse) { waitResponse false; if (this.byte(1) 0x59) { testStepPass(诊断响应正确); g_test_passed; } else if (this.byte(1) 0x7F) { testStepFail(否定响应0x%x, this.byte(3)); g_test_failed; } } } on test { g_test_total 0; g_test_passed 0; g_test_failed 0; // 测试 19 服务 02读 DTC testStepBegin(UDS_19_02_读 DTC); g_test_total; byte data[5]; send_diag_request(0x19, 0x02, data); testWaitForTimeout(500); } on end { write(诊断测试完成通过%d/%d, g_test_passed, g_test_total); }3.2 DTC 读取模板/* * * DTC 读取模板 * * 功能读取 ECU 故障码 * 版本1.0 * 作者测试工程师 * 日期2025-03-18 * */ #include Canoe2.inc variables { message CANMessage msg_Diag; long dtcCount; boolean waitResponse; } on start { dtcCount 0; waitResponse false; } proc void read_dtc() { msg_Diag.canID 0x7DF; msg_Diag.dlc 8; msg_Diag.byte(0) 0x02; msg_Diag.byte(1) 0x19; msg_Diag.byte(2) 0x02; output(msg_Diag); waitResponse true; } on message * { if (waitResponse this.canID 0x7E8) { waitResponse false; if (this.byte(1) 0x59) { dtcCount this.byte(2); write(读取到%d个 DTC, dtcCount); } else if (this.byte(1) 0x7F) { write(DTC 读取失败NRC0x%x, this.byte(3)); } } } on test { write(开始读取 DTC); read_dtc(); testWaitForTimeout(1000); }四、日志记录模板4.1 基础日志模板/* * * 基础日志模板 * * 功能记录测试日志到文件 * 版本1.0 * 作者测试工程师 * 日期2025-03-18 * */ #include Canoe2.inc variables { long logHandle; } proc void log_init(char filename[]) { logHandle fileOpen(filename, FILE_WRITE); if (logHandle 0) { fileWrite(logHandle, 测试日志 \n); fileWrite(logHandle, 开始时间 timeNow() \n); } } proc void log_write(char msg[]) { if (logHandle 0) { fileWrite(logHandle, [ timeNow() ] msg \n); } } proc void log_close() { if (logHandle 0) { fileWrite(logHandle, 结束时间 timeNow() \n); fileWrite(logHandle, 日志结束 \n); fileClose(logHandle); } } on start { log_init(test_log.txt); log_write(测试开始); } on test { log_write(执行测试用例); } on end { log_write(测试结束); log_close(); }4.2 CSV 日志模板/* * * CSV 日志模板 * * 功能记录 CSV 格式日志 * 版本1.0 * 作者测试工程师 * 日期2025-03-18 * */ #include Canoe2.inc variables { long csvHandle; } proc void csv_init(char filename[]) { csvHandle fileOpen(filename, FILE_WRITE); if (csvHandle 0) { // 写入表头 fileWrite(csvHandle, Time,TestName,Result,Value\n); } } proc void csv_write(char testName[], char result[], long value) { if (csvHandle 0) { char buffer[200]; sprintf(buffer, %d,%s,%s,%d\n, timeNow(), testName, result, value); fileWrite(csvHandle, buffer); } } proc void csv_close() { if (csvHandle 0) { fileClose(csvHandle); } } on start { csv_init(test_results.csv); } on test { csv_write(TC001, PASS, 100); csv_write(TC002, FAIL, 200); } on end { csv_close(); }4.3 报文日志模板/* * * 报文日志模板 * * 功能记录所有 CAN 报文 * 版本1.0 * 作者测试工程师 * 日期2025-03-18 * */ #include Canoe2.inc variables { long logHandle; } on start { logHandle fileOpen(can_log.csv, FILE_WRITE); if (logHandle 0) { fileWrite(logHandle, Time,CanID,DLC,Data0,Data1,Data2,Data3,Data4,Data5,Data6,Data7\n); } } on message * { if (logHandle 0) { char buffer[200]; sprintf(buffer, %d,0x%x,%d,%d,%d,%d,%d,%d,%d,%d,%d\n, timeNow(), this.canID, this.dlc, this.byte(0), this.byte(1), this.byte(2), this.byte(3), this.byte(4), this.byte(5), this.byte(6), this.byte(7)); fileWrite(logHandle, buffer); } } on end { if (logHandle 0) { fileClose(logHandle); } }五、HIL 测试模板5.1 HIL 基础测试模板/* * * HIL 基础测试模板 * * 功能HIL 台架测试框架 * 版本1.0 * 作者测试工程师 * 日期2025-03-18 * */ #include Canoe2.inc variables { long hil_io_handle; long g_test_total; long g_test_passed; long g_test_failed; } proc void hil_init() { // 初始化 HIL 台架 hil_io_handle 1; // 示例 g_test_total 0; g_test_passed 0; g_test_failed 0; } proc void hil_set_output(int channel, float value) { // 设置 HIL 输出 write(HIL 通道%d输出%f, channel, value); } proc float hil_get_input(int channel) { // 读取 HIL 输入 return 0.0; // 示例 } proc void hil_test_result(char test_name[], boolean pass) { g_test_total; if (pass) { testStepPass(test_name); g_test_passed; } else { testStepFail(test_name); g_test_failed; } } on start { hil_init(); } on test { // 设置输入 hil_set_output(1, 12.0); // 12V 电源 testWaitForTimeout(100); // 读取输出 float voltage hil_get_input(1); // 验证 hil_test_result(电源电压测试, voltage 11.0 voltage 13.0); } on end { write(HIL 测试完成通过%d/%d, g_test_passed, g_test_total); }5.2 HIL 信号仿真模板/* * * HIL 信号仿真模板 * * 功能仿真 ECU 信号 * 版本1.0 * 作者测试工程师 * 日期2025-03-18 * */ #include Canoe2.inc variables { message CANMessage msg_ECU; msTimer simTimer; float simSpeed; float simRpm; } on start { simSpeed 0; simRpm 800; // 怠速 msg_ECU.canID 0x123; msg_ECU.dlc 8; setTimer(simTimer, 100); } on timer simTimer { // 仿真车速缓慢增加 if (simSpeed 120) { simSpeed 10; } // 仿真转速随车速变化 simRpm 800 simSpeed * 30; // 设置信号 msg_ECU.VehicleSpeed simSpeed; msg_ECU.EngineSpeed simRpm; // 发送报文 output(msg_ECU); write(仿真车速%f km/h, 转速%f rpm, simSpeed, simRpm); setTimer(simTimer, 100); }六、工具函数模板6.1 字符串处理模板/* * * 字符串处理模板 * * 功能常用字符串处理函数 * 版本1.0 * 作者测试工程师 * 日期2025-03-18 * */ #include Canoe2.inc // 字符串转大写 proc void str_to_upper(char str[]) { int len strlen(str); for (int i 0; i len; i) { if (str[i] a str[i] z) { str[i] str[i] - 32; } } } // 字符串转小写 proc void str_to_lower(char str[]) { int len strlen(str); for (int i 0; i len; i) { if (str[i] A str[i] Z) { str[i] str[i] 32; } } } // 去除首尾空格 proc void str_trim(char str[]) { int len strlen(str); if (len 0 str[len-1] ) { str[len-1] \0; } } // 字符串包含 proc int str_contains(char str[], char substr[]) { if (strstr(str, substr) ! NULL) { return 1; } return 0; }6.2 数学计算模板/* * * 数学计算模板 * * 功能常用数学计算函数 * 版本1.0 * 作者测试工程师 * 日期2025-03-18 * */ #include Canoe2.inc // 最大值 proc int max(int a, int b) { if (a b) return a; return b; } // 最小值 proc int min(int a, int b) { if (a b) return a; return b; } // 限幅 proc int clamp(int value, int min_val, int max_val) { if (value min_val) return min_val; if (value max_val) return max_val; return value; } // 映射 proc int map(int value, int in_min, int in_max, int out_min, int out_max) { return (value - in_min) * (out_max - out_min) / (in_max - in_min) out_min; }七、模板使用建议7.1 如何选择模板场景推荐模板基础测试基础测试模板信号测试信号测试模板周期测试周期发送测试模板诊断测试UDS 诊断测试模板日志记录CSV 日志模板HIL 测试HIL 基础测试模板7.2 模板定制步骤复制基础模板修改功能描述添加特定逻辑测试验证保存到模板库7.3 模板管理建议用 Git 管理模板版本号管理变更记录定期 review八、练习题目练习 1修改测试模板// 要求在基础测试模板中添加第 3 个测试用例练习 2创建信号测试// 要求用信号测试模板测试转速信号练习 3添加日志功能// 要求在测试模板中添加 CSV 日志记录练习 4创建诊断测试// 要求用 UDS 模板测试 22 服务读数据练习 5定制 HIL 模板// 要求修改 HIL 模板添加电源测试九、常见问题Q1模板可以直接用吗答可以但需要根据项目调整。修改报文 ID修改信号名添加特定逻辑Q2如何管理多个模板答用文件夹分类用 Git 版本管理写使用说明定期更新Q3模板怎么分享答放到团队共享目录写使用文档组织培训收集反馈Q4模板需要测试吗答需要。每个模板都要测试确保没有 Bug定期回归测试Q5如何更新模板答记录变更内容更新版本号通知使用者保留旧版本十、学习建议10.1 模板学习路线第 1 天熟悉基础测试模板 第 2 天学习信号测试模板 第 3 天学习报文操作模板 第 4 天学习诊断测试模板 第 5 天学习日志记录模板 第 6 天学习 HIL 测试模板 第 7 天综合练习10.2 实战建议先模仿— 用现成模板再修改— 根据需求调整后创新— 设计自己的模板持续优化— 收集反馈改进10.3 避坑指南坑说明避免方法直接复制不修改就用检查后再用不测试模板有 Bug先测试再用不更新模板过时定期更新不分享团队不知道主动分享不维护模板废弃指定专人维护写在最后模板是提高效率的利器。用模板开发时间3 天 → 0.5 天代码复用20% → 80%新人上手1 周 → 1 天建议不要重复造轮子。把常用代码整理成模板效率提升 10 倍。下一篇预告《CAPL 高级特性》类和对象回调函数动态内存高级定时器多节点通信如果本文对你有帮助欢迎点赞、收藏、关注专栏第一时间获取更新有任何问题欢迎在评论区留言我会逐一回复。练习答案会在第 10 篇公布先自己试试看