CAPL Script

以项目串联知识点

学习目标

完成本篇后,你将能够:

  • 掌握在同一个 CANoe 工程中整合仿真节点和测试模块的方法
  • 理解手动测试与自动化测试的区别和各自优势
  • 学会使用 Interactive Generator 进行手动测试和验证
  • 学会查看和分析 Test Report(测试报告)
  • 建立系统集成思维,理解"开发-测试-验证"的完整工作流
  • 具备为现有项目添加新功能和测试用例的能力

项目回顾:两大成果的整合

经过前面两篇的学习,我们已经构建了两个独立的 CAPL 程序:

第六篇成果:DoorModule 仿真节点

我们创建了一个完整的车门模块仿真节点,它能够:

// 接收命令
on message LockCmd { doorLockState = 1; }
on message WindowCmd { /* Handle window control */ }

// 自动行为
on timer windowTimer { /* Simulate window movement */ }

// 状态反馈
on timer statusTimer { output(DoorStatus); }

核心特性

  • 命令响应:接收并处理上锁、解锁、车窗控制命令
  • 内部状态:维护门锁状态、车窗位置、移动状态
  • 自动行为:模拟真实的车窗升降过程(2秒渐进式移动)
  • 安全检查:车窗移动时禁止上锁/解锁、车门锁定时禁止控制车窗
  • 状态反馈:每 500ms 发送一次 DoorStatus 消息,报告当前状态

第七篇成果:DoorModule_Test 测试模块

我们创建了一个专业的测试模块,它能够:

testcase TC_DoorLockTest()
{
    // Send stimulation
    message 0x200 lockCmd;
    output(lockCmd);

    // Verify response
    long result = TestWaitForMessage(DoorStatus, 1000);
    if (result == 1 && doorStatus == 0x01)
    {
        TestStepPass("Door locked successfully");
    }
}

核心功能

  • 自动化测试:使用 testcase 组织测试用例
  • 激励生成:主动发送测试命令(output)
  • 响应验证:使用 TestWaitForMessage 等待并验证响应
  • TSL 支持:使用 Checks 监控信号范围、Stimulus 生成测试数据
  • 报告生成:自动生成详细的测试执行报告

本篇目标:搭建"仿真-测试"一体化台架

现在,我们将把仿真节点测试模块整合到一个 CANoe 工程中,构建一个完整的仿真与测试台架

这个台架将让你能够:

  1. 手动测试:通过 Interactive Generator 手动发送命令,观察仿真节点的响应
  2. 自动测试:运行测试模块,自动执行所有测试用例并生成报告
  3. 对比验证:比较手动和自动测试的结果,加深对系统的理解

📝 提示:这正是真实汽车电子开发的工作流程——先通过仿真验证功能,再通过自动化测试确保质量。


工程配置:搭建测试台架

让我们一步步搭建这个集成环境。

步骤 1:创建新的 CANoe 工程

  1. 打开 CANoe,选择 FileNew
  2. 在工程向导中选择适当的总线类型(通常选择 CAN
  3. 输入工程名称,如 DoorModule_Project
  4. 选择保存位置

[!SCREENSHOT]
位置:CANoe 启动界面
内容:New Project 向导对话框
标注:圈出工程类型选择和名称输入框

步骤 2:配置网络和数据库

在 CANoe 工程中,我们需要配置网络和数据库:

  1. 网络配置

    • Simulation Setup 中,确保有一个 CAN 网络
    • 双击 CAN 网络,配置通道和波特率(通常是 500 kbps)
  2. 数据库文件(可选,但推荐):

    • 右键点击 Databases,选择 Add
    • 创建或导入 DBC 文件,定义 DoorStatus、LockCmd 等消息

简化方案:如果不使用 DBC,也可以直接在 CAPL 中使用原始消息 ID(如 0x200、0x300)。

步骤 3:添加 DoorModule 仿真节点

  1. Simulation Setup 窗口中
  2. 右键点击 Network 节点,选择 Insert Network Node
  3. 选择 CAPL 类型
  4. 输入节点名称:DoorModule
  5. 在文件浏览器中选择第六篇保存的 DoorModule.can 文件
  6. 点击 OK

[!SCREENSHOT]
位置:CANoe Simulation Setup 窗口
内容:显示新添加的 DoorModule 节点
标注:圈出仿真节点和其 CAPL 文件路径

步骤 4:添加 DoorModule_Test 测试模块

  1. 在菜单栏选择 TestTest Setup
  2. 右键点击 Test Environment,选择 Insert Test Module
  3. 输入模块名称:DoorModule_Test
  4. 选择第七篇保存的 DoorModule_Test.can 文件
  5. 点击 OK

[!SCREENSHOT]
位置:CANoe Test Setup 窗口
内容:显示新添加的测试模块
标注:圈出测试模块和 Test Units 面板

步骤 5:配置 Interactive Generator 面板

为了进行手动测试,我们需要添加 Interactive Generator:

  1. Test Setup 窗口中,右键点击测试模块
  2. 选择 InsertInteractive Generator
  3. 配置要发送的消息:
    • 添加 LockCmd(ID: 0x200)
    • 添加 UnlockCmd(ID: 0x201)
    • 添加 WindowCmd(ID: 0x202)

[!SCREENSHOT]
位置:CANoe Test Setup 窗口
内容:展示 Interactive Generator 配置
标注:圈出消息配置区域和发送按钮

步骤 6:验证配置

确保所有组件都已正确添加:

  • Simulation Setup:DoorModule 仿真节点
  • Test Setup:DoorModule_Test 测试模块
  • Interactive Generator:可发送测试命令的面板

提示:如果编译时遇到错误,请检查 CAPL 文件的路径是否正确,确保没有语法错误。


运行与观察:两种测试方式的对比

现在让我们运行项目,观察两种不同的测试方式。

方式一:手动测试(Interactive Generator)

手动测试模拟了开发工程师在实际调试过程中的操作。

步骤 1:启动仿真节点

  1. 点击 Start(开始测量)按钮
  2. 观察 Write 窗口,你会看到:
DoorModule: Initialization started
DoorModule: Initialization complete - Unlocked, Windows up

步骤 2:使用 Interactive Generator 发送命令

  1. Test Setup 中,找到 Interactive Generator 面板
  2. LockCmd 消息行,输入数据 01(上锁命令)
  3. 点击 Send 按钮
  4. 观察 Write 窗口:
DoorModule: Door locked

步骤 3:观察状态反馈

  1. 打开 Trace 窗口(ViewTrace
  2. 你会看到 DoorStatus 消息(ID 0x300)每 500ms 发送一次
  3. 解析消息数据:
    • Byte 0: 01(门锁状态:已锁)
    • Byte 1: 00(车窗位置:0%,完全升起)
    • Byte 2: 00(移动状态:停止)

步骤 4:测试车窗控制

  1. 在 Interactive Generator 中发送 WindowCmd 消息,数据 02(车窗上升)
  2. 观察 Write 窗口:
DoorModule: Window moving up
DoorModule: Window fully up
  1. 观察 Trace 窗口中的 DoorStatus 消息:
    • WindowPosition 从 0 逐渐增加到 100(如果是下降命令)
    • WindowMovement 从 0 变为 1 或 2(移动中)

[!SCREENSHOT]
位置:CANoe Trace 窗口
内容:显示 DoorStatus 消息的连续记录
标注:圈出时间戳和消息数据,标注 WindowPosition 的变化

手动测试的优势与局限

方面 手动测试 自动化测试
操作方式 人工点击 Interactive Generator 程序自动执行
时间消耗 10 分钟/次 10 秒/次
准确性 依赖操作者注意力 100% 一致
重复性 难以保证完全一致 完全可重复
记录方式 手动记录观察结果 自动生成结构化报告
适用场景 调试、探索性测试 回归测试、压力测试

方式二:自动化测试(Test Module)

现在让我们运行自动化测试模块,看看它如何替代手动操作。

步骤 1:运行测试模块

  1. Test Setup 窗口中,右键点击 DoorModule_Test
  2. 选择 Start with Report
  3. 测试会自动执行,无需人工干预

步骤 2:观察测试执行过程

打开 Test Report 窗口,你会看到测试的执行过程:

[19:43:25] Test Module Started: DoorModule_Test
[19:43:25] Test Case Started: TC_DoorLockTest
[19:43:26] Step 1: Send lock command
[19:43:26] Step 2: Wait for response (timeout: 1000ms)
[19:43:26] Step 3: Verify door status
[19:43:26] Step 3.1: PASS - Door status message received
[19:43:26] Step 3.2: PASS - Door is locked
[19:43:27] Test Case Finished: TC_DoorLockTest - PASSED

步骤 3:查看完整测试报告

测试完成后,CANoe 会生成 HTML 格式的测试报告,包含:

测试用例列表

  • TC_DoorLockTest - PASSED
  • TC_WindowControlTest - PASSED
  • (其他测试用例)

详细步骤记录
每个 testcase 中的所有 TestStep 都会记录,包括:

  • 步骤编号和描述
  • 执行时间
  • 判定结果(Pass/Fail)
  • 实际值与期望值的比较

[!SCREENSHOT]
位置:CANoe Test Report Viewer
内容:显示完整的测试执行报告
标注:圈出测试用例状态、详细步骤、判定结果

自动化测试的优势

优势

  • 高效:所有测试用例自动执行,节省人工时间
  • 准确:每次执行完全一致,避免人为错误
  • 可重复:可以反复运行,确保代码修改不影响已有功能
  • 详细记录:自动生成结构化的测试报告
  • 可扩展:可以轻松添加新的测试用例

对比手动测试的提升

手动测试(10分钟)          vs    自动化测试(10秒)
- 需要手动输入每个命令            - 自动发送所有命令
- 需要手动观察和记录            - 自动验证和记录
- 结果依赖人工判断              - 结果基于预设逻辑
- 难以验证边界条件              - 轻松测试所有场景

代码对比:同一消息的"双重身份"

有趣的是,同一条消息在不同角色中有着完全不同的作用:

LockCmd 消息

在仿真节点中(第六篇)

// 角色:被接收的命令
on message LockCmd
{
    // Process the received command
    doorLockState = 1;
    write("DoorModule: Door locked");
}

在测试模块中(第七篇)

// 角色:发送的激励
testcase TC_DoorLockTest()
{
    // Generate stimulation
    message 0x200 lockCmd;
    lockCmd.byte(0) = 0x01;
    output(lockCmd);
}

DoorStatus 消息

在仿真节点中

// 角色:发送的状态反馈
on timer statusTimer
{
    DoorStatus.byte(0) = doorLockState;
    output(DoorStatus);  // Send to bus
}

在测试模块中

// 角色:期望的响应
testcase TC_DoorLockTest()
{
    // Send command first...
    output(lockCmd);

    // Then wait for expected response
    long result = TestWaitForMessage(DoorStatus, 1000);
    if (result == 1)
    {
        TestStepPass("Door status received");
    }
}

这体现了什么?

同一个 CAN 消息,在仿真节点中是"输入"(命令)或"输出"(状态),在测试模块中是"激励"(刺激)或"验证点"(检查目标)。

这种"角色切换"帮助你理解:

  • ECU 是如何与其他节点交互的
  • 测试是如何模拟真实使用场景的
  • 开发与测试的辩证关系

拓展思考:功能增强与进阶

完成基础整合后,让我们探索如何为项目添加新功能。

增强一:防夹手逻辑

真实的车窗控制器都有防夹手功能——当车窗上升遇到阻力时,会自动下降。

在仿真节点中添加防夹手检测

variables
{
    byte faultStatus = 0;  // Fault indicator: 0=no fault, 1=fault detected
}

on timer windowTimer
{
    if (windowMovement == 1)  // Moving up
    {
        // Anti-pinch logic: simulate obstacle detection
        // Note: random() returns 0.0-1.0, so <0.1 means 10% chance
        if (windowPosition > 50 && random() < 0.1)
        {
            // Jam detected! Stop movement immediately for safety
            windowMovement = 0;  // Reset movement state
            windowPosition = windowPosition - 10;  // Reverse by 10% for safety margin
            write("DoorModule: Window jam detected! Auto-reversing");

            // Set fault flag to indicate anomaly (for diagnostic purposes)
            faultStatus = 1;  // This can be read via diagnostic DID later
        }
        else
        {
            // Normal movement: continue raising window
            windowPosition = windowPosition - 5;  // Move 5% per 20ms cycle
            if (windowPosition <= 0)
            {
                // Reached fully closed position
                windowPosition = 0;
                windowMovement = 0;  // Stop movement
                write("DoorModule: Window fully up");
            }
        }
    }
    // Note: Similar logic needed for windowMovement == 2 (moving down)

    // Continue timer if window is still moving
    // This creates smooth, continuous movement over ~2 seconds
    if (windowMovement != 0)
    {
        setTimer(windowTimer, 20);  // Next update in 20ms
    }
}

在测试模块中添加防夹手测试用例

variables
{
    byte faultStatus = 0;  // External variable to read fault status from simulation
}

testcase TC_WindowJamTest()
{
    dword checkId, stimId;

    TestCaseTitle("TC 3.0", "Window Anti-Pinch Test");
    TestCaseDescription("Test that window stops and reverses when jam is detected");

    // Setup: Create monitoring check
    // This check runs in background, monitoring FaultStatus signal
    // It will automatically fail the test if faultStatus goes out of range (0-1)
    checkId = ChkCreate_MsgSignalValueRangeViolation(
        DoorStatus, DoorStatus::FaultStatus, 0, 1
    );
    TestAddConstraint(checkId);  // Activate the constraint

    // Stimulus: Generate test scenario
    // Use TSL Stimulus to send window up command
    // This simulates user holding window control button
    TestStep(1, "Stimulus", "Generate window up command with simulated obstacle");
    stimId = StmCreate_Toggle(
        WindowCmd, WindowCmd::Command,
        0, 2, 500, 1  // Value 2 = window up, toggle once, 500ms interval
    );
    StmControl_Start(stimId);  // Begin stimulus generation

    // Wait for the anti-pinch system to trigger
    // Based on simulation logic, jam should be detected within 3 seconds
    TestStep(1, "Monitor", "Wait for jam detection (max 3000ms)");
    TestWaitForTimeout(3000);  // Give enough time for random jam detection

    // Verification: Check if system responded correctly
    TestStep(2, "Verify", "Check if fault status is set");
    if (faultStatus == 1)
    {
        // Fault status was set = anti-pinch system detected the jam
        TestStepPass("Jam detection working correctly");
    }
    else
    {
        // Fault status not set = system failed to detect jam
        TestStepFail("Jam detection not triggered");
    }

    // Cleanup: Stop and destroy TSL resources
    // Important: Always clean up to avoid resource leaks
    StmControl_Stop(stimId);        // Stop stimulus generation
    StmControl_Destroy(stimId);     // Free stimulus resources
    TestRemoveConstraint(checkId);  // Deactivate check
    ChkControl_Destroy(checkId);    // Free check resources
}

增强二:诊断功能入门

诊断是汽车电子的重要部分。让我们添加一个简单的诊断功能。

在仿真节点中添加诊断响应

// Diagnostic request handler for DID (Data Identifier) read
// This allows external diagnostic tools to read ECU internal data
on diagRequest DoorModule_DID
{
    // Create response object - will be sent back to diagnostic tester
    diagResponse response;

    // Pack current ECU state into diagnostic response
    // DID format follows UDS (Unified Diagnostic Services) standard
    // Each byte has specific meaning defined in diagnostic specification

    // Byte 0: Door lock state
    // 0x00 = unlocked, 0x01 = locked
    response.byte(0) = doorLockState;

    // Byte 1: Window position (0-100%)
    response.byte(1) = windowPosition;

    // Byte 2: Window movement state
    // 0x00 = stopped, 0x01 = moving up, 0x02 = moving down
    response.byte(2) = windowMovement;

    // Byte 3: Fault status
    // 0x00 = no fault, 0x01 = fault detected (e.g., window jam)
    response.byte(3) = faultStatus;

    // Send response back to diagnostic tester
    // This completes the diagnostic transaction
    DiagResponseSend(response);  // Capital D and S

    write("DoorModule: DID 0xF101 data sent to diagnostic tool");
}

测试诊断功能

testcase TC_DiagnosticTest()
{
    TestCaseTitle("TC 4.0", "Diagnostic Communication Test");
    TestCaseDescription("Test diagnostic DID read functionality");

    // Construct diagnostic request following UDS protocol
    // 0x22 is the UDS service for "Read Data By Identifier"
    diagRequest request;
    request.byte(0) = 0x22;  // UDS service: Read DID
    request.byte(1) = 0xF1;  // DID identifier (manufacturer specific)
    request.byte(2) = 0x01;  // Lower byte of DID 0xF101

    // Send request to ECU
    output(request);

    // Wait for ECU to respond (timeout: 2000ms)
    // This is a blocking call - test pauses until response or timeout
    // Note: First parameter is the expected response (request object or response ID)
    long result;
    result = TestWaitForDiagResponse(request, 2000);

    // Verify response
    if (result == 1)
    {
        // Response received successfully
        TestStepPass("Diagnostic response received");
        // Additional verification could check response data here
        // e.g., check that doorLockState matches expected value
    }
    else
    {
        // No response or timeout occurred
        TestStepFail("No diagnostic response within 2000ms");
    }
}

增强三:更复杂的测试场景

使用 TSL 的 Checks 和 Stimulus,我们可以创建更复杂的测试:

testcase TC_PerformanceTest()
{
    dword checkId, stimId;

    TestCaseTitle("TC 5.0", "System Performance Test");
    TestCaseDescription("Test system behavior under continuous operations for 15 seconds");

    // Setup: Create cycle time monitoring check
    // This verifies that DoorStatus messages are sent at consistent intervals
    // Requirement: Every 500ms ± 50ms (450-550ms range)
    checkId = ChkCreate_MsgAbsCycleTimeViolation(
        DoorStatus, 450, 550  // Min/Max cycle time in milliseconds
    );
    TestAddConstraint(checkId);  // Activate background monitoring

    // Create stimulus: continuous window operations
    // This simulates user repeatedly pressing window control buttons
    // Toggle between window up (2) and window down (1) every 1 second
    // Total of 10 operations = 10 seconds of stimulus
    stimId = StmCreate_Toggle(
        WindowCmd, WindowCmd::Command,
        1, 2, 1000, 10  // Value1=down, Value2=up, Interval=1000ms, Count=10
    );

    // Execute stress test: run for 15 seconds total
    // This gives enough time for 10 toggle operations plus observation
    StmControl_Start(stimId);  // Begin generating stimulus
    TestStep(1, "Monitor", "Running continuous operations (15s)");
    TestWaitForTimeout(15000);  // Let the system run under load
    StmControl_Stop(stimId);   // Stop stimulus generation

    // Verification: Check if cycle time was maintained under load
    TestStep(2, "Verify", "Check if status messages maintained cycle time");
    // The check (constraint) automatically validates this
    // It has been monitoring cycle time throughout the 15-second test
    TestStepPass("Performance test completed - cycle time within spec");

    // Cleanup: Always release TSL resources
    TestRemoveConstraint(checkId);  // Deactivate cycle time check
    ChkControl_Destroy(checkId);    // Free check resources
    StmControl_Destroy(stimId);     // Free stimulus resources
}

项目总结:知识点的串联应用

通过这个完整的"车门模块"项目,我们实现了从零到完整系统的跨越。

完整项目流程

[第一阶段:基础学习]
第一篇 → 什么是CAPL?      (认知建立)
第二篇 → 开发环境搭建      (工具使用)
第三篇 → 编程基础          (语法掌握)
第四篇 → 核心交互          (事件机制)
第五篇 → 调试技巧          (问题排查)

[第二阶段:实践应用]
第六篇 → 仿真节点开发      (构建ECU模型)
第七篇 → 测试模块开发      (自动化验证)
第八篇 → 综合项目          (系统集成) ← 本篇

知识点映射

让我们看看项目中用到的每个知识点:

知识点 对应篇章 在项目中的应用
变量与函数 第三篇 doorLockState, windowPosition 变量
事件驱动 第四篇 on message, on timer 事件
消息处理 第四篇 this.byte(0) 访问消息数据
调试输出 第五篇 write() 记录状态变化
状态机 第六篇 windowMovement 状态管理
定时器 第六篇 windowTimer 实现自动行为
测试用例 第七篇 testcase TC_* 组织测试
测试步骤 第七篇 TestStepPass/Fail 判定结果
TSL检查 第七篇 ChkCreate_* 监控信号
TSL激励 第七篇 StmCreate_* 生成测试数据
系统集成 第八篇 仿真+测试一体化运行

工程化思维:完整的开发工作流

需求分析 → 设计架构 → 编码实现 → 仿真验证 → 自动化测试 → 问题修复 → 回归测试
    ↓           ↓           ↓           ↓           ↓           ↓           ↓
  定义功能   状态机设计   仿真节点     手动调试    测试模块     缺陷修复    持续集成

本项目展示了这种思维

  1. 需求:车门控制器功能需求(第六篇开始)
  2. 设计:状态机、消息接口(第六篇)
  3. 实现:CAPL代码编写(第六篇)
  4. 仿真:验证功能正确性(第六篇+第八篇手动测试)
  5. 测试:自动化验证(第七篇+第八篇自动测试)
  6. 增强:添加防夹手、诊断(第八篇拓展)
  7. 回归:重新运行测试确保稳定性(第八篇总结)

关键收获

  1. 双重视角

    • 开发者视角:关注如何实现功能(仿真节点)
    • 测试者视角:关注如何验证功能(测试模块)
  2. 消息角色

    • 同一消息在不同上下文中扮演不同角色
    • 理解这种转换是掌握CAPL的关键
  3. 工程化

    • 从代码到系统:从函数到完整工程
    • 从手动到自动:提高测试效率和质量
  4. 可扩展性

    • 基础框架搭建后,可以轻松添加新功能
    • 测试用例可以随着功能增加而扩展

课程总结与进阶方向

恭喜你完成了整个CAPL技术教程系列!

本系列回顾

我们从零基础开始,逐步构建了完整的知识体系:

  1. 认知建立:理解CAPL是什么、为什么需要它
  2. 工具掌握:熟悉CAPL Browser、开发环境
  3. 语法基础:掌握CAPL语言特性
  4. 核心机制:理解事件驱动编程
  5. 调试能力:学会排查问题
  6. 实践应用:构建仿真节点
  7. 质量保证:编写自动化测试
  8. 系统集成:整合开发与测试

CAPL学习路径建议

[入门阶段]
✓ 本教程系列(1-8篇)
✓ 官方文档阅读
✓ 示例工程分析

[进阶阶段]
→ 复杂协议:CANopen、J1939、FlexRay
→ 诊断服务:UDS、KWP2000、OBD-II
→ 以太网:SOME/IP、AVB
→ V2X通信:车联网协议

[高级阶段]
→ 与其他工具集成:CANalyzer、CANoe Expense
→ 测试自动化:CI/CD集成
→ 自定义工具:扩展CAPL功能
→ 性能优化:大型系统工程

进阶主题推荐

  1. 诊断服务开发

    • 深入学习 on diagRequest 事件
    • UDS服务实现(读DID、写DID、清除DTC)
    • 复杂诊断序列和条件判断
  2. 复杂网络仿真

    • 多ECU交互仿真
    • 网关节点开发
    • 网络拓扑和负载仿真
  3. 自动化测试台架

    • 大规模测试用例管理
    • 测试报告分析和可视化
    • 持续集成(CI)中的自动化测试
  4. 工具集成

    • 与MATLAB/Simulink联仿
    • 与版本控制(Git)集成
    • 自定义测试工具开发

结语

CAPL是一门实用的技能,它让你能够:

  • 快速验证想法:通过仿真快速原型
  • 提高测试效率:自动化测试减少重复工作
  • 确保产品质量:系统化的验证流程
  • 深化系统理解:从代码层面理解汽车网络

最重要的是,本教程通过"车门模块"这个具体案例,展示了如何将分散的知识点整合成完整的工程解决方案。

继续实践:尝试为其他ECU(如发动机管理、空调控制)构建仿真和测试。

持续学习:关注Vector官方文档更新,学习新的协议和功能。

分享交流:与其他工程师分享你的项目经验,共同进步。


课后练习

  1. 基础练习:为 DoorModule 添加新功能——座椅调节控制(位置、记忆)。编写相应的仿真和测试代码。

  2. 进阶练习:使用 TSL 创建一个"压力测试":连续发送1000次车窗升降命令,检查系统是否出现性能问题或内存泄漏。

  3. 挑战练习:实现一个简单的"学习模式":仿真节点记录用户的车窗使用习惯(升降频率、最终位置),并在测试模块中验证这些习惯数据是否正确保存。