CAPL Script

如何开发一个仿真节点

学习目标

完成本篇后,你将能够:

  • 理解仿真节点的设计思路和状态机概念
  • 掌握在 CAPL 中实现 ECU 行为模型的方法
  • 学会使用 on start 初始化变量、 on message 响应命令、 on timer 实现自动行为
  • 构建一个完整的、可交互的仿真节点案例

从需求到设计:仿真节点的思考方式

在之前的篇章中,我们学习了 CAPL 的基础语法和事件机制。现在,是时候把这些知识整合起来,构建一个真正的 ECU 行为模型了。

什么是仿真节点?

仿真节点(Simulation Node)是 CANoe 中用 CAPL 编写的虚拟 ECU。它不是简单的"回声"(收到什么就转发什么),而是具有业务逻辑的实体:它能接收命令、更新内部状态、产生自动行为、并反馈状态信息。

想象一下,真实的 ECU 会:

  • 接收来自其他 ECU 的命令(如"上锁")
  • 根据命令改变自己的内部状态(如门锁状态)
  • 执行自动化任务(如车窗升降)
  • 向总线报告当前状态(如"已上锁")

我们的仿真节点也要做到这些。

车门模块案例

接下来,我们会一步步构建一个 车门模块(DoorModule) 的仿真节点。这个模块的功能包括:

接收命令:

  • LockCmd - 上锁命令
  • UnlockCmd - 解锁命令
  • WindowControlCmd - 车窗控制命令

内部状态:

  • 门锁状态(已锁/未锁)
  • 车窗位置(0-100%,0 表示完全升起,100 表示完全降下)

自动行为:

  • 接收到车窗下降命令后,自动执行升降过程(约 2 秒完成)
  • 升降过程中禁止接收新命令(防夹手逻辑的简化版)

状态反馈:

  • 周期性发送 DoorStatus 消息,报告当前的门锁状态和车窗位置

📝 提示:这个案例将在第六、七、八篇中持续使用。第七篇我们会为它编写测试用例,第八篇会将仿真与测试整合运行。


步骤一:初始化与状态定义

每个仿真节点都需要在测量开始时初始化其内部状态。这就像 ECU 上电后的自检和初始化过程。

在 CAPL 中,我们使用 on start 事件来处理初始化:

variables
{
    // Door lock state: 0=unlocked, 1=locked
    byte doorLockState = 0;

    // Window position: 0-100% (0=fully up, 100=fully down)
    byte windowPosition = 0;

    // Window movement state: 0=stopped, 1=moving up, 2=moving down
    byte windowMovement = 0;

    // Timers
    msTimer windowTimer;         // For smooth window movement
    msTimer statusTimer;         // For periodic status transmission

    // Message declarations
    message 0x200 LockCmd;      // Lock command from central control
    message 0x201 UnlockCmd;    // Unlock command from central control
    message 0x202 WindowCmd;    // Window control command
    message 0x300 DoorStatus;   // Status message to dashboard
}

on start
{
    write("DoorModule: Initialization started");

    // Initialize to known state
    doorLockState = 0;          // Start unlocked
    windowPosition = 0;         // Windows fully up
    windowMovement = 0;         // Not moving

    // Start periodic status transmission (every 500ms)
    setTimer(statusTimer, 500);

    write("DoorModule: Initialization complete - Unlocked, Windows up");
}

让我们理解这段代码:

变量声明部分:

  • doorLockStatewindowPositionwindowMovement — 这些是仿真节点的"记忆",记录当前状态
  • windowTimer — 用于控制车窗升降的时间(自动行为需要时间)
  • 四个 message 变量 — 声明我们要处理和发送的消息

on start 事件:

  • 测量开始时触发一次
  • 设置初始状态(未锁、车窗升起)
  • 启动定时器,定期发送状态消息

⚠️ 重要提示on start 是最适合初始化变量的地方。根据官方文档,此时测量系统已经完全初始化,可以安全地使用所有 CAPL 功能。


步骤二:实现命令响应

现在,我们需要响应来自其他 ECU 的命令。这是通过 on message 事件实现的。

让我们先看最核心的上锁/解锁逻辑:

// Handle lock command
on message LockCmd
{
    // Check if window is moving (safety check)
    if (windowMovement != 0)
    {
        write("DoorModule: Cannot lock - window is moving");
        return;
    }

    // Update internal state
    doorLockState = 1;

    write("DoorModule: Door locked");
}

// Handle unlock command
on message UnlockCmd
{
    // Check if window is moving (safety check)
    if (windowMovement != 0)
    {
        write("DoorModule: Cannot unlock - window is moving");
        return;
    }

    // Update internal state
    doorLockState = 0;

    write("DoorModule: Door unlocked");
}

代码解释:

  1. 事件过滤on message LockCmd 只响应 ID 为 0x200 的消息
  2. 安全检查:如果车窗正在移动,禁止操作(防止车窗半降时上锁)
  3. 状态更新:修改内部变量 doorLockState
  4. 日志记录:使用 write() 输出到 Write 窗口,方便调试

接下来是车窗控制命令:

// Handle window control command
on message WindowCmd
{
    byte command;

    // Extract command from first byte of message
    command = WindowCmd.byte(0);

    // Only process if door is unlocked
    if (doorLockState == 1)
    {
        write("DoorModule: Cannot control window - door is locked");
        return;
    }

    // Only process if window is not currently moving
    if (windowMovement != 0)
    {
        write("DoorModule: Cannot control window - already moving");
        return;
    }

    if (command == 1)  // Window down
    {
        windowMovement = 2;  // Mark as moving down
        setTimer(windowTimer, 20);  // Update every 20ms for smooth movement

        write("DoorModule: Window moving down");
    }
    else if (command == 2)  // Window up
    {
        windowMovement = 1;  // Mark as moving up
        setTimer(windowTimer, 20);  // Update every 20ms

        write("DoorModule: Window moving up");
    }
}

代码解释:

  1. 消息解析:使用 this.byte(0) 访问接收到的消息数据
  2. 状态检查
    • 车门必须解锁才能控制车窗(安全考虑)
    • 车窗不能在移动中接收新命令
  3. 命令执行:根据命令类型设置 windowMovement 状态并启动定时器

📝 提示this 关键字在 on message 事件中代表"刚刚收到的消息",是 CAPL 事件驱动编程的核心概念。


步骤三:实现自动行为

车窗升降不是瞬间完成的——它需要时间。在真实 ECU 中,这由电机驱动电路处理。在我们的仿真节点中,使用 on timer 来模拟这个过程。

// Timer event for window movement
on timer windowTimer
{
    byte movementStep = 5;  // Move 5% per tick

    if (windowMovement == 1)  // Moving up
    {
        // Decrease position
        windowPosition = windowPosition - movementStep;
        if (windowPosition <= 0)
        {
            windowPosition = 0;
            windowMovement = 0;  // Stop moving
            write("DoorModule: Window fully up");
        }
    }
    else if (windowMovement == 2)  // Moving down
    {
        // Increase position
        windowPosition = windowPosition + movementStep;
        if (windowPosition >= 100)
        {
            windowPosition = 100;
            windowMovement = 0;  // Stop moving
            write("DoorModule: Window fully down");
        }
    }

    // Continue timer if still moving
    if (windowMovement != 0)
    {
        setTimer(windowTimer, 20);
    }
}

代码解释:

  1. 平滑移动:每 20ms 更新一次位置,每次移动 5%
  2. 边界检查:到达上限(0%)或下限(100%)时停止
  3. 状态重置:移动结束时将 windowMovement 设为 0
  4. 循环定时器:如果仍在移动,重新设置定时器继续下一次更新

这样,车窗升降变成了一个渐进的过程,而不是瞬间跳跃,符合真实 ECU 的行为。


步骤四:状态反馈

ECU 不仅要执行命令,还要主动报告自己的状态。在汽车网络中,仪表盘、BCM(车身控制器)等需要知道各个模块的状态。

我们使用周期性定时器来发送状态消息:

on timer statusTimer
{
    // Pack current state into DoorStatus message

    // Byte 0: Door lock state (0=unlocked, 1=locked)
    DoorStatus.byte(0) = doorLockState;

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

    // Byte 2: Window movement state (0=stopped, 1=moving up, 2=moving down)
    DoorStatus.byte(2) = windowMovement;

    // Set ID if not already set
    DoorStatus.id = 0x300;

    // Send message to bus
    output(DoorStatus);

    // Reset timer for next transmission
    setTimer(statusTimer, 500);
}

代码解释:

  1. 数据打包:将内部状态变量映射到消息字节
    • DoorStatus.byte(0) = 门锁状态
    • DoorStatus.byte(1) = 车窗位置
    • DoorStatus.byte(2) = 车窗运动状态
  2. 消息发送output() 函数将消息发送到 CAN 总线
  3. 周期性更新:每次发送后重新设置定时器,实现每 500ms 发送一次

📝 关键概念output() 是 CAPL 中发送消息到总线的核心函数。它接收一个 message 类型的变量,并将其放到指定的 CAN 通道上。


完整代码展示

将所有部分组合起来,这就是一个完整的仿真节点:

variables
{
    // Internal state variables
    byte doorLockState = 0;      // 0=unlocked, 1=locked
    byte windowPosition = 0;     // 0-100% (0=up, 100=down)
    byte windowMovement = 0;     // 0=stopped, 1=up, 2=down

    // Timers
    msTimer windowTimer;         // For smooth window movement
    msTimer statusTimer;         // For periodic status transmission

    // Message declarations
    message 0x200 LockCmd;       // Lock command
    message 0x201 UnlockCmd;     // Unlock command
    message 0x202 WindowCmd;     // Window control
    message 0x300 DoorStatus;    // Status message
}

on start
{
    write("DoorModule: Initialization started");

    // Initialize to known state
    doorLockState = 0;
    windowPosition = 0;
    windowMovement = 0;

    // Start periodic status transmission
    setTimer(statusTimer, 500);

    write("DoorModule: Ready - Unlocked, Windows up");
}

on message LockCmd
{
    // Safety check
    if (windowMovement != 0)
    {
        write("DoorModule: Cannot lock - window is moving");
        return;
    }

    doorLockState = 1;
    write("DoorModule: Door locked");
}

on message UnlockCmd
{
    // Safety check
    if (windowMovement != 0)
    {
        write("DoorModule: Cannot unlock - window is moving");
        return;
    }

    doorLockState = 0;
    write("DoorModule: Door unlocked");
}

on message WindowCmd
{
    byte command;
    command = WindowCmd.byte(0);

    // Check door state
    if (doorLockState == 1)
    {
        write("DoorModule: Cannot control window - door is locked");
        return;
    }

    // Check if already moving
    if (windowMovement != 0)
    {
        write("DoorModule: Cannot control window - already moving");
        return;
    }

    if (command == 1)  // Window down
    {
        windowMovement = 2;
        setTimer(windowTimer, 20);
        write("DoorModule: Window moving down");
    }
    else if (command == 2)  // Window up
    {
        windowMovement = 1;
        setTimer(windowTimer, 20);
        write("DoorModule: Window moving up");
    }
}

on timer windowTimer
{
    byte movementStep = 5;

    if (windowMovement == 1)  // Moving up
    {
        windowPosition = windowPosition - movementStep;
        if (windowPosition <= 0)
        {
            windowPosition = 0;
            windowMovement = 0;
            write("DoorModule: Window fully up");
        }
    }
    else if (windowMovement == 2)  // Moving down
    {
        windowPosition = windowPosition + movementStep;
        if (windowPosition >= 100)
        {
            windowPosition = 100;
            windowMovement = 0;
            write("DoorModule: Window fully down");
        }
    }

    // Continue if still moving
    if (windowMovement != 0)
    {
        setTimer(windowTimer, 20);
    }
}

on timer statusTimer
{
    // Pack status into message
    DoorStatus.byte(0) = doorLockState;
    DoorStatus.byte(1) = windowPosition;
    DoorStatus.byte(2) = windowMovement;
    DoorStatus.id = 0x300;

    // Transmit status
    output(DoorStatus);

    // Schedule next transmission
    setTimer(statusTimer, 500);
}

如何测试这个仿真节点?

步骤 1:创建 CANoe 工程

  1. 新建一个 CANoe 工程
  2. Simulation Setup 中添加我们的 DoorModule 节点(关联 .can 文件)
  3. 添加一个 Interactive Generator 面板,用于手动发送命令

📸 截图:CANoe Simulation Setup 窗口

[此处应显示 CANoe Simulation Setup 窗口截图]

图 6.1:DoorModule 仿真节点配置

说明:此截图展示了在 CANoe Simulation Setup 中添加 DoorModule 仿真节点和 Interactive Generator 面板的操作步骤。

步骤 2:配置 Interactive Generator

在 Interactive Generator 中,配置三个消息:

消息 ID 名称 数据
0x200 LockCmd 任意(忽略)
0x201 UnlockCmd 任意(忽略)
0x202 WindowCmd Byte 0 = 1(降下)或 2(升起)

步骤 3:运行测试

  1. 启动测量
  2. 在 Write 窗口观察 DoorModule 的日志输出
  3. 使用 Interactive Generator 发送命令:
    • 发送 LockCmd → 观察门锁状态变化
    • 发送 WindowCmd (0x01) → 观察车窗下降过程
    • 发送 WindowCmd (0x02) → 观察车窗上升过程

📸 截图:CANoe Write 窗口输出

[此处应显示 CANoe Write 窗口截图]

图 6.2:DoorModule 运行日志

说明:此截图展示了 DoorModule 仿真节点运行时的日志输出,包括初始化、命令处理和状态变化等信息。

步骤 4:观察状态消息

添加一个 Trace 窗口或 Data Monitor,观察 0x300 DoorStatus 消息的变化:

  • Byte 0 应显示门锁状态(0=未锁,1=已锁)
  • Byte 1 应显示车窗位置(0-100)
  • Byte 2 应显示车窗运动状态(0=静止,1=上升,2=下降)

进阶思考:如何让仿真更真实?

当前的仿真节点已经具备了基本功能,但还可以进一步改进:

1. 防夹手逻辑

真实的车窗升降有防夹手功能——当遇到阻力时自动停止。在仿真中,我们可以模拟这个逻辑:

// Enhanced window movement with anti-pinch simulation
on timer windowTimer
{
    byte movementStep = 5;
    byte resistance = 0;  // Simulated resistance (0=none, 1=resistance detected)

    // Randomly simulate resistance (10% chance)
    // Note: random() returns a value between 0.0 and 1.0
    if (random() < 0.1)
    {
        resistance = 1;
    }

    if (windowMovement == 1)  // Moving up
    {
        if (resistance == 1)
        {
            write("DoorModule: Anti-pinch triggered - window stopped");
            windowMovement = 0;
            return;
        }

        windowPosition = windowPosition - movementStep;
        if (windowPosition <= 0)
        {
            windowPosition = 0;
            windowMovement = 0;
        }
    }
    // ... similar for moving down ...

    if (windowMovement != 0)
    {
        setTimer(windowTimer, 20);
    }
}

2. 错误处理和诊断

添加对错误情况的处理:

on message WindowCmd
{
    byte command;
    command = this.byte(0);

    // Check for invalid command
    if (command > 2)
    {
        write("DoorModule: ERROR - Invalid window command: %d", command);
        return;
    }

    // ... rest of the code ...
}

3. 状态机模式

对于更复杂的逻辑,可以使用状态机模式,将不同的操作状态清晰分离:

enum DoorState
{
    DOOR_UNLOCKED_IDLE,
    DOOR_LOCKED_IDLE,
    DOOR_WINDOW_MOVING
};

DoorState currentState = DOOR_UNLOCKED_IDLE;

// State transition logic
void transitionTo(DoorState newState)
{
    write("DoorModule: State transition %d -> %d", currentState, newState);
    currentState = newState;
}

本篇总结

通过构建车门模块仿真节点,我们学会了:

  • 仿真节点的设计思路:从需求分析到状态定义
  • on start 初始化:设置初始状态和启动定时器
  • on message 命令响应:接收和处理外部命令
  • on timer 自动行为:实现时间相关的业务逻辑
  • output() 状态反馈:主动发送状态信息

这个仿真节点具有:

  • 内部状态(门锁、车窗位置、运动状态)
  • 命令响应(上锁、解锁、车窗控制)
  • 自动行为(平滑升降过程)
  • 安全检查(移动时禁止操作)
  • 状态反馈(周期性发送状态消息)

课后练习

  1. 扩展练习:为 DoorModule 添加 WindowAutoDown 功能——解锁后自动将车窗降下 50%。提示:使用 on message UnlockCmd 触发。

  2. 优化练习:当前车窗移动是线性的。改为非线性移动:前 80% 快速移动(10%/次),最后 20% 慢速移动(2%/次),模拟真实车窗特性。

  3. 调试练习:故意在代码中引入一个 bug(比如忘记检查 windowMovement 状态),然后使用断点调试工具定位问题。提示:在 CAPL Browser 中设置断点,单步执行,观察变量值的变化。

  4. 挑战练习:设计一个灯光控制仿真节点,实现:

    • 接收 LightCmd 命令(开/关)
    • 支持自动灯光(天黑时自动开启)
    • 发送 LightStatus 状态反馈
    • 使用环境变量(on envVar)模拟光线传感器